BMAD-METHOD/tools/sync-from-manifest.js

172 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Sync agent instructions from WDS repo to Design Space.
* Reads sync-manifest.json to determine what to sync.
*
* Usage:
* node sync-from-manifest.js # sync all stable
* node sync-from-manifest.js --channel beta # sync beta channel
* node sync-from-manifest.js --agent saga # sync one agent
* node sync-from-manifest.js --dry-run # preview only
*
* Environment:
* DESIGN_SPACE_URL Supabase project URL
* DESIGN_SPACE_ANON_KEY Supabase anon key
* WDS_ROOT Path to WDS repo (default: auto-detect)
*/
import { readFileSync, existsSync } from 'fs';
import { join, basename, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = process.env.WDS_ROOT || join(__dirname, '..');
// --- Load env ---
for (const envPath of [join(REPO_ROOT, '.env'), join(__dirname, '../../design-space/.env')]) {
if (existsSync(envPath)) {
for (const line of readFileSync(envPath, 'utf8').replace(/\r/g, '').split('\n')) {
const match = line.match(/^([^#=]+)=(.*)$/);
if (match && !process.env[match[1].trim()]) {
process.env[match[1].trim()] = match[2].trim();
}
}
}
}
const SUPABASE_URL = process.env.DESIGN_SPACE_URL;
const SUPABASE_KEY = process.env.DESIGN_SPACE_ANON_KEY;
if (!SUPABASE_URL || !SUPABASE_KEY) {
console.error('Missing DESIGN_SPACE_URL or DESIGN_SPACE_ANON_KEY');
process.exit(1);
}
// --- Args ---
const DRY_RUN = process.argv.includes('--dry-run');
const CHANNEL = (() => {
const idx = process.argv.indexOf('--channel');
return idx !== -1 ? process.argv[idx + 1] : 'stable';
})();
const AGENT_FILTER = (() => {
const idx = process.argv.indexOf('--agent');
return idx !== -1 ? process.argv[idx + 1] : null;
})();
// --- Load manifest ---
const manifestPath = join(REPO_ROOT, 'src/sync-manifest.json');
if (!existsSync(manifestPath)) {
console.error(`Manifest not found: ${manifestPath}`);
process.exit(1);
}
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
console.log(`WDS Instruction Sync v${manifest.version}`);
console.log(`Repo: ${REPO_ROOT}`);
console.log(`Channel: ${CHANNEL}`);
if (AGENT_FILTER) console.log(`Agent filter: ${AGENT_FILTER}`);
console.log();
// --- Filter and collect ---
const filtered = manifest.instructions.filter(i => {
if (i.channel !== CHANNEL) return false;
if (AGENT_FILTER && i.agent !== AGENT_FILTER && i.agent !== '*') return false;
return true;
});
console.log(`${filtered.length} instructions to sync`);
console.log();
// --- Process ---
let uploaded = 0;
let skipped = 0;
let failed = 0;
for (const instr of filtered) {
const filePath = join(REPO_ROOT, instr.file);
if (!existsSync(filePath)) {
console.warn(` SKIP (not found): ${instr.file}`);
skipped++;
continue;
}
const content = readFileSync(filePath, 'utf8');
const hash = createHash('sha256').update(content).digest('hex').substring(0, 12);
const name = basename(instr.file);
if (DRY_RUN) {
console.log(` [${instr.agent}] ${instr.type}/${name} (${content.length} chars, hash: ${hash})`);
continue;
}
try {
// Check if this exact version exists (by source_file + hash in metadata)
const checkRes = await fetch(`${SUPABASE_URL}/rest/v1/design_space?select=id,metadata&category=eq.agent_instruction&source_file=eq.${instr.file.replace(/\//g, '%2F')}&limit=1`, {
headers: {
'Authorization': `Bearer ${SUPABASE_KEY}`,
'apikey': SUPABASE_KEY,
},
});
const existing = await checkRes.json();
if (existing?.[0]?.metadata?.hash === hash) {
skipped++;
continue;
}
// Delete old version if exists
if (existing?.[0]?.id) {
await fetch(`${SUPABASE_URL}/rest/v1/design_space?id=eq.${existing[0].id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${SUPABASE_KEY}`,
'apikey': SUPABASE_KEY,
},
});
}
// Upload new version
const res = await fetch(`${SUPABASE_URL}/functions/v1/capture-design-space`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SUPABASE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content,
category: 'agent_instruction',
project: 'wds',
designer: instr.agent,
topics: [instr.type, instr.agent, `channel:${instr.channel}`],
components: [name],
source: 'sync-manifest',
source_file: instr.file,
metadata: {
agent: instr.agent,
type: instr.type,
name,
layer: 'framework',
channel: instr.channel,
hash,
manifest_version: manifest.version,
},
}),
});
if (res.ok) {
uploaded++;
console.log(` OK [${instr.agent}] ${instr.type}/${name}`);
} else {
failed++;
console.error(` FAIL [${instr.agent}] ${name}: ${await res.text()}`);
}
} catch (err) {
failed++;
console.error(` FAIL [${instr.agent}] ${name}: ${err.message}`);
}
}
console.log();
console.log(`Done: ${uploaded} uploaded, ${skipped} unchanged, ${failed} failed`);
if (failed > 0) process.exit(1);