Spaces:
Sleeping
Sleeping
| 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<unknown> { | |
| 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<OnboardingState | null> { | |
| 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; | |
| } | |