#!/usr/bin/env node // Backfill component metadata 進 registry index // SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 1 // // 用法: // node scripts/backfill-index.mjs --dry-run # 看會送什麼 // node scripts/backfill-index.mjs # 真的灌 // // 流程: // 1. 掃 ../components/*/component.contract.yaml // 2. 解析 YAML(用 zero-dep 簡易 parser,contract 是 well-formed YAML) // 3. 對每個 contract POST registry.arcrun.dev/components/index-only // 4. 印 success / already_indexed / fail 統計 import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const COMPONENTS_DIR = join(__dirname, '..', 'components'); const REGISTRY_URL = process.env.REGISTRY_URL ?? 'https://registry.arcrun.dev'; const DRY_RUN = process.argv.includes('--dry-run'); // YAML 是 well-formed contract.yaml,用 js-yaml 解析最穩 async function parseYaml(text) { const { load } = await import('js-yaml'); return load(text); } function listComponents() { return readdirSync(COMPONENTS_DIR) .filter((name) => { const p = join(COMPONENTS_DIR, name); return statSync(p).isDirectory(); }) .sort(); } async function readContract(name) { const path = join(COMPONENTS_DIR, name, 'component.contract.yaml'); const text = readFileSync(path, 'utf-8'); return parseYaml(text); } async function postIndexOnly(contract) { const res = await fetch(`${REGISTRY_URL}/components/index-only`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contract }), }); const body = await res.text(); let parsed; try { parsed = JSON.parse(body); } catch { parsed = { raw: body }; } return { status: res.status, body: parsed }; } async function main() { console.log('=== arcrun Component Registry Backfill ==='); console.log(`Registry: ${REGISTRY_URL}`); console.log(`Mode: ${DRY_RUN ? 'DRY RUN' : 'LIVE'}`); console.log(); const names = listComponents(); console.log(`Found ${names.length} components in ${COMPONENTS_DIR}`); console.log(); const stats = { created: 0, already: 0, fail: 0 }; for (const name of names) { let contract; try { contract = await readContract(name); } catch (e) { console.log(` ✗ ${name.padEnd(28)} READ FAIL: ${e.message}`); stats.fail++; continue; } const cid = contract.canonical_id ?? '(no canonical_id)'; const ver = contract.version ?? '(no version)'; if (DRY_RUN) { console.log(` → ${name.padEnd(28)} ${cid} ${ver}`); continue; } try { const { status, body } = await postIndexOnly(contract); if (status === 200 && body.already_indexed) { console.log(` = ${name.padEnd(28)} ${cid} ${ver} [already]`); stats.already++; } else if (status === 201) { console.log(` ✓ ${name.padEnd(28)} ${cid} ${ver} [${body.component_hash_id}]`); stats.created++; } else { console.log(` ✗ ${name.padEnd(28)} ${cid} ${ver} HTTP ${status}: ${JSON.stringify(body).slice(0, 200)}`); stats.fail++; } } catch (e) { console.log(` ✗ ${name.padEnd(28)} POST FAIL: ${e.message}`); stats.fail++; } } console.log(); console.log('=== Summary ==='); console.log(`Created: ${stats.created}`); console.log(`Already indexed: ${stats.already}`); console.log(`Failed: ${stats.fail}`); process.exit(stats.fail > 0 ? 1 : 0); } main().catch((e) => { console.error('Fatal:', e); process.exit(1); });