claudeup 4.15.1 → 4.17.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.
- package/package.json +1 -1
- package/src/__tests__/gitignore-detector.test.ts +156 -0
- package/src/__tests__/gitignore-fixer.test.ts +197 -0
- package/src/__tests__/gitignore-prerun.test.ts +75 -0
- package/src/__tests__/gitignore-resolver.test.ts +166 -0
- package/src/__tests__/gitignore-service.test.ts +93 -0
- package/src/__tests__/useGitignoreModal.test.ts +71 -0
- package/src/data/gitignore-defaults.js +24 -0
- package/src/data/gitignore-defaults.ts +26 -0
- package/src/data/gitignore-templates.js +21 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.js +17 -1
- package/src/data/marketplaces.ts +16 -1
- package/src/prerunner/index.js +17 -0
- package/src/prerunner/index.ts +20 -0
- package/src/services/gitignore-detector.js +155 -0
- package/src/services/gitignore-detector.ts +157 -0
- package/src/services/gitignore-fixer.js +171 -0
- package/src/services/gitignore-fixer.ts +198 -0
- package/src/services/gitignore-prerun.js +46 -0
- package/src/services/gitignore-prerun.ts +59 -0
- package/src/services/gitignore-resolver.js +143 -0
- package/src/services/gitignore-resolver.ts +190 -0
- package/src/services/gitignore-service.js +99 -0
- package/src/services/gitignore-service.ts +142 -0
- package/src/types/gitignore.js +6 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +1 -0
- package/src/ui/App.js +47 -3
- package/src/ui/App.tsx +51 -2
- package/src/ui/components/TabBar.js +1 -0
- package/src/ui/components/TabBar.tsx +1 -0
- package/src/ui/hooks/useGitignoreModal.js +75 -0
- package/src/ui/hooks/useGitignoreModal.ts +97 -0
- package/src/ui/renderers/gitignoreRenderers.js +33 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +70 -0
- package/src/ui/screens/GitignoreScreen.js +227 -0
- package/src/ui/screens/GitignoreScreen.tsx +364 -0
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/types.ts +4 -2
package/src/ui/App.js
CHANGED
|
@@ -5,7 +5,10 @@ import fs from "node:fs";
|
|
|
5
5
|
import { AppProvider, useApp, useNavigation, useModal, } from "./state/AppContext.js";
|
|
6
6
|
import { DimensionsProvider, useDimensions, } from "./state/DimensionsContext.js";
|
|
7
7
|
import { ModalContainer } from "./components/modals/index.js";
|
|
8
|
-
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
|
|
8
|
+
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, GitignoreScreen, } from "./screens/index.js";
|
|
9
|
+
import { checkGitignore } from "../services/gitignore-prerun.js";
|
|
10
|
+
import { loadGitignoreState, applyAllSafeFixes, } from "../services/gitignore-service.js";
|
|
11
|
+
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
9
12
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
10
13
|
import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
|
|
11
14
|
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
@@ -40,6 +43,8 @@ function Router() {
|
|
|
40
43
|
return _jsx(ProfilesScreen, {});
|
|
41
44
|
case "skills":
|
|
42
45
|
return _jsx(SkillsScreen, {});
|
|
46
|
+
case "gitignore":
|
|
47
|
+
return _jsx(GitignoreScreen, {});
|
|
43
48
|
default:
|
|
44
49
|
return _jsx(PluginsScreen, {});
|
|
45
50
|
}
|
|
@@ -81,7 +86,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
81
86
|
// Don't handle keys when modal is open or searching
|
|
82
87
|
if (state.modal || state.isSearching)
|
|
83
88
|
return;
|
|
84
|
-
// Global navigation shortcuts (1-
|
|
89
|
+
// Global navigation shortcuts (1-7) - include mcp-registry as it's a sub-screen of mcp
|
|
85
90
|
const isTopLevel = [
|
|
86
91
|
"plugins",
|
|
87
92
|
"mcp",
|
|
@@ -91,6 +96,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
91
96
|
"model-selector",
|
|
92
97
|
"profiles",
|
|
93
98
|
"skills",
|
|
99
|
+
"gitignore",
|
|
94
100
|
].includes(state.currentRoute.screen);
|
|
95
101
|
if (isTopLevel) {
|
|
96
102
|
if (input === "1")
|
|
@@ -105,6 +111,8 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
105
111
|
navigateToScreen("profiles");
|
|
106
112
|
else if (input === "6")
|
|
107
113
|
navigateToScreen("cli-tools");
|
|
114
|
+
else if (input === "7")
|
|
115
|
+
navigateToScreen("gitignore");
|
|
108
116
|
// Tab navigation cycling
|
|
109
117
|
if (key.tab) {
|
|
110
118
|
const screens = [
|
|
@@ -114,6 +122,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
114
122
|
"settings",
|
|
115
123
|
"profiles",
|
|
116
124
|
"cli-tools",
|
|
125
|
+
"gitignore",
|
|
117
126
|
];
|
|
118
127
|
const currentIndex = screens.indexOf(state.currentRoute.screen);
|
|
119
128
|
if (currentIndex !== -1) {
|
|
@@ -148,7 +157,7 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
|
|
|
148
157
|
? This help
|
|
149
158
|
|
|
150
159
|
Quick Navigation
|
|
151
|
-
1 Plugins 4 Settings
|
|
160
|
+
1 Plugins 4 Settings 7 Gitignore
|
|
152
161
|
2 Skills 5 Profiles
|
|
153
162
|
3 MCP Servers 6 CLI Tools
|
|
154
163
|
|
|
@@ -193,6 +202,8 @@ function AppContent({ onExit }) {
|
|
|
193
202
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
194
203
|
const [mismatchData, setMismatchData] = useState(null);
|
|
195
204
|
const mismatchModal = useMismatchModal();
|
|
205
|
+
const [gitignoreData, setGitignoreData] = useState(null);
|
|
206
|
+
const gitignoreModal = useGitignoreModal();
|
|
196
207
|
// Check for updates on startup (non-blocking)
|
|
197
208
|
useEffect(() => {
|
|
198
209
|
checkForUpdates()
|
|
@@ -213,6 +224,23 @@ function AppContent({ onExit }) {
|
|
|
213
224
|
mismatchModal.show(mismatchData);
|
|
214
225
|
setMismatchData(null);
|
|
215
226
|
}, [mismatchData, mismatchModal]);
|
|
227
|
+
// Show gitignore modal — wait for the modal slot to be free so we don't
|
|
228
|
+
// stomp on the mismatch modal that may have just been shown.
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!gitignoreData)
|
|
231
|
+
return;
|
|
232
|
+
if (state.modal)
|
|
233
|
+
return;
|
|
234
|
+
gitignoreModal.show({
|
|
235
|
+
violationCount: gitignoreData.violationCount,
|
|
236
|
+
safeCount: gitignoreData.safeCount,
|
|
237
|
+
onFixSafe: async () => {
|
|
238
|
+
const s = await loadGitignoreState(state.projectPath);
|
|
239
|
+
await applyAllSafeFixes(state.projectPath, s.violations);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
setGitignoreData(null);
|
|
243
|
+
}, [gitignoreData, gitignoreModal, state.modal, state.projectPath]);
|
|
216
244
|
// Auto-refresh marketplaces on startup
|
|
217
245
|
useEffect(() => {
|
|
218
246
|
const noRefresh = process.argv.includes("--no-refresh");
|
|
@@ -277,6 +305,22 @@ function AppContent({ onExit }) {
|
|
|
277
305
|
}
|
|
278
306
|
})
|
|
279
307
|
.catch(() => { }); // non-fatal
|
|
308
|
+
// Check for gitignore manifest violations and surface a popup if any.
|
|
309
|
+
// Loads twice (once for count, once when user picks "fix safe") because
|
|
310
|
+
// the second load happens after edits — keeps detection cheap on startup.
|
|
311
|
+
(async () => {
|
|
312
|
+
try {
|
|
313
|
+
const result = await checkGitignore(process.cwd());
|
|
314
|
+
if (result.violationCount > 0) {
|
|
315
|
+
const s = await loadGitignoreState(process.cwd());
|
|
316
|
+
const safeCount = s.violations.filter((v) => v.severity === "safe").length;
|
|
317
|
+
setGitignoreData({ violationCount: result.violationCount, safeCount });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// non-fatal
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
280
324
|
repairAllMarketplaces()
|
|
281
325
|
.then(async () => {
|
|
282
326
|
dispatch({ type: "HIDE_PROGRESS" });
|
package/src/ui/App.tsx
CHANGED
|
@@ -21,8 +21,15 @@ import {
|
|
|
21
21
|
ModelSelectorScreen,
|
|
22
22
|
ProfilesScreen,
|
|
23
23
|
SkillsScreen,
|
|
24
|
+
GitignoreScreen,
|
|
24
25
|
} from "./screens/index.js";
|
|
25
26
|
import type { Screen } from "./state/types.js";
|
|
27
|
+
import { checkGitignore } from "../services/gitignore-prerun.js";
|
|
28
|
+
import {
|
|
29
|
+
loadGitignoreState,
|
|
30
|
+
applyAllSafeFixes,
|
|
31
|
+
} from "../services/gitignore-service.js";
|
|
32
|
+
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
26
33
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
27
34
|
import {
|
|
28
35
|
migrateMarketplaceRename,
|
|
@@ -70,6 +77,8 @@ function Router() {
|
|
|
70
77
|
return <ProfilesScreen />;
|
|
71
78
|
case "skills":
|
|
72
79
|
return <SkillsScreen />;
|
|
80
|
+
case "gitignore":
|
|
81
|
+
return <GitignoreScreen />;
|
|
73
82
|
default:
|
|
74
83
|
return <PluginsScreen />;
|
|
75
84
|
}
|
|
@@ -120,7 +129,7 @@ function GlobalKeyHandler({
|
|
|
120
129
|
// Don't handle keys when modal is open or searching
|
|
121
130
|
if (state.modal || state.isSearching) return;
|
|
122
131
|
|
|
123
|
-
// Global navigation shortcuts (1-
|
|
132
|
+
// Global navigation shortcuts (1-7) - include mcp-registry as it's a sub-screen of mcp
|
|
124
133
|
const isTopLevel = [
|
|
125
134
|
"plugins",
|
|
126
135
|
"mcp",
|
|
@@ -130,6 +139,7 @@ function GlobalKeyHandler({
|
|
|
130
139
|
"model-selector",
|
|
131
140
|
"profiles",
|
|
132
141
|
"skills",
|
|
142
|
+
"gitignore",
|
|
133
143
|
].includes(state.currentRoute.screen);
|
|
134
144
|
|
|
135
145
|
if (isTopLevel) {
|
|
@@ -139,6 +149,7 @@ function GlobalKeyHandler({
|
|
|
139
149
|
else if (input === "4") navigateToScreen("settings");
|
|
140
150
|
else if (input === "5") navigateToScreen("profiles");
|
|
141
151
|
else if (input === "6") navigateToScreen("cli-tools");
|
|
152
|
+
else if (input === "7") navigateToScreen("gitignore");
|
|
142
153
|
|
|
143
154
|
// Tab navigation cycling
|
|
144
155
|
if (key.tab) {
|
|
@@ -149,6 +160,7 @@ function GlobalKeyHandler({
|
|
|
149
160
|
"settings",
|
|
150
161
|
"profiles",
|
|
151
162
|
"cli-tools",
|
|
163
|
+
"gitignore",
|
|
152
164
|
];
|
|
153
165
|
const currentIndex = screens.indexOf(
|
|
154
166
|
state.currentRoute.screen as Screen,
|
|
@@ -187,7 +199,7 @@ function GlobalKeyHandler({
|
|
|
187
199
|
? This help
|
|
188
200
|
|
|
189
201
|
Quick Navigation
|
|
190
|
-
1 Plugins 4 Settings
|
|
202
|
+
1 Plugins 4 Settings 7 Gitignore
|
|
191
203
|
2 Skills 5 Profiles
|
|
192
204
|
3 MCP Servers 6 CLI Tools
|
|
193
205
|
|
|
@@ -319,6 +331,11 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
319
331
|
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
320
332
|
const [mismatchData, setMismatchData] = useState<VersionMismatchInfo[] | null>(null);
|
|
321
333
|
const mismatchModal = useMismatchModal();
|
|
334
|
+
const [gitignoreData, setGitignoreData] = useState<{
|
|
335
|
+
violationCount: number;
|
|
336
|
+
safeCount: number;
|
|
337
|
+
} | null>(null);
|
|
338
|
+
const gitignoreModal = useGitignoreModal();
|
|
322
339
|
|
|
323
340
|
// Check for updates on startup (non-blocking)
|
|
324
341
|
useEffect(() => {
|
|
@@ -341,6 +358,22 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
341
358
|
setMismatchData(null);
|
|
342
359
|
}, [mismatchData, mismatchModal]);
|
|
343
360
|
|
|
361
|
+
// Show gitignore modal — wait for the modal slot to be free so we don't
|
|
362
|
+
// stomp on the mismatch modal that may have just been shown.
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!gitignoreData) return;
|
|
365
|
+
if (state.modal) return;
|
|
366
|
+
gitignoreModal.show({
|
|
367
|
+
violationCount: gitignoreData.violationCount,
|
|
368
|
+
safeCount: gitignoreData.safeCount,
|
|
369
|
+
onFixSafe: async () => {
|
|
370
|
+
const s = await loadGitignoreState(state.projectPath);
|
|
371
|
+
await applyAllSafeFixes(state.projectPath, s.violations);
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
setGitignoreData(null);
|
|
375
|
+
}, [gitignoreData, gitignoreModal, state.modal, state.projectPath]);
|
|
376
|
+
|
|
344
377
|
// Auto-refresh marketplaces on startup
|
|
345
378
|
useEffect(() => {
|
|
346
379
|
const noRefresh = process.argv.includes("--no-refresh");
|
|
@@ -409,6 +442,22 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
409
442
|
})
|
|
410
443
|
.catch(() => {}); // non-fatal
|
|
411
444
|
|
|
445
|
+
// Check for gitignore manifest violations and surface a popup if any.
|
|
446
|
+
// Loads twice (once for count, once when user picks "fix safe") because
|
|
447
|
+
// the second load happens after edits — keeps detection cheap on startup.
|
|
448
|
+
(async () => {
|
|
449
|
+
try {
|
|
450
|
+
const result = await checkGitignore(process.cwd());
|
|
451
|
+
if (result.violationCount > 0) {
|
|
452
|
+
const s = await loadGitignoreState(process.cwd());
|
|
453
|
+
const safeCount = s.violations.filter((v) => v.severity === "safe").length;
|
|
454
|
+
setGitignoreData({ violationCount: result.violationCount, safeCount });
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// non-fatal
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
|
|
412
461
|
repairAllMarketplaces()
|
|
413
462
|
.then(async () => {
|
|
414
463
|
dispatch({ type: "HIDE_PROGRESS" });
|
|
@@ -6,6 +6,7 @@ const TABS = [
|
|
|
6
6
|
{ key: "4", label: "Settings", screen: "settings" },
|
|
7
7
|
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
8
8
|
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
9
|
+
{ key: "7", label: "Gitignore", screen: "gitignore" },
|
|
9
10
|
];
|
|
10
11
|
export function TabBar({ currentScreen }) {
|
|
11
12
|
return (_jsx("box", { flexDirection: "row", gap: 0, children: TABS.map((tab, index) => {
|
|
@@ -14,6 +14,7 @@ const TABS: Tab[] = [
|
|
|
14
14
|
{ key: "4", label: "Settings", screen: "settings" },
|
|
15
15
|
{ key: "5", label: "Profiles", screen: "profiles" },
|
|
16
16
|
{ key: "6", label: "CLI", screen: "cli-tools" },
|
|
17
|
+
{ key: "7", label: "Gitignore", screen: "gitignore" },
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
interface TabBarProps {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp, useNavigation } from "../state/AppContext.js";
|
|
3
|
+
export function buildGitignoreModal(args) {
|
|
4
|
+
const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
|
|
5
|
+
const options = [
|
|
6
|
+
{
|
|
7
|
+
label: safeCount > 0
|
|
8
|
+
? `Append ${safeCount} safe entr${safeCount === 1 ? "y" : "ies"} to .gitignore`
|
|
9
|
+
: "(no safe-fixable entries)",
|
|
10
|
+
value: "fix-safe",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
label: "Open Gitignore tab to review",
|
|
14
|
+
value: "open-tab",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
label: "Dismiss",
|
|
18
|
+
value: "dismiss",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
return {
|
|
22
|
+
type: "select",
|
|
23
|
+
title: "Gitignore violations detected",
|
|
24
|
+
message: `Found ${violationCount} issue(s) where the resolved gitignore manifest ` +
|
|
25
|
+
`disagrees with your working tree.\n\n` +
|
|
26
|
+
`Safe fixes append to .gitignore (no index mutation). Destructive fixes ` +
|
|
27
|
+
`(\`git rm --cached\`, removing .gitignore lines) must be applied one at ` +
|
|
28
|
+
`a time from the Gitignore tab.`,
|
|
29
|
+
options,
|
|
30
|
+
onSelect: (value) => {
|
|
31
|
+
if (value === "fix-safe")
|
|
32
|
+
onFixSafe();
|
|
33
|
+
else if (value === "open-tab")
|
|
34
|
+
onOpenTab();
|
|
35
|
+
else
|
|
36
|
+
onDismiss();
|
|
37
|
+
},
|
|
38
|
+
onCancel: onDismiss,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function useGitignoreModal() {
|
|
42
|
+
const { dispatch } = useApp();
|
|
43
|
+
const { navigateToScreen } = useNavigation();
|
|
44
|
+
const show = useCallback((args) => {
|
|
45
|
+
if (args.violationCount === 0)
|
|
46
|
+
return;
|
|
47
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
48
|
+
const modal = buildGitignoreModal({
|
|
49
|
+
violationCount: args.violationCount,
|
|
50
|
+
safeCount: args.safeCount,
|
|
51
|
+
onFixSafe: () => {
|
|
52
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
53
|
+
void Promise.resolve(args.onFixSafe()).catch((err) => {
|
|
54
|
+
dispatch({
|
|
55
|
+
type: "SHOW_MODAL",
|
|
56
|
+
modal: {
|
|
57
|
+
type: "message",
|
|
58
|
+
title: "Safe fixes failed",
|
|
59
|
+
message: err instanceof Error ? err.message : String(err),
|
|
60
|
+
variant: "error",
|
|
61
|
+
onDismiss: dismiss,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
onOpenTab: () => {
|
|
67
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
68
|
+
navigateToScreen("gitignore");
|
|
69
|
+
},
|
|
70
|
+
onDismiss: dismiss,
|
|
71
|
+
});
|
|
72
|
+
dispatch({ type: "SHOW_MODAL", modal });
|
|
73
|
+
}, [dispatch, navigateToScreen]);
|
|
74
|
+
return { show };
|
|
75
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp, useNavigation } from "../state/AppContext.js";
|
|
3
|
+
import type { ModalState } from "../state/types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Startup gitignore-violation modal. Mirrors `useMismatchModal` so the
|
|
7
|
+
* two flows feel identical to the user — claudeup launches, detects
|
|
8
|
+
* a problem, asks "fix safe entries / open the tab / dismiss".
|
|
9
|
+
*
|
|
10
|
+
* Pure builder is exported separately for tests.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface BuildModalArgs {
|
|
14
|
+
violationCount: number;
|
|
15
|
+
safeCount: number;
|
|
16
|
+
onFixSafe: () => void;
|
|
17
|
+
onOpenTab: () => void;
|
|
18
|
+
onDismiss: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
22
|
+
const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
|
|
23
|
+
const options = [
|
|
24
|
+
{
|
|
25
|
+
label:
|
|
26
|
+
safeCount > 0
|
|
27
|
+
? `Append ${safeCount} safe entr${safeCount === 1 ? "y" : "ies"} to .gitignore`
|
|
28
|
+
: "(no safe-fixable entries)",
|
|
29
|
+
value: "fix-safe",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "Open Gitignore tab to review",
|
|
33
|
+
value: "open-tab",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "Dismiss",
|
|
37
|
+
value: "dismiss",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
type: "select",
|
|
42
|
+
title: "Gitignore violations detected",
|
|
43
|
+
message:
|
|
44
|
+
`Found ${violationCount} issue(s) where the resolved gitignore manifest ` +
|
|
45
|
+
`disagrees with your working tree.\n\n` +
|
|
46
|
+
`Safe fixes append to .gitignore (no index mutation). Destructive fixes ` +
|
|
47
|
+
`(\`git rm --cached\`, removing .gitignore lines) must be applied one at ` +
|
|
48
|
+
`a time from the Gitignore tab.`,
|
|
49
|
+
options,
|
|
50
|
+
onSelect: (value: string) => {
|
|
51
|
+
if (value === "fix-safe") onFixSafe();
|
|
52
|
+
else if (value === "open-tab") onOpenTab();
|
|
53
|
+
else onDismiss();
|
|
54
|
+
},
|
|
55
|
+
onCancel: onDismiss,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useGitignoreModal() {
|
|
60
|
+
const { dispatch } = useApp();
|
|
61
|
+
const { navigateToScreen } = useNavigation();
|
|
62
|
+
|
|
63
|
+
const show = useCallback(
|
|
64
|
+
(args: { violationCount: number; safeCount: number; onFixSafe: () => Promise<void> | void }) => {
|
|
65
|
+
if (args.violationCount === 0) return;
|
|
66
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
67
|
+
const modal = buildGitignoreModal({
|
|
68
|
+
violationCount: args.violationCount,
|
|
69
|
+
safeCount: args.safeCount,
|
|
70
|
+
onFixSafe: () => {
|
|
71
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
72
|
+
void Promise.resolve(args.onFixSafe()).catch((err) => {
|
|
73
|
+
dispatch({
|
|
74
|
+
type: "SHOW_MODAL",
|
|
75
|
+
modal: {
|
|
76
|
+
type: "message",
|
|
77
|
+
title: "Safe fixes failed",
|
|
78
|
+
message: err instanceof Error ? err.message : String(err),
|
|
79
|
+
variant: "error",
|
|
80
|
+
onDismiss: dismiss,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
onOpenTab: () => {
|
|
86
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
87
|
+
navigateToScreen("gitignore");
|
|
88
|
+
},
|
|
89
|
+
onDismiss: dismiss,
|
|
90
|
+
});
|
|
91
|
+
dispatch({ type: "SHOW_MODAL", modal });
|
|
92
|
+
},
|
|
93
|
+
[dispatch, navigateToScreen],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return { show };
|
|
97
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
const SELECTION_BG = "#7e57c2";
|
|
3
|
+
const SELECTION_FG = "white";
|
|
4
|
+
// Layout: " X Verb path" — 2 (gutter) + 2 (status) + 7 (verb) + 4 (gap)
|
|
5
|
+
// = 15 prefix chars. On a 38-col panel inner width that leaves 23 chars for
|
|
6
|
+
// the path; truncate at 22 to keep 1 col safety margin (OpenTUI <text>
|
|
7
|
+
// reserves a second line if the rendered string equals the inner width).
|
|
8
|
+
const MAX_PATH_LEN = 22;
|
|
9
|
+
function truncatePath(p) {
|
|
10
|
+
return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
|
|
11
|
+
}
|
|
12
|
+
export function renderRuleRow(row, isSelected) {
|
|
13
|
+
const { rule, violation } = row;
|
|
14
|
+
const path = truncatePath(rule.pattern);
|
|
15
|
+
const key = `${rule.action}:${rule.pattern}`;
|
|
16
|
+
// Layout: status-glyph (2) + verb (7) + 2 spaces + path
|
|
17
|
+
// OK rule: ✓ Ignore path
|
|
18
|
+
// Violated:! Track path
|
|
19
|
+
const verb = rule.action === "ignore" ? "Ignore " : "Track ";
|
|
20
|
+
if (!violation) {
|
|
21
|
+
// Clean rule — muted, just confirmation
|
|
22
|
+
if (isSelected) {
|
|
23
|
+
return (_jsx("text", { bg: SELECTION_BG, fg: SELECTION_FG, children: `▶ ✓ ${verb} ${path}` }, key));
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: "green", children: ` ✓ ` }), _jsx("span", { fg: "gray", children: `${verb} ${path}` })] }, key));
|
|
26
|
+
}
|
|
27
|
+
// Violated rule — color encodes severity of the fix
|
|
28
|
+
const sevColor = violation.severity === "safe" ? "yellow" : "red";
|
|
29
|
+
if (isSelected) {
|
|
30
|
+
return (_jsx("text", { bg: SELECTION_BG, fg: SELECTION_FG, children: `▶ ! ${verb} ${path}` }, key));
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs("text", { children: [_jsx("span", { fg: sevColor, children: ` ! ` }), _jsx("span", { fg: "white", children: `${verb} ${path}` })] }, key));
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ResolvedRule, Violation } from "../../types/gitignore.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A unified row for the Gitignore screen — represents one resolved rule,
|
|
6
|
+
* optionally annotated with the violation it has against the working tree.
|
|
7
|
+
*/
|
|
8
|
+
export interface RuleRow {
|
|
9
|
+
rule: ResolvedRule;
|
|
10
|
+
violation?: Violation;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SELECTION_BG = "#7e57c2";
|
|
14
|
+
const SELECTION_FG = "white";
|
|
15
|
+
|
|
16
|
+
// Layout: " X Verb path" — 2 (gutter) + 2 (status) + 7 (verb) + 4 (gap)
|
|
17
|
+
// = 15 prefix chars. On a 38-col panel inner width that leaves 23 chars for
|
|
18
|
+
// the path; truncate at 22 to keep 1 col safety margin (OpenTUI <text>
|
|
19
|
+
// reserves a second line if the rendered string equals the inner width).
|
|
20
|
+
const MAX_PATH_LEN = 22;
|
|
21
|
+
|
|
22
|
+
function truncatePath(p: string): string {
|
|
23
|
+
return p.length > MAX_PATH_LEN ? p.slice(0, MAX_PATH_LEN - 1) + "…" : p;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderRuleRow(row: RuleRow, isSelected: boolean): React.ReactNode {
|
|
27
|
+
const { rule, violation } = row;
|
|
28
|
+
const path = truncatePath(rule.pattern);
|
|
29
|
+
const key = `${rule.action}:${rule.pattern}`;
|
|
30
|
+
|
|
31
|
+
// Layout: status-glyph (2) + verb (7) + 2 spaces + path
|
|
32
|
+
// OK rule: ✓ Ignore path
|
|
33
|
+
// Violated:! Track path
|
|
34
|
+
|
|
35
|
+
const verb = rule.action === "ignore" ? "Ignore " : "Track ";
|
|
36
|
+
|
|
37
|
+
if (!violation) {
|
|
38
|
+
// Clean rule — muted, just confirmation
|
|
39
|
+
if (isSelected) {
|
|
40
|
+
return (
|
|
41
|
+
<text key={key} bg={SELECTION_BG} fg={SELECTION_FG}>
|
|
42
|
+
{`▶ ✓ ${verb} ${path}`}
|
|
43
|
+
</text>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
<text key={key}>
|
|
48
|
+
<span fg="green">{` ✓ `}</span>
|
|
49
|
+
<span fg="gray">{`${verb} ${path}`}</span>
|
|
50
|
+
</text>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Violated rule — color encodes severity of the fix
|
|
55
|
+
const sevColor = violation.severity === "safe" ? "yellow" : "red";
|
|
56
|
+
|
|
57
|
+
if (isSelected) {
|
|
58
|
+
return (
|
|
59
|
+
<text key={key} bg={SELECTION_BG} fg={SELECTION_FG}>
|
|
60
|
+
{`▶ ! ${verb} ${path}`}
|
|
61
|
+
</text>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<text key={key}>
|
|
66
|
+
<span fg={sevColor}>{` ! `}</span>
|
|
67
|
+
<span fg="white">{`${verb} ${path}`}</span>
|
|
68
|
+
</text>
|
|
69
|
+
);
|
|
70
|
+
}
|