@outfitter/cli 0.5.3 → 1.0.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.
Files changed (106) hide show
  1. package/README.md +105 -2
  2. package/dist/actions.d.ts +5 -2
  3. package/dist/actions.js +2 -2
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +8 -1
  6. package/dist/command.d.ts +3 -43
  7. package/dist/command.js +241 -13
  8. package/dist/envelope.d.ts +5 -0
  9. package/dist/envelope.js +160 -0
  10. package/dist/flags.d.ts +5 -189
  11. package/dist/flags.js +5 -1
  12. package/dist/hints.d.ts +34 -0
  13. package/dist/hints.js +26 -0
  14. package/dist/index.d.ts +3 -2
  15. package/dist/input.d.ts +3 -124
  16. package/dist/input.js +14 -359
  17. package/dist/internal/envelope-helpers.d.ts +4 -0
  18. package/dist/internal/envelope-helpers.js +24 -0
  19. package/dist/internal/envelope-types.d.ts +3 -0
  20. package/dist/internal/envelope-types.js +1 -0
  21. package/dist/internal/flag-builders.d.ts +3 -0
  22. package/dist/internal/flag-builders.js +155 -0
  23. package/dist/internal/flag-types.d.ts +3 -0
  24. package/dist/internal/flag-types.js +13 -0
  25. package/dist/internal/hint-action-graph.d.ts +5 -0
  26. package/dist/internal/hint-action-graph.js +11 -0
  27. package/dist/internal/hint-command-tree.d.ts +5 -0
  28. package/dist/internal/hint-command-tree.js +9 -0
  29. package/dist/internal/hint-error-recovery.d.ts +2 -0
  30. package/dist/internal/hint-error-recovery.js +7 -0
  31. package/dist/internal/hint-types.d.ts +4 -0
  32. package/dist/internal/hint-types.js +1 -0
  33. package/dist/internal/input-helpers.d.ts +18 -0
  34. package/dist/internal/input-helpers.js +11 -0
  35. package/dist/internal/input-normalization.d.ts +3 -0
  36. package/dist/internal/input-normalization.js +9 -0
  37. package/dist/internal/input-parsers.d.ts +3 -0
  38. package/dist/internal/input-parsers.js +19 -0
  39. package/dist/internal/input-security.d.ts +22 -0
  40. package/dist/internal/input-security.js +11 -0
  41. package/dist/internal/output-formatting.d.ts +3 -0
  42. package/dist/internal/output-formatting.js +21 -0
  43. package/dist/internal/presets.d.ts +3 -0
  44. package/dist/{shared/@outfitter/cli-pdb7znbq.js → internal/presets.js} +49 -223
  45. package/dist/internal/schema-commands.d.ts +3 -0
  46. package/dist/{shared/@outfitter/cli-5vtr4bdt.js → internal/schema-commands.js} +8 -99
  47. package/dist/internal/schema-formatting.d.ts +2 -0
  48. package/dist/internal/schema-formatting.js +7 -0
  49. package/dist/internal/schema-types.d.ts +2 -0
  50. package/dist/internal/schema-types.js +1 -0
  51. package/dist/output.d.ts +4 -3
  52. package/dist/output.js +13 -166
  53. package/dist/pagination.d.ts +1 -1
  54. package/dist/query.d.ts +84 -2
  55. package/dist/query.js +8 -45
  56. package/dist/schema-input.d.ts +80 -0
  57. package/dist/schema-input.js +15 -0
  58. package/dist/schema.d.ts +4 -1
  59. package/dist/schema.js +1 -1
  60. package/dist/shared/@outfitter/cli-10wxfc78.d.ts +45 -0
  61. package/dist/shared/@outfitter/cli-16wg5mka.d.ts +71 -0
  62. package/dist/shared/@outfitter/cli-1q5redaj.js +267 -0
  63. package/dist/shared/@outfitter/cli-2dfxs239.js +98 -0
  64. package/dist/shared/@outfitter/cli-30mt7c5w.d.ts +112 -0
  65. package/dist/shared/@outfitter/cli-3jta1h1h.js +134 -0
  66. package/dist/shared/@outfitter/cli-4h85mpth.js +76 -0
  67. package/dist/shared/@outfitter/cli-6shkwxdc.js +28 -0
  68. package/dist/shared/@outfitter/cli-89335n9a.js +16 -0
  69. package/dist/shared/@outfitter/cli-8999qjdd.js +3 -0
  70. package/dist/shared/@outfitter/cli-8cfxdady.js +60 -0
  71. package/dist/shared/@outfitter/cli-bcajqy33.d.ts +25 -0
  72. package/dist/shared/@outfitter/cli-c09332vm.d.ts +39 -0
  73. package/dist/shared/@outfitter/cli-cgha038c.d.ts +3 -0
  74. package/dist/shared/@outfitter/{cli-zahqsaby.js → cli-d40m2x1d.js} +19 -3
  75. package/dist/shared/@outfitter/cli-dg0cz7rw.js +127 -0
  76. package/dist/shared/@outfitter/cli-dv8kk4jw.d.ts +24 -0
  77. package/dist/shared/@outfitter/cli-g43887b7.js +20 -0
  78. package/dist/shared/@outfitter/cli-gqtkhgw4.js +52 -0
  79. package/dist/shared/@outfitter/cli-h4ejpmjs.d.ts +104 -0
  80. package/dist/shared/@outfitter/cli-htzez8v2.js +70 -0
  81. package/dist/shared/@outfitter/cli-hvg2m5gf.js +79 -0
  82. package/dist/shared/@outfitter/cli-n54zs151.d.ts +78 -0
  83. package/dist/shared/@outfitter/cli-nbpgw7z7.d.ts +15 -0
  84. package/dist/shared/@outfitter/cli-nkt399zf.d.ts +94 -0
  85. package/dist/shared/@outfitter/cli-pmd04gtv.d.ts +60 -0
  86. package/dist/shared/@outfitter/{cli-xy3gs50c.d.ts → cli-q6csxmeh.d.ts} +19 -12
  87. package/dist/shared/@outfitter/cli-qcskd96y.d.ts +11 -0
  88. package/dist/shared/@outfitter/cli-ry7btmy4.js +118 -0
  89. package/dist/shared/@outfitter/cli-sy99pjyj.js +32 -0
  90. package/dist/shared/@outfitter/cli-tm2fzngs.d.ts +23 -0
  91. package/dist/shared/@outfitter/cli-vvvhjwks.js +106 -0
  92. package/dist/shared/@outfitter/cli-wjv7g1aq.d.ts +16 -0
  93. package/dist/shared/@outfitter/{cli-98aa9104.d.ts → cli-x6qr7bnd.d.ts} +338 -16
  94. package/dist/shared/@outfitter/cli-xde45xcc.d.ts +53 -0
  95. package/dist/shared/@outfitter/cli-xw8ys1je.d.ts +123 -0
  96. package/dist/shared/@outfitter/cli-yfewnyc2.d.ts +43 -0
  97. package/dist/shared/@outfitter/cli-zkzj0q4q.js +99 -0
  98. package/dist/shared/@outfitter/cli-zv3ah6f0.js +3 -0
  99. package/dist/streaming.d.ts +47 -0
  100. package/dist/streaming.js +13 -0
  101. package/dist/truncation.d.ts +104 -0
  102. package/dist/truncation.js +111 -0
  103. package/dist/types.d.ts +2 -2
  104. package/dist/verbs.d.ts +1 -1
  105. package/package.json +55 -25
  106. package/dist/shared/@outfitter/cli-n1k0d23k.d.ts +0 -33
package/README.md CHANGED
@@ -361,6 +361,105 @@ if (flags.reset) {
361
361
  }
362
362
  ```
363
363
 
364
+ ## NDJSON Streaming (`@outfitter/cli/streaming`)
365
+
366
+ Real-time progress reporting via newline-delimited JSON. Handlers emit progress events through an optional `ctx.progress` callback; the CLI adapter writes them as NDJSON lines to stdout.
367
+
368
+ ```typescript
369
+ import { streamPreset } from "@outfitter/cli/query";
370
+ import { runHandler } from "@outfitter/cli/envelope";
371
+
372
+ command("deploy")
373
+ .preset(streamPreset())
374
+ .action(async ({ flags, input }) => {
375
+ await runHandler({
376
+ command: "deploy",
377
+ handler: async (input, ctx) => {
378
+ ctx.progress?.({ type: "progress", current: 1, total: 3 });
379
+ // ... work ...
380
+ return Result.ok({ status: "deployed" });
381
+ },
382
+ input,
383
+ stream: Boolean(flags.stream),
384
+ });
385
+ });
386
+ ```
387
+
388
+ ```
389
+ $ mycli deploy --stream
390
+ {"type":"start","command":"deploy","ts":"2025-01-01T00:00:00.000Z"}
391
+ {"type":"progress","current":1,"total":3}
392
+ {"ok":true,"command":"deploy","result":{"status":"deployed"}}
393
+ ```
394
+
395
+ `--stream` is orthogonal to output mode — it controls delivery (streaming vs batch), not serialization. Types live in `@outfitter/contracts/stream`.
396
+
397
+ Low-level utilities (`writeNdjsonLine`, `writeStreamEnvelope`, `createNdjsonProgress`) are exported from `@outfitter/cli/streaming` for custom setups.
398
+
399
+ ## Output Truncation (`@outfitter/cli/truncation`)
400
+
401
+ Truncate array output with pagination hints and optional file pointers for very large results.
402
+
403
+ ```typescript
404
+ import { truncateOutput } from "@outfitter/cli/truncation";
405
+
406
+ const result = truncateOutput(items, {
407
+ limit: 20,
408
+ offset: 0,
409
+ commandName: "list",
410
+ });
411
+ // result.data — sliced items (length <= 20)
412
+ // result.metadata — { showing: 20, total: 500, truncated: true }
413
+ // result.hints — [{ description: "Show next 20 of 480...", command: "list --offset 20 --limit 20" }]
414
+ ```
415
+
416
+ When output exceeds `filePointerThreshold` (default: 1000 items), the full result is written to a temp file and `metadata.full_output` contains the file path. File write failures degrade gracefully with a warning hint instead of crashing.
417
+
418
+ **Exports:** `truncateOutput`, `TruncationOptions`, `TruncationResult`, `TruncationMetadata`, `DEFAULT_FILE_POINTER_THRESHOLD`
419
+
420
+ ## CommandBuilder
421
+
422
+ The `command()` builder provides a fluent API for defining CLI commands with typed input schemas, safety metadata, and hint generation:
423
+
424
+ ```typescript
425
+ import { command } from "@outfitter/cli/command";
426
+ import { runHandler } from "@outfitter/cli/envelope";
427
+
428
+ command("delete <id>")
429
+ .description("Delete a resource")
430
+ .input(z.object({ id: z.string(), force: z.boolean().default(false) }))
431
+ .destructive(true) // auto-adds --dry-run flag
432
+ .relatedTo("list", { description: "List remaining resources" })
433
+ .hints((result, input) => [
434
+ { description: "Verify", command: `show ${input.id}` },
435
+ ])
436
+ .onError((error, input) => [
437
+ { description: "Check exists", command: `show ${input.id}` },
438
+ ])
439
+ .action(async ({ input, flags }) => {
440
+ await runHandler({
441
+ command: "delete",
442
+ handler: deleteHandler,
443
+ input,
444
+ dryRun: Boolean(flags.dryRun),
445
+ });
446
+ })
447
+ .build();
448
+ ```
449
+
450
+ **Safety metadata methods:**
451
+
452
+ | Method | Effect |
453
+ | ------------------- | ----------------------------------------------------------------------------- |
454
+ | `.destructive()` | Auto-adds `--dry-run` flag; `runHandler({ dryRun })` generates execution hint |
455
+ | `.readOnly()` | Surfaces in command tree JSON and maps to MCP `readOnlyHint` |
456
+ | `.idempotent()` | Surfaces in command tree JSON and maps to MCP `idempotentHint` |
457
+ | `.relatedTo()` | Declares action graph edges for tier-4 hint generation |
458
+ | `.input(schema)` | Zod schema for auto-derived flags and validation |
459
+ | `.context(factory)` | Async factory for handler context construction |
460
+ | `.hints(fn)` | Success hint function called with `(result, input)` |
461
+ | `.onError(fn)` | Error hint function called with `(error, input)` |
462
+
364
463
  ## Conventions
365
464
 
366
465
  Composable flag presets provide typed, reusable CLI flag definitions. See the [full conventions guide](../../docs/cli/conventions.md) for the complete catalog.
@@ -384,13 +483,17 @@ command("deploy")
384
483
  });
385
484
  ```
386
485
 
387
- **Available presets:** `verbosePreset`, `cwdPreset`, `dryRunPreset`, `forcePreset`, `interactionPreset`, `strictPreset`, `colorPreset`, `projectionPreset`, `paginationPreset`, `timeWindowPreset`, `executionPreset`, `outputModePreset`, `jqPreset`
486
+ **Available presets:** `verbosePreset`, `cwdPreset`, `dryRunPreset`, `forcePreset`, `interactionPreset`, `strictPreset`, `colorPreset`, `projectionPreset`, `paginationPreset`, `timeWindowPreset`, `executionPreset`, `outputModePreset`, `jqPreset`, `streamPreset`
388
487
 
389
488
  **Additional modules:**
390
489
 
391
490
  - `@outfitter/cli/verbs` — Standard verb families (`create`, `modify`, `remove`, `list`, `show`)
392
- - `@outfitter/cli/query` — Output mode and jq expression presets
491
+ - `@outfitter/cli/query` — Output mode, jq expression, and streaming presets
393
492
  - `@outfitter/cli/completion` — Shell completion script generation
493
+ - `@outfitter/cli/streaming` — NDJSON streaming primitives
494
+ - `@outfitter/cli/truncation` — Output truncation with pagination hints
495
+ - `@outfitter/cli/envelope` — Response envelope construction and `runHandler()` lifecycle
496
+ - `@outfitter/cli/hints` — Hint generation tiers (command tree, error recovery, schema params, action graph)
394
497
 
395
498
  ## Configuration
396
499
 
package/dist/actions.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { SchemaCommandOptions } from "./shared/@outfitter/cli-n1k0d23k.js";
2
- import { FlagPreset } from "./shared/@outfitter/cli-98aa9104.js";
1
+ import "./shared/@outfitter/cli-cgha038c.js";
2
+ import "./shared/@outfitter/cli-nbpgw7z7.js";
3
+ import { SchemaCommandOptions } from "./shared/@outfitter/cli-wjv7g1aq.js";
4
+ import "./shared/@outfitter/cli-qcskd96y.js";
5
+ import { FlagPreset } from "./shared/@outfitter/cli-x6qr7bnd.js";
3
6
  import { ActionCliInputContext, ActionCliOption, ActionRegistry, ActionSurface, AnyActionSpec, HandlerContext } from "@outfitter/contracts";
4
7
  import { Command } from "commander";
5
8
  interface BuildCliCommandsOptions {
package/dist/actions.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // @bun
2
2
  import {
3
3
  createSchemaCommand
4
- } from "./shared/@outfitter/cli-5vtr4bdt.js";
4
+ } from "./shared/@outfitter/cli-zv3ah6f0.js";
5
5
  import {
6
6
  composePresets
7
- } from "./shared/@outfitter/cli-pdb7znbq.js";
7
+ } from "./shared/@outfitter/cli-8999qjdd.js";
8
8
 
9
9
  // packages/cli/src/actions.ts
10
10
  import {
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CLI, CLIConfig } from "./shared/@outfitter/cli-98aa9104.js";
1
+ import { CLI, CLIConfig } from "./shared/@outfitter/cli-x6qr7bnd.js";
2
2
  /**
3
3
  * Create a new CLI instance with the given configuration.
4
4
  *
package/dist/cli.js CHANGED
@@ -1,7 +1,14 @@
1
1
  // @bun
2
2
  import {
3
3
  createCLI
4
- } from "./shared/@outfitter/cli-zahqsaby.js";
4
+ } from "./shared/@outfitter/cli-d40m2x1d.js";
5
+ import"./shared/@outfitter/cli-89335n9a.js";
6
+ import"./shared/@outfitter/cli-htzez8v2.js";
7
+ import"./shared/@outfitter/cli-hvg2m5gf.js";
8
+ import"./shared/@outfitter/cli-2dfxs239.js";
9
+ import"./shared/@outfitter/cli-4h85mpth.js";
10
+ import"./shared/@outfitter/cli-dg0cz7rw.js";
11
+ import"./shared/@outfitter/cli-3jta1h1h.js";
5
12
  export {
6
13
  createCLI
7
14
  };
package/dist/command.d.ts CHANGED
@@ -1,43 +1,3 @@
1
- import { CLI, CLIConfig, CommandAction, CommandBuilder, CommandConfig, CommandFlags, FlagPreset } from "./shared/@outfitter/cli-98aa9104.js";
2
- /**
3
- * Create a CLI instance with a portable return type from this module.
4
- */
5
- declare function createCLI(config: CLIConfig): CLI;
6
- /**
7
- * Create a new command builder with the given name.
8
- *
9
- * The command builder provides a fluent API for defining CLI commands
10
- * with typed flags, arguments, and actions.
11
- *
12
- * @param name - Command name and optional argument syntax (e.g., "list" or "get <id>")
13
- * @returns A CommandBuilder instance for fluent configuration
14
- *
15
- * @example
16
- * ```typescript
17
- * import { command, output } from "@outfitter/cli";
18
- *
19
- * const list = command("list")
20
- * .description("List all notes")
21
- * .option("--limit <n>", "Max results", "20")
22
- * .option("--json", "Output as JSON")
23
- * .option("--next", "Continue from last position")
24
- * .action(async ({ flags }) => {
25
- * const results = await listNotes(flags);
26
- * output(results);
27
- * });
28
- * ```
29
- *
30
- * @example
31
- * ```typescript
32
- * // Command with required argument
33
- * const get = command("get <id>")
34
- * .description("Get a note by ID")
35
- * .action(async ({ args }) => {
36
- * const [id] = args;
37
- * const note = await getNote(id);
38
- * output(note);
39
- * });
40
- * ```
41
- */
42
- declare function command(name: string): CommandBuilder;
43
- export { createCLI, command, FlagPreset, CommandFlags, CommandConfig, CommandBuilder, CommandAction, CLIConfig, CLI };
1
+ import { CommandMetadata, command, createCLI } from "./shared/@outfitter/cli-xde45xcc.js";
2
+ import { AnyPreset, CLI, CLIConfig, CommandAction, CommandBuilder, CommandConfig, CommandFlags, ContextFactory, ErrorHintFn, FlagPreset, RelatedToDeclaration, RelatedToOptions, SchemaPreset, SuccessHintFn, ZodObjectLike } from "./shared/@outfitter/cli-x6qr7bnd.js";
3
+ export { createCLI, command, ZodObjectLike, SuccessHintFn, SchemaPreset, RelatedToOptions, RelatedToDeclaration, FlagPreset, ErrorHintFn, ContextFactory, CommandMetadata, CommandFlags, CommandConfig, CommandBuilder, CommandAction, CLIConfig, CLI, AnyPreset };
package/dist/command.js CHANGED
@@ -1,10 +1,87 @@
1
1
  // @bun
2
2
  import {
3
3
  createCLI
4
- } from "./shared/@outfitter/cli-zahqsaby.js";
4
+ } from "./shared/@outfitter/cli-d40m2x1d.js";
5
+ import {
6
+ resolveOutputMode
7
+ } from "./shared/@outfitter/cli-vvvhjwks.js";
8
+ import {
9
+ isSchemaPreset
10
+ } from "./shared/@outfitter/cli-8999qjdd.js";
11
+ import"./shared/@outfitter/cli-89335n9a.js";
12
+ import"./shared/@outfitter/cli-htzez8v2.js";
13
+ import"./shared/@outfitter/cli-hvg2m5gf.js";
14
+ import"./shared/@outfitter/cli-2dfxs239.js";
15
+ import {
16
+ exitWithError
17
+ } from "./shared/@outfitter/cli-4h85mpth.js";
18
+ import"./shared/@outfitter/cli-dg0cz7rw.js";
19
+ import {
20
+ createCommanderOption,
21
+ deriveFlags,
22
+ validateInput
23
+ } from "./shared/@outfitter/cli-3jta1h1h.js";
5
24
 
6
25
  // packages/cli/src/command.ts
7
26
  import { Command } from "commander";
27
+ import { z } from "zod";
28
+ function buildMergedSchema(inputSchema, schemaPresets) {
29
+ if (!inputSchema && schemaPresets.length === 0)
30
+ return;
31
+ if (schemaPresets.length === 0)
32
+ return inputSchema;
33
+ if (!inputSchema && schemaPresets.length === 1) {
34
+ return schemaPresets[0].schema;
35
+ }
36
+ const inputKeys = new Set(Object.keys(inputSchema?.shape ?? {}));
37
+ const omitOverriddenPresetFields = (schema) => {
38
+ if (inputKeys.size === 0) {
39
+ return schema;
40
+ }
41
+ const filteredEntries = Object.entries(schema.shape).filter(([key]) => !inputKeys.has(key));
42
+ if (filteredEntries.length === 0) {
43
+ return;
44
+ }
45
+ const filteredShape = Object.fromEntries(filteredEntries);
46
+ return z.object(filteredShape);
47
+ };
48
+ const mergedShape = {};
49
+ const allSchemas = [];
50
+ for (const preset of schemaPresets) {
51
+ const filteredPresetSchema = omitOverriddenPresetFields(preset.schema);
52
+ if (!filteredPresetSchema) {
53
+ continue;
54
+ }
55
+ Object.assign(mergedShape, filteredPresetSchema.shape);
56
+ allSchemas.push(filteredPresetSchema);
57
+ }
58
+ if (inputSchema) {
59
+ Object.assign(mergedShape, inputSchema.shape);
60
+ allSchemas.push(inputSchema);
61
+ }
62
+ return {
63
+ shape: mergedShape,
64
+ safeParse(data) {
65
+ let merged = {};
66
+ for (const schema of allSchemas) {
67
+ const schemaData = {};
68
+ if (data && typeof data === "object") {
69
+ for (const key of Object.keys(schema.shape)) {
70
+ if (key in data) {
71
+ schemaData[key] = data[key];
72
+ }
73
+ }
74
+ }
75
+ const result = schema.safeParse(schemaData);
76
+ if (!result.success) {
77
+ return result;
78
+ }
79
+ merged = { ...merged, ...result.data };
80
+ }
81
+ return { success: true, data: merged };
82
+ }
83
+ };
84
+ }
8
85
  function parseCommandSignature(signature) {
9
86
  const trimmed = signature.trim();
10
87
  const firstWhitespace = trimmed.search(/\s/);
@@ -23,51 +100,202 @@ function createCLI2(config) {
23
100
  }
24
101
 
25
102
  class CommandBuilderImpl {
26
- command;
103
+ cmd;
104
+ inputSchema;
105
+ ctxFactory;
106
+ successHintFn;
107
+ errorHintsFn;
108
+ explicitLongFlags = new Set;
109
+ schemaPresets = [];
110
+ schemaFlagsApplied = false;
111
+ _destructive = false;
112
+ _readOnly = false;
113
+ _idempotent = false;
114
+ _relatedTo = [];
27
115
  constructor(signature) {
28
116
  const { name, argumentsSpec } = parseCommandSignature(signature);
29
- this.command = new Command(name);
117
+ this.cmd = new Command(name);
30
118
  if (argumentsSpec) {
31
- this.command.arguments(argumentsSpec);
119
+ this.cmd.arguments(argumentsSpec);
32
120
  }
33
121
  }
34
122
  description(text) {
35
- this.command.description(text);
123
+ this.cmd.description(text);
36
124
  return this;
37
125
  }
38
126
  option(flags, description, defaultValue) {
39
- this.command.option(flags, description, defaultValue);
127
+ const longMatch = flags.match(/--([a-z][a-z0-9-]*)/i);
128
+ if (longMatch) {
129
+ this.explicitLongFlags.add(`--${longMatch[1]}`);
130
+ }
131
+ this.cmd.option(flags, description, defaultValue);
40
132
  return this;
41
133
  }
42
134
  requiredOption(flags, description, defaultValue) {
43
- this.command.requiredOption(flags, description, defaultValue);
135
+ const longMatch = flags.match(/--([a-z][a-z0-9-]*)/i);
136
+ if (longMatch) {
137
+ this.explicitLongFlags.add(`--${longMatch[1]}`);
138
+ }
139
+ this.cmd.requiredOption(flags, description, defaultValue);
44
140
  return this;
45
141
  }
46
142
  alias(alias) {
47
- this.command.alias(alias);
143
+ this.cmd.alias(alias);
144
+ return this;
145
+ }
146
+ input(schema) {
147
+ this.inputSchema = schema;
148
+ return this;
149
+ }
150
+ context(factory) {
151
+ this.ctxFactory = factory;
152
+ return this;
153
+ }
154
+ hints(fn) {
155
+ this.successHintFn = fn;
156
+ return this;
157
+ }
158
+ onError(fn) {
159
+ this.errorHintsFn = fn;
160
+ return this;
161
+ }
162
+ destructive(isDest) {
163
+ this._destructive = isDest;
164
+ return this;
165
+ }
166
+ readOnly(isReadOnly) {
167
+ this._readOnly = isReadOnly;
168
+ return this;
169
+ }
170
+ idempotent(isIdempotent) {
171
+ this._idempotent = isIdempotent;
172
+ return this;
173
+ }
174
+ relatedTo(target, options) {
175
+ this._relatedTo.push({
176
+ target,
177
+ ...options?.description ? { description: options.description } : {}
178
+ });
48
179
  return this;
49
180
  }
50
181
  preset(preset) {
182
+ if (isSchemaPreset(preset)) {
183
+ this.schemaPresets.push(preset);
184
+ return this;
185
+ }
51
186
  for (const opt of preset.options) {
187
+ const longMatch = opt.flags.match(/--([a-z][a-z0-9-]*)/i);
188
+ if (longMatch) {
189
+ this.explicitLongFlags.add(`--${longMatch[1]}`);
190
+ }
52
191
  if (opt.required) {
53
- this.command.requiredOption(opt.flags, opt.description, opt.defaultValue);
192
+ this.cmd.requiredOption(opt.flags, opt.description, opt.defaultValue);
54
193
  } else {
55
- this.command.option(opt.flags, opt.description, opt.defaultValue);
194
+ this.cmd.option(opt.flags, opt.description, opt.defaultValue);
56
195
  }
57
196
  }
58
197
  return this;
59
198
  }
60
199
  action(handler) {
61
- this.command.action(async (...args) => {
200
+ const schema = this.inputSchema;
201
+ const contextFactory = this.ctxFactory;
202
+ const presets = [...this.schemaPresets];
203
+ this.applySchemaFlags();
204
+ const mergedSchema = buildMergedSchema(schema, presets);
205
+ this.cmd.action(async (...args) => {
62
206
  const command = args.at(-1);
63
207
  const flags = command.optsWithGlobals?.() ?? command.opts();
64
208
  const positional = command.args;
65
- await handler({ args: positional, flags, command });
209
+ let input;
210
+ let ctx;
211
+ try {
212
+ input = mergedSchema ? validateInput(flags, mergedSchema) : undefined;
213
+ if (input !== undefined && presets.length > 0) {
214
+ for (const preset of presets) {
215
+ const resolved = preset.resolve(flags);
216
+ Object.assign(input, resolved);
217
+ }
218
+ }
219
+ if (contextFactory) {
220
+ const factoryArg = input !== undefined ? input : flags;
221
+ ctx = await contextFactory(factoryArg);
222
+ }
223
+ } catch (err) {
224
+ const error = err instanceof Error ? err : new Error(String(err));
225
+ const { mode } = resolveOutputMode(flags);
226
+ exitWithError(error, mode);
227
+ return;
228
+ }
229
+ await handler({
230
+ args: positional,
231
+ flags,
232
+ command,
233
+ input,
234
+ ctx
235
+ });
66
236
  });
67
237
  return this;
68
238
  }
69
239
  build() {
70
- return this.command;
240
+ this.applySchemaFlags();
241
+ this.applyDestructiveFlag();
242
+ if (this.successHintFn) {
243
+ this.cmd.__successHintFn = this.successHintFn;
244
+ }
245
+ if (this.errorHintsFn) {
246
+ this.cmd.__errorHintFn = this.errorHintsFn;
247
+ }
248
+ const metadata = {};
249
+ if (this._readOnly) {
250
+ metadata.readOnly = true;
251
+ }
252
+ if (this._idempotent) {
253
+ metadata.idempotent = true;
254
+ }
255
+ if (metadata.readOnly !== undefined || metadata.idempotent !== undefined) {
256
+ this.cmd.__metadata = metadata;
257
+ }
258
+ if (this._relatedTo.length > 0) {
259
+ this.cmd.__relatedTo = [...this._relatedTo];
260
+ }
261
+ return this.cmd;
262
+ }
263
+ applySchemaFlags() {
264
+ if (this.schemaFlagsApplied)
265
+ return;
266
+ if (!this.inputSchema && this.schemaPresets.length === 0)
267
+ return;
268
+ this.schemaFlagsApplied = true;
269
+ const existingLongs = new Set(this.explicitLongFlags);
270
+ for (const opt of this.cmd.options) {
271
+ if (opt.long) {
272
+ existingLongs.add(opt.long);
273
+ }
274
+ }
275
+ if (this.inputSchema) {
276
+ const derived = deriveFlags(this.inputSchema, existingLongs);
277
+ for (const flag of derived) {
278
+ const option = createCommanderOption(flag, this.inputSchema);
279
+ this.cmd.addOption(option);
280
+ existingLongs.add(flag.longFlag);
281
+ }
282
+ }
283
+ for (const schemaPreset of this.schemaPresets) {
284
+ const derived = deriveFlags(schemaPreset.schema, existingLongs);
285
+ for (const flag of derived) {
286
+ const option = createCommanderOption(flag, schemaPreset.schema);
287
+ this.cmd.addOption(option);
288
+ existingLongs.add(flag.longFlag);
289
+ }
290
+ }
291
+ }
292
+ applyDestructiveFlag() {
293
+ if (!this._destructive)
294
+ return;
295
+ const hasDryRun = this.cmd.options.some((o) => o.long === "--dry-run");
296
+ if (hasDryRun)
297
+ return;
298
+ this.cmd.option("--dry-run", "Preview changes without applying (destructive command safety)", false);
71
299
  }
72
300
  }
73
301
  function command(name) {
@@ -0,0 +1,5 @@
1
+ import { runHandler } from "./shared/@outfitter/cli-c09332vm.js";
2
+ import { createErrorEnvelope, createSuccessEnvelope } from "./shared/@outfitter/cli-nkt399zf.js";
3
+ import { CommandEnvelope, ErrorEnvelope, RunHandlerOptions, SuccessEnvelope } from "./shared/@outfitter/cli-30mt7c5w.js";
4
+ import "./shared/@outfitter/cli-x6qr7bnd.js";
5
+ export { runHandler, createSuccessEnvelope, createErrorEnvelope, SuccessEnvelope, RunHandlerOptions, ErrorEnvelope, CommandEnvelope };
@@ -0,0 +1,160 @@
1
+ // @bun
2
+ import {
3
+ buildDryRunHint,
4
+ createErrorEnvelope,
5
+ createSuccessEnvelope,
6
+ extractCategory,
7
+ extractMessage,
8
+ extractRetryAfterSeconds,
9
+ formatEnvelopeHuman,
10
+ getExitCode,
11
+ safeCallHintFn
12
+ } from "./shared/@outfitter/cli-ry7btmy4.js";
13
+ import {
14
+ createNdjsonProgress,
15
+ writeNdjsonLine,
16
+ writeStreamEnvelope
17
+ } from "./shared/@outfitter/cli-g43887b7.js";
18
+ import {
19
+ output
20
+ } from "./shared/@outfitter/cli-4h85mpth.js";
21
+ import {
22
+ detectMode
23
+ } from "./shared/@outfitter/cli-dg0cz7rw.js";
24
+
25
+ // packages/cli/src/envelope.ts
26
+ import { safeStringify } from "@outfitter/contracts";
27
+ async function runHandler(options) {
28
+ const {
29
+ command: commandName,
30
+ handler,
31
+ input,
32
+ format,
33
+ contextFactory,
34
+ hints: hintsFn,
35
+ onError: onErrorFn,
36
+ stream: isStreaming = false,
37
+ dryRun: isDryRun = false
38
+ } = options;
39
+ const inputValue = input;
40
+ const resolvedFormat = format ?? detectMode();
41
+ const isJsonMode = resolvedFormat === "json" || resolvedFormat === "jsonl";
42
+ let progressCallback;
43
+ if (isStreaming) {
44
+ const startEvent = {
45
+ type: "start",
46
+ command: commandName,
47
+ ts: new Date().toISOString()
48
+ };
49
+ writeNdjsonLine(startEvent);
50
+ const writeProgress = createNdjsonProgress(commandName);
51
+ progressCallback = (event) => {
52
+ if (event.type === "start") {
53
+ return;
54
+ }
55
+ writeProgress(event);
56
+ };
57
+ }
58
+ let context;
59
+ if (contextFactory) {
60
+ try {
61
+ const rawContext = await contextFactory(inputValue);
62
+ if (isStreaming && rawContext && typeof rawContext === "object") {
63
+ context = Object.assign(Object.create(Object.getPrototypeOf(rawContext)), rawContext, { progress: progressCallback });
64
+ } else {
65
+ context = rawContext;
66
+ }
67
+ } catch (err) {
68
+ const error = err instanceof Error ? err : new Error(String(err));
69
+ const category = extractCategory(error);
70
+ const message = extractMessage(error);
71
+ const retryAfter = extractRetryAfterSeconds(error);
72
+ const errorHints = onErrorFn ? safeCallHintFn(() => onErrorFn(error, inputValue)) : undefined;
73
+ const envelope = createErrorEnvelope(commandName, category, message, errorHints, retryAfter);
74
+ if (isStreaming) {
75
+ writeStreamEnvelope(envelope);
76
+ const exitCode = getExitCode(category);
77
+ process.exit(exitCode);
78
+ }
79
+ outputErrorEnvelope(envelope, isJsonMode);
80
+ return;
81
+ }
82
+ } else if (isStreaming) {
83
+ context = { progress: progressCallback };
84
+ } else {
85
+ context = undefined;
86
+ }
87
+ let result;
88
+ try {
89
+ result = await handler(inputValue, context);
90
+ } catch (err) {
91
+ const error = err instanceof Error ? err : new Error(String(err));
92
+ const category = extractCategory(error);
93
+ const message = extractMessage(error);
94
+ const retryAfter = extractRetryAfterSeconds(error);
95
+ const errorHints = onErrorFn ? safeCallHintFn(() => onErrorFn(error, inputValue)) : undefined;
96
+ const envelope = createErrorEnvelope(commandName, category, message, errorHints, retryAfter);
97
+ if (isStreaming) {
98
+ writeStreamEnvelope(envelope);
99
+ const exitCode = getExitCode(category);
100
+ process.exit(exitCode);
101
+ }
102
+ outputErrorEnvelope(envelope, isJsonMode);
103
+ return;
104
+ }
105
+ if (result.isOk()) {
106
+ let successHints = hintsFn ? safeCallHintFn(() => hintsFn(result.value, inputValue)) : undefined;
107
+ if (isDryRun) {
108
+ const dryRunHint = buildDryRunHint(options.argv);
109
+ if (dryRunHint) {
110
+ successHints = successHints ? [...successHints, dryRunHint] : [dryRunHint];
111
+ }
112
+ }
113
+ const envelope = createSuccessEnvelope(commandName, result.value, successHints);
114
+ if (isStreaming) {
115
+ writeStreamEnvelope(envelope);
116
+ return;
117
+ }
118
+ if (isJsonMode) {
119
+ await output(envelope, resolvedFormat);
120
+ } else {
121
+ const formatted = formatEnvelopeHuman(envelope);
122
+ if (formatted.stdout) {
123
+ process.stdout.write(`${formatted.stdout}
124
+ `);
125
+ }
126
+ }
127
+ } else {
128
+ const error = result.error;
129
+ const category = extractCategory(error);
130
+ const message = extractMessage(error);
131
+ const retryAfter = extractRetryAfterSeconds(error);
132
+ const errorHints = onErrorFn ? safeCallHintFn(() => onErrorFn(error, inputValue)) : undefined;
133
+ const envelope = createErrorEnvelope(commandName, category, message, errorHints, retryAfter);
134
+ if (isStreaming) {
135
+ writeStreamEnvelope(envelope);
136
+ const exitCode = getExitCode(category);
137
+ process.exit(exitCode);
138
+ }
139
+ outputErrorEnvelope(envelope, isJsonMode);
140
+ }
141
+ }
142
+ function outputErrorEnvelope(envelope, isJsonMode) {
143
+ const exitCode = getExitCode(envelope.error.category);
144
+ if (isJsonMode) {
145
+ process.stderr.write(`${safeStringify(envelope)}
146
+ `);
147
+ } else {
148
+ const formatted = formatEnvelopeHuman(envelope);
149
+ if (formatted.stderr) {
150
+ process.stderr.write(`${formatted.stderr}
151
+ `);
152
+ }
153
+ }
154
+ process.exit(exitCode);
155
+ }
156
+ export {
157
+ runHandler,
158
+ createSuccessEnvelope,
159
+ createErrorEnvelope
160
+ };