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:
+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 {
|
||||
|
||||
Reference in New Issue
Block a user