@exposify/cli 0.0.2 → 0.0.4

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
@@ -1,6 +1,6 @@
1
1
  # @exposify/cli
2
2
 
3
- The framework-neutral engine behind `exposify-express` and `exposify-next`:
3
+ The framework-neutral engine behind `@exposify/express` and `@exposify/next`:
4
4
  the endpoint checklist, the startup-log question, config + clipboard + verify,
5
5
  tool descriptions (`describe`), and `inspect` snapshots. You don't use it
6
6
  directly: install an adapter (`@exposify/express`, `@exposify/nestjs`,
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ interface ScannedRoute {
23
23
  exposed: boolean;
24
24
  /** Exposed only when listed in `expose`, even a read (core's RouteInfo.optIn), e.g. a Pages Router method detected in code. */
25
25
  optIn?: boolean;
26
+ /** Not a tool unless listed in `expose`, so setup leaves it unticked though it's a read: a page that's just a form, a route that finishes a sign-in. */
27
+ offByDefault?: boolean;
26
28
  /** "page": an App Router page read as a tool (its HTML, like a browser), not a route handler. */
27
29
  kind?: "page";
28
30
  reason: string;
@@ -132,6 +134,22 @@ interface ScanResult {
132
134
  fields: string[];
133
135
  others?: string[];
134
136
  };
137
+ /**
138
+ * Supabase Auth with `@supabase/ssr` (Next.js): which sign-ins Connect
139
+ * makes on Supabase directly, and where the app signs in (plan
140
+ * `2026-10-24-supabase-connect.md` Part 2).
141
+ */
142
+ supabase?: {
143
+ direct: Array<"password" | "email-code" | "social">;
144
+ /** The sign-in calls found, one line each; empty: none found. */
145
+ found: string[];
146
+ /** Server sign-ins the form can't run. */
147
+ unusable?: string[];
148
+ /** Routes that finish a sign-in; Connect's sign-ins skip them. */
149
+ callbacks?: string[];
150
+ /** `@supabase/auth-helpers-nextjs`: an older cookie format, not supported. */
151
+ authHelpers?: boolean;
152
+ };
135
153
  }
136
154
  interface ScanFailure {
137
155
  ok: false;
@@ -199,8 +217,13 @@ interface ApplyPlan {
199
217
  after: string;
200
218
  }
201
219
  /** Either a plan, or why this one can't be applied safely (setup then prints the paste instructions). */
220
+ /** `also`: edits to other files that go with the plan, each with what to do by hand when it isn't written. */
202
221
  type ApplyOutcome = {
203
222
  plan: ApplyPlan;
223
+ also?: Array<{
224
+ plan: ApplyPlan;
225
+ manual: string;
226
+ }>;
204
227
  } | {
205
228
  blocked: string;
206
229
  };
@@ -278,12 +301,14 @@ interface ConfigStepInput<R extends ScanResult = ScanResult> {
278
301
  readLine: (site: CallSite) => string | undefined;
279
302
  }
280
303
  interface CliAdapter<R extends ScanResult = ScanResult> {
281
- /** The command name, e.g. "exposify-express". */
304
+ /** The command as users type it in hints, e.g. "npx @exposify/next". */
282
305
  bin: string;
283
306
  /** An example for "pass it explicitly", e.g. "src/server.ts". */
284
307
  entryExample: string;
285
308
  /** What `--entry` names, for the usage text, e.g. "The file that creates your Express app". */
286
309
  entryHelp: string;
310
+ /** The adapter's own commands, for the usage text: ["generate", "Writes …"]. Its bin runs them, not runCli. */
311
+ commands?: Array<[name: string, help: string]>;
287
312
  locate(cwd: string, entryFlag?: string): AppLocation | undefined;
288
313
  scan(location: AppLocation, env: Record<string, string>): Promise<AdapterScanOutcome<R>>;
289
314
  /**
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ import { DESCRIPTIONS_FILE, parseDescriptions } from "@exposify/core";
47
47
  // src/checklist.ts
48
48
  var routeKey = (r) => `${r.method} ${r.path}`;
49
49
  var isRead = (r) => r.method === "GET" || r.method === "HEAD";
50
+ var readByDefault = (r) => isRead(r) && !r.offByDefault;
50
51
  function isLocked(r) {
51
52
  return r.marker !== void 0;
52
53
  }
@@ -55,10 +56,10 @@ function initialState(routes, opts) {
55
56
  const listed = new Set(opts.listedActions ?? []);
56
57
  const selected = /* @__PURE__ */ new Set();
57
58
  for (const r of routes) {
58
- const ticked = opts.hasConfig || isLocked(r) ? r.exposed : isRead(r);
59
+ const ticked = opts.hasConfig || isLocked(r) ? r.exposed : readByDefault(r);
59
60
  if (ticked) selected.add(routeKey(r));
60
61
  }
61
- const readsOnly = new Set(routes.filter((r) => isLocked(r) ? r.exposed : isRead(r)).map(routeKey));
62
+ const readsOnly = new Set(routes.filter((r) => isLocked(r) ? r.exposed : readByDefault(r)).map(routeKey));
62
63
  return {
63
64
  routes,
64
65
  selected,
@@ -130,7 +131,7 @@ function reduce(state, action) {
130
131
  case "none":
131
132
  return setTicks(state, () => false, true);
132
133
  case "readsOnly":
133
- return setTicks(state, isRead, true);
134
+ return setTicks(state, readByDefault, true);
134
135
  case "filter":
135
136
  return { ...state, filter: action.text, cursor: 0 };
136
137
  case "enterNuclear": {
@@ -1345,14 +1346,18 @@ function openInBrowser(url) {
1345
1346
  }
1346
1347
 
1347
1348
  // src/run.ts
1348
- var usage = (bin, entryHelp) => `Usage: ${bin} <command> [options]
1349
+ var usage = ({ bin, entryHelp, commands: commands2 = [] }) => `Usage: ${bin} <command> [options]
1349
1350
 
1350
1351
  Commands:
1351
1352
  setup Pick which endpoints agents can call, then get the aiReady() config to paste
1352
1353
  inspect Show what agents see: every route, whether it's a tool, and why
1353
1354
  describe Give tools real descriptions (AI-proposed and reviewed, or your own) in
1354
1355
  ${DESCRIPTIONS_FILE}, which aiReady() loads
1355
-
1356
+ ${commands2.map(([name, help]) => name.length > 8 ? ` ${name}
1357
+ ${help}
1358
+ ` : ` ${name.padEnd(8)} ${help}
1359
+ `).join("")}${commands2.length ? ` (${bin} <command> --help for its options)
1360
+ ` : ""}
1356
1361
  Options:
1357
1362
  --entry <path> ${entryHelp} (default: detected)
1358
1363
  --env-vars <path> Load these variables into the scan (setup asks about .env otherwise)
@@ -1561,7 +1566,38 @@ function inspect(result, cwd) {
1561
1566
  if (result.actions && result.actions.length > 0) inspectActions(result, cwd);
1562
1567
  inspectLogin(result);
1563
1568
  }
1569
+ function supabaseSignIn(s, login) {
1570
+ if (s.authHelpers) return { row: "none: @supabase/auth-helpers-nextjs isn't supported", why: ["It keeps the session in an older cookie format. Move to @supabase/ssr, Supabase's replacement for it."] };
1571
+ const passwordDirect = s.direct.includes("password");
1572
+ const why = [];
1573
+ let row;
1574
+ if (s.found.length === 0) {
1575
+ row = "your Supabase Auth, directly";
1576
+ why.push("Couldn't find where your users sign in, so AI assistants sign in to Supabase directly: a password on /mcp/login, social and email codes on Exposify's page.");
1577
+ } else if (!passwordDirect && login) {
1578
+ row = `your app's own login for passwords (${login.what}); your Supabase Auth, directly, for social and email codes`;
1579
+ why.push('Your password sign-in runs on your server, which may hold your own checks, so the form runs it. Switch to "Sign in with Supabase" on the dashboard to skip it.');
1580
+ } else if (!passwordDirect) {
1581
+ row = "your Supabase Auth, directly, for social and email codes; no password sign-in";
1582
+ why.push(`Your app signs in on the server where the form can't run it: ${(s.unusable ?? []).join("; ")}.`);
1583
+ why.push('Switch on "Sign in with Supabase" on the dashboard to have passwords go to Supabase directly (it skips what that code checks).');
1584
+ } else {
1585
+ row = "your Supabase Auth, directly";
1586
+ why.push(`Your sign-in runs in the browser (${s.found[0]}), so there's no server check to skip.`);
1587
+ }
1588
+ if (s.callbacks?.length) why.push(`Connect's sign-ins don't go through ${s.callbacks.join(", ")}; anything those check is skipped.`);
1589
+ return { row, why };
1590
+ }
1564
1591
  function inspectLogin(result) {
1592
+ if (result.supabase) {
1593
+ const { row, why } = supabaseSignIn(result.supabase, result.login);
1594
+ console.log(`
1595
+ Sign-in for AI assistants: ${row}.`);
1596
+ for (const line of why) console.log(dim(` ${line}`));
1597
+ if (result.supabase.found.length > 1) for (const f of result.supabase.found) console.log(dim(` found ${f}`));
1598
+ if (result.supabase.direct.length && !result.supabase.authHelpers) console.log(dim(" Check it on a running app: npx @exposify/next test-sign-in"));
1599
+ return;
1600
+ }
1565
1601
  if (!result.login) return;
1566
1602
  console.log(`
1567
1603
  Sign-in for AI assistants: your app's own login, ${result.login.what} (${result.login.fields.join(", ")}).`);
@@ -1748,7 +1784,7 @@ ${bold("Step 2/5")} \xB7 Print "[agentready] MCP ready \u2026" when your server
1748
1784
  console.log(`
1749
1785
  ${green("\u2713 Already set up")}: your aiReady() config already matches. Nothing to change.`);
1750
1786
  if (tty && !args.quick) {
1751
- await signInStep(args, root, cwd, "Step 4/5", result.login);
1787
+ await signInStep(args, root, cwd, "Step 4/5", result.login, result.supabase);
1752
1788
  const code2 = await describeStep(args, toolRows(result.routes, result.actions), root, cwd, "Step 5/5");
1753
1789
  if (code2 !== 0) return code2;
1754
1790
  }
@@ -1764,6 +1800,7 @@ ${bold("Step 3/5")} \xB7 Your aiReady() config${copyNote}
1764
1800
  `);
1765
1801
  console.log(code.replace(/^/gm, " "));
1766
1802
  let applied = false;
1803
+ let manual = apply && "plan" in apply ? (apply.also ?? []).map((a) => a.manual) : [];
1767
1804
  if (args.apply !== false && (args.apply || tty && !args.quick)) {
1768
1805
  if (!apply) {
1769
1806
  if (args.apply) console.log(yellow("\n Can't apply automatically here: paste it as described below."));
@@ -1772,20 +1809,31 @@ ${bold("Step 3/5")} \xB7 Your aiReady() config${copyNote}
1772
1809
  Can't apply automatically: ${apply.blocked}. Paste it as described below.`));
1773
1810
  } else {
1774
1811
  const { summary } = apply.plan;
1812
+ const also = apply.also ?? [];
1813
+ const all = [summary, ...also.map((a) => a.plan.summary.replace(/^./, (c) => c.toLowerCase()))];
1775
1814
  const yes = args.apply === true || ["y", "yes"].includes((await ask(`
1776
- Apply this: ${summary}? ${dim("(y/N)")} `)).toLowerCase());
1815
+ Apply this: ${all.join(", and ")}? ${dim("(y/N)")} `)).toLowerCase());
1777
1816
  if (yes) {
1778
1817
  const written = writePlan(apply.plan);
1779
1818
  if (written.ok) {
1780
1819
  applied = true;
1781
1820
  console.log(green(` \u2713 Applied: ${summary}.`) + dim(apply.plan.before === null ? "" : " Review it with git diff."));
1821
+ manual = [];
1822
+ for (const a of also) {
1823
+ const done = writePlan(a.plan);
1824
+ if (done.ok) console.log(green(` \u2713 Applied: ${a.plan.summary}.`) + dim(" Review it with git diff."));
1825
+ else {
1826
+ console.log(yellow(` Couldn't apply: ${a.plan.summary} (${done.error}).`));
1827
+ manual.push(a.manual);
1828
+ }
1829
+ }
1782
1830
  } else console.log(yellow(` Couldn't apply: ${written.error}. Paste it as described below.`));
1783
1831
  }
1784
1832
  }
1785
1833
  }
1786
1834
  if (!applied) console.log(`
1787
1835
  ${hint.lines[0]}`);
1788
- for (const line of hint.lines.slice(1)) console.log(` ${line}`);
1836
+ for (const line of [...hint.lines.slice(1), ...manual]) console.log(` ${line}`);
1789
1837
  if (hint.warning) console.log(yellow(` ! ${hint.warning}`));
1790
1838
  if (droppedGlobs.length > 0) {
1791
1839
  console.log(yellow(` ! Dropped exclude ${droppedGlobs.map((g) => `"${g}"`).join(", ")}: it would hide endpoints you ticked.`));
@@ -1834,7 +1882,7 @@ ${bold("Step 3/5")} \xB7 Your aiReady() config${copyNote}
1834
1882
  }
1835
1883
  const finalRoutes = verified ?? toolRows(routesAsSelected(registered, selected), actionRows.map((a) => ({ ...a, exposed: a.marker ? a.exposed : pickedActions.has(a.configKey) })));
1836
1884
  if (tty && !args.quick) {
1837
- await signInStep(args, root, cwd, "Step 4/5", result.login);
1885
+ await signInStep(args, root, cwd, "Step 4/5", result.login, result.supabase);
1838
1886
  const code2 = await describeStep(args, finalRoutes, root, cwd, "Step 5/5");
1839
1887
  if (code2 !== 0) return code2;
1840
1888
  const proposed = await proposeStep(args, verifiedActions, root, cwd);
@@ -1879,7 +1927,7 @@ ${bold("Optional")} \xB7 Save the tools agents get to ${shown}?`);
1879
1927
  }
1880
1928
  if (save) writeSnapshot(routes, file, cwd);
1881
1929
  }
1882
- async function signInStep(args, root, cwd, title, login) {
1930
+ async function signInStep(args, root, cwd, title, login, supabase) {
1883
1931
  if (!args.signIn) return;
1884
1932
  const stacks = detectAuthStacks(root);
1885
1933
  const oauth = stacks.find((s) => s.path === "oauth");
@@ -1892,13 +1940,17 @@ ${bold(title)} \xB7 How your users sign in`);
1892
1940
  console.log(dim(" The README's OAuth section lists the settings to turn on in the provider's dashboard."));
1893
1941
  return;
1894
1942
  }
1895
- const own = login ? `your app's own login, ${login.what} (${login.fields.join(", ")})` : null;
1896
- if (own) console.log(` Found ${own}. AI assistants sign in with it on a form your app serves at /mcp/login, so there's nothing to set up for it.`);
1943
+ const sb = supabase && !supabase.authHelpers ? supabaseSignIn(supabase, login) : null;
1944
+ const own = sb ? sb.row : login ? `your app's own login, ${login.what} (${login.fields.join(", ")})` : null;
1945
+ if (sb) {
1946
+ console.log(` AI assistants sign in with ${sb.row}. Nothing to set up for it.`);
1947
+ for (const line of sb.why) console.log(dim(` ${line}`));
1948
+ } else if (own) console.log(` Found ${own}. AI assistants sign in with it on a form your app serves at /mcp/login, so there's nothing to set up for it.`);
1897
1949
  const signedOut = "agents use your app signed out, and your own checks decide what they can do";
1898
1950
  const have = existingKey(root, process.env);
1899
1951
  if (have) {
1900
1952
  console.log(
1901
- green(` \u2713 ${KEY_ENV} is set (${have.from}):`) + (own ? " agents sign in with your app's login, through Exposify's broker." : ` until you add a sign-in method in the dashboard, ${signedOut}.`)
1953
+ green(` \u2713 ${KEY_ENV} is set (${have.from}):`) + (sb ? ` agents sign in with ${sb.row}, through Exposify's broker.` : own ? " agents sign in with your app's login, through Exposify's broker." : ` until you add a sign-in method in the dashboard, ${signedOut}.`)
1902
1954
  );
1903
1955
  return;
1904
1956
  }
@@ -1931,7 +1983,7 @@ ${bold(title)} \xB7 How your users sign in`);
1931
1983
  process.env[KEY_ENV] = outcome.key;
1932
1984
  console.log(green(` \u2713 ${written.created ? "Created" : "Updated"} ${relative(cwd, written.path) || written.path} with ${KEY_ENV}`) + ` (project "${outcome.projectName}").`);
1933
1985
  if (!written.ignored) console.log(yellow(` ! Add .env to .gitignore: it now holds a secret.`));
1934
- if (own) console.log(` Your app tells Exposify about its login when it starts with ${KEY_ENV}: nothing to add in the dashboard.`);
1986
+ if (own) console.log(` Your app tells Exposify about its ${sb ? "Supabase Auth" : "login"} when it starts with ${KEY_ENV}: nothing to add in the dashboard.`);
1935
1987
  else {
1936
1988
  console.log(` No login form was found, so ${signedOut}.`);
1937
1989
  console.log(` To have agents act as a signed-in user, add a sign-in method in the dashboard (presets for GitHub, Google, Supabase, \u2026).`);
@@ -2155,11 +2207,11 @@ async function runCli(argv, forAdapter, cwd = process.cwd()) {
2155
2207
  args = parseArgs(argv);
2156
2208
  } catch (err) {
2157
2209
  console.error(red(err.message));
2158
- console.error(usage(adapter.bin, adapter.entryHelp));
2210
+ console.error(usage(adapter));
2159
2211
  return 2;
2160
2212
  }
2161
2213
  if (args.help || !args.command) {
2162
- console.log(usage(adapter.bin, adapter.entryHelp));
2214
+ console.log(usage(adapter));
2163
2215
  return args.help ? 0 : 2;
2164
2216
  }
2165
2217
  if (args.ci) disableColor();
@@ -2180,7 +2232,7 @@ async function runCli(argv, forAdapter, cwd = process.cwd()) {
2180
2232
  if (args.command === "setup") return setup(args, cwd);
2181
2233
  if (args.command === "describe") return describeCommand(args, cwd);
2182
2234
  console.error(red(`unknown command: ${args.command}`));
2183
- console.error(usage(adapter.bin, adapter.entryHelp));
2235
+ console.error(usage(adapter));
2184
2236
  return 2;
2185
2237
  }
2186
2238
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@babel/parser": "^8.0.0",
25
- "@exposify/core": "0.3.14"
25
+ "@exposify/core": "0.3.16"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.0.0",
@@ -33,7 +33,7 @@
33
33
  "publishConfig": {
34
34
  "access": "public"
35
35
  },
36
- "description": "The framework-neutral engine behind exposify-express and exposify-next: guided setup, inspect and describe for Exposify adapters.",
36
+ "description": "The framework-neutral engine behind @exposify/express and @exposify/next: guided setup, inspect and describe for Exposify adapters.",
37
37
  "author": "Exposify",
38
38
  "keywords": [
39
39
  "mcp",