argsbarg 3.3.13 → 3.4.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.
@@ -1,5 +1,6 @@
1
1
  import type { CliCapabilities } from "../capabilities.ts";
2
2
  import { resolveCapabilities } from "../capabilities.ts";
3
+ import { presentationNode, visibleOptions } from "../hidden.ts";
3
4
  import type { CliLeaf, CliNode, CliProgram, CliRouter } from "../types.ts";
4
5
  import { isCliLeaf, isCliRouter } from "../types.ts";
5
6
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
@@ -8,10 +9,10 @@ import { cliBuiltinMcpCommand } from "./mcp.ts";
8
9
  import { cliBuiltinVersionCommand } from "./version.ts";
9
10
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
10
11
 
11
- /** Built-in command nodes injected for help, schema, and completions. */
12
- export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
12
+ /** All built-in command nodes for argv parsing (includes hidden builtins). */
13
+ export function parseBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
13
14
  const builtins: CliNode[] = [
14
- cliBuiltinCompletionGroup(program.key),
15
+ cliBuiltinCompletionGroup(program),
15
16
  cliBuiltinVersionCommand(),
16
17
  ];
17
18
  if (caps.install) {
@@ -22,14 +23,49 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
22
23
  builtins.push(docsGroup);
23
24
  }
24
25
  if (caps.mcp) {
25
- builtins.push(cliBuiltinMcpCommand());
26
+ builtins.push(cliBuiltinMcpCommand(program));
26
27
  }
27
28
  return builtins;
28
29
  }
29
30
 
31
+ /** Built-in subtrees visible in help, schema, and completions (hidden builtins omitted). */
32
+ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
33
+ return parseBuiltins(program, caps).filter((b) => !b.hidden);
34
+ }
35
+
36
+ /**
37
+ * Full command tree for argv parsing, including hidden commands and builtins.
38
+ * Routing programs merge user commands with builtins; leaf programs wrap builtins only.
39
+ */
40
+ export function cliParseRoot(program: CliProgram): CliRouter {
41
+ const caps = resolveCapabilities(program);
42
+ const builtins = parseBuiltins(program, caps);
43
+
44
+ if (isCliLeaf(program)) {
45
+ return {
46
+ key: program.key,
47
+ description: program.description,
48
+ notes: program.notes,
49
+ options: program.options,
50
+ commands: builtins,
51
+ };
52
+ }
53
+
54
+ return {
55
+ key: program.key,
56
+ description: program.description,
57
+ notes: program.notes,
58
+ options: program.options,
59
+ fallbackCommand: program.fallbackCommand,
60
+ fallbackMode: program.fallbackMode,
61
+ commands: [...program.commands, ...builtins],
62
+ };
63
+ }
64
+
30
65
  /**
31
66
  * Returns a schema suitable for help display, including capability-built-in subtrees.
32
- * Routing programs get builtins merged; leaf programs are wrapped as a tiny router.
67
+ * Hidden commands and options are omitted. Routing programs get builtins merged;
68
+ * leaf programs are wrapped as a tiny router.
33
69
  */
34
70
  export function cliPresentationRoot(program: CliProgram): CliRouter {
35
71
  const caps = resolveCapabilities(program);
@@ -41,19 +77,23 @@ export function cliPresentationRoot(program: CliProgram): CliRouter {
41
77
  key: program.key,
42
78
  description: program.description,
43
79
  notes,
44
- options: program.options,
80
+ options: visibleOptions(program.options),
45
81
  commands: builtins,
46
82
  };
47
83
  }
48
84
 
85
+ const userCommands = program.commands
86
+ .map((ch) => presentationNode(ch))
87
+ .filter((ch): ch is CliNode => ch !== null);
88
+
49
89
  return {
50
90
  key: program.key,
51
91
  description: program.description,
52
92
  notes,
53
- options: program.options,
93
+ options: visibleOptions(program.options),
54
94
  fallbackCommand: program.fallbackCommand,
55
95
  fallbackMode: program.fallbackMode,
56
- commands: [...program.commands, ...builtins],
96
+ commands: [...userCommands, ...builtins],
57
97
  };
58
98
  }
59
99
 
@@ -64,8 +104,7 @@ export function presentationRootNotes(program: CliProgram, caps: CliCapabilities
64
104
  parts.push(program.notes!.trim());
65
105
  }
66
106
  if (caps.docs) {
67
- const cmd = `${program.key} docs skill`;
68
- parts.push(`Agents: run \`${cmd}\` to learn how to use this app`);
107
+ parts.push(`For AI agents: \`${program.key} docs skill\`.`);
69
108
  }
70
109
  if (parts.length === 0) {
71
110
  return undefined;
@@ -75,4 +114,3 @@ export function presentationRootNotes(program: CliProgram, caps: CliCapabilities
75
114
 
76
115
  /** Presentation tree may include builtin leaf stubs. */
77
116
  export type CliPresentationNode = CliNode | CliLeaf;
78
-
@@ -105,3 +105,33 @@ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
105
105
  const md = generateApiGuide(fixture);
106
106
  expect(md).toContain("Invoke `myapp run`.");
107
107
  });
108
+
109
+ test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
110
+ const fixture: CliProgram = {
111
+ key: "myapp",
112
+ version: "1.0.0",
113
+ description: "Demo app.",
114
+ commands: [
115
+ {
116
+ key: "run",
117
+ description: "Run.",
118
+ outputSchema: {
119
+ type: "object",
120
+ properties: { id: { type: "string" } },
121
+ required: ["id"],
122
+ },
123
+ handler: () => {},
124
+ },
125
+ ],
126
+ };
127
+ const schema = cliSchemaExport(fixture);
128
+ expect(schema.commands![0]!.outputSchema).toEqual({
129
+ type: "object",
130
+ properties: { id: { type: "string" } },
131
+ required: ["id"],
132
+ });
133
+ const md = generateApiGuide(fixture);
134
+ expect(md).toContain("#### Output");
135
+ expect(md).toContain('"id"');
136
+ expect(md).toContain('"type": "string"');
137
+ });
@@ -52,6 +52,20 @@ function formatNotesBlockquote(notes: string, appKey: string): string {
52
52
  .join("\n");
53
53
  }
54
54
 
55
+ /** Markdown section for leaf outputSchema (docs api / skill reference). */
56
+ function formatOutputSchemaSection(schema: Record<string, unknown>): string[] {
57
+ return [
58
+ "#### Output",
59
+ "",
60
+ "JSON Schema for structured stdout (typically with `--json`, or via MCP when the handler emits JSON):",
61
+ "",
62
+ "```json",
63
+ JSON.stringify(schema, null, 2),
64
+ "```",
65
+ "",
66
+ ];
67
+ }
68
+
55
69
  /** Fallback routing note when present on a router node. */
56
70
  function fallbackLine(node: CliSchemaExport): string | null {
57
71
  if (node.fallbackCommand === undefined) {
@@ -103,6 +117,10 @@ function renderCommandNode(
103
117
  lines.push("");
104
118
  }
105
119
 
120
+ if (node.outputSchema !== undefined) {
121
+ lines.push(...formatOutputSchemaSection(node.outputSchema));
122
+ }
123
+
106
124
  const children = node.commands ?? [];
107
125
  if (children.length > 0) {
108
126
  lines.push("#### Subcommands", "");
@@ -36,6 +36,11 @@ function docsLeaf(program: CliProgram, key: string, description: string): CliLea
36
36
  };
37
37
  }
38
38
 
39
+ /** Help notes for the `docs` router. */
40
+ function docsRouterNotes(): string {
41
+ return "Topics print to stdout. Add --save to write files under ./docs/.";
42
+ }
43
+
39
44
  /** Built-in `docs` router with bundled topic subcommands. */
40
45
  export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
41
46
  const docs = program.docs!;
@@ -54,26 +59,14 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
54
59
 
55
60
  leaves.push(
56
61
  docsLeaf(program, "schema", "Print the full command tree as JSON."),
57
- docsLeaf(program, "api", "Print the command tree as markdown."),
58
- {
59
- key: "skill",
60
- description: "Print generated SKILL.md (compact command index).",
61
- notes: [
62
- "Prefer `{argsbarg:program} install --skill --yes` for agents: it persists an optimized skill bundle",
63
- "(`SKILL.md` index + `reference.md` full API) to your skill directory.",
64
- "`docs skill` prints the index only; use `docs api` or installed `reference.md` for full detail.",
65
- ].join(" "),
66
- options: [DOCS_SAVE_OPTION],
67
- mcpTool: { enabled: false },
68
- handler: (ctx) => {
69
- runDocsTopic(program, "skill", ctx);
70
- },
71
- },
62
+ docsLeaf(program, "api", "Print the full command reference as markdown."),
63
+ docsLeaf(program, "skill", "Print a reference agent SKILL, use `install --skill` for optimized."),
72
64
  );
73
65
 
74
66
  return {
75
67
  key: "docs",
76
68
  description: docs.description ?? DOCS_ROUTER_DESCRIPTION,
69
+ notes: docsRouterNotes(),
77
70
  options: [DOCS_SAVE_OPTION],
78
71
  fallbackCommand: docsEffectiveDefaultTopic(docs),
79
72
  fallbackMode: CliFallbackMode.MissingOnly,
@@ -153,7 +153,7 @@ test("docs skill prints Cursor SKILL.md", async () => {
153
153
  expect(result.stdout).toContain("---");
154
154
  expect(result.stdout).toContain("name: myapp");
155
155
  expect(result.stdout).toContain("## Commands");
156
- expect(result.stdout).toContain("read `reference.md`");
156
+ expect(result.stdout).toContain("For full detail, open `reference.md`");
157
157
  expect(result.stdout).not.toContain("#### Options");
158
158
  expect(result.stdout).not.toContain("mcp.json");
159
159
  });
@@ -164,9 +164,11 @@ test("docs skill help recommends install --skill", async () => {
164
164
  expect(docsNode && "commands" in docsNode).toBe(true);
165
165
  if (docsNode && "commands" in docsNode) {
166
166
  const skill = docsNode.commands.find((c) => c.key === "skill");
167
- expect(skill?.description).toContain("compact command index");
168
- expect(skill?.notes).toContain("install --skill --yes");
169
- expect(skill?.notes).toContain("reference.md");
167
+ expect(skill?.description).toContain("reference agent SKILL");
168
+ expect(skill?.description).toContain("install --skill");
169
+ expect(skill?.notes).toBeUndefined();
170
+ expect(docsNode.notes).toContain("--save");
171
+ expect(docsNode.notes).not.toContain("install --skill");
170
172
  }
171
173
  });
172
174
 
package/src/help.ts CHANGED
@@ -8,6 +8,7 @@ style no matter how help is reached.
8
8
  */
9
9
 
10
10
  import { CliNode, CliOption, CliOptionKind, CliPositional, CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
11
+ import { visibleOptions, visibleSubcommands } from "./hidden.ts";
11
12
 
12
13
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
13
14
 
@@ -372,9 +373,9 @@ function rowsForPositionals(defs: CliPositional[], color: boolean): HelpRow[] {
372
373
  return defs.map((p) => ({ label: cliPositionalLabel(p, color), description: p.description }));
373
374
  }
374
375
 
375
- /** Table rows for subcommands, sorted by key. */
376
+ /** Table rows for subcommands, sorted by key (hidden commands omitted). */
376
377
  function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
377
- return cmds
378
+ return visibleSubcommands(cmds)
378
379
  .sort((a, b) => a.key.localeCompare(b.key))
379
380
  .map((c) => ({ label: c.key, description: c.description }));
380
381
  }
@@ -420,7 +421,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
420
421
  ).join("\n"),
421
422
  );
422
423
 
423
- const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color), hw, color);
424
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(schema.options), color), hw, color);
424
425
  if (optBox.length > 0) {
425
426
  lines.push("");
426
427
  lines.push(optBox.join("\n"));
@@ -470,7 +471,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
470
471
  ).join("\n"),
471
472
  );
472
473
 
473
- const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color), hw, color);
474
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
474
475
  if (optBox.length > 0) {
475
476
  lines.push("");
476
477
  lines.push(optBox.join("\n"));
@@ -0,0 +1,154 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { cliPresentationRoot, cliParseRoot } from "./builtins/presentation.ts";
6
+ import { exportPresentationBuiltins } from "./builtins/export.ts";
7
+ import { cliHelpRender } from "./help.ts";
8
+ import { cliSchemaExport } from "./schema.ts";
9
+ import { collectMcpTools } from "./mcp/tools.ts";
10
+ import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
11
+ import { CliOptionKind, type CliProgram } from "./types.ts";
12
+
13
+ const hiddenFixture: CliProgram = {
14
+ key: "myapp",
15
+ version: "1.0.0",
16
+ description: "Hidden demo.",
17
+ mcpServer: { enabled: true },
18
+ commands: [
19
+ {
20
+ key: "public",
21
+ description: "Visible command.",
22
+ handler: () => {},
23
+ },
24
+ {
25
+ key: "secret",
26
+ hidden: true,
27
+ description: "Hidden command.",
28
+ handler: () => {},
29
+ },
30
+ {
31
+ key: "flags",
32
+ description: "Command with hidden option.",
33
+ options: [
34
+ {
35
+ name: "visible",
36
+ description: "Shown in help.",
37
+ kind: CliOptionKind.Presence,
38
+ },
39
+ {
40
+ name: "secret-flag",
41
+ hidden: true,
42
+ description: "Hidden option.",
43
+ kind: CliOptionKind.Presence,
44
+ },
45
+ ],
46
+ handler: () => {},
47
+ },
48
+ ],
49
+ };
50
+
51
+ describe("hidden commands and options", () => {
52
+ test("parse root includes hidden commands", () => {
53
+ const parse = cliParseRoot(hiddenFixture);
54
+ expect(parse.commands?.map((c) => c.key)).toContain("secret");
55
+ });
56
+
57
+ test("presentation root omits hidden commands", () => {
58
+ const presentation = cliPresentationRoot(hiddenFixture);
59
+ const keys = presentation.commands?.map((c) => c.key) ?? [];
60
+ expect(keys).toContain("public");
61
+ expect(keys).not.toContain("secret");
62
+ });
63
+
64
+ test("root help omits hidden commands", () => {
65
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), [], false);
66
+ expect(help).toContain("public");
67
+ expect(help).not.toContain("secret");
68
+ });
69
+
70
+ test("hidden command -h still works", () => {
71
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["secret"], false);
72
+ expect(help).toContain("Hidden command.");
73
+ });
74
+
75
+ test("help omits hidden options", () => {
76
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["flags"], false);
77
+ expect(help).toContain("--visible");
78
+ expect(help).not.toContain("secret-flag");
79
+ });
80
+
81
+ test("schema export omits hidden nodes and options", () => {
82
+ const schema = cliSchemaExport(hiddenFixture);
83
+ const keys = schema.commands?.map((c) => c.key) ?? [];
84
+ expect(keys).toContain("public");
85
+ expect(keys).not.toContain("secret");
86
+ const flags = schema.commands?.find((c) => c.key === "flags");
87
+ expect(flags?.options?.map((o) => o.name)).toEqual(["visible"]);
88
+ });
89
+
90
+ test("MCP tools omit hidden commands", () => {
91
+ const tools = collectMcpTools(hiddenFixture);
92
+ expect(tools.map((t) => t.name)).toEqual(["public", "flags"]);
93
+ });
94
+ });
95
+
96
+ describe("mcp router", () => {
97
+ test("presentation exposes mcp bundle but not hidden serve", () => {
98
+ const builtins = exportPresentationBuiltins(hiddenFixture);
99
+ const mcp = builtins.find((b) => b.key === "mcp");
100
+ expect(mcp).toBeDefined();
101
+ expect(mcp?.commands?.map((c) => c.key)).toEqual(["bundle"]);
102
+ expect(mcp?.fallbackCommand).toBe("serve");
103
+ });
104
+
105
+ test("mcp help lists bundle", () => {
106
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["mcp"], false);
107
+ expect(help).toContain("bundle");
108
+ expect(help).not.toMatch(/│ serve\s/);
109
+ });
110
+ });
111
+
112
+ describe("mcp bundle", () => {
113
+ test("generateMcpManifest uses mcpServerId and binary entry", () => {
114
+ const manifest = generateMcpManifest(hiddenFixture, "myapp");
115
+ expect(manifest.name).toBe("myapp");
116
+ expect(manifest.manifest_version).toBe("0.3");
117
+ expect((manifest.server as { type: string }).type).toBe("binary");
118
+ expect((manifest.server as { entry_point: string }).entry_point).toBe("myapp");
119
+ const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } }).mcp_config;
120
+ expect(mcpConfig.command).toBe("${__dirname}/myapp");
121
+ expect(mcpConfig.args).toEqual(["mcp"]);
122
+ expect((manifest.compatibility as { platforms: string[] }).platforms).toEqual(["darwin"]);
123
+ });
124
+
125
+ test("defaultMcpBundlePaths", () => {
126
+ const cwd = "/tmp/work";
127
+ const paths = defaultMcpBundlePaths(hiddenFixture, cwd);
128
+ expect(paths.binaryPath).toBe(join(cwd, "dist", "myapp"));
129
+ expect(paths.outPath).toBe(join(cwd, "dist", "myapp.mcpb"));
130
+ });
131
+
132
+ test("packMcpBundle writes zip with manifest and binary on darwin", () => {
133
+ if (process.platform !== "darwin") {
134
+ return;
135
+ }
136
+ const work = mkdtempSync(join(tmpdir(), "mcpb-test-"));
137
+ try {
138
+ const dist = join(work, "dist");
139
+ mkdirSync(dist, { recursive: true });
140
+ const binaryPath = join(dist, "myapp");
141
+ writeFileSync(binaryPath, "#!/bin/sh\necho hi\n", { mode: 0o755 });
142
+
143
+ const outPath = packMcpBundle(hiddenFixture, { cwd: work });
144
+ expect(outPath).toBe(join(dist, "myapp.mcpb"));
145
+
146
+ const zip = readFileSync(outPath);
147
+ expect(zip.length).toBeGreaterThan(0);
148
+ expect(zip.indexOf(Buffer.from("manifest.json"))).toBeGreaterThanOrEqual(0);
149
+ expect(zip.indexOf(Buffer.from("myapp"))).toBeGreaterThanOrEqual(0);
150
+ } finally {
151
+ rmSync(work, { recursive: true, force: true });
152
+ }
153
+ });
154
+ });
package/src/hidden.ts ADDED
@@ -0,0 +1,32 @@
1
+ /*
2
+ Filters hidden commands and options from presentation surfaces (help, schema, completions).
3
+ Parsing still uses the full tree via cliParseRoot.
4
+ */
5
+
6
+ import type { CliNode, CliOption, CliRouter } from "./types.ts";
7
+ import { isCliLeaf, isCliRouter } from "./types.ts";
8
+
9
+ /** Options visible in help, schema, completions, and MCP tool schemas. */
10
+ export function visibleOptions(options: CliOption[] | undefined): CliOption[] {
11
+ return (options ?? []).filter((o) => !o.hidden);
12
+ }
13
+
14
+ /** Strips hidden commands and options from one node for presentation export. */
15
+ export function presentationNode(node: CliNode): CliNode | null {
16
+ if (node.hidden) {
17
+ return null;
18
+ }
19
+ const options = visibleOptions(node.options);
20
+ if (isCliRouter(node)) {
21
+ const commands = node.commands
22
+ .map((ch) => presentationNode(ch))
23
+ .filter((ch): ch is CliNode => ch !== null);
24
+ return { ...node, options, commands };
25
+ }
26
+ return { ...node, options };
27
+ }
28
+
29
+ /** Subcommands visible in help listings (parent may be any node in the parse tree). */
30
+ export function visibleSubcommands(cmds: CliNode[]): CliNode[] {
31
+ return cmds.filter((c) => !c.hidden);
32
+ }