claudeup 6.2.1 → 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__/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 +18 -4
- package/src/__tests__/plugin-setup.test.ts +12 -12
- 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 +13 -3
- 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/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 +9 -6
- package/src/services/plugin-setup.ts +7 -9
- 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/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);
|
package/src/services/manifest.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* structural subset of a v2 manifest entry, so migration is a version bump.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import fs from "fs-extra";
|
|
11
10
|
import path from "node:path";
|
|
11
|
+
import fs from "fs-extra";
|
|
12
12
|
import type { ProfileManifest, ProfileManifestEntry } from "../types/index.js";
|
|
13
13
|
|
|
14
14
|
const PROFILES_FILE = "profiles.json";
|