feat(arcrun): implement arcrun MVP — open-source AI workflow engine
Phase 1-5 complete per .agents/specs/u6u-core-mvp/: **Phase 1 — Cherry-pick & cleanup** - Create arcrun/ from cypher-executor, credentials, builtins, registry - Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME) - Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error) - Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB) - Clean all KV namespace IDs and InkStone internal URLs from config files **Phase 2 — contract.yaml completeness** - Add credentials_required to gmail, google_sheets, telegram, line_notify - Add config_example to all 21 components with annotated field descriptions **Phase 3 — Credential injection** - Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV - Integrate into GraphExecutor before WASM execution - Structured errors with repair instructions when credential missing **Phase 4 — CLI (acr)** - cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora - 8 commands: init, creds push, push, run, validate, parts, list, logs - Standard mode: writes directly to user's CF KV via CF REST API - acr init: interactive setup with arcrun.dev API Key registration **Phase 5 — Open source release prep** - README.md: 5-minute quickstart, component table, workflow YAML syntax - CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow - Security audit: no InkStone internal URLs/IDs in committed files - .gitignore: exclude credentials.yaml, .wrangler, *.wasm https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 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 } 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://api.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: { selfHosted?: boolean }): Promise<void> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
console.log(chalk.bold('\n arcrun 初始化設定\n'));
|
||||
|
||||
try {
|
||||
if (options.selfHosted) {
|
||||
await initSelfHosted(rl);
|
||||
} else {
|
||||
await initStandard(rl);
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
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: cfApiToken }),
|
||||
});
|
||||
|
||||
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; tenant_id: 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 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 = require('node:fs').readFileSync(gitignorePath, 'utf8');
|
||||
if (!content.includes('credentials.yaml')) {
|
||||
require('node:fs').appendFileSync(gitignorePath, '\ncredentials.yaml\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user