219 lines
7.7 KiB
TypeScript
219 lines
7.7 KiB
TypeScript
#!/usr/bin/env tsx
|
||
/**
|
||
* Seed GooseFactory Database from MCP Command Center State
|
||
*
|
||
* Reads the current MCP pipeline state from mcpengine-repo
|
||
* and converts it into GooseFactory pipeline records.
|
||
*
|
||
* Usage: npx tsx scripts/seed-from-factory.ts
|
||
*/
|
||
|
||
import { readFileSync, existsSync } from "node:fs";
|
||
import { resolve } from "node:path";
|
||
|
||
// ─── Configuration ───
|
||
|
||
const STATE_PATHS = [
|
||
resolve(import.meta.dirname ?? ".", "../../../mcpengine-repo/infra/command-center/state.json"),
|
||
resolve(import.meta.dirname ?? ".", "../../mcpengine-repo/infra/command-center/state.json"),
|
||
resolve(import.meta.dirname ?? ".", "../../../mcp-command-center/state.json"),
|
||
];
|
||
|
||
const DB_URL = process.env.DATABASE_URL || "postgresql://goosefactory:goosefactory_dev@localhost:5432/goosefactory";
|
||
|
||
// ─── Types for the old state format ───
|
||
|
||
interface OldState {
|
||
version: number;
|
||
phases: Array<{ id: number; name: string; stages: number[] }>;
|
||
stages: Array<{ id: number; name: string; phase: number }>;
|
||
mcps: Array<{
|
||
id: number;
|
||
name: string;
|
||
slug?: string;
|
||
stage: number;
|
||
priority?: string;
|
||
status?: string;
|
||
assignee?: string;
|
||
tags?: string[];
|
||
metadata?: Record<string, unknown>;
|
||
startedAt?: string;
|
||
completedAt?: string;
|
||
stageHistory?: Array<{ stage: number; enteredAt: string; completedAt?: string }>;
|
||
}>;
|
||
decisions: {
|
||
pending: unknown[];
|
||
history: unknown[];
|
||
};
|
||
}
|
||
|
||
// ─── Stage Mapping (old stage IDs → GooseFactory stages) ───
|
||
|
||
function mapStageToFactory(stageId: number, stages: OldState["stages"]): string {
|
||
const stage = stages.find((s) => s.id === stageId);
|
||
if (!stage) return "intake";
|
||
|
||
const name = stage.name.toLowerCase();
|
||
|
||
// Map old phases/stages to GooseFactory pipeline stages
|
||
if (name.includes("identified") || name.includes("research") || name.includes("architecture")) return "intake";
|
||
if (name.includes("scaffold") || name.includes("bootstrap") || name.includes("init")) return "scaffolding";
|
||
if (name.includes("build") || name.includes("implement") || name.includes("develop") || name.includes("code")) return "building";
|
||
if (name.includes("test") || name.includes("qa") || name.includes("validation")) return "testing";
|
||
if (name.includes("review") || name.includes("audit")) return "review";
|
||
if (name.includes("staging") || name.includes("deploy") || name.includes("pre-prod")) return "staging";
|
||
if (name.includes("production") || name.includes("live") || name.includes("release")) return "production";
|
||
if (name.includes("publish") || name.includes("marketplace") || name.includes("launched")) return "published";
|
||
|
||
// Phase-based fallback
|
||
const phaseStage = stages.find((s) => s.id === stageId);
|
||
if (phaseStage) {
|
||
const phaseId = phaseStage.phase;
|
||
if (phaseId <= 1) return "intake";
|
||
if (phaseId === 2) return "building";
|
||
if (phaseId === 3) return "testing";
|
||
if (phaseId === 4) return "review";
|
||
if (phaseId === 5) return "staging";
|
||
if (phaseId === 6) return "production";
|
||
if (phaseId >= 7) return "published";
|
||
}
|
||
|
||
return "intake";
|
||
}
|
||
|
||
function mapPriority(priority?: string): string {
|
||
if (!priority) return "medium";
|
||
const p = priority.toLowerCase();
|
||
if (p === "critical" || p === "urgent") return "critical";
|
||
if (p === "high") return "high";
|
||
if (p === "low") return "low";
|
||
return "medium";
|
||
}
|
||
|
||
function slugify(name: string): string {
|
||
return name
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-|-$/g, "");
|
||
}
|
||
|
||
// ─── Main ───
|
||
|
||
async function main() {
|
||
console.log("🌱 GooseFactory Seed Script\n");
|
||
|
||
// Find state file
|
||
let statePath: string | null = null;
|
||
for (const p of STATE_PATHS) {
|
||
if (existsSync(p)) {
|
||
statePath = p;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!statePath) {
|
||
console.log("⚠️ No MCP command center state.json found at:");
|
||
STATE_PATHS.forEach((p) => console.log(` ${p}`));
|
||
console.log("\n Skipping seed. You can seed manually later.");
|
||
process.exit(0);
|
||
}
|
||
|
||
console.log(`📄 Reading state from: ${statePath}`);
|
||
const raw = readFileSync(statePath, "utf-8");
|
||
const state: OldState = JSON.parse(raw);
|
||
|
||
console.log(` Found ${state.mcps.length} MCP servers`);
|
||
console.log(` Found ${state.stages.length} stages across ${state.phases.length} phases\n`);
|
||
|
||
// Convert MCPs to GooseFactory pipelines
|
||
const pipelines = state.mcps.map((mcp) => {
|
||
const factoryStage = mapStageToFactory(mcp.stage, state.stages);
|
||
const slug = mcp.slug || slugify(mcp.name);
|
||
|
||
// Determine status based on stage
|
||
let status = "active";
|
||
if (factoryStage === "published") status = "completed";
|
||
if (mcp.status === "paused" || mcp.status === "blocked") status = "paused";
|
||
if (mcp.status === "failed") status = "failed";
|
||
|
||
return {
|
||
name: mcp.name,
|
||
slug,
|
||
template: "mcp-server-standard",
|
||
platform: slug.replace(/-mcp.*/, ""),
|
||
currentStage: factoryStage,
|
||
status,
|
||
priority: mapPriority(mcp.priority),
|
||
config: mcp.metadata || {},
|
||
metadata: {
|
||
importedFrom: "mcp-command-center",
|
||
originalStageId: mcp.stage,
|
||
originalId: mcp.id,
|
||
tags: mcp.tags || [],
|
||
},
|
||
startedAt: mcp.startedAt || new Date().toISOString(),
|
||
completedAt: mcp.completedAt || null,
|
||
};
|
||
});
|
||
|
||
// Generate SQL inserts
|
||
console.log("📝 Generated pipeline records:\n");
|
||
|
||
const sqlStatements: string[] = [];
|
||
|
||
for (const p of pipelines) {
|
||
const sql = `INSERT INTO pipelines (name, slug, template, platform, current_stage, status, priority, config, metadata, started_at, completed_at)
|
||
VALUES ('${p.name.replace(/'/g, "''")}', '${p.slug.replace(/'/g, "''")}', '${p.template}', '${p.platform.replace(/'/g, "''")}', '${p.currentStage}', '${p.status}', '${p.priority}', '${JSON.stringify(p.config).replace(/'/g, "''")}', '${JSON.stringify(p.metadata).replace(/'/g, "''")}', '${p.startedAt}', ${p.completedAt ? `'${p.completedAt}'` : "NULL"})
|
||
ON CONFLICT (slug) DO UPDATE SET
|
||
current_stage = EXCLUDED.current_stage,
|
||
status = EXCLUDED.status,
|
||
priority = EXCLUDED.priority,
|
||
metadata = EXCLUDED.metadata,
|
||
updated_at = NOW();`;
|
||
|
||
sqlStatements.push(sql);
|
||
console.log(` 📦 ${p.name} → ${p.currentStage} (${p.status})`);
|
||
}
|
||
|
||
// Write SQL file for manual import
|
||
const sqlPath = resolve(import.meta.dirname ?? ".", "../infra/db/seed/seed-from-factory.sql");
|
||
const sqlDir = resolve(import.meta.dirname ?? ".", "../infra/db/seed");
|
||
|
||
const { mkdirSync, writeFileSync } = await import("node:fs");
|
||
mkdirSync(sqlDir, { recursive: true });
|
||
writeFileSync(sqlPath, `-- GooseFactory Seed Data\n-- Generated from MCP Command Center state.json\n-- Generated at: ${new Date().toISOString()}\n\n${sqlStatements.join("\n\n")}\n`);
|
||
|
||
console.log(`\n✅ SQL seed file written to: ${sqlPath}`);
|
||
console.log(` Total pipelines: ${pipelines.length}`);
|
||
|
||
// Try to execute against database
|
||
try {
|
||
// Dynamic import to avoid hard dependency on postgres
|
||
const { default: postgres } = await import("postgres");
|
||
const sql = postgres(DB_URL);
|
||
|
||
console.log("\n🔌 Connecting to database...");
|
||
|
||
for (const stmt of sqlStatements) {
|
||
await sql.unsafe(stmt);
|
||
}
|
||
|
||
console.log(`✅ Seeded ${pipelines.length} pipelines into database`);
|
||
await sql.end();
|
||
} catch (err) {
|
||
const error = err as Error;
|
||
if (error.message?.includes("Cannot find module") || error.message?.includes("connect")) {
|
||
console.log("\n⚠️ Could not connect to database — SQL file saved for manual import:");
|
||
console.log(` psql "$DATABASE_URL" < ${sqlPath}`);
|
||
} else {
|
||
console.error("\n⚠️ Database seed error:", error.message);
|
||
console.log(` SQL file saved for manual import: ${sqlPath}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error("Fatal error:", err);
|
||
process.exit(1);
|
||
});
|