sin-supabase / src /runtime.ts
Jeremy
implement bootstrap.apply action
b2bbc55
Raw
History Blame Contribute Delete
3.71 kB
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;
}