2aa26a5bdd
leo 2026-06-29 重構根因:「telegram 與 google 不一致」其實是查錯表——舊世界要找一個能力 必須先決定它是 component/recipe/workflow 才查對清單,但查到答案前根本不知道屬哪類 → 「先知道分類」成了查詢前置,倒因為果(leo:連我都會查錯表)。這是搜尋設計缺陷,非粗心。 兩部分互補修法: 1. acr parts 清理(W3.1/W3.3):移除 5 個 hardcode 的 recipe(gmail_send/google_sheets_append/ telegram_send/line_notify_send/notion)——它們是 recipe(動態,存 store)不是零件(component)。 只留真 WASM 零件。檔頭寫死分流原則 + 移除處留註解防再 hardcode。footer 改指向動態清單 + acr search。 2. acr search <term> 新增(W3.2,正向修法):cli/src/commands/search.ts。fan-out 4 來源—— component(靜態 BUILTIN_COMPONENTS) + recipe(GET /recipes) + auth-recipe(GET /auth-recipes) + workflow(GET /webhooks/named),依類別回 counts+命中。各來源獨立 try(離線降級指路、不連坐)。 查的人不必先選表 → 結構性消除「查錯表」。 薄殼合規(rule 07 §2):search 只 fan-out 既有清單端點 + 過濾 + 分組 + 印出(介面層暴露/格式化), 不在介面層拼裝能力。component 靜態(PR-only)、recipe/auth/workflow 動態(store) 分流明確、不再 conflate。 實證(對 leo21c 跑,read-only GET): - acr search telegram → component:0 recipe:1 auth-recipe:0 workflow:1(一眼看出 telegram 是 recipe) - acr parts → 只剩 http_request 等真零件,5 個 recipe 已不在 - tsc 全綠、build 通過 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
241 lines
10 KiB
JavaScript
241 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* arcrun CLI — acr
|
||
* AI Workflow CLI for Cloudflare Workers + WASM
|
||
*
|
||
* 安裝:npm i -g arcrun
|
||
* 使用:acr <指令>
|
||
*/
|
||
import { Command } from 'commander';
|
||
import { readFileSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import { cmdInit } from './commands/init.js';
|
||
import { cmdConfig } from './commands/config.js';
|
||
import { cmdWhoami } from './commands/whoami.js';
|
||
import { cmdCredsPush } from './commands/creds.js';
|
||
import { cmdPush } from './commands/push.js';
|
||
import { cmdRun } from './commands/run.js';
|
||
import { cmdValidate } from './commands/validate.js';
|
||
import { cmdParts, cmdPartsScaffold, cmdPartsPublish } from './commands/parts.js';
|
||
import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete, cmdRecipeSearch, cmdRecipePull, cmdRecipeSubmitP } from './commands/recipe.js';
|
||
import { cmdList } from './commands/list.js';
|
||
import { cmdSearch } from './commands/search.js';
|
||
import { cmdLogs } from './commands/logs.js';
|
||
import { cmdUpdate } from './commands/update.js';
|
||
import { cmdInstallHarness } from './commands/install-harness.js';
|
||
import { cmdMcpSetup } from './commands/mcp-setup.js';
|
||
import {
|
||
cmdKbdbTemplateCreate,
|
||
cmdKbdbTemplateList,
|
||
cmdKbdbRecordCreate,
|
||
cmdKbdbRecordGet,
|
||
cmdKbdbQuery,
|
||
cmdKbdbSearch,
|
||
} from './commands/kbdb.js';
|
||
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
|
||
|
||
const program = new Command();
|
||
|
||
// 版本從 package.json 動態讀(dist/index.js 旁的 ../package.json),不 hardcode → 永不漂移。
|
||
// 之前 hardcode '1.1.0' 與 package.json '1.2.0' 不一致,正是「忘了改」的反例。
|
||
function readVersion(): string {
|
||
try {
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
return (JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')) as { version?: string }).version ?? '0.0.0';
|
||
} catch {
|
||
return '0.0.0';
|
||
}
|
||
}
|
||
|
||
program
|
||
.name('acr')
|
||
.description('arcrun — AI Workflow CLI for Cloudflare Workers + WASM')
|
||
.version(readVersion());
|
||
|
||
// acr init [--self-hosted] [--account-id <id>] [--api-token <token>]
|
||
program
|
||
.command('init')
|
||
.description('互動式初始化設定(建立 ~/.arcrun/config.yaml)')
|
||
.option('--local', '本機模式:不需要 Cloudflare 帳號,直接在本機測試 workflow')
|
||
.option('--self-hosted', '完全 Self-hosted 模式:自行部署所有 Cloudflare Worker')
|
||
.option('--account-id <id>', 'self-hosted:CF Account ID(非互動;亦可用 CLOUDFLARE_ACCOUNT_ID env)')
|
||
.option('--api-token <token>', 'self-hosted:CF API Token(非互動;亦可用 CLOUDFLARE_API_TOKEN env)')
|
||
.action((options: { local?: boolean; selfHosted?: boolean; accountId?: string; apiToken?: string }) =>
|
||
cmdInit(options),
|
||
);
|
||
|
||
// acr config [--where]:印出目前生效的設定與每個值的來源層(env / 專案層 / 全域)
|
||
program
|
||
.command('config')
|
||
.description('顯示目前生效的設定與來源層(避免在錯的資料夾用錯帳號)')
|
||
.option('--where', '顯示每個設定值來自哪一層(env > 專案層 .arcrun.yaml > 全域)')
|
||
.action((options: { where?: boolean }) => cmdConfig(options));
|
||
|
||
// acr whoami [--json]:一眼看當前身份(mode / 連哪台 cypher / 帳號來源層)。§7.8 P1 D2 修法。
|
||
program
|
||
.command('whoami')
|
||
.description('顯示目前生效的身份(帳號、連哪台 cypher、來源層)——AI 別自己 curl 猜帳號')
|
||
.option('--json', '結構化輸出(給 AI / 腳本讀取)')
|
||
.action((options: { json?: boolean }) => cmdWhoami(options));
|
||
|
||
// acr creds push [credentials.yaml]
|
||
const credsCmd = program.command('creds').description('Credential 管理');
|
||
credsCmd
|
||
.command('push [file]')
|
||
.description('加密上傳 credentials.yaml 至你的 CF KV(不經過 arcrun.dev)')
|
||
.action((file: string) => cmdCredsPush(file ?? 'credentials.yaml'));
|
||
|
||
// acr push <workflow.yaml>
|
||
program
|
||
.command('push <file>')
|
||
.description('解析 workflow.yaml 並部署至你的 CF KV')
|
||
.action((file: string) => cmdPush(file));
|
||
|
||
// acr run <workflow_name> [--input key=value...]
|
||
program
|
||
.command('run <workflow>')
|
||
.description('執行指定 workflow')
|
||
.option('-i, --input <pairs...>', 'input 參數(格式:key=value)')
|
||
.action((workflow: string, options: { input?: string[] }) => cmdRun(workflow, options));
|
||
|
||
// acr validate <workflow.yaml>
|
||
program
|
||
.command('validate <file>')
|
||
.description('執行前驗證 workflow.yaml(格式、關係詞、零件存在性、credentials)')
|
||
.option('--offline', '離線模式:跳過零件存在性與 credentials 的遠端檢查')
|
||
.action((file: string, options: { offline?: boolean }) => cmdValidate(file, options));
|
||
|
||
// acr parts
|
||
// acr parts scaffold <component>
|
||
// acr parts publish <component> [--status <submission_id>]
|
||
const partsCmd = program.command('parts').description('零件庫管理');
|
||
partsCmd
|
||
.action(() => cmdParts());
|
||
|
||
partsCmd
|
||
.command('scaffold <component>')
|
||
.description('輸出零件的 config 範本(可直接貼入 workflow.yaml)')
|
||
.action((component: string) => cmdPartsScaffold(component));
|
||
|
||
partsCmd
|
||
.command('publish <component-dir>')
|
||
.description('提交零件至 arcrun.dev 公眾 registry')
|
||
.option('--status <submission_id>', '查詢提交審核進度')
|
||
.action((dir: string, options: { status?: string }) => cmdPartsPublish(dir, options));
|
||
|
||
// acr recipe push / list / delete
|
||
const recipeCmd = program.command('recipe').description('API Recipe 管理');
|
||
recipeCmd
|
||
.command('push <file>')
|
||
.description('上傳 recipe YAML 到 arcrun.dev(不需要 deploy Worker)')
|
||
.action((file: string) => cmdRecipePush(file));
|
||
recipeCmd
|
||
.command('list')
|
||
.description('列出已上傳的 recipe')
|
||
.action(() => cmdRecipeList());
|
||
recipeCmd
|
||
.command('delete <id>')
|
||
.description('刪除 recipe(canonical_id 或 rec_hash)')
|
||
.action((id: string) => cmdRecipeDelete(id));
|
||
// 公庫互動(kbdb-base §7.5)
|
||
recipeCmd
|
||
.command('search <query>')
|
||
.description('搜尋公庫 recipe(同名可多作者,附市場數據)')
|
||
.action((query: string) => cmdRecipeSearch(query));
|
||
recipeCmd
|
||
.command('pull <canonical_id>')
|
||
.description('從公庫取一份 recipe 寫進自己私庫')
|
||
.option('--author <name>', '指定作者版本(不指定取市場最佳)')
|
||
.action((canonicalId: string, opts: { author?: string }) => cmdRecipePull(canonicalId, opts.author));
|
||
recipeCmd
|
||
.command('submit-p <canonical_id>')
|
||
.description('把私庫某 recipe 投稿到公庫(新增作者版本,需暴露同意)')
|
||
.option('--author <name>', '署名作者(預設用 recipe 既有 author)')
|
||
.action((canonicalId: string, opts: { author?: string }) => cmdRecipeSubmitP(canonicalId, opts.author));
|
||
|
||
// acr auth-recipe list / info / scaffold
|
||
const authRecipeCmd = program.command('auth-recipe').description('第三方服務認證 Recipe(新增服務整合)');
|
||
authRecipeCmd
|
||
.command('list')
|
||
.description('列出所有平台預建的服務整合(Notion、Slack、GitHub 等)')
|
||
.action(() => cmdAuthRecipeList());
|
||
authRecipeCmd
|
||
.command('info <service>')
|
||
.description('顯示服務 recipe 詳情(需要哪些 credential)')
|
||
.action((service: string) => cmdAuthRecipeInfo(service));
|
||
authRecipeCmd
|
||
.command('scaffold <service>')
|
||
.description('輸出 credentials.yaml 範本 + workflow.yaml 使用範例')
|
||
.action((service: string) => cmdAuthRecipeScaffold(service));
|
||
|
||
// acr kbdb — KBDB 資料層薄殼(kbdb-base 9.2,透過 cypher KBDB proxy;與 MCP kbdb_* 同能力)
|
||
const kbdbCmd = program.command('kbdb').description('KBDB 資料層(template/record/query/search;不建表、不寫 SQL)');
|
||
const kbdbTemplateCmd = kbdbCmd.command('template').description('template = 虛擬表定義(name + slots)');
|
||
kbdbTemplateCmd
|
||
.command('create <name>')
|
||
.description('建一個 template(虛擬表定義),如 --slots name,email,phone')
|
||
.requiredOption('--slots <list>', '欄位名清單,逗號分隔,如 name,email,phone')
|
||
.action((name: string, opts: { slots: string }) => cmdKbdbTemplateCreate(name, opts));
|
||
kbdbTemplateCmd
|
||
.command('list')
|
||
.description('列出所有 template')
|
||
.action(() => cmdKbdbTemplateList());
|
||
const kbdbRecordCmd = kbdbCmd.command('record').description('record = 依 template 填的一筆資料');
|
||
kbdbRecordCmd
|
||
.command('create <template>')
|
||
.description('填一筆 record,如 --values name=Leo --values email=x@y.com(可重複)')
|
||
.option('--values <pair...>', 'slot=內容(可重複)')
|
||
.action((template: string, opts: { values?: string[] }) => cmdKbdbRecordCreate(template, opts));
|
||
kbdbRecordCmd
|
||
.command('get <record_id>')
|
||
.description('取單筆 record 全文')
|
||
.action((recordId: string) => cmdKbdbRecordGet(recordId));
|
||
kbdbCmd
|
||
.command('query <template>')
|
||
.description('列某 template 下本租戶的所有 record')
|
||
.action((template: string) => cmdKbdbQuery(template));
|
||
kbdbCmd
|
||
.command('search <q>')
|
||
.description('關鍵字搜尋本租戶內容(LIKE,基本盤)')
|
||
.action((q: string) => cmdKbdbSearch(q));
|
||
|
||
// acr list
|
||
program
|
||
.command('list')
|
||
.description('列出 CF KV 中所有已部署的 workflow')
|
||
.action(() => cmdList());
|
||
|
||
// acr search <term> — 跨類統一搜尋(component / recipe / auth-recipe / workflow),免先選表
|
||
program
|
||
.command('search <term>')
|
||
.description('跨類搜尋能力(零件/recipe/auth-recipe/workflow),一次掃全部、依類別回 counts+命中')
|
||
.action((term: string) => cmdSearch(term));
|
||
|
||
// acr logs <workflow_name>
|
||
program
|
||
.command('logs <workflow>')
|
||
.description('顯示 workflow 最近執行記錄')
|
||
.action((workflow: string) => cmdLogs(workflow));
|
||
|
||
// acr update(self-hosted:拉新 release 重新部署零件/引擎)
|
||
program
|
||
.command('update')
|
||
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
|
||
.option('--force', '強制重部所有 worker(忽略未變動跳過快取)')
|
||
.action((opts: { force?: boolean }) => cmdUpdate({ force: opts.force }));
|
||
|
||
// acr install-harness(把 arcrun 的 CC harness 裝進當前專案)
|
||
program
|
||
.command('install-harness')
|
||
.description('把 arcrun 的 CC harness(mindset/提醒/防做歪 hook/指令)裝進當前專案')
|
||
.action(() => cmdInstallHarness());
|
||
|
||
// acr mcp-setup(依 config 解析的 mcp_url 寫專案 .mcp.json,讓 Claude Code 連對的 MCP)
|
||
program
|
||
.command('mcp-setup')
|
||
.description('在目前資料夾寫 .mcp.json 連對的 arcrun MCP(依 config 的 mcp_url;接案切資料夾自動切)')
|
||
.action(() => cmdMcpSetup());
|
||
|
||
program.parse(process.argv);
|