Files
Arcrun/cli/src/commands/init.ts
T
Leo 2594f8371d feat: add /register endpoint + fix acr run Mode 1 (inline YAML execution)
- POST /register on cypher.arcrun.dev: HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{32hex}, no DB needed
- acr run: Mode 1 (standard/local) now finds local YAML and POSTs to /cypher/execute inline
- acr init: fix register URL → cypher.arcrun.dev/register; fix local mode description
- acr init --local: creates hello.yaml example workflow
- cli v1.0.3 published

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:04:14 +08:00

190 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<typeof createInterface>, question: string): Promise<string> {
const answer = await rl.question(chalk.cyan(`? ${question}: `));
return answer.trim();
}
export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }): 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);
} else {
await initStandard(rl);
}
} finally {
rl.close();
}
}
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.yamllocal 模式)'));
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 initStandard 模式)。\n'));
}
async function initStandard(rl: ReturnType<typeof createInterface>): Promise<void> {
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 KVarcrun 不會儲存它們。\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_name>') + ' # 執行 workflow\n');
}
async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<void> {
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 TokenKV 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');
}
}
}