burgee 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import parser, { decamelize } from '../yargs-parser.js';
2
+ export function aliasMap(flags) {
3
+ const out = {};
4
+ for (const [name, spec] of Object.entries(flags)) {
5
+ const names = [];
6
+ if (typeof spec.shortFlag === 'string')
7
+ names.push(spec.shortFlag);
8
+ if (typeof spec.alias === 'string')
9
+ names.push(spec.alias);
10
+ for (const extra of spec.aliases ?? [])
11
+ names.push(extra);
12
+ const decamelized = decamelize(name, '-');
13
+ if (decamelized !== name)
14
+ names.push(decamelized);
15
+ if (names.length > 0)
16
+ Object.defineProperty(out, name, { value: names, writable: true, enumerable: true, configurable: true });
17
+ }
18
+ return out;
19
+ }
20
+ export function typeofDefault(value) {
21
+ if (typeof value === 'boolean')
22
+ return 'boolean';
23
+ if (typeof value === 'number')
24
+ return 'number';
25
+ if (typeof value === 'string')
26
+ return 'string';
27
+ if (Array.isArray(value))
28
+ return typeofDefault(value[0]);
29
+ return undefined;
30
+ }
31
+ export function splitAtCommand(argv, parserOptions, opts) {
32
+ const probe = parser.detailed(argv, parserOptions);
33
+ const first = probe.argv['_'][0];
34
+ if (first === undefined)
35
+ return { parent: argv, input: [] };
36
+ const word = String(first);
37
+ const at = argv.indexOf(word);
38
+ const parent = at === -1 ? argv : argv.slice(0, at);
39
+ const input = at === -1 ? [] : argv.slice(at + 1);
40
+ if (!(opts.commands ?? []).includes(word))
41
+ return { parent, input, unknownCommand: word };
42
+ return { parent, input, command: word };
43
+ }
@@ -0,0 +1,35 @@
1
+ import { type Options } from './types.js';
2
+ /** The nearest `package.json` above the caller's module, which is what `importMeta` is for. */
3
+ /** The nearest `package.json` above the caller's module, which is what `importMeta` is for. */
4
+ export declare function readPackageUp(importMeta: ImportMeta | undefined): Record<string, unknown>;
5
+ /**
6
+ * meow's help block.
7
+ *
8
+ * Indented by `helpIndent` (2 by default) — but only when there is more than one line to
9
+ * indent, which is why `{description: false, help: 'single line'}` comes back flush. The
10
+ * suite states both shapes and they disagree about the indent, not about the text.
11
+ */
12
+ /**
13
+ * meow's help block.
14
+ *
15
+ * Indented by `helpIndent` (2 by default) — but only when there is more than one line to
16
+ * indent, which is why `{description: false, help: 'single line'}` comes back flush. The
17
+ * suite states both shapes and they disagree about the indent, not about the text.
18
+ */
19
+ export declare function buildHelp(options: Options, pkg: Record<string, unknown>): string;
20
+ /** `common-tags`' `stripIndent`, for the template literals meow's callers write help in. */
21
+ /** `common-tags`' `stripIndent`, for the template literals meow's callers write help in. */
22
+ export declare function stripIndent(text: string): string;
23
+ /** Every flag name a caller may write, and the canonical name each maps to. */
24
+ /**
25
+ * The half of `normalize-package-data` meow's callers can see.
26
+ *
27
+ * `bin` as a string becomes `{ [name]: path }` — the suite reads `cli.pkg.bin['browser-sync']`
28
+ * — and an absent `version` becomes `''`. A copy, because `pkg normalization is lazy` asserts
29
+ * the object the caller passed in is untouched.
30
+ */
31
+ export declare function normalizePackage(pkg: Record<string, unknown>): Record<string, unknown>;
32
+ /** meow renames the process after the binary it is, which `ps` and a crash report both read. */
33
+ /** meow renames the process after the binary it is, which `ps` and a crash report both read. */
34
+ export declare function setProcessTitle(pkg: Record<string, unknown>): void;
35
+ /** An own property under a key the caller chose — never the prototype setter. */
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { host } from '../runtime.js';
5
+ const indent = (text, spaces) => text.replace(/^(?!\s*$)/gmu, ' '.repeat(spaces));
6
+ export function readPackageUp(importMeta) {
7
+ if (importMeta?.url === undefined)
8
+ return {};
9
+ let dir = dirname(fileURLToPath(importMeta.url));
10
+ for (let depth = 0; depth < 64; depth += 1) {
11
+ try {
12
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
13
+ }
14
+ catch {
15
+ const up = dirname(dir);
16
+ if (up === dir)
17
+ break;
18
+ dir = up;
19
+ }
20
+ }
21
+ return {};
22
+ }
23
+ export function buildHelp(options, pkg) {
24
+ const description = options.description === false ? '' : (options.description ?? pkg['description'] ?? '');
25
+ const body = options.help === false ? '' : stripIndent(options.help ?? '');
26
+ const parts = [description, body].filter((p) => p !== '');
27
+ if (parts.length === 0)
28
+ return '';
29
+ const text = parts.join('\n\n');
30
+ const width = options.helpIndent ?? 2;
31
+ return `\n${text.includes('\n') ? indent(text, width) : text}\n`;
32
+ }
33
+ export function stripIndent(text) {
34
+ const lines = text.split('\n');
35
+ const widths = lines.filter((l) => l.trim() !== '').map((l) => (/^(\s*)/u.exec(l)?.[1] ?? '').length);
36
+ const smallest = widths.length === 0 ? 0 : Math.min(...widths);
37
+ return lines
38
+ .map((l) => l.slice(smallest))
39
+ .join('\n')
40
+ .trim();
41
+ }
42
+ export function normalizePackage(pkg) {
43
+ const out = { ...pkg };
44
+ const name = typeof out['name'] === 'string' ? out['name'].replace(/^@[^/]+\//u, '') : undefined;
45
+ if (typeof out['bin'] === 'string' && name !== undefined)
46
+ out['bin'] = Object.fromEntries([[name, out['bin']]]);
47
+ if (out['version'] === undefined)
48
+ out['version'] = '';
49
+ return out;
50
+ }
51
+ export function setProcessTitle(pkg) {
52
+ const bin = pkg['bin'];
53
+ const first = typeof bin === 'object' && bin !== null ? Object.keys(bin)[0] : undefined;
54
+ const name = typeof pkg['name'] === 'string' ? pkg['name'].replace(/^@[^/]+\//u, '') : undefined;
55
+ const title = first ?? name;
56
+ if (title !== undefined && title !== '')
57
+ host.setTitle(title);
58
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `burgee/meow` — the option and result shapes meow's callers write against.
3
+ *
4
+ * Kept beside the implementation rather than inside it because three files read them and a
5
+ * façade's contract is the part most worth seeing on its own.
6
+ */
7
+ export interface AnyFlag {
8
+ type?: 'string' | 'boolean' | 'number';
9
+ alias?: string;
10
+ aliases?: string[];
11
+ shortFlag?: string;
12
+ default?: unknown;
13
+ isRequired?: boolean | ((flags: Record<string, unknown>, input: string[]) => boolean);
14
+ isMultiple?: boolean;
15
+ choices?: unknown[];
16
+ }
17
+ export interface Options {
18
+ importMeta?: ImportMeta;
19
+ argv?: readonly string[];
20
+ description?: string | false;
21
+ help?: string | false;
22
+ version?: string | false;
23
+ autoHelp?: boolean;
24
+ autoVersion?: boolean;
25
+ helpIndent?: number;
26
+ flags?: Record<string, AnyFlag>;
27
+ pkg?: Record<string, unknown>;
28
+ input?: unknown;
29
+ inferType?: boolean;
30
+ booleanDefault?: boolean | null | undefined;
31
+ hardRejection?: boolean;
32
+ allowUnknownFlags?: boolean;
33
+ commands?: string[];
34
+ }
35
+ export interface Result {
36
+ input: string[];
37
+ flags: Record<string, unknown>;
38
+ unnormalizedFlags: Record<string, unknown>;
39
+ pkg: Record<string, unknown>;
40
+ help: string;
41
+ version: string;
42
+ command?: string;
43
+ showHelp: (code?: number) => never;
44
+ showVersion: () => void;
45
+ }
46
+ /** An own property under a key the caller chose — never the prototype setter. */
47
+ export declare function own(target: Record<string, unknown>, key: string, value: unknown): void;
@@ -0,0 +1,3 @@
1
+ export function own(target, key, value) {
2
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
3
+ }
@@ -0,0 +1,35 @@
1
+ import { type AnyFlag, type Options } from './types.js';
2
+ export declare function validateFlags(flags: Record<string, AnyFlag>): void;
3
+ /** meow refuses to guess where the caller's `package.json` is. */
4
+ /** meow refuses to guess where the caller's `package.json` is. */
5
+ export declare function requireImportMeta(importMeta: ImportMeta | undefined): void;
6
+ /** meow, over burgee's parser. */
7
+ /** `commands` is an array of bare words, and meow says so in three different sentences. */
8
+ export declare function validateCommands(commands: string[] | undefined): void;
9
+ /**
10
+ * Where the parent's arguments end and the command's begin.
11
+ *
12
+ * The first token the parser reads as positional is the command word; everything after it in
13
+ * the *raw* argv is the child's, unparsed. `--` is not a fence here — the suite states that
14
+ * `-- --unknown run` reports `Unknown command: --unknown`, so a post-separator word is a
15
+ * candidate command like any other.
16
+ */
17
+ export declare function checkChoices(specs: Record<string, AnyFlag>, flags: Record<string, unknown>): void;
18
+ export declare function checkRequired(specs: Record<string, AnyFlag>, flags: Record<string, unknown>, input: string[]): void;
19
+ export declare function checkUnknown(specs: Record<string, AnyFlag>, parsedFlags: Record<string, unknown>, argv: string[], opts: Options): void;
20
+ /** `commands` is an array of bare words, and meow says so in three different sentences. */
21
+ /** meow's `input` option may demand positionals, as a boolean or as a predicate. */
22
+ export declare function checkInput(opts: Options, input: string[], flags: Record<string, unknown>): void;
23
+ /** A flag declared once may be given once — the parser collects repeats into an array. */
24
+ /** A flag declared once may be given once — the parser collects repeats into an array. */
25
+ export declare function checkSetOnce(specs: Record<string, AnyFlag>, parsed: Record<string, unknown>): void;
26
+ /**
27
+ * The half of `normalize-package-data` meow's callers can see.
28
+ *
29
+ * `bin` as a string becomes `{ [name]: path }` — the suite reads `cli.pkg.bin['browser-sync']`
30
+ * — and an absent `version` becomes `''`. A copy, because `pkg normalization is lazy` asserts
31
+ * the object the caller passed in is untouched.
32
+ */
33
+ /** stderr and exit 2 — how meow ends a run the flags do not support. */
34
+ export declare function reportAndExit(message: string): never;
35
+ /** meow's `input` option may demand positionals, as a boolean or as a predicate. */
@@ -0,0 +1,144 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { ExitCode } from '../exit-code.js';
3
+ import { host } from '../runtime.js';
4
+ import { camelCase, decamelize } from '../yargs-parser.js';
5
+ export function validateFlags(flags) {
6
+ const errors = [];
7
+ const kebab = Object.keys(flags).filter((name) => name.includes('-') && name !== '--');
8
+ if (kebab.length > 0)
9
+ errors.push(`Flag keys may not contain '-'. Invalid flags: ${kebab.map((k) => `\`${k}\``).join(', ')}`);
10
+ const renamed = Object.entries(flags).filter(([, spec]) => typeof spec.alias === 'string');
11
+ if (renamed.length > 0) {
12
+ errors.push(`The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: ${renamed.map(([n]) => `\`--${n}\``).join(', ')}`);
13
+ }
14
+ const badChoices = Object.entries(flags).filter(([, spec]) => spec.choices !== undefined && !Array.isArray(spec.choices));
15
+ if (badChoices.length > 0) {
16
+ errors.push(`The option \`choices\` must be an array. Invalid flags: ${badChoices.map(([n]) => `\`--${n}\``).join(', ')}`);
17
+ }
18
+ for (const [name, spec] of Object.entries(flags)) {
19
+ if (spec.default === undefined || spec.type === undefined)
20
+ continue;
21
+ const values = Array.isArray(spec.default) ? spec.default : [spec.default];
22
+ const wrong = values.find((v) => typeof v !== spec.type);
23
+ if (wrong !== undefined)
24
+ errors.push(`Expected "${name}" default value to be of type "${String(spec.type)}", got "${typeof wrong}"`);
25
+ }
26
+ if (errors.length > 0)
27
+ throw new Error(`${errors.join('\n')}`);
28
+ }
29
+ export function requireImportMeta(importMeta) {
30
+ if (importMeta === undefined || typeof importMeta !== 'object' || typeof importMeta.url !== 'string') {
31
+ throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.');
32
+ }
33
+ try {
34
+ fileURLToPath(importMeta.url);
35
+ }
36
+ catch {
37
+ throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.');
38
+ }
39
+ }
40
+ export function validateCommands(commands) {
41
+ if (commands === undefined)
42
+ return;
43
+ if (!Array.isArray(commands))
44
+ throw new TypeError('The `commands` option must be an array of strings.');
45
+ if (commands.length === 0)
46
+ throw new TypeError('The `commands` option must contain at least one command.');
47
+ const bad = commands.some((c) => typeof c !== 'string' || c === '' || /\s/u.test(c) || c.startsWith('-'));
48
+ if (bad)
49
+ throw new TypeError('The `commands` option must be an array of non-empty strings without whitespace that do not start with `-`.');
50
+ }
51
+ export function checkChoices(specs, flags) {
52
+ const errors = [];
53
+ const badDefaults = Object.entries(specs).filter(([, spec]) => {
54
+ if (spec.default === undefined || !Array.isArray(spec.choices))
55
+ return false;
56
+ return (Array.isArray(spec.default) ? spec.default : [spec.default]).some((d) => !spec.choices?.includes(d));
57
+ });
58
+ if (badDefaults.length > 0) {
59
+ throw new Error(`Each value of the option \`default\` must exist within the option \`choices\`. Invalid flags: ${badDefaults.map(([n]) => `\`--${n}\``).join(', ')}`);
60
+ }
61
+ for (const [name, spec] of Object.entries(specs)) {
62
+ if (spec.choices === undefined || !Array.isArray(spec.choices))
63
+ continue;
64
+ const label = `--${decamelize(name, '-')}`;
65
+ const allowed = `[${spec.choices.map((c) => `\`${String(c)}\``).join(', ')}]`;
66
+ const value = flags[name];
67
+ const required = typeof spec.isRequired === 'function' ? true : spec.isRequired === true;
68
+ if (value === undefined || value === '') {
69
+ if (required)
70
+ errors.push(`Flag \`${label}\` has no value. Value must be one of: ${allowed}`);
71
+ continue;
72
+ }
73
+ const values = Array.isArray(value) ? value : [value];
74
+ const bad = values.filter((v) => !spec.choices?.includes(v));
75
+ if (bad.length > 0) {
76
+ errors.push(`Unknown value${bad.length > 1 ? 's' : ''} for flag \`${label}\`: ${bad.map((b) => `\`${String(b)}\``).join(', ')}. Value must be one of: ${allowed}`);
77
+ }
78
+ }
79
+ if (errors.length > 0)
80
+ throw new Error(`${errors.join('\n')}`);
81
+ }
82
+ export function checkRequired(specs, flags, input) {
83
+ const missing = [];
84
+ for (const [name, spec] of Object.entries(specs)) {
85
+ let required = spec.isRequired;
86
+ if (typeof spec.isRequired === 'function') {
87
+ required = spec.isRequired(flags, input);
88
+ if (typeof required !== 'boolean') {
89
+ throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof required} was returned.`);
90
+ }
91
+ }
92
+ if (required !== true)
93
+ continue;
94
+ const value = flags[name];
95
+ if (value !== undefined && value !== '' && !(Array.isArray(value) && value.length === 0))
96
+ continue;
97
+ const short = typeof spec.shortFlag === 'string' ? `, -${spec.shortFlag}` : '';
98
+ missing.push(`--${decamelize(name, '-')}${short}`);
99
+ }
100
+ if (missing.length > 0) {
101
+ reportAndExit(`Missing required flag${missing.length > 1 ? 's' : ''}\n${missing.map((m) => `\t${m}`).join('\n')}`);
102
+ }
103
+ }
104
+ export function checkUnknown(specs, parsedFlags, argv, opts) {
105
+ const known = new Set(['--']);
106
+ if (opts.autoHelp !== false)
107
+ known.add('help');
108
+ if (opts.autoVersion !== false)
109
+ known.add('version');
110
+ for (const [name, spec] of Object.entries(specs)) {
111
+ known.add(name);
112
+ known.add(decamelize(name, '-'));
113
+ if (typeof spec.shortFlag === 'string')
114
+ known.add(spec.shortFlag);
115
+ if (typeof spec.alias === 'string')
116
+ known.add(spec.alias);
117
+ }
118
+ const unknown = Object.keys(parsedFlags).filter((k) => !known.has(k) && !known.has(camelCase(k)) && !known.has(decamelize(k, '-')));
119
+ if (unknown.length > 0) {
120
+ const names = unknown.map((u) => (u.length === 1 ? `-${u}` : `--${decamelize(u, '-')}`));
121
+ reportAndExit(`Unknown flag${names.length > 1 ? 's' : ''}\n${names.join('\n')}`);
122
+ }
123
+ void argv;
124
+ }
125
+ export function checkInput(opts, input, flags) {
126
+ if (typeof opts.input !== 'object' || opts.input === null)
127
+ return;
128
+ const spec = opts.input;
129
+ const required = typeof spec.isRequired === 'function' ? spec.isRequired(flags, input) : spec.isRequired === true;
130
+ if (required && input.length === 0)
131
+ reportAndExit('Missing required input');
132
+ }
133
+ export function checkSetOnce(specs, parsed) {
134
+ for (const [name, spec] of Object.entries(specs)) {
135
+ if (spec.isMultiple === true || name === '--')
136
+ continue;
137
+ if (Array.isArray(parsed[name]))
138
+ throw new Error(`The flag --${decamelize(name, '-')} can only be set once.`);
139
+ }
140
+ }
141
+ export function reportAndExit(message) {
142
+ host.stderr.write(`${message}\n`);
143
+ return host.exit(ExitCode.USAGE);
144
+ }
package/dist/meow.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type Options, type Result } from './meow/types.js';
2
+ export { type AnyFlag, type Options, type Result } from './meow/types.js';
3
+ /** meow, over burgee's parser. */
4
+ declare function meow(helpText: string | Options, options?: Options): Result;
5
+ declare const meowFacade: typeof meow;
6
+ export default meowFacade;
package/dist/meow.js ADDED
@@ -0,0 +1,146 @@
1
+ import { ExitCode } from './exit-code.js';
2
+ import { aliasMap, splitAtCommand, typeofDefault } from './meow/parse.js';
3
+ import { buildHelp, normalizePackage, readPackageUp, setProcessTitle } from './meow/present.js';
4
+ import { own } from './meow/types.js';
5
+ import { checkChoices, checkInput, checkRequired, checkSetOnce, checkUnknown, requireImportMeta, validateCommands, validateFlags } from './meow/validate.js';
6
+ import { host } from './runtime.js';
7
+ import parser, { camelCase } from './yargs-parser.js';
8
+ function meow(helpText, options = {}) {
9
+ const opts = typeof helpText === 'string' ? { help: helpText, ...options } : helpText;
10
+ if (typeof helpText !== 'string' && Object.keys(options).length > 0)
11
+ Object.assign(opts, options);
12
+ if (opts.input !== undefined && typeof opts.input !== 'string' && !Array.isArray(opts.input) && typeof opts.input !== 'object') {
13
+ throw new TypeError('The `input` option must be a string or an object.');
14
+ }
15
+ const flagSpecs = (opts.flags ?? {});
16
+ if (typeof flagSpecs !== 'object' || flagSpecs === null || Array.isArray(flagSpecs)) {
17
+ throw new TypeError('The `flags` option must be an object.');
18
+ }
19
+ if (typeof opts.input === 'object' && opts.input !== null) {
20
+ const required = opts.input.isRequired;
21
+ if (required !== undefined && typeof required !== 'boolean' && typeof required !== 'function') {
22
+ throw new TypeError('The `input.isRequired` option must be a boolean or a function.');
23
+ }
24
+ }
25
+ validateFlags(flagSpecs);
26
+ if (opts.pkg === undefined)
27
+ requireImportMeta(opts.importMeta);
28
+ const pkg = normalizePackage(opts.pkg ?? readPackageUp(opts.importMeta));
29
+ validateCommands(opts.commands);
30
+ const rawArgv = [...(opts.argv ?? host.argv.slice(2))];
31
+ const booleans = [];
32
+ const strings = [];
33
+ const numbers = [];
34
+ const defaults = {};
35
+ const arrays = [];
36
+ for (const [name, spec] of Object.entries(flagSpecs)) {
37
+ if (name === '--')
38
+ continue;
39
+ const type = spec.type ?? (spec.default === undefined ? undefined : typeofDefault(spec.default));
40
+ if (type === 'boolean')
41
+ booleans.push(name);
42
+ else if (type === 'number')
43
+ numbers.push(name);
44
+ else if (type === 'string')
45
+ strings.push(name);
46
+ if (spec.isMultiple === true)
47
+ arrays.push(name);
48
+ if (spec.default !== undefined)
49
+ own(defaults, name, spec.default);
50
+ else if (type === 'boolean' && 'booleanDefault' in opts) {
51
+ if (opts.booleanDefault !== undefined)
52
+ own(defaults, name, opts.booleanDefault);
53
+ }
54
+ else if (type === 'boolean')
55
+ own(defaults, name, false);
56
+ if (spec.isMultiple === true && spec.default === undefined) {
57
+ own(defaults, name, type === 'boolean' && !('booleanDefault' in opts) ? [false] : []);
58
+ }
59
+ }
60
+ const parserOptions = {
61
+ alias: aliasMap(flagSpecs),
62
+ array: arrays,
63
+ boolean: booleans,
64
+ string: strings,
65
+ number: numbers,
66
+ default: defaults,
67
+ configuration: {
68
+ 'camel-case-expansion': true,
69
+ 'greedy-arrays': false,
70
+ 'parse-numbers': opts.inferType === true,
71
+ 'parse-positional-numbers': opts.inferType === true,
72
+ 'populate--': flagSpecs['--'] !== undefined,
73
+ 'strip-aliased': false,
74
+ 'strip-dashed': true,
75
+ 'unknown-options-as-args': false,
76
+ },
77
+ };
78
+ const split = opts.commands === undefined ? undefined : splitAtCommand(rawArgv, parserOptions, opts);
79
+ const argv = split?.parent ?? rawArgv;
80
+ const parsed = parser.detailed(argv, parserOptions);
81
+ const { _: positional, ...rest } = parsed.argv;
82
+ const inputType = typeof opts.input === 'object' && opts.input !== null ? opts.input.type : opts.input;
83
+ const coerce = (v) => {
84
+ if (inputType === 'number')
85
+ return Number(v);
86
+ if (inputType === 'string')
87
+ return String(v);
88
+ return opts.inferType === true ? v : String(v);
89
+ };
90
+ const input = (split === undefined ? positional.map(coerce) : split.input);
91
+ const command = split?.command;
92
+ const dropped = new Set();
93
+ for (const [name, spec] of Object.entries(flagSpecs)) {
94
+ if (typeof spec.shortFlag === 'string')
95
+ dropped.add(spec.shortFlag);
96
+ if (typeof spec.alias === 'string')
97
+ dropped.add(spec.alias);
98
+ for (const extra of spec.aliases ?? [])
99
+ dropped.add(extra);
100
+ void name;
101
+ }
102
+ const flags = {};
103
+ const unnormalizedFlags = { ...rest };
104
+ for (const [key, value] of Object.entries(rest)) {
105
+ if (key === '--' || dropped.has(key))
106
+ continue;
107
+ Object.defineProperty(flags, camelCase(key), { value, writable: true, enumerable: true, configurable: true });
108
+ }
109
+ if (flagSpecs['--'] !== undefined)
110
+ flags['--'] = rest['--'] ?? [];
111
+ const help = buildHelp(opts, pkg);
112
+ const declared = opts.version === undefined ? undefined : String(opts.version);
113
+ const fromPkg = typeof pkg['version'] === 'string' && pkg['version'] !== '' ? pkg['version'] : undefined;
114
+ const version = declared ?? fromPkg ?? 'No version found';
115
+ const showHelp = (code = ExitCode.USAGE) => {
116
+ host.stdout.write(help === '' ? '\n' : `${help}\n`);
117
+ return host.exit(code);
118
+ };
119
+ const showVersion = () => {
120
+ host.stdout.write(`${version}\n`);
121
+ host.exit(ExitCode.OK);
122
+ };
123
+ setProcessTitle(pkg);
124
+ const result = { input, flags, unnormalizedFlags, pkg, help, version, showHelp, showVersion };
125
+ if (opts.commands !== undefined && command !== undefined)
126
+ result.command = command;
127
+ if (split?.unknownCommand !== undefined) {
128
+ host.stderr.write(`Unknown command: ${split.unknownCommand}\nAvailable commands: ${opts.commands?.join(', ') ?? ''}\n${help}\n`);
129
+ host.exit(ExitCode.USAGE);
130
+ }
131
+ if (input.length === 0) {
132
+ if (opts.autoHelp !== false && flagSpecs['help'] === undefined && flags['help'] === true)
133
+ showHelp(ExitCode.OK);
134
+ if (opts.autoVersion !== false && flagSpecs['version'] === undefined && flags['version'] === true)
135
+ showVersion();
136
+ }
137
+ checkSetOnce(flagSpecs, rest);
138
+ checkInput(opts, input, flags);
139
+ checkChoices(flagSpecs, flags);
140
+ checkRequired(flagSpecs, flags, input);
141
+ if (opts.allowUnknownFlags === false)
142
+ checkUnknown(flagSpecs, rest, argv, opts);
143
+ return result;
144
+ }
145
+ const meowFacade = meow;
146
+ export default meowFacade;
package/dist/plugin.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface Plugin {
19
19
  };
20
20
  enforce?: 'pre' | 'post';
21
21
  }
22
- export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT';
22
+ export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_CONTRIBUTION';
23
23
  /** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
24
24
  export declare class PluginError extends Error {
25
25
  readonly code: PluginErrorCode;
package/dist/plugin.js CHANGED
@@ -68,7 +68,7 @@ function checkCommands(commands, name, taken) {
68
68
  }
69
69
  seen.add(path);
70
70
  try {
71
- checkCommand(path, (node['options'] ?? {}), node['effects'], node['run'] !== undefined || node['load'] !== undefined);
71
+ checkCommand(path, node);
72
72
  }
73
73
  catch (error) {
74
74
  throw schema(`${at}: ${error.message}`, 'a plugin command is declared exactly as a first-party one');
package/dist/runtime.d.ts CHANGED
@@ -66,6 +66,8 @@ export declare const host: {
66
66
  readonly stdin: NodeJS.ReadStream;
67
67
  readonly stdout: NodeJS.WriteStream;
68
68
  readonly stderr: NodeJS.WriteStream;
69
+ /** What `ps` shows. meow renames the process after the binary it is; nothing else sets it. */
70
+ setTitle(value: string): void;
69
71
  /**
70
72
  * The terminal's width, or undefined when there is no terminal to ask. Guarded on
71
73
  * `process` itself because cliui's upstream is guarded there: `getWindowWidth` is reached
package/dist/runtime.js CHANGED
@@ -18,6 +18,9 @@ export const host = {
18
18
  get stderr() {
19
19
  return process.stderr;
20
20
  },
21
+ setTitle(value) {
22
+ process.title = value;
23
+ },
21
24
  get columns() {
22
25
  if (typeof process === 'undefined')
23
26
  return undefined;
@@ -0,0 +1,3 @@
1
+ /** `burgee/schema` — the schema surface and the manifest it reads, by itself. See `index.ts`. */
2
+ export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
3
+ export { Manifest } from './manifest.js';
@@ -0,0 +1,2 @@
1
+ export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf } from './schema.js';
2
+ export { Manifest } from './manifest.js';
package/dist/schema.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]}}}
1
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"propertyNames":{"enum":["error","warn","ok","hint","muted","command","flag","value","heading","ground"],"description":"roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}},"capabilities":{"$ref":"#/$defs/capabilities"},"widths":{"$ref":"#/$defs/widths"},"resolvers":{"type":"object","description":"bellpull resolvers by name: extra directories searched for an executable, before PATH (negative rank) or after it (positive). Two resolvers with one name shadow; two names both apply, in rank order.","additionalProperties":{"$ref":"#/$defs/resolver"}},"widgets":{"type":"object","description":"caique prompt widgets by kind. The six built-in kinds — text, confirm, select, multiselect, password, path — cannot be replaced: a plugin adds kinds of its own.","additionalProperties":{"$ref":"#/$defs/widget"}},"handlers":{"type":"array","description":"closeout exit handlers. The phase decides the order they run in, not their position in this list.","items":{"$ref":"#/$defs/handler"}},"sources":{"type":"object","description":"seniority configuration sources by name, ranked against the built-in layers: a flag is 0 and a declared default is 40, and a plugin source sits strictly between.","additionalProperties":{"$ref":"#/$defs/source"}},"commands":{"type":"array","description":"burgee commands this plugin contributes. Each is read by exactly the code a program's own command is, and a burgee plugin must declare `contract`.","items":{"$ref":"#/$defs/command"}},"hooks":{"type":"object","description":"burgee lifecycle hooks. preRun opens around a command, and exactly one of postRun or onError closes.","additionalProperties":false,"properties":{"preRun":{"$ref":"#/$defs/hook"},"postRun":{"$ref":"#/$defs/hook"},"onError":{"$ref":"#/$defs/hook"}}},"enforce":{"type":"string","pattern":"^(pre|post)$","description":"burgee: order this plugin's hooks before (`pre`) or after (`post`) the others."}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}},"capability":{"type":"object","required":["name","osc","when","encode","fallback"],"additionalProperties":false,"description":"One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.","properties":{"name":{"type":"string","minLength":1,"description":"How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."},"osc":{"description":"The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.","oneOf":[{"type":"integer","minimum":0},{"const":"BEL"}]},"when":{"type":"object","description":"When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.","additionalProperties":false,"properties":{"tty":{"type":"boolean","description":"Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."},"termProgram":{"type":"array","items":{"type":"string"},"description":"Any one of these TERM_PROGRAM values."},"envAny":{"type":"array","items":{"type":"string"},"description":"Any one of these environment variables merely being set, as VTE announces itself."},"term":{"type":"string","description":"An exact TERM — Kitty is xterm-kitty."}}},"encode":{"type":"string","minLength":1,"description":"The bytes, as a template, for a terminal that does understand."},"fallback":{"type":"string","description":"What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."}},"examples":[{"name":"kitty-image","osc":"BEL","when":{"tty":true,"term":"xterm-kitty"},"encode":"\u001b_Ga=T,f=100;{base64}\u001b\\","fallback":"{caption}"}]},"capabilities":{"type":"object","description":"paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/capability"}},"capabilityDocument":{"description":"What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.","oneOf":[{"description":"The family shape: a plugin carrying its capabilities under `capabilities`.","type":"object","required":["name","capabilities"],"properties":{"name":{"type":"string","minLength":1},"contract":{"type":"integer","minimum":1},"capabilities":{"$ref":"#/$defs/capabilities"}}},{"deprecated":true,"description":"Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.","$ref":"#/$defs/capability"}]},"widthRange":{"type":"array","description":"One inclusive code-point range, as [low, high]. A single code point is written [n, n].","items":{"type":"integer","minimum":0,"maximum":1114111},"minItems":2,"maxItems":2},"widthOverride":{"type":"object","description":"A linegauge width override: the column count a named set of code-point ranges occupies, for a terminal that disagrees with the Unicode tables. Data only — no function, so it can be written in a config file, diffed, and printed by `linegauge check` without running anyone code.","required":["ranges","columns","why"],"additionalProperties":false,"properties":{"ranges":{"type":"array","description":"The code points this override applies to.","items":{"$ref":"#/$defs/widthRange"},"minItems":1},"columns":{"type":"integer","description":"Columns each cluster in those ranges occupies: 0 for a zero-width mark, 1 narrow, 2 wide.","minimum":0,"maximum":2},"why":{"type":"string","description":"The terminal or the reason. Required, because a width table with no provenance is one nobody can audit when it turns out to be wrong — which is the normal outcome for ambiguous width.","minLength":1}}},"widths":{"type":"object","description":"linegauge width overrides by name — the measurement section of a plugin, read the way flagstaff reads `spinners`.","additionalProperties":{"$ref":"#/$defs/widthOverride"}},"resolver":{"type":"object","required":["rank","paths"],"description":"One bellpull resolver: where to look for an executable, and when.","properties":{"rank":{"type":"number","description":"Search order against PATH: negative searches before it, positive after."},"paths":{"type":"array","minItems":1,"description":"Directories to search. `{VAR}` is substituted from the environment; each must be absolute, or start with a `{VAR}` that holds an absolute path.","items":{"type":"string","minLength":1,"pattern":"^(/|\\{|[A-Za-z]:[\\\\/]|\\\\\\\\)"}},"extensions":{"type":"array","items":{"type":"string"},"description":"Extensions to try on Windows, in place of PATHEXT."},"when":{"type":"object","description":"When the resolver applies. Leave it out and it always does.","properties":{"platform":{"type":"array","items":{"type":"string"},"description":"process.platform values it applies on."},"envAny":{"type":"array","items":{"type":"string"},"description":"It applies when any of these environment variables is set."}}}}},"widget":{"type":"object","required":["static"],"description":"One caique widget: how a prompt kind of the plugin's own is drawn.","properties":{"static":{"description":"(spec) => string. Required: what a pipe, an agent or a screen reader gets instead of the interactive prompt."},"frame":{"description":"(t, spec) => string. Optional: the animated form; leave it out and the widget is line-mode only."},"sample":{"type":"object","required":["running","done"],"description":"Two named states of plain data that `caique check` renders the widget with."}}},"handler":{"type":"object","required":["name","run"],"description":"One closeout exit handler.","properties":{"name":{"type":"string","minLength":1,"description":"How the handler is named in a report, including the one the deadline prints when it does not return."},"phase":{"type":"string","pattern":"^(flush|release)$","description":"When it runs. Defaults to `release`; `restore` is closeout's own last phase and a plugin may not use it."},"run":{"description":"(info) => void | Promise<void>. Required: the cleanup itself."}}},"source":{"type":"object","required":["rank"],"description":"One seniority source. Exactly one of `values` (static) or `read(runtime)` (fetched) gives its answer.","properties":{"rank":{"type":"integer","minimum":1,"maximum":39,"description":"Precedence, strictly between the flag (0) and the declared default (40): a source may not beat what the user typed, nor sink below the default."},"values":{"type":"object","description":"Option name to value."},"read":{"description":"(runtime) => { values, location? } | undefined."},"location":{"type":"string","description":"The file, URL or variable set a person would go and look at."}}},"option":{"type":"object","required":["type"],"description":"One burgee option, keyed by its camelCase name; the flag is its kebab-case form.","properties":{"type":{"type":"string","pattern":"^(string|boolean|number)$"},"description":{"type":"string"},"short":{"type":"string","minLength":1},"required":{"type":"boolean"},"env":{"type":"string"},"choices":{"type":"array","items":{"type":"string"}},"dependsOn":{"type":"array","items":{"type":"string"},"description":"Options that must be set with this one; each must be another option of the same command."},"exclusive":{"type":"array","items":{"type":"string"},"description":"Options that may not be set with this one; each must be another option of the same command."},"deprecated":{"description":"What replaces this option, e.g. '--force'. `true` alone, or an empty string, is refused: a deprecation names its replacement."}}},"command":{"type":"object","required":["path"],"description":"One burgee command node, declared exactly as a program's own command is.","properties":{"path":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"description":"The words a user types. May not repeat a path the program or an earlier plugin already declares."},"description":{"type":"string"},"options":{"type":"object","additionalProperties":{"$ref":"#/$defs/option"}},"effects":{"type":"string","pattern":"^(read_only|idempotent|non_idempotent|withheld)$","description":"What running it does to the world. Required on any command that runs; `withheld` serves it to people and keeps it out of the MCP tool list."},"deprecated":{"description":"What replaces this command. `true` alone, or an empty string, is refused."},"run":{"description":"(ctx) => unknown. What the command does; its return value is the `--json` envelope's data."},"load":{"description":"() => Promise<module>. A lazy form of `run`, imported on dispatch."}}},"hook":{"type":"object","required":["handler"],"description":"One burgee hook.","properties":{"filter":{"type":"object","description":"`{ command: RegExp }`: fire only for commands whose path matches."},"handler":{"description":"(ctx) => void | Promise<void>. Required."}}}}}