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.
Files changed (119) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +91 -85
  3. package/docs/README.md +8 -8
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +5 -5
  6. package/docs/cli-program.md +13 -11
  7. package/docs/config-schema.md +36 -9
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +7 -7
  10. package/docs/distribution-homebrew.md +104 -0
  11. package/docs/mcp.md +11 -12
  12. package/docs/output-schema.md +1 -1
  13. package/examples/full-example/Formula/.gitkeep +0 -0
  14. package/examples/full-example/README.md +98 -0
  15. package/examples/full-example/biome.json +22 -0
  16. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  17. package/examples/full-example/justfile +134 -0
  18. package/examples/{consumer-app → full-example}/package.json +10 -3
  19. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  20. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  21. package/examples/full-example/scripts/create-identity.ts +11 -0
  22. package/examples/full-example/scripts/formula-shared.ts +73 -0
  23. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  24. package/examples/full-example/scripts/print-identity.ts +27 -0
  25. package/examples/full-example/src/commands/echo/command.ts +21 -0
  26. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  27. package/examples/full-example/src/commands/status/command.ts +36 -0
  28. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  29. package/examples/full-example/src/index.ts +10 -0
  30. package/examples/full-example/src/program.ts +57 -0
  31. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  32. package/examples/nested.ts +1 -3
  33. package/index.d.ts +49 -81
  34. package/package.json +2 -2
  35. package/src/builtins/builtins.test.ts +84 -64
  36. package/src/builtins/completion-group.ts +17 -17
  37. package/src/builtins/configure-copy.ts +86 -0
  38. package/src/builtins/configure.ts +70 -0
  39. package/src/builtins/dispatch.ts +13 -9
  40. package/src/builtins/index.ts +1 -1
  41. package/src/builtins/mcp.ts +2 -2
  42. package/src/builtins/registry.ts +6 -4
  43. package/src/capabilities.ts +22 -15
  44. package/src/cli-tool/cli-smoke.test.ts +29 -0
  45. package/src/cli-tool/create.test.ts +141 -0
  46. package/src/cli-tool/create.ts +402 -0
  47. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +16 -15
  48. package/src/cli-tool/main.ts +8 -0
  49. package/src/cli-tool/post-create.ts +111 -0
  50. package/src/cli-tool/program.ts +97 -0
  51. package/src/cli-tool/prompt.ts +28 -0
  52. package/src/cli-tool/run-create.ts +138 -0
  53. package/src/cli.ts +0 -2
  54. package/src/config/bootstrap.ts +27 -18
  55. package/src/config/file.test.ts +1 -1
  56. package/src/config/resolve.test.ts +167 -0
  57. package/src/config/resolve.ts +52 -8
  58. package/src/configure/configure.test.ts +148 -0
  59. package/src/configure/index.ts +284 -0
  60. package/src/configure/prompt.ts +40 -0
  61. package/src/docs/api-guide.test.ts +4 -5
  62. package/src/docs/builtin.ts +3 -5
  63. package/src/docs/docs.test.ts +6 -5
  64. package/src/docs/mcp-guide.ts +11 -12
  65. package/src/index.ts +5 -12
  66. package/src/install/binary-placement.test.ts +101 -0
  67. package/src/install/binary-placement.ts +47 -0
  68. package/src/install/install-validate.test.ts +5 -5
  69. package/src/install/normalize-uninstall.ts +11 -0
  70. package/src/install/normalize.ts +4 -19
  71. package/src/install/opts.ts +17 -0
  72. package/src/install/paths.ts +0 -22
  73. package/src/install/plan.ts +14 -6
  74. package/src/install/shell.ts +0 -14
  75. package/src/install/status.test.ts +6 -6
  76. package/src/install/status.ts +0 -6
  77. package/src/install/target-effective.ts +8 -10
  78. package/src/install/target-scope.ts +26 -36
  79. package/src/install/target-types.ts +0 -16
  80. package/src/install/targets/app.ts +19 -28
  81. package/src/install/targets/configure.ts +6 -2
  82. package/src/install/targets/index.ts +0 -3
  83. package/src/install/targets.test.ts +26 -44
  84. package/src/invoke.test.ts +1 -1
  85. package/src/mcp/env.test.ts +92 -0
  86. package/src/mcp/env.ts +15 -14
  87. package/src/mcp/tools.ts +1 -1
  88. package/src/mcp.integration.test.ts +4 -4
  89. package/src/parse.test.ts +13 -14
  90. package/src/prompt.ts +10 -0
  91. package/src/schema.ts +1 -1
  92. package/src/skill/hint.ts +2 -2
  93. package/src/types.ts +48 -22
  94. package/src/validate.ts +22 -28
  95. package/docs/install.md +0 -290
  96. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  97. package/examples/config-app/main.ts +0 -20
  98. package/examples/config-app/program.ts +0 -78
  99. package/examples/config-app/schema.ts +0 -37
  100. package/examples/config-app/types.ts +0 -19
  101. package/examples/consumer-app/README.md +0 -56
  102. package/examples/consumer-app/src/main.ts +0 -15
  103. package/examples/consumer-app/src/program.ts +0 -108
  104. package/src/builtins/install.ts +0 -136
  105. package/src/install/app.ts +0 -94
  106. package/src/install/bootstrap.ts +0 -22
  107. package/src/install/completions.ts +0 -56
  108. package/src/install/index.ts +0 -415
  109. package/src/install/install.test.ts +0 -333
  110. package/src/install/targets/completions.ts +0 -133
  111. package/src/install/update.test.ts +0 -123
  112. package/src/install/update.ts +0 -54
  113. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  114. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  115. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  116. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  117. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  118. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  119. /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("validateInstallConfig", () => {
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
- install: { targets: { allSkills: true } },
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
- install: {
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
- install: {
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
- install: {
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
+ }
@@ -1,30 +1,15 @@
1
- /** Normalizes bare `install` / `install --uninstall` to `--all`. */
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") || flag("update")) {
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") && !flag("uninstall") && !hasInstallTarget) {
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
+ }
@@ -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
  };
@@ -15,26 +15,34 @@ export function buildInstallPlan(
15
15
  return buildInstallPlanFromTargets(root, paths, opts);
16
16
  }
17
17
 
18
- /** Builds update/reinstall actions for detected artifacts within effective targets. */
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
- return buildInstallPlan(root, paths, { ...opts, reinstall: true, all: true });
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(opts: InstallOpts): boolean {
29
- return !!(opts.all || opts.app || opts.reinstall);
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(opts: InstallOpts): boolean {
37
- return !!(opts.all || opts.completions);
44
+ export function wantsInstallCompletions(_opts: InstallOpts): boolean {
45
+ return false;
38
46
  }
39
47
 
40
48
  /** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
@@ -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).toContain("completions");
39
- expect(preview.skill).toEqual([]);
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).toContain("app");
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
  }
@@ -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
- CliInstallConfig,
4
- CliInstallTargets,
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
- install?: CliInstallConfig,
18
+ configure?: CliConfigureConfig,
19
19
  mcpEnabled = false,
20
20
  ): InstallAgentIntegration {
21
- return install?.agentIntegration ?? (mcpEnabled ? "mcp" : "skill");
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: CliInstallTargets | undefined,
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
- install?: CliInstallConfig,
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(install, mcpEnabled);
86
- const user = install?.targets;
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 { CliInstallTargets, CliProgram } from "../types.ts";
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
- function isNonInteractiveInstall(opts: InstallOpts): boolean {
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: CliInstallTargets | undefined,
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: CliInstallTargets | undefined,
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?: CliInstallTargets,
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
- if (scope.configure) return key === "configure";
142
- if (scope.app) return key === "app";
143
- if (scope.completions) return key === "completions";
125
+
126
+ let inScope = false;
144
127
  if (scope.skill) {
145
- return agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
128
+ inScope = inScope || agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
146
129
  }
147
130
  if (scope.mcp) {
148
- return agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
131
+ inScope = inScope || agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
149
132
  }
150
- if (!scoped && mode === "install-scoped") return false;
151
- return false;
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.install?.targets;
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 (!detected[key]) return false;
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.install, 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.install, program);
253
- const integration = resolveAgentIntegration(program.install, program.mcpServer?.enabled === true);
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) =>