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 跳過此檢查)`);
|
||||
|
||||
+14
-2
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { cmdInit } from './commands/init.js';
|
||||
import { cmdConfig } from './commands/config.js';
|
||||
import { cmdCredsPush } from './commands/creds.js';
|
||||
import { cmdPush } from './commands/push.js';
|
||||
import { cmdRun } from './commands/run.js';
|
||||
@@ -27,13 +28,24 @@ program
|
||||
.description('arcrun — AI Workflow CLI for Cloudflare Workers + WASM')
|
||||
.version('1.1.0');
|
||||
|
||||
// acr init [--self-hosted]
|
||||
// acr init [--self-hosted] [--account-id <id>] [--api-token <token>]
|
||||
program
|
||||
.command('init')
|
||||
.description('互動式初始化設定(建立 ~/.arcrun/config.yaml)')
|
||||
.option('--local', '本機模式:不需要 Cloudflare 帳號,直接在本機測試 workflow')
|
||||
.option('--self-hosted', '完全 Self-hosted 模式:自行部署所有 Cloudflare Worker')
|
||||
.action((options: { local?: boolean; selfHosted?: boolean }) => cmdInit(options));
|
||||
.option('--account-id <id>', 'self-hosted:CF Account ID(非互動;亦可用 CLOUDFLARE_ACCOUNT_ID env)')
|
||||
.option('--api-token <token>', 'self-hosted:CF API Token(非互動;亦可用 CLOUDFLARE_API_TOKEN env)')
|
||||
.action((options: { local?: boolean; selfHosted?: boolean; accountId?: string; apiToken?: string }) =>
|
||||
cmdInit(options),
|
||||
);
|
||||
|
||||
// acr config [--where]:印出目前生效的設定與每個值的來源層(env / 專案層 / 全域)
|
||||
program
|
||||
.command('config')
|
||||
.description('顯示目前生效的設定與來源層(避免在錯的資料夾用錯帳號)')
|
||||
.option('--where', '顯示每個設定值來自哪一層(env > 專案層 .arcrun.yaml > 全域)')
|
||||
.action((options: { where?: boolean }) => cmdConfig(options));
|
||||
|
||||
// acr creds push [credentials.yaml]
|
||||
const credsCmd = program.command('creds').description('Credential 管理');
|
||||
|
||||
+2
-16
@@ -78,7 +78,8 @@ export class CfKvClient {
|
||||
/**
|
||||
* Cloudflare Account-level API wrapper(self-hosted installer 用)。
|
||||
*
|
||||
* 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、建 R2 bucket、查 workers.dev subdomain。
|
||||
* 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、查 workers.dev subdomain。
|
||||
* (不建 R2:R2 是 dead storage 且綁卡違背開源免費;見 init.ts step 2 + registry-canon Phase 1.5。)
|
||||
* 與 CfKvClient(綁單一 namespace 的 KV 操作)職責不同——這個是帳號層級的資源管理。
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §3 step 1-2
|
||||
*/
|
||||
@@ -138,21 +139,6 @@ export class CfAccountClient {
|
||||
return result.id;
|
||||
}
|
||||
|
||||
/** 建立 R2 bucket(已存在則略過,冪等)。*/
|
||||
async ensureR2Bucket(name: string): Promise<void> {
|
||||
try {
|
||||
await this.cf<{ name: string }>('/r2/buckets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
} catch (e) {
|
||||
// bucket 已存在 → CF 回 10004 之類;視為冪等成功
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (/already exists|10004/i.test(msg)) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用,組對內 component URL)。*/
|
||||
async getWorkersSubdomain(): Promise<string> {
|
||||
const result = await this.cf<{ subdomain: string }>('/workers/subdomain');
|
||||
|
||||
+102
-10
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* CLI 設定檔管理(~/.arcrun/config.yaml)
|
||||
* CLI 設定檔管理 — 三層分層解析(SDD: sdk-and-website/config-layering.md)
|
||||
* 優先序:env 變數 > 專案層 .arcrun.yaml(就近往上找)> 全域 ~/.arcrun/config.yaml
|
||||
* 解壓測 #7(AI/CI 非互動)+ #8(接案多帳號),仿 git config / Claude Code MCP 模式。
|
||||
*/
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { join, dirname, parse as parsePath } from 'node:path';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
export interface ArcrunConfig {
|
||||
@@ -18,7 +20,6 @@ export interface ArcrunConfig {
|
||||
cypher_executor_url?: string;
|
||||
credentials_kv_namespace_id?: string;
|
||||
webhooks_kv_namespace_id?: string;
|
||||
wasm_bucket?: string;
|
||||
// 共用
|
||||
multi_tenant?: boolean;
|
||||
// 資料外流警示:本機記住「已同意暴露 / 選擇不再警示」的資源,避免每次 push 重問(§3 首次問記住)。
|
||||
@@ -30,17 +31,108 @@ export interface ArcrunConfig {
|
||||
const CONFIG_DIR = join(homedir(), '.arcrun');
|
||||
const CONFIG_PATH = join(CONFIG_DIR, 'config.yaml');
|
||||
|
||||
/** 專案層設定檔名(就近往上找)。含憑證 → 必須 gitignore(見 createCredentialsYamlIfMissing)。*/
|
||||
export const PROJECT_CONFIG_NAME = '.arcrun.yaml';
|
||||
|
||||
/** 設定來源層級(acr config --where 用,讓使用者知道每個值來自哪一層,避免用錯帳號)。*/
|
||||
export type ConfigSource = 'env' | 'project' | 'global' | 'default';
|
||||
|
||||
/** env 變數 → config 欄位映射(最高層覆蓋)。CF 兩個沿用 wrangler 慣用名,CI 設一次兩邊通用。*/
|
||||
const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
||||
ARCRUN_MODE: 'mode',
|
||||
ARCRUN_API_KEY: 'api_key',
|
||||
ARCRUN_ENCRYPTION_KEY: 'encryption_key',
|
||||
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
||||
CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id',
|
||||
CLOUDFLARE_API_TOKEN: 'cf_api_token',
|
||||
};
|
||||
|
||||
export function configExists(): boolean {
|
||||
return existsSync(CONFIG_PATH);
|
||||
return existsSync(CONFIG_PATH) || findProjectConfig() !== undefined;
|
||||
}
|
||||
|
||||
export function loadConfig(): ArcrunConfig {
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
// 未初始化時回傳 local 模式預設值,讓 validate --offline 等指令能在無設定下運作
|
||||
return { mode: 'local' };
|
||||
/** 從 startDir 就近往上逐層找專案層 .arcrun.yaml,回傳第一個命中的路徑(停在檔案系統根)。*/
|
||||
export function findProjectConfig(startDir: string = process.cwd()): string | undefined {
|
||||
let dir = startDir;
|
||||
const root = parsePath(dir).root;
|
||||
// 防呆上界:層數不會無限(root 一定到得了),但仍加保險避免異常路徑死迴圈。
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const candidate = join(dir, PROJECT_CONFIG_NAME);
|
||||
if (existsSync(candidate)) return candidate;
|
||||
if (dir === root) break;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
const raw = readFileSync(CONFIG_PATH, 'utf8');
|
||||
return yaml.load(raw) as ArcrunConfig;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 讀全域設定(不分層)。無檔回 undefined。*/
|
||||
function readGlobalConfig(): Partial<ArcrunConfig> | undefined {
|
||||
if (!existsSync(CONFIG_PATH)) return undefined;
|
||||
return (yaml.load(readFileSync(CONFIG_PATH, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
|
||||
}
|
||||
|
||||
/** 讀專案層設定(不分層)。無檔回 undefined。*/
|
||||
function readProjectConfig(): Partial<ArcrunConfig> | undefined {
|
||||
const path = findProjectConfig();
|
||||
if (!path) return undefined;
|
||||
return (yaml.load(readFileSync(path, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
|
||||
}
|
||||
|
||||
/** 蒐集 env 覆蓋(只取有設值的 env,欄位級)。*/
|
||||
function readEnvOverrides(): Partial<ArcrunConfig> {
|
||||
const out: Partial<ArcrunConfig> = {};
|
||||
for (const [envName, field] of Object.entries(ENV_MAP)) {
|
||||
const v = process.env[envName];
|
||||
if (v !== undefined && v !== '') {
|
||||
// mode 需窄型別;其餘皆 string 欄位。
|
||||
(out as Record<string, unknown>)[field] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三層分層解析:全域 → 疊專案層 → 疊 env(欄位級 merge,高層只覆蓋它提供的欄位)。
|
||||
* 任一層都沒有 mode 時 fallback 'local',讓 validate --offline 等在無設定下可運作。
|
||||
*/
|
||||
export function loadConfig(): ArcrunConfig {
|
||||
const merged: Partial<ArcrunConfig> = {
|
||||
...(readGlobalConfig() ?? {}),
|
||||
...(readProjectConfig() ?? {}),
|
||||
...readEnvOverrides(),
|
||||
};
|
||||
if (!merged.mode) merged.mode = 'local';
|
||||
return merged as ArcrunConfig;
|
||||
}
|
||||
|
||||
/** 解析每個關鍵欄位的最終值與來源層(acr config --where 用)。*/
|
||||
export function resolveConfigSources(): Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> {
|
||||
const global = readGlobalConfig() ?? {};
|
||||
const project = readProjectConfig() ?? {};
|
||||
const env = readEnvOverrides();
|
||||
const fields: (keyof ArcrunConfig)[] = [
|
||||
'mode', 'api_key', 'encryption_key', 'cloudflare_account_id',
|
||||
'cf_api_token', 'cypher_executor_url',
|
||||
];
|
||||
const rows: Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> = [];
|
||||
for (const f of fields) {
|
||||
let value: unknown;
|
||||
let source: ConfigSource = 'default';
|
||||
if (f in env) { value = env[f]; source = 'env'; }
|
||||
else if (f in project) { value = project[f]; source = 'project'; }
|
||||
else if (f in global) { value = global[f]; source = 'global'; }
|
||||
else if (f === 'mode') { value = 'local'; source = 'default'; }
|
||||
else continue;
|
||||
rows.push({ field: f, value: String(value), source });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 回傳本次解析實際採用的專案層設定檔路徑(無則 undefined)。acr config --where 顯示用。*/
|
||||
export function activeProjectConfigPath(): string | undefined {
|
||||
return findProjectConfig();
|
||||
}
|
||||
|
||||
export function saveConfig(config: ArcrunConfig): void {
|
||||
|
||||
+51
-4
@@ -28,9 +28,6 @@ export const REQUIRED_KV_NAMESPACES = [
|
||||
'EXEC_CONTEXT',
|
||||
] as const;
|
||||
|
||||
/** init 要建立的 R2 bucket。*/
|
||||
export const REQUIRED_R2_BUCKET = 'WASM_BUCKET';
|
||||
|
||||
/** 部署後要提示用戶手動 `wrangler secret put ENCRYPTION_KEY` 的 Worker。*/
|
||||
export const SECRET_TARGET_WORKERS = [
|
||||
'arcrun-cypher-executor',
|
||||
@@ -172,7 +169,19 @@ function discoverWorkerDirs(root: string): { tier1: string[]; tier2: string[] }
|
||||
return { tier1, tier2 };
|
||||
}
|
||||
|
||||
/** 注入用戶的 KV namespace id(取代 wrangler.toml 中各 binding 的 id)+ cypher WORKER_SUBDOMAIN。*/
|
||||
/**
|
||||
* 注入用戶的 KV namespace id(取代 wrangler.toml 中各 binding 的 id)+ cypher WORKER_SUBDOMAIN,
|
||||
* 並 strip 掉只有 arcrun 官方帳號才有的綁定(self-hosted fork 帳號沒有)。
|
||||
*
|
||||
* 為何 strip 而非刪 repo 內 toml(壓測 2026-06-04 阻斷項 #1#2#3#4):
|
||||
* - repo 內各 worker toml 的 `[[routes]] zone_name="arcrun.dev"` 是**官方 prod CI 部署**需要的
|
||||
* (對外開放零件)。直接從 repo 刪會破壞官方部署。
|
||||
* - 但 fork 用戶**沒有 arcrun.dev zone** → wrangler deploy 找不到 zone 而失敗。
|
||||
* - deploy.ts 只在 self-hosted 路徑跑,且改的是「暫存目錄副本」(SDD self-hosted-init.md §3 step 4),
|
||||
* 不碰用戶 repo。所以在注入時 strip 掉這些官方專屬綁定 = 對的層級。
|
||||
* - 每個 worker toml 都有 `workers_dev = true` → strip routes 後純靠 workers.dev URL,自架可達。
|
||||
* - R2(`[[r2_buckets]]`)是 dead storage(registry-canon Phase 1.5),且綁卡違背開源免費 → 一併移除。
|
||||
*/
|
||||
function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void {
|
||||
if (!existsSync(tomlPath)) return;
|
||||
let toml = readFileSync(tomlPath, 'utf8');
|
||||
@@ -196,9 +205,47 @@ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void {
|
||||
);
|
||||
}
|
||||
|
||||
toml = stripOfficialOnlyBindings(toml);
|
||||
|
||||
writeFileSync(tomlPath, toml, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 self-hosted fork 帳號沒有、會導致 wrangler deploy 失敗的官方專屬 TOML 區塊:
|
||||
* - `[[routes]]`(含 pattern/zone_name):fork 沒有 arcrun.dev zone
|
||||
* - `[[r2_buckets]]`:dead storage + 綁卡違背開源免費(registry-canon 1.5)
|
||||
* - `[ai]`(Workers AI binding):免費帳號未必啟用,且自架預設不需要
|
||||
* 純文字行級移除(TOML table 以空行 / 下一個 `[` 區塊結束)。worker 仍靠 `workers_dev = true` 對外。
|
||||
*/
|
||||
export function stripOfficialOnlyBindings(toml: string): string {
|
||||
const lines = toml.split('\n');
|
||||
const out: string[] = [];
|
||||
let skipping = false;
|
||||
|
||||
const isBlockHeader = (l: string) =>
|
||||
/^\s*\[\[?(routes|r2_buckets|ai)\]?\]\s*$/.test(l);
|
||||
|
||||
for (const line of lines) {
|
||||
if (isBlockHeader(line)) {
|
||||
skipping = true; // 進入要移除的區塊,連同 header 一起丟
|
||||
continue;
|
||||
}
|
||||
if (skipping) {
|
||||
// 區塊結束條件:遇到下一個 table header(`[...]`)或空行
|
||||
if (/^\s*\[/.test(line)) {
|
||||
skipping = false; // 這行是新區塊的開頭,保留並由下方邏輯處理
|
||||
} else if (line.trim() === '') {
|
||||
skipping = false; // 空行結束區塊;空行本身丟掉避免堆疊空白
|
||||
continue;
|
||||
} else {
|
||||
continue; // 仍在被移除區塊內(pattern/zone_name/binding/bucket_name 等)
|
||||
}
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
/** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。*/
|
||||
function runWranglerDeploy(dir: string, ctx: DeployContext): void {
|
||||
// 先裝依賴(cypher-executor/registry 是 TS,wrangler 內建 esbuild bundle 需 node_modules)
|
||||
|
||||
Reference in New Issue
Block a user