claudeup 6.2.1 → 6.3.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 (150) hide show
  1. package/README.md +22 -0
  2. package/bin/claudeup.js +6 -1
  3. package/package.json +6 -6
  4. package/src/__tests__/alias-adopt.test.ts +331 -329
  5. package/src/__tests__/alias-parser.test.ts +384 -376
  6. package/src/__tests__/alias-shell-writer.test.ts +638 -625
  7. package/src/__tests__/alias-store.test.ts +143 -143
  8. package/src/__tests__/catalog-cache-store.test.ts +265 -265
  9. package/src/__tests__/catalog-notice.test.ts +123 -123
  10. package/src/__tests__/cli-apply-seams.test.ts +12 -8
  11. package/src/__tests__/cli-router.test.ts +132 -3
  12. package/src/__tests__/cli-update-view.test.ts +27 -8
  13. package/src/__tests__/community-staleness.test.ts +0 -2
  14. package/src/__tests__/component-badge.test.ts +4 -4
  15. package/src/__tests__/content-drift.test.ts +5 -1
  16. package/src/__tests__/conventions-integration.test.ts +774 -689
  17. package/src/__tests__/conventions-manager.test.ts +1216 -1152
  18. package/src/__tests__/doctor-bins.test.ts +14 -6
  19. package/src/__tests__/doctor.test.ts +438 -425
  20. package/src/__tests__/dual-write-prevention.test.ts +277 -282
  21. package/src/__tests__/enabled-not-installed.test.ts +71 -63
  22. package/src/__tests__/file-locking.test.ts +196 -196
  23. package/src/__tests__/gap-fill-versions.test.ts +318 -311
  24. package/src/__tests__/github-budget.test.ts +170 -164
  25. package/src/__tests__/gitignore-detector.test.ts +160 -152
  26. package/src/__tests__/gitignore-fixer.test.ts +285 -247
  27. package/src/__tests__/gitignore-prerun.test.ts +57 -57
  28. package/src/__tests__/gitignore-resolver.test.ts +173 -146
  29. package/src/__tests__/gitignore-service.test.ts +121 -119
  30. package/src/__tests__/install-command.test.ts +3 -3
  31. package/src/__tests__/install-plan.test.ts +13 -7
  32. package/src/__tests__/manifest.test.ts +16 -5
  33. package/src/__tests__/markdown-renderer.test.ts +0 -1
  34. package/src/__tests__/marketplace-badge.test.ts +111 -90
  35. package/src/__tests__/marketplace-version.test.ts +64 -61
  36. package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
  37. package/src/__tests__/plugin-requires.test.ts +18 -4
  38. package/src/__tests__/plugin-setup.test.ts +12 -12
  39. package/src/__tests__/profile-command.test.ts +1 -7
  40. package/src/__tests__/profile-materializer.test.ts +31 -8
  41. package/src/__tests__/profile-sync.test.ts +23 -5
  42. package/src/__tests__/registry-version-resolution.test.ts +61 -47
  43. package/src/__tests__/resolve-executable.test.ts +1 -1
  44. package/src/__tests__/resolver.test.ts +40 -15
  45. package/src/__tests__/scope-action.test.ts +52 -50
  46. package/src/__tests__/scope-squares.test.tsx +4 -5
  47. package/src/__tests__/skill-install-files.test.ts +111 -103
  48. package/src/__tests__/symlink-manager.test.ts +27 -10
  49. package/src/__tests__/theme-env.test.ts +281 -0
  50. package/src/__tests__/theme-resolve.test.ts +380 -0
  51. package/src/__tests__/update-apply.test.ts +1 -3
  52. package/src/__tests__/useGitignoreModal.test.ts +69 -61
  53. package/src/__tests__/version-snapshot.test.ts +181 -165
  54. package/src/cli/ansi.ts +23 -15
  55. package/src/cli/doctor.ts +10 -9
  56. package/src/cli/install.ts +3 -3
  57. package/src/cli/profile.ts +5 -4
  58. package/src/cli/prompt.ts +0 -1
  59. package/src/cli/router.ts +69 -2
  60. package/src/cli/update-view.ts +11 -6
  61. package/src/cli/update.ts +9 -4
  62. package/src/cli/upgrade.ts +6 -4
  63. package/src/data/alias-flags.ts +260 -260
  64. package/src/data/gitignore-defaults.ts +20 -20
  65. package/src/data/gitignore-reasons.ts +86 -92
  66. package/src/data/gitignore-templates.ts +14 -14
  67. package/src/data/predefined-profiles.ts +8 -9
  68. package/src/data/skill-repos.ts +43 -35
  69. package/src/main.tsx +29 -6
  70. package/src/opentui.d.ts +1 -3
  71. package/src/prerunner/index.ts +7 -8
  72. package/src/services/alias-settings.ts +20 -23
  73. package/src/services/alias-shell-writer.ts +800 -803
  74. package/src/services/alias-store.ts +77 -77
  75. package/src/services/claude-settings.ts +27 -18
  76. package/src/services/content-drift.ts +13 -3
  77. package/src/services/conventions-manager.ts +692 -617
  78. package/src/services/doctor.ts +464 -403
  79. package/src/services/gitignore-detector.ts +113 -103
  80. package/src/services/gitignore-fixer.ts +204 -189
  81. package/src/services/gitignore-prerun.ts +26 -26
  82. package/src/services/gitignore-resolver.ts +162 -144
  83. package/src/services/gitignore-service.ts +120 -117
  84. package/src/services/install-plan.ts +4 -4
  85. package/src/services/manifest.ts +1 -1
  86. package/src/services/marketplace-catalog-git.ts +5 -2
  87. package/src/services/marketplace-fetcher.ts +241 -235
  88. package/src/services/plugin-manager.ts +17 -20
  89. package/src/services/plugin-mcp-config.ts +2 -2
  90. package/src/services/plugin-requires.ts +9 -6
  91. package/src/services/plugin-setup.ts +7 -9
  92. package/src/services/plugin-version-check.ts +4 -4
  93. package/src/services/profile-materializer.ts +9 -5
  94. package/src/services/profile-sync.ts +12 -4
  95. package/src/services/profiles.ts +3 -3
  96. package/src/services/resolver.ts +10 -6
  97. package/src/services/settings-manager.ts +82 -46
  98. package/src/services/skills-manager.ts +11 -9
  99. package/src/services/symlink-manager.ts +2 -5
  100. package/src/services/toolchain.ts +3 -3
  101. package/src/services/update-cache.ts +1 -1
  102. package/src/services/update-engine.ts +4 -4
  103. package/src/services/update-plan.ts +9 -11
  104. package/src/services/version-snapshot.ts +88 -88
  105. package/src/types/gitignore.ts +28 -28
  106. package/src/ui/App.tsx +36 -36
  107. package/src/ui/adapters/settingsAdapter.ts +2 -2
  108. package/src/ui/components/CategoryHeader.tsx +1 -1
  109. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  110. package/src/ui/components/ScopeIndicator.tsx +3 -1
  111. package/src/ui/components/ScrollableList.tsx +2 -1
  112. package/src/ui/components/StyledText.tsx +1 -1
  113. package/src/ui/components/layout/FooterHints.tsx +5 -3
  114. package/src/ui/components/layout/Panel.tsx +2 -2
  115. package/src/ui/components/layout/ScreenLayout.tsx +5 -4
  116. package/src/ui/components/modals/InputModal.tsx +7 -1
  117. package/src/ui/components/modals/ModalContainer.tsx +3 -3
  118. package/src/ui/components/modals/SelectModal.tsx +1 -1
  119. package/src/ui/components/primitives/DetailSection.tsx +1 -1
  120. package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
  121. package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
  122. package/src/ui/hooks/index.ts +0 -1
  123. package/src/ui/hooks/useGitignoreModal.ts +1 -5
  124. package/src/ui/hooks/useMismatchModal.ts +4 -10
  125. package/src/ui/renderers/cliToolRenderers.tsx +2 -2
  126. package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
  127. package/src/ui/renderers/mcpRenderers.tsx +1 -1
  128. package/src/ui/renderers/pluginRenderers.tsx +3 -9
  129. package/src/ui/renderers/profileRenderers.tsx +9 -8
  130. package/src/ui/renderers/settingsRenderers.tsx +4 -4
  131. package/src/ui/renderers/skillRenderers.tsx +14 -14
  132. package/src/ui/screens/AliasScreen.tsx +31 -34
  133. package/src/ui/screens/CliToolsScreen.tsx +14 -13
  134. package/src/ui/screens/EnvVarsScreen.tsx +8 -8
  135. package/src/ui/screens/GitignoreScreen.tsx +15 -14
  136. package/src/ui/screens/McpRegistryScreen.tsx +10 -7
  137. package/src/ui/screens/McpScreen.tsx +10 -10
  138. package/src/ui/screens/PluginsScreen.tsx +17 -21
  139. package/src/ui/screens/ProfilesScreen.tsx +25 -24
  140. package/src/ui/screens/SkillsScreen.tsx +16 -14
  141. package/src/ui/state/AppContext.tsx +4 -8
  142. package/src/ui/state/DimensionsContext.tsx +3 -2
  143. package/src/ui/theme-mode.ts +20 -38
  144. package/src/ui/theme-resolve.ts +148 -0
  145. package/src/utils/clipboard.ts +5 -5
  146. package/src/utils/command-utils.ts +2 -2
  147. package/src/utils/config-dir.ts +1 -3
  148. package/src/utils/file-locking.ts +88 -86
  149. package/src/ui/hooks/useAsyncData.ts +0 -127
  150. package/src/ui/state/AnimationContext.tsx +0 -76
@@ -3,11 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import fs from "fs-extra";
6
- import {
7
- listProfiles,
8
- showProfile,
9
- switchProfile,
10
- } from "../cli/profile.js";
6
+ import { listProfiles, showProfile, switchProfile } from "../cli/profile.js";
11
7
  import { activeProfile } from "../services/symlink-manager.js";
12
8
  import type { ProfileManifest } from "../types/index.js";
13
9
 
@@ -17,9 +13,7 @@ let errs: string[];
17
13
  beforeEach(() => {
18
14
  logs = [];
19
15
  errs = [];
20
- // biome-ignore lint/suspicious/noExplicitAny: test double
21
16
  console.log = mock((...a: unknown[]) => logs.push(a.join(" "))) as any;
22
- // biome-ignore lint/suspicious/noExplicitAny: test double
23
17
  console.error = mock((...a: unknown[]) => errs.push(a.join(" "))) as any;
24
18
  });
25
19
  afterEach(() => mock.restore());
@@ -65,7 +65,9 @@ describe("buildProfileSettings", () => {
65
65
  });
66
66
 
67
67
  test("no union given -> no false entries (unchanged legacy shape)", () => {
68
- const s = buildProfileSettings(closure({ plugins: { "dev@magus": "latest" } }));
68
+ const s = buildProfileSettings(
69
+ closure({ plugins: { "dev@magus": "latest" } }),
70
+ );
69
71
  expect(s.enabledPlugins).toEqual({ "dev@magus": true });
70
72
  });
71
73
  });
@@ -98,14 +100,28 @@ describe("materializeProfile", () => {
98
100
  project,
99
101
  );
100
102
  expect(dir).toBe(profileDir("frontend", project));
101
- expect((await fs.readJson(join(dir, "settings.json"))).enabledPlugins["dev@magus"]).toBe(true);
102
- expect((await fs.readJson(join(dir, "mcp.json"))).mcpServers.x.command).toBe("y");
103
+ expect(
104
+ (await fs.readJson(join(dir, "settings.json"))).enabledPlugins[
105
+ "dev@magus"
106
+ ],
107
+ ).toBe(true);
108
+ expect(
109
+ (await fs.readJson(join(dir, "mcp.json"))).mcpServers.x.command,
110
+ ).toBe("y");
103
111
  expect(await fs.pathExists(join(dir, "skills"))).toBe(true);
104
112
  });
105
113
 
106
114
  test("is idempotent (re-materialize overwrites cleanly)", async () => {
107
- await materializeProfile("p", closure({ plugins: { "a@m": "latest" } }), project);
108
- const dir = await materializeProfile("p", closure({ plugins: { "b@m": "latest" } }), project);
115
+ await materializeProfile(
116
+ "p",
117
+ closure({ plugins: { "a@m": "latest" } }),
118
+ project,
119
+ );
120
+ const dir = await materializeProfile(
121
+ "p",
122
+ closure({ plugins: { "b@m": "latest" } }),
123
+ project,
124
+ );
109
125
  const settings = await fs.readJson(join(dir, "settings.json"));
110
126
  expect(settings.enabledPlugins).toEqual({ "b@m": true });
111
127
  });
@@ -120,7 +136,10 @@ describe("materializeProfile", () => {
120
136
  const dir = await materializeProfile("p", closure(), project);
121
137
 
122
138
  expect(
123
- await fs.readFile(join(dir, "skills", "systematic-debugging", "SKILL.md"), "utf8"),
139
+ await fs.readFile(
140
+ join(dir, "skills", "systematic-debugging", "SKILL.md"),
141
+ "utf8",
142
+ ),
124
143
  ).toContain("committed project skill");
125
144
  });
126
145
 
@@ -157,11 +176,15 @@ describe("materializeProfile", () => {
157
176
  project,
158
177
  union,
159
178
  );
160
- expect((await fs.readJson(join(fe, "settings.json"))).enabledPlugins).toEqual({
179
+ expect(
180
+ (await fs.readJson(join(fe, "settings.json"))).enabledPlugins,
181
+ ).toEqual({
161
182
  "designer@magus": true,
162
183
  "terminal@magus": false,
163
184
  });
164
- expect((await fs.readJson(join(be, "settings.json"))).enabledPlugins).toEqual({
185
+ expect(
186
+ (await fs.readJson(join(be, "settings.json"))).enabledPlugins,
187
+ ).toEqual({
165
188
  "designer@magus": false,
166
189
  "terminal@magus": true,
167
190
  });
@@ -31,7 +31,10 @@ describe("computeSync (pure)", () => {
31
31
  expect(next.plugins!["dev@magus"]).toBe("2.9.0");
32
32
  // new plugin added as latest
33
33
  expect(next.plugins!["designer@magus"]).toBe("latest");
34
- expect(changes).toContainEqual({ kind: "plugin-added", detail: "designer@magus" });
34
+ expect(changes).toContainEqual({
35
+ kind: "plugin-added",
36
+ detail: "designer@magus",
37
+ });
35
38
  });
36
39
 
37
40
  test("removes a plugin that was disabled", () => {
@@ -41,7 +44,10 @@ describe("computeSync (pure)", () => {
41
44
  { mcpServers: {} },
42
45
  );
43
46
  expect(next.plugins!["terminal@magus"]).toBeUndefined();
44
- expect(changes).toContainEqual({ kind: "plugin-removed", detail: "terminal@magus" });
47
+ expect(changes).toContainEqual({
48
+ kind: "plugin-removed",
49
+ detail: "terminal@magus",
50
+ });
45
51
  });
46
52
 
47
53
  test("captures a settings change and an mcp change", () => {
@@ -75,7 +81,14 @@ describe("computeSync (pure)", () => {
75
81
  const withStamp: ProfileManifestEntry = { ...entry, updatedAt: "OLD" };
76
82
  const { entry: changed } = computeSync(
77
83
  withStamp,
78
- { enabledPlugins: { "dev@magus": true, "terminal@magus": true, "new@magus": true }, effortLevel: "high" },
84
+ {
85
+ enabledPlugins: {
86
+ "dev@magus": true,
87
+ "terminal@magus": true,
88
+ "new@magus": true,
89
+ },
90
+ effortLevel: "high",
91
+ },
79
92
  { mcpServers: {} },
80
93
  "2026-07-27T00:00:00.000Z",
81
94
  );
@@ -84,7 +97,10 @@ describe("computeSync (pure)", () => {
84
97
  // Unchanged sync leaves updatedAt alone.
85
98
  const { entry: same } = computeSync(
86
99
  withStamp,
87
- { enabledPlugins: { "dev@magus": true, "terminal@magus": true }, effortLevel: "high" },
100
+ {
101
+ enabledPlugins: { "dev@magus": true, "terminal@magus": true },
102
+ effortLevel: "high",
103
+ },
88
104
  { mcpServers: {} },
89
105
  "2026-07-27T00:00:00.000Z",
90
106
  );
@@ -99,7 +115,9 @@ describe("syncProfile (io)", () => {
99
115
  await fs.ensureDir(join(dir, ".claude"));
100
116
  await fs.writeJson(join(dir, ".claude", "profiles.json"), {
101
117
  version: 2,
102
- profiles: { frontend: { name: "Frontend", plugins: { "dev@magus": "2.9.0" } } },
118
+ profiles: {
119
+ frontend: { name: "Frontend", plugins: { "dev@magus": "2.9.0" } },
120
+ },
103
121
  });
104
122
  });
105
123
  afterEach(async () => {
@@ -14,61 +14,75 @@ import type { InstalledPluginEntry } from "../types/index.js";
14
14
  */
15
15
 
16
16
  const entry = (
17
- scope: InstalledPluginEntry["scope"],
18
- version: string,
19
- projectPath?: string,
17
+ scope: InstalledPluginEntry["scope"],
18
+ version: string,
19
+ projectPath?: string,
20
20
  ): InstalledPluginEntry => ({
21
- scope,
22
- version,
23
- projectPath,
24
- installPath: `/cache/${version}`,
25
- installedAt: "2026-01-01T00:00:00.000Z",
26
- lastUpdated: "2026-01-01T00:00:00.000Z",
21
+ scope,
22
+ version,
23
+ projectPath,
24
+ installPath: `/cache/${version}`,
25
+ installedAt: "2026-01-01T00:00:00.000Z",
26
+ lastUpdated: "2026-01-01T00:00:00.000Z",
27
27
  });
28
28
 
29
29
  describe("pickRegistryEntry — scope resolution", () => {
30
- test("user scope takes the user entry, ignoring project installs", () => {
31
- const entries = [
32
- entry("project", "1.0.0", "/proj/a"),
33
- entry("user", "2.0.0"),
34
- entry("project", "3.0.0", "/proj/b"),
35
- ];
36
- expect(pickRegistryEntry(entries, "user")?.version).toBe("2.0.0");
37
- });
30
+ test("user scope takes the user entry, ignoring project installs", () => {
31
+ const entries = [
32
+ entry("project", "1.0.0", "/proj/a"),
33
+ entry("user", "2.0.0"),
34
+ entry("project", "3.0.0", "/proj/b"),
35
+ ];
36
+ expect(pickRegistryEntry(entries, "user")?.version).toBe("2.0.0");
37
+ });
38
38
 
39
- test("project scope prefers the entry pinned to that exact project", () => {
40
- const entries = [
41
- entry("user", "2.0.0"),
42
- entry("project", "1.0.0", "/proj/a"),
43
- entry("project", "3.0.0", "/proj/b"),
44
- ];
45
- expect(pickRegistryEntry(entries, "project", "/proj/b")?.version).toBe("3.0.0");
46
- });
39
+ test("project scope prefers the entry pinned to that exact project", () => {
40
+ const entries = [
41
+ entry("user", "2.0.0"),
42
+ entry("project", "1.0.0", "/proj/a"),
43
+ entry("project", "3.0.0", "/proj/b"),
44
+ ];
45
+ expect(pickRegistryEntry(entries, "project", "/proj/b")?.version).toBe(
46
+ "3.0.0",
47
+ );
48
+ });
47
49
 
48
- test("project scope falls back to the user entry when the project has no install", () => {
49
- // This is the statusline case: installed at user scope, absent from project
50
- // settings, previously surfaced as "no version" and skipped by update checks.
51
- const entries = [entry("user", "2.1.2"), entry("project", "1.0.0", "/proj/other")];
52
- expect(pickRegistryEntry(entries, "project", "/proj/mine")?.version).toBe("2.1.2");
53
- });
50
+ test("project scope falls back to the user entry when the project has no install", () => {
51
+ // This is the statusline case: installed at user scope, absent from project
52
+ // settings, previously surfaced as "no version" and skipped by update checks.
53
+ const entries = [
54
+ entry("user", "2.1.2"),
55
+ entry("project", "1.0.0", "/proj/other"),
56
+ ];
57
+ expect(pickRegistryEntry(entries, "project", "/proj/mine")?.version).toBe(
58
+ "2.1.2",
59
+ );
60
+ });
54
61
 
55
- test("project paths are compared resolved, not as raw strings", () => {
56
- const entries = [entry("project", "4.2.0", "/proj/a/../a")];
57
- expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe("4.2.0");
58
- });
62
+ test("project paths are compared resolved, not as raw strings", () => {
63
+ const entries = [entry("project", "4.2.0", "/proj/a/../a")];
64
+ expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe(
65
+ "4.2.0",
66
+ );
67
+ });
59
68
 
60
- test("a local-scope entry satisfies a project lookup for the same path", () => {
61
- const entries = [entry("local", "5.0.0", "/proj/a"), entry("user", "1.0.0")];
62
- expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe("5.0.0");
63
- });
69
+ test("a local-scope entry satisfies a project lookup for the same path", () => {
70
+ const entries = [
71
+ entry("local", "5.0.0", "/proj/a"),
72
+ entry("user", "1.0.0"),
73
+ ];
74
+ expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe(
75
+ "5.0.0",
76
+ );
77
+ });
64
78
 
65
- test("no project path and no user entry yields undefined, not a wrong project's version", () => {
66
- const entries = [entry("project", "9.9.9", "/proj/somewhere-else")];
67
- expect(pickRegistryEntry(entries, "project")).toBeUndefined();
68
- });
79
+ test("no project path and no user entry yields undefined, not a wrong project's version", () => {
80
+ const entries = [entry("project", "9.9.9", "/proj/somewhere-else")];
81
+ expect(pickRegistryEntry(entries, "project")).toBeUndefined();
82
+ });
69
83
 
70
- test("empty and missing entry lists are handled", () => {
71
- expect(pickRegistryEntry([], "user")).toBeUndefined();
72
- expect(pickRegistryEntry(undefined, "user")).toBeUndefined();
73
- });
84
+ test("empty and missing entry lists are handled", () => {
85
+ expect(pickRegistryEntry([], "user")).toBeUndefined();
86
+ expect(pickRegistryEntry(undefined, "user")).toBeUndefined();
87
+ });
74
88
  });
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm, symlink, writeFile, chmod } from "node:fs/promises";
2
+ import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { resolveExecutable } from "../utils/command-utils.js";
@@ -53,7 +53,11 @@ describe("dedupeBins — precedence by order", () => {
53
53
 
54
54
  test("a later entry fills a version the winner left unset", () => {
55
55
  const loose: BinRequirement = { name: "tmux", via: "brew" };
56
- const pinned: BinRequirement = { name: "tmux", via: "brew", version: "3.4" };
56
+ const pinned: BinRequirement = {
57
+ name: "tmux",
58
+ via: "brew",
59
+ version: "3.4",
60
+ };
57
61
  const result = dedupeBins([
58
62
  { bin: loose, source: "plugin:a" },
59
63
  { bin: pinned, source: "plugin:b" },
@@ -71,8 +75,8 @@ describe("resolveExtends", () => {
71
75
  settings: { effortLevel: "max" },
72
76
  };
73
77
  const merged = resolveExtends(entry);
74
- // must-have contributes statusline@magus + multimodel@magus at "latest".
75
- expect(merged.plugins!["statusline@magus"]).toBe("latest");
78
+ // must-have contributes setup@magus + multimodel@magus at "latest".
79
+ expect(merged.plugins!["setup@magus"]).toBe("latest");
76
80
  expect(merged.plugins!["multimodel@magus"]).toBe("latest");
77
81
  // Entry's own plugin pin is preserved.
78
82
  expect(merged.plugins!["dev@magus"]).toBe("2.9.0");
@@ -150,9 +154,9 @@ describe("resolveProfile", () => {
150
154
  });
151
155
 
152
156
  test("throws for an unknown profile id", async () => {
153
- await expect(
154
- resolveProfile(manifestOf({}), "ghost"),
155
- ).rejects.toThrow('Profile "ghost" not found');
157
+ await expect(resolveProfile(manifestOf({}), "ghost")).rejects.toThrow(
158
+ 'Profile "ghost" not found',
159
+ );
156
160
  });
157
161
  });
158
162
 
@@ -191,12 +195,22 @@ describe("resolveAllProfiles — union", () => {
191
195
 
192
196
  test("conflicting explicit pins resolve to the higher semver, order-independent, and are reported", async () => {
193
197
  const mk = (order: "ab" | "ba"): ProfileManifest => {
194
- const a: ProfileManifestEntry = { name: "A", plugins: { "dev@magus": "2.9.0" } };
195
- const b: ProfileManifestEntry = { name: "B", plugins: { "dev@magus": "2.11.0" } };
198
+ const a: ProfileManifestEntry = {
199
+ name: "A",
200
+ plugins: { "dev@magus": "2.9.0" },
201
+ };
202
+ const b: ProfileManifestEntry = {
203
+ name: "B",
204
+ plugins: { "dev@magus": "2.11.0" },
205
+ };
196
206
  return manifestOf(order === "ab" ? { a, b } : { b, a });
197
207
  };
198
- const ab = await resolveAllProfiles(mk("ab"), { binResolver: fakeBinResolver({}) });
199
- const ba = await resolveAllProfiles(mk("ba"), { binResolver: fakeBinResolver({}) });
208
+ const ab = await resolveAllProfiles(mk("ab"), {
209
+ binResolver: fakeBinResolver({}),
210
+ });
211
+ const ba = await resolveAllProfiles(mk("ba"), {
212
+ binResolver: fakeBinResolver({}),
213
+ });
200
214
 
201
215
  // Deterministic: higher pin wins regardless of profile order.
202
216
  expect(ab.plugins["dev@magus"]).toBe("2.11.0");
@@ -211,8 +225,12 @@ describe("resolveAllProfiles — union", () => {
211
225
  a: { name: "A", cliTools: { claudish: "1.2.0" } },
212
226
  b: { name: "B", cliTools: { claudish: "1.5.0" } },
213
227
  });
214
- const union = await resolveAllProfiles(manifest, { binResolver: fakeBinResolver({}) });
215
- expect(union.bins.find((x) => x.name === "claudish")?.version).toBe("1.5.0");
228
+ const union = await resolveAllProfiles(manifest, {
229
+ binResolver: fakeBinResolver({}),
230
+ });
231
+ expect(union.bins.find((x) => x.name === "claudish")?.version).toBe(
232
+ "1.5.0",
233
+ );
216
234
  expect(union.conflicts?.some((c) => c.includes("claudish"))).toBe(true);
217
235
  });
218
236
  });
@@ -255,7 +273,9 @@ describe("marketplaces derived from plugin ids", () => {
255
273
  },
256
274
  });
257
275
  const c = await resolveProfile(manifest, "mixed", noBins);
258
- expect(c.marketplaces["magus-marketing"]?.repo).toBe("MadAppGang/magus-marketing");
276
+ expect(c.marketplaces["magus-marketing"]?.repo).toBe(
277
+ "MadAppGang/magus-marketing",
278
+ );
259
279
  expect(c.marketplaces["magus-alpha"]?.repo).toBe("MadAppGang/magus-alpha");
260
280
  });
261
281
 
@@ -263,7 +283,9 @@ describe("marketplaces derived from plugin ids", () => {
263
283
  const manifest = manifestOf({
264
284
  pinned: {
265
285
  name: "Pinned",
266
- marketplaces: { magus: { source: "github", repo: "MadAppGang/magus-fork" } },
286
+ marketplaces: {
287
+ magus: { source: "github", repo: "MadAppGang/magus-fork" },
288
+ },
267
289
  plugins: { "dev@magus": "latest" },
268
290
  },
269
291
  });
@@ -273,7 +295,10 @@ describe("marketplaces derived from plugin ids", () => {
273
295
 
274
296
  test("an unknown marketplace is left for the manifest to declare", async () => {
275
297
  const manifest = manifestOf({
276
- third: { name: "Third", plugins: { "thing@someones-marketplace": "latest" } },
298
+ third: {
299
+ name: "Third",
300
+ plugins: { "thing@someones-marketplace": "latest" },
301
+ },
277
302
  });
278
303
  const c = await resolveProfile(manifest, "third", noBins);
279
304
  expect(c.marketplaces["someones-marketplace"]).toBeUndefined();
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
- isInstalledInScope,
4
- resolveScopeAction,
3
+ isInstalledInScope,
4
+ resolveScopeAction,
5
5
  } from "../services/plugin-manager.js";
6
6
 
7
7
  /**
@@ -19,64 +19,66 @@ import {
19
19
  */
20
20
 
21
21
  describe("isInstalledInScope", () => {
22
- test("enabled with a registry version is installed", () => {
23
- expect(isInstalledInScope({ enabled: true, version: "3.0.2" })).toBe(true);
24
- });
22
+ test("enabled with a registry version is installed", () => {
23
+ expect(isInstalledInScope({ enabled: true, version: "3.0.2" })).toBe(true);
24
+ });
25
25
 
26
- test("enabled with NO version is not installed — the broken state", () => {
27
- // dev@magus, measured: `.claude/settings.json` had "dev@magus": true while
28
- // installed_plugins.json had no entry resolving for the project path.
29
- expect(isInstalledInScope({ enabled: true })).toBe(false);
30
- });
26
+ test("enabled with NO version is not installed — the broken state", () => {
27
+ // dev@magus, measured: `.claude/settings.json` had "dev@magus": true while
28
+ // installed_plugins.json had no entry resolving for the project path.
29
+ expect(isInstalledInScope({ enabled: true })).toBe(false);
30
+ });
31
31
 
32
- test("a version with the flag off is not installed either", () => {
33
- expect(isInstalledInScope({ enabled: false, version: "3.0.2" })).toBe(false);
34
- });
32
+ test("a version with the flag off is not installed either", () => {
33
+ expect(isInstalledInScope({ enabled: false, version: "3.0.2" })).toBe(
34
+ false,
35
+ );
36
+ });
35
37
 
36
- test("an absent scope is not installed", () => {
37
- expect(isInstalledInScope(undefined)).toBe(false);
38
- });
38
+ test("an absent scope is not installed", () => {
39
+ expect(isInstalledInScope(undefined)).toBe(false);
40
+ });
39
41
 
40
- test('"0.0.0" counts as installed — Anthropic official plugins record it', () => {
41
- expect(isInstalledInScope({ enabled: true, version: "0.0.0" })).toBe(true);
42
- });
42
+ test('"0.0.0" counts as installed — Anthropic official plugins record it', () => {
43
+ expect(isInstalledInScope({ enabled: true, version: "0.0.0" })).toBe(true);
44
+ });
43
45
  });
44
46
 
45
47
  describe("resolveScopeAction", () => {
46
- test("REGRESSION: enabled but not installed resolves to install, never uninstall", () => {
47
- // This is the reported bug. Before the fix this returned "uninstall".
48
- expect(resolveScopeAction({ enabled: true }, "3.0.2")).toBe("install");
49
- });
48
+ test("REGRESSION: enabled but not installed resolves to install, never uninstall", () => {
49
+ // This is the reported bug. Before the fix this returned "uninstall".
50
+ expect(resolveScopeAction({ enabled: true }, "3.0.2")).toBe("install");
51
+ });
50
52
 
51
- test("a scope that was never enabled resolves to install", () => {
52
- expect(resolveScopeAction(undefined, "3.0.2")).toBe("install");
53
- expect(resolveScopeAction({ enabled: false }, "3.0.2")).toBe("install");
54
- });
53
+ test("a scope that was never enabled resolves to install", () => {
54
+ expect(resolveScopeAction(undefined, "3.0.2")).toBe("install");
55
+ expect(resolveScopeAction({ enabled: false }, "3.0.2")).toBe("install");
56
+ });
55
57
 
56
- test("a healthy install at the latest version resolves to uninstall (toggle off)", () => {
57
- expect(resolveScopeAction({ enabled: true, version: "3.0.2" }, "3.0.2")).toBe(
58
- "uninstall",
59
- );
60
- });
58
+ test("a healthy install at the latest version resolves to uninstall (toggle off)", () => {
59
+ expect(
60
+ resolveScopeAction({ enabled: true, version: "3.0.2" }, "3.0.2"),
61
+ ).toBe("uninstall");
62
+ });
61
63
 
62
- test("a healthy install behind the catalog resolves to update", () => {
63
- expect(resolveScopeAction({ enabled: true, version: "2.9.0" }, "3.0.2")).toBe(
64
- "update",
65
- );
66
- });
64
+ test("a healthy install behind the catalog resolves to update", () => {
65
+ expect(
66
+ resolveScopeAction({ enabled: true, version: "2.9.0" }, "3.0.2"),
67
+ ).toBe("update");
68
+ });
67
69
 
68
- test('an unknown catalog version ("0.0.0") never fabricates an update', () => {
69
- // There is nothing to update *to*, so toggling must still mean uninstall.
70
- expect(resolveScopeAction({ enabled: true, version: "3.0.2" }, "0.0.0")).toBe(
71
- "uninstall",
72
- );
73
- });
70
+ test('an unknown catalog version ("0.0.0") never fabricates an update', () => {
71
+ // There is nothing to update *to*, so toggling must still mean uninstall.
72
+ expect(
73
+ resolveScopeAction({ enabled: true, version: "3.0.2" }, "0.0.0"),
74
+ ).toBe("uninstall");
75
+ });
74
76
 
75
- test("installed with unknown version resolves to update when a real version is published", () => {
76
- // "0.0.0" installed against a real catalog version is a genuine upgrade
77
- // path, not the broken state — it must not be treated as uninstall.
78
- expect(resolveScopeAction({ enabled: true, version: "0.0.0" }, "3.0.2")).toBe(
79
- "update",
80
- );
81
- });
77
+ test("installed with unknown version resolves to update when a real version is published", () => {
78
+ // "0.0.0" installed against a real catalog version is a genuine upgrade
79
+ // path, not the broken state — it must not be treated as uninstall.
80
+ expect(
81
+ resolveScopeAction({ enabled: true, version: "0.0.0" }, "3.0.2"),
82
+ ).toBe("update");
83
+ });
82
84
  });
@@ -2,7 +2,6 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import type React from "react";
3
3
  import { ScopeSquares } from "../ui/components/primitives/ScopeSquares.js";
4
4
  import {
5
- SCOPE_OFF_FALLBACK,
6
5
  SCOPE_OFF_FILL,
7
6
  resetThemeMode,
8
7
  setThemeMode,
@@ -19,8 +18,8 @@ import { CONTRAST_REFERENCE, theme } from "../ui/theme.js";
19
18
  *
20
19
  * An unlit segment has to recede toward the page, which is exactly what a fixed
21
20
  * colour cannot do — a pale fill that sits quietly on cream is a bright block on
22
- * near-black. So the fill follows the detected terminal, and these tests pin both
23
- * that behaviour and its fallback.
21
+ * near-black. So the fill follows the resolved mode, and these tests pin both
22
+ * that behaviour and the dark default that applies before resolution.
24
23
  */
25
24
 
26
25
  interface SpanLike {
@@ -94,10 +93,10 @@ describe("fill distinguishes lit from unlit", () => {
94
93
  ]);
95
94
  });
96
95
 
97
- test("an undetected terminal still gets a visible fill, never nothing", () => {
96
+ test("before resolution the fill is the dark pair", () => {
98
97
  resetThemeMode();
99
98
  const el = ScopeSquares({ user: false, project: false, local: false });
100
- expect(new Set(fills(el))).toEqual(new Set([SCOPE_OFF_FALLBACK]));
99
+ expect(new Set(fills(el))).toEqual(new Set([SCOPE_OFF_FILL.dark]));
101
100
  });
102
101
  });
103
102