claudeup 4.16.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 +19 -1
- package/src/data/marketplaces.ts +17 -1
- 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/types/index.ts +1 -0
- 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
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { useRenderer } from "@opentui/react";
|
|
3
5
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
4
6
|
import { useKeyboardHandler } from "../hooks/useKeyboardHandler.js";
|
|
5
7
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
8
|
import {
|
|
7
9
|
loadGitignoreState,
|
|
8
|
-
applyAllSafeFixes,
|
|
9
10
|
applyDestructiveFix,
|
|
11
|
+
ensureProjectManifestForEdit,
|
|
10
12
|
applyTemplate,
|
|
11
13
|
type GitignoreState,
|
|
12
14
|
} from "../../services/gitignore-service.js";
|
|
13
15
|
import { BUILTIN_TEMPLATES } from "../../data/gitignore-templates.js";
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
+
import { getGitignoreRuleReason } from "../../data/gitignore-reasons.js";
|
|
17
|
+
import {
|
|
18
|
+
renderRuleRow,
|
|
19
|
+
type RuleRow,
|
|
20
|
+
} from "../renderers/gitignoreRenderers.js";
|
|
21
|
+
import type { ResolvedRule, Tier, Violation } from "../../types/gitignore.js";
|
|
22
|
+
|
|
23
|
+
// Clean rows are grouped by the manifest that defined them. Order = display
|
|
24
|
+
// order, top to bottom. "Default" is the user-facing name for the built-in
|
|
25
|
+
// rules. Each label names where you'd go to change that group's rules.
|
|
26
|
+
const SOURCE_ORDER: Tier[] = ["project", "global", "profile", "builtin"];
|
|
27
|
+
const SOURCE_LABELS: Record<Tier, string> = {
|
|
28
|
+
project: "Project · .claude/gitignore.json",
|
|
29
|
+
global: "Global · ~/.claude/gitignore.json",
|
|
30
|
+
profile: "Profile",
|
|
31
|
+
builtin: "Default · built into claudeup",
|
|
32
|
+
};
|
|
16
33
|
|
|
17
34
|
export function GitignoreScreen() {
|
|
18
35
|
const { state } = useApp();
|
|
19
36
|
const modal = useModal();
|
|
37
|
+
const renderer = useRenderer();
|
|
20
38
|
const cwd = state.projectPath;
|
|
21
39
|
|
|
22
40
|
const [data, setData] = useState<GitignoreState | null>(null);
|
|
@@ -41,31 +59,38 @@ export function GitignoreScreen() {
|
|
|
41
59
|
void reload();
|
|
42
60
|
}, [reload]);
|
|
43
61
|
|
|
44
|
-
// Build the
|
|
45
|
-
//
|
|
62
|
+
// Build the managed list: every desired gitignore rule, with all matching
|
|
63
|
+
// violations grouped under the rule that owns them.
|
|
46
64
|
const rows: RuleRow[] = useMemo(() => {
|
|
47
65
|
if (!data) return [];
|
|
48
|
-
const violationByPattern = new Map<string, Violation>();
|
|
49
|
-
for (const v of data.violations) {
|
|
50
|
-
// A violation's `path` is the rule's pattern for ignored-but-should-track
|
|
51
|
-
// and missing-from-gitignore; for tracked-but-should-ignore it's the
|
|
52
|
-
// actual tracked file under a directory rule. Index by rule pattern by
|
|
53
|
-
// matching either path or its prefix.
|
|
54
|
-
// Simplest: index by path; lookup will try exact then prefix.
|
|
55
|
-
violationByPattern.set(v.path, v);
|
|
56
|
-
}
|
|
57
66
|
|
|
58
67
|
const annotated: RuleRow[] = data.resolved.rules.map((rule) => {
|
|
59
|
-
const v
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return { rule,
|
|
68
|
+
const violations = data.violations.filter((v) =>
|
|
69
|
+
violationMatchesRule(rule, v),
|
|
70
|
+
);
|
|
71
|
+
return { rule, violations };
|
|
63
72
|
});
|
|
64
73
|
|
|
74
|
+
// Display order = flat index order (the keyboard handler walks this array),
|
|
75
|
+
// so the on-screen layout must match this sort exactly:
|
|
76
|
+
// 1. violations first (most violations first)
|
|
77
|
+
// 2. then clean rows grouped by source manifest (SOURCE_ORDER)
|
|
78
|
+
// 3. alphabetical within each group
|
|
79
|
+
const sourceRank = (tier: ResolvedRule["source"]): number => {
|
|
80
|
+
const i = SOURCE_ORDER.indexOf(tier);
|
|
81
|
+
return i === -1 ? SOURCE_ORDER.length : i;
|
|
82
|
+
};
|
|
65
83
|
return annotated.sort((a, b) => {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
84
|
+
const aViol = a.violations.length > 0;
|
|
85
|
+
const bViol = b.violations.length > 0;
|
|
86
|
+
if (aViol !== bViol) return aViol ? -1 : 1;
|
|
87
|
+
if (aViol && bViol && a.violations.length !== b.violations.length) {
|
|
88
|
+
return b.violations.length - a.violations.length;
|
|
89
|
+
}
|
|
90
|
+
if (!aViol) {
|
|
91
|
+
const sr = sourceRank(a.rule.source) - sourceRank(b.rule.source);
|
|
92
|
+
if (sr !== 0) return sr;
|
|
93
|
+
}
|
|
69
94
|
return a.rule.pattern.localeCompare(b.rule.pattern);
|
|
70
95
|
});
|
|
71
96
|
}, [data]);
|
|
@@ -78,52 +103,96 @@ export function GitignoreScreen() {
|
|
|
78
103
|
}, [rows.length, selectedIdx]);
|
|
79
104
|
|
|
80
105
|
const selectedRow = rows[selectedIdx];
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
const ignoreRuleCount =
|
|
107
|
+
data?.resolved.rules.filter((rule) => rule.action === "ignore").length ?? 0;
|
|
108
|
+
const trackRuleCount =
|
|
109
|
+
data?.resolved.rules.filter((rule) => rule.action === "track").length ?? 0;
|
|
110
|
+
const toFixCount = data?.violations.length ?? 0;
|
|
111
|
+
|
|
112
|
+
// Group rows for display while preserving each row's flat index in `rows`
|
|
113
|
+
// (the keyboard handler walks `rows` by index, so the highlight must track
|
|
114
|
+
// the original index, not the position within a rendered group).
|
|
115
|
+
const indexed = rows.map((row, index) => ({ row, index }));
|
|
116
|
+
const toFixRows = indexed.filter((r) => r.row.violations.length > 0);
|
|
117
|
+
// Clean rows grouped by which manifest defined them. Sections render only
|
|
118
|
+
// when non-empty, so e.g. the Profile group stays hidden until profiles
|
|
119
|
+
// actually contribute rules.
|
|
120
|
+
const cleanIndexed = indexed.filter((r) => r.row.violations.length === 0);
|
|
121
|
+
const sourceGroups = SOURCE_ORDER.map((tier) => ({
|
|
122
|
+
tier,
|
|
123
|
+
label: SOURCE_LABELS[tier],
|
|
124
|
+
items: cleanIndexed.filter((r) => r.row.rule.source === tier),
|
|
125
|
+
})).filter((g) => g.items.length > 0);
|
|
126
|
+
const cleanCount = cleanIndexed.length;
|
|
85
127
|
|
|
86
128
|
// ── Action handlers ───────────────────────────────────────────────────
|
|
87
129
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
// Fix every out-of-sync row. Iterates the grouped rows (not the flat
|
|
131
|
+
// violation list) so each .gitignore entry uses the rule's pattern/glob,
|
|
132
|
+
// not one specific path under it — same association handleApplyOne relies on.
|
|
133
|
+
const handleFixAll = useCallback(async () => {
|
|
134
|
+
if (!data || toFixRows.length === 0) return;
|
|
135
|
+
try {
|
|
136
|
+
const messages: string[] = [];
|
|
137
|
+
const failures: string[] = [];
|
|
138
|
+
for (const { row } of toFixRows) {
|
|
139
|
+
for (const v of row.violations) {
|
|
140
|
+
const r = await applyDestructiveFix(cwd, v, row.rule.pattern);
|
|
141
|
+
(r.applied ? messages : failures).push(r.message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await reload();
|
|
145
|
+
if (failures.length > 0) {
|
|
146
|
+
await modal.message(
|
|
147
|
+
"Some fixes didn't apply",
|
|
148
|
+
[...failures, ...messages].join("\n"),
|
|
149
|
+
"error",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
await modal.message(
|
|
154
|
+
"Fix failed",
|
|
155
|
+
err instanceof Error ? err.message : String(err),
|
|
156
|
+
"error",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}, [cwd, data, toFixRows, reload, modal]);
|
|
106
160
|
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
// Fix just the highlighted row.
|
|
162
|
+
const handleFixOne = useCallback(async () => {
|
|
163
|
+
if (!selectedRow) {
|
|
164
|
+
await modal.message(
|
|
165
|
+
"No row selected",
|
|
166
|
+
"Use ↑↓ to highlight a row that needs fixing, then press f.",
|
|
167
|
+
"info",
|
|
168
|
+
);
|
|
109
169
|
return;
|
|
110
170
|
}
|
|
111
|
-
|
|
112
|
-
const confirmed = await modal.confirm(
|
|
113
|
-
"Apply destructive fix?",
|
|
114
|
-
`${v.kind} on ${v.path}\n\n` +
|
|
115
|
-
`This mutates the git index (e.g. \`git rm --cached\` or \`git add\`).\n` +
|
|
116
|
-
`You can review the change with \`git status\` afterwards.`,
|
|
117
|
-
);
|
|
118
|
-
if (!confirmed) return;
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const result = await applyDestructiveFix(cwd, v);
|
|
171
|
+
if (selectedRow.violations.length === 0) {
|
|
122
172
|
await modal.message(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
173
|
+
"Nothing to fix",
|
|
174
|
+
`${selectedRow.rule.pattern} already matches. f only applies to rows that need fixing.`,
|
|
175
|
+
"info",
|
|
126
176
|
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const results: Array<{ applied: boolean; message: string }> = [];
|
|
182
|
+
for (const v of selectedRow.violations) {
|
|
183
|
+
results.push(await applyDestructiveFix(cwd, v, selectedRow.rule.pattern));
|
|
184
|
+
}
|
|
185
|
+
const failed = results.filter((r) => !r.applied);
|
|
186
|
+
await reload();
|
|
187
|
+
// Success is silent — the reload removes the fixed row. Only surface a
|
|
188
|
+
// modal when something didn't apply.
|
|
189
|
+
if (failed.length > 0) {
|
|
190
|
+
await modal.message(
|
|
191
|
+
"Some fixes didn't apply",
|
|
192
|
+
results.map((r) => r.message).join("\n"),
|
|
193
|
+
"error",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
127
196
|
} catch (err) {
|
|
128
197
|
await modal.message(
|
|
129
198
|
"Fix failed",
|
|
@@ -131,13 +200,16 @@ export function GitignoreScreen() {
|
|
|
131
200
|
"error",
|
|
132
201
|
);
|
|
133
202
|
}
|
|
134
|
-
|
|
135
|
-
}, [selectedRow, handleApplySafe, modal, cwd, reload]);
|
|
203
|
+
}, [selectedRow, modal, cwd, reload]);
|
|
136
204
|
|
|
137
205
|
const handleApplyTemplate = useCallback(async () => {
|
|
138
206
|
const names = Object.keys(BUILTIN_TEMPLATES);
|
|
139
207
|
if (names.length === 0) {
|
|
140
|
-
await modal.message(
|
|
208
|
+
await modal.message(
|
|
209
|
+
"No templates",
|
|
210
|
+
"No built-in templates available.",
|
|
211
|
+
"info",
|
|
212
|
+
);
|
|
141
213
|
return;
|
|
142
214
|
}
|
|
143
215
|
const selected = await modal.select(
|
|
@@ -170,17 +242,37 @@ export function GitignoreScreen() {
|
|
|
170
242
|
}, [cwd, modal, reload]);
|
|
171
243
|
|
|
172
244
|
const handleEditManifest = useCallback(async () => {
|
|
173
|
-
const
|
|
245
|
+
const current = data ?? await loadGitignoreState(cwd);
|
|
246
|
+
const projectManifest = await ensureProjectManifestForEdit(
|
|
247
|
+
cwd,
|
|
248
|
+
current.resolved,
|
|
249
|
+
);
|
|
174
250
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
251
|
+
renderer.suspend();
|
|
252
|
+
try {
|
|
253
|
+
await new Promise<void>((resolveEdit) => {
|
|
254
|
+
const child = spawn(editor, [projectManifest], { stdio: "inherit" });
|
|
255
|
+
child.on("exit", () => resolveEdit());
|
|
256
|
+
child.on("error", () => resolveEdit());
|
|
257
|
+
});
|
|
258
|
+
} finally {
|
|
259
|
+
renderer.resume();
|
|
260
|
+
}
|
|
180
261
|
await reload();
|
|
181
|
-
|
|
262
|
+
renderer.requestRender();
|
|
263
|
+
}, [cwd, data, reload, renderer]);
|
|
182
264
|
|
|
183
265
|
useKeyboardHandler((input, key) => {
|
|
266
|
+
if (process.env.CLAUDEUP_DEBUG_KEYS === "1") {
|
|
267
|
+
try {
|
|
268
|
+
appendFileSync(
|
|
269
|
+
"/tmp/claudeup-keys.log",
|
|
270
|
+
`${new Date().toISOString()} screen=gitignore input=${JSON.stringify(input)} name=${JSON.stringify(key.name)} shift=${key.shift} ctrl=${key.ctrl} meta=${key.meta}\n`,
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
// best-effort logging — never break the UI
|
|
274
|
+
}
|
|
275
|
+
}
|
|
184
276
|
if (state.modal) return;
|
|
185
277
|
if (input === "j" || key.downArrow) {
|
|
186
278
|
setSelectedIdx((i) => Math.min(i + 1, Math.max(0, rows.length - 1)));
|
|
@@ -190,12 +282,13 @@ export function GitignoreScreen() {
|
|
|
190
282
|
setSelectedIdx((i) => Math.max(i - 1, 0));
|
|
191
283
|
return;
|
|
192
284
|
}
|
|
193
|
-
|
|
194
|
-
|
|
285
|
+
// F = fix everything out of sync; f = fix just the selected row.
|
|
286
|
+
if (input === "F" || (input === "f" && key.shift)) {
|
|
287
|
+
void handleFixAll();
|
|
195
288
|
return;
|
|
196
289
|
}
|
|
197
|
-
if (input === "
|
|
198
|
-
void
|
|
290
|
+
if (input === "f") {
|
|
291
|
+
void handleFixOne();
|
|
199
292
|
return;
|
|
200
293
|
}
|
|
201
294
|
if (input === "t") {
|
|
@@ -213,7 +306,9 @@ export function GitignoreScreen() {
|
|
|
213
306
|
|
|
214
307
|
// ── Rendering ─────────────────────────────────────────────────────────
|
|
215
308
|
|
|
216
|
-
const summary = data
|
|
309
|
+
const summary = data
|
|
310
|
+
? renderSummary(rows.length, ignoreRuleCount, trackRuleCount, toFixCount)
|
|
311
|
+
: null;
|
|
217
312
|
|
|
218
313
|
const listBody = loading ? (
|
|
219
314
|
<text fg="gray">Loading...</text>
|
|
@@ -223,14 +318,35 @@ export function GitignoreScreen() {
|
|
|
223
318
|
<text fg="gray">No rules resolved.</text>
|
|
224
319
|
) : (
|
|
225
320
|
<box flexDirection="column">
|
|
226
|
-
{
|
|
321
|
+
{toFixRows.length > 0 && (
|
|
322
|
+
<box flexDirection="column">
|
|
323
|
+
<text fg="red">{`Needs fixing (${toFixRows.length})`}</text>
|
|
324
|
+
{toFixRows
|
|
325
|
+
.slice(0, 100)
|
|
326
|
+
.map(({ row, index }) => renderRuleRow(row, index === selectedIdx))}
|
|
327
|
+
<text> </text>
|
|
328
|
+
</box>
|
|
329
|
+
)}
|
|
330
|
+
<text fg="gray">{`Managed items (${cleanCount})`}</text>
|
|
331
|
+
{/* One section per manifest source (Default / Global / Project /
|
|
332
|
+
Profile). Empty sources are dropped, so headers only appear when
|
|
333
|
+
they have rows. The header names where you'd change those rules. */}
|
|
334
|
+
{sourceGroups.map((group, gi) => (
|
|
335
|
+
<box flexDirection="column" key={group.tier}>
|
|
336
|
+
{gi > 0 && <text> </text>}
|
|
337
|
+
<text fg="gray">{` ${group.label}`}</text>
|
|
338
|
+
{group.items.map(({ row, index }) =>
|
|
339
|
+
renderRuleRow(row, index === selectedIdx),
|
|
340
|
+
)}
|
|
341
|
+
</box>
|
|
342
|
+
))}
|
|
227
343
|
</box>
|
|
228
344
|
);
|
|
229
345
|
|
|
230
346
|
const listPanel = (
|
|
231
347
|
<box flexDirection="column">
|
|
232
348
|
{summary}
|
|
233
|
-
<text>
|
|
349
|
+
<text> </text>
|
|
234
350
|
{listBody}
|
|
235
351
|
</box>
|
|
236
352
|
);
|
|
@@ -238,52 +354,60 @@ export function GitignoreScreen() {
|
|
|
238
354
|
const detailPanel = renderDetail(selectedRow, data?.warnings ?? []);
|
|
239
355
|
|
|
240
356
|
const subtitle = data
|
|
241
|
-
?
|
|
242
|
-
?
|
|
243
|
-
: `${
|
|
357
|
+
? toFixCount === 0
|
|
358
|
+
? `${rows.length} managed`
|
|
359
|
+
: `${toFixCount} to fix`
|
|
244
360
|
: "";
|
|
245
361
|
|
|
246
362
|
return (
|
|
247
363
|
<ScreenLayout
|
|
248
|
-
title="
|
|
364
|
+
title="Managed Git State"
|
|
249
365
|
subtitle={subtitle}
|
|
250
366
|
currentScreen="gitignore"
|
|
251
|
-
footerHints=
|
|
367
|
+
footerHints={[
|
|
368
|
+
{ keys: ["↑", "↓"], label: "move" },
|
|
369
|
+
{ keys: ["f"], label: "fix selected" },
|
|
370
|
+
{ keys: ["F"], label: "fix all" },
|
|
371
|
+
{ keys: ["t"], label: "template" },
|
|
372
|
+
{ keys: ["e"], label: "edit" },
|
|
373
|
+
{ keys: ["r"], label: "reload" },
|
|
374
|
+
]}
|
|
252
375
|
listPanel={listPanel}
|
|
253
376
|
detailPanel={detailPanel}
|
|
254
377
|
/>
|
|
255
378
|
);
|
|
256
379
|
}
|
|
257
380
|
|
|
258
|
-
function
|
|
381
|
+
function violationMatchesRule(
|
|
259
382
|
rule: ResolvedRule,
|
|
260
|
-
|
|
261
|
-
):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!rule.pattern.endsWith("/") && !rule.pattern.includes("*")) return undefined;
|
|
383
|
+
violation: Violation,
|
|
384
|
+
): boolean {
|
|
385
|
+
if (violation.path === rule.pattern) return true;
|
|
386
|
+
if (!rule.pattern.endsWith("/") && !rule.pattern.includes("*")) return false;
|
|
265
387
|
const prefix = rule.pattern.replace(/\*+$/, "");
|
|
266
|
-
return
|
|
388
|
+
return violation.path.startsWith(prefix);
|
|
267
389
|
}
|
|
268
390
|
|
|
269
391
|
function renderSummary(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
392
|
+
rules: number,
|
|
393
|
+
ignoreRules: number,
|
|
394
|
+
trackRules: number,
|
|
395
|
+
toFix: number,
|
|
273
396
|
): React.ReactNode {
|
|
274
|
-
if (violations === 0) {
|
|
275
|
-
return <text fg="green">✓ All rules satisfied — nothing to fix.</text>;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
397
|
return (
|
|
279
|
-
<
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
398
|
+
<box flexDirection="column">
|
|
399
|
+
<text>
|
|
400
|
+
<span fg="white">{`${rules} managed item${rules === 1 ? "" : "s"}`}</span>
|
|
401
|
+
</text>
|
|
402
|
+
<text>
|
|
403
|
+
<span fg="gray">{`${ignoreRules} ignored ${trackRules} tracked`}</span>
|
|
404
|
+
</text>
|
|
405
|
+
{toFix === 0 ? (
|
|
406
|
+
<text fg="green">Everything matches — nothing to fix.</text>
|
|
407
|
+
) : (
|
|
408
|
+
<text fg="yellow">{`${toFix} need${toFix === 1 ? "s" : ""} fixing — f one, F all`}</text>
|
|
409
|
+
)}
|
|
410
|
+
</box>
|
|
287
411
|
);
|
|
288
412
|
}
|
|
289
413
|
|
|
@@ -299,7 +423,9 @@ function renderDetail(
|
|
|
299
423
|
<box flexDirection="column" marginTop={1}>
|
|
300
424
|
<text fg="yellow">Warnings:</text>
|
|
301
425
|
{warnings.map((w, i) => (
|
|
302
|
-
<text fg="gray" key={i}>
|
|
426
|
+
<text fg="gray" key={i}>
|
|
427
|
+
{w}
|
|
428
|
+
</text>
|
|
303
429
|
))}
|
|
304
430
|
</box>
|
|
305
431
|
)}
|
|
@@ -307,58 +433,112 @@ function renderDetail(
|
|
|
307
433
|
);
|
|
308
434
|
}
|
|
309
435
|
|
|
310
|
-
const { rule,
|
|
436
|
+
const { rule, violations } = row;
|
|
437
|
+
const reason = getGitignoreRuleReason(rule.action, rule.pattern);
|
|
438
|
+
|
|
439
|
+
const setIn = sourceName(rule.source);
|
|
311
440
|
|
|
312
|
-
if (
|
|
313
|
-
// Rule is satisfied — show what it enforces and where it came from
|
|
441
|
+
if (violations.length === 0) {
|
|
314
442
|
return (
|
|
315
443
|
<box flexDirection="column">
|
|
316
|
-
<text fg="white"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<text>
|
|
320
|
-
<text fg="
|
|
444
|
+
<text fg="white">
|
|
445
|
+
<strong>{rule.pattern}</strong>
|
|
446
|
+
</text>
|
|
447
|
+
<text> </text>
|
|
448
|
+
<text fg="green">{`✓ Already ${rule.action === "ignore" ? "ignored" : "tracked"} — nothing to do.`}</text>
|
|
449
|
+
<text> </text>
|
|
450
|
+
<text fg="gray">{reason}</text>
|
|
451
|
+
<text> </text>
|
|
452
|
+
<text fg="gray">{`Set in: ${setIn}`}</text>
|
|
453
|
+
{renderWarnings(warnings)}
|
|
321
454
|
</box>
|
|
322
455
|
);
|
|
323
456
|
}
|
|
324
457
|
|
|
325
|
-
// Rule is violated — explain and show the action
|
|
326
458
|
return (
|
|
327
459
|
<box flexDirection="column">
|
|
328
|
-
<text fg="white"
|
|
329
|
-
|
|
330
|
-
<text fg="white">{describeKind(violation.kind)}</text>
|
|
331
|
-
<text>{" "}</text>
|
|
332
|
-
<text fg="gray">{`Rule from ${rule.source} tier.`}</text>
|
|
333
|
-
<text fg={violation.severity === "safe" ? "yellow" : "red"}>
|
|
334
|
-
{violation.severity === "safe"
|
|
335
|
-
? "Safe — only appends to .gitignore."
|
|
336
|
-
: "Destructive — mutates the git index."}
|
|
460
|
+
<text fg="white">
|
|
461
|
+
<strong>{rule.pattern}</strong>
|
|
337
462
|
</text>
|
|
338
|
-
<text>
|
|
339
|
-
<text
|
|
463
|
+
<text> </text>
|
|
464
|
+
<text>
|
|
465
|
+
<span fg="gray">Should be: </span>
|
|
466
|
+
<span fg="white">
|
|
467
|
+
{rule.action === "ignore" ? "ignored by git" : "tracked by git"}
|
|
468
|
+
</span>
|
|
469
|
+
</text>
|
|
470
|
+
<text fg="gray">{reason}</text>
|
|
471
|
+
<text fg="gray">{`Set in: ${setIn}`}</text>
|
|
472
|
+
<text> </text>
|
|
473
|
+
<text fg="yellow">{`Needs fixing (${violations.length}):`}</text>
|
|
474
|
+
{violations.map((v, i) => (
|
|
475
|
+
<box
|
|
476
|
+
key={`${v.kind}:${v.path}:${i}`}
|
|
477
|
+
flexDirection="column"
|
|
478
|
+
marginTop={i === 0 ? 0 : 1}
|
|
479
|
+
>
|
|
480
|
+
<text fg="white">{v.path}</text>
|
|
481
|
+
<text fg="gray">{describeKind(v.kind)}</text>
|
|
482
|
+
<text fg={v.severity === "safe" ? "cyan" : "red"}>
|
|
483
|
+
{actionHint(v.kind)}
|
|
484
|
+
</text>
|
|
485
|
+
</box>
|
|
486
|
+
))}
|
|
487
|
+
{renderWarnings(warnings)}
|
|
340
488
|
</box>
|
|
341
489
|
);
|
|
342
490
|
}
|
|
343
491
|
|
|
492
|
+
/** Friendly, where-to-change-it name for a rule's source manifest. */
|
|
493
|
+
function sourceName(source: ResolvedRule["source"]): string {
|
|
494
|
+
switch (source) {
|
|
495
|
+
case "project":
|
|
496
|
+
return "Project (.claude/gitignore.json)";
|
|
497
|
+
case "global":
|
|
498
|
+
return "Global (~/.claude/gitignore.json)";
|
|
499
|
+
case "profile":
|
|
500
|
+
return "Profile";
|
|
501
|
+
case "builtin":
|
|
502
|
+
return "Default (built into claudeup)";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
344
506
|
function describeKind(kind: Violation["kind"]): string {
|
|
345
507
|
switch (kind) {
|
|
346
|
-
case "tracked-but-should-ignore":
|
|
347
|
-
|
|
348
|
-
case "
|
|
349
|
-
|
|
508
|
+
case "tracked-but-should-ignore":
|
|
509
|
+
return "Git is tracking it, but it should be ignored.";
|
|
510
|
+
case "ignored-but-should-track":
|
|
511
|
+
return "A .gitignore rule hides it, but it should be in git.";
|
|
512
|
+
case "untracked-and-should-track":
|
|
513
|
+
return "It's on disk but git isn't tracking it yet.";
|
|
514
|
+
case "missing-from-gitignore":
|
|
515
|
+
return "It should be ignored, but nothing in .gitignore covers it.";
|
|
350
516
|
}
|
|
351
517
|
}
|
|
352
518
|
|
|
353
519
|
function actionHint(kind: Violation["kind"]): string {
|
|
354
520
|
switch (kind) {
|
|
355
521
|
case "tracked-but-should-ignore":
|
|
356
|
-
return "
|
|
522
|
+
return "Press f to stop tracking it and add it to .gitignore.";
|
|
357
523
|
case "ignored-but-should-track":
|
|
358
|
-
return "
|
|
524
|
+
return "Press f to remove the .gitignore line and add it to git.";
|
|
359
525
|
case "untracked-and-should-track":
|
|
360
|
-
return "
|
|
526
|
+
return "Press f to add it to git.";
|
|
361
527
|
case "missing-from-gitignore":
|
|
362
|
-
return "
|
|
528
|
+
return "Press f to add it to .gitignore (F to fix all).";
|
|
363
529
|
}
|
|
364
530
|
}
|
|
531
|
+
|
|
532
|
+
function renderWarnings(warnings: string[]): React.ReactNode {
|
|
533
|
+
if (warnings.length === 0) return null;
|
|
534
|
+
return (
|
|
535
|
+
<box flexDirection="column" marginTop={1}>
|
|
536
|
+
<text fg="yellow">Warnings:</text>
|
|
537
|
+
{warnings.map((w, i) => (
|
|
538
|
+
<text fg="gray" key={i}>
|
|
539
|
+
{w}
|
|
540
|
+
</text>
|
|
541
|
+
))}
|
|
542
|
+
</box>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
@@ -214,8 +214,19 @@ export function McpRegistryScreen() {
|
|
|
214
214
|
};
|
|
215
215
|
// Footer hints
|
|
216
216
|
const footerHints = isSearchActive
|
|
217
|
-
?
|
|
218
|
-
|
|
217
|
+
? [
|
|
218
|
+
{ keys: ["type"], label: "search" },
|
|
219
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
220
|
+
{ keys: ["Enter"], label: "done" },
|
|
221
|
+
{ keys: ["Esc"], label: "cancel" },
|
|
222
|
+
]
|
|
223
|
+
: [
|
|
224
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
225
|
+
{ keys: ["Enter"], label: "install" },
|
|
226
|
+
{ keys: ["/"], label: "search" },
|
|
227
|
+
{ keys: ["R"], label: "refresh" },
|
|
228
|
+
{ keys: ["l"], label: "local" },
|
|
229
|
+
];
|
|
219
230
|
// Status for search placeholder
|
|
220
231
|
const searchPlaceholder = `${servers.length} servers │ / to search`;
|
|
221
232
|
return (_jsx(ScreenLayout, { title: "claudeup MCP Registry", subtitle: "Powered by MCP Registry", currentScreen: "mcp-registry", search: {
|
|
@@ -303,8 +303,19 @@ export function McpRegistryScreen() {
|
|
|
303
303
|
|
|
304
304
|
// Footer hints
|
|
305
305
|
const footerHints = isSearchActive
|
|
306
|
-
?
|
|
307
|
-
|
|
306
|
+
? [
|
|
307
|
+
{ keys: ["type"], label: "search" },
|
|
308
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
309
|
+
{ keys: ["Enter"], label: "done" },
|
|
310
|
+
{ keys: ["Esc"], label: "cancel" },
|
|
311
|
+
]
|
|
312
|
+
: [
|
|
313
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
314
|
+
{ keys: ["Enter"], label: "install" },
|
|
315
|
+
{ keys: ["/"], label: "search" },
|
|
316
|
+
{ keys: ["R"], label: "refresh" },
|
|
317
|
+
{ keys: ["l"], label: "local" },
|
|
318
|
+
];
|
|
308
319
|
|
|
309
320
|
// Status for search placeholder
|
|
310
321
|
const searchPlaceholder = `${servers.length} servers │ / to search`;
|
|
@@ -166,6 +166,11 @@ export function McpScreen() {
|
|
|
166
166
|
const installedCount = Object.keys(mcp.installedServers).length;
|
|
167
167
|
const enabledCount = Object.values(mcp.installedServers).filter((v) => v === true).length;
|
|
168
168
|
const subtitle = `${enabledCount} enabled │ ${installedCount} configured │ / to search`;
|
|
169
|
-
return (_jsx(ScreenLayout, { title: "claudeup MCP Servers", subtitle: subtitle, currentScreen: "mcp", footerHints:
|
|
169
|
+
return (_jsx(ScreenLayout, { title: "claudeup MCP Servers", subtitle: subtitle, currentScreen: "mcp", footerHints: [
|
|
170
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
171
|
+
{ keys: ["Enter"], label: "toggle" },
|
|
172
|
+
{ keys: ["/"], label: "search" },
|
|
173
|
+
{ keys: ["r"], label: "registry" },
|
|
174
|
+
], listPanel: _jsx(ScrollableList, { items: allListItems, selectedIndex: mcp.selectedIndex, renderItem: renderMcpRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderMcpDetail(selectedItem, isLoading) }));
|
|
170
175
|
}
|
|
171
176
|
export default McpScreen;
|