@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 +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +69 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @exposify/cli
|
|
2
2
|
|
|
3
|
-
The framework-neutral engine behind
|
|
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
|
|
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 :
|
|
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 :
|
|
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,
|
|
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: ${
|
|
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
|
|
1896
|
-
|
|
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
|
|
2210
|
+
console.error(usage(adapter));
|
|
2159
2211
|
return 2;
|
|
2160
2212
|
}
|
|
2161
2213
|
if (args.help || !args.command) {
|
|
2162
|
-
console.log(usage(adapter
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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",
|