agentlink-sh 0.26.1 → 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.
- package/README.md +90 -94
- package/dist/{chunk-DM6KG5YU.js → chunk-KECTTRCF.js} +59 -3
- package/dist/{chunk-4CW46BPC.js → chunk-KRE7FEMO.js} +446 -83
- package/dist/{cloud-ZXVJMV5Q.js → cloud-OZQOOVJO.js} +16 -4
- package/dist/index.js +8255 -2090
- package/dist/{oauth-JGWRORJM.js → oauth-52Q6XDJL.js} +3 -5
- package/package.json +3 -2
- package/dist/chunk-7NV5CYOF.js +0 -1064
- package/dist/chunk-IV5ZSOKF.js +0 -194
- package/dist/chunk-MHI6VJ75.js +0 -27
- package/dist/constants-PWT7TUWD.js +0 -27
- package/dist/db-DNK3TD5Y.js +0 -31
- package/dist/utils-7LT4QSYL.js +0 -27
|
@@ -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-
|
|
10
|
+
} from "./chunk-KECTTRCF.js";
|
|
17
11
|
|
|
18
12
|
// src/cloud.ts
|
|
19
|
-
import
|
|
13
|
+
import fs2 from "fs";
|
|
20
14
|
import os from "os";
|
|
21
|
-
import
|
|
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
|
|
230
|
+
return path2.join(os.homedir(), ".config", "agentlink", "credentials.json");
|
|
56
231
|
}
|
|
57
232
|
function loadCredentials() {
|
|
58
233
|
try {
|
|
59
|
-
return JSON.parse(
|
|
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
|
-
|
|
67
|
-
|
|
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-
|
|
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 (
|
|
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 =
|
|
115
|
-
if (
|
|
311
|
+
const envPath = path2.join(opts.cwd, ".env.local");
|
|
312
|
+
if (fs2.existsSync(envPath)) {
|
|
116
313
|
try {
|
|
117
|
-
let content =
|
|
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
|
-
|
|
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 =
|
|
159
|
-
if (
|
|
160
|
-
let content =
|
|
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
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
await
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
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
|
+
}
|
|
218
505
|
}
|
|
219
|
-
|
|
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
|
+
);
|
|
512
|
+
}
|
|
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
|
-
|
|
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`
|
|
@@ -586,6 +919,18 @@ async function updateAuthConfig(projectRef, config) {
|
|
|
586
919
|
throw new Error(`Failed to update auth config (${res.status}): ${body}`);
|
|
587
920
|
}
|
|
588
921
|
}
|
|
922
|
+
async function getAuthConfig(projectRef) {
|
|
923
|
+
try {
|
|
924
|
+
const res = await authenticatedFetch(
|
|
925
|
+
`https://api.supabase.com/v1/projects/${projectRef}/config/auth`,
|
|
926
|
+
{ method: "GET" }
|
|
927
|
+
);
|
|
928
|
+
if (!res.ok) return null;
|
|
929
|
+
return await res.json();
|
|
930
|
+
} catch {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
589
934
|
async function waitForProjectReady(ref, spinner, timeoutMs = 3e5) {
|
|
590
935
|
const start = Date.now();
|
|
591
936
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -749,9 +1094,23 @@ async function setSecrets(projectRef, secrets) {
|
|
|
749
1094
|
|
|
750
1095
|
export {
|
|
751
1096
|
selectYesNo,
|
|
1097
|
+
initLog,
|
|
1098
|
+
runCommand,
|
|
1099
|
+
assertGitClean,
|
|
1100
|
+
listChangedFiles,
|
|
1101
|
+
delay,
|
|
1102
|
+
checkCommand,
|
|
1103
|
+
skillDisplayName,
|
|
1104
|
+
generateDbPassword,
|
|
1105
|
+
slugifyProjectName,
|
|
1106
|
+
validateProjectName,
|
|
1107
|
+
ensureGitignorePattern,
|
|
752
1108
|
credentialsPath,
|
|
753
1109
|
loadCredentials,
|
|
754
1110
|
saveCredentials,
|
|
1111
|
+
getResendDefaults,
|
|
1112
|
+
setResendDefaults,
|
|
1113
|
+
clearResendDefaults,
|
|
755
1114
|
signOut,
|
|
756
1115
|
authenticatedFetch,
|
|
757
1116
|
ensureAccessToken,
|
|
@@ -764,6 +1123,9 @@ export {
|
|
|
764
1123
|
refreshOAuthEntry,
|
|
765
1124
|
probeTokenAuth,
|
|
766
1125
|
clearOrgCredential,
|
|
1126
|
+
listCachedOrgs,
|
|
1127
|
+
refreshCachedOrg,
|
|
1128
|
+
refreshAllCachedOrgs,
|
|
767
1129
|
createProject,
|
|
768
1130
|
getProject,
|
|
769
1131
|
deleteProject,
|
|
@@ -771,6 +1133,7 @@ export {
|
|
|
771
1133
|
runCloudSQL,
|
|
772
1134
|
updatePostgrestConfig,
|
|
773
1135
|
updateAuthConfig,
|
|
1136
|
+
getAuthConfig,
|
|
774
1137
|
waitForProjectReady,
|
|
775
1138
|
waitForDatabaseReady,
|
|
776
1139
|
fetchPoolerUrl,
|