argsbarg 4.1.0 → 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 +74 -1
- package/README.md +91 -85
- package/docs/README.md +8 -8
- package/docs/ai-skills.md +9 -9
- package/docs/bundled-docs.md +5 -5
- package/docs/cli-program.md +13 -11
- package/docs/config-schema.md +36 -9
- package/docs/configure.md +177 -0
- package/docs/developing.md +7 -7
- package/docs/distribution-homebrew.md +104 -0
- package/docs/mcp.md +11 -12
- package/docs/output-schema.md +1 -1
- package/examples/full-example/Formula/.gitkeep +0 -0
- package/examples/full-example/README.md +98 -0
- package/examples/full-example/biome.json +22 -0
- package/examples/{consumer-app → full-example}/bun.lock +2 -0
- package/examples/full-example/justfile +134 -0
- package/examples/{consumer-app → full-example}/package.json +10 -3
- package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
- package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/create-identity.ts +11 -0
- package/examples/full-example/scripts/formula-shared.ts +73 -0
- package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
- package/examples/full-example/scripts/print-identity.ts +27 -0
- package/examples/full-example/src/commands/echo/command.ts +21 -0
- package/examples/full-example/src/commands/status/command.test.ts +10 -0
- package/examples/full-example/src/commands/status/command.ts +36 -0
- package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
- package/examples/full-example/src/index.ts +10 -0
- package/examples/full-example/src/program.ts +57 -0
- package/examples/{consumer-app → full-example}/src/types.ts +1 -1
- package/examples/nested.ts +1 -3
- package/index.d.ts +49 -81
- package/package.json +2 -2
- package/src/builtins/builtins.test.ts +84 -64
- package/src/builtins/completion-group.ts +17 -17
- package/src/builtins/configure-copy.ts +86 -0
- package/src/builtins/configure.ts +70 -0
- package/src/builtins/dispatch.ts +13 -9
- package/src/builtins/index.ts +1 -1
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/registry.ts +6 -4
- package/src/capabilities.ts +22 -15
- package/src/cli-tool/cli-smoke.test.ts +29 -0
- package/src/cli-tool/create.test.ts +141 -0
- package/src/cli-tool/create.ts +402 -0
- package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
- package/src/cli-tool/main.ts +8 -0
- package/src/cli-tool/post-create.ts +111 -0
- package/src/cli-tool/program.ts +97 -0
- package/src/cli-tool/prompt.ts +28 -0
- package/src/cli-tool/run-create.ts +138 -0
- package/src/cli.ts +0 -2
- package/src/config/bootstrap.ts +27 -18
- package/src/config/file.test.ts +1 -1
- package/src/config/resolve.test.ts +167 -0
- package/src/config/resolve.ts +52 -8
- 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/api-guide.test.ts +4 -5
- package/src/docs/builtin.ts +3 -5
- package/src/docs/docs.test.ts +6 -5
- package/src/docs/mcp-guide.ts +11 -12
- package/src/index.ts +5 -12
- package/src/install/binary-placement.test.ts +101 -0
- package/src/install/binary-placement.ts +47 -0
- package/src/install/install-validate.test.ts +5 -5
- package/src/install/normalize-uninstall.ts +11 -0
- package/src/install/normalize.ts +4 -19
- package/src/install/opts.ts +17 -0
- package/src/install/paths.ts +0 -22
- package/src/install/plan.ts +14 -6
- package/src/install/shell.ts +0 -14
- package/src/install/status.test.ts +6 -6
- package/src/install/status.ts +0 -6
- package/src/install/target-effective.ts +8 -10
- package/src/install/target-scope.ts +26 -36
- package/src/install/target-types.ts +0 -16
- package/src/install/targets/app.ts +19 -28
- package/src/install/targets/configure.ts +6 -2
- package/src/install/targets/index.ts +0 -3
- package/src/install/targets.test.ts +26 -44
- package/src/invoke.test.ts +1 -1
- package/src/mcp/env.test.ts +92 -0
- package/src/mcp/env.ts +15 -14
- package/src/mcp/tools.ts +1 -1
- package/src/mcp.integration.test.ts +4 -4
- package/src/parse.test.ts +13 -14
- package/src/prompt.ts +10 -0
- package/src/schema.ts +1 -1
- package/src/skill/hint.ts +2 -2
- package/src/types.ts +48 -22
- package/src/validate.ts +22 -28
- package/docs/install.md +0 -290
- package/docs/templates/cursor/rules/cli-program.mdc +0 -31
- package/examples/config-app/main.ts +0 -20
- package/examples/config-app/program.ts +0 -78
- package/examples/config-app/schema.ts +0 -37
- package/examples/config-app/types.ts +0 -19
- package/examples/consumer-app/README.md +0 -56
- package/examples/consumer-app/src/main.ts +0 -15
- package/examples/consumer-app/src/program.ts +0 -108
- package/src/builtins/install.ts +0 -136
- package/src/install/app.ts +0 -94
- package/src/install/bootstrap.ts +0 -22
- package/src/install/completions.ts +0 -56
- package/src/install/index.ts +0 -415
- package/src/install/install.test.ts +0 -333
- package/src/install/targets/completions.ts +0 -133
- package/src/install/update.test.ts +0 -123
- package/src/install/update.ts +0 -54
- /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
- /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
- /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { CliProgram } from "../types.ts";
|
|
6
|
+
import {
|
|
7
|
+
isAppInstalled,
|
|
8
|
+
isExternallyManagedBinary,
|
|
9
|
+
resolvePathCommand,
|
|
10
|
+
} from "./binary-placement.ts";
|
|
11
|
+
|
|
12
|
+
const program: CliProgram = {
|
|
13
|
+
key: "placementapp",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
description: "x",
|
|
16
|
+
handler: () => {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let tmp: string;
|
|
20
|
+
let prevPath: string | undefined;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmp = mkdtempSync(join(tmpdir(), "argsbarg-placement-"));
|
|
24
|
+
prevPath = process.env.PATH;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
if (prevPath === undefined) delete process.env.PATH;
|
|
29
|
+
else process.env.PATH = prevPath;
|
|
30
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("isExternallyManagedBinary", () => {
|
|
34
|
+
test("false when command is not on PATH", () => {
|
|
35
|
+
process.env.PATH = tmp;
|
|
36
|
+
expect(isExternallyManagedBinary("placementapp-not-on-path")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("true when PATH resolves to execPath", () => {
|
|
40
|
+
const bin = join(tmp, "placementapp");
|
|
41
|
+
writeFileSync(bin, "#!/bin/sh\n", "utf8");
|
|
42
|
+
chmodSync(bin, 0o755);
|
|
43
|
+
process.env.PATH = tmp;
|
|
44
|
+
expect(isExternallyManagedBinary("placementapp", bin)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("true when PATH entry is a symlink to execPath", () => {
|
|
48
|
+
const bin = join(tmp, "placementapp");
|
|
49
|
+
const target = join(tmp, "real-binary");
|
|
50
|
+
writeFileSync(target, "fake", "utf8");
|
|
51
|
+
symlinkSync(target, bin);
|
|
52
|
+
process.env.PATH = tmp;
|
|
53
|
+
expect(isExternallyManagedBinary("placementapp", target)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("false when PATH points at a different binary", () => {
|
|
57
|
+
const bin = join(tmp, "placementapp");
|
|
58
|
+
const other = join(tmp, "other");
|
|
59
|
+
writeFileSync(bin, "a", "utf8");
|
|
60
|
+
writeFileSync(other, "b", "utf8");
|
|
61
|
+
process.env.PATH = tmp;
|
|
62
|
+
expect(isExternallyManagedBinary("placementapp", other)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("isAppInstalled", () => {
|
|
67
|
+
test("true when externally managed", () => {
|
|
68
|
+
const bin = join(tmp, program.key);
|
|
69
|
+
const prevExec = process.execPath;
|
|
70
|
+
writeFileSync(bin, "x", "utf8");
|
|
71
|
+
chmodSync(bin, 0o755);
|
|
72
|
+
process.env.PATH = tmp;
|
|
73
|
+
process.execPath = bin;
|
|
74
|
+
try {
|
|
75
|
+
expect(isAppInstalled(program)).toBe(true);
|
|
76
|
+
} finally {
|
|
77
|
+
process.execPath = prevExec;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("false when not on PATH and no local copy", () => {
|
|
82
|
+
const home = mkdtempSync(join(tmpdir(), "argsbarg-placement-home-"));
|
|
83
|
+
const prevHome = process.env.HOME;
|
|
84
|
+
process.env.HOME = home;
|
|
85
|
+
process.env.PATH = tmp;
|
|
86
|
+
try {
|
|
87
|
+
expect(isAppInstalled(program)).toBe(false);
|
|
88
|
+
} finally {
|
|
89
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
90
|
+
else process.env.HOME = prevHome;
|
|
91
|
+
rmSync(home, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("resolvePathCommand", () => {
|
|
97
|
+
test("returns undefined when missing", () => {
|
|
98
|
+
process.env.PATH = tmp;
|
|
99
|
+
expect(resolvePathCommand("missing-cmd")).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import type { CliProgram } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
/** Resolves a command name on PATH to its real path, if present. */
|
|
6
|
+
export function resolvePathCommand(key: string): string | undefined {
|
|
7
|
+
const pathEnv = process.env.PATH ?? "";
|
|
8
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
9
|
+
if (!dir) continue;
|
|
10
|
+
const candidate = join(dir, key);
|
|
11
|
+
try {
|
|
12
|
+
accessSync(candidate, constants.F_OK);
|
|
13
|
+
return realpathSync(candidate);
|
|
14
|
+
} catch {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const found = Bun.which(key);
|
|
18
|
+
if (found === null) return undefined;
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(found);
|
|
21
|
+
} catch {
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function realpathOrSelf(path: string): string {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
} catch {
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when PATH resolves to the running executable (e.g. Homebrew Cellar). */
|
|
35
|
+
export function isExternallyManagedBinary(
|
|
36
|
+
key: string,
|
|
37
|
+
execPath: string = process.execPath,
|
|
38
|
+
): boolean {
|
|
39
|
+
const resolved = resolvePathCommand(key);
|
|
40
|
+
if (!resolved) return false;
|
|
41
|
+
return resolved === realpathOrSelf(execPath);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the app binary is available on PATH (e.g. Homebrew). */
|
|
45
|
+
export function isAppInstalled(program: CliProgram): boolean {
|
|
46
|
+
return isExternallyManagedBinary(program.key);
|
|
47
|
+
}
|
|
@@ -10,7 +10,7 @@ const base: CliProgram = {
|
|
|
10
10
|
handler: () => {},
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
describe("
|
|
13
|
+
describe("validateConfigureConfig", () => {
|
|
14
14
|
test("accepts empty install config", () => {
|
|
15
15
|
expect(() => cliValidateProgram(base)).not.toThrow();
|
|
16
16
|
});
|
|
@@ -18,7 +18,7 @@ describe("validateInstallConfig", () => {
|
|
|
18
18
|
test("rejects removed allSkills shorthand", () => {
|
|
19
19
|
const program = {
|
|
20
20
|
...base,
|
|
21
|
-
|
|
21
|
+
configure: { targets: { allSkills: true } },
|
|
22
22
|
} as CliProgram;
|
|
23
23
|
expect(() => cliValidateProgram(program)).toThrow(CliSchemaValidationError);
|
|
24
24
|
expect(() => cliValidateProgram(program)).toThrow(/allSkills/);
|
|
@@ -28,7 +28,7 @@ describe("validateInstallConfig", () => {
|
|
|
28
28
|
const program: CliProgram = {
|
|
29
29
|
...base,
|
|
30
30
|
mcpServer: { enabled: true },
|
|
31
|
-
|
|
31
|
+
configure: {
|
|
32
32
|
agentIntegration: "mcp",
|
|
33
33
|
targets: { cursorMcp: true, cursorSkill: true },
|
|
34
34
|
},
|
|
@@ -39,7 +39,7 @@ describe("validateInstallConfig", () => {
|
|
|
39
39
|
test("rejects explicit MCP target in skill integration mode", () => {
|
|
40
40
|
const program: CliProgram = {
|
|
41
41
|
...base,
|
|
42
|
-
|
|
42
|
+
configure: {
|
|
43
43
|
agentIntegration: "skill",
|
|
44
44
|
targets: { cursorMcp: true },
|
|
45
45
|
},
|
|
@@ -51,7 +51,7 @@ describe("validateInstallConfig", () => {
|
|
|
51
51
|
const program: CliProgram = {
|
|
52
52
|
...base,
|
|
53
53
|
mcpServer: { enabled: true },
|
|
54
|
-
|
|
54
|
+
configure: {
|
|
55
55
|
agentIntegration: "both",
|
|
56
56
|
targets: { cursorMcp: true, cursorSkill: true },
|
|
57
57
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Normalizes bare `uninstall` to `--all`. */
|
|
2
|
+
export function normalizeUninstallRawOpts(raw: Record<string, string>): Record<string, string> {
|
|
3
|
+
const flag = (name: string) => raw[name] === "1";
|
|
4
|
+
const out: Record<string, string> = { ...raw, uninstall: "1" };
|
|
5
|
+
|
|
6
|
+
const hasUninstallTarget = flag("all") || flag("skill") || flag("mcp") || flag("configure");
|
|
7
|
+
if (!hasUninstallTarget) {
|
|
8
|
+
out.all = "1";
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
package/src/install/normalize.ts
CHANGED
|
@@ -1,30 +1,15 @@
|
|
|
1
|
-
/** Normalizes bare `install`
|
|
1
|
+
/** Normalizes bare `install` to `--all`. */
|
|
2
2
|
export function normalizeInstallRawOpts(raw: Record<string, string>): Record<string, string> {
|
|
3
3
|
const flag = (name: string) => raw[name] === "1";
|
|
4
4
|
const out = { ...raw };
|
|
5
5
|
|
|
6
|
-
if (flag("status") || flag("reinstall")
|
|
6
|
+
if (flag("status") || flag("reinstall")) {
|
|
7
7
|
return out;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const hasInstallTarget =
|
|
11
|
-
flag("all") || flag("app") || flag("completions") || flag("skill") || flag("mcp");
|
|
12
|
-
const hasUninstallTarget =
|
|
13
|
-
flag("all") ||
|
|
14
|
-
flag("app") ||
|
|
15
|
-
flag("completions") ||
|
|
16
|
-
flag("skill") ||
|
|
17
|
-
flag("mcp") ||
|
|
18
|
-
flag("configure");
|
|
10
|
+
const hasInstallTarget = flag("all") || flag("skill") || flag("mcp");
|
|
19
11
|
|
|
20
|
-
if (flag("configure") && !
|
|
21
|
-
return out;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (flag("uninstall")) {
|
|
25
|
-
if (!hasUninstallTarget) {
|
|
26
|
-
out.all = "1";
|
|
27
|
-
}
|
|
12
|
+
if (flag("configure") && !hasInstallTarget) {
|
|
28
13
|
return out;
|
|
29
14
|
}
|
|
30
15
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InstallOpts } from "./target-types.ts";
|
|
2
|
+
|
|
3
|
+
export function parseInstallOpts(raw: Record<string, string>): InstallOpts {
|
|
4
|
+
const flag = (name: string) => raw[name] === "1";
|
|
5
|
+
return {
|
|
6
|
+
all: flag("all"),
|
|
7
|
+
skill: flag("skill"),
|
|
8
|
+
mcp: flag("mcp"),
|
|
9
|
+
reinstall: flag("reinstall"),
|
|
10
|
+
status: flag("status"),
|
|
11
|
+
uninstall: flag("uninstall"),
|
|
12
|
+
configure: flag("configure"),
|
|
13
|
+
yes: flag("yes"),
|
|
14
|
+
dry: flag("dry"),
|
|
15
|
+
json: flag("json"),
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/install/paths.ts
CHANGED
|
@@ -8,11 +8,6 @@ import { resolveOpenclawConfigPath } from "./mcp-openclaw.ts";
|
|
|
8
8
|
import { resolveOpenCodeConfigPathForInstall } from "./mcp-opencode.ts";
|
|
9
9
|
|
|
10
10
|
export interface InstallPaths {
|
|
11
|
-
appDir: string;
|
|
12
|
-
appPath: string;
|
|
13
|
-
bashCompletion: string;
|
|
14
|
-
zshCompletion: string;
|
|
15
|
-
fishCompletion: string;
|
|
16
11
|
cursorSkillDir: string;
|
|
17
12
|
claudeSkillDir: string;
|
|
18
13
|
codexSkillDir: string;
|
|
@@ -25,8 +20,6 @@ export interface InstallPaths {
|
|
|
25
20
|
chatGptMcpPath: string;
|
|
26
21
|
codexConfigPath: string;
|
|
27
22
|
openclawConfigPath: string;
|
|
28
|
-
bashRc: string;
|
|
29
|
-
zshRc: string;
|
|
30
23
|
mcpName: string;
|
|
31
24
|
skillDirName: string;
|
|
32
25
|
}
|
|
@@ -38,11 +31,6 @@ export function displayInstallPath(path: string): string {
|
|
|
38
31
|
return displayHomePath(path);
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
/** App install directory (`~/.local/bin`). */
|
|
42
|
-
export function resolveAppDir(): string {
|
|
43
|
-
return join(userHome(), ".local", "bin");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
/** Resolves Claude Desktop `claude_desktop_config.json` for the current OS. */
|
|
47
35
|
export function resolveClaudeDesktopMcpPath(home: string): string {
|
|
48
36
|
if (process.platform === "darwin") {
|
|
@@ -80,22 +68,14 @@ export function chatGptDesktopPresent(_home: string, configPath: string): boolea
|
|
|
80
68
|
/** Resolves all install artifact paths for a program root. */
|
|
81
69
|
export function resolveInstallPaths(root: CliProgram): InstallPaths {
|
|
82
70
|
const home = userHome();
|
|
83
|
-
const appDir = resolveAppDir();
|
|
84
|
-
const key = root.key;
|
|
85
71
|
const skillDirName = sanitizeToolSegment(root.key);
|
|
86
72
|
const codexSlug = skillDirNameForTarget(root.key, "codex");
|
|
87
73
|
const opencodeSlug = skillDirNameForTarget(root.key, "opencode");
|
|
88
74
|
const openclawSlug = skillDirNameForTarget(root.key, "openclaw");
|
|
89
|
-
const xdgConfig = xdgConfigHome(home);
|
|
90
75
|
const claudeDesktopMcpPath = resolveClaudeDesktopMcpPath(home);
|
|
91
76
|
const chatGptMcpPath = resolveChatGptMcpPath(home);
|
|
92
77
|
|
|
93
78
|
return {
|
|
94
|
-
appDir,
|
|
95
|
-
appPath: join(appDir, key),
|
|
96
|
-
bashCompletion: join(home, ".bash_completion.d", key),
|
|
97
|
-
zshCompletion: join(home, ".zsh", "completions", `_${key}`),
|
|
98
|
-
fishCompletion: join(xdgConfig, "fish", "completions", `${key}.fish`),
|
|
99
79
|
cursorSkillDir: join(home, ".cursor", "skills", skillDirName),
|
|
100
80
|
claudeSkillDir: join(home, ".claude", "skills", skillDirName),
|
|
101
81
|
codexSkillDir: join(home, ".codex", "skills", codexSlug),
|
|
@@ -108,8 +88,6 @@ export function resolveInstallPaths(root: CliProgram): InstallPaths {
|
|
|
108
88
|
chatGptMcpPath,
|
|
109
89
|
codexConfigPath: join(home, ".codex", "config.toml"),
|
|
110
90
|
openclawConfigPath: resolveOpenclawConfigPath(home),
|
|
111
|
-
bashRc: join(home, ".bashrc"),
|
|
112
|
-
zshRc: join(home, ".zshrc"),
|
|
113
91
|
mcpName: mcpServerId(root),
|
|
114
92
|
skillDirName,
|
|
115
93
|
};
|
package/src/install/plan.ts
CHANGED
|
@@ -15,26 +15,34 @@ export function buildInstallPlan(
|
|
|
15
15
|
return buildInstallPlanFromTargets(root, paths, opts);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** Builds update/reinstall actions
|
|
18
|
+
/** Builds update/reinstall actions; greenfield fallback when nothing detected. */
|
|
19
19
|
export function buildUpdatePlan(
|
|
20
20
|
root: CliProgram,
|
|
21
21
|
paths: InstallPaths,
|
|
22
22
|
opts: InstallOpts,
|
|
23
23
|
): InstallAction[] {
|
|
24
|
-
|
|
24
|
+
const refresh = buildInstallPlanFromTargets(root, paths, {
|
|
25
|
+
...opts,
|
|
26
|
+
reinstall: true,
|
|
27
|
+
all: true,
|
|
28
|
+
});
|
|
29
|
+
if (refresh.length > 0) {
|
|
30
|
+
return refresh;
|
|
31
|
+
}
|
|
32
|
+
return buildInstallPlanFromTargets(root, paths, { ...opts, reinstall: false, all: true });
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
|
28
|
-
export function wantsInstallApp(
|
|
29
|
-
return
|
|
36
|
+
export function wantsInstallApp(_opts: InstallOpts): boolean {
|
|
37
|
+
return false;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/** @deprecated Use {@link wantsInstallApp}. */
|
|
33
41
|
export const wantsInstallBin = wantsInstallApp;
|
|
34
42
|
|
|
35
43
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
|
36
|
-
export function wantsInstallCompletions(
|
|
37
|
-
return
|
|
44
|
+
export function wantsInstallCompletions(_opts: InstallOpts): boolean {
|
|
45
|
+
return false;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
/** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
|
package/src/install/shell.ts
CHANGED
|
@@ -21,20 +21,6 @@ export function rcMarkerEnd(appKey: string, tag: string): string {
|
|
|
21
21
|
return `# end ${appKey}:${tag}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Returns rc snippet block for PATH, or null if already present. */
|
|
25
|
-
export function buildPathRcBlock(appKey: string, appDir: string): string {
|
|
26
|
-
const start = rcMarkerStart(appKey, "path");
|
|
27
|
-
const end = rcMarkerEnd(appKey, "path");
|
|
28
|
-
return [start, `export PATH="${appDir}:$PATH"`, end].join("\n");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Returns rc snippet block for zsh fpath, or null if already present. */
|
|
32
|
-
export function buildZshFpathRcBlock(appKey: string, completionsDir: string): string {
|
|
33
|
-
const start = rcMarkerStart(appKey, "fpath");
|
|
34
|
-
const end = rcMarkerEnd(appKey, "fpath");
|
|
35
|
-
return [start, `fpath=(${completionsDir} $fpath)`, end].join("\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
24
|
/** Removes a marker-delimited block from rc file content. */
|
|
39
25
|
export function removeRcBlock(content: string, appKey: string, tag: string): string {
|
|
40
26
|
const start = rcMarkerStart(appKey, tag);
|
|
@@ -34,9 +34,9 @@ describe("resolveInstallTargetPreview", () => {
|
|
|
34
34
|
const paths = resolveInstallPaths(program);
|
|
35
35
|
const preview = resolveInstallTargetPreview(program, paths);
|
|
36
36
|
expect(preview.agentIntegration).toBe("mcp");
|
|
37
|
-
expect(preview.all).toContain("app");
|
|
38
|
-
expect(preview.all).
|
|
39
|
-
expect(preview.
|
|
37
|
+
expect(preview.all).not.toContain("app");
|
|
38
|
+
expect(preview.all.length).toBeGreaterThan(0);
|
|
39
|
+
expect(preview.all.every((k) => k.endsWith("Mcp") || k === "configure")).toBe(true);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test("shell app previews skill keys", () => {
|
|
@@ -49,7 +49,8 @@ describe("resolveInstallTargetPreview", () => {
|
|
|
49
49
|
const paths = resolveInstallPaths(program);
|
|
50
50
|
const preview = resolveInstallTargetPreview(program, paths);
|
|
51
51
|
expect(preview.agentIntegration).toBe("skill");
|
|
52
|
-
expect(preview.all).toContain("app");
|
|
52
|
+
expect(preview.all).not.toContain("app");
|
|
53
|
+
expect(preview.all.length).toBeGreaterThan(0);
|
|
53
54
|
expect(preview.mcp).toEqual([]);
|
|
54
55
|
});
|
|
55
56
|
});
|
|
@@ -76,8 +77,7 @@ describe("printInstallStatus json", () => {
|
|
|
76
77
|
effective: { all: string[]; mcp: string[]; skill: string[] };
|
|
77
78
|
};
|
|
78
79
|
expect(parsed.agentIntegration).toBe("mcp");
|
|
79
|
-
expect(parsed.effective.all).
|
|
80
|
-
expect(Array.isArray(parsed.effective.skill)).toBe(true);
|
|
80
|
+
expect(parsed.effective.all.length).toBeGreaterThan(0);
|
|
81
81
|
} finally {
|
|
82
82
|
process.stdout.write = orig;
|
|
83
83
|
}
|
package/src/install/status.ts
CHANGED
|
@@ -42,9 +42,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
|
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
44
|
if (status.app) json.app = status.app;
|
|
45
|
-
if (status.bashCompletion) json.bashCompletion = status.bashCompletion;
|
|
46
|
-
if (status.zshCompletion) json.zshCompletion = status.zshCompletion;
|
|
47
|
-
if (status.fishCompletion) json.fishCompletion = status.fishCompletion;
|
|
48
45
|
if (status.cursorSkill) json.cursorSkill = status.cursorSkill;
|
|
49
46
|
if (status.claudeSkill) json.claudeSkill = status.claudeSkill;
|
|
50
47
|
if (status.cursorMcp) json.cursorMcp = status.cursorMcp;
|
|
@@ -60,9 +57,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
|
|
|
60
57
|
installOut(`Installed artifacts for ${root.key}:`, opts);
|
|
61
58
|
const lines: [string, string | undefined][] = [
|
|
62
59
|
["app", status.app],
|
|
63
|
-
["bash completion", status.bashCompletion],
|
|
64
|
-
["zsh completion", status.zshCompletion],
|
|
65
|
-
["fish completion", status.fishCompletion],
|
|
66
60
|
["cursor skill", status.cursorSkill],
|
|
67
61
|
["claude skill", status.claudeSkill],
|
|
68
62
|
["cursor mcp", status.cursorMcp],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveCapabilities } from "../capabilities.ts";
|
|
2
2
|
import type {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CliConfigureConfig,
|
|
4
|
+
CliConfigureTargets,
|
|
5
5
|
CliProgram,
|
|
6
6
|
InstallAgentIntegration,
|
|
7
7
|
InstallTargetSpec,
|
|
@@ -15,10 +15,10 @@ export type { InstallTargetSpec, ResolvedInstallTarget } from "../types.ts";
|
|
|
15
15
|
export type { InstallPlanMode, InstallScope } from "./target-types.ts";
|
|
16
16
|
|
|
17
17
|
export function resolveAgentIntegration(
|
|
18
|
-
|
|
18
|
+
configure?: CliConfigureConfig,
|
|
19
19
|
mcpEnabled = false,
|
|
20
20
|
): InstallAgentIntegration {
|
|
21
|
-
return
|
|
21
|
+
return configure?.agentIntegration ?? (mcpEnabled ? "mcp" : "skill");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Resolves a boolean or object target spec against category defaults. */
|
|
@@ -52,7 +52,7 @@ function artifactDefaults(
|
|
|
52
52
|
|
|
53
53
|
function applyAgentPairDedupe(
|
|
54
54
|
out: Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>,
|
|
55
|
-
user:
|
|
55
|
+
user: CliConfigureTargets | undefined,
|
|
56
56
|
integration: InstallAgentIntegration,
|
|
57
57
|
): void {
|
|
58
58
|
if (integration === "both") return;
|
|
@@ -78,12 +78,12 @@ function applyAgentPairDedupe(
|
|
|
78
78
|
|
|
79
79
|
/** Effective per-artifact gates for install.targets. */
|
|
80
80
|
export function resolveEffectiveInstallTargets(
|
|
81
|
-
|
|
81
|
+
configure?: CliConfigureConfig,
|
|
82
82
|
program?: Pick<CliProgram, "mcpServer">,
|
|
83
83
|
): Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }> {
|
|
84
84
|
const mcpEnabled = program?.mcpServer?.enabled === true;
|
|
85
|
-
const integration = resolveAgentIntegration(
|
|
86
|
-
const user =
|
|
85
|
+
const integration = resolveAgentIntegration(configure, mcpEnabled);
|
|
86
|
+
const user = configure?.targets;
|
|
87
87
|
const out = {} as Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>;
|
|
88
88
|
for (const key of INSTALL_ARTIFACT_KEYS) {
|
|
89
89
|
out[key] = resolveInstallTargetSpec(user?.[key], artifactDefaults(key, integration));
|
|
@@ -107,8 +107,6 @@ export interface InstallTargetPreview {
|
|
|
107
107
|
/** Derives plan mode from install CLI flags. */
|
|
108
108
|
export function resolveInstallPlanMode(opts: {
|
|
109
109
|
all?: boolean;
|
|
110
|
-
app?: boolean;
|
|
111
|
-
completions?: boolean;
|
|
112
110
|
skill?: boolean;
|
|
113
111
|
mcp?: boolean;
|
|
114
112
|
configure?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveCapabilities } from "../capabilities.ts";
|
|
2
2
|
import { appConfigInstalled } from "../config/file.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { CliConfigureTargets, CliProgram } from "../types.ts";
|
|
4
4
|
import type { InstallPaths } from "./paths.ts";
|
|
5
5
|
import {
|
|
6
6
|
type InstallPlanMode,
|
|
@@ -30,9 +30,6 @@ export { mcpCategoryEnabled } from "./target-effective.ts";
|
|
|
30
30
|
function emptyInstalledArtifacts(): InstalledArtifacts {
|
|
31
31
|
return {
|
|
32
32
|
app: false,
|
|
33
|
-
bashCompletion: false,
|
|
34
|
-
zshCompletion: false,
|
|
35
|
-
fishCompletion: false,
|
|
36
33
|
cursorSkill: false,
|
|
37
34
|
claudeSkill: false,
|
|
38
35
|
codexSkill: false,
|
|
@@ -45,8 +42,6 @@ function emptyInstalledArtifacts(): InstalledArtifacts {
|
|
|
45
42
|
codexMcp: false,
|
|
46
43
|
openclawMcp: false,
|
|
47
44
|
chatGptMcp: false,
|
|
48
|
-
bashRcPath: false,
|
|
49
|
-
zshRcFpath: false,
|
|
50
45
|
};
|
|
51
46
|
}
|
|
52
47
|
|
|
@@ -62,18 +57,10 @@ export function detectInstalledArtifacts(
|
|
|
62
57
|
return out;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
return !!(opts.yes || opts.json || opts.reinstall || opts.update || opts.dry);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Scope flags; non-interactive `install --mcp` implicitly includes app. */
|
|
60
|
+
/** Scope flags for install/uninstall. */
|
|
70
61
|
export function resolveInstallScope(opts: InstallOpts): InstallScope {
|
|
71
|
-
const assumeApp =
|
|
72
|
-
!opts.uninstall && opts.mcp && !opts.all && !opts.app && isNonInteractiveInstall(opts);
|
|
73
62
|
return {
|
|
74
63
|
all: opts.all,
|
|
75
|
-
app: opts.app || assumeApp,
|
|
76
|
-
completions: opts.completions,
|
|
77
64
|
skill: opts.skill,
|
|
78
65
|
mcp: opts.mcp,
|
|
79
66
|
configure: opts.configure,
|
|
@@ -91,7 +78,7 @@ export function buildDetectedSnapshot(root: CliProgram, paths: InstallPaths): De
|
|
|
91
78
|
}
|
|
92
79
|
|
|
93
80
|
function targetExplicitlyConfigured(
|
|
94
|
-
user:
|
|
81
|
+
user: CliConfigureTargets | undefined,
|
|
95
82
|
key: CliInstallArtifactKey,
|
|
96
83
|
): boolean {
|
|
97
84
|
return user?.[key] !== undefined;
|
|
@@ -102,7 +89,7 @@ function agentCategoryInScope(
|
|
|
102
89
|
categoryKeys: readonly CliInstallArtifactKey[],
|
|
103
90
|
category: "skill" | "mcp",
|
|
104
91
|
effective: Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>,
|
|
105
|
-
targets:
|
|
92
|
+
targets: CliConfigureTargets | undefined,
|
|
106
93
|
root: CliProgram,
|
|
107
94
|
): boolean {
|
|
108
95
|
if (!categoryKeys.includes(key)) return false;
|
|
@@ -118,7 +105,7 @@ export function isArtifactInScope(
|
|
|
118
105
|
effective: Record<CliInstallArtifactKey, { enabled: boolean; includedInAll: boolean }>,
|
|
119
106
|
mode: InstallPlanMode,
|
|
120
107
|
root: CliProgram,
|
|
121
|
-
targets?:
|
|
108
|
+
targets?: CliConfigureTargets,
|
|
122
109
|
): boolean {
|
|
123
110
|
if (mode === "uninstall-all") {
|
|
124
111
|
return true;
|
|
@@ -133,22 +120,20 @@ export function isArtifactInScope(
|
|
|
133
120
|
return t.enabled && t.includedInAll;
|
|
134
121
|
}
|
|
135
122
|
|
|
136
|
-
const scoped =
|
|
137
|
-
scope.app || scope.completions || scope.skill || scope.mcp || scope.configure || false;
|
|
138
|
-
|
|
139
123
|
if (mode === "uninstall-scoped" || mode === "install-scoped") {
|
|
140
124
|
if (!effective[key].enabled) return false;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (scope.completions) return key === "completions";
|
|
125
|
+
|
|
126
|
+
let inScope = false;
|
|
144
127
|
if (scope.skill) {
|
|
145
|
-
|
|
128
|
+
inScope = inScope || agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
|
|
146
129
|
}
|
|
147
130
|
if (scope.mcp) {
|
|
148
|
-
|
|
131
|
+
inScope = inScope || agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
|
|
149
132
|
}
|
|
150
|
-
if (
|
|
151
|
-
|
|
133
|
+
if (scope.configure && key === "configure") {
|
|
134
|
+
inScope = true;
|
|
135
|
+
}
|
|
136
|
+
return inScope;
|
|
152
137
|
}
|
|
153
138
|
|
|
154
139
|
return false;
|
|
@@ -165,7 +150,7 @@ export function shouldIncludeArtifact(
|
|
|
165
150
|
detected?: Partial<Record<CliInstallArtifactKey, boolean>>,
|
|
166
151
|
): boolean {
|
|
167
152
|
const target = installTargetForKey(key);
|
|
168
|
-
const targets = root.
|
|
153
|
+
const targets = root.configure?.targets;
|
|
169
154
|
|
|
170
155
|
if (key !== "configure" && target && !target.isAvailable(root, paths)) {
|
|
171
156
|
return false;
|
|
@@ -174,7 +159,11 @@ export function shouldIncludeArtifact(
|
|
|
174
159
|
return false;
|
|
175
160
|
}
|
|
176
161
|
if (mode === "refresh" && detected) {
|
|
177
|
-
if (
|
|
162
|
+
if (key === "app") {
|
|
163
|
+
// app is status-only; refresh skills/MCP only
|
|
164
|
+
} else if (!detected[key]) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
178
167
|
}
|
|
179
168
|
if ((mode === "uninstall-all" || mode === "uninstall-scoped") && detected) {
|
|
180
169
|
const det = detected[key];
|
|
@@ -194,6 +183,7 @@ export function shouldIncludeArtifact(
|
|
|
194
183
|
return isArtifactInScope(key, scope, effective, mode, root, targets);
|
|
195
184
|
}
|
|
196
185
|
if (mode === "refresh") {
|
|
186
|
+
if (key === "app") return false;
|
|
197
187
|
return effective[key].enabled;
|
|
198
188
|
}
|
|
199
189
|
return isArtifactInScope(key, scope, effective, mode, root, targets);
|
|
@@ -206,7 +196,7 @@ export function buildTargetPlanContext(
|
|
|
206
196
|
opts: InstallOpts,
|
|
207
197
|
detected: DetectedSnapshot,
|
|
208
198
|
): TargetPlanContext {
|
|
209
|
-
const effective = resolveEffectiveInstallTargets(root.
|
|
199
|
+
const effective = resolveEffectiveInstallTargets(root.configure, root);
|
|
210
200
|
const mode = resolveInstallPlanMode(opts);
|
|
211
201
|
const scope = resolveInstallScope(opts);
|
|
212
202
|
const detPartial = Object.fromEntries(
|
|
@@ -235,9 +225,6 @@ export function detectedForArtifact(
|
|
|
235
225
|
detected: DetectedSnapshot,
|
|
236
226
|
): boolean {
|
|
237
227
|
const target = installTargetForKey(key);
|
|
238
|
-
if (key === "completions") {
|
|
239
|
-
return detected.bashCompletion || detected.zshCompletion || detected.fishCompletion;
|
|
240
|
-
}
|
|
241
228
|
if (key === "configure") {
|
|
242
229
|
return detected.appConfig ?? false;
|
|
243
230
|
}
|
|
@@ -249,8 +236,11 @@ export function resolveInstallTargetPreview(
|
|
|
249
236
|
program: CliProgram,
|
|
250
237
|
paths: InstallPaths,
|
|
251
238
|
): InstallTargetPreview {
|
|
252
|
-
const effective = resolveEffectiveInstallTargets(program.
|
|
253
|
-
const integration = resolveAgentIntegration(
|
|
239
|
+
const effective = resolveEffectiveInstallTargets(program.configure, program);
|
|
240
|
+
const integration = resolveAgentIntegration(
|
|
241
|
+
program.configure,
|
|
242
|
+
program.mcpServer?.enabled === true,
|
|
243
|
+
);
|
|
254
244
|
|
|
255
245
|
const keysForScope = (scope: InstallScope, mode: InstallPlanMode): CliInstallArtifactKey[] =>
|
|
256
246
|
INSTALL_ARTIFACT_KEYS.filter((key) =>
|