Skip to content

Commit 1fd306d

Browse files
Claudeclaude
authored andcommitted
feat: activate resolves credentials from env vars, config, or prompt
Credential resolution now follows a priority chain: 1. Environment variables (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENROUTER_API_KEY) 2. Existing values in .gitmem/config.json 3. Interactive prompt (TTY only) Users who don't want to paste secrets into a terminal can set env vars or edit config.json directly. Non-interactive connection failures are now warnings (credentials saved anyway) rather than hard exits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d817beb commit 1fd306d

1 file changed

Lines changed: 185 additions & 114 deletions

File tree

src/commands/activate.ts

Lines changed: 185 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
*
44
* Usage: npx gitmem-mcp activate [license-key]
55
*
6+
* Credential resolution (priority order):
7+
* 1. Environment variables (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENROUTER_API_KEY)
8+
* 2. Existing values in .gitmem/config.json (re-activation)
9+
* 3. Interactive prompt (TTY only)
10+
*
611
* Steps:
712
* 1. Accept key as argument or prompt for it
813
* 2. Validate key against our endpoint (register device)
9-
* 3. Prompt for Supabase URL + service role key (with re-activation safety check)
14+
* 3. Resolve Supabase URL + service role key (env → config → prompt)
1015
* 4. Test Supabase connection (verify tables exist)
11-
* 5. Prompt for OpenRouter API key
12-
* 6. Tell user to run schema setup manually if tables missing
13-
* 7. Write everything to ~/.gitmem/config.json
16+
* 5. Resolve OpenRouter API key (env → config → prompt)
17+
* 6. Write everything to ~/.gitmem/config.json
1418
*/
1519

1620
import * as fs from "fs";
@@ -32,6 +36,47 @@ function ask(rl: readline.Interface, question: string): Promise<string> {
3236
});
3337
}
3438

39+
/**
40+
* Resolve a credential value using the priority chain:
41+
* 1. Environment variable
42+
* 2. Existing config value
43+
* 3. Interactive prompt (if TTY available)
44+
*
45+
* Returns the resolved value or empty string.
46+
*/
47+
async function resolveCredential(opts: {
48+
envVar: string;
49+
configValue: string | undefined;
50+
promptLabel: string;
51+
required: boolean;
52+
rl: readline.Interface | null;
53+
existingHint?: string;
54+
}): Promise<{ value: string; source: "env" | "config" | "prompt" | "none" }> {
55+
// 1. Environment variable
56+
const envValue = process.env[opts.envVar];
57+
if (envValue) {
58+
return { value: envValue, source: "env" };
59+
}
60+
61+
// 2. Existing config value
62+
if (opts.configValue) {
63+
return { value: opts.configValue, source: "config" };
64+
}
65+
66+
// 3. Interactive prompt
67+
if (opts.rl) {
68+
const prompt = opts.existingHint
69+
? ` ${opts.promptLabel} [${opts.existingHint}]: `
70+
: ` ${opts.promptLabel}: `;
71+
const input = await ask(opts.rl, prompt);
72+
if (input) {
73+
return { value: input, source: "prompt" };
74+
}
75+
}
76+
77+
return { value: "", source: "none" };
78+
}
79+
3580
/**
3681
* Test basic Supabase connectivity via REST API
3782
*/
@@ -67,7 +112,6 @@ async function checkSchemaExists(url: string, key: string): Promise<string[]> {
67112
Authorization: `Bearer ${key}`,
68113
},
69114
});
70-
// 404 or 400 means table doesn't exist; 200 (even empty) means it does
71115
if (!response.ok) {
72116
missing.push(table);
73117
}
@@ -106,13 +150,13 @@ export async function main(args: string[]): Promise<void> {
106150
config.install_id = installId;
107151
}
108152

109-
// Step 1: Get license key
110-
let apiKey = args[0] || "";
153+
// Step 1: Get license key (arg → env → prompt)
154+
let apiKey = args[0] || process.env.GITMEM_API_KEY || (config.api_key as string) || "";
111155

112156
if (!apiKey) {
113-
// Check if non-interactive (piped stdin)
114157
if (!process.stdin.isTTY) {
115158
console.error("Error: License key required. Usage: npx gitmem-mcp activate <key>");
159+
console.error(" Or set GITMEM_API_KEY environment variable.");
116160
process.exit(1);
117161
}
118162

@@ -144,135 +188,150 @@ export async function main(args: string[]): Promise<void> {
144188
console.log(`✓ Key validated (${result.tier} tier)`);
145189
console.log("");
146190

147-
// Interactive mode for credentials
148-
if (!process.stdin.isTTY) {
149-
// Non-interactive: just save the key
150-
config.api_key = apiKey;
151-
if (!fs.existsSync(gitmemDir)) {
152-
fs.mkdirSync(gitmemDir, { recursive: true });
153-
}
154-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
155-
console.log("License key saved. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY env vars for Supabase.");
156-
return;
157-
}
191+
// Create readline only if TTY is available
192+
const rl = process.stdin.isTTY ? createReadline() : null;
158193

159-
const rl = createReadline();
160-
161-
// Step 3: Supabase credentials (with re-activation safety check)
194+
// Step 3: Resolve Supabase credentials (env → config → prompt)
162195
const existingUrl = config.supabase_url as string | undefined;
196+
const existingKey = config.supabase_key as string | undefined;
163197

164-
console.log("Supabase Setup");
165-
console.log(" (Create a free project at https://database.new)");
166-
if (existingUrl) {
167-
console.log(` Current: ${existingUrl}`);
168-
}
169-
console.log("");
170-
171-
let supabaseUrl: string;
172-
if (existingUrl) {
173-
const urlInput = await ask(rl, ` Project URL [${existingUrl}]: `);
174-
supabaseUrl = urlInput || existingUrl;
175-
176-
// Warn if changing to a different Supabase instance
177-
if (urlInput && urlInput !== existingUrl) {
178-
console.log("");
179-
console.log(" ⚠ WARNING: You are changing your Supabase URL.");
180-
console.log(` Old: ${existingUrl}`);
181-
console.log(` New: ${urlInput}`);
182-
console.log(" Your existing data in the old project will NOT be migrated.");
183-
console.log("");
184-
const confirm = await ask(rl, " Continue with new URL? (y/N): ");
185-
if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
186-
console.log(" Keeping existing URL.");
187-
supabaseUrl = existingUrl;
188-
}
198+
if (rl) {
199+
console.log("Supabase Setup");
200+
console.log(" (Create a free project at https://database.new)");
201+
if (existingUrl) {
202+
console.log(` Current: ${existingUrl}`);
189203
}
190-
} else {
191-
supabaseUrl = await ask(rl, " Project URL: ");
204+
console.log("");
192205
}
193206

194-
if (!supabaseUrl) {
195-
console.error("Error: Supabase URL is required for Pro tier.");
196-
rl.close();
197-
process.exit(1);
198-
}
207+
const supabaseUrlResult = await resolveCredential({
208+
envVar: "SUPABASE_URL",
209+
configValue: existingUrl,
210+
promptLabel: "Project URL",
211+
required: true,
212+
rl,
213+
});
199214

200-
const existingKey = config.supabase_key as string | undefined;
201-
let supabaseKey: string;
202-
if (existingKey && supabaseUrl === existingUrl) {
203-
// Same URL, offer to keep existing key
204-
const keyInput = await ask(rl, " Service Role Key [keep existing]: ");
205-
supabaseKey = keyInput || existingKey;
206-
} else {
207-
supabaseKey = await ask(rl, " Service Role Key: ");
208-
}
215+
const supabaseUrl = supabaseUrlResult.value;
209216

210-
if (!supabaseKey) {
211-
console.error("Error: Service Role Key is required.");
212-
rl.close();
213-
process.exit(1);
217+
// Re-activation safety: warn if changing URL interactively
218+
if (rl && existingUrl && supabaseUrl !== existingUrl && supabaseUrlResult.source === "prompt") {
219+
console.log("");
220+
console.log(" ⚠ WARNING: You are changing your Supabase URL.");
221+
console.log(` Old: ${existingUrl}`);
222+
console.log(` New: ${supabaseUrl}`);
223+
console.log(" Your existing data in the old project will NOT be migrated.");
224+
console.log("");
225+
const confirm = await ask(rl, " Continue with new URL? (y/N): ");
226+
if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
227+
console.log(" Keeping existing URL.");
228+
// Fall back handled below by using existingUrl
229+
}
214230
}
215231

216-
// Step 4: Test connection and check schema
217-
console.log(" Testing connection...");
218-
const connected = await testSupabaseConnection(supabaseUrl, supabaseKey);
219-
if (!connected) {
220-
console.error(" ✗ Could not connect to Supabase. Check your URL and key.");
221-
rl.close();
222-
process.exit(1);
223-
}
224-
console.log(" ✓ Connected to Supabase");
232+
// Resolve service role key — if same URL, offer to keep existing
233+
const supabaseKeyResult = await resolveCredential({
234+
envVar: "SUPABASE_SERVICE_ROLE_KEY",
235+
configValue: (supabaseUrl === existingUrl) ? existingKey : undefined,
236+
promptLabel: "Service Role Key",
237+
required: true,
238+
rl,
239+
existingHint: (existingKey && supabaseUrl === existingUrl) ? "keep existing" : undefined,
240+
});
225241

226-
// Check if required tables exist
227-
const missingTables = await checkSchemaExists(supabaseUrl, supabaseKey);
228-
if (missingTables.length > 0) {
229-
console.log("");
230-
console.log(" ⚠ Missing tables: " + missingTables.join(", "));
231-
console.log(" Run the schema setup in your Supabase SQL Editor:");
232-
console.log("");
233-
console.log(" npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)");
234-
console.log(" npx gitmem-mcp setup (prints SQL to paste manually)");
242+
const supabaseKey = supabaseKeyResult.value;
243+
244+
// Step 4: Test connection if we have credentials
245+
let missingTables: string[] = [];
246+
let connectionFailed = false;
247+
if (supabaseUrl && supabaseKey) {
248+
if (supabaseUrlResult.source !== "config" || supabaseKeyResult.source !== "config") {
249+
// Only test if credentials are new (not just re-read from config)
250+
console.log(" Testing connection...");
251+
const connected = await testSupabaseConnection(supabaseUrl, supabaseKey);
252+
if (!connected) {
253+
connectionFailed = true;
254+
if (rl) {
255+
// Interactive: hard failure — user can re-enter
256+
console.error(" ✗ Could not connect to Supabase. Check your URL and key.");
257+
rl.close();
258+
process.exit(1);
259+
} else {
260+
// Non-interactive: warn but save credentials anyway
261+
console.log(" ⚠ Could not connect to Supabase (credentials saved anyway).");
262+
console.log(" Verify your SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are correct.");
263+
}
264+
} else {
265+
console.log(" ✓ Connected to Supabase");
266+
}
267+
}
268+
269+
if (!connectionFailed) {
270+
missingTables = await checkSchemaExists(supabaseUrl, supabaseKey);
271+
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");
280+
} else {
281+
console.log(" ✓ Schema verified (all tables present)");
282+
}
283+
}
235284
console.log("");
236-
console.log(" Then: Supabase Dashboard → SQL Editor → New query → Paste → Run");
285+
} else if (!supabaseUrl || !supabaseKey) {
286+
console.log(" ⚠ Supabase credentials not provided.");
287+
console.log(" Pro features require Supabase. Set via:");
288+
console.log(" - Environment: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY");
289+
console.log(" - Config: edit .gitmem/config.json (supabase_url, supabase_key)");
290+
console.log(" - Re-run: npx gitmem-mcp activate (interactive)");
237291
console.log("");
238-
} else {
239-
console.log(" ✓ Schema verified (all tables present)");
240292
}
241-
console.log("");
242-
243-
// Step 5: OpenRouter key
244-
console.log("OpenRouter Setup");
245-
console.log(" (Get a key at https://openrouter.ai/keys)");
246293

247-
const existingOpenRouter = config.openrouter_key as string | undefined;
248-
if (existingOpenRouter) {
249-
console.log(` Current: ${existingOpenRouter.substring(0, 12)}...`);
294+
// Step 5: Resolve OpenRouter key (env → config → prompt)
295+
if (rl) {
296+
console.log("OpenRouter Setup");
297+
console.log(" (Get a key at https://openrouter.ai/keys)");
298+
const existingOpenRouter = config.openrouter_key as string | undefined;
299+
if (existingOpenRouter) {
300+
console.log(` Current: ${existingOpenRouter.substring(0, 12)}...`);
301+
}
302+
console.log("");
250303
}
251-
console.log("");
252304

253-
let openrouterKey: string;
254-
if (existingOpenRouter) {
255-
const orInput = await ask(rl, " API Key [keep existing]: ");
256-
openrouterKey = orInput || existingOpenRouter;
257-
} else {
258-
openrouterKey = await ask(rl, " API Key: ");
259-
}
305+
const openrouterResult = await resolveCredential({
306+
envVar: "OPENROUTER_API_KEY",
307+
configValue: config.openrouter_key as string | undefined,
308+
promptLabel: "API Key",
309+
required: false,
310+
rl,
311+
existingHint: (config.openrouter_key as string) ? "keep existing" : undefined,
312+
});
313+
314+
const openrouterKey = openrouterResult.value;
260315

261316
if (openrouterKey) {
262-
console.log(" ✓ OpenRouter configured");
263-
} else {
317+
if (openrouterResult.source === "env") {
318+
console.log(" ✓ OpenRouter configured (from env)");
319+
} else if (openrouterResult.source === "config") {
320+
// Silent — already configured
321+
} else {
322+
console.log(" ✓ OpenRouter configured");
323+
}
324+
} else if (rl) {
264325
console.log(" ⚠ Skipped (semantic search will not work without embeddings)");
265326
}
266327

267-
rl.close();
328+
if (rl) rl.close();
268329

269330
// Step 6: Write config (preserves existing fields like project, install_id, feedback_enabled)
270331
config.api_key = apiKey;
271-
config.supabase_url = supabaseUrl;
272-
config.supabase_key = supabaseKey;
273-
if (openrouterKey) {
274-
config.openrouter_key = openrouterKey;
275-
}
332+
if (supabaseUrl) config.supabase_url = supabaseUrl;
333+
if (supabaseKey) config.supabase_key = supabaseKey;
334+
if (openrouterKey) config.openrouter_key = openrouterKey;
276335

277336
if (!fs.existsSync(gitmemDir)) {
278337
fs.mkdirSync(gitmemDir, { recursive: true });
@@ -282,13 +341,25 @@ export async function main(args: string[]): Promise<void> {
282341
// Clear any stale license cache
283342
clearLicenseCache();
284343

344+
// Summary
285345
console.log("");
286346
console.log("─────────────────────");
287-
if (missingTables.length > 0) {
347+
348+
const sources: string[] = [];
349+
if (supabaseUrl) sources.push(`Supabase (${supabaseUrlResult.source})`);
350+
if (openrouterKey) sources.push(`OpenRouter (${openrouterResult.source})`);
351+
352+
if (!supabaseUrl) {
353+
console.log("License key activated. Supabase credentials still needed for Pro features.");
354+
} else if (missingTables.length > 0) {
288355
console.log("Pro tier activated! Run schema setup, then restart your editor.");
289356
} else {
290357
console.log("Pro tier activated! Restart your editor to apply.");
291358
}
292-
console.log(`Config saved to ${configPath}`);
359+
360+
if (sources.length > 0) {
361+
console.log(` Credentials: ${sources.join(", ")}`);
362+
}
363+
console.log(` Config: ${configPath}`);
293364
console.log("");
294365
}

0 commit comments

Comments
 (0)