feat: bidirectional research dashboard + agent config fixes
- Integrate apaw_agent_model_research_v3.html as standalone dashboard - Add model-benchmarks.json with 32 agents, 11 scored models, 11 recommendations - Add build-research-dashboard.ts: inject live data into template → standalone HTML - Add rebuild-template.cjs: regenerate template from v3.html source - Add sync-benchmarks-from-yaml.cjs: sync YAML → JSON round-trip - Add sync-model-research.ts: apply recommendation matrix to config files - Add model-benchmarks.schema.json and model-research.schema.json for validation - Add bidirectional-data-flow.md architecture documentation - Add log-execution.cjs pipeline hook - Update capability-index.yaml: add fallback_models, failover_strategy - Update kilo-meta.json, kilo.jsonc, KILO_SPEC.md with synced models - Update evolution.md / research.md / self-evolution.md / evolutionary-sync.md docs - Fix security-auditor.md: quote YAML color (#DC2626) - Fix orchestrator.md: remove duplicate devops-engineer key - Build research-dashboard.html (106KB standalone) + dated archive
This commit is contained in:
237
agent-evolution/scripts/build-research-dashboard.ts
Normal file
237
agent-evolution/scripts/build-research-dashboard.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Build APAW Agent Model Research Dashboard from live data.
|
||||
*
|
||||
* Reads model-benchmarks.json and injects into template HTML.
|
||||
* Creates standalone dashboard with embedded JSON data.
|
||||
*
|
||||
* Usage:
|
||||
* bun run agent-evolution/scripts/build-research-dashboard.ts # build once
|
||||
* bun run agent-evolution/scripts/build-research-dashboard.ts --watch # watch mode
|
||||
* bun run agent-evolution/scripts/build-research-dashboard.ts --template path/to/custom.html
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, watch } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DATA_FILE = join(__dirname, '../data/model-benchmarks.json');
|
||||
const DEFAULT_TEMPLATE = join(__dirname, '../research-dashboard.template.html');
|
||||
const OUTPUT_FILE = join(__dirname, '../research-dashboard.html');
|
||||
const DIST_DIR = join(__dirname, '../dist');
|
||||
|
||||
interface BenchmarksData {
|
||||
version: string;
|
||||
generated: string;
|
||||
source: string;
|
||||
total_agents: number;
|
||||
total_models_tracked: number;
|
||||
providers: string[];
|
||||
models: any[];
|
||||
agent_model_scores: any[];
|
||||
agent_current_config: any[];
|
||||
groq_models: any[];
|
||||
recommendations: any[];
|
||||
impact_data: any[];
|
||||
}
|
||||
|
||||
function buildDashboard(templatePath: string = DEFAULT_TEMPLATE): boolean {
|
||||
console.log('🔧 Building APAW Agent Model Research Dashboard');
|
||||
|
||||
// Validate inputs
|
||||
if (!existsSync(DATA_FILE)) {
|
||||
console.error(`❌ Data file not found: ${DATA_FILE}`);
|
||||
console.error(' Please run research cycle first: bun run /research models');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!existsSync(templatePath)) {
|
||||
console.error(`❌ Template file not found: ${templatePath}`);
|
||||
console.error(' Using default template:', DEFAULT_TEMPLATE);
|
||||
if (!existsSync(DEFAULT_TEMPLATE)) {
|
||||
console.error(' Default template also missing. Create template first.');
|
||||
return false;
|
||||
}
|
||||
templatePath = DEFAULT_TEMPLATE;
|
||||
}
|
||||
|
||||
// Read and validate JSON data
|
||||
let data: BenchmarksData;
|
||||
try {
|
||||
const rawData = readFileSync(DATA_FILE, 'utf-8');
|
||||
data = JSON.parse(rawData);
|
||||
console.log(`📖 Read model-benchmarks.json (${rawData.length} bytes)`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to parse JSON data: ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!data.models || !Array.isArray(data.models)) {
|
||||
console.error('❌ Missing or invalid "models" array in data');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!data.agent_model_scores || !Array.isArray(data.agent_model_scores)) {
|
||||
console.error('❌ Missing or invalid "agent_model_scores" array in data');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` Models: ${data.models.length}`);
|
||||
console.log(` Agents: ${data.agent_model_scores.length}`);
|
||||
console.log(` Providers: ${data.providers?.join(', ') || 'unknown'}`);
|
||||
console.log(` Generated: ${data.generated}`);
|
||||
|
||||
// Read HTML template
|
||||
let html: string;
|
||||
try {
|
||||
html = readFileSync(templatePath, 'utf-8');
|
||||
console.log(`📖 Read template: ${templatePath} (${html.length} bytes)`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to read template: ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find and replace placeholder — must match exact text in template
|
||||
const placeholder = '// BENCHMARK_DATA_PLACEHOLDER - will be replaced by build script\nconst EMBEDDED_DATA = {};\n';
|
||||
if (!html.includes(placeholder)) {
|
||||
// Try looser match with any line endings
|
||||
const loosePlaceholder = html.match(/\/\/\s*BENCHMARK_DATA_PLACEHOLDER[^\n]*\r?\n\s*const\s+EMBEDDED_DATA\s*=\s*\{\}\s*;\r?\n/);
|
||||
if (!loosePlaceholder) {
|
||||
console.error('❌ Placeholder not found in template');
|
||||
console.error(' Expected: "// BENCHMARK_DATA_PLACEHOLDER - will be replaced by build script\\nconst EMBEDDED_DATA = {};\\n"');
|
||||
const match = html.match(/BENCHMARK_DATA_PLACEHOLDER/);
|
||||
if (match) {
|
||||
const start = Math.max(0, match.index - 20);
|
||||
const end = Math.min(html.length, match.index + 120);
|
||||
console.error(' Found near:', JSON.stringify(html.slice(start, end)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
html = html.replace(loosePlaceholder[0], `// BENCHMARK_DATA_PLACEHOLDER - REPLACED BY BUILD SCRIPT\n// Generated from ${basename(DATA_FILE)} on ${new Date().toISOString()}\nconst EMBEDDED_DATA = ${JSON.stringify(data, null, 2)};\n`);
|
||||
} else {
|
||||
html = html.replace(placeholder, `// BENCHMARK_DATA_PLACEHOLDER - REPLACED BY BUILD SCRIPT\n// Generated from ${basename(DATA_FILE)} on ${new Date().toISOString()}\nconst EMBEDDED_DATA = ${JSON.stringify(data, null, 2)};\n`);
|
||||
}
|
||||
|
||||
// Update title with metadata if present (match any tag with APAW... in it)
|
||||
const titleRegex = /<title>[^<]*APAW[^<]*<\/title>/;
|
||||
if (titleRegex.test(html)) {
|
||||
const newTitle = `APAW Agent Model Research — generated ${data.generated.slice(0, 10)}`;
|
||||
html = html.replace(titleRegex, `<title>${newTitle}</title>`);
|
||||
}
|
||||
|
||||
// Update subtitle if present
|
||||
const subtitlePattern = /<div class="sub">([^<]*)<\/div>/;
|
||||
const newSubtitle = `<div class="sub">Live dashboard • ${data.models.length} models × ${data.agent_model_scores.length} agents • ${data.generated.slice(0, 10)}</div>`;
|
||||
if (subtitlePattern.test(html)) {
|
||||
html = html.replace(subtitlePattern, newSubtitle);
|
||||
}
|
||||
|
||||
// Write output file
|
||||
try {
|
||||
writeFileSync(OUTPUT_FILE, html);
|
||||
console.log(`✅ Output written to: ${OUTPUT_FILE} (${html.length} bytes)`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to write output: ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create dated version in dist directory
|
||||
try {
|
||||
if (!existsSync(DIST_DIR)) {
|
||||
require('fs').mkdirSync(DIST_DIR, { recursive: true });
|
||||
}
|
||||
const dateStr = data.generated.slice(0, 10).replace(/-/g, '_');
|
||||
const distFile = join(DIST_DIR, `research-dashboard-${dateStr}.html`);
|
||||
writeFileSync(distFile, html);
|
||||
console.log(`📁 Dated copy: ${distFile}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Could not create dated copy: ${error}`);
|
||||
}
|
||||
|
||||
// Print summary
|
||||
const recommendations = data.recommendations || [];
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` • Agents tracked: ${data.total_agents || data.agent_model_scores.length}`);
|
||||
console.log(` • Models benchmarked: ${data.total_models_tracked || data.models.length}`);
|
||||
console.log(` • Providers: ${data.providers?.join(', ')}`);
|
||||
console.log(` • Recommendations: ${recommendations.length}`);
|
||||
|
||||
if (recommendations.length >577.0) {
|
||||
const highImpact = recommendations.filter((r: any) => r.impact === 'high').length;
|
||||
const applied = recommendations.filter((r: any) => r.to_model?.includes('✅')).length;
|
||||
console.log(` • High-impact recommendations: ${highImpact}`);
|
||||
console.log(` • Applied recommendations: ${applied}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchMode(): void {
|
||||
console.log('👀 Watch mode enabled - monitoring data and template files');
|
||||
console.log(' Press Ctrl+C to stop');
|
||||
|
||||
let timeout: Timer | null = null;
|
||||
|
||||
watch(DATA_FILE, (eventType) => {
|
||||
if (eventType === 'change') {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
console.log('\n🔄 Data file changed, rebuilding...');
|
||||
buildDashboard();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
watch(DEFAULT_TEMPLATE, (eventType) => {
|
||||
if (eventType === 'change') {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
console.log('\n🔄 Template file changed, rebuilding...');
|
||||
buildDashboard();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parse CLI arguments
|
||||
const args = process.argv.slice(2);
|
||||
let watchModeEnabled = false;
|
||||
let customTemplate: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--watch') {
|
||||
watchModeEnabled = true;
|
||||
} else if (args[i] === '--template' && i + 1 < args.length) {
|
||||
customTemplate = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
Usage: bun run agent-evolution/scripts/build-research-dashboard.ts [options]
|
||||
|
||||
Options:
|
||||
--watch Watch for changes and rebuild automatically
|
||||
--template <path> Use custom HTML template file
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
bun run agent-evolution/scripts/build-research-dashboard.ts
|
||||
bun run agent-evolution/scripts/build-research-dashboard.ts --watch
|
||||
bun run agent-evolution/scripts/build-research-dashboard.ts --template custom.html
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
if (watchModeEnabled) {
|
||||
// Build once then watch
|
||||
buildDashboard(customTemplate);
|
||||
watchMode();
|
||||
} else {
|
||||
const success = buildDashboard(customTemplate);
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
74
agent-evolution/scripts/rebuild-template.cjs
Normal file
74
agent-evolution/scripts/rebuild-template.cjs
Normal file
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs');
|
||||
const v3 = fs.readFileSync('agent-evolution/ideas/apaw_agent_model_research_v3.html', 'utf8');
|
||||
|
||||
const dataStart = v3.indexOf('// ACTUAL STATE from _kilo.zip');
|
||||
const renderStart = v3.indexOf('// ======================= RENDER =======================');
|
||||
|
||||
if (dataStart === -1 || renderStart === -1) {
|
||||
console.error('Cannot find markers');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mapping = `// BENCHMARK_DATA_PLACEHOLDER - will be replaced by build script
|
||||
const EMBEDDED_DATA = {};
|
||||
|
||||
// === MAP EMBEDDED_DATA -> original v3 format ===
|
||||
const allModels = EMBEDDED_DATA.models || [];
|
||||
const scoreModelIds = Object.keys((EMBEDDED_DATA.agent_model_scores || [])[0]?.scores || {});
|
||||
const activeModels = allModels.filter(m => scoreModelIds.includes(m.id));
|
||||
|
||||
const cfg = (EMBEDDED_DATA.agent_current_config || []).map(c => {
|
||||
const modelId = (c.model || '').replace('ollama-cloud/', '');
|
||||
const badge = c.badge_type || (
|
||||
modelId.includes('qwen3') ? 'qwen' :
|
||||
modelId.includes('minimax') ? 'minimax' :
|
||||
modelId.includes('nemotron') ? 'nemotron' :
|
||||
modelId.includes('glm') ? 'glm' :
|
||||
modelId.includes('kimi') ? 'kimi' :
|
||||
modelId.includes('deepseek') ? 'deepseek' : 'groq'
|
||||
);
|
||||
return { a: c.agent, m: modelId, p: c.provider || 'Ollama', cat: c.category || 'General', b: badge, fit: c.fit_score || 0, s: c.status || 'good', prev: c.previous_model };
|
||||
});
|
||||
|
||||
const groqModels = (EMBEDDED_DATA.groq_models || []).map(g => ({
|
||||
id: g.id, rpm: g.rpm, rpd: g.rpd, tpm: g.tpm, tpd: g.tpd, speed: g.speed, use: g.use_case
|
||||
}));
|
||||
|
||||
const ollamaModels = activeModels.map(m => ({
|
||||
n: m.name, org: m.organization, par: m.parameters, ctx: m.context_window,
|
||||
swe: m.swe_bench, ifScore: m.if_score, cat: m.categories || [],
|
||||
str: m.description, tags: m.tags || [], or: m.openrouter, groqSpeed: m.speed_tps
|
||||
}));
|
||||
|
||||
const ifScores = {};
|
||||
activeModels.forEach((m, i) => { if (m.if_score) ifScores[i] = m.if_score; });
|
||||
|
||||
const hmModels = activeModels.map(m => ({
|
||||
n: m.display_name || m.name?.split(' ').pop() || m.id,
|
||||
p: m.provider === 'ollama-cloud' ? 'Ollama Cloud' : m.provider === 'openrouter' ? 'OpenRouter' : m.provider || 'Ollama',
|
||||
if: m.if_score || 0
|
||||
}));
|
||||
|
||||
const hmAgents = (EMBEDDED_DATA.agent_model_scores || []).map(ag => {
|
||||
const scores = activeModels.map(m => ag.scores?.[m.id] ?? 0);
|
||||
const fullModelId = allModels[ag.current_model_index]?.id;
|
||||
const c = activeModels.findIndex(m => m.id === fullModelId);
|
||||
return { n: ag.agent, c: c, re: ag.reasoning_effort || 'M', s: scores };
|
||||
});
|
||||
|
||||
const recs = (EMBEDDED_DATA.recommendations || []).map(r => ({
|
||||
a: r.agent, from: r.from_model, fromP: r.from_provider || 'Ollama',
|
||||
to: r.to_model, toP: r.to_provider || 'Ollama', imp: r.impact || 'low',
|
||||
q: r.quality_change || '0', sp: r.speed_change || '=', ctx: r.context_change || '-',
|
||||
prov: r.provider_change || r.to_provider || 'Ollama', r: r.rationale
|
||||
}));
|
||||
|
||||
const impactData = (EMBEDDED_DATA.impact_data || []).map(d => ({
|
||||
cat: d.category, b: d.before, a: d.after, d: d.delta, n: d.notes || d.note
|
||||
}));
|
||||
|
||||
`;
|
||||
|
||||
const final = v3.substring(0, dataStart) + mapping + v3.substring(renderStart);
|
||||
fs.writeFileSync('agent-evolution/research-dashboard.template.html', final);
|
||||
console.log('Template written:', final.length, 'chars,', final.split('\n').length, 'lines');
|
||||
136
agent-evolution/scripts/sync-benchmarks-from-yaml.cjs
Normal file
136
agent-evolution/scripts/sync-benchmarks-from-yaml.cjs
Normal file
@@ -0,0 +1,136 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Parse simple YAML structure with 2-space indentation
|
||||
function parseCapabilityIndex(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const agents = {};
|
||||
let currentAgent = '';
|
||||
let currentList = '';
|
||||
|
||||
for (const line of lines) {
|
||||
const indent = line.length - line.trimStart().length;
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (indent === 2 && trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
||||
// Agent name
|
||||
currentAgent = trimmed.slice(0, -1);
|
||||
agents[currentAgent] = {};
|
||||
currentList = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indent === 4 && trimmed.endsWith(':') && !trimmed.startsWith('-')) {
|
||||
// Scalar property or list start
|
||||
const key = trimmed.slice(0, -1);
|
||||
currentList = key;
|
||||
if (!Array.isArray(agents[currentAgent][key])) {
|
||||
agents[currentAgent][key] = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indent === 4 && trimmed.includes(':') && !trimmed.startsWith('-')) {
|
||||
// key: value
|
||||
const [key, ...rest] = trimmed.split(':');
|
||||
const value = rest.join(':').trim();
|
||||
agents[currentAgent][key.trim()] = value;
|
||||
currentList = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indent >= 6 && trimmed.startsWith('- ')) {
|
||||
// List item
|
||||
const value = trimmed.slice(2).trim();
|
||||
if (currentList) {
|
||||
if (!agents[currentAgent][currentList]) agents[currentAgent][currentList] = [];
|
||||
agents[currentAgent][currentList].push(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset list context on unknown indentation
|
||||
if (indent < 4) {
|
||||
currentList = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out non-agent entries (flat sections like capability_routing, etc.)
|
||||
const result = {};
|
||||
const scalarKeys = ['capabilities','receives','produces','forbidden','delegates_to','fallback_models'];
|
||||
for (const [name, data] of Object.entries(agents)) {
|
||||
const hasAgentProps = scalarKeys.some(k => k in data) || 'model' in data;
|
||||
if (hasAgentProps) result[name] = data;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const yaml = fs.readFileSync('.kilo/capability-index.yaml', 'utf8');
|
||||
const parsed = parseCapabilityIndex(yaml);
|
||||
console.log('Parsed agents:', Object.keys(parsed).length);
|
||||
|
||||
// Read existing benchmarks
|
||||
const bench = JSON.parse(fs.readFileSync('agent-evolution/data/model-benchmarks.json', 'utf8'));
|
||||
|
||||
// Update agent_current_config
|
||||
bench.agent_current_config = Object.entries(parsed).map(([agent, data]) => {
|
||||
const rawModel = data.model || '';
|
||||
const modelId = rawModel.replace('ollama-cloud/', '');
|
||||
const badge = modelId.includes('qwen3') ? 'qwen' :
|
||||
modelId.includes('minimax') ? 'minimax' :
|
||||
modelId.includes('nemotron') ? 'nemotron' :
|
||||
modelId.includes('glm') ? 'glm' :
|
||||
modelId.includes('kimi') ? 'kimi' :
|
||||
modelId.includes('deepseek') ? 'deepseek' : 'groq';
|
||||
return {
|
||||
agent,
|
||||
model: rawModel,
|
||||
provider: data.mode === 'all' ? 'Ollama Cloud' : (rawModel.startsWith('ollama-cloud/') ? 'Ollama Cloud' : 'Ollama'),
|
||||
category: 'Process',
|
||||
badge_type: badge,
|
||||
fit_score: 0,
|
||||
status: 'good',
|
||||
previous_model: null
|
||||
};
|
||||
});
|
||||
|
||||
// Update agent_model_scores — preserve existing scores, fix current_model_id
|
||||
const existingScores = {};
|
||||
(bench.agent_model_scores || []).forEach(s => {
|
||||
existingScores[s.agent] = s.scores || {};
|
||||
});
|
||||
|
||||
bench.agent_model_scores = Object.entries(parsed).map(([agent, data]) => {
|
||||
const rawModel = data.model || '';
|
||||
const modelId = rawModel.replace('ollama-cloud/', '');
|
||||
const currentIndex = bench.models.findIndex(m => m.id === modelId);
|
||||
// Preserve existing scores or empty
|
||||
const scores = existingScores[agent] || {};
|
||||
return {
|
||||
agent,
|
||||
current_model_index: currentIndex >= 0 ? currentIndex : -1,
|
||||
current_model_id: modelId,
|
||||
reasoning_effort: data.variant === 'thinking' ? 'H' : 'M',
|
||||
scores
|
||||
};
|
||||
});
|
||||
|
||||
// Update metadata
|
||||
bench.generated = new Date().toISOString();
|
||||
bench.source = '.kilo/capability-index.yaml (synced v2)';
|
||||
bench.total_agents = bench.agent_current_config.length;
|
||||
|
||||
fs.writeFileSync('agent-evolution/data/model-benchmarks.json', JSON.stringify(bench, null, 2));
|
||||
console.log('Synced', bench.agent_current_config.length, 'agents');
|
||||
console.log('Generated:', bench.generated);
|
||||
|
||||
// Verify
|
||||
let mismatches = 0;
|
||||
bench.agent_current_config.forEach(c => {
|
||||
const scores = bench.agent_model_scores.find(s => s.agent === c.agent);
|
||||
if (scores && scores.current_model_id !== c.model.replace('ollama-cloud/', '')) {
|
||||
console.log(' MISMATCH:', c.agent, scores.current_model_id, '->', c.model);
|
||||
mismatches++;
|
||||
}
|
||||
});
|
||||
console.log('Mismatches:', mismatches);
|
||||
651
agent-evolution/scripts/sync-model-research.ts
Normal file
651
agent-evolution/scripts/sync-model-research.ts
Normal file
@@ -0,0 +1,651 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Model Research Synchronization Script
|
||||
* Applies model recommendations from research output to agent configuration files.
|
||||
*
|
||||
* Usage:
|
||||
* bun run agent-evolution/scripts/sync-model-research.ts # apply latest
|
||||
* bun run agent-evolution/scripts/sync-model-research.ts --dry-run # preview only
|
||||
* bun run agent-evolution/scripts/sync-model-research.ts --input path/to.json # custom input
|
||||
* bun run agent-evolution/scripts/sync-model-research.ts --agent planner # single agent
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
// Types based on model-research.schema.json
|
||||
interface Recommendation {
|
||||
agent: string;
|
||||
action: "update_model" | "confirm_model" | "add_fallback" | "redesign_agent";
|
||||
current_model: string;
|
||||
recommended_model: string;
|
||||
impact: "critical" | "high" | "medium" | "low";
|
||||
rationale: string;
|
||||
applied: boolean;
|
||||
applied_date?: string | null;
|
||||
score_delta?: number;
|
||||
}
|
||||
|
||||
interface ModelResearchData {
|
||||
version: string;
|
||||
generated: string;
|
||||
source: string;
|
||||
recommendations: Recommendation[];
|
||||
capability_index_patch?: Array<{
|
||||
agent: string;
|
||||
set: Record<string, unknown>;
|
||||
}>;
|
||||
summary?: {
|
||||
total_recommendations: number;
|
||||
applied_count: number;
|
||||
pending_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChangeSummary {
|
||||
total_recommendations: number;
|
||||
applied: number;
|
||||
confirmed: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
files_modified: string[];
|
||||
agents_updated: string[];
|
||||
dashboard_rebuilt: boolean;
|
||||
}
|
||||
|
||||
// Default paths
|
||||
const DEFAULT_RESEARCH_FILE = path.join(__dirname, "../data/model-research-latest.json");
|
||||
const SCHEMA_FILE = path.join(__dirname, "../data/model-research.schema.json");
|
||||
const CAPABILITY_INDEX = path.join(process.cwd(), ".kilo/capability-index.yaml");
|
||||
const AGENT_VERSIONS = path.join(__dirname, "../data/agent-versions.json");
|
||||
const KILO_META = path.join(process.cwd(), "kilo-meta.json");
|
||||
const SYNC_SCRIPT = path.join(process.cwd(), "scripts/sync-agents.cjs");
|
||||
|
||||
// Parse command line arguments
|
||||
function parseArgs(): {
|
||||
dryRun: boolean;
|
||||
inputFile: string;
|
||||
singleAgent?: string;
|
||||
} {
|
||||
const args = process.argv.slice(2);
|
||||
const options: { dryRun: boolean; inputFile: string; singleAgent?: string } = {
|
||||
dryRun: false,
|
||||
inputFile: DEFAULT_RESEARCH_FILE,
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--dry-run" || arg === "-n") {
|
||||
options.dryRun = true;
|
||||
} else if (arg === "--input" || arg === "-i") {
|
||||
options.inputFile = args[++i] || DEFAULT_RESEARCH_FILE;
|
||||
} else if (arg === "--agent" || arg === "-a") {
|
||||
options.singleAgent = args[++i];
|
||||
} else if (!arg.startsWith("-")) {
|
||||
// Positional argument as input file
|
||||
options.inputFile = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// Load research data
|
||||
function loadResearchData(filePath: string): ModelResearchData {
|
||||
console.log(`📖 Loading research data from: ${filePath}`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Research file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const data = JSON.parse(content);
|
||||
|
||||
// Basic validation (we don't implement full schema validation for simplicity)
|
||||
if (!data.version || !data.generated || !Array.isArray(data.recommendations)) {
|
||||
throw new Error("Invalid research data structure");
|
||||
}
|
||||
|
||||
console.log(` Found ${data.recommendations.length} recommendations`);
|
||||
console.log(` Generated: ${data.generated}`);
|
||||
console.log(` Source: ${data.source}`);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Validate schema (basic check)
|
||||
function validateSchema(data: ModelResearchData): boolean {
|
||||
// For now, just check required fields
|
||||
const required = [
|
||||
"version",
|
||||
"generated",
|
||||
"source",
|
||||
"recommendations",
|
||||
];
|
||||
|
||||
for (const field of required) {
|
||||
if (!(field in data)) {
|
||||
console.warn(`⚠️ Missing required field: ${field}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load capability-index.yaml
|
||||
function loadCapabilityIndex(): string {
|
||||
return fs.readFileSync(CAPABILITY_INDEX, "utf-8");
|
||||
}
|
||||
|
||||
// Update model in capability-index.yaml
|
||||
function replaceModelInYaml(content: string, agentName: string, newModel: string): { content: string; changed: boolean } {
|
||||
// Find the agent block section
|
||||
const agentStart = content.indexOf(` ${agentName}:`);
|
||||
if (agentStart === -1) {
|
||||
throw new Error(`Agent ${agentName} not found in capability-index.yaml`);
|
||||
}
|
||||
|
||||
// Find next agent section (at same indent level)
|
||||
const remaining = content.substring(agentStart);
|
||||
const nextAgentMatch = remaining.match(/\n \w/);
|
||||
const agentEnd = nextAgentMatch ? agentStart + nextAgentMatch.index! : content.length;
|
||||
|
||||
const agentBlock = content.substring(agentStart, agentEnd);
|
||||
|
||||
// Find and replace the model line (more flexible regex for whitespace)
|
||||
const modelLineRegex = /^\s+model:\s+.+$/gm;
|
||||
const match = agentBlock.match(modelLineRegex);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Model line not found in agent ${agentName} block`);
|
||||
}
|
||||
|
||||
const currentModelLine = match[0];
|
||||
const currentModelMatch = currentModelLine.match(/:\s*(.+)$/);
|
||||
const currentModel = currentModelMatch ? currentModelMatch[1].trim() : '';
|
||||
|
||||
// Check if model already matches
|
||||
if (currentModel === newModel) {
|
||||
console.log(` ⏭️ Model already set to ${newModel}, skipping`);
|
||||
return { content, changed: false }; // No change needed
|
||||
}
|
||||
|
||||
// Replace model line with new model
|
||||
const updatedBlock = agentBlock.replace(modelLineRegex, currentModelLine.replace(currentModel, newModel));
|
||||
|
||||
if (updatedBlock === agentBlock) {
|
||||
throw new Error(`Failed to replace model line in agent ${agentName} block`);
|
||||
}
|
||||
|
||||
console.log(` 🔄 Updating model: ${currentModel} → ${newModel}`);
|
||||
const newContent = content.substring(0, agentStart) + updatedBlock + content.substring(agentEnd);
|
||||
return { content: newContent, changed: true };
|
||||
}
|
||||
|
||||
// Update kilo-meta.json
|
||||
function updateKiloMeta(agentName: string, newModel: string): void {
|
||||
const content = fs.readFileSync(KILO_META, "utf-8");
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (!data.agents[agentName]) {
|
||||
throw new Error(`Agent ${agentName} not found in kilo-meta.json`);
|
||||
}
|
||||
|
||||
data.agents[agentName].model = newModel;
|
||||
data.lastSync = new Date().toISOString();
|
||||
|
||||
fs.writeFileSync(KILO_META, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Update kilo.jsonc (manual update required per evolutionary-sync.md rules)
|
||||
function updateKiloJsonc(agentName: string, newModel: string): void {
|
||||
const content = fs.readFileSync(path.join(process.cwd(), "kilo.jsonc"), "utf-8");
|
||||
|
||||
// Simple regex replacement for agent block
|
||||
// Find agent block: "agentName": { ... "model": "old", ... }
|
||||
const agentRegex = new RegExp(`"${agentName}":\\s*{[\\s\\S]*?"model":\\s*"[^"]*"`, 'm');
|
||||
const match = content.match(agentRegex);
|
||||
|
||||
if (!match) {
|
||||
console.warn(`⚠️ Could not find agent ${agentName} in kilo.jsonc - manual update required`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldMatch = match[0];
|
||||
const newMatch = oldMatch.replace(/"model":\s*"[^"]*"/, `"model": "${newModel}"`);
|
||||
const updatedContent = content.replace(oldMatch, newMatch);
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "kilo.jsonc"), updatedContent);
|
||||
}
|
||||
|
||||
// Load agent-versions.json
|
||||
function loadAgentVersions(): any {
|
||||
const content = fs.readFileSync(AGENT_VERSIONS, "utf-8");
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
// Update agent-versions.json with model change
|
||||
function updateAgentVersions(
|
||||
agentVersions: any,
|
||||
agentName: string,
|
||||
fromModel: string,
|
||||
toModel: string,
|
||||
reason: string
|
||||
): any {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (!agentVersions.agents[agentName]) {
|
||||
agentVersions.agents[agentName] = {
|
||||
current: {},
|
||||
history: [],
|
||||
performance_log: [],
|
||||
};
|
||||
}
|
||||
|
||||
const agent = agentVersions.agents[agentName];
|
||||
|
||||
// Add history entry
|
||||
agent.history.push({
|
||||
date: now,
|
||||
commit: "model-research-sync",
|
||||
type: "model_change",
|
||||
from: fromModel,
|
||||
to: toModel,
|
||||
reason,
|
||||
source: "research",
|
||||
});
|
||||
|
||||
// Update current model
|
||||
if (!agent.current) agent.current = {};
|
||||
agent.current.model = toModel;
|
||||
agent.current.provider = detectProvider(toModel);
|
||||
|
||||
// Update lastUpdated
|
||||
agentVersions.lastUpdated = now;
|
||||
|
||||
return agentVersions;
|
||||
}
|
||||
|
||||
// Provider detection
|
||||
function detectProvider(model: string): string {
|
||||
if (model.startsWith("ollama-cloud/") || model.startsWith("ollama/")) return "Ollama";
|
||||
if (model.startsWith("openrouter/") || model.includes("openrouter")) return "OpenRouter";
|
||||
if (model.startsWith("groq/")) return "Groq";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Apply a single recommendation
|
||||
function applyRecommendation(
|
||||
rec: Recommendation,
|
||||
dryRun: boolean,
|
||||
singleAgent?: string
|
||||
): { applied: boolean; error?: string; filesModified?: string[] } {
|
||||
if (singleAgent && rec.agent !== singleAgent) {
|
||||
return { applied: false };
|
||||
}
|
||||
|
||||
console.log(`\n🔧 Applying recommendation for ${rec.agent}`);
|
||||
console.log(` Action: ${rec.action}`);
|
||||
console.log(` Current: ${rec.current_model}`);
|
||||
console.log(` Recommended: ${rec.recommended_model}`);
|
||||
console.log(` Impact: ${rec.impact}`);
|
||||
console.log(` Rationale: ${rec.rationale}`);
|
||||
|
||||
// Skip if already applied
|
||||
if (rec.applied) {
|
||||
console.log(` ⏭️ Already applied, skipping`);
|
||||
return { applied: false };
|
||||
}
|
||||
|
||||
if (rec.action === "update_model") {
|
||||
try {
|
||||
// 1. Update capability-index.yaml
|
||||
const capIndexContent = loadCapabilityIndex();
|
||||
const { content: updatedContent, changed: yamlChanged } = replaceModelInYaml(capIndexContent, rec.agent, rec.recommended_model);
|
||||
|
||||
if (!dryRun && yamlChanged) {
|
||||
fs.writeFileSync(CAPABILITY_INDEX, updatedContent);
|
||||
console.log(` ✅ Updated capability-index.yaml`);
|
||||
} else if (!dryRun) {
|
||||
console.log(` ⏭️ Skipping capability-index.yaml (no change needed)`);
|
||||
} else {
|
||||
console.log(` 📋 Would update capability-index.yaml`);
|
||||
}
|
||||
|
||||
// Only update other files if YAML was actually changed
|
||||
if (!yamlChanged) {
|
||||
return {
|
||||
applied: false,
|
||||
filesModified: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Update kilo-meta.json (source of truth)
|
||||
if (!dryRun) {
|
||||
updateKiloMeta(rec.agent, rec.recommended_model);
|
||||
console.log(` ✅ Updated kilo-meta.json`);
|
||||
} else {
|
||||
console.log(` 📋 Would update kilo-meta.json`);
|
||||
}
|
||||
|
||||
// 3. Update agent-versions.json
|
||||
const agentVersions = loadAgentVersions();
|
||||
const updatedVersions = updateAgentVersions(
|
||||
agentVersions,
|
||||
rec.agent,
|
||||
rec.current_model,
|
||||
rec.recommended_model,
|
||||
rec.rationale
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(AGENT_VERSIONS, JSON.stringify(updatedVersions, null, 2));
|
||||
console.log(` ✅ Updated agent-versions.json`);
|
||||
} else {
|
||||
console.log(` 📋 Would update agent-versions.json`);
|
||||
}
|
||||
|
||||
// 4. Attempt to update kilo.jsonc (manual verification still required)
|
||||
if (!dryRun) {
|
||||
try {
|
||||
updateKiloJsonc(rec.agent, rec.recommended_model);
|
||||
console.log(` ✅ Updated kilo.jsonc`);
|
||||
} catch (error: any) {
|
||||
console.warn(` ⚠️ Could not update kilo.jsonc: ${error.message}`);
|
||||
console.log(` ⚠️ Manual update required per evolutionary-sync.md rules`);
|
||||
}
|
||||
} else {
|
||||
console.log(` 📋 Would update kilo.jsonc`);
|
||||
}
|
||||
|
||||
return {
|
||||
applied: true,
|
||||
filesModified: [CAPABILITY_INDEX, KILO_META, AGENT_VERSIONS],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
applied: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
} else if (rec.action === "confirm_model") {
|
||||
// Mark as confirmed in agent-versions.json
|
||||
try {
|
||||
const agentVersions = loadAgentVersions();
|
||||
|
||||
if (agentVersions.agents[rec.agent]) {
|
||||
// Add confirmation history entry
|
||||
agentVersions.agents[rec.agent].history.push({
|
||||
date: new Date().toISOString(),
|
||||
commit: "model-research-confirm",
|
||||
type: "model_change",
|
||||
from: rec.current_model,
|
||||
to: rec.current_model, // same model
|
||||
reason: `Confirmed: ${rec.rationale}`,
|
||||
source: "research",
|
||||
});
|
||||
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(AGENT_VERSIONS, JSON.stringify(agentVersions, null, 2));
|
||||
console.log(` ✅ Confirmed current model in agent-versions.json`);
|
||||
} else {
|
||||
console.log(` 📋 Would confirm current model`);
|
||||
}
|
||||
|
||||
return {
|
||||
applied: true,
|
||||
filesModified: [AGENT_VERSIONS],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
applied: false,
|
||||
error: `Agent ${rec.agent} not found in agent-versions.json`,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
applied: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Unsupported action
|
||||
console.log(` ⏭️ Unsupported action: ${rec.action}`);
|
||||
return { applied: false };
|
||||
}
|
||||
|
||||
// Run sync-agents.js --fix
|
||||
function runSyncAgentsFix(): boolean {
|
||||
console.log(`\n🔄 Running sync-agents.js --fix...`);
|
||||
|
||||
const result = spawnSync("node", [SYNC_SCRIPT, "--fix"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf-8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(`❌ Sync script failed with exit code ${result.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`✅ Sync script completed`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run sync-agents.js --check
|
||||
function runSyncAgentsCheck(): boolean {
|
||||
console.log(`\n✅ Running sync-agents.js --check...`);
|
||||
|
||||
const result = spawnSync("node", [SYNC_SCRIPT, "--check"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf-8",
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(`❌ Sync check failed with exit code ${result.status}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`✅ Sync check passed`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run build-research-dashboard script
|
||||
function runBuildDashboard(): { success: boolean; error?: string } {
|
||||
console.log("\n📊 Rebuilding research dashboard...");
|
||||
|
||||
try {
|
||||
// Try to import buildResearchDashboard from build-research-dashboard.ts
|
||||
const dashboardScript = path.join(__dirname, "build-research-dashboard.ts");
|
||||
const standaloneScript = path.join(__dirname, "build-standalone.cjs");
|
||||
|
||||
// Check which build script exists
|
||||
let scriptToRun = "";
|
||||
let args: string[] = [];
|
||||
|
||||
if (fs.existsSync(dashboardScript)) {
|
||||
scriptToRun = "bun";
|
||||
args = ["run", dashboardScript];
|
||||
} else if (fs.existsSync(standaloneScript)) {
|
||||
scriptToRun = "node";
|
||||
args = [standaloneScript];
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: "No dashboard build script found (build-research-dashboard.ts or build-standalone.cjs)"
|
||||
};
|
||||
}
|
||||
|
||||
const result = spawnSync(scriptToRun, args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf-8",
|
||||
stdio: "inherit",
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.stderr || `Build script failed with exit code ${result.status}`
|
||||
};
|
||||
}
|
||||
|
||||
console.log(result.stdout);
|
||||
console.log("✅ Dashboard rebuilt: agent-evolution/index.standalone.html");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
function printSummary(summary: ChangeSummary): void {
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log("📊 SYNC SUMMARY");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
console.log(`Total recommendations: ${summary.total_recommendations}`);
|
||||
console.log(`Applied: ${summary.applied}`);
|
||||
console.log(`Confirmed: ${summary.confirmed}`);
|
||||
console.log(`Skipped: ${summary.skipped}`);
|
||||
|
||||
if (summary.dashboard_rebuilt) {
|
||||
console.log(`Dashboard rebuilt: ✅ Yes`);
|
||||
}
|
||||
|
||||
if (summary.agents_updated.length > 0) {
|
||||
console.log(`\nAgents updated:`);
|
||||
summary.agents_updated.forEach(agent => console.log(` - ${agent}`));
|
||||
}
|
||||
|
||||
if (summary.files_modified.length > 0) {
|
||||
console.log(`\nFiles modified:`);
|
||||
summary.files_modified.forEach(file => console.log(` - ${file}`));
|
||||
}
|
||||
|
||||
if (summary.errors.length > 0) {
|
||||
console.log(`\nErrors:`);
|
||||
summary.errors.forEach(error => console.log(` - ${error}`));
|
||||
}
|
||||
|
||||
console.log("=".repeat(60));
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function main() {
|
||||
const options = parseArgs();
|
||||
|
||||
console.log("🧬 Model Research Synchronization");
|
||||
console.log(` Dry run: ${options.dryRun ? "YES" : "NO"}`);
|
||||
console.log(` Input: ${options.inputFile}`);
|
||||
if (options.singleAgent) {
|
||||
console.log(` Single agent: ${options.singleAgent}`);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
// Load research data
|
||||
const researchData = loadResearchData(options.inputFile);
|
||||
|
||||
if (!validateSchema(researchData)) {
|
||||
console.warn("⚠️ Schema validation issues detected, but continuing...");
|
||||
}
|
||||
|
||||
// Filter recommendations
|
||||
let recommendations = researchData.recommendations;
|
||||
if (options.singleAgent) {
|
||||
recommendations = recommendations.filter(r => r.agent === options.singleAgent);
|
||||
console.log(`Filtered to ${recommendations.length} recommendations for ${options.singleAgent}`);
|
||||
}
|
||||
|
||||
// Initialize summary
|
||||
const summary: ChangeSummary = {
|
||||
total_recommendations: recommendations.length,
|
||||
applied: 0,
|
||||
confirmed: 0,
|
||||
skipped: 0,
|
||||
errors: [],
|
||||
files_modified: [],
|
||||
agents_updated: [],
|
||||
dashboard_rebuilt: false,
|
||||
};
|
||||
|
||||
// Apply recommendations
|
||||
for (const rec of recommendations) {
|
||||
const result = applyRecommendation(rec, options.dryRun, options.singleAgent);
|
||||
|
||||
if (result.applied) {
|
||||
if (rec.action === "update_model") {
|
||||
summary.applied++;
|
||||
summary.agents_updated.push(rec.agent);
|
||||
if (result.filesModified) {
|
||||
summary.files_modified.push(...result.filesModified);
|
||||
}
|
||||
} else if (rec.action === "confirm_model") {
|
||||
summary.confirmed++;
|
||||
}
|
||||
} else {
|
||||
if (result.error) {
|
||||
summary.errors.push(`${rec.agent}: ${result.error}`);
|
||||
} else {
|
||||
summary.skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate files from files_modified
|
||||
summary.files_modified = [...new Set(summary.files_modified)];
|
||||
|
||||
// Run sync-agents.js if we made changes (and not dry run)
|
||||
if (summary.applied > 0 && !options.dryRun) {
|
||||
console.log(`\n📦 Propagating changes to all agent files...`);
|
||||
const syncOk = runSyncAgentsFix();
|
||||
|
||||
if (syncOk) {
|
||||
console.log(`\n✅ Validating changes...`);
|
||||
const checkOk = runSyncAgentsCheck();
|
||||
|
||||
if (checkOk) {
|
||||
// Rebuild research dashboard
|
||||
const buildResult = runBuildDashboard();
|
||||
if (buildResult.success) {
|
||||
console.log("✅ Dashboard rebuilt: agent-evolution/index.standalone.html");
|
||||
summary.dashboard_rebuilt = true;
|
||||
} else {
|
||||
console.warn(`⚠️ Dashboard rebuild failed: ${buildResult.error}`);
|
||||
summary.errors.push(`Dashboard rebuild failed: ${buildResult.error}`);
|
||||
}
|
||||
} else {
|
||||
summary.errors.push("Sync check failed after applying changes");
|
||||
}
|
||||
} else {
|
||||
summary.errors.push("Sync fix script failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
printSummary(summary);
|
||||
|
||||
// Exit with error if any errors occurred
|
||||
if (summary.errors.length > 0) {
|
||||
console.error(`\n❌ Sync completed with ${summary.errors.length} errors`);
|
||||
process.exit(1);
|
||||
} else if (summary.applied === 0 && summary.confirmed === 0) {
|
||||
console.warn(`\n⚠️ No changes applied`);
|
||||
} else {
|
||||
console.log(`\n🎉 Sync completed successfully!`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user