@agentlink.sh/cli 0.11.1 → 0.12.1
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.
|
@@ -181,7 +181,7 @@ async function ensureAccessToken(nonInteractive, projectDir) {
|
|
|
181
181
|
}
|
|
182
182
|
if (creds.oauth.refresh_token) {
|
|
183
183
|
try {
|
|
184
|
-
const { refreshOAuthToken } = await import("./oauth-
|
|
184
|
+
const { refreshOAuthToken } = await import("./oauth-BGBLVE52.js");
|
|
185
185
|
const tokens = await refreshOAuthToken(creds.oauth.refresh_token);
|
|
186
186
|
const updated = {
|
|
187
187
|
...creds,
|
|
@@ -230,7 +230,7 @@ async function ensureAccessToken(nonInteractive, projectDir) {
|
|
|
230
230
|
]
|
|
231
231
|
});
|
|
232
232
|
if (method === "oauth") {
|
|
233
|
-
const { oauthLogin } = await import("./oauth-
|
|
233
|
+
const { oauthLogin } = await import("./oauth-BGBLVE52.js");
|
|
234
234
|
const tokens = await oauthLogin();
|
|
235
235
|
process.env.SUPABASE_ACCESS_TOKEN = tokens.access_token;
|
|
236
236
|
saveCredentials({
|
|
@@ -505,6 +505,42 @@ async function waitForProjectReady(ref, spinner, timeoutMs = 3e5) {
|
|
|
505
505
|
Check status at: https://supabase.com/dashboard/project/${ref}`
|
|
506
506
|
);
|
|
507
507
|
}
|
|
508
|
+
async function fetchPoolerUrl(projectRef) {
|
|
509
|
+
const token = process.env.SUPABASE_ACCESS_TOKEN;
|
|
510
|
+
if (!token) {
|
|
511
|
+
throw new Error("SUPABASE_ACCESS_TOKEN is required to fetch pooler config");
|
|
512
|
+
}
|
|
513
|
+
const res = await fetch(
|
|
514
|
+
`https://api.supabase.com/v1/projects/${projectRef}/config/database/pooler`,
|
|
515
|
+
{
|
|
516
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
if (!res.ok) {
|
|
520
|
+
const body = await res.text();
|
|
521
|
+
throw new Error(`Failed to fetch pooler config (${res.status}): ${body}`);
|
|
522
|
+
}
|
|
523
|
+
const entries = await res.json();
|
|
524
|
+
const pooler = entries.find(
|
|
525
|
+
(e) => e.database_type === "PRIMARY" && e.pool_mode === "transaction"
|
|
526
|
+
) ?? entries.find(
|
|
527
|
+
(e) => e.database_type === "PRIMARY"
|
|
528
|
+
) ?? entries[0];
|
|
529
|
+
if (!pooler) {
|
|
530
|
+
throw new Error("No pooler configuration found for project");
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
host: pooler.db_host,
|
|
534
|
+
port: pooler.db_port,
|
|
535
|
+
user: pooler.db_user,
|
|
536
|
+
dbName: pooler.db_name,
|
|
537
|
+
connectionString: pooler.connection_string
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function resolvePoolerDbUrl(projectRef, password) {
|
|
541
|
+
const pooler = await fetchPoolerUrl(projectRef);
|
|
542
|
+
return `postgresql://${pooler.user}:${encodeURIComponent(password)}@${pooler.host}:${pooler.port}/${pooler.dbName}`;
|
|
543
|
+
}
|
|
508
544
|
function saveProjectCredential(projectRef, dbPassword) {
|
|
509
545
|
const creds = loadCredentials();
|
|
510
546
|
if (!creds.project_credentials) {
|
|
@@ -584,6 +620,8 @@ export {
|
|
|
584
620
|
updatePostgrestConfig,
|
|
585
621
|
updateAuthConfig,
|
|
586
622
|
waitForProjectReady,
|
|
623
|
+
fetchPoolerUrl,
|
|
624
|
+
resolvePoolerDbUrl,
|
|
587
625
|
saveProjectCredential,
|
|
588
626
|
loadProjectCredential,
|
|
589
627
|
resolveDbUrlForEnv,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
credentialsPath,
|
|
5
5
|
detectClosestRegion,
|
|
6
6
|
ensureAccessToken,
|
|
7
|
+
fetchPoolerUrl,
|
|
7
8
|
getApiKeys,
|
|
8
9
|
getProject,
|
|
9
10
|
listOrganizations,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
loadCredentials,
|
|
14
15
|
loadProjectCredential,
|
|
15
16
|
resolveDbUrlForEnv,
|
|
17
|
+
resolvePoolerDbUrl,
|
|
16
18
|
runCloudSQL,
|
|
17
19
|
saveCredentials,
|
|
18
20
|
saveProjectCredential,
|
|
@@ -20,13 +22,14 @@ import {
|
|
|
20
22
|
updateAuthConfig,
|
|
21
23
|
updatePostgrestConfig,
|
|
22
24
|
waitForProjectReady
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-3TPH54EQ.js";
|
|
24
26
|
import "./chunk-G3HVUCWY.js";
|
|
25
27
|
export {
|
|
26
28
|
createProject,
|
|
27
29
|
credentialsPath,
|
|
28
30
|
detectClosestRegion,
|
|
29
31
|
ensureAccessToken,
|
|
32
|
+
fetchPoolerUrl,
|
|
30
33
|
getApiKeys,
|
|
31
34
|
getProject,
|
|
32
35
|
listOrganizations,
|
|
@@ -36,6 +39,7 @@ export {
|
|
|
36
39
|
loadCredentials,
|
|
37
40
|
loadProjectCredential,
|
|
38
41
|
resolveDbUrlForEnv,
|
|
42
|
+
resolvePoolerDbUrl,
|
|
39
43
|
runCloudSQL,
|
|
40
44
|
saveCredentials,
|
|
41
45
|
saveProjectCredential,
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
loadCredentials,
|
|
17
17
|
loadProjectCredential,
|
|
18
18
|
resolveDbUrlForEnv,
|
|
19
|
+
resolvePoolerDbUrl,
|
|
19
20
|
runCloudSQL,
|
|
20
21
|
runCommand,
|
|
21
22
|
saveCredentials,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
updatePostgrestConfig,
|
|
27
28
|
validateProjectName,
|
|
28
29
|
waitForProjectReady
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-3TPH54EQ.js";
|
|
30
31
|
import {
|
|
31
32
|
SKILLS_VERSION,
|
|
32
33
|
SUPPORTED_SUPABASE_CLI,
|
|
@@ -1530,6 +1531,7 @@ async function check(envFlag) {
|
|
|
1530
1531
|
import fs5 from "fs";
|
|
1531
1532
|
import os from "os";
|
|
1532
1533
|
import path5 from "path";
|
|
1534
|
+
import { input } from "@inquirer/prompts";
|
|
1533
1535
|
function stripQuotes(val) {
|
|
1534
1536
|
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
1535
1537
|
return val.slice(1, -1);
|
|
@@ -1734,12 +1736,130 @@ Migration created: supabase/migrations/${filename}`);
|
|
|
1734
1736
|
fs5.rmSync(tmpDir, { recursive: true, force: true });
|
|
1735
1737
|
}
|
|
1736
1738
|
}
|
|
1739
|
+
async function dbRebuild(options) {
|
|
1740
|
+
const cwd = options.cwd;
|
|
1741
|
+
const migrationsDir = path5.join(cwd, "supabase", "migrations");
|
|
1742
|
+
if (fs5.existsSync(migrationsDir)) {
|
|
1743
|
+
const files = fs5.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql"));
|
|
1744
|
+
for (const file of files) {
|
|
1745
|
+
fs5.unlinkSync(path5.join(migrationsDir, file));
|
|
1746
|
+
}
|
|
1747
|
+
console.log(`Deleted ${files.length} migration file(s)`);
|
|
1748
|
+
}
|
|
1749
|
+
const progressPath2 = path5.join(cwd, ".agentlink-progress.json");
|
|
1750
|
+
if (fs5.existsSync(progressPath2)) {
|
|
1751
|
+
fs5.unlinkSync(progressPath2);
|
|
1752
|
+
console.log("Cleared scaffold progress");
|
|
1753
|
+
}
|
|
1754
|
+
console.log("\nRe-applying schemas...");
|
|
1755
|
+
await dbApply({ cwd, dbUrl: options.dbUrl, env: options.env, skipTypes: true });
|
|
1756
|
+
console.log("\nRegenerating migration...");
|
|
1757
|
+
await dbMigrate("agentlink_setup", { cwd, dbUrl: options.dbUrl, env: options.env });
|
|
1758
|
+
console.log("\nGenerating types...");
|
|
1759
|
+
try {
|
|
1760
|
+
await dbTypes({ cwd, dbUrl: options.dbUrl, env: options.env });
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
console.warn(`Warning: type generation failed \u2014 ${err.message}`);
|
|
1763
|
+
}
|
|
1764
|
+
console.log("\nDone. Database rebuilt from schema files.");
|
|
1765
|
+
}
|
|
1766
|
+
async function dbUrlCheck(options) {
|
|
1767
|
+
const mode = await resolveDbMode(options.cwd, options.dbUrl, options.env);
|
|
1768
|
+
if (mode.kind !== "cloud" || !mode.projectRef) {
|
|
1769
|
+
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
1770
|
+
console.log(dbUrl);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
await ensureAccessToken(true, options.cwd);
|
|
1774
|
+
const password = loadProjectCredential(mode.projectRef);
|
|
1775
|
+
if (!password) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`No stored password for project ${mode.projectRef}.
|
|
1778
|
+
Run: agentlink env relink dev`
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
const poolerUrl = await resolvePoolerDbUrl(mode.projectRef, password);
|
|
1782
|
+
console.log(`Pooler URL: ${poolerUrl}`);
|
|
1783
|
+
const currentUrl = readEnvValue(options.cwd, "SUPABASE_DB_URL");
|
|
1784
|
+
if (currentUrl === poolerUrl) {
|
|
1785
|
+
console.log("\u2714 .env.local is up to date");
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (currentUrl) {
|
|
1789
|
+
console.log(`
|
|
1790
|
+
Current: ${currentUrl}`);
|
|
1791
|
+
console.log(`Expected: ${poolerUrl}`);
|
|
1792
|
+
}
|
|
1793
|
+
if (options.fix) {
|
|
1794
|
+
const envPath = path5.join(options.cwd, ".env.local");
|
|
1795
|
+
if (fs5.existsSync(envPath)) {
|
|
1796
|
+
let content = fs5.readFileSync(envPath, "utf-8");
|
|
1797
|
+
if (/^SUPABASE_DB_URL=/m.test(content)) {
|
|
1798
|
+
content = content.replace(/^SUPABASE_DB_URL=.*$/m, `SUPABASE_DB_URL=${poolerUrl}`);
|
|
1799
|
+
} else {
|
|
1800
|
+
content = content.trimEnd() + `
|
|
1801
|
+
SUPABASE_DB_URL=${poolerUrl}
|
|
1802
|
+
`;
|
|
1803
|
+
}
|
|
1804
|
+
fs5.writeFileSync(envPath, content);
|
|
1805
|
+
console.log("\n\u2714 .env.local updated with correct pooler URL");
|
|
1806
|
+
}
|
|
1807
|
+
} else if (currentUrl !== poolerUrl) {
|
|
1808
|
+
console.log("\nRun with --fix to update .env.local");
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
var passwordTheme = {
|
|
1812
|
+
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
1813
|
+
style: {
|
|
1814
|
+
answer: (text) => amber(text),
|
|
1815
|
+
message: (text) => bold(text),
|
|
1816
|
+
help: (text) => dim(text)
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
async function dbPassword(cwd, value) {
|
|
1820
|
+
const manifest = readManifest(cwd);
|
|
1821
|
+
if (!manifest) {
|
|
1822
|
+
throw new Error("No agentlink.json found. Run `agentlink` to scaffold a project first.");
|
|
1823
|
+
}
|
|
1824
|
+
const defaultEnv = manifest.cloud?.default;
|
|
1825
|
+
if (!defaultEnv || defaultEnv === "local" || !manifest.cloud?.environments?.[defaultEnv]) {
|
|
1826
|
+
throw new Error("No cloud environment configured. This command is for cloud projects.");
|
|
1827
|
+
}
|
|
1828
|
+
const env2 = manifest.cloud.environments[defaultEnv];
|
|
1829
|
+
const projectRef = env2.projectRef;
|
|
1830
|
+
if (value) {
|
|
1831
|
+
saveProjectCredential(projectRef, value);
|
|
1832
|
+
console.log(`Password saved for project ${dim(projectRef)}`);
|
|
1833
|
+
} else {
|
|
1834
|
+
const current = loadProjectCredential(projectRef);
|
|
1835
|
+
if (current) {
|
|
1836
|
+
const masked = current.length > 8 ? current.slice(0, 4) + "\u25CF".repeat(current.length - 8) + current.slice(-4) : "\u25CF".repeat(current.length);
|
|
1837
|
+
console.log(`Current password: ${dim(masked)} (project: ${projectRef})`);
|
|
1838
|
+
console.log();
|
|
1839
|
+
}
|
|
1840
|
+
const resetUrl = `https://supabase.com/dashboard/project/${projectRef}/settings/database`;
|
|
1841
|
+
console.log(` ${dim("Reset your password at:")}`);
|
|
1842
|
+
console.log(` ${dim(resetUrl)}`);
|
|
1843
|
+
console.log();
|
|
1844
|
+
const newPassword = await input({
|
|
1845
|
+
message: "Database password:",
|
|
1846
|
+
theme: passwordTheme,
|
|
1847
|
+
transformer: (v, { isFinal }) => {
|
|
1848
|
+
if (isFinal) return "\u25CF".repeat(Math.min(v.length, 8));
|
|
1849
|
+
return "\u25CF".repeat(v.length);
|
|
1850
|
+
},
|
|
1851
|
+
validate: (val) => val.trim().length > 0 || "Password is required"
|
|
1852
|
+
});
|
|
1853
|
+
saveProjectCredential(projectRef, newPassword.trim());
|
|
1854
|
+
console.log(`Password saved for project ${dim(projectRef)}`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1737
1857
|
|
|
1738
1858
|
// src/deploy.ts
|
|
1739
1859
|
import fs6 from "fs";
|
|
1740
1860
|
import os2 from "os";
|
|
1741
1861
|
import path6 from "path";
|
|
1742
|
-
import { confirm, input } from "@inquirer/prompts";
|
|
1862
|
+
import { confirm, input as input2 } from "@inquirer/prompts";
|
|
1743
1863
|
|
|
1744
1864
|
// src/migration-analysis.ts
|
|
1745
1865
|
var PATTERNS = [
|
|
@@ -1946,8 +2066,8 @@ async function resolveEnvironments(cwd, opts) {
|
|
|
1946
2066
|
}
|
|
1947
2067
|
await ensureAccessToken(opts.ci, cwd);
|
|
1948
2068
|
const targetEnv = resolveCloudEnv(manifest.cloud, targetName);
|
|
1949
|
-
const
|
|
1950
|
-
const targetDbUrl = resolveDbUrlForEnv(targetEnv,
|
|
2069
|
+
const dbPassword2 = resolveTargetPassword(targetEnv.projectRef);
|
|
2070
|
+
const targetDbUrl = resolveDbUrlForEnv(targetEnv, dbPassword2);
|
|
1951
2071
|
const devDbUrl = await resolveDbUrl(cwd);
|
|
1952
2072
|
return { devDbUrl, targetEnv, targetDbUrl, targetName };
|
|
1953
2073
|
}
|
|
@@ -2112,7 +2232,7 @@ async function syncSecrets(cwd, resolved, opts) {
|
|
|
2112
2232
|
`);
|
|
2113
2233
|
const toSet = {};
|
|
2114
2234
|
for (const key of missingKeys) {
|
|
2115
|
-
const value = await
|
|
2235
|
+
const value = await input2({
|
|
2116
2236
|
message: `${key}:`,
|
|
2117
2237
|
theme,
|
|
2118
2238
|
validate: (val) => val.trim().length > 0 || "Value is required (press Ctrl+C to skip all)"
|
|
@@ -2128,7 +2248,7 @@ async function syncSecrets(cwd, resolved, opts) {
|
|
|
2128
2248
|
// src/env.ts
|
|
2129
2249
|
import fs8 from "fs";
|
|
2130
2250
|
import path8 from "path";
|
|
2131
|
-
import { confirm as confirm2, input as
|
|
2251
|
+
import { confirm as confirm2, input as input3, select } from "@inquirer/prompts";
|
|
2132
2252
|
|
|
2133
2253
|
// src/claude-md.ts
|
|
2134
2254
|
import fs7 from "fs";
|
|
@@ -2272,14 +2392,14 @@ async function envAdd(name, cwd, opts = {}) {
|
|
|
2272
2392
|
}
|
|
2273
2393
|
await ensureAccessToken(opts.nonInteractive, cwd);
|
|
2274
2394
|
let env2;
|
|
2275
|
-
let
|
|
2395
|
+
let dbPassword2;
|
|
2276
2396
|
if (opts.projectRef) {
|
|
2277
2397
|
const projects = await listProjects();
|
|
2278
2398
|
const project = projects.find((p) => p.id === opts.projectRef);
|
|
2279
2399
|
if (!project) {
|
|
2280
2400
|
throw new Error(`Project ${opts.projectRef} not found.`);
|
|
2281
2401
|
}
|
|
2282
|
-
|
|
2402
|
+
dbPassword2 = await promptForPassword(opts.nonInteractive, project.id);
|
|
2283
2403
|
env2 = {
|
|
2284
2404
|
projectRef: project.id,
|
|
2285
2405
|
region: project.region,
|
|
@@ -2299,17 +2419,17 @@ async function envAdd(name, cwd, opts = {}) {
|
|
|
2299
2419
|
if (method === "existing") {
|
|
2300
2420
|
const result = await connectExistingProject();
|
|
2301
2421
|
env2 = result.env;
|
|
2302
|
-
|
|
2422
|
+
dbPassword2 = result.dbPassword;
|
|
2303
2423
|
} else {
|
|
2304
2424
|
const result = await createNewProject(name, opts);
|
|
2305
2425
|
env2 = result.env;
|
|
2306
|
-
|
|
2426
|
+
dbPassword2 = result.dbPassword;
|
|
2307
2427
|
}
|
|
2308
2428
|
}
|
|
2309
2429
|
addCloudEnvironment(cwd, name, env2);
|
|
2310
2430
|
console.log(`
|
|
2311
2431
|
${blue("\u2714")} Environment "${name}" added to agentlink.json`);
|
|
2312
|
-
saveProjectCredential(env2.projectRef,
|
|
2432
|
+
saveProjectCredential(env2.projectRef, dbPassword2);
|
|
2313
2433
|
console.log(` ${blue("\u2714")} Credentials stored securely`);
|
|
2314
2434
|
if (name.startsWith("dev") || name === "staging") {
|
|
2315
2435
|
const shouldSync = opts.nonInteractive ? false : await confirm2({
|
|
@@ -2331,7 +2451,7 @@ async function envAdd(name, cwd, opts = {}) {
|
|
|
2331
2451
|
console.log(` Deploy with: ${dim("npx @agentlink.sh/cli@latest deploy --env " + name)}`);
|
|
2332
2452
|
}
|
|
2333
2453
|
}
|
|
2334
|
-
async function promptForPassword(nonInteractive) {
|
|
2454
|
+
async function promptForPassword(nonInteractive, projectRef) {
|
|
2335
2455
|
if (nonInteractive) {
|
|
2336
2456
|
const envPass = process.env.SUPABASE_DB_PASSWORD;
|
|
2337
2457
|
if (!envPass) {
|
|
@@ -2339,7 +2459,13 @@ async function promptForPassword(nonInteractive) {
|
|
|
2339
2459
|
}
|
|
2340
2460
|
return envPass;
|
|
2341
2461
|
}
|
|
2342
|
-
|
|
2462
|
+
if (projectRef) {
|
|
2463
|
+
const resetUrl = `https://supabase.com/dashboard/project/${projectRef}/settings/database`;
|
|
2464
|
+
console.log(` ${dim("Don't know the password? Reset it at:")}`);
|
|
2465
|
+
console.log(` ${dim(resetUrl)}`);
|
|
2466
|
+
console.log();
|
|
2467
|
+
}
|
|
2468
|
+
return await input3({
|
|
2343
2469
|
message: "Database password:",
|
|
2344
2470
|
theme: theme2,
|
|
2345
2471
|
transformer: (value, { isFinal }) => {
|
|
@@ -2363,18 +2489,18 @@ async function connectExistingProject() {
|
|
|
2363
2489
|
}))
|
|
2364
2490
|
});
|
|
2365
2491
|
const project = projects.find((p) => p.id === projectRef);
|
|
2366
|
-
const
|
|
2492
|
+
const dbPassword2 = await promptForPassword(false, project.id);
|
|
2367
2493
|
return {
|
|
2368
2494
|
env: {
|
|
2369
2495
|
projectRef: project.id,
|
|
2370
2496
|
region: project.region,
|
|
2371
2497
|
apiUrl: `https://${project.id}.supabase.co`
|
|
2372
2498
|
},
|
|
2373
|
-
dbPassword
|
|
2499
|
+
dbPassword: dbPassword2
|
|
2374
2500
|
};
|
|
2375
2501
|
}
|
|
2376
2502
|
async function createNewProject(envName, opts) {
|
|
2377
|
-
const { listOrganizations: listOrganizations2, listRegions: listRegions2 } = await import("./cloud-
|
|
2503
|
+
const { listOrganizations: listOrganizations2, listRegions: listRegions2 } = await import("./cloud-G7CIHVYT.js");
|
|
2378
2504
|
const orgs = await listOrganizations2();
|
|
2379
2505
|
if (orgs.length === 0) {
|
|
2380
2506
|
throw new Error("No Supabase organizations found. Create one at https://supabase.com/dashboard.");
|
|
@@ -2390,13 +2516,13 @@ async function createNewProject(envName, opts) {
|
|
|
2390
2516
|
theme: theme2,
|
|
2391
2517
|
choices: regions.map((r) => ({ name: `${r.name} (${r.id})`, value: r.id }))
|
|
2392
2518
|
});
|
|
2393
|
-
const projectName = await
|
|
2519
|
+
const projectName = await input3({
|
|
2394
2520
|
message: "Project name:",
|
|
2395
2521
|
default: `agentlink-${envName}`,
|
|
2396
2522
|
theme: theme2
|
|
2397
2523
|
});
|
|
2398
2524
|
const { randomBytes } = await import("crypto");
|
|
2399
|
-
const
|
|
2525
|
+
const dbPassword2 = randomBytes(24).toString("base64url");
|
|
2400
2526
|
console.log(`
|
|
2401
2527
|
Creating project "${projectName}" in ${region}...
|
|
2402
2528
|
`);
|
|
@@ -2404,7 +2530,7 @@ async function createNewProject(envName, opts) {
|
|
|
2404
2530
|
orgId,
|
|
2405
2531
|
name: projectName,
|
|
2406
2532
|
region,
|
|
2407
|
-
dbPass:
|
|
2533
|
+
dbPass: dbPassword2
|
|
2408
2534
|
});
|
|
2409
2535
|
await waitForProjectReady(project.id);
|
|
2410
2536
|
console.log(` ${blue("\u2714")} Project created: ${project.id}`);
|
|
@@ -2414,7 +2540,7 @@ async function createNewProject(envName, opts) {
|
|
|
2414
2540
|
region: project.region,
|
|
2415
2541
|
apiUrl: `https://${project.id}.supabase.co`
|
|
2416
2542
|
},
|
|
2417
|
-
dbPassword
|
|
2543
|
+
dbPassword: dbPassword2
|
|
2418
2544
|
};
|
|
2419
2545
|
}
|
|
2420
2546
|
async function envUse(name, cwd) {
|
|
@@ -2463,7 +2589,7 @@ async function envUse(name, cwd) {
|
|
|
2463
2589
|
await ensureAccessToken(true, cwd);
|
|
2464
2590
|
const env2 = manifest.cloud.environments[name];
|
|
2465
2591
|
const keys = await getApiKeys(env2.projectRef);
|
|
2466
|
-
const
|
|
2592
|
+
const dbPassword2 = loadProjectCredential(env2.projectRef);
|
|
2467
2593
|
const frontend = manifest.frontend;
|
|
2468
2594
|
const urlKey = frontend === "nextjs" ? "NEXT_PUBLIC_SUPABASE_URL" : "VITE_SUPABASE_URL";
|
|
2469
2595
|
const keyKey = frontend === "nextjs" ? "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY" : "VITE_SUPABASE_PUBLISHABLE_KEY";
|
|
@@ -2473,8 +2599,8 @@ async function envUse(name, cwd) {
|
|
|
2473
2599
|
SB_PUBLISHABLE_KEY: keys.publishableKey,
|
|
2474
2600
|
SB_SECRET_KEY: keys.secretKey
|
|
2475
2601
|
};
|
|
2476
|
-
if (
|
|
2477
|
-
vars.SUPABASE_DB_URL = resolveDbUrlForEnv(env2,
|
|
2602
|
+
if (dbPassword2) {
|
|
2603
|
+
vars.SUPABASE_DB_URL = resolveDbUrlForEnv(env2, dbPassword2);
|
|
2478
2604
|
}
|
|
2479
2605
|
}
|
|
2480
2606
|
writeManagedEnv(cwd, name, vars);
|
|
@@ -2529,6 +2655,120 @@ async function envRemove(name, cwd, opts = {}) {
|
|
|
2529
2655
|
removeCloudEnvironment(cwd, name);
|
|
2530
2656
|
console.log(` ${blue("\u2714")} Environment "${name}" removed.`);
|
|
2531
2657
|
}
|
|
2658
|
+
async function envRelink(name, cwd, opts = {}) {
|
|
2659
|
+
const manifest = readManifest(cwd);
|
|
2660
|
+
if (!manifest) {
|
|
2661
|
+
throw new Error("No agentlink.json found. Run `agentlink` to scaffold a project first.");
|
|
2662
|
+
}
|
|
2663
|
+
if (opts.token) {
|
|
2664
|
+
process.env.SUPABASE_ACCESS_TOKEN = opts.token;
|
|
2665
|
+
}
|
|
2666
|
+
await ensureAccessToken(opts.nonInteractive, cwd);
|
|
2667
|
+
let env2;
|
|
2668
|
+
let dbPassword2;
|
|
2669
|
+
if (opts.projectRef) {
|
|
2670
|
+
const projects = await listProjects();
|
|
2671
|
+
const project = projects.find((p) => p.id === opts.projectRef);
|
|
2672
|
+
if (!project) {
|
|
2673
|
+
throw new Error(`Project ${opts.projectRef} not found.`);
|
|
2674
|
+
}
|
|
2675
|
+
dbPassword2 = await promptForPassword(opts.nonInteractive, project.id);
|
|
2676
|
+
env2 = {
|
|
2677
|
+
projectRef: project.id,
|
|
2678
|
+
region: project.region,
|
|
2679
|
+
apiUrl: `https://${project.id}.supabase.co`
|
|
2680
|
+
};
|
|
2681
|
+
} else if (opts.nonInteractive) {
|
|
2682
|
+
throw new Error("--project-ref is required in non-interactive mode.");
|
|
2683
|
+
} else {
|
|
2684
|
+
const method = await select({
|
|
2685
|
+
message: `How do you want to relink the "${name}" environment?`,
|
|
2686
|
+
theme: theme2,
|
|
2687
|
+
choices: [
|
|
2688
|
+
{ name: "Connect an existing Supabase project", value: "existing" },
|
|
2689
|
+
{ name: "Create a new Supabase project", value: "new" }
|
|
2690
|
+
]
|
|
2691
|
+
});
|
|
2692
|
+
if (method === "existing") {
|
|
2693
|
+
const result = await connectExistingProject();
|
|
2694
|
+
env2 = result.env;
|
|
2695
|
+
dbPassword2 = result.dbPassword;
|
|
2696
|
+
} else {
|
|
2697
|
+
const result = await createNewProject(name, opts);
|
|
2698
|
+
env2 = result.env;
|
|
2699
|
+
dbPassword2 = result.dbPassword;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (!manifest.cloud) {
|
|
2703
|
+
manifest.cloud = { default: "local", environments: {} };
|
|
2704
|
+
}
|
|
2705
|
+
manifest.cloud.environments[name] = env2;
|
|
2706
|
+
writeManifest(cwd, manifest);
|
|
2707
|
+
console.log(`
|
|
2708
|
+
${blue("\u2714")} Environment "${name}" relinked to ${env2.projectRef}`);
|
|
2709
|
+
saveProjectCredential(env2.projectRef, dbPassword2);
|
|
2710
|
+
console.log(` ${blue("\u2714")} Credentials stored securely`);
|
|
2711
|
+
const keys = await getApiKeys(env2.projectRef);
|
|
2712
|
+
let dbUrl;
|
|
2713
|
+
try {
|
|
2714
|
+
dbUrl = await resolvePoolerDbUrl(env2.projectRef, dbPassword2);
|
|
2715
|
+
} catch {
|
|
2716
|
+
dbUrl = resolveDbUrlForEnv(env2, dbPassword2);
|
|
2717
|
+
}
|
|
2718
|
+
const frontend = manifest.frontend;
|
|
2719
|
+
const urlKey = frontend === "nextjs" ? "NEXT_PUBLIC_SUPABASE_URL" : "VITE_SUPABASE_URL";
|
|
2720
|
+
const keyKey = frontend === "nextjs" ? "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY" : "VITE_SUPABASE_PUBLISHABLE_KEY";
|
|
2721
|
+
writeManagedEnv(cwd, name, {
|
|
2722
|
+
[urlKey]: env2.apiUrl,
|
|
2723
|
+
[keyKey]: keys.publishableKey,
|
|
2724
|
+
SB_PUBLISHABLE_KEY: keys.publishableKey,
|
|
2725
|
+
SB_SECRET_KEY: keys.secretKey,
|
|
2726
|
+
SUPABASE_DB_URL: dbUrl
|
|
2727
|
+
});
|
|
2728
|
+
console.log(` ${blue("\u2714")} .env.local updated (pooler URL from API)`);
|
|
2729
|
+
writeClaudeMd({
|
|
2730
|
+
projectDir: cwd,
|
|
2731
|
+
mode: "cloud",
|
|
2732
|
+
projectRef: env2.projectRef,
|
|
2733
|
+
region: env2.region,
|
|
2734
|
+
envName: name
|
|
2735
|
+
});
|
|
2736
|
+
console.log(` ${blue("\u2714")} CLAUDE.md updated`);
|
|
2737
|
+
console.log(`
|
|
2738
|
+
${blue("\u25CF")} Linking to Supabase project...`);
|
|
2739
|
+
await runCommand(
|
|
2740
|
+
`${sb()} link --project-ref ${env2.projectRef}`,
|
|
2741
|
+
cwd,
|
|
2742
|
+
void 0,
|
|
2743
|
+
{ SUPABASE_DB_PASSWORD: dbPassword2 }
|
|
2744
|
+
);
|
|
2745
|
+
console.log(` ${blue("\u2714")} Linked`);
|
|
2746
|
+
const migrationsDir = path8.join(cwd, "supabase", "migrations");
|
|
2747
|
+
const hasMigrations = fs8.existsSync(migrationsDir) && fs8.readdirSync(migrationsDir).some((f) => f.endsWith(".sql"));
|
|
2748
|
+
if (hasMigrations) {
|
|
2749
|
+
console.log(`
|
|
2750
|
+
${blue("\u25CF")} Pushing migrations to new project...`);
|
|
2751
|
+
await runCommand(
|
|
2752
|
+
`${sb()} db push`,
|
|
2753
|
+
cwd,
|
|
2754
|
+
void 0,
|
|
2755
|
+
{ SUPABASE_DB_PASSWORD: dbPassword2 }
|
|
2756
|
+
);
|
|
2757
|
+
console.log(` ${blue("\u2714")} Migrations pushed`);
|
|
2758
|
+
}
|
|
2759
|
+
const functionsDir = path8.join(cwd, "supabase", "functions");
|
|
2760
|
+
if (fs8.existsSync(functionsDir)) {
|
|
2761
|
+
const hasFunctions = fs8.readdirSync(functionsDir, { withFileTypes: true }).some((e) => e.isDirectory() && !e.name.startsWith("_"));
|
|
2762
|
+
if (hasFunctions) {
|
|
2763
|
+
console.log(`
|
|
2764
|
+
${blue("\u25CF")} Deploying edge functions...`);
|
|
2765
|
+
await runCommand(`${sb()} functions deploy --project-ref ${env2.projectRef}`, cwd);
|
|
2766
|
+
console.log(` ${blue("\u2714")} Edge functions deployed`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
console.log(`
|
|
2770
|
+
${blue("Done.")} Environment "${name}" relinked to ${dim(env2.projectRef)}`);
|
|
2771
|
+
}
|
|
2532
2772
|
async function scaffoldCiTemplate(cwd, envName) {
|
|
2533
2773
|
const workflowDir = path8.join(cwd, ".github", "workflows");
|
|
2534
2774
|
fs8.mkdirSync(workflowDir, { recursive: true });
|
|
@@ -3395,7 +3635,7 @@ import { execSync } from "child_process";
|
|
|
3395
3635
|
import fs15 from "fs";
|
|
3396
3636
|
import path15 from "path";
|
|
3397
3637
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3398
|
-
import { confirm as confirm5, input as
|
|
3638
|
+
import { confirm as confirm5, input as input5, select as select3 } from "@inquirer/prompts";
|
|
3399
3639
|
|
|
3400
3640
|
// src/banner.ts
|
|
3401
3641
|
var TITLE = [
|
|
@@ -3517,7 +3757,7 @@ function clearSession(name) {
|
|
|
3517
3757
|
// src/scaffold.ts
|
|
3518
3758
|
import fs13 from "fs";
|
|
3519
3759
|
import path13 from "path";
|
|
3520
|
-
import { confirm as confirm3, input as
|
|
3760
|
+
import { confirm as confirm3, input as input4 } from "@inquirer/prompts";
|
|
3521
3761
|
|
|
3522
3762
|
// src/claude-settings.ts
|
|
3523
3763
|
import fs12 from "fs";
|
|
@@ -3761,7 +4001,7 @@ async function scaffold(options) {
|
|
|
3761
4001
|
console.log(dim(` If you don't know this password you can reset it from your Supabase dashboard:`));
|
|
3762
4002
|
console.log(dim(` ${link(resetUrl)}`));
|
|
3763
4003
|
console.log();
|
|
3764
|
-
const dbPass2 = await
|
|
4004
|
+
const dbPass2 = await input4({
|
|
3765
4005
|
message: "Database password for existing project",
|
|
3766
4006
|
theme: theme3,
|
|
3767
4007
|
validate: (val) => val.trim().length > 0 || "Password is required"
|
|
@@ -3849,16 +4089,19 @@ SUPABASE_DB_PASSWORD=${ctx.dbPass}
|
|
|
3849
4089
|
}
|
|
3850
4090
|
spinner.text = "Setting up database \u2014 Pushing infrastructure migrations";
|
|
3851
4091
|
await runCommand(`${sb()} db push`, projectDir, void 0, cloudEnv);
|
|
3852
|
-
spinner.text = "Setting up database \u2014 Writing application migration";
|
|
3853
|
-
const setupBase = /* @__PURE__ */ new Date();
|
|
3854
|
-
const setupContent = getTemplateSQLEntries().join("\n\n");
|
|
3855
4092
|
const migrationsDir = path13.join(projectDir, "supabase", "migrations");
|
|
3856
|
-
const
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4093
|
+
const existingSetup = fs13.readdirSync(migrationsDir).find((f) => f.endsWith("_agentlink_setup.sql"));
|
|
4094
|
+
if (!existingSetup) {
|
|
4095
|
+
spinner.text = "Setting up database \u2014 Writing application migration";
|
|
4096
|
+
const setupBase = /* @__PURE__ */ new Date();
|
|
4097
|
+
const setupContent = getTemplateSQLEntries().join("\n\n");
|
|
4098
|
+
const setupVersion = setupBase.toISOString().replace(/\D/g, "").slice(0, 14);
|
|
4099
|
+
const setupFilename = `${setupVersion}_agentlink_setup.sql`;
|
|
4100
|
+
fs13.writeFileSync(path13.join(migrationsDir, setupFilename), setupContent);
|
|
4101
|
+
spinner.text = "Setting up database \u2014 Writing post-setup migrations";
|
|
4102
|
+
const postBase = new Date(setupBase.getTime() + 2e3);
|
|
4103
|
+
writeMigrationTemplates(projectDir, POST_SETUP_MIGRATIONS, postBase);
|
|
4104
|
+
}
|
|
3862
4105
|
spinner.text = "Setting up database \u2014 Pushing migrations";
|
|
3863
4106
|
await runCommand(`${sb()} db push`, projectDir, void 0, cloudEnv);
|
|
3864
4107
|
spinner.text = "Setting up database \u2014 Storing vault secrets";
|
|
@@ -3903,19 +4146,22 @@ SUPABASE_DB_PASSWORD=${ctx.dbPass}
|
|
|
3903
4146
|
for (const sql of getTemplateSQLEntries()) {
|
|
3904
4147
|
await runSQL(sql, ctx.dbUrl);
|
|
3905
4148
|
}
|
|
3906
|
-
spinner.text = "Setting up database \u2014 Writing application migration";
|
|
3907
|
-
const setupBase = /* @__PURE__ */ new Date();
|
|
3908
|
-
const setupContent = getTemplateSQLEntries().join("\n\n");
|
|
3909
4149
|
const migrationsDir = path13.join(projectDir, "supabase", "migrations");
|
|
3910
|
-
const
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
await repairMigrations(projectDir,
|
|
4150
|
+
const existingSetup = fs13.readdirSync(migrationsDir).find((f) => f.endsWith("_agentlink_setup.sql"));
|
|
4151
|
+
if (!existingSetup) {
|
|
4152
|
+
spinner.text = "Setting up database \u2014 Writing application migration";
|
|
4153
|
+
const setupBase = /* @__PURE__ */ new Date();
|
|
4154
|
+
const setupContent = getTemplateSQLEntries().join("\n\n");
|
|
4155
|
+
const setupVersion = setupBase.toISOString().replace(/\D/g, "").slice(0, 14);
|
|
4156
|
+
const setupFilename = `${setupVersion}_agentlink_setup.sql`;
|
|
4157
|
+
fs13.writeFileSync(path13.join(migrationsDir, setupFilename), setupContent);
|
|
4158
|
+
await repairMigrations(projectDir, [setupVersion]);
|
|
4159
|
+
spinner.text = "Setting up database \u2014 Writing post-setup migrations";
|
|
4160
|
+
const postBase = new Date(setupBase.getTime() + 2e3);
|
|
4161
|
+
const postVersions = writeMigrationTemplates(projectDir, POST_SETUP_MIGRATIONS, postBase);
|
|
4162
|
+
if (postVersions.length > 0) {
|
|
4163
|
+
await repairMigrations(projectDir, postVersions);
|
|
4164
|
+
}
|
|
3919
4165
|
}
|
|
3920
4166
|
spinner.text = "Setting up database \u2014 Storing vault secrets";
|
|
3921
4167
|
await runSQL(seedSQL(ctx.publishableKey, ctx.secretKey), ctx.dbUrl);
|
|
@@ -4733,7 +4979,7 @@ Run without --resume to start fresh.`
|
|
|
4733
4979
|
if (nonInteractive) {
|
|
4734
4980
|
throw new Error("Project name required in non-interactive mode. Provide a name as the first argument.");
|
|
4735
4981
|
}
|
|
4736
|
-
name = await
|
|
4982
|
+
name = await input5({
|
|
4737
4983
|
message: "Project name",
|
|
4738
4984
|
default: "my-app",
|
|
4739
4985
|
theme: theme5,
|
|
@@ -4757,7 +5003,7 @@ ${red("Error:")} ${error}`);
|
|
|
4757
5003
|
if (nonInteractive) {
|
|
4758
5004
|
throw new Error("Project name required in non-interactive mode. Provide a name as the first argument.");
|
|
4759
5005
|
}
|
|
4760
|
-
name = await
|
|
5006
|
+
name = await input5({
|
|
4761
5007
|
message: "Project name",
|
|
4762
5008
|
default: "my-app",
|
|
4763
5009
|
theme: theme5,
|
|
@@ -4829,7 +5075,7 @@ ${red("Error:")} No organizations found. Create one at ${link("https://supabase.
|
|
|
4829
5075
|
const match = regions.find((r) => r.id === detected);
|
|
4830
5076
|
region = match?.id ?? "us-east-1";
|
|
4831
5077
|
const label2 = match?.name ?? "US East (N. Virginia)";
|
|
4832
|
-
console.log(`${blue("\u2714")} ${bold("
|
|
5078
|
+
console.log(`${blue("\u2714")} ${bold("Closest region")} ${amber(label2)}`);
|
|
4833
5079
|
}
|
|
4834
5080
|
const label = "dev";
|
|
4835
5081
|
cloudConfig = { orgId, region, label };
|
|
@@ -4971,8 +5217,8 @@ program.command("info [name]").description("Show documentation about AgentLink c
|
|
|
4971
5217
|
});
|
|
4972
5218
|
program.command("login").description("Authenticate with Supabase via OAuth (opens browser)").action(async () => {
|
|
4973
5219
|
try {
|
|
4974
|
-
const { oauthLogin } = await import("./oauth-
|
|
4975
|
-
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-
|
|
5220
|
+
const { oauthLogin } = await import("./oauth-BGBLVE52.js");
|
|
5221
|
+
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-G7CIHVYT.js");
|
|
4976
5222
|
console.log();
|
|
4977
5223
|
const tokens = await oauthLogin();
|
|
4978
5224
|
saveCredentials2({
|
|
@@ -5086,6 +5332,35 @@ ${red("Error:")} ${err.message}`);
|
|
|
5086
5332
|
process.exit(1);
|
|
5087
5333
|
}
|
|
5088
5334
|
});
|
|
5335
|
+
db.command("rebuild").description("Nuke migrations, re-apply schemas, and regenerate migration files").option("--debug", "Write debug log to agentlink-debug.log").option("--db-url <url>", "Database URL (auto-detects from .env.local or supabase status)").option("--env <name>", "Target cloud environment").action(async (opts) => {
|
|
5336
|
+
if (opts.debug) initLog(true);
|
|
5337
|
+
try {
|
|
5338
|
+
await dbRebuild({ dbUrl: opts.dbUrl, cwd: process.cwd(), env: opts.env });
|
|
5339
|
+
} catch (err) {
|
|
5340
|
+
console.error(`
|
|
5341
|
+
${red("Error:")} ${err.message}`);
|
|
5342
|
+
process.exit(1);
|
|
5343
|
+
}
|
|
5344
|
+
});
|
|
5345
|
+
db.command("url").description("Show the correct pooler DB URL from Supabase (--fix to update .env.local)").option("--fix", "Update .env.local with the correct URL").option("--debug", "Write debug log to agentlink-debug.log").option("--db-url <url>", "Database URL (auto-detects from .env.local or supabase status)").option("--env <name>", "Target cloud environment").action(async (opts) => {
|
|
5346
|
+
if (opts.debug) initLog(true);
|
|
5347
|
+
try {
|
|
5348
|
+
await dbUrlCheck({ dbUrl: opts.dbUrl, cwd: process.cwd(), env: opts.env, fix: opts.fix });
|
|
5349
|
+
} catch (err) {
|
|
5350
|
+
console.error(`
|
|
5351
|
+
${red("Error:")} ${err.message}`);
|
|
5352
|
+
process.exit(1);
|
|
5353
|
+
}
|
|
5354
|
+
});
|
|
5355
|
+
db.command("password [value]").description("Show or set the database password for the active cloud project").action(async (value) => {
|
|
5356
|
+
try {
|
|
5357
|
+
await dbPassword(process.cwd(), value);
|
|
5358
|
+
} catch (err) {
|
|
5359
|
+
console.error(`
|
|
5360
|
+
${red("Error:")} ${err.message}`);
|
|
5361
|
+
process.exit(1);
|
|
5362
|
+
}
|
|
5363
|
+
});
|
|
5089
5364
|
program.addCommand(db);
|
|
5090
5365
|
program.command("deploy").description("Deploy schema changes and edge functions to a target environment").option("--env <name>", "Target environment (default: prod)").option("--ci", "Non-interactive mode for CI/CD").option("--allow-warnings", "Proceed past data-risk warnings (CI only)").option("--dry-run", "Show what would be deployed without applying").option("--setup-ci", "Scaffold GitHub Actions workflow").option("--debug", "Write debug log to agentlink-debug.log").action(async (opts) => {
|
|
5091
5366
|
if (opts.debug) initLog(true);
|
|
@@ -5127,6 +5402,16 @@ ${red("Error:")} ${err.message}`);
|
|
|
5127
5402
|
process.exit(1);
|
|
5128
5403
|
}
|
|
5129
5404
|
});
|
|
5405
|
+
env.command("relink <name>").description("Relink an environment to a new Supabase project (keeps migrations)").option("--token <token>", "Supabase access token").option("--org <id>", "Supabase organization ID").option("--region <region>", "Supabase Cloud region").option("--project-ref <ref>", "Connect to an existing Supabase project").option("--non-interactive", "Error instead of prompting").option("--debug", "Write debug log to agentlink-debug.log").action(async (name, opts) => {
|
|
5406
|
+
if (opts.debug) initLog(true);
|
|
5407
|
+
try {
|
|
5408
|
+
await envRelink(name, process.cwd(), opts);
|
|
5409
|
+
} catch (err) {
|
|
5410
|
+
console.error(`
|
|
5411
|
+
${red("Error:")} ${err.message}`);
|
|
5412
|
+
process.exit(1);
|
|
5413
|
+
}
|
|
5414
|
+
});
|
|
5130
5415
|
env.command("remove <name>").description("Remove a cloud environment").option("-y, --yes", "Skip confirmation").option("--debug", "Write debug log to agentlink-debug.log").action(async (name, opts) => {
|
|
5131
5416
|
if (opts.debug) initLog(true);
|
|
5132
5417
|
try {
|
|
@@ -98,63 +98,71 @@ function openBrowser(url) {
|
|
|
98
98
|
console.log(` ${dim(url)}`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
function
|
|
102
|
-
let
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
146
|
-
res.end(SUCCESS_HTML);
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
server.close();
|
|
149
|
-
resolve(authCode);
|
|
150
|
-
});
|
|
151
|
-
server.listen(port, "127.0.0.1");
|
|
101
|
+
function createCallbackServer(port, state) {
|
|
102
|
+
let codeResolve = null;
|
|
103
|
+
let receivedCode = null;
|
|
104
|
+
let done = false;
|
|
105
|
+
const server = createServer((req, res) => {
|
|
106
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
107
|
+
if (url.pathname !== "/callback") {
|
|
108
|
+
res.writeHead(404);
|
|
109
|
+
res.end();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const error = url.searchParams.get("error");
|
|
113
|
+
if (error) {
|
|
114
|
+
const desc = url.searchParams.get("error_description") ?? error;
|
|
115
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
116
|
+
res.end(ERROR_HTML(desc));
|
|
117
|
+
console.log(` ${amber("\u25B2")} OAuth error: ${desc}`);
|
|
118
|
+
done = true;
|
|
119
|
+
codeResolve?.(null);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const returnedState = url.searchParams.get("state");
|
|
123
|
+
if (returnedState !== state) {
|
|
124
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
125
|
+
res.end(ERROR_HTML("State mismatch \u2014 possible CSRF attack."));
|
|
126
|
+
done = true;
|
|
127
|
+
codeResolve?.(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const authCode = url.searchParams.get("code");
|
|
131
|
+
if (!authCode) {
|
|
132
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
133
|
+
res.end(ERROR_HTML("No authorization code received."));
|
|
134
|
+
done = true;
|
|
135
|
+
codeResolve?.(null);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
139
|
+
res.end(SUCCESS_HTML);
|
|
140
|
+
receivedCode = authCode;
|
|
141
|
+
done = true;
|
|
142
|
+
codeResolve?.(authCode);
|
|
152
143
|
});
|
|
144
|
+
server.listen(port, "127.0.0.1");
|
|
153
145
|
return {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
/** Wait up to `ms` for the callback. Returns code or null on timeout. Server stays alive. */
|
|
147
|
+
waitForCode(ms) {
|
|
148
|
+
if (receivedCode) return Promise.resolve(receivedCode);
|
|
149
|
+
if (done) return Promise.resolve(null);
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
codeResolve = resolve;
|
|
152
|
+
const timer = setTimeout(() => {
|
|
153
|
+
codeResolve = null;
|
|
154
|
+
resolve(null);
|
|
155
|
+
}, ms);
|
|
156
|
+
const origResolve = resolve;
|
|
157
|
+
codeResolve = (code) => {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
codeResolve = null;
|
|
160
|
+
origResolve(code);
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
close() {
|
|
165
|
+
server.close();
|
|
158
166
|
}
|
|
159
167
|
};
|
|
160
168
|
}
|
|
@@ -176,33 +184,40 @@ async function oauthLogin() {
|
|
|
176
184
|
openBrowser(authorizeUrl.toString());
|
|
177
185
|
console.log(` ${dim("Waiting for authorization...")}`);
|
|
178
186
|
console.log();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (code) {
|
|
183
|
-
return exchangeCode(code, redirectUri, codeVerifier);
|
|
184
|
-
}
|
|
185
|
-
while (true) {
|
|
186
|
-
const action = await select({
|
|
187
|
-
message: "No response yet. What would you like to do?",
|
|
188
|
-
theme,
|
|
189
|
-
choices: [
|
|
190
|
-
{ name: "Keep waiting (30s more)", value: "wait" },
|
|
191
|
-
{ name: "Retry (open browser again)", value: "retry" },
|
|
192
|
-
{ name: "Cancel", value: "cancel" }
|
|
193
|
-
]
|
|
194
|
-
});
|
|
195
|
-
if (action === "cancel") {
|
|
196
|
-
throw new Error("OAuth login cancelled.");
|
|
197
|
-
}
|
|
198
|
-
if (action === "retry") {
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
const next = waitForCallback(port, state, CHECK_IN_MS);
|
|
202
|
-
code = await next.promise;
|
|
187
|
+
const callback = createCallbackServer(port, state);
|
|
188
|
+
try {
|
|
189
|
+
let code = await callback.waitForCode(CHECK_IN_MS);
|
|
203
190
|
if (code) {
|
|
191
|
+
callback.close();
|
|
204
192
|
return exchangeCode(code, redirectUri, codeVerifier);
|
|
205
193
|
}
|
|
194
|
+
while (true) {
|
|
195
|
+
const action = await select({
|
|
196
|
+
message: "No response yet. What would you like to do?",
|
|
197
|
+
theme,
|
|
198
|
+
choices: [
|
|
199
|
+
{ name: "Keep waiting", value: "wait" },
|
|
200
|
+
{ name: "Retry (open browser again)", value: "retry" },
|
|
201
|
+
{ name: "Cancel", value: "cancel" }
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
if (action === "cancel") {
|
|
205
|
+
callback.close();
|
|
206
|
+
throw new Error("OAuth login cancelled.");
|
|
207
|
+
}
|
|
208
|
+
if (action === "retry") {
|
|
209
|
+
callback.close();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
code = await callback.waitForCode(CHECK_IN_MS);
|
|
213
|
+
if (code) {
|
|
214
|
+
callback.close();
|
|
215
|
+
return exchangeCode(code, redirectUri, codeVerifier);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
callback.close();
|
|
220
|
+
throw err;
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
}
|