Skip to content

Commit a92debf

Browse files
Claudeclaude
authored andcommitted
feat: activate auto-applies schema to user's Supabase
When tables are missing during activation, the command now automatically applies the setup SQL via the Supabase Management API. Requires SUPABASE_ACCESS_TOKEN (from `npx supabase login`). Falls back to manual instructions if the token isn't available. After applying schema, sends NOTIFY pgrst to reload PostgREST's schema cache and waits 2s before verifying tables exist. Full user journey now: create Supabase project → set env vars → run `gitmem-mcp activate <key>` → everything works. No manual SQL copy-paste needed. E2E verified: blank Supabase → activate → 8/8 tools pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4b63cd8 commit a92debf

1 file changed

Lines changed: 140 additions & 8 deletions

File tree

src/commands/activate.ts

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import * as fs from "fs";
2121
import * as path from "path";
2222
import * as readline from "readline";
23+
import { fileURLToPath } from "url";
2324
import { getGitmemDir, getInstallId } from "../services/gitmem-dir.js";
2425
import { validateLicense, clearLicenseCache } from "../services/license.js";
2526

@@ -123,6 +124,96 @@ async function checkSchemaExists(url: string, key: string): Promise<string[]> {
123124
return missing;
124125
}
125126

127+
/**
128+
* Get the Supabase access token for Management API
129+
* Priority: SUPABASE_ACCESS_TOKEN env var → ~/.supabase/access-token file
130+
*/
131+
function getSupabaseAccessToken(): string | null {
132+
const envToken = process.env.SUPABASE_ACCESS_TOKEN;
133+
if (envToken) return envToken;
134+
135+
try {
136+
const tokenPath = path.join(
137+
process.env.HOME || process.env.USERPROFILE || "",
138+
".supabase",
139+
"access-token"
140+
);
141+
if (fs.existsSync(tokenPath)) {
142+
return fs.readFileSync(tokenPath, "utf-8").trim();
143+
}
144+
} catch {
145+
// No stored token
146+
}
147+
return null;
148+
}
149+
150+
/**
151+
* Extract project ref from Supabase URL
152+
* e.g., "https://abcdef.supabase.co" → "abcdef"
153+
*/
154+
function extractProjectRef(url: string): string | null {
155+
const match = url.match(/https?:\/\/([^.]+)\.supabase\.co/);
156+
return match ? match[1] : null;
157+
}
158+
159+
/**
160+
* Load the setup SQL from the schema file bundled with the package
161+
* Strips the license management section (not for user's Supabase)
162+
*/
163+
function loadSetupSql(): string | null {
164+
try {
165+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
166+
const sqlPath = path.join(__dirname, "..", "..", "schema", "setup.sql");
167+
if (!fs.existsSync(sqlPath)) return null;
168+
169+
let sql = fs.readFileSync(sqlPath, "utf-8");
170+
171+
// Strip license management tables (they belong on our infra, not user's)
172+
const licenseIdx = sql.indexOf("-- License Management Tables");
173+
if (licenseIdx > 0) {
174+
sql = sql.substring(0, licenseIdx);
175+
}
176+
177+
return sql;
178+
} catch {
179+
return null;
180+
}
181+
}
182+
183+
/**
184+
* Apply schema SQL to user's Supabase via Management API
185+
*/
186+
async function applySchema(
187+
projectRef: string,
188+
accessToken: string,
189+
sql: string
190+
): Promise<{ success: boolean; error?: string }> {
191+
try {
192+
const response = await fetch(
193+
`https://api.supabase.com/v1/projects/${projectRef}/database/query`,
194+
{
195+
method: "POST",
196+
headers: {
197+
"Content-Type": "application/json",
198+
Authorization: `Bearer ${accessToken}`,
199+
},
200+
body: JSON.stringify({ query: sql }),
201+
signal: AbortSignal.timeout(30_000),
202+
}
203+
);
204+
205+
if (!response.ok) {
206+
const text = await response.text();
207+
return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` };
208+
}
209+
210+
return { success: true };
211+
} catch (err: unknown) {
212+
const message = err instanceof Error ? err.message : "Unknown error";
213+
return { success: false, error: message };
214+
}
215+
}
216+
126217
export async function main(args: string[]): Promise<void> {
127218
console.log("");
128219
console.log("GitMem Pro Activation");
@@ -269,14 +360,55 @@ export async function main(args: string[]): Promise<void> {
269360
if (!connectionFailed) {
270361
missingTables = await checkSchemaExists(supabaseUrl, supabaseKey);
271362
if (missingTables.length > 0) {
272-
console.log("");
273-
console.log(" ⚠ Missing tables: " + missingTables.join(", "));
274-
console.log(" Run the schema setup in your Supabase SQL Editor:");
275-
console.log("");
276-
console.log(" npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)");
277-
console.log(" npx gitmem-mcp setup (prints SQL to paste manually)");
278-
console.log("");
279-
console.log(" Then: Supabase Dashboard → SQL Editor → New query → Paste → Run");
363+
console.log(" Setting up schema...");
364+
365+
const projectRef = extractProjectRef(supabaseUrl);
366+
const accessToken = getSupabaseAccessToken();
367+
const setupSql = loadSetupSql();
368+
369+
if (projectRef && accessToken && setupSql) {
370+
// Auto-apply schema via Management API
371+
const result = await applySchema(projectRef, accessToken, setupSql);
372+
if (result.success) {
373+
// Reload PostgREST schema cache so new tables are visible via REST API
374+
await applySchema(projectRef, accessToken, "NOTIFY pgrst, 'reload schema'");
375+
// Brief wait for PostgREST to pick up the notification
376+
await new Promise((r) => setTimeout(r, 2000));
377+
378+
// Verify tables now exist
379+
const stillMissing = await checkSchemaExists(supabaseUrl, supabaseKey);
380+
if (stillMissing.length === 0) {
381+
console.log(" ✓ Schema applied automatically");
382+
missingTables = [];
383+
} else {
384+
console.log(" ⚠ Schema applied but some tables still missing: " + stillMissing.join(", "));
385+
console.log(" PostgREST may need a moment to reload. Try: npx gitmem-mcp check");
386+
missingTables = stillMissing;
387+
}
388+
} else {
389+
console.log(" ⚠ Auto-schema failed: " + result.error);
390+
console.log(" Apply manually:");
391+
console.log("");
392+
console.log(" npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)");
393+
console.log(" npx gitmem-mcp setup (prints SQL to paste manually)");
394+
console.log("");
395+
console.log(" Then: Supabase Dashboard → SQL Editor → New query → Paste → Run");
396+
}
397+
} else if (!setupSql) {
398+
console.log(" ⚠ Could not load schema SQL file");
399+
} else {
400+
// No access token — give clear instructions
401+
console.log(" ⚠ Missing tables: " + missingTables.join(", "));
402+
console.log("");
403+
console.log(" To apply automatically, set SUPABASE_ACCESS_TOKEN:");
404+
console.log(" npx supabase login");
405+
console.log(" Then re-run: npx gitmem-mcp activate");
406+
console.log("");
407+
console.log(" Or apply manually:");
408+
console.log(" npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)");
409+
console.log(" npx gitmem-mcp setup (prints SQL to paste manually)");
410+
console.log(" Then: Supabase Dashboard → SQL Editor → Paste → Run");
411+
}
280412
} else {
281413
console.log(" ✓ Schema verified (all tables present)");
282414
}

0 commit comments

Comments
 (0)