argsbarg 3.0.0 → 3.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +6 -6
  3. package/bun.lock +21 -0
  4. package/docs/ai-skills.md +8 -4
  5. package/docs/bundled-docs.md +91 -0
  6. package/docs/install.md +16 -4
  7. package/docs/mcp.md +4 -6
  8. package/examples/minimal.ts +6 -0
  9. package/examples/nested.ts +6 -0
  10. package/index.d.ts +46 -1
  11. package/package.json +4 -1
  12. package/src/builtins/completion-bash.ts +1 -8
  13. package/src/builtins/completion-fish.ts +0 -5
  14. package/src/builtins/completion-zsh.ts +1 -9
  15. package/src/builtins/dispatch.ts +40 -0
  16. package/src/builtins/export.ts +9 -0
  17. package/src/builtins/install.ts +9 -3
  18. package/src/builtins/presentation.ts +9 -0
  19. package/src/builtins/shell-helpers.ts +0 -2
  20. package/src/builtins/update.ts +14 -0
  21. package/src/capabilities.ts +12 -1
  22. package/src/docs/api-guide.test.ts +55 -0
  23. package/src/docs/api-guide.ts +129 -0
  24. package/src/docs/builtin.ts +61 -0
  25. package/src/docs/docs.test.ts +167 -0
  26. package/src/docs/mcp-guide.ts +118 -0
  27. package/src/docs/resolve.ts +119 -0
  28. package/src/help.ts +3 -7
  29. package/src/index.test.ts +40 -66
  30. package/src/index.ts +4 -0
  31. package/src/install/binary.ts +8 -3
  32. package/src/install/index.ts +55 -30
  33. package/src/install/plan.ts +5 -3
  34. package/src/install/update.test.ts +106 -0
  35. package/src/install/update.ts +55 -0
  36. package/src/invoke.ts +17 -15
  37. package/src/mcp/tools.ts +1 -1
  38. package/src/parse.ts +0 -27
  39. package/src/runtime.ts +12 -6
  40. package/src/schema.ts +7 -2
  41. package/src/skill/generate.ts +6 -39
  42. package/src/types.ts +47 -0
  43. package/src/validate.ts +53 -6
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { CliProgram } from "../types.ts";
3
+ import { runInstallMutation } from "./index.ts";
4
+ import { installErr } from "./status.ts";
5
+
6
+ /** Downloads the latest release and reinstalls installed artifacts (`myapp update`). */
7
+ export async function cliUpdate(root: CliProgram): Promise<never> {
8
+ const hook = root.install?.updateGetLatest;
9
+ if (!hook) {
10
+ installErr("update is not configured. Set install.updateGetLatest on the program root.");
11
+ process.exit(1);
12
+ }
13
+
14
+ let artifact: Awaited<ReturnType<typeof hook>>;
15
+ try {
16
+ artifact = await hook({ version: root.version });
17
+ } catch (err) {
18
+ const message = err instanceof Error ? err.message : String(err);
19
+ installErr(message);
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!artifact.path || !existsSync(artifact.path)) {
24
+ installErr(`updateGetLatest returned missing binary: ${JSON.stringify(artifact.path)}`);
25
+ process.exit(1);
26
+ }
27
+
28
+ if (artifact.version !== undefined && artifact.version === root.version) {
29
+ process.stdout.write(`Already at v${root.version}\n`);
30
+ await artifact.cleanup?.();
31
+ process.exit(0);
32
+ }
33
+
34
+ const currentVersion = root.version;
35
+ try {
36
+ await runInstallMutation(root, {
37
+ reinstall: "1",
38
+ yes: "1",
39
+ quiet: "1",
40
+ from: artifact.path,
41
+ });
42
+ } catch (err) {
43
+ await artifact.cleanup?.();
44
+ installErr(err instanceof Error ? err.message : String(err));
45
+ process.exit(1);
46
+ }
47
+
48
+ await artifact.cleanup?.();
49
+
50
+ if (artifact.version !== undefined && artifact.version !== currentVersion) {
51
+ process.stdout.write(`Updated ${root.key} ${currentVersion} → ${artifact.version}\n`);
52
+ }
53
+
54
+ process.exit(0);
55
+ }
package/src/invoke.ts CHANGED
@@ -5,12 +5,14 @@ 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. */
13
- export type CliInvokeKind = "ok" | "help" | "schema" | "error";
15
+ export type CliInvokeKind = "ok" | "help" | "error";
14
16
 
15
17
  /** Result of cliInvoke: captured output and exit metadata without process.exit. */
16
18
  export interface CliInvokeResult {
@@ -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 {
@@ -62,16 +74,6 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
62
74
  };
63
75
  }
64
76
 
65
- if (pr.kind === ParseKind.Schema) {
66
- return {
67
- kind: "schema",
68
- exitCode: 1,
69
- stdout: "",
70
- stderr: "",
71
- errorMsg: "Schema export is not available via MCP tool calls.",
72
- };
73
- }
74
-
75
77
  if (pr.kind === ParseKind.Error) {
76
78
  return {
77
79
  kind: "error",
@@ -82,7 +84,7 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
82
84
  };
83
85
  }
84
86
 
85
- let current: CliNode = root;
87
+ let current: CliNode = parseRoot;
86
88
  for (const seg of pr.path) {
87
89
  if (!isCliRouter(current)) {
88
90
  return {
package/src/mcp/tools.ts CHANGED
@@ -134,7 +134,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
134
134
  const builtIn: McpResourceEntry = {
135
135
  uri: schemaUri,
136
136
  name: "cli-schema",
137
- description: "Full CLI command tree (same as --schema).",
137
+ description: "Full CLI command tree (same as docs schema).",
138
138
  mimeType: "application/json",
139
139
  load: () => cliSchemaJson(root),
140
140
  };
package/src/parse.ts CHANGED
@@ -29,8 +29,6 @@ export enum ParseKind {
29
29
  Ok = "ok",
30
30
  /** User requested help (explicit or implicit). */
31
31
  Help = "help",
32
- /** User requested machine-readable schema export (`--schema`). */
33
- Schema = "schema",
34
32
  /** User error (unknown command, bad option, etc.). */
35
33
  Error = "error",
36
34
  }
@@ -59,18 +57,12 @@ export interface ParseResult {
59
57
 
60
58
  const helpShort = "-h";
61
59
  const helpLong = "--help";
62
- const schemaLong = "--schema";
63
60
 
64
61
  /** Returns true if the argv token is `-h` or `--help`. */
65
62
  function isHelpTok(tok: string): boolean {
66
63
  return tok === helpShort || tok === helpLong;
67
64
  }
68
65
 
69
- /** Returns true if the argv token is `--schema`. */
70
- function isSchemaTok(tok: string): boolean {
71
- return tok === schemaLong;
72
- }
73
-
74
66
  /** Looks up a subcommand or routing node by `key`. */
75
67
  function findChild(cmds: CliNode[], name: string): CliNode | undefined {
76
68
  return cmds.find((c) => c.key === name);
@@ -195,7 +187,6 @@ function consumeOptions(
195
187
  const tok = argv[idx];
196
188
 
197
189
  if (isHelpTok(tok)) break;
198
- if (isSchemaTok(tok)) break;
199
190
  if (!tok.startsWith("-")) break;
200
191
 
201
192
  if (tok === "--") {
@@ -371,20 +362,6 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
371
362
  };
372
363
  }
373
364
 
374
- /** Builds a schema-export result for the program root. */
375
- function schemaResult(): ParseResult {
376
- return {
377
- kind: ParseKind.Schema,
378
- path: [],
379
- opts: {},
380
- args: [],
381
- helpExplicit: false,
382
- helpPath: [],
383
- errorMsg: "",
384
- errorHelpPath: [],
385
- };
386
- }
387
-
388
365
  /**
389
366
  * Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
390
367
  */
@@ -420,10 +397,6 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
420
397
  return helpResult([], true);
421
398
  }
422
399
 
423
- if (i < argv.length && !forcePositionals && isSchemaTok(argv[i])) {
424
- return schemaResult();
425
- }
426
-
427
400
  // Determine which subcommand to route to
428
401
  let cmdName: string;
429
402
  let node: CliNode | undefined;
package/src/runtime.ts CHANGED
@@ -10,7 +10,6 @@ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.t
10
10
  import { CliContext } from "./context.ts";
11
11
  import { cliHelpRender } from "./help.ts";
12
12
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
13
- import { cliSchemaJson } from "./schema.ts";
14
13
  import { cliValidateProgram } from "./validate.ts";
15
14
 
16
15
  function cliRootMergedWithBuiltins(program: CliProgram): CliRouter {
@@ -41,6 +40,18 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
41
40
  process.exit(1);
42
41
  }
43
42
 
43
+ if (argv.length >= 1 && argv[0] === "update" && !caps.update) {
44
+ process.stderr.write(
45
+ "update is not enabled. Set install.updateGetLatest on the program root.\n",
46
+ );
47
+ process.exit(1);
48
+ }
49
+
50
+ if (argv.length >= 1 && argv[0] === "docs" && !caps.docs) {
51
+ process.stderr.write("docs is not enabled. Set docs: { enabled: true } on the program root.\n");
52
+ process.exit(1);
53
+ }
54
+
44
55
  let parseRoot: CliNode;
45
56
  let completionParseRoot: CliRouter = cliRootMergedWithBuiltins(program);
46
57
  let isLeafCompletionIntercept = false;
@@ -68,11 +79,6 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
68
79
  process.exit(pr.helpExplicit ? 0 : 1);
69
80
  }
70
81
 
71
- if (pr.kind === ParseKind.Schema) {
72
- process.stdout.write(cliSchemaJson(program));
73
- process.exit(0);
74
- }
75
-
76
82
  if (pr.kind === "error") {
77
83
  const color = process.stderr.isTTY;
78
84
  const msg = color ? `\u001B[31m${pr.errorMsg}\u001B[0m` : pr.errorMsg;
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", "update"]);
9
9
 
10
10
  function exportCommand(cmd: CliNode): CliSchemaExport {
11
11
  const out: CliSchemaExport = {
@@ -44,8 +44,13 @@ function exportCommand(cmd: CliNode): CliSchemaExport {
44
44
  return out;
45
45
  }
46
46
 
47
+ /** Returns the JSON-safe command tree (handlers omitted). */
48
+ export function cliSchemaExport(root: CliProgram): CliSchemaExport {
49
+ return exportCommand(root);
50
+ }
51
+
47
52
  export function cliSchemaJson(root: CliProgram): string {
48
- return JSON.stringify(exportCommand(root), null, 2) + "\n";
53
+ return JSON.stringify(cliSchemaExport(root), null, 2) + "\n";
49
54
  }
50
55
 
51
56
  export type { CliSchemaExport };
@@ -4,7 +4,7 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
4
4
 
5
5
  import { collectOptionDefs } from "../parse.ts";
6
6
  import { cliSchemaJson } from "../schema.ts";
7
- import { collectMcpTools, mcpServerId, sanitizeToolSegment } from "../mcp/tools.ts";
7
+ import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
8
8
  import { CliProgram, CliOptionKind } from "../types.ts";
9
9
 
10
10
  export type SkillTarget = "cursor" | "claude";
@@ -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?.enabled === true) {
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
- [mcpServerId(root)]: {
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,12 +92,11 @@ 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",
131
98
  "",
132
- "See `reference.md` in this skill directory for the full `--schema` JSON export.",
99
+ "See `reference.md` in this skill directory for the full `docs schema` JSON export.",
133
100
  "",
134
101
  );
135
102
 
@@ -163,7 +130,7 @@ function buildReferenceMd(root: CliProgram): string {
163
130
  return [
164
131
  `# ${root.key} — CLI reference`,
165
132
  "",
166
- "Generated from the program `--schema` export. Handlers and runtime-only nodes are omitted.",
133
+ "Generated from the program `docs schema` export. Handlers and runtime-only nodes are omitted.",
167
134
  "",
168
135
  "```json",
169
136
  cliSchemaJson(root).trimEnd(),
package/src/types.ts CHANGED
@@ -153,11 +153,56 @@ export interface CliMcpToolConfig {
153
153
  /**
154
154
  * Opt-out and defaults for the `install` built-in (program root only).
155
155
  */
156
+ export interface CliUpdateArtifact {
157
+ /** Path to an executable binary to copy into the install location. */
158
+ path: string;
159
+ /** Release version of `path` (used for already-current checks and success messages). */
160
+ version?: string;
161
+ /** Called after reinstall completes (e.g. remove a temp download directory). */
162
+ cleanup?: () => void | Promise<void>;
163
+ }
164
+
165
+ /** Fetches the latest release binary for the `update` built-in. */
166
+ export type CliUpdateGetLatest = (ctx: { version: string }) => Promise<CliUpdateArtifact>;
167
+
156
168
  export interface CliInstallConfig {
157
169
  /** When `false`, hide/disable `install` (default: enabled). */
158
170
  enabled?: boolean;
159
171
  /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
160
172
  prefix?: string;
173
+ /**
174
+ * When set, enables the `update` built-in (`myapp update`).
175
+ * Should download or locate the latest release binary and return its path.
176
+ */
177
+ updateGetLatest?: CliUpdateGetLatest;
178
+ }
179
+
180
+ /**
181
+ * One bundled documentation topic for the `docs` built-in (program root only).
182
+ */
183
+ export interface CliDocsTopic {
184
+ /** Bundled markdown (use compile-time text imports in the consumer). */
185
+ text: string;
186
+ /** Leaf help text for `myapp docs <key> -h`. Auto-generated from key when omitted. */
187
+ description?: string;
188
+ }
189
+
190
+ /**
191
+ * Enables `myapp docs` and bundled markdown topics (program root only).
192
+ * Must include `enabled: true`; omit `docs` entirely to disable.
193
+ */
194
+ export interface CliDocsConfig {
195
+ /** When `true`, enables the `docs` built-in command group. */
196
+ enabled: boolean;
197
+ /** Router description for `myapp docs` (default: "Print bundled CLI documentation."). */
198
+ description?: string;
199
+ /**
200
+ * Subcommand for bare `myapp docs` (maps to router `fallbackCommand`).
201
+ * When omitted, uses the first key in `topics` (insertion order).
202
+ */
203
+ defaultTopic?: string;
204
+ /** Topic key → bundled markdown. Reserved keys: `mcp`, `all` (supplied by the built-in). */
205
+ topics: Record<string, CliDocsTopic>;
161
206
  }
162
207
 
163
208
  /**
@@ -214,6 +259,8 @@ export type CliProgram = CliNode & {
214
259
  mcpServer?: CliMcpServerConfig;
215
260
  /** Opt-out and defaults for `install`. */
216
261
  install?: CliInstallConfig;
262
+ /** When set with `enabled: true`, enables the `docs` built-in command group. */
263
+ docs?: CliDocsConfig;
217
264
  };
218
265
 
219
266
  /** True when the node is a leaf (has a handler). */
package/src/validate.ts CHANGED
@@ -13,6 +13,33 @@ import {
13
13
  isCliRouter,
14
14
  } from "./types.ts";
15
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 {
@@ -26,6 +53,27 @@ export function cliValidateProgram(program: CliProgram): void {
26
53
  );
27
54
  }
28
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
+
66
+ if (program.install?.updateGetLatest !== undefined) {
67
+ if (program.install.enabled === false) {
68
+ throw new CliSchemaValidationError(
69
+ "install.updateGetLatest requires install to be enabled (omit install.enabled: false)",
70
+ );
71
+ }
72
+ if (typeof program.install.updateGetLatest !== "function") {
73
+ throw new CliSchemaValidationError("install.updateGetLatest must be a function");
74
+ }
75
+ }
76
+
29
77
  const caps = resolveCapabilities(program);
30
78
  const reserved = reservedCommandNames(caps);
31
79
 
@@ -53,6 +101,11 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
53
101
  "install is only supported on the program root (not on " + node.key + ")",
54
102
  );
55
103
  }
104
+ if (rogue.docs !== undefined) {
105
+ throw new CliSchemaValidationError(
106
+ "docs is only supported on the program root (not on " + node.key + ")",
107
+ );
108
+ }
56
109
  }
57
110
 
58
111
  if (isCliLeaf(node)) {
@@ -124,12 +177,6 @@ function validateOptions(scopeKey: string, options: import("./types.ts").CliOpti
124
177
  );
125
178
  }
126
179
 
127
- if (opt.name === "schema") {
128
- throw new CliSchemaValidationError(
129
- `Option name "schema" is reserved for --schema: ${scopeKey}/${opt.name}`,
130
- );
131
- }
132
-
133
180
  if (opt.shortName !== undefined) {
134
181
  if (opt.shortName === "h") {
135
182
  throw new CliSchemaValidationError(