claudeup 6.2.1 → 6.3.2
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 +40 -15
- 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 +181 -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 +8 -9
- 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
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* with real plugin.json files. runDoctor is tested by mocking the registry.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { tmpdir } from
|
|
17
|
-
import { join } from
|
|
18
|
-
import { checkProject, fixDoctorIssues } from
|
|
19
|
-
import type { DoctorResult } from
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { checkProject, fixDoctorIssues } from "../services/doctor.js";
|
|
19
|
+
import type { DoctorResult } from "../services/doctor.js";
|
|
20
20
|
|
|
21
21
|
// ============================================================
|
|
22
22
|
// HELPERS
|
|
@@ -26,425 +26,438 @@ import type { DoctorResult } from '../services/doctor.js';
|
|
|
26
26
|
* Create a minimal plugin directory with plugin.json and optional template.
|
|
27
27
|
*/
|
|
28
28
|
async function createPluginDir(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
baseDir: string,
|
|
30
|
+
pluginName: string,
|
|
31
|
+
conventions?: {
|
|
32
|
+
gitignore?: { project?: string[] };
|
|
33
|
+
claudemd?: { section: string; template: string; version: string };
|
|
34
|
+
},
|
|
35
|
+
templateContent?: string,
|
|
36
36
|
): Promise<string> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
const pluginDir = join(baseDir, pluginName);
|
|
38
|
+
await mkdir(pluginDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const manifest: Record<string, unknown> = {
|
|
41
|
+
name: pluginName,
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
description: `Test plugin ${pluginName}`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (conventions) {
|
|
47
|
+
manifest.conventions = conventions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await writeFile(
|
|
51
|
+
join(pluginDir, "plugin.json"),
|
|
52
|
+
JSON.stringify(manifest, null, 2),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (templateContent && conventions?.claudemd?.template) {
|
|
56
|
+
const templateDir = join(pluginDir, "templates");
|
|
57
|
+
await mkdir(templateDir, { recursive: true });
|
|
58
|
+
await writeFile(
|
|
59
|
+
join(templateDir, "claude-md-conventions.md"),
|
|
60
|
+
templateContent,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return pluginDir;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
describe(
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
67
|
+
describe("doctor", () => {
|
|
68
|
+
let testDir: string;
|
|
69
|
+
let projectDir: string;
|
|
70
|
+
let pluginsDir: string;
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
testDir = await mkdtemp(join(tmpdir(), "doctor-test-"));
|
|
74
|
+
projectDir = join(testDir, "project");
|
|
75
|
+
pluginsDir = join(testDir, "plugins");
|
|
76
|
+
await mkdir(projectDir, { recursive: true });
|
|
77
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await rm(testDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ============================================================
|
|
85
|
+
// checkProject
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
88
|
+
describe("checkProject", () => {
|
|
89
|
+
it("should report fail when project path does not exist", async () => {
|
|
90
|
+
const result = await checkProject(
|
|
91
|
+
"/nonexistent/path/that/does/not/exist",
|
|
92
|
+
["some-plugin@magus"],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(result.projectPath).toBe("/nonexistent/path/that/does/not/exist");
|
|
96
|
+
expect(result.checks).toHaveLength(1);
|
|
97
|
+
expect(result.checks[0].name).toBe("project-accessible");
|
|
98
|
+
expect(result.checks[0].status).toBe("fail");
|
|
99
|
+
expect(result.checks[0].fixable).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should return empty checks when plugins have no conventions", async () => {
|
|
103
|
+
// checkProject with pluginIds that cannot be resolved will produce no convention checks
|
|
104
|
+
// (since resolvePluginPath returns null for unknown IDs)
|
|
105
|
+
const result = await checkProject(projectDir, [
|
|
106
|
+
"nonexistent-plugin@nonexistent-marketplace",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// No convention checks should be generated since the plugin could not be resolved
|
|
110
|
+
const conventionChecks = result.checks.filter(
|
|
111
|
+
(c) => c.name !== "project-accessible",
|
|
112
|
+
);
|
|
113
|
+
expect(conventionChecks).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// checkProject with real conventions (using checkProject directly with
|
|
119
|
+
// pre-setup gitignore/CLAUDE.md, bypassing plugin resolution)
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
describe("checkProject gitignore checks", () => {
|
|
123
|
+
it("should pass when gitignore has all required entries", async () => {
|
|
124
|
+
// Since checkProject resolves plugins via marketplace, we test the
|
|
125
|
+
// underlying behavior by checking project files directly.
|
|
126
|
+
// For unit testing, we verify checkProject handles the missing-plugin case gracefully.
|
|
127
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
128
|
+
await writeFile(gitignorePath, "node_modules/\n.mnemex/\n");
|
|
129
|
+
|
|
130
|
+
// With no resolvable plugins, no gitignore checks are generated
|
|
131
|
+
const result = await checkProject(projectDir, []);
|
|
132
|
+
expect(result.checks).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("checkProject CLAUDE.md checks", () => {
|
|
137
|
+
it("should detect malformed CLAUDE.md markers", async () => {
|
|
138
|
+
const claudeMdPath = join(projectDir, "CLAUDE.md");
|
|
139
|
+
// Write CLAUDE.md with unclosed marker
|
|
140
|
+
await writeFile(
|
|
141
|
+
claudeMdPath,
|
|
142
|
+
"# Project\n\n<!-- BEGIN magus:test-plugin v1.0.0 -->\nSome content\n",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// checkProject won't find the marker issue unless it resolves plugins.
|
|
146
|
+
// But the parse is done on the file itself. Let's verify via parseClaudeMdSections directly.
|
|
147
|
+
const { parseClaudeMdSections } = await import(
|
|
148
|
+
"../services/conventions-manager.js"
|
|
149
|
+
);
|
|
150
|
+
const parseResult = await parseClaudeMdSections(claudeMdPath);
|
|
151
|
+
expect(parseResult.errors).toHaveLength(1);
|
|
152
|
+
expect(parseResult.errors[0].type).toBe("missing_end");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ============================================================
|
|
157
|
+
// runDoctor with no registered projects
|
|
158
|
+
// ============================================================
|
|
159
|
+
|
|
160
|
+
describe("runDoctor", () => {
|
|
161
|
+
it("should return empty results when no projects are registered", async () => {
|
|
162
|
+
// Mock the registry to return empty
|
|
163
|
+
const doctorModule = await import("../services/doctor.js");
|
|
164
|
+
|
|
165
|
+
// We can't easily mock the internal collectProjects, but we can
|
|
166
|
+
// test via the exported runDoctor if the registry is empty.
|
|
167
|
+
// In practice, runDoctor reads the real registry, so we test the
|
|
168
|
+
// output shape is correct.
|
|
169
|
+
const { runDoctor } = doctorModule;
|
|
170
|
+
const result = await runDoctor();
|
|
171
|
+
|
|
172
|
+
// The result shape should be valid even if we have real projects
|
|
173
|
+
expect(result).toHaveProperty("projects");
|
|
174
|
+
expect(result).toHaveProperty("summary");
|
|
175
|
+
expect(result.summary).toHaveProperty("totalProjects");
|
|
176
|
+
expect(result.summary).toHaveProperty("totalChecks");
|
|
177
|
+
expect(result.summary).toHaveProperty("passed");
|
|
178
|
+
expect(result.summary).toHaveProperty("warnings");
|
|
179
|
+
expect(result.summary).toHaveProperty("failures");
|
|
180
|
+
expect(result.summary).toHaveProperty("fixable");
|
|
181
|
+
expect(typeof result.summary.totalProjects).toBe("number");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should compute summary counts correctly", async () => {
|
|
185
|
+
// Build a DoctorResult manually and verify summary computation
|
|
186
|
+
// This tests the summary logic without needing real plugin resolution
|
|
187
|
+
const { runDoctor } = await import("../services/doctor.js");
|
|
188
|
+
const result = await runDoctor();
|
|
189
|
+
|
|
190
|
+
// Verify summary math
|
|
191
|
+
const { summary, projects } = result;
|
|
192
|
+
let expectedChecks = 0;
|
|
193
|
+
let expectedPassed = 0;
|
|
194
|
+
let expectedWarnings = 0;
|
|
195
|
+
let expectedFailures = 0;
|
|
196
|
+
let expectedFixable = 0;
|
|
197
|
+
|
|
198
|
+
for (const project of projects) {
|
|
199
|
+
for (const check of project.checks) {
|
|
200
|
+
expectedChecks++;
|
|
201
|
+
if (check.status === "pass") expectedPassed++;
|
|
202
|
+
else if (check.status === "warn") expectedWarnings++;
|
|
203
|
+
else if (check.status === "fail") expectedFailures++;
|
|
204
|
+
if (check.fixable) expectedFixable++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
expect(summary.totalChecks).toBe(expectedChecks);
|
|
209
|
+
expect(summary.passed).toBe(expectedPassed);
|
|
210
|
+
expect(summary.warnings).toBe(expectedWarnings);
|
|
211
|
+
expect(summary.failures).toBe(expectedFailures);
|
|
212
|
+
expect(summary.fixable).toBe(expectedFixable);
|
|
213
|
+
expect(summary.totalProjects).toBe(projects.length);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ============================================================
|
|
218
|
+
// fixDoctorIssues
|
|
219
|
+
// ============================================================
|
|
220
|
+
|
|
221
|
+
describe("fixDoctorIssues", () => {
|
|
222
|
+
it("should add missing gitignore entries", async () => {
|
|
223
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
224
|
+
await writeFile(gitignorePath, "node_modules/\n");
|
|
225
|
+
|
|
226
|
+
const doctorResult: DoctorResult = {
|
|
227
|
+
projects: [
|
|
228
|
+
{
|
|
229
|
+
projectPath: projectDir,
|
|
230
|
+
plugins: ["test-plugin@magus"],
|
|
231
|
+
checks: [
|
|
232
|
+
{
|
|
233
|
+
name: "gitignore-entries",
|
|
234
|
+
status: "fail",
|
|
235
|
+
message: ".gitignore missing: .mnemex/",
|
|
236
|
+
fixable: true,
|
|
237
|
+
_fixData: {
|
|
238
|
+
type: "add-gitignore-entries",
|
|
239
|
+
gitignorePath,
|
|
240
|
+
entries: [".mnemex/"],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
summary: {
|
|
247
|
+
totalProjects: 1,
|
|
248
|
+
totalChecks: 1,
|
|
249
|
+
passed: 0,
|
|
250
|
+
warnings: 0,
|
|
251
|
+
failures: 1,
|
|
252
|
+
fixable: 1,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
257
|
+
|
|
258
|
+
expect(fixResult.fixed).toBe(1);
|
|
259
|
+
expect(fixResult.failed).toBe(0);
|
|
260
|
+
|
|
261
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
262
|
+
expect(content).toContain(".mnemex/");
|
|
263
|
+
expect(content).toContain("node_modules/");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should inject missing CLAUDE.md sections", async () => {
|
|
267
|
+
const claudeMdPath = join(projectDir, "CLAUDE.md");
|
|
268
|
+
await writeFile(claudeMdPath, "# My Project\n\nSome existing content.\n");
|
|
269
|
+
|
|
270
|
+
// Create a plugin dir with template
|
|
271
|
+
const pluginDir = await createPluginDir(
|
|
272
|
+
pluginsDir,
|
|
273
|
+
"test-plugin",
|
|
274
|
+
{
|
|
275
|
+
claudemd: {
|
|
276
|
+
section: "test-plugin",
|
|
277
|
+
template: "./templates/claude-md-conventions.md",
|
|
278
|
+
version: "1.0.0",
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
"## Test Plugin\n\nDo not commit .test-data/ to git.",
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const doctorResult: DoctorResult = {
|
|
285
|
+
projects: [
|
|
286
|
+
{
|
|
287
|
+
projectPath: projectDir,
|
|
288
|
+
plugins: ["test-plugin@magus"],
|
|
289
|
+
checks: [
|
|
290
|
+
{
|
|
291
|
+
name: "claudemd-section-test-plugin",
|
|
292
|
+
status: "warn",
|
|
293
|
+
message: "CLAUDE.md missing test-plugin conventions",
|
|
294
|
+
fixable: true,
|
|
295
|
+
_fixData: {
|
|
296
|
+
type: "inject-section",
|
|
297
|
+
claudeMdPath,
|
|
298
|
+
pluginName: "test-plugin",
|
|
299
|
+
pluginPath: pluginDir,
|
|
300
|
+
conventions: {
|
|
301
|
+
claudemd: {
|
|
302
|
+
section: "test-plugin",
|
|
303
|
+
template: "./templates/claude-md-conventions.md",
|
|
304
|
+
version: "1.0.0",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
summary: {
|
|
313
|
+
totalProjects: 1,
|
|
314
|
+
totalChecks: 1,
|
|
315
|
+
passed: 0,
|
|
316
|
+
warnings: 1,
|
|
317
|
+
failures: 0,
|
|
318
|
+
fixable: 1,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
323
|
+
|
|
324
|
+
expect(fixResult.fixed).toBe(1);
|
|
325
|
+
expect(fixResult.failed).toBe(0);
|
|
326
|
+
|
|
327
|
+
const content = await readFile(claudeMdPath, "utf-8");
|
|
328
|
+
expect(content).toContain("<!-- BEGIN magus:test-plugin v1.0.0 -->");
|
|
329
|
+
expect(content).toContain("Do not commit .test-data/ to git.");
|
|
330
|
+
expect(content).toContain("<!-- END magus:test-plugin -->");
|
|
331
|
+
expect(content).toContain("# My Project");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should create gitignore when project has none", async () => {
|
|
335
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
336
|
+
expect(existsSync(gitignorePath)).toBe(false);
|
|
337
|
+
|
|
338
|
+
const doctorResult: DoctorResult = {
|
|
339
|
+
projects: [
|
|
340
|
+
{
|
|
341
|
+
projectPath: projectDir,
|
|
342
|
+
plugins: ["test-plugin@magus"],
|
|
343
|
+
checks: [
|
|
344
|
+
{
|
|
345
|
+
name: "gitignore-exists",
|
|
346
|
+
status: "warn",
|
|
347
|
+
message: ".gitignore does not exist",
|
|
348
|
+
fixable: true,
|
|
349
|
+
_fixData: {
|
|
350
|
+
type: "create-gitignore",
|
|
351
|
+
projectPath: projectDir,
|
|
352
|
+
entries: [".mnemex/", ".claudemem/"],
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
summary: {
|
|
359
|
+
totalProjects: 1,
|
|
360
|
+
totalChecks: 1,
|
|
361
|
+
passed: 0,
|
|
362
|
+
warnings: 1,
|
|
363
|
+
failures: 0,
|
|
364
|
+
fixable: 1,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
369
|
+
|
|
370
|
+
expect(fixResult.fixed).toBe(1);
|
|
371
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
372
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
373
|
+
expect(content).toContain(".mnemex/");
|
|
374
|
+
expect(content).toContain(".claudemem/");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should skip unfixable issues", async () => {
|
|
378
|
+
const doctorResult: DoctorResult = {
|
|
379
|
+
projects: [
|
|
380
|
+
{
|
|
381
|
+
projectPath: "/nonexistent/path",
|
|
382
|
+
plugins: ["some-plugin@magus"],
|
|
383
|
+
checks: [
|
|
384
|
+
{
|
|
385
|
+
name: "project-accessible",
|
|
386
|
+
status: "fail",
|
|
387
|
+
message: "Project path does not exist",
|
|
388
|
+
fixable: false,
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
summary: {
|
|
394
|
+
totalProjects: 1,
|
|
395
|
+
totalChecks: 1,
|
|
396
|
+
passed: 0,
|
|
397
|
+
warnings: 0,
|
|
398
|
+
failures: 1,
|
|
399
|
+
fixable: 0,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
404
|
+
|
|
405
|
+
expect(fixResult.fixed).toBe(0);
|
|
406
|
+
expect(fixResult.failed).toBe(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("should report fix count correctly with mixed results", async () => {
|
|
410
|
+
const gitignorePath = join(projectDir, ".gitignore");
|
|
411
|
+
await writeFile(gitignorePath, "node_modules/\n");
|
|
412
|
+
|
|
413
|
+
const doctorResult: DoctorResult = {
|
|
414
|
+
projects: [
|
|
415
|
+
{
|
|
416
|
+
projectPath: projectDir,
|
|
417
|
+
plugins: ["test-plugin@magus"],
|
|
418
|
+
checks: [
|
|
419
|
+
{
|
|
420
|
+
name: "gitignore-entries",
|
|
421
|
+
status: "fail",
|
|
422
|
+
message: ".gitignore missing: .mnemex/",
|
|
423
|
+
fixable: true,
|
|
424
|
+
_fixData: {
|
|
425
|
+
type: "add-gitignore-entries",
|
|
426
|
+
gitignorePath,
|
|
427
|
+
entries: [".mnemex/"],
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "project-accessible",
|
|
432
|
+
status: "fail",
|
|
433
|
+
message: "Project path does not exist",
|
|
434
|
+
fixable: false,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "claudemd-section-test-plugin",
|
|
438
|
+
status: "warn",
|
|
439
|
+
message: "CLAUDE.md missing test-plugin conventions",
|
|
440
|
+
fixable: true,
|
|
441
|
+
// Missing _fixData -- should count as failed
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
summary: {
|
|
447
|
+
totalProjects: 1,
|
|
448
|
+
totalChecks: 3,
|
|
449
|
+
passed: 0,
|
|
450
|
+
warnings: 1,
|
|
451
|
+
failures: 2,
|
|
452
|
+
fixable: 2,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
457
|
+
|
|
458
|
+
// 1 fixed (gitignore), 1 failed (missing _fixData), 1 skipped (not fixable)
|
|
459
|
+
expect(fixResult.fixed).toBe(1);
|
|
460
|
+
expect(fixResult.failed).toBe(1);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
450
463
|
});
|