claudeup 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -13,178 +13,196 @@
|
|
|
13
13
|
* overrides to track."
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
16
17
|
import { readFile } from "node:fs/promises";
|
|
17
|
-
import { join } from "node:path";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
|
-
import {
|
|
20
|
-
import type {
|
|
21
|
-
GitignoreManifest,
|
|
22
|
-
ResolvedManifest,
|
|
23
|
-
ResolvedRule,
|
|
24
|
-
ResolvedConflict,
|
|
25
|
-
Tier,
|
|
26
|
-
RuleAction,
|
|
27
|
-
} from "../types/gitignore.js";
|
|
19
|
+
import { join } from "node:path";
|
|
28
20
|
import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults.js";
|
|
29
21
|
import { BUILTIN_TEMPLATES } from "../data/gitignore-templates.js";
|
|
22
|
+
import type {
|
|
23
|
+
GitignoreManifest,
|
|
24
|
+
ResolvedConflict,
|
|
25
|
+
ResolvedManifest,
|
|
26
|
+
ResolvedRule,
|
|
27
|
+
RuleAction,
|
|
28
|
+
Tier,
|
|
29
|
+
} from "../types/gitignore.js";
|
|
30
30
|
|
|
31
31
|
export interface ResolveOptions {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/** Override $HOME (test seam). */
|
|
33
|
+
homeDir?: string;
|
|
34
|
+
/** Optional logger for non-fatal warnings (malformed JSON, missing template). */
|
|
35
|
+
logger?: (msg: string) => void;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const TIER_ORDER: Tier[] = ["builtin", "global", "project"];
|
|
39
39
|
|
|
40
40
|
export async function resolveGitignore(
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
projectCwd: string,
|
|
42
|
+
opts: ResolveOptions = {},
|
|
43
43
|
): Promise<ResolvedManifest> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
44
|
+
const home = opts.homeDir ?? homedir();
|
|
45
|
+
const log = opts.logger ?? (() => {});
|
|
46
|
+
|
|
47
|
+
const tierManifests = new Map<Tier, GitignoreManifest>();
|
|
48
|
+
|
|
49
|
+
// 1. Built-in (always present)
|
|
50
|
+
tierManifests.set("builtin", BUILTIN_DEFAULTS);
|
|
51
|
+
|
|
52
|
+
// 2. Global ~/.claude/gitignore.json
|
|
53
|
+
const globalPath = join(home, ".claude", "gitignore.json");
|
|
54
|
+
const globalManifest = await readManifest(globalPath, log);
|
|
55
|
+
if (globalManifest) tierManifests.set("global", globalManifest);
|
|
56
|
+
|
|
57
|
+
// 3. Project .claude/gitignore.json
|
|
58
|
+
const projectPath = join(projectCwd, ".claude", "gitignore.json");
|
|
59
|
+
const projectManifest = await readManifest(projectPath, log);
|
|
60
|
+
if (projectManifest) tierManifests.set("project", projectManifest);
|
|
61
|
+
|
|
62
|
+
// Apply `extends` for each tier (templates count as part of their tier)
|
|
63
|
+
for (const tier of TIER_ORDER) {
|
|
64
|
+
const m = tierManifests.get(tier);
|
|
65
|
+
if (!m?.extends?.length) continue;
|
|
66
|
+
const merged: GitignoreManifest = {
|
|
67
|
+
ignore: [...m.ignore],
|
|
68
|
+
track: [...m.track],
|
|
69
|
+
};
|
|
70
|
+
for (const tplName of m.extends) {
|
|
71
|
+
const tpl = BUILTIN_TEMPLATES[tplName];
|
|
72
|
+
if (!tpl) {
|
|
73
|
+
log(`[gitignore] unknown template "${tplName}" in ${tier} manifest`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
merged.ignore.push(...tpl.ignore);
|
|
77
|
+
merged.track.push(...tpl.track);
|
|
78
|
+
}
|
|
79
|
+
tierManifests.set(tier, merged);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate intra-tier consistency
|
|
83
|
+
for (const [tier, m] of tierManifests.entries()) {
|
|
84
|
+
const ig = new Set(m.ignore);
|
|
85
|
+
for (const t of m.track) {
|
|
86
|
+
if (ig.has(t)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Intra-tier conflict in ${tier} manifest: "${t}" is in both ignore and track`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Merge across tiers — last-wins, track conflicts
|
|
95
|
+
const ruleByPattern = new Map<
|
|
96
|
+
string,
|
|
97
|
+
{
|
|
98
|
+
action: RuleAction;
|
|
99
|
+
source: Tier;
|
|
100
|
+
history: Array<{ tier: Tier; action: RuleAction }>;
|
|
101
|
+
}
|
|
102
|
+
>();
|
|
103
|
+
|
|
104
|
+
for (const tier of TIER_ORDER) {
|
|
105
|
+
const m = tierManifests.get(tier);
|
|
106
|
+
if (!m) continue;
|
|
107
|
+
for (const p of dedupe(m.ignore)) {
|
|
108
|
+
pushRule(ruleByPattern, p, "ignore", tier);
|
|
109
|
+
}
|
|
110
|
+
for (const p of dedupe(m.track)) {
|
|
111
|
+
pushRule(ruleByPattern, p, "track", tier);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rules: ResolvedRule[] = [];
|
|
116
|
+
const conflicts: ResolvedConflict[] = [];
|
|
117
|
+
|
|
118
|
+
for (const [pattern, entry] of ruleByPattern.entries()) {
|
|
119
|
+
rules.push({ pattern, action: entry.action, source: entry.source });
|
|
120
|
+
|
|
121
|
+
const losers = entry.history.filter(
|
|
122
|
+
(h) => h.tier !== entry.source || h.action !== entry.action,
|
|
123
|
+
);
|
|
124
|
+
if (losers.length > 0 && hasActionDisagreement(entry.history)) {
|
|
125
|
+
conflicts.push({
|
|
126
|
+
pattern,
|
|
127
|
+
winner: entry.source,
|
|
128
|
+
losers: losers.filter((l) => l.action !== entry.action),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { rules, conflicts };
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
function pushRule(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
map: Map<
|
|
138
|
+
string,
|
|
139
|
+
{
|
|
140
|
+
action: RuleAction;
|
|
141
|
+
source: Tier;
|
|
142
|
+
history: Array<{ tier: Tier; action: RuleAction }>;
|
|
143
|
+
}
|
|
144
|
+
>,
|
|
145
|
+
pattern: string,
|
|
146
|
+
action: RuleAction,
|
|
147
|
+
tier: Tier,
|
|
140
148
|
): void {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
const existing = map.get(pattern);
|
|
150
|
+
if (existing) {
|
|
151
|
+
existing.history.push({ tier, action });
|
|
152
|
+
existing.action = action;
|
|
153
|
+
existing.source = tier;
|
|
154
|
+
} else {
|
|
155
|
+
map.set(pattern, { action, source: tier, history: [{ tier, action }] });
|
|
156
|
+
}
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
function hasActionDisagreement(
|
|
152
|
-
|
|
160
|
+
history: Array<{ tier: Tier; action: RuleAction }>,
|
|
153
161
|
): boolean {
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
const actions = new Set(history.map((h) => h.action));
|
|
163
|
+
return actions.size > 1;
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
function dedupe(xs: string[]): string[] {
|
|
159
|
-
|
|
167
|
+
return Array.from(new Set(xs));
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
async function readManifest(
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
path: string,
|
|
172
|
+
log: (msg: string) => void,
|
|
165
173
|
): Promise<GitignoreManifest | null> {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
if (!existsSync(path)) return null;
|
|
175
|
+
try {
|
|
176
|
+
const raw = await readFile(path, "utf8");
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
if (!isManifest(parsed)) {
|
|
179
|
+
log(
|
|
180
|
+
`[gitignore] ${path} is not a valid manifest (expected { ignore: string[], track: string[] })`,
|
|
181
|
+
);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return parsed;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log(
|
|
187
|
+
`[gitignore] failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
188
|
+
);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
function isManifest(x: unknown): x is GitignoreManifest {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
if (!x || typeof x !== "object") return false;
|
|
195
|
+
const m = x as Record<string, unknown>;
|
|
196
|
+
if (!Array.isArray(m.ignore) || !m.ignore.every((p) => typeof p === "string"))
|
|
197
|
+
return false;
|
|
198
|
+
if (!Array.isArray(m.track) || !m.track.every((p) => typeof p === "string"))
|
|
199
|
+
return false;
|
|
200
|
+
if (m.extends !== undefined) {
|
|
201
|
+
if (
|
|
202
|
+
!Array.isArray(m.extends) ||
|
|
203
|
+
!m.extends.every((p) => typeof p === "string")
|
|
204
|
+
)
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
190
208
|
}
|
|
@@ -5,80 +5,83 @@
|
|
|
5
5
|
* built-in template).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
9
8
|
import { existsSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { detectViolations } from "./gitignore-detector.js";
|
|
13
|
-
import {
|
|
14
|
-
applySafeFixes as coreApplySafeFixes,
|
|
15
|
-
applyDestructiveFix as coreApplyDestructiveFix,
|
|
16
|
-
type SafeFixResult,
|
|
17
|
-
type DestructiveFixResult,
|
|
18
|
-
} from "./gitignore-fixer.js";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
19
11
|
import { BUILTIN_TEMPLATES } from "../data/gitignore-templates.js";
|
|
20
12
|
import type {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
GitignoreManifest,
|
|
14
|
+
ResolvedManifest,
|
|
15
|
+
Violation,
|
|
24
16
|
} from "../types/gitignore.js";
|
|
17
|
+
import { detectViolations } from "./gitignore-detector.js";
|
|
18
|
+
import {
|
|
19
|
+
type DestructiveFixResult,
|
|
20
|
+
type SafeFixResult,
|
|
21
|
+
applyDestructiveFix as coreApplyDestructiveFix,
|
|
22
|
+
applySafeFixes as coreApplySafeFixes,
|
|
23
|
+
} from "./gitignore-fixer.js";
|
|
24
|
+
import { resolveGitignore } from "./gitignore-resolver.js";
|
|
25
25
|
|
|
26
26
|
export interface GitignoreState {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
resolved: ResolvedManifest;
|
|
28
|
+
violations: Violation[];
|
|
29
|
+
/** Logger lines from resolution (malformed JSON, missing template, etc). */
|
|
30
|
+
warnings: string[];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function loadGitignoreState(cwd: string): Promise<GitignoreState> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const warnings: string[] = [];
|
|
35
|
+
const resolved = await resolveGitignore(cwd, {
|
|
36
|
+
logger: (m) => warnings.push(m),
|
|
37
|
+
});
|
|
38
|
+
const violations = await detectViolations(cwd, resolved);
|
|
39
|
+
return { resolved, violations, warnings };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export async function applyAllSafeFixes(
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
cwd: string,
|
|
44
|
+
violations: Violation[],
|
|
45
45
|
): Promise<SafeFixResult> {
|
|
46
|
-
|
|
46
|
+
return coreApplySafeFixes(cwd, violations);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export async function applyDestructiveFix(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
cwd: string,
|
|
51
|
+
violation: Violation,
|
|
52
|
+
rulePattern?: string,
|
|
53
53
|
): Promise<DestructiveFixResult> {
|
|
54
|
-
|
|
54
|
+
return coreApplyDestructiveFix(cwd, violation, rulePattern);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function ensureProjectManifestForEdit(
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
cwd: string,
|
|
59
|
+
resolved: ResolvedManifest,
|
|
60
60
|
): Promise<string> {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
const projectManifestPath = join(cwd, ".claude", "gitignore.json");
|
|
62
|
+
if (existsSync(projectManifestPath)) {
|
|
63
|
+
const existing = await readFile(projectManifestPath, "utf8");
|
|
64
|
+
if (existing.trim().length > 0) return projectManifestPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const manifest: GitignoreManifest = { ignore: [], track: [] };
|
|
68
|
+
for (const rule of resolved.rules) {
|
|
69
|
+
if (rule.action === "ignore") manifest.ignore.push(rule.pattern);
|
|
70
|
+
else manifest.track.push(rule.pattern);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await mkdir(dirname(projectManifestPath), { recursive: true });
|
|
74
|
+
await writeFile(
|
|
75
|
+
projectManifestPath,
|
|
76
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
77
|
+
);
|
|
78
|
+
return projectManifestPath;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export interface ApplyTemplateResult {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
appliedTo: string;
|
|
83
|
+
added: { ignore: string[]; track: string[] };
|
|
84
|
+
conflicts: string[];
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|
|
@@ -87,78 +90,78 @@ export interface ApplyTemplateResult {
|
|
|
87
90
|
* skipped and reported back to the caller.
|
|
88
91
|
*/
|
|
89
92
|
export async function applyTemplate(
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
cwd: string,
|
|
94
|
+
templateName: string,
|
|
92
95
|
): Promise<ApplyTemplateResult> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
96
|
+
const tpl = BUILTIN_TEMPLATES[templateName];
|
|
97
|
+
if (!tpl) throw new Error(`Unknown template: ${templateName}`);
|
|
98
|
+
|
|
99
|
+
const projectManifestPath = join(cwd, ".claude", "gitignore.json");
|
|
100
|
+
const existing = await readManifestOrEmpty(projectManifestPath);
|
|
101
|
+
|
|
102
|
+
const existingIgnore = new Set(existing.ignore);
|
|
103
|
+
const existingTrack = new Set(existing.track);
|
|
104
|
+
|
|
105
|
+
const conflicts: string[] = [];
|
|
106
|
+
const newIgnore: string[] = [];
|
|
107
|
+
const newTrack: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const p of tpl.ignore) {
|
|
110
|
+
if (existingTrack.has(p)) {
|
|
111
|
+
conflicts.push(p);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!existingIgnore.has(p)) {
|
|
115
|
+
newIgnore.push(p);
|
|
116
|
+
existingIgnore.add(p);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const p of tpl.track) {
|
|
120
|
+
if (existingIgnore.has(p)) {
|
|
121
|
+
conflicts.push(p);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!existingTrack.has(p)) {
|
|
125
|
+
newTrack.push(p);
|
|
126
|
+
existingTrack.add(p);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const merged: GitignoreManifest = {
|
|
131
|
+
ignore: [...existing.ignore, ...newIgnore],
|
|
132
|
+
track: [...existing.track, ...newTrack],
|
|
133
|
+
extends: existing.extends,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await mkdir(dirname(projectManifestPath), { recursive: true });
|
|
137
|
+
await writeFile(projectManifestPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
appliedTo: projectManifestPath,
|
|
141
|
+
added: { ignore: newIgnore, track: newTrack },
|
|
142
|
+
conflicts,
|
|
143
|
+
};
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
async function readManifestOrEmpty(path: string): Promise<GitignoreManifest> {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
if (!existsSync(path)) return { ignore: [], track: [] };
|
|
148
|
+
try {
|
|
149
|
+
const raw = await readFile(path, "utf8");
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
if (!isManifest(parsed)) return { ignore: [], track: [] };
|
|
152
|
+
return parsed;
|
|
153
|
+
} catch {
|
|
154
|
+
return { ignore: [], track: [] };
|
|
155
|
+
}
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
function isManifest(x: unknown): x is GitignoreManifest {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
if (!x || typeof x !== "object") return false;
|
|
160
|
+
const m = x as Record<string, unknown>;
|
|
161
|
+
return (
|
|
162
|
+
Array.isArray(m.ignore) &&
|
|
163
|
+
m.ignore.every((p) => typeof p === "string") &&
|
|
164
|
+
Array.isArray(m.track) &&
|
|
165
|
+
m.track.every((p) => typeof p === "string")
|
|
166
|
+
);
|
|
164
167
|
}
|
|
@@ -24,8 +24,8 @@ export interface VersionDrift {
|
|
|
24
24
|
/** Highest valid semver in a list, or the raw first value if none parse. */
|
|
25
25
|
function maxVersion(versions: string[]): string | null {
|
|
26
26
|
const valid = versions.filter((v) => semver.valid(v));
|
|
27
|
-
|
|
28
|
-
return versions[0] ?? null;
|
|
27
|
+
const highest = valid.sort(semver.rcompare)[0];
|
|
28
|
+
return highest ?? versions[0] ?? null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -86,8 +86,8 @@ export function summarizeClosure(closure: ResolvedClosure): string[] {
|
|
|
86
86
|
const lines: string[] = [];
|
|
87
87
|
const mps = Object.keys(closure.marketplaces);
|
|
88
88
|
if (mps.length) lines.push(`marketplaces ${mps.join(", ")}`);
|
|
89
|
-
const plugins = Object.entries(closure.plugins).map(
|
|
90
|
-
|
|
89
|
+
const plugins = Object.entries(closure.plugins).map(([id, v]) =>
|
|
90
|
+
v === "latest" ? id : `${id}@${v}`,
|
|
91
91
|
);
|
|
92
92
|
if (plugins.length) lines.push(`plugins ${plugins.join(", ")}`);
|
|
93
93
|
const mcps = Object.keys(closure.mcpServers);
|
|
@@ -113,11 +113,13 @@ function getGitRemote(marketplacePath: string): string | undefined {
|
|
|
113
113
|
* Names of the skills under `skillsDir`, recursing through grouping directories.
|
|
114
114
|
*
|
|
115
115
|
* A skill is a directory holding a SKILL.md, and it may sit one or more levels
|
|
116
|
-
* down: dev
|
|
117
|
-
* and so on, and mattpocock groups its 37 under
|
|
118
|
-
* only the top level counted the *groups* — dev
|
|
119
|
-
* disk, mattpocock 5 against 37 — and that
|
|
120
|
-
* panel prints.
|
|
116
|
+
* down: dev nests its skills by category under `skills/frontend/`,
|
|
117
|
+
* `skills/backend/` and so on, and mattpocock groups its 37 under
|
|
118
|
+
* `skills/engineering/`. Reading only the top level counted the *groups* — dev
|
|
119
|
+
* once reported 9 skills against 43 on disk, mattpocock 5 against 37 — and that
|
|
120
|
+
* count is what the plugin detail panel prints. dev also ships a `knowledge/`
|
|
121
|
+
* tree beside `skills/`: reference manuals reached by path, holding no SKILL.md
|
|
122
|
+
* and registering nothing, so this scan correctly never sees them.
|
|
121
123
|
*
|
|
122
124
|
* Depth is capped because this runs over every plugin of every cloned
|
|
123
125
|
* marketplace on each list load, and a skill nested four levels below `skills/`
|