burgee 0.5.0 → 0.6.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.
- package/dist/execute.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/yargs/cliui.d.ts +6 -10
- package/dist/yargs/cliui.js +79 -245
- package/package.json +4 -2
- package/dist/config.d.ts +0 -28
- package/dist/config.js +0 -98
- package/dist/precedence.d.ts +0 -55
- package/dist/precedence.js +0 -100
package/dist/execute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { ConfigError, explain, resolve as resolveLayers } from 'seniority';
|
|
3
4
|
import { detectAgent } from './agent.js';
|
|
4
5
|
import { ExitCode, isExitCode } from './exit-code.js';
|
|
5
6
|
import { renderHelp } from './help.js';
|
|
@@ -7,7 +8,6 @@ import { Manifest } from './manifest.js';
|
|
|
7
8
|
import { serveMcp } from './mcp.js';
|
|
8
9
|
import { camel, kebab } from './names.js';
|
|
9
10
|
import { nearestPackage } from './pkg.js';
|
|
10
|
-
import { ConfigError, explain, resolve as resolveLayers } from './precedence.js';
|
|
11
11
|
import { commandSchemaOf, machineJson, schemaOf, summaryOf } from './schema.js';
|
|
12
12
|
import { checkDefinition, checkRelations, coerce, UsageError } from './validate.js';
|
|
13
13
|
function helpFields(c) {
|
|
@@ -159,7 +159,7 @@ function packageLayer(pkg, name) {
|
|
|
159
159
|
return typeof field === 'object' && field !== null && !Array.isArray(field) ? { path: pkg.path, data: field } : undefined;
|
|
160
160
|
}
|
|
161
161
|
async function configLayers(name, values, io) {
|
|
162
|
-
const { discover } = await import('
|
|
162
|
+
const { discover } = await import('seniority');
|
|
163
163
|
const explicit = values['config'];
|
|
164
164
|
const disabled = values['noConfig'] === true;
|
|
165
165
|
const loaded = await discover({ name, cwd: io.cwd, env: io.env, ...(typeof explicit === 'string' ? { explicit } : {}), disabled });
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { camel, checkDefinition, kebab, UsageError } from './validate.js';
|
|
|
11
11
|
export { AGENT_PROBES, detectAgent, type AgentProbe, type Detection } from './agent.js';
|
|
12
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
|
-
export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from '
|
|
14
|
+
export { ConfigError, envName, explain, resolve, screaming, type Candidate, type Layers, type Provenance, type Resolution, type Source } from 'seniority';
|
|
15
15
|
export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf, type CommandSchema, type JsonSchema, type ProgramSchema, type SchemaSummary } from './schema.js';
|
|
16
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
|
@@ -4,6 +4,6 @@ export { camel, checkDefinition, kebab, UsageError } from './validate.js';
|
|
|
4
4
|
export { AGENT_PROBES, detectAgent } from './agent.js';
|
|
5
5
|
export { renderHelp } from './help.js';
|
|
6
6
|
export { annotationsOf, MCP_PROTOCOL_VERSION, serveMcp, toolsOf } from './mcp.js';
|
|
7
|
-
export { ConfigError, envName, explain, resolve, screaming } from '
|
|
7
|
+
export { ConfigError, envName, explain, resolve, screaming } from 'seniority';
|
|
8
8
|
export { commandSchemaOf, inputSchemaOf, schemaOf, summaryOf } from './schema.js';
|
|
9
9
|
export { definePlugin, Manifest, } from './manifest.js';
|
package/dist/yargs/cliui.d.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
+
import { type WrapOptions } from "linegauge";
|
|
1
2
|
/**
|
|
2
|
-
* cliui 9 — the column layout yargs' usage renders through — with the
|
|
3
|
-
*
|
|
3
|
+
* cliui 9 — the column layout yargs' usage renders through — with the wrap-ansi it depends
|
|
4
|
+
* on, ported for `burgee/yargs`. Width and escape-stripping come from linegauge. The wrapping
|
|
4
5
|
* arithmetic is byte-for-byte the upstream's: yargs' usage tests compare whole help
|
|
5
6
|
* screens.
|
|
6
7
|
*/
|
|
7
|
-
export declare
|
|
8
|
-
export declare
|
|
9
|
-
interface WrapOptions {
|
|
10
|
-
hard?: boolean;
|
|
11
|
-
wordWrap?: boolean;
|
|
12
|
-
trim?: boolean;
|
|
13
|
-
}
|
|
8
|
+
export declare const stripAnsi: (str: string) => string;
|
|
9
|
+
export declare const stringWidth: (input: string) => number;
|
|
14
10
|
export declare function wrapAnsi(string: string, columns: number, options?: WrapOptions): string;
|
|
15
11
|
export interface Column {
|
|
16
12
|
text: string;
|
|
17
13
|
width?: number | undefined;
|
|
18
|
-
align?:
|
|
14
|
+
align?: "right" | "left" | "center";
|
|
19
15
|
padding: number[];
|
|
20
16
|
border?: boolean;
|
|
21
17
|
}
|
package/dist/yargs/cliui.js
CHANGED
|
@@ -1,221 +1,36 @@
|
|
|
1
|
+
import { strip, width, wrap } from "linegauge";
|
|
1
2
|
/**
|
|
2
|
-
* cliui 9 — the column layout yargs' usage renders through — with the
|
|
3
|
-
*
|
|
3
|
+
* cliui 9 — the column layout yargs' usage renders through — with the wrap-ansi it depends
|
|
4
|
+
* on, ported for `burgee/yargs`. Width and escape-stripping come from linegauge. The wrapping
|
|
4
5
|
* arithmetic is byte-for-byte the upstream's: yargs' usage tests compare whole help
|
|
5
6
|
* screens.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(code >= 0x20000 && code <= 0x3fffd));
|
|
32
|
-
}
|
|
33
|
-
export function stringWidth(input) {
|
|
34
|
-
if (typeof input !== 'string' || input.length === 0)
|
|
35
|
-
return 0;
|
|
36
|
-
const string = stripAnsi(input);
|
|
37
|
-
if (string.length === 0)
|
|
38
|
-
return 0;
|
|
39
|
-
let width = 0;
|
|
40
|
-
for (const { segment } of segmenter.segment(string)) {
|
|
41
|
-
const code = segment.codePointAt(0);
|
|
42
|
-
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
|
|
43
|
-
continue;
|
|
44
|
-
if ((code >= 0x200b && code <= 0x200f) || code === 0xfeff)
|
|
45
|
-
continue;
|
|
46
|
-
if (code >= 0x300 && code <= 0x36f)
|
|
47
|
-
continue;
|
|
48
|
-
if (EMOJI.test(segment)) {
|
|
49
|
-
width += 2;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
width += isWide(code) ? 2 : 1;
|
|
53
|
-
}
|
|
54
|
-
return width;
|
|
55
|
-
}
|
|
56
|
-
/** ansi-styles' open→close map, the part wrap-ansi reads. */
|
|
57
|
-
function closeCode(code) {
|
|
58
|
-
if (code === 1 || code === 2)
|
|
59
|
-
return 22;
|
|
60
|
-
if (code === 3)
|
|
61
|
-
return 23;
|
|
62
|
-
if (code === 4)
|
|
63
|
-
return 24;
|
|
64
|
-
if (code === 7)
|
|
65
|
-
return 27;
|
|
66
|
-
if (code === 8)
|
|
67
|
-
return 28;
|
|
68
|
-
if (code === 9)
|
|
69
|
-
return 29;
|
|
70
|
-
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97))
|
|
71
|
-
return 39;
|
|
72
|
-
if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107))
|
|
73
|
-
return 49;
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
const ESC = '\u001B';
|
|
77
|
-
const ESCAPES = new Set([ESC, '\u009B']);
|
|
78
|
-
const END_CODE = 39;
|
|
79
|
-
const ANSI_ESCAPE_BELL = '\u0007';
|
|
80
|
-
const ANSI_CSI = '[';
|
|
81
|
-
const ANSI_OSC = ']';
|
|
82
|
-
const ANSI_SGR_TERMINATOR = 'm';
|
|
83
|
-
const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
|
|
84
|
-
const wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
|
|
85
|
-
const wrapAnsiHyperlink = (uri) => `${ESC}${ANSI_ESCAPE_LINK}${uri}${ANSI_ESCAPE_BELL}`;
|
|
86
|
-
const wordLengths = (string) => string.split(' ').map((character) => stringWidth(character));
|
|
87
|
-
function wrapWord(rows, word, columns) {
|
|
88
|
-
const characters = [...word];
|
|
89
|
-
let isInsideEscape = false;
|
|
90
|
-
let isInsideLinkEscape = false;
|
|
91
|
-
let visible = stringWidth(stripAnsi(rows.at(-1)));
|
|
92
|
-
for (const [index, character] of characters.entries()) {
|
|
93
|
-
const characterLength = stringWidth(character);
|
|
94
|
-
if (visible + characterLength <= columns)
|
|
95
|
-
rows[rows.length - 1] += character;
|
|
96
|
-
else {
|
|
97
|
-
rows.push(character);
|
|
98
|
-
visible = 0;
|
|
99
|
-
}
|
|
100
|
-
if (ESCAPES.has(character)) {
|
|
101
|
-
isInsideEscape = true;
|
|
102
|
-
isInsideLinkEscape = characters.slice(index + 1).join('').startsWith(ANSI_ESCAPE_LINK);
|
|
103
|
-
}
|
|
104
|
-
if (isInsideEscape) {
|
|
105
|
-
if (isInsideLinkEscape) {
|
|
106
|
-
if (character === ANSI_ESCAPE_BELL) {
|
|
107
|
-
isInsideEscape = false;
|
|
108
|
-
isInsideLinkEscape = false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else if (character === ANSI_SGR_TERMINATOR)
|
|
112
|
-
isInsideEscape = false;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
visible += characterLength;
|
|
116
|
-
if (visible === columns && index < characters.length - 1) {
|
|
117
|
-
rows.push('');
|
|
118
|
-
visible = 0;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (!visible && rows.at(-1).length > 0 && rows.length > 1)
|
|
122
|
-
rows[rows.length - 2] += rows.pop();
|
|
123
|
-
}
|
|
124
|
-
function stringVisibleTrimSpacesRight(string) {
|
|
125
|
-
const words = string.split(' ');
|
|
126
|
-
let last = words.length;
|
|
127
|
-
while (last > 0) {
|
|
128
|
-
if (stringWidth(words[last - 1]) > 0)
|
|
129
|
-
break;
|
|
130
|
-
last--;
|
|
131
|
-
}
|
|
132
|
-
if (last === words.length)
|
|
133
|
-
return string;
|
|
134
|
-
return words.slice(0, last).join(' ') + words.slice(last).join('');
|
|
135
|
-
}
|
|
136
|
-
function execWrap(string, columns, options = {}) {
|
|
137
|
-
if (options.trim !== false && string.trim() === '')
|
|
138
|
-
return '';
|
|
139
|
-
let returnValue = '';
|
|
140
|
-
let escapeCode;
|
|
141
|
-
let escapeUrl;
|
|
142
|
-
const lengths = wordLengths(string);
|
|
143
|
-
let rows = [''];
|
|
144
|
-
for (const [index, word] of string.split(' ').entries()) {
|
|
145
|
-
if (options.trim !== false)
|
|
146
|
-
rows[rows.length - 1] = rows.at(-1).trimStart();
|
|
147
|
-
let rowLength = stringWidth(rows.at(-1));
|
|
148
|
-
if (index !== 0) {
|
|
149
|
-
if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
|
|
150
|
-
rows.push('');
|
|
151
|
-
rowLength = 0;
|
|
152
|
-
}
|
|
153
|
-
if (rowLength > 0 || options.trim === false) {
|
|
154
|
-
rows[rows.length - 1] += ' ';
|
|
155
|
-
rowLength++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const length = lengths[index];
|
|
159
|
-
if (options.hard && length > columns) {
|
|
160
|
-
const remainingColumns = columns - rowLength;
|
|
161
|
-
const breaksStartingThisLine = 1 + Math.floor((length - remainingColumns - 1) / columns);
|
|
162
|
-
const breaksStartingNextLine = Math.floor((length - 1) / columns);
|
|
163
|
-
if (breaksStartingNextLine < breaksStartingThisLine)
|
|
164
|
-
rows.push('');
|
|
165
|
-
wrapWord(rows, word, columns);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (rowLength + length > columns && rowLength > 0 && length > 0) {
|
|
169
|
-
if (options.wordWrap === false && rowLength < columns) {
|
|
170
|
-
wrapWord(rows, word, columns);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
rows.push('');
|
|
174
|
-
}
|
|
175
|
-
if (rowLength + length > columns && options.wordWrap === false) {
|
|
176
|
-
wrapWord(rows, word, columns);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
rows[rows.length - 1] += word;
|
|
180
|
-
}
|
|
181
|
-
if (options.trim !== false)
|
|
182
|
-
rows = rows.map(stringVisibleTrimSpacesRight);
|
|
183
|
-
const pre = [...rows.join('\n')];
|
|
184
|
-
for (const [index, character] of pre.entries()) {
|
|
185
|
-
returnValue += character;
|
|
186
|
-
if (ESCAPES.has(character)) {
|
|
187
|
-
const found = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`).exec(pre.slice(index).join(''));
|
|
188
|
-
const groups = found?.groups ?? {};
|
|
189
|
-
if (groups.code !== undefined) {
|
|
190
|
-
const code = Number.parseFloat(groups.code);
|
|
191
|
-
escapeCode = code === END_CODE ? undefined : code;
|
|
192
|
-
}
|
|
193
|
-
else if (groups.uri !== undefined)
|
|
194
|
-
escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
|
|
195
|
-
}
|
|
196
|
-
const code = closeCode(Number(escapeCode));
|
|
197
|
-
if (pre[index + 1] === '\n') {
|
|
198
|
-
if (escapeUrl)
|
|
199
|
-
returnValue += wrapAnsiHyperlink('');
|
|
200
|
-
if (escapeCode && code)
|
|
201
|
-
returnValue += wrapAnsiCode(code);
|
|
202
|
-
}
|
|
203
|
-
else if (character === '\n') {
|
|
204
|
-
if (escapeCode && code)
|
|
205
|
-
returnValue += wrapAnsiCode(escapeCode);
|
|
206
|
-
if (escapeUrl)
|
|
207
|
-
returnValue += wrapAnsiHyperlink(escapeUrl);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return returnValue;
|
|
211
|
-
}
|
|
8
|
+
/*
|
|
9
|
+
* `width` and `strip` are linegauge's — measuring text and removing escapes is the layer
|
|
10
|
+
* below this one, and this file had its own copies of both. They were not merely duplicated,
|
|
11
|
+
* they were wrong: the ITU T.416 sub-parameter form `ESC[38:2::255:0:0m`, which chalk emits
|
|
12
|
+
* for truecolor, left `:2::255:0:0m` behind and measured a 13-column string as 25. linegauge
|
|
13
|
+
* fixed that in its own `strip`; burgee inherited nothing because it was not asking.
|
|
14
|
+
*
|
|
15
|
+
* The names stay, because `usage.ts` and `shim.ts` import them and the upstream port reads
|
|
16
|
+
* the way cliui reads.
|
|
17
|
+
*/
|
|
18
|
+
export const stripAnsi = (str) => typeof str === "string" ? strip(str) : str;
|
|
19
|
+
export const stringWidth = (input) => typeof input === "string" ? width(input) : 0;
|
|
20
|
+
/*
|
|
21
|
+
* The wrap is linegauge's too. This file carried a wrap-ansi port — ansi-styles' open→close
|
|
22
|
+
* map, an escape-state machine over every character, hyperlink re-opening — to wrap a line
|
|
23
|
+
* without breaking the styling across it. That is the same job `linegauge/wrap` exists for
|
|
24
|
+
* and grades against wrap-ansi, so burgee kept a second implementation of a thing the layer
|
|
25
|
+
* below already owned.
|
|
26
|
+
*
|
|
27
|
+
* Checked before swapping, because yargs' usage tests compare whole help screens: the two
|
|
28
|
+
* agreed on all ten shapes probed — plain prose at several widths, an unbreakable word,
|
|
29
|
+
* SGR-coloured text, single characters, CJK, an embedded newline, leading and trailing
|
|
30
|
+
* spaces, empty, and a string exactly the column width.
|
|
31
|
+
*/
|
|
212
32
|
export function wrapAnsi(string, columns, options) {
|
|
213
|
-
return String(string)
|
|
214
|
-
.normalize()
|
|
215
|
-
.replace(/\r\n/g, '\n')
|
|
216
|
-
.split('\n')
|
|
217
|
-
.map((line) => execWrap(line, columns, options))
|
|
218
|
-
.join('\n');
|
|
33
|
+
return wrap(String(string).normalize().replace(/\r\n/g, "\n"), columns, options);
|
|
219
34
|
}
|
|
220
35
|
const align = { right: alignRight, center: alignCenter };
|
|
221
36
|
const top = 0;
|
|
@@ -240,21 +55,24 @@ export class UI {
|
|
|
240
55
|
}
|
|
241
56
|
div(...args) {
|
|
242
57
|
if (args.length === 0)
|
|
243
|
-
this.div(
|
|
244
|
-
if (this.wrap &&
|
|
58
|
+
this.div("");
|
|
59
|
+
if (this.wrap &&
|
|
60
|
+
this.shouldApplyLayoutDSL(...args) &&
|
|
61
|
+
typeof args[0] === "string")
|
|
245
62
|
return this.applyLayoutDSL(args[0]);
|
|
246
|
-
const cols = args.map((arg) =>
|
|
63
|
+
const cols = args.map((arg) => typeof arg === "string" ? this.colFromString(arg) : arg);
|
|
247
64
|
this.rows.push(cols);
|
|
248
65
|
return cols;
|
|
249
66
|
}
|
|
250
67
|
shouldApplyLayoutDSL(...args) {
|
|
251
|
-
return args.length === 1 && typeof args[0] ===
|
|
68
|
+
return (args.length === 1 && typeof args[0] === "string" && /[\t\n]/.test(args[0]));
|
|
252
69
|
}
|
|
253
70
|
applyLayoutDSL(str) {
|
|
254
|
-
const rows = str.split(
|
|
71
|
+
const rows = str.split("\n").map((row) => row.split("\t"));
|
|
255
72
|
let leftColumnWidth = 0;
|
|
256
73
|
rows.forEach((columns) => {
|
|
257
|
-
if (columns.length > 1 &&
|
|
74
|
+
if (columns.length > 1 &&
|
|
75
|
+
stringWidth(columns[0]) > leftColumnWidth) {
|
|
258
76
|
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), stringWidth(columns[0]));
|
|
259
77
|
}
|
|
260
78
|
});
|
|
@@ -271,8 +89,20 @@ export class UI {
|
|
|
271
89
|
return { text, padding: this.measurePadding(text) };
|
|
272
90
|
}
|
|
273
91
|
measurePadding(str) {
|
|
92
|
+
// An unanchored `\s*$` was the upstream spelling, and it is quadratic: the engine
|
|
93
|
+
// retries at every position, so a cell of 50,000 spaces then an `x` costs 1,346 ms
|
|
94
|
+
// here — CodeQL alert 35, "polynomial regular expression used on uncontrolled data",
|
|
95
|
+
// raised once `stripAnsi` became library input. Spaces, not the tabs the alert names:
|
|
96
|
+
// a tab routes into `applyLayoutDSL`, which splits on it long before this runs.
|
|
97
|
+
// `trim{Start,End}` remove exactly the set `\s` matches (WhiteSpace + LineTerminator)
|
|
98
|
+
// and are linear, so this is the same measurement without the backtracking.
|
|
274
99
|
const noAnsi = stripAnsi(str);
|
|
275
|
-
return [
|
|
100
|
+
return [
|
|
101
|
+
0,
|
|
102
|
+
noAnsi.length - noAnsi.trimEnd().length,
|
|
103
|
+
0,
|
|
104
|
+
noAnsi.length - noAnsi.trimStart().length,
|
|
105
|
+
];
|
|
276
106
|
}
|
|
277
107
|
toString() {
|
|
278
108
|
const lines = [];
|
|
@@ -282,36 +112,36 @@ export class UI {
|
|
|
282
112
|
return lines
|
|
283
113
|
.filter((line) => !line.hidden)
|
|
284
114
|
.map((line) => line.text)
|
|
285
|
-
.join(
|
|
115
|
+
.join("\n");
|
|
286
116
|
}
|
|
287
117
|
rowToString(row, lines) {
|
|
288
118
|
this.rasterize(row).forEach((rrow, r) => {
|
|
289
|
-
let str =
|
|
119
|
+
let str = "";
|
|
290
120
|
rrow.forEach((col, c) => {
|
|
291
121
|
const column = row[c];
|
|
292
122
|
const { width } = column;
|
|
293
123
|
const wrapWidth = this.negatePadding(column);
|
|
294
124
|
let ts = col;
|
|
295
125
|
if (wrapWidth > stringWidth(col))
|
|
296
|
-
ts +=
|
|
297
|
-
if (column.align && column.align !==
|
|
126
|
+
ts += " ".repeat(wrapWidth - stringWidth(col));
|
|
127
|
+
if (column.align && column.align !== "left" && this.wrap) {
|
|
298
128
|
const fn = align[column.align];
|
|
299
129
|
ts = fn(ts, wrapWidth);
|
|
300
130
|
if (stringWidth(ts) < wrapWidth)
|
|
301
|
-
ts +=
|
|
131
|
+
ts += " ".repeat((width || 0) - stringWidth(ts) - 1);
|
|
302
132
|
}
|
|
303
133
|
const padding = column.padding || [0, 0, 0, 0];
|
|
304
134
|
if (padding[left])
|
|
305
|
-
str +=
|
|
306
|
-
str += addBorder(column, ts,
|
|
135
|
+
str += " ".repeat(padding[left]);
|
|
136
|
+
str += addBorder(column, ts, "| ");
|
|
307
137
|
str += ts;
|
|
308
|
-
str += addBorder(column, ts,
|
|
138
|
+
str += addBorder(column, ts, " |");
|
|
309
139
|
if (padding[right])
|
|
310
|
-
str +=
|
|
140
|
+
str += " ".repeat(padding[right]);
|
|
311
141
|
if (r === 0 && lines.length > 0)
|
|
312
142
|
str = this.renderInline(str, lines.at(-1));
|
|
313
143
|
});
|
|
314
|
-
lines.push({ text: str.replace(/ +$/,
|
|
144
|
+
lines.push({ text: str.replace(/ +$/, ""), span: row.span });
|
|
315
145
|
});
|
|
316
146
|
return lines;
|
|
317
147
|
}
|
|
@@ -329,7 +159,9 @@ export class UI {
|
|
|
329
159
|
if (leadingWhitespace < targetTextWidth)
|
|
330
160
|
return source;
|
|
331
161
|
previousLine.hidden = true;
|
|
332
|
-
return target.trimEnd() +
|
|
162
|
+
return (target.trimEnd() +
|
|
163
|
+
" ".repeat(leadingWhitespace - targetTextWidth) +
|
|
164
|
+
source.trimStart());
|
|
333
165
|
}
|
|
334
166
|
rasterize(row) {
|
|
335
167
|
const rrows = [];
|
|
@@ -338,16 +170,18 @@ export class UI {
|
|
|
338
170
|
row.forEach((col, c) => {
|
|
339
171
|
col.width = widths[c];
|
|
340
172
|
if (this.wrap)
|
|
341
|
-
wrapped = wrapAnsi(col.text, this.negatePadding(col), {
|
|
173
|
+
wrapped = wrapAnsi(col.text, this.negatePadding(col), {
|
|
174
|
+
hard: true,
|
|
175
|
+
}).split("\n");
|
|
342
176
|
else
|
|
343
|
-
wrapped = col.text.split(
|
|
177
|
+
wrapped = col.text.split("\n");
|
|
344
178
|
if (col.border) {
|
|
345
|
-
wrapped.unshift(`.${
|
|
346
|
-
wrapped.push(`'${
|
|
179
|
+
wrapped.unshift(`.${"-".repeat(this.negatePadding(col) + 2)}.`);
|
|
180
|
+
wrapped.push(`'${"-".repeat(this.negatePadding(col) + 2)}'`);
|
|
347
181
|
}
|
|
348
182
|
if (col.padding) {
|
|
349
|
-
wrapped.unshift(...new Array(col.padding[top] || 0).fill(
|
|
350
|
-
wrapped.push(...new Array(col.padding[bottom] || 0).fill(
|
|
183
|
+
wrapped.unshift(...new Array(col.padding[top] || 0).fill(""));
|
|
184
|
+
wrapped.push(...new Array(col.padding[bottom] || 0).fill(""));
|
|
351
185
|
}
|
|
352
186
|
wrapped.forEach((str, r) => {
|
|
353
187
|
if (!rrows[r])
|
|
@@ -355,7 +189,7 @@ export class UI {
|
|
|
355
189
|
const rrow = rrows[r];
|
|
356
190
|
for (let i = 0; i < c; i++) {
|
|
357
191
|
if (rrow[i] === undefined)
|
|
358
|
-
rrow.push(
|
|
192
|
+
rrow.push("");
|
|
359
193
|
}
|
|
360
194
|
rrow.push(str);
|
|
361
195
|
});
|
|
@@ -384,18 +218,18 @@ export class UI {
|
|
|
384
218
|
return undefined;
|
|
385
219
|
});
|
|
386
220
|
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
|
|
387
|
-
return widths.map((w, i) =>
|
|
221
|
+
return widths.map((w, i) => w === undefined ? Math.max(unsetWidth, minWidth(row[i])) : w);
|
|
388
222
|
}
|
|
389
223
|
}
|
|
390
224
|
function addBorder(col, ts, style) {
|
|
391
225
|
if (col.border) {
|
|
392
226
|
if (/[.']-+[.']/.test(ts))
|
|
393
|
-
return
|
|
227
|
+
return "";
|
|
394
228
|
if (ts.trim().length !== 0)
|
|
395
229
|
return style;
|
|
396
|
-
return
|
|
230
|
+
return " ";
|
|
397
231
|
}
|
|
398
|
-
return
|
|
232
|
+
return "";
|
|
399
233
|
}
|
|
400
234
|
function minWidth(col) {
|
|
401
235
|
const padding = col.padding || [];
|
|
@@ -405,7 +239,7 @@ function minWidth(col) {
|
|
|
405
239
|
return min;
|
|
406
240
|
}
|
|
407
241
|
function getWindowWidth() {
|
|
408
|
-
if (typeof process ===
|
|
242
|
+
if (typeof process === "object" && process.stdout && process.stdout.columns)
|
|
409
243
|
return process.stdout.columns;
|
|
410
244
|
return 80;
|
|
411
245
|
}
|
|
@@ -413,7 +247,7 @@ function alignRight(str, width) {
|
|
|
413
247
|
str = str.trim();
|
|
414
248
|
const strWidth = stringWidth(str);
|
|
415
249
|
if (strWidth < width)
|
|
416
|
-
return
|
|
250
|
+
return " ".repeat(width - strWidth) + str;
|
|
417
251
|
return str;
|
|
418
252
|
}
|
|
419
253
|
function alignCenter(str, width) {
|
|
@@ -421,7 +255,7 @@ function alignCenter(str, width) {
|
|
|
421
255
|
const strWidth = stringWidth(str);
|
|
422
256
|
if (strWidth >= width)
|
|
423
257
|
return str;
|
|
424
|
-
return
|
|
258
|
+
return " ".repeat((width - strWidth) >> 1) + str;
|
|
425
259
|
}
|
|
426
260
|
export function cliui(opts) {
|
|
427
261
|
return new UI({ width: opts?.width || getWindowWidth(), wrap: opts?.wrap });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "burgee",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -96,7 +96,9 @@
|
|
|
96
96
|
"provenance": true
|
|
97
97
|
},
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"
|
|
99
|
+
"linegauge": "^0.2.0",
|
|
100
|
+
"roundel": "^0.3.0",
|
|
101
|
+
"seniority": "^0.1.0"
|
|
100
102
|
},
|
|
101
103
|
"devDependencies": {
|
|
102
104
|
"vitest": "^5.0.0"
|
package/dist/config.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { type Layer } from './precedence.js';
|
|
2
|
-
export interface Discovery {
|
|
3
|
-
name: string;
|
|
4
|
-
cwd: string;
|
|
5
|
-
env: Record<string, string | undefined>;
|
|
6
|
-
/** `--config <path>`. */
|
|
7
|
-
explicit?: string;
|
|
8
|
-
/** `--no-config`. */
|
|
9
|
-
disabled?: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface Loaded extends Layer {
|
|
12
|
-
/** The files merged, outermost first — what `--explain` shows. */
|
|
13
|
-
chain: string[];
|
|
14
|
-
}
|
|
15
|
-
/** Objects merge recursively; anything else, the later value wins. */
|
|
16
|
-
export declare function deepMerge(base: Record<string, unknown>, over: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
-
/** Load a file and everything it extends, outermost first, the file's own keys winning. */
|
|
18
|
-
export declare function loadWithExtends(path: string, seen?: string[]): Promise<Loaded>;
|
|
19
|
-
/** The discovery order as candidate paths, first hit wins; each entry says why it was tried. */
|
|
20
|
-
export declare function candidates(d: Discovery): {
|
|
21
|
-
path: string;
|
|
22
|
-
reason: string;
|
|
23
|
-
}[];
|
|
24
|
-
/**
|
|
25
|
-
* Discover and load. `package.json#<name>` is not a file here — the engine reads the
|
|
26
|
-
* owning package.json itself and treats the field as its own layer, below config.
|
|
27
|
-
*/
|
|
28
|
-
export declare function discover(d: Discovery): Promise<Loaded | undefined>;
|
package/dist/config.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import { ConfigError } from './precedence.js';
|
|
6
|
-
const EXTENSIONS = ['.json', '.mjs', '.js', '.cjs'];
|
|
7
|
-
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
8
|
-
export function deepMerge(base, over) {
|
|
9
|
-
const out = new Map(Object.entries(base));
|
|
10
|
-
for (const [k, v] of Object.entries(over)) {
|
|
11
|
-
const prev = out.get(k);
|
|
12
|
-
out.set(k, isObject(prev) && isObject(v) ? deepMerge(prev, v) : v);
|
|
13
|
-
}
|
|
14
|
-
return Object.fromEntries(out);
|
|
15
|
-
}
|
|
16
|
-
async function loadFile(path) {
|
|
17
|
-
if (path.endsWith('.json')) {
|
|
18
|
-
try {
|
|
19
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
-
if (!isObject(parsed))
|
|
21
|
-
throw new ConfigError(`${path} must contain an object`);
|
|
22
|
-
return parsed;
|
|
23
|
-
}
|
|
24
|
-
catch (cause) {
|
|
25
|
-
if (cause instanceof ConfigError)
|
|
26
|
-
throw cause;
|
|
27
|
-
throw new ConfigError(`${path} is not valid JSON`, cause instanceof Error ? cause.message : undefined);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const mod = (await import(pathToFileURL(path).href));
|
|
31
|
-
const value = typeof mod.default === 'function' ? await mod.default() : mod.default;
|
|
32
|
-
if (!isObject(value))
|
|
33
|
-
throw new ConfigError(`${path} must export an object (or a function returning one)`);
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
function resolveExtends(spec, from) {
|
|
37
|
-
if (spec.startsWith('.') || isAbsolute(spec))
|
|
38
|
-
return resolve(dirname(from), spec);
|
|
39
|
-
try {
|
|
40
|
-
return createRequire(from).resolve(spec);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
throw new ConfigError(`${from} extends "${spec}", which cannot be resolved`, 'use a relative path or an installed package');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function extendsList(value) {
|
|
47
|
-
if (typeof value === 'string')
|
|
48
|
-
return [value];
|
|
49
|
-
return Array.isArray(value) ? value.map(String) : [];
|
|
50
|
-
}
|
|
51
|
-
export async function loadWithExtends(path, seen = []) {
|
|
52
|
-
if (seen.includes(path))
|
|
53
|
-
throw new ConfigError(`config extends itself: ${[...seen, path].join(' → ')}`);
|
|
54
|
-
const own = await loadFile(path);
|
|
55
|
-
const parents = own['extends'];
|
|
56
|
-
const specs = extendsList(parents);
|
|
57
|
-
let data = {};
|
|
58
|
-
const chain = [];
|
|
59
|
-
for (const spec of specs) {
|
|
60
|
-
const parent = await loadWithExtends(resolveExtends(spec, path), [...seen, path]);
|
|
61
|
-
data = deepMerge(data, parent.data);
|
|
62
|
-
chain.push(...parent.chain);
|
|
63
|
-
}
|
|
64
|
-
const { extends: _ignored, ...rest } = own;
|
|
65
|
-
return { path, data: deepMerge(data, rest), chain: [...chain, path] };
|
|
66
|
-
}
|
|
67
|
-
const userConfigDir = (env) => {
|
|
68
|
-
const base = env['XDG_CONFIG_HOME'] ?? (env['HOME'] === undefined ? undefined : join(env['HOME'], '.config'));
|
|
69
|
-
return base;
|
|
70
|
-
};
|
|
71
|
-
export function candidates(d) {
|
|
72
|
-
const out = [];
|
|
73
|
-
const fromEnv = d.env[`${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG`];
|
|
74
|
-
if (fromEnv !== undefined)
|
|
75
|
-
out.push({ path: resolve(d.cwd, fromEnv), reason: `${d.name.toUpperCase().replaceAll('-', '_')}_CONFIG` });
|
|
76
|
-
for (const ext of EXTENSIONS)
|
|
77
|
-
out.push({ path: join(d.cwd, `${d.name}.config${ext}`), reason: 'current directory' });
|
|
78
|
-
const user = userConfigDir(d.env);
|
|
79
|
-
if (user !== undefined)
|
|
80
|
-
out.push({ path: join(user, d.name, 'config.json'), reason: 'user config directory' });
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
export async function discover(d) {
|
|
84
|
-
if (d.disabled === true)
|
|
85
|
-
return undefined;
|
|
86
|
-
if (d.explicit !== undefined) {
|
|
87
|
-
const path = resolve(d.cwd, d.explicit);
|
|
88
|
-
if (!existsSync(path))
|
|
89
|
-
throw new ConfigError(`config file not found: ${path}`, 'check --config, or drop it to use discovery');
|
|
90
|
-
return await loadWithExtends(path);
|
|
91
|
-
}
|
|
92
|
-
const found = candidates(d)
|
|
93
|
-
.map((c) => c.path)
|
|
94
|
-
.find((path) => existsSync(path));
|
|
95
|
-
if (found === undefined)
|
|
96
|
-
return undefined;
|
|
97
|
-
return await loadWithExtends(found);
|
|
98
|
-
}
|
package/dist/precedence.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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;
|
package/dist/precedence.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
}
|