burgee 0.0.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.
Files changed (116) hide show
  1. package/README.md +28 -1
  2. package/dist/agent.d.ts +19 -0
  3. package/dist/agent.js +25 -0
  4. package/dist/brand.d.ts +186 -0
  5. package/dist/brand.js +232 -0
  6. package/dist/cli.d.ts +62 -0
  7. package/dist/cli.js +121 -0
  8. package/dist/commander-argument.d.ts +22 -0
  9. package/dist/commander-argument.js +72 -0
  10. package/dist/commander-command.d.ts +355 -0
  11. package/dist/commander-command.js +1621 -0
  12. package/dist/commander-error.d.ts +10 -0
  13. package/dist/commander-error.js +20 -0
  14. package/dist/commander-help.d.ts +67 -0
  15. package/dist/commander-help.js +319 -0
  16. package/dist/commander-option.d.ts +58 -0
  17. package/dist/commander-option.js +164 -0
  18. package/dist/commander-suggest.d.ts +2 -0
  19. package/dist/commander-suggest.js +59 -0
  20. package/dist/commander.d.ts +18 -0
  21. package/dist/commander.js +12 -0
  22. package/dist/completions.d.ts +39 -0
  23. package/dist/completions.js +224 -0
  24. package/dist/config.d.ts +28 -0
  25. package/dist/config.js +98 -0
  26. package/dist/contrast.d.ts +70 -0
  27. package/dist/contrast.js +93 -0
  28. package/dist/dev.d.ts +47 -0
  29. package/dist/dev.js +132 -0
  30. package/dist/execute.d.ts +118 -0
  31. package/dist/execute.js +469 -0
  32. package/dist/exit-code.d.ts +18 -0
  33. package/dist/exit-code.js +12 -0
  34. package/dist/help.d.ts +34 -0
  35. package/dist/help.js +187 -0
  36. package/dist/index.d.ts +16 -1
  37. package/dist/index.js +9 -2
  38. package/dist/manifest.d.ts +207 -0
  39. package/dist/manifest.js +66 -0
  40. package/dist/mcp.d.ts +52 -0
  41. package/dist/mcp.js +124 -0
  42. package/dist/names.d.ts +5 -0
  43. package/dist/names.js +6 -0
  44. package/dist/pkg.d.ts +5 -0
  45. package/dist/pkg.js +21 -0
  46. package/dist/precedence.d.ts +55 -0
  47. package/dist/precedence.js +100 -0
  48. package/dist/runtime.d.ts +39 -0
  49. package/dist/runtime.js +24 -0
  50. package/dist/schema.d.ts +79 -0
  51. package/dist/schema.js +116 -0
  52. package/dist/testing-helpers.d.ts +79 -0
  53. package/dist/testing-helpers.js +145 -0
  54. package/dist/testing.d.ts +10 -0
  55. package/dist/testing.js +3 -0
  56. package/dist/validate.d.ts +27 -0
  57. package/dist/validate.js +133 -0
  58. package/dist/yargs-burgee.d.ts +50 -0
  59. package/dist/yargs-burgee.js +104 -0
  60. package/dist/yargs-cliui.d.ts +56 -0
  61. package/dist/yargs-cliui.js +421 -0
  62. package/dist/yargs-command.d.ts +82 -0
  63. package/dist/yargs-command.js +414 -0
  64. package/dist/yargs-completion.d.ts +41 -0
  65. package/dist/yargs-completion.js +271 -0
  66. package/dist/yargs-factory.d.ts +193 -0
  67. package/dist/yargs-factory.js +1606 -0
  68. package/dist/yargs-helpers.d.ts +6 -0
  69. package/dist/yargs-helpers.js +2 -0
  70. package/dist/yargs-middleware.d.ts +32 -0
  71. package/dist/yargs-middleware.js +81 -0
  72. package/dist/yargs-parser.d.ts +41 -0
  73. package/dist/yargs-parser.js +929 -0
  74. package/dist/yargs-shim.d.ts +54 -0
  75. package/dist/yargs-shim.js +84 -0
  76. package/dist/yargs-usage.d.ts +42 -0
  77. package/dist/yargs-usage.js +479 -0
  78. package/dist/yargs-utils.d.ts +33 -0
  79. package/dist/yargs-utils.js +209 -0
  80. package/dist/yargs-validation.d.ts +26 -0
  81. package/dist/yargs-validation.js +261 -0
  82. package/dist/yargs-y18n.d.ts +21 -0
  83. package/dist/yargs-y18n.js +117 -0
  84. package/dist/yargs.d.ts +6 -0
  85. package/dist/yargs.js +8 -0
  86. package/locales/be.json +46 -0
  87. package/locales/cs.json +51 -0
  88. package/locales/de.json +46 -0
  89. package/locales/en.json +55 -0
  90. package/locales/es.json +46 -0
  91. package/locales/fi.json +49 -0
  92. package/locales/fr.json +53 -0
  93. package/locales/he.json +55 -0
  94. package/locales/hi.json +49 -0
  95. package/locales/hu.json +46 -0
  96. package/locales/id.json +50 -0
  97. package/locales/it.json +46 -0
  98. package/locales/ja.json +51 -0
  99. package/locales/ka.json +55 -0
  100. package/locales/ko.json +49 -0
  101. package/locales/nb.json +44 -0
  102. package/locales/nl.json +49 -0
  103. package/locales/nn.json +44 -0
  104. package/locales/pirate.json +13 -0
  105. package/locales/pl.json +49 -0
  106. package/locales/pt.json +45 -0
  107. package/locales/pt_BR.json +48 -0
  108. package/locales/ru.json +51 -0
  109. package/locales/th.json +46 -0
  110. package/locales/tr.json +48 -0
  111. package/locales/uk_UA.json +51 -0
  112. package/locales/uz.json +52 -0
  113. package/locales/zh_CN.json +48 -0
  114. package/locales/zh_TW.json +51 -0
  115. package/package.json +62 -9
  116. package/dist/index.js.map +0 -1
package/dist/pkg.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface Package {
2
+ path: string;
3
+ data: Record<string, unknown>;
4
+ }
5
+ export declare function nearestPackage(from: string): Package | undefined;
package/dist/pkg.js ADDED
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ export function nearestPackage(from) {
4
+ let dir = from;
5
+ for (let i = 0; i < 64; i++) {
6
+ const at = join(dir, 'package.json');
7
+ if (existsSync(at)) {
8
+ try {
9
+ return { path: at, data: JSON.parse(readFileSync(at, 'utf8')) };
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ const up = dirname(dir);
16
+ if (up === dir)
17
+ return undefined;
18
+ dir = up;
19
+ }
20
+ return undefined;
21
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * One precedence order, fixed and not configurable (commander-env V1–V3, V5):
3
+ *
4
+ * flag > env > config file > package.json field > default
5
+ *
6
+ * `resolve` is pure — it takes the layers and returns the values with their provenance —
7
+ * which is what makes `--explain` trustworthy and `meta.provenance` cheap. Env applies
8
+ * only to the options the running command declares (yargs #873), never to a sibling's.
9
+ */
10
+ import { type OptionSpec } from './manifest.js';
11
+ export type Source = 'flag' | 'env' | 'config' | 'package' | 'default';
12
+ export interface Provenance {
13
+ source: Source;
14
+ /** The env name, the config file, or `package.json` — where a person would look. */
15
+ location?: string;
16
+ }
17
+ export interface Candidate {
18
+ source: Source;
19
+ location: string;
20
+ /** `undefined` when the layer had nothing for this option. */
21
+ value: unknown;
22
+ }
23
+ export interface Layer {
24
+ path: string;
25
+ data: Record<string, unknown>;
26
+ }
27
+ export interface Layers {
28
+ /** What the user typed: only options present on the command line. */
29
+ flags: Record<string, unknown>;
30
+ env: Record<string, string | undefined>;
31
+ /** With a prefix, an option without `env:` reads `PREFIX_OPTION_NAME` (R2). */
32
+ envPrefix?: string;
33
+ config?: Layer;
34
+ /** The `package.json` field named after the program, when present. */
35
+ pkg?: Layer;
36
+ }
37
+ export interface Resolution {
38
+ values: Record<string, unknown>;
39
+ provenance: Record<string, Provenance>;
40
+ candidates: Record<string, Candidate[]>;
41
+ }
42
+ /** A value that cannot be used as configured: exit CONFIG (3), never a stack (E1, E3). */
43
+ export declare class ConfigError extends Error {
44
+ readonly hint?: string | undefined;
45
+ constructor(message: string, hint?: string | undefined);
46
+ }
47
+ /** `region` → `REGION`, `dryRun` → `DRY_RUN`, `log-level` → `LOG_LEVEL` (yargs #2005: never camel-cased back). */
48
+ export declare function screaming(name: string): string;
49
+ export declare function envName(name: string, spec: OptionSpec, prefix: string | undefined): string | undefined;
50
+ /** Booleans from env accept one spelling each way (R7). */
51
+ export declare function envBoolean(raw: string): boolean | undefined;
52
+ /** Every declared option, resolved through the layers; a missing required one is left undefined for the caller to report. */
53
+ export declare function resolve(specs: Record<string, OptionSpec>, layers: Layers): Resolution;
54
+ /** `--explain <option>`: the winning source and every candidate it beat, or that was unset (V3). */
55
+ export declare function explain(name: string, resolution: Resolution): string;
@@ -0,0 +1,100 @@
1
+ export class ConfigError extends Error {
2
+ hint;
3
+ constructor(message, hint) {
4
+ super(message);
5
+ this.hint = hint;
6
+ }
7
+ }
8
+ export function screaming(name) {
9
+ return name
10
+ .replaceAll(/([a-z0-9])([A-Z])/g, '$1_$2')
11
+ .replaceAll('-', '_')
12
+ .toUpperCase();
13
+ }
14
+ export function envName(name, spec, prefix) {
15
+ if (spec.env !== undefined)
16
+ return spec.env;
17
+ return prefix === undefined ? undefined : `${prefix}_${screaming(name)}`;
18
+ }
19
+ const TRUE = new Set(['1', 'true', 'yes']);
20
+ const FALSE = new Set(['0', 'false', 'no']);
21
+ export function envBoolean(raw) {
22
+ const v = raw.trim().toLowerCase();
23
+ if (TRUE.has(v))
24
+ return true;
25
+ if (FALSE.has(v))
26
+ return false;
27
+ return undefined;
28
+ }
29
+ function fromEnv(name, spec, layers) {
30
+ const variable = envName(name, spec, layers.envPrefix);
31
+ if (variable === undefined)
32
+ return undefined;
33
+ if (spec.type === 'boolean' && layers.envPrefix !== undefined) {
34
+ const negated = `${layers.envPrefix}_NO_${screaming(name)}`;
35
+ if (layers.env[negated] !== undefined) {
36
+ throw new ConfigError(`${negated} is not supported`, `set ${variable}=false instead`);
37
+ }
38
+ }
39
+ const raw = layers.env[variable];
40
+ if (raw === undefined)
41
+ return { source: 'env', location: variable, value: undefined };
42
+ if (spec.type !== 'boolean')
43
+ return { source: 'env', location: variable, value: raw };
44
+ const parsed = envBoolean(raw);
45
+ if (parsed === undefined)
46
+ throw new ConfigError(`${variable}="${raw}" is not a boolean`, `use ${variable}=true or ${variable}=false`);
47
+ return { source: 'env', location: variable, value: parsed };
48
+ }
49
+ function candidatesFor(name, spec, layers) {
50
+ const out = [{ source: 'flag', location: `--${name}`, value: layers.flags[name] }];
51
+ const env = fromEnv(name, spec, layers);
52
+ if (env !== undefined)
53
+ out.push(env);
54
+ if (layers.config !== undefined)
55
+ out.push({ source: 'config', location: layers.config.path, value: layers.config.data[name] });
56
+ if (layers.pkg !== undefined)
57
+ out.push({ source: 'package', location: layers.pkg.path, value: layers.pkg.data[name] });
58
+ out.push({ source: 'default', location: 'default', value: spec.default });
59
+ return out;
60
+ }
61
+ export function resolve(specs, layers) {
62
+ const values = new Map();
63
+ const provenance = new Map();
64
+ const candidates = new Map();
65
+ for (const [name, spec] of Object.entries(specs)) {
66
+ const list = candidatesFor(name, spec, layers);
67
+ candidates.set(name, list);
68
+ const winner = list.find((c) => c.value !== undefined);
69
+ if (winner === undefined)
70
+ continue;
71
+ values.set(name, winner.value);
72
+ provenance.set(name, winner.source === 'default' ? { source: 'default' } : { source: winner.source, location: winner.location });
73
+ }
74
+ return { values: Object.fromEntries(values), provenance: Object.fromEntries(provenance), candidates: Object.fromEntries(candidates) };
75
+ }
76
+ const describe = (c) => {
77
+ switch (c.source) {
78
+ case 'flag':
79
+ return `flag ${c.location}`;
80
+ case 'env':
81
+ return `env ${c.location}`;
82
+ case 'config':
83
+ return `config file ${c.location}`;
84
+ case 'package':
85
+ return `package.json field in ${c.location}`;
86
+ case 'default':
87
+ return 'default';
88
+ }
89
+ };
90
+ export function explain(name, resolution) {
91
+ const list = resolution.candidates[name];
92
+ if (list === undefined)
93
+ return `${name} is not an option of this command\n`;
94
+ const winner = list.find((c) => c.value !== undefined);
95
+ const head = winner === undefined ? `${name} is unset` : `${name} = ${JSON.stringify(winner.value)} from ${describe(winner)}`;
96
+ const rest = list
97
+ .filter((c) => c !== winner)
98
+ .map((c) => `${describe(c)} ${c.value === undefined ? '(unset)' : JSON.stringify(c.value)}`);
99
+ return `${head}\n${rest.length === 0 ? '' : ` candidates: ${rest.join(', ')}\n`}`;
100
+ }
@@ -0,0 +1,39 @@
1
+ import { type ExitCode } from './exit-code.js';
2
+ /** Anything that accepts text; `process.stdout` satisfies it, so does an array push. */
3
+ export interface Writer {
4
+ write(chunk: string): unknown;
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
+ }
16
+ /**
17
+ * The world, as the layers above the parser see it (design R1 of
18
+ * `cli-testing-harness`). Nothing above the parser reads `process.*` directly; it
19
+ * reads its `Runtime`, so a test can substitute every part of it.
20
+ */
21
+ export interface Runtime {
22
+ argv: string[];
23
+ env: Record<string, string | undefined>;
24
+ cwd: string;
25
+ stdin: NodeJS.ReadableStream;
26
+ stdout: Writer;
27
+ stderr: Writer;
28
+ isTTY: {
29
+ stdin: boolean;
30
+ stdout: boolean;
31
+ stderr: boolean;
32
+ };
33
+ /** Ends the run with an E1 code. In the real runtime this never returns. */
34
+ exit(code: ExitCode): never;
35
+ /** `performance.now` and `setTimeout` in the real runtime; a manual tick in the harness. */
36
+ clock: Clock;
37
+ }
38
+ /** The one place in the layer that touches `process`. Locked by `process-reference.lock.test.ts`. */
39
+ export declare const processRuntime: Runtime;
@@ -0,0 +1,24 @@
1
+ const ARGV_PROGRAM_AND_SCRIPT = 2;
2
+ export const processRuntime = {
3
+ argv: process.argv.slice(ARGV_PROGRAM_AND_SCRIPT),
4
+ env: process.env,
5
+ cwd: process.cwd(),
6
+ stdin: process.stdin,
7
+ stdout: process.stdout,
8
+ stderr: process.stderr,
9
+ isTTY: {
10
+ stdin: Boolean(process.stdin.isTTY),
11
+ stdout: Boolean(process.stdout.isTTY),
12
+ stderr: Boolean(process.stderr.isTTY),
13
+ },
14
+ exit(code) {
15
+ process.exit(code);
16
+ },
17
+ clock: {
18
+ now: () => performance.now(),
19
+ schedule(fn, ms) {
20
+ const handle = setTimeout(fn, ms);
21
+ return () => clearTimeout(handle);
22
+ },
23
+ },
24
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `--schema` — the program as data (F1). It is the one command an agent runs first, so it
3
+ * must succeed with no authentication, no config file and no network (N8): it reads the
4
+ * manifest and nothing else. Choices are carried as data (N9), the same data that becomes
5
+ * an MCP tool's input schema.
6
+ */
7
+ import type { ArgumentSpec, CommandNode, Effects, Example, Manifest, OptionSpec } from './manifest.js';
8
+ export interface JsonSchema {
9
+ type: 'object';
10
+ properties: Record<string, JsonSchemaProperty>;
11
+ required: string[];
12
+ additionalProperties: false;
13
+ }
14
+ export interface JsonSchemaProperty {
15
+ type: 'string' | 'boolean' | 'number' | 'integer' | 'array';
16
+ description?: string;
17
+ enum?: string[];
18
+ default?: string | boolean | number | readonly (string | number)[];
19
+ items?: {
20
+ type: 'string' | 'boolean' | 'number' | 'integer';
21
+ enum?: string[];
22
+ };
23
+ minimum?: number;
24
+ maximum?: number;
25
+ /** The command-line spelling of the option (S5). */
26
+ flag?: string;
27
+ /** The shared set this option was copied from (M4). */
28
+ sharedFrom?: string;
29
+ }
30
+ export interface CommandSchema {
31
+ /** The command as typed, without the program name: `config get`. */
32
+ name: string;
33
+ description?: string;
34
+ summary?: string;
35
+ effects?: Effects;
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;
43
+ arguments: ArgumentSpec[];
44
+ options: Record<string, OptionSpec>;
45
+ examples: Example[];
46
+ /** The arguments and options as one JSON Schema object — what an MCP tool call takes. */
47
+ inputSchema: JsonSchema;
48
+ }
49
+ export interface ProgramSchema {
50
+ schemaVersion: 1;
51
+ name: string;
52
+ version?: string;
53
+ description?: string;
54
+ commands: CommandSchema[];
55
+ }
56
+ export declare function inputSchemaOf(node: CommandNode): JsonSchema;
57
+ /** The typed name of a node: its path without the program's own name. */
58
+ export declare function typedName(node: CommandNode, root: string[]): string;
59
+ export declare function commandSchemaOf(node: CommandNode, root: string[]): CommandSchema;
60
+ /** Every runnable, visible command, in declaration order. Groups are structure, not commands. */
61
+ export declare function runnable(manifest: Manifest): CommandNode[];
62
+ export interface SchemaSummary {
63
+ schemaVersion: 1;
64
+ name: string;
65
+ version?: string;
66
+ description?: string;
67
+ /** The full schema exceeded the budget; this lists every command and how to get one in full. */
68
+ summarised: true;
69
+ budget: number;
70
+ commands: {
71
+ name: string;
72
+ summary?: string;
73
+ effects?: Effects;
74
+ }[];
75
+ hint: string;
76
+ }
77
+ /** Above the budget (N13): every command by name and summary, and the drilling command for one in full. */
78
+ export declare function summaryOf(manifest: Manifest, budget: number): SchemaSummary;
79
+ export declare function schemaOf(manifest: Manifest): ProgramSchema;
package/dist/schema.js ADDED
@@ -0,0 +1,116 @@
1
+ import { kebab } from './names.js';
2
+ function argumentProperty(a) {
3
+ const p = a.variadic === true ? { type: 'array', items: { type: 'string' } } : { type: 'string' };
4
+ if (a.description !== undefined)
5
+ p.description = a.description;
6
+ if (a.default !== undefined)
7
+ p.default = a.default;
8
+ return p;
9
+ }
10
+ function scalarType(spec) {
11
+ if (spec.type === 'number')
12
+ return spec.integer === true ? 'integer' : 'number';
13
+ return spec.type;
14
+ }
15
+ function optionProperty(name, spec) {
16
+ const scalar = scalarType(spec);
17
+ const p = spec.multiple === true ? { type: 'array', items: { type: scalar } } : { type: scalar };
18
+ p.flag = `--${kebab(name)}`;
19
+ if (spec.description !== undefined)
20
+ p.description = spec.description;
21
+ if (spec.choices !== undefined) {
22
+ if (p.items !== undefined)
23
+ p.items.enum = [...spec.choices];
24
+ else
25
+ p.enum = [...spec.choices];
26
+ }
27
+ if (spec.default !== undefined)
28
+ p.default = spec.default;
29
+ if (spec.minimum !== undefined)
30
+ p.minimum = spec.minimum;
31
+ if (spec.maximum !== undefined)
32
+ p.maximum = spec.maximum;
33
+ if (spec.sharedFrom !== undefined)
34
+ p.sharedFrom = spec.sharedFrom;
35
+ return p;
36
+ }
37
+ export function inputSchemaOf(node) {
38
+ const properties = new Map();
39
+ const required = [];
40
+ for (const a of node.arguments ?? []) {
41
+ properties.set(a.name, argumentProperty(a));
42
+ if (a.required !== false)
43
+ required.push(a.name);
44
+ }
45
+ for (const [name, spec] of Object.entries(node.options)) {
46
+ if (spec.hidden === true)
47
+ continue;
48
+ properties.set(name, optionProperty(name, spec));
49
+ if (spec.required === true && spec.default === undefined)
50
+ required.push(name);
51
+ }
52
+ return { type: 'object', properties: Object.fromEntries(properties), required, additionalProperties: false };
53
+ }
54
+ export function typedName(node, root) {
55
+ return node.path.slice(root.length).join(' ');
56
+ }
57
+ export function commandSchemaOf(node, root) {
58
+ const out = {
59
+ name: typedName(node, root),
60
+ arguments: node.arguments ?? [],
61
+ options: node.options,
62
+ examples: node.examples ?? [],
63
+ inputSchema: inputSchemaOf(node),
64
+ };
65
+ if (node.description !== undefined)
66
+ out.description = node.description;
67
+ if (node.summary !== undefined)
68
+ out.summary = node.summary;
69
+ if (node.effects !== undefined)
70
+ out.effects = node.effects;
71
+ if (node.deprecated !== undefined)
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;
79
+ return out;
80
+ }
81
+ export function runnable(manifest) {
82
+ return manifest.commands.filter((c) => c.run !== undefined && c.hidden !== true);
83
+ }
84
+ export function summaryOf(manifest, budget) {
85
+ const root = manifest.rootPath;
86
+ const program = manifest.find(root);
87
+ const out = {
88
+ schemaVersion: 1,
89
+ name: root.join(' '),
90
+ summarised: true,
91
+ budget,
92
+ commands: runnable(manifest).map((c) => ({
93
+ name: typedName(c, root),
94
+ ...(c.summary ?? c.description === undefined ? {} : { summary: c.summary ?? c.description ?? '' }),
95
+ ...(c.effects === undefined ? {} : { effects: c.effects }),
96
+ })),
97
+ hint: `run \`${root.join(' ')} <command> --schema\` for one command in full`,
98
+ };
99
+ if (manifest.version !== undefined)
100
+ out.version = manifest.version;
101
+ const description = program?.description;
102
+ if (description !== undefined)
103
+ out.description = description;
104
+ return out;
105
+ }
106
+ export function schemaOf(manifest) {
107
+ const root = manifest.rootPath;
108
+ const program = manifest.find(root);
109
+ const out = { schemaVersion: 1, name: root.join(' '), commands: runnable(manifest).map((c) => commandSchemaOf(c, root)) };
110
+ if (manifest.version !== undefined)
111
+ out.version = manifest.version;
112
+ const description = program?.description;
113
+ if (description !== undefined)
114
+ out.description = description;
115
+ return out;
116
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Test-side helpers shared by `commander-harness` and `yargs-harness` (design R2,
3
+ * R5, R7 of `cli-testing-harness`). This is the second and last file in the layer
4
+ * that may touch `process`, because swapping `process.env` for the duration of a run
5
+ * is the one thing a harness cannot avoid: commander reads `Option#env()` from
6
+ * `process.env` at parse time (commander #2549), and yargs' `.env()` likewise.
7
+ */
8
+ import { Readable } from 'node:stream';
9
+ import { ExitCode } from './exit-code.js';
10
+ import { type Manifest } from './manifest.js';
11
+ import { type Clock, type Runtime } from './runtime.js';
12
+ export interface RunOptions {
13
+ argv: string[];
14
+ env?: Record<string, string>;
15
+ stdin?: string | Readable;
16
+ cwd?: string;
17
+ /** `true` = every stream is a TTY; an object sets each; default: none is. */
18
+ tty?: boolean | Partial<Runtime['isTTY']>;
19
+ /** The clock the runtime reports; a fresh `fakeClock()` at 0 when not given. */
20
+ clock?: FakeClock;
21
+ }
22
+ export interface RunResult {
23
+ code: ExitCode;
24
+ stdout: string;
25
+ stderr: string;
26
+ /** Parsed stdout when `--json` was in argv and stdout parsed; see `finish`. */
27
+ json?: unknown;
28
+ durationMs: number;
29
+ }
30
+ /** Thrown by `fakeRuntime.exit` so a handler that exits unwinds to the harness. */
31
+ export declare class RuntimeExit extends Error {
32
+ readonly code: ExitCode;
33
+ constructor(code: ExitCode);
34
+ }
35
+ export interface FakeRuntime extends Runtime {
36
+ out: string[];
37
+ err: string[];
38
+ clock: FakeClock;
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;
54
+ /** A `Runtime` whose every part is under the test's control. */
55
+ export declare function fakeRuntime(opts: RunOptions): FakeRuntime;
56
+ export declare function swapEnv(env: Record<string, string> | undefined): () => void;
57
+ /**
58
+ * Route `console.*` into the fake runtime while a run lasts. Hosts print through
59
+ * `console` in a few paths a public seam does not cover (yargs' help printer, a
60
+ * handler that never met the output layer); a harness that let those reach the real
61
+ * terminal would be measuring the wrong thing.
62
+ */
63
+ export declare function captureConsole(rt: FakeRuntime): () => void;
64
+ /** Strip ANSI escape sequences — the decision from the intent: `stdout` stays raw. */
65
+ export declare function stripAnsi(text: string): string;
66
+ /**
67
+ * Assemble the result (R2). `json` is set only when `--json` was in argv and stdout
68
+ * parsed; a parse failure turns the run into `RUNTIME` with the parse error on
69
+ * stderr, so a broken envelope cannot pass a test by accident.
70
+ */
71
+ export declare function finish(rt: FakeRuntime, code: ExitCode, startedAt: number): RunResult;
72
+ /** The E1 code an unwound error carries, or `RUNTIME` when it carries none. */
73
+ export declare function codeOf(e: unknown): ExitCode;
74
+ /**
75
+ * Run a burgee program in-process — the T1 harness for burgee itself. Env is injected,
76
+ * never swapped: the engine reads env-bound options from the runtime it is given, so
77
+ * `process.env` is untouched by construction. Exit unwinds through `RuntimeExit`.
78
+ */
79
+ export declare function runBurgee(program: Manifest, opts: RunOptions): Promise<RunResult>;
@@ -0,0 +1,145 @@
1
+ import { Readable } from 'node:stream';
2
+ import { beforeTerminator, execute } from './execute.js';
3
+ import { ExitCode, isExitCode } from './exit-code.js';
4
+ export class RuntimeExit extends Error {
5
+ code;
6
+ constructor(code) {
7
+ super(`runtime.exit(${code})`);
8
+ this.code = code;
9
+ this.name = 'RuntimeExit';
10
+ }
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
+ }
46
+ function ttyOf(tty) {
47
+ if (tty === true)
48
+ return { stdin: true, stdout: true, stderr: true };
49
+ if (tty === false || tty === undefined)
50
+ return { stdin: false, stdout: false, stderr: false };
51
+ return { stdin: false, stdout: false, stderr: false, ...tty };
52
+ }
53
+ export function fakeRuntime(opts) {
54
+ const out = [];
55
+ const err = [];
56
+ const stdin = typeof opts.stdin === 'string' ? Readable.from([opts.stdin]) : (opts.stdin ?? Readable.from([]));
57
+ return {
58
+ argv: opts.argv,
59
+ env: { ...(opts.env ?? {}) },
60
+ cwd: opts.cwd ?? '/',
61
+ stdin,
62
+ stdout: { write: (s) => out.push(s) },
63
+ stderr: { write: (s) => err.push(s) },
64
+ isTTY: ttyOf(opts.tty),
65
+ exit(code) {
66
+ throw new RuntimeExit(code);
67
+ },
68
+ clock: opts.clock ?? fakeClock(),
69
+ out,
70
+ err,
71
+ };
72
+ }
73
+ const noop = () => undefined;
74
+ export function swapEnv(env) {
75
+ if (!env)
76
+ return noop;
77
+ const saved = { ...process.env };
78
+ for (const key of Object.keys(process.env))
79
+ delete process.env[key];
80
+ Object.assign(process.env, env);
81
+ return () => {
82
+ for (const key of Object.keys(process.env))
83
+ delete process.env[key];
84
+ Object.assign(process.env, saved);
85
+ };
86
+ }
87
+ const CONSOLE_METHODS = ['log', 'info', 'debug', 'warn', 'error'];
88
+ const into = (sink) => (...args) => {
89
+ sink.push(`${args.map(String).join(' ')}\n`);
90
+ };
91
+ export function captureConsole(rt) {
92
+ const saved = Object.fromEntries(CONSOLE_METHODS.map((m) => [m, console[m]]));
93
+ console.log = into(rt.out);
94
+ console.info = into(rt.out);
95
+ console.debug = into(rt.out);
96
+ console.warn = into(rt.err);
97
+ console.error = into(rt.err);
98
+ return () => {
99
+ for (const m of CONSOLE_METHODS)
100
+ console[m] = saved[m];
101
+ };
102
+ }
103
+ export function stripAnsi(text) {
104
+ return text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, '');
105
+ }
106
+ export function finish(rt, code, startedAt) {
107
+ const stdout = rt.out.join('');
108
+ const stderr = rt.err.join('');
109
+ const result = { code, stdout, stderr, durationMs: performance.now() - startedAt };
110
+ if (beforeTerminator(rt.argv).includes('--json')) {
111
+ const source = stdout.trim() === '' && code !== ExitCode.OK ? stderr.split('\n')[0] ?? '' : stdout;
112
+ try {
113
+ result.json = JSON.parse(source);
114
+ }
115
+ catch (e) {
116
+ return { ...result, code: ExitCode.RUNTIME, stderr: `${stderr}--json output did not parse: ${e.message}\n` };
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+ export function codeOf(e) {
122
+ if (e instanceof RuntimeExit)
123
+ return e.code;
124
+ const exitCode = e?.exitCode;
125
+ return isExitCode(exitCode) ? exitCode : ExitCode.RUNTIME;
126
+ }
127
+ export async function runBurgee(program, opts) {
128
+ const startedAt = performance.now();
129
+ const rt = fakeRuntime(opts);
130
+ let code = ExitCode.OK;
131
+ try {
132
+ await execute(program, {
133
+ argv: rt.argv,
134
+ env: rt.env,
135
+ stdout: rt.stdout,
136
+ stderr: rt.stderr,
137
+ exit: (c) => rt.exit(c),
138
+ root: program.rootPath,
139
+ });
140
+ }
141
+ catch (e) {
142
+ code = codeOf(e);
143
+ }
144
+ return finish(rt, code, startedAt);
145
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `burgee/testing` — run a burgee CLI in-process, with argv, env, stdin, cwd and
3
+ * TTY-ness injected, and get back `{ code, stdout, stderr, json }`. Requirement T1.
4
+ *
5
+ * A separate entry point so it is paid for per import (K6): a user's shipped CLI
6
+ * imports `burgee` and never pulls a byte of this.
7
+ */
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
+ export { ExitCode, isExitCode, type ExitCode as ExitCodeValue } from './exit-code.js';