claudeup 6.2.1 → 6.3.0

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 +38 -13
  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 +183 -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 +1 -2
  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
@@ -1,135 +1,137 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdtemp, rm, writeFile, mkdir, readFile } from "node:fs/promises";
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
5
- import { spawnSync } from "node:child_process";
6
6
  import {
7
- loadGitignoreState,
8
- applyTemplate,
9
- ensureProjectManifestForEdit,
7
+ applyTemplate,
8
+ ensureProjectManifestForEdit,
9
+ loadGitignoreState,
10
10
  } from "../services/gitignore-service";
11
11
 
12
12
  function git(cwd: string, ...args: string[]): void {
13
- spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
13
+ spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
14
14
  }
15
15
 
16
16
  describe("loadGitignoreState", () => {
17
- let projectDir: string;
18
-
19
- beforeEach(async () => {
20
- projectDir = await mkdtemp(join(tmpdir(), "gitig-svc-"));
21
- });
22
-
23
- afterEach(async () => {
24
- await rm(projectDir, { recursive: true, force: true });
25
- });
26
-
27
- it("returns resolved manifest + violations + warnings", async () => {
28
- git(projectDir, "init", "-q");
29
- const s = await loadGitignoreState(projectDir);
30
- expect(s.resolved).toBeDefined();
31
- expect(Array.isArray(s.resolved.rules)).toBe(true);
32
- expect(Array.isArray(s.violations)).toBe(true);
33
- expect(Array.isArray(s.warnings)).toBe(true);
34
- });
17
+ let projectDir: string;
18
+
19
+ beforeEach(async () => {
20
+ projectDir = await mkdtemp(join(tmpdir(), "gitig-svc-"));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await rm(projectDir, { recursive: true, force: true });
25
+ });
26
+
27
+ it("returns resolved manifest + violations + warnings", async () => {
28
+ git(projectDir, "init", "-q");
29
+ const s = await loadGitignoreState(projectDir);
30
+ expect(s.resolved).toBeDefined();
31
+ expect(Array.isArray(s.resolved.rules)).toBe(true);
32
+ expect(Array.isArray(s.violations)).toBe(true);
33
+ expect(Array.isArray(s.warnings)).toBe(true);
34
+ });
35
35
  });
36
36
 
37
37
  describe("applyTemplate", () => {
38
- let projectDir: string;
39
-
40
- beforeEach(async () => {
41
- projectDir = await mkdtemp(join(tmpdir(), "gitig-tpl-"));
42
- await mkdir(join(projectDir, ".claude"), { recursive: true });
43
- });
44
-
45
- afterEach(async () => {
46
- await rm(projectDir, { recursive: true, force: true });
47
- });
48
-
49
- it("creates project manifest and merges template entries", async () => {
50
- const r = await applyTemplate(projectDir, "common-dev");
51
- expect(r.added.ignore).toContain("node_modules/");
52
- expect(r.added.ignore).toContain(".env");
53
- expect(r.conflicts).toEqual([]);
54
-
55
- const written = JSON.parse(
56
- await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
57
- );
58
- expect(written.ignore).toContain("node_modules/");
59
- });
60
-
61
- it("does not duplicate entries on second apply", async () => {
62
- await applyTemplate(projectDir, "common-dev");
63
- const r2 = await applyTemplate(projectDir, "common-dev");
64
- expect(r2.added.ignore).toEqual([]);
65
- expect(r2.added.track).toEqual([]);
66
-
67
- const written = JSON.parse(
68
- await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
69
- );
70
- const occurrences = written.ignore.filter((p: string) => p === "node_modules/").length;
71
- expect(occurrences).toBe(1);
72
- });
73
-
74
- it("reports conflicts when an existing pattern would flip action", async () => {
75
- await writeFile(
76
- join(projectDir, ".claude", "gitignore.json"),
77
- JSON.stringify({ ignore: [], track: ["node_modules/"] }),
78
- );
79
- const r = await applyTemplate(projectDir, "common-dev");
80
- expect(r.conflicts).toContain("node_modules/");
81
- // node_modules/ should NOT have been added to ignore
82
- const written = JSON.parse(
83
- await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
84
- );
85
- expect(written.ignore).not.toContain("node_modules/");
86
- expect(written.track).toContain("node_modules/");
87
- });
88
-
89
- it("throws on unknown template name", async () => {
90
- await expect(applyTemplate(projectDir, "does-not-exist")).rejects.toThrow(
91
- /Unknown template/,
92
- );
93
- });
38
+ let projectDir: string;
39
+
40
+ beforeEach(async () => {
41
+ projectDir = await mkdtemp(join(tmpdir(), "gitig-tpl-"));
42
+ await mkdir(join(projectDir, ".claude"), { recursive: true });
43
+ });
44
+
45
+ afterEach(async () => {
46
+ await rm(projectDir, { recursive: true, force: true });
47
+ });
48
+
49
+ it("creates project manifest and merges template entries", async () => {
50
+ const r = await applyTemplate(projectDir, "common-dev");
51
+ expect(r.added.ignore).toContain("node_modules/");
52
+ expect(r.added.ignore).toContain(".env");
53
+ expect(r.conflicts).toEqual([]);
54
+
55
+ const written = JSON.parse(
56
+ await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
57
+ );
58
+ expect(written.ignore).toContain("node_modules/");
59
+ });
60
+
61
+ it("does not duplicate entries on second apply", async () => {
62
+ await applyTemplate(projectDir, "common-dev");
63
+ const r2 = await applyTemplate(projectDir, "common-dev");
64
+ expect(r2.added.ignore).toEqual([]);
65
+ expect(r2.added.track).toEqual([]);
66
+
67
+ const written = JSON.parse(
68
+ await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
69
+ );
70
+ const occurrences = written.ignore.filter(
71
+ (p: string) => p === "node_modules/",
72
+ ).length;
73
+ expect(occurrences).toBe(1);
74
+ });
75
+
76
+ it("reports conflicts when an existing pattern would flip action", async () => {
77
+ await writeFile(
78
+ join(projectDir, ".claude", "gitignore.json"),
79
+ JSON.stringify({ ignore: [], track: ["node_modules/"] }),
80
+ );
81
+ const r = await applyTemplate(projectDir, "common-dev");
82
+ expect(r.conflicts).toContain("node_modules/");
83
+ // node_modules/ should NOT have been added to ignore
84
+ const written = JSON.parse(
85
+ await readFile(join(projectDir, ".claude", "gitignore.json"), "utf8"),
86
+ );
87
+ expect(written.ignore).not.toContain("node_modules/");
88
+ expect(written.track).toContain("node_modules/");
89
+ });
90
+
91
+ it("throws on unknown template name", async () => {
92
+ await expect(applyTemplate(projectDir, "does-not-exist")).rejects.toThrow(
93
+ /Unknown template/,
94
+ );
95
+ });
94
96
  });
95
97
 
96
98
  describe("ensureProjectManifestForEdit", () => {
97
- let projectDir: string;
98
-
99
- beforeEach(async () => {
100
- projectDir = await mkdtemp(join(tmpdir(), "gitig-edit-"));
101
- });
102
-
103
- afterEach(async () => {
104
- await rm(projectDir, { recursive: true, force: true });
105
- });
106
-
107
- it("creates an editable project manifest from resolved rules", async () => {
108
- const path = await ensureProjectManifestForEdit(projectDir, {
109
- rules: [
110
- { pattern: ".env", action: "ignore", source: "builtin" },
111
- { pattern: ".mcp.json", action: "track", source: "builtin" },
112
- ],
113
- conflicts: [],
114
- });
115
-
116
- expect(path).toBe(join(projectDir, ".claude", "gitignore.json"));
117
- const written = JSON.parse(await readFile(path, "utf8"));
118
- expect(written.ignore).toEqual([".env"]);
119
- expect(written.track).toEqual([".mcp.json"]);
120
- });
121
-
122
- it("keeps an existing non-empty project manifest", async () => {
123
- const path = join(projectDir, ".claude", "gitignore.json");
124
- await mkdir(join(projectDir, ".claude"), { recursive: true });
125
- await writeFile(path, JSON.stringify({ ignore: ["custom"], track: [] }));
126
-
127
- await ensureProjectManifestForEdit(projectDir, {
128
- rules: [{ pattern: ".env", action: "ignore", source: "builtin" }],
129
- conflicts: [],
130
- });
131
-
132
- const written = JSON.parse(await readFile(path, "utf8"));
133
- expect(written.ignore).toEqual(["custom"]);
134
- });
99
+ let projectDir: string;
100
+
101
+ beforeEach(async () => {
102
+ projectDir = await mkdtemp(join(tmpdir(), "gitig-edit-"));
103
+ });
104
+
105
+ afterEach(async () => {
106
+ await rm(projectDir, { recursive: true, force: true });
107
+ });
108
+
109
+ it("creates an editable project manifest from resolved rules", async () => {
110
+ const path = await ensureProjectManifestForEdit(projectDir, {
111
+ rules: [
112
+ { pattern: ".env", action: "ignore", source: "builtin" },
113
+ { pattern: ".mcp.json", action: "track", source: "builtin" },
114
+ ],
115
+ conflicts: [],
116
+ });
117
+
118
+ expect(path).toBe(join(projectDir, ".claude", "gitignore.json"));
119
+ const written = JSON.parse(await readFile(path, "utf8"));
120
+ expect(written.ignore).toEqual([".env"]);
121
+ expect(written.track).toEqual([".mcp.json"]);
122
+ });
123
+
124
+ it("keeps an existing non-empty project manifest", async () => {
125
+ const path = join(projectDir, ".claude", "gitignore.json");
126
+ await mkdir(join(projectDir, ".claude"), { recursive: true });
127
+ await writeFile(path, JSON.stringify({ ignore: ["custom"], track: [] }));
128
+
129
+ await ensureProjectManifestForEdit(projectDir, {
130
+ rules: [{ pattern: ".env", action: "ignore", source: "builtin" }],
131
+ conflicts: [],
132
+ });
133
+
134
+ const written = JSON.parse(await readFile(path, "utf8"));
135
+ expect(written.ignore).toEqual(["custom"]);
136
+ });
135
137
  });
@@ -18,9 +18,7 @@ import { runInstallCommand } from "../cli/install.js";
18
18
 
19
19
  let logs: string[];
20
20
  let errs: string[];
21
- // biome-ignore lint/suspicious/noExplicitAny: saved console refs
22
21
  let origLog: any;
23
- // biome-ignore lint/suspicious/noExplicitAny: saved console refs
24
22
  let origErr: any;
25
23
  beforeEach(() => {
26
24
  logs = [];
@@ -69,7 +67,9 @@ describe("runInstallCommand — early-return paths", () => {
69
67
  test("unknown profile → exit 1", async () => {
70
68
  dir = await project({
71
69
  version: 2,
72
- profiles: { frontend: { name: "Frontend", plugins: { "dev@magus": "latest" } } },
70
+ profiles: {
71
+ frontend: { name: "Frontend", plugins: { "dev@magus": "latest" } },
72
+ },
73
73
  });
74
74
  expect(await runInstallCommand(["ghost", "--check"], dir)).toBe(1);
75
75
  });
@@ -22,9 +22,7 @@ function closure(overrides: Partial<ResolvedClosure> = {}): ResolvedClosure {
22
22
  };
23
23
  }
24
24
 
25
- function registry(
26
- plugins: Record<string, string[]>,
27
- ): InstalledPluginsRegistry {
25
+ function registry(plugins: Record<string, string[]>): InstalledPluginsRegistry {
28
26
  const out: InstalledPluginsRegistry = { version: 2, plugins: {} };
29
27
  for (const [id, versions] of Object.entries(plugins)) {
30
28
  out.plugins[id] = versions.map((v) => ({
@@ -41,12 +39,16 @@ function registry(
41
39
  describe("computeVersionDrift", () => {
42
40
  test("no drift when installed matches the pin", () => {
43
41
  const c = closure({ plugins: { "dev@magus": "2.9.0" } });
44
- expect(computeVersionDrift(c, registry({ "dev@magus": ["2.9.0"] }))).toEqual([]);
42
+ expect(
43
+ computeVersionDrift(c, registry({ "dev@magus": ["2.9.0"] })),
44
+ ).toEqual([]);
45
45
  });
46
46
 
47
47
  test("'latest' pins are never drift-checked", () => {
48
48
  const c = closure({ plugins: { "dev@magus": "latest" } });
49
- expect(computeVersionDrift(c, registry({ "dev@magus": ["1.0.0"] }))).toEqual([]);
49
+ expect(
50
+ computeVersionDrift(c, registry({ "dev@magus": ["1.0.0"] })),
51
+ ).toEqual([]);
50
52
  });
51
53
 
52
54
  test("flags a mismatch, reporting the highest installed version", () => {
@@ -75,7 +77,9 @@ describe("computeEnvStatus", () => {
75
77
  });
76
78
  const status = computeEnvStatus(c, { SET_VAR: "x", OPT: "" });
77
79
  expect(status.missingRequired).toEqual(["FIGMA_TOKEN"]);
78
- expect(status.required.find((e) => e.name === "SET_VAR")!.present).toBe(true);
80
+ expect(status.required.find((e) => e.name === "SET_VAR")!.present).toBe(
81
+ true,
82
+ );
79
83
  // empty string counts as missing
80
84
  expect(status.optional[0]!.present).toBe(false);
81
85
  });
@@ -86,7 +90,9 @@ describe("summarizeClosure", () => {
86
90
  const c = closure({
87
91
  marketplaces: { magus: { source: "github", repo: "MadAppGang/magus" } },
88
92
  plugins: { "dev@magus": "latest", "terminal@magus": "4.1.0" },
89
- bins: [{ name: "claudish", via: "bun", version: "1.2.0", sources: ["x"] }],
93
+ bins: [
94
+ { name: "claudish", via: "bun", version: "1.2.0", sources: ["x"] },
95
+ ],
90
96
  env: { required: ["FIGMA_TOKEN"], optional: [] },
91
97
  });
92
98
  const lines = summarizeClosure(c);
@@ -46,9 +46,14 @@ describe("migrateToV2", () => {
46
46
  });
47
47
 
48
48
  test("strictVersions is carried through as a boolean", () => {
49
- expect(migrateToV2({ version: 2, strictVersions: true, profiles: {} }).strictVersions).toBe(true);
49
+ expect(
50
+ migrateToV2({ version: 2, strictVersions: true, profiles: {} })
51
+ .strictVersions,
52
+ ).toBe(true);
50
53
  // Omitted (not written as false) to keep a committed manifest minimal.
51
- expect(migrateToV2({ version: 2, profiles: {} }).strictVersions).toBeUndefined();
54
+ expect(
55
+ migrateToV2({ version: 2, profiles: {} }).strictVersions,
56
+ ).toBeUndefined();
52
57
  });
53
58
  });
54
59
 
@@ -94,8 +99,12 @@ describe("validateManifest", () => {
94
99
  },
95
100
  },
96
101
  });
97
- expect(errors.some((e) => e.path === "profiles.p.marketplaces.m")).toBe(true);
98
- expect(errors.some((e) => e.path.startsWith("profiles.p.skills"))).toBe(true);
102
+ expect(errors.some((e) => e.path === "profiles.p.marketplaces.m")).toBe(
103
+ true,
104
+ );
105
+ expect(errors.some((e) => e.path.startsWith("profiles.p.skills"))).toBe(
106
+ true,
107
+ );
99
108
  });
100
109
  });
101
110
 
@@ -119,7 +128,9 @@ describe("readManifest / writeManifest", () => {
119
128
  profiles: { p: { name: "P", plugins: { "dev@magus": "1.0.0" } } },
120
129
  };
121
130
  await writeManifest(manifest, dir);
122
- expect(await fs.pathExists(join(dir, ".claude", "profiles.json"))).toBe(true);
131
+ expect(await fs.pathExists(join(dir, ".claude", "profiles.json"))).toBe(
132
+ true,
133
+ );
123
134
  expect(await readManifest(dir)).toEqual(manifest);
124
135
  });
125
136
 
@@ -8,7 +8,6 @@ import { renderMarkdown } from "../ui/renderers/markdownRenderer.js";
8
8
  */
9
9
 
10
10
  /** Flatten a rendered node tree to the text a reader would see. */
11
- // biome-ignore lint/suspicious/noExplicitAny: walking opaque element trees
12
11
  function textOf(node: any): string {
13
12
  if (node === null || node === undefined || typeof node === "boolean") {
14
13
  return "";
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { buildPluginBrowserItems } from "../ui/adapters/pluginsAdapter.js";
3
2
  import type { Marketplace } from "../types/index.js";
3
+ import { buildPluginBrowserItems } from "../ui/adapters/pluginsAdapter.js";
4
4
 
5
5
  /**
6
6
  * The marketplace category badge is the line users read on every claudeup launch,
@@ -9,111 +9,132 @@ import type { Marketplace } from "../types/index.js";
9
9
  */
10
10
 
11
11
  const mp = (over: Partial<Marketplace> & { name: string }): Marketplace => ({
12
- displayName: over.displayName ?? over.name,
13
- source: { source: "github", repo: `someone/${over.name}` },
14
- description: "",
15
- ...over,
12
+ displayName: over.displayName ?? over.name,
13
+ source: { source: "github", repo: `someone/${over.name}` },
14
+ description: "",
15
+ ...over,
16
16
  });
17
17
 
18
18
  /** Badge for a marketplace when it has plugins and is expanded. */
19
19
  function badgeFor(marketplace: Marketplace): string | undefined {
20
- const items = buildPluginBrowserItems({
21
- marketplaces: [marketplace],
22
- plugins: [
23
- {
24
- id: `demo@${marketplace.name}`,
25
- name: "demo",
26
- marketplace: marketplace.name,
27
- // Only the fields the adapter groups on matter here.
28
- } as never,
29
- ],
30
- collapsedMarketplaces: new Set(),
31
- });
32
- const category = items.find((i) => i.kind === "category");
33
- return category && "badge" in category ? category.badge : undefined;
20
+ const items = buildPluginBrowserItems({
21
+ marketplaces: [marketplace],
22
+ plugins: [
23
+ {
24
+ id: `demo@${marketplace.name}`,
25
+ name: "demo",
26
+ marketplace: marketplace.name,
27
+ // Only the fields the adapter groups on matter here.
28
+ } as never,
29
+ ],
30
+ collapsedMarketplaces: new Set(),
31
+ });
32
+ const category = items.find((i) => i.kind === "category");
33
+ return category && "badge" in category ? category.badge : undefined;
34
34
  }
35
35
 
36
36
  /** Tone for a marketplace when it has plugins and is expanded. */
37
37
  function toneFor(marketplace: Marketplace): string | undefined {
38
- const items = buildPluginBrowserItems({
39
- marketplaces: [marketplace],
40
- plugins: [
41
- { id: `demo@${marketplace.name}`, name: "demo", marketplace: marketplace.name } as never,
42
- ],
43
- collapsedMarketplaces: new Set(),
44
- });
45
- const category = items.find((i) => i.kind === "category");
46
- return category && "tone" in category ? category.tone : undefined;
38
+ const items = buildPluginBrowserItems({
39
+ marketplaces: [marketplace],
40
+ plugins: [
41
+ {
42
+ id: `demo@${marketplace.name}`,
43
+ name: "demo",
44
+ marketplace: marketplace.name,
45
+ } as never,
46
+ ],
47
+ collapsedMarketplaces: new Set(),
48
+ });
49
+ const category = items.find((i) => i.kind === "category");
50
+ return category && "tone" in category ? category.tone : undefined;
47
51
  }
48
52
 
49
53
  describe("marketplace category badge", () => {
50
- test("the byline uses a low-key tone, not an attention-grabbing one", () => {
51
- // It is a signature, not a status. Green reads as "action succeeded" and
52
- // competes with the plugin rows for attention.
53
- expect(
54
- toneFor(mp({ name: "magus", source: { source: "github", repo: "MadAppGang/magus" } })),
55
- ).toBe("byline");
54
+ test("the byline uses a low-key tone, not an attention-grabbing one", () => {
55
+ // It is a signature, not a status. Green reads as "action succeeded" and
56
+ // competes with the plugin rows for attention.
57
+ expect(
58
+ toneFor(
59
+ mp({
60
+ name: "magus",
61
+ source: { source: "github", repo: "MadAppGang/magus" },
62
+ }),
63
+ ),
64
+ ).toBe("byline");
56
65
 
57
- // A third-party marketplace still uses the green "Added" status tone.
58
- expect(
59
- toneFor(
60
- mp({ name: "sp", source: { source: "github", repo: "obra/superpowers-marketplace" } }),
61
- ),
62
- ).toBe("green");
63
- });
66
+ // A third-party marketplace still uses the green "Added" status tone.
67
+ expect(
68
+ toneFor(
69
+ mp({
70
+ name: "sp",
71
+ source: { source: "github", repo: "obra/superpowers-marketplace" },
72
+ }),
73
+ ),
74
+ ).toBe("green");
75
+ });
64
76
 
65
- test("MadAppGang marketplaces carry the byline instead of a generic label", () => {
66
- for (const name of ["magus", "magus-marketing", "magus-alpha"]) {
67
- const badge = badgeFor(
68
- mp({ name, source: { source: "github", repo: `MadAppGang/${name}` } }),
69
- );
70
- expect(badge).toBe("by MadAppGang");
71
- }
72
- });
77
+ test("MadAppGang marketplaces carry the byline instead of a generic label", () => {
78
+ for (const name of ["magus", "magus-marketing", "magus-alpha"]) {
79
+ const badge = badgeFor(
80
+ mp({ name, source: { source: "github", repo: `MadAppGang/${name}` } }),
81
+ );
82
+ expect(badge).toBe("by MadAppGang");
83
+ }
84
+ });
73
85
 
74
- test("owner match is case-insensitive", () => {
75
- // The npm/GitHub casing of the org has varied in config; don't depend on it.
76
- const badge = badgeFor(
77
- mp({ name: "magus", source: { source: "github", repo: "madappgang/magus" } }),
78
- );
79
- expect(badge).toBe("by MadAppGang");
80
- });
86
+ test("owner match is case-insensitive", () => {
87
+ // The npm/GitHub casing of the org has varied in config; don't depend on it.
88
+ const badge = badgeFor(
89
+ mp({
90
+ name: "magus",
91
+ source: { source: "github", repo: "madappgang/magus" },
92
+ }),
93
+ );
94
+ expect(badge).toBe("by MadAppGang");
95
+ });
81
96
 
82
- test("someone else's marketplace does NOT claim the byline", () => {
83
- const badge = badgeFor(
84
- mp({
85
- name: "superpowers-marketplace",
86
- source: { source: "github", repo: "obra/superpowers-marketplace" },
87
- }),
88
- );
89
- expect(badge).toBe("✓ Added");
90
- });
97
+ test("someone else's marketplace does NOT claim the byline", () => {
98
+ const badge = badgeFor(
99
+ mp({
100
+ name: "superpowers-marketplace",
101
+ source: { source: "github", repo: "obra/superpowers-marketplace" },
102
+ }),
103
+ );
104
+ expect(badge).toBe("✓ Added");
105
+ });
91
106
 
92
- test("a repo merely containing 'madappgang' downstream does not match", () => {
93
- // Guards against a substring check — only the owner segment counts.
94
- const badge = badgeFor(
95
- mp({ name: "impostor", source: { source: "github", repo: "evil/madappgang-magus" } }),
96
- );
97
- expect(badge).toBe(" Added");
98
- });
107
+ test("a repo merely containing 'madappgang' downstream does not match", () => {
108
+ // Guards against a substring check — only the owner segment counts.
109
+ const badge = badgeFor(
110
+ mp({
111
+ name: "impostor",
112
+ source: { source: "github", repo: "evil/madappgang-magus" },
113
+ }),
114
+ );
115
+ expect(badge).toBe("✓ Added");
116
+ });
99
117
 
100
- test("Official and Deprecated still take precedence", () => {
101
- expect(
102
- badgeFor(
103
- mp({
104
- name: "claude-plugins-official",
105
- source: { source: "github", repo: "anthropics/claude-plugins-official" },
106
- }),
107
- ),
108
- ).toBe("★ Official");
118
+ test("Official and Deprecated still take precedence", () => {
119
+ expect(
120
+ badgeFor(
121
+ mp({
122
+ name: "claude-plugins-official",
123
+ source: {
124
+ source: "github",
125
+ repo: "anthropics/claude-plugins-official",
126
+ },
127
+ }),
128
+ ),
129
+ ).toBe("★ Official");
109
130
 
110
- expect(
111
- badgeFor(
112
- mp({
113
- name: "claude-code-plugins",
114
- source: { source: "github", repo: "anthropics/claude-code" },
115
- }),
116
- ),
117
- ).toBe("⚠ Deprecated");
118
- });
131
+ expect(
132
+ badgeFor(
133
+ mp({
134
+ name: "claude-code-plugins",
135
+ source: { source: "github", repo: "anthropics/claude-code" },
136
+ }),
137
+ ),
138
+ ).toBe("⚠ Deprecated");
139
+ });
119
140
  });