@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, zero questions with name + prompt)
45
- agentlink my-app "Build a project management tool with kanban boards"
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 (asks project name and frontend choice)
50
+ # Interactive wizard
48
51
  agentlink
49
52
  ```
50
53
 
51
54
  ## Commands
52
55
 
53
- ### `agentlink [name] [prompt]`
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 name and prompt (zero questions express mode)
65
- agentlink my-app "Build a project management tool with kanban boards"
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-J4X62CY4.js");
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-J4X62CY4.js");
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({
@@ -20,7 +20,7 @@ import {
20
20
  updateAuthConfig,
21
21
  updatePostgrestConfig,
22
22
  waitForProjectReady
23
- } from "./chunk-DOWH47I5.js";
23
+ } from "./chunk-2CNK2BQY.js";
24
24
  import "./chunk-G3HVUCWY.js";
25
25
  export {
26
26
  createProject,
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  updatePostgrestConfig,
27
27
  validateProjectName,
28
28
  waitForProjectReady
29
- } from "./chunk-DOWH47I5.js";
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-N7CILHLD.js");
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 (checkCommand("psql")) {
3668
- spinner.text = "Checking prerequisites \u2014 PostgreSQL client installed";
3669
- } else {
3670
- throw new Error(
3671
- "psql is required but not installed.\n macOS: brew install libpq && brew link --force libpq\n Ubuntu: sudo apt install postgresql-client\n See: https://www.postgresql.org/download/"
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
- const launchArgs = prompt ? `${JSON.stringify(prompt)}` : "";
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, initialPrompt, opts = {}) {
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
- execSync("claude auth login", { stdio: "inherit" });
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 && initialPrompt) {
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 = initialPrompt ?? "";
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 projectDir = mode === "existing" ? process.cwd() : path15.resolve(process.cwd(), name);
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 (mode === "new") {
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").argument("[prompt]", "What do you want to build?").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 (alternative to positional arg)").option("--resume", "Resume a previously failed scaffold").option("--non-interactive", "Error instead of prompting when info is missing").action(async (name, prompt) => {
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, prompt, {
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-J4X62CY4.js");
4885
- const { saveCredentials: saveCredentials2, loadCredentials: loadCredentials2, credentialsPath: credentialsPath2 } = await import("./cloud-N7CILHLD.js");
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 TIMEOUT_MS = 12e4;
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
- async function oauthLogin() {
91
- const codeVerifier = generateCodeVerifier();
92
- const codeChallenge = generateCodeChallenge(codeVerifier);
93
- const state = randomBytes(16).toString("hex");
94
- const port = await findOpenPort();
95
- const redirectUri = `http://localhost:${port}/callback`;
96
- const authorizeUrl = new URL(AUTHORIZE_URL);
97
- authorizeUrl.searchParams.set("client_id", OAUTH_CLIENT_ID);
98
- authorizeUrl.searchParams.set("redirect_uri", redirectUri);
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(timeout);
121
+ clearTimeout(timer);
121
122
  server.close();
122
- reject(new Error(`OAuth error: ${desc}`));
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(timeout);
131
+ clearTimeout(timer);
130
132
  server.close();
131
- reject(new Error("OAuth state mismatch."));
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(timeout);
140
+ clearTimeout(timer);
139
141
  server.close();
140
- reject(new Error("No authorization code in callback."));
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(timeout);
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 exchangeCode(code, redirectUri, codeVerifier);
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, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlink.sh/cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for scaffolding Supabase apps with AI agents",
5
5
  "bin": {
6
6
  "agentlink": "dist/index.js"