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
package/src/services/doctor.ts
CHANGED
|
@@ -12,90 +12,97 @@
|
|
|
12
12
|
* - plugin-manager.js (resolve plugin source paths)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync } from
|
|
16
|
-
import { join } from
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import type { PluginConventions } from "../types/index.js";
|
|
18
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
19
|
+
import { readInstalledPluginsRegistry } from "./claude-settings.js";
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from
|
|
28
|
-
import {
|
|
29
|
-
import type { PluginConventions } from '../types/index.js';
|
|
21
|
+
checkGitignoreEntries,
|
|
22
|
+
ensureGitignoreEntries,
|
|
23
|
+
injectClaudeMdSection,
|
|
24
|
+
parseClaudeMdSections,
|
|
25
|
+
readConventionTemplate,
|
|
26
|
+
removeClaudeMdSection,
|
|
27
|
+
resolvePluginConventions,
|
|
28
|
+
} from "./conventions-manager.js";
|
|
29
|
+
import { getLocalMarketplacesInfo } from "./plugin-manager.js";
|
|
30
30
|
|
|
31
31
|
// ============================================================
|
|
32
32
|
// TYPES
|
|
33
33
|
// ============================================================
|
|
34
34
|
|
|
35
|
-
export type CheckStatus =
|
|
35
|
+
export type CheckStatus = "pass" | "warn" | "fail";
|
|
36
36
|
|
|
37
37
|
export interface DoctorCheck {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
name: string;
|
|
39
|
+
status: CheckStatus;
|
|
40
|
+
message: string;
|
|
41
|
+
fixable: boolean;
|
|
42
|
+
/** Metadata used by fixDoctorIssues to know what to fix */
|
|
43
|
+
_fixData?: unknown;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface ProjectDoctorResult {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
projectPath: string;
|
|
48
|
+
checks: DoctorCheck[];
|
|
49
|
+
plugins: string[];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface DoctorResult {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
projects: ProjectDoctorResult[];
|
|
54
|
+
summary: {
|
|
55
|
+
totalProjects: number;
|
|
56
|
+
totalChecks: number;
|
|
57
|
+
passed: number;
|
|
58
|
+
warnings: number;
|
|
59
|
+
failures: number;
|
|
60
|
+
fixable: number;
|
|
61
|
+
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export type DoctorLogger = (
|
|
64
|
+
export type DoctorLogger = (
|
|
65
|
+
level: "info" | "warn" | "error",
|
|
66
|
+
message: string,
|
|
67
|
+
) => void;
|
|
65
68
|
|
|
66
69
|
// ============================================================
|
|
67
70
|
// INTERNAL HELPERS
|
|
68
71
|
// ============================================================
|
|
69
72
|
|
|
70
|
-
import { homedir } from
|
|
73
|
+
import { homedir } from "node:os";
|
|
71
74
|
|
|
72
75
|
/** Path constants matching plugin-manager.ts */
|
|
73
|
-
const MARKETPLACES_DIR = join(homedir(),
|
|
76
|
+
const MARKETPLACES_DIR = join(homedir(), ".claude", "plugins", "marketplaces");
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
79
|
* Resolve a plugin ID to its filesystem source path.
|
|
77
80
|
* Mirrors the private resolvePluginSourcePath in plugin-manager.ts.
|
|
78
81
|
*/
|
|
79
82
|
async function resolvePluginPath(pluginId: string): Promise<string | null> {
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const parsed = parsePluginId(pluginId);
|
|
84
|
+
if (!parsed) return null;
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const { pluginName, marketplace: mpName } = parsed;
|
|
87
|
+
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
88
|
+
const localMp = localMarketplaces.get(mpName);
|
|
89
|
+
if (!localMp) return null;
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const localPlugin = localMp.plugins.find((p) => p.name === pluginName);
|
|
92
|
+
if (!localPlugin?.source || typeof localPlugin.source !== "string")
|
|
93
|
+
return null;
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
const marketplacePath = join(MARKETPLACES_DIR, mpName);
|
|
96
|
+
const pluginPath = join(
|
|
97
|
+
marketplacePath,
|
|
98
|
+
localPlugin.source.replace("./", ""),
|
|
99
|
+
);
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
if (existsSync(pluginPath)) {
|
|
102
|
+
return pluginPath;
|
|
103
|
+
}
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
return null;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
/**
|
|
@@ -103,37 +110,37 @@ async function resolvePluginPath(pluginId: string): Promise<string | null> {
|
|
|
103
110
|
* along with the plugin IDs installed in each project.
|
|
104
111
|
*/
|
|
105
112
|
async function collectProjects(): Promise<Map<string, string[]>> {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return projectMap;
|
|
113
|
+
const registry = await readInstalledPluginsRegistry();
|
|
114
|
+
const projectMap = new Map<string, string[]>();
|
|
115
|
+
|
|
116
|
+
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.projectPath) {
|
|
119
|
+
const ids = projectMap.get(entry.projectPath) ?? [];
|
|
120
|
+
ids.push(pluginId);
|
|
121
|
+
projectMap.set(entry.projectPath, ids);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return projectMap;
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
/**
|
|
124
130
|
* Resolve conventions for a plugin, returning both the conventions object
|
|
125
131
|
* and the plugin source path.
|
|
126
132
|
*/
|
|
127
|
-
async function resolveConventionsForPlugin(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
async function resolveConventionsForPlugin(pluginId: string): Promise<{
|
|
134
|
+
conventions: PluginConventions | null;
|
|
135
|
+
pluginPath: string | null;
|
|
136
|
+
}> {
|
|
137
|
+
const pluginPath = await resolvePluginPath(pluginId);
|
|
138
|
+
if (!pluginPath) {
|
|
139
|
+
return { conventions: null, pluginPath: null };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const conventions = await resolvePluginConventions(pluginPath);
|
|
143
|
+
return { conventions, pluginPath };
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// ============================================================
|
|
@@ -149,179 +156,205 @@ async function resolveConventionsForPlugin(
|
|
|
149
156
|
* @returns Check results for this project
|
|
150
157
|
*/
|
|
151
158
|
export async function checkProject(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
projectPath: string,
|
|
160
|
+
pluginIds: string[],
|
|
161
|
+
logger?: DoctorLogger,
|
|
155
162
|
): Promise<ProjectDoctorResult> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
163
|
+
const checks: DoctorCheck[] = [];
|
|
164
|
+
|
|
165
|
+
// Check 6: Project path is accessible
|
|
166
|
+
if (!existsSync(projectPath)) {
|
|
167
|
+
checks.push({
|
|
168
|
+
name: "project-accessible",
|
|
169
|
+
status: "fail",
|
|
170
|
+
message: "Project path does not exist",
|
|
171
|
+
fixable: false,
|
|
172
|
+
});
|
|
173
|
+
return { projectPath, checks, plugins: pluginIds };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Aggregate all required gitignore entries and claudemd conventions across plugins
|
|
177
|
+
const allGitignoreEntries: string[] = [];
|
|
178
|
+
const pluginConventionsMap = new Map<
|
|
179
|
+
string,
|
|
180
|
+
{ conventions: PluginConventions; pluginPath: string }
|
|
181
|
+
>();
|
|
182
|
+
|
|
183
|
+
for (const pluginId of pluginIds) {
|
|
184
|
+
const { conventions, pluginPath } =
|
|
185
|
+
await resolveConventionsForPlugin(pluginId);
|
|
186
|
+
if (conventions && pluginPath) {
|
|
187
|
+
const parsed = parsePluginId(pluginId);
|
|
188
|
+
if (parsed) {
|
|
189
|
+
pluginConventionsMap.set(parsed.pluginName, {
|
|
190
|
+
conventions,
|
|
191
|
+
pluginPath,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (conventions.gitignore?.project) {
|
|
196
|
+
allGitignoreEntries.push(...conventions.gitignore.project);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check 1: .gitignore exists
|
|
202
|
+
const gitignorePath = join(projectPath, ".gitignore");
|
|
203
|
+
if (!existsSync(gitignorePath)) {
|
|
204
|
+
if (allGitignoreEntries.length > 0) {
|
|
205
|
+
checks.push({
|
|
206
|
+
name: "gitignore-exists",
|
|
207
|
+
status: "warn",
|
|
208
|
+
message: ".gitignore does not exist",
|
|
209
|
+
fixable: true,
|
|
210
|
+
_fixData: {
|
|
211
|
+
type: "create-gitignore",
|
|
212
|
+
projectPath,
|
|
213
|
+
entries: allGitignoreEntries,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} else if (allGitignoreEntries.length > 0) {
|
|
218
|
+
// Check 2: Required gitignore entries present
|
|
219
|
+
const { missing } = await checkGitignoreEntries(
|
|
220
|
+
gitignorePath,
|
|
221
|
+
allGitignoreEntries,
|
|
222
|
+
);
|
|
223
|
+
if (missing.length > 0) {
|
|
224
|
+
checks.push({
|
|
225
|
+
name: "gitignore-entries",
|
|
226
|
+
status: "fail",
|
|
227
|
+
message: `.gitignore missing: ${missing.join(", ")}`,
|
|
228
|
+
fixable: true,
|
|
229
|
+
_fixData: {
|
|
230
|
+
type: "add-gitignore-entries",
|
|
231
|
+
gitignorePath,
|
|
232
|
+
entries: missing,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
checks.push({
|
|
237
|
+
name: "gitignore-entries",
|
|
238
|
+
status: "pass",
|
|
239
|
+
message: ".gitignore has all required entries",
|
|
240
|
+
fixable: false,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check 5: CLAUDE.md markers well-formed (run before section checks)
|
|
246
|
+
const claudeMdPath = join(projectPath, "CLAUDE.md");
|
|
247
|
+
let hasMarkerErrors = false;
|
|
248
|
+
|
|
249
|
+
if (existsSync(claudeMdPath)) {
|
|
250
|
+
const parseResult = await parseClaudeMdSections(claudeMdPath);
|
|
251
|
+
|
|
252
|
+
if (parseResult.errors.length > 0) {
|
|
253
|
+
hasMarkerErrors = true;
|
|
254
|
+
const errorDesc = parseResult.errors
|
|
255
|
+
.map((e) => {
|
|
256
|
+
if (e.type === "missing_end") return `missing END for "${e.section}"`;
|
|
257
|
+
if (e.type === "mismatched_end")
|
|
258
|
+
return `mismatched END for "${e.section}" (expected "${(e as { expected: string }).expected}")`;
|
|
259
|
+
if (e.type === "duplicate_section")
|
|
260
|
+
return `duplicate section "${e.section}"`;
|
|
261
|
+
if (e.type === "marker_in_code_block")
|
|
262
|
+
return `marker for "${e.section}" inside code block`;
|
|
263
|
+
return "unknown error";
|
|
264
|
+
})
|
|
265
|
+
.join("; ");
|
|
266
|
+
|
|
267
|
+
checks.push({
|
|
268
|
+
name: "claudemd-markers",
|
|
269
|
+
status: "fail",
|
|
270
|
+
message: `CLAUDE.md has malformed markers: ${errorDesc}`,
|
|
271
|
+
fixable: true,
|
|
272
|
+
_fixData: {
|
|
273
|
+
type: "repair-markers",
|
|
274
|
+
claudeMdPath,
|
|
275
|
+
errors: parseResult.errors,
|
|
276
|
+
pluginConventionsMap: Object.fromEntries(pluginConventionsMap),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check 3 & 4: CLAUDE.md convention sections present and current
|
|
282
|
+
if (!hasMarkerErrors) {
|
|
283
|
+
for (const [
|
|
284
|
+
pluginName,
|
|
285
|
+
{ conventions, pluginPath },
|
|
286
|
+
] of pluginConventionsMap) {
|
|
287
|
+
if (!conventions.claudemd) continue;
|
|
288
|
+
|
|
289
|
+
const existingSection = parseResult.sections.find(
|
|
290
|
+
(s) => s.section === pluginName,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (!existingSection) {
|
|
294
|
+
checks.push({
|
|
295
|
+
name: `claudemd-section-${pluginName}`,
|
|
296
|
+
status: "warn",
|
|
297
|
+
message: `CLAUDE.md missing ${pluginName} conventions`,
|
|
298
|
+
fixable: true,
|
|
299
|
+
_fixData: {
|
|
300
|
+
type: "inject-section",
|
|
301
|
+
claudeMdPath,
|
|
302
|
+
pluginName,
|
|
303
|
+
pluginPath,
|
|
304
|
+
conventions,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
} else if (existingSection.version !== conventions.claudemd.version) {
|
|
308
|
+
checks.push({
|
|
309
|
+
name: `claudemd-section-${pluginName}`,
|
|
310
|
+
status: "warn",
|
|
311
|
+
message: `CLAUDE.md ${pluginName} conventions outdated (v${existingSection.version} -> v${conventions.claudemd.version})`,
|
|
312
|
+
fixable: true,
|
|
313
|
+
_fixData: {
|
|
314
|
+
type: "update-section",
|
|
315
|
+
claudeMdPath,
|
|
316
|
+
pluginName,
|
|
317
|
+
pluginPath,
|
|
318
|
+
conventions,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
checks.push({
|
|
323
|
+
name: `claudemd-section-${pluginName}`,
|
|
324
|
+
status: "pass",
|
|
325
|
+
message: `CLAUDE.md has ${pluginName} conventions (v${conventions.claudemd.version})`,
|
|
326
|
+
fixable: false,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// CLAUDE.md doesn't exist -- warn for each plugin that needs a section
|
|
333
|
+
for (const [
|
|
334
|
+
pluginName,
|
|
335
|
+
{ conventions, pluginPath },
|
|
336
|
+
] of pluginConventionsMap) {
|
|
337
|
+
if (!conventions.claudemd) continue;
|
|
338
|
+
|
|
339
|
+
checks.push({
|
|
340
|
+
name: `claudemd-section-${pluginName}`,
|
|
341
|
+
status: "warn",
|
|
342
|
+
message: `CLAUDE.md missing ${pluginName} conventions`,
|
|
343
|
+
fixable: true,
|
|
344
|
+
_fixData: {
|
|
345
|
+
type: "inject-section",
|
|
346
|
+
claudeMdPath,
|
|
347
|
+
pluginName,
|
|
348
|
+
pluginPath,
|
|
349
|
+
conventions,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
logger?.("info", `Checked ${projectPath}: ${checks.length} checks`);
|
|
356
|
+
|
|
357
|
+
return { projectPath, checks, plugins: pluginIds };
|
|
325
358
|
}
|
|
326
359
|
|
|
327
360
|
// ============================================================
|
|
@@ -334,48 +367,46 @@ export async function checkProject(
|
|
|
334
367
|
* @param logger - Optional logger
|
|
335
368
|
* @returns Full doctor result with per-project checks and summary
|
|
336
369
|
*/
|
|
337
|
-
export async function runDoctor(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
},
|
|
378
|
-
};
|
|
370
|
+
export async function runDoctor(logger?: DoctorLogger): Promise<DoctorResult> {
|
|
371
|
+
const projectMap = await collectProjects();
|
|
372
|
+
|
|
373
|
+
logger?.("info", `Found ${projectMap.size} project(s) to check`);
|
|
374
|
+
|
|
375
|
+
const projects: ProjectDoctorResult[] = [];
|
|
376
|
+
|
|
377
|
+
for (const [projectPath, pluginIds] of projectMap) {
|
|
378
|
+
const result = await checkProject(projectPath, pluginIds, logger);
|
|
379
|
+
projects.push(result);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Compute summary
|
|
383
|
+
let totalChecks = 0;
|
|
384
|
+
let passed = 0;
|
|
385
|
+
let warnings = 0;
|
|
386
|
+
let failures = 0;
|
|
387
|
+
let fixable = 0;
|
|
388
|
+
|
|
389
|
+
for (const project of projects) {
|
|
390
|
+
for (const check of project.checks) {
|
|
391
|
+
totalChecks++;
|
|
392
|
+
if (check.status === "pass") passed++;
|
|
393
|
+
else if (check.status === "warn") warnings++;
|
|
394
|
+
else if (check.status === "fail") failures++;
|
|
395
|
+
if (check.fixable) fixable++;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
projects,
|
|
401
|
+
summary: {
|
|
402
|
+
totalProjects: projects.length,
|
|
403
|
+
totalChecks,
|
|
404
|
+
passed,
|
|
405
|
+
warnings,
|
|
406
|
+
failures,
|
|
407
|
+
fixable,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
379
410
|
}
|
|
380
411
|
|
|
381
412
|
// ============================================================
|
|
@@ -391,119 +422,149 @@ export async function runDoctor(
|
|
|
391
422
|
* @returns Number of issues fixed and failed
|
|
392
423
|
*/
|
|
393
424
|
export async function fixDoctorIssues(
|
|
394
|
-
|
|
395
|
-
|
|
425
|
+
result: DoctorResult,
|
|
426
|
+
logger?: DoctorLogger,
|
|
396
427
|
): Promise<{ fixed: number; failed: number }> {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
428
|
+
let fixed = 0;
|
|
429
|
+
let failed = 0;
|
|
430
|
+
|
|
431
|
+
for (const project of result.projects) {
|
|
432
|
+
for (const check of project.checks) {
|
|
433
|
+
if (!check.fixable || check.status === "pass") continue;
|
|
434
|
+
|
|
435
|
+
const fixData = check._fixData as Record<string, unknown> | undefined;
|
|
436
|
+
if (!fixData) {
|
|
437
|
+
failed++;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
switch (fixData.type) {
|
|
443
|
+
case "create-gitignore":
|
|
444
|
+
case "add-gitignore-entries": {
|
|
445
|
+
const gitignorePath =
|
|
446
|
+
fixData.type === "create-gitignore"
|
|
447
|
+
? join(fixData.projectPath as string, ".gitignore")
|
|
448
|
+
: (fixData.gitignorePath as string);
|
|
449
|
+
const entries = fixData.entries as string[];
|
|
450
|
+
|
|
451
|
+
await ensureGitignoreEntries(
|
|
452
|
+
gitignorePath,
|
|
453
|
+
entries,
|
|
454
|
+
"# Managed by magus plugin conventions",
|
|
455
|
+
);
|
|
456
|
+
logger?.(
|
|
457
|
+
"info",
|
|
458
|
+
`Fixed: added ${entries.join(", ")} to ${gitignorePath}`,
|
|
459
|
+
);
|
|
460
|
+
fixed++;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case "inject-section":
|
|
465
|
+
case "update-section": {
|
|
466
|
+
const claudeMdPath = fixData.claudeMdPath as string;
|
|
467
|
+
const pluginPath = fixData.pluginPath as string;
|
|
468
|
+
const conventions = fixData.conventions as PluginConventions;
|
|
469
|
+
|
|
470
|
+
if (!conventions.claudemd) {
|
|
471
|
+
failed++;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const template = await readConventionTemplate(
|
|
476
|
+
pluginPath,
|
|
477
|
+
conventions.claudemd.template,
|
|
478
|
+
);
|
|
479
|
+
const injectResult = await injectClaudeMdSection(
|
|
480
|
+
claudeMdPath,
|
|
481
|
+
conventions.claudemd.section,
|
|
482
|
+
conventions.claudemd.version,
|
|
483
|
+
template,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (injectResult.action === "error") {
|
|
487
|
+
logger?.(
|
|
488
|
+
"error",
|
|
489
|
+
`Failed to fix ${claudeMdPath}: ${injectResult.error}`,
|
|
490
|
+
);
|
|
491
|
+
failed++;
|
|
492
|
+
} else {
|
|
493
|
+
logger?.(
|
|
494
|
+
"info",
|
|
495
|
+
`Fixed: ${injectResult.action} ${conventions.claudemd.section} in ${claudeMdPath}`,
|
|
496
|
+
);
|
|
497
|
+
fixed++;
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case "repair-markers": {
|
|
503
|
+
const claudeMdPath = fixData.claudeMdPath as string;
|
|
504
|
+
const errors = fixData.errors as Array<{
|
|
505
|
+
type: string;
|
|
506
|
+
section: string;
|
|
507
|
+
}>;
|
|
508
|
+
const conventionsMap = fixData.pluginConventionsMap as Record<
|
|
509
|
+
string,
|
|
510
|
+
{ conventions: PluginConventions; pluginPath: string }
|
|
511
|
+
>;
|
|
512
|
+
|
|
513
|
+
// Collect unique sections with errors
|
|
514
|
+
const brokenSections = new Set(errors.map((e) => e.section));
|
|
515
|
+
|
|
516
|
+
for (const section of brokenSections) {
|
|
517
|
+
// Remove the broken section (force=true to bypass parse error guard)
|
|
518
|
+
await removeClaudeMdSection(claudeMdPath, section, {
|
|
519
|
+
force: true,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Re-inject if we have conventions for it
|
|
523
|
+
const entry = conventionsMap[section];
|
|
524
|
+
if (entry?.conventions?.claudemd) {
|
|
525
|
+
try {
|
|
526
|
+
const template = await readConventionTemplate(
|
|
527
|
+
entry.pluginPath,
|
|
528
|
+
entry.conventions.claudemd.template,
|
|
529
|
+
);
|
|
530
|
+
await injectClaudeMdSection(
|
|
531
|
+
claudeMdPath,
|
|
532
|
+
entry.conventions.claudemd.section,
|
|
533
|
+
entry.conventions.claudemd.version,
|
|
534
|
+
template,
|
|
535
|
+
);
|
|
536
|
+
} catch (reInjectErr) {
|
|
537
|
+
const reInjectMsg =
|
|
538
|
+
reInjectErr instanceof Error
|
|
539
|
+
? reInjectErr.message
|
|
540
|
+
: String(reInjectErr);
|
|
541
|
+
logger?.(
|
|
542
|
+
"warn",
|
|
543
|
+
`Re-inject of section "${section}" failed after marker repair: ${reInjectMsg}`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
logger?.(
|
|
550
|
+
"info",
|
|
551
|
+
`Fixed: repaired malformed markers in ${claudeMdPath}`,
|
|
552
|
+
);
|
|
553
|
+
fixed++;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
default:
|
|
558
|
+
failed++;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
} catch (err) {
|
|
562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
563
|
+
logger?.("error", `Fix failed: ${message}`);
|
|
564
|
+
failed++;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return { fixed, failed };
|
|
509
570
|
}
|