2594f8371d
- 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>
190 lines
7.1 KiB
TypeScript
190 lines
7.1 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';
|
||
|
||
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.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<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 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_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 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');
|
||
}
|
||
}
|
||
}
|