argsbarg 3.6.4 → 4.0.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 (98) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/README.md +21 -9
  3. package/docs/README.md +12 -8
  4. package/docs/bundled-docs.md +1 -1
  5. package/docs/cli-program.md +62 -2
  6. package/docs/config-schema.md +192 -0
  7. package/docs/developing.md +13 -0
  8. package/docs/install.md +38 -1
  9. package/docs/mcp.md +43 -19
  10. package/docs/output-schema.md +74 -52
  11. package/docs/templates/cursor/rules/cli-program.mdc +10 -5
  12. package/examples/config-app/main.ts +20 -0
  13. package/examples/config-app/program.ts +81 -0
  14. package/examples/config-app/schema.ts +37 -0
  15. package/examples/config-app/types.ts +19 -0
  16. package/examples/consumer-app/README.md +56 -0
  17. package/examples/consumer-app/bun.lock +75 -0
  18. package/examples/consumer-app/capabilities.test.ts +69 -0
  19. package/examples/consumer-app/package.json +17 -0
  20. package/examples/consumer-app/schemas/configSchemas.ts +6 -0
  21. package/examples/consumer-app/schemas/generated/app-config.json +40 -0
  22. package/examples/consumer-app/schemas/generated/status.json +28 -0
  23. package/examples/consumer-app/schemas/outputSchemas.ts +6 -0
  24. package/examples/consumer-app/scripts/schemagen/discover-schema-roots.test.ts +25 -0
  25. package/examples/consumer-app/scripts/schemagen/discover-schema-roots.ts +93 -0
  26. package/examples/consumer-app/scripts/schemagen/naming.ts +82 -0
  27. package/examples/consumer-app/scripts/schemagen.ts +76 -0
  28. package/examples/consumer-app/src/commands/status/types.ts +11 -0
  29. package/examples/consumer-app/src/main.ts +15 -0
  30. package/examples/consumer-app/src/program.ts +116 -0
  31. package/examples/consumer-app/src/types.ts +23 -0
  32. package/examples/consumer-app/tsconfig.json +14 -0
  33. package/examples/formats.ts +10 -3
  34. package/examples/mcp-test.ts +27 -8
  35. package/examples/minimal.ts +4 -3
  36. package/examples/nested.ts +5 -4
  37. package/examples/option-required.ts +8 -4
  38. package/index.d.ts +152 -75
  39. package/package.json +1 -1
  40. package/src/builtins/builtins.test.ts +13 -0
  41. package/src/builtins/config.test.ts +82 -0
  42. package/src/builtins/config.ts +220 -0
  43. package/src/builtins/dispatch.ts +18 -9
  44. package/src/builtins/export.ts +8 -33
  45. package/src/builtins/index.ts +1 -0
  46. package/src/builtins/install.ts +13 -0
  47. package/src/builtins/mcp.ts +1 -1
  48. package/src/builtins/presentation.ts +2 -17
  49. package/src/builtins/registry.ts +40 -0
  50. package/src/capabilities.ts +46 -0
  51. package/src/cli-errors.ts +15 -0
  52. package/src/cli.ts +389 -0
  53. package/src/config/bootstrap.ts +265 -0
  54. package/src/config/context.test.ts +61 -0
  55. package/src/config/context.ts +98 -0
  56. package/src/config/entry.ts +81 -0
  57. package/src/config/file.test.ts +112 -0
  58. package/src/config/file.ts +120 -0
  59. package/src/config/manifest.ts +62 -0
  60. package/src/config/resolve.test.ts +88 -0
  61. package/src/config/resolve.ts +167 -0
  62. package/src/config/schema.ts +101 -0
  63. package/src/config/validate.test.ts +63 -0
  64. package/src/config/validate.ts +292 -0
  65. package/src/config.integration.test.ts +100 -0
  66. package/src/context.ts +5 -0
  67. package/src/docs/docs.test.ts +16 -16
  68. package/src/docs/mcp-guide.ts +35 -11
  69. package/src/hidden-mcpb.test.ts +40 -2
  70. package/src/index.ts +4 -3
  71. package/src/install/index.ts +46 -3
  72. package/src/install/paths.ts +5 -20
  73. package/src/install/plan.ts +6 -0
  74. package/src/install/status.ts +12 -0
  75. package/src/install/uninstall.ts +11 -0
  76. package/src/install/update.test.ts +5 -5
  77. package/src/invoke.test.ts +207 -0
  78. package/src/mcp/bundle.ts +9 -116
  79. package/src/mcp/claude.test.ts +73 -0
  80. package/src/mcp/claude.ts +168 -0
  81. package/src/mcp/env.ts +3 -37
  82. package/src/mcp/server.ts +18 -10
  83. package/src/mcp/tools.ts +3 -7
  84. package/src/mcp/zip.ts +82 -0
  85. package/src/mcp.integration.test.ts +502 -0
  86. package/src/{index.test.ts → parse.test.ts} +24 -935
  87. package/src/paths/host.ts +40 -0
  88. package/src/schema.ts +1 -1
  89. package/src/skill/generate.ts +18 -5
  90. package/src/skill/hint.ts +18 -0
  91. package/src/skill/install.ts +1 -5
  92. package/src/test-fixtures.ts +192 -0
  93. package/src/types.ts +39 -13
  94. package/src/validate.ts +70 -0
  95. package/src/completion.ts +0 -13
  96. package/src/invoke.ts +0 -217
  97. package/src/mcp.ts +0 -28
  98. package/src/runtime.ts +0 -134
@@ -0,0 +1,220 @@
1
+ /*
2
+ Built-in `config get` / `config set` commands.
3
+ */
4
+
5
+ import { bootstrapAppConfig } from "../config/bootstrap.ts";
6
+ import { configEntrySensitive, defaultConfigEntryTitle } from "../config/entry.ts";
7
+ import { writeAppConfigFile } from "../config/file.ts";
8
+ import { exportConfigToEnv, resolveAppConfig } from "../config/resolve.ts";
9
+ import { configPropertySchema, effectiveJsonSchema } from "../config/schema.ts";
10
+ import { parseConfigSetValue } from "../config/validate.ts";
11
+ import type { CliLeaf, CliOption, CliProgram, CliRouter } from "../types.ts";
12
+ import { CliOptionKind } from "../types.ts";
13
+
14
+ const JSON_OPTION: CliOption = {
15
+ name: "json",
16
+ description: "Emit JSON (compact).",
17
+ kind: CliOptionKind.Presence,
18
+ };
19
+
20
+ const PRETTY_OPTION: CliOption = {
21
+ name: "pretty",
22
+ description: "Pretty-print JSON (requires --json).",
23
+ kind: CliOptionKind.Presence,
24
+ };
25
+
26
+ function configGetOutput(
27
+ program: CliProgram,
28
+ key: string | undefined,
29
+ json: boolean,
30
+ pretty: boolean,
31
+ ): void {
32
+ const appConfig = program.appConfig;
33
+ if (!appConfig) {
34
+ return;
35
+ }
36
+ const { resolved } = bootstrapAppConfig(program, { validateFile: true });
37
+ const entries = appConfig.entries;
38
+
39
+ if (key !== undefined) {
40
+ if (!(key in entries)) {
41
+ process.stderr.write(`Unknown configuration key: ${key}\n`);
42
+ process.exit(1);
43
+ }
44
+ const entry = entries[key];
45
+ if (!entry) {
46
+ process.exit(1);
47
+ }
48
+ const value = resolved[key];
49
+ if (json) {
50
+ const out =
51
+ configEntrySensitive(key, entry) && value !== undefined && String(value).length > 0
52
+ ? { set: true }
53
+ : (value ?? null);
54
+ const space = pretty ? 2 : undefined;
55
+ process.stdout.write(`${JSON.stringify(out, null, space)}\n`);
56
+ return;
57
+ }
58
+ if (configEntrySensitive(key, entry)) {
59
+ const set = value !== undefined && value !== null && String(value).length > 0;
60
+ process.stdout.write(set ? "REDACTED\n" : "(not set)\n");
61
+ return;
62
+ }
63
+ if (value === undefined) {
64
+ process.stdout.write("(not set)\n");
65
+ return;
66
+ }
67
+ if (typeof value === "object") {
68
+ process.stdout.write(`${JSON.stringify(value)}\n`);
69
+ return;
70
+ }
71
+ process.stdout.write(`${String(value)}\n`);
72
+ return;
73
+ }
74
+
75
+ const out: Record<string, unknown> = {};
76
+ for (const [k, entry] of Object.entries(entries)) {
77
+ const value = resolved[k];
78
+ if (configEntrySensitive(k, entry)) {
79
+ out[k] =
80
+ value !== undefined && value !== null && String(value).length > 0
81
+ ? json
82
+ ? { set: true }
83
+ : "REDACTED"
84
+ : json
85
+ ? null
86
+ : "(not set)";
87
+ } else {
88
+ out[k] = value ?? (json ? null : "(not set)");
89
+ }
90
+ }
91
+
92
+ if (json) {
93
+ const space = pretty ? 2 : undefined;
94
+ process.stdout.write(`${JSON.stringify(out, null, space)}\n`);
95
+ return;
96
+ }
97
+
98
+ for (const [k, v] of Object.entries(out)) {
99
+ const title = entries[k]?.title ?? defaultConfigEntryTitle(k);
100
+ process.stdout.write(`${title}: ${String(v)}\n`);
101
+ }
102
+ }
103
+
104
+ function configSetRun(program: CliProgram, key: string, rawValue: string, useJson: boolean): void {
105
+ const appConfig = program.appConfig;
106
+ if (!appConfig) {
107
+ return;
108
+ }
109
+ const entries = appConfig.entries;
110
+ if (!(key in entries)) {
111
+ process.stderr.write(`Unknown configuration key: ${key}\n`);
112
+ process.exit(1);
113
+ }
114
+
115
+ const jsonSchema = effectiveJsonSchema(program);
116
+ if (!jsonSchema) {
117
+ process.stderr.write("Internal error: missing effective jsonSchema.\n");
118
+ process.exit(1);
119
+ }
120
+
121
+ const propSchema = configPropertySchema(jsonSchema, key);
122
+ let parsed: unknown;
123
+ try {
124
+ parsed = parseConfigSetValue(rawValue, propSchema, jsonSchema, useJson);
125
+ } catch (err) {
126
+ const msg = err instanceof Error ? err.message : String(err);
127
+ process.stderr.write(`${msg}\n`);
128
+ process.exit(1);
129
+ }
130
+
131
+ const { fileData } = bootstrapAppConfig(program, { validateFile: true });
132
+ const next = { ...fileData, [key]: parsed };
133
+ writeAppConfigFile(program, next);
134
+ const resolved = resolveAppConfig(program, next);
135
+ exportConfigToEnv(program, resolved);
136
+ }
137
+
138
+ function configGetLeaf(program: CliProgram): CliLeaf {
139
+ return {
140
+ key: "get",
141
+ description: "Print resolved configuration value(s).",
142
+ options: [JSON_OPTION, PRETTY_OPTION],
143
+ positionals: [
144
+ {
145
+ name: "key",
146
+ description: "Schema key to read (omit for all keys).",
147
+ kind: CliOptionKind.String,
148
+ argMin: 0,
149
+ argMax: 1,
150
+ },
151
+ ],
152
+ handler: (ctx) => {
153
+ const key = ctx.args[0];
154
+ configGetOutput(program, key, ctx.hasFlag("json"), ctx.hasFlag("pretty"));
155
+ },
156
+ };
157
+ }
158
+
159
+ function configSetLeaf(program: CliProgram, mcpSetEnabled: boolean): CliLeaf {
160
+ return {
161
+ key: "set",
162
+ description: "Write one configuration key to the config file.",
163
+ options: [JSON_OPTION],
164
+ mcpTool: mcpSetEnabled ? undefined : { enabled: false },
165
+ positionals: [
166
+ {
167
+ name: "key",
168
+ description: "Schema key to write.",
169
+ kind: CliOptionKind.String,
170
+ argMin: 1,
171
+ argMax: 1,
172
+ },
173
+ {
174
+ name: "value",
175
+ description: "Value to store (use --json for objects/arrays).",
176
+ kind: CliOptionKind.String,
177
+ argMin: 0,
178
+ argMax: 1,
179
+ },
180
+ ],
181
+ handler: (ctx) => {
182
+ const key = ctx.args[0];
183
+ if (!key) {
184
+ process.stderr.write("config set requires a key.\n");
185
+ process.exit(1);
186
+ }
187
+ const raw = ctx.args[1];
188
+ if (raw === undefined || raw.length === 0) {
189
+ if (!ctx.hasFlag("json")) {
190
+ process.stderr.write("config set requires a value (or --json).\n");
191
+ process.exit(1);
192
+ }
193
+ process.stderr.write("config set requires a value.\n");
194
+ process.exit(1);
195
+ }
196
+ configSetRun(program, key, raw, ctx.hasFlag("json"));
197
+ },
198
+ };
199
+ }
200
+
201
+ import { configCommandsEnabled, configMcpSetEnabled } from "../config/entry.ts";
202
+
203
+ /** Built-in `config` router when program.appConfig is set. */
204
+ export function cliBuiltinConfigGroup(program: CliProgram): CliRouter {
205
+ if (!program.appConfig) {
206
+ throw new Error("config not enabled");
207
+ }
208
+ return {
209
+ key: "config",
210
+ description: "Read or write app configuration.",
211
+ commands: [configGetLeaf(program), configSetLeaf(program, configMcpSetEnabled(program))],
212
+ };
213
+ }
214
+
215
+ export function cliBuiltinConfigGroupIfEnabled(program: CliProgram): CliRouter | undefined {
216
+ if (!configCommandsEnabled(program)) {
217
+ return undefined;
218
+ }
219
+ return cliBuiltinConfigGroup(program);
220
+ }
@@ -1,8 +1,8 @@
1
- import { resolveCapabilities } from "../capabilities.ts";
1
+ import { capabilityDeniedMessage, resolveCapabilities } from "../capabilities.ts";
2
+ import { Cli } from "../cli.ts";
2
3
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
3
4
  import { cliInstall } from "../install/index.ts";
4
5
  import { runMcpBundle } from "../mcp/bundle.ts";
5
- import { cliMcpServeStdio } from "../mcp.ts";
6
6
  import type { ParseResult } from "../parse.ts";
7
7
  import { ParseKind } from "../parse.ts";
8
8
  import type { CliNode, CliProgram, CliRouter } from "../types.ts";
@@ -11,6 +11,7 @@ import { completionBashScript } from "./completion-bash.ts";
11
11
  import { completionFishScript } from "./completion-fish.ts";
12
12
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
13
13
  import { completionZshScript } from "./completion-zsh.ts";
14
+ import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
14
15
  import { cliBuiltinInstallCommand } from "./install.ts";
15
16
  import { cliBuiltinMcpCommand } from "./mcp.ts";
16
17
  import { cliPresentationRoot } from "./presentation.ts";
@@ -70,14 +71,12 @@ export async function dispatchBuiltin(
70
71
 
71
72
  if (pr.path[0] === "mcp") {
72
73
  if (!caps.mcp) {
73
- process.stderr.write(
74
- "MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n",
75
- );
74
+ process.stderr.write(capabilityDeniedMessage("mcp"));
76
75
  process.exit(1);
77
76
  }
78
77
  const sub = pr.path[1];
79
78
  if (pr.path.length === 1 || sub === "serve") {
80
- await cliMcpServeStdio(program);
79
+ await new Cli(program).serveMcp();
81
80
  process.exit(0);
82
81
  }
83
82
  if (pr.path.length === 2 && sub === "bundle") {
@@ -95,9 +94,7 @@ export async function dispatchBuiltin(
95
94
 
96
95
  if (pr.path[0] === "install") {
97
96
  if (!caps.install) {
98
- process.stderr.write(
99
- "install is disabled. Remove install.enabled: false from the program root.\n",
100
- );
97
+ process.stderr.write(capabilityDeniedMessage("install"));
101
98
  process.exit(1);
102
99
  }
103
100
  if (pr.path.length !== 1) {
@@ -176,5 +173,17 @@ export function builtinInterceptRoot(
176
173
  };
177
174
  }
178
175
 
176
+ const configGroup = cliBuiltinConfigGroupIfEnabled(program);
177
+ if (first === "config" && configGroup) {
178
+ return {
179
+ parseRoot: {
180
+ key: program.key,
181
+ description: program.description,
182
+ commands: [configGroup],
183
+ },
184
+ isLeafCompletionIntercept: false,
185
+ };
186
+ }
187
+
179
188
  return { parseRoot: program, isLeafCompletionIntercept: false };
180
189
  }
@@ -1,19 +1,15 @@
1
- import { type CliCapabilities, resolveCapabilities } from "../capabilities.ts";
2
- import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
1
+ import { resolveCapabilities } from "../capabilities.ts";
3
2
  import { visibleOptions } from "../hidden.ts";
4
3
  import type { CliFallbackMode, CliNode, CliOption, CliPositional, CliProgram } from "../types.ts";
5
4
  import { isCliRouter } from "../types.ts";
6
- import { cliBuiltinCompletionGroup } from "./completion-group.ts";
7
- import { cliBuiltinInstallCommand } from "./install.ts";
8
- import { cliBuiltinMcpCommand } from "./mcp.ts";
9
- import { cliBuiltinVersionCommand } from "./version.ts";
5
+ import { resolveBuiltins } from "./registry.ts";
10
6
 
11
7
  /** JSON-safe command node (no handlers). */
12
8
  export interface CliSchemaExport {
13
9
  key: string;
14
10
  description: string;
15
11
  notes?: string;
16
- /** JSON Schema for structured stdout when set on the leaf `mcpTool`. */
12
+ /** JSON Schema for structured stdout when set on the leaf. */
17
13
  outputSchema?: Record<string, unknown>;
18
14
  options?: CliOption[];
19
15
  fallbackCommand?: string;
@@ -55,31 +51,10 @@ function exportBuiltinNode(cmd: CliNode): CliSchemaExport | null {
55
51
  return out;
56
52
  }
57
53
 
58
- function pushExportedBuiltin(builtins: CliSchemaExport[], cmd: CliNode): void {
59
- const node = exportBuiltinNode(cmd);
60
- if (node) {
61
- builtins.push(node);
62
- }
63
- }
64
-
65
54
  /** Built-in subtrees matching help visibility for `--schema` export. */
66
- export function exportPresentationBuiltins(
67
- program: CliProgram,
68
- caps?: CliCapabilities,
69
- ): CliSchemaExport[] {
70
- const resolved = caps ?? resolveCapabilities(program);
71
- const builtins: CliSchemaExport[] = [];
72
- pushExportedBuiltin(builtins, cliBuiltinCompletionGroup(program));
73
- pushExportedBuiltin(builtins, cliBuiltinVersionCommand());
74
- if (resolved.install) {
75
- pushExportedBuiltin(builtins, cliBuiltinInstallCommand(program));
76
- }
77
- const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
78
- if (docsGroup) {
79
- pushExportedBuiltin(builtins, docsGroup);
80
- }
81
- if (resolved.mcp) {
82
- pushExportedBuiltin(builtins, cliBuiltinMcpCommand(program));
83
- }
84
- return builtins;
55
+ export function exportPresentationBuiltins(program: CliProgram): CliSchemaExport[] {
56
+ const caps = resolveCapabilities(program);
57
+ return resolveBuiltins(program, caps)
58
+ .map((cmd) => exportBuiltinNode(cmd))
59
+ .filter((node): node is CliSchemaExport => node !== null);
85
60
  }
@@ -12,4 +12,5 @@ export {
12
12
  parseBuiltins,
13
13
  presentationBuiltins,
14
14
  } from "./presentation.ts";
15
+ export { resolveBuiltins } from "./registry.ts";
15
16
  export { collectScopes, type ScopeRec } from "./scopes.ts";
@@ -92,6 +92,19 @@ export function installBuiltinOptions(root: CliProgram): CliOption[] {
92
92
  });
93
93
  }
94
94
 
95
+ if (root.appConfig) {
96
+ opts.push({
97
+ name: "configure",
98
+ description: "Interactively write app config (~/.config/<key>/config).",
99
+ kind: CliOptionKind.Presence,
100
+ });
101
+ opts.push({
102
+ name: "config",
103
+ description: "Remove app config file (with --uninstall).",
104
+ kind: CliOptionKind.Presence,
105
+ });
106
+ }
107
+
95
108
  return opts;
96
109
  }
97
110
 
@@ -28,7 +28,7 @@ export function cliBuiltinMcpCommand(program: CliProgram): CliRouter {
28
28
 
29
29
  const bundle: CliLeaf = {
30
30
  key: "bundle",
31
- description: "Pack a Claude Desktop MCP Bundle (.mcpb) from dist/<key>.",
31
+ description: "Pack Claude Desktop `.mcpb` and Claude Code plugin zip from dist/<key>.",
32
32
  handler: () => {},
33
33
  };
34
34
 
@@ -1,28 +1,13 @@
1
1
  import type { CliCapabilities } from "../capabilities.ts";
2
2
  import { resolveCapabilities } from "../capabilities.ts";
3
- import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
4
3
  import { presentationNode, visibleOptions } from "../hidden.ts";
5
4
  import type { CliLeaf, CliNode, CliProgram, CliRouter } from "../types.ts";
6
5
  import { isCliLeaf } from "../types.ts";
7
- import { cliBuiltinCompletionGroup } from "./completion-group.ts";
8
- import { cliBuiltinInstallCommand } from "./install.ts";
9
- import { cliBuiltinMcpCommand } from "./mcp.ts";
10
- import { cliBuiltinVersionCommand } from "./version.ts";
6
+ import { resolveBuiltins } from "./registry.ts";
11
7
 
12
8
  /** All built-in command nodes for argv parsing (includes hidden builtins). */
13
9
  export function parseBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
14
- const builtins: CliNode[] = [cliBuiltinCompletionGroup(program), cliBuiltinVersionCommand()];
15
- if (caps.install) {
16
- builtins.push(cliBuiltinInstallCommand(program));
17
- }
18
- const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
19
- if (docsGroup) {
20
- builtins.push(docsGroup);
21
- }
22
- if (caps.mcp) {
23
- builtins.push(cliBuiltinMcpCommand(program));
24
- }
25
- return builtins;
10
+ return resolveBuiltins(program, caps);
26
11
  }
27
12
 
28
13
  /** Built-in subtrees visible in help, schema, and completions (hidden builtins omitted). */
@@ -0,0 +1,40 @@
1
+ import type { CliCapabilities } from "../capabilities.ts";
2
+ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
3
+ import type { CliNode, CliProgram } from "../types.ts";
4
+ import { cliBuiltinCompletionGroup } from "./completion-group.ts";
5
+ import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
6
+ import { cliBuiltinInstallCommand } from "./install.ts";
7
+ import { cliBuiltinMcpCommand } from "./mcp.ts";
8
+ import { cliBuiltinVersionCommand } from "./version.ts";
9
+
10
+ type BuiltinFactory = (program: CliProgram) => CliNode | null;
11
+
12
+ function pushBuiltin(
13
+ builtins: CliNode[],
14
+ program: CliProgram,
15
+ factory: BuiltinFactory | null,
16
+ ): void {
17
+ if (!factory) {
18
+ return;
19
+ }
20
+ const node = factory(program);
21
+ if (node) {
22
+ builtins.push(node);
23
+ }
24
+ }
25
+
26
+ /** Capability-gated built-in command nodes in stable order (parse, help, export). */
27
+ export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
28
+ const builtins: CliNode[] = [];
29
+ pushBuiltin(builtins, program, (p) => cliBuiltinCompletionGroup(p));
30
+ pushBuiltin(builtins, program, () => cliBuiltinVersionCommand());
31
+ if (caps.install) {
32
+ pushBuiltin(builtins, program, (p) => cliBuiltinInstallCommand(p));
33
+ }
34
+ pushBuiltin(builtins, program, (p) => cliBuiltinDocsGroupIfEnabled(p) ?? null);
35
+ if (caps.mcp) {
36
+ pushBuiltin(builtins, program, (p) => cliBuiltinMcpCommand(p));
37
+ }
38
+ pushBuiltin(builtins, program, (p) => cliBuiltinConfigGroupIfEnabled(p) ?? null);
39
+ return builtins;
40
+ }
@@ -3,6 +3,7 @@ Internal capability resolver — decides which platform builtins are active for
3
3
  Not exported from the public package barrel.
4
4
  */
5
5
 
6
+ import { configCommandsEnabled } from "./config/entry.ts";
6
7
  import type { CliProgram } from "./types.ts";
7
8
 
8
9
  /** Platform builtins derived from program config and runtime. */
@@ -12,6 +13,7 @@ export interface CliCapabilities {
12
13
  install: boolean;
13
14
  docs: boolean;
14
15
  update: boolean;
16
+ configCommands: boolean;
15
17
  }
16
18
 
17
19
  /** Resolves which capabilities are enabled for a program. */
@@ -23,6 +25,7 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
23
25
  install,
24
26
  docs: program.docs?.enabled === true,
25
27
  update: install && typeof program.install?.updateGetLatest === "function",
28
+ configCommands: configCommandsEnabled(program),
26
29
  };
27
30
  }
28
31
 
@@ -38,5 +41,48 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
38
41
  if (caps.mcp) {
39
42
  names.push("mcp");
40
43
  }
44
+ if (caps.configCommands) {
45
+ names.push("config");
46
+ }
41
47
  return names;
42
48
  }
49
+
50
+ export type CapabilityFeature = "mcp" | "install" | "docs" | "config";
51
+
52
+ /** Stderr message when a disabled built-in is invoked from the CLI. */
53
+ export function capabilityDeniedMessage(feature: CapabilityFeature): string {
54
+ switch (feature) {
55
+ case "mcp":
56
+ return "MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n";
57
+ case "install":
58
+ return "install is disabled. Remove install.enabled: false from the program root.\n";
59
+ case "docs":
60
+ return "docs is not enabled. Set docs: { enabled: true } on the program root.\n";
61
+ case "config":
62
+ return "config commands are disabled. Set program.appConfig or enable config.commands.\n";
63
+ }
64
+ }
65
+
66
+ /** Exit 1 when argv[0] names a built-in that capabilities disallow. */
67
+ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): void {
68
+ if (argv.length < 1) {
69
+ return;
70
+ }
71
+ const first = argv[0];
72
+ if (first === "mcp" && !caps.mcp) {
73
+ process.stderr.write(capabilityDeniedMessage("mcp"));
74
+ process.exit(1);
75
+ }
76
+ if (first === "install" && !caps.install) {
77
+ process.stderr.write(capabilityDeniedMessage("install"));
78
+ process.exit(1);
79
+ }
80
+ if (first === "docs" && !caps.docs) {
81
+ process.stderr.write(capabilityDeniedMessage("docs"));
82
+ process.exit(1);
83
+ }
84
+ if (first === "config" && !caps.configCommands) {
85
+ process.stderr.write(capabilityDeniedMessage("config"));
86
+ process.exit(1);
87
+ }
88
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ Handler error helper with contextual help.
3
+ */
4
+
5
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
6
+ import type { CliContext } from "./context.ts";
7
+ import { cliHelpRender } from "./help.ts";
8
+
9
+ export function cliErrWithHelp(ctx: CliContext, msg: string): never {
10
+ const color = process.stderr.isTTY;
11
+ const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
12
+ process.stderr.write(`${line}\n`);
13
+ process.stderr.write(cliHelpRender(cliPresentationRoot(ctx.program), ctx.commandPath, true));
14
+ process.exit(1);
15
+ }