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.
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export type {
20
20
  CliMcpServerConfig,
21
21
  CliMcpToolConfig,
22
22
  CliInstallConfig,
23
+ CliDocsConfig,
24
+ CliDocsTopic,
23
25
  CliOption,
24
26
  CliPositional,
25
27
  } from "./types.ts";
@@ -1,4 +1,5 @@
1
1
  import { readSync } from "node:fs";
2
+ import { resolveCapabilities } from "../capabilities.ts";
2
3
  import { CliProgram } from "../types.ts";
3
4
  import { cliSkillInstall } from "../skill/install.ts";
4
5
  import { checkMcpConflict, expectedMcpEntry } from "./mcp-config.ts";
@@ -130,7 +131,7 @@ export async function cliInstall(root: CliProgram, rawOpts: Record<string, strin
130
131
  }
131
132
 
132
133
  // MCP conflict checks before planning
133
- if (!opts.uninstall && root.mcpServer && (opts.all || opts.mcp)) {
134
+ if (!opts.uninstall && resolveCapabilities(root).mcp && (opts.all || opts.mcp)) {
134
135
  const entry = expectedMcpEntry(root);
135
136
  const yes = !!opts.yes;
136
137
  for (const p of [paths.cursorMcpPath, paths.claudeMcpPath]) {
@@ -11,8 +11,9 @@ import { parseInstallOpts } from "./index.ts";
11
11
 
12
12
  const fixture: CliProgram = {
13
13
  key: "testapp",
14
+ version: "0.0.0",
14
15
  description: "Test",
15
- mcpServer: { name: "testapp" },
16
+ mcpServer: { enabled: true },
16
17
  handler: () => {},
17
18
  };
18
19
 
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { CliProgram } from "../types.ts";
4
- import { sanitizeToolSegment } from "../mcp/tools.ts";
4
+ import { sanitizeToolSegment, mcpServerId } from "../mcp/tools.ts";
5
5
 
6
6
  export interface InstallPaths {
7
7
  bindir: string;
@@ -63,7 +63,7 @@ export function resolveInstallPaths(root: CliProgram, opts: { prefix?: string })
63
63
  claudeMcpPath: join(home, ".claude.json"),
64
64
  bashRc: join(home, ".bashrc"),
65
65
  zshRc: join(home, ".zshrc"),
66
- mcpName: root.mcpServer?.name ?? root.key,
66
+ mcpName: mcpServerId(root),
67
67
  skillDirName,
68
68
  };
69
69
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveCapabilities } from "../capabilities.ts";
3
4
  import { CliProgram } from "../types.ts";
4
5
  import { installBinary } from "./binary.ts";
5
6
  import { installCompletions } from "./completions.ts";
@@ -46,7 +47,7 @@ function wantsSkill(opts: InstallOpts): boolean {
46
47
  }
47
48
 
48
49
  function wantsMcp(opts: InstallOpts, root: CliProgram): boolean {
49
- return !!(opts.all || opts.mcp) && root.mcpServer !== undefined;
50
+ return !!(opts.mcp || opts.all) && resolveCapabilities(root).mcp;
50
51
  }
51
52
 
52
53
  /** Builds install actions for normal mode (--all / scoped targets). */
@@ -154,7 +155,7 @@ export function buildUpdatePlan(root: CliProgram, paths: InstallPaths, opts: Ins
154
155
  bin: true,
155
156
  completions: detected.bashCompletion || detected.zshCompletion || detected.fishCompletion,
156
157
  skill: detected.cursorSkill || detected.claudeSkill,
157
- mcp: (detected.cursorMcp || detected.claudeMcp) && root.mcpServer !== undefined,
158
+ mcp: (detected.cursorMcp || detected.claudeMcp) && resolveCapabilities(root).mcp,
158
159
  dry: opts.dry,
159
160
  };
160
161
  const plan = buildInstallPlan(root, paths, scoped);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { resolveCapabilities } from "../capabilities.ts";
3
4
  import { CliProgram } from "../types.ts";
4
5
  import { uninstallBinary } from "./binary.ts";
5
6
  import { uninstallCompletions } from "./completions.ts";
@@ -77,7 +78,7 @@ export function buildUninstallPlan(
77
78
  });
78
79
  }
79
80
 
80
- if ((all || opts.mcp) && root.mcpServer !== undefined) {
81
+ if ((all || opts.mcp) && resolveCapabilities(root).mcp) {
81
82
  if (detected.cursorMcp) {
82
83
  actions.push({
83
84
  summary: `cursor mcp: ${paths.cursorMcpPath}`,
package/src/invoke.ts CHANGED
@@ -5,8 +5,10 @@ process.exit so MCP tool calls can run handlers repeatedly.
5
5
  */
6
6
 
7
7
  import { CliContext } from "./context.ts";
8
+ import { builtinInterceptRoot } from "./builtins/dispatch.ts";
9
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
8
10
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
9
- import { type CliNode, type CliProgram, isCliRouter } from "./types.ts";
11
+ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
10
12
  import { format } from "node:util";
11
13
 
12
14
  /** Outcome of a non-exiting CLI invocation. */
@@ -49,8 +51,18 @@ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
49
51
  * Never calls process.exit.
50
52
  */
51
53
  export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult> {
52
- let pr = parse(root, argv);
53
- pr = postParseValidate(root, pr);
54
+ let parseRoot: CliNode = root;
55
+ if (isCliLeaf(root)) {
56
+ const intercept = builtinInterceptRoot(root, argv);
57
+ if (intercept.parseRoot !== root) {
58
+ parseRoot = intercept.parseRoot;
59
+ }
60
+ } else {
61
+ parseRoot = cliPresentationRoot(root);
62
+ }
63
+
64
+ let pr = parse(parseRoot, argv);
65
+ pr = postParseValidate(parseRoot, pr);
54
66
 
55
67
  if (pr.kind === ParseKind.Help) {
56
68
  return {
@@ -82,7 +94,7 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
82
94
  };
83
95
  }
84
96
 
85
- let current: CliProgram = root;
97
+ let current: CliNode = parseRoot;
86
98
  for (const seg of pr.path) {
87
99
  if (!isCliRouter(current)) {
88
100
  return {
package/src/mcp/tools.ts CHANGED
@@ -3,14 +3,24 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
3
3
  flat JSON tool arguments into argv for cliInvoke.
4
4
  */
5
5
 
6
- import { readFileSync } from "node:fs";
7
- import { join } from "node:path";
8
6
  import { collectOptionDefs } from "../parse.ts";
9
7
  import { cliSchemaJson } from "../schema.ts";
10
8
  import { CliProgram, CliLeaf, CliNode, CliOption, CliOptionKind, CliPositional, isCliLeaf, isCliRouter } from "../types.ts";
11
9
 
12
- /** Default URI for the CLI schema MCP resource. */
13
- export const MCP_SCHEMA_URI_DEFAULT = "argsbarg://schema";
10
+ /** Default URI pattern for the CLI schema MCP resource (`<mcpId>://schema`). */
11
+ export function defaultMcpSchemaUri(mcpId: string): string {
12
+ return `${mcpId}://schema`;
13
+ }
14
+
15
+ /** Sanitizes a command key segment for MCP tool names and server identity. */
16
+ export function sanitizeToolSegment(key: string): string {
17
+ return key.replace(/[^a-zA-Z0-9]/g, "_");
18
+ }
19
+
20
+ /** MCP server id derived from the program root key (sanitized). */
21
+ export function mcpServerId(root: CliProgram): string {
22
+ return sanitizeToolSegment(root.key);
23
+ }
14
24
 
15
25
  /** One MCP tool derived from a leaf CLI command. */
16
26
  export interface McpToolDef {
@@ -32,11 +42,6 @@ export function mcpToolDescription(path: string[], rootKey: string, description:
32
42
  return `${prefix} — ${description}`;
33
43
  }
34
44
 
35
- /** Sanitizes a command key segment for MCP tool names. */
36
- export function sanitizeToolSegment(key: string): string {
37
- return key.replace(/[^a-zA-Z0-9]/g, "_");
38
- }
39
-
40
45
  /** Builds the MCP tool name for a leaf at the given path. */
41
46
  export function mcpToolName(root: CliProgram, path: string[]): string {
42
47
  if (path.length === 0) {
@@ -150,7 +155,7 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
150
155
  /** Walks the command tree and appends leaf tools. */
151
156
  function walk(cmd: CliNode, path: string[]): void {
152
157
  if (isCliLeaf(cmd)) {
153
- if (cmd.key === "completion" || cmd.key === "install" || cmd.key === "mcp") {
158
+ if (cmd.key === "completion" || cmd.key === "install" || cmd.key === "mcp" || cmd.key === "version") {
154
159
  return;
155
160
  }
156
161
  if (cmd.mcpTool?.enabled === false) {
@@ -181,28 +186,20 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
181
186
  return out;
182
187
  }
183
188
 
184
- /** Reads package.json version from cwd synchronously. */
185
- function resolveMcpVersionFromPackageJson(): string | undefined {
186
- try {
187
- const text = readFileSync(join(process.cwd(), "package.json"), "utf8");
188
- const version = (JSON.parse(text) as { version?: string }).version;
189
- return typeof version === "string" ? version : undefined;
190
- } catch {
191
- return undefined;
192
- }
193
- }
194
-
195
189
  /** Resolves MCP server name and version for initialize. */
196
190
  export function resolveMcpServerInfo(root: CliProgram): { name: string; version: string } {
197
191
  return {
198
- name: root.mcpServer?.name ?? root.key,
199
- version: root.mcpServer?.version ?? resolveMcpVersionFromPackageJson() ?? "0.0.0",
192
+ name: mcpServerId(root),
193
+ version: root.version,
200
194
  };
201
195
  }
202
196
 
203
197
  /** Resolves the schema resource URI for this app. */
204
198
  export function resolveMcpSchemaUri(root: CliProgram): string {
205
- return root.mcpServer?.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
199
+ if (root.mcpServer?.schemaResourceUri) {
200
+ return root.mcpServer.schemaResourceUri;
201
+ }
202
+ return defaultMcpSchemaUri(mcpServerId(root));
206
203
  }
207
204
 
208
205
  /** Converts flat MCP tool arguments to argv for cliInvoke. */
package/src/runtime.ts CHANGED
@@ -32,7 +32,7 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
32
32
  const caps = resolveCapabilities(program);
33
33
 
34
34
  if (argv.length >= 1 && argv[0] === "mcp" && !caps.mcp) {
35
- process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
35
+ process.stderr.write("MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n");
36
36
  process.exit(1);
37
37
  }
38
38
 
@@ -41,6 +41,11 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
41
41
  process.exit(1);
42
42
  }
43
43
 
44
+ if (argv.length >= 1 && argv[0] === "docs" && !caps.docs) {
45
+ process.stderr.write("docs is not enabled. Set docs: { enabled: true } on the program root.\n");
46
+ process.exit(1);
47
+ }
48
+
44
49
  let parseRoot: CliNode;
45
50
  let completionParseRoot: CliRouter = cliRootMergedWithBuiltins(program);
46
51
  let isLeafCompletionIntercept = false;
package/src/schema.ts CHANGED
@@ -5,7 +5,7 @@ This module serializes the CLI schema tree to JSON for machine-readable introspe
5
5
  import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
6
6
  import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
7
7
 
8
- const RESERVED = new Set(["completion", "install", "mcp"]);
8
+ const RESERVED = new Set(["completion", "install", "docs", "mcp", "version"]);
9
9
 
10
10
  function exportCommand(cmd: CliNode): CliSchemaExport {
11
11
  const out: CliSchemaExport = {
@@ -69,46 +69,14 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
69
69
  "",
70
70
  "## When to use",
71
71
  "",
72
- `Use this skill when working with **${root.key}** — shell commands, automation, or agent tool calls for this application.`,
72
+ `Use this skill when working with **${root.key}** — shell commands and automation for this application.`,
73
73
  "",
74
74
  "## Execution",
75
75
  "",
76
+ "Invoke via shell:",
77
+ "",
76
78
  ];
77
79
 
78
- if (root.mcpServer !== undefined) {
79
- lines.push(
80
- "**Prefer MCP** when a host has the server connected:",
81
- "",
82
- "```bash",
83
- `${root.key} mcp`,
84
- "```",
85
- "",
86
- "Example Cursor `mcp.json` entry:",
87
- "",
88
- "```json",
89
- JSON.stringify(
90
- {
91
- mcpServers: {
92
- [root.mcpServer.name ?? root.key]: {
93
- command: root.key,
94
- args: ["mcp"],
95
- },
96
- },
97
- },
98
- null,
99
- 2,
100
- ),
101
- "```",
102
- "",
103
- "When MCP tools are available, use `tools/call` with flat JSON arguments. Read the schema resource for full shapes.",
104
- "",
105
- "Otherwise invoke via shell:",
106
- "",
107
- );
108
- } else {
109
- lines.push("Invoke via shell:", "");
110
- }
111
-
112
80
  lines.push("```bash", `${root.key} <subcommand> [options] [args]`, "```", "", "## Commands", "");
113
81
 
114
82
  if (tools.length === 0) {
@@ -124,7 +92,6 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
124
92
  "## Pitfalls",
125
93
  "",
126
94
  "- Use `--` before tokens that look like flags when they are positional arguments.",
127
- "- Under MCP (`ctx.invocation === \"mcp\"`), child processes must not inherit stdout — use piped stdout.",
128
95
  "- Required environment variables are listed per command in descriptions (`requires env`).",
129
96
  "",
130
97
  "## Reference",
package/src/types.test.ts CHANGED
@@ -18,8 +18,9 @@ const _leafOnly: CliLeaf = {
18
18
 
19
19
  const _program: CliProgram = {
20
20
  key: "app",
21
+ version: "0.0.0",
21
22
  description: "",
22
- mcpServer: {},
23
+ mcpServer: { enabled: true },
23
24
  commands: [],
24
25
  };
25
26
 
@@ -27,7 +28,7 @@ const _badMcpOnNode = {
27
28
  key: "x",
28
29
  description: "",
29
30
  // @ts-expect-error mcpServer is program-root only
30
- mcpServer: {},
31
+ mcpServer: { enabled: true },
31
32
  commands: [],
32
33
  } satisfies CliNode;
33
34
 
package/src/types.ts CHANGED
@@ -89,13 +89,12 @@ export interface CliPositional {
89
89
 
90
90
  /**
91
91
  * Enables `myapp mcp` and MCP stdio server metadata (program root only).
92
+ * Must include `enabled: true`; omit `mcpServer` entirely to disable MCP.
92
93
  */
93
94
  export interface CliMcpServerConfig {
94
- /** `initialize` serverInfo.name (default: root `key`). */
95
- name?: string;
96
- /** `initialize` serverInfo.version (default: see resolveMcpVersion). */
97
- version?: string;
98
- /** Resource URI for schema export (default: `"argsbarg://schema"`). */
95
+ /** When `true`, enables the `mcp` built-in and MCP stdio server. */
96
+ enabled: boolean;
97
+ /** Resource URI for schema export (default: `<sanitized root key>://schema`). */
99
98
  schemaResourceUri?: string;
100
99
  /**
101
100
  * Capture the user's login shell environment at MCP server start and merge it
@@ -110,7 +109,7 @@ export interface CliMcpServerConfig {
110
109
  */
111
110
  envFile?: string;
112
111
  /**
113
- * Custom MCP resources exposed alongside the built-in argsbarg://schema resource.
112
+ * Custom MCP resources exposed alongside the built-in schema resource.
114
113
  * URIs must be unique and must not equal schemaResourceUri.
115
114
  */
116
115
  resources?: CliMcpResource[];
@@ -161,6 +160,34 @@ export interface CliInstallConfig {
161
160
  prefix?: string;
162
161
  }
163
162
 
163
+ /**
164
+ * One bundled documentation topic for the `docs` built-in (program root only).
165
+ */
166
+ export interface CliDocsTopic {
167
+ /** Bundled markdown (use compile-time text imports in the consumer). */
168
+ text: string;
169
+ /** Leaf help text for `myapp docs <key> -h`. Auto-generated from key when omitted. */
170
+ description?: string;
171
+ }
172
+
173
+ /**
174
+ * Enables `myapp docs` and bundled markdown topics (program root only).
175
+ * Must include `enabled: true`; omit `docs` entirely to disable.
176
+ */
177
+ export interface CliDocsConfig {
178
+ /** When `true`, enables the `docs` built-in command group. */
179
+ enabled: boolean;
180
+ /** Router description for `myapp docs` (default: "Print bundled CLI documentation."). */
181
+ description?: string;
182
+ /**
183
+ * Subcommand for bare `myapp docs` (maps to router `fallbackCommand`).
184
+ * When omitted, uses the first key in `topics` (insertion order).
185
+ */
186
+ defaultTopic?: string;
187
+ /** Topic key → bundled markdown. Reserved keys: `mcp`, `all` (supplied by the built-in). */
188
+ topics: Record<string, CliDocsTopic>;
189
+ }
190
+
164
191
  /**
165
192
  * Base properties shared by all nodes in the user command tree.
166
193
  */
@@ -209,10 +236,14 @@ export type CliNode = CliLeaf | CliRouter;
209
236
  * May be a leaf or router, plus optional program-level MCP and install config.
210
237
  */
211
238
  export type CliProgram = CliNode & {
212
- /** When set, enables the `mcp` built-in subcommand. */
239
+ /** Program version (printed by the `version` built-in and MCP serverInfo). */
240
+ version: string;
241
+ /** When set with `enabled: true`, enables the `mcp` built-in subcommand. */
213
242
  mcpServer?: CliMcpServerConfig;
214
243
  /** Opt-out and defaults for `install`. */
215
244
  install?: CliInstallConfig;
245
+ /** When set with `enabled: true`, enables the `docs` built-in command group. */
246
+ docs?: CliDocsConfig;
216
247
  };
217
248
 
218
249
  /** True when the node is a leaf (has a handler). */
package/src/validate.ts CHANGED
@@ -12,10 +12,57 @@ import {
12
12
  isCliLeaf,
13
13
  isCliRouter,
14
14
  } from "./types.ts";
15
- import { MCP_SCHEMA_URI_DEFAULT } from "./mcp/tools.ts";
15
+ import { resolveMcpSchemaUri } from "./mcp/tools.ts";
16
+ import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
17
+
18
+ /** Validates `docs` configuration on the program root. */
19
+ function validateDocsConfig(docs: import("./types.ts").CliDocsConfig): void {
20
+ const keys = Object.keys(docs.topics);
21
+ if (keys.length === 0) {
22
+ throw new CliSchemaValidationError("docs.topics must be non-empty");
23
+ }
24
+ for (const reserved of DOCS_BUILTIN_TOPIC_KEYS) {
25
+ if (reserved in docs.topics) {
26
+ throw new CliSchemaValidationError(
27
+ `docs.topics key '${reserved}' is reserved for the docs built-in`,
28
+ );
29
+ }
30
+ }
31
+ if (docs.defaultTopic !== undefined && !(docs.defaultTopic in docs.topics)) {
32
+ throw new CliSchemaValidationError(
33
+ `docs.defaultTopic '${docs.defaultTopic}' is not a key in docs.topics`,
34
+ );
35
+ }
36
+ for (const key of keys) {
37
+ const text = docs.topics[key]?.text;
38
+ if (text === undefined || text.length === 0) {
39
+ throw new CliSchemaValidationError(`docs.topics['${key}'].text must be non-empty`);
40
+ }
41
+ }
42
+ }
16
43
 
17
44
  /** Validates a program schema. */
18
45
  export function cliValidateProgram(program: CliProgram): void {
46
+ if (!program.version || program.version.trim().length === 0) {
47
+ throw new CliSchemaValidationError("CliProgram.version is required");
48
+ }
49
+
50
+ if (program.mcpServer !== undefined && program.mcpServer.enabled !== true) {
51
+ throw new CliSchemaValidationError(
52
+ "mcpServer requires enabled: true; omit mcpServer to disable MCP",
53
+ );
54
+ }
55
+
56
+ if (program.docs !== undefined && program.docs.enabled !== true) {
57
+ throw new CliSchemaValidationError(
58
+ "docs requires enabled: true; omit docs to disable bundled documentation",
59
+ );
60
+ }
61
+
62
+ if (program.docs?.enabled === true) {
63
+ validateDocsConfig(program.docs);
64
+ }
65
+
19
66
  const caps = resolveCapabilities(program);
20
67
  const reserved = reservedCommandNames(caps);
21
68
 
@@ -43,6 +90,11 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
43
90
  "install is only supported on the program root (not on " + node.key + ")",
44
91
  );
45
92
  }
93
+ if (rogue.docs !== undefined) {
94
+ throw new CliSchemaValidationError(
95
+ "docs is only supported on the program root (not on " + node.key + ")",
96
+ );
97
+ }
46
98
  }
47
99
 
48
100
  if (isCliLeaf(node)) {
@@ -58,8 +110,8 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
58
110
  }
59
111
  }
60
112
 
61
- if (isRoot && program.mcpServer?.resources) {
62
- const schemaUri = program.mcpServer.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
113
+ if (isRoot && program.mcpServer?.enabled === true && program.mcpServer.resources) {
114
+ const schemaUri = resolveMcpSchemaUri(program);
63
115
  const uris = program.mcpServer.resources.map((r) => r.uri);
64
116
  if (uris.includes(schemaUri)) {
65
117
  throw new CliSchemaValidationError(