agentlink-sh 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- delay,
4
- runCommand
5
- } from "./chunk-IV5ZSOKF.js";
6
2
  import {
7
3
  amber,
8
4
  blue,
9
5
  bold,
10
6
  dim,
11
7
  link,
12
- red
13
- } from "./chunk-MHI6VJ75.js";
14
- import {
8
+ red,
15
9
  sb
16
- } from "./chunk-ILLYV7U7.js";
10
+ } from "./chunk-KECTTRCF.js";
17
11
 
18
12
  // src/cloud.ts
19
- import fs from "fs";
13
+ import fs2 from "fs";
20
14
  import os from "os";
21
- import path from "path";
15
+ import path2 from "path";
22
16
  import { input, select as select2 } from "@inquirer/prompts";
23
17
 
24
18
  // src/prompts.ts
@@ -40,6 +34,187 @@ async function selectYesNo(opts) {
40
34
 
41
35
  // src/cloud.ts
42
36
  import open from "open";
37
+
38
+ // src/utils.ts
39
+ import { execSync, spawn } from "child_process";
40
+ import crypto from "crypto";
41
+ import fs from "fs";
42
+ import path from "path";
43
+ var LOG_FILE = "agentlink-debug.log";
44
+ var debugEnabled = false;
45
+ var logInitialized = false;
46
+ function ensureLogFile() {
47
+ if (!logInitialized) {
48
+ fs.writeFileSync(LOG_FILE, "");
49
+ logInitialized = true;
50
+ }
51
+ }
52
+ function appendLog(msg, force = false) {
53
+ if (!debugEnabled && !force) return;
54
+ ensureLogFile();
55
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
56
+ fs.appendFileSync(LOG_FILE, `[${timestamp}] ${msg}
57
+ `);
58
+ }
59
+ function initLog(debug) {
60
+ debugEnabled = debug;
61
+ if (debug) {
62
+ ensureLogFile();
63
+ }
64
+ }
65
+ function runCommand(cmd, cwd, onData, env) {
66
+ appendLog(`$ ${cmd}${cwd ? ` (in ${cwd})` : ""}`);
67
+ return new Promise((resolve, reject) => {
68
+ const child = spawn(cmd, {
69
+ cwd,
70
+ stdio: ["ignore", "pipe", "pipe"],
71
+ shell: true,
72
+ env: env ? { ...process.env, ...env } : void 0
73
+ });
74
+ const stdout = [];
75
+ const stderr = [];
76
+ child.stdout.on("data", (data) => {
77
+ const text = data.toString();
78
+ stdout.push(text);
79
+ if (onData) {
80
+ for (const line of text.split("\n").filter(Boolean)) {
81
+ onData(line.trim());
82
+ }
83
+ }
84
+ });
85
+ child.stderr.on("data", (data) => {
86
+ const text = data.toString();
87
+ stderr.push(text);
88
+ if (onData) {
89
+ for (const line of text.split("\n").filter(Boolean)) {
90
+ onData(line.trim());
91
+ }
92
+ }
93
+ });
94
+ child.on("close", (code) => {
95
+ const out = stdout.join("").trim();
96
+ const errOut = stderr.join("").trim();
97
+ if (out) appendLog(out);
98
+ if (errOut) appendLog(`STDERR: ${errOut}`);
99
+ if (code !== 0) {
100
+ appendLog(`$ ${cmd}${cwd ? ` (in ${cwd})` : ""}`, true);
101
+ appendLog(`EXIT CODE ${code}`, true);
102
+ if (errOut) appendLog(`STDERR: ${errOut}`, true);
103
+ const detail = errOut;
104
+ const isUnixCacheError = detail.includes("EACCES") && detail.includes(".npm");
105
+ const isWindowsCacheError = detail.includes("EPERM") && detail.toLowerCase().includes("npm-cache");
106
+ if (isUnixCacheError || isWindowsCacheError) {
107
+ const fix = process.platform === "win32" ? ` npm cache clean --force` : ` sudo chown -R $(id -u):$(id -g) ~/.npm`;
108
+ reject(new Error(
109
+ `npm cache has incorrect permissions.
110
+
111
+ Fix it by running:
112
+
113
+ ${fix}
114
+
115
+ Then re-run the command.`
116
+ ));
117
+ return;
118
+ }
119
+ const msg = detail ? `Command failed: ${cmd}
120
+ ${detail}` : `Command failed: ${cmd}`;
121
+ reject(new Error(msg));
122
+ } else {
123
+ resolve(out);
124
+ }
125
+ });
126
+ child.on("error", (err) => {
127
+ appendLog(`$ ${cmd}${cwd ? ` (in ${cwd})` : ""}`, true);
128
+ appendLog(`SPAWN ERROR: ${err.message}`, true);
129
+ reject(
130
+ new Error(`Command failed: ${cmd}
131
+ ${err.message}`)
132
+ );
133
+ });
134
+ });
135
+ }
136
+ var GIT_CLEAN_IGNORED = /* @__PURE__ */ new Set([
137
+ ".agentlink-progress.json",
138
+ "agentlink-debug.log"
139
+ ]);
140
+ async function assertGitClean(projectDir, allowDirty) {
141
+ if (allowDirty) return;
142
+ let status;
143
+ try {
144
+ status = await runCommand("git status --porcelain", projectDir);
145
+ } catch {
146
+ return;
147
+ }
148
+ const dirty = status.split("\n").filter((line) => {
149
+ if (!line.trim()) return false;
150
+ const filePath = line.slice(3).split(" -> ").pop()?.trim() ?? "";
151
+ return !GIT_CLEAN_IGNORED.has(filePath);
152
+ });
153
+ if (dirty.length > 0) {
154
+ throw new Error(
155
+ "You have uncommitted changes. Commit or stash them first so the update\nis easy to review and rollback (git diff / git checkout .).\n git stash \u2014 stash changes temporarily\n git commit -am \u2026 \u2014 commit changes\n --allow-dirty \u2014 skip this check (rollback will be messy)"
156
+ );
157
+ }
158
+ }
159
+ async function listChangedFiles(projectDir) {
160
+ let status;
161
+ try {
162
+ status = await runCommand("git status --porcelain", projectDir);
163
+ } catch {
164
+ return [];
165
+ }
166
+ return status.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.slice(3).split(" -> ").pop()?.trim() ?? "").filter((p) => p && !GIT_CLEAN_IGNORED.has(p));
167
+ }
168
+ function delay(ms) {
169
+ return new Promise((resolve) => setTimeout(resolve, ms));
170
+ }
171
+ function checkCommand(cmd) {
172
+ const locator = process.platform === "win32" ? "where" : "which";
173
+ try {
174
+ execSync(`${locator} ${cmd}`, { stdio: "ignore" });
175
+ return true;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+ function skillDisplayName(skill) {
181
+ if (skill.includes("@")) return skill.split("@").pop();
182
+ const flag = skill.match(/--skill\s+(\S+)/);
183
+ if (flag) return flag[1];
184
+ return skill.split("/").pop();
185
+ }
186
+ function generateDbPassword(length = 32) {
187
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
188
+ const bytes = crypto.randomBytes(length);
189
+ return Array.from(bytes, (b) => chars[b % chars.length]).join("");
190
+ }
191
+ function slugifyProjectName(name) {
192
+ return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9.-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
193
+ }
194
+ function validateProjectName(name, mode = "new") {
195
+ const slug = slugifyProjectName(name);
196
+ if (!slug) return "Project name is required.";
197
+ if (mode === "new" && fs.existsSync(slug))
198
+ return `Directory "${slug}" already exists.`;
199
+ return void 0;
200
+ }
201
+ function ensureGitignorePattern(cwd, pattern, comment = "Agent Link \u2014 database backups (may contain sensitive data)") {
202
+ const gitignorePath = path.join(cwd, ".gitignore");
203
+ let content = "";
204
+ if (fs.existsSync(gitignorePath)) {
205
+ content = fs.readFileSync(gitignorePath, "utf-8");
206
+ const patternNoSlash = pattern.replace(/\/$/, "");
207
+ const alreadyPresent = content.split("\n").map((l) => l.trim()).some((l) => l === pattern || l === patternNoSlash);
208
+ if (alreadyPresent) return;
209
+ }
210
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
211
+ const block = (needsLeadingNewline ? "\n" : "") + (content.length > 0 ? "\n" : "") + `# ${comment}
212
+ ${pattern}
213
+ `;
214
+ fs.writeFileSync(gitignorePath, content + block);
215
+ }
216
+
217
+ // src/cloud.ts
43
218
  var theme = {
44
219
  prefix: { idle: blue("?"), done: blue("\u2714") },
45
220
  style: {
@@ -52,19 +227,41 @@ var theme = {
52
227
  }
53
228
  };
54
229
  function credentialsPath() {
55
- return path.join(os.homedir(), ".config", "agentlink", "credentials.json");
230
+ return path2.join(os.homedir(), ".config", "agentlink", "credentials.json");
56
231
  }
57
232
  function loadCredentials() {
58
233
  try {
59
- return JSON.parse(fs.readFileSync(credentialsPath(), "utf-8"));
234
+ return JSON.parse(fs2.readFileSync(credentialsPath(), "utf-8"));
60
235
  } catch {
61
236
  return {};
62
237
  }
63
238
  }
64
239
  function saveCredentials(creds) {
65
240
  const filePath = credentialsPath();
66
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
67
- fs.writeFileSync(filePath, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
241
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
242
+ fs2.writeFileSync(filePath, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
243
+ }
244
+ function getResendDefaults() {
245
+ const creds = loadCredentials();
246
+ const d = creds.resend_defaults;
247
+ if (!d || !d.api_key || !d.from_email) return null;
248
+ return { api_key: d.api_key, from_email: d.from_email, saved_at: d.saved_at };
249
+ }
250
+ function setResendDefaults(api_key, from_email) {
251
+ const creds = loadCredentials();
252
+ creds.resend_defaults = {
253
+ api_key,
254
+ from_email,
255
+ saved_at: (/* @__PURE__ */ new Date()).toISOString()
256
+ };
257
+ saveCredentials(creds);
258
+ }
259
+ function clearResendDefaults() {
260
+ const creds = loadCredentials();
261
+ if (!creds.resend_defaults) return false;
262
+ delete creds.resend_defaults;
263
+ saveCredentials(creds);
264
+ return true;
68
265
  }
69
266
  var _authContext = {};
70
267
  function isAuthError(err) {
@@ -77,7 +274,7 @@ async function refreshIfNeeded(entry) {
77
274
  }
78
275
  if (!entry.refresh_token) return null;
79
276
  try {
80
- const { refreshOAuthToken } = await import("./oauth-2QHJGBWP.js");
277
+ const { refreshOAuthToken } = await import("./oauth-52Q6XDJL.js");
81
278
  const tokens = await refreshOAuthToken(entry.refresh_token);
82
279
  return {
83
280
  ...entry,
@@ -93,7 +290,7 @@ async function signOut(opts = {}) {
93
290
  const notes = [];
94
291
  const filePath = credentialsPath();
95
292
  let agentlinkStoreCleared = false;
96
- if (fs.existsSync(filePath)) {
293
+ if (fs2.existsSync(filePath)) {
97
294
  try {
98
295
  const before = loadCredentials();
99
296
  const hadAnything = !!before.oauth || Object.keys(before.oauth_by_org ?? {}).length > 0 || !!before.supabase_access_token;
@@ -111,13 +308,13 @@ async function signOut(opts = {}) {
111
308
  const envVarCleared = !!process.env.SUPABASE_ACCESS_TOKEN;
112
309
  delete process.env.SUPABASE_ACCESS_TOKEN;
113
310
  if (opts.cwd) {
114
- const envPath = path.join(opts.cwd, ".env.local");
115
- if (fs.existsSync(envPath)) {
311
+ const envPath = path2.join(opts.cwd, ".env.local");
312
+ if (fs2.existsSync(envPath)) {
116
313
  try {
117
- let content = fs.readFileSync(envPath, "utf-8");
314
+ let content = fs2.readFileSync(envPath, "utf-8");
118
315
  if (/^SUPABASE_ACCESS_TOKEN=/m.test(content)) {
119
316
  content = content.replace(/^SUPABASE_ACCESS_TOKEN=.*\n?/m, "");
120
- fs.writeFileSync(envPath, content);
317
+ fs2.writeFileSync(envPath, content);
121
318
  notes.push(`Stripped SUPABASE_ACCESS_TOKEN from ${envPath}`);
122
319
  }
123
320
  } catch (err) {
@@ -155,16 +352,51 @@ function clearAccessToken(opts = {}) {
155
352
  }
156
353
  saveCredentials(creds);
157
354
  if (opts.projectDir) {
158
- const envPath = path.join(opts.projectDir, ".env.local");
159
- if (fs.existsSync(envPath)) {
160
- let content = fs.readFileSync(envPath, "utf-8");
355
+ const envPath = path2.join(opts.projectDir, ".env.local");
356
+ if (fs2.existsSync(envPath)) {
357
+ let content = fs2.readFileSync(envPath, "utf-8");
161
358
  if (/^SUPABASE_ACCESS_TOKEN=/m.test(content)) {
162
359
  content = content.replace(/^SUPABASE_ACCESS_TOKEN=.*\n?/m, "");
163
- fs.writeFileSync(envPath, content);
360
+ fs2.writeFileSync(envPath, content);
164
361
  }
165
362
  }
166
363
  }
167
364
  }
365
+ function isInteractiveTty() {
366
+ return process.stdout.isTTY === true && process.stdin.isTTY === true;
367
+ }
368
+ async function runOAuthLogin(opts) {
369
+ const { orgId, orgSlug } = opts;
370
+ const { oauthLogin } = await import("./oauth-52Q6XDJL.js");
371
+ const tokens = await oauthLogin({ organizationSlug: orgSlug });
372
+ process.env.SUPABASE_ACCESS_TOKEN = tokens.access_token;
373
+ let derivedOrg;
374
+ try {
375
+ const orgs = await listOrganizations();
376
+ derivedOrg = orgs[0];
377
+ } catch {
378
+ }
379
+ const entry = {
380
+ access_token: tokens.access_token,
381
+ refresh_token: tokens.refresh_token,
382
+ expires_at: Math.floor(Date.now() / 1e3) + tokens.expires_in,
383
+ org_name: derivedOrg?.name,
384
+ org_slug: derivedOrg?.slug
385
+ };
386
+ const targetOrgId = derivedOrg?.id ?? orgId;
387
+ const current = loadCredentials();
388
+ if (targetOrgId) {
389
+ saveCredentials({
390
+ ...current,
391
+ oauth_by_org: { ...current.oauth_by_org ?? {}, [targetOrgId]: entry }
392
+ });
393
+ } else {
394
+ saveCredentials({ ...current, oauth: entry });
395
+ }
396
+ console.log(` ${blue("\u2714")} Authenticated via Supabase OAuth${derivedOrg?.name ? ` for ${derivedOrg.name}` : ""}`);
397
+ console.log(` ${dim("Token stored at " + credentialsPath())}`);
398
+ console.log();
399
+ }
168
400
  async function authenticatedFetch(url, init) {
169
401
  const token = process.env.SUPABASE_ACCESS_TOKEN;
170
402
  if (!token) {
@@ -173,23 +405,56 @@ async function authenticatedFetch(url, init) {
173
405
  const headers = new Headers(init?.headers);
174
406
  headers.set("Authorization", `Bearer ${token}`);
175
407
  const res = await fetch(url, { ...init, headers });
176
- if (res.status === 401 || res.status === 403) {
177
- console.log(`
178
- ${amber("\u25B2")} Access token expired or revoked. Re-authenticating...
179
- `);
180
- clearAccessToken({ projectDir: _authContext.projectDir, orgId: _authContext.orgId });
181
- await ensureAccessToken({
182
- nonInteractive: _authContext.nonInteractive,
183
- projectDir: _authContext.projectDir,
184
- orgId: _authContext.orgId,
185
- orgSlug: _authContext.orgSlug
186
- });
187
- const newToken = process.env.SUPABASE_ACCESS_TOKEN;
188
- const retryHeaders = new Headers(init?.headers);
189
- retryHeaders.set("Authorization", `Bearer ${newToken}`);
190
- return fetch(url, { ...init, headers: retryHeaders });
408
+ if (res.status !== 401 && res.status !== 403) return res;
409
+ const orgId = _authContext.orgId;
410
+ const initialCreds = loadCredentials();
411
+ const entry = orgId ? initialCreds.oauth_by_org?.[orgId] : initialCreds.oauth;
412
+ if (entry?.refresh_token) {
413
+ const { refreshOAuthToken } = await import("./oauth-52Q6XDJL.js");
414
+ let fresh = null;
415
+ try {
416
+ fresh = await refreshOAuthToken(entry.refresh_token);
417
+ } catch {
418
+ }
419
+ if (fresh) {
420
+ const refreshedEntry = {
421
+ ...entry,
422
+ access_token: fresh.access_token,
423
+ refresh_token: fresh.refresh_token,
424
+ expires_at: Math.floor(Date.now() / 1e3) + fresh.expires_in
425
+ };
426
+ const latest = loadCredentials();
427
+ if (orgId) {
428
+ saveCredentials({
429
+ ...latest,
430
+ oauth_by_org: { ...latest.oauth_by_org ?? {}, [orgId]: refreshedEntry }
431
+ });
432
+ } else {
433
+ saveCredentials({ ...latest, oauth: refreshedEntry });
434
+ }
435
+ process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
436
+ const retryHeaders2 = new Headers(init?.headers);
437
+ retryHeaders2.set("Authorization", `Bearer ${fresh.access_token}`);
438
+ const retry = await fetch(url, { ...init, headers: retryHeaders2 });
439
+ if (retry.status !== 401 && retry.status !== 403) return retry;
440
+ }
191
441
  }
192
- return res;
442
+ if (!isInteractiveTty()) {
443
+ throw new Error(
444
+ `Authentication failed and no TTY available for re-auth.
445
+ Run interactively: npx agentlink-sh@latest sb login` + (orgId ? ` (org: ${orgId})` : "") + `
446
+ Or set SUPABASE_ACCESS_TOKEN to a PAT with the required scope.`
447
+ );
448
+ }
449
+ console.log();
450
+ console.log(` ${amber("\u25B2")} Access token expired or revoked. Re-authenticating via Supabase OAuth...`);
451
+ console.log();
452
+ clearAccessToken({ projectDir: _authContext.projectDir, orgId: _authContext.orgId });
453
+ await runOAuthLogin({ orgId: _authContext.orgId, orgSlug: _authContext.orgSlug });
454
+ const newToken = process.env.SUPABASE_ACCESS_TOKEN;
455
+ const retryHeaders = new Headers(init?.headers);
456
+ retryHeaders.set("Authorization", `Bearer ${newToken}`);
457
+ return fetch(url, { ...init, headers: retryHeaders });
193
458
  }
194
459
  async function authenticatedRunCommand(cmd, cwd) {
195
460
  if (!process.env.SUPABASE_ACCESS_TOKEN) {
@@ -203,20 +468,54 @@ async function authenticatedRunCommand(cmd, cwd) {
203
468
  try {
204
469
  return await runCommand(cmd, cwd);
205
470
  } catch (err) {
206
- if (isAuthError(err)) {
207
- console.log(`
208
- ${amber("\u25B2")} Access token expired or revoked. Re-authenticating...
209
- `);
210
- clearAccessToken({ projectDir: _authContext.projectDir, orgId: _authContext.orgId });
211
- await ensureAccessToken({
212
- nonInteractive: _authContext.nonInteractive,
213
- projectDir: _authContext.projectDir,
214
- orgId: _authContext.orgId,
215
- orgSlug: _authContext.orgSlug
216
- });
217
- return runCommand(cmd, cwd);
471
+ if (!isAuthError(err)) throw err;
472
+ const orgId = _authContext.orgId;
473
+ const initialCreds = loadCredentials();
474
+ const entry = orgId ? initialCreds.oauth_by_org?.[orgId] : initialCreds.oauth;
475
+ if (entry?.refresh_token) {
476
+ const { refreshOAuthToken } = await import("./oauth-52Q6XDJL.js");
477
+ let fresh = null;
478
+ try {
479
+ fresh = await refreshOAuthToken(entry.refresh_token);
480
+ } catch {
481
+ }
482
+ if (fresh) {
483
+ const refreshedEntry = {
484
+ ...entry,
485
+ access_token: fresh.access_token,
486
+ refresh_token: fresh.refresh_token,
487
+ expires_at: Math.floor(Date.now() / 1e3) + fresh.expires_in
488
+ };
489
+ const latest = loadCredentials();
490
+ if (orgId) {
491
+ saveCredentials({
492
+ ...latest,
493
+ oauth_by_org: { ...latest.oauth_by_org ?? {}, [orgId]: refreshedEntry }
494
+ });
495
+ } else {
496
+ saveCredentials({ ...latest, oauth: refreshedEntry });
497
+ }
498
+ process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
499
+ try {
500
+ return await runCommand(cmd, cwd);
501
+ } catch (err2) {
502
+ if (!isAuthError(err2)) throw err2;
503
+ }
504
+ }
505
+ }
506
+ if (!isInteractiveTty()) {
507
+ throw new Error(
508
+ `Authentication failed and no TTY available for re-auth.
509
+ Run interactively: npx agentlink-sh@latest sb login` + (orgId ? ` (org: ${orgId})` : "") + `
510
+ Or set SUPABASE_ACCESS_TOKEN to a PAT with the required scope.`
511
+ );
218
512
  }
219
- throw err;
513
+ console.log();
514
+ console.log(` ${amber("\u25B2")} Access token expired or revoked. Re-authenticating via Supabase OAuth...`);
515
+ console.log();
516
+ clearAccessToken({ projectDir: _authContext.projectDir, orgId: _authContext.orgId });
517
+ await runOAuthLogin({ orgId: _authContext.orgId, orgSlug: _authContext.orgSlug });
518
+ return runCommand(cmd, cwd);
220
519
  }
221
520
  }
222
521
  async function ensureAccessToken(opts = {}) {
@@ -238,6 +537,20 @@ async function ensureAccessToken(opts = {}) {
238
537
  process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
239
538
  return;
240
539
  }
540
+ if (isInteractiveTty()) {
541
+ console.log();
542
+ console.log(` ${amber("\u25B2")} OAuth token for ${orgSlug ? `"${orgSlug}"` : `org ${orgId}`} expired and could not be refreshed.`);
543
+ console.log(` Re-authenticating via Supabase OAuth...`);
544
+ console.log();
545
+ clearAccessToken({ projectDir, orgId });
546
+ await runOAuthLogin({ orgId, orgSlug });
547
+ return;
548
+ }
549
+ throw new Error(
550
+ `OAuth token for organization ${orgId} expired and refresh failed.
551
+ Run interactively: npx agentlink-sh@latest sb login
552
+ Or set SUPABASE_ACCESS_TOKEN to a PAT with access to this org.`
553
+ );
241
554
  }
242
555
  }
243
556
  if (creds.oauth) {
@@ -301,7 +614,7 @@ async function ensureAccessToken(opts = {}) {
301
614
  }
302
615
  if (nonInteractive) {
303
616
  throw new Error(
304
- "SUPABASE_ACCESS_TOKEN is required for cloud mode.\n Set it via --token, SUPABASE_ACCESS_TOKEN env var, or run `agentlink sb login`."
617
+ "SUPABASE_ACCESS_TOKEN is required for cloud mode.\n Set it via --token, SUPABASE_ACCESS_TOKEN env var, or run `npx agentlink-sh@latest sb login`."
305
618
  );
306
619
  }
307
620
  const method = await select2({
@@ -313,35 +626,7 @@ async function ensureAccessToken(opts = {}) {
313
626
  ]
314
627
  });
315
628
  if (method === "oauth") {
316
- const { oauthLogin } = await import("./oauth-2QHJGBWP.js");
317
- const tokens = await oauthLogin({ organizationSlug: orgSlug });
318
- process.env.SUPABASE_ACCESS_TOKEN = tokens.access_token;
319
- let derivedOrg;
320
- try {
321
- const orgs = await listOrganizations();
322
- derivedOrg = orgs[0];
323
- } catch {
324
- }
325
- const entry = {
326
- access_token: tokens.access_token,
327
- refresh_token: tokens.refresh_token,
328
- expires_at: Math.floor(Date.now() / 1e3) + tokens.expires_in,
329
- org_name: derivedOrg?.name,
330
- org_slug: derivedOrg?.slug
331
- };
332
- const targetOrgId = derivedOrg?.id ?? orgId;
333
- const current = loadCredentials();
334
- if (targetOrgId) {
335
- saveCredentials({
336
- ...current,
337
- oauth_by_org: { ...current.oauth_by_org ?? {}, [targetOrgId]: entry }
338
- });
339
- } else {
340
- saveCredentials({ ...current, oauth: entry });
341
- }
342
- console.log(` ${blue("\u2714")} Authenticated via Supabase OAuth${derivedOrg?.name ? ` for ${derivedOrg.name}` : ""}`);
343
- console.log(` ${dim("Token stored at " + credentialsPath())}`);
344
- console.log();
629
+ await runOAuthLogin({ orgId, orgSlug });
345
630
  return;
346
631
  }
347
632
  const tokenUrl = "https://supabase.com/dashboard/account/tokens";
@@ -497,6 +782,54 @@ function clearOrgCredential(orgId) {
497
782
  saveCredentials(creds);
498
783
  }
499
784
  }
785
+ function listCachedOrgs() {
786
+ const byOrg = loadCredentials().oauth_by_org ?? {};
787
+ return Object.entries(byOrg).map(([id, entry]) => ({
788
+ id,
789
+ name: entry.org_name,
790
+ slug: entry.org_slug,
791
+ expiresAt: entry.expires_at
792
+ }));
793
+ }
794
+ async function refreshCachedOrg(orgId) {
795
+ const creds = loadCredentials();
796
+ const entry = creds.oauth_by_org?.[orgId];
797
+ if (!entry) return null;
798
+ const fresh = await refreshIfNeeded(entry);
799
+ if (!fresh) return null;
800
+ const discovered = (await listOrganizationsForToken(fresh.access_token))[0];
801
+ if (!discovered) return null;
802
+ const latest = loadCredentials();
803
+ const newByOrg = { ...latest.oauth_by_org ?? {} };
804
+ if (orgId !== discovered.id) delete newByOrg[orgId];
805
+ newByOrg[discovered.id] = {
806
+ ...fresh,
807
+ org_name: discovered.name,
808
+ org_slug: discovered.slug
809
+ };
810
+ saveCredentials({ ...latest, oauth_by_org: newByOrg });
811
+ return {
812
+ id: discovered.id,
813
+ name: discovered.name,
814
+ slug: discovered.slug,
815
+ expiresAt: newByOrg[discovered.id].expires_at
816
+ };
817
+ }
818
+ async function refreshAllCachedOrgs() {
819
+ const cached = listCachedOrgs();
820
+ const results = await Promise.allSettled(cached.map((o) => refreshCachedOrg(o.id)));
821
+ const refreshed = [];
822
+ const failed = [];
823
+ results.forEach((r, i) => {
824
+ const original = cached[i];
825
+ if (r.status === "fulfilled" && r.value) {
826
+ refreshed.push(r.value);
827
+ } else {
828
+ failed.push({ id: original.id, name: original.name });
829
+ }
830
+ });
831
+ return { refreshed, failed };
832
+ }
500
833
  async function createProject(opts) {
501
834
  const out = await authenticatedRunCommand(
502
835
  `${sb()} projects create ${JSON.stringify(opts.name)} --org-id ${opts.orgId} --region ${opts.region} --db-password ${opts.dbPass} -o json`
@@ -761,9 +1094,23 @@ async function setSecrets(projectRef, secrets) {
761
1094
 
762
1095
  export {
763
1096
  selectYesNo,
1097
+ initLog,
1098
+ runCommand,
1099
+ assertGitClean,
1100
+ listChangedFiles,
1101
+ delay,
1102
+ checkCommand,
1103
+ skillDisplayName,
1104
+ generateDbPassword,
1105
+ slugifyProjectName,
1106
+ validateProjectName,
1107
+ ensureGitignorePattern,
764
1108
  credentialsPath,
765
1109
  loadCredentials,
766
1110
  saveCredentials,
1111
+ getResendDefaults,
1112
+ setResendDefaults,
1113
+ clearResendDefaults,
767
1114
  signOut,
768
1115
  authenticatedFetch,
769
1116
  ensureAccessToken,
@@ -776,6 +1123,9 @@ export {
776
1123
  refreshOAuthEntry,
777
1124
  probeTokenAuth,
778
1125
  clearOrgCredential,
1126
+ listCachedOrgs,
1127
+ refreshCachedOrg,
1128
+ refreshAllCachedOrgs,
779
1129
  createProject,
780
1130
  getProject,
781
1131
  deleteProject,