burgee 0.2.0 → 0.3.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.
package/dist/cli.d.ts CHANGED
@@ -48,4 +48,15 @@ export declare const brandCommand: import("./execute.js").Command<{
48
48
  readonly description: "emit anyway when a contrast check fails. Says so in the output";
49
49
  };
50
50
  }>;
51
+ /**
52
+ * `burgee dev <entry>` — watch the entry, reload it on change, serve it as MCP on stdio
53
+ * and print every surface on each save (dev-loop). Loaded only when asked for, so the
54
+ * CLI's own weight and the framework's stay what they were (W4).
55
+ */
56
+ export declare const devCommand: import("./execute.js").Command<{
57
+ readonly 'no-watch': {
58
+ readonly type: "boolean";
59
+ readonly description: "load once and serve; do not watch for changes";
60
+ };
61
+ }>;
51
62
  export declare const program: import("./manifest.js").Manifest;
package/dist/cli.js CHANGED
@@ -97,9 +97,25 @@ export const brandCommand = defineCommand({
97
97
  return { out: options.out, files: written.map((s) => s.file), contrast };
98
98
  },
99
99
  });
100
+ export const devCommand = defineCommand({
101
+ name: 'dev',
102
+ description: 'Watch a CLI entry, reload it on change, and serve it as MCP on stdio while you write it',
103
+ arguments: [{ name: 'entry', description: 'the module that exports the program, as program or as its default export', required: true }],
104
+ options: {
105
+ 'no-watch': { type: 'boolean', description: 'load once and serve; do not watch for changes' },
106
+ },
107
+ run: async ({ positionals, options }) => {
108
+ const [entry] = positionals;
109
+ if (entry === undefined)
110
+ throw new Error('an entry file is required');
111
+ const [{ dev }, { processRuntime }] = await Promise.all([import('./dev.js'), import('./runtime.js')]);
112
+ const handle = dev({ entry, input: processRuntime.stdin, output: processRuntime.stdout, log: processRuntime.stderr, watch: options['no-watch'] !== true });
113
+ await handle.done;
114
+ },
115
+ });
100
116
  export const program = defineProgram({
101
117
  name: 'burgee',
102
118
  description: 'The agent-native CLI framework, and the tools that come with it',
103
- commands: [brandCommand],
119
+ commands: [brandCommand, devCommand],
104
120
  });
105
121
  run(program);
@@ -127,6 +127,9 @@ export declare class Command extends EventEmitter {
127
127
  _manifest: Manifest | undefined;
128
128
  /** burgee: what this command does to the world (N6); declaring it exposes the command as an MCP tool. */
129
129
  _effects: Effects | undefined;
130
+ /** burgee: `true`, or the replacement's name (M5). Shown in help, schema and a one-line warning on use. */
131
+ _deprecated: boolean | string | undefined;
132
+ _deprecationWarned: boolean;
130
133
  /** burgee: set for the duration of a parse that injected the streams or the exit. */
131
134
  _burgee: Burgee | undefined;
132
135
  constructor(name?: string);
@@ -313,6 +316,11 @@ export declare class Command extends EventEmitter {
313
316
  _optionSpecs(): Record<string, OptionSpec>;
314
317
  /** burgee: declare what the command does to the world (N6). This is what exposes it as an MCP tool (N2). */
315
318
  effects(value: Effects): this;
319
+ /**
320
+ * burgee: mark the command deprecated (M5). Help and `--schema` show it; running it prints
321
+ * `warning: 'old' is deprecated, use 'new'` on stderr once and goes on, exit unchanged.
322
+ */
323
+ deprecate(use?: string): this;
316
324
  /**
317
325
  * burgee: `--schema` and `--mcp` on a commander-syntax program, from its manifest (J2).
318
326
  * Only when the program declares neither option itself; `--mcp` runs commands through
@@ -331,6 +339,8 @@ export declare class Command extends EventEmitter {
331
339
  _takeJson(unknown: string[]): void;
332
340
  /** The action, wrapped in the plugin hooks and followed by the envelope or the rendering. */
333
341
  _runAction(): unknown;
342
+ /** burgee (M5): once per process, on stderr; only for a command that asked, so commander's own output is untouched. */
343
+ _warnDeprecated(): void;
334
344
  /** burgee: where every option value came from, from commander's own value sources (V3). */
335
345
  _provenance(): Record<string, {
336
346
  source: string;
@@ -98,6 +98,8 @@ export class Command extends EventEmitter {
98
98
  _usage = undefined;
99
99
  _manifest = undefined;
100
100
  _effects = undefined;
101
+ _deprecated = undefined;
102
+ _deprecationWarned = false;
101
103
  _burgee = undefined;
102
104
  constructor(name) {
103
105
  super();
@@ -1375,6 +1377,8 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1375
1377
  ...(cmd._description ? { description: cmd._description } : {}),
1376
1378
  ...(cmd._summary ? { summary: cmd._summary } : {}),
1377
1379
  ...(cmd._effects === undefined ? {} : { effects: cmd._effects }),
1380
+ ...(cmd._deprecated === undefined ? {} : { deprecated: cmd._deprecated }),
1381
+ ...(cmd._helpGroupHeading === undefined || cmd._helpGroupHeading === '' ? {} : { group: cmd._helpGroupHeading }),
1378
1382
  ...(cmd._hidden ? { hidden: true } : {}),
1379
1383
  options: cmd._optionSpecs(),
1380
1384
  arguments: cmd.registeredArguments.map((a) => ({ name: a.name(), required: a.required, variadic: a.variadic, ...(a.description ? { description: a.description } : {}) })),
@@ -1407,6 +1411,10 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1407
1411
  this._effects = value;
1408
1412
  return this;
1409
1413
  }
1414
+ deprecate(use) {
1415
+ this._deprecated = use ?? true;
1416
+ return this;
1417
+ }
1410
1418
  _burgeeSurface(userArgs) {
1411
1419
  const root = this._root();
1412
1420
  const declared = (flag, at = root) => at._findOption(flag) !== undefined || at.commands.some((sub) => declared(flag, sub));
@@ -1529,6 +1537,7 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1529
1537
  const handler = this._actionHandler;
1530
1538
  if (handler === null)
1531
1539
  return undefined;
1540
+ this._warnDeprecated();
1532
1541
  const root = this._root();
1533
1542
  const manifest = root._manifest;
1534
1543
  const settle = (value) => this._emitResult(value);
@@ -1546,6 +1555,13 @@ Expecting one of '${HELP_POSITIONS.join("', '")}'`);
1546
1555
  settle(value);
1547
1556
  });
1548
1557
  }
1558
+ _warnDeprecated() {
1559
+ if (this._deprecated === undefined || this._deprecated === false || this._deprecationWarned)
1560
+ return;
1561
+ this._deprecationWarned = true;
1562
+ const use = typeof this._deprecated === 'string' ? `, use '${this._deprecated}'` : '';
1563
+ this._outputConfiguration.writeErr(`warning: '${this.name()}' is deprecated${use}\n`);
1564
+ }
1549
1565
  _provenance() {
1550
1566
  const out = {};
1551
1567
  const names = { cli: 'flag', env: 'env', config: 'config', default: 'default', implied: 'implied' };
package/dist/dev.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { Manifest } from './manifest.js';
2
+ import { type Invoke } from './mcp.js';
3
+ export interface Writer {
4
+ write: (s: string) => unknown;
5
+ }
6
+ export interface DevOptions {
7
+ /** The module that exports the program: `program` (or `default`) as a burgee manifest, a commander `Command` or a yargs instance. */
8
+ entry: string;
9
+ /** The MCP channel. */
10
+ input: NodeJS.ReadableStream;
11
+ output: Writer;
12
+ /** Where the reload report goes: never stdout, which carries MCP. */
13
+ log: Writer;
14
+ /** Off for tests that drive `reload()` themselves. */
15
+ watch?: boolean;
16
+ /** Files changed within this window coalesce into one reload. */
17
+ debounceMs?: number;
18
+ }
19
+ export interface Loaded {
20
+ manifest: Manifest;
21
+ invoke: Invoke;
22
+ kind: 'burgee' | 'commander' | 'yargs';
23
+ /** Milliseconds from the trigger to the manifest being served (W6). */
24
+ ms: number;
25
+ }
26
+ export interface DevHandle {
27
+ ready: Promise<Loaded>;
28
+ /** Re-import the entry, print the diff and the help, swap the served manifest. */
29
+ reload: () => Promise<Loaded>;
30
+ /** Settles when the MCP input closes. */
31
+ done: Promise<void>;
32
+ close: () => void;
33
+ }
34
+ /**
35
+ * Import the entry as a fresh module graph. The query is what discards the previous
36
+ * graph: the same URL would hand back the cached module and its stale handlers.
37
+ */
38
+ export declare function load(entry: string, generation: number): Promise<Loaded>;
39
+ /** What changed between two manifests, by command path: added, removed, or a different schema. */
40
+ export declare function diffManifests(before: Manifest | undefined, after: Manifest): {
41
+ added: string[];
42
+ removed: string[];
43
+ changed: string[];
44
+ };
45
+ /** The reload report: one save, every surface (W3). */
46
+ export declare function report(before: Manifest | undefined, loaded: Loaded): string;
47
+ export declare function dev(opts: DevOptions): DevHandle;
package/dist/dev.js ADDED
@@ -0,0 +1,132 @@
1
+ import { watch } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { execute } from './execute.js';
5
+ import { renderHelp } from './help.js';
6
+ import { Manifest } from './manifest.js';
7
+ import { startMcp, toolsOf } from './mcp.js';
8
+ import { commandSchemaOf } from './schema.js';
9
+ const DEFAULT_DEBOUNCE_MS = 50;
10
+ const SOURCE = /\.(m?[jt]s|c[jt]s|json)$/;
11
+ const isObject = (x) => x !== null && x !== undefined && typeof x === 'object' && !Array.isArray(x);
12
+ function isManifest(x) {
13
+ return x instanceof Manifest || (isObject(x) && Array.isArray(x['commands']) && Array.isArray(x['rootPath']));
14
+ }
15
+ function isYargs(x) {
16
+ return isObject(x) && typeof x['burgee'] === 'function' && isObject(x['manifest']);
17
+ }
18
+ function isCommander(x) {
19
+ return isObject(x) && typeof x['parseAsync'] === 'function' && isObject(x['manifest']);
20
+ }
21
+ function invokeOn(program, kind, entry) {
22
+ return async (argv) => {
23
+ const out = [];
24
+ const err = [];
25
+ let code = 0;
26
+ const seam = { stdout: { write: (s) => out.push(s) }, stderr: { write: (s) => err.push(s) }, exit: (c) => void (code = c) };
27
+ if (kind === 'burgee')
28
+ await execute(program, { argv, from: 'user', entry, ...seam });
29
+ else if (kind === 'yargs')
30
+ await program.burgee(seam).parseAsync(argv);
31
+ else
32
+ await program.parseAsync(argv, { from: 'user', ...seam });
33
+ return { stdout: out.join(''), stderr: err.join(''), code };
34
+ };
35
+ }
36
+ export async function load(entry, generation) {
37
+ const started = performance.now();
38
+ const url = pathToFileURL(resolve(entry));
39
+ url.searchParams.set('burgee-dev', String(generation));
40
+ const mod = (await import(url.href));
41
+ const exported = mod['program'] ?? mod['default'];
42
+ if (isManifest(exported))
43
+ return { manifest: exported, invoke: invokeOn(exported, 'burgee', entry), kind: 'burgee', ms: performance.now() - started };
44
+ if (isYargs(exported))
45
+ return { manifest: exported.manifest, invoke: invokeOn(exported, 'yargs', entry), kind: 'yargs', ms: performance.now() - started };
46
+ if (isCommander(exported))
47
+ return { manifest: exported.manifest, invoke: invokeOn(exported, 'commander', entry), kind: 'commander', ms: performance.now() - started };
48
+ throw new Error(`${entry} exports no program: export a burgee manifest, a commander Command or a yargs instance as \`program\` or default`);
49
+ }
50
+ const schemasByPath = (m) => new Map(m.commands.map((c) => [c.path.join(' '), JSON.stringify(commandSchemaOf(c, m.rootPath))]));
51
+ export function diffManifests(before, after) {
52
+ const was = before === undefined ? new Map() : schemasByPath(before);
53
+ const now = schemasByPath(after);
54
+ return {
55
+ added: [...now.keys()].filter((k) => !was.has(k)),
56
+ removed: [...was.keys()].filter((k) => !now.has(k)),
57
+ changed: [...now.keys()].filter((k) => was.has(k) && was.get(k) !== now.get(k)),
58
+ };
59
+ }
60
+ export function report(before, loaded) {
61
+ const { manifest } = loaded;
62
+ const diff = diffManifests(before, manifest);
63
+ const lines = [];
64
+ lines.push(`${before === undefined ? 'loaded' : 'reloaded'} ${manifest.rootPath.join(' ')} (${loaded.kind}) in ${loaded.ms.toFixed(0)} ms — ${manifest.commands.length} commands, ${toolsOf(manifest).length} MCP tools`);
65
+ for (const p of diff.added)
66
+ lines.push(` + ${p}`);
67
+ for (const p of diff.removed)
68
+ lines.push(` - ${p}`);
69
+ for (const p of diff.changed)
70
+ lines.push(` ~ ${p}`);
71
+ const root = manifest.find(manifest.rootPath);
72
+ if (root !== undefined)
73
+ lines.push('', renderHelp(manifest, root).trimEnd());
74
+ return `${lines.join('\n')}\n`;
75
+ }
76
+ export function dev(opts) {
77
+ let generation = 0;
78
+ let current;
79
+ let server;
80
+ let watcher;
81
+ let timer;
82
+ let chain = Promise.resolve();
83
+ const entry = resolve(opts.entry);
84
+ const reloadNow = async () => {
85
+ generation += 1;
86
+ const loaded = await load(entry, generation);
87
+ opts.log.write(report(current, loaded));
88
+ current = loaded.manifest;
89
+ if (server === undefined)
90
+ server = startMcp(loaded.manifest, { input: opts.input, output: opts.output, invoke: loaded.invoke });
91
+ else
92
+ server.swap(loaded.manifest, loaded.invoke);
93
+ return loaded;
94
+ };
95
+ const reload = async () => {
96
+ await chain;
97
+ const next = reloadNow();
98
+ chain = next.catch((err) => {
99
+ opts.log.write(`reload failed: ${err instanceof Error ? err.message : String(err)}\n`);
100
+ });
101
+ return await next;
102
+ };
103
+ const ready = reload();
104
+ if (opts.watch !== false) {
105
+ watcher = watch(dirname(entry), { recursive: true }, (_event, filename) => {
106
+ const name = filename === null ? '' : String(filename);
107
+ if (name !== '' && (!SOURCE.test(name) || name.includes('node_modules')))
108
+ return;
109
+ clearTimeout(timer);
110
+ timer = setTimeout(() => void reload().catch(() => undefined), opts.debounceMs ?? DEFAULT_DEBOUNCE_MS);
111
+ });
112
+ }
113
+ const untilInputCloses = async () => {
114
+ try {
115
+ await ready;
116
+ await server.done;
117
+ }
118
+ finally {
119
+ watcher?.close();
120
+ }
121
+ };
122
+ const done = untilInputCloses();
123
+ return {
124
+ ready,
125
+ reload,
126
+ done,
127
+ close: () => {
128
+ clearTimeout(timer);
129
+ watcher?.close();
130
+ },
131
+ };
132
+ }
package/dist/execute.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ArgumentSpec, type Effects, type Example, Manifest, type OptionSpec, type Relation, type RunContext } from './manifest.js';
1
+ import { type ArgumentSpec, type CommandNode, type Effects, type Example, type LazyModule, Manifest, type OptionSpec, type Relation, type RunContext } from './manifest.js';
2
2
  export interface CommandContext<O> extends Omit<RunContext, 'options'> {
3
3
  options: O;
4
4
  }
@@ -39,6 +39,8 @@ export interface Command<S extends OptionSpecs = OptionSpecs> {
39
39
  relations?: readonly Relation[];
40
40
  /** Absent on a group that only holds subcommands. `NoInfer`: the spec fixes S, the handler only reads it. */
41
41
  run?: (ctx: CommandContext<InferOptions<NoInfer<S>>>) => unknown;
42
+ /** The handler's module, imported on dispatch only (M2); everything else about the command is declared here. */
43
+ load?: () => Promise<LazyModule>;
42
44
  commands?: AnyCommand[];
43
45
  }
44
46
  export interface Program {
@@ -59,6 +61,13 @@ export interface Program {
59
61
  commands: AnyCommand[];
60
62
  }
61
63
  export declare function defineCommand<const S extends OptionSpecs = OptionSpecs>(command: Command<S>): Command<S>;
64
+ /**
65
+ * Options declared once and spread into each command that takes them (M4): never global,
66
+ * so `--schema` stays a tree and each command's help lists them as its own, and the
67
+ * handler's `options` type carries them like any other. Every copy is tagged
68
+ * `sharedFrom` with the set's name, so the schema says where it came from.
69
+ */
70
+ export declare function sharedOptions<const T extends OptionSpecs>(name: string, specs: T): T;
62
71
  /** A native multi-command program. The manifest it builds is the same one the façades fill. */
63
72
  export declare function defineProgram(program: Program): Manifest;
64
73
  export interface RunOptions {
@@ -89,6 +98,18 @@ export declare function execute(manifest: Manifest, opts?: RunOptions & {
89
98
  root?: string[];
90
99
  from?: 'node' | 'user';
91
100
  }): Promise<void>;
101
+ /** M6: which command argv names, or null — the same longest-prefix match `execute` uses. */
102
+ export declare function resolveCommand(manifest: Manifest, argv: readonly string[]): CommandNode | null;
103
+ export interface RunResult {
104
+ code: number;
105
+ stdout: string;
106
+ stderr: string;
107
+ }
108
+ /**
109
+ * M6: run a command programmatically — the harness's own entry, public. The streams are
110
+ * captured, the exit recorded; env, cwd and the entry can be injected like `execute`'s.
111
+ */
112
+ export declare function runCommand(manifest: Manifest, argv: readonly string[], opts?: Pick<RunOptions, 'env' | 'cwd' | 'entry' | 'stdin'>): Promise<RunResult>;
92
113
  /**
93
114
  * The one-file entry: a single command, or a program from `defineProgram`. Both go
94
115
  * through `execute`, so there is exactly one code path from argv to exit.
package/dist/execute.js CHANGED
@@ -51,11 +51,15 @@ function addTree(manifest, parent, commands) {
51
51
  ...helpFields(c),
52
52
  options: c.options ?? {},
53
53
  ...(c.run === undefined ? {} : { run: c.run }),
54
+ ...(c.load === undefined ? {} : { load: c.load }),
54
55
  });
55
56
  if (c.commands !== undefined)
56
57
  addTree(manifest, path, c.commands);
57
58
  }
58
59
  }
60
+ export function sharedOptions(name, specs) {
61
+ return Object.fromEntries(Object.entries(specs).map(([key, spec]) => [key, { ...spec, sharedFrom: name }]));
62
+ }
59
63
  export function defineProgram(program) {
60
64
  const manifest = new Manifest();
61
65
  manifest.rootPath = [program.name];
@@ -342,6 +346,8 @@ async function dispatch(manifest, { node, rest, name }, io) {
342
346
  checkRelations(node.relations, resolved.values, provenance);
343
347
  const values = await coerce(node.options, resolved.values);
344
348
  const { positionals, passthrough } = splitPositionals(parsed.tokens);
349
+ requirePositionals(node, positionals);
350
+ warnDeprecated(node, io);
345
351
  await manifest.fire('preRun', name, values);
346
352
  const exit = (code) => {
347
353
  io.exit(code);
@@ -353,6 +359,25 @@ async function dispatch(manifest, { node, rest, name }, io) {
353
359
  const changed = changedOf(node, data);
354
360
  return { json, data, provenance, ...(changed === undefined ? {} : { changed }) };
355
361
  }
362
+ function requirePositionals(node, positionals) {
363
+ const required = (node.arguments ?? []).filter((a) => a.required !== false && a.variadic !== true);
364
+ const missing = required[positionals.length];
365
+ if (positionals.length < required.length && missing !== undefined) {
366
+ throw new UsageError(`missing required argument "${missing.name}"`, `run --help to see what "${node.path.slice(1).join(' ')}" takes`);
367
+ }
368
+ }
369
+ const warned = new Set();
370
+ function warnDeprecated(node, io) {
371
+ if (node.deprecated === undefined || node.deprecated === false)
372
+ return;
373
+ const key = node.path.join(' ');
374
+ if (warned.has(key))
375
+ return;
376
+ warned.add(key);
377
+ const typed = node.path.slice(1).join(' ') || key;
378
+ const use = typeof node.deprecated === 'string' ? `, use '${node.deprecated}'` : '';
379
+ io.err.write(`warning: '${typed}' is deprecated${use}\n`);
380
+ }
356
381
  function emit(io, outcome) {
357
382
  if (outcome.text !== undefined) {
358
383
  io.out.write(outcome.text);
@@ -418,6 +443,17 @@ export async function execute(manifest, opts = {}) {
418
443
  return await report(cause, { manifest, io, argv, json, name });
419
444
  }
420
445
  }
446
+ export function resolveCommand(manifest, argv) {
447
+ const { node } = manifest.resolve(beforeTerminator(argv), manifest.rootPath);
448
+ return node ?? null;
449
+ }
450
+ export async function runCommand(manifest, argv, opts = {}) {
451
+ const out = [];
452
+ const err = [];
453
+ let code = ExitCode.OK;
454
+ await execute(manifest, { ...opts, argv: [...argv], from: 'user', stdout: { write: (s) => out.push(s) }, stderr: { write: (s) => err.push(s) }, exit: (c) => void (code = c) });
455
+ return { code, stdout: out.join(''), stderr: err.join('') };
456
+ }
421
457
  export async function run(target, opts = {}) {
422
458
  if (target instanceof Manifest)
423
459
  return await execute(target, opts);
package/dist/help.d.ts CHANGED
@@ -1,22 +1,34 @@
1
+ import type { CommandNode, Manifest } from './manifest.js';
2
+ /** The token names of `roundel`'s R3, typed structurally: burgee never imports them (U13). */
3
+ export type HelpToken = 'error' | 'warn' | 'ok' | 'hint' | 'muted' | 'command' | 'flag' | 'value' | 'heading';
1
4
  /**
2
- * Help, rendered from the manifest and nothing else (H1). One layout for every node,
3
- * so a subcommand's help has everything the root's has (yargs #1500, #1331, #1025).
5
+ * Per-token styling for help (R7). A user who has `roundel` passes its tokens; a user
6
+ * who does not gets the defaults. Help reads `heading`, `command`, `flag` and `value`;
7
+ * the others are accepted so one theme object serves the whole output stack.
4
8
  *
5
- * Section order is fixed (R2): usage, description, arguments, options, global options,
6
- * commands (grouped, yargs #684), examples, environment, epilogue. Empty sections are
7
- * omitted. Width comes from the caller — the runtime, in practice (H3) — default 100.
9
+ * What the theme does not touch: the command name after `Usage:` and every `$ example`
10
+ * line are rendered plain, whatever the theme says. Styling wraps a finished cell, so a
11
+ * name is measured and padded plain and never coloured in the manifest (yargs #1699).
8
12
  */
9
- import type { CommandNode, Manifest } from './manifest.js';
13
+ export type HelpTheme = Partial<Record<HelpToken, (s: string) => string>>;
10
14
  export interface HelpOptions {
11
15
  /** Columns available; 100 when unknown, never `process.stdout` directly (H3). */
12
16
  width?: number;
13
17
  /** Show type hints such as `[string]`; off by default (H6). */
14
18
  verbose?: boolean;
19
+ /** Apply colour (R7). Off by default: the renderer is pure, so the TTY and NO_COLOR decision stays with the caller. */
20
+ color?: boolean;
21
+ /**
22
+ * Replaces the default styling token by token; read only when `color` is on. See
23
+ * `HelpTheme` for the lines it leaves plain: the `Usage:` command name and `$ example`.
24
+ */
25
+ theme?: HelpTheme;
15
26
  }
16
27
  /** Word-wrap one paragraph; lines the author indented are kept verbatim (yargs #2120). */
17
28
  export declare function wrap(text: string, width: number): string[];
18
29
  /**
19
30
  * Render help for one node — a runnable command, a group, or both — as text.
20
- * Deterministic for a given node and width; a snapshot suite pins it.
31
+ * Deterministic for a given node and width; a snapshot suite pins it. With `color`
32
+ * off — the default — a theme changes nothing; with it on, only ANSI is added.
21
33
  */
22
34
  export declare function renderHelp(manifest: Manifest, node: CommandNode, opts?: HelpOptions): string;
package/dist/help.js CHANGED
@@ -1,11 +1,36 @@
1
+ import { styleText } from 'node:util';
1
2
  import { kebab } from './names.js';
3
+ const identity = (s) => s;
4
+ const PLAIN = { heading: identity, command: identity, flag: identity, value: identity };
5
+ const DEFAULTS = {
6
+ heading: (s) => styleText('bold', s, { validateStream: false }),
7
+ command: (s) => styleText('bold', s, { validateStream: false }),
8
+ flag: (s) => styleText('cyan', s, { validateStream: false }),
9
+ value: (s) => styleText('dim', s, { validateStream: false }),
10
+ };
11
+ function painterFor(paint, kind) {
12
+ if (kind === 'command')
13
+ return paint.command;
14
+ return kind === 'flag' ? paint.flag : paint.value;
15
+ }
16
+ function paintOf(opts) {
17
+ if (opts.color !== true)
18
+ return PLAIN;
19
+ const theme = opts.theme ?? {};
20
+ return {
21
+ heading: theme.heading ?? DEFAULTS.heading,
22
+ command: theme.command ?? DEFAULTS.command,
23
+ flag: theme.flag ?? DEFAULTS.flag,
24
+ value: theme.value ?? DEFAULTS.value,
25
+ };
26
+ }
2
27
  const DEFAULT_WIDTH = 100;
3
28
  const INDENT = ' ';
4
29
  const GUTTER = 2;
5
30
  const TERM_SHARE = 0.4;
6
31
  const GLOBAL = [
7
- { term: '--json', text: 'machine-readable output' },
8
- { term: '--help', text: 'show this help' },
32
+ { term: '--json', text: 'machine-readable output', kind: 'flag' },
33
+ { term: '--help', text: 'show this help', kind: 'flag' },
9
34
  ];
10
35
  function deprecation(d) {
11
36
  if (d === undefined || d === false)
@@ -41,7 +66,7 @@ function optionTerm(name, spec) {
41
66
  function optionRows(options, verbose) {
42
67
  return Object.entries(options)
43
68
  .filter(([, spec]) => spec.hidden !== true)
44
- .map(([name, spec]) => ({ term: optionTerm(name, spec), text: annotate(spec.description ?? '', spec, verbose) }));
69
+ .map(([name, spec]) => ({ term: optionTerm(name, spec), text: annotate(spec.description ?? '', spec, verbose), kind: 'flag' }));
45
70
  }
46
71
  const argumentTerm = (a) => {
47
72
  const name = a.variadic === true ? `${a.name}...` : a.name;
@@ -51,6 +76,7 @@ function argumentRows(args) {
51
76
  return args.map((a) => ({
52
77
  term: argumentTerm(a),
53
78
  text: [a.description ?? '', a.default === undefined ? '' : `(default: ${a.default})`].filter((p) => p !== '').join(' '),
79
+ kind: 'value',
54
80
  }));
55
81
  }
56
82
  function commandSections(manifest, node) {
@@ -59,7 +85,7 @@ function commandSections(manifest, node) {
59
85
  for (const c of children) {
60
86
  const heading = c.group ?? 'Commands:';
61
87
  const rows = groups.get(heading) ?? [];
62
- rows.push({ term: c.path[c.path.length - 1] ?? '', text: `${c.summary ?? c.description ?? ''}${deprecation(c.deprecated)}`.trim() });
88
+ rows.push({ term: c.path[c.path.length - 1] ?? '', text: `${c.summary ?? c.description ?? ''}${deprecation(c.deprecated)}`.trim(), kind: 'command' });
63
89
  groups.set(heading, rows);
64
90
  }
65
91
  return [...groups].map(([title, rows]) => ({ title, rows }));
@@ -67,9 +93,9 @@ function commandSections(manifest, node) {
67
93
  function environmentRows(options) {
68
94
  return Object.entries(options)
69
95
  .filter(([, spec]) => spec.env !== undefined && spec.hidden !== true)
70
- .map(([name, spec]) => ({ term: spec.env ?? '', text: `--${kebab(name)}` }));
96
+ .map(([name, spec]) => ({ term: spec.env ?? '', text: `--${kebab(name)}`, kind: 'value' }));
71
97
  }
72
- function usageLine(node, root, hasChildren) {
98
+ function usageLine(node, root, hasChildren, paint) {
73
99
  const shown = node.path.slice(root.length).join(' ') || node.path.join(' ');
74
100
  const parts = [shown];
75
101
  if (hasChildren)
@@ -77,7 +103,7 @@ function usageLine(node, root, hasChildren) {
77
103
  parts.push('[options]');
78
104
  for (const a of node.arguments ?? [])
79
105
  parts.push(argumentTerm(a));
80
- return `Usage: ${parts.join(' ')}`;
106
+ return `${paint.heading('Usage:')} ${parts.join(' ')}`;
81
107
  }
82
108
  export function wrap(text, width) {
83
109
  const out = [];
@@ -103,21 +129,23 @@ function termColumn(rows, width) {
103
129
  const longest = Math.max(0, ...rows.map((r) => r.term.length));
104
130
  return Math.min(longest, Math.floor(width * TERM_SHARE));
105
131
  }
106
- function layout(rows, width, column) {
132
+ function layout(rows, width, column, paint) {
107
133
  const textWidth = Math.max(1, width - INDENT.length - column - GUTTER);
108
134
  const lines = [];
109
- for (const { term, text } of rows) {
135
+ for (const { term, text, kind } of rows) {
136
+ const cell = painterFor(paint, kind)(term);
110
137
  if (text === '') {
111
- lines.push(`${INDENT}${term}`);
138
+ lines.push(`${INDENT}${cell}`);
112
139
  continue;
113
140
  }
114
141
  const wrapped = wrap(text, textWidth);
115
142
  const continuation = INDENT + ' '.repeat(column + GUTTER);
116
143
  if (term.length > column) {
117
- lines.push(`${INDENT}${term}`, ...wrapped.map((l) => `${continuation}${l}`));
144
+ lines.push(`${INDENT}${cell}`, ...wrapped.map((l) => `${continuation}${l}`));
118
145
  continue;
119
146
  }
120
- lines.push(`${INDENT}${term.padEnd(column + GUTTER)}${wrapped[0] ?? ''}`, ...wrapped.slice(1).map((l) => `${continuation}${l}`));
147
+ const pad = ' '.repeat(column + GUTTER - term.length);
148
+ lines.push(`${INDENT}${cell}${pad}${wrapped[0] ?? ''}`, ...wrapped.slice(1).map((l) => `${continuation}${l}`));
121
149
  }
122
150
  return lines;
123
151
  }
@@ -130,28 +158,29 @@ function exampleLines(examples, width) {
130
158
  }
131
159
  return lines;
132
160
  }
133
- function section(title, body) {
134
- return body.length === 0 ? [] : [title, ...body, ''];
161
+ function section(title, body, paint) {
162
+ return body.length === 0 ? [] : [paint.heading(title), ...body, ''];
135
163
  }
136
164
  export function renderHelp(manifest, node, opts = {}) {
137
165
  const width = opts.width ?? DEFAULT_WIDTH;
138
166
  const verbose = opts.verbose === true;
167
+ const paint = paintOf(opts);
139
168
  const root = manifest.rootPath;
140
169
  const commands = commandSections(manifest, node);
141
170
  const args = argumentRows(node.arguments ?? []);
142
171
  const options = optionRows(node.options, verbose);
143
172
  const env = environmentRows(node.options);
144
173
  const column = termColumn([...args, ...options, ...GLOBAL, ...commands.flatMap((s) => s.rows), ...env], width);
145
- const lines = [usageLine(node, root, commands.length > 0), ''];
174
+ const lines = [usageLine(node, root, commands.length > 0, paint), ''];
146
175
  if (node.description !== undefined)
147
176
  lines.push(...wrap(`${node.description}${deprecation(node.deprecated)}`, width), '');
148
- lines.push(...section('Arguments:', layout(args, width, column)));
149
- lines.push(...section('Options:', layout(options, width, column)));
150
- lines.push(...section('Global options:', layout(GLOBAL, width, column)));
177
+ lines.push(...section('Arguments:', layout(args, width, column, paint), paint));
178
+ lines.push(...section('Options:', layout(options, width, column, paint), paint));
179
+ lines.push(...section('Global options:', layout(GLOBAL, width, column, paint), paint));
151
180
  for (const s of commands)
152
- lines.push(...section(s.title, layout(s.rows, width, column)));
153
- lines.push(...section('Examples:', exampleLines(node.examples ?? [], width)));
154
- lines.push(...section('Environment:', layout(env, width, column)));
181
+ lines.push(...section(s.title, layout(s.rows, width, column, paint), paint));
182
+ lines.push(...section('Examples:', exampleLines(node.examples ?? [], width), paint));
183
+ lines.push(...section('Environment:', layout(env, width, column, paint), paint));
155
184
  if (node.epilogue !== undefined)
156
185
  lines.push(...wrap(node.epilogue, width), '');
157
186
  return `${lines.join('\n').trimEnd()}\n`;
package/dist/index.d.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * façade can import it without pulling this barrel.
7
7
  */
8
8
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
9
- export { defineCommand, defineProgram, execute, run, type AnyCommand, type Command, type CommandContext, type InferOptions, type OptionSpecs, type Program, type RunOptions, } from './execute.js';
9
+ export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, type AnyCommand, type Command, type CommandContext, type InferOptions, type OptionSpecs, type Program, type RunOptions, type RunResult, } from './execute.js';
10
10
  export { camel, checkDefinition, kebab, UsageError } from './validate.js';
11
11
  export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
12
- export { renderHelp, type HelpOptions } from './help.js';
12
+ export { renderHelp, type HelpOptions, type HelpTheme, type HelpToken } from './help.js';
13
13
  export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf, type Invoke, type ServeOptions, type Tool, type ToolAnnotations } from './mcp.js';
14
14
  export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from './precedence.js';
15
15
  export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
16
- export { definePlugin, Manifest, type ArgumentSpec, type CommandNode, type Effects, type Example, type Hook, type OptionSpec, type ActionRequiredSpec, type Plugin, type Relation, type RunContext, type StandardResult, type StandardSchemaV1, } from './manifest.js';
16
+ export { definePlugin, Manifest, type ArgumentSpec, type CommandNode, type Effects, type Example, type Hook, type LazyModule, type OptionSpec, type ActionRequiredSpec, type Plugin, type Relation, type RunContext, type StandardResult, type StandardSchemaV1, } from './manifest.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ExitCode, isExitCode } from './exit-code.js';
2
- export { defineCommand, defineProgram, execute, run, } from './execute.js';
2
+ export { defineCommand, defineProgram, execute, resolveCommand, run, runCommand, sharedOptions, } from './execute.js';
3
3
  export { camel, checkDefinition, kebab, UsageError } from './validate.js';
4
4
  export { AGENT_PROBES, detectAgent } from './agent.js';
5
5
  export { renderHelp } from './help.js';
@@ -53,6 +53,13 @@ export interface OptionSpec {
53
53
  /** `true` renders `(deprecated)`; a string names the replacement: `(deprecated: use --force)` (yargs #2248). */
54
54
  deprecated?: boolean | string;
55
55
  hidden?: boolean;
56
+ /** The shared set this option was copied from (M4); `--schema` carries it, help lists the option like any other. */
57
+ sharedFrom?: string;
58
+ }
59
+ /** What a lazily loaded command module exports: the handler as `run` or as the default export (M2). */
60
+ export interface LazyModule {
61
+ default?: (ctx: RunContext) => unknown;
62
+ run?: (ctx: RunContext) => unknown;
56
63
  }
57
64
  /**
58
65
  * What running a command does to the world (N6). Declared, never inferred: it decides
@@ -133,9 +140,17 @@ export interface CommandNode {
133
140
  /** Required for a command to be served as an MCP tool (N2, N6). */
134
141
  effects?: Effects;
135
142
  run?: (ctx: RunContext) => unknown;
143
+ /**
144
+ * The handler's module, imported on dispatch only (M2): the manifest — help, schema,
145
+ * completions, MCP tool list — is complete from this node without loading it. A node
146
+ * with `load` and no `run` gets a `run` that imports on first call.
147
+ */
148
+ load?: () => Promise<LazyModule>;
136
149
  /** Which plugin contributed this, if any. Declared, never diffed (M3). */
137
150
  plugin?: string;
138
151
  }
152
+ /** `run` for a lazy node: the module is imported on the first call and never before (M2). */
153
+ export declare function lazyRun(load: () => Promise<LazyModule>): (ctx: RunContext) => unknown;
139
154
  /** A hook may declare which commands it applies to, as data. */
140
155
  export interface HookFilter {
141
156
  command?: RegExp;
package/dist/manifest.js CHANGED
@@ -1,3 +1,14 @@
1
+ export function lazyRun(load) {
2
+ let loaded;
3
+ return async (ctx) => {
4
+ loaded ??= load();
5
+ const mod = await loaded;
6
+ const handler = mod.run ?? mod.default;
7
+ if (handler === undefined)
8
+ throw new Error('burgee: a lazy command module must export its handler as run or as the default export');
9
+ return await handler(ctx);
10
+ };
11
+ }
1
12
  export function definePlugin(plugin) {
2
13
  return plugin;
3
14
  }
@@ -16,7 +27,7 @@ export class Manifest {
16
27
  config;
17
28
  schemaBudget;
18
29
  add(node) {
19
- this.commands.push(node);
30
+ this.commands.push(node.load !== undefined && node.run === undefined ? { ...node, run: lazyRun(node.load) } : node);
20
31
  }
21
32
  use(plugin) {
22
33
  this.plugins.push(plugin);
package/dist/mcp.d.ts CHANGED
@@ -33,8 +33,20 @@ export interface ServeOptions {
33
33
  };
34
34
  invoke: Invoke;
35
35
  }
36
+ /** A running server: `done` settles when the input closes; `swap` serves a new manifest and says so (W2). */
37
+ export interface McpServer {
38
+ done: Promise<void>;
39
+ /**
40
+ * Serve this manifest (and its invoke) from the next request on, and emit
41
+ * `notifications/tools/list_changed` so a connected client re-lists. A call already in
42
+ * flight finishes against the manifest it started on.
43
+ */
44
+ swap: (manifest: Manifest, invoke?: Invoke) => void;
45
+ }
36
46
  /**
37
- * Serve until the input closes. Notifications (no `id`) get no reply; a malformed line gets
38
- * a JSON-RPC error with a null id, as the spec asks.
47
+ * Start serving; `done` settles when the input closes. Notifications (no `id`) get no
48
+ * reply; a malformed line gets a JSON-RPC error with a null id, as the spec asks.
39
49
  */
50
+ export declare function startMcp(manifest: Manifest, opts: ServeOptions): McpServer;
51
+ /** Serve until the input closes. */
40
52
  export declare function serveMcp(manifest: Manifest, opts: ServeOptions): Promise<void>;
package/dist/mcp.js CHANGED
@@ -81,14 +81,27 @@ async function handle(session, request) {
81
81
  return { error: { code: JSON_RPC_METHOD_NOT_FOUND, message: `method not found: ${request.method}` } };
82
82
  }
83
83
  }
84
- export async function serveMcp(manifest, opts) {
84
+ export function startMcp(manifest, opts) {
85
85
  const session = {
86
86
  manifest,
87
87
  invoke: opts.invoke,
88
88
  serverInfo: { name: manifest.rootPath.join(' ') || 'burgee', version: manifest.version ?? '0.0.0' },
89
89
  };
90
90
  const reply = (body) => void opts.output.write(`${JSON.stringify({ jsonrpc: '2.0', ...body })}\n`);
91
- for await (const line of createInterface({ input: opts.input, crlfDelay: Infinity })) {
91
+ const swap = (next, invoke) => {
92
+ session.manifest = next;
93
+ if (invoke !== undefined)
94
+ session.invoke = invoke;
95
+ reply({ method: 'notifications/tools/list_changed' });
96
+ };
97
+ const done = serve(session, opts.input, reply);
98
+ return { done, swap };
99
+ }
100
+ export async function serveMcp(manifest, opts) {
101
+ return await startMcp(manifest, opts).done;
102
+ }
103
+ async function serve(session, input, reply) {
104
+ for await (const line of createInterface({ input, crlfDelay: Infinity })) {
92
105
  if (line.trim() === '')
93
106
  continue;
94
107
  let request;
package/dist/runtime.d.ts CHANGED
@@ -3,6 +3,16 @@ import { type ExitCode } from './exit-code.js';
3
3
  export interface Writer {
4
4
  write(chunk: string): unknown;
5
5
  }
6
+ /**
7
+ * Time, as the layers above the parser see it (R14 of `cli-output-stack`). `now` is
8
+ * monotonic milliseconds from an arbitrary origin; `schedule` runs `fn` after `ms` and
9
+ * hands back the cancel. Typed structurally so the output stack can accept a `Runtime`
10
+ * without importing one.
11
+ */
12
+ export interface Clock {
13
+ now(): number;
14
+ schedule(fn: () => void, ms: number): () => void;
15
+ }
6
16
  /**
7
17
  * The world, as the layers above the parser see it (design R1 of
8
18
  * `cli-testing-harness`). Nothing above the parser reads `process.*` directly; it
@@ -22,6 +32,8 @@ export interface Runtime {
22
32
  };
23
33
  /** Ends the run with an E1 code. In the real runtime this never returns. */
24
34
  exit(code: ExitCode): never;
35
+ /** `performance.now` and `setTimeout` in the real runtime; a manual tick in the harness. */
36
+ clock: Clock;
25
37
  }
26
38
  /** The one place in the layer that touches `process`. Locked by `process-reference.lock.test.ts`. */
27
39
  export declare const processRuntime: Runtime;
package/dist/runtime.js CHANGED
@@ -14,4 +14,11 @@ export const processRuntime = {
14
14
  exit(code) {
15
15
  process.exit(code);
16
16
  },
17
+ clock: {
18
+ now: () => performance.now(),
19
+ schedule(fn, ms) {
20
+ const handle = setTimeout(fn, ms);
21
+ return () => clearTimeout(handle);
22
+ },
23
+ },
17
24
  };
package/dist/schema.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface JsonSchemaProperty {
24
24
  maximum?: number;
25
25
  /** The command-line spelling of the option (S5). */
26
26
  flag?: string;
27
+ /** The shared set this option was copied from (M4). */
28
+ sharedFrom?: string;
27
29
  }
28
30
  export interface CommandSchema {
29
31
  /** The command as typed, without the program name: `config get`. */
@@ -32,6 +34,12 @@ export interface CommandSchema {
32
34
  summary?: string;
33
35
  effects?: Effects;
34
36
  deprecated?: boolean | string;
37
+ /** The heading it is listed under (M1). */
38
+ group?: string;
39
+ /** Its handler loads on dispatch (M2): this schema was complete without it. */
40
+ lazy?: true;
41
+ /** Which plugin contributed it (M3). */
42
+ plugin?: string;
35
43
  arguments: ArgumentSpec[];
36
44
  options: Record<string, OptionSpec>;
37
45
  examples: Example[];
package/dist/schema.js CHANGED
@@ -30,6 +30,8 @@ function optionProperty(name, spec) {
30
30
  p.minimum = spec.minimum;
31
31
  if (spec.maximum !== undefined)
32
32
  p.maximum = spec.maximum;
33
+ if (spec.sharedFrom !== undefined)
34
+ p.sharedFrom = spec.sharedFrom;
33
35
  return p;
34
36
  }
35
37
  export function inputSchemaOf(node) {
@@ -68,6 +70,12 @@ export function commandSchemaOf(node, root) {
68
70
  out.effects = node.effects;
69
71
  if (node.deprecated !== undefined)
70
72
  out.deprecated = node.deprecated;
73
+ if (node.group !== undefined)
74
+ out.group = node.group;
75
+ if (node.load !== undefined)
76
+ out.lazy = true;
77
+ if (node.plugin !== undefined)
78
+ out.plugin = node.plugin;
71
79
  return out;
72
80
  }
73
81
  export function runnable(manifest) {
@@ -8,7 +8,7 @@
8
8
  import { Readable } from 'node:stream';
9
9
  import { ExitCode } from './exit-code.js';
10
10
  import { type Manifest } from './manifest.js';
11
- import { type Runtime } from './runtime.js';
11
+ import { type Clock, type Runtime } from './runtime.js';
12
12
  export interface RunOptions {
13
13
  argv: string[];
14
14
  env?: Record<string, string>;
@@ -16,6 +16,8 @@ export interface RunOptions {
16
16
  cwd?: string;
17
17
  /** `true` = every stream is a TTY; an object sets each; default: none is. */
18
18
  tty?: boolean | Partial<Runtime['isTTY']>;
19
+ /** The clock the runtime reports; a fresh `fakeClock()` at 0 when not given. */
20
+ clock?: FakeClock;
19
21
  }
20
22
  export interface RunResult {
21
23
  code: ExitCode;
@@ -33,7 +35,22 @@ export declare class RuntimeExit extends Error {
33
35
  export interface FakeRuntime extends Runtime {
34
36
  out: string[];
35
37
  err: string[];
38
+ clock: FakeClock;
36
39
  }
40
+ /** A `Clock` that moves only when the test says so (R14): `tick` is the only source of time. */
41
+ export interface FakeClock extends Clock {
42
+ /**
43
+ * Advance by `ms`, running every callback that falls due, earliest first, then by order
44
+ * scheduled. A callback that schedules inside the window runs in the same tick, so one that
45
+ * reschedules itself at `0` would never leave the loop (real Node yields between turns);
46
+ * a tick runs at most `TICK_CAP` callbacks and throws, naming the cap, when exceeded.
47
+ */
48
+ tick(ms: number): void;
49
+ /** Callbacks scheduled and neither run nor cancelled. */
50
+ pending(): number;
51
+ }
52
+ /** Deterministic time for the harness. Starts at `start` (0 by default) and never moves on its own. */
53
+ export declare function fakeClock(start?: number): FakeClock;
37
54
  /** A `Runtime` whose every part is under the test's control. */
38
55
  export declare function fakeRuntime(opts: RunOptions): FakeRuntime;
39
56
  export declare function swapEnv(env: Record<string, string> | undefined): () => void;
@@ -9,6 +9,40 @@ export class RuntimeExit extends Error {
9
9
  this.name = 'RuntimeExit';
10
10
  }
11
11
  }
12
+ const TICK_CAP = 1000;
13
+ export function fakeClock(start = 0) {
14
+ let now = start;
15
+ let nextId = 0;
16
+ const timers = [];
17
+ const remove = (id) => {
18
+ const at = timers.findIndex((t) => t.id === id);
19
+ if (at !== -1)
20
+ timers.splice(at, 1);
21
+ };
22
+ return {
23
+ now: () => now,
24
+ schedule(fn, ms) {
25
+ const id = nextId++;
26
+ timers.push({ id, at: now + Math.max(0, ms), fn });
27
+ return () => remove(id);
28
+ },
29
+ tick(ms) {
30
+ const until = now + ms;
31
+ const nextDue = () => timers.filter((t) => t.at <= until).sort((a, b) => a.at - b.at || a.id - b.id)[0];
32
+ let ran = 0;
33
+ for (let due = nextDue(); due !== undefined; due = nextDue()) {
34
+ if (ran === TICK_CAP)
35
+ throw new Error(`fakeClock.tick(${ms}) ran ${TICK_CAP} callbacks (TICK_CAP): a callback keeps rescheduling inside the window`);
36
+ ran += 1;
37
+ remove(due.id);
38
+ now = Math.max(now, due.at);
39
+ due.fn();
40
+ }
41
+ now = until;
42
+ },
43
+ pending: () => timers.length,
44
+ };
45
+ }
12
46
  function ttyOf(tty) {
13
47
  if (tty === true)
14
48
  return { stdin: true, stdout: true, stderr: true };
@@ -31,6 +65,7 @@ export function fakeRuntime(opts) {
31
65
  exit(code) {
32
66
  throw new RuntimeExit(code);
33
67
  },
68
+ clock: opts.clock ?? fakeClock(),
34
69
  out,
35
70
  err,
36
71
  };
package/dist/testing.d.ts CHANGED
@@ -5,6 +5,6 @@
5
5
  * A separate entry point so it is paid for per import (K6): a user's shipped CLI
6
6
  * imports `burgee` and never pulls a byte of this.
7
7
  */
8
- export { processRuntime, type Runtime, type Writer } from './runtime.js';
9
- export { captureConsole, codeOf, fakeRuntime, finish, runBurgee, RuntimeExit, stripAnsi, swapEnv, type FakeRuntime, type RunOptions, type RunResult, } from './testing-helpers.js';
8
+ export { processRuntime, type Clock, type Runtime, type Writer } from './runtime.js';
9
+ export { captureConsole, codeOf, fakeClock, fakeRuntime, finish, runBurgee, RuntimeExit, stripAnsi, swapEnv, type FakeClock, type FakeRuntime, type RunOptions, type RunResult, } from './testing-helpers.js';
10
10
  export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';
package/dist/testing.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { processRuntime } from './runtime.js';
2
- export { captureConsole, codeOf, fakeRuntime, finish, runBurgee, RuntimeExit, stripAnsi, swapEnv, } from './testing-helpers.js';
2
+ export { captureConsole, codeOf, fakeClock, fakeRuntime, finish, runBurgee, RuntimeExit, stripAnsi, swapEnv, } from './testing-helpers.js';
3
3
  export { ExitCode, isExitCode } from './exit-code.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "burgee",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "An agent-native CLI framework, drop-in compatible with commander and yargs. One declaration; help, --json, --schema, --mcp and completions all projected from it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -95,7 +95,7 @@
95
95
  "provenance": true
96
96
  },
97
97
  "devDependencies": {
98
- "vitest": "^4.0.0"
98
+ "vitest": "^5.0.0"
99
99
  },
100
100
  "bin": {
101
101
  "burgee": "./dist/cli.js"