@runa-cmd/core 0.2.0-beta.0 → 0.2.0-beta.1

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 (73) hide show
  1. package/README.md +125 -23
  2. package/dist/cli.d.ts +20 -4
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +361 -123
  5. package/dist/cli.js.map +1 -1
  6. package/dist/errors.d.ts +25 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +72 -6
  9. package/dist/errors.js.map +1 -1
  10. package/dist/flag-name.d.ts +12 -0
  11. package/dist/flag-name.d.ts.map +1 -0
  12. package/dist/flag-name.js +19 -0
  13. package/dist/flag-name.js.map +1 -0
  14. package/dist/help.d.ts +21 -0
  15. package/dist/help.d.ts.map +1 -0
  16. package/dist/help.js +151 -0
  17. package/dist/help.js.map +1 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/internal.d.ts +10 -0
  22. package/dist/internal.d.ts.map +1 -0
  23. package/dist/internal.js +9 -0
  24. package/dist/internal.js.map +1 -0
  25. package/dist/introspect.d.ts.map +1 -1
  26. package/dist/introspect.js +10 -3
  27. package/dist/introspect.js.map +1 -1
  28. package/dist/lifecycle.js +1 -1
  29. package/dist/lifecycle.js.map +1 -1
  30. package/dist/parse/adapters/zod-v4.d.ts.map +1 -1
  31. package/dist/parse/adapters/zod-v4.js +6 -3
  32. package/dist/parse/adapters/zod-v4.js.map +1 -1
  33. package/dist/parse/deprecated.d.ts +12 -0
  34. package/dist/parse/deprecated.d.ts.map +1 -0
  35. package/dist/parse/deprecated.js +21 -0
  36. package/dist/parse/deprecated.js.map +1 -0
  37. package/dist/parse/parseargs-bridge.d.ts.map +1 -1
  38. package/dist/parse/parseargs-bridge.js +14 -10
  39. package/dist/parse/parseargs-bridge.js.map +1 -1
  40. package/dist/parse/resolve.d.ts +34 -4
  41. package/dist/parse/resolve.d.ts.map +1 -1
  42. package/dist/parse/resolve.js +134 -54
  43. package/dist/parse/resolve.js.map +1 -1
  44. package/dist/parse/schema-adapter.d.ts +5 -3
  45. package/dist/parse/schema-adapter.d.ts.map +1 -1
  46. package/dist/parse/schema-adapter.js.map +1 -1
  47. package/dist/parse/schema-walker.d.ts.map +1 -1
  48. package/dist/parse/schema-walker.js +1 -0
  49. package/dist/parse/schema-walker.js.map +1 -1
  50. package/dist/parse/strict.d.ts +33 -2
  51. package/dist/parse/strict.d.ts.map +1 -1
  52. package/dist/parse/strict.js +39 -13
  53. package/dist/parse/strict.js.map +1 -1
  54. package/dist/plugin.d.ts +12 -3
  55. package/dist/plugin.d.ts.map +1 -1
  56. package/dist/plugin.js +60 -9
  57. package/dist/plugin.js.map +1 -1
  58. package/dist/stop.d.ts +25 -0
  59. package/dist/stop.d.ts.map +1 -0
  60. package/dist/stop.js +110 -0
  61. package/dist/stop.js.map +1 -0
  62. package/dist/suggest.d.ts +3 -2
  63. package/dist/suggest.d.ts.map +1 -1
  64. package/dist/suggest.js +5 -4
  65. package/dist/suggest.js.map +1 -1
  66. package/dist/tool-channel.d.ts +48 -0
  67. package/dist/tool-channel.d.ts.map +1 -0
  68. package/dist/tool-channel.js +46 -0
  69. package/dist/tool-channel.js.map +1 -0
  70. package/dist/types.d.ts +43 -7
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. 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,7 @@ 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
+ | `deprecated` | `string` | Deprecation notice, shown in help and as a stderr warning when the option is used |
100
99
 
101
100
  #### Run Context
102
101
 
@@ -109,6 +108,8 @@ interface RunContext {
109
108
  globalOptions: Record<string, unknown>;
110
109
  command: CommandMeta; // Current command metadata
111
110
  rawArgs: string[]; // Original argv
111
+ rest: string[]; // After `--`, what didn't fill a positional. Unvalidated
112
+ signal: AbortSignal; // Aborted when the run must stop
112
113
  }
113
114
  ```
114
115
 
@@ -151,22 +152,90 @@ cli.run();
151
152
  | `plugins` | `PluginConfig[]` | No | Plugins to load |
152
153
  | `middleware` | `Middleware[]` | No | Global middleware chain |
153
154
  | `globalOptions` | `Record<string, ZodType>` | No | Options available to all commands |
154
- | `globalMeta` | `Record<string, OptionMeta>` | No | Metadata for global options |
155
+ | `globalMeta` | `{ options?: Record<string, OptionMeta> }` | No | Metadata for global options |
155
156
  | `config` | `CLIConfigOptions` | No | Config file loading settings |
157
+ | `strictOptions` | `boolean` | No | Reject unknown options. Default: `true` |
156
158
 
157
- ### `runCLI(command, argv?)`
159
+ ### `runCLI(command, options?)`
158
160
 
159
- Shorthand for single-command CLIs. Wraps the command in `defineCLI` and runs it.
161
+ 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
162
 
161
163
  ```ts
162
- runCLI(myCommand);
163
- // Equivalent to:
164
- defineCLI({
165
- meta: { name: command.meta.name },
166
- commands: { [command.meta.name]: command },
167
- }).run();
164
+ runCLI(myCommand, {
165
+ plugins: [helpPlugin(), mcpPlugin()],
166
+ argv: ['hello'], // Default: process.argv.slice(2)
167
+ });
168
+ ```
169
+
170
+ 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`.
171
+
172
+ `defineCLI` with a single command also works, but it's deprecated: adding a plugin that registers a command silently changes how it routes.
173
+
174
+ ### Options that repeat
175
+
176
+ 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())`.
177
+
178
+ | Source | Form | Result |
179
+ |---|---|---|
180
+ | Terminal | `--tag a --tag b` | `['a', 'b']` |
181
+ | Terminal | `--tag a,b` | `['a,b']` |
182
+ | Env | `TAGS=a, b` | `['a', 'b']` |
183
+ | Config | `{ "tag": ["a", "b"] }` | `['a', 'b']` |
184
+
185
+ Each source replaces the ones below it, without adding up. A value with a comma can't go through env: use the config file.
186
+
187
+ ### Option names
188
+
189
+ Write `darkColor` in code and `--dark-color` at the terminal. `run()`, config files and MCP tools keep the key.
190
+
191
+ | Key | Flag |
192
+ |---|---|
193
+ | `darkColor` | `--dark-color` |
194
+ | `outputURL` | `--output-url` |
195
+ | `dark-color` | `--dark-color`, a key with hyphens stays as written |
196
+
197
+ `--darkColor` is a usage error that points to `--dark-color`. `--no-dark-mode` negates `darkMode`.
198
+
199
+ A boolean named `noCache` stops the CLI at startup, because `--no-cache` already negates `cache`. Declare `cache` with `.default(true)` instead.
200
+
201
+ ### `--help`
202
+
203
+ 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.
204
+
205
+ 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:
206
+
207
+ ```ts
208
+ definePlugin({
209
+ meta: { name: 'my-help' },
210
+ capabilities: { replaceHelp: true },
211
+ setup(api) {
212
+ api.replaceHelp((target, output) => output.write(`${render(target)}\n`));
213
+ },
214
+ });
168
215
  ```
169
216
 
217
+ `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.
218
+
219
+ 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.
220
+
221
+ `--help` and `--version` work even if a required global option is missing.
222
+
223
+ ### `--version`
224
+
225
+ 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.
226
+
227
+ ### The `--` separator
228
+
229
+ Everything after the first `--` is never read as a flag. Those tokens fill the positionals still empty, where a variadic counts as one, and what's left arrives in `ctx.rest`.
230
+
231
+ | Input | Result |
232
+ |---|---|
233
+ | `tool -- -text` | `args.content` is `-text` |
234
+ | `copy a b -- -c` | `ctx.rest` is `['-c']` |
235
+ | `lint -- -a -b` | `args.files` is `['-a', '-b']` |
236
+ | `lint a.ts -- --fix` | `args.files` is `['a.ts']`, `ctx.rest` is `['--fix']` |
237
+ | `run -- npm test` (no positionals) | `ctx.rest` is `['npm', 'test']` |
238
+
170
239
  ### `defineMiddleware(handler)`
171
240
 
172
241
  Creates middleware using the onion model (like Koa/Hono):
@@ -214,6 +283,7 @@ The `setup(api)` function receives:
214
283
  | `api.hook(name, handler)` | Register a lifecycle hook handler |
215
284
  | `api.getSchema()` | Get the introspected CLI schema |
216
285
  | `api.getCommands()` | Get the live command tree reference |
286
+ | `api.getConfigValues()` | Get the values of the CLI's config file, if it has one |
217
287
 
218
288
  ### `getSchema(config)`
219
289
 
@@ -228,15 +298,15 @@ const schema = getSchema(cliConfig);
228
298
 
229
299
  ### `jsonLoader()`
230
300
 
231
- Built-in JSON config file loader:
301
+ Built-in JSON config file loader, always active when `config` is set:
232
302
 
233
303
  ```ts
234
304
  const cli = defineCLI({
235
305
  // ...
236
306
  config: {
237
- name: 'my-tool', // Searches for my-tool.config.json, .my-toolrc.json, etc.
238
- loaders: [jsonLoader()],
239
- searchPaths: ['./'], // Where to search
307
+ name: 'my-tool', // Searches for my-tool.config.json
308
+ searchPaths: ['.', '~'], // Where to search. This is the default
309
+ // loaders: [...] // Extra formats. The first match wins
240
310
  },
241
311
  });
242
312
  ```
@@ -255,6 +325,36 @@ import {
255
325
  throw new CommandError('Deployment failed', { code: 'DEPLOY_FAILED', exitCode: 3 });
256
326
  ```
257
327
 
328
+ #### How a failed run ends
329
+
330
+ No failure exits silently. The reason goes to stderr, `cleanup` runs, and then the process exits.
331
+
332
+ | Case | stderr | Exit code |
333
+ |---|---|---|
334
+ | `RunaError` with a message | The message, as written | The error's |
335
+ | Any other error | `Unexpected error in '<command>': <message>` and a hint | 1 |
336
+ | Error without a message | `'<command>' failed without an error message (<type>).` and a hint | The error's, or 1 |
337
+ | `onError` sets `ctx.handled` | Nothing from Runa | The error's, or 1 |
338
+
339
+ - Usage errors end with `Run '<cli> --help' for usage.`
340
+ - Throw `CommandError` for messages meant for people using your CLI.
341
+ - Set `RUNA_DEBUG=1` to add the stack trace, error code and cause.
342
+ - `ctx.handled` only stops the default output. If a plugin truly recovered, it sets `process.exitCode = 0` in `onError`.
343
+
344
+ #### How a run stops
345
+
346
+ 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.
347
+
348
+ Pass `signal` to whatever the command starts, so the stop reaches it too:
349
+
350
+ ```ts
351
+ async run({ signal }) {
352
+ await fetch(url, { signal });
353
+ }
354
+ ```
355
+
356
+ A command that blocks the event loop can't react to a signal until it yields. Use `Ctrl+\` to end it at once.
357
+
258
358
  ### Constants
259
359
 
260
360
  ```ts
@@ -309,7 +409,7 @@ beforeRun
309
409
  ↓
310
410
  afterRun
311
411
  ↓
312
- cleanup ← (ALWAYS runs, even on error)
412
+ cleanup ← (Runs on errors and on signals, within the stop deadline)
313
413
 
314
414
  On error → onError hook fires before cleanup
315
415
  ```
@@ -328,6 +428,8 @@ For example, if an option has `env: 'PORT'` and `z.number().default(3000)`:
328
428
  3. Config file value wins over default
329
429
  4. `3000` is the fallback
330
430
 
431
+ 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.
432
+
331
433
  ## Types
332
434
 
333
435
  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;AAmnBD;;;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"}