argsbarg 4.1.1 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +5 -5
  3. package/docs/README.md +3 -3
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +4 -4
  6. package/docs/cli-program.md +8 -8
  7. package/docs/config-schema.md +2 -2
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +1 -1
  10. package/docs/distribution-homebrew.md +12 -9
  11. package/docs/mcp.md +9 -9
  12. package/examples/full-example/README.md +3 -3
  13. package/examples/full-example/justfile +6 -6
  14. package/examples/full-example/scripts/formula-shared.ts +3 -2
  15. package/examples/full-example/src/program.ts +3 -3
  16. package/examples/nested.ts +1 -1
  17. package/index.d.ts +23 -16
  18. package/package.json +1 -1
  19. package/src/builtins/builtins.test.ts +105 -61
  20. package/src/builtins/completion-group.ts +4 -6
  21. package/src/builtins/config.test.ts +8 -0
  22. package/src/builtins/configure-copy.ts +86 -0
  23. package/src/builtins/configure.ts +70 -0
  24. package/src/builtins/dispatch.ts +13 -33
  25. package/src/builtins/index.ts +1 -1
  26. package/src/builtins/mcp.ts +2 -2
  27. package/src/builtins/registry.ts +6 -6
  28. package/src/capabilities.ts +22 -13
  29. package/src/cli-tool/cli-smoke.test.ts +21 -3
  30. package/src/cli-tool/create.test.ts +36 -1
  31. package/src/cli-tool/create.ts +26 -4
  32. package/src/cli-tool/full-example-capabilities.test.ts +10 -2
  33. package/src/cli-tool/main.ts +0 -0
  34. package/src/cli-tool/program.ts +20 -5
  35. package/src/cli-tool/run-create.ts +8 -19
  36. package/src/config/bootstrap.ts +11 -9
  37. package/src/config/context.test.ts +8 -0
  38. package/src/config/file.test.ts +13 -1
  39. package/src/config/resolve.test.ts +18 -0
  40. package/src/config/resolve.ts +2 -2
  41. package/src/config/validate.test.ts +11 -0
  42. package/src/config.integration.test.ts +5 -0
  43. package/src/configure/configure.test.ts +170 -0
  44. package/src/configure/index.ts +298 -0
  45. package/src/configure/prompt.ts +47 -0
  46. package/src/docs/api-guide.test.ts +10 -0
  47. package/src/docs/builtin.ts +3 -5
  48. package/src/docs/docs.test.ts +32 -5
  49. package/src/docs/mcp-guide.ts +4 -4
  50. package/src/docs/mcp-resources.test.ts +10 -0
  51. package/src/formats.test.ts +9 -0
  52. package/src/headless.test.ts +10 -0
  53. package/src/hidden-mcpb.test.ts +22 -0
  54. package/src/index.ts +2 -2
  55. package/src/install/binary-placement.test.ts +14 -0
  56. package/src/install/gh-release-update.test.ts +9 -0
  57. package/src/install/install-validate.test.ts +15 -5
  58. package/src/install/mcp-codex.test.ts +7 -0
  59. package/src/install/mcp-openclaw.test.ts +6 -0
  60. package/src/install/mcp-opencode.test.ts +10 -0
  61. package/src/install/opts.ts +17 -0
  62. package/src/install/status.test.ts +9 -0
  63. package/src/install/target-effective.ts +8 -8
  64. package/src/install/target-scope.ts +11 -8
  65. package/src/install/targets/configure.ts +1 -1
  66. package/src/install/targets.test.ts +23 -4
  67. package/src/invoke.test.ts +15 -1
  68. package/src/mcp/claude.test.ts +10 -0
  69. package/src/mcp/env.test.ts +12 -0
  70. package/src/mcp/tools.ts +1 -1
  71. package/src/mcp/zip.test.ts +5 -0
  72. package/src/mcp.integration.test.ts +43 -4
  73. package/src/parse.test.ts +82 -13
  74. package/src/schema.ts +1 -9
  75. package/src/skill/hint.ts +2 -2
  76. package/src/types.ts +22 -14
  77. package/src/validate.ts +17 -17
  78. package/docs/install.md +0 -206
  79. package/src/builtins/install.ts +0 -106
  80. package/src/builtins/uninstall.ts +0 -80
  81. package/src/install/index.ts +0 -409
  82. package/src/install/install.test.ts +0 -317
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for config/file module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
7
  import { tmpdir } from "node:os";
@@ -40,7 +44,9 @@ function withHome<T>(fn: (home: string) => T): T {
40
44
  }
41
45
  }
42
46
 
47
+ /** Tests for config/file. */
43
48
  describe("config/file", () => {
49
+ /** BuildProgramUserConfig from program.appConfig env entries. */
44
50
  test("buildProgramUserConfig from program.appConfig env entries", () => {
45
51
  const cfg = buildProgramUserConfig(program);
46
52
  expect(cfg?.api_token).toMatchObject({
@@ -51,6 +57,7 @@ describe("config/file", () => {
51
57
  expect(cfg?.port).toBeUndefined();
52
58
  });
53
59
 
60
+ /** ResolveAppConfigPath uses config.json. */
54
61
  test("resolveAppConfigPath uses config.json", () => {
55
62
  withHome((home) => {
56
63
  expect(resolveAppConfigPath(program)).toBe(
@@ -59,6 +66,7 @@ describe("config/file", () => {
59
66
  });
60
67
  });
61
68
 
69
+ /** ResolveAppConfig prefers host env over file. */
62
70
  test("resolveAppConfig prefers host env over file", () => {
63
71
  withHome((_home) => {
64
72
  const prevToken = process.env.API_TOKEN;
@@ -76,6 +84,7 @@ describe("config/file", () => {
76
84
  });
77
85
  });
78
86
 
87
+ /** MissingRequiredConfig and formatMissingConfigMessage. */
79
88
  test("missingRequiredConfig and formatMissingConfigMessage", () => {
80
89
  const prev = process.env.API_TOKEN;
81
90
  delete process.env.API_TOKEN;
@@ -85,12 +94,13 @@ describe("config/file", () => {
85
94
  expect(missing).toContain("apiToken");
86
95
  expect(missing).not.toContain("port");
87
96
  const msg = formatMissingConfigMessage(program, missing);
88
- expect(msg).toContain("install --configure");
97
+ expect(msg).toContain("configure");
89
98
  } finally {
90
99
  if (prev !== undefined) process.env.API_TOKEN = prev;
91
100
  }
92
101
  });
93
102
 
103
+ /** Rejects unknown keys on read. */
94
104
  test("rejects unknown keys on read", () => {
95
105
  withHome(() => {
96
106
  const configPath = resolveAppConfigPath(program);
@@ -100,6 +110,7 @@ describe("config/file", () => {
100
110
  });
101
111
  });
102
112
 
113
+ /** WriteAppConfigFile round-trip. */
103
114
  test("writeAppConfigFile round-trip", () => {
104
115
  withHome(() => {
105
116
  writeAppConfigFile(program, { apiToken: "saved" });
@@ -108,6 +119,7 @@ describe("config/file", () => {
108
119
  });
109
120
  });
110
121
 
122
+ /** Tests that uninstallAppConfig removes config directory recursively. */
111
123
  test("uninstallAppConfig removes config directory recursively", () => {
112
124
  withHome(() => {
113
125
  writeAppConfigFile(program, { apiToken: "saved" });
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for config/resolve module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import type { CliProgram } from "../types.ts";
3
7
  import { captureMappedHostEnv, exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
@@ -25,7 +29,9 @@ const program: CliProgram = {
25
29
  handler: () => {},
26
30
  };
27
31
 
32
+ /** Tests for config/resolve. */
28
33
  describe("config/resolve", () => {
34
+ /** Tests that prefers env over file for mapped keys. */
29
35
  test("prefers env over file for mapped keys", () => {
30
36
  const prev = process.env.API_TOKEN;
31
37
  process.env.API_TOKEN = "from-env";
@@ -38,6 +44,7 @@ describe("config/resolve", () => {
38
44
  }
39
45
  });
40
46
 
47
+ /** Tests that uses file when env empty. */
41
48
  test("uses file when env empty", () => {
42
49
  const prev = process.env.API_TOKEN;
43
50
  delete process.env.API_TOKEN;
@@ -49,6 +56,7 @@ describe("config/resolve", () => {
49
56
  }
50
57
  });
51
58
 
59
+ /** Tests that empty string in env or file counts as missing. */
52
60
  test("empty string in env or file counts as missing", () => {
53
61
  const prev = process.env.API_TOKEN;
54
62
  process.env.API_TOKEN = "";
@@ -61,6 +69,7 @@ describe("config/resolve", () => {
61
69
  }
62
70
  });
63
71
 
72
+ /** Applies jsonSchema default when file and env absent. */
64
73
  test("applies jsonSchema default when file and env absent", () => {
65
74
  const prev = process.env.API_TOKEN;
66
75
  delete process.env.API_TOKEN;
@@ -73,6 +82,7 @@ describe("config/resolve", () => {
73
82
  }
74
83
  });
75
84
 
85
+ /** All-string mode uses entry.default. */
76
86
  test("all-string mode uses entry.default", () => {
77
87
  const stringProgram: CliProgram = {
78
88
  ...program,
@@ -86,6 +96,7 @@ describe("config/resolve", () => {
86
96
  expect(resolved.greeting).toBe("world");
87
97
  });
88
98
 
99
+ /** Tests that prefers captured host env over file when process.env was exported from file. */
89
100
  test("prefers captured host env over file when process.env was exported from file", () => {
90
101
  const hostEnv = { API_TOKEN: "from-host" };
91
102
  process.env.API_TOKEN = "from-file-export";
@@ -97,6 +108,7 @@ describe("config/resolve", () => {
97
108
  }
98
109
  });
99
110
 
111
+ /** ExportConfigToEnv does not overwrite host env. */
100
112
  test("exportConfigToEnv does not overwrite host env", () => {
101
113
  const prev = process.env.API_TOKEN;
102
114
  process.env.API_TOKEN = "from-host";
@@ -110,6 +122,7 @@ describe("config/resolve", () => {
110
122
  }
111
123
  });
112
124
 
125
+ /** Resolve callback supplies value when env and file are absent. */
113
126
  test("resolve callback supplies value when env and file are absent", () => {
114
127
  const resolveProgram: CliProgram = {
115
128
  ...program,
@@ -135,6 +148,7 @@ describe("config/resolve", () => {
135
148
  }
136
149
  });
137
150
 
151
+ /** Env overrides resolve callback. */
138
152
  test("env overrides resolve callback", () => {
139
153
  const resolveProgram: CliProgram = {
140
154
  ...program,
@@ -161,6 +175,7 @@ describe("config/resolve", () => {
161
175
  }
162
176
  });
163
177
 
178
+ /** File overrides resolve callback. */
164
179
  test("file overrides resolve callback", () => {
165
180
  const resolveProgram: CliProgram = {
166
181
  ...program,
@@ -186,6 +201,7 @@ describe("config/resolve", () => {
186
201
  }
187
202
  });
188
203
 
204
+ /** Tests that falls back to env when resolve returns undefined. */
189
205
  test("falls back to env when resolve returns undefined", () => {
190
206
  const resolveProgram: CliProgram = {
191
207
  ...program,
@@ -212,6 +228,7 @@ describe("config/resolve", () => {
212
228
  }
213
229
  });
214
230
 
231
+ /** Resolve callback is skipped when env is set. */
215
232
  test("resolve callback is skipped when env is set", () => {
216
233
  let resolveCalled = false;
217
234
  const resolveProgram: CliProgram = {
@@ -243,6 +260,7 @@ describe("config/resolve", () => {
243
260
  }
244
261
  });
245
262
 
263
+ /** Tests that async resolve is ignored with stderr warning. */
246
264
  test("async resolve is ignored with stderr warning", () => {
247
265
  const resolveProgram: CliProgram = {
248
266
  ...program,
@@ -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} install --configure`,
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} install --configure`,
242
+ `Configure: ${program.key} configure`,
243
243
  `Or set via: ${program.key} config set`,
244
244
  `Config file: ${path}`,
245
245
  ].join("\n");
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for config/validate module behavior.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { parseConfigSetValue, validateConfigDocument } from "./validate.ts";
3
7
 
@@ -17,7 +21,9 @@ const rootSchema = {
17
21
  },
18
22
  };
19
23
 
24
+ /** Tests for config/validate. */
20
25
  describe("config/validate", () => {
26
+ /** Tests that accepts valid document. */
21
27
  test("accepts valid document", () => {
22
28
  const result = validateConfigDocument(
23
29
  { apiToken: "x", maxRetries: 3, prefs: { ttl: 3600 } },
@@ -27,12 +33,14 @@ describe("config/validate", () => {
27
33
  expect(result.errors).toEqual([]);
28
34
  });
29
35
 
36
+ /** Rejects missing required property. */
30
37
  test("rejects missing required property", () => {
31
38
  const result = validateConfigDocument({ apiToken: "x" }, rootSchema);
32
39
  expect(result.valid).toBe(false);
33
40
  expect(result.errors.some((e) => e.includes("maxRetries"))).toBe(true);
34
41
  });
35
42
 
43
+ /** Rejects unknown property when additionalProperties is false. */
36
44
  test("rejects unknown property when additionalProperties is false", () => {
37
45
  const result = validateConfigDocument(
38
46
  { apiToken: "x", maxRetries: 1, extra: true },
@@ -42,16 +50,19 @@ describe("config/validate", () => {
42
50
  expect(result.errors.some((e) => e.includes("extra"))).toBe(true);
43
51
  });
44
52
 
53
+ /** Rejects type mismatch. */
45
54
  test("rejects type mismatch", () => {
46
55
  const result = validateConfigDocument({ apiToken: "x", maxRetries: "nope" }, rootSchema);
47
56
  expect(result.valid).toBe(false);
48
57
  });
49
58
 
59
+ /** ParseConfigSetValue coerces number and boolean. */
50
60
  test("parseConfigSetValue coerces number and boolean", () => {
51
61
  expect(parseConfigSetValue("5", { type: "integer" }, rootSchema, false)).toBe(5);
52
62
  expect(parseConfigSetValue("true", { type: "boolean" }, rootSchema, false)).toBe(true);
53
63
  });
54
64
 
65
+ /** ParseConfigSetValue requires --json for objects. */
55
66
  test("parseConfigSetValue requires --json for objects", () => {
56
67
  expect(() => parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, false)).toThrow(
57
68
  /--json/,
@@ -10,6 +10,7 @@ import { bootstrapAppConfig } from "./config/bootstrap.ts";
10
10
  import { resolveAppConfigPath } from "./config/file.ts";
11
11
  import { mcpRequest, testProgram } from "./test-fixtures.ts";
12
12
 
13
+ /** Tests that bootstrapAppConfig prefers host env over config file. */
13
14
  test("bootstrapAppConfig prefers host env over config file", () => {
14
15
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-env-"));
15
16
  const prevHome = process.env.HOME;
@@ -43,6 +44,7 @@ test("bootstrapAppConfig prefers host env over config file", () => {
43
44
  }
44
45
  });
45
46
 
47
+ /** MCP program.appConfig fails when required config missing. */
46
48
  test("MCP program.appConfig fails when required config missing", async () => {
47
49
  const responses = await mcpRequest(
48
50
  [
@@ -60,6 +62,7 @@ test("MCP program.appConfig fails when required config missing", async () => {
60
62
  expect(res.result.content[0]?.text).toContain("argsTestSecret");
61
63
  });
62
64
 
65
+ /** MCP program.appConfig succeeds when env present. */
63
66
  test("MCP program.appConfig succeeds when env present", async () => {
64
67
  const responses = await mcpRequest(
65
68
  [
@@ -77,6 +80,7 @@ test("MCP program.appConfig succeeds when env present", async () => {
77
80
  expect(res.result.content[0]?.text.trim()).toBe("sekrit");
78
81
  });
79
82
 
83
+ /** MCP config file loads and exports vars for tool handlers. */
80
84
  test("MCP config file loads and exports vars for tool handlers", async () => {
81
85
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-mcp-"));
82
86
  const configFile = join(dir, ".local", "lib", "mcp_test", "config");
@@ -106,6 +110,7 @@ test("MCP config file loads and exports vars for tool handlers", async () => {
106
110
  rmSync(dir, { recursive: true, force: true });
107
111
  });
108
112
 
113
+ /** Cli.run docs api skips required appConfig exit. */
109
114
  test("Cli.run docs api skips required appConfig exit", async () => {
110
115
  const dir = mkdtempSync(join(tmpdir(), "argsbarg-docs-skip-"));
111
116
  const configFile = join(dir, ".local", "lib", "docs_skip_test", "config");
@@ -0,0 +1,170 @@
1
+ /*
2
+ Tests for configure/configure module behavior.
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import {
10
+ appConfigHasEntries,
11
+ parseConfigureOpts,
12
+ validateConfigureOpts,
13
+ } from "../configure/index.ts";
14
+ import { detectInstalledArtifacts } from "../install/detect-installed.ts";
15
+ import { parseInstallOpts } from "../install/opts.ts";
16
+ import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "../install/paths.ts";
17
+ import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
18
+ import { buildUninstallPlan } from "../install/uninstall.ts";
19
+ import type { CliProgram } from "../types.ts";
20
+
21
+ const fixture: CliProgram = {
22
+ key: "testapp",
23
+ version: "0.0.0",
24
+ description: "Test",
25
+ mcpServer: { enabled: true },
26
+ handler: () => {},
27
+ };
28
+
29
+ let home: string;
30
+ let prevHome: string | undefined;
31
+ let prevXdg: string | undefined;
32
+
33
+ beforeEach(() => {
34
+ home = mkdtempSync(join(tmpdir(), "argsbarg-configure-"));
35
+ prevHome = process.env.HOME;
36
+ prevXdg = process.env.XDG_CONFIG_HOME;
37
+ process.env.HOME = home;
38
+ delete process.env.XDG_CONFIG_HOME;
39
+ });
40
+
41
+ afterEach(() => {
42
+ if (prevHome === undefined) delete process.env.HOME;
43
+ else process.env.HOME = prevHome;
44
+ if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
45
+ else process.env.XDG_CONFIG_HOME = prevXdg;
46
+ rmSync(home, { recursive: true, force: true });
47
+ });
48
+
49
+ /** Tests for configure opts. */
50
+ describe("configure opts", () => {
51
+ /** Validate rejects multiple modes. */
52
+ test("validate rejects multiple modes", () => {
53
+ const opts = parseConfigureOpts({ sync: "1", status: "1" });
54
+ expect(validateConfigureOpts(opts)).toContain("only one");
55
+ });
56
+
57
+ /** Sync requires --yes. */
58
+ test("sync requires --yes", () => {
59
+ const opts = parseConfigureOpts({ sync: "1" });
60
+ expect(validateConfigureOpts(opts)).toContain("--yes");
61
+ });
62
+
63
+ /** Remove-all requires --yes. */
64
+ test("remove-all requires --yes", () => {
65
+ const opts = parseConfigureOpts({ "remove-all": "1" });
66
+ expect(validateConfigureOpts(opts)).toContain("--yes");
67
+ });
68
+ });
69
+
70
+ /** Tests for install paths. */
71
+ describe("install paths", () => {
72
+ /** ResolveInstallPaths includes skill and mcp paths. */
73
+ test("resolveInstallPaths includes skill and mcp paths", () => {
74
+ const paths = resolveInstallPaths(fixture);
75
+ expect(paths.cursorSkillDir).toContain(".cursor/skills");
76
+ expect(paths.cursorMcpPath).toContain("mcp.json");
77
+ });
78
+
79
+ /** Claude desktop mcp path on darwin. */
80
+ test("claude desktop mcp path on darwin", () => {
81
+ const prev = process.platform;
82
+ Object.defineProperty(process, "platform", { value: "darwin" });
83
+ try {
84
+ expect(resolveClaudeDesktopMcpPath(home)).toBe(
85
+ join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
86
+ );
87
+ } finally {
88
+ Object.defineProperty(process, "platform", { value: prev });
89
+ }
90
+ });
91
+ });
92
+
93
+ /** Tests for detect installed. */
94
+ describe("detect installed", () => {
95
+ /** Detects cursor mcp when configured. */
96
+ test("detects cursor mcp when configured", () => {
97
+ const paths = resolveInstallPaths(fixture);
98
+ mkdirSync(join(home, ".cursor"), { recursive: true });
99
+ writeFileSync(
100
+ paths.cursorMcpPath,
101
+ JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
102
+ "utf8",
103
+ );
104
+
105
+ const detected = detectInstalledArtifacts(paths, fixture);
106
+ expect(detected.cursorMcp).toBe(true);
107
+ expect(detected.app).toBe(false);
108
+ });
109
+ });
110
+
111
+ /** Tests for sync plan. */
112
+ describe("sync plan", () => {
113
+ /** BuildUpdatePlan greenfield includes agent targets. */
114
+ test("buildUpdatePlan greenfield includes agent targets", () => {
115
+ const paths = resolveInstallPaths(fixture);
116
+ const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
117
+ expect(plan.length).toBeGreaterThan(0);
118
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
119
+ });
120
+
121
+ /** BuildInstallPlan --all omits app self-install. */
122
+ test("buildInstallPlan --all omits app self-install", () => {
123
+ const paths = resolveInstallPaths(fixture);
124
+ const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
125
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
126
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
127
+ });
128
+
129
+ /** BuildInstallPlan respects configure.agentIntegration skill. */
130
+ test("buildInstallPlan respects configure.agentIntegration skill", () => {
131
+ const skillApp: CliProgram = {
132
+ ...fixture,
133
+ configure: { agentIntegration: "skill" },
134
+ };
135
+ const paths = resolveInstallPaths(skillApp);
136
+ mkdirSync(join(home, ".claude"), { recursive: true });
137
+ mkdirSync(join(home, ".cursor"), { recursive: true });
138
+ const plan = buildInstallPlan(skillApp, paths, parseInstallOpts({ mcp: "1" }));
139
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(false);
140
+ });
141
+ });
142
+
143
+ /** Tests for remove plan. */
144
+ describe("remove plan", () => {
145
+ /** BuildUninstallPlan --all with nothing installed is empty. */
146
+ test("buildUninstallPlan --all with nothing installed is empty", () => {
147
+ const paths = resolveInstallPaths(fixture);
148
+ const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
149
+ expect(plan.length).toBe(0);
150
+ });
151
+ });
152
+
153
+ /** Tests for app config wizard. */
154
+ describe("app config wizard", () => {
155
+ /** Tests that appConfigHasEntries when entries exist. */
156
+ test("appConfigHasEntries when entries exist", () => {
157
+ expect(
158
+ appConfigHasEntries({
159
+ ...fixture,
160
+ appConfig: { entries: { token: { description: "API token." } } },
161
+ }),
162
+ ).toBe(true);
163
+ });
164
+
165
+ /** Tests that appConfigHasEntries false for empty entries or missing appConfig. */
166
+ test("appConfigHasEntries false for empty entries or missing appConfig", () => {
167
+ expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
168
+ expect(appConfigHasEntries(fixture)).toBe(false);
169
+ });
170
+ });