/** * acr push * 解析三元組,轉成執行圖,直接寫入用戶的 USER_KV(key = workflow:{name}) */ import chalk from 'chalk'; import ora from 'ora'; import { loadConfig, getCypherExecutorUrl } from '../lib/config.js'; import { CfKvClient } from '../lib/cf-api.js'; import { loadWorkflowYaml, parseTriplets, validateRelations } from '../lib/yaml-parser.js'; export async function cmdPush(filePath: string): Promise { const config = loadConfig(); const spinner = ora('解析 workflow.yaml').start(); let workflow; try { workflow = loadWorkflowYaml(filePath); spinner.text = `驗證三元組(${workflow.flow.length} 條)`; const triplets = parseTriplets(workflow.flow); validateRelations(triplets); spinner.succeed(`解析完成:${workflow.name}(${triplets.length} 條三元組)`); } catch (e) { spinner.fail(chalk.red(`解析失敗:${e instanceof Error ? e.message : e}`)); process.exit(1); } // POST 到 cypher-executor 取得執行圖 const executorUrl = getCypherExecutorUrl(config); const triplets = parseTriplets(workflow.flow); const searchSpinner = ora(`向 ${executorUrl} 解析執行圖`).start(); let graph: unknown; try { const headers: Record = { 'Content-Type': 'application/json' }; if (config.api_key) headers['X-Arcrun-API-Key'] = config.api_key; const res = await fetch(`${executorUrl}/cypher/search`, { method: 'POST', headers, body: JSON.stringify({ triplets: workflow.flow }), }); if (!res.ok) { const err = await res.text(); searchSpinner.fail(chalk.red(`執行圖解析失敗(${res.status}):${err.slice(0, 200)}`)); process.exit(1); } const data = await res.json() as { cypher: unknown; missing: string[] }; if (data.missing.length > 0) { searchSpinner.fail(chalk.red(`以下零件不存在:${data.missing.join(', ')}\n執行 acr parts 查看可用零件。`)); process.exit(1); } graph = data.cypher; searchSpinner.succeed('執行圖解析完成'); } catch (e) { searchSpinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); process.exit(1); } // 寫入 USER_KV(key = workflow:{name}) const namespaceId = config.mode === 'standard' ? config.user_kv_namespace_id! : config.webhooks_kv_namespace_id!; if (!namespaceId || !config.cloudflare_account_id || !config.cf_api_token) { console.error(chalk.red('缺少 KV 設定,請執行 acr init')); process.exit(1); } const kv = new CfKvClient({ accountId: config.cloudflare_account_id, namespaceId, apiToken: config.cf_api_token, }); const kvSpinner = ora('寫入 workflow 至 CF KV').start(); try { const workflowDef = { name: workflow.name, description: workflow.description ?? '', graph, config: workflow.config ?? {}, created_at: new Date().toISOString(), }; await kv.put(`workflow:${workflow.name}`, JSON.stringify(workflowDef)); kvSpinner.succeed(chalk.green(`✓ workflow "${workflow.name}" 已寫入你的 CF KV`)); } catch (e) { kvSpinner.fail(chalk.red(`KV 寫入失敗:${e instanceof Error ? e.message : e}`)); process.exit(1); } const executorBase = getCypherExecutorUrl(config); const webhookUrl = `${executorBase}/webhooks/${workflow.name}`; console.log(chalk.bold(`\n Webhook URL:${chalk.cyan(webhookUrl)}`)); if (config.api_key) { console.log(chalk.gray(` (使用時需帶 X-Arcrun-API-Key: ${config.api_key.slice(0, 8)}...)\n`)); } console.log(` 執行:${chalk.cyan(`acr run ${workflow.name}`)}\n`); }