|
20 | 20 | import * as fs from "fs"; |
21 | 21 | import * as path from "path"; |
22 | 22 | import * as readline from "readline"; |
| 23 | +import { fileURLToPath } from "url"; |
23 | 24 | import { getGitmemDir, getInstallId } from "../services/gitmem-dir.js"; |
24 | 25 | import { validateLicense, clearLicenseCache } from "../services/license.js"; |
25 | 26 |
|
@@ -123,6 +124,96 @@ async function checkSchemaExists(url: string, key: string): Promise<string[]> { |
123 | 124 | return missing; |
124 | 125 | } |
125 | 126 |
|
| 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 | + |
126 | 217 | export async function main(args: string[]): Promise<void> { |
127 | 218 | console.log(""); |
128 | 219 | console.log("GitMem Pro Activation"); |
@@ -269,14 +360,55 @@ export async function main(args: string[]): Promise<void> { |
269 | 360 | if (!connectionFailed) { |
270 | 361 | missingTables = await checkSchemaExists(supabaseUrl, supabaseKey); |
271 | 362 | 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 | + } |
280 | 412 | } else { |
281 | 413 | console.log(" ✓ Schema verified (all tables present)"); |
282 | 414 | } |
|
0 commit comments