@outfitter/cli 0.4.0 → 0.5.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.
@@ -0,0 +1,357 @@
1
+ // @bun
2
+ // packages/cli/src/flags.ts
3
+ function createPreset(config) {
4
+ const preset = {
5
+ id: config.id,
6
+ options: config.options,
7
+ resolve: config.resolve
8
+ };
9
+ preset[PRESET_IDS] = [config.id];
10
+ return preset;
11
+ }
12
+ var PRESET_IDS = Symbol("presetIds");
13
+ function getPresetIds(preset) {
14
+ const ids = preset[PRESET_IDS];
15
+ return ids && ids.length > 0 ? ids : [preset.id];
16
+ }
17
+ function composePresets(...presets) {
18
+ const seen = new Set;
19
+ const mergedIds = [];
20
+ const mergedOptions = [];
21
+ const resolvers = [];
22
+ for (const preset of presets) {
23
+ const presetIds = getPresetIds(preset);
24
+ if (presetIds.every((id) => seen.has(id)))
25
+ continue;
26
+ for (const id of presetIds) {
27
+ if (seen.has(id))
28
+ continue;
29
+ seen.add(id);
30
+ mergedIds.push(id);
31
+ }
32
+ mergedOptions.push(...preset.options);
33
+ resolvers.push(preset.resolve);
34
+ }
35
+ const composed = {
36
+ id: mergedIds.join("+"),
37
+ options: mergedOptions,
38
+ resolve: (flags) => {
39
+ let result = {};
40
+ for (const resolver of resolvers) {
41
+ result = { ...result, ...resolver(flags) };
42
+ }
43
+ return result;
44
+ }
45
+ };
46
+ composed[PRESET_IDS] = mergedIds;
47
+ return composed;
48
+ }
49
+ function verbosePreset() {
50
+ return createPreset({
51
+ id: "verbose",
52
+ options: [
53
+ {
54
+ flags: "-v, --verbose",
55
+ description: "Verbose output",
56
+ defaultValue: false
57
+ }
58
+ ],
59
+ resolve: (flags) => ({
60
+ verbose: Boolean(flags["verbose"])
61
+ })
62
+ });
63
+ }
64
+ function cwdPreset() {
65
+ return createPreset({
66
+ id: "cwd",
67
+ options: [
68
+ {
69
+ flags: "--cwd <path>",
70
+ description: "Working directory"
71
+ }
72
+ ],
73
+ resolve: (flags) => ({
74
+ cwd: typeof flags["cwd"] === "string" ? flags["cwd"] : process.cwd()
75
+ })
76
+ });
77
+ }
78
+ function dryRunPreset() {
79
+ return createPreset({
80
+ id: "dryRun",
81
+ options: [
82
+ {
83
+ flags: "--dry-run",
84
+ description: "Preview changes without applying",
85
+ defaultValue: false
86
+ }
87
+ ],
88
+ resolve: (flags) => ({
89
+ dryRun: Boolean(flags["dryRun"] ?? flags["dry-run"])
90
+ })
91
+ });
92
+ }
93
+ function forcePreset() {
94
+ return createPreset({
95
+ id: "force",
96
+ options: [
97
+ {
98
+ flags: "-f, --force",
99
+ description: "Force operation (skip confirmations)",
100
+ defaultValue: false
101
+ }
102
+ ],
103
+ resolve: (flags) => ({
104
+ force: Boolean(flags["force"])
105
+ })
106
+ });
107
+ }
108
+ function interactionPreset() {
109
+ return createPreset({
110
+ id: "interaction",
111
+ options: [
112
+ {
113
+ flags: "--non-interactive",
114
+ description: "Disable interactive prompts",
115
+ defaultValue: false
116
+ },
117
+ {
118
+ flags: "--no-input",
119
+ description: "Disable interactive prompts (alias)"
120
+ },
121
+ {
122
+ flags: "-y, --yes",
123
+ description: "Auto-confirm prompts",
124
+ defaultValue: false
125
+ }
126
+ ],
127
+ resolve: (flags) => {
128
+ const nonInteractive = flags["input"] === false || Boolean(flags["nonInteractive"]) || Boolean(flags["non-interactive"]) || Boolean(flags["noInput"]) || Boolean(flags["no-input"]);
129
+ return {
130
+ interactive: !nonInteractive,
131
+ yes: Boolean(flags["yes"])
132
+ };
133
+ }
134
+ });
135
+ }
136
+ function strictPreset() {
137
+ return createPreset({
138
+ id: "strict",
139
+ options: [
140
+ {
141
+ flags: "--strict",
142
+ description: "Enable strict mode (treat warnings as errors)",
143
+ defaultValue: false
144
+ }
145
+ ],
146
+ resolve: (flags) => ({
147
+ strict: Boolean(flags["strict"])
148
+ })
149
+ });
150
+ }
151
+ var COLOR_MODES = new Set(["auto", "always", "never"]);
152
+ function colorPreset() {
153
+ return createPreset({
154
+ id: "color",
155
+ options: [
156
+ {
157
+ flags: "--color [mode]",
158
+ description: "Color output mode (auto, always, never)",
159
+ defaultValue: "auto"
160
+ },
161
+ {
162
+ flags: "--no-color",
163
+ description: "Disable color output"
164
+ }
165
+ ],
166
+ resolve: (flags) => {
167
+ const raw = flags["color"];
168
+ if (raw === false)
169
+ return { color: "never" };
170
+ if (raw === true)
171
+ return { color: "always" };
172
+ if (typeof raw === "string" && COLOR_MODES.has(raw)) {
173
+ return { color: raw };
174
+ }
175
+ return { color: "auto" };
176
+ }
177
+ });
178
+ }
179
+ function parseCommaSeparated(value) {
180
+ if (typeof value !== "string")
181
+ return;
182
+ const items = value.split(",").map((s) => s.trim()).filter(Boolean);
183
+ return items.length > 0 ? items : undefined;
184
+ }
185
+ function projectionPreset() {
186
+ return createPreset({
187
+ id: "projection",
188
+ options: [
189
+ {
190
+ flags: "--fields <fields>",
191
+ description: "Comma-separated list of fields to include"
192
+ },
193
+ {
194
+ flags: "--exclude-fields <fields>",
195
+ description: "Comma-separated list of fields to exclude"
196
+ },
197
+ {
198
+ flags: "--count",
199
+ description: "Output only the count of results",
200
+ defaultValue: false
201
+ }
202
+ ],
203
+ resolve: (flags) => ({
204
+ fields: parseCommaSeparated(flags["fields"]),
205
+ excludeFields: parseCommaSeparated(flags["excludeFields"] ?? flags["exclude-fields"]),
206
+ count: Boolean(flags["count"])
207
+ })
208
+ });
209
+ }
210
+ var DURATION_SUFFIXES = {
211
+ w: 7 * 24 * 60 * 60 * 1000,
212
+ d: 24 * 60 * 60 * 1000,
213
+ h: 60 * 60 * 1000,
214
+ m: 60 * 1000
215
+ };
216
+ function parseDate(value, nowMs = Date.now()) {
217
+ if (typeof value !== "string" || value === "")
218
+ return;
219
+ const durationMatch = value.match(/^(\d+(?:\.\d+)?)(w|d|h|m)$/);
220
+ if (durationMatch) {
221
+ const amount = Number(durationMatch[1]);
222
+ const suffix = durationMatch[2];
223
+ const multiplier = suffix ? DURATION_SUFFIXES[suffix] : undefined;
224
+ if (amount > 0 && multiplier !== undefined) {
225
+ return new Date(nowMs - amount * multiplier);
226
+ }
227
+ return;
228
+ }
229
+ const date = new Date(value);
230
+ if (Number.isNaN(date.getTime()))
231
+ return;
232
+ return date;
233
+ }
234
+ function sanitizePositiveInteger(value, fallback) {
235
+ const parsed = Number(value);
236
+ if (!Number.isFinite(parsed) || parsed <= 0)
237
+ return fallback;
238
+ return Math.floor(parsed);
239
+ }
240
+ function timeWindowPreset(config) {
241
+ return createPreset({
242
+ id: "timeWindow",
243
+ options: [
244
+ {
245
+ flags: "--since <date>",
246
+ description: "Start of time window (ISO date or duration: 7d, 24h, 2w)"
247
+ },
248
+ {
249
+ flags: "--until <date>",
250
+ description: "End of time window (ISO date or duration: 7d, 24h, 2w)"
251
+ }
252
+ ],
253
+ resolve: (flags) => {
254
+ const now = Date.now();
255
+ const since = parseDate(flags["since"], now);
256
+ const until = parseDate(flags["until"], now);
257
+ if (since && until && typeof config?.maxRange === "number" && Number.isFinite(config.maxRange) && config.maxRange > 0) {
258
+ const range = Math.abs(until.getTime() - since.getTime());
259
+ if (range > config.maxRange) {
260
+ return {
261
+ since: undefined,
262
+ until: undefined
263
+ };
264
+ }
265
+ }
266
+ return { since, until };
267
+ }
268
+ });
269
+ }
270
+ function executionPreset(config) {
271
+ const defaultTimeout = config?.defaultTimeout;
272
+ const defaultRetries = config?.defaultRetries ?? 0;
273
+ const maxRetries = config?.maxRetries ?? 10;
274
+ return createPreset({
275
+ id: "execution",
276
+ options: [
277
+ {
278
+ flags: "--timeout <ms>",
279
+ description: "Timeout in milliseconds"
280
+ },
281
+ {
282
+ flags: "--retries <n>",
283
+ description: `Number of retries (default: ${defaultRetries}, max: ${maxRetries})`
284
+ },
285
+ {
286
+ flags: "--offline",
287
+ description: "Operate in offline mode",
288
+ defaultValue: false
289
+ }
290
+ ],
291
+ resolve: (flags) => {
292
+ let timeout = defaultTimeout;
293
+ const rawTimeout = flags["timeout"];
294
+ if (rawTimeout !== undefined) {
295
+ const parsed = Number(rawTimeout);
296
+ if (Number.isFinite(parsed) && parsed > 0) {
297
+ timeout = Math.floor(parsed);
298
+ } else {
299
+ timeout = undefined;
300
+ }
301
+ }
302
+ let retries = defaultRetries;
303
+ const rawRetries = flags["retries"];
304
+ if (rawRetries !== undefined) {
305
+ const parsed = Number(rawRetries);
306
+ if (Number.isFinite(parsed) && parsed >= 0) {
307
+ retries = Math.min(Math.floor(parsed), maxRetries);
308
+ }
309
+ }
310
+ return {
311
+ timeout,
312
+ retries,
313
+ offline: Boolean(flags["offline"])
314
+ };
315
+ }
316
+ });
317
+ }
318
+ function paginationPreset(config) {
319
+ const maxLimit = sanitizePositiveInteger(config?.maxLimit, 100);
320
+ const defaultLimit = Math.min(sanitizePositiveInteger(config?.defaultLimit, 20), maxLimit);
321
+ return createPreset({
322
+ id: "pagination",
323
+ options: [
324
+ {
325
+ flags: "-l, --limit <n>",
326
+ description: `Maximum number of results (default: ${defaultLimit}, max: ${maxLimit})`
327
+ },
328
+ {
329
+ flags: "--next",
330
+ description: "Continue from last position",
331
+ defaultValue: false
332
+ },
333
+ {
334
+ flags: "--reset",
335
+ description: "Clear saved cursor and start fresh",
336
+ defaultValue: false
337
+ }
338
+ ],
339
+ resolve: (flags) => {
340
+ let limit = defaultLimit;
341
+ const rawLimit = flags["limit"];
342
+ if (rawLimit !== undefined) {
343
+ const parsed = Number(rawLimit);
344
+ if (Number.isFinite(parsed) && parsed > 0) {
345
+ limit = Math.min(Math.floor(parsed), maxLimit);
346
+ }
347
+ }
348
+ return {
349
+ limit,
350
+ next: Boolean(flags["next"]),
351
+ reset: Boolean(flags["reset"])
352
+ };
353
+ }
354
+ });
355
+ }
356
+
357
+ export { createPreset, composePresets, verbosePreset, cwdPreset, dryRunPreset, forcePreset, interactionPreset, strictPreset, colorPreset, projectionPreset, timeWindowPreset, executionPreset, paginationPreset };
@@ -0,0 +1,319 @@
1
+ // @bun
2
+ // packages/cli/src/schema.ts
3
+ import { mkdir, writeFile } from "fs/promises";
4
+ import { dirname, join } from "path";
5
+ import {
6
+ diffSurfaceMaps,
7
+ formatManifestMarkdown,
8
+ generateManifest,
9
+ generateSurfaceMap,
10
+ readSurfaceMap,
11
+ resolveSnapshotPath,
12
+ writeSurfaceMap
13
+ } from "@outfitter/schema";
14
+ import { Command } from "commander";
15
+ import { generateManifest as generateManifest2 } from "@outfitter/schema";
16
+ function formatManifestHuman(manifest, programName, actionId) {
17
+ if (actionId) {
18
+ return formatActionDetail(manifest, actionId);
19
+ }
20
+ return formatSummary(manifest, programName);
21
+ }
22
+ function formatSummary(manifest, programName) {
23
+ const lines = [];
24
+ const name = programName ?? "cli";
25
+ const actionCount = manifest.actions.length;
26
+ const surfaceCount = manifest.surfaces.length;
27
+ const surfaceLabel = surfaceCount === 1 ? `${surfaceCount} surface` : `${surfaceCount} surfaces`;
28
+ lines.push(`${name} \u2014 ${actionCount} actions across ${surfaceLabel}`);
29
+ lines.push("");
30
+ const grouped = new Map;
31
+ const ungrouped = [];
32
+ for (const action of manifest.actions) {
33
+ const group = action.cli?.group;
34
+ if (group) {
35
+ const existing = grouped.get(group) ?? [];
36
+ existing.push(action);
37
+ grouped.set(group, existing);
38
+ } else {
39
+ ungrouped.push(action);
40
+ }
41
+ }
42
+ for (const [groupName, groupActions] of grouped.entries()) {
43
+ lines.push(groupName);
44
+ for (const action of groupActions) {
45
+ const commandPart = action.cli?.command ?? action.id;
46
+ const isBase = !commandPart || commandPart.startsWith("[") || commandPart.startsWith("<");
47
+ const displayCommand = isBase ? ` ${groupName} ${commandPart ?? ""}`.trimEnd() : ` ${groupName} ${commandPart}`;
48
+ const desc = action.cli?.description ?? action.description ?? "";
49
+ lines.push(padCommand(displayCommand, desc));
50
+ }
51
+ lines.push("");
52
+ }
53
+ for (const action of ungrouped) {
54
+ const commandPart = action.cli?.command ?? action.id;
55
+ const desc = action.cli?.description ?? action.description ?? "";
56
+ lines.push(padCommand(`${commandPart}`, desc));
57
+ }
58
+ lines.push("");
59
+ lines.push("Use --output json for machine-readable format.");
60
+ lines.push("Use --surface <name> to filter (cli, mcp, api, server).");
61
+ return lines.join(`
62
+ `);
63
+ }
64
+ function padCommand(command, description) {
65
+ const padding = Math.max(1, 32 - command.length);
66
+ return `${command}${" ".repeat(padding)}${description}`;
67
+ }
68
+ function formatActionDetail(manifest, actionId) {
69
+ const entry = manifest.actions.find((a) => a.id === actionId);
70
+ if (!entry) {
71
+ return `Unknown action: ${actionId}`;
72
+ }
73
+ const lines = [];
74
+ const desc = entry.cli?.description ?? entry.description ?? "";
75
+ lines.push(`${entry.id} \u2014 ${desc}`);
76
+ lines.push("");
77
+ if (entry.cli) {
78
+ const group = entry.cli.group;
79
+ const commandPart = entry.cli.command ?? entry.id;
80
+ const fullCommand = group ? `${group} ${commandPart}` : commandPart;
81
+ lines.push(` Command: ${fullCommand}`);
82
+ }
83
+ lines.push(` Surfaces: ${entry.surfaces.join(", ")}`);
84
+ if (entry.cli?.group) {
85
+ lines.push(` Group: ${entry.cli.group}`);
86
+ }
87
+ if (entry.cli?.aliases && entry.cli.aliases.length > 0) {
88
+ lines.push(` Aliases: ${entry.cli.aliases.join(", ")}`);
89
+ }
90
+ if (entry.cli?.options && entry.cli.options.length > 0) {
91
+ lines.push("");
92
+ lines.push(" Options:");
93
+ for (const opt of entry.cli.options) {
94
+ const defaultStr = opt.defaultValue !== undefined ? ` [${String(opt.defaultValue)}]` : "";
95
+ lines.push(padCommand(` ${opt.flags}`, `${opt.description}${defaultStr}`));
96
+ }
97
+ }
98
+ if (entry.mcp) {
99
+ lines.push("");
100
+ lines.push(" MCP:");
101
+ if (entry.mcp.tool) {
102
+ lines.push(` Tool: ${entry.mcp.tool}`);
103
+ }
104
+ if (entry.mcp.description) {
105
+ lines.push(` Description: ${entry.mcp.description}`);
106
+ }
107
+ }
108
+ return lines.join(`
109
+ `);
110
+ }
111
+ function handleShow(source, programName, parentCmd, actionArg, cmdOptions) {
112
+ const manifestOptions = {};
113
+ if (cmdOptions.surface) {
114
+ manifestOptions.surface = cmdOptions.surface;
115
+ }
116
+ const manifest = generateManifest(source, manifestOptions);
117
+ if (cmdOptions.output === "json") {
118
+ const indent = cmdOptions.pretty ? 2 : undefined;
119
+ if (actionArg) {
120
+ const entry = manifest.actions.find((a) => a.id === actionArg);
121
+ if (!entry) {
122
+ process.stderr.write(`Unknown action: ${actionArg}
123
+ `);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ process.stdout.write(`${JSON.stringify(entry, null, indent)}
128
+ `);
129
+ } else {
130
+ process.stdout.write(`${JSON.stringify(manifest, null, indent)}
131
+ `);
132
+ }
133
+ return;
134
+ }
135
+ const resolvedName = programName ?? parentCmd.parent?.name() ?? undefined;
136
+ const output = formatManifestHuman(manifest, resolvedName, actionArg);
137
+ process.stdout.write(`${output}
138
+ `);
139
+ }
140
+ function createSchemaCommand(source, options) {
141
+ const cmd = new Command("schema").description("Show CLI schema for machine or human consumption").argument("[action]", "Show detail for a specific action").option("--output <mode>", "Output mode (human, json)", "human").option("--surface <name>", "Filter by surface (cli, mcp, api, server)").option("--pretty", "Pretty-print JSON output").action((actionArg, cmdOptions) => {
142
+ handleShow(source, options?.programName, cmd, actionArg, cmdOptions);
143
+ });
144
+ const showCmd = new Command("show").description("Show schema for all actions or a specific action").argument("[action]", "Show detail for a specific action").option("--output <mode>", "Output mode (human, json)", "human").option("--surface <name>", "Filter by surface (cli, mcp, api, server)").option("--pretty", "Pretty-print JSON output").action((actionArg, cmdOptions) => {
145
+ handleShow(source, options?.programName, cmd, actionArg, cmdOptions);
146
+ });
147
+ cmd.addCommand(showCmd);
148
+ if (options?.surface) {
149
+ const surfaceOpts = options.surface;
150
+ const cwd = surfaceOpts.cwd ?? process.cwd();
151
+ const outputDir = surfaceOpts.outputDir ?? ".outfitter";
152
+ const generateCmd = new Command("generate").description("Generate surface map and write to disk").option("--dry-run", "Print surface map without writing to disk").option("--snapshot <version>", "Write snapshot to .outfitter/snapshots/<version>.json").action(async (genOptions) => {
153
+ const surfaceMap = generateSurfaceMap(source, {
154
+ generator: "build"
155
+ });
156
+ if (genOptions.dryRun) {
157
+ process.stdout.write(`${JSON.stringify(surfaceMap, null, 2)}
158
+ `);
159
+ return;
160
+ }
161
+ if (genOptions.snapshot) {
162
+ const snapshotPath = resolveSnapshotPath(cwd, outputDir, genOptions.snapshot);
163
+ await writeSurfaceMap(surfaceMap, snapshotPath);
164
+ process.stdout.write(`Snapshot written to ${snapshotPath}
165
+ `);
166
+ return;
167
+ }
168
+ const outputPath = join(cwd, outputDir, "surface.json");
169
+ await writeSurfaceMap(surfaceMap, outputPath);
170
+ process.stdout.write(`Surface map written to ${outputPath}
171
+ `);
172
+ });
173
+ cmd.addCommand(generateCmd);
174
+ const diffCmd = new Command("diff").description("Compare runtime schema against committed surface map").option("--output <mode>", "Output mode (human, json)", "human").option("--against <version>", "Compare runtime against a named snapshot").option("--from <version>", "Base snapshot for snapshot-to-snapshot diff").option("--to <version>", "Target snapshot for snapshot-to-snapshot diff").action(async (diffOptions) => {
175
+ let left;
176
+ let right;
177
+ if (diffOptions.from && !diffOptions.to || !diffOptions.from && diffOptions.to) {
178
+ process.stderr.write(`Both --from and --to are required for snapshot-to-snapshot diff.
179
+ `);
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ if (diffOptions.from && diffOptions.to) {
184
+ const fromPath = resolveSnapshotPath(cwd, outputDir, diffOptions.from);
185
+ const toPath = resolveSnapshotPath(cwd, outputDir, diffOptions.to);
186
+ try {
187
+ left = await readSurfaceMap(fromPath);
188
+ } catch (err) {
189
+ if (err.code === "ENOENT") {
190
+ process.stderr.write(`No snapshot at ${fromPath}
191
+ `);
192
+ } else {
193
+ process.stderr.write(`Failed to read snapshot at ${fromPath}: ${err instanceof Error ? err.message : String(err)}
194
+ `);
195
+ }
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ try {
200
+ right = await readSurfaceMap(toPath);
201
+ } catch (err) {
202
+ if (err.code === "ENOENT") {
203
+ process.stderr.write(`No snapshot at ${toPath}
204
+ `);
205
+ } else {
206
+ process.stderr.write(`Failed to read snapshot at ${toPath}: ${err instanceof Error ? err.message : String(err)}
207
+ `);
208
+ }
209
+ process.exitCode = 1;
210
+ return;
211
+ }
212
+ } else if (diffOptions.against) {
213
+ const snapshotPath = resolveSnapshotPath(cwd, outputDir, diffOptions.against);
214
+ try {
215
+ left = await readSurfaceMap(snapshotPath);
216
+ } catch (err) {
217
+ if (err.code === "ENOENT") {
218
+ process.stderr.write(`No snapshot at ${snapshotPath}
219
+ `);
220
+ } else {
221
+ process.stderr.write(`Failed to read snapshot at ${snapshotPath}: ${err instanceof Error ? err.message : String(err)}
222
+ `);
223
+ }
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+ right = generateSurfaceMap(source, { generator: "runtime" });
228
+ } else {
229
+ const surfacePath = join(cwd, outputDir, "surface.json");
230
+ try {
231
+ left = await readSurfaceMap(surfacePath);
232
+ } catch (err) {
233
+ if (err.code === "ENOENT") {
234
+ process.stderr.write(`No committed surface map at ${surfacePath}
235
+ `);
236
+ process.stderr.write(`Run 'schema generate' first.
237
+ `);
238
+ } else {
239
+ process.stderr.write(`Failed to read surface map at ${surfacePath}: ${err instanceof Error ? err.message : String(err)}
240
+ `);
241
+ }
242
+ process.exitCode = 1;
243
+ return;
244
+ }
245
+ right = generateSurfaceMap(source, { generator: "runtime" });
246
+ }
247
+ const result = diffSurfaceMaps(left, right);
248
+ if (diffOptions.output === "json") {
249
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
250
+ `);
251
+ } else {
252
+ if (!result.hasChanges) {
253
+ process.stdout.write(`No schema drift detected.
254
+ `);
255
+ return;
256
+ }
257
+ const lines = [];
258
+ lines.push(`Schema drift detected:
259
+ `);
260
+ if (result.added.length > 0) {
261
+ lines.push(" Added:");
262
+ for (const entry of result.added) {
263
+ lines.push(` + ${entry.id}`);
264
+ }
265
+ }
266
+ if (result.removed.length > 0) {
267
+ lines.push(" Removed:");
268
+ for (const entry of result.removed) {
269
+ lines.push(` - ${entry.id}`);
270
+ }
271
+ }
272
+ if (result.modified.length > 0) {
273
+ lines.push(" Modified:");
274
+ for (const entry of result.modified) {
275
+ lines.push(` ~ ${entry.id} (${entry.changes.join(", ")})`);
276
+ }
277
+ }
278
+ if (result.metadataChanges.length > 0) {
279
+ lines.push(" Metadata:");
280
+ for (const field of result.metadataChanges) {
281
+ lines.push(` ~ ${field}`);
282
+ }
283
+ }
284
+ process.stdout.write(`${lines.join(`
285
+ `)}
286
+ `);
287
+ }
288
+ if (result.hasChanges) {
289
+ process.exitCode = 1;
290
+ }
291
+ });
292
+ cmd.addCommand(diffCmd);
293
+ const docsCmd = new Command("docs").description("Generate markdown reference documentation").option("--surface <name>", "Which surface to document (cli, mcp)", "mcp").option("--output-dir <dir>", "Directory to write the reference doc", "docs/reference").option("--dry-run", "Print to stdout instead of writing to disk").action(async (docsOptions) => {
294
+ const surface = docsOptions.surface ?? "mcp";
295
+ if (surface !== "mcp" && surface !== "cli") {
296
+ process.stderr.write(`Unsupported surface for docs: "${surface}". Use "mcp" or "cli".
297
+ `);
298
+ process.exitCode = 1;
299
+ return;
300
+ }
301
+ const manifest = generateManifest(source, { surface });
302
+ const markdown = formatManifestMarkdown(manifest, { surface });
303
+ if (docsOptions.dryRun) {
304
+ process.stdout.write(markdown);
305
+ return;
306
+ }
307
+ const outDir = docsOptions.outputDir ?? "docs/reference";
308
+ const outputPath = join(cwd, outDir, `${surface.toUpperCase()}_REFERENCE.md`);
309
+ await mkdir(dirname(outputPath), { recursive: true });
310
+ await writeFile(outputPath, markdown, "utf-8");
311
+ process.stdout.write(`Reference written to ${outputPath}
312
+ `);
313
+ });
314
+ cmd.addCommand(docsCmd);
315
+ }
316
+ return cmd;
317
+ }
318
+
319
+ export { formatManifestHuman, createSchemaCommand, generateManifest2 as generateManifest };
@@ -1,4 +1,4 @@
1
- import { OutputOptions } from "./cli-02kyvj7h";
1
+ import { OutputOptions } from "./cli-md9347gn";
2
2
  /**
3
3
  * Output data to the console with automatic mode selection.
4
4
  *