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
@@ -15,251 +15,266 @@
15
15
  * `formatViolationSummary` is a pure helper for prerun warning + TUI.
16
16
  */
17
17
 
18
- import { readFile, writeFile, appendFile } from "node:fs/promises";
18
+ import { spawn } from "node:child_process";
19
19
  import { existsSync } from "node:fs";
20
+ import { appendFile, readFile, writeFile } from "node:fs/promises";
20
21
  import { join } from "node:path";
21
- import { spawn } from "node:child_process";
22
22
  import { getGitignoreRuleReason } from "../data/gitignore-reasons.js";
23
23
  import type { Violation } from "../types/gitignore.js";
24
24
 
25
25
  const CLAUDEUP_HEADER = "# Added by claudeup";
26
26
 
27
27
  export interface SafeFixResult {
28
- appended: string[];
29
- alreadyPresent: string[];
28
+ appended: string[];
29
+ alreadyPresent: string[];
30
30
  }
31
31
 
32
32
  export async function applySafeFixes(
33
- cwd: string,
34
- violations: Violation[],
33
+ cwd: string,
34
+ violations: Violation[],
35
35
  ): Promise<SafeFixResult> {
36
- const safe = violations.filter(
37
- (v) => v.severity === "safe" && v.kind === "missing-from-gitignore",
38
- );
39
- if (safe.length === 0) {
40
- return { appended: [], alreadyPresent: [] };
41
- }
36
+ const safe = violations.filter(
37
+ (v) => v.severity === "safe" && v.kind === "missing-from-gitignore",
38
+ );
39
+ if (safe.length === 0) {
40
+ return { appended: [], alreadyPresent: [] };
41
+ }
42
42
 
43
- const gitignorePath = join(cwd, ".gitignore");
44
- const existing = existsSync(gitignorePath)
45
- ? await readFile(gitignorePath, "utf8")
46
- : "";
47
- const lines = new Set(
48
- existing.split("\n").map((l) => l.trim()).filter(Boolean),
49
- );
43
+ const gitignorePath = join(cwd, ".gitignore");
44
+ const existing = existsSync(gitignorePath)
45
+ ? await readFile(gitignorePath, "utf8")
46
+ : "";
47
+ const lines = new Set(
48
+ existing
49
+ .split("\n")
50
+ .map((l) => l.trim())
51
+ .filter(Boolean),
52
+ );
50
53
 
51
- const appended: string[] = [];
52
- const alreadyPresent: string[] = [];
53
- for (const v of safe) {
54
- if (lines.has(v.path)) {
55
- alreadyPresent.push(v.path);
56
- } else {
57
- appended.push(v.path);
58
- lines.add(v.path);
59
- }
60
- }
54
+ const appended: string[] = [];
55
+ const alreadyPresent: string[] = [];
56
+ for (const v of safe) {
57
+ if (lines.has(v.path)) {
58
+ alreadyPresent.push(v.path);
59
+ } else {
60
+ appended.push(v.path);
61
+ lines.add(v.path);
62
+ }
63
+ }
61
64
 
62
- if (appended.length === 0) return { appended, alreadyPresent };
65
+ if (appended.length === 0) return { appended, alreadyPresent };
63
66
 
64
- const block = [
65
- "",
66
- CLAUDEUP_HEADER,
67
- ...appended.flatMap(formatGitignoreEntry),
68
- "",
69
- ].join("\n");
67
+ const block = [
68
+ "",
69
+ CLAUDEUP_HEADER,
70
+ ...appended.flatMap(formatGitignoreEntry),
71
+ "",
72
+ ].join("\n");
70
73
 
71
- if (existing.length > 0 && !existing.endsWith("\n")) {
72
- await appendFile(gitignorePath, "\n" + block);
73
- } else {
74
- await appendFile(gitignorePath, block);
75
- }
74
+ if (existing.length > 0 && !existing.endsWith("\n")) {
75
+ await appendFile(gitignorePath, `\n${block}`);
76
+ } else {
77
+ await appendFile(gitignorePath, block);
78
+ }
76
79
 
77
- return { appended, alreadyPresent };
80
+ return { appended, alreadyPresent };
78
81
  }
79
82
 
80
83
  export interface DestructiveFixResult {
81
- applied: boolean;
82
- message: string;
84
+ applied: boolean;
85
+ message: string;
83
86
  }
84
87
 
85
88
  export async function applyDestructiveFix(
86
- cwd: string,
87
- violation: Violation,
88
- rulePattern = violation.path,
89
+ cwd: string,
90
+ violation: Violation,
91
+ rulePattern = violation.path,
89
92
  ): Promise<DestructiveFixResult> {
90
- switch (violation.kind) {
91
- case "tracked-but-should-ignore": {
92
- await runGit(["rm", "--cached", "--", violation.path], cwd);
93
- // Then make sure .gitignore has the entry so it stays out
94
- await appendIfMissing(cwd, rulePattern);
95
- return {
96
- applied: true,
97
- message: `Removed ${violation.path} from index and added ${rulePattern} to .gitignore`,
98
- };
99
- }
100
- case "ignored-but-should-track": {
101
- await stripGitignoreLine(cwd, violation.path);
102
- await appendTrackExceptionsIfNeeded(cwd, violation.path);
103
- if (!existsSync(join(cwd, violation.path))) {
104
- return {
105
- applied: true,
106
- message: `Made ${violation.path} trackable; file does not exist yet`,
107
- };
108
- }
109
- try {
110
- await runGit(["add", "--", violation.path], cwd);
111
- } catch (err) {
112
- return {
113
- applied: false,
114
- message: `Removed .gitignore line but git add failed: ${
115
- err instanceof Error ? err.message : String(err)
116
- }`,
117
- };
118
- }
119
- return {
120
- applied: true,
121
- message: `Removed .gitignore line and staged ${violation.path}`,
122
- };
123
- }
124
- case "untracked-and-should-track": {
125
- await appendTrackExceptionsIfNeeded(cwd, violation.path);
126
- await runGit(["add", "--", violation.path], cwd);
127
- return { applied: true, message: `Staged ${violation.path}` };
128
- }
129
- case "missing-from-gitignore": {
130
- // Treat as a safe append for symmetry — caller can route through here too.
131
- await appendIfMissing(cwd, rulePattern);
132
- return { applied: true, message: `Added ${rulePattern} to .gitignore` };
133
- }
134
- }
93
+ switch (violation.kind) {
94
+ case "tracked-but-should-ignore": {
95
+ await runGit(["rm", "--cached", "--", violation.path], cwd);
96
+ // Then make sure .gitignore has the entry so it stays out
97
+ await appendIfMissing(cwd, rulePattern);
98
+ return {
99
+ applied: true,
100
+ message: `Removed ${violation.path} from index and added ${rulePattern} to .gitignore`,
101
+ };
102
+ }
103
+ case "ignored-but-should-track": {
104
+ await stripGitignoreLine(cwd, violation.path);
105
+ await appendTrackExceptionsIfNeeded(cwd, violation.path);
106
+ if (!existsSync(join(cwd, violation.path))) {
107
+ return {
108
+ applied: true,
109
+ message: `Made ${violation.path} trackable; file does not exist yet`,
110
+ };
111
+ }
112
+ try {
113
+ await runGit(["add", "--", violation.path], cwd);
114
+ } catch (err) {
115
+ return {
116
+ applied: false,
117
+ message: `Removed .gitignore line but git add failed: ${
118
+ err instanceof Error ? err.message : String(err)
119
+ }`,
120
+ };
121
+ }
122
+ return {
123
+ applied: true,
124
+ message: `Removed .gitignore line and staged ${violation.path}`,
125
+ };
126
+ }
127
+ case "untracked-and-should-track": {
128
+ await appendTrackExceptionsIfNeeded(cwd, violation.path);
129
+ await runGit(["add", "--", violation.path], cwd);
130
+ return { applied: true, message: `Staged ${violation.path}` };
131
+ }
132
+ case "missing-from-gitignore": {
133
+ // Treat as a safe append for symmetry — caller can route through here too.
134
+ await appendIfMissing(cwd, rulePattern);
135
+ return { applied: true, message: `Added ${rulePattern} to .gitignore` };
136
+ }
137
+ }
135
138
  }
136
139
 
137
140
  export function formatViolationSummary(violations: Violation[]): string {
138
- if (violations.length === 0) return "No gitignore issues detected.";
141
+ if (violations.length === 0) return "No gitignore issues detected.";
139
142
 
140
- const byKind = new Map<string, Violation[]>();
141
- for (const v of violations) {
142
- const arr = byKind.get(v.kind) ?? [];
143
- arr.push(v);
144
- byKind.set(v.kind, arr);
145
- }
143
+ const byKind = new Map<string, Violation[]>();
144
+ for (const v of violations) {
145
+ const arr = byKind.get(v.kind) ?? [];
146
+ arr.push(v);
147
+ byKind.set(v.kind, arr);
148
+ }
146
149
 
147
- const labels: Record<Violation["kind"], string> = {
148
- "tracked-but-should-ignore": "tracked but should be ignored",
149
- "ignored-but-should-track": "ignored but should be tracked",
150
- "untracked-and-should-track":"exist but not tracked (should be)",
151
- "missing-from-gitignore": "missing from .gitignore",
152
- };
150
+ const labels: Record<Violation["kind"], string> = {
151
+ "tracked-but-should-ignore": "tracked but should be ignored",
152
+ "ignored-but-should-track": "ignored but should be tracked",
153
+ "untracked-and-should-track": "exist but not tracked (should be)",
154
+ "missing-from-gitignore": "missing from .gitignore",
155
+ };
153
156
 
154
- const lines: string[] = [];
155
- for (const [kind, vs] of byKind.entries()) {
156
- lines.push(` ${labels[kind as Violation["kind"]]}: ${vs.length}`);
157
- for (const v of vs.slice(0, 5)) {
158
- lines.push(` - ${v.path}`);
159
- }
160
- if (vs.length > 5) lines.push(` … and ${vs.length - 5} more`);
161
- }
162
- return lines.join("\n");
157
+ const lines: string[] = [];
158
+ for (const [kind, vs] of byKind.entries()) {
159
+ lines.push(` ${labels[kind as Violation["kind"]]}: ${vs.length}`);
160
+ for (const v of vs.slice(0, 5)) {
161
+ lines.push(` - ${v.path}`);
162
+ }
163
+ if (vs.length > 5) lines.push(` … and ${vs.length - 5} more`);
164
+ }
165
+ return lines.join("\n");
163
166
  }
164
167
 
165
168
  async function appendIfMissing(cwd: string, pattern: string): Promise<void> {
166
- const gitignorePath = join(cwd, ".gitignore");
167
- const existing = existsSync(gitignorePath)
168
- ? await readFile(gitignorePath, "utf8")
169
- : "";
170
- const lines = existing.split("\n").map((l) => l.trim());
171
- if (lines.includes(pattern)) return;
172
- // Only emit the "# Added by claudeup" header once — when fixing several
173
- // entries in one pass (F), the file already has the header from the first.
174
- const needsHeader = !lines.includes(CLAUDEUP_HEADER);
175
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
176
- const header = needsHeader ? `${CLAUDEUP_HEADER}\n` : "";
177
- const block = `${prefix}${header}${formatGitignoreEntry(pattern).join("\n")}\n`;
178
- await appendFile(gitignorePath, block);
169
+ const gitignorePath = join(cwd, ".gitignore");
170
+ const existing = existsSync(gitignorePath)
171
+ ? await readFile(gitignorePath, "utf8")
172
+ : "";
173
+ const lines = existing.split("\n").map((l) => l.trim());
174
+ if (lines.includes(pattern)) return;
175
+ // Only emit the "# Added by claudeup" header once — when fixing several
176
+ // entries in one pass (F), the file already has the header from the first.
177
+ const needsHeader = !lines.includes(CLAUDEUP_HEADER);
178
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
179
+ const header = needsHeader ? `${CLAUDEUP_HEADER}\n` : "";
180
+ const block = `${prefix}${header}${formatGitignoreEntry(pattern).join("\n")}\n`;
181
+ await appendFile(gitignorePath, block);
179
182
  }
180
183
 
181
184
  function formatGitignoreEntry(pattern: string): string[] {
182
- return [`# ${getGitignoreRuleReason("ignore", pattern)}`, pattern];
185
+ return [`# ${getGitignoreRuleReason("ignore", pattern)}`, pattern];
183
186
  }
184
187
 
185
188
  async function appendTrackExceptionsIfNeeded(
186
- cwd: string,
187
- path: string,
189
+ cwd: string,
190
+ path: string,
188
191
  ): Promise<void> {
189
- if (!(await pathIsIgnored(cwd, path))) return;
192
+ if (!(await pathIsIgnored(cwd, path))) return;
190
193
 
191
- const gitignorePath = join(cwd, ".gitignore");
192
- const existing = existsSync(gitignorePath)
193
- ? await readFile(gitignorePath, "utf8")
194
- : "";
195
- const lines = new Set(
196
- existing.split("\n").map((line) => line.trim()).filter(Boolean),
197
- );
198
- const additions = unignorePatternsForPath(path).filter((line) => !lines.has(line));
199
- if (additions.length === 0) return;
194
+ const gitignorePath = join(cwd, ".gitignore");
195
+ const existing = existsSync(gitignorePath)
196
+ ? await readFile(gitignorePath, "utf8")
197
+ : "";
198
+ const lines = new Set(
199
+ existing
200
+ .split("\n")
201
+ .map((line) => line.trim())
202
+ .filter(Boolean),
203
+ );
204
+ const additions = unignorePatternsForPath(path).filter(
205
+ (line) => !lines.has(line),
206
+ );
207
+ if (additions.length === 0) return;
200
208
 
201
- const block = [
202
- existing.length > 0 && !existing.endsWith("\n") ? "\n" : "",
203
- CLAUDEUP_HEADER,
204
- `# ${getGitignoreRuleReason("track", path)}`,
205
- ...additions,
206
- "",
207
- ].join("\n");
208
- await appendFile(gitignorePath, block);
209
+ const block = [
210
+ existing.length > 0 && !existing.endsWith("\n") ? "\n" : "",
211
+ CLAUDEUP_HEADER,
212
+ `# ${getGitignoreRuleReason("track", path)}`,
213
+ ...additions,
214
+ "",
215
+ ].join("\n");
216
+ await appendFile(gitignorePath, block);
209
217
  }
210
218
 
211
219
  function unignorePatternsForPath(path: string): string[] {
212
- const normalized = path.replace(/^\/+/, "").replace(/\/+$/, "");
213
- if (!normalized) return [];
214
- const parts = normalized.split("/");
215
- const patterns: string[] = [];
216
- for (let i = 0; i < parts.length - 1; i++) {
217
- patterns.push(`!/${parts.slice(0, i + 1).join("/")}/`);
218
- }
219
- patterns.push(`!/${normalized}`);
220
- return patterns;
220
+ const normalized = path.replace(/^\/+/, "").replace(/\/+$/, "");
221
+ if (!normalized) return [];
222
+ const parts = normalized.split("/");
223
+ const patterns: string[] = [];
224
+ for (let i = 0; i < parts.length - 1; i++) {
225
+ patterns.push(`!/${parts.slice(0, i + 1).join("/")}/`);
226
+ }
227
+ patterns.push(`!/${normalized}`);
228
+ return patterns;
221
229
  }
222
230
 
223
231
  async function stripGitignoreLine(cwd: string, pattern: string): Promise<void> {
224
- const gitignorePath = join(cwd, ".gitignore");
225
- if (!existsSync(gitignorePath)) return;
226
- const content = await readFile(gitignorePath, "utf8");
227
- const filtered = content
228
- .split("\n")
229
- .filter((line) => line.trim() !== pattern)
230
- .join("\n");
231
- if (filtered !== content) {
232
- await writeFile(gitignorePath, filtered);
233
- }
232
+ const gitignorePath = join(cwd, ".gitignore");
233
+ if (!existsSync(gitignorePath)) return;
234
+ const content = await readFile(gitignorePath, "utf8");
235
+ const filtered = content
236
+ .split("\n")
237
+ .filter((line) => line.trim() !== pattern)
238
+ .join("\n");
239
+ if (filtered !== content) {
240
+ await writeFile(gitignorePath, filtered);
241
+ }
234
242
  }
235
243
 
236
244
  async function pathIsIgnored(cwd: string, path: string): Promise<boolean> {
237
- try {
238
- await runGit(["check-ignore", "--quiet", "--", path], cwd);
239
- return true;
240
- } catch (err) {
241
- if (err instanceof GitError && err.code === 1) return false;
242
- return false;
243
- }
245
+ try {
246
+ await runGit(["check-ignore", "--quiet", "--", path], cwd);
247
+ return true;
248
+ } catch (err) {
249
+ if (err instanceof GitError && err.code === 1) return false;
250
+ return false;
251
+ }
244
252
  }
245
253
 
246
254
  class GitError extends Error {
247
- constructor(public code: number, public stderr: string) {
248
- super(`git exited ${code}: ${stderr}`);
249
- }
255
+ constructor(
256
+ public code: number,
257
+ public stderr: string,
258
+ ) {
259
+ super(`git exited ${code}: ${stderr}`);
260
+ }
250
261
  }
251
262
 
252
263
  function runGit(args: string[], cwd: string): Promise<string> {
253
- return new Promise((resolve, reject) => {
254
- const proc = spawn("git", args, { cwd });
255
- let stdout = "";
256
- let stderr = "";
257
- proc.stdout.on("data", (b) => { stdout += b.toString(); });
258
- proc.stderr.on("data", (b) => { stderr += b.toString(); });
259
- proc.on("error", reject);
260
- proc.on("close", (code) => {
261
- if (code === 0) resolve(stdout);
262
- else reject(new GitError(code ?? -1, stderr));
263
- });
264
- });
264
+ return new Promise((resolve, reject) => {
265
+ const proc = spawn("git", args, { cwd });
266
+ let stdout = "";
267
+ let stderr = "";
268
+ proc.stdout.on("data", (b) => {
269
+ stdout += b.toString();
270
+ });
271
+ proc.stderr.on("data", (b) => {
272
+ stderr += b.toString();
273
+ });
274
+ proc.on("error", reject);
275
+ proc.on("close", (code) => {
276
+ if (code === 0) resolve(stdout);
277
+ else reject(new GitError(code ?? -1, stderr));
278
+ });
279
+ });
265
280
  }
@@ -8,33 +8,33 @@
8
8
  * where prerun *warns* and the TUI provides the interactive fix dialog.
9
9
  */
10
10
 
11
- import { resolveGitignore } from "./gitignore-resolver.js";
11
+ import type { Violation } from "../types/gitignore.js";
12
12
  import { detectViolations } from "./gitignore-detector.js";
13
13
  import { formatViolationSummary } from "./gitignore-fixer.js";
14
- import type { Violation } from "../types/gitignore.js";
14
+ import { resolveGitignore } from "./gitignore-resolver.js";
15
15
 
16
16
  export interface PrerunResult {
17
- violationCount: number;
18
- warnings: string[];
17
+ violationCount: number;
18
+ warnings: string[];
19
19
  }
20
20
 
21
21
  export async function checkGitignore(cwd: string): Promise<PrerunResult> {
22
- const warnings: string[] = [];
23
- try {
24
- const resolved = await resolveGitignore(cwd, {
25
- logger: (m) => warnings.push(m),
26
- });
27
- const violations = await detectViolations(cwd, resolved);
28
- return {
29
- violationCount: violations.length,
30
- warnings,
31
- };
32
- } catch (err) {
33
- warnings.push(
34
- `[gitignore] check failed: ${err instanceof Error ? err.message : String(err)}`,
35
- );
36
- return { violationCount: 0, warnings };
37
- }
22
+ const warnings: string[] = [];
23
+ try {
24
+ const resolved = await resolveGitignore(cwd, {
25
+ logger: (m) => warnings.push(m),
26
+ });
27
+ const violations = await detectViolations(cwd, resolved);
28
+ return {
29
+ violationCount: violations.length,
30
+ warnings,
31
+ };
32
+ } catch (err) {
33
+ warnings.push(
34
+ `[gitignore] check failed: ${err instanceof Error ? err.message : String(err)}`,
35
+ );
36
+ return { violationCount: 0, warnings };
37
+ }
38
38
  }
39
39
 
40
40
  /**
@@ -42,8 +42,8 @@ export async function checkGitignore(cwd: string): Promise<PrerunResult> {
42
42
  * nothing to warn about.
43
43
  */
44
44
  export function formatPrerunWarning(result: PrerunResult): string | null {
45
- if (result.violationCount === 0) return null;
46
- return `⚠ ${result.violationCount} git state issue(s) detected. Run: claudeup → Git State tab to review and fix`;
45
+ if (result.violationCount === 0) return null;
46
+ return `⚠ ${result.violationCount} git state issue(s) detected. Run: claudeup → Git State tab to review and fix`;
47
47
  }
48
48
 
49
49
  /**
@@ -51,9 +51,9 @@ export function formatPrerunWarning(result: PrerunResult): string | null {
51
51
  * to know what's wrong before deciding what to do.
52
52
  */
53
53
  export async function describeViolations(
54
- cwd: string,
55
- violations: Violation[],
54
+ cwd: string,
55
+ violations: Violation[],
56
56
  ): Promise<string> {
57
- void cwd;
58
- return formatViolationSummary(violations);
57
+ void cwd;
58
+ return formatViolationSummary(violations);
59
59
  }