claudeup 4.17.0 → 4.18.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__/alias-parser.test.ts +317 -0
- package/src/__tests__/alias-shell-writer.test.ts +661 -0
- package/src/__tests__/alias-store.test.ts +86 -0
- package/src/__tests__/gitignore-fixer.test.ts +64 -1
- package/src/__tests__/gitignore-prerun.test.ts +2 -2
- package/src/__tests__/gitignore-service.test.ts +42 -0
- package/src/__tests__/marketplaces.test.ts +40 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +120 -0
- package/src/__tests__/useGitignoreModal.test.ts +2 -2
- package/src/data/alias-flags.js +196 -0
- package/src/data/alias-flags.ts +291 -0
- package/src/data/gitignore-reasons.js +97 -0
- package/src/data/gitignore-reasons.ts +103 -0
- package/src/data/marketplaces.js +5 -3
- package/src/data/marketplaces.ts +5 -4
- package/src/services/alias-settings.js +51 -0
- package/src/services/alias-settings.ts +63 -0
- package/src/services/alias-shell-writer.js +764 -0
- package/src/services/alias-shell-writer.ts +873 -0
- package/src/services/alias-store.js +77 -0
- package/src/services/alias-store.ts +112 -0
- package/src/services/gitignore-fixer.js +70 -10
- package/src/services/gitignore-fixer.ts +76 -9
- package/src/services/gitignore-prerun.js +3 -3
- package/src/services/gitignore-prerun.ts +3 -3
- package/src/services/gitignore-service.js +20 -2
- package/src/services/gitignore-service.ts +23 -1
- package/src/services/marketplace-fetcher.js +96 -0
- package/src/services/marketplace-fetcher.ts +137 -0
- package/src/services/plugin-manager.js +6 -59
- package/src/services/plugin-manager.ts +16 -91
- package/src/services/skillsmp-client.js +29 -9
- package/src/services/skillsmp-client.ts +38 -8
- package/src/types/gitignore.ts +1 -1
- package/src/ui/App.js +10 -4
- package/src/ui/App.tsx +9 -3
- package/src/ui/components/TabBar.js +2 -1
- package/src/ui/components/TabBar.tsx +2 -1
- package/src/ui/components/layout/FooterHints.js +29 -0
- package/src/ui/components/layout/FooterHints.tsx +52 -0
- package/src/ui/components/layout/ScreenLayout.js +2 -1
- package/src/ui/components/layout/ScreenLayout.tsx +12 -3
- package/src/ui/components/layout/index.js +1 -0
- package/src/ui/components/layout/index.ts +5 -0
- package/src/ui/components/modals/SelectModal.js +8 -1
- package/src/ui/components/modals/SelectModal.tsx +12 -1
- package/src/ui/hooks/useGitignoreModal.js +7 -8
- package/src/ui/hooks/useGitignoreModal.ts +8 -9
- package/src/ui/renderers/gitignoreRenderers.js +36 -23
- package/src/ui/renderers/gitignoreRenderers.tsx +50 -41
- package/src/ui/screens/AliasScreen.js +1008 -0
- package/src/ui/screens/AliasScreen.tsx +1402 -0
- package/src/ui/screens/CliToolsScreen.js +6 -1
- package/src/ui/screens/CliToolsScreen.tsx +6 -1
- package/src/ui/screens/EnvVarsScreen.js +6 -1
- package/src/ui/screens/EnvVarsScreen.tsx +6 -1
- package/src/ui/screens/GitignoreScreen.js +189 -88
- package/src/ui/screens/GitignoreScreen.tsx +312 -132
- package/src/ui/screens/McpRegistryScreen.js +13 -2
- package/src/ui/screens/McpRegistryScreen.tsx +13 -2
- package/src/ui/screens/McpScreen.js +6 -1
- package/src/ui/screens/McpScreen.tsx +6 -1
- package/src/ui/screens/ModelSelectorScreen.js +8 -2
- package/src/ui/screens/ModelSelectorScreen.tsx +8 -2
- package/src/ui/screens/PluginsScreen.js +13 -2
- package/src/ui/screens/PluginsScreen.tsx +13 -2
- package/src/ui/screens/ProfilesScreen.js +8 -1
- package/src/ui/screens/ProfilesScreen.tsx +8 -1
- package/src/ui/screens/SkillsScreen.js +21 -4
- package/src/ui/screens/SkillsScreen.tsx +39 -5
- package/src/ui/screens/StatusLineScreen.js +7 -1
- package/src/ui/screens/StatusLineScreen.tsx +7 -1
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/types.ts +4 -2
|
@@ -328,6 +328,11 @@ export function CliToolsScreen() {
|
|
|
328
328
|
const installedCount = toolStatuses.filter((s) => s.installed).length;
|
|
329
329
|
const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
|
|
330
330
|
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed: " }), _jsxs("span", { fg: "cyan", children: [installedCount, "/", toolStatuses.length] }), updateCount > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { fg: "gray", children: " \u2502 Updates: " }), _jsx("span", { fg: "yellow", children: updateCount })] }))] }));
|
|
331
|
-
return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints:
|
|
331
|
+
return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: [
|
|
332
|
+
{ keys: ["Enter"], label: "install" },
|
|
333
|
+
{ keys: ["a"], label: "update all" },
|
|
334
|
+
{ keys: ["c"], label: "fix conflict" },
|
|
335
|
+
{ keys: ["r"], label: "refresh" },
|
|
336
|
+
], listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderCliToolRow, maxHeight: dimensions.listPanelHeight, getKey: (status) => status.tool.name }), detailPanel: renderCliToolDetail(selectedStatus) }));
|
|
332
337
|
}
|
|
333
338
|
export default CliToolsScreen;
|
|
@@ -417,7 +417,12 @@ export function CliToolsScreen() {
|
|
|
417
417
|
title="claudeup CLI Tools"
|
|
418
418
|
currentScreen="cli-tools"
|
|
419
419
|
statusLine={statusContent}
|
|
420
|
-
footerHints=
|
|
420
|
+
footerHints={[
|
|
421
|
+
{ keys: ["Enter"], label: "install" },
|
|
422
|
+
{ keys: ["a"], label: "update all" },
|
|
423
|
+
{ keys: ["c"], label: "fix conflict" },
|
|
424
|
+
{ keys: ["r"], label: "refresh" },
|
|
425
|
+
]}
|
|
421
426
|
listPanel={
|
|
422
427
|
<ScrollableList
|
|
423
428
|
items={toolStatuses}
|
|
@@ -140,7 +140,12 @@ export function SettingsScreen() {
|
|
|
140
140
|
? Array.from(settings.values.data.values()).filter((v) => v.user !== undefined || v.project !== undefined).length
|
|
141
141
|
: 0;
|
|
142
142
|
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Settings: " }), _jsxs("span", { fg: "cyan", children: [totalSet, " configured"] }), _jsx("span", { fg: "gray", children: " \u2502 u:user p:project" })] }));
|
|
143
|
-
return (_jsx(ScreenLayout, { title: "claudeup Settings", currentScreen: "settings", statusLine: statusContent, footerHints:
|
|
143
|
+
return (_jsx(ScreenLayout, { title: "claudeup Settings", currentScreen: "settings", statusLine: statusContent, footerHints: [
|
|
144
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
145
|
+
{ keys: ["u"], label: "user scope" },
|
|
146
|
+
{ keys: ["p"], label: "project scope" },
|
|
147
|
+
{ keys: ["Enter"], label: "project" },
|
|
148
|
+
], listPanel: settings.values.status !== "success" ? (_jsx("text", { fg: "gray", children: settings.values.status === "loading"
|
|
144
149
|
? "Loading..."
|
|
145
150
|
: "Error loading settings" })) : (_jsx(ScrollableList, { items: listItems, selectedIndex: settings.selectedIndex, renderItem: renderSettingRow, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
|
|
146
151
|
}
|
|
@@ -185,7 +185,12 @@ export function SettingsScreen() {
|
|
|
185
185
|
title="claudeup Settings"
|
|
186
186
|
currentScreen="settings"
|
|
187
187
|
statusLine={statusContent}
|
|
188
|
-
footerHints=
|
|
188
|
+
footerHints={[
|
|
189
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
190
|
+
{ keys: ["u"], label: "user scope" },
|
|
191
|
+
{ keys: ["p"], label: "project scope" },
|
|
192
|
+
{ keys: ["Enter"], label: "project" },
|
|
193
|
+
]}
|
|
189
194
|
listPanel={
|
|
190
195
|
settings.values.status !== "success" ? (
|
|
191
196
|
<text fg="gray">
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { appendFileSync } from "node:fs";
|
|
5
|
+
import { useRenderer } from "@opentui/react";
|
|
4
6
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
5
7
|
import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
|
|
6
8
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { loadGitignoreState,
|
|
9
|
+
import { loadGitignoreState, applyDestructiveFix, ensureProjectManifestForEdit, applyTemplate, } from "../../services/gitignore-service.js";
|
|
8
10
|
import { BUILTIN_TEMPLATES } from "../../data/gitignore-templates.js";
|
|
9
|
-
import {
|
|
11
|
+
import { getGitignoreRuleReason } from "../../data/gitignore-reasons.js";
|
|
12
|
+
import { renderRuleRow, } from "../renderers/gitignoreRenderers.js";
|
|
13
|
+
// Clean rows are grouped by the manifest that defined them. Order = display
|
|
14
|
+
// order, top to bottom. "Default" is the user-facing name for the built-in
|
|
15
|
+
// rules. Each label names where you'd go to change that group's rules.
|
|
16
|
+
const SOURCE_ORDER = ["project", "global", "profile", "builtin"];
|
|
17
|
+
const SOURCE_LABELS = {
|
|
18
|
+
project: "Project · .claude/gitignore.json",
|
|
19
|
+
global: "Global · ~/.claude/gitignore.json",
|
|
20
|
+
profile: "Profile",
|
|
21
|
+
builtin: "Default · built into claudeup",
|
|
22
|
+
};
|
|
10
23
|
export function GitignoreScreen() {
|
|
11
24
|
const { state } = useApp();
|
|
12
25
|
const modal = useModal();
|
|
26
|
+
const renderer = useRenderer();
|
|
13
27
|
const cwd = state.projectPath;
|
|
14
28
|
const [data, setData] = useState(null);
|
|
15
29
|
const [loading, setLoading] = useState(true);
|
|
@@ -32,30 +46,37 @@ export function GitignoreScreen() {
|
|
|
32
46
|
useEffect(() => {
|
|
33
47
|
void reload();
|
|
34
48
|
}, [reload]);
|
|
35
|
-
// Build the
|
|
36
|
-
//
|
|
49
|
+
// Build the managed list: every desired gitignore rule, with all matching
|
|
50
|
+
// violations grouped under the rule that owns them.
|
|
37
51
|
const rows = useMemo(() => {
|
|
38
52
|
if (!data)
|
|
39
53
|
return [];
|
|
40
|
-
const violationByPattern = new Map();
|
|
41
|
-
for (const v of data.violations) {
|
|
42
|
-
// A violation's `path` is the rule's pattern for ignored-but-should-track
|
|
43
|
-
// and missing-from-gitignore; for tracked-but-should-ignore it's the
|
|
44
|
-
// actual tracked file under a directory rule. Index by rule pattern by
|
|
45
|
-
// matching either path or its prefix.
|
|
46
|
-
// Simplest: index by path; lookup will try exact then prefix.
|
|
47
|
-
violationByPattern.set(v.path, v);
|
|
48
|
-
}
|
|
49
54
|
const annotated = data.resolved.rules.map((rule) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
return { rule, violation: v };
|
|
55
|
+
const violations = data.violations.filter((v) => violationMatchesRule(rule, v));
|
|
56
|
+
return { rule, violations };
|
|
53
57
|
});
|
|
58
|
+
// Display order = flat index order (the keyboard handler walks this array),
|
|
59
|
+
// so the on-screen layout must match this sort exactly:
|
|
60
|
+
// 1. violations first (most violations first)
|
|
61
|
+
// 2. then clean rows grouped by source manifest (SOURCE_ORDER)
|
|
62
|
+
// 3. alphabetical within each group
|
|
63
|
+
const sourceRank = (tier) => {
|
|
64
|
+
const i = SOURCE_ORDER.indexOf(tier);
|
|
65
|
+
return i === -1 ? SOURCE_ORDER.length : i;
|
|
66
|
+
};
|
|
54
67
|
return annotated.sort((a, b) => {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
68
|
+
const aViol = a.violations.length > 0;
|
|
69
|
+
const bViol = b.violations.length > 0;
|
|
70
|
+
if (aViol !== bViol)
|
|
71
|
+
return aViol ? -1 : 1;
|
|
72
|
+
if (aViol && bViol && a.violations.length !== b.violations.length) {
|
|
73
|
+
return b.violations.length - a.violations.length;
|
|
74
|
+
}
|
|
75
|
+
if (!aViol) {
|
|
76
|
+
const sr = sourceRank(a.rule.source) - sourceRank(b.rule.source);
|
|
77
|
+
if (sr !== 0)
|
|
78
|
+
return sr;
|
|
79
|
+
}
|
|
59
80
|
return a.rule.pattern.localeCompare(b.rule.pattern);
|
|
60
81
|
});
|
|
61
82
|
}, [data]);
|
|
@@ -66,41 +87,76 @@ export function GitignoreScreen() {
|
|
|
66
87
|
}
|
|
67
88
|
}, [rows.length, selectedIdx]);
|
|
68
89
|
const selectedRow = rows[selectedIdx];
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
90
|
+
const ignoreRuleCount = data?.resolved.rules.filter((rule) => rule.action === "ignore").length ?? 0;
|
|
91
|
+
const trackRuleCount = data?.resolved.rules.filter((rule) => rule.action === "track").length ?? 0;
|
|
92
|
+
const toFixCount = data?.violations.length ?? 0;
|
|
93
|
+
// Group rows for display while preserving each row's flat index in `rows`
|
|
94
|
+
// (the keyboard handler walks `rows` by index, so the highlight must track
|
|
95
|
+
// the original index, not the position within a rendered group).
|
|
96
|
+
const indexed = rows.map((row, index) => ({ row, index }));
|
|
97
|
+
const toFixRows = indexed.filter((r) => r.row.violations.length > 0);
|
|
98
|
+
// Clean rows grouped by which manifest defined them. Sections render only
|
|
99
|
+
// when non-empty, so e.g. the Profile group stays hidden until profiles
|
|
100
|
+
// actually contribute rules.
|
|
101
|
+
const cleanIndexed = indexed.filter((r) => r.row.violations.length === 0);
|
|
102
|
+
const sourceGroups = SOURCE_ORDER.map((tier) => ({
|
|
103
|
+
tier,
|
|
104
|
+
label: SOURCE_LABELS[tier],
|
|
105
|
+
items: cleanIndexed.filter((r) => r.row.rule.source === tier),
|
|
106
|
+
})).filter((g) => g.items.length > 0);
|
|
107
|
+
const cleanCount = cleanIndexed.length;
|
|
72
108
|
// ── Action handlers ───────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? `\n\n${r.alreadyPresent.length} pattern(s) were already present.`
|
|
79
|
-
: ""}`, "success");
|
|
80
|
-
await reload();
|
|
81
|
-
}, [cwd, data, safeCount, modal, reload]);
|
|
82
|
-
const handleApplySelected = useCallback(async () => {
|
|
83
|
-
if (!selectedRow?.violation)
|
|
109
|
+
// Fix every out-of-sync row. Iterates the grouped rows (not the flat
|
|
110
|
+
// violation list) so each .gitignore entry uses the rule's pattern/glob,
|
|
111
|
+
// not one specific path under it — same association handleApplyOne relies on.
|
|
112
|
+
const handleFixAll = useCallback(async () => {
|
|
113
|
+
if (!data || toFixRows.length === 0)
|
|
84
114
|
return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
try {
|
|
116
|
+
const messages = [];
|
|
117
|
+
const failures = [];
|
|
118
|
+
for (const { row } of toFixRows) {
|
|
119
|
+
for (const v of row.violations) {
|
|
120
|
+
const r = await applyDestructiveFix(cwd, v, row.rule.pattern);
|
|
121
|
+
(r.applied ? messages : failures).push(r.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
await reload();
|
|
125
|
+
if (failures.length > 0) {
|
|
126
|
+
await modal.message("Some fixes didn't apply", [...failures, ...messages].join("\n"), "error");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
await modal.message("Fix failed", err instanceof Error ? err.message : String(err), "error");
|
|
131
|
+
}
|
|
132
|
+
}, [cwd, data, toFixRows, reload, modal]);
|
|
133
|
+
// Fix just the highlighted row.
|
|
134
|
+
const handleFixOne = useCallback(async () => {
|
|
135
|
+
if (!selectedRow) {
|
|
136
|
+
await modal.message("No row selected", "Use ↑↓ to highlight a row that needs fixing, then press f.", "info");
|
|
88
137
|
return;
|
|
89
138
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`You can review the change with \`git status\` afterwards.`);
|
|
93
|
-
if (!confirmed)
|
|
139
|
+
if (selectedRow.violations.length === 0) {
|
|
140
|
+
await modal.message("Nothing to fix", `${selectedRow.rule.pattern} already matches. f only applies to rows that need fixing.`, "info");
|
|
94
141
|
return;
|
|
142
|
+
}
|
|
95
143
|
try {
|
|
96
|
-
const
|
|
97
|
-
|
|
144
|
+
const results = [];
|
|
145
|
+
for (const v of selectedRow.violations) {
|
|
146
|
+
results.push(await applyDestructiveFix(cwd, v, selectedRow.rule.pattern));
|
|
147
|
+
}
|
|
148
|
+
const failed = results.filter((r) => !r.applied);
|
|
149
|
+
await reload();
|
|
150
|
+
// Success is silent — the reload removes the fixed row. Only surface a
|
|
151
|
+
// modal when something didn't apply.
|
|
152
|
+
if (failed.length > 0) {
|
|
153
|
+
await modal.message("Some fixes didn't apply", results.map((r) => r.message).join("\n"), "error");
|
|
154
|
+
}
|
|
98
155
|
}
|
|
99
156
|
catch (err) {
|
|
100
157
|
await modal.message("Fix failed", err instanceof Error ? err.message : String(err), "error");
|
|
101
158
|
}
|
|
102
|
-
|
|
103
|
-
}, [selectedRow, handleApplySafe, modal, cwd, reload]);
|
|
159
|
+
}, [selectedRow, modal, cwd, reload]);
|
|
104
160
|
const handleApplyTemplate = useCallback(async () => {
|
|
105
161
|
const names = Object.keys(BUILTIN_TEMPLATES);
|
|
106
162
|
if (names.length === 0) {
|
|
@@ -125,16 +181,32 @@ export function GitignoreScreen() {
|
|
|
125
181
|
await reload();
|
|
126
182
|
}, [cwd, modal, reload]);
|
|
127
183
|
const handleEditManifest = useCallback(async () => {
|
|
128
|
-
const
|
|
184
|
+
const current = data ?? await loadGitignoreState(cwd);
|
|
185
|
+
const projectManifest = await ensureProjectManifestForEdit(cwd, current.resolved);
|
|
129
186
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
187
|
+
renderer.suspend();
|
|
188
|
+
try {
|
|
189
|
+
await new Promise((resolveEdit) => {
|
|
190
|
+
const child = spawn(editor, [projectManifest], { stdio: "inherit" });
|
|
191
|
+
child.on("exit", () => resolveEdit());
|
|
192
|
+
child.on("error", () => resolveEdit());
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
renderer.resume();
|
|
197
|
+
}
|
|
135
198
|
await reload();
|
|
136
|
-
|
|
199
|
+
renderer.requestRender();
|
|
200
|
+
}, [cwd, data, reload, renderer]);
|
|
137
201
|
useKeyboardHandler((input, key) => {
|
|
202
|
+
if (process.env.CLAUDEUP_DEBUG_KEYS === "1") {
|
|
203
|
+
try {
|
|
204
|
+
appendFileSync("/tmp/claudeup-keys.log", `${new Date().toISOString()} screen=gitignore input=${JSON.stringify(input)} name=${JSON.stringify(key.name)} shift=${key.shift} ctrl=${key.ctrl} meta=${key.meta}\n`);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// best-effort logging — never break the UI
|
|
208
|
+
}
|
|
209
|
+
}
|
|
138
210
|
if (state.modal)
|
|
139
211
|
return;
|
|
140
212
|
if (input === "j" || key.downArrow) {
|
|
@@ -145,12 +217,13 @@ export function GitignoreScreen() {
|
|
|
145
217
|
setSelectedIdx((i) => Math.max(i - 1, 0));
|
|
146
218
|
return;
|
|
147
219
|
}
|
|
148
|
-
|
|
149
|
-
|
|
220
|
+
// F = fix everything out of sync; f = fix just the selected row.
|
|
221
|
+
if (input === "F" || (input === "f" && key.shift)) {
|
|
222
|
+
void handleFixAll();
|
|
150
223
|
return;
|
|
151
224
|
}
|
|
152
|
-
if (input === "
|
|
153
|
-
void
|
|
225
|
+
if (input === "f") {
|
|
226
|
+
void handleFixOne();
|
|
154
227
|
return;
|
|
155
228
|
}
|
|
156
229
|
if (input === "t") {
|
|
@@ -166,62 +239,90 @@ export function GitignoreScreen() {
|
|
|
166
239
|
}
|
|
167
240
|
});
|
|
168
241
|
// ── Rendering ─────────────────────────────────────────────────────────
|
|
169
|
-
const summary = data
|
|
170
|
-
|
|
242
|
+
const summary = data
|
|
243
|
+
? renderSummary(rows.length, ignoreRuleCount, trackRuleCount, toFixCount)
|
|
244
|
+
: null;
|
|
245
|
+
const listBody = loading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : rows.length === 0 ? (_jsx("text", { fg: "gray", children: "No rules resolved." })) : (_jsxs("box", { flexDirection: "column", children: [toFixRows.length > 0 && (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "red", children: `Needs fixing (${toFixRows.length})` }), toFixRows
|
|
246
|
+
.slice(0, 100)
|
|
247
|
+
.map(({ row, index }) => renderRuleRow(row, index === selectedIdx)), _jsx("text", { children: " " })] })), _jsx("text", { fg: "gray", children: `Managed items (${cleanCount})` }), sourceGroups.map((group, gi) => (_jsxs("box", { flexDirection: "column", children: [gi > 0 && _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: ` ${group.label}` }), group.items.map(({ row, index }) => renderRuleRow(row, index === selectedIdx))] }, group.tier)))] }));
|
|
171
248
|
const listPanel = (_jsxs("box", { flexDirection: "column", children: [summary, _jsx("text", { children: " " }), listBody] }));
|
|
172
249
|
const detailPanel = renderDetail(selectedRow, data?.warnings ?? []);
|
|
173
250
|
const subtitle = data
|
|
174
|
-
?
|
|
175
|
-
?
|
|
176
|
-
: `${
|
|
251
|
+
? toFixCount === 0
|
|
252
|
+
? `${rows.length} managed`
|
|
253
|
+
: `${toFixCount} to fix`
|
|
177
254
|
: "";
|
|
178
|
-
return (_jsx(ScreenLayout, { title: "
|
|
255
|
+
return (_jsx(ScreenLayout, { title: "Managed Git State", subtitle: subtitle, currentScreen: "gitignore", footerHints: [
|
|
256
|
+
{ keys: ["↑", "↓"], label: "move" },
|
|
257
|
+
{ keys: ["f"], label: "fix selected" },
|
|
258
|
+
{ keys: ["F"], label: "fix all" },
|
|
259
|
+
{ keys: ["t"], label: "template" },
|
|
260
|
+
{ keys: ["e"], label: "edit" },
|
|
261
|
+
{ keys: ["r"], label: "reload" },
|
|
262
|
+
], listPanel: listPanel, detailPanel: detailPanel }));
|
|
179
263
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
264
|
+
function violationMatchesRule(rule, violation) {
|
|
265
|
+
if (violation.path === rule.pattern)
|
|
266
|
+
return true;
|
|
183
267
|
if (!rule.pattern.endsWith("/") && !rule.pattern.includes("*"))
|
|
184
|
-
return
|
|
268
|
+
return false;
|
|
185
269
|
const prefix = rule.pattern.replace(/\*+$/, "");
|
|
186
|
-
return
|
|
270
|
+
return violation.path.startsWith(prefix);
|
|
187
271
|
}
|
|
188
|
-
function renderSummary(
|
|
189
|
-
|
|
190
|
-
return _jsx("text", { fg: "green", children: "\u2713 All rules satisfied \u2014 nothing to fix." });
|
|
191
|
-
}
|
|
192
|
-
return (_jsxs("text", { children: [_jsx("span", { fg: "white", children: `${violations} violation${violations === 1 ? "" : "s"} ` }), _jsx("span", { fg: "gray", children: `(` }), safe > 0 && _jsx("span", { fg: "yellow", children: `${safe} safe` }), safe > 0 && destructive > 0 && _jsx("span", { fg: "gray", children: `, ` }), destructive > 0 && _jsx("span", { fg: "red", children: `${destructive} destructive` }), _jsx("span", { fg: "gray", children: `)` })] }));
|
|
272
|
+
function renderSummary(rules, ignoreRules, trackRules, toFix) {
|
|
273
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { children: _jsx("span", { fg: "white", children: `${rules} managed item${rules === 1 ? "" : "s"}` }) }), _jsx("text", { children: _jsx("span", { fg: "gray", children: `${ignoreRules} ignored ${trackRules} tracked` }) }), toFix === 0 ? (_jsx("text", { fg: "green", children: "Everything matches \u2014 nothing to fix." })) : (_jsx("text", { fg: "yellow", children: `${toFix} need${toFix === 1 ? "s" : ""} fixing — f one, F all` }))] }));
|
|
193
274
|
}
|
|
194
275
|
function renderDetail(row, warnings) {
|
|
195
276
|
if (!row) {
|
|
196
277
|
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No rule selected." }), warnings.length > 0 && (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: "yellow", children: "Warnings:" }), warnings.map((w, i) => (_jsx("text", { fg: "gray", children: w }, i)))] }))] }));
|
|
197
278
|
}
|
|
198
|
-
const { rule,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
279
|
+
const { rule, violations } = row;
|
|
280
|
+
const reason = getGitignoreRuleReason(rule.action, rule.pattern);
|
|
281
|
+
const setIn = sourceName(rule.source);
|
|
282
|
+
if (violations.length === 0) {
|
|
283
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "white", children: _jsx("strong", { children: rule.pattern }) }), _jsx("text", { children: " " }), _jsx("text", { fg: "green", children: `✓ Already ${rule.action === "ignore" ? "ignored" : "tracked"} — nothing to do.` }), _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: reason }), _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: `Set in: ${setIn}` }), renderWarnings(warnings)] }));
|
|
284
|
+
}
|
|
285
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "white", children: _jsx("strong", { children: rule.pattern }) }), _jsx("text", { children: " " }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Should be: " }), _jsx("span", { fg: "white", children: rule.action === "ignore" ? "ignored by git" : "tracked by git" })] }), _jsx("text", { fg: "gray", children: reason }), _jsx("text", { fg: "gray", children: `Set in: ${setIn}` }), _jsx("text", { children: " " }), _jsx("text", { fg: "yellow", children: `Needs fixing (${violations.length}):` }), violations.map((v, i) => (_jsxs("box", { flexDirection: "column", marginTop: i === 0 ? 0 : 1, children: [_jsx("text", { fg: "white", children: v.path }), _jsx("text", { fg: "gray", children: describeKind(v.kind) }), _jsx("text", { fg: v.severity === "safe" ? "cyan" : "red", children: actionHint(v.kind) })] }, `${v.kind}:${v.path}:${i}`))), renderWarnings(warnings)] }));
|
|
286
|
+
}
|
|
287
|
+
/** Friendly, where-to-change-it name for a rule's source manifest. */
|
|
288
|
+
function sourceName(source) {
|
|
289
|
+
switch (source) {
|
|
290
|
+
case "project":
|
|
291
|
+
return "Project (.claude/gitignore.json)";
|
|
292
|
+
case "global":
|
|
293
|
+
return "Global (~/.claude/gitignore.json)";
|
|
294
|
+
case "profile":
|
|
295
|
+
return "Profile";
|
|
296
|
+
case "builtin":
|
|
297
|
+
return "Default (built into claudeup)";
|
|
202
298
|
}
|
|
203
|
-
// Rule is violated — explain and show the action
|
|
204
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "white", children: _jsx("strong", { children: violation.path }) }), _jsx("text", { children: " " }), _jsx("text", { fg: "white", children: describeKind(violation.kind) }), _jsx("text", { children: " " }), _jsx("text", { fg: "gray", children: `Rule from ${rule.source} tier.` }), _jsx("text", { fg: violation.severity === "safe" ? "yellow" : "red", children: violation.severity === "safe"
|
|
205
|
-
? "Safe — only appends to .gitignore."
|
|
206
|
-
: "Destructive — mutates the git index." }), _jsx("text", { children: " " }), _jsx("text", { fg: "cyan", children: actionHint(violation.kind) })] }));
|
|
207
299
|
}
|
|
208
300
|
function describeKind(kind) {
|
|
209
301
|
switch (kind) {
|
|
210
|
-
case "tracked-but-should-ignore":
|
|
211
|
-
|
|
212
|
-
case "
|
|
213
|
-
|
|
302
|
+
case "tracked-but-should-ignore":
|
|
303
|
+
return "Git is tracking it, but it should be ignored.";
|
|
304
|
+
case "ignored-but-should-track":
|
|
305
|
+
return "A .gitignore rule hides it, but it should be in git.";
|
|
306
|
+
case "untracked-and-should-track":
|
|
307
|
+
return "It's on disk but git isn't tracking it yet.";
|
|
308
|
+
case "missing-from-gitignore":
|
|
309
|
+
return "It should be ignored, but nothing in .gitignore covers it.";
|
|
214
310
|
}
|
|
215
311
|
}
|
|
216
312
|
function actionHint(kind) {
|
|
217
313
|
switch (kind) {
|
|
218
314
|
case "tracked-but-should-ignore":
|
|
219
|
-
return "
|
|
315
|
+
return "Press f to stop tracking it and add it to .gitignore.";
|
|
220
316
|
case "ignored-but-should-track":
|
|
221
|
-
return "
|
|
317
|
+
return "Press f to remove the .gitignore line and add it to git.";
|
|
222
318
|
case "untracked-and-should-track":
|
|
223
|
-
return "
|
|
319
|
+
return "Press f to add it to git.";
|
|
224
320
|
case "missing-from-gitignore":
|
|
225
|
-
return "
|
|
321
|
+
return "Press f to add it to .gitignore (F to fix all).";
|
|
226
322
|
}
|
|
227
323
|
}
|
|
324
|
+
function renderWarnings(warnings) {
|
|
325
|
+
if (warnings.length === 0)
|
|
326
|
+
return null;
|
|
327
|
+
return (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { fg: "yellow", children: "Warnings:" }), warnings.map((w, i) => (_jsx("text", { fg: "gray", children: w }, i)))] }));
|
|
328
|
+
}
|