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,76 +1,76 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdtemp, rm, writeFile, mkdir } 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, 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
- checkGitignore,
8
- formatPrerunWarning,
7
+ checkGitignore,
8
+ formatPrerunWarning,
9
9
  } from "../services/gitignore-prerun";
10
10
 
11
11
  function git(cwd: string, ...args: string[]): void {
12
- spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
12
+ spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
13
13
  }
14
14
 
15
15
  describe("checkGitignore", () => {
16
- let projectDir: string;
16
+ let projectDir: string;
17
17
 
18
- beforeEach(async () => {
19
- projectDir = await mkdtemp(join(tmpdir(), "gitig-prerun-"));
20
- });
18
+ beforeEach(async () => {
19
+ projectDir = await mkdtemp(join(tmpdir(), "gitig-prerun-"));
20
+ });
21
21
 
22
- afterEach(async () => {
23
- await rm(projectDir, { recursive: true, force: true });
24
- });
22
+ afterEach(async () => {
23
+ await rm(projectDir, { recursive: true, force: true });
24
+ });
25
25
 
26
- it("returns 0 violations for non-git directory", async () => {
27
- const r = await checkGitignore(projectDir);
28
- expect(r.violationCount).toBe(0);
29
- });
26
+ it("returns 0 violations for non-git directory", async () => {
27
+ const r = await checkGitignore(projectDir);
28
+ expect(r.violationCount).toBe(0);
29
+ });
30
30
 
31
- it("flags missing built-in patterns inside a fresh git repo", async () => {
32
- git(projectDir, "init", "-q");
33
- const r = await checkGitignore(projectDir);
34
- // BUILTIN_DEFAULTS includes ".mnemex/" etc. — none are tracked, none in
35
- // .gitignore, so each one becomes "missing-from-gitignore".
36
- expect(r.violationCount).toBeGreaterThan(0);
37
- });
31
+ it("flags missing built-in patterns inside a fresh git repo", async () => {
32
+ git(projectDir, "init", "-q");
33
+ const r = await checkGitignore(projectDir);
34
+ // BUILTIN_DEFAULTS includes ".mnemex/" etc. — none are tracked, none in
35
+ // .gitignore, so each one becomes "missing-from-gitignore".
36
+ expect(r.violationCount).toBeGreaterThan(0);
37
+ });
38
38
 
39
- it("returns 0 when .gitignore covers all built-in patterns and tracked files match track rules", async () => {
40
- git(projectDir, "init", "-q");
41
- git(projectDir, "config", "user.email", "t@t");
42
- git(projectDir, "config", "user.name", "t");
43
- // Suppress all built-in ignore rules
44
- await writeFile(
45
- join(projectDir, ".gitignore"),
46
- [
47
- ".claude/settings.local.json",
48
- ".claude/scheduled_tasks.lock",
49
- ".claude/cache/",
50
- ".claude/.statusline-worktree-*",
51
- ".claude/_profiles/",
52
- "ai-docs/sessions/",
53
- ".mnemex/",
54
- ".claudemem/",
55
- "gtd/sessions/",
56
- ".agents/",
57
- ].join("\n") + "\n",
58
- );
59
- // Tracked files don't have to exist for the absent-track-path case to be ignored
60
- const r = await checkGitignore(projectDir);
61
- expect(r.violationCount).toBe(0);
62
- });
39
+ it("returns 0 when .gitignore covers all built-in patterns and tracked files match track rules", async () => {
40
+ git(projectDir, "init", "-q");
41
+ git(projectDir, "config", "user.email", "t@t");
42
+ git(projectDir, "config", "user.name", "t");
43
+ // Suppress all built-in ignore rules
44
+ await writeFile(
45
+ join(projectDir, ".gitignore"),
46
+ `${[
47
+ ".claude/settings.local.json",
48
+ ".claude/scheduled_tasks.lock",
49
+ ".claude/cache/",
50
+ ".claude/.statusline-worktree-*",
51
+ ".claude/_profiles/",
52
+ "ai-docs/sessions/",
53
+ ".mnemex/",
54
+ ".claudemem/",
55
+ "gtd/sessions/",
56
+ ".agents/",
57
+ ].join("\n")}\n`,
58
+ );
59
+ // Tracked files don't have to exist for the absent-track-path case to be ignored
60
+ const r = await checkGitignore(projectDir);
61
+ expect(r.violationCount).toBe(0);
62
+ });
63
63
  });
64
64
 
65
65
  describe("formatPrerunWarning", () => {
66
- it("returns null when no violations", () => {
67
- expect(formatPrerunWarning({ violationCount: 0, warnings: [] })).toBeNull();
68
- });
66
+ it("returns null when no violations", () => {
67
+ expect(formatPrerunWarning({ violationCount: 0, warnings: [] })).toBeNull();
68
+ });
69
69
 
70
- it("returns a warning string when violations exist", () => {
71
- const w = formatPrerunWarning({ violationCount: 3, warnings: [] });
72
- expect(w).not.toBeNull();
73
- expect(w).toContain("3 git state");
74
- expect(w).toContain("Git State tab");
75
- });
70
+ it("returns a warning string when violations exist", () => {
71
+ const w = formatPrerunWarning({ violationCount: 3, warnings: [] });
72
+ expect(w).not.toBeNull();
73
+ expect(w).toContain("3 git state");
74
+ expect(w).toContain("Git State tab");
75
+ });
76
76
  });
@@ -1,166 +1,193 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { resolveGitignore } from "../services/gitignore-resolver";
6
5
  import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults";
6
+ import { resolveGitignore } from "../services/gitignore-resolver";
7
7
 
8
8
  describe("resolveGitignore", () => {
9
- let projectDir: string;
10
- let homeDir: string;
9
+ let projectDir: string;
10
+ let homeDir: string;
11
11
 
12
- beforeEach(async () => {
13
- projectDir = await mkdtemp(join(tmpdir(), "gitig-proj-"));
14
- homeDir = await mkdtemp(join(tmpdir(), "gitig-home-"));
15
- await mkdir(join(projectDir, ".claude"), { recursive: true });
16
- await mkdir(join(homeDir, ".claude"), { recursive: true });
17
- });
12
+ beforeEach(async () => {
13
+ projectDir = await mkdtemp(join(tmpdir(), "gitig-proj-"));
14
+ homeDir = await mkdtemp(join(tmpdir(), "gitig-home-"));
15
+ await mkdir(join(projectDir, ".claude"), { recursive: true });
16
+ await mkdir(join(homeDir, ".claude"), { recursive: true });
17
+ });
18
18
 
19
- afterEach(async () => {
20
- await rm(projectDir, { recursive: true, force: true });
21
- await rm(homeDir, { recursive: true, force: true });
22
- });
19
+ afterEach(async () => {
20
+ await rm(projectDir, { recursive: true, force: true });
21
+ await rm(homeDir, { recursive: true, force: true });
22
+ });
23
23
 
24
- it("returns built-in defaults when no global or project manifest exists", async () => {
25
- const r = await resolveGitignore(projectDir, { homeDir });
26
- expect(r.conflicts).toEqual([]);
27
- const ignored = r.rules.filter((x) => x.action === "ignore").map((x) => x.pattern);
28
- for (const p of BUILTIN_DEFAULTS.ignore) {
29
- expect(ignored).toContain(p);
30
- }
31
- const tracked = r.rules.filter((x) => x.action === "track").map((x) => x.pattern);
32
- for (const p of BUILTIN_DEFAULTS.track) {
33
- expect(tracked).toContain(p);
34
- }
35
- for (const rule of r.rules) {
36
- expect(rule.source).toBe("builtin");
37
- }
38
- });
24
+ it("returns built-in defaults when no global or project manifest exists", async () => {
25
+ const r = await resolveGitignore(projectDir, { homeDir });
26
+ expect(r.conflicts).toEqual([]);
27
+ const ignored = r.rules
28
+ .filter((x) => x.action === "ignore")
29
+ .map((x) => x.pattern);
30
+ for (const p of BUILTIN_DEFAULTS.ignore) {
31
+ expect(ignored).toContain(p);
32
+ }
33
+ const tracked = r.rules
34
+ .filter((x) => x.action === "track")
35
+ .map((x) => x.pattern);
36
+ for (const p of BUILTIN_DEFAULTS.track) {
37
+ expect(tracked).toContain(p);
38
+ }
39
+ for (const rule of r.rules) {
40
+ expect(rule.source).toBe("builtin");
41
+ }
42
+ });
39
43
 
40
- it("project tier overrides global tier", async () => {
41
- await writeFile(
42
- join(homeDir, ".claude", "gitignore.json"),
43
- JSON.stringify({ ignore: [".env"], track: [] }),
44
- );
45
- await writeFile(
46
- join(projectDir, ".claude", "gitignore.json"),
47
- JSON.stringify({ ignore: [], track: [".env"] }),
48
- );
44
+ it("project tier overrides global tier", async () => {
45
+ await writeFile(
46
+ join(homeDir, ".claude", "gitignore.json"),
47
+ JSON.stringify({ ignore: [".env"], track: [] }),
48
+ );
49
+ await writeFile(
50
+ join(projectDir, ".claude", "gitignore.json"),
51
+ JSON.stringify({ ignore: [], track: [".env"] }),
52
+ );
49
53
 
50
- const r = await resolveGitignore(projectDir, { homeDir });
51
- const env = r.rules.find((x) => x.pattern === ".env");
52
- expect(env?.action).toBe("track");
53
- expect(env?.source).toBe("project");
54
- expect(r.conflicts.find((c) => c.pattern === ".env")?.winner).toBe("project");
55
- });
54
+ const r = await resolveGitignore(projectDir, { homeDir });
55
+ const env = r.rules.find((x) => x.pattern === ".env");
56
+ expect(env?.action).toBe("track");
57
+ expect(env?.source).toBe("project");
58
+ expect(r.conflicts.find((c) => c.pattern === ".env")?.winner).toBe(
59
+ "project",
60
+ );
61
+ });
56
62
 
57
- it("global tier overrides built-in", async () => {
58
- await writeFile(
59
- join(homeDir, ".claude", "gitignore.json"),
60
- JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
61
- );
62
- const r = await resolveGitignore(projectDir, { homeDir });
63
- const rule = r.rules.find((x) => x.pattern === ".claude/settings.local.json");
64
- expect(rule?.action).toBe("track");
65
- expect(rule?.source).toBe("global");
66
- });
63
+ it("global tier overrides built-in", async () => {
64
+ await writeFile(
65
+ join(homeDir, ".claude", "gitignore.json"),
66
+ JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
67
+ );
68
+ const r = await resolveGitignore(projectDir, { homeDir });
69
+ const rule = r.rules.find(
70
+ (x) => x.pattern === ".claude/settings.local.json",
71
+ );
72
+ expect(rule?.action).toBe("track");
73
+ expect(rule?.source).toBe("global");
74
+ });
67
75
 
68
- it("expands `extends: ['common-dev']` into the tier's rules", async () => {
69
- await writeFile(
70
- join(projectDir, ".claude", "gitignore.json"),
71
- JSON.stringify({ ignore: [], track: [], extends: ["common-dev"] }),
72
- );
73
- const r = await resolveGitignore(projectDir, { homeDir });
74
- const ignored = r.rules.filter((x) => x.action === "ignore").map((x) => x.pattern);
75
- expect(ignored).toContain("node_modules/");
76
- expect(ignored).toContain(".env");
77
- expect(ignored).toContain(".DS_Store");
78
- // Template rules carry the project source since they merged into project tier
79
- const nodeMods = r.rules.find((x) => x.pattern === "node_modules/");
80
- expect(nodeMods?.source).toBe("project");
81
- });
76
+ it("expands `extends: ['common-dev']` into the tier's rules", async () => {
77
+ await writeFile(
78
+ join(projectDir, ".claude", "gitignore.json"),
79
+ JSON.stringify({ ignore: [], track: [], extends: ["common-dev"] }),
80
+ );
81
+ const r = await resolveGitignore(projectDir, { homeDir });
82
+ const ignored = r.rules
83
+ .filter((x) => x.action === "ignore")
84
+ .map((x) => x.pattern);
85
+ expect(ignored).toContain("node_modules/");
86
+ expect(ignored).toContain(".env");
87
+ expect(ignored).toContain(".DS_Store");
88
+ // Template rules carry the project source since they merged into project tier
89
+ const nodeMods = r.rules.find((x) => x.pattern === "node_modules/");
90
+ expect(nodeMods?.source).toBe("project");
91
+ });
82
92
 
83
- it("logs and skips unknown templates", async () => {
84
- await writeFile(
85
- join(projectDir, ".claude", "gitignore.json"),
86
- JSON.stringify({ ignore: [], track: [], extends: ["does-not-exist"] }),
87
- );
88
- const messages: string[] = [];
89
- const r = await resolveGitignore(projectDir, { homeDir, logger: (m) => messages.push(m) });
90
- expect(messages.some((m) => m.includes("does-not-exist"))).toBe(true);
91
- // Should still return built-in defaults
92
- expect(r.rules.length).toBeGreaterThan(0);
93
- });
93
+ it("logs and skips unknown templates", async () => {
94
+ await writeFile(
95
+ join(projectDir, ".claude", "gitignore.json"),
96
+ JSON.stringify({ ignore: [], track: [], extends: ["does-not-exist"] }),
97
+ );
98
+ const messages: string[] = [];
99
+ const r = await resolveGitignore(projectDir, {
100
+ homeDir,
101
+ logger: (m) => messages.push(m),
102
+ });
103
+ expect(messages.some((m) => m.includes("does-not-exist"))).toBe(true);
104
+ // Should still return built-in defaults
105
+ expect(r.rules.length).toBeGreaterThan(0);
106
+ });
94
107
 
95
- it("throws on intra-tier conflict", async () => {
96
- await writeFile(
97
- join(projectDir, ".claude", "gitignore.json"),
98
- JSON.stringify({ ignore: ["foo"], track: ["foo"] }),
99
- );
100
- await expect(resolveGitignore(projectDir, { homeDir })).rejects.toThrow(
101
- /Intra-tier conflict/,
102
- );
103
- });
108
+ it("throws on intra-tier conflict", async () => {
109
+ await writeFile(
110
+ join(projectDir, ".claude", "gitignore.json"),
111
+ JSON.stringify({ ignore: ["foo"], track: ["foo"] }),
112
+ );
113
+ await expect(resolveGitignore(projectDir, { homeDir })).rejects.toThrow(
114
+ /Intra-tier conflict/,
115
+ );
116
+ });
104
117
 
105
- it("ignores malformed manifest JSON without throwing", async () => {
106
- await writeFile(join(projectDir, ".claude", "gitignore.json"), "{not json");
107
- const messages: string[] = [];
108
- const r = await resolveGitignore(projectDir, { homeDir, logger: (m) => messages.push(m) });
109
- expect(messages.some((m) => /failed to read/.test(m))).toBe(true);
110
- // Falls back to built-in
111
- expect(r.rules.find((x) => x.pattern === ".mnemex/")?.source).toBe("builtin");
112
- });
118
+ it("ignores malformed manifest JSON without throwing", async () => {
119
+ await writeFile(join(projectDir, ".claude", "gitignore.json"), "{not json");
120
+ const messages: string[] = [];
121
+ const r = await resolveGitignore(projectDir, {
122
+ homeDir,
123
+ logger: (m) => messages.push(m),
124
+ });
125
+ expect(messages.some((m) => /failed to read/.test(m))).toBe(true);
126
+ // Falls back to built-in
127
+ expect(r.rules.find((x) => x.pattern === ".mnemex/")?.source).toBe(
128
+ "builtin",
129
+ );
130
+ });
113
131
 
114
- it("ignores manifest with wrong shape without throwing", async () => {
115
- await writeFile(
116
- join(projectDir, ".claude", "gitignore.json"),
117
- JSON.stringify({ ignore: "not-array" }),
118
- );
119
- const messages: string[] = [];
120
- const r = await resolveGitignore(projectDir, { homeDir, logger: (m) => messages.push(m) });
121
- expect(messages.some((m) => /not a valid manifest/.test(m))).toBe(true);
122
- expect(r.rules.length).toBeGreaterThan(0);
123
- });
132
+ it("ignores manifest with wrong shape without throwing", async () => {
133
+ await writeFile(
134
+ join(projectDir, ".claude", "gitignore.json"),
135
+ JSON.stringify({ ignore: "not-array" }),
136
+ );
137
+ const messages: string[] = [];
138
+ const r = await resolveGitignore(projectDir, {
139
+ homeDir,
140
+ logger: (m) => messages.push(m),
141
+ });
142
+ expect(messages.some((m) => /not a valid manifest/.test(m))).toBe(true);
143
+ expect(r.rules.length).toBeGreaterThan(0);
144
+ });
124
145
 
125
- it("dedupes patterns within a tier", async () => {
126
- await writeFile(
127
- join(projectDir, ".claude", "gitignore.json"),
128
- JSON.stringify({ ignore: ["foo", "foo", "bar"], track: [] }),
129
- );
130
- const r = await resolveGitignore(projectDir, { homeDir });
131
- const fooRules = r.rules.filter((x) => x.pattern === "foo");
132
- expect(fooRules.length).toBe(1);
133
- });
146
+ it("dedupes patterns within a tier", async () => {
147
+ await writeFile(
148
+ join(projectDir, ".claude", "gitignore.json"),
149
+ JSON.stringify({ ignore: ["foo", "foo", "bar"], track: [] }),
150
+ );
151
+ const r = await resolveGitignore(projectDir, { homeDir });
152
+ const fooRules = r.rules.filter((x) => x.pattern === "foo");
153
+ expect(fooRules.length).toBe(1);
154
+ });
134
155
 
135
- it("does not mark same-action overrides as conflicts", async () => {
136
- await writeFile(
137
- join(homeDir, ".claude", "gitignore.json"),
138
- JSON.stringify({ ignore: [".env"], track: [] }),
139
- );
140
- await writeFile(
141
- join(projectDir, ".claude", "gitignore.json"),
142
- JSON.stringify({ ignore: [".env"], track: [] }),
143
- );
144
- const r = await resolveGitignore(projectDir, { homeDir });
145
- expect(r.conflicts.find((c) => c.pattern === ".env")).toBeUndefined();
146
- });
156
+ it("does not mark same-action overrides as conflicts", async () => {
157
+ await writeFile(
158
+ join(homeDir, ".claude", "gitignore.json"),
159
+ JSON.stringify({ ignore: [".env"], track: [] }),
160
+ );
161
+ await writeFile(
162
+ join(projectDir, ".claude", "gitignore.json"),
163
+ JSON.stringify({ ignore: [".env"], track: [] }),
164
+ );
165
+ const r = await resolveGitignore(projectDir, { homeDir });
166
+ expect(r.conflicts.find((c) => c.pattern === ".env")).toBeUndefined();
167
+ });
147
168
 
148
- it("records conflict losers across multiple tiers", async () => {
149
- // built-in ignores .claude/settings.local.json; global tracks it; project ignores it again
150
- await writeFile(
151
- join(homeDir, ".claude", "gitignore.json"),
152
- JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
153
- );
154
- await writeFile(
155
- join(projectDir, ".claude", "gitignore.json"),
156
- JSON.stringify({ ignore: [".claude/settings.local.json"], track: [] }),
157
- );
158
- const r = await resolveGitignore(projectDir, { homeDir });
159
- const winner = r.rules.find((x) => x.pattern === ".claude/settings.local.json");
160
- expect(winner?.action).toBe("ignore");
161
- expect(winner?.source).toBe("project");
162
- const conflict = r.conflicts.find((c) => c.pattern === ".claude/settings.local.json");
163
- expect(conflict?.winner).toBe("project");
164
- expect(conflict?.losers.some((l) => l.tier === "global" && l.action === "track")).toBe(true);
165
- });
169
+ it("records conflict losers across multiple tiers", async () => {
170
+ // built-in ignores .claude/settings.local.json; global tracks it; project ignores it again
171
+ await writeFile(
172
+ join(homeDir, ".claude", "gitignore.json"),
173
+ JSON.stringify({ ignore: [], track: [".claude/settings.local.json"] }),
174
+ );
175
+ await writeFile(
176
+ join(projectDir, ".claude", "gitignore.json"),
177
+ JSON.stringify({ ignore: [".claude/settings.local.json"], track: [] }),
178
+ );
179
+ const r = await resolveGitignore(projectDir, { homeDir });
180
+ const winner = r.rules.find(
181
+ (x) => x.pattern === ".claude/settings.local.json",
182
+ );
183
+ expect(winner?.action).toBe("ignore");
184
+ expect(winner?.source).toBe("project");
185
+ const conflict = r.conflicts.find(
186
+ (c) => c.pattern === ".claude/settings.local.json",
187
+ );
188
+ expect(conflict?.winner).toBe("project");
189
+ expect(
190
+ conflict?.losers.some((l) => l.tier === "global" && l.action === "track"),
191
+ ).toBe(true);
192
+ });
166
193
  });