argsbarg 3.4.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -1
- package/README.md +24 -8
- package/biome.json +29 -6
- package/bun.lock +22 -0
- package/docs/cli-program.md +75 -1
- package/docs/install.md +1 -1
- package/docs/mcp.md +45 -1
- package/docs/templates/cursor/rules/cli-program.mdc +15 -21
- package/index.d.ts +95 -50
- package/justfile +24 -6
- package/package.json +4 -2
- package/scripts/release.ts +26 -9
- package/src/builtins/builtins.test.ts +9 -4
- package/src/builtins/completion-bash.ts +74 -50
- package/src/builtins/completion-fish.ts +3 -8
- package/src/builtins/completion-group.ts +1 -1
- package/src/builtins/completion-zsh.ts +80 -42
- package/src/builtins/dispatch.ts +20 -16
- package/src/builtins/export.ts +19 -10
- package/src/builtins/index.ts +9 -4
- package/src/builtins/install.ts +10 -10
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +8 -8
- package/src/builtins/scopes.ts +1 -1
- package/src/builtins/version.ts +1 -1
- package/src/completion.ts +4 -4
- package/src/context.ts +91 -15
- package/src/docs/api-guide.test.ts +2 -2
- package/src/docs/api-guide.ts +19 -5
- package/src/docs/builtin.ts +27 -8
- package/src/docs/docs.test.ts +18 -11
- package/src/docs/mcp-guide.ts +108 -25
- package/src/docs/resolve.ts +10 -3
- package/src/docs/save.ts +11 -3
- package/src/formats.test.ts +35 -0
- package/src/formats.ts +135 -0
- package/src/headless.test.ts +8 -16
- package/src/help.ts +73 -43
- package/src/hidden-mcpb.test.ts +7 -6
- package/src/hidden.ts +2 -2
- package/src/index.test.ts +120 -96
- package/src/index.ts +36 -24
- package/src/install/binary.ts +12 -5
- package/src/install/completions.ts +7 -3
- package/src/install/detect-installed.ts +29 -4
- package/src/install/gh-release-update.ts +31 -23
- package/src/install/index.ts +69 -19
- package/src/install/install.test.ts +31 -8
- package/src/install/mcp-codex.test.ts +57 -0
- package/src/install/mcp-codex.ts +125 -0
- package/src/install/mcp-config.ts +12 -5
- package/src/install/mcp-opencode.test.ts +98 -0
- package/src/install/mcp-opencode.ts +149 -0
- package/src/install/paths.ts +29 -3
- package/src/install/plan.ts +73 -6
- package/src/install/shell.ts +1 -4
- package/src/install/status.ts +12 -6
- package/src/install/uninstall.ts +38 -4
- package/src/install/update.test.ts +2 -2
- package/src/install/update.ts +3 -1
- package/src/invoke.ts +12 -9
- package/src/mcp/bundle.ts +36 -8
- package/src/mcp/env.ts +7 -13
- package/src/mcp/server.ts +12 -6
- package/src/mcp/tools.ts +83 -18
- package/src/mcp.ts +3 -3
- package/src/parse.ts +129 -27
- package/src/runtime.ts +22 -12
- package/src/schema.ts +11 -5
- package/src/skill/generate.ts +4 -4
- package/src/skill/install.ts +6 -2
- package/src/types.ts +24 -0
- package/src/validate.ts +75 -16
package/src/formats.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Named string format validation and parsing for CLI options.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CliValueFormat } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const DURATION_RE = /^\d+[hdms]?$/i;
|
|
8
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
|
|
10
|
+
/** Parses a duration string (e.g. 20m, 1h, 30s) into milliseconds. */
|
|
11
|
+
export function parseDurationMs(durationStr: string): number {
|
|
12
|
+
const match = durationStr.trim().match(/^(\d+)([hdms]?)$/i);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error("Invalid duration format. Use e.g. 30s, 20m, 1h, 2d");
|
|
15
|
+
}
|
|
16
|
+
const amountText = match[1];
|
|
17
|
+
if (!amountText) {
|
|
18
|
+
throw new Error("Invalid duration format. Use e.g. 30s, 20m, 1h, 2d");
|
|
19
|
+
}
|
|
20
|
+
const amount = Number.parseInt(amountText, 10);
|
|
21
|
+
const unit = (match[2] || "m").toLowerCase();
|
|
22
|
+
if (unit === "s") return amount * 1000;
|
|
23
|
+
if (unit === "m") return amount * 60 * 1000;
|
|
24
|
+
if (unit === "h") return amount * 60 * 60 * 1000;
|
|
25
|
+
if (unit === "d") return amount * 24 * 60 * 60 * 1000;
|
|
26
|
+
return amount * 60 * 1000;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateDuration(s: string): void {
|
|
30
|
+
if (!DURATION_RE.test(s.trim())) {
|
|
31
|
+
throw new Error("Invalid duration format. Use e.g. 30s, 20m, 1h, 2d");
|
|
32
|
+
}
|
|
33
|
+
parseDurationMs(s);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Splits a comma-separated string into trimmed non-empty tokens. */
|
|
37
|
+
export function parseCommaList(s: string): string[] {
|
|
38
|
+
return s
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((part) => part.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateCommaList(s: string): void {
|
|
45
|
+
if (parseCommaList(s).length === 0) {
|
|
46
|
+
throw new Error("Comma-separated list must contain at least one value");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns canonical YYYY-MM-DD after validation. */
|
|
51
|
+
export function parseDate(s: string): string {
|
|
52
|
+
const trimmed = s.trim();
|
|
53
|
+
if (!DATE_RE.test(trimmed)) {
|
|
54
|
+
throw new Error("Invalid date. Use YYYY-MM-DD");
|
|
55
|
+
}
|
|
56
|
+
const [y, m, d] = trimmed.split("-").map((part) => Number.parseInt(part, 10));
|
|
57
|
+
const year = y ?? 0;
|
|
58
|
+
const month = m ?? 0;
|
|
59
|
+
const day = d ?? 0;
|
|
60
|
+
const dt = new Date(Date.UTC(year, month - 1, day));
|
|
61
|
+
if (dt.getUTCFullYear() !== year || dt.getUTCMonth() !== month - 1 || dt.getUTCDate() !== day) {
|
|
62
|
+
throw new Error("Invalid date. Use YYYY-MM-DD");
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function validateDate(s: string): void {
|
|
68
|
+
parseDate(s);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
72
|
+
|
|
73
|
+
/** Returns normalized ISO 8601 UTC after validation. */
|
|
74
|
+
export function parseDateTime(s: string): string {
|
|
75
|
+
const trimmed = s.trim();
|
|
76
|
+
if (!DATE_TIME_RE.test(trimmed)) {
|
|
77
|
+
throw new Error("Invalid date-time. Use RFC 3339, e.g. 2026-06-22T15:00:00Z");
|
|
78
|
+
}
|
|
79
|
+
const ms = Date.parse(trimmed);
|
|
80
|
+
if (Number.isNaN(ms)) {
|
|
81
|
+
throw new Error("Invalid date-time. Use RFC 3339, e.g. 2026-06-22T15:00:00Z");
|
|
82
|
+
}
|
|
83
|
+
return new Date(ms).toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateDateTime(s: string): void {
|
|
87
|
+
parseDateTime(s);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validatePattern(s: string, pattern: string): void {
|
|
91
|
+
const re = new RegExp(pattern);
|
|
92
|
+
if (!re.test(s)) {
|
|
93
|
+
throw new Error(`Value does not match required pattern: ${pattern}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatValidationError(format: CliValueFormat, value: string): string {
|
|
98
|
+
switch (format) {
|
|
99
|
+
case CliValueFormat.Duration:
|
|
100
|
+
return `Invalid duration: ${value} (use e.g. 30s, 20m, 1h, 2d)`;
|
|
101
|
+
case CliValueFormat.CommaList:
|
|
102
|
+
return `Invalid comma-separated list: ${value}`;
|
|
103
|
+
case CliValueFormat.Date:
|
|
104
|
+
return `Invalid date: ${value} (use YYYY-MM-DD)`;
|
|
105
|
+
case CliValueFormat.DateTime:
|
|
106
|
+
return `Invalid date-time: ${value} (use RFC 3339, e.g. 2026-06-22T15:00:00Z)`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Validates a string value against format and/or pattern metadata. */
|
|
111
|
+
export function validateFormatValue(
|
|
112
|
+
value: string,
|
|
113
|
+
format?: CliValueFormat,
|
|
114
|
+
pattern?: string,
|
|
115
|
+
): void {
|
|
116
|
+
if (format !== undefined) {
|
|
117
|
+
switch (format) {
|
|
118
|
+
case CliValueFormat.Duration:
|
|
119
|
+
validateDuration(value);
|
|
120
|
+
return;
|
|
121
|
+
case CliValueFormat.CommaList:
|
|
122
|
+
validateCommaList(value);
|
|
123
|
+
return;
|
|
124
|
+
case CliValueFormat.Date:
|
|
125
|
+
validateDate(value);
|
|
126
|
+
return;
|
|
127
|
+
case CliValueFormat.DateTime:
|
|
128
|
+
validateDateTime(value);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (pattern !== undefined) {
|
|
133
|
+
validatePattern(value, pattern);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/headless.test.ts
CHANGED
|
@@ -22,28 +22,20 @@ test("shouldRunHeadless is true for MCP and json", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
|
|
25
|
-
expect(
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
expect(
|
|
29
|
-
|
|
30
|
-
)
|
|
25
|
+
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
|
|
26
|
+
false,
|
|
27
|
+
);
|
|
28
|
+
expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, ["a"], false, false)).toBe(
|
|
29
|
+
true,
|
|
30
|
+
);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
|
|
34
34
|
expect(
|
|
35
|
-
shouldRunHeadlessWithYes(
|
|
36
|
-
{ invocation: "cli" },
|
|
37
|
-
{ yes: true, hasRequiredArgs: true },
|
|
38
|
-
false,
|
|
39
|
-
),
|
|
35
|
+
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: true, hasRequiredArgs: true }, false),
|
|
40
36
|
).toBe(true);
|
|
41
37
|
expect(
|
|
42
|
-
shouldRunHeadlessWithYes(
|
|
43
|
-
{ invocation: "cli" },
|
|
44
|
-
{ yes: false, hasRequiredArgs: true },
|
|
45
|
-
false,
|
|
46
|
-
),
|
|
38
|
+
shouldRunHeadlessWithYes({ invocation: "cli" }, { yes: false, hasRequiredArgs: true }, false),
|
|
47
39
|
).toBe(false);
|
|
48
40
|
expect(
|
|
49
41
|
shouldRunHeadlessWithYes(
|
package/src/help.ts
CHANGED
|
@@ -7,8 +7,16 @@ It keeps help formatting shared across help and error paths so users see one con
|
|
|
7
7
|
style no matter how help is reached.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { CliNode, CliOption, CliOptionKind, CliPositional, CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
|
|
11
10
|
import { visibleOptions, visibleSubcommands } from "./hidden.ts";
|
|
11
|
+
import {
|
|
12
|
+
type CliNode,
|
|
13
|
+
type CliOption,
|
|
14
|
+
CliOptionKind,
|
|
15
|
+
type CliPositional,
|
|
16
|
+
type CliRouter,
|
|
17
|
+
isCliLeaf,
|
|
18
|
+
isCliRouter,
|
|
19
|
+
} from "./types.ts";
|
|
12
20
|
|
|
13
21
|
// ── ANSI Style Helpers ────────────────────────────────────────────────────────
|
|
14
22
|
|
|
@@ -52,10 +60,10 @@ const style = {
|
|
|
52
60
|
|
|
53
61
|
const kBoxTL = "\u256D"; // ╭
|
|
54
62
|
const kBoxTR = "\u256E"; // ╮
|
|
55
|
-
const kBoxV = "\u2502";
|
|
63
|
+
const kBoxV = "\u2502"; // │
|
|
56
64
|
const kBoxBL = "\u2570"; // ╰
|
|
57
65
|
const kBoxBR = "\u256F"; // ╯
|
|
58
|
-
const kBoxH = "\u2500";
|
|
66
|
+
const kBoxH = "\u2500"; // ─
|
|
59
67
|
|
|
60
68
|
// ── Terminal Detection ────────────────────────────────────────────────────────
|
|
61
69
|
|
|
@@ -119,7 +127,7 @@ function wrapParagraph(text: string, width: number): string[] {
|
|
|
119
127
|
continue;
|
|
120
128
|
}
|
|
121
129
|
if (cur.length + 1 + word.length <= available) {
|
|
122
|
-
cur +=
|
|
130
|
+
cur += ` ${word}`;
|
|
123
131
|
} else {
|
|
124
132
|
out.push(cur);
|
|
125
133
|
cur = word;
|
|
@@ -166,24 +174,24 @@ function optKindLabel(k: CliOptionKind, o?: CliOption): string {
|
|
|
166
174
|
return " <choice>";
|
|
167
175
|
}
|
|
168
176
|
if (choices.length <= 4) {
|
|
169
|
-
return
|
|
177
|
+
return ` <${choices.join("|")}>`;
|
|
170
178
|
}
|
|
171
|
-
return
|
|
179
|
+
return ` <${choices.slice(0, 3).join("|")}|…>`;
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
/** Formats a flag/value option for help tables: `--name`, optional short, optional kind hint. */
|
|
177
185
|
export function cliOptionLabel(o: CliOption, color: boolean): string {
|
|
178
|
-
let r =
|
|
179
|
-
if (o.shortName) r +=
|
|
186
|
+
let r = `--${o.name}${optKindLabel(o.kind, o)}`;
|
|
187
|
+
if (o.shortName) r += `, -${o.shortName}`;
|
|
180
188
|
if (!color) return r;
|
|
181
189
|
|
|
182
190
|
const sepIdx = r.indexOf(", ");
|
|
183
191
|
if (sepIdx === -1) return style.aquaBold(r);
|
|
184
192
|
const left = r.slice(0, sepIdx);
|
|
185
193
|
const right = r.slice(sepIdx + 2);
|
|
186
|
-
return style.aquaBold(left)
|
|
194
|
+
return `${style.aquaBold(left)} ${style.greenBright(right)}`;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
/** Placeholder in `notes` for the root program key (resolved in help, schema, and docs api). */
|
|
@@ -199,9 +207,9 @@ export function cliPositionalLabel(p: CliPositional, color: boolean): string {
|
|
|
199
207
|
const { argMin = 1, argMax = 1 } = p;
|
|
200
208
|
let r: string;
|
|
201
209
|
if (argMax === 1) {
|
|
202
|
-
r = argMin === 0 ?
|
|
210
|
+
r = argMin === 0 ? `[${p.name}]` : `<${p.name}>`;
|
|
203
211
|
} else {
|
|
204
|
-
r = argMin === 0 ?
|
|
212
|
+
r = argMin === 0 ? `[${p.name}...]` : `<${p.name}...>`;
|
|
205
213
|
}
|
|
206
214
|
if (!color) return r;
|
|
207
215
|
return style.aquaBold(r);
|
|
@@ -222,8 +230,8 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
|
|
|
222
230
|
if (lines.length === 0) return [];
|
|
223
231
|
|
|
224
232
|
const titleLead = color
|
|
225
|
-
? style.gray(kBoxH
|
|
226
|
-
: kBoxH
|
|
233
|
+
? style.gray(`${kBoxH} `) + style.grayBoldTitle(title) + style.gray(" ")
|
|
234
|
+
: `${kBoxH} ${title} `;
|
|
227
235
|
|
|
228
236
|
let contentWidth = visibleWidth(titleLead) + 1;
|
|
229
237
|
for (const line of lines) {
|
|
@@ -238,19 +246,25 @@ function renderTextBox(title: string, lines: string[], hw: number, color: boolea
|
|
|
238
246
|
const out: string[] = [];
|
|
239
247
|
out.push(
|
|
240
248
|
(color ? style.gray(kBoxTL) : kBoxTL) +
|
|
241
|
-
|
|
242
|
-
|
|
249
|
+
titleLead +
|
|
250
|
+
(color ? style.gray(repeatBoxH(headerFill) + kBoxTR) : repeatBoxH(headerFill) + kBoxTR),
|
|
243
251
|
);
|
|
244
252
|
|
|
245
253
|
for (const line of lines) {
|
|
246
254
|
const padded = padVisible(line, contentWidth);
|
|
247
255
|
out.push(
|
|
248
|
-
(color ? style.gray(kBoxV) : kBoxV) +
|
|
256
|
+
(color ? style.gray(kBoxV) : kBoxV) +
|
|
257
|
+
" " +
|
|
258
|
+
padded +
|
|
259
|
+
" " +
|
|
260
|
+
(color ? style.gray(kBoxV) : kBoxV),
|
|
249
261
|
);
|
|
250
262
|
}
|
|
251
263
|
|
|
252
264
|
out.push(
|
|
253
|
-
|
|
265
|
+
color
|
|
266
|
+
? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
|
|
267
|
+
: kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
|
|
254
268
|
);
|
|
255
269
|
|
|
256
270
|
return out;
|
|
@@ -265,7 +279,7 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
265
279
|
labelWidth = Math.max(labelWidth, visibleWidth(row.label));
|
|
266
280
|
}
|
|
267
281
|
|
|
268
|
-
const titleChunk = kBoxH
|
|
282
|
+
const titleChunk = `${kBoxH} ${title} `;
|
|
269
283
|
const minimumContentWidth = Math.max(visibleWidth(titleChunk) + 1, labelWidth + 2 + 18);
|
|
270
284
|
let contentWidth = Math.max(hw - 2, minimumContentWidth);
|
|
271
285
|
const descWidth = Math.max(1, contentWidth - labelWidth - 2);
|
|
@@ -274,20 +288,22 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
274
288
|
for (const row of rows) {
|
|
275
289
|
const wrapped = wrapText(row.description, descWidth);
|
|
276
290
|
const first =
|
|
277
|
-
row.label +
|
|
291
|
+
row.label +
|
|
292
|
+
spaces(labelWidth - visibleWidth(row.label)) +
|
|
293
|
+
" " +
|
|
278
294
|
(color ? style.white(wrapped[0]) : wrapped[0]);
|
|
279
295
|
bodyLines.push(first);
|
|
280
296
|
for (let idx = 1; idx < wrapped.length; idx++) {
|
|
281
297
|
const pad = color ? style.gray(spaces(labelWidth)) : spaces(labelWidth);
|
|
282
|
-
bodyLines.push(pad
|
|
298
|
+
bodyLines.push(`${pad} ${color ? style.white(wrapped[idx]) : wrapped[idx]}`);
|
|
283
299
|
}
|
|
284
300
|
}
|
|
285
301
|
|
|
286
302
|
let titleLead: string;
|
|
287
303
|
if (color) {
|
|
288
|
-
titleLead = style.gray(kBoxH
|
|
304
|
+
titleLead = style.gray(`${kBoxH} `) + style.grayBoldTitle(title) + style.gray(" ");
|
|
289
305
|
} else {
|
|
290
|
-
titleLead = kBoxH
|
|
306
|
+
titleLead = `${kBoxH} ${title} `;
|
|
291
307
|
}
|
|
292
308
|
|
|
293
309
|
contentWidth = Math.max(contentWidth, visibleWidth(titleLead) + 1);
|
|
@@ -302,19 +318,25 @@ function renderTableBox(title: string, rows: HelpRow[], hw: number, color: boole
|
|
|
302
318
|
const out: string[] = [];
|
|
303
319
|
out.push(
|
|
304
320
|
(color ? style.gray(kBoxTL) : kBoxTL) +
|
|
305
|
-
|
|
306
|
-
|
|
321
|
+
titleLead +
|
|
322
|
+
(color ? style.gray(repeatBoxH(headerFill) + kBoxTR) : repeatBoxH(headerFill) + kBoxTR),
|
|
307
323
|
);
|
|
308
324
|
|
|
309
325
|
for (const line of bodyLines) {
|
|
310
326
|
const padded = padVisible(line, contentWidth);
|
|
311
327
|
out.push(
|
|
312
|
-
(color ? style.gray(kBoxV) : kBoxV) +
|
|
328
|
+
(color ? style.gray(kBoxV) : kBoxV) +
|
|
329
|
+
" " +
|
|
330
|
+
padded +
|
|
331
|
+
" " +
|
|
332
|
+
(color ? style.gray(kBoxV) : kBoxV),
|
|
313
333
|
);
|
|
314
334
|
}
|
|
315
335
|
|
|
316
336
|
out.push(
|
|
317
|
-
|
|
337
|
+
color
|
|
338
|
+
? style.gray(kBoxBL + repeatBoxH(borderWidth) + kBoxBR)
|
|
339
|
+
: kBoxBL + repeatBoxH(borderWidth) + kBoxBR,
|
|
318
340
|
);
|
|
319
341
|
|
|
320
342
|
return out;
|
|
@@ -332,7 +354,7 @@ function usageLines(
|
|
|
332
354
|
): string[] {
|
|
333
355
|
let fullPath = appName;
|
|
334
356
|
for (const seg of helpPath) {
|
|
335
|
-
fullPath +=
|
|
357
|
+
fullPath += ` ${seg}`;
|
|
336
358
|
}
|
|
337
359
|
const usageOpts = color ? style.aquaBold("[OPTIONS]") : "[OPTIONS]";
|
|
338
360
|
const usageCmd = color ? style.aquaBold("COMMAND") : "COMMAND";
|
|
@@ -341,15 +363,15 @@ function usageLines(
|
|
|
341
363
|
const out: string[] = [];
|
|
342
364
|
if (helpPath.length === 0) {
|
|
343
365
|
if (hasCommands) {
|
|
344
|
-
out.push(fullPath
|
|
366
|
+
out.push(`${fullPath} ${usageOpts} ${usageCmd} ${usageArgs}`);
|
|
345
367
|
} else {
|
|
346
|
-
out.push(fullPath
|
|
368
|
+
out.push(`${fullPath} ${usageOpts}`);
|
|
347
369
|
}
|
|
348
370
|
return out;
|
|
349
371
|
}
|
|
350
|
-
out.push(fullPath
|
|
372
|
+
out.push(`${fullPath} ${usageOpts}${hasArgs ? ` ${usageArgs}` : ""}`);
|
|
351
373
|
if (hasCommands) {
|
|
352
|
-
out.push(fullPath
|
|
374
|
+
out.push(`${fullPath} ${usageCmd} ${usageArgs}`);
|
|
353
375
|
}
|
|
354
376
|
return out;
|
|
355
377
|
}
|
|
@@ -357,12 +379,10 @@ function usageLines(
|
|
|
357
379
|
/** Table rows for named options, including synthetic built-in rows. */
|
|
358
380
|
function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
|
|
359
381
|
const rows: HelpRow[] = [];
|
|
360
|
-
const helpLabel = color
|
|
361
|
-
? style.aquaBold("--help, ") + style.greenBright("-h")
|
|
362
|
-
: "--help, -h";
|
|
382
|
+
const helpLabel = color ? style.aquaBold("--help, ") + style.greenBright("-h") : "--help, -h";
|
|
363
383
|
rows.push({ label: helpLabel, description: "Show help for this command." });
|
|
364
384
|
for (const o of defs) {
|
|
365
|
-
const desc = o.required ?
|
|
385
|
+
const desc = o.required ? `(required) ${o.description}` : o.description;
|
|
366
386
|
rows.push({ label: cliOptionLabel(o, color), description: desc });
|
|
367
387
|
}
|
|
368
388
|
return rows;
|
|
@@ -392,7 +412,7 @@ function appendNotesBox(
|
|
|
392
412
|
if ((notes ?? "").length === 0) {
|
|
393
413
|
return;
|
|
394
414
|
}
|
|
395
|
-
const resolved = cliResolveNotes(notes
|
|
415
|
+
const resolved = cliResolveNotes(notes ?? "", appKey);
|
|
396
416
|
lines.push("");
|
|
397
417
|
lines.push(renderTextBox("Notes", wrapText(resolved, hw - 4), hw, color).join("\n"));
|
|
398
418
|
}
|
|
@@ -401,7 +421,7 @@ function appendNotesBox(
|
|
|
401
421
|
* Renders full help for the app root or a nested command, following `helpPath` from the root key.
|
|
402
422
|
* `useStderr` is reserved for call-site consistency; width and color use stdout TTY.
|
|
403
423
|
*/
|
|
404
|
-
export function cliHelpRender(schema: CliRouter, helpPath: string[],
|
|
424
|
+
export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr: boolean): string {
|
|
405
425
|
const hw = getHelpWidth();
|
|
406
426
|
const color = isStdoutTTY();
|
|
407
427
|
|
|
@@ -421,7 +441,12 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
421
441
|
).join("\n"),
|
|
422
442
|
);
|
|
423
443
|
|
|
424
|
-
const optBox = renderTableBox(
|
|
444
|
+
const optBox = renderTableBox(
|
|
445
|
+
"Options",
|
|
446
|
+
rowsForOptions(visibleOptions(schema.options), color),
|
|
447
|
+
hw,
|
|
448
|
+
color,
|
|
449
|
+
);
|
|
425
450
|
if (optBox.length > 0) {
|
|
426
451
|
lines.push("");
|
|
427
452
|
lines.push(optBox.join("\n"));
|
|
@@ -433,7 +458,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
433
458
|
);
|
|
434
459
|
}
|
|
435
460
|
appendNotesBox(lines, schema.notes, schema.key, hw, color);
|
|
436
|
-
return lines.join("\n")
|
|
461
|
+
return `${lines.join("\n")}\n\n`;
|
|
437
462
|
}
|
|
438
463
|
|
|
439
464
|
let layer = schema.commands ?? [];
|
|
@@ -441,13 +466,13 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
441
466
|
for (const seg of helpPath) {
|
|
442
467
|
const ch = layer.find((c: CliNode) => c.key === seg);
|
|
443
468
|
if (!ch) {
|
|
444
|
-
return
|
|
469
|
+
return `${color ? style.red("Unknown help path.") : "Unknown help path."}\n`;
|
|
445
470
|
}
|
|
446
471
|
node = ch;
|
|
447
472
|
layer = isCliRouter(ch) ? ch.commands : [];
|
|
448
473
|
}
|
|
449
474
|
if (!node) {
|
|
450
|
-
return
|
|
475
|
+
return `${color ? style.red("Unknown help path.") : "Unknown help path."}\n`;
|
|
451
476
|
}
|
|
452
477
|
|
|
453
478
|
const lines: string[] = [];
|
|
@@ -471,7 +496,12 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
471
496
|
).join("\n"),
|
|
472
497
|
);
|
|
473
498
|
|
|
474
|
-
const optBox = renderTableBox(
|
|
499
|
+
const optBox = renderTableBox(
|
|
500
|
+
"Options",
|
|
501
|
+
rowsForOptions(visibleOptions(node.options), color),
|
|
502
|
+
hw,
|
|
503
|
+
color,
|
|
504
|
+
);
|
|
475
505
|
if (optBox.length > 0) {
|
|
476
506
|
lines.push("");
|
|
477
507
|
lines.push(optBox.join("\n"));
|
|
@@ -499,5 +529,5 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
|
|
|
499
529
|
appendNotesBox(lines, node.notes, schema.key, hw, color);
|
|
500
530
|
}
|
|
501
531
|
|
|
502
|
-
return lines.join("\n")
|
|
532
|
+
return `${lines.join("\n")}\n\n`;
|
|
503
533
|
}
|
package/src/hidden-mcpb.test.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
6
5
|
import { exportPresentationBuiltins } from "./builtins/export.ts";
|
|
6
|
+
import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
|
|
7
7
|
import { cliHelpRender } from "./help.ts";
|
|
8
|
-
import { cliSchemaExport } from "./schema.ts";
|
|
9
|
-
import { collectMcpTools } from "./mcp/tools.ts";
|
|
10
8
|
import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
9
|
+
import { collectMcpTools } from "./mcp/tools.ts";
|
|
10
|
+
import { cliSchemaExport } from "./schema.ts";
|
|
11
11
|
import { CliOptionKind, type CliProgram } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
const hiddenFixture: CliProgram = {
|
|
@@ -116,7 +116,8 @@ describe("mcp bundle", () => {
|
|
|
116
116
|
expect(manifest.manifest_version).toBe("0.3");
|
|
117
117
|
expect((manifest.server as { type: string }).type).toBe("binary");
|
|
118
118
|
expect((manifest.server as { entry_point: string }).entry_point).toBe("myapp");
|
|
119
|
-
const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } })
|
|
119
|
+
const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } })
|
|
120
|
+
.mcp_config;
|
|
120
121
|
expect(mcpConfig.command).toBe("${__dirname}/myapp");
|
|
121
122
|
expect(mcpConfig.args).toEqual(["mcp"]);
|
|
122
123
|
expect((manifest.compatibility as { platforms: string[] }).platforms).toEqual(["darwin"]);
|
package/src/hidden.ts
CHANGED
|
@@ -3,8 +3,8 @@ Filters hidden commands and options from presentation surfaces (help, schema, co
|
|
|
3
3
|
Parsing still uses the full tree via cliParseRoot.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { CliNode, CliOption
|
|
7
|
-
import {
|
|
6
|
+
import type { CliNode, CliOption } from "./types.ts";
|
|
7
|
+
import { isCliRouter } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
/** Options visible in help, schema, completions, and MCP tool schemas. */
|
|
10
10
|
export function visibleOptions(options: CliOption[] | undefined): CliOption[] {
|