argsbarg 2.1.1 → 3.1.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.
@@ -10,23 +10,28 @@ export interface CliCapabilities {
10
10
  completion: true;
11
11
  mcp: boolean;
12
12
  install: boolean;
13
+ docs: boolean;
13
14
  }
14
15
 
15
16
  /** Resolves which capabilities are enabled for a program. */
16
17
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
17
18
  return {
18
19
  completion: true,
19
- mcp: program.mcpServer !== undefined,
20
+ mcp: program.mcpServer?.enabled === true,
20
21
  install: program.install?.enabled !== false,
22
+ docs: program.docs?.enabled === true,
21
23
  };
22
24
  }
23
25
 
24
26
  /** Reserved top-level command names for the given capabilities. */
25
27
  export function reservedCommandNames(caps: CliCapabilities): string[] {
26
- const names = ["completion"];
28
+ const names = ["completion", "version"];
27
29
  if (caps.install) {
28
30
  names.push("install");
29
31
  }
32
+ if (caps.docs) {
33
+ names.push("docs");
34
+ }
30
35
  if (caps.mcp) {
31
36
  names.push("mcp");
32
37
  }
@@ -0,0 +1,58 @@
1
+ import { CliFallbackMode, type CliLeaf, type CliProgram, type CliRouter } from "../types.ts";
2
+ import {
3
+ DOCS_ROUTER_DESCRIPTION,
4
+ docsEffectiveDefaultTopic,
5
+ docsEnabled,
6
+ docsIncludesMcpTopic,
7
+ docsTopicDescription,
8
+ docsUserTopicKeys,
9
+ printDocsTopic,
10
+ } from "./resolve.ts";
11
+
12
+ function docsLeaf(program: CliProgram, key: string, description: string): CliLeaf {
13
+ return {
14
+ key,
15
+ description,
16
+ mcpTool: { enabled: false },
17
+ handler: () => {
18
+ printDocsTopic(program, key);
19
+ },
20
+ };
21
+ }
22
+
23
+ /** Built-in `docs` router with bundled topic subcommands. */
24
+ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
25
+ const docs = program.docs!;
26
+ const leaves: CliLeaf[] = [];
27
+
28
+ for (const key of docsUserTopicKeys(docs)) {
29
+ const topic = docs.topics[key]!;
30
+ leaves.push(docsLeaf(program, key, docsTopicDescription(key, topic.description)));
31
+ }
32
+
33
+ if (docsIncludesMcpTopic(program)) {
34
+ leaves.push(
35
+ docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."),
36
+ );
37
+ }
38
+
39
+ leaves.push(
40
+ docsLeaf(program, "all", "Print all bundled documentation combined."),
41
+ );
42
+
43
+ return {
44
+ key: "docs",
45
+ description: docs.description ?? DOCS_ROUTER_DESCRIPTION,
46
+ fallbackCommand: docsEffectiveDefaultTopic(docs),
47
+ fallbackMode: CliFallbackMode.MissingOnly,
48
+ commands: leaves,
49
+ };
50
+ }
51
+
52
+ /** Returns the docs built-in when enabled. */
53
+ export function cliBuiltinDocsGroupIfEnabled(program: CliProgram): CliRouter | null {
54
+ if (!docsEnabled(program)) {
55
+ return null;
56
+ }
57
+ return cliBuiltinDocsGroup(program);
58
+ }
@@ -0,0 +1,121 @@
1
+ import { expect, test } from "bun:test";
2
+ import { cliPresentationRoot } from "../builtins/presentation.ts";
3
+ import { completionBashScript } from "../completion.ts";
4
+ import { cliInvoke } from "../index.ts";
5
+ import type { CliProgram } from "../types.ts";
6
+ import { cliValidateProgram } from "../validate.ts";
7
+ import { combineAllDocs, docsEffectiveDefaultTopic } from "./resolve.ts";
8
+ import { generateMcpGuide } from "./mcp-guide.ts";
9
+
10
+ function docsFixture(mcp = true): CliProgram {
11
+ return {
12
+ key: "myapp",
13
+ version: "1.0.0",
14
+ description: "Demo app.",
15
+ mcpServer: mcp ? { enabled: true } : undefined,
16
+ docs: {
17
+ enabled: true,
18
+ topics: {
19
+ readme: { text: "# Hello README\n" },
20
+ arch: { text: "# Architecture\n", description: "Contributor notes." },
21
+ },
22
+ },
23
+ commands: [
24
+ {
25
+ key: "run",
26
+ description: "Run something.",
27
+ handler: () => {},
28
+ },
29
+ ],
30
+ };
31
+ }
32
+
33
+ test("docs reserved when enabled", () => {
34
+ const root: CliProgram = {
35
+ ...docsFixture(),
36
+ commands: [
37
+ {
38
+ key: "docs",
39
+ description: "conflict",
40
+ handler: () => {},
41
+ },
42
+ ],
43
+ };
44
+ expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: docs/);
45
+ });
46
+
47
+ test("docs rejects reserved topic keys", () => {
48
+ const root = docsFixture();
49
+ root.docs!.topics.mcp = { text: "nope" };
50
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
51
+ });
52
+
53
+ test("docsEffectiveDefaultTopic uses first topic key", () => {
54
+ expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
55
+ });
56
+
57
+ test("bare docs prints first topic via cliInvoke", async () => {
58
+ const result = await cliInvoke(docsFixture(), ["docs"]);
59
+ expect(result.exitCode).toBe(0);
60
+ expect(result.stdout).toContain("Hello README");
61
+ });
62
+
63
+ test("docs readme prints bundled text", async () => {
64
+ const result = await cliInvoke(docsFixture(), ["docs", "readme"]);
65
+ expect(result.exitCode).toBe(0);
66
+ expect(result.stdout).toContain("Hello README");
67
+ });
68
+
69
+ test("docs defaultTopic override", async () => {
70
+ const root = docsFixture();
71
+ root.docs!.defaultTopic = "arch";
72
+ const result = await cliInvoke(root, ["docs"]);
73
+ expect(result.stdout).toContain("Architecture");
74
+ });
75
+
76
+ test("docs mcp when MCP enabled", async () => {
77
+ const result = await cliInvoke(docsFixture(true), ["docs", "mcp"]);
78
+ expect(result.exitCode).toBe(0);
79
+ expect(result.stdout).toContain("MCP server (myapp)");
80
+ expect(result.stdout).toContain("myapp mcp");
81
+ });
82
+
83
+ test("docs mcp absent from router when MCP disabled", async () => {
84
+ const root = docsFixture(false);
85
+ const presentation = cliPresentationRoot(root);
86
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
87
+ expect(docsNode && "commands" in docsNode).toBe(true);
88
+ if (docsNode && "commands" in docsNode) {
89
+ expect(docsNode.commands.some((c) => c.key === "mcp")).toBe(false);
90
+ }
91
+ const result = await cliInvoke(root, ["docs", "mcp"]);
92
+ expect(result.exitCode).not.toBe(0);
93
+ });
94
+
95
+ test("docs all concatenates user topics and mcp", () => {
96
+ const program = docsFixture(true);
97
+ const combined = combineAllDocs(program);
98
+ expect(combined).toContain("Hello README");
99
+ expect(combined).toContain("Architecture");
100
+ expect(combined).toContain("MCP server (myapp)");
101
+ });
102
+
103
+ test("presentation includes docs subtree", () => {
104
+ const presentation = cliPresentationRoot(docsFixture());
105
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
106
+ expect(docsNode).toBeDefined();
107
+ expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(
108
+ true,
109
+ );
110
+ });
111
+
112
+ test("completions offer docs subcommands", () => {
113
+ const bash = completionBashScript(cliPresentationRoot(docsFixture()));
114
+ expect(bash).toContain("docs) echo");
115
+ expect(bash).toContain("readme) echo");
116
+ });
117
+
118
+ test("generateMcpGuide includes schema URI", () => {
119
+ const guide = generateMcpGuide(docsFixture(true));
120
+ expect(guide).toContain("myapp://schema");
121
+ });
@@ -0,0 +1,118 @@
1
+ import { collectOptionDefs } from "../parse.ts";
2
+ import {
3
+ collectMcpTools,
4
+ mcpServerId,
5
+ resolveMcpSchemaUri,
6
+ type McpToolDef,
7
+ } from "../mcp/tools.ts";
8
+ import { type CliProgram, CliOptionKind } from "../types.ts";
9
+
10
+ /** Formats one exposed MCP tool for the auto-generated MCP guide. */
11
+ function formatToolLine(root: CliProgram, tool: McpToolDef): string {
12
+ const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
13
+ let line = `- \`${cliPath}\` — ${tool.description}`;
14
+ const opts = collectOptionDefs(root, tool.path);
15
+ const flags = opts.filter((o) => o.kind === CliOptionKind.Presence).map((o) => `--${o.name}`);
16
+ if (flags.length > 0) {
17
+ line += ` (flags: ${flags.join(", ")})`;
18
+ }
19
+ return line;
20
+ }
21
+
22
+ /** Generates the auto `docs mcp` markdown guide from schema and MCP config. */
23
+ export function generateMcpGuide(root: CliProgram): string {
24
+ const tools = collectMcpTools(root);
25
+ const schemaUri = resolveMcpSchemaUri(root);
26
+ const serverId = mcpServerId(root);
27
+ const mcp = root.mcpServer!;
28
+
29
+ const lines: string[] = [
30
+ `# MCP server (${root.key})`,
31
+ "",
32
+ `${root.key} exposes an MCP server via argsbarg. Each exposed leaf command becomes an MCP tool.`,
33
+ "",
34
+ "## Quick start",
35
+ "",
36
+ "```bash",
37
+ `${root.key} mcp`,
38
+ "```",
39
+ "",
40
+ "Cursor / Claude `mcp.json` entry:",
41
+ "",
42
+ "```json",
43
+ JSON.stringify(
44
+ {
45
+ mcpServers: {
46
+ [serverId]: {
47
+ command: root.key,
48
+ args: ["mcp"],
49
+ },
50
+ },
51
+ },
52
+ null,
53
+ 2,
54
+ ),
55
+ "```",
56
+ "",
57
+ "Or run:",
58
+ "",
59
+ "```bash",
60
+ `${root.key} install --mcp --yes`,
61
+ "```",
62
+ "",
63
+ ];
64
+
65
+ if (mcp.shellEnv || mcp.envFile) {
66
+ lines.push("## Environment", "");
67
+ if (mcp.shellEnv) {
68
+ lines.push(
69
+ "- **`shellEnv`** — captures login-shell environment at MCP startup (PATH, toolchain shims, exports).",
70
+ );
71
+ }
72
+ if (mcp.envFile) {
73
+ lines.push(
74
+ "- **`envFile`** — loads `" + mcp.envFile + "` after shell env (overrides for its keys).",
75
+ );
76
+ }
77
+ lines.push("");
78
+ }
79
+
80
+ lines.push(
81
+ "## What agents get",
82
+ "",
83
+ "| Mechanism | Purpose |",
84
+ "|-----------|---------|",
85
+ "| `tools/list` | Callable tools for exposed leaf commands |",
86
+ "| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
87
+ `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} --schema\` |`,
88
+ "",
89
+ "## Exposed tools",
90
+ "",
91
+ );
92
+
93
+ if (tools.length === 0) {
94
+ lines.push("(No MCP tools exposed.)", "");
95
+ } else {
96
+ for (const tool of tools) {
97
+ lines.push(formatToolLine(root, tool));
98
+ }
99
+ lines.push("");
100
+ }
101
+
102
+ lines.push(
103
+ "## Tool arguments",
104
+ "",
105
+ "Arguments are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
106
+ `See \`${root.key} --schema\` or the schema resource for per-tool shapes.`,
107
+ "",
108
+ "Varargs positionals accept a JSON array or a comma-separated string.",
109
+ "",
110
+ "## Protocol",
111
+ "",
112
+ "Stdio NDJSON JSON-RPC. Help and `--schema` are not available through tool calls.",
113
+ `Run \`${root.key} docs\` for bundled user documentation.`,
114
+ "",
115
+ );
116
+
117
+ return lines.join("\n");
118
+ }
@@ -0,0 +1,88 @@
1
+ import type { CliDocsConfig, CliProgram } from "../types.ts";
2
+ import { generateMcpGuide } from "./mcp-guide.ts";
3
+
4
+ /** Built-in docs subcommand keys not allowed in `docs.topics`. */
5
+ export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all"] as const;
6
+
7
+ export type DocsBuiltinTopicKey = (typeof DOCS_BUILTIN_TOPIC_KEYS)[number];
8
+
9
+ /** Default router description for the `docs` built-in. */
10
+ export const DOCS_ROUTER_DESCRIPTION = "Print bundled CLI documentation.";
11
+
12
+ /** Returns whether bundled docs are enabled on the program root. */
13
+ export function docsEnabled(program: CliProgram): boolean {
14
+ return program.docs?.enabled === true;
15
+ }
16
+
17
+ /** User topic keys in declaration order. */
18
+ export function docsUserTopicKeys(docs: CliDocsConfig): string[] {
19
+ return Object.keys(docs.topics);
20
+ }
21
+
22
+ /** Subcommand used when argv is bare `myapp docs`. */
23
+ export function docsEffectiveDefaultTopic(docs: CliDocsConfig): string {
24
+ if (docs.defaultTopic !== undefined) {
25
+ return docs.defaultTopic;
26
+ }
27
+ const keys = docsUserTopicKeys(docs);
28
+ if (keys.length === 0) {
29
+ throw new Error("docs.topics must be non-empty");
30
+ }
31
+ return keys[0]!;
32
+ }
33
+
34
+ /** Whether MCP auto-guide topic is included. */
35
+ export function docsIncludesMcpTopic(program: CliProgram): boolean {
36
+ return docsEnabled(program) && program.mcpServer?.enabled === true;
37
+ }
38
+
39
+ /** Leaf help description for a user topic. */
40
+ export function docsTopicDescription(key: string, custom?: string): string {
41
+ if (custom) {
42
+ return custom;
43
+ }
44
+ if (key === "readme") {
45
+ return "Print README (user guide).";
46
+ }
47
+ const label = key.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
48
+ return `Print ${label} documentation.`;
49
+ }
50
+
51
+ /** Ordered keys for `docs all` (user topics, then auto `mcp` when enabled). */
52
+ export function docsPrintOrder(program: CliProgram): string[] {
53
+ const docs = program.docs!;
54
+ const order = docsUserTopicKeys(docs);
55
+ if (docsIncludesMcpTopic(program)) {
56
+ order.push("mcp");
57
+ }
58
+ return order;
59
+ }
60
+
61
+ /** Markdown body for one docs topic key. */
62
+ export function docsTopicText(program: CliProgram, topic: string): string {
63
+ const docs = program.docs!;
64
+ if (topic === "mcp") {
65
+ if (!docsIncludesMcpTopic(program)) {
66
+ throw new Error("Unknown docs topic 'mcp'.");
67
+ }
68
+ return generateMcpGuide(program);
69
+ }
70
+ const entry = docs.topics[topic];
71
+ if (!entry) {
72
+ throw new Error(`Unknown docs topic '${topic}'.`);
73
+ }
74
+ return entry.text;
75
+ }
76
+
77
+ /** All bundled docs concatenated with horizontal rules. */
78
+ export function combineAllDocs(program: CliProgram): string {
79
+ return docsPrintOrder(program)
80
+ .map((key) => docsTopicText(program, key).trim())
81
+ .join("\n\n---\n\n");
82
+ }
83
+
84
+ /** Writes one docs topic (or `all`) to stdout. */
85
+ export function printDocsTopic(program: CliProgram, topic: string): void {
86
+ const content = topic === "all" ? combineAllDocs(program) : docsTopicText(program, topic);
87
+ process.stdout.write(`${content}\n`);
88
+ }