@agentlink.sh/cli 0.10.0 → 0.10.2
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
|
@@ -4,7 +4,7 @@ Teach AI agents the right way to build on Supabase.
|
|
|
4
4
|
|
|
5
5
|
[Agent Link](https://agentlink.sh) is an opinionated skill set for Claude Code — and this CLI is how you start a project with it. One command scaffolds a Supabase backend with schema isolation, RPC-first data access, row-level security, multi-tenancy, and edge functions already wired up — plus a frontend (React + Vite or Next.js) with auth, theming, and shadcn/ui ready to go. Your agent doesn't have to figure out the architecture — it's already there. From that foundation, five domain skills give it one clear path for every decision: how to structure tables, write RLS policies, expose APIs, handle background work, and connect a frontend. No ambiguity, no wrong turns. First-try results.
|
|
6
6
|
|
|
7
|
-
## Install
|
|
7
|
+
## Install & Run
|
|
8
8
|
|
|
9
9
|
### macOS
|
|
10
10
|
|
|
@@ -24,16 +24,16 @@ wget -qO- https://agentlink.sh/install | sh
|
|
|
24
24
|
irm https://agentlink.sh/install.ps1 | iex
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### npx (any platform with Node.js 18+)
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
|
|
30
|
+
npx @agentlink.sh/cli@latest
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Or run directly with npx (requires Node.js 18+):
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
npx @
|
|
36
|
+
npx @agentlink.sh/cli@latest
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The install scripts automatically detect your OS, install Node.js if needed, and set up the CLI globally.
|
|
@@ -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,
|
|
@@ -1671,7 +1671,7 @@ async function dbApply(options) {
|
|
|
1671
1671
|
console.warn(`Warning: type generation failed \u2014 ${err.message}`);
|
|
1672
1672
|
}
|
|
1673
1673
|
}
|
|
1674
|
-
console.log("\nDone. Run 'npx @
|
|
1674
|
+
console.log("\nDone. Run 'npx @agentlink.sh/cli check' to verify.");
|
|
1675
1675
|
}
|
|
1676
1676
|
async function dbMigrate(name, options) {
|
|
1677
1677
|
const dbUrl = await resolveDbUrl(options.cwd, options.dbUrl);
|
|
@@ -1928,7 +1928,7 @@ async function resolveEnvironments(cwd, opts) {
|
|
|
1928
1928
|
if (!manifest.cloud?.environments?.[targetName]) {
|
|
1929
1929
|
throw new Error(
|
|
1930
1930
|
`Target environment "${targetName}" not found.
|
|
1931
|
-
Run: npx @
|
|
1931
|
+
Run: npx @agentlink.sh/cli@latest env add ${targetName}`
|
|
1932
1932
|
);
|
|
1933
1933
|
}
|
|
1934
1934
|
await ensureAccessToken(opts.ci, cwd);
|
|
@@ -2138,16 +2138,16 @@ This is a **cloud** Supabase project.
|
|
|
2138
2138
|
|
|
2139
2139
|
| Task | Command |
|
|
2140
2140
|
|------|---------|
|
|
2141
|
-
| Apply schemas | \`npx @
|
|
2142
|
-
| Generate migration | \`npx @
|
|
2141
|
+
| Apply schemas | \`npx @agentlink.sh/cli@latest db apply\` |
|
|
2142
|
+
| Generate migration | \`npx @agentlink.sh/cli@latest db migrate name\` then \`supabase db push\` |
|
|
2143
2143
|
| Push migrations | \`supabase db push\` |
|
|
2144
|
-
| Generate types | \`npx @
|
|
2145
|
-
| Run SQL | \`npx @
|
|
2144
|
+
| Generate types | \`npx @agentlink.sh/cli@latest db types\` |
|
|
2145
|
+
| Run SQL | \`npx @agentlink.sh/cli@latest db sql "SELECT ..."\` |
|
|
2146
2146
|
| Deploy edge functions | \`supabase functions deploy\` |
|
|
2147
2147
|
| Set secrets | \`supabase secrets set KEY=value\` |
|
|
2148
|
-
| Deploy to production | \`npx @
|
|
2149
|
-
| Switch environment | \`npx @
|
|
2150
|
-
| List environments | \`npx @
|
|
2148
|
+
| Deploy to production | \`npx @agentlink.sh/cli@latest deploy\` |
|
|
2149
|
+
| Switch environment | \`npx @agentlink.sh/cli@latest env use <name>\` |
|
|
2150
|
+
| List environments | \`npx @agentlink.sh/cli@latest env list\` |
|
|
2151
2151
|
|
|
2152
2152
|
## Important
|
|
2153
2153
|
|
|
@@ -2169,14 +2169,14 @@ This is a **local** Supabase project running in Docker.
|
|
|
2169
2169
|
| Task | Command |
|
|
2170
2170
|
|------|---------|
|
|
2171
2171
|
| Start Supabase | \`supabase start\` |
|
|
2172
|
-
| Apply schemas | \`npx @
|
|
2173
|
-
| Generate migration | \`npx @
|
|
2174
|
-
| Generate types | \`npx @
|
|
2175
|
-
| Run SQL | \`npx @
|
|
2172
|
+
| Apply schemas | \`npx @agentlink.sh/cli@latest db apply\` |
|
|
2173
|
+
| Generate migration | \`npx @agentlink.sh/cli@latest db migrate name\` |
|
|
2174
|
+
| Generate types | \`npx @agentlink.sh/cli@latest db types\` |
|
|
2175
|
+
| Run SQL | \`npx @agentlink.sh/cli@latest db sql "SELECT ..."\` |
|
|
2176
2176
|
| Serve edge functions | \`supabase functions serve\` |
|
|
2177
|
-
| Deploy to production | \`npx @
|
|
2178
|
-
| Switch environment | \`npx @
|
|
2179
|
-
| List environments | \`npx @
|
|
2177
|
+
| Deploy to production | \`npx @agentlink.sh/cli@latest deploy\` |
|
|
2178
|
+
| Switch environment | \`npx @agentlink.sh/cli@latest env use <name>\` |
|
|
2179
|
+
| List environments | \`npx @agentlink.sh/cli@latest env list\` |
|
|
2180
2180
|
|
|
2181
2181
|
## Finding connection details
|
|
2182
2182
|
|
|
@@ -2315,7 +2315,7 @@ async function envAdd(name, cwd, opts = {}) {
|
|
|
2315
2315
|
console.log(`
|
|
2316
2316
|
${blue("Done.")} Environment "${name}" is ready.`);
|
|
2317
2317
|
if (!name.startsWith("dev") && name !== "staging" && name !== "local") {
|
|
2318
|
-
console.log(` Deploy with: ${dim("npx @
|
|
2318
|
+
console.log(` Deploy with: ${dim("npx @agentlink.sh/cli@latest deploy --env " + name)}`);
|
|
2319
2319
|
}
|
|
2320
2320
|
}
|
|
2321
2321
|
async function promptForPassword(nonInteractive) {
|
|
@@ -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.");
|
|
@@ -2541,7 +2541,7 @@ jobs:
|
|
|
2541
2541
|
- run: npm ci
|
|
2542
2542
|
|
|
2543
2543
|
- name: Deploy to ${envName}
|
|
2544
|
-
run: npx @
|
|
2544
|
+
run: npx @agentlink.sh/cli@latest deploy --ci --env ${envName}
|
|
2545
2545
|
env:
|
|
2546
2546
|
SUPABASE_ACCESS_TOKEN: \${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
|
2547
2547
|
SUPABASE_DB_PASSWORD: \${{ secrets.SUPABASE_DB_PASSWORD }}
|
|
@@ -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();
|
|
@@ -4286,6 +4288,60 @@ ${red("Error:")} Supabase is not running.
|
|
|
4286
4288
|
// src/wizard.ts
|
|
4287
4289
|
var __dirname2 = path15.dirname(fileURLToPath2(import.meta.url));
|
|
4288
4290
|
var pkg2 = JSON.parse(fs15.readFileSync(path15.join(__dirname2, "..", "package.json"), "utf-8"));
|
|
4291
|
+
async function checkForCliUpdate(autoYes) {
|
|
4292
|
+
try {
|
|
4293
|
+
const latest = execSync(`npm view @agentlink.sh/cli version 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
4294
|
+
if (!latest || latest === pkg2.version) return;
|
|
4295
|
+
const current = pkg2.version.split(".").map(Number);
|
|
4296
|
+
const remote = latest.split(".").map(Number);
|
|
4297
|
+
const isNewer = remote[0] > current[0] || remote[0] === current[0] && remote[1] > current[1] || remote[0] === current[0] && remote[1] === current[1] && remote[2] > current[2];
|
|
4298
|
+
if (!isNewer) return;
|
|
4299
|
+
console.log(` ${amber("\u25B2")} New version available: ${dim(pkg2.version)} \u2192 ${bold(latest)}`);
|
|
4300
|
+
const shouldUpdate = autoYes || await confirm5({
|
|
4301
|
+
message: "Update now?",
|
|
4302
|
+
default: true,
|
|
4303
|
+
theme: theme5
|
|
4304
|
+
});
|
|
4305
|
+
if (!shouldUpdate) {
|
|
4306
|
+
console.log();
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
console.log(` ${blue("\u25CF")} Updating...`);
|
|
4310
|
+
execSync("npm install -g @agentlink.sh/cli@latest", { stdio: "ignore" });
|
|
4311
|
+
console.log(` ${blue("\u2714")} Updated to v${latest}`);
|
|
4312
|
+
console.log();
|
|
4313
|
+
const args = process.argv.slice(2).map((a) => JSON.stringify(a)).join(" ");
|
|
4314
|
+
execSync(`agentlink ${args}`, { stdio: "inherit", cwd: process.cwd() });
|
|
4315
|
+
process.exit(0);
|
|
4316
|
+
} catch {
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
async function checkForPluginUpdate(autoYes) {
|
|
4320
|
+
try {
|
|
4321
|
+
const listOutput = execSync("claude plugin list --json 2>/dev/null", { encoding: "utf-8" });
|
|
4322
|
+
const plugins = JSON.parse(listOutput);
|
|
4323
|
+
const installed = plugins.find((p) => p.id === "link@agentlink");
|
|
4324
|
+
if (!installed) return;
|
|
4325
|
+
const availableOutput = execSync("claude plugin list --json --available 2>/dev/null", { encoding: "utf-8" });
|
|
4326
|
+
const available = JSON.parse(availableOutput);
|
|
4327
|
+
const latest = available.find((p) => p.id === "link@agentlink");
|
|
4328
|
+
if (!latest || latest.version === installed.version) return;
|
|
4329
|
+
console.log(` ${amber("\u25B2")} Plugin update available: ${dim(installed.version)} \u2192 ${bold(latest.version)}`);
|
|
4330
|
+
const shouldUpdate = autoYes || await confirm5({
|
|
4331
|
+
message: "Update Agent Link plugin?",
|
|
4332
|
+
default: true,
|
|
4333
|
+
theme: theme5
|
|
4334
|
+
});
|
|
4335
|
+
if (!shouldUpdate) {
|
|
4336
|
+
console.log();
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
execSync(`claude plugin update link@agentlink --scope ${installed.scope}`, { stdio: "ignore" });
|
|
4340
|
+
console.log(` ${blue("\u2714")} Plugin updated to v${latest.version}`);
|
|
4341
|
+
console.log();
|
|
4342
|
+
} catch {
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4289
4345
|
var theme5 = {
|
|
4290
4346
|
prefix: { idle: blue("?"), done: blue("\u2714") },
|
|
4291
4347
|
style: {
|
|
@@ -4404,6 +4460,10 @@ async function wizard(initialName, initialPrompt, opts = {}) {
|
|
|
4404
4460
|
printBanner();
|
|
4405
4461
|
console.log(dim(` v${pkg2.version}`));
|
|
4406
4462
|
console.log();
|
|
4463
|
+
if (!nonInteractive) {
|
|
4464
|
+
await checkForCliUpdate(autoYes);
|
|
4465
|
+
await checkForPluginUpdate(autoYes);
|
|
4466
|
+
}
|
|
4407
4467
|
if (!opts.resume) {
|
|
4408
4468
|
let sessions;
|
|
4409
4469
|
if (initialName && initialName !== ".") {
|
|
@@ -4561,14 +4621,34 @@ Run without --resume to start fresh.`
|
|
|
4561
4621
|
}
|
|
4562
4622
|
if (!checkCommand("claude")) {
|
|
4563
4623
|
console.log(amber(" Installing Claude Code..."));
|
|
4564
|
-
|
|
4624
|
+
try {
|
|
4625
|
+
await runCommand("npm install -g @anthropic-ai/claude-code");
|
|
4626
|
+
} catch {
|
|
4627
|
+
await runCommand("sudo npm install -g @anthropic-ai/claude-code");
|
|
4628
|
+
}
|
|
4565
4629
|
console.log(blue(" \u2714 Claude Code installed."));
|
|
4566
4630
|
}
|
|
4567
4631
|
const authStatus = await runCommand("claude auth status").catch(() => "");
|
|
4568
4632
|
const isLoggedIn = authStatus.includes('"loggedIn": true') || authStatus.includes('"loggedIn":true');
|
|
4569
4633
|
if (!isLoggedIn) {
|
|
4570
4634
|
console.log(amber(" Claude Code requires authentication. Opening login..."));
|
|
4571
|
-
|
|
4635
|
+
try {
|
|
4636
|
+
execSync("claude auth login", { stdio: "inherit" });
|
|
4637
|
+
} catch {
|
|
4638
|
+
throw new Error(
|
|
4639
|
+
`Claude Code login failed.
|
|
4640
|
+
Run ${dim("claude auth login")} manually, then retry with:
|
|
4641
|
+
${dim("npx @agentlink.sh/cli@latest")}`
|
|
4642
|
+
);
|
|
4643
|
+
}
|
|
4644
|
+
const recheck = await runCommand("claude auth status").catch(() => "");
|
|
4645
|
+
if (!recheck.includes('"loggedIn": true') && !recheck.includes('"loggedIn":true')) {
|
|
4646
|
+
throw new Error(
|
|
4647
|
+
`Claude Code authentication did not complete.
|
|
4648
|
+
Run ${dim("claude auth login")} manually, then retry with:
|
|
4649
|
+
${dim("npx @agentlink.sh/cli@latest")}`
|
|
4650
|
+
);
|
|
4651
|
+
}
|
|
4572
4652
|
}
|
|
4573
4653
|
const hasSupabase = fs15.existsSync(path15.join(process.cwd(), "supabase", "config.toml"));
|
|
4574
4654
|
const hasPackageJson = fs15.existsSync(path15.join(process.cwd(), "package.json"));
|
|
@@ -4819,8 +4899,8 @@ program.command("info [name]").description("Show documentation about AgentLink c
|
|
|
4819
4899
|
});
|
|
4820
4900
|
program.command("login").description("Authenticate with Supabase via OAuth (opens browser)").action(async () => {
|
|
4821
4901
|
try {
|
|
4822
|
-
const { oauthLogin } = await import("./oauth-
|
|
4823
|
-
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-
|
|
4902
|
+
const { oauthLogin } = await import("./oauth-VTSNCG7U.js");
|
|
4903
|
+
const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-2AWCJAG5.js");
|
|
4824
4904
|
console.log();
|
|
4825
4905
|
const tokens = await oauthLogin();
|
|
4826
4906
|
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, {
|