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 +11 -0
- package/dist/cli.js +17 -1
- package/dist/commander-command.d.ts +10 -0
- package/dist/commander-command.js +16 -0
- package/dist/dev.d.ts +47 -0
- package/dist/dev.js +132 -0
- package/dist/execute.d.ts +22 -1
- package/dist/execute.js +36 -0
- package/dist/help.d.ts +19 -7
- package/dist/help.js +50 -21
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +12 -1
- package/dist/mcp.d.ts +14 -2
- package/dist/mcp.js +15 -2
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +7 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +8 -0
- package/dist/testing-helpers.d.ts +18 -1
- package/dist/testing-helpers.js +35 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +1 -1
- package/package.json +2 -2
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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
|
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}${
|
|
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}${
|
|
144
|
+
lines.push(`${INDENT}${cell}`, ...wrapped.map((l) => `${continuation}${l}`));
|
|
118
145
|
continue;
|
|
119
146
|
}
|
|
120
|
-
|
|
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';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
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;
|
package/dist/testing-helpers.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
98
|
+
"vitest": "^5.0.0"
|
|
99
99
|
},
|
|
100
100
|
"bin": {
|
|
101
101
|
"burgee": "./dist/cli.js"
|