05333b8fe6
acr init 加印手動指令 wrangler secret put CF_SECRETS_API_TOKEN (比照既有 ENCRYPTION_KEY 模式,不發明新模式);CF_ACCOUNT_ID 已在 T5 的 deploy.ts 改動中自動注入(同 WORKER_SUBDOMAIN 機制)。 誠實缺口 closure:對本次真實改動過的 committed code(leo21c arcrun-cypher-executor)做第二次 wrangler deploy,確認 CF_SECRETS_API_TOKEN/ENCRYPTION_KEY 兩個 secret 存活 + 寫入路徑 redeploy 後仍正常運作(真實 curl 驗證),比 T1.5 spike 對 throwaway worker 的舊證據更貼近本次改動。 刻意不跑 acr update:mistakes #23 已知限制(硬綁 GitHub codeload 舊碼,會拉回覆蓋剛部署的 T4/T5 成果),跑了只會摧毀測試環境又 證明不了新東西,SDD 本身允許此替代路徑。此結構性衝突留 leo 裁決。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018D6QoC5waFkcjc2N7csJBB
420 lines
20 KiB
TypeScript
420 lines
20 KiB
TypeScript
/**
|
||
* acr init — 互動式初始化設定
|
||
* 詢問 CF Account ID、KV namespace、API Token、email,
|
||
* 呼叫 arcrun.dev 取得 API Key,寫入 ~/.arcrun/config.yaml
|
||
*/
|
||
import { createInterface } from 'node:readline/promises';
|
||
import { writeFileSync, existsSync, readFileSync, appendFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import chalk from 'chalk';
|
||
import { saveConfig, type ArcrunConfig } from '../lib/config.js';
|
||
import { CfAccountClient } from '../lib/cf-api.js';
|
||
import {
|
||
REQUIRED_KV_NAMESPACES,
|
||
SECRET_TARGET_WORKERS,
|
||
downloadAndDeploy,
|
||
type DeployContext,
|
||
} from '../lib/deploy.js';
|
||
import { cmdInstallHarness } from './install-harness.js';
|
||
import { cmdMcpSetup } from './mcp-setup.js';
|
||
import { detectEnvironment, printPreflight, verifyInstall } from '../lib/preflight.js';
|
||
|
||
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
|
||
|
||
async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
|
||
const answer = await rl.question(chalk.cyan(`? ${question}: `));
|
||
return answer.trim();
|
||
}
|
||
|
||
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'));
|
||
|
||
try {
|
||
if (options.local) {
|
||
await initLocal();
|
||
} else if (options.selfHosted) {
|
||
await initSelfHosted(rl, options);
|
||
} else {
|
||
await initStandard(rl);
|
||
}
|
||
} finally {
|
||
rl.close();
|
||
}
|
||
|
||
// init 末尾順便裝 CC harness 進當前專案(SDD user-cc-harness §2:init 裝 + 可單獨裝)。
|
||
// 失敗不影響 init 本身(harness 是加分,可事後 acr install-harness 補)。
|
||
try {
|
||
await cmdInstallHarness();
|
||
} catch (e) {
|
||
console.log(chalk.gray(` (harness 安裝略過:${e instanceof Error ? e.message : e};可稍後跑 acr install-harness)`));
|
||
}
|
||
|
||
// 順便寫專案 .mcp.json,讓 Claude Code 連對的 MCP(依 config 的 mcp_url,SDD mcp-account-source.md)。
|
||
// 失敗不影響 init(可事後 acr mcp-setup 補)。
|
||
try {
|
||
cmdMcpSetup();
|
||
} catch (e) {
|
||
console.log(chalk.gray(` (.mcp.json 略過:${e instanceof Error ? e.message : e};可稍後跑 acr mcp-setup)`));
|
||
}
|
||
|
||
// 冷啟動環境檢查(harness:機制強制,不靠提醒)。
|
||
// 失敗會 exit 2,用戶必須修復後重跑 acr init。
|
||
console.log('');
|
||
try {
|
||
const { execSync } = await import('child_process');
|
||
const hookPath = new URL('../../../.claude/hooks/pre-cold-startup-check.sh', import.meta.url);
|
||
execSync(`bash "${hookPath.pathname}"`, { stdio: 'inherit' });
|
||
} catch (e) {
|
||
if ((e as any)?.status === 2) {
|
||
// exit 2 = 環境檢查失敗,停下,不繼續
|
||
process.exit(2);
|
||
}
|
||
// 其他錯誤(hook 不存在等)只警告,不擋 init
|
||
console.log(chalk.gray(` (環境檢查略過,可手動跑 bash .claude/hooks/pre-cold-startup-check.sh)`));
|
||
}
|
||
}
|
||
|
||
async function initLocal(): Promise<void> {
|
||
console.log(chalk.gray(' Local 模式:不需要 Cloudflare 帳號,workflow 由 arcrun.dev 雲端引擎執行\n'));
|
||
|
||
const config: ArcrunConfig = {
|
||
mode: 'local',
|
||
};
|
||
|
||
saveConfig(config);
|
||
createHelloYamlIfMissing();
|
||
|
||
console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml(local 模式)'));
|
||
console.log(chalk.green(' ✓ 建立 hello.yaml 範例 workflow\n'));
|
||
console.log(' 你可以立刻開始:');
|
||
console.log(chalk.cyan(' acr validate hello.yaml --offline') + ' # 驗證 workflow 格式');
|
||
console.log(chalk.cyan(' acr run hello --input input="Hello, arcrun!"') + ' # 執行,輸出大寫字串\n');
|
||
console.log(chalk.gray(' Local 模式:YAML 留在本機,workflow 由 arcrun.dev 引擎執行。'));
|
||
console.log(chalk.gray(' 需要用自己的 CF 帳號存放 credentials?執行 acr init(Standard 模式)。\n'));
|
||
}
|
||
|
||
async function initStandard(rl: ReturnType<typeof createInterface>): Promise<void> {
|
||
console.log(chalk.gray(' Standard 模式:只需要 email,不需要 Cloudflare 帳號\n'));
|
||
|
||
const email = await prompt(rl, 'Email(用來取得 API Key)');
|
||
|
||
process.stdout.write(chalk.gray('\n → 向 arcrun.dev 取得 API Key...'));
|
||
|
||
let apiKey = '';
|
||
let encryptionKey = '';
|
||
try {
|
||
const res = await fetch(ARCRUN_REGISTER_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email }),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.text();
|
||
throw new Error(`取得失敗(${res.status}):${err}`);
|
||
}
|
||
|
||
const data = await res.json() as { api_key: string; encryption_key: string };
|
||
apiKey = data.api_key;
|
||
encryptionKey = data.encryption_key;
|
||
console.log(chalk.green(' ✓'));
|
||
} catch (e) {
|
||
console.log(chalk.yellow(` ✗ ${e instanceof Error ? e.message : e}`));
|
||
console.log(chalk.yellow(' 請確認網路連線後重新執行 acr init\n'));
|
||
process.exit(1);
|
||
}
|
||
|
||
const config: ArcrunConfig = {
|
||
mode: 'standard',
|
||
api_key: apiKey,
|
||
encryption_key: encryptionKey,
|
||
};
|
||
|
||
saveConfig(config);
|
||
createCredentialsYamlIfMissing();
|
||
|
||
console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml'));
|
||
console.log(chalk.green(` ✓ API Key:${apiKey.slice(0, 8)}...`));
|
||
console.log(chalk.green(' ✓ 建立 credentials.yaml(已加入 .gitignore)\n'));
|
||
console.log(' 下一步:');
|
||
console.log(chalk.cyan(' acr parts scaffold <component>') + ' # 查看零件 config 範本');
|
||
console.log(chalk.cyan(' acr creds push credentials.yaml') + ' # 上傳加密 credentials');
|
||
console.log(chalk.cyan(' acr push workflow.yaml') + ' # 部署 workflow 並取得 Webhook URL\n');
|
||
}
|
||
|
||
/**
|
||
* Self-hosted installer:用戶只提供 CF Account ID + API Token,其餘自動。
|
||
* 驗 token → 建 KV(冪等,數量見 REQUIRED_KV_NAMESPACES)→ 查 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>,
|
||
options: InitOptions,
|
||
): Promise<void> {
|
||
console.log(chalk.gray(' Self-hosted 模式:自動部署整套 arcrun 到你的 Cloudflare 帳號\n'));
|
||
console.log(chalk.gray(' 你只需提供 CF Account ID + API Token,其餘 CLI 自動完成。\n'));
|
||
|
||
// §7.8 P0:偵測先於動作(pip 式)——先看環境有什麼,缺前置就停下給補救指令,
|
||
// 不假設齊備直接動手(test_arcrun/4:D1 缺了 AI 跑去讀原始碼自己想辦法的災難)。
|
||
const pre = detectEnvironment();
|
||
printPreflight('環境偵測(安裝前)', pre.items);
|
||
if (pre.fatal) {
|
||
console.log(chalk.yellow('\n ✗ 缺少必要前置(見上方 →)。補齊後重新執行 acr init --self-hosted。'));
|
||
console.log(chalk.gray(' init 冪等:補好重跑,已就緒的會自動跳過。\n'));
|
||
process.exit(1);
|
||
}
|
||
|
||
// 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);
|
||
|
||
// 1. 驗 token / account 可達
|
||
process.stdout.write(chalk.gray('\n → 驗證 Cloudflare 憑證...'));
|
||
try {
|
||
await cf.verifyAccess();
|
||
console.log(chalk.green(' ✓'));
|
||
} catch (e) {
|
||
console.log(chalk.yellow(` ✗ ${e instanceof Error ? e.message : e}`));
|
||
console.log(chalk.yellow(' 請確認 Account ID 與 API Token(含權限)正確後重試\n'));
|
||
process.exit(1);
|
||
}
|
||
|
||
// 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();
|
||
for (const title of REQUIRED_KV_NAMESPACES) {
|
||
process.stdout.write(chalk.gray(` → KV ${title}...`));
|
||
const id = await cf.ensureKvNamespace(title, existing);
|
||
kvNamespaceIds[title] = id;
|
||
console.log(chalk.green(' ✓'));
|
||
}
|
||
} catch (e) {
|
||
console.log(chalk.yellow(`\n ✗ 建立資源失敗:${e instanceof Error ? e.message : e}\n`));
|
||
process.exit(1);
|
||
}
|
||
|
||
// 2.5 build D1 for KBDB Base (atomic universal table). Free on Workers Free, no credit card
|
||
// (kbdb-base SDD Q4). idempotent: reuse if exists.
|
||
let d1DatabaseId = '';
|
||
try {
|
||
process.stdout.write(chalk.gray(' → D1 arcrun-kbdb...'));
|
||
d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb');
|
||
console.log(chalk.green(' ✓'));
|
||
} catch (e) {
|
||
const em = e instanceof Error ? e.message : String(e);
|
||
console.log(chalk.yellow(`\n ⚠ D1 build failed (${em})`));
|
||
if (/auth/i.test(em)) {
|
||
// 最常見根因:CF token 沒勾 D1 權限(KV/Worker 建得起來但 D1 報 Authentication error)。
|
||
console.log(chalk.yellow(' 多半是 CF token 缺 D1 權限 → 去 token 補勾「Account / D1 / Edit」'));
|
||
console.log(chalk.gray(' 重產 token 填回 .env 後跑 acr update。D1 存 workflow/recipe,沒它後續會受限。'));
|
||
} else {
|
||
console.log(chalk.gray(' KBDB Base 暫不可用,可 acr update 重試。'));
|
||
}
|
||
}
|
||
|
||
// 3. 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用)
|
||
let workerSubdomain = '';
|
||
try {
|
||
workerSubdomain = await cf.getWorkersSubdomain();
|
||
console.log(chalk.gray(` → workers.dev subdomain: ${workerSubdomain}`));
|
||
} catch (e) {
|
||
console.log(chalk.yellow(` ⚠ 查 subdomain 失敗(${e instanceof Error ? e.message : e}),稍後可手動補`));
|
||
}
|
||
|
||
// 3.5 語義查詢開關(issue #7 / T2.4):問用戶要不要開(預設關,free-tier 友善)。
|
||
// 開 → deploy 建 CF Vectorize index + 注入 binding。關 → base 維持 LIKE keyword,零花費。
|
||
// 之後想開:跟 CC 說「幫我開語義查詢」或設 kbdb_embed:true + acr update(不必重 init)。
|
||
const embedAns = (await prompt(
|
||
rl,
|
||
'要開語義查詢嗎?(KBDB 加 AI 向量搜尋;用 CF Vectorize,可能多花費;預設關,之後可隨時開) [y/N]',
|
||
)).trim().toLowerCase();
|
||
const kbdbEmbed = embedAns === 'y' || embedAns === 'yes';
|
||
if (kbdbEmbed) console.log(chalk.gray(' → 已選開語義查詢:部署時會建 Vectorize index。'));
|
||
|
||
// 4. 下載 repo 部署物(含預編譯 wasm)+ 注入 KV id + wrangler deploy 全部 Worker
|
||
console.log(chalk.gray('\n → 下載部署物 + 部署 Worker(從 GitHub 拉預編譯 wasm,用你的 CF token 部署)...'));
|
||
// selfHosted: true → deploy 注入 MULTI_TENANT="false"(mcp-account-source §5.5,修 MCP 401)。
|
||
// init.ts 這條本就是 --self-hosted 分支(config.mode 稍後寫 'self-hosted')。
|
||
const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds, d1DatabaseId, selfHosted: true, kbdbEmbed };
|
||
const deploy = await downloadAndDeploy(deployCtx);
|
||
const cypherUrl = deploy.cypherExecutorUrl
|
||
?? (workerSubdomain ? `https://arcrun-cypher-executor.${workerSubdomain}.workers.dev` : '');
|
||
// self-hosted 自己的 MCP worker URL(mcp-account-source §3:.mcp.json 指自己,不 fallback 官方)。
|
||
const mcpUrl = deploy.mcpUrl
|
||
?? (workerSubdomain ? `https://arcrun-mcp.${workerSubdomain}.workers.dev/mcp` : '');
|
||
// 誠實回報部署結果;但**不**用「全部成功」字串 gate 後續 seed(壓測 §4.1:
|
||
// registry 一個無關 worker 失敗就連坐讓 seed 永遠被跳過)。seed 只看 cypher-executor 是否可達。
|
||
const deployFullyOk = /全部成功/.test(deploy.message);
|
||
console.log(deployFullyOk ? chalk.green(` ✓ ${deploy.message}`) : chalk.yellow(` ⚠ ${deploy.message}`));
|
||
|
||
// 5. 寫 config(資源資訊存好,供後續 acr push / update / seed)
|
||
const config: ArcrunConfig = {
|
||
mode: 'self-hosted',
|
||
cloudflare_account_id: accountId,
|
||
cf_api_token: cfApiToken,
|
||
cypher_executor_url: cypherUrl,
|
||
mcp_url: mcpUrl || undefined, // 指自己的 MCP(mcp-account-source §3),無 subdomain 才留空 fallback 官方
|
||
webhooks_kv_namespace_id: kvNamespaceIds['WEBHOOKS'],
|
||
credentials_kv_namespace_id: kvNamespaceIds['CREDENTIALS_KV'],
|
||
multi_tenant: false,
|
||
kbdb_embed: kbdbEmbed, // 語義查詢開關(issue #7);存進 config 讓後續 acr update 維持一致
|
||
};
|
||
saveConfig(config);
|
||
createCredentialsYamlIfMissing();
|
||
|
||
// 6.5 config 寫好後重寫 .mcp.json,讓它指向「自己的」MCP(init 開頭的 cmdMcpSetup 在 config 前跑,
|
||
// 那時 mcp_url 還沒設 → 會 fallback 官方;這裡 config 已含自己的 mcp_url,重跑一次蓋成自己的)。
|
||
try {
|
||
cmdMcpSetup();
|
||
} catch (e) {
|
||
console.log(chalk.gray(` (.mcp.json 重寫略過:${e instanceof Error ? e.message : e})`));
|
||
}
|
||
|
||
// 6. seed recipe(薄殼:呼叫 API 的 /init/seed 一次,由 API 灌 API recipe + auth recipe)。
|
||
// 只要 cypher-executor 可達就 seed——不被無關 worker(registry)的失敗連坐(壓測 §4.1)。
|
||
if (cypherUrl) {
|
||
await callSeedEndpoint(cypherUrl);
|
||
}
|
||
|
||
// §7.8 P0 裝完驗收:實查 CF(KV/D1)+ 打 cypher /health 確認真就緒,缺哪項明確報哪項
|
||
// + 給一鍵補裝指令(不靜默印灰字)。假綠零容忍(mindset §7):看實際狀態,非看 config 寫了沒。
|
||
const verify = await verifyInstall({
|
||
cf,
|
||
requiredKv: REQUIRED_KV_NAMESPACES,
|
||
expectD1Name: d1DatabaseId ? 'arcrun-kbdb' : undefined,
|
||
cypherUrl,
|
||
});
|
||
printPreflight('安裝驗收(裝完檢查)', verify.items);
|
||
if (!verify.allOk) {
|
||
console.log(chalk.yellow('\n ⚠ 部分項目未就緒(見上方 →)。多數可跑 acr update 冪等補裝。'));
|
||
console.log(chalk.gray(' (worker 剛部署可能需數十秒生效,可稍候再跑 acr update 重驗。)'));
|
||
}
|
||
|
||
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
|
||
console.log(chalk.green(`\n ✓ Cloudflare 資源就緒(${REQUIRED_KV_NAMESPACES.length} KV,免費額度即可,無需綁卡)`));
|
||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||
|
||
// 下一步:身份設定(self-hosted 單租戶——namespace 明碼用戶自填、encryption_key 用戶自保管)。
|
||
// 工具不生成、不 hash、不外傳任何 key(守 rule 05 精神:secret 不進自動化,由用戶持有)。
|
||
console.log(chalk.bold('\n 下一步 ①:在這個專案建 .env(你自己填,工具不碰):'));
|
||
console.log(chalk.cyan(' NAMESPACE=leo # 你的資料分區標籤(明碼即可,不是密碼)'));
|
||
console.log(chalk.cyan(' ENCRYPTION_KEY=<64+ hex> # credential 加密金鑰,你自己保管'));
|
||
console.log(chalk.gray(' 生成 key:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'));
|
||
console.log(chalk.gray(' (NAMESPACE 是分區標籤非密碼;要防外部呼叫請對 webhook 加保護。'));
|
||
console.log(chalk.gray(' ENCRYPTION_KEY 忘了 = 解不開已上傳的 credential。.env 已被 gitignore。)'));
|
||
|
||
console.log(chalk.bold('\n 下一步 ②:把同一把 ENCRYPTION_KEY 設進你的 worker(runtime 解密要用):'));
|
||
for (const w of SECRET_TARGET_WORKERS) {
|
||
console.log(chalk.cyan(` wrangler secret put ENCRYPTION_KEY --name ${w}`));
|
||
}
|
||
console.log(chalk.gray(` ${SECRET_TARGET_WORKERS.length} 個 Worker 共用同一把(與 .env 的 ENCRYPTION_KEY 一致)。`));
|
||
console.log(chalk.gray(' 不想自己跑?跑 acr init 時授權(明示同意)我可代設——但預設由你自己 put(你持有 key)。\n'));
|
||
|
||
// credential-store-migration T3(§2.3):cypher worker 要有一把「能打 CF Workers Scripts
|
||
// secrets 管理 API 的 token」才能讓 POST/PUT /credentials 把密文寫進 Workers Secrets。
|
||
// 比照 ENCRYPTION_KEY 的既有模式(印手動指令,不是工具自動 put)——CF_ACCOUNT_ID 非機密,
|
||
// 已由 downloadAndDeploy/injectWranglerConfig 自動注入(同 WORKER_SUBDOMAIN 模式),
|
||
// 只有 CF_SECRETS_API_TOKEN(機密)需要用戶手動 put。
|
||
console.log(chalk.bold(' 下一步 ③:把能打 Workers Scripts secrets API 的 CF token 設進 cypher worker:'));
|
||
console.log(chalk.cyan(` wrangler secret put CF_SECRETS_API_TOKEN --name arcrun-cypher-executor`));
|
||
console.log(chalk.gray(' 貼你剛才用來部署的同一個 CF API Token(需含 Workers Scripts:Edit 權限)。'));
|
||
console.log(chalk.gray(' 用途:POST/PUT /credentials 把密文寫進 Workers per-script Secrets(credential-store-migration T5)。'));
|
||
console.log(chalk.gray(' CF_ACCOUNT_ID 已自動注入(非機密),不需手動設定。\n'));
|
||
}
|
||
|
||
/**
|
||
* 薄殼:呼叫 API 的 /init/seed 一次(rule 07)。
|
||
* seed 的編排 + 種子資料全在 cypher-executor(routes/init-seed.ts),CLI 不自己迴圈 POST。
|
||
* 之前 seedApiRecipes 在 CLI 迴圈 POST + deployFullyOk gate 是壓測 §4.1 的反例,已移除。
|
||
*/
|
||
async function callSeedEndpoint(cypherUrl: string): Promise<void> {
|
||
process.stdout.write(chalk.gray(' → seed recipe(API recipe + auth recipe,由 API 灌入)...'));
|
||
try {
|
||
const res = await fetch(`${cypherUrl}/init/seed`, { method: 'POST' });
|
||
const body = await res.json().catch(() => null) as
|
||
| { success?: boolean; message?: string }
|
||
| null;
|
||
if (res.ok && body?.success) {
|
||
console.log(chalk.green(` ✓ ${body.message ?? ''}`));
|
||
} else {
|
||
// 誠實:不假綠。seed 沒全成就明說,提示 acr update 可重跑(冪等)。
|
||
console.log(chalk.yellow(` ⚠ ${body?.message ?? `HTTP ${res.status}`}(可 acr update 重跑,seed 冪等)`));
|
||
}
|
||
} catch (e) {
|
||
console.log(chalk.yellow(` ⚠ seed 端點呼叫失敗(${e instanceof Error ? e.message : e});cypher 穩定後 acr update 重跑`));
|
||
}
|
||
}
|
||
|
||
function createHelloYamlIfMissing(): void {
|
||
const helloPath = join(process.cwd(), 'hello.yaml');
|
||
if (!existsSync(helloPath)) {
|
||
writeFileSync(helloPath,
|
||
'# arcrun hello world workflow\n' +
|
||
'# 執行:acr run hello --input input="Hello, arcrun!"\n\n' +
|
||
'name: hello\n' +
|
||
'description: "Hello world — 示範字串轉大寫"\n\n' +
|
||
'flow:\n' +
|
||
' - "input >> ON_SUCCESS >> transform"\n\n' +
|
||
'config:\n' +
|
||
' transform:\n' +
|
||
' component: string_ops\n' +
|
||
' operation: upper\n',
|
||
'utf8'
|
||
);
|
||
}
|
||
}
|
||
|
||
function createCredentialsYamlIfMissing(): void {
|
||
const credPath = join(process.cwd(), 'credentials.yaml');
|
||
if (!existsSync(credPath)) {
|
||
writeFileSync(credPath,
|
||
'# arcrun credentials — 不要提交至 git!\n' +
|
||
'# 執行 acr creds push 上傳加密後的 credential 到你的 CF KV\n\n' +
|
||
'# gmail_token: "your-google-oauth-token"\n' +
|
||
'# telegram_bot_token: "your-telegram-bot-token"\n' +
|
||
'# google_oauth: "your-google-oauth-token"\n' +
|
||
'# line_token: "your-line-notify-token"\n',
|
||
'utf8'
|
||
);
|
||
}
|
||
|
||
// 確保 .gitignore 排除 credentials.yaml + 專案層 .arcrun.yaml(可能含 cf_api_token)
|
||
// 壓測 §1.2 安全附帶發現:憑證進版控 = 帳號控制權外洩。
|
||
const gitignorePath = join(process.cwd(), '.gitignore');
|
||
if (existsSync(gitignorePath)) {
|
||
const content = readFileSync(gitignorePath, 'utf8');
|
||
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');
|
||
}
|
||
}
|