@agentlink.sh/cli 0.10.1 → 0.11.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
CHANGED
|
@@ -41,16 +41,19 @@ The install scripts automatically detect your OS, install Node.js if needed, and
|
|
|
41
41
|
## Quick start
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
# Create a new project (cloud by default
|
|
45
|
-
agentlink my-app
|
|
44
|
+
# Create a new project (cloud by default)
|
|
45
|
+
agentlink my-app
|
|
46
|
+
|
|
47
|
+
# With a prompt for Claude Code
|
|
48
|
+
agentlink my-app --prompt "Build a project management tool with kanban boards"
|
|
46
49
|
|
|
47
|
-
# Interactive wizard
|
|
50
|
+
# Interactive wizard
|
|
48
51
|
agentlink
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
## Commands
|
|
52
55
|
|
|
53
|
-
### `agentlink [name]
|
|
56
|
+
### `agentlink [name]`
|
|
54
57
|
|
|
55
58
|
Interactive wizard that scaffolds a new project or updates an existing one. Uses Supabase Cloud by default — region is auto-detected from your timezone, all recommended skills are installed automatically.
|
|
56
59
|
|
|
@@ -61,8 +64,8 @@ agentlink
|
|
|
61
64
|
# With project name
|
|
62
65
|
agentlink my-app
|
|
63
66
|
|
|
64
|
-
# With
|
|
65
|
-
agentlink my-app "Build a project management tool
|
|
67
|
+
# With prompt (passed to Claude Code on launch)
|
|
68
|
+
agentlink my-app --prompt "Build a project management tool"
|
|
66
69
|
|
|
67
70
|
# Local Docker mode (instead of cloud)
|
|
68
71
|
agentlink my-app --local
|
|
@@ -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-VTSNCG7U.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-VTSNCG7U.js");
|
|
234
234
|
const tokens = await oauthLogin();
|
|
235
235
|
process.env.SUPABASE_ACCESS_TOKEN = tokens.access_token;
|
|
236
236
|
saveCredentials({
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
updatePostgrestConfig,
|
|
27
27
|
validateProjectName,
|
|
28
28
|
waitForProjectReady
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-2CNK2BQY.js";
|
|
30
30
|
import {
|
|
31
31
|
SKILLS_VERSION,
|
|
32
32
|
SUPPORTED_SUPABASE_CLI,
|
|
@@ -2361,7 +2361,7 @@ async function connectExistingProject() {
|
|
|
2361
2361
|
};
|
|
2362
2362
|
}
|
|
2363
2363
|
async function createNewProject(envName, opts) {
|
|
2364
|
-
const { listOrganizations: listOrganizations2, listRegions: listRegions2 } = await import("./cloud-
|
|
2364
|
+
const { listOrganizations: listOrganizations2, listRegions: listRegions2 } = await import("./cloud-2AWCJAG5.js");
|
|
2365
2365
|
const orgs = await listOrganizations2();
|
|
2366
2366
|
if (orgs.length === 0) {
|
|
2367
2367
|
throw new Error("No Supabase organizations found. Create one at https://supabase.com/dashboard.");
|
|
@@ -3664,12 +3664,14 @@ async function scaffold(options) {
|
|
|
3664
3664
|
"pgdelta binary not found. Run `npm install` in the CLI package first."
|
|
3665
3665
|
);
|
|
3666
3666
|
}
|
|
3667
|
-
if (
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3667
|
+
if (!options.cloud) {
|
|
3668
|
+
if (checkCommand("psql")) {
|
|
3669
|
+
spinner.text = "Checking prerequisites \u2014 PostgreSQL client installed";
|
|
3670
|
+
} else {
|
|
3671
|
+
throw new Error(
|
|
3672
|
+
"psql is required for local mode but not installed.\n macOS: brew install libpq && brew link --force libpq\n Ubuntu: sudo apt install postgresql-client\n Or remove --local to use Supabase Cloud (no psql needed).\n See: https://www.postgresql.org/download/"
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3673
3675
|
}
|
|
3674
3676
|
});
|
|
3675
3677
|
console.log();
|
|
@@ -3709,6 +3711,9 @@ async function scaffold(options) {
|
|
|
3709
3711
|
if (options.cloud) {
|
|
3710
3712
|
await trackedStep("start-supabase", "Creating Supabase cloud project", async (spinner) => {
|
|
3711
3713
|
if (isResume && ctx.projectRef) {
|
|
3714
|
+
if (ctx.apiUrl && ctx.publishableKey && ctx.secretKey) {
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3712
3717
|
spinner.text = "Creating Supabase cloud project \u2014 Waiting for project to be ready";
|
|
3713
3718
|
await waitForProjectReady(ctx.projectRef, spinner);
|
|
3714
3719
|
return;
|
|
@@ -3773,6 +3778,9 @@ async function scaffold(options) {
|
|
|
3773
3778
|
});
|
|
3774
3779
|
}
|
|
3775
3780
|
await trackedStep("read-config", "Reading Supabase configuration", async () => {
|
|
3781
|
+
if (ctx.apiUrl && ctx.publishableKey && ctx.secretKey && (ctx.dbUrl || options.cloud)) {
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3776
3784
|
if (options.cloud) {
|
|
3777
3785
|
const keys = await getApiKeys(ctx.projectRef);
|
|
3778
3786
|
ctx.apiUrl = `https://${ctx.projectRef}.supabase.co`;
|
|
@@ -4436,25 +4444,71 @@ function finishScaffold(summary, frontend, cloudConfig, prompt, launchClaude, sk
|
|
|
4436
4444
|
console.log(` ${dim("Switch env")} agentlink env use <name>`);
|
|
4437
4445
|
console.log(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4438
4446
|
console.log();
|
|
4439
|
-
if (launchClaude === false || nonInteractive) {
|
|
4447
|
+
if (launchClaude === false || nonInteractive || !prompt) {
|
|
4440
4448
|
return;
|
|
4441
4449
|
}
|
|
4442
4450
|
console.log(blue(" Launching Claude Code..."));
|
|
4443
4451
|
console.log();
|
|
4444
|
-
|
|
4445
|
-
execSync(`claude ${launchArgs}`, {
|
|
4452
|
+
execSync(`claude ${JSON.stringify(prompt)}`, {
|
|
4446
4453
|
cwd: summary.projectDir,
|
|
4447
4454
|
stdio: "inherit"
|
|
4448
4455
|
});
|
|
4449
4456
|
}
|
|
4450
|
-
async function wizard(initialName,
|
|
4457
|
+
async function wizard(initialName, opts = {}) {
|
|
4451
4458
|
const { forceUpdate, frontend: initialFrontend, local: localMode, skills: installSkills, launch: launchClaude, yes: autoYes, nonInteractive } = opts;
|
|
4452
|
-
if (opts.prompt) {
|
|
4453
|
-
initialPrompt = opts.prompt;
|
|
4454
|
-
}
|
|
4455
4459
|
if (opts.token) {
|
|
4456
4460
|
process.env.SUPABASE_ACCESS_TOKEN = opts.token;
|
|
4457
4461
|
}
|
|
4462
|
+
if (opts.link) {
|
|
4463
|
+
const { projectRef, dbUrl, apiUrl, publishableKey, secretKey } = opts.link;
|
|
4464
|
+
if (!projectRef || !dbUrl || !apiUrl || !publishableKey || !secretKey) {
|
|
4465
|
+
throw new Error(
|
|
4466
|
+
"--link requires all connection flags:\n --project-ref, --db-url, --api-url, --publishable-key, --secret-key"
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4469
|
+
const name2 = initialName === "." ? path15.basename(process.cwd()) : initialName ?? "my-app";
|
|
4470
|
+
const scaffoldHere2 = initialName === ".";
|
|
4471
|
+
const projectDir2 = scaffoldHere2 ? process.cwd() : path15.resolve(process.cwd(), name2);
|
|
4472
|
+
const frontend2 = initialFrontend ?? "vite";
|
|
4473
|
+
const skills2 = installSkills === false ? [] : getCompanionSkills(frontend2);
|
|
4474
|
+
const region = projectRef.length > 0 ? "us-east-1" : "us-east-1";
|
|
4475
|
+
if (!scaffoldHere2) {
|
|
4476
|
+
fs15.mkdirSync(projectDir2, { recursive: true });
|
|
4477
|
+
}
|
|
4478
|
+
const cloudConfig2 = { orgId: "", region, label: "dev" };
|
|
4479
|
+
printBanner();
|
|
4480
|
+
console.log(dim(` v${pkg2.version}`));
|
|
4481
|
+
console.log();
|
|
4482
|
+
console.log(` ${blue("\u25CF")} Linking to Supabase project ${dim(projectRef)}`);
|
|
4483
|
+
console.log();
|
|
4484
|
+
const scaffoldOpts2 = { name: name2, skills: skills2, mode: "new", frontend: frontend2, cloud: cloudConfig2, nonInteractive: true };
|
|
4485
|
+
saveProgress(projectDir2, {
|
|
4486
|
+
version: pkg2.version,
|
|
4487
|
+
options: scaffoldOpts2,
|
|
4488
|
+
ctx: {
|
|
4489
|
+
dbUrl,
|
|
4490
|
+
apiUrl,
|
|
4491
|
+
publishableKey,
|
|
4492
|
+
secretKey,
|
|
4493
|
+
projectRef
|
|
4494
|
+
},
|
|
4495
|
+
completedSteps: [],
|
|
4496
|
+
prompt: opts.prompt ?? "",
|
|
4497
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4498
|
+
});
|
|
4499
|
+
saveSession(projectDir2, name2);
|
|
4500
|
+
const summary2 = await scaffold({
|
|
4501
|
+
...scaffoldOpts2,
|
|
4502
|
+
resume: {
|
|
4503
|
+
completedSteps: [],
|
|
4504
|
+
ctx: { dbUrl, apiUrl, publishableKey, secretKey, projectRef },
|
|
4505
|
+
prompt: opts.prompt ?? "",
|
|
4506
|
+
projectDir: projectDir2
|
|
4507
|
+
}
|
|
4508
|
+
});
|
|
4509
|
+
finishScaffold(summary2, frontend2, cloudConfig2, opts.prompt ?? "", launchClaude, skills2, true);
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4458
4512
|
printBanner();
|
|
4459
4513
|
console.log(dim(` v${pkg2.version}`));
|
|
4460
4514
|
console.log();
|
|
@@ -4630,7 +4684,23 @@ Run without --resume to start fresh.`
|
|
|
4630
4684
|
const isLoggedIn = authStatus.includes('"loggedIn": true') || authStatus.includes('"loggedIn":true');
|
|
4631
4685
|
if (!isLoggedIn) {
|
|
4632
4686
|
console.log(amber(" Claude Code requires authentication. Opening login..."));
|
|
4633
|
-
|
|
4687
|
+
try {
|
|
4688
|
+
execSync("claude auth login", { stdio: "inherit" });
|
|
4689
|
+
} catch {
|
|
4690
|
+
throw new Error(
|
|
4691
|
+
`Claude Code login failed.
|
|
4692
|
+
Run ${dim("claude auth login")} manually, then retry with:
|
|
4693
|
+
${dim("npx @agentlink.sh/cli@latest")}`
|
|
4694
|
+
);
|
|
4695
|
+
}
|
|
4696
|
+
const recheck = await runCommand("claude auth status").catch(() => "");
|
|
4697
|
+
if (!recheck.includes('"loggedIn": true') && !recheck.includes('"loggedIn":true')) {
|
|
4698
|
+
throw new Error(
|
|
4699
|
+
`Claude Code authentication did not complete.
|
|
4700
|
+
Run ${dim("claude auth login")} manually, then retry with:
|
|
4701
|
+
${dim("npx @agentlink.sh/cli@latest")}`
|
|
4702
|
+
);
|
|
4703
|
+
}
|
|
4634
4704
|
}
|
|
4635
4705
|
const hasSupabase = fs15.existsSync(path15.join(process.cwd(), "supabase", "config.toml"));
|
|
4636
4706
|
const hasPackageJson = fs15.existsSync(path15.join(process.cwd(), "package.json"));
|
|
@@ -4658,7 +4728,6 @@ Run without --resume to start fresh.`
|
|
|
4658
4728
|
});
|
|
4659
4729
|
}
|
|
4660
4730
|
} else if (initialName === ".") {
|
|
4661
|
-
mode = "existing";
|
|
4662
4731
|
name = path15.basename(process.cwd());
|
|
4663
4732
|
} else if (initialName) {
|
|
4664
4733
|
if (mode === "new") {
|
|
@@ -4686,7 +4755,7 @@ ${red("Error:")} ${error}`);
|
|
|
4686
4755
|
if (mode === "new") {
|
|
4687
4756
|
if (initialFrontend !== void 0) {
|
|
4688
4757
|
frontend = initialFrontend;
|
|
4689
|
-
} else if (nonInteractive || initialName &&
|
|
4758
|
+
} else if (nonInteractive || initialName && opts.prompt) {
|
|
4690
4759
|
frontend = "vite";
|
|
4691
4760
|
} else {
|
|
4692
4761
|
frontend = await select3({
|
|
@@ -4757,7 +4826,7 @@ ${red("Error:")} No organizations found. Create one at ${link("https://supabase.
|
|
|
4757
4826
|
if (mode === "existing") {
|
|
4758
4827
|
prompt = "Analyze the current project and plan a migration to the architecture proposed by AgentLink";
|
|
4759
4828
|
} else {
|
|
4760
|
-
prompt =
|
|
4829
|
+
prompt = opts.prompt ?? "";
|
|
4761
4830
|
}
|
|
4762
4831
|
if (!useCloud && mode === "existing" && !autoYes && !nonInteractive) {
|
|
4763
4832
|
const proceed = await confirm5({
|
|
@@ -4773,9 +4842,10 @@ ${amber("Aborted.")} Stop other Supabase instances manually before running again
|
|
|
4773
4842
|
process.exit(0);
|
|
4774
4843
|
}
|
|
4775
4844
|
}
|
|
4776
|
-
const
|
|
4845
|
+
const scaffoldHere = mode === "existing" || initialName === ".";
|
|
4846
|
+
const projectDir = scaffoldHere ? process.cwd() : path15.resolve(process.cwd(), name);
|
|
4777
4847
|
const scaffoldOpts = { name, skills, mode, frontend, cloud: cloudConfig, nonInteractive };
|
|
4778
|
-
if (
|
|
4848
|
+
if (!scaffoldHere) {
|
|
4779
4849
|
fs15.mkdirSync(projectDir, { recursive: true });
|
|
4780
4850
|
}
|
|
4781
4851
|
saveProgress(projectDir, {
|
|
@@ -4801,7 +4871,7 @@ function enableDebug(cmd) {
|
|
|
4801
4871
|
if (debug) initLog(true);
|
|
4802
4872
|
}
|
|
4803
4873
|
program.hook("preAction", (thisCommand) => enableDebug(thisCommand));
|
|
4804
|
-
program.name("agentlink").description("CLI for scaffolding Supabase apps with AI agents").version(pkg3.version).argument("[name]", "Project name
|
|
4874
|
+
program.name("agentlink").description("CLI for scaffolding Supabase apps with AI agents").version(pkg3.version).argument("[name]", "Project name (use . to scaffold in current directory)").option("--debug", "Write debug log to agentlink-debug.log").option("--force-update", "Force update even if project is up to date").option("--no-frontend", "Skip frontend scaffolding").option("--nextjs", "Use NextJS instead of React + Vite for frontend").option("--local", "Use local Docker instead of Supabase Cloud").option("--no-skills", "Skip companion skill installation").option("--no-launch", "Skip launching Claude Code after scaffold").option("-y, --yes", "Auto-confirm all prompts").option("--token <token>", "Supabase access token (skips interactive login)").option("--org <id>", "Supabase organization ID").option("--region <region>", "Supabase Cloud region (e.g. us-east-1)").option("--prompt <prompt>", "What to build (passed to Claude Code on launch)").option("--resume", "Resume a previously failed scaffold").option("--non-interactive", "Error instead of prompting when info is missing").option("--link", "Link to an existing Supabase project (requires --project-ref and connection flags)").option("--project-ref <ref>", "Supabase project ref (used with --link)").option("--db-url <url>", "Database URL (used with --link)").option("--api-url <url>", "Supabase API URL (used with --link)").option("--publishable-key <key>", "Supabase publishable key (used with --link)").option("--secret-key <key>", "Supabase secret key (used with --link)").action(async (name) => {
|
|
4805
4875
|
const opts = program.opts();
|
|
4806
4876
|
const debug = opts.debug ?? false;
|
|
4807
4877
|
initLog(debug);
|
|
@@ -4814,7 +4884,7 @@ program.name("agentlink").description("CLI for scaffolding Supabase apps with AI
|
|
|
4814
4884
|
} else if (opts.frontend !== void 0) {
|
|
4815
4885
|
frontend = "vite";
|
|
4816
4886
|
}
|
|
4817
|
-
await wizard(name,
|
|
4887
|
+
await wizard(name, {
|
|
4818
4888
|
forceUpdate: opts.forceUpdate,
|
|
4819
4889
|
frontend,
|
|
4820
4890
|
local: opts.local,
|
|
@@ -4826,7 +4896,14 @@ program.name("agentlink").description("CLI for scaffolding Supabase apps with AI
|
|
|
4826
4896
|
region: opts.region,
|
|
4827
4897
|
prompt: opts.prompt,
|
|
4828
4898
|
resume: opts.resume,
|
|
4829
|
-
nonInteractive: opts.nonInteractive
|
|
4899
|
+
nonInteractive: opts.nonInteractive,
|
|
4900
|
+
link: opts.link ? {
|
|
4901
|
+
projectRef: opts.projectRef,
|
|
4902
|
+
dbUrl: opts.dbUrl,
|
|
4903
|
+
apiUrl: opts.apiUrl,
|
|
4904
|
+
publishableKey: opts.publishableKey,
|
|
4905
|
+
secretKey: opts.secretKey
|
|
4906
|
+
} : void 0
|
|
4830
4907
|
});
|
|
4831
4908
|
} catch (err) {
|
|
4832
4909
|
const msg = `
|
|
@@ -4881,8 +4958,8 @@ program.command("info [name]").description("Show documentation about AgentLink c
|
|
|
4881
4958
|
});
|
|
4882
4959
|
program.command("login").description("Authenticate with Supabase via OAuth (opens browser)").action(async () => {
|
|
4883
4960
|
try {
|
|
4884
|
-
const { oauthLogin } = await import("./oauth-
|
|
4885
|
-
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-
|
|
4961
|
+
const { oauthLogin } = await import("./oauth-VTSNCG7U.js");
|
|
4962
|
+
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-2AWCJAG5.js");
|
|
4886
4963
|
console.log();
|
|
4887
4964
|
const tokens = await oauthLogin();
|
|
4888
4965
|
saveCredentials2({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
OAUTH_CLIENT_SECRET,
|
|
5
5
|
amber,
|
|
6
6
|
blue,
|
|
7
|
+
bold,
|
|
7
8
|
dim
|
|
8
9
|
} from "./chunk-G3HVUCWY.js";
|
|
9
10
|
|
|
@@ -12,11 +13,21 @@ import { createHash, randomBytes } from "crypto";
|
|
|
12
13
|
import { createServer } from "http";
|
|
13
14
|
import { URL } from "url";
|
|
14
15
|
import { execSync } from "child_process";
|
|
16
|
+
import { select } from "@inquirer/prompts";
|
|
17
|
+
var theme = {
|
|
18
|
+
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
19
|
+
style: {
|
|
20
|
+
answer: (text) => amber(text),
|
|
21
|
+
message: (text) => bold(text),
|
|
22
|
+
highlight: (text) => blue(text),
|
|
23
|
+
key: (text) => blue(bold(`<${text}>`))
|
|
24
|
+
}
|
|
25
|
+
};
|
|
15
26
|
var AUTHORIZE_URL = "https://api.supabase.com/v1/oauth/authorize";
|
|
16
27
|
var TOKEN_URL = "https://api.supabase.com/v1/oauth/token";
|
|
17
28
|
var PORT_RANGE_START = 54320;
|
|
18
29
|
var PORT_RANGE_END = 54330;
|
|
19
|
-
var
|
|
30
|
+
var CHECK_IN_MS = 3e4;
|
|
20
31
|
function generateCodeVerifier() {
|
|
21
32
|
return randomBytes(64).toString("base64url");
|
|
22
33
|
}
|
|
@@ -87,25 +98,15 @@ function openBrowser(url) {
|
|
|
87
98
|
console.log(` ${dim(url)}`);
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
100
|
-
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
101
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
102
|
-
authorizeUrl.searchParams.set("state", state);
|
|
103
|
-
const code = await new Promise((resolve, reject) => {
|
|
104
|
-
const timeout = setTimeout(() => {
|
|
105
|
-
server.close();
|
|
106
|
-
reject(new Error("OAuth login timed out. Try again."));
|
|
107
|
-
}, TIMEOUT_MS);
|
|
108
|
-
const server = createServer((req, res) => {
|
|
101
|
+
function waitForCallback(port, state, timeoutMs) {
|
|
102
|
+
let server;
|
|
103
|
+
let timer;
|
|
104
|
+
const promise = new Promise((resolve) => {
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
server?.close();
|
|
107
|
+
resolve(null);
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
server = createServer((req, res) => {
|
|
109
110
|
const url = new URL(req.url, `http://localhost:${port}`);
|
|
110
111
|
if (url.pathname !== "/callback") {
|
|
111
112
|
res.writeHead(404);
|
|
@@ -117,43 +118,93 @@ async function oauthLogin() {
|
|
|
117
118
|
const desc = url.searchParams.get("error_description") ?? error;
|
|
118
119
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
119
120
|
res.end(ERROR_HTML(desc));
|
|
120
|
-
clearTimeout(
|
|
121
|
+
clearTimeout(timer);
|
|
121
122
|
server.close();
|
|
122
|
-
|
|
123
|
+
console.log(` ${amber("\u25B2")} OAuth error: ${desc}`);
|
|
124
|
+
resolve(null);
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
125
127
|
const returnedState = url.searchParams.get("state");
|
|
126
128
|
if (returnedState !== state) {
|
|
127
129
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
128
130
|
res.end(ERROR_HTML("State mismatch \u2014 possible CSRF attack."));
|
|
129
|
-
clearTimeout(
|
|
131
|
+
clearTimeout(timer);
|
|
130
132
|
server.close();
|
|
131
|
-
|
|
133
|
+
resolve(null);
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
136
|
const authCode = url.searchParams.get("code");
|
|
135
137
|
if (!authCode) {
|
|
136
138
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
137
139
|
res.end(ERROR_HTML("No authorization code received."));
|
|
138
|
-
clearTimeout(
|
|
140
|
+
clearTimeout(timer);
|
|
139
141
|
server.close();
|
|
140
|
-
|
|
142
|
+
resolve(null);
|
|
141
143
|
return;
|
|
142
144
|
}
|
|
143
145
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
144
146
|
res.end(SUCCESS_HTML);
|
|
145
|
-
clearTimeout(
|
|
147
|
+
clearTimeout(timer);
|
|
146
148
|
server.close();
|
|
147
149
|
resolve(authCode);
|
|
148
150
|
});
|
|
149
|
-
server.listen(port, "127.0.0.1"
|
|
150
|
-
console.log(` ${blue("\u25CF")} Opening browser for Supabase login...`);
|
|
151
|
-
openBrowser(authorizeUrl.toString());
|
|
152
|
-
console.log(` ${dim("Waiting for authorization...")}`);
|
|
153
|
-
console.log();
|
|
154
|
-
});
|
|
151
|
+
server.listen(port, "127.0.0.1");
|
|
155
152
|
});
|
|
156
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
promise,
|
|
155
|
+
cleanup: () => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
server?.close();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function oauthLogin() {
|
|
162
|
+
while (true) {
|
|
163
|
+
const codeVerifier = generateCodeVerifier();
|
|
164
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
165
|
+
const state = randomBytes(16).toString("hex");
|
|
166
|
+
const port = await findOpenPort();
|
|
167
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
168
|
+
const authorizeUrl = new URL(AUTHORIZE_URL);
|
|
169
|
+
authorizeUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
|
|
170
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
171
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
172
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
173
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
174
|
+
authorizeUrl.searchParams.set("state", state);
|
|
175
|
+
console.log(` ${blue("\u25CF")} Opening browser for Supabase login...`);
|
|
176
|
+
openBrowser(authorizeUrl.toString());
|
|
177
|
+
console.log(` ${dim("Waiting for authorization...")}`);
|
|
178
|
+
console.log();
|
|
179
|
+
let code = null;
|
|
180
|
+
const first = waitForCallback(port, state, CHECK_IN_MS);
|
|
181
|
+
code = await first.promise;
|
|
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;
|
|
203
|
+
if (code) {
|
|
204
|
+
return exchangeCode(code, redirectUri, codeVerifier);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
157
208
|
}
|
|
158
209
|
async function exchangeCode(code, redirectUri, codeVerifier) {
|
|
159
210
|
const res = await fetch(TOKEN_URL, {
|