claudeup 4.19.0 → 4.22.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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +7 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +0 -4
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +0 -101
- package/src/ui/state/types.ts +0 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -205
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -1018
- package/src/services/alias-store.js +0 -129
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -499
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -355
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/FlagDetailEditor.js +0 -0
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1111
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -769
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -547
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
checkBinaries,
|
|
4
|
+
missingBinaries,
|
|
5
|
+
} from "../services/doctor-bins.js";
|
|
6
|
+
import type { ResolvedBin } from "../types/index.js";
|
|
7
|
+
|
|
8
|
+
const bins: ResolvedBin[] = [
|
|
9
|
+
{ name: "tmux", via: "brew", formula: "tmux", sources: ["plugin:terminal@magus"] },
|
|
10
|
+
{ name: "claudish", via: "bun", package: "claudish", version: "1.2.0", sources: ["profile:x.cliTools"] },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe("checkBinaries", () => {
|
|
14
|
+
test("reports presence and an install command per bin", async () => {
|
|
15
|
+
const present = new Set(["claudish"]);
|
|
16
|
+
const results = await checkBinaries(bins, async (n) => present.has(n));
|
|
17
|
+
|
|
18
|
+
const tmux = results.find((r) => r.name === "tmux")!;
|
|
19
|
+
expect(tmux.present).toBe(false);
|
|
20
|
+
expect(tmux.installCommand).toBe("brew install tmux");
|
|
21
|
+
expect(tmux.sources).toEqual(["plugin:terminal@magus"]);
|
|
22
|
+
|
|
23
|
+
const claudish = results.find((r) => r.name === "claudish")!;
|
|
24
|
+
expect(claudish.present).toBe(true);
|
|
25
|
+
expect(claudish.installCommand).toBe("bun install -g claudish@1.2.0");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("missingBinaries", () => {
|
|
30
|
+
test("returns only the absent ones", async () => {
|
|
31
|
+
const results = await checkBinaries(bins, async (n) => n === "claudish");
|
|
32
|
+
const missing = missingBinaries(results);
|
|
33
|
+
expect(missing.map((m) => m.name)).toEqual(["tmux"]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for doctor service
|
|
3
|
+
*
|
|
4
|
+
* Tests cover:
|
|
5
|
+
* - checkProject: project accessible, gitignore entries, claudemd sections, malformed markers
|
|
6
|
+
* - runDoctor: empty registry, registered projects, summary counts
|
|
7
|
+
* - fixDoctorIssues: add gitignore entries, inject claudemd sections, repair markers
|
|
8
|
+
*
|
|
9
|
+
* Strategy: checkProject and fixDoctorIssues are tested against real temp directories
|
|
10
|
+
* with real plugin.json files. runDoctor is tested by mocking the registry.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
14
|
+
import { mkdtemp, rm, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
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
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// HELPERS
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a minimal plugin directory with plugin.json and optional template.
|
|
27
|
+
*/
|
|
28
|
+
async function createPluginDir(
|
|
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
|
+
): Promise<string> {
|
|
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(join(pluginDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
|
|
51
|
+
|
|
52
|
+
if (templateContent && conventions?.claudemd?.template) {
|
|
53
|
+
const templateDir = join(pluginDir, 'templates');
|
|
54
|
+
await mkdir(templateDir, { recursive: true });
|
|
55
|
+
await writeFile(join(templateDir, 'claude-md-conventions.md'), templateContent);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return pluginDir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('doctor', () => {
|
|
62
|
+
let testDir: string;
|
|
63
|
+
let projectDir: string;
|
|
64
|
+
let pluginsDir: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
testDir = await mkdtemp(join(tmpdir(), 'doctor-test-'));
|
|
68
|
+
projectDir = join(testDir, 'project');
|
|
69
|
+
pluginsDir = join(testDir, 'plugins');
|
|
70
|
+
await mkdir(projectDir, { recursive: true });
|
|
71
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(async () => {
|
|
75
|
+
await rm(testDir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// checkProject
|
|
80
|
+
// ============================================================
|
|
81
|
+
|
|
82
|
+
describe('checkProject', () => {
|
|
83
|
+
it('should report fail when project path does not exist', async () => {
|
|
84
|
+
const result = await checkProject('/nonexistent/path/that/does/not/exist', ['some-plugin@magus']);
|
|
85
|
+
|
|
86
|
+
expect(result.projectPath).toBe('/nonexistent/path/that/does/not/exist');
|
|
87
|
+
expect(result.checks).toHaveLength(1);
|
|
88
|
+
expect(result.checks[0].name).toBe('project-accessible');
|
|
89
|
+
expect(result.checks[0].status).toBe('fail');
|
|
90
|
+
expect(result.checks[0].fixable).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return empty checks when plugins have no conventions', async () => {
|
|
94
|
+
// checkProject with pluginIds that cannot be resolved will produce no convention checks
|
|
95
|
+
// (since resolvePluginPath returns null for unknown IDs)
|
|
96
|
+
const result = await checkProject(projectDir, ['nonexistent-plugin@nonexistent-marketplace']);
|
|
97
|
+
|
|
98
|
+
// No convention checks should be generated since the plugin could not be resolved
|
|
99
|
+
const conventionChecks = result.checks.filter(
|
|
100
|
+
(c) => c.name !== 'project-accessible',
|
|
101
|
+
);
|
|
102
|
+
expect(conventionChecks).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// checkProject with real conventions (using checkProject directly with
|
|
108
|
+
// pre-setup gitignore/CLAUDE.md, bypassing plugin resolution)
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
describe('checkProject gitignore checks', () => {
|
|
112
|
+
it('should pass when gitignore has all required entries', async () => {
|
|
113
|
+
// Since checkProject resolves plugins via marketplace, we test the
|
|
114
|
+
// underlying behavior by checking project files directly.
|
|
115
|
+
// For unit testing, we verify checkProject handles the missing-plugin case gracefully.
|
|
116
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
117
|
+
await writeFile(gitignorePath, 'node_modules/\n.mnemex/\n');
|
|
118
|
+
|
|
119
|
+
// With no resolvable plugins, no gitignore checks are generated
|
|
120
|
+
const result = await checkProject(projectDir, []);
|
|
121
|
+
expect(result.checks).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('checkProject CLAUDE.md checks', () => {
|
|
126
|
+
it('should detect malformed CLAUDE.md markers', async () => {
|
|
127
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
128
|
+
// Write CLAUDE.md with unclosed marker
|
|
129
|
+
await writeFile(
|
|
130
|
+
claudeMdPath,
|
|
131
|
+
'# Project\n\n<!-- BEGIN magus:test-plugin v1.0.0 -->\nSome content\n',
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// checkProject won't find the marker issue unless it resolves plugins.
|
|
135
|
+
// But the parse is done on the file itself. Let's verify via parseClaudeMdSections directly.
|
|
136
|
+
const { parseClaudeMdSections } = await import('../services/conventions-manager.js');
|
|
137
|
+
const parseResult = await parseClaudeMdSections(claudeMdPath);
|
|
138
|
+
expect(parseResult.errors).toHaveLength(1);
|
|
139
|
+
expect(parseResult.errors[0].type).toBe('missing_end');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ============================================================
|
|
144
|
+
// runDoctor with no registered projects
|
|
145
|
+
// ============================================================
|
|
146
|
+
|
|
147
|
+
describe('runDoctor', () => {
|
|
148
|
+
it('should return empty results when no projects are registered', async () => {
|
|
149
|
+
// Mock the registry to return empty
|
|
150
|
+
const doctorModule = await import('../services/doctor.js');
|
|
151
|
+
|
|
152
|
+
// We can't easily mock the internal collectProjects, but we can
|
|
153
|
+
// test via the exported runDoctor if the registry is empty.
|
|
154
|
+
// In practice, runDoctor reads the real registry, so we test the
|
|
155
|
+
// output shape is correct.
|
|
156
|
+
const { runDoctor } = doctorModule;
|
|
157
|
+
const result = await runDoctor();
|
|
158
|
+
|
|
159
|
+
// The result shape should be valid even if we have real projects
|
|
160
|
+
expect(result).toHaveProperty('projects');
|
|
161
|
+
expect(result).toHaveProperty('summary');
|
|
162
|
+
expect(result.summary).toHaveProperty('totalProjects');
|
|
163
|
+
expect(result.summary).toHaveProperty('totalChecks');
|
|
164
|
+
expect(result.summary).toHaveProperty('passed');
|
|
165
|
+
expect(result.summary).toHaveProperty('warnings');
|
|
166
|
+
expect(result.summary).toHaveProperty('failures');
|
|
167
|
+
expect(result.summary).toHaveProperty('fixable');
|
|
168
|
+
expect(typeof result.summary.totalProjects).toBe('number');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should compute summary counts correctly', async () => {
|
|
172
|
+
// Build a DoctorResult manually and verify summary computation
|
|
173
|
+
// This tests the summary logic without needing real plugin resolution
|
|
174
|
+
const { runDoctor } = await import('../services/doctor.js');
|
|
175
|
+
const result = await runDoctor();
|
|
176
|
+
|
|
177
|
+
// Verify summary math
|
|
178
|
+
const { summary, projects } = result;
|
|
179
|
+
let expectedChecks = 0;
|
|
180
|
+
let expectedPassed = 0;
|
|
181
|
+
let expectedWarnings = 0;
|
|
182
|
+
let expectedFailures = 0;
|
|
183
|
+
let expectedFixable = 0;
|
|
184
|
+
|
|
185
|
+
for (const project of projects) {
|
|
186
|
+
for (const check of project.checks) {
|
|
187
|
+
expectedChecks++;
|
|
188
|
+
if (check.status === 'pass') expectedPassed++;
|
|
189
|
+
else if (check.status === 'warn') expectedWarnings++;
|
|
190
|
+
else if (check.status === 'fail') expectedFailures++;
|
|
191
|
+
if (check.fixable) expectedFixable++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
expect(summary.totalChecks).toBe(expectedChecks);
|
|
196
|
+
expect(summary.passed).toBe(expectedPassed);
|
|
197
|
+
expect(summary.warnings).toBe(expectedWarnings);
|
|
198
|
+
expect(summary.failures).toBe(expectedFailures);
|
|
199
|
+
expect(summary.fixable).toBe(expectedFixable);
|
|
200
|
+
expect(summary.totalProjects).toBe(projects.length);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ============================================================
|
|
205
|
+
// fixDoctorIssues
|
|
206
|
+
// ============================================================
|
|
207
|
+
|
|
208
|
+
describe('fixDoctorIssues', () => {
|
|
209
|
+
it('should add missing gitignore entries', async () => {
|
|
210
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
211
|
+
await writeFile(gitignorePath, 'node_modules/\n');
|
|
212
|
+
|
|
213
|
+
const doctorResult: DoctorResult = {
|
|
214
|
+
projects: [
|
|
215
|
+
{
|
|
216
|
+
projectPath: projectDir,
|
|
217
|
+
plugins: ['test-plugin@magus'],
|
|
218
|
+
checks: [
|
|
219
|
+
{
|
|
220
|
+
name: 'gitignore-entries',
|
|
221
|
+
status: 'fail',
|
|
222
|
+
message: '.gitignore missing: .mnemex/',
|
|
223
|
+
fixable: true,
|
|
224
|
+
_fixData: {
|
|
225
|
+
type: 'add-gitignore-entries',
|
|
226
|
+
gitignorePath,
|
|
227
|
+
entries: ['.mnemex/'],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
summary: {
|
|
234
|
+
totalProjects: 1,
|
|
235
|
+
totalChecks: 1,
|
|
236
|
+
passed: 0,
|
|
237
|
+
warnings: 0,
|
|
238
|
+
failures: 1,
|
|
239
|
+
fixable: 1,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
244
|
+
|
|
245
|
+
expect(fixResult.fixed).toBe(1);
|
|
246
|
+
expect(fixResult.failed).toBe(0);
|
|
247
|
+
|
|
248
|
+
const content = await readFile(gitignorePath, 'utf-8');
|
|
249
|
+
expect(content).toContain('.mnemex/');
|
|
250
|
+
expect(content).toContain('node_modules/');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should inject missing CLAUDE.md sections', async () => {
|
|
254
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
255
|
+
await writeFile(claudeMdPath, '# My Project\n\nSome existing content.\n');
|
|
256
|
+
|
|
257
|
+
// Create a plugin dir with template
|
|
258
|
+
const pluginDir = await createPluginDir(
|
|
259
|
+
pluginsDir,
|
|
260
|
+
'test-plugin',
|
|
261
|
+
{
|
|
262
|
+
claudemd: {
|
|
263
|
+
section: 'test-plugin',
|
|
264
|
+
template: './templates/claude-md-conventions.md',
|
|
265
|
+
version: '1.0.0',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
'## Test Plugin\n\nDo not commit .test-data/ to git.',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const doctorResult: DoctorResult = {
|
|
272
|
+
projects: [
|
|
273
|
+
{
|
|
274
|
+
projectPath: projectDir,
|
|
275
|
+
plugins: ['test-plugin@magus'],
|
|
276
|
+
checks: [
|
|
277
|
+
{
|
|
278
|
+
name: 'claudemd-section-test-plugin',
|
|
279
|
+
status: 'warn',
|
|
280
|
+
message: 'CLAUDE.md missing test-plugin conventions',
|
|
281
|
+
fixable: true,
|
|
282
|
+
_fixData: {
|
|
283
|
+
type: 'inject-section',
|
|
284
|
+
claudeMdPath,
|
|
285
|
+
pluginName: 'test-plugin',
|
|
286
|
+
pluginPath: pluginDir,
|
|
287
|
+
conventions: {
|
|
288
|
+
claudemd: {
|
|
289
|
+
section: 'test-plugin',
|
|
290
|
+
template: './templates/claude-md-conventions.md',
|
|
291
|
+
version: '1.0.0',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
summary: {
|
|
300
|
+
totalProjects: 1,
|
|
301
|
+
totalChecks: 1,
|
|
302
|
+
passed: 0,
|
|
303
|
+
warnings: 1,
|
|
304
|
+
failures: 0,
|
|
305
|
+
fixable: 1,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
310
|
+
|
|
311
|
+
expect(fixResult.fixed).toBe(1);
|
|
312
|
+
expect(fixResult.failed).toBe(0);
|
|
313
|
+
|
|
314
|
+
const content = await readFile(claudeMdPath, 'utf-8');
|
|
315
|
+
expect(content).toContain('<!-- BEGIN magus:test-plugin v1.0.0 -->');
|
|
316
|
+
expect(content).toContain('Do not commit .test-data/ to git.');
|
|
317
|
+
expect(content).toContain('<!-- END magus:test-plugin -->');
|
|
318
|
+
expect(content).toContain('# My Project');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should create gitignore when project has none', async () => {
|
|
322
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
323
|
+
expect(existsSync(gitignorePath)).toBe(false);
|
|
324
|
+
|
|
325
|
+
const doctorResult: DoctorResult = {
|
|
326
|
+
projects: [
|
|
327
|
+
{
|
|
328
|
+
projectPath: projectDir,
|
|
329
|
+
plugins: ['test-plugin@magus'],
|
|
330
|
+
checks: [
|
|
331
|
+
{
|
|
332
|
+
name: 'gitignore-exists',
|
|
333
|
+
status: 'warn',
|
|
334
|
+
message: '.gitignore does not exist',
|
|
335
|
+
fixable: true,
|
|
336
|
+
_fixData: {
|
|
337
|
+
type: 'create-gitignore',
|
|
338
|
+
projectPath: projectDir,
|
|
339
|
+
entries: ['.mnemex/', '.claudemem/'],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
summary: {
|
|
346
|
+
totalProjects: 1,
|
|
347
|
+
totalChecks: 1,
|
|
348
|
+
passed: 0,
|
|
349
|
+
warnings: 1,
|
|
350
|
+
failures: 0,
|
|
351
|
+
fixable: 1,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
356
|
+
|
|
357
|
+
expect(fixResult.fixed).toBe(1);
|
|
358
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
359
|
+
const content = await readFile(gitignorePath, 'utf-8');
|
|
360
|
+
expect(content).toContain('.mnemex/');
|
|
361
|
+
expect(content).toContain('.claudemem/');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should skip unfixable issues', async () => {
|
|
365
|
+
const doctorResult: DoctorResult = {
|
|
366
|
+
projects: [
|
|
367
|
+
{
|
|
368
|
+
projectPath: '/nonexistent/path',
|
|
369
|
+
plugins: ['some-plugin@magus'],
|
|
370
|
+
checks: [
|
|
371
|
+
{
|
|
372
|
+
name: 'project-accessible',
|
|
373
|
+
status: 'fail',
|
|
374
|
+
message: 'Project path does not exist',
|
|
375
|
+
fixable: false,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
summary: {
|
|
381
|
+
totalProjects: 1,
|
|
382
|
+
totalChecks: 1,
|
|
383
|
+
passed: 0,
|
|
384
|
+
warnings: 0,
|
|
385
|
+
failures: 1,
|
|
386
|
+
fixable: 0,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
391
|
+
|
|
392
|
+
expect(fixResult.fixed).toBe(0);
|
|
393
|
+
expect(fixResult.failed).toBe(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should report fix count correctly with mixed results', async () => {
|
|
397
|
+
const gitignorePath = join(projectDir, '.gitignore');
|
|
398
|
+
await writeFile(gitignorePath, 'node_modules/\n');
|
|
399
|
+
|
|
400
|
+
const doctorResult: DoctorResult = {
|
|
401
|
+
projects: [
|
|
402
|
+
{
|
|
403
|
+
projectPath: projectDir,
|
|
404
|
+
plugins: ['test-plugin@magus'],
|
|
405
|
+
checks: [
|
|
406
|
+
{
|
|
407
|
+
name: 'gitignore-entries',
|
|
408
|
+
status: 'fail',
|
|
409
|
+
message: '.gitignore missing: .mnemex/',
|
|
410
|
+
fixable: true,
|
|
411
|
+
_fixData: {
|
|
412
|
+
type: 'add-gitignore-entries',
|
|
413
|
+
gitignorePath,
|
|
414
|
+
entries: ['.mnemex/'],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'project-accessible',
|
|
419
|
+
status: 'fail',
|
|
420
|
+
message: 'Project path does not exist',
|
|
421
|
+
fixable: false,
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: 'claudemd-section-test-plugin',
|
|
425
|
+
status: 'warn',
|
|
426
|
+
message: 'CLAUDE.md missing test-plugin conventions',
|
|
427
|
+
fixable: true,
|
|
428
|
+
// Missing _fixData -- should count as failed
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
summary: {
|
|
434
|
+
totalProjects: 1,
|
|
435
|
+
totalChecks: 3,
|
|
436
|
+
passed: 0,
|
|
437
|
+
warnings: 1,
|
|
438
|
+
failures: 2,
|
|
439
|
+
fixable: 2,
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const fixResult = await fixDoctorIssues(doctorResult);
|
|
444
|
+
|
|
445
|
+
// 1 fixed (gitignore), 1 failed (missing _fixData), 1 skipped (not fixable)
|
|
446
|
+
expect(fixResult.fixed).toBe(1);
|
|
447
|
+
expect(fixResult.failed).toBe(1);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
@@ -15,6 +15,11 @@ import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import * as url from "node:url";
|
|
18
|
+
// Real modules captured before the mock.module() overrides below, so the mocks
|
|
19
|
+
// can spread the full export surface instead of stripping it.
|
|
20
|
+
import * as actualClaudeSettings from "../services/claude-settings.js";
|
|
21
|
+
import * as actualClaudeCli from "../services/claude-cli.js";
|
|
22
|
+
import * as actualPluginManager from "../services/plugin-manager.js";
|
|
18
23
|
|
|
19
24
|
// ---------------------------------------------------------------------------
|
|
20
25
|
// PART 1: PluginsScreen source-level policy tests
|
|
@@ -93,11 +98,14 @@ let mockUpdatePlugin: ReturnType<typeof mock>;
|
|
|
93
98
|
let mockIsClaudeAvailable: ReturnType<typeof mock>;
|
|
94
99
|
let mockGetAvailablePlugins: ReturnType<typeof mock>;
|
|
95
100
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// module is
|
|
99
|
-
|
|
101
|
+
// Spread the REAL modules and override only the functions under test, so this
|
|
102
|
+
// file's mock.module doesn't strip other exports the module graph needs (which
|
|
103
|
+
// breaks both this file in isolation and — because bun's mock.module is global
|
|
104
|
+
// and not restored across files — every later test file). The `import * as`
|
|
105
|
+
// bindings are evaluated before these mock.module() calls run, so they capture
|
|
106
|
+
// the real modules.
|
|
100
107
|
mock.module("../services/claude-settings.js", () => ({
|
|
108
|
+
...actualClaudeSettings,
|
|
101
109
|
recoverMarketplaceSettings: mock(() =>
|
|
102
110
|
Promise.resolve({ enabledAutoUpdate: [], removed: [], reregistered: [] }),
|
|
103
111
|
),
|
|
@@ -120,6 +128,7 @@ mock.module("../services/claude-settings.js", () => ({
|
|
|
120
128
|
}));
|
|
121
129
|
|
|
122
130
|
mock.module("../services/claude-cli.js", () => ({
|
|
131
|
+
...actualClaudeCli,
|
|
123
132
|
updatePlugin: (...args: unknown[]) => mockUpdatePlugin(...args),
|
|
124
133
|
isClaudeAvailable: (...args: unknown[]) => mockIsClaudeAvailable(...args),
|
|
125
134
|
addMarketplace: mock(() => Promise.resolve()),
|
|
@@ -127,6 +136,7 @@ mock.module("../services/claude-cli.js", () => ({
|
|
|
127
136
|
}));
|
|
128
137
|
|
|
129
138
|
mock.module("../services/plugin-manager.js", () => ({
|
|
139
|
+
...actualPluginManager,
|
|
130
140
|
getAvailablePlugins: (...args: unknown[]) =>
|
|
131
141
|
mockGetAvailablePlugins(...args),
|
|
132
142
|
clearMarketplaceCache: mock(() => undefined),
|