@runa-cmd/core 0.2.0-beta.0 → 0.2.0-beta.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.
Files changed (76) hide show
  1. package/README.md +137 -23
  2. package/dist/cli.d.ts +20 -4
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +370 -123
  5. package/dist/cli.js.map +1 -1
  6. package/dist/command.d.ts.map +1 -1
  7. package/dist/command.js +1 -0
  8. package/dist/command.js.map +1 -1
  9. package/dist/errors.d.ts +25 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +72 -6
  12. package/dist/errors.js.map +1 -1
  13. package/dist/flag-name.d.ts +12 -0
  14. package/dist/flag-name.d.ts.map +1 -0
  15. package/dist/flag-name.js +19 -0
  16. package/dist/flag-name.js.map +1 -0
  17. package/dist/help.d.ts +21 -0
  18. package/dist/help.d.ts.map +1 -0
  19. package/dist/help.js +152 -0
  20. package/dist/help.js.map +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/internal.d.ts +10 -0
  25. package/dist/internal.d.ts.map +1 -0
  26. package/dist/internal.js +9 -0
  27. package/dist/internal.js.map +1 -0
  28. package/dist/introspect.d.ts.map +1 -1
  29. package/dist/introspect.js +12 -3
  30. package/dist/introspect.js.map +1 -1
  31. package/dist/lifecycle.js +1 -1
  32. package/dist/lifecycle.js.map +1 -1
  33. package/dist/parse/adapters/zod-v4.d.ts.map +1 -1
  34. package/dist/parse/adapters/zod-v4.js +6 -3
  35. package/dist/parse/adapters/zod-v4.js.map +1 -1
  36. package/dist/parse/deprecated.d.ts +12 -0
  37. package/dist/parse/deprecated.d.ts.map +1 -0
  38. package/dist/parse/deprecated.js +21 -0
  39. package/dist/parse/deprecated.js.map +1 -0
  40. package/dist/parse/parseargs-bridge.d.ts.map +1 -1
  41. package/dist/parse/parseargs-bridge.js +14 -10
  42. package/dist/parse/parseargs-bridge.js.map +1 -1
  43. package/dist/parse/resolve.d.ts +34 -4
  44. package/dist/parse/resolve.d.ts.map +1 -1
  45. package/dist/parse/resolve.js +134 -54
  46. package/dist/parse/resolve.js.map +1 -1
  47. package/dist/parse/schema-adapter.d.ts +5 -3
  48. package/dist/parse/schema-adapter.d.ts.map +1 -1
  49. package/dist/parse/schema-adapter.js.map +1 -1
  50. package/dist/parse/schema-walker.d.ts.map +1 -1
  51. package/dist/parse/schema-walker.js +1 -0
  52. package/dist/parse/schema-walker.js.map +1 -1
  53. package/dist/parse/strict.d.ts +36 -2
  54. package/dist/parse/strict.d.ts.map +1 -1
  55. package/dist/parse/strict.js +53 -15
  56. package/dist/parse/strict.js.map +1 -1
  57. package/dist/plugin.d.ts +12 -3
  58. package/dist/plugin.d.ts.map +1 -1
  59. package/dist/plugin.js +60 -9
  60. package/dist/plugin.js.map +1 -1
  61. package/dist/stop.d.ts +25 -0
  62. package/dist/stop.d.ts.map +1 -0
  63. package/dist/stop.js +110 -0
  64. package/dist/stop.js.map +1 -0
  65. package/dist/suggest.d.ts +3 -2
  66. package/dist/suggest.d.ts.map +1 -1
  67. package/dist/suggest.js +5 -4
  68. package/dist/suggest.js.map +1 -1
  69. package/dist/tool-channel.d.ts +48 -0
  70. package/dist/tool-channel.d.ts.map +1 -0
  71. package/dist/tool-channel.js +46 -0
  72. package/dist/tool-channel.js.map +1 -0
  73. package/dist/types.d.ts +51 -8
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js.map +1 -1
  76. package/package.json +5 -1
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # @runa-cmd/core
2
2
 
3
- The engine of the Runa CLI framework. Schema-driven, TypeScript-first, zero runtime dependencies.
3
+ The engine of the Runa CLI framework. Schema-driven, TypeScript-first, with zero runtime dependencies besides Zod as a required peer.
4
4
 
5
- Define your CLI with Zod schemas and get full type inference, validation, help text, and more — from a single source of truth.
5
+ Define your CLI with Zod schemas and get full type inference, validation, and metadata for help, completions and MCP plugins — from a single source of truth.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- pnpm add @runa-cmd/core zod
10
+ pnpm add @runa-cmd/core@beta zod
11
11
  ```
12
12
 
13
- > Requires Node 18.3+ (uses `util.parseArgs`). Zod v4 is a peer dependency.
13
+ > Requires Node 24+. Zod v4 is a required peer dependency. Without `@beta`, npm installs the older `0.1.0`.
14
14
 
15
15
  ## Quick Start
16
16
 
@@ -30,7 +30,7 @@ runCLI(greet);
30
30
  ```
31
31
 
32
32
  ```
33
- $ my-cli greet world
33
+ $ greet world
34
34
  Hello, world!
35
35
  ```
36
36
 
@@ -47,7 +47,7 @@ const deploy = defineCommand({
47
47
  description: 'Deploy to an environment',
48
48
  options: {
49
49
  env: { alias: ['-e'], env: 'DEPLOY_ENV' },
50
- replicas: { group: 'Scaling', hint: '<count>' },
50
+ replicas: { group: 'Scaling' },
51
51
  },
52
52
  },
53
53
  args: {
@@ -80,7 +80,7 @@ const deploy = defineCommand({
80
80
  | `meta.name` | `string` | Yes | Command name |
81
81
  | `meta.description` | `string` | Yes | One-line description |
82
82
  | `meta.version` | `string` | No | Command version |
83
- | `meta.options` | `Record<string, OptionMeta>` | No | Per-option metadata (alias, env, group, deprecated, hint) |
83
+ | `meta.options` | `Record<string, OptionMeta>` | No | Per-option metadata (alias, env, group, deprecated) |
84
84
  | `args` | `Record<string, ZodType>` | No | Positional arguments (insertion order matters) |
85
85
  | `options` | `Record<string, ZodType>` | No | Named options (--flag style) |
86
86
  | `output` | `ZodType` | No | Output schema — if set, `run()` must return matching data |
@@ -95,8 +95,8 @@ Operational metadata lives in `meta.options`, separate from the Zod schema:
95
95
  | `alias` | `string[]` | Short flags, e.g. `['-e']` (must include dash prefix) |
96
96
  | `env` | `string` | Environment variable name, e.g. `'DEPLOY_ENV'` |
97
97
  | `group` | `string` | Group name for help display, e.g. `'Scaling'` |
98
- | `deprecated` | `string \| boolean` | Deprecation notice |
99
- | `hint` | `string` | Value hint for help, e.g. `'<count>'` |
98
+ | `valueName` | `string` | What help calls the value, e.g. `'hex'` for `--color <hex>`. Defaults to its type |
99
+ | `deprecated` | `string` | Deprecation notice, shown in help and as a stderr warning when the option is used |
100
100
 
101
101
  #### Run Context
102
102
 
@@ -109,6 +109,8 @@ interface RunContext {
109
109
  globalOptions: Record<string, unknown>;
110
110
  command: CommandMeta; // Current command metadata
111
111
  rawArgs: string[]; // Original argv
112
+ rest: string[]; // With `rest: true`, what's left after `--`. Unvalidated
113
+ signal: AbortSignal; // Aborted when the run must stop
112
114
  }
113
115
  ```
114
116
 
@@ -151,22 +153,101 @@ cli.run();
151
153
  | `plugins` | `PluginConfig[]` | No | Plugins to load |
152
154
  | `middleware` | `Middleware[]` | No | Global middleware chain |
153
155
  | `globalOptions` | `Record<string, ZodType>` | No | Options available to all commands |
154
- | `globalMeta` | `Record<string, OptionMeta>` | No | Metadata for global options |
156
+ | `globalMeta` | `{ options?: Record<string, OptionMeta> }` | No | Metadata for global options |
155
157
  | `config` | `CLIConfigOptions` | No | Config file loading settings |
158
+ | `strictOptions` | `boolean` | No | Reject unknown options. Default: `true` |
156
159
 
157
- ### `runCLI(command, argv?)`
160
+ ### `runCLI(command, options?)`
158
161
 
159
- Shorthand for single-command CLIs. Wraps the command in `defineCLI` and runs it.
162
+ The API for a CLI made of one command. It takes the same options as `defineCLI`, except `meta` and `commands`, which come from the command, plus `argv`.
160
163
 
161
164
  ```ts
162
- runCLI(myCommand);
163
- // Equivalent to:
164
- defineCLI({
165
- meta: { name: command.meta.name },
166
- commands: { [command.meta.name]: command },
167
- }).run();
165
+ runCLI(myCommand, {
166
+ plugins: [helpPlugin(), mcpPlugin()],
167
+ argv: ['hello'], // Default: process.argv.slice(2)
168
+ });
169
+ ```
170
+
171
+ The command is the root of the CLI, so a value equal to its name is data: `tool tool` passes `"tool"`. If a plugin adds a subcommand, that name routes to it, and `--` passes it as data: `tool -- completions`.
172
+
173
+ `defineCLI` with a single command also works, but it's deprecated: adding a plugin that registers a command silently changes how it routes.
174
+
175
+ ### Options that repeat
176
+
177
+ A `z.array()` option repeats, and each element is converted to its type: `--port 80 --port 81` gives `[80, 81]` with `z.array(z.number())`.
178
+
179
+ | Source | Form | Result |
180
+ |---|---|---|
181
+ | Terminal | `--tag a --tag b` | `['a', 'b']` |
182
+ | Terminal | `--tag a,b` | `['a,b']` |
183
+ | Env | `TAGS=a, b` | `['a', 'b']` |
184
+ | Config | `{ "tag": ["a", "b"] }` | `['a', 'b']` |
185
+
186
+ Each source replaces the ones below it, without adding up. A value with a comma can't go through env: use the config file.
187
+
188
+ ### Option names
189
+
190
+ Write `darkColor` in code and `--dark-color` at the terminal. `run()`, config files and MCP tools keep the key.
191
+
192
+ | Key | Flag |
193
+ |---|---|
194
+ | `darkColor` | `--dark-color` |
195
+ | `outputURL` | `--output-url` |
196
+ | `dark-color` | `--dark-color`, a key with hyphens stays as written |
197
+
198
+ `--darkColor` is a usage error that points to `--dark-color`. `--no-dark-mode` negates `darkMode`.
199
+
200
+ A boolean named `noCache` stops the CLI at startup, because `--no-cache` already negates `cache`. Declare `cache` with `.default(true)` instead.
201
+
202
+ ### `--help`
203
+
204
+ Every CLI has `--help` and `-h`, with no plugin: `tool --help` shows the CLI or the root command, and `tool db migrate --help` shows that command, with the global options that work on it.
205
+
206
+ Each flag yields separately. If a command declares `-h`, for `--host` say, it keeps it and `--help` still shows help. For themes and layout, add `@runa-cmd/help`. To show help your own way from a plugin, declare the `replaceHelp` capability:
207
+
208
+ ```ts
209
+ definePlugin({
210
+ meta: { name: 'my-help' },
211
+ capabilities: { replaceHelp: true },
212
+ setup(api) {
213
+ api.replaceHelp((target, output) => output.write(`${render(target)}\n`));
214
+ },
215
+ });
216
+ ```
217
+
218
+ `target` is a `CLISchema` or a `CommandSchema`. `output` is stdout, or stderr when there was nothing to run. Only one plugin can replace the help.
219
+
220
+ With nothing to run, as in `tool` on a CLI with several commands or `tool db` on a group, help goes to stderr and the exit code is 2. A script with an empty command fails instead of passing. A name that doesn't exist is still a 127, with a suggestion.
221
+
222
+ `--help` and `--version` work even if a required global option is missing.
223
+
224
+ ### `--version`
225
+
226
+ Printed when `meta.version` is set, from `defineCLI` or from the command in `runCLI`. It has no short alias. If the command declares its own `--version` option, that option wins.
227
+
228
+ ### The `--` separator
229
+
230
+ Everything after the first `--` is never read as a flag. Those tokens fill the positionals still empty, where a variadic counts as one.
231
+
232
+ What's left is a usage error, so nothing gets lost in silence. A command that wraps another one declares `rest: true` to receive it in `ctx.rest`:
233
+
234
+ ```ts
235
+ const run = defineCommand({
236
+ meta: { name: 'run', description: 'Run a program' },
237
+ rest: true,
238
+ run({ rest }) {
239
+ spawn(rest[0], rest.slice(1));
240
+ },
241
+ });
168
242
  ```
169
243
 
244
+ | Input | Without `rest: true` | With `rest: true` |
245
+ |---|---|---|
246
+ | `tool -- -text` | `args.content` is `-text` | Same |
247
+ | `lint -- -a -b` | `args.files` is `['-a', '-b']` | Same |
248
+ | `copy a b -- -c` | Exit 2 | `ctx.rest` is `['-c']` |
249
+ | `run -- npm test` (no positionals) | Exit 2 | `ctx.rest` is `['npm', 'test']` |
250
+
170
251
  ### `defineMiddleware(handler)`
171
252
 
172
253
  Creates middleware using the onion model (like Koa/Hono):
@@ -214,6 +295,7 @@ The `setup(api)` function receives:
214
295
  | `api.hook(name, handler)` | Register a lifecycle hook handler |
215
296
  | `api.getSchema()` | Get the introspected CLI schema |
216
297
  | `api.getCommands()` | Get the live command tree reference |
298
+ | `api.getConfigValues()` | Get the values of the CLI's config file, if it has one |
217
299
 
218
300
  ### `getSchema(config)`
219
301
 
@@ -228,15 +310,15 @@ const schema = getSchema(cliConfig);
228
310
 
229
311
  ### `jsonLoader()`
230
312
 
231
- Built-in JSON config file loader:
313
+ Built-in JSON config file loader, always active when `config` is set:
232
314
 
233
315
  ```ts
234
316
  const cli = defineCLI({
235
317
  // ...
236
318
  config: {
237
- name: 'my-tool', // Searches for my-tool.config.json, .my-toolrc.json, etc.
238
- loaders: [jsonLoader()],
239
- searchPaths: ['./'], // Where to search
319
+ name: 'my-tool', // Searches for my-tool.config.json
320
+ searchPaths: ['.', '~'], // Where to search. This is the default
321
+ // loaders: [...] // Extra formats. The first match wins
240
322
  },
241
323
  });
242
324
  ```
@@ -255,6 +337,36 @@ import {
255
337
  throw new CommandError('Deployment failed', { code: 'DEPLOY_FAILED', exitCode: 3 });
256
338
  ```
257
339
 
340
+ #### How a failed run ends
341
+
342
+ No failure exits silently. The reason goes to stderr, `cleanup` runs, and then the process exits.
343
+
344
+ | Case | stderr | Exit code |
345
+ |---|---|---|
346
+ | `RunaError` with a message | The message, as written | The error's |
347
+ | Any other error | `Unexpected error in '<command>': <message>` and a hint | 1 |
348
+ | Error without a message | `'<command>' failed without an error message (<type>).` and a hint | The error's, or 1 |
349
+ | `onError` sets `ctx.handled` | Nothing from Runa | The error's, or 1 |
350
+
351
+ - Usage errors end with `Run '<cli> --help' for usage.`
352
+ - Throw `CommandError` for messages meant for people using your CLI.
353
+ - Set `RUNA_DEBUG=1` to add the stack trace, error code and cause.
354
+ - `ctx.handled` only stops the default output. If a plugin truly recovered, it sets `process.exitCode = 0` in `onError`.
355
+
356
+ #### How a run stops
357
+
358
+ On `SIGINT` or `SIGTERM`, Runa aborts `ctx.signal` and runs `cleanup`, and the process exits with 130 or 143. A command that ignores the signal stops being waited for after 2 seconds, and the process ends after 5 at most. A second signal leaves at once.
359
+
360
+ Pass `signal` to whatever the command starts, so the stop reaches it too:
361
+
362
+ ```ts
363
+ async run({ signal }) {
364
+ await fetch(url, { signal });
365
+ }
366
+ ```
367
+
368
+ A command that blocks the event loop can't react to a signal until it yields. Use `Ctrl+\` to end it at once.
369
+
258
370
  ### Constants
259
371
 
260
372
  ```ts
@@ -309,7 +421,7 @@ beforeRun
309
421
  ↓
310
422
  afterRun
311
423
  ↓
312
- cleanup ← (ALWAYS runs, even on error)
424
+ cleanup ← (Runs on errors and on signals, within the stop deadline)
313
425
 
314
426
  On error → onError hook fires before cleanup
315
427
  ```
@@ -328,6 +440,8 @@ For example, if an option has `env: 'PORT'` and `z.number().default(3000)`:
328
440
  3. Config file value wins over default
329
441
  4. `3000` is the fallback
330
442
 
443
+ Boolean options accept `true`, `1`, `false` and `0`, in any case, from every source: `MINIFY=1`, `--minify=false`, or `"minify": true` in a config file. Any other value is a usage error.
444
+
331
445
  ## Types
332
446
 
333
447
  All types are exported for consumers who need them:
package/dist/cli.d.ts CHANGED
@@ -1,12 +1,28 @@
1
- import type { CLI, CLIConfig, Command } from './types.js';
1
+ import type { CLI, CLIConfig, Command, RunCLIOptions } from './types.js';
2
+ /** A command to run from an object, with no argv to parse (ADR-0030). */
3
+ export interface ToolCall {
4
+ /** Command path in the tree, e.g. ['config', 'set'] */
5
+ path: string[];
6
+ input: Record<string, unknown>;
7
+ }
2
8
  /**
3
9
  * Create a CLI from a configuration.
4
10
  * Returns a branded CLI object with a .run() method.
5
11
  */
6
12
  export declare function defineCLI(config: CLIConfig): CLI;
7
13
  /**
8
- * Single-command shorthand.
9
- * Wraps a command in defineCLI() and runs it.
14
+ * Run a CLI made of one command. Accepts everything defineCLI() does except
15
+ * meta and commands, which come from the command: plugins, middleware, config.
10
16
  */
11
- export declare function runCLI(command: Command, argv?: string[]): Promise<void>;
17
+ export declare function runCLI(command: Command, options?: RunCLIOptions): Promise<void>;
18
+ /**
19
+ * Run a command from an object, through the same lifecycle as a CLI run.
20
+ * Internal, for @runa-cmd/mcp: not exported from the package barrel.
21
+ *
22
+ * @param options.argv - Startup arguments, read for global options only
23
+ */
24
+ export declare function runToolCall(config: CLIConfig, call: ToolCall, options?: {
25
+ argv?: string[];
26
+ rootName?: string;
27
+ }): Promise<void>;
12
28
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACX,GAAG,EACH,SAAS,EACT,OAAO,EAOP,MAAM,YAAY,CAAC;AAkdpB;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,GAAG,CAShD;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EACX,GAAG,EACH,SAAS,EAET,OAAO,EAOP,aAAa,EACb,MAAM,YAAY,CAAC;AA0IpB,yEAAyE;AACzE,MAAM,WAAW,QAAQ;IACxB,uDAAuD;IACvD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA4nBD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,GAAG,CAQhD;AAED;;;GAGG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CASzF;AAkBD;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,OAAO,CAAC,IAAI,CAAC,CAEf"}