argsbarg 4.1.1 → 5.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 +34 -1
- package/README.md +5 -5
- package/docs/README.md +3 -3
- package/docs/ai-skills.md +9 -9
- package/docs/bundled-docs.md +4 -4
- package/docs/cli-program.md +8 -8
- package/docs/config-schema.md +2 -2
- package/docs/configure.md +177 -0
- package/docs/developing.md +1 -1
- package/docs/distribution-homebrew.md +12 -9
- package/docs/mcp.md +9 -9
- package/examples/full-example/README.md +3 -3
- package/examples/full-example/justfile +6 -6
- package/examples/full-example/scripts/formula-shared.ts +3 -2
- package/examples/full-example/src/program.ts +3 -3
- package/examples/nested.ts +1 -1
- package/index.d.ts +23 -16
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +105 -61
- package/src/builtins/completion-group.ts +4 -6
- package/src/builtins/config.test.ts +8 -0
- package/src/builtins/configure-copy.ts +86 -0
- package/src/builtins/configure.ts +70 -0
- package/src/builtins/dispatch.ts +13 -33
- package/src/builtins/index.ts +1 -1
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/registry.ts +6 -6
- package/src/capabilities.ts +22 -13
- package/src/cli-tool/cli-smoke.test.ts +21 -3
- package/src/cli-tool/create.test.ts +36 -1
- package/src/cli-tool/create.ts +26 -4
- package/src/cli-tool/full-example-capabilities.test.ts +10 -2
- package/src/cli-tool/main.ts +0 -0
- package/src/cli-tool/program.ts +20 -5
- package/src/cli-tool/run-create.ts +8 -19
- package/src/config/bootstrap.ts +11 -9
- package/src/config/context.test.ts +8 -0
- package/src/config/file.test.ts +13 -1
- package/src/config/resolve.test.ts +18 -0
- package/src/config/resolve.ts +2 -2
- package/src/config/validate.test.ts +11 -0
- package/src/config.integration.test.ts +5 -0
- package/src/configure/configure.test.ts +170 -0
- package/src/configure/index.ts +298 -0
- package/src/configure/prompt.ts +47 -0
- package/src/docs/api-guide.test.ts +10 -0
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +32 -5
- package/src/docs/mcp-guide.ts +4 -4
- package/src/docs/mcp-resources.test.ts +10 -0
- package/src/formats.test.ts +9 -0
- package/src/headless.test.ts +10 -0
- package/src/hidden-mcpb.test.ts +22 -0
- package/src/index.ts +2 -2
- package/src/install/binary-placement.test.ts +14 -0
- package/src/install/gh-release-update.test.ts +9 -0
- package/src/install/install-validate.test.ts +15 -5
- package/src/install/mcp-codex.test.ts +7 -0
- package/src/install/mcp-openclaw.test.ts +6 -0
- package/src/install/mcp-opencode.test.ts +10 -0
- package/src/install/opts.ts +17 -0
- package/src/install/status.test.ts +9 -0
- package/src/install/target-effective.ts +8 -8
- package/src/install/target-scope.ts +11 -8
- package/src/install/targets/configure.ts +1 -1
- package/src/install/targets.test.ts +23 -4
- package/src/invoke.test.ts +15 -1
- package/src/mcp/claude.test.ts +10 -0
- package/src/mcp/env.test.ts +12 -0
- package/src/mcp/tools.ts +1 -1
- package/src/mcp/zip.test.ts +5 -0
- package/src/mcp.integration.test.ts +43 -4
- package/src/parse.test.ts +82 -13
- package/src/schema.ts +1 -9
- package/src/skill/hint.ts +2 -2
- package/src/types.ts +22 -14
- package/src/validate.ts +17 -17
- package/docs/install.md +0 -206
- package/src/builtins/install.ts +0 -106
- package/src/builtins/uninstall.ts +0 -80
- package/src/install/index.ts +0 -409
- package/src/install/install.test.ts +0 -317
package/src/builtins/dispatch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { capabilityDeniedMessage, resolveCapabilities } from "../capabilities.ts";
|
|
2
2
|
import { Cli } from "../cli.ts";
|
|
3
|
+
import { cliConfigure } from "../configure/index.ts";
|
|
3
4
|
import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
|
|
4
|
-
import { cliInstall, cliUninstall } from "../install/index.ts";
|
|
5
5
|
import { runMcpBundle } from "../mcp/bundle.ts";
|
|
6
6
|
import type { ParseResult } from "../parse.ts";
|
|
7
7
|
import { ParseKind } from "../parse.ts";
|
|
@@ -12,10 +12,9 @@ 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
14
|
import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { cliBuiltinConfigureCommand } from "./configure.ts";
|
|
16
16
|
import { cliBuiltinMcpCommand } from "./mcp.ts";
|
|
17
17
|
import { cliPresentationRoot } from "./presentation.ts";
|
|
18
|
-
import { cliBuiltinUninstallCommand } from "./uninstall.ts";
|
|
19
18
|
import { cliBuiltinVersionCommand } from "./version.ts";
|
|
20
19
|
|
|
21
20
|
export interface DispatchBuiltinOpts {
|
|
@@ -45,6 +44,10 @@ export async function dispatchBuiltin(
|
|
|
45
44
|
const caps = resolveCapabilities(program);
|
|
46
45
|
|
|
47
46
|
if (pr.path[0] === "completion") {
|
|
47
|
+
if (!caps.completion) {
|
|
48
|
+
process.stderr.write(capabilityDeniedMessage("completion"));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
48
51
|
const schemaForCompletion = completionSchema(program, opts);
|
|
49
52
|
if (pr.path[1] === "bash") {
|
|
50
53
|
process.stdout.write(completionBashScript(schemaForCompletion));
|
|
@@ -93,28 +96,16 @@ export async function dispatchBuiltin(
|
|
|
93
96
|
process.exit(1);
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
if (pr.path[0] === "
|
|
97
|
-
if (!caps.
|
|
98
|
-
process.stderr.write(capabilityDeniedMessage("
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
if (pr.path.length !== 1) {
|
|
102
|
-
process.stderr.write(`Unknown subcommand: install ${pr.path.slice(1).join(" ")}\n`);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
await cliInstall(program, pr.opts);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (pr.path[0] === "uninstall") {
|
|
109
|
-
if (!caps.install) {
|
|
110
|
-
process.stderr.write(capabilityDeniedMessage("install"));
|
|
99
|
+
if (pr.path[0] === "configure") {
|
|
100
|
+
if (!caps.configure) {
|
|
101
|
+
process.stderr.write(capabilityDeniedMessage("configure"));
|
|
111
102
|
process.exit(1);
|
|
112
103
|
}
|
|
113
104
|
if (pr.path.length !== 1) {
|
|
114
|
-
process.stderr.write(`Unknown subcommand:
|
|
105
|
+
process.stderr.write(`Unknown subcommand: configure ${pr.path.slice(1).join(" ")}\n`);
|
|
115
106
|
process.exit(1);
|
|
116
107
|
}
|
|
117
|
-
await
|
|
108
|
+
await cliConfigure(program, pr.opts);
|
|
118
109
|
}
|
|
119
110
|
}
|
|
120
111
|
|
|
@@ -141,23 +132,12 @@ export function builtinInterceptRoot(
|
|
|
141
132
|
};
|
|
142
133
|
}
|
|
143
134
|
|
|
144
|
-
if (first === "
|
|
145
|
-
return {
|
|
146
|
-
parseRoot: {
|
|
147
|
-
key: program.key,
|
|
148
|
-
description: program.description,
|
|
149
|
-
commands: [cliBuiltinInstallCommand(program)],
|
|
150
|
-
},
|
|
151
|
-
isLeafCompletionIntercept: false,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (first === "uninstall" && caps.install) {
|
|
135
|
+
if (first === "configure" && caps.configure) {
|
|
156
136
|
return {
|
|
157
137
|
parseRoot: {
|
|
158
138
|
key: program.key,
|
|
159
139
|
description: program.description,
|
|
160
|
-
commands: [
|
|
140
|
+
commands: [cliBuiltinConfigureCommand(program)],
|
|
161
141
|
},
|
|
162
142
|
isLeafCompletionIntercept: false,
|
|
163
143
|
};
|
package/src/builtins/index.ts
CHANGED
|
@@ -2,9 +2,9 @@ export { completionBashScript } from "./completion-bash.ts";
|
|
|
2
2
|
export { completionFishScript } from "./completion-fish.ts";
|
|
3
3
|
export { cliBuiltinCompletionGroup } from "./completion-group.ts";
|
|
4
4
|
export { completionZshScript } from "./completion-zsh.ts";
|
|
5
|
+
export { cliBuiltinConfigureCommand, configureBuiltinOptions } from "./configure.ts";
|
|
5
6
|
export { builtinInterceptRoot, dispatchBuiltin } from "./dispatch.ts";
|
|
6
7
|
export { type CliSchemaExport, exportPresentationBuiltins } from "./export.ts";
|
|
7
|
-
export { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
|
|
8
8
|
export { cliBuiltinMcpCommand } from "./mcp.ts";
|
|
9
9
|
export {
|
|
10
10
|
cliParseRoot,
|
package/src/builtins/mcp.ts
CHANGED
|
@@ -12,8 +12,8 @@ export function cliBuiltinMcpCommand(program: CliProgram): CliRouter {
|
|
|
12
12
|
" args: mcp",
|
|
13
13
|
"",
|
|
14
14
|
];
|
|
15
|
-
if (caps.
|
|
16
|
-
lines.push("Or:", "", " {argsbarg:program}
|
|
15
|
+
if (caps.configure) {
|
|
16
|
+
lines.push("Or:", "", " {argsbarg:program} configure", "");
|
|
17
17
|
}
|
|
18
18
|
if (docsEnabled(program)) {
|
|
19
19
|
lines.push("Full setup guide: {argsbarg:program} docs mcp");
|
package/src/builtins/registry.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
|
|
|
3
3
|
import type { CliNode, CliProgram } from "../types.ts";
|
|
4
4
|
import { cliBuiltinCompletionGroup } from "./completion-group.ts";
|
|
5
5
|
import { cliBuiltinConfigGroupIfEnabled } from "./config.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { cliBuiltinConfigureCommand } from "./configure.ts";
|
|
7
7
|
import { cliBuiltinMcpCommand } from "./mcp.ts";
|
|
8
|
-
import { cliBuiltinUninstallCommand } from "./uninstall.ts";
|
|
9
8
|
import { cliBuiltinVersionCommand } from "./version.ts";
|
|
10
9
|
|
|
11
10
|
type BuiltinFactory = (program: CliProgram) => CliNode | null;
|
|
@@ -27,11 +26,12 @@ function pushBuiltin(
|
|
|
27
26
|
/** Capability-gated built-in command nodes in stable order (parse, help, export). */
|
|
28
27
|
export function resolveBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
|
|
29
28
|
const builtins: CliNode[] = [];
|
|
30
|
-
|
|
29
|
+
if (caps.completion) {
|
|
30
|
+
pushBuiltin(builtins, program, (p) => cliBuiltinCompletionGroup(p));
|
|
31
|
+
}
|
|
31
32
|
pushBuiltin(builtins, program, () => cliBuiltinVersionCommand());
|
|
32
|
-
if (caps.
|
|
33
|
-
pushBuiltin(builtins, program, (p) =>
|
|
34
|
-
pushBuiltin(builtins, program, (p) => cliBuiltinUninstallCommand(p));
|
|
33
|
+
if (caps.configure) {
|
|
34
|
+
pushBuiltin(builtins, program, (p) => cliBuiltinConfigureCommand(p));
|
|
35
35
|
}
|
|
36
36
|
pushBuiltin(builtins, program, (p) => cliBuiltinDocsGroupIfEnabled(p) ?? null);
|
|
37
37
|
if (caps.mcp) {
|
package/src/capabilities.ts
CHANGED
|
@@ -8,20 +8,20 @@ import type { CliProgram } from "./types.ts";
|
|
|
8
8
|
|
|
9
9
|
/** Platform builtins derived from program config and runtime. */
|
|
10
10
|
export interface CliCapabilities {
|
|
11
|
-
completion:
|
|
11
|
+
completion: boolean;
|
|
12
12
|
mcp: boolean;
|
|
13
|
-
|
|
13
|
+
configure: boolean;
|
|
14
14
|
docs: boolean;
|
|
15
15
|
configCommands: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Resolves which capabilities are enabled for a program. */
|
|
19
19
|
export function resolveCapabilities(program: CliProgram): CliCapabilities {
|
|
20
|
-
const
|
|
20
|
+
const configure = program.configure?.enabled !== false;
|
|
21
21
|
return {
|
|
22
|
-
completion:
|
|
22
|
+
completion: program.completion?.enabled !== false,
|
|
23
23
|
mcp: program.mcpServer?.enabled === true,
|
|
24
|
-
|
|
24
|
+
configure,
|
|
25
25
|
docs: program.docs?.enabled === true,
|
|
26
26
|
configCommands: configCommandsEnabled(program),
|
|
27
27
|
};
|
|
@@ -29,9 +29,12 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
|
|
|
29
29
|
|
|
30
30
|
/** Reserved top-level command names for the given capabilities. */
|
|
31
31
|
export function reservedCommandNames(caps: CliCapabilities): string[] {
|
|
32
|
-
const names = ["
|
|
33
|
-
if (caps.
|
|
34
|
-
names.
|
|
32
|
+
const names = ["version"];
|
|
33
|
+
if (caps.completion) {
|
|
34
|
+
names.unshift("completion");
|
|
35
|
+
}
|
|
36
|
+
if (caps.configure) {
|
|
37
|
+
names.push("configure");
|
|
35
38
|
}
|
|
36
39
|
if (caps.docs) {
|
|
37
40
|
names.push("docs");
|
|
@@ -57,15 +60,17 @@ export function skipsRequiredAppConfigExit(path: string[], caps: CliCapabilities
|
|
|
57
60
|
return false;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
export type CapabilityFeature = "mcp" | "
|
|
63
|
+
export type CapabilityFeature = "mcp" | "configure" | "docs" | "config" | "completion";
|
|
61
64
|
|
|
62
65
|
/** Stderr message when a disabled built-in is invoked from the CLI. */
|
|
63
66
|
export function capabilityDeniedMessage(feature: CapabilityFeature): string {
|
|
64
67
|
switch (feature) {
|
|
68
|
+
case "completion":
|
|
69
|
+
return "Shell completion is not available for this app.\n";
|
|
65
70
|
case "mcp":
|
|
66
71
|
return "MCP is not available for this app.\n";
|
|
67
|
-
case "
|
|
68
|
-
return "
|
|
72
|
+
case "configure":
|
|
73
|
+
return "Configure is not available for this app.\n";
|
|
69
74
|
case "docs":
|
|
70
75
|
return "Documentation commands are not available for this app.\n";
|
|
71
76
|
case "config":
|
|
@@ -79,12 +84,16 @@ export function assertBuiltinAllowed(argv: string[], caps: CliCapabilities): voi
|
|
|
79
84
|
return;
|
|
80
85
|
}
|
|
81
86
|
const first = argv[0];
|
|
87
|
+
if (first === "completion" && !caps.completion) {
|
|
88
|
+
process.stderr.write(capabilityDeniedMessage("completion"));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
82
91
|
if (first === "mcp" && !caps.mcp) {
|
|
83
92
|
process.stderr.write(capabilityDeniedMessage("mcp"));
|
|
84
93
|
process.exit(1);
|
|
85
94
|
}
|
|
86
|
-
if (first === "
|
|
87
|
-
process.stderr.write(capabilityDeniedMessage("
|
|
95
|
+
if (first === "configure" && !caps.configure) {
|
|
96
|
+
process.stderr.write(capabilityDeniedMessage("configure"));
|
|
88
97
|
process.exit(1);
|
|
89
98
|
}
|
|
90
99
|
if (first === "docs" && !caps.docs) {
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for cli-tool/cli-smoke module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { describe, expect, test } from "bun:test";
|
|
2
6
|
import { spawnSync } from "node:child_process";
|
|
3
7
|
import { join } from "node:path";
|
|
4
8
|
|
|
5
9
|
const main = join(import.meta.dir, "main.ts");
|
|
6
10
|
|
|
11
|
+
/** Tests for argsbarg cli-tool. */
|
|
7
12
|
describe("argsbarg cli-tool", () => {
|
|
13
|
+
/** Tests that version subcommand prints version. */
|
|
8
14
|
test("version subcommand prints version", () => {
|
|
9
15
|
const proc = spawnSync("bun", [main, "version"], { encoding: "utf8" });
|
|
10
16
|
expect(proc.status).toBe(0);
|
|
11
17
|
expect(proc.stdout.trim().length).toBeGreaterThan(0);
|
|
12
18
|
});
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
/** Tests that help lists create and version only (no install, completion, mcp). */
|
|
21
|
+
test("help lists create and version only (no install, completion, mcp)", () => {
|
|
22
|
+
const proc = spawnSync("bun", [main, "--help"], { encoding: "utf8" });
|
|
16
23
|
expect(proc.status).toBe(0);
|
|
17
|
-
expect(proc.stdout).toContain("
|
|
24
|
+
expect(proc.stdout).toContain("create");
|
|
25
|
+
expect(proc.stdout).toContain("version");
|
|
26
|
+
expect(proc.stdout).not.toContain("configure");
|
|
27
|
+
expect(proc.stdout).not.toContain("completion");
|
|
28
|
+
expect(proc.stdout).not.toContain("mcp");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Completion subcommand is disabled. */
|
|
32
|
+
test("completion subcommand is disabled", () => {
|
|
33
|
+
const proc = spawnSync("bun", [main, "completion", "bash"], { encoding: "utf8" });
|
|
34
|
+
expect(proc.status).toBe(1);
|
|
35
|
+
expect(proc.stderr).toContain("Shell completion is not available");
|
|
18
36
|
});
|
|
19
37
|
});
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for cli-tool/create module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { describe, expect, test } from "bun:test";
|
|
2
6
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -8,10 +12,13 @@ import {
|
|
|
8
12
|
diffCreate,
|
|
9
13
|
diffCreateDetails,
|
|
10
14
|
renderCreateTree,
|
|
15
|
+
resolveCreateOptions,
|
|
11
16
|
substituteTemplateContent,
|
|
12
17
|
} from "./create.ts";
|
|
13
18
|
|
|
19
|
+
/** Tests for argsbarg create. */
|
|
14
20
|
describe("argsbarg create", () => {
|
|
21
|
+
/** Tests that substitutes {key} tokens. */
|
|
15
22
|
test("substitutes {key} tokens", () => {
|
|
16
23
|
const out = substituteTemplateContent(
|
|
17
24
|
"key={key} class={className} env={envPrefix}_API_TOKEN tap={tap} org={tapOrg}",
|
|
@@ -37,10 +44,35 @@ describe("argsbarg create", () => {
|
|
|
37
44
|
expect(out).not.toContain("{key}");
|
|
38
45
|
});
|
|
39
46
|
|
|
47
|
+
/** Tests that classNameFromKey. */
|
|
40
48
|
test("classNameFromKey", () => {
|
|
41
49
|
expect(classNameFromKey("sqsp-i18n")).toBe("SqspI18n");
|
|
50
|
+
expect(classNameFromKey("at1")).toBe("At1");
|
|
51
|
+
expect(classNameFromKey("1password")).toBe("App1password");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** ResolveCreateOptions derives identity defaults from key. */
|
|
55
|
+
test("resolveCreateOptions derives identity defaults from key", () => {
|
|
56
|
+
expect(resolveCreateOptions({ key: "1password", releaseRepo: "org/1password" }).className).toBe(
|
|
57
|
+
"App1password",
|
|
58
|
+
);
|
|
59
|
+
expect(
|
|
60
|
+
resolveCreateOptions({ key: "my-cli", className: "Custom", releaseRepo: "org/my-cli" })
|
|
61
|
+
.className,
|
|
62
|
+
).toBe("Custom");
|
|
63
|
+
const opts = resolveCreateOptions({ key: "at1", releaseRepo: "bdombro/at1" });
|
|
64
|
+
expect(opts.tap).toBe("bdombro/at1");
|
|
65
|
+
expect(opts.releaseRepo).toBe("bdombro/at1");
|
|
66
|
+
expect(opts.homepage).toBe("https://github.com/bdombro/at1");
|
|
67
|
+
expect(opts.desc).toBe("At1 CLI");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** ResolveCreateOptions requires release repo. */
|
|
71
|
+
test("resolveCreateOptions requires release repo", () => {
|
|
72
|
+
expect(() => resolveCreateOptions({ key: "at1" })).toThrow(/release repo/i);
|
|
42
73
|
});
|
|
43
74
|
|
|
75
|
+
/** Tests that renderCreateTree includes justfile and create-identity. */
|
|
44
76
|
test("renderCreateTree includes justfile and create-identity", () => {
|
|
45
77
|
const tree = renderCreateTree({
|
|
46
78
|
key: "testapp",
|
|
@@ -66,6 +98,7 @@ describe("argsbarg create", () => {
|
|
|
66
98
|
expect(formula).toContain("create-identity.ts");
|
|
67
99
|
});
|
|
68
100
|
|
|
101
|
+
/** Tests that --check detects drift. */
|
|
69
102
|
test("--check detects drift", () => {
|
|
70
103
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
|
|
71
104
|
try {
|
|
@@ -89,6 +122,7 @@ describe("argsbarg create", () => {
|
|
|
89
122
|
}
|
|
90
123
|
});
|
|
91
124
|
|
|
125
|
+
/** Tests that --check infers options from create-identity.ts. */
|
|
92
126
|
test("--check infers options from create-identity.ts", () => {
|
|
93
127
|
const dir = mkdtempSync(join(tmpdir(), "argsbarg-create-"));
|
|
94
128
|
try {
|
|
@@ -112,8 +146,9 @@ describe("argsbarg create", () => {
|
|
|
112
146
|
}
|
|
113
147
|
});
|
|
114
148
|
|
|
149
|
+
/** Tests that --diff captures drift details. */
|
|
115
150
|
test("--diff captures drift details", () => {
|
|
116
|
-
const drifts = diffCreateDetails("/nonexistent", { key: "x" });
|
|
151
|
+
const drifts = diffCreateDetails("/nonexistent", { key: "x", releaseRepo: "org/x" });
|
|
117
152
|
expect(drifts.length).toBeGreaterThan(0);
|
|
118
153
|
});
|
|
119
154
|
});
|
package/src/cli-tool/create.ts
CHANGED
|
@@ -44,11 +44,20 @@ export function keyToEnvPrefix(key: string): string {
|
|
|
44
44
|
.toUpperCase();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** PascalCase Homebrew formula class from CLI key; prefix `App` when Ruby constant rules require it. */
|
|
47
48
|
export function classNameFromKey(key: string): string {
|
|
48
|
-
|
|
49
|
+
const name = key
|
|
49
50
|
.split(/[-_]/)
|
|
51
|
+
.filter((s) => s.length > 0)
|
|
50
52
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
51
53
|
.join("");
|
|
54
|
+
if (name.length === 0) {
|
|
55
|
+
throw new Error(`Invalid CLI key: ${key}`);
|
|
56
|
+
}
|
|
57
|
+
if (!/^[A-Z]/.test(name)) {
|
|
58
|
+
return `App${name}`;
|
|
59
|
+
}
|
|
60
|
+
return name;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
function tapLibraryParts(tap: string): { org: string; repo: string } {
|
|
@@ -131,6 +140,12 @@ export function inferCreateOptions(
|
|
|
131
140
|
};
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
function assertReleaseRepoFormat(releaseRepo: string): void {
|
|
144
|
+
if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(releaseRepo)) {
|
|
145
|
+
throw new Error(`Invalid release repo (expected org/repo): ${releaseRepo}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
134
149
|
export function resolveCreateOptions(
|
|
135
150
|
partial: Partial<CreateOptions>,
|
|
136
151
|
baseDir?: string,
|
|
@@ -139,13 +154,20 @@ export function resolveCreateOptions(
|
|
|
139
154
|
const tmpl = templateIdentity();
|
|
140
155
|
const key = merged.key ?? tmpl.key ?? "full-example";
|
|
141
156
|
const className = merged.className ?? classNameFromKey(key);
|
|
157
|
+
const releaseRepo = merged.releaseRepo;
|
|
158
|
+
if (!releaseRepo) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"GitHub release repo (org/repo) is required. Pass --release-repo or use the interactive wizard.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
assertReleaseRepoFormat(releaseRepo);
|
|
142
164
|
const devTemplate = merged.devTemplate ?? (baseDir ? isDevTemplateDir(baseDir) : false);
|
|
143
165
|
return {
|
|
144
166
|
key,
|
|
145
167
|
className,
|
|
146
|
-
tap: merged.tap ??
|
|
147
|
-
homepage: merged.homepage ?? `https://github.com/${
|
|
148
|
-
releaseRepo
|
|
168
|
+
tap: merged.tap ?? releaseRepo,
|
|
169
|
+
homepage: merged.homepage ?? `https://github.com/${releaseRepo}`,
|
|
170
|
+
releaseRepo,
|
|
149
171
|
desc: merged.desc ?? `${className} CLI`,
|
|
150
172
|
force: merged.force ?? false,
|
|
151
173
|
dryRun: merged.dryRun ?? false,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for cli-tool/full-example-capabilities module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { describe, expect, test } from "bun:test";
|
|
2
6
|
import { readFileSync } from "node:fs";
|
|
3
7
|
import { join } from "node:path";
|
|
@@ -22,7 +26,7 @@ const sinkProgram = {
|
|
|
22
26
|
topics: { readme: { text: "# readme\n" } },
|
|
23
27
|
},
|
|
24
28
|
mcpServer: { enabled: true },
|
|
25
|
-
|
|
29
|
+
configure: {},
|
|
26
30
|
commands: [
|
|
27
31
|
{
|
|
28
32
|
key: "status",
|
|
@@ -45,7 +49,9 @@ const sinkProgram = {
|
|
|
45
49
|
],
|
|
46
50
|
} satisfies CliProgram;
|
|
47
51
|
|
|
52
|
+
/** Tests for full-example template. */
|
|
48
53
|
describe("full-example template", () => {
|
|
54
|
+
/** Tests that program source enables every builtin flag. */
|
|
49
55
|
test("program source enables every builtin flag", () => {
|
|
50
56
|
expect(programSource).toContain("mcpServer: {");
|
|
51
57
|
expect(programSource).toContain("enabled: true");
|
|
@@ -53,16 +59,18 @@ describe("full-example template", () => {
|
|
|
53
59
|
expect(programSource).toContain("appConfig:");
|
|
54
60
|
});
|
|
55
61
|
|
|
62
|
+
/** Status command defines outputSchema. */
|
|
56
63
|
test("status command defines outputSchema", () => {
|
|
57
64
|
const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
|
|
58
65
|
expect(statusSource).toContain("outputSchema:");
|
|
59
66
|
});
|
|
60
67
|
|
|
68
|
+
/** ResolveCapabilities matches full sink shape. */
|
|
61
69
|
test("resolveCapabilities matches full sink shape", () => {
|
|
62
70
|
expect(resolveCapabilities(sinkProgram)).toEqual({
|
|
63
71
|
completion: true,
|
|
64
72
|
mcp: true,
|
|
65
|
-
|
|
73
|
+
configure: true,
|
|
66
74
|
docs: true,
|
|
67
75
|
configCommands: true,
|
|
68
76
|
});
|
package/src/cli-tool/main.ts
CHANGED
|
File without changes
|
package/src/cli-tool/program.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const program = {
|
|
|
10
10
|
key: "argsbarg",
|
|
11
11
|
version: pkg.version,
|
|
12
12
|
description: "Argsbarg developer tools — bootstrap CLIs from the full-example template.",
|
|
13
|
+
completion: { enabled: false },
|
|
14
|
+
configure: { enabled: false },
|
|
13
15
|
commands: [
|
|
14
16
|
{
|
|
15
17
|
key: "create",
|
|
@@ -18,17 +20,30 @@ export const program = {
|
|
|
18
20
|
{ name: "key", description: "CLI binary name.", kind: CliOptionKind.String },
|
|
19
21
|
{
|
|
20
22
|
name: "class-name",
|
|
21
|
-
description: "Homebrew formula class
|
|
23
|
+
description: "Homebrew formula Ruby class (default: derived from --key).",
|
|
24
|
+
kind: CliOptionKind.String,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "tap",
|
|
28
|
+
description: "Homebrew tap (default: same as --release-repo).",
|
|
29
|
+
kind: CliOptionKind.String,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "homepage",
|
|
33
|
+
description: "Formula homepage (default: https://github.com/<release-repo>).",
|
|
22
34
|
kind: CliOptionKind.String,
|
|
23
35
|
},
|
|
24
|
-
{ name: "tap", description: "Homebrew tap (org/repo).", kind: CliOptionKind.String },
|
|
25
|
-
{ name: "homepage", description: "Formula homepage URL.", kind: CliOptionKind.String },
|
|
26
36
|
{
|
|
27
37
|
name: "release-repo",
|
|
28
|
-
description: "GitHub org/repo for
|
|
38
|
+
description: "GitHub org/repo for releases (required in non-interactive mode).",
|
|
39
|
+
kind: CliOptionKind.String,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "desc",
|
|
43
|
+
description:
|
|
44
|
+
"App + formula description (default: <ClassName> CLI; stored in create-identity.ts).",
|
|
29
45
|
kind: CliOptionKind.String,
|
|
30
46
|
},
|
|
31
|
-
{ name: "desc", description: "Formula description.", kind: CliOptionKind.String },
|
|
32
47
|
{ name: "force", description: "Overwrite existing files.", kind: CliOptionKind.Presence },
|
|
33
48
|
{
|
|
34
49
|
name: "dry-run",
|
|
@@ -5,7 +5,6 @@ import { relative, resolve } from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
applyCreate,
|
|
7
7
|
type CreateOptions,
|
|
8
|
-
classNameFromKey,
|
|
9
8
|
diffCreateDetails,
|
|
10
9
|
parseCreateArgv,
|
|
11
10
|
printCreateDiffs,
|
|
@@ -27,36 +26,23 @@ function collectInteractiveOptions(
|
|
|
27
26
|
const targetDir = promptOptional("Target directory", dir) ?? dir;
|
|
28
27
|
const baseDir = resolve(process.cwd(), targetDir);
|
|
29
28
|
const key = promptRequired("CLI key (binary name)", partial.key);
|
|
30
|
-
const
|
|
31
|
-
promptOptional("Formula class name", partial.className ?? classNameFromKey(key)) ??
|
|
32
|
-
classNameFromKey(key);
|
|
33
|
-
const releaseRepo =
|
|
34
|
-
promptOptional("GitHub release repo (org/repo)", partial.releaseRepo ?? `example/${key}`) ??
|
|
35
|
-
`example/${key}`;
|
|
36
|
-
const homepage =
|
|
37
|
-
promptOptional("Homepage URL", partial.homepage ?? `https://github.com/${releaseRepo}`) ??
|
|
38
|
-
`https://github.com/${releaseRepo}`;
|
|
39
|
-
const tap =
|
|
40
|
-
promptOptional("Homebrew tap (org/repo)", partial.tap ?? `local/${key}`) ?? `local/${key}`;
|
|
41
|
-
const desc =
|
|
42
|
-
promptOptional("Formula description", partial.desc ?? `${className} CLI`) ?? `${className} CLI`;
|
|
29
|
+
const releaseRepo = promptRequired("GitHub release repo (org/repo)", partial.releaseRepo);
|
|
43
30
|
|
|
44
31
|
const opts = resolveCreateOptions(
|
|
45
32
|
{
|
|
46
33
|
...partial,
|
|
47
34
|
key,
|
|
48
|
-
className,
|
|
49
|
-
tap,
|
|
50
|
-
homepage,
|
|
51
35
|
releaseRepo,
|
|
52
|
-
desc,
|
|
53
36
|
force: partial.force ?? false,
|
|
54
37
|
},
|
|
55
38
|
baseDir,
|
|
56
39
|
);
|
|
57
40
|
|
|
58
41
|
process.stderr.write(`\nTarget: ${baseDir}\n`);
|
|
59
|
-
process.stderr.write(
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
`Key: ${opts.key} Class: ${opts.className} Tap: ${opts.tap} Release: ${opts.releaseRepo}\n`,
|
|
44
|
+
);
|
|
45
|
+
process.stderr.write(`Homepage: ${opts.homepage}\n\n`);
|
|
60
46
|
const tree = renderCreateTree(opts);
|
|
61
47
|
process.stderr.write(`Files (${tree.size}):\n`);
|
|
62
48
|
for (const rel of [...tree.keys()].sort()) {
|
|
@@ -104,6 +90,9 @@ export async function runCreate(input: Partial<CreateOptions> & { dir?: string }
|
|
|
104
90
|
if (!partial.key) {
|
|
105
91
|
throw new Error("--key is required in non-interactive mode.");
|
|
106
92
|
}
|
|
93
|
+
if (!partial.releaseRepo) {
|
|
94
|
+
throw new Error("--release-repo is required in non-interactive mode.");
|
|
95
|
+
}
|
|
107
96
|
opts = resolveCreateOptions(partial, baseDir);
|
|
108
97
|
return runCreateApply(baseDir, opts, partial.dryRun ?? false);
|
|
109
98
|
} catch (err) {
|
package/src/config/bootstrap.ts
CHANGED
|
@@ -176,10 +176,12 @@ function promptConfigKey(
|
|
|
176
176
|
return { value: undefined, userTyped: false };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/** Options for the interactive
|
|
179
|
+
/** Options for the interactive configure wizard (app config target). */
|
|
180
180
|
export interface RunInstallConfigureOpts {
|
|
181
181
|
/** Where the wizard was started from (standalone command vs right after install). */
|
|
182
182
|
context?: "standalone" | "after-install";
|
|
183
|
+
/** When false, omit the "Configuration Setup" banner (e.g. inside interactive `configure`). */
|
|
184
|
+
showHeading?: boolean;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
/** Print the "Configuration Setup" heading before the configure prompts. */
|
|
@@ -225,13 +227,13 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
|
|
|
225
227
|
return updates;
|
|
226
228
|
}
|
|
227
229
|
|
|
228
|
-
/** Run the full interactive
|
|
229
|
-
export function
|
|
230
|
+
/** Run the full interactive app config wizard (`configure`). */
|
|
231
|
+
export function runConfigure(
|
|
230
232
|
program: CliProgram,
|
|
231
|
-
|
|
233
|
+
opts: RunInstallConfigureOpts = {},
|
|
232
234
|
): { path: string; changed: boolean } {
|
|
233
235
|
if (!program.appConfig) {
|
|
234
|
-
throw new Error("
|
|
236
|
+
throw new Error("configure requires program.appConfig on the program root.");
|
|
235
237
|
}
|
|
236
238
|
if (!process.stdin.isTTY) {
|
|
237
239
|
const { resolved } = bootstrapAppConfig(program, { validateFile: false });
|
|
@@ -239,7 +241,7 @@ export function runInstallConfigure(
|
|
|
239
241
|
if (missing.length > 0) {
|
|
240
242
|
process.stderr.write(`${formatMissingConfigMessage(program, missing)}\n`);
|
|
241
243
|
} else {
|
|
242
|
-
process.stderr.write("
|
|
244
|
+
process.stderr.write("configure requires an interactive terminal.\n");
|
|
243
245
|
}
|
|
244
246
|
process.exit(1);
|
|
245
247
|
}
|
|
@@ -253,7 +255,7 @@ export function runInstallConfigure(
|
|
|
253
255
|
const next: Record<string, unknown> = { ...existing };
|
|
254
256
|
let changed = false;
|
|
255
257
|
|
|
256
|
-
if (shouldShowConfigureSetupHeading(program)) {
|
|
258
|
+
if (opts.showHeading !== false && shouldShowConfigureSetupHeading(program)) {
|
|
257
259
|
writeConfigureSetupHeading();
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -286,7 +288,7 @@ export function runInstallConfigure(
|
|
|
286
288
|
return { path, changed: false };
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
/** Summary for `
|
|
291
|
+
/** Summary for `configure --status`: config path, whether the file exists, and which required settings are set (never their values). */
|
|
290
292
|
export function appConfigStatus(program: CliProgram):
|
|
291
293
|
| {
|
|
292
294
|
path: string;
|
|
@@ -343,7 +345,7 @@ export function ensureAppConfig(
|
|
|
343
345
|
|
|
344
346
|
if (opts.interactive && process.stdin.isTTY) {
|
|
345
347
|
if (opts.configure) {
|
|
346
|
-
|
|
348
|
+
runConfigure(program, { context: "standalone" });
|
|
347
349
|
fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
|
|
348
350
|
resolved = resolveAppConfig(program, fileData, hostEnv);
|
|
349
351
|
exportConfigToEnv(program, resolved, hostEnv);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tests for config/context module behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import { describe, expect, test } from "bun:test";
|
|
2
6
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
7
|
import { tmpdir } from "node:os";
|
|
@@ -20,7 +24,9 @@ const program: CliProgram = {
|
|
|
20
24
|
handler: () => {},
|
|
21
25
|
};
|
|
22
26
|
|
|
27
|
+
/** Tests for config/context. */
|
|
23
28
|
describe("config/context", () => {
|
|
29
|
+
/** Tests that AppConfigSnapshot get, require, read, set. */
|
|
24
30
|
test("AppConfigSnapshot get, require, read, set", () => {
|
|
25
31
|
const dir = mkdtempSync(join(tmpdir(), "ctx-test-"));
|
|
26
32
|
const prevHome = process.env.HOME;
|
|
@@ -50,6 +56,7 @@ describe("config/context", () => {
|
|
|
50
56
|
}
|
|
51
57
|
});
|
|
52
58
|
|
|
59
|
+
/** Tests that EmptyAppConfigSnapshot when program.appConfig unset. */
|
|
53
60
|
test("EmptyAppConfigSnapshot when program.appConfig unset", () => {
|
|
54
61
|
const programWithoutConfig: CliProgram = {
|
|
55
62
|
key: "x",
|
|
@@ -65,6 +72,7 @@ describe("config/context", () => {
|
|
|
65
72
|
expect(empty.dir).toBe(dirname(empty.path));
|
|
66
73
|
});
|
|
67
74
|
|
|
75
|
+
/** Tests that AppConfigSnapshot path uses OS default from program key. */
|
|
68
76
|
test("AppConfigSnapshot path uses OS default from program key", () => {
|
|
69
77
|
const ctx = createAppConfigSnapshot(program, {}, {});
|
|
70
78
|
expect(ctx.path).toContain("ctx_test");
|