argsbarg 4.0.0 → 4.0.2
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/CHANGELOG.md +9 -1
- package/docs/cli-program.md +2 -2
- package/docs/config-schema.md +1 -1
- package/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/capabilities.ts +12 -0
- package/src/cli.ts +9 -4
- package/src/config/context.test.ts +18 -0
- package/src/config/context.ts +14 -2
- package/src/config.integration.test.ts +44 -0
- package/src/context.ts +1 -1
- package/src/docs/docs.test.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [4.0.2] - 2026-06-24
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [4.0.1] - 2026-06-24
|
|
14
|
+
|
|
15
|
+
|
|
10
16
|
## [4.0.0] - 2026-06-24
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -449,7 +455,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
449
455
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
450
456
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
451
457
|
|
|
452
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.
|
|
458
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.2...HEAD
|
|
459
|
+
[4.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.2
|
|
460
|
+
[4.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.1
|
|
453
461
|
[4.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.0
|
|
454
462
|
[3.6.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.6.4
|
|
455
463
|
[3.6.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.6.3
|
package/docs/cli-program.md
CHANGED
|
@@ -425,13 +425,13 @@ await cli.run();
|
|
|
425
425
|
- Default: `$XDG_CONFIG_HOME/<sanitized-key>/config` or `%APPDATA%/<key>/config`.
|
|
426
426
|
- JSON: flat object keyed by schema names — `{ "apiToken": "…", "maxRetries": 5 }`.
|
|
427
427
|
- **Strict:** unknown keys rejected on load.
|
|
428
|
-
- **CLI:** missing required config exits 1 before the leaf handler (TTY prompt when interactive). Built-in `config get`/`set` skip this exit.
|
|
428
|
+
- **CLI:** missing required config exits 1 before the leaf handler (TTY prompt when interactive). Built-in `docs` and `config get`/`set` skip this exit.
|
|
429
429
|
- **MCP:** server stays up; missing config returns `isError: true` at `tools/call`.
|
|
430
430
|
- **Configure:** `myapp install --configure` (not part of `--all`).
|
|
431
431
|
|
|
432
432
|
See [config-schema.md](config-schema.md) for codegen, [install.md](install.md), and [mcp.md](mcp.md).
|
|
433
433
|
|
|
434
|
-
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read` — prefer over `process.env` in handlers; env export remains for subprocess inheritance.
|
|
434
|
+
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read`, `path` — prefer over `process.env` in handlers; env export remains for subprocess inheritance. `path` is the resolved absolute config file path (`program.appConfig.path` when set, otherwise the OS default from `program.key`).
|
|
435
435
|
|
|
436
436
|
## Reserved names
|
|
437
437
|
|
package/docs/config-schema.md
CHANGED
|
@@ -42,7 +42,7 @@ await cli.run();
|
|
|
42
42
|
| `install --configure` / `--status` | Interactive setup and status |
|
|
43
43
|
| Built-in `config get` / `config set` | Read/write resolved values (opt-out via `commands: false`) |
|
|
44
44
|
| MCP bundle / Claude plugin | `userConfig` for entries with `env` set |
|
|
45
|
-
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read` — prefer over `process.env` |
|
|
45
|
+
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `path` — prefer over `process.env` |
|
|
46
46
|
|
|
47
47
|
**Validation at runtime** — argsbarg validates the config file and `config set` / `ctx.appConfig.set` against the effective JSON Schema (block `jsonSchema` or synthesized all-string schema).
|
|
48
48
|
|
package/index.d.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
export type ResolvedConfig = Record<string, unknown>;
|
|
4
4
|
declare class EmptyAppConfigSnapshot {
|
|
5
|
+
private readonly program;
|
|
6
|
+
constructor(program: CliProgram);
|
|
5
7
|
get(_key: string): undefined;
|
|
6
8
|
require(key: string): never;
|
|
7
9
|
set(_key: string, _value: unknown): void;
|
|
8
10
|
read(): ResolvedConfig;
|
|
11
|
+
/** Resolved absolute path to the app JSON config file (OS default from `program.key`). */
|
|
12
|
+
get path(): string;
|
|
9
13
|
}
|
|
10
14
|
declare class AppConfigSnapshot {
|
|
11
15
|
private readonly program;
|
|
@@ -16,6 +20,8 @@ declare class AppConfigSnapshot {
|
|
|
16
20
|
require(key: string): unknown;
|
|
17
21
|
set(key: string, value: unknown): void;
|
|
18
22
|
read(): ResolvedConfig;
|
|
23
|
+
/** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
|
|
24
|
+
get path(): string;
|
|
19
25
|
/** Replace snapshot after external bootstrap (internal). */
|
|
20
26
|
refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void;
|
|
21
27
|
private assertEntryKey;
|
package/package.json
CHANGED
package/src/capabilities.ts
CHANGED
|
@@ -47,6 +47,18 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
|
|
|
47
47
|
return names;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Commands that may run without required appConfig values (read-only / config introspection). */
|
|
51
|
+
export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities): boolean {
|
|
52
|
+
const root = path[0];
|
|
53
|
+
if (root === "config" && caps.configCommands) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (root === "docs" && caps.docs) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
export type CapabilityFeature = "mcp" | "install" | "docs" | "config";
|
|
51
63
|
|
|
52
64
|
/** Stderr message when a disabled built-in is invoked from the CLI. */
|
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,12 @@ Runtime entry point: validate program, cache derived state, run / invoke / MCP s
|
|
|
5
5
|
import { format } from "node:util";
|
|
6
6
|
import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
|
|
7
7
|
import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertBuiltinAllowed,
|
|
10
|
+
type CliCapabilities,
|
|
11
|
+
resolveCapabilities,
|
|
12
|
+
skipsRequiredAppConfigExit,
|
|
13
|
+
} from "./capabilities.ts";
|
|
9
14
|
import {
|
|
10
15
|
bootstrapAppConfig,
|
|
11
16
|
type EnsureAppConfigOpts,
|
|
@@ -113,10 +118,10 @@ export class Cli {
|
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
const
|
|
121
|
+
const skipRequiredConfig = skipsRequiredAppConfigExit(pr.path, this.caps);
|
|
117
122
|
const snapshot = this.buildAppConfigSnapshot({
|
|
118
|
-
interactive: !
|
|
119
|
-
exitOnMissing: !
|
|
123
|
+
interactive: !skipRequiredConfig && !!process.stdin.isTTY,
|
|
124
|
+
exitOnMissing: !skipRequiredConfig,
|
|
120
125
|
});
|
|
121
126
|
|
|
122
127
|
const ctx = new CliContext(
|
|
@@ -36,6 +36,7 @@ describe("config/context", () => {
|
|
|
36
36
|
|
|
37
37
|
expect(ctx.get("note")).toBe("hello");
|
|
38
38
|
expect(ctx.require("apiToken")).toBe("tok");
|
|
39
|
+
expect(ctx.path).toBe(path);
|
|
39
40
|
|
|
40
41
|
ctx.set("note", "updated");
|
|
41
42
|
expect(ctx.get("note")).toBe("updated");
|
|
@@ -57,5 +58,22 @@ describe("config/context", () => {
|
|
|
57
58
|
const empty = createAppConfigSnapshot(program, {}, {});
|
|
58
59
|
expect(empty.get("any")).toBeUndefined();
|
|
59
60
|
expect(() => empty.set("any", "v")).toThrow(/program.appConfig is not set/);
|
|
61
|
+
expect(empty.path).toContain("x");
|
|
62
|
+
expect(empty.path.endsWith("/config") || empty.path.endsWith("\\config")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("AppConfigSnapshot path uses OS default when program.appConfig.path omitted", () => {
|
|
66
|
+
const program: CliProgram = {
|
|
67
|
+
key: "ctx-test",
|
|
68
|
+
version: "1.0.0",
|
|
69
|
+
description: "Context test.",
|
|
70
|
+
appConfig: {
|
|
71
|
+
entries: { note: { description: "Note." } },
|
|
72
|
+
},
|
|
73
|
+
handler: () => {},
|
|
74
|
+
};
|
|
75
|
+
const ctx = createAppConfigSnapshot(program, {}, {});
|
|
76
|
+
expect(ctx.path).toContain("ctx_test");
|
|
77
|
+
expect(ctx.path.endsWith("/config") || ctx.path.endsWith("\\config")).toBe(true);
|
|
60
78
|
});
|
|
61
79
|
});
|
package/src/config/context.ts
CHANGED
|
@@ -3,12 +3,14 @@ Handler-facing resolved app config snapshot (ctx.appConfig).
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { CliProgram } from "../types.ts";
|
|
6
|
-
import { writeAppConfigFile } from "./file.ts";
|
|
6
|
+
import { resolveAppConfigPath, writeAppConfigFile } from "./file.ts";
|
|
7
7
|
import type { ResolvedConfig } from "./resolve.ts";
|
|
8
8
|
import { exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
|
|
9
9
|
|
|
10
10
|
/** Empty snapshot when program.appConfig is not set. */
|
|
11
11
|
export class EmptyAppConfigSnapshot {
|
|
12
|
+
constructor(private readonly program: CliProgram) {}
|
|
13
|
+
|
|
12
14
|
get(_key: string): undefined {
|
|
13
15
|
return undefined;
|
|
14
16
|
}
|
|
@@ -24,6 +26,11 @@ export class EmptyAppConfigSnapshot {
|
|
|
24
26
|
read(): ResolvedConfig {
|
|
25
27
|
return {};
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Resolved absolute path to the app JSON config file (OS default from `program.key`). */
|
|
31
|
+
get path(): string {
|
|
32
|
+
return resolveAppConfigPath(this.program);
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/** Resolved config for handlers with program.appConfig set. */
|
|
@@ -70,6 +77,11 @@ export class AppConfigSnapshot {
|
|
|
70
77
|
return { ...this.snapshot };
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
/** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
|
|
81
|
+
get path(): string {
|
|
82
|
+
return resolveAppConfigPath(this.program);
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
/** Replace snapshot after external bootstrap (internal). */
|
|
74
86
|
refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void {
|
|
75
87
|
this.fileData = { ...fileData };
|
|
@@ -92,7 +104,7 @@ export function createAppConfigSnapshot(
|
|
|
92
104
|
resolved: ResolvedConfig,
|
|
93
105
|
): AnyAppConfigSnapshot {
|
|
94
106
|
if (!program.appConfig) {
|
|
95
|
-
return new EmptyAppConfigSnapshot();
|
|
107
|
+
return new EmptyAppConfigSnapshot(program);
|
|
96
108
|
}
|
|
97
109
|
return new AppConfigSnapshot(program, fileData, resolved);
|
|
98
110
|
}
|
|
@@ -98,3 +98,47 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
|
|
|
98
98
|
expect(res.result.isError).toBe(false);
|
|
99
99
|
expect(res.result.content[0]?.text.trim()).toBe("present");
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
test("Cli.run docs api skips required appConfig exit", async () => {
|
|
103
|
+
const dir = mkdtempSync(join(tmpdir(), "argsbarg-docs-skip-"));
|
|
104
|
+
const configFile = join(dir, "config");
|
|
105
|
+
writeFileSync(configFile, "{}\n");
|
|
106
|
+
const entry = join(import.meta.dir, "index.ts");
|
|
107
|
+
const mainPath = join(dir, "run-docs.ts");
|
|
108
|
+
writeFileSync(
|
|
109
|
+
mainPath,
|
|
110
|
+
`import { Cli, type CliProgram } from ${JSON.stringify(entry)};
|
|
111
|
+
const program = {
|
|
112
|
+
key: "docs-skip-test",
|
|
113
|
+
version: "1.0.0",
|
|
114
|
+
description: "test",
|
|
115
|
+
docs: { enabled: true, topics: { readme: { text: "# readme\\n" } } },
|
|
116
|
+
appConfig: {
|
|
117
|
+
path: ${JSON.stringify(configFile)},
|
|
118
|
+
entries: { token: { description: "Token.", env: "DOCS_SKIP_RUN_TOKEN" } },
|
|
119
|
+
},
|
|
120
|
+
handler: () => {},
|
|
121
|
+
} satisfies CliProgram;
|
|
122
|
+
await new Cli(program).run(process.argv.slice(2));
|
|
123
|
+
`,
|
|
124
|
+
);
|
|
125
|
+
const env = { ...process.env };
|
|
126
|
+
delete env.DOCS_SKIP_RUN_TOKEN;
|
|
127
|
+
try {
|
|
128
|
+
const proc = Bun.spawn(["bun", "run", mainPath, "docs", "api"], {
|
|
129
|
+
stdout: "pipe",
|
|
130
|
+
stderr: "pipe",
|
|
131
|
+
env,
|
|
132
|
+
});
|
|
133
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
134
|
+
new Response(proc.stdout).text(),
|
|
135
|
+
new Response(proc.stderr).text(),
|
|
136
|
+
proc.exited,
|
|
137
|
+
]);
|
|
138
|
+
expect(exitCode).toBe(0);
|
|
139
|
+
expect(stdout).toContain("# docs-skip-test — CLI API reference");
|
|
140
|
+
expect(stderr).not.toContain("Missing required configuration");
|
|
141
|
+
} finally {
|
|
142
|
+
rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
package/src/context.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class CliContext {
|
|
|
38
38
|
opts: Record<string, string>,
|
|
39
39
|
program: CliProgram,
|
|
40
40
|
invocation: CliInvocation = "cli",
|
|
41
|
-
appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(),
|
|
41
|
+
appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(program),
|
|
42
42
|
) {
|
|
43
43
|
this.appName = appName;
|
|
44
44
|
this.commandPath = commandPath;
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { completionBashScript } from "../builtins/index.ts";
|
|
6
6
|
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
7
|
+
import { resolveCapabilities, skipsRequiredAppConfigExit } from "../capabilities.ts";
|
|
7
8
|
import { Cli } from "../index.ts";
|
|
8
9
|
import type { CliProgram } from "../types.ts";
|
|
9
10
|
import { cliValidateProgram } from "../validate.ts";
|
|
@@ -151,6 +152,19 @@ test("docs api prints markdown reference", async () => {
|
|
|
151
152
|
expect(result.stdout).toContain("myapp docs schema");
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
|
|
156
|
+
const program = {
|
|
157
|
+
...docsFixture(),
|
|
158
|
+
appConfig: {
|
|
159
|
+
entries: { token: { description: "Token.", env: "DOCS_SKIP_TOKEN" } },
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const caps = resolveCapabilities(program);
|
|
163
|
+
expect(skipsRequiredAppConfigExit(["docs", "api"], caps)).toBe(true);
|
|
164
|
+
expect(skipsRequiredAppConfigExit(["config", "get"], caps)).toBe(true);
|
|
165
|
+
expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
154
168
|
test("docs skill prints Cursor SKILL.md", async () => {
|
|
155
169
|
const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
|
|
156
170
|
expect(result.exitCode).toBe(0);
|