argsbarg 4.1.1 → 5.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.
- package/CHANGELOG.md +27 -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 +10 -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 +2 -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 +82 -61
- package/src/builtins/completion-group.ts +4 -6
- 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 +13 -3
- package/src/cli-tool/create.test.ts +23 -1
- package/src/cli-tool/create.ts +26 -4
- package/src/cli-tool/full-example-capabilities.test.ts +2 -2
- 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/file.test.ts +1 -1
- package/src/config/resolve.ts +2 -2
- package/src/configure/configure.test.ts +148 -0
- package/src/configure/index.ts +284 -0
- package/src/configure/prompt.ts +40 -0
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +5 -5
- package/src/docs/mcp-guide.ts +4 -4
- package/src/index.ts +2 -2
- package/src/install/install-validate.test.ts +5 -5
- package/src/install/opts.ts +17 -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 +4 -4
- package/src/invoke.test.ts +1 -1
- package/src/mcp/tools.ts +1 -1
- package/src/mcp.integration.test.ts +4 -4
- package/src/parse.test.ts +11 -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/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) {
|
|
@@ -11,9 +11,19 @@ describe("argsbarg cli-tool", () => {
|
|
|
11
11
|
expect(proc.stdout.trim().length).toBeGreaterThan(0);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
test("
|
|
15
|
-
const proc = spawnSync("bun", [main, "
|
|
14
|
+
test("help lists create and version only (no install, completion, mcp)", () => {
|
|
15
|
+
const proc = spawnSync("bun", [main, "--help"], { encoding: "utf8" });
|
|
16
16
|
expect(proc.status).toBe(0);
|
|
17
|
-
expect(proc.stdout).toContain("
|
|
17
|
+
expect(proc.stdout).toContain("create");
|
|
18
|
+
expect(proc.stdout).toContain("version");
|
|
19
|
+
expect(proc.stdout).not.toContain("configure");
|
|
20
|
+
expect(proc.stdout).not.toContain("completion");
|
|
21
|
+
expect(proc.stdout).not.toContain("mcp");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("completion subcommand is disabled", () => {
|
|
25
|
+
const proc = spawnSync("bun", [main, "completion", "bash"], { encoding: "utf8" });
|
|
26
|
+
expect(proc.status).toBe(1);
|
|
27
|
+
expect(proc.stderr).toContain("Shell completion is not available");
|
|
18
28
|
});
|
|
19
29
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
diffCreate,
|
|
9
9
|
diffCreateDetails,
|
|
10
10
|
renderCreateTree,
|
|
11
|
+
resolveCreateOptions,
|
|
11
12
|
substituteTemplateContent,
|
|
12
13
|
} from "./create.ts";
|
|
13
14
|
|
|
@@ -39,6 +40,27 @@ describe("argsbarg create", () => {
|
|
|
39
40
|
|
|
40
41
|
test("classNameFromKey", () => {
|
|
41
42
|
expect(classNameFromKey("sqsp-i18n")).toBe("SqspI18n");
|
|
43
|
+
expect(classNameFromKey("at1")).toBe("At1");
|
|
44
|
+
expect(classNameFromKey("1password")).toBe("App1password");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("resolveCreateOptions derives identity defaults from key", () => {
|
|
48
|
+
expect(resolveCreateOptions({ key: "1password", releaseRepo: "org/1password" }).className).toBe(
|
|
49
|
+
"App1password",
|
|
50
|
+
);
|
|
51
|
+
expect(
|
|
52
|
+
resolveCreateOptions({ key: "my-cli", className: "Custom", releaseRepo: "org/my-cli" })
|
|
53
|
+
.className,
|
|
54
|
+
).toBe("Custom");
|
|
55
|
+
const opts = resolveCreateOptions({ key: "at1", releaseRepo: "bdombro/at1" });
|
|
56
|
+
expect(opts.tap).toBe("bdombro/at1");
|
|
57
|
+
expect(opts.releaseRepo).toBe("bdombro/at1");
|
|
58
|
+
expect(opts.homepage).toBe("https://github.com/bdombro/at1");
|
|
59
|
+
expect(opts.desc).toBe("At1 CLI");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("resolveCreateOptions requires release repo", () => {
|
|
63
|
+
expect(() => resolveCreateOptions({ key: "at1" })).toThrow(/release repo/i);
|
|
42
64
|
});
|
|
43
65
|
|
|
44
66
|
test("renderCreateTree includes justfile and create-identity", () => {
|
|
@@ -113,7 +135,7 @@ describe("argsbarg create", () => {
|
|
|
113
135
|
});
|
|
114
136
|
|
|
115
137
|
test("--diff captures drift details", () => {
|
|
116
|
-
const drifts = diffCreateDetails("/nonexistent", { key: "x" });
|
|
138
|
+
const drifts = diffCreateDetails("/nonexistent", { key: "x", releaseRepo: "org/x" });
|
|
117
139
|
expect(drifts.length).toBeGreaterThan(0);
|
|
118
140
|
});
|
|
119
141
|
});
|
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,
|
|
@@ -22,7 +22,7 @@ const sinkProgram = {
|
|
|
22
22
|
topics: { readme: { text: "# readme\n" } },
|
|
23
23
|
},
|
|
24
24
|
mcpServer: { enabled: true },
|
|
25
|
-
|
|
25
|
+
configure: {},
|
|
26
26
|
commands: [
|
|
27
27
|
{
|
|
28
28
|
key: "status",
|
|
@@ -62,7 +62,7 @@ describe("full-example template", () => {
|
|
|
62
62
|
expect(resolveCapabilities(sinkProgram)).toEqual({
|
|
63
63
|
completion: true,
|
|
64
64
|
mcp: true,
|
|
65
|
-
|
|
65
|
+
configure: true,
|
|
66
66
|
docs: true,
|
|
67
67
|
configCommands: true,
|
|
68
68
|
});
|
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);
|
package/src/config/file.test.ts
CHANGED
|
@@ -85,7 +85,7 @@ describe("config/file", () => {
|
|
|
85
85
|
expect(missing).toContain("apiToken");
|
|
86
86
|
expect(missing).not.toContain("port");
|
|
87
87
|
const msg = formatMissingConfigMessage(program, missing);
|
|
88
|
-
expect(msg).toContain("
|
|
88
|
+
expect(msg).toContain("configure");
|
|
89
89
|
} finally {
|
|
90
90
|
if (prev !== undefined) process.env.API_TOKEN = prev;
|
|
91
91
|
}
|
package/src/config/resolve.ts
CHANGED
|
@@ -226,7 +226,7 @@ export function formatMissingConfigMessage(program: CliProgram, keys: string[]):
|
|
|
226
226
|
const path = displayAppConfigPath(program);
|
|
227
227
|
return [
|
|
228
228
|
`Missing required configuration: ${list}`,
|
|
229
|
-
`Configure interactively: ${program.key}
|
|
229
|
+
`Configure interactively: ${program.key} configure`,
|
|
230
230
|
`Or set via: ${program.key} config set <key> <value>`,
|
|
231
231
|
`Config file: ${path}`,
|
|
232
232
|
`See: ${program.key} docs mcp`,
|
|
@@ -239,7 +239,7 @@ export function formatMcpMissingConfigMessage(program: CliProgram, keys: string[
|
|
|
239
239
|
const path = displayAppConfigPath(program);
|
|
240
240
|
return [
|
|
241
241
|
`Missing required configuration: ${list}`,
|
|
242
|
-
`Configure: ${program.key}
|
|
242
|
+
`Configure: ${program.key} configure`,
|
|
243
243
|
`Or set via: ${program.key} config set`,
|
|
244
244
|
`Config file: ${path}`,
|
|
245
245
|
].join("\n");
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
appConfigHasEntries,
|
|
7
|
+
parseConfigureOpts,
|
|
8
|
+
validateConfigureOpts,
|
|
9
|
+
} from "../configure/index.ts";
|
|
10
|
+
import { detectInstalledArtifacts } from "../install/detect-installed.ts";
|
|
11
|
+
import { parseInstallOpts } from "../install/opts.ts";
|
|
12
|
+
import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "../install/paths.ts";
|
|
13
|
+
import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
|
|
14
|
+
import { buildUninstallPlan } from "../install/uninstall.ts";
|
|
15
|
+
import type { CliProgram } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
const fixture: CliProgram = {
|
|
18
|
+
key: "testapp",
|
|
19
|
+
version: "0.0.0",
|
|
20
|
+
description: "Test",
|
|
21
|
+
mcpServer: { enabled: true },
|
|
22
|
+
handler: () => {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let home: string;
|
|
26
|
+
let prevHome: string | undefined;
|
|
27
|
+
let prevXdg: string | undefined;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), "argsbarg-configure-"));
|
|
31
|
+
prevHome = process.env.HOME;
|
|
32
|
+
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
33
|
+
process.env.HOME = home;
|
|
34
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
39
|
+
else process.env.HOME = prevHome;
|
|
40
|
+
if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
41
|
+
else process.env.XDG_CONFIG_HOME = prevXdg;
|
|
42
|
+
rmSync(home, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("configure opts", () => {
|
|
46
|
+
test("validate rejects multiple modes", () => {
|
|
47
|
+
const opts = parseConfigureOpts({ sync: "1", status: "1" });
|
|
48
|
+
expect(validateConfigureOpts(opts)).toContain("only one");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("sync requires --yes", () => {
|
|
52
|
+
const opts = parseConfigureOpts({ sync: "1" });
|
|
53
|
+
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("remove-all requires --yes", () => {
|
|
57
|
+
const opts = parseConfigureOpts({ "remove-all": "1" });
|
|
58
|
+
expect(validateConfigureOpts(opts)).toContain("--yes");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("install paths", () => {
|
|
63
|
+
test("resolveInstallPaths includes skill and mcp paths", () => {
|
|
64
|
+
const paths = resolveInstallPaths(fixture);
|
|
65
|
+
expect(paths.cursorSkillDir).toContain(".cursor/skills");
|
|
66
|
+
expect(paths.cursorMcpPath).toContain("mcp.json");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("claude desktop mcp path on darwin", () => {
|
|
70
|
+
const prev = process.platform;
|
|
71
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
72
|
+
try {
|
|
73
|
+
expect(resolveClaudeDesktopMcpPath(home)).toBe(
|
|
74
|
+
join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
Object.defineProperty(process, "platform", { value: prev });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("detect installed", () => {
|
|
83
|
+
test("detects cursor mcp when configured", () => {
|
|
84
|
+
const paths = resolveInstallPaths(fixture);
|
|
85
|
+
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
86
|
+
writeFileSync(
|
|
87
|
+
paths.cursorMcpPath,
|
|
88
|
+
JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
|
|
89
|
+
"utf8",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const detected = detectInstalledArtifacts(paths, fixture);
|
|
93
|
+
expect(detected.cursorMcp).toBe(true);
|
|
94
|
+
expect(detected.app).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("sync plan", () => {
|
|
99
|
+
test("buildUpdatePlan greenfield includes agent targets", () => {
|
|
100
|
+
const paths = resolveInstallPaths(fixture);
|
|
101
|
+
const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
|
|
102
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
103
|
+
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("buildInstallPlan --all omits app self-install", () => {
|
|
107
|
+
const paths = resolveInstallPaths(fixture);
|
|
108
|
+
const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
|
|
109
|
+
expect(plan.some((a) => a.kind === "app")).toBe(false);
|
|
110
|
+
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("buildInstallPlan respects configure.agentIntegration skill", () => {
|
|
114
|
+
const skillApp: CliProgram = {
|
|
115
|
+
...fixture,
|
|
116
|
+
configure: { agentIntegration: "skill" },
|
|
117
|
+
};
|
|
118
|
+
const paths = resolveInstallPaths(skillApp);
|
|
119
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
120
|
+
mkdirSync(join(home, ".cursor"), { recursive: true });
|
|
121
|
+
const plan = buildInstallPlan(skillApp, paths, parseInstallOpts({ mcp: "1" }));
|
|
122
|
+
expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("remove plan", () => {
|
|
127
|
+
test("buildUninstallPlan --all with nothing installed is empty", () => {
|
|
128
|
+
const paths = resolveInstallPaths(fixture);
|
|
129
|
+
const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
|
|
130
|
+
expect(plan.length).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("app config wizard", () => {
|
|
135
|
+
test("appConfigHasEntries when entries exist", () => {
|
|
136
|
+
expect(
|
|
137
|
+
appConfigHasEntries({
|
|
138
|
+
...fixture,
|
|
139
|
+
appConfig: { entries: { token: { description: "API token." } } },
|
|
140
|
+
}),
|
|
141
|
+
).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("appConfigHasEntries false for empty entries or missing appConfig", () => {
|
|
145
|
+
expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
|
|
146
|
+
expect(appConfigHasEntries(fixture)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|