fix(self-hosted): 修壓測四阻斷項 + 設定分層 + init 非互動
壓測(docs/壓測報告.md)發現 acr init --self-hosted 對任何非官方 CF 帳號都裝不起來,且設定寫死全域單檔 + 強制 TTY。本次一併修: R2 dead storage 全清(#3#4,registry-canon Phase 1.5 補完): - cypher-executor wrangler.toml/test.toml/types.ts 移除 WASM_BUCKET binding - CLI deploy.ts/init.ts/cf-api.ts/config.ts 移除 R2 建立邏輯與 wasm_bucket - R2 綁信用卡違背「開源免費自架」核心;bucket 名 WASM_BUCKET 本就非法 → self-hosted 改為只需 Workers + KV(皆免費額度、不綁卡) fork 帳號部署阻斷(#1#2): - deploy.ts 新增 stripOfficialOnlyBindings(),注入暫存副本時移除 [[routes]]/zone_name/[[r2_buckets]]/[ai](fork 沒有 arcrun.dev zone) - 不刪 repo 內 toml(官方 prod CI 部署仍需 routes),只在 CLI self-hosted 路徑 strip 設定分層 + 非互動(#7#8): - config.ts loadConfig 改三層:env > 專案層 .arcrun.yaml(就近往上找)> 全域 - init 支援 --account-id/--api-token flag + CLOUDFLARE_* env,缺才互動 - 新增 acr config --where 顯示每個值的來源層(token 自動遮罩) - gitignore 一併排除 .arcrun.yaml 驗收:tsc 全綠;三層 merge 端對端測試 8/8;strip 對真實 toml 驗證 routes/R2/AI 移除而 name/workers_dev/KV 保留。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* acr config [--where] — 顯示目前生效的設定與每個值的來源層。
|
||||
* 解壓測 §1.2 建議 #3:讓使用者一眼確認「現在這個資料夾正用哪個帳號」,避免用錯帳號部署。
|
||||
* SDD: sdk-and-website/config-layering.md §3.1
|
||||
*/
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
resolveConfigSources,
|
||||
activeProjectConfigPath,
|
||||
type ConfigSource,
|
||||
} from '../lib/config.js';
|
||||
|
||||
const SOURCE_LABEL: Record<ConfigSource, string> = {
|
||||
env: 'env 變數',
|
||||
project: '專案層 .arcrun.yaml',
|
||||
global: '全域 ~/.arcrun/config.yaml',
|
||||
default: '預設值',
|
||||
};
|
||||
|
||||
/** 敏感欄位只印前綴,避免把 token 完整印到終端 / log。*/
|
||||
const SENSITIVE = new Set(['api_key', 'encryption_key', 'cf_api_token']);
|
||||
|
||||
function mask(field: string, value: string): string {
|
||||
if (SENSITIVE.has(field) && value.length > 8) return `${value.slice(0, 8)}…`;
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function cmdConfig(_options: { where?: boolean }): Promise<void> {
|
||||
const rows = resolveConfigSources();
|
||||
const projectPath = activeProjectConfigPath();
|
||||
|
||||
console.log(chalk.bold('\n arcrun 目前生效的設定\n'));
|
||||
|
||||
if (projectPath) {
|
||||
console.log(chalk.gray(` 專案層設定:${projectPath}(覆蓋全域)`));
|
||||
} else {
|
||||
console.log(chalk.gray(' 專案層設定:無(此資料夾未放 .arcrun.yaml,使用全域)'));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
const fieldWidth = Math.max(...rows.map(r => r.field.length), 4);
|
||||
for (const { field, value, source } of rows) {
|
||||
const name = field.padEnd(fieldWidth);
|
||||
console.log(
|
||||
` ${chalk.cyan(name)} ${mask(field, value)} ${chalk.gray(`← ${SOURCE_LABEL[source]}`)}`,
|
||||
);
|
||||
}
|
||||
console.log(chalk.gray('\n 優先序:env 變數 > 專案層 .arcrun.yaml > 全域 ~/.arcrun/config.yaml\n'));
|
||||
}
|
||||
+39
-17
@@ -11,7 +11,6 @@ import { saveConfig, type ArcrunConfig } from '../lib/config.js';
|
||||
import { CfAccountClient } from '../lib/cf-api.js';
|
||||
import {
|
||||
REQUIRED_KV_NAMESPACES,
|
||||
REQUIRED_R2_BUCKET,
|
||||
SECRET_TARGET_WORKERS,
|
||||
wranglerAvailable,
|
||||
downloadAndDeploy,
|
||||
@@ -27,7 +26,14 @@ async function prompt(rl: ReturnType<typeof createInterface>, question: string):
|
||||
return answer.trim();
|
||||
}
|
||||
|
||||
export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }): Promise<void> {
|
||||
export interface InitOptions {
|
||||
local?: boolean;
|
||||
selfHosted?: boolean;
|
||||
accountId?: string;
|
||||
apiToken?: string;
|
||||
}
|
||||
|
||||
export async function cmdInit(options: InitOptions): Promise<void> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
console.log(chalk.bold('\n arcrun 初始化設定\n'));
|
||||
@@ -36,7 +42,7 @@ export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }
|
||||
if (options.local) {
|
||||
await initLocal();
|
||||
} else if (options.selfHosted) {
|
||||
await initSelfHosted(rl);
|
||||
await initSelfHosted(rl, options);
|
||||
} else {
|
||||
await initStandard(rl);
|
||||
}
|
||||
@@ -123,11 +129,14 @@ async function initStandard(rl: ReturnType<typeof createInterface>): Promise<voi
|
||||
|
||||
/**
|
||||
* Self-hosted installer:用戶只提供 CF Account ID + API Token,其餘自動。
|
||||
* 驗 token → 建 7 KV + R2(冪等)→ 查 subdomain → 下載 release 部署 Worker
|
||||
* 驗 token → 建 7 KV(冪等)→ 查 subdomain → 下載 release 部署 Worker
|
||||
* → seed auth+api recipe → 寫 config → 印手動 secret 提示。
|
||||
* SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md
|
||||
*/
|
||||
async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<void> {
|
||||
async function initSelfHosted(
|
||||
rl: ReturnType<typeof createInterface>,
|
||||
options: InitOptions,
|
||||
): Promise<void> {
|
||||
console.log(chalk.gray(' Self-hosted 模式:自動部署整套 arcrun 到你的 Cloudflare 帳號\n'));
|
||||
console.log(chalk.gray(' 你只需提供 CF Account ID + API Token,其餘 CLI 自動完成。\n'));
|
||||
|
||||
@@ -138,8 +147,21 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const accountId = await prompt(rl, '你的 Cloudflare Account ID');
|
||||
const cfApiToken = await prompt(rl, 'CF API Token(需 Workers Scripts Edit + KV Edit + R2 Edit)');
|
||||
// account-id / api-token 取得順序:flag > env(CLOUDFLARE_*)> 互動問答。
|
||||
// 解壓測 #7:AI/CI 可非互動完成。帳號設定值非風險確認(mindset §7),flag/env 合法。
|
||||
// SDD: sdk-and-website/config-layering.md §2.3
|
||||
const accountId =
|
||||
options.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? (await prompt(rl, '你的 Cloudflare Account ID'));
|
||||
const cfApiToken =
|
||||
options.apiToken ?? process.env.CLOUDFLARE_API_TOKEN
|
||||
?? (await prompt(rl, 'CF API Token(需 Workers Scripts Edit + Workers KV Storage Edit)'));
|
||||
|
||||
if (!accountId || !cfApiToken) {
|
||||
console.log(chalk.yellow('\n ✗ 缺少 Account ID 或 API Token。'));
|
||||
console.log(chalk.yellow(' 非互動用法:acr init --self-hosted --account-id <id> --api-token <token>'));
|
||||
console.log(chalk.yellow(' 或設環境變數 CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cf = new CfAccountClient(accountId, cfApiToken);
|
||||
|
||||
@@ -154,7 +176,9 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. 建 KV namespace(冪等)+ R2 bucket
|
||||
// 2. 建 KV namespace(冪等)
|
||||
// 不建 R2:R2 是 dead storage(registry-canon Phase 1.5),且 CF R2 首次啟用強制綁信用卡,
|
||||
// 違背 arcrun「開源免費自架,Workers + KV 免費額度即可運行」核心理念(壓測 2026-06-04 #3)。
|
||||
const kvNamespaceIds: Record<string, string> = {};
|
||||
try {
|
||||
const existing = await cf.listKvNamespaces();
|
||||
@@ -164,9 +188,6 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
|
||||
kvNamespaceIds[title] = id;
|
||||
console.log(chalk.green(' ✓'));
|
||||
}
|
||||
process.stdout.write(chalk.gray(` → R2 ${REQUIRED_R2_BUCKET}...`));
|
||||
await cf.ensureR2Bucket(REQUIRED_R2_BUCKET);
|
||||
console.log(chalk.green(' ✓'));
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`\n ✗ 建立資源失敗:${e instanceof Error ? e.message : e}\n`));
|
||||
process.exit(1);
|
||||
@@ -198,7 +219,6 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
|
||||
cypher_executor_url: cypherUrl,
|
||||
webhooks_kv_namespace_id: kvNamespaceIds['WEBHOOKS'],
|
||||
credentials_kv_namespace_id: kvNamespaceIds['CREDENTIALS_KV'],
|
||||
wasm_bucket: REQUIRED_R2_BUCKET,
|
||||
multi_tenant: false,
|
||||
};
|
||||
saveConfig(config);
|
||||
@@ -212,7 +232,7 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
|
||||
}
|
||||
|
||||
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
|
||||
console.log(chalk.green('\n ✓ Cloudflare 資源就緒(7 KV + R2)'));
|
||||
console.log(chalk.green('\n ✓ Cloudflare 資源就緒(7 KV,免費額度即可,無需綁卡)'));
|
||||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||||
|
||||
@@ -289,12 +309,14 @@ function createCredentialsYamlIfMissing(): void {
|
||||
);
|
||||
}
|
||||
|
||||
// 確保 .gitignore 排除 credentials.yaml
|
||||
// 確保 .gitignore 排除 credentials.yaml + 專案層 .arcrun.yaml(可能含 cf_api_token)
|
||||
// 壓測 §1.2 安全附帶發現:憑證進版控 = 帳號控制權外洩。
|
||||
const gitignorePath = join(process.cwd(), '.gitignore');
|
||||
if (existsSync(gitignorePath)) {
|
||||
const content = readFileSync(gitignorePath, 'utf8');
|
||||
if (!content.includes('credentials.yaml')) {
|
||||
appendFileSync(gitignorePath, '\ncredentials.yaml\n');
|
||||
}
|
||||
const toAdd: string[] = [];
|
||||
if (!content.includes('credentials.yaml')) toAdd.push('credentials.yaml');
|
||||
if (!content.includes('.arcrun.yaml')) toAdd.push('.arcrun.yaml');
|
||||
if (toAdd.length > 0) appendFileSync(gitignorePath, '\n' + toAdd.join('\n') + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ export async function cmdValidate(filePath: string, options: { offline?: boolean
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { missing: string[] };
|
||||
if (data.missing.length > 0) {
|
||||
check('零件存在性', false, `WASM_BUCKET 中找不到:${data.missing.join(', ')}`);
|
||||
check('零件存在性', false, `registry 中找不到零件:${data.missing.join(', ')}`);
|
||||
allPassed = false;
|
||||
} else {
|
||||
check('零件存在性', true, '所有零件均已在 WASM_BUCKET');
|
||||
check('零件存在性', true, '所有零件均已在 registry');
|
||||
}
|
||||
} else {
|
||||
check('零件存在性', false, `無法連線 ${executorUrl}(加 --offline 跳過此檢查)`);
|
||||
|
||||
Reference in New Issue
Block a user