argsbarg 3.6.4 → 4.0.1

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 +32 -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 +158 -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 +79 -0
  55. package/src/config/context.ts +110 -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,23 @@
1
+ /**
2
+ * Config schema
3
+ *
4
+ * Application settings for `consumer-app` (`program.appConfig`).
5
+ */
6
+ export interface AppConfig {
7
+ /** API token from the provider dashboard. */
8
+ apiToken: string;
9
+ /**
10
+ * AWS region (default us-east-1).
11
+ * @default us-east-1
12
+ */
13
+ defaultRegion?: string;
14
+ /**
15
+ * HTTP retry count (default 3).
16
+ * @default 3
17
+ */
18
+ maxRetries: number;
19
+ /** Local preferences (file-only; not mapped to process.env). */
20
+ prefs?: {
21
+ ttl: number;
22
+ };
23
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "resolveJsonModule": true,
8
+ "noEmit": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "types": ["bun"]
12
+ },
13
+ "include": ["src/**/*.ts", "scripts/**/*.ts"]
14
+ }
@@ -6,9 +6,15 @@
6
6
  */
7
7
 
8
8
  import pkg from "../package.json" with { type: "json" };
9
- import { cliRun, CliFallbackMode, CliOptionKind, CliValueFormat, type CliProgram } from "../src/index.ts";
9
+ import {
10
+ Cli,
11
+ CliFallbackMode,
12
+ CliOptionKind,
13
+ type CliProgram,
14
+ CliValueFormat,
15
+ } from "../src/index.ts";
10
16
 
11
- const cli = {
17
+ const program = {
12
18
  key: "formats.ts",
13
19
  version: pkg.version,
14
20
  description: "Value formats and readLeafInputs demo.",
@@ -63,4 +69,5 @@ const cli = {
63
69
  ],
64
70
  } satisfies CliProgram;
65
71
 
66
- await cliRun(cli);
72
+ const cli = new Cli(program);
73
+ await cli.run();
@@ -3,17 +3,38 @@
3
3
  MCP test fixture for subprocess integration tests only.
4
4
  */
5
5
 
6
- import { cliRun, CliProgram, CliOptionKind } from "../src/index.ts";
6
+ import { Cli, CliOptionKind, type CliProgram } from "../src/index.ts";
7
7
 
8
- const envFilePath = process.env.ARGS_TEST_ENV_FILE;
8
+ const configPath = process.env.ARGS_TEST_CONFIG_FILE;
9
9
 
10
- const cli = {
10
+ const program = {
11
11
  key: "mcp-test",
12
12
  version: "0.0.0-test",
13
13
  description: "MCP integration test fixture.",
14
+ ...(configPath
15
+ ? {
16
+ appConfig: {
17
+ path: configPath,
18
+ entries: {
19
+ argsTestSecret: {
20
+ description: "Test secret for integration tests.",
21
+ env: "ARGS_TEST_SECRET",
22
+ },
23
+ },
24
+ },
25
+ }
26
+ : {
27
+ appConfig: {
28
+ entries: {
29
+ argsTestSecret: {
30
+ description: "Test secret for integration tests.",
31
+ env: "ARGS_TEST_SECRET",
32
+ },
33
+ },
34
+ },
35
+ }),
14
36
  mcpServer: {
15
37
  enabled: true,
16
- ...(envFilePath ? { envFile: envFilePath } : {}),
17
38
  resources: [
18
39
  {
19
40
  uri: "test://hello",
@@ -28,9 +49,6 @@ const cli = {
28
49
  {
29
50
  key: "echo-env",
30
51
  description: "Echo an env var.",
31
- mcpTool: {
32
- requiresEnv: ["ARGS_TEST_SECRET"],
33
- },
34
52
  options: [
35
53
  {
36
54
  name: "name",
@@ -63,4 +81,5 @@ const cli = {
63
81
  ],
64
82
  } satisfies CliProgram;
65
83
 
66
- await cliRun(cli);
84
+ const cli = new Cli(program);
85
+ await cli.run();
@@ -8,9 +8,9 @@ It demonstrates the minimal Bun integration path.
8
8
  */
9
9
 
10
10
  import pkg from "../package.json" with { type: "json" };
11
- import { cliRun, CliProgram, CliOptionKind } from "../src/index.ts";
11
+ import { Cli, CliOptionKind, type CliProgram } from "../src/index.ts";
12
12
 
13
- const cli = {
13
+ const program = {
14
14
  key: "minimal.ts",
15
15
  version: pkg.version,
16
16
  description: "Tiny demo.",
@@ -46,4 +46,5 @@ const cli = {
46
46
  },
47
47
  } satisfies CliProgram;
48
48
 
49
- await cliRun(cli);
49
+ const cli = new Cli(program);
50
+ await cli.run();
@@ -8,9 +8,9 @@ It demonstrates how the schema scales beyond one command.
8
8
  */
9
9
 
10
10
  import pkg from "../package.json" with { type: "json" };
11
- import { cliRun, CliProgram, CliOptionKind, CliFallbackMode } from "../src/index.ts";
11
+ import { Cli, CliFallbackMode, CliOptionKind, type CliProgram } from "../src/index.ts";
12
12
 
13
- const cli = {
13
+ const program = {
14
14
  key: "nested.ts",
15
15
  version: pkg.version,
16
16
  description: "Nested groups demo.",
@@ -99,7 +99,7 @@ const cli = {
99
99
  const text = await file.text();
100
100
  const firstLine = text.split("\n")[0];
101
101
  console.log(`${path}: ${firstLine}`);
102
- } catch (err) {
102
+ } catch {
103
103
  console.error(`Cannot open: ${path}`);
104
104
  }
105
105
  }
@@ -110,4 +110,5 @@ const cli = {
110
110
  fallbackMode: CliFallbackMode.MissingOrUnknown,
111
111
  } satisfies CliProgram;
112
112
 
113
- await cliRun(cli);
113
+ const cli = new Cli(program);
114
+ await cli.run();
@@ -8,9 +8,9 @@ It demonstrates the minimal Bun integration path.
8
8
  */
9
9
 
10
10
  import pkg from "../package.json" with { type: "json" };
11
- import { cliRun, CliProgram, CliOptionKind, CliFallbackMode, isInteractiveTty } from "../src/index.ts";
11
+ import { Cli, CliOptionKind, type CliProgram, isInteractiveTty } from "../src/index.ts";
12
12
 
13
- const cli = {
13
+ const program = {
14
14
  key: "option-required.ts",
15
15
  version: pkg.version,
16
16
  description: "Demo of a required option.",
@@ -37,7 +37,10 @@ const cli = {
37
37
  },
38
38
  ],
39
39
  handler: (ctx) => {
40
- const requiredAlways = ctx.stringOpt("requiredAlways")!;
40
+ const requiredAlways = ctx.stringOpt("requiredAlways");
41
+ if (requiredAlways === undefined) {
42
+ throw new Error("requiredAlways missing after validation");
43
+ }
41
44
  const requiredNonTty = ctx.stringOpt("requiredNonTty") ?? "valueWhenOmitted";
42
45
  const optional = ctx.stringOpt("optional") ?? "valueWhenOmitted";
43
46
  console.log(`requiredAlways: ${requiredAlways}`);
@@ -46,4 +49,5 @@ const cli = {
46
49
  },
47
50
  } satisfies CliProgram;
48
51
 
49
- await cliRun(cli);
52
+ const cli = new Cli(program);
53
+ await cli.run();
package/index.d.ts CHANGED
@@ -1,5 +1,75 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export type ResolvedConfig = Record<string, unknown>;
4
+ declare class EmptyAppConfigSnapshot {
5
+ private readonly program;
6
+ constructor(program: CliProgram);
7
+ get(_key: string): undefined;
8
+ require(key: string): never;
9
+ set(_key: string, _value: unknown): void;
10
+ read(): ResolvedConfig;
11
+ /** Resolved absolute path to the app JSON config file (OS default from `program.key`). */
12
+ get path(): string;
13
+ }
14
+ declare class AppConfigSnapshot {
15
+ private readonly program;
16
+ private snapshot;
17
+ private fileData;
18
+ constructor(program: CliProgram, fileData: Record<string, unknown>, resolved: ResolvedConfig);
19
+ get(key: string): unknown;
20
+ require(key: string): unknown;
21
+ set(key: string, value: unknown): void;
22
+ read(): ResolvedConfig;
23
+ /** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
24
+ get path(): string;
25
+ /** Replace snapshot after external bootstrap (internal). */
26
+ refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void;
27
+ private assertEntryKey;
28
+ }
29
+ export type AnyAppConfigSnapshot = AppConfigSnapshot | EmptyAppConfigSnapshot;
30
+ /** Coerced leaf inputs keyed by option and positional names. */
31
+ export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
32
+ /**
33
+ * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
34
+ */
35
+ export declare class CliContext {
36
+ readonly appName: string;
37
+ readonly commandPath: string[];
38
+ readonly args: string[];
39
+ readonly program: CliProgram;
40
+ readonly opts: Record<string, string>;
41
+ readonly invocation: CliInvocation;
42
+ readonly appConfig: AnyAppConfigSnapshot;
43
+ /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
44
+ constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation, appConfig?: AnyAppConfigSnapshot);
45
+ /** Returns whether a presence flag was set (including implicit "1" for boolean options). */
46
+ hasFlag(name: string): boolean;
47
+ /** Returns the string value for a string-valued option, if present. */
48
+ stringOpt(name: string): string | undefined;
49
+ /** Parses a stored string as a number; returns null if missing or not a strict double string. */
50
+ numberOpt(name: string): number | null;
51
+ /**
52
+ * Generic typed accessor: parses a stored string using the provided parse function.
53
+ * This is the TypeScript-native advantage over the Swift version.
54
+ */
55
+ typedOpt<T>(name: string, parse: (s: string) => T): T | null;
56
+ /** Duration option in milliseconds (post-parse validated). */
57
+ durationOpt(name: string): number | undefined;
58
+ /** Comma-list option as a string array (post-parse validated). */
59
+ commaListOpt(name: string): string[] | undefined;
60
+ /** Date option as canonical YYYY-MM-DD (post-parse validated). */
61
+ dateOpt(name: string): string | undefined;
62
+ /** Date-time option as normalized ISO 8601 UTC (post-parse validated). */
63
+ dateTimeOpt(name: string): string | undefined;
64
+ /** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
65
+ positional(name: string): string | string[] | undefined;
66
+ /** Reads coerced option and positional values for the current leaf from schema metadata. */
67
+ readLeafInputs(): CliLeafInputs;
68
+ private _readOptionValue;
69
+ private _leafNode;
70
+ private _posMap;
71
+ private _positionalMap;
72
+ }
3
73
  /**
4
74
  * How a leaf handler was dispatched.
5
75
  */
@@ -128,12 +198,6 @@ export interface CliMcpServerConfig {
128
198
  * and shell exports that MCP hosts (e.g. Cursor) don't inherit.
129
199
  */
130
200
  shellEnv?: boolean | string;
131
- /**
132
- * Path to a .env file loaded into process.env at MCP server start, after shellEnv.
133
- * Supports `~` expansion. Warns on stderr if the file does not exist.
134
- * Always overwrites — envFile is authoritative for its keys.
135
- */
136
- envFile?: string;
137
201
  /**
138
202
  * Custom MCP resources exposed alongside the built-in schema resource.
139
203
  * URIs must be unique and must not equal schemaResourceUri.
@@ -168,12 +232,6 @@ export interface CliMcpToolConfig {
168
232
  * Default: auto-generated from command path and description.
169
233
  */
170
234
  description?: string;
171
- /**
172
- * Environment variable names required at runtime.
173
- * Appended to auto-generated MCP tool descriptions; enforced at tools/call time.
174
- * Empty string counts as absent.
175
- */
176
- requiresEnv?: string[];
177
235
  /**
178
236
  * @deprecated Set `outputSchema` on the leaf command instead.
179
237
  */
@@ -194,6 +252,43 @@ export interface CliUpdateArtifact {
194
252
  export type CliUpdateGetLatest = (ctx: {
195
253
  version: string;
196
254
  }) => Promise<CliUpdateArtifact>;
255
+ /**
256
+ * Metadata overlay for one key in {@link CliAppConfig.entries}.
257
+ * Types and validation come from {@link CliAppConfig.jsonSchema} when set; otherwise all values are strings.
258
+ */
259
+ export interface CliAppConfigEntry {
260
+ /** Help text for prompts, MCP manifests, and generated docs. */
261
+ description: string;
262
+ /** Short label in host UIs and CLI prompts. Default: the config key. */
263
+ title?: string;
264
+ /** Default when `jsonSchema` is omitted (all-string mode). */
265
+ default?: string;
266
+ /** When `false`, optional for bootstrap and MCP enforcement. Default: `true`. */
267
+ required?: boolean;
268
+ /**
269
+ * Mask stdin during prompts and redact on `config get`.
270
+ * Default: `/key|token|secret|password/i.test(name)`.
271
+ */
272
+ sensitive?: boolean;
273
+ /** When set: non-empty `process.env[env]` overrides file; value exported after resolve. */
274
+ env?: string;
275
+ }
276
+ /**
277
+ * App configuration block on the program root ({@link CliProgram.appConfig}).
278
+ */
279
+ export interface CliAppConfig {
280
+ /** Default: `~/.config/<sanitized-key>/config` (or `%APPDATA%/<key>/config` on Windows). */
281
+ path?: string;
282
+ /** Built-in `config get` / `config set`. Default: enabled when `appConfig` is set. */
283
+ commands?: boolean | {
284
+ enabled?: boolean;
285
+ mcpSet?: boolean;
286
+ };
287
+ /** Block JSON Schema (draft-07). When omitted, synthesize all-string schema from `entries`. */
288
+ jsonSchema?: Record<string, unknown>;
289
+ /** Per-key metadata; keys must match `jsonSchema.properties` when `jsonSchema` is set. */
290
+ entries: Record<string, CliAppConfigEntry>;
291
+ }
197
292
  export interface CliInstallConfig {
198
293
  /** When `false`, hide/disable `install` (default: enabled). */
199
294
  enabled?: boolean;
@@ -278,12 +373,14 @@ export type CliRouter = CliNodeBase & {
278
373
  */
279
374
  export type CliNode = CliLeaf | CliRouter;
280
375
  /**
281
- * Program root passed to `cliRun` / `cliInvoke`.
376
+ * Program root passed to {@link Cli}.
282
377
  * May be a leaf or router, plus optional program-level MCP and install config.
283
378
  */
284
379
  export type CliProgram = CliNode & {
285
380
  /** Program version (printed by the `version` built-in and MCP serverInfo). */
286
381
  version: string;
382
+ /** Schema-driven app config file, bootstrap, and MCP metadata. */
383
+ appConfig?: CliAppConfig;
287
384
  /** When set with `enabled: true`, enables the `mcp` built-in subcommand. */
288
385
  mcpServer?: CliMcpServerConfig;
289
386
  /** Opt-out and defaults for `install`. */
@@ -303,48 +400,56 @@ export declare class CliSchemaValidationError extends Error {
303
400
  /** Creates a schema validation error with a human-readable rule violation. */
304
401
  constructor(message: string);
305
402
  }
306
- /** Coerced leaf inputs keyed by option and positional names. */
307
- export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
308
- /**
309
- * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
310
- */
311
- export declare class CliContext {
312
- readonly appName: string;
313
- readonly commandPath: string[];
314
- readonly args: string[];
403
+ /** Platform builtins derived from program config and runtime. */
404
+ export interface CliCapabilities {
405
+ completion: true;
406
+ mcp: boolean;
407
+ install: boolean;
408
+ docs: boolean;
409
+ update: boolean;
410
+ configCommands: boolean;
411
+ }
412
+ /** JSON-safe command node (no handlers). */
413
+ export interface CliSchemaExport {
414
+ key: string;
415
+ description: string;
416
+ notes?: string;
417
+ /** JSON Schema for structured stdout when set on the leaf. */
418
+ outputSchema?: Record<string, unknown>;
419
+ options?: CliOption[];
420
+ fallbackCommand?: string;
421
+ fallbackMode?: CliFallbackMode;
422
+ commands?: CliSchemaExport[];
423
+ positionals?: CliPositional[];
424
+ }
425
+ /** Outcome of a non-exiting CLI invocation. */
426
+ export type CliInvokeKind = "ok" | "help" | "error";
427
+ /** Result of Cli.invoke: captured output and exit metadata without process.exit. */
428
+ export interface CliInvokeResult {
429
+ kind: CliInvokeKind;
430
+ exitCode: number;
431
+ stdout: string;
432
+ stderr: string;
433
+ errorMsg?: string;
434
+ }
435
+ /** Argsbarg runtime for a validated, frozen {@link CliProgram}. */
436
+ export declare class Cli {
315
437
  readonly program: CliProgram;
316
- readonly opts: Record<string, string>;
317
- readonly invocation: CliInvocation;
318
- /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
319
- constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation);
320
- /** Returns whether a presence flag was set (including implicit "1" for boolean options). */
321
- hasFlag(name: string): boolean;
322
- /** Returns the string value for a string-valued option, if present. */
323
- stringOpt(name: string): string | undefined;
324
- /** Parses a stored string as a number; returns null if missing or not a strict double string. */
325
- numberOpt(name: string): number | null;
326
- /**
327
- * Generic typed accessor: parses a stored string using the provided parse function.
328
- * This is the TypeScript-native advantage over the Swift version.
329
- */
330
- typedOpt<T>(name: string, parse: (s: string) => T): T | null;
331
- /** Duration option in milliseconds (post-parse validated). */
332
- durationOpt(name: string): number | undefined;
333
- /** Comma-list option as a string array (post-parse validated). */
334
- commaListOpt(name: string): string[] | undefined;
335
- /** Date option as canonical YYYY-MM-DD (post-parse validated). */
336
- dateOpt(name: string): string | undefined;
337
- /** Date-time option as normalized ISO 8601 UTC (post-parse validated). */
338
- dateTimeOpt(name: string): string | undefined;
339
- /** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
340
- positional(name: string): string | string[] | undefined;
341
- /** Reads coerced option and positional values for the current leaf from schema metadata. */
342
- readLeafInputs(): CliLeafInputs;
343
- private _readOptionValue;
344
- private _leafNode;
345
- private _posMap;
346
- private _positionalMap;
438
+ readonly caps: CliCapabilities;
439
+ private readonly parseRootMerged;
440
+ private readonly presentationRoot;
441
+ private _appConfig?;
442
+ constructor(program: CliProgram);
443
+ get appConfig(): AnyAppConfigSnapshot;
444
+ exportCommandSchema(): CliSchemaExport;
445
+ exportAppConfigSchema(): Record<string, unknown> | undefined;
446
+ run(argv?: string[]): Promise<never>;
447
+ invoke(argv: string[]): Promise<CliInvokeResult>;
448
+ serveMcp(): Promise<never>;
449
+ private prepareDispatch;
450
+ private buildAppConfigSnapshot;
347
451
  }
452
+ export declare function cliErrWithHelp(ctx: CliContext, msg: string): never;
348
453
  /** Parses a duration string (e.g. 20m, 1h, 30s) into milliseconds. */
349
454
  export declare function parseDurationMs(durationStr: string): number;
350
455
  /** Splits a comma-separated string into trimmed non-empty tokens. */
@@ -426,26 +531,6 @@ export declare function createGhVersionCheck(config: GhVersionCheckConfig): {
426
531
  };
427
532
  /** Shared `gh release view` fetcher for hooks and version-check refresh. */
428
533
  export declare function createGhFetchLatest(config: Pick<GhReleaseUpdateConfig, "repo" | "repoEnvHint">): () => Promise<string>;
429
- /** Outcome of a non-exiting CLI invocation. */
430
- export type CliInvokeKind = "ok" | "help" | "error";
431
- /** Result of cliInvoke: captured output and exit metadata without process.exit. */
432
- export interface CliInvokeResult {
433
- /** Invocation outcome. */
434
- kind: CliInvokeKind;
435
- /** Simulated exit code. */
436
- exitCode: number;
437
- /** Captured stdout during handler execution. */
438
- stdout: string;
439
- /** Captured stderr during handler execution. */
440
- stderr: string;
441
- /** Set when kind === "error" (parse/validation message). */
442
- errorMsg?: string;
443
- }
444
- /**
445
- * Parses argv against the user root, runs the leaf handler, and returns captured output.
446
- * Never calls process.exit.
447
- */
448
- export declare function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult>;
449
534
  /** Resolved paths for `mcp bundle`. */
450
535
  export interface McpBundlePaths {
451
536
  binaryPath: string;
@@ -466,8 +551,6 @@ export interface PackMcpBundleOpts {
466
551
  * Requires the compiled binary to exist.
467
552
  */
468
553
  export declare function packMcpBundle(program: CliProgram, opts?: PackMcpBundleOpts): string;
469
- export declare function cliRun(program: CliProgram, argv?: string[]): Promise<never>;
470
- export declare function cliErrWithHelp(ctx: CliContext, msg: string): never;
471
554
  /** True when stdin is a TTY. */
472
555
  export declare const isInteractiveTty: boolean;
473
556
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.6.4",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -127,6 +127,19 @@ describe("completion emitters", () => {
127
127
  });
128
128
 
129
129
  describe("schema export builtins", () => {
130
+ test("exportPresentationBuiltins includes config when appConfig set", () => {
131
+ const withConfig: CliProgram = {
132
+ ...fixture,
133
+ appConfig: {
134
+ entries: {
135
+ apiToken: { description: "Token.", env: "API_TOKEN" },
136
+ },
137
+ },
138
+ };
139
+ const builtins = exportPresentationBuiltins(withConfig);
140
+ expect(builtins.map((b) => b.key)).toContain("config");
141
+ });
142
+
130
143
  test("exportPresentationBuiltins includes install options", () => {
131
144
  const builtins = exportPresentationBuiltins(fixture);
132
145
  const install = builtins.find((b) => b.key === "install");
@@ -0,0 +1,82 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Cli, type CliProgram } from "../index.ts";
6
+
7
+ function configFixture(configPath: string): CliProgram {
8
+ return {
9
+ key: "cfg-app",
10
+ version: "1.0.0",
11
+ description: "Config builtin test.",
12
+ appConfig: {
13
+ path: configPath,
14
+ entries: {
15
+ apiToken: { description: "Token.", env: "API_TOKEN", sensitive: true },
16
+ port: { description: "Port.", required: false },
17
+ },
18
+ },
19
+ commands: [{ key: "run", description: "Run.", handler: () => {} }],
20
+ };
21
+ }
22
+
23
+ describe("builtins/config", () => {
24
+ test("config get redacts sensitive values", async () => {
25
+ const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
26
+ const configPath = join(dir, "config");
27
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
28
+ const prev = process.env.API_TOKEN;
29
+ delete process.env.API_TOKEN;
30
+ try {
31
+ const result = await new Cli(configFixture(configPath)).invoke(["config", "get", "apiToken"]);
32
+ expect(result.exitCode).toBe(0);
33
+ expect(result.stdout.trim()).toBe("REDACTED");
34
+ } finally {
35
+ if (prev !== undefined) process.env.API_TOKEN = prev;
36
+ rmSync(dir, { recursive: true, force: true });
37
+ }
38
+ });
39
+
40
+ test("config get --json redacts sensitive as { set: true }", async () => {
41
+ const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
42
+ const configPath = join(dir, "config");
43
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "secret" })}\n`);
44
+ const prev = process.env.API_TOKEN;
45
+ delete process.env.API_TOKEN;
46
+ try {
47
+ const result = await new Cli(configFixture(configPath)).invoke([
48
+ "config",
49
+ "get",
50
+ "apiToken",
51
+ "--json",
52
+ ]);
53
+ expect(result.exitCode).toBe(0);
54
+ expect(JSON.parse(result.stdout.trim())).toEqual({ set: true });
55
+ } finally {
56
+ if (prev !== undefined) process.env.API_TOKEN = prev;
57
+ rmSync(dir, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test("config set writes and resolves without required exit", async () => {
62
+ const dir = mkdtempSync(join(tmpdir(), "cfg-builtin-"));
63
+ const configPath = join(dir, "config");
64
+ writeFileSync(configPath, `${JSON.stringify({ apiToken: "present" })}\n`);
65
+ const prev = process.env.API_TOKEN;
66
+ delete process.env.API_TOKEN;
67
+ try {
68
+ const result = await new Cli(configFixture(configPath)).invoke([
69
+ "config",
70
+ "set",
71
+ "port",
72
+ "9090",
73
+ ]);
74
+ expect(result.exitCode).toBe(0);
75
+ const get = await new Cli(configFixture(configPath)).invoke(["config", "get", "port"]);
76
+ expect(get.stdout.trim()).toBe("9090");
77
+ } finally {
78
+ if (prev !== undefined) process.env.API_TOKEN = prev;
79
+ rmSync(dir, { recursive: true, force: true });
80
+ }
81
+ });
82
+ });