agentlink-sh 0.26.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 +485 -0
- package/dist/chunk-4CW46BPC.js +787 -0
- package/dist/chunk-7NV5CYOF.js +1064 -0
- package/dist/chunk-DM6KG5YU.js +81 -0
- package/dist/chunk-IV5ZSOKF.js +194 -0
- package/dist/chunk-MHI6VJ75.js +27 -0
- package/dist/cloud-ZXVJMV5Q.js +78 -0
- package/dist/constants-PWT7TUWD.js +27 -0
- package/dist/db-DNK3TD5Y.js +31 -0
- package/dist/index.js +7524 -0
- package/dist/oauth-JGWRORJM.js +269 -0
- package/dist/utils-7LT4QSYL.js +27 -0
- package/package.json +35 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
delay,
|
|
4
|
+
runCommand
|
|
5
|
+
} from "./chunk-IV5ZSOKF.js";
|
|
6
|
+
import {
|
|
7
|
+
amber,
|
|
8
|
+
blue,
|
|
9
|
+
bold,
|
|
10
|
+
dim,
|
|
11
|
+
link,
|
|
12
|
+
red
|
|
13
|
+
} from "./chunk-MHI6VJ75.js";
|
|
14
|
+
import {
|
|
15
|
+
sb
|
|
16
|
+
} from "./chunk-DM6KG5YU.js";
|
|
17
|
+
|
|
18
|
+
// src/cloud.ts
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import os from "os";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { input, select as select2 } from "@inquirer/prompts";
|
|
23
|
+
|
|
24
|
+
// src/prompts.ts
|
|
25
|
+
import { select } from "@inquirer/prompts";
|
|
26
|
+
async function selectYesNo(opts) {
|
|
27
|
+
const def = opts.default ?? false;
|
|
28
|
+
const yesChoice = { name: opts.yesLabel ?? "Yes", value: true };
|
|
29
|
+
const noChoice = { name: opts.noLabel ?? "No", value: false };
|
|
30
|
+
const choices = def ? [yesChoice, noChoice] : [noChoice, yesChoice];
|
|
31
|
+
return await select({
|
|
32
|
+
message: opts.message,
|
|
33
|
+
// `theme` is typed as `unknown` here; cast at the call boundary so
|
|
34
|
+
// the helper signature stays simple.
|
|
35
|
+
theme: opts.theme,
|
|
36
|
+
default: def,
|
|
37
|
+
choices
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/cloud.ts
|
|
42
|
+
import open from "open";
|
|
43
|
+
var theme = {
|
|
44
|
+
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
45
|
+
style: {
|
|
46
|
+
answer: (text) => amber(text),
|
|
47
|
+
message: (text) => bold(text),
|
|
48
|
+
error: (text) => red(`> ${text}`),
|
|
49
|
+
help: (text) => dim(text),
|
|
50
|
+
highlight: (text) => blue(text),
|
|
51
|
+
key: (text) => blue(bold(`<${text}>`))
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function credentialsPath() {
|
|
55
|
+
return path.join(os.homedir(), ".config", "agentlink", "credentials.json");
|
|
56
|
+
}
|
|
57
|
+
function loadCredentials() {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(credentialsPath(), "utf-8"));
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function saveCredentials(creds) {
|
|
65
|
+
const filePath = credentialsPath();
|
|
66
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
67
|
+
fs.writeFileSync(filePath, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
|
|
68
|
+
}
|
|
69
|
+
var _authContext = {};
|
|
70
|
+
function isAuthError(err) {
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return /\(401\)|unauthorized|invalid.*token/i.test(msg);
|
|
73
|
+
}
|
|
74
|
+
async function refreshIfNeeded(entry) {
|
|
75
|
+
if (entry.expires_at > Date.now() / 1e3 + 60) {
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
if (!entry.refresh_token) return null;
|
|
79
|
+
try {
|
|
80
|
+
const { refreshOAuthToken } = await import("./oauth-JGWRORJM.js");
|
|
81
|
+
const tokens = await refreshOAuthToken(entry.refresh_token);
|
|
82
|
+
return {
|
|
83
|
+
...entry,
|
|
84
|
+
access_token: tokens.access_token,
|
|
85
|
+
refresh_token: tokens.refresh_token,
|
|
86
|
+
expires_at: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function signOut(opts = {}) {
|
|
93
|
+
const notes = [];
|
|
94
|
+
const filePath = credentialsPath();
|
|
95
|
+
let agentlinkStoreCleared = false;
|
|
96
|
+
if (fs.existsSync(filePath)) {
|
|
97
|
+
try {
|
|
98
|
+
const before = loadCredentials();
|
|
99
|
+
const hadAnything = !!before.oauth || Object.keys(before.oauth_by_org ?? {}).length > 0 || !!before.supabase_access_token;
|
|
100
|
+
saveCredentials({});
|
|
101
|
+
agentlinkStoreCleared = hadAnything;
|
|
102
|
+
notes.push(
|
|
103
|
+
hadAnything ? `Cleared credentials at ${filePath}` : `Credentials store at ${filePath} was already empty`
|
|
104
|
+
);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
notes.push(`Could not clear ${filePath}: ${err?.message ?? err}`);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
notes.push(`No credentials file at ${filePath} (already clean)`);
|
|
110
|
+
}
|
|
111
|
+
const envVarCleared = !!process.env.SUPABASE_ACCESS_TOKEN;
|
|
112
|
+
delete process.env.SUPABASE_ACCESS_TOKEN;
|
|
113
|
+
if (opts.cwd) {
|
|
114
|
+
const envPath = path.join(opts.cwd, ".env.local");
|
|
115
|
+
if (fs.existsSync(envPath)) {
|
|
116
|
+
try {
|
|
117
|
+
let content = fs.readFileSync(envPath, "utf-8");
|
|
118
|
+
if (/^SUPABASE_ACCESS_TOKEN=/m.test(content)) {
|
|
119
|
+
content = content.replace(/^SUPABASE_ACCESS_TOKEN=.*\n?/m, "");
|
|
120
|
+
fs.writeFileSync(envPath, content);
|
|
121
|
+
notes.push(`Stripped SUPABASE_ACCESS_TOKEN from ${envPath}`);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
notes.push(`Could not clean ${envPath}: ${err?.message ?? err}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let keychainCleared = false;
|
|
129
|
+
try {
|
|
130
|
+
await runCommand(`${sb()} logout --yes`);
|
|
131
|
+
keychainCleared = true;
|
|
132
|
+
notes.push("Cleared OS keychain entry via `supabase logout`");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const msg = err?.message ?? String(err);
|
|
135
|
+
if (/not\s+(?:logged\s+in|authenticated)|no\s+access\s+token|access token not provided/i.test(msg)) {
|
|
136
|
+
notes.push("OS keychain had no Supabase CLI entry (already clean)");
|
|
137
|
+
} else {
|
|
138
|
+
notes.push(`Could not run \`supabase logout\`: ${msg.split("\n")[0]}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
agentlinkStoreCleared,
|
|
143
|
+
envVarCleared,
|
|
144
|
+
keychainCleared,
|
|
145
|
+
credentialsPath: filePath,
|
|
146
|
+
notes
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function clearAccessToken(opts = {}) {
|
|
150
|
+
delete process.env.SUPABASE_ACCESS_TOKEN;
|
|
151
|
+
const creds = loadCredentials();
|
|
152
|
+
delete creds.oauth;
|
|
153
|
+
if (opts.orgId && creds.oauth_by_org) {
|
|
154
|
+
delete creds.oauth_by_org[opts.orgId];
|
|
155
|
+
}
|
|
156
|
+
saveCredentials(creds);
|
|
157
|
+
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");
|
|
161
|
+
if (/^SUPABASE_ACCESS_TOKEN=/m.test(content)) {
|
|
162
|
+
content = content.replace(/^SUPABASE_ACCESS_TOKEN=.*\n?/m, "");
|
|
163
|
+
fs.writeFileSync(envPath, content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function authenticatedFetch(url, init) {
|
|
169
|
+
const token = process.env.SUPABASE_ACCESS_TOKEN;
|
|
170
|
+
if (!token) {
|
|
171
|
+
throw new Error("SUPABASE_ACCESS_TOKEN is required");
|
|
172
|
+
}
|
|
173
|
+
const headers = new Headers(init?.headers);
|
|
174
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
175
|
+
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 });
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
async function authenticatedRunCommand(cmd, cwd) {
|
|
195
|
+
if (!process.env.SUPABASE_ACCESS_TOKEN) {
|
|
196
|
+
await ensureAccessToken({
|
|
197
|
+
nonInteractive: _authContext.nonInteractive,
|
|
198
|
+
projectDir: _authContext.projectDir,
|
|
199
|
+
orgId: _authContext.orgId,
|
|
200
|
+
orgSlug: _authContext.orgSlug
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
return await runCommand(cmd, cwd);
|
|
205
|
+
} 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);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function ensureAccessToken(opts = {}) {
|
|
223
|
+
const { orgId, orgSlug, nonInteractive, projectDir } = opts;
|
|
224
|
+
_authContext = { projectDir, nonInteractive, orgId, orgSlug };
|
|
225
|
+
if (process.env.SUPABASE_ACCESS_TOKEN) return;
|
|
226
|
+
const creds = loadCredentials();
|
|
227
|
+
if (orgId) {
|
|
228
|
+
const entry = creds.oauth_by_org?.[orgId];
|
|
229
|
+
if (entry) {
|
|
230
|
+
const fresh = await refreshIfNeeded(entry);
|
|
231
|
+
if (fresh) {
|
|
232
|
+
if (fresh !== entry) {
|
|
233
|
+
saveCredentials({
|
|
234
|
+
...creds,
|
|
235
|
+
oauth_by_org: { ...creds.oauth_by_org ?? {}, [orgId]: fresh }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (creds.oauth) {
|
|
244
|
+
const fresh = await refreshIfNeeded(creds.oauth);
|
|
245
|
+
if (fresh) {
|
|
246
|
+
if (fresh !== creds.oauth) {
|
|
247
|
+
saveCredentials({ ...loadCredentials(), oauth: fresh });
|
|
248
|
+
}
|
|
249
|
+
process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
|
|
250
|
+
try {
|
|
251
|
+
const derived = (await listOrganizations())[0];
|
|
252
|
+
if (derived) {
|
|
253
|
+
const updated = loadCredentials();
|
|
254
|
+
if (!updated.oauth_by_org?.[derived.id]) {
|
|
255
|
+
updated.oauth_by_org = {
|
|
256
|
+
...updated.oauth_by_org ?? {},
|
|
257
|
+
[derived.id]: { ...fresh, org_name: derived.name, org_slug: derived.slug }
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
delete updated.oauth;
|
|
261
|
+
saveCredentials(updated);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!orgId && creds.oauth_by_org) {
|
|
269
|
+
for (const [id, entry] of Object.entries(creds.oauth_by_org)) {
|
|
270
|
+
const fresh = await refreshIfNeeded(entry);
|
|
271
|
+
if (fresh) {
|
|
272
|
+
if (fresh !== entry) {
|
|
273
|
+
const latest = loadCredentials();
|
|
274
|
+
saveCredentials({
|
|
275
|
+
...latest,
|
|
276
|
+
oauth_by_org: { ...latest.oauth_by_org ?? {}, [id]: fresh }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
process.env.SUPABASE_ACCESS_TOKEN = fresh.access_token;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (creds.supabase_access_token) {
|
|
285
|
+
process.env.SUPABASE_ACCESS_TOKEN = creds.supabase_access_token;
|
|
286
|
+
const masked = creds.supabase_access_token.slice(0, 8) + "..." + creds.supabase_access_token.slice(-4);
|
|
287
|
+
console.log(` ${blue("\u25CF")} Existing access token found ${dim(`(source: ${credentialsPath()})`)}`);
|
|
288
|
+
console.log(` ${dim(masked)}`);
|
|
289
|
+
console.log();
|
|
290
|
+
if (!nonInteractive) {
|
|
291
|
+
const replace = await selectYesNo({
|
|
292
|
+
message: "Use stored token?",
|
|
293
|
+
default: true,
|
|
294
|
+
theme
|
|
295
|
+
});
|
|
296
|
+
if (replace) return;
|
|
297
|
+
delete process.env.SUPABASE_ACCESS_TOKEN;
|
|
298
|
+
} else {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (nonInteractive) {
|
|
303
|
+
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`."
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const method = await select2({
|
|
308
|
+
message: "How would you like to authenticate?",
|
|
309
|
+
theme,
|
|
310
|
+
choices: [
|
|
311
|
+
{ name: "Login with Supabase (opens browser)", value: "oauth" },
|
|
312
|
+
{ name: "Enter access token manually", value: "manual" }
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
if (method === "oauth") {
|
|
316
|
+
const { oauthLogin } = await import("./oauth-JGWRORJM.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();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const tokenUrl = "https://supabase.com/dashboard/account/tokens";
|
|
348
|
+
try {
|
|
349
|
+
await open(tokenUrl);
|
|
350
|
+
console.log(` ${blue("\u25CF")} Opened ${link(tokenUrl)}`);
|
|
351
|
+
} catch {
|
|
352
|
+
console.log(` ${amber("Could not open browser.")} Visit: ${link(tokenUrl)}`);
|
|
353
|
+
}
|
|
354
|
+
console.log();
|
|
355
|
+
const token = await input({
|
|
356
|
+
message: "Enter your Supabase access token:",
|
|
357
|
+
theme,
|
|
358
|
+
transformer: (value, { isFinal }) => {
|
|
359
|
+
if (isFinal) {
|
|
360
|
+
const t = value.trim();
|
|
361
|
+
return t.length > 12 ? t.slice(0, 8) + "\u25CF".repeat(t.length - 12) + t.slice(-4) : "\u25CF".repeat(t.length);
|
|
362
|
+
}
|
|
363
|
+
return "\u25CF".repeat(value.length);
|
|
364
|
+
},
|
|
365
|
+
validate: (val) => val.trim().length > 0 || "Token is required"
|
|
366
|
+
});
|
|
367
|
+
const trimmed = token.trim();
|
|
368
|
+
process.env.SUPABASE_ACCESS_TOKEN = trimmed;
|
|
369
|
+
saveCredentials({ ...loadCredentials(), supabase_access_token: trimmed });
|
|
370
|
+
console.log(` ${blue("\u25CF")} Token stored at ${dim(credentialsPath())}`);
|
|
371
|
+
console.log();
|
|
372
|
+
}
|
|
373
|
+
var REGIONS = [
|
|
374
|
+
{ id: "us-east-1", name: "US East (N. Virginia)" },
|
|
375
|
+
{ id: "us-east-2", name: "US East (Ohio)" },
|
|
376
|
+
{ id: "us-west-1", name: "US West (N. California)" },
|
|
377
|
+
{ id: "us-west-2", name: "US West (Oregon)" },
|
|
378
|
+
{ id: "ca-central-1", name: "Canada (Central)" },
|
|
379
|
+
{ id: "sa-east-1", name: "South America (S\xE3o Paulo)" },
|
|
380
|
+
{ id: "eu-west-1", name: "EU West (Ireland)" },
|
|
381
|
+
{ id: "eu-west-2", name: "EU West (London)" },
|
|
382
|
+
{ id: "eu-west-3", name: "EU West (Paris)" },
|
|
383
|
+
{ id: "eu-central-1", name: "EU Central (Frankfurt)" },
|
|
384
|
+
{ id: "eu-central-2", name: "EU Central (Zurich)" },
|
|
385
|
+
{ id: "eu-north-1", name: "EU North (Stockholm)" },
|
|
386
|
+
{ id: "ap-southeast-1", name: "Asia Pacific (Singapore)" },
|
|
387
|
+
{ id: "ap-southeast-2", name: "Asia Pacific (Sydney)" },
|
|
388
|
+
{ id: "ap-northeast-1", name: "Asia Pacific (Tokyo)" },
|
|
389
|
+
{ id: "ap-south-1", name: "Asia Pacific (Mumbai)" }
|
|
390
|
+
];
|
|
391
|
+
async function listOrganizations() {
|
|
392
|
+
const out = await authenticatedRunCommand(`${sb()} orgs list -o json`);
|
|
393
|
+
return JSON.parse(out);
|
|
394
|
+
}
|
|
395
|
+
async function listOrganizationsForToken(accessToken) {
|
|
396
|
+
try {
|
|
397
|
+
const res = await fetch("https://api.supabase.com/v1/organizations", {
|
|
398
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
399
|
+
});
|
|
400
|
+
if (!res.ok) return [];
|
|
401
|
+
return await res.json();
|
|
402
|
+
} catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function listRegions() {
|
|
407
|
+
return REGIONS;
|
|
408
|
+
}
|
|
409
|
+
function detectClosestRegion() {
|
|
410
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
411
|
+
const cityMap = {
|
|
412
|
+
"America/New_York": "us-east-1",
|
|
413
|
+
"America/Toronto": "us-east-1",
|
|
414
|
+
"America/Detroit": "us-east-1",
|
|
415
|
+
"America/Chicago": "us-east-2",
|
|
416
|
+
"America/Denver": "us-west-2",
|
|
417
|
+
"America/Phoenix": "us-west-2",
|
|
418
|
+
"America/Los_Angeles": "us-west-1",
|
|
419
|
+
"America/Vancouver": "us-west-1",
|
|
420
|
+
"America/Edmonton": "ca-central-1",
|
|
421
|
+
"America/Winnipeg": "ca-central-1",
|
|
422
|
+
"America/Sao_Paulo": "sa-east-1",
|
|
423
|
+
"America/Argentina/Buenos_Aires": "sa-east-1",
|
|
424
|
+
"America/Bogota": "sa-east-1",
|
|
425
|
+
"America/Lima": "sa-east-1",
|
|
426
|
+
"America/Santiago": "sa-east-1",
|
|
427
|
+
"America/Mexico_City": "us-east-1",
|
|
428
|
+
"Europe/London": "eu-west-2",
|
|
429
|
+
"Europe/Dublin": "eu-west-1",
|
|
430
|
+
"Europe/Paris": "eu-west-3",
|
|
431
|
+
"Europe/Madrid": "eu-west-3",
|
|
432
|
+
"Europe/Rome": "eu-west-3",
|
|
433
|
+
"Europe/Brussels": "eu-west-3",
|
|
434
|
+
"Europe/Berlin": "eu-central-1",
|
|
435
|
+
"Europe/Amsterdam": "eu-central-1",
|
|
436
|
+
"Europe/Vienna": "eu-central-1",
|
|
437
|
+
"Europe/Zurich": "eu-central-2",
|
|
438
|
+
"Europe/Stockholm": "eu-north-1",
|
|
439
|
+
"Europe/Helsinki": "eu-north-1",
|
|
440
|
+
"Europe/Oslo": "eu-north-1",
|
|
441
|
+
"Europe/Warsaw": "eu-central-1",
|
|
442
|
+
"Asia/Tokyo": "ap-northeast-1",
|
|
443
|
+
"Asia/Seoul": "ap-northeast-1",
|
|
444
|
+
"Asia/Singapore": "ap-southeast-1",
|
|
445
|
+
"Asia/Kuala_Lumpur": "ap-southeast-1",
|
|
446
|
+
"Asia/Jakarta": "ap-southeast-1",
|
|
447
|
+
"Asia/Bangkok": "ap-southeast-1",
|
|
448
|
+
"Asia/Kolkata": "ap-south-1",
|
|
449
|
+
"Asia/Mumbai": "ap-south-1",
|
|
450
|
+
"Asia/Dubai": "ap-south-1",
|
|
451
|
+
"Australia/Sydney": "ap-southeast-2",
|
|
452
|
+
"Australia/Melbourne": "ap-southeast-2",
|
|
453
|
+
"Pacific/Auckland": "ap-southeast-2"
|
|
454
|
+
};
|
|
455
|
+
if (cityMap[tz]) return cityMap[tz];
|
|
456
|
+
if (tz.startsWith("America/")) return "us-east-1";
|
|
457
|
+
if (tz.startsWith("US/")) return "us-east-1";
|
|
458
|
+
if (tz.startsWith("Canada/")) return "ca-central-1";
|
|
459
|
+
if (tz.startsWith("Europe/")) return "eu-central-1";
|
|
460
|
+
if (tz.startsWith("Asia/")) return "ap-southeast-1";
|
|
461
|
+
if (tz.startsWith("Australia/") || tz.startsWith("Pacific/")) return "ap-southeast-2";
|
|
462
|
+
if (tz.startsWith("Africa/")) return "eu-west-3";
|
|
463
|
+
return "us-east-1";
|
|
464
|
+
}
|
|
465
|
+
async function listProjects() {
|
|
466
|
+
const out = await authenticatedRunCommand(`${sb()} projects list -o json`);
|
|
467
|
+
return JSON.parse(out) ?? [];
|
|
468
|
+
}
|
|
469
|
+
async function listProjectsForToken(accessToken) {
|
|
470
|
+
try {
|
|
471
|
+
const res = await fetch("https://api.supabase.com/v1/projects", {
|
|
472
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
473
|
+
});
|
|
474
|
+
if (!res.ok) return [];
|
|
475
|
+
return await res.json();
|
|
476
|
+
} catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function refreshOAuthEntry(entry) {
|
|
481
|
+
return refreshIfNeeded(entry);
|
|
482
|
+
}
|
|
483
|
+
async function probeTokenAuth(accessToken) {
|
|
484
|
+
try {
|
|
485
|
+
const res = await fetch("https://api.supabase.com/v1/projects", {
|
|
486
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
487
|
+
});
|
|
488
|
+
return res.status !== 401 && res.status !== 403;
|
|
489
|
+
} catch {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function clearOrgCredential(orgId) {
|
|
494
|
+
const creds = loadCredentials();
|
|
495
|
+
if (creds.oauth_by_org?.[orgId]) {
|
|
496
|
+
delete creds.oauth_by_org[orgId];
|
|
497
|
+
saveCredentials(creds);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function createProject(opts) {
|
|
501
|
+
const out = await authenticatedRunCommand(
|
|
502
|
+
`${sb()} projects create ${JSON.stringify(opts.name)} --org-id ${opts.orgId} --region ${opts.region} --db-password ${opts.dbPass} -o json`
|
|
503
|
+
);
|
|
504
|
+
return JSON.parse(out);
|
|
505
|
+
}
|
|
506
|
+
async function getProject(ref) {
|
|
507
|
+
const out = await authenticatedRunCommand(`${sb()} projects list -o json`);
|
|
508
|
+
const projects = JSON.parse(out);
|
|
509
|
+
const project = projects.find((p) => p.id === ref);
|
|
510
|
+
if (!project) {
|
|
511
|
+
throw new Error(`Project ${ref} not found`);
|
|
512
|
+
}
|
|
513
|
+
return project;
|
|
514
|
+
}
|
|
515
|
+
async function deleteProject(ref) {
|
|
516
|
+
const res = await authenticatedFetch(
|
|
517
|
+
`https://api.supabase.com/v1/projects/${ref}`,
|
|
518
|
+
{ method: "DELETE" }
|
|
519
|
+
);
|
|
520
|
+
if (res.status === 404) return;
|
|
521
|
+
if (!res.ok) {
|
|
522
|
+
const body = await res.text();
|
|
523
|
+
throw new Error(`Failed to delete project ${ref} (${res.status}): ${body}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function getApiKeys(ref) {
|
|
527
|
+
const res = await authenticatedFetch(
|
|
528
|
+
`https://api.supabase.com/v1/projects/${ref}/api-keys?reveal=true`
|
|
529
|
+
);
|
|
530
|
+
if (!res.ok) {
|
|
531
|
+
const body = await res.text();
|
|
532
|
+
throw new Error(`Failed to fetch API keys (${res.status}): ${body}`);
|
|
533
|
+
}
|
|
534
|
+
const keys = await res.json();
|
|
535
|
+
const publishable = keys.find((k) => k.type === "publishable");
|
|
536
|
+
const secret = keys.find((k) => k.type === "secret");
|
|
537
|
+
if (!publishable?.api_key || !secret?.api_key) {
|
|
538
|
+
throw new Error("Could not find publishable/secret API keys for project");
|
|
539
|
+
}
|
|
540
|
+
return { publishableKey: publishable.api_key, secretKey: secret.api_key };
|
|
541
|
+
}
|
|
542
|
+
async function runCloudSQL(sql, projectRef) {
|
|
543
|
+
const res = await authenticatedFetch(
|
|
544
|
+
`https://api.supabase.com/v1/projects/${projectRef}/database/query`,
|
|
545
|
+
{
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: { "Content-Type": "application/json" },
|
|
548
|
+
body: JSON.stringify({ query: sql })
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
const body = await res.text();
|
|
553
|
+
throw new Error(`Cloud SQL query failed (${res.status}): ${body}`);
|
|
554
|
+
}
|
|
555
|
+
const rows = await res.json();
|
|
556
|
+
if (!Array.isArray(rows) || rows.length === 0) return "";
|
|
557
|
+
const firstValue = Object.values(rows[0])[0];
|
|
558
|
+
if (typeof firstValue === "object" && firstValue !== null) return JSON.stringify(firstValue);
|
|
559
|
+
return String(firstValue ?? "");
|
|
560
|
+
}
|
|
561
|
+
async function updatePostgrestConfig(projectRef, config) {
|
|
562
|
+
const res = await authenticatedFetch(
|
|
563
|
+
`https://api.supabase.com/v1/projects/${projectRef}/postgrest`,
|
|
564
|
+
{
|
|
565
|
+
method: "PATCH",
|
|
566
|
+
headers: { "Content-Type": "application/json" },
|
|
567
|
+
body: JSON.stringify(config)
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
if (!res.ok) {
|
|
571
|
+
const body = await res.text();
|
|
572
|
+
throw new Error(`Failed to update PostgREST config (${res.status}): ${body}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function updateAuthConfig(projectRef, config) {
|
|
576
|
+
const res = await authenticatedFetch(
|
|
577
|
+
`https://api.supabase.com/v1/projects/${projectRef}/config/auth`,
|
|
578
|
+
{
|
|
579
|
+
method: "PATCH",
|
|
580
|
+
headers: { "Content-Type": "application/json" },
|
|
581
|
+
body: JSON.stringify(config)
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
if (!res.ok) {
|
|
585
|
+
const body = await res.text();
|
|
586
|
+
throw new Error(`Failed to update auth config (${res.status}): ${body}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function waitForProjectReady(ref, spinner, timeoutMs = 3e5) {
|
|
590
|
+
const start = Date.now();
|
|
591
|
+
while (Date.now() - start < timeoutMs) {
|
|
592
|
+
try {
|
|
593
|
+
const out = await authenticatedRunCommand(`${sb()} projects list -o json`);
|
|
594
|
+
const projects = JSON.parse(out);
|
|
595
|
+
const project = projects.find((p) => p.id === ref);
|
|
596
|
+
if (project?.status === "ACTIVE_HEALTHY") {
|
|
597
|
+
return project;
|
|
598
|
+
}
|
|
599
|
+
if (spinner && project) {
|
|
600
|
+
spinner.text = `Creating Supabase cloud project \u2014 status: ${project.status}`;
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
await delay(5e3);
|
|
605
|
+
}
|
|
606
|
+
throw new Error(
|
|
607
|
+
`Project ${ref} did not become ready within ${timeoutMs / 1e3}s.
|
|
608
|
+
Check status at: https://supabase.com/dashboard/project/${ref}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
async function waitForDatabaseReady(projectRef, spinner, timeoutMs = 18e4) {
|
|
612
|
+
const start = Date.now();
|
|
613
|
+
let attempt = 0;
|
|
614
|
+
while (Date.now() - start < timeoutMs) {
|
|
615
|
+
attempt++;
|
|
616
|
+
try {
|
|
617
|
+
await runCloudSQL("SELECT 1", projectRef);
|
|
618
|
+
return;
|
|
619
|
+
} catch {
|
|
620
|
+
if (spinner) {
|
|
621
|
+
spinner.text = `Waiting for Postgres to accept connections (attempt ${attempt})`;
|
|
622
|
+
}
|
|
623
|
+
await delay(5e3);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Postgres on project ${projectRef} did not accept connections within ${timeoutMs / 1e3}s.
|
|
628
|
+
Check status at: https://supabase.com/dashboard/project/${projectRef}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
async function fetchPoolerUrl(projectRef) {
|
|
632
|
+
const res = await authenticatedFetch(
|
|
633
|
+
`https://api.supabase.com/v1/projects/${projectRef}/config/database/pooler`
|
|
634
|
+
);
|
|
635
|
+
if (!res.ok) {
|
|
636
|
+
const body = await res.text();
|
|
637
|
+
throw new Error(`Failed to fetch pooler config (${res.status}): ${body}`);
|
|
638
|
+
}
|
|
639
|
+
const entries = await res.json();
|
|
640
|
+
const pooler = entries.find(
|
|
641
|
+
(e) => e.database_type === "PRIMARY" && e.pool_mode === "transaction"
|
|
642
|
+
) ?? entries.find(
|
|
643
|
+
(e) => e.database_type === "PRIMARY"
|
|
644
|
+
) ?? entries[0];
|
|
645
|
+
if (!pooler) {
|
|
646
|
+
throw new Error("No pooler configuration found for project");
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
host: pooler.db_host,
|
|
650
|
+
port: pooler.db_port,
|
|
651
|
+
user: pooler.db_user,
|
|
652
|
+
dbName: pooler.db_name,
|
|
653
|
+
connectionString: pooler.connection_string
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async function resolvePoolerDbUrl(projectRef, password) {
|
|
657
|
+
const pooler = await fetchPoolerUrl(projectRef);
|
|
658
|
+
return `postgresql://${pooler.user}:${encodeURIComponent(password)}@${pooler.host}:${pooler.port}/${pooler.dbName}`;
|
|
659
|
+
}
|
|
660
|
+
async function resolvePoolerDbUrlWithRetry(projectRef, password, attempts = 6, delayMs = 5e3) {
|
|
661
|
+
let lastErr;
|
|
662
|
+
for (let i = 0; i < attempts; i++) {
|
|
663
|
+
try {
|
|
664
|
+
return await resolvePoolerDbUrl(projectRef, password);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
lastErr = err;
|
|
667
|
+
if (i < attempts - 1) await delay(delayMs);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
throw lastErr instanceof Error ? lastErr : new Error(`Failed to resolve pooler URL after ${attempts} attempts`);
|
|
671
|
+
}
|
|
672
|
+
function saveProjectCredential(projectRef, dbPassword) {
|
|
673
|
+
const creds = loadCredentials();
|
|
674
|
+
if (!creds.project_credentials) creds.project_credentials = {};
|
|
675
|
+
const existing = creds.project_credentials[projectRef];
|
|
676
|
+
creds.project_credentials[projectRef] = {
|
|
677
|
+
...existing ?? {},
|
|
678
|
+
db_password: dbPassword
|
|
679
|
+
};
|
|
680
|
+
saveCredentials(creds);
|
|
681
|
+
}
|
|
682
|
+
function loadProjectCredential(projectRef) {
|
|
683
|
+
const creds = loadCredentials();
|
|
684
|
+
return creds.project_credentials?.[projectRef]?.db_password;
|
|
685
|
+
}
|
|
686
|
+
function saveProjectSecretKey(projectRef, secretKey) {
|
|
687
|
+
if (!projectRef || !secretKey) return;
|
|
688
|
+
const creds = loadCredentials();
|
|
689
|
+
if (!creds.project_credentials) creds.project_credentials = {};
|
|
690
|
+
const existing = creds.project_credentials[projectRef];
|
|
691
|
+
creds.project_credentials[projectRef] = {
|
|
692
|
+
db_password: existing?.db_password ?? "",
|
|
693
|
+
...existing,
|
|
694
|
+
secret_key: secretKey
|
|
695
|
+
};
|
|
696
|
+
saveCredentials(creds);
|
|
697
|
+
}
|
|
698
|
+
function loadProjectSecretKey(projectRef) {
|
|
699
|
+
const creds = loadCredentials();
|
|
700
|
+
return creds.project_credentials?.[projectRef]?.secret_key;
|
|
701
|
+
}
|
|
702
|
+
function removeProjectCredential(projectRef) {
|
|
703
|
+
const creds = loadCredentials();
|
|
704
|
+
if (creds.project_credentials?.[projectRef]) {
|
|
705
|
+
delete creds.project_credentials[projectRef];
|
|
706
|
+
saveCredentials(creds);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function getProjectOrg(projectRef) {
|
|
710
|
+
const res = await authenticatedFetch(
|
|
711
|
+
`https://api.supabase.com/v1/projects/${projectRef}`
|
|
712
|
+
);
|
|
713
|
+
if (!res.ok) {
|
|
714
|
+
const body = await res.text();
|
|
715
|
+
throw new Error(`Failed to fetch project details (${res.status}): ${body}`);
|
|
716
|
+
}
|
|
717
|
+
const project = await res.json();
|
|
718
|
+
if (!project.organization_id) {
|
|
719
|
+
throw new Error(`Project ${projectRef} has no organization_id in its API response.`);
|
|
720
|
+
}
|
|
721
|
+
return project.organization_id;
|
|
722
|
+
}
|
|
723
|
+
async function listSecrets(projectRef) {
|
|
724
|
+
const res = await authenticatedFetch(
|
|
725
|
+
`https://api.supabase.com/v1/projects/${projectRef}/secrets`
|
|
726
|
+
);
|
|
727
|
+
if (!res.ok) {
|
|
728
|
+
const body = await res.text();
|
|
729
|
+
throw new Error(`Failed to list secrets (${res.status}): ${body}`);
|
|
730
|
+
}
|
|
731
|
+
const secrets = await res.json();
|
|
732
|
+
return secrets.map((s) => s.name);
|
|
733
|
+
}
|
|
734
|
+
async function setSecrets(projectRef, secrets) {
|
|
735
|
+
const body = Object.entries(secrets).map(([name, value]) => ({ name, value }));
|
|
736
|
+
const res = await authenticatedFetch(
|
|
737
|
+
`https://api.supabase.com/v1/projects/${projectRef}/secrets`,
|
|
738
|
+
{
|
|
739
|
+
method: "POST",
|
|
740
|
+
headers: { "Content-Type": "application/json" },
|
|
741
|
+
body: JSON.stringify(body)
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
const respBody = await res.text();
|
|
746
|
+
throw new Error(`Failed to set secrets (${res.status}): ${respBody}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export {
|
|
751
|
+
selectYesNo,
|
|
752
|
+
credentialsPath,
|
|
753
|
+
loadCredentials,
|
|
754
|
+
saveCredentials,
|
|
755
|
+
signOut,
|
|
756
|
+
authenticatedFetch,
|
|
757
|
+
ensureAccessToken,
|
|
758
|
+
listOrganizations,
|
|
759
|
+
listOrganizationsForToken,
|
|
760
|
+
listRegions,
|
|
761
|
+
detectClosestRegion,
|
|
762
|
+
listProjects,
|
|
763
|
+
listProjectsForToken,
|
|
764
|
+
refreshOAuthEntry,
|
|
765
|
+
probeTokenAuth,
|
|
766
|
+
clearOrgCredential,
|
|
767
|
+
createProject,
|
|
768
|
+
getProject,
|
|
769
|
+
deleteProject,
|
|
770
|
+
getApiKeys,
|
|
771
|
+
runCloudSQL,
|
|
772
|
+
updatePostgrestConfig,
|
|
773
|
+
updateAuthConfig,
|
|
774
|
+
waitForProjectReady,
|
|
775
|
+
waitForDatabaseReady,
|
|
776
|
+
fetchPoolerUrl,
|
|
777
|
+
resolvePoolerDbUrl,
|
|
778
|
+
resolvePoolerDbUrlWithRetry,
|
|
779
|
+
saveProjectCredential,
|
|
780
|
+
loadProjectCredential,
|
|
781
|
+
saveProjectSecretKey,
|
|
782
|
+
loadProjectSecretKey,
|
|
783
|
+
removeProjectCredential,
|
|
784
|
+
getProjectOrg,
|
|
785
|
+
listSecrets,
|
|
786
|
+
setSecrets
|
|
787
|
+
};
|