@exposify/cli 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Exposify
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,10 @@
1
- # Temporary Holding Version
1
+ # @exposify/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The framework-neutral engine behind `exposify-express` and `exposify-next`:
4
+ the endpoint checklist, the startup-log question, config + clipboard + verify,
5
+ tool descriptions (`describe`), and `inspect` snapshots. You don't use it
6
+ directly: install an adapter (`@exposify/express`, `@exposify/nestjs`,
7
+ `@exposify/next`) and run its command.
8
+
9
+ An adapter implements `CliAdapter` (`locate`, `scan`, `configStep`) and its bin
10
+ calls `runCli(process.argv.slice(2), adapter)`.
@@ -0,0 +1,318 @@
1
+ /** A source location in the user's code, from a stack trace. */
2
+ interface CallSite {
3
+ file: string;
4
+ line: number;
5
+ }
6
+ type MarkerKind = "exposed" | "hidden" | "described";
7
+ interface ScannedRoute {
8
+ method: string;
9
+ path: string;
10
+ params: string[];
11
+ /** Set when the route is wrapped in exposed()/hidden()/describe() in code. */
12
+ marker?: MarkerKind;
13
+ /** describe()'s own name/description, when given. */
14
+ describedName?: string;
15
+ describedText?: string;
16
+ /** The matching OpenAPI operation's summary/description, from aiReady({ openapi }). */
17
+ openApiText?: string;
18
+ /** The tool name it gets (or would get) under the current config. */
19
+ tool: string;
20
+ /** deriveDescription()'s text, the fallback an agent sees today. */
21
+ derived?: string;
22
+ /** Exposure under the app's current aiReady() options (defaults if none). */
23
+ exposed: boolean;
24
+ /** Exposed only when listed in `expose`, even a read (core's RouteInfo.optIn), e.g. a Pages Router method detected in code. */
25
+ optIn?: boolean;
26
+ /** "page": an App Router page read as a tool (its HTML, like a browser), not a route handler. */
27
+ kind?: "page";
28
+ reason: string;
29
+ /** The route's own handler (last in its stack) as source text, capped; for `describe`. */
30
+ source?: string;
31
+ /** Names of the functions ahead of it on the route (e.g. auth middleware). */
32
+ middleware?: string[];
33
+ }
34
+ /**
35
+ * A server-side function agents can call besides routes (Next.js Server
36
+ * Actions). The adapter works out everything here; the CLI only shows it,
37
+ * lets the user pick, and checks it.
38
+ */
39
+ interface ScannedAction {
40
+ /** The key it's listed under in `aiReady({ actions })`. */
41
+ configKey: string;
42
+ /** Where it's defined, e.g. "placeOrder" in app/actions/checkout.ts:12. */
43
+ export: string;
44
+ file: string;
45
+ line: number;
46
+ /** The tool name it gets (or would get). */
47
+ tool: string;
48
+ /** What its tool call is, for the snapshot: "POST /mcp/actions/place_order". */
49
+ route: string;
50
+ /**
51
+ * complete: its input is known from its code and forms; partial: some of
52
+ * it isn't (see notes); flagged: it looks like sign-in/out, or takes a file.
53
+ */
54
+ status: "complete" | "partial" | "flagged";
55
+ /** One line each: what's unresolved, which forms submit to it, why it's flagged. */
56
+ notes: string[];
57
+ /** Set when exposed()/hidden()/describe() in code decides it. */
58
+ marker?: MarkerKind;
59
+ exposed: boolean;
60
+ reason: string;
61
+ /** Where its hand-written input and the scan disagree. */
62
+ drift: string[];
63
+ /** Its `actions` entry to paste, key included: `placeOrder: true,`, or `{ input: { … } }` with TODO lines for the gaps. */
64
+ stub: string;
65
+ /** Its source, capped; for `describe`. */
66
+ source?: string;
67
+ /** A description set in config or describe(): `describe` leaves it alone. */
68
+ describedText?: string;
69
+ /**
70
+ * An input `describe` can propose: set for an action that's opted in but
71
+ * whose input the scan can't complete (and isn't written). `status` is
72
+ * whether exposify.descriptions.json already has an accepted one.
73
+ */
74
+ proposal?: {
75
+ status: "needed" | "current" | "stale";
76
+ /** The files `request.forms` was read from, project-relative: named when asking before sending. */
77
+ formFiles?: string[];
78
+ /** What the service gets (source is masked before sending). */
79
+ request: {
80
+ name: string;
81
+ file: string;
82
+ source?: string;
83
+ forms?: string[];
84
+ fields: Array<{
85
+ name: string;
86
+ type: string;
87
+ required?: boolean;
88
+ enum?: string[];
89
+ }>;
90
+ unresolved: string[];
91
+ };
92
+ };
93
+ }
94
+ /** The JSON-safe part of the current aiReady() options. */
95
+ interface CurrentOptions {
96
+ expose?: string[] | "*";
97
+ exclude?: string[];
98
+ startupLog?: boolean;
99
+ /** The keys listed in `actions` (Server Actions). */
100
+ actions?: string[];
101
+ /** Other option keys present (auth, oauth, mounts, ...): kept as-is by the wizard. */
102
+ otherKeys: string[];
103
+ }
104
+ /** What every adapter's scan reports; adapters extend it with their own facts (e.g. Express's `uses`). */
105
+ interface ScanResult {
106
+ ok: true;
107
+ /** Set when the app threw during boot after registering routes; routes up to then are scanned. */
108
+ crashed?: string;
109
+ /** Things the adapter couldn't turn into routes, printed after the count, e.g. handlers whose methods weren't detected. */
110
+ notes?: string[];
111
+ /** The current aiReady() call, when there is one. */
112
+ aiReady?: {
113
+ site?: CallSite;
114
+ options: CurrentOptions;
115
+ };
116
+ routes: ScannedRoute[];
117
+ /** Server Actions (Next.js), when the framework has them. */
118
+ actions?: ScannedAction[];
119
+ /** Printed under the actions when any is exposed or picked, e.g. that middleware path rules don't cover them. */
120
+ actionsNote?: string;
121
+ /** Code changes the scan suggests (e.g. moving an inline Server Action into its own file): `setup` prints them, nothing applies them. */
122
+ suggestions?: Array<{
123
+ title: string;
124
+ diff: string;
125
+ }>;
126
+ /**
127
+ * The app's own login, as the adapter found it: with the hosted broker, AI
128
+ * assistants sign in with it on a form at `/mcp/login` (Connect with no code).
129
+ */
130
+ login?: {
131
+ what: string;
132
+ fields: string[];
133
+ others?: string[];
134
+ };
135
+ }
136
+ interface ScanFailure {
137
+ ok: false;
138
+ error: string;
139
+ /** A one-line suggestion. */
140
+ fix?: string;
141
+ }
142
+ type ScanMessage<R extends ScanResult = ScanResult> = R | ScanFailure;
143
+
144
+ interface WizardConfig {
145
+ expose?: string[] | "*";
146
+ exclude?: string[];
147
+ startupLog?: false;
148
+ /** Server Actions to list in `actions`, by config key; the adapter renders their entries. */
149
+ actions?: string[];
150
+ }
151
+ interface BuiltConfig {
152
+ config: WizardConfig;
153
+ /** Existing glob excludes dropped because they'd hide a ticked route. */
154
+ droppedGlobs: string[];
155
+ }
156
+ /**
157
+ * The minimal config for `selected`:
158
+ * - ticked writes go in `expose` (an explicit list unless `nuclear`), and
159
+ * so do ticked opt-in rows, reads included (`expose: "*"` skips those);
160
+ * - un-ticked reads, and in nuclear mode un-ticked writes too, become
161
+ * method-qualified `exclude` entries (they beat `expose`);
162
+ * - existing glob excludes are kept, since they also cover future routes,
163
+ * unless one would hide a route that's now ticked;
164
+ * - rows with a marker in code are left out: the marker decides them.
165
+ */
166
+ declare function buildConfig(routes: ScannedRoute[], selected: Set<string>, opts: {
167
+ nuclear: boolean;
168
+ startupLog: boolean;
169
+ current?: CurrentOptions;
170
+ }): BuiltConfig;
171
+ /** Which routes `config` exposes, by core's own policy: used to check buildConfig and to verify a paste. */
172
+ declare function exposedUnder(routes: ScannedRoute[], config: WizardConfig): Set<string>;
173
+ /**
174
+ * The aiReady() call to paste. `importFrom` adds the import line (the app
175
+ * doesn't import aiReady yet), from that package.
176
+ */
177
+ declare function renderConfig(config: WizardConfig, opts: {
178
+ importFrom?: string;
179
+ otherKeys?: string[];
180
+ }): string;
181
+ /** The option lines `config` adds to aiReady({ … }), indented two spaces, as `setup` writes them (and `--apply` edits in). */
182
+ declare function renderProps(config: WizardConfig): string[];
183
+
184
+ /** Where to put the config: what an adapter tells the user in step 3. */
185
+ interface PasteHint {
186
+ /** Sentences to print, the first one a full instruction. */
187
+ lines: string[];
188
+ /** A problem with the current placement, e.g. aiReady after the routers on Express 5. */
189
+ warning?: string;
190
+ }
191
+
192
+ /** One file edit, computed ahead of time so setup can say what it would do. */
193
+ interface ApplyPlan {
194
+ file: string;
195
+ /** For the prompt: "Replace the aiReady() options in src/server.ts". */
196
+ summary: string;
197
+ /** The file as the plan saw it; null when the plan creates it. Writing refuses if it changed since. */
198
+ before: string | null;
199
+ after: string;
200
+ }
201
+ /** Either a plan, or why this one can't be applied safely (setup then prints the paste instructions). */
202
+ type ApplyOutcome = {
203
+ plan: ApplyPlan;
204
+ } | {
205
+ blocked: string;
206
+ };
207
+ interface ReplaceCallInput {
208
+ source: string;
209
+ file: string;
210
+ /** The call's line, from the scan. */
211
+ line: number;
212
+ /** Function names the call may have: ["aiReady"], or ["mountAiReady"] for @exposify/nestjs. */
213
+ callNames: string[];
214
+ /** Which argument is the options object (aiReady({ … }) is 0, aiReady(exposify, { … }) is 1). */
215
+ optionsArg: number;
216
+ config: WizardConfig;
217
+ /** Server Actions: config key -> the entry to write, for a key that has none yet (an existing entry is kept as written). */
218
+ actionEntries?: Record<string, string>;
219
+ /** An import to make sure is there (e.g. zod, when an action stub uses it). */
220
+ needsImport?: {
221
+ from: string;
222
+ name: string;
223
+ };
224
+ }
225
+ /**
226
+ * Rewrites the options of an existing aiReady() call to `config`. Only the
227
+ * options setup manages (expose, exclude, startupLog, and actions when the
228
+ * adapter passes entries) are replaced; everything else the user wrote stays.
229
+ */
230
+ declare function replaceAiReadyOptions(input: ReplaceCallInput): {
231
+ after: string;
232
+ } | {
233
+ blocked: string;
234
+ };
235
+ interface InsertCallInput {
236
+ source: string;
237
+ file: string;
238
+ /** The statement to add, e.g. `app.use(aiReady({ … }));`, written for an app variable called `app`. */
239
+ code: string;
240
+ importFrom: string;
241
+ /** Insert above the statement that starts on this line (required on Express 5: aiReady must come before the routers)… */
242
+ beforeLine?: number;
243
+ /** …else after the statement that starts on this line. */
244
+ afterLine?: number;
245
+ /** The line where the app is created, to learn what the app variable is called. */
246
+ createdLine?: number;
247
+ }
248
+ /** Adds a new `app.use(aiReady(…))` statement, and its import, to the file that creates the app. */
249
+ declare function insertAiReadyCall(input: InsertCallInput): {
250
+ after: string;
251
+ } | {
252
+ blocked: string;
253
+ };
254
+
255
+ interface AppLocation {
256
+ /** The file the scan starts from (an entry file, or the app directory). */
257
+ entry: string;
258
+ /** The project root: where the descriptions file and snapshot live. */
259
+ root: string;
260
+ /** How it was found, e.g. "scripts.dev" or "nest-cli.json". */
261
+ via: string;
262
+ }
263
+ type AdapterScanOutcome<R extends ScanResult = ScanResult> = {
264
+ ok: true;
265
+ result: R;
266
+ } | {
267
+ ok: false;
268
+ error: string;
269
+ fix?: string;
270
+ output?: string;
271
+ loader?: string;
272
+ };
273
+ interface ConfigStepInput<R extends ScanResult = ScanResult> {
274
+ result: R;
275
+ config: WizardConfig;
276
+ current?: CurrentOptions;
277
+ root: string;
278
+ readLine: (site: CallSite) => string | undefined;
279
+ }
280
+ interface CliAdapter<R extends ScanResult = ScanResult> {
281
+ /** The command name, e.g. "exposify-express". */
282
+ bin: string;
283
+ /** An example for "pass it explicitly", e.g. "src/server.ts". */
284
+ entryExample: string;
285
+ /** What `--entry` names, for the usage text, e.g. "The file that creates your Express app". */
286
+ entryHelp: string;
287
+ locate(cwd: string, entryFlag?: string): AppLocation | undefined;
288
+ scan(location: AppLocation, env: Record<string, string>): Promise<AdapterScanOutcome<R>>;
289
+ /**
290
+ * Step 3: the code to paste, and where it goes. `apply` is the same change
291
+ * made in the user's own file (`setup --apply`): a plan, or why it can't be
292
+ * done safely. Omit it when an adapter can't edit code.
293
+ */
294
+ configStep(input: ConfigStepInput<R>): {
295
+ code: string;
296
+ hint: PasteHint;
297
+ apply?: ApplyOutcome;
298
+ };
299
+ }
300
+
301
+ /** CI providers set CI (GitHub Actions, GitLab, CircleCI, Buildkite, ...); "false"/"0" opt out. */
302
+ declare function isCiEnv(env: NodeJS.ProcessEnv): boolean;
303
+ /** Runs the CLI for `forAdapter`; resolves to the exit code. */
304
+ declare function runCli<R extends ScanResult>(argv: string[], forAdapter: CliAdapter<R>, cwd?: string): Promise<number>;
305
+
306
+ declare const routeKey: (r: Pick<ScannedRoute, "method" | "path">) => string;
307
+ declare const isRead: (r: Pick<ScannedRoute, "method">) => boolean;
308
+ /** Rows whose exposure is set in code (exposed()/hidden()/describe()); config can't change them. */
309
+ declare function isLocked(r: Pick<ScannedRoute, "marker">): boolean;
310
+
311
+ /**
312
+ * What a description was written from: method, path, params and the
313
+ * handler's source. A change means the text may no longer be true. Only
314
+ * staleness uses it; it never decides precedence.
315
+ */
316
+ declare function signatureFor(route: Pick<ScannedRoute, "method" | "path" | "params" | "source">): string;
317
+
318
+ export { type AdapterScanOutcome, type AppLocation, type ApplyOutcome, type ApplyPlan, type BuiltConfig, type CallSite, type CliAdapter, type ConfigStepInput, type CurrentOptions, type MarkerKind, type PasteHint, type ScanFailure, type ScanMessage, type ScanResult, type ScannedAction, type ScannedRoute, type WizardConfig, buildConfig, exposedUnder, insertAiReadyCall, isCiEnv, isLocked, isRead, renderConfig, renderProps, replaceAiReadyOptions, routeKey, runCli, signatureFor };