/** * 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'; const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register'; async function prompt(rl: ReturnType, question: string): Promise { const answer = await rl.question(chalk.cyan(`? ${question}: `)); return answer.trim(); } export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }): Promise { 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); } else { await initStandard(rl); } } finally { rl.close(); } } async function initLocal(): Promise { 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') + ' # 執行 hello workflow\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): Promise { console.log(chalk.gray(' Standard 模式:使用 arcrun.dev 的執行引擎,credential 存在你自己的 CF KV\n')); const accountId = await prompt(rl, '你的 Cloudflare Account ID'); const kvNamespaceId = await prompt(rl, 'USER_KV Namespace ID(先至 CF Dashboard 建立一個 KV 後貼上)' ); const cfApiToken = await prompt(rl, 'CF API Token(只需 KV Edit 權限,供 acr 讀寫你的 KV)' ); const email = await prompt(rl, 'Email(取得 arcrun.dev API Key)'); process.stdout.write(chalk.gray('\n → 向 arcrun.dev 取得 API Key...')); let apiKey: string; try { const res = await fetch(ARCRUN_REGISTER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), // CF API Token 永遠不離開本機 }); if (!res.ok) { const err = await res.text(); throw new Error(`API Key 取得失敗(${res.status}):${err}`); } const data = await res.json() as { api_key: string }; apiKey = data.api_key; console.log(chalk.green(' ✓')); } catch (e) { console.log(chalk.yellow(' ✗(離線模式,請稍後執行 acr init 重試)')); apiKey = ''; } const config: ArcrunConfig = { mode: 'standard', cloudflare_account_id: accountId, user_kv_namespace_id: kvNamespaceId, cf_api_token: cfApiToken, api_key: apiKey, }; saveConfig(config); // 建立空白 credentials.yaml createCredentialsYamlIfMissing(); console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml')); if (apiKey) { console.log(chalk.green(` ✓ API Key:${apiKey.slice(0, 8)}...(已安全儲存)`)); } console.log(chalk.green(' ✓ 建立 credentials.yaml(已加入 .gitignore)\n')); console.log(chalk.gray(' 你的 credential 與 workflow 存在你自己的 CF KV,arcrun 不會儲存它們。\n')); console.log(' 下一步:'); console.log(chalk.cyan(' acr creds push credentials.yaml') + ' # 上傳加密 credentials'); console.log(chalk.cyan(' acr push workflow.yaml') + ' # 部署 workflow'); console.log(chalk.cyan(' acr run ') + ' # 執行 workflow\n'); } async function initSelfHosted(rl: ReturnType): Promise { console.log(chalk.gray(' Self-hosted 模式:自行部署所有 Worker 到你的 Cloudflare 帳號\n')); const accountId = await prompt(rl, '你的 Cloudflare Account ID'); const cypherUrl = await prompt(rl, 'Cypher Executor URL(部署後的 workers.dev URL)'); const webhooksKvId = await prompt(rl, 'WEBHOOKS KV Namespace ID'); const credentialsKvId = await prompt(rl, 'CREDENTIALS_KV Namespace ID'); const wasmBucket = await prompt(rl, 'WASM_BUCKET 名稱'); const cfApiToken = await prompt(rl, 'CF API Token(KV Edit 權限)'); const config: ArcrunConfig = { mode: 'self-hosted', cloudflare_account_id: accountId, cypher_executor_url: cypherUrl, webhooks_kv_namespace_id: webhooksKvId, credentials_kv_namespace_id: credentialsKvId, wasm_bucket: wasmBucket, cf_api_token: cfApiToken, multi_tenant: false, }; saveConfig(config); createCredentialsYamlIfMissing(); console.log(chalk.green('\n ✓ 設定完成 → ~/.arcrun/config.yaml')); console.log(chalk.green(' ✓ 建立 credentials.yaml\n')); } function createHelloYamlIfMissing(): void { const helloPath = join(process.cwd(), 'hello.yaml'); if (!existsSync(helloPath)) { writeFileSync(helloPath, '# arcrun hello world workflow\n' + '# 執行:acr run hello\n\n' + 'name: hello\n' + 'description: "Hello world — 示範如何讓 AI 處理訊息後傳送通知"\n\n' + 'flow:\n' + ' - "input >> ON_SUCCESS >> ai_reply"\n' + ' - "ai_reply >> ON_SUCCESS >> log_output"\n\n' + 'config:\n' + ' ai_reply:\n' + ' component: "component://cmp_openai_chat"\n' + ' model: "gpt-4o-mini"\n' + ' prompt: "請用繁體中文說 Hello World,並解釋 arcrun 是什麼"\n' + ' log_output:\n' + ' component: "component://cmp_log_stdout"\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 const gitignorePath = join(process.cwd(), '.gitignore'); if (existsSync(gitignorePath)) { const content = readFileSync(gitignorePath, 'utf8'); if (!content.includes('credentials.yaml')) { appendFileSync(gitignorePath, '\ncredentials.yaml\n'); } } }