File size: 3,710 Bytes
b2bbc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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;
}