import { mkdir, readFile, writeFile, readdir } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { homedir } from 'node:os'; import { exec } from 'node:child_process'; import { promisify } from 'node:util'; const execAsync = promisify(exec); const DEFAULT_STATE_PATH = join(homedir(), '.config', 'sin', 'sin-supabase', 'onboarding-state.json'); type OnboardingState = { ownerEmail?: string; notes?: string; updatedAt?: string; }; export type TemplateAgentAction = | { action: 'agent.help' } | { action: 'sin.supabase.health' } | { action: 'sin.supabase.onboarding.status' } | { action: 'sin.supabase.onboarding.save'; ownerEmail?: string; notes?: string; confirm?: boolean } | { action: 'sin.supabase.bootstrap.apply'; projectRoot: string; databaseUrl?: string; confirm?: boolean }; export async function executeTemplateAgentAction(action: TemplateAgentAction): Promise { switch (action.action) { case 'agent.help': return buildHelpPayload(); case 'sin.supabase.health': return { ok: true, agent: 'sin-supabase', primaryModel: 'openai/gpt-5.2', team: 'Team - Infrastructure', }; case 'sin.supabase.onboarding.status': return { ok: true, statePath: DEFAULT_STATE_PATH, state: await readState(), }; case 'sin.supabase.onboarding.save': if (!action.confirm) { throw new Error('input_required:confirm=true required'); } return { ok: true, statePath: DEFAULT_STATE_PATH, state: await writeState({ ownerEmail: clean(action.ownerEmail), notes: clean(action.notes), updatedAt: new Date().toISOString(), }), }; case 'sin.supabase.bootstrap.apply': if (!action.confirm) { throw new Error('input_required:confirm=true required'); } return await applyMigrations(action.projectRoot, action.databaseUrl); } } async function applyMigrations(projectRoot: string, databaseUrl?: string) { const migrationsDir = resolve(projectRoot, 'infra/supabase/migrations'); const dsn = databaseUrl || process.env.DATABASE_URL; if (!dsn) { throw new Error('DATABASE_URL missing in action params or environment'); } const files = (await readdir(migrationsDir)) .filter(f => f.endsWith('.sql')) .sort(); const results = []; for (const file of files) { const filePath = join(migrationsDir, file); const sql = await readFile(filePath, 'utf8'); try { const { stdout, stderr } = await execAsync(`psql "${dsn}" -f "${filePath}"`); results.push({ file, status: 'success', output: stdout }); } catch (e: any) { results.push({ file, status: 'error', error: e.message }); break; } } return { ok: results.every(r => r.status === 'success'), applied: results, }; } function buildHelpPayload() { return { ok: true, agent: 'sin-supabase', actions: [ 'sin.supabase.health', 'sin.supabase.onboarding.status', 'sin.supabase.onboarding.save', 'sin.supabase.bootstrap.apply' ], }; } async function readState(): Promise { try { return JSON.parse(await readFile(DEFAULT_STATE_PATH, 'utf8')) as OnboardingState; } catch { return null; } } async function writeState(state: OnboardingState) { await mkdir(dirname(DEFAULT_STATE_PATH), { recursive: true }); await writeFile(DEFAULT_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, 'utf8'); return state; } function clean(value: string | undefined) { const normalized = String(value || '').trim(); return normalized || undefined; }