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
@@ -13,178 +13,196 @@
13
13
  * overrides to track."
14
14
  */
15
15
 
16
+ import { existsSync } from "node:fs";
16
17
  import { readFile } from "node:fs/promises";
17
- import { join } from "node:path";
18
18
  import { homedir } from "node:os";
19
- import { existsSync } from "node:fs";
20
- import type {
21
- GitignoreManifest,
22
- ResolvedManifest,
23
- ResolvedRule,
24
- ResolvedConflict,
25
- Tier,
26
- RuleAction,
27
- } from "../types/gitignore.js";
19
+ import { join } from "node:path";
28
20
  import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults.js";
29
21
  import { BUILTIN_TEMPLATES } from "../data/gitignore-templates.js";
22
+ import type {
23
+ GitignoreManifest,
24
+ ResolvedConflict,
25
+ ResolvedManifest,
26
+ ResolvedRule,
27
+ RuleAction,
28
+ Tier,
29
+ } from "../types/gitignore.js";
30
30
 
31
31
  export interface ResolveOptions {
32
- /** Override $HOME (test seam). */
33
- homeDir?: string;
34
- /** Optional logger for non-fatal warnings (malformed JSON, missing template). */
35
- logger?: (msg: string) => void;
32
+ /** Override $HOME (test seam). */
33
+ homeDir?: string;
34
+ /** Optional logger for non-fatal warnings (malformed JSON, missing template). */
35
+ logger?: (msg: string) => void;
36
36
  }
37
37
 
38
38
  const TIER_ORDER: Tier[] = ["builtin", "global", "project"];
39
39
 
40
40
  export async function resolveGitignore(
41
- projectCwd: string,
42
- opts: ResolveOptions = {},
41
+ projectCwd: string,
42
+ opts: ResolveOptions = {},
43
43
  ): Promise<ResolvedManifest> {
44
- const home = opts.homeDir ?? homedir();
45
- const log = opts.logger ?? (() => {});
46
-
47
- const tierManifests = new Map<Tier, GitignoreManifest>();
48
-
49
- // 1. Built-in (always present)
50
- tierManifests.set("builtin", BUILTIN_DEFAULTS);
51
-
52
- // 2. Global ~/.claude/gitignore.json
53
- const globalPath = join(home, ".claude", "gitignore.json");
54
- const globalManifest = await readManifest(globalPath, log);
55
- if (globalManifest) tierManifests.set("global", globalManifest);
56
-
57
- // 3. Project .claude/gitignore.json
58
- const projectPath = join(projectCwd, ".claude", "gitignore.json");
59
- const projectManifest = await readManifest(projectPath, log);
60
- if (projectManifest) tierManifests.set("project", projectManifest);
61
-
62
- // Apply `extends` for each tier (templates count as part of their tier)
63
- for (const tier of TIER_ORDER) {
64
- const m = tierManifests.get(tier);
65
- if (!m?.extends?.length) continue;
66
- const merged: GitignoreManifest = {
67
- ignore: [...m.ignore],
68
- track: [...m.track],
69
- };
70
- for (const tplName of m.extends) {
71
- const tpl = BUILTIN_TEMPLATES[tplName];
72
- if (!tpl) {
73
- log(`[gitignore] unknown template "${tplName}" in ${tier} manifest`);
74
- continue;
75
- }
76
- merged.ignore.push(...tpl.ignore);
77
- merged.track.push(...tpl.track);
78
- }
79
- tierManifests.set(tier, merged);
80
- }
81
-
82
- // Validate intra-tier consistency
83
- for (const [tier, m] of tierManifests.entries()) {
84
- const ig = new Set(m.ignore);
85
- for (const t of m.track) {
86
- if (ig.has(t)) {
87
- throw new Error(
88
- `Intra-tier conflict in ${tier} manifest: "${t}" is in both ignore and track`,
89
- );
90
- }
91
- }
92
- }
93
-
94
- // Merge across tiers — last-wins, track conflicts
95
- const ruleByPattern = new Map<
96
- string,
97
- { action: RuleAction; source: Tier; history: Array<{ tier: Tier; action: RuleAction }> }
98
- >();
99
-
100
- for (const tier of TIER_ORDER) {
101
- const m = tierManifests.get(tier);
102
- if (!m) continue;
103
- for (const p of dedupe(m.ignore)) {
104
- pushRule(ruleByPattern, p, "ignore", tier);
105
- }
106
- for (const p of dedupe(m.track)) {
107
- pushRule(ruleByPattern, p, "track", tier);
108
- }
109
- }
110
-
111
- const rules: ResolvedRule[] = [];
112
- const conflicts: ResolvedConflict[] = [];
113
-
114
- for (const [pattern, entry] of ruleByPattern.entries()) {
115
- rules.push({ pattern, action: entry.action, source: entry.source });
116
-
117
- const losers = entry.history.filter(
118
- (h) => h.tier !== entry.source || h.action !== entry.action,
119
- );
120
- if (losers.length > 0 && hasActionDisagreement(entry.history)) {
121
- conflicts.push({
122
- pattern,
123
- winner: entry.source,
124
- losers: losers.filter((l) => l.action !== entry.action),
125
- });
126
- }
127
- }
128
-
129
- return { rules, conflicts };
44
+ const home = opts.homeDir ?? homedir();
45
+ const log = opts.logger ?? (() => {});
46
+
47
+ const tierManifests = new Map<Tier, GitignoreManifest>();
48
+
49
+ // 1. Built-in (always present)
50
+ tierManifests.set("builtin", BUILTIN_DEFAULTS);
51
+
52
+ // 2. Global ~/.claude/gitignore.json
53
+ const globalPath = join(home, ".claude", "gitignore.json");
54
+ const globalManifest = await readManifest(globalPath, log);
55
+ if (globalManifest) tierManifests.set("global", globalManifest);
56
+
57
+ // 3. Project .claude/gitignore.json
58
+ const projectPath = join(projectCwd, ".claude", "gitignore.json");
59
+ const projectManifest = await readManifest(projectPath, log);
60
+ if (projectManifest) tierManifests.set("project", projectManifest);
61
+
62
+ // Apply `extends` for each tier (templates count as part of their tier)
63
+ for (const tier of TIER_ORDER) {
64
+ const m = tierManifests.get(tier);
65
+ if (!m?.extends?.length) continue;
66
+ const merged: GitignoreManifest = {
67
+ ignore: [...m.ignore],
68
+ track: [...m.track],
69
+ };
70
+ for (const tplName of m.extends) {
71
+ const tpl = BUILTIN_TEMPLATES[tplName];
72
+ if (!tpl) {
73
+ log(`[gitignore] unknown template "${tplName}" in ${tier} manifest`);
74
+ continue;
75
+ }
76
+ merged.ignore.push(...tpl.ignore);
77
+ merged.track.push(...tpl.track);
78
+ }
79
+ tierManifests.set(tier, merged);
80
+ }
81
+
82
+ // Validate intra-tier consistency
83
+ for (const [tier, m] of tierManifests.entries()) {
84
+ const ig = new Set(m.ignore);
85
+ for (const t of m.track) {
86
+ if (ig.has(t)) {
87
+ throw new Error(
88
+ `Intra-tier conflict in ${tier} manifest: "${t}" is in both ignore and track`,
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ // Merge across tiers — last-wins, track conflicts
95
+ const ruleByPattern = new Map<
96
+ string,
97
+ {
98
+ action: RuleAction;
99
+ source: Tier;
100
+ history: Array<{ tier: Tier; action: RuleAction }>;
101
+ }
102
+ >();
103
+
104
+ for (const tier of TIER_ORDER) {
105
+ const m = tierManifests.get(tier);
106
+ if (!m) continue;
107
+ for (const p of dedupe(m.ignore)) {
108
+ pushRule(ruleByPattern, p, "ignore", tier);
109
+ }
110
+ for (const p of dedupe(m.track)) {
111
+ pushRule(ruleByPattern, p, "track", tier);
112
+ }
113
+ }
114
+
115
+ const rules: ResolvedRule[] = [];
116
+ const conflicts: ResolvedConflict[] = [];
117
+
118
+ for (const [pattern, entry] of ruleByPattern.entries()) {
119
+ rules.push({ pattern, action: entry.action, source: entry.source });
120
+
121
+ const losers = entry.history.filter(
122
+ (h) => h.tier !== entry.source || h.action !== entry.action,
123
+ );
124
+ if (losers.length > 0 && hasActionDisagreement(entry.history)) {
125
+ conflicts.push({
126
+ pattern,
127
+ winner: entry.source,
128
+ losers: losers.filter((l) => l.action !== entry.action),
129
+ });
130
+ }
131
+ }
132
+
133
+ return { rules, conflicts };
130
134
  }
131
135
 
132
136
  function pushRule(
133
- map: Map<
134
- string,
135
- { action: RuleAction; source: Tier; history: Array<{ tier: Tier; action: RuleAction }> }
136
- >,
137
- pattern: string,
138
- action: RuleAction,
139
- tier: Tier,
137
+ map: Map<
138
+ string,
139
+ {
140
+ action: RuleAction;
141
+ source: Tier;
142
+ history: Array<{ tier: Tier; action: RuleAction }>;
143
+ }
144
+ >,
145
+ pattern: string,
146
+ action: RuleAction,
147
+ tier: Tier,
140
148
  ): void {
141
- const existing = map.get(pattern);
142
- if (existing) {
143
- existing.history.push({ tier, action });
144
- existing.action = action;
145
- existing.source = tier;
146
- } else {
147
- map.set(pattern, { action, source: tier, history: [{ tier, action }] });
148
- }
149
+ const existing = map.get(pattern);
150
+ if (existing) {
151
+ existing.history.push({ tier, action });
152
+ existing.action = action;
153
+ existing.source = tier;
154
+ } else {
155
+ map.set(pattern, { action, source: tier, history: [{ tier, action }] });
156
+ }
149
157
  }
150
158
 
151
159
  function hasActionDisagreement(
152
- history: Array<{ tier: Tier; action: RuleAction }>,
160
+ history: Array<{ tier: Tier; action: RuleAction }>,
153
161
  ): boolean {
154
- const actions = new Set(history.map((h) => h.action));
155
- return actions.size > 1;
162
+ const actions = new Set(history.map((h) => h.action));
163
+ return actions.size > 1;
156
164
  }
157
165
 
158
166
  function dedupe(xs: string[]): string[] {
159
- return Array.from(new Set(xs));
167
+ return Array.from(new Set(xs));
160
168
  }
161
169
 
162
170
  async function readManifest(
163
- path: string,
164
- log: (msg: string) => void,
171
+ path: string,
172
+ log: (msg: string) => void,
165
173
  ): Promise<GitignoreManifest | null> {
166
- if (!existsSync(path)) return null;
167
- try {
168
- const raw = await readFile(path, "utf8");
169
- const parsed = JSON.parse(raw);
170
- if (!isManifest(parsed)) {
171
- log(`[gitignore] ${path} is not a valid manifest (expected { ignore: string[], track: string[] })`);
172
- return null;
173
- }
174
- return parsed;
175
- } catch (err) {
176
- log(`[gitignore] failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
177
- return null;
178
- }
174
+ if (!existsSync(path)) return null;
175
+ try {
176
+ const raw = await readFile(path, "utf8");
177
+ const parsed = JSON.parse(raw);
178
+ if (!isManifest(parsed)) {
179
+ log(
180
+ `[gitignore] ${path} is not a valid manifest (expected { ignore: string[], track: string[] })`,
181
+ );
182
+ return null;
183
+ }
184
+ return parsed;
185
+ } catch (err) {
186
+ log(
187
+ `[gitignore] failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`,
188
+ );
189
+ return null;
190
+ }
179
191
  }
180
192
 
181
193
  function isManifest(x: unknown): x is GitignoreManifest {
182
- if (!x || typeof x !== "object") return false;
183
- const m = x as Record<string, unknown>;
184
- if (!Array.isArray(m.ignore) || !m.ignore.every((p) => typeof p === "string")) return false;
185
- if (!Array.isArray(m.track) || !m.track.every((p) => typeof p === "string")) return false;
186
- if (m.extends !== undefined) {
187
- if (!Array.isArray(m.extends) || !m.extends.every((p) => typeof p === "string")) return false;
188
- }
189
- return true;
194
+ if (!x || typeof x !== "object") return false;
195
+ const m = x as Record<string, unknown>;
196
+ if (!Array.isArray(m.ignore) || !m.ignore.every((p) => typeof p === "string"))
197
+ return false;
198
+ if (!Array.isArray(m.track) || !m.track.every((p) => typeof p === "string"))
199
+ return false;
200
+ if (m.extends !== undefined) {
201
+ if (
202
+ !Array.isArray(m.extends) ||
203
+ !m.extends.every((p) => typeof p === "string")
204
+ )
205
+ return false;
206
+ }
207
+ return true;
190
208
  }
@@ -5,80 +5,83 @@
5
5
  * built-in template).
6
6
  */
7
7
 
8
- import { readFile, writeFile, mkdir } from "node:fs/promises";
9
8
  import { existsSync } from "node:fs";
10
- import { join, dirname } from "node:path";
11
- import { resolveGitignore } from "./gitignore-resolver.js";
12
- import { detectViolations } from "./gitignore-detector.js";
13
- import {
14
- applySafeFixes as coreApplySafeFixes,
15
- applyDestructiveFix as coreApplyDestructiveFix,
16
- type SafeFixResult,
17
- type DestructiveFixResult,
18
- } from "./gitignore-fixer.js";
9
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
19
11
  import { BUILTIN_TEMPLATES } from "../data/gitignore-templates.js";
20
12
  import type {
21
- GitignoreManifest,
22
- ResolvedManifest,
23
- Violation,
13
+ GitignoreManifest,
14
+ ResolvedManifest,
15
+ Violation,
24
16
  } from "../types/gitignore.js";
17
+ import { detectViolations } from "./gitignore-detector.js";
18
+ import {
19
+ type DestructiveFixResult,
20
+ type SafeFixResult,
21
+ applyDestructiveFix as coreApplyDestructiveFix,
22
+ applySafeFixes as coreApplySafeFixes,
23
+ } from "./gitignore-fixer.js";
24
+ import { resolveGitignore } from "./gitignore-resolver.js";
25
25
 
26
26
  export interface GitignoreState {
27
- resolved: ResolvedManifest;
28
- violations: Violation[];
29
- /** Logger lines from resolution (malformed JSON, missing template, etc). */
30
- warnings: string[];
27
+ resolved: ResolvedManifest;
28
+ violations: Violation[];
29
+ /** Logger lines from resolution (malformed JSON, missing template, etc). */
30
+ warnings: string[];
31
31
  }
32
32
 
33
33
  export async function loadGitignoreState(cwd: string): Promise<GitignoreState> {
34
- const warnings: string[] = [];
35
- const resolved = await resolveGitignore(cwd, {
36
- logger: (m) => warnings.push(m),
37
- });
38
- const violations = await detectViolations(cwd, resolved);
39
- return { resolved, violations, warnings };
34
+ const warnings: string[] = [];
35
+ const resolved = await resolveGitignore(cwd, {
36
+ logger: (m) => warnings.push(m),
37
+ });
38
+ const violations = await detectViolations(cwd, resolved);
39
+ return { resolved, violations, warnings };
40
40
  }
41
41
 
42
42
  export async function applyAllSafeFixes(
43
- cwd: string,
44
- violations: Violation[],
43
+ cwd: string,
44
+ violations: Violation[],
45
45
  ): Promise<SafeFixResult> {
46
- return coreApplySafeFixes(cwd, violations);
46
+ return coreApplySafeFixes(cwd, violations);
47
47
  }
48
48
 
49
49
  export async function applyDestructiveFix(
50
- cwd: string,
51
- violation: Violation,
52
- rulePattern?: string,
50
+ cwd: string,
51
+ violation: Violation,
52
+ rulePattern?: string,
53
53
  ): Promise<DestructiveFixResult> {
54
- return coreApplyDestructiveFix(cwd, violation, rulePattern);
54
+ return coreApplyDestructiveFix(cwd, violation, rulePattern);
55
55
  }
56
56
 
57
57
  export async function ensureProjectManifestForEdit(
58
- cwd: string,
59
- resolved: ResolvedManifest,
58
+ cwd: string,
59
+ resolved: ResolvedManifest,
60
60
  ): Promise<string> {
61
- const projectManifestPath = join(cwd, ".claude", "gitignore.json");
62
- if (existsSync(projectManifestPath)) {
63
- const existing = await readFile(projectManifestPath, "utf8");
64
- if (existing.trim().length > 0) return projectManifestPath;
65
- }
66
-
67
- const manifest: GitignoreManifest = { ignore: [], track: [] };
68
- for (const rule of resolved.rules) {
69
- if (rule.action === "ignore") manifest.ignore.push(rule.pattern);
70
- else manifest.track.push(rule.pattern);
71
- }
72
-
73
- await mkdir(dirname(projectManifestPath), { recursive: true });
74
- await writeFile(projectManifestPath, JSON.stringify(manifest, null, 2) + "\n");
75
- return projectManifestPath;
61
+ const projectManifestPath = join(cwd, ".claude", "gitignore.json");
62
+ if (existsSync(projectManifestPath)) {
63
+ const existing = await readFile(projectManifestPath, "utf8");
64
+ if (existing.trim().length > 0) return projectManifestPath;
65
+ }
66
+
67
+ const manifest: GitignoreManifest = { ignore: [], track: [] };
68
+ for (const rule of resolved.rules) {
69
+ if (rule.action === "ignore") manifest.ignore.push(rule.pattern);
70
+ else manifest.track.push(rule.pattern);
71
+ }
72
+
73
+ await mkdir(dirname(projectManifestPath), { recursive: true });
74
+ await writeFile(
75
+ projectManifestPath,
76
+ `${JSON.stringify(manifest, null, 2)}\n`,
77
+ );
78
+ return projectManifestPath;
76
79
  }
77
80
 
78
81
  export interface ApplyTemplateResult {
79
- appliedTo: string;
80
- added: { ignore: string[]; track: string[] };
81
- conflicts: string[];
82
+ appliedTo: string;
83
+ added: { ignore: string[]; track: string[] };
84
+ conflicts: string[];
82
85
  }
83
86
 
84
87
  /**
@@ -87,78 +90,78 @@ export interface ApplyTemplateResult {
87
90
  * skipped and reported back to the caller.
88
91
  */
89
92
  export async function applyTemplate(
90
- cwd: string,
91
- templateName: string,
93
+ cwd: string,
94
+ templateName: string,
92
95
  ): Promise<ApplyTemplateResult> {
93
- const tpl = BUILTIN_TEMPLATES[templateName];
94
- if (!tpl) throw new Error(`Unknown template: ${templateName}`);
95
-
96
- const projectManifestPath = join(cwd, ".claude", "gitignore.json");
97
- const existing = await readManifestOrEmpty(projectManifestPath);
98
-
99
- const existingIgnore = new Set(existing.ignore);
100
- const existingTrack = new Set(existing.track);
101
-
102
- const conflicts: string[] = [];
103
- const newIgnore: string[] = [];
104
- const newTrack: string[] = [];
105
-
106
- for (const p of tpl.ignore) {
107
- if (existingTrack.has(p)) {
108
- conflicts.push(p);
109
- continue;
110
- }
111
- if (!existingIgnore.has(p)) {
112
- newIgnore.push(p);
113
- existingIgnore.add(p);
114
- }
115
- }
116
- for (const p of tpl.track) {
117
- if (existingIgnore.has(p)) {
118
- conflicts.push(p);
119
- continue;
120
- }
121
- if (!existingTrack.has(p)) {
122
- newTrack.push(p);
123
- existingTrack.add(p);
124
- }
125
- }
126
-
127
- const merged: GitignoreManifest = {
128
- ignore: [...existing.ignore, ...newIgnore],
129
- track: [...existing.track, ...newTrack],
130
- extends: existing.extends,
131
- };
132
-
133
- await mkdir(dirname(projectManifestPath), { recursive: true });
134
- await writeFile(projectManifestPath, JSON.stringify(merged, null, 2) + "\n");
135
-
136
- return {
137
- appliedTo: projectManifestPath,
138
- added: { ignore: newIgnore, track: newTrack },
139
- conflicts,
140
- };
96
+ const tpl = BUILTIN_TEMPLATES[templateName];
97
+ if (!tpl) throw new Error(`Unknown template: ${templateName}`);
98
+
99
+ const projectManifestPath = join(cwd, ".claude", "gitignore.json");
100
+ const existing = await readManifestOrEmpty(projectManifestPath);
101
+
102
+ const existingIgnore = new Set(existing.ignore);
103
+ const existingTrack = new Set(existing.track);
104
+
105
+ const conflicts: string[] = [];
106
+ const newIgnore: string[] = [];
107
+ const newTrack: string[] = [];
108
+
109
+ for (const p of tpl.ignore) {
110
+ if (existingTrack.has(p)) {
111
+ conflicts.push(p);
112
+ continue;
113
+ }
114
+ if (!existingIgnore.has(p)) {
115
+ newIgnore.push(p);
116
+ existingIgnore.add(p);
117
+ }
118
+ }
119
+ for (const p of tpl.track) {
120
+ if (existingIgnore.has(p)) {
121
+ conflicts.push(p);
122
+ continue;
123
+ }
124
+ if (!existingTrack.has(p)) {
125
+ newTrack.push(p);
126
+ existingTrack.add(p);
127
+ }
128
+ }
129
+
130
+ const merged: GitignoreManifest = {
131
+ ignore: [...existing.ignore, ...newIgnore],
132
+ track: [...existing.track, ...newTrack],
133
+ extends: existing.extends,
134
+ };
135
+
136
+ await mkdir(dirname(projectManifestPath), { recursive: true });
137
+ await writeFile(projectManifestPath, `${JSON.stringify(merged, null, 2)}\n`);
138
+
139
+ return {
140
+ appliedTo: projectManifestPath,
141
+ added: { ignore: newIgnore, track: newTrack },
142
+ conflicts,
143
+ };
141
144
  }
142
145
 
143
146
  async function readManifestOrEmpty(path: string): Promise<GitignoreManifest> {
144
- if (!existsSync(path)) return { ignore: [], track: [] };
145
- try {
146
- const raw = await readFile(path, "utf8");
147
- const parsed = JSON.parse(raw);
148
- if (!isManifest(parsed)) return { ignore: [], track: [] };
149
- return parsed;
150
- } catch {
151
- return { ignore: [], track: [] };
152
- }
147
+ if (!existsSync(path)) return { ignore: [], track: [] };
148
+ try {
149
+ const raw = await readFile(path, "utf8");
150
+ const parsed = JSON.parse(raw);
151
+ if (!isManifest(parsed)) return { ignore: [], track: [] };
152
+ return parsed;
153
+ } catch {
154
+ return { ignore: [], track: [] };
155
+ }
153
156
  }
154
157
 
155
158
  function isManifest(x: unknown): x is GitignoreManifest {
156
- if (!x || typeof x !== "object") return false;
157
- const m = x as Record<string, unknown>;
158
- return (
159
- Array.isArray(m.ignore) &&
160
- m.ignore.every((p) => typeof p === "string") &&
161
- Array.isArray(m.track) &&
162
- m.track.every((p) => typeof p === "string")
163
- );
159
+ if (!x || typeof x !== "object") return false;
160
+ const m = x as Record<string, unknown>;
161
+ return (
162
+ Array.isArray(m.ignore) &&
163
+ m.ignore.every((p) => typeof p === "string") &&
164
+ Array.isArray(m.track) &&
165
+ m.track.every((p) => typeof p === "string")
166
+ );
164
167
  }
@@ -24,8 +24,8 @@ export interface VersionDrift {
24
24
  /** Highest valid semver in a list, or the raw first value if none parse. */
25
25
  function maxVersion(versions: string[]): string | null {
26
26
  const valid = versions.filter((v) => semver.valid(v));
27
- if (valid.length > 0) return valid.sort(semver.rcompare)[0]!;
28
- return versions[0] ?? null;
27
+ const highest = valid.sort(semver.rcompare)[0];
28
+ return highest ?? versions[0] ?? null;
29
29
  }
30
30
 
31
31
  /**
@@ -86,8 +86,8 @@ export function summarizeClosure(closure: ResolvedClosure): string[] {
86
86
  const lines: string[] = [];
87
87
  const mps = Object.keys(closure.marketplaces);
88
88
  if (mps.length) lines.push(`marketplaces ${mps.join(", ")}`);
89
- const plugins = Object.entries(closure.plugins).map(
90
- ([id, v]) => (v === "latest" ? id : `${id}@${v}`),
89
+ const plugins = Object.entries(closure.plugins).map(([id, v]) =>
90
+ v === "latest" ? id : `${id}@${v}`,
91
91
  );
92
92
  if (plugins.length) lines.push(`plugins ${plugins.join(", ")}`);
93
93
  const mcps = Object.keys(closure.mcpServers);
@@ -7,8 +7,8 @@
7
7
  * structural subset of a v2 manifest entry, so migration is a version bump.
8
8
  */
9
9
 
10
- import fs from "fs-extra";
11
10
  import path from "node:path";
11
+ import fs from "fs-extra";
12
12
  import type { ProfileManifest, ProfileManifestEntry } from "../types/index.js";
13
13
 
14
14
  const PROFILES_FILE = "profiles.json";