claudeup 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/bin/claudeup.js +6 -1
- package/package.json +6 -6
- package/src/__tests__/alias-adopt.test.ts +331 -329
- package/src/__tests__/alias-parser.test.ts +384 -376
- package/src/__tests__/alias-shell-writer.test.ts +638 -625
- package/src/__tests__/alias-store.test.ts +143 -143
- package/src/__tests__/catalog-cache-store.test.ts +265 -265
- package/src/__tests__/catalog-notice.test.ts +123 -123
- package/src/__tests__/cli-apply-seams.test.ts +12 -8
- package/src/__tests__/cli-router.test.ts +132 -3
- package/src/__tests__/cli-update-view.test.ts +27 -8
- package/src/__tests__/community-staleness.test.ts +0 -2
- package/src/__tests__/component-badge.test.ts +4 -4
- package/src/__tests__/content-drift.test.ts +5 -1
- package/src/__tests__/conventions-integration.test.ts +774 -689
- package/src/__tests__/conventions-manager.test.ts +1216 -1152
- package/src/__tests__/doctor-bins.test.ts +14 -6
- package/src/__tests__/doctor.test.ts +438 -425
- package/src/__tests__/dual-write-prevention.test.ts +277 -282
- package/src/__tests__/enabled-not-installed.test.ts +71 -63
- package/src/__tests__/file-locking.test.ts +196 -196
- package/src/__tests__/gap-fill-versions.test.ts +318 -311
- package/src/__tests__/github-budget.test.ts +170 -164
- package/src/__tests__/gitignore-detector.test.ts +160 -152
- package/src/__tests__/gitignore-fixer.test.ts +285 -247
- package/src/__tests__/gitignore-prerun.test.ts +57 -57
- package/src/__tests__/gitignore-resolver.test.ts +173 -146
- package/src/__tests__/gitignore-service.test.ts +121 -119
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/install-command.test.ts +3 -3
- package/src/__tests__/install-plan.test.ts +13 -7
- package/src/__tests__/manifest.test.ts +16 -5
- package/src/__tests__/markdown-renderer.test.ts +0 -1
- package/src/__tests__/marketplace-badge.test.ts +111 -90
- package/src/__tests__/marketplace-version.test.ts +64 -61
- package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
- package/src/__tests__/plugin-requires.test.ts +55 -1
- package/src/__tests__/plugin-setup.test.ts +111 -9
- package/src/__tests__/profile-command.test.ts +1 -7
- package/src/__tests__/profile-materializer.test.ts +31 -8
- package/src/__tests__/profile-sync.test.ts +23 -5
- package/src/__tests__/registry-version-resolution.test.ts +61 -47
- package/src/__tests__/resolve-executable.test.ts +1 -1
- package/src/__tests__/resolver.test.ts +38 -13
- package/src/__tests__/scope-action.test.ts +52 -50
- package/src/__tests__/scope-squares.test.tsx +4 -5
- package/src/__tests__/skill-install-files.test.ts +111 -103
- package/src/__tests__/symlink-manager.test.ts +27 -10
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/update-apply.test.ts +1 -3
- package/src/__tests__/useGitignoreModal.test.ts +69 -61
- package/src/__tests__/version-snapshot.test.ts +183 -165
- package/src/cli/ansi.ts +23 -15
- package/src/cli/doctor.ts +10 -9
- package/src/cli/install.ts +3 -3
- package/src/cli/profile.ts +5 -4
- package/src/cli/prompt.ts +0 -1
- package/src/cli/router.ts +69 -2
- package/src/cli/update-view.ts +11 -6
- package/src/cli/update.ts +9 -4
- package/src/cli/upgrade.ts +6 -4
- package/src/data/alias-flags.ts +260 -260
- package/src/data/gitignore-defaults.ts +20 -20
- package/src/data/gitignore-reasons.ts +86 -92
- package/src/data/gitignore-templates.ts +14 -14
- package/src/data/predefined-profiles.ts +1 -2
- package/src/data/skill-repos.ts +43 -35
- package/src/main.tsx +29 -6
- package/src/opentui.d.ts +1 -3
- package/src/prerunner/index.ts +7 -8
- package/src/services/alias-settings.ts +20 -23
- package/src/services/alias-shell-writer.ts +800 -803
- package/src/services/alias-store.ts +77 -77
- package/src/services/claude-settings.ts +27 -18
- package/src/services/content-drift.ts +14 -4
- package/src/services/conventions-manager.ts +692 -617
- package/src/services/doctor.ts +464 -403
- package/src/services/gitignore-detector.ts +113 -103
- package/src/services/gitignore-fixer.ts +204 -189
- package/src/services/gitignore-prerun.ts +26 -26
- package/src/services/gitignore-resolver.ts +162 -144
- package/src/services/gitignore-service.ts +120 -117
- package/src/services/install-plan.ts +4 -4
- package/src/services/local-marketplace.ts +7 -5
- package/src/services/manifest.ts +1 -1
- package/src/services/marketplace-catalog-git.ts +5 -2
- package/src/services/marketplace-fetcher.ts +241 -235
- package/src/services/plugin-manager.ts +17 -20
- package/src/services/plugin-mcp-config.ts +2 -2
- package/src/services/plugin-requires.ts +16 -8
- package/src/services/plugin-setup.ts +32 -15
- package/src/services/plugin-version-check.ts +4 -4
- package/src/services/profile-materializer.ts +9 -5
- package/src/services/profile-sync.ts +12 -4
- package/src/services/profiles.ts +3 -3
- package/src/services/resolver.ts +10 -6
- package/src/services/settings-manager.ts +82 -46
- package/src/services/skills-manager.ts +11 -9
- package/src/services/symlink-manager.ts +2 -5
- package/src/services/toolchain.ts +3 -3
- package/src/services/update-cache.ts +1 -1
- package/src/services/update-engine.ts +4 -4
- package/src/services/update-plan.ts +9 -11
- package/src/services/version-snapshot.ts +88 -88
- package/src/types/gitignore.ts +28 -28
- package/src/ui/App.tsx +36 -36
- package/src/ui/adapters/settingsAdapter.ts +2 -2
- package/src/ui/components/CategoryHeader.tsx +1 -1
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +3 -1
- package/src/ui/components/ScrollableList.tsx +2 -1
- package/src/ui/components/StyledText.tsx +1 -1
- package/src/ui/components/layout/FooterHints.tsx +5 -3
- package/src/ui/components/layout/Panel.tsx +2 -2
- package/src/ui/components/layout/ScreenLayout.tsx +5 -4
- package/src/ui/components/modals/InputModal.tsx +7 -1
- package/src/ui/components/modals/ModalContainer.tsx +3 -3
- package/src/ui/components/modals/SelectModal.tsx +1 -1
- package/src/ui/components/primitives/DetailSection.tsx +1 -1
- package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +1 -5
- package/src/ui/hooks/useMismatchModal.ts +4 -10
- package/src/ui/renderers/cliToolRenderers.tsx +2 -2
- package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +3 -9
- package/src/ui/renderers/profileRenderers.tsx +9 -8
- package/src/ui/renderers/settingsRenderers.tsx +4 -4
- package/src/ui/renderers/skillRenderers.tsx +14 -14
- package/src/ui/screens/AliasScreen.tsx +31 -34
- package/src/ui/screens/CliToolsScreen.tsx +14 -13
- package/src/ui/screens/EnvVarsScreen.tsx +8 -8
- package/src/ui/screens/GitignoreScreen.tsx +15 -14
- package/src/ui/screens/McpRegistryScreen.tsx +10 -7
- package/src/ui/screens/McpScreen.tsx +10 -10
- package/src/ui/screens/PluginsScreen.tsx +17 -21
- package/src/ui/screens/ProfilesScreen.tsx +25 -24
- package/src/ui/screens/SkillsScreen.tsx +16 -14
- package/src/ui/state/AppContext.tsx +4 -8
- package/src/ui/state/DimensionsContext.tsx +3 -2
- package/src/ui/theme-mode.ts +20 -38
- package/src/ui/theme-resolve.ts +148 -0
- package/src/utils/clipboard.ts +5 -5
- package/src/utils/command-utils.ts +2 -2
- package/src/utils/config-dir.ts +1 -3
- package/src/utils/file-locking.ts +88 -86
- package/src/utils/go-module.ts +36 -0
- package/src/ui/hooks/useAsyncData.ts +0 -127
- package/src/ui/state/AnimationContext.tsx +0 -76
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* but NOT when CLI is unavailable or update fails (no phantom state).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, mock } 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
|
+
import * as actualClaudeCli from "../services/claude-cli.js";
|
|
18
19
|
// Real modules captured before the mock.module() overrides below, so the mocks
|
|
19
20
|
// can spread the full export surface instead of stripping it.
|
|
20
21
|
import * as actualClaudeSettings from "../services/claude-settings.js";
|
|
21
|
-
import * as actualClaudeCli from "../services/claude-cli.js";
|
|
22
22
|
import * as actualPluginManager from "../services/plugin-manager.js";
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -34,55 +34,55 @@ import * as actualPluginManager from "../services/plugin-manager.js";
|
|
|
34
34
|
|
|
35
35
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
36
36
|
const PLUGINS_SCREEN_PATH = path.resolve(
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
__dirname,
|
|
38
|
+
"../ui/screens/PluginsScreen.tsx",
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
describe("PluginsScreen — no dual-write after CLI operations", () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
42
|
+
let source: string;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
source = fs.readFileSync(PLUGINS_SCREEN_PATH, "utf8");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not call saveVersionAfterInstall immediately after cliUpdatePlugin", () => {
|
|
49
|
+
// Find every occurrence of cliUpdatePlugin in the source. Each one should
|
|
50
|
+
// NOT be immediately followed (within 3 lines) by saveVersionAfterInstall.
|
|
51
|
+
const lines = source.split("\n");
|
|
52
|
+
const violations: number[] = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
if (lines[i].includes("cliUpdatePlugin(")) {
|
|
56
|
+
// Check the next 3 lines for a dual-write call
|
|
57
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
58
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
59
|
+
violations.push(i + 1); // 1-indexed line number
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(violations).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not call saveVersionAfterInstall immediately after cliInstallPlugin", () => {
|
|
70
|
+
const lines = source.split("\n");
|
|
71
|
+
const violations: number[] = [];
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
if (lines[i].includes("cliInstallPlugin(")) {
|
|
75
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
76
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
77
|
+
violations.push(i + 1);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
expect(violations).toEqual([]);
|
|
85
|
+
});
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
@@ -108,92 +108,91 @@ let mockGetAvailablePlugins: ReturnType<typeof mock>;
|
|
|
108
108
|
// bindings are evaluated before these mock.module() calls run, so they capture
|
|
109
109
|
// the real modules.
|
|
110
110
|
mock.module("../services/claude-settings.js", () => ({
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
...actualClaudeSettings,
|
|
112
|
+
recoverMarketplaceSettings: mock(() =>
|
|
113
|
+
Promise.resolve({ enabledAutoUpdate: [], removed: [], reregistered: [] }),
|
|
114
|
+
),
|
|
115
|
+
migrateMarketplaceRename: mock(() =>
|
|
116
|
+
Promise.resolve({
|
|
117
|
+
projectMigrated: 0,
|
|
118
|
+
globalMigrated: 0,
|
|
119
|
+
localMigrated: 0,
|
|
120
|
+
registryMigrated: 0,
|
|
121
|
+
knownMarketplacesMigrated: false,
|
|
122
|
+
}),
|
|
123
|
+
),
|
|
124
|
+
getGlobalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
125
|
+
getEnabledPlugins: mock(() => Promise.resolve({})),
|
|
126
|
+
getLocalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
127
|
+
saveGlobalInstalledPluginVersion: (...args: unknown[]) =>
|
|
128
|
+
mockSaveGlobal(...args),
|
|
129
|
+
saveLocalInstalledPluginVersion: (...args: unknown[]) =>
|
|
130
|
+
mockSaveLocal(...args),
|
|
131
|
+
readGlobalSettings: mock(() => Promise.resolve({ hooks: {} })),
|
|
132
|
+
writeGlobalSettings: mock(() => Promise.resolve()),
|
|
133
133
|
}));
|
|
134
134
|
|
|
135
135
|
mock.module("../services/claude-cli.js", () => ({
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
...actualClaudeCli,
|
|
137
|
+
updatePlugin: (...args: unknown[]) => mockUpdatePlugin(...args),
|
|
138
|
+
repairPlugin: (...args: unknown[]) => mockRepairPlugin(...args),
|
|
139
|
+
isClaudeAvailable: (...args: unknown[]) => mockIsClaudeAvailable(...args),
|
|
140
|
+
addMarketplace: mock(() => Promise.resolve()),
|
|
141
|
+
updateMarketplace: mock(() => Promise.resolve()),
|
|
142
142
|
}));
|
|
143
143
|
|
|
144
144
|
mock.module("../services/plugin-manager.js", () => ({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
clearMarketplaceCache: mock(() => undefined),
|
|
145
|
+
...actualPluginManager,
|
|
146
|
+
getAvailablePlugins: (...args: unknown[]) => mockGetAvailablePlugins(...args),
|
|
147
|
+
saveInstalledPluginVersion: (...args: unknown[]) => mockSaveProject(...args),
|
|
148
|
+
clearMarketplaceCache: mock(() => undefined),
|
|
150
149
|
}));
|
|
151
150
|
|
|
152
151
|
mock.module("../services/update-cache.js", () => ({
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
UpdateCache: class {
|
|
153
|
+
shouldCheckForUpdates = mock(() => Promise.resolve(true));
|
|
154
|
+
saveCheck = mock(() => Promise.resolve());
|
|
155
|
+
},
|
|
157
156
|
}));
|
|
158
157
|
|
|
159
158
|
mock.module("../services/claude-runner.js", () => ({
|
|
160
|
-
|
|
159
|
+
runClaude: mock(() => Promise.resolve(0)),
|
|
161
160
|
}));
|
|
162
161
|
|
|
163
162
|
mock.module("../data/marketplaces.js", () => ({
|
|
164
|
-
|
|
163
|
+
defaultMarketplaces: [],
|
|
165
164
|
}));
|
|
166
165
|
|
|
167
166
|
mock.module("../utils/string-utils.js", () => ({
|
|
168
|
-
|
|
167
|
+
parsePluginId: mock((_id: string) => null),
|
|
169
168
|
}));
|
|
170
169
|
|
|
171
170
|
mock.module("fs-extra", () => ({
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
default: {
|
|
172
|
+
pathExists: mock(() => Promise.resolve(false)),
|
|
173
|
+
readJson: mock(() => Promise.resolve({})),
|
|
174
|
+
writeJson: mock(() => Promise.resolve()),
|
|
175
|
+
ensureDir: mock(() => Promise.resolve()),
|
|
176
|
+
},
|
|
178
177
|
}));
|
|
179
178
|
|
|
180
179
|
// Now import the module under test (after mock.module registrations).
|
|
181
180
|
const { prerunClaude } = await import("../prerunner/index.js");
|
|
182
181
|
|
|
183
182
|
interface ScopeStatusLike {
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
enabled: boolean;
|
|
184
|
+
version?: string;
|
|
186
185
|
}
|
|
187
186
|
interface PluginLike {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
id: string;
|
|
188
|
+
enabled: boolean;
|
|
189
|
+
hasUpdate: boolean;
|
|
190
|
+
contentStale?: boolean;
|
|
191
|
+
installedVersion: string;
|
|
192
|
+
version: string;
|
|
193
|
+
userScope?: ScopeStatusLike;
|
|
194
|
+
projectScope?: ScopeStatusLike;
|
|
195
|
+
localScope?: ScopeStatusLike;
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
/**
|
|
@@ -202,184 +201,180 @@ interface PluginLike {
|
|
|
202
201
|
* no scope status describes a plugin installed nowhere and is correctly skipped.
|
|
203
202
|
*/
|
|
204
203
|
function makePlugin(overrides: Partial<PluginLike> = {}): PluginLike {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
return {
|
|
205
|
+
id: "test-plugin@magus",
|
|
206
|
+
enabled: true,
|
|
207
|
+
hasUpdate: true,
|
|
208
|
+
installedVersion: "1.0.0",
|
|
209
|
+
version: "1.1.0",
|
|
210
|
+
userScope: { enabled: true, version: "1.0.0" },
|
|
211
|
+
...overrides,
|
|
212
|
+
};
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
describe("prerunner — saveGlobalInstalledPluginVersion call count", () => {
|
|
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
|
-
await prerunClaude(["--help"], { force: true });
|
|
382
|
-
|
|
383
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
384
|
-
});
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
mockSaveGlobal = mock(() => Promise.resolve());
|
|
218
|
+
mockSaveProject = mock(() => Promise.resolve());
|
|
219
|
+
mockSaveLocal = mock(() => Promise.resolve());
|
|
220
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
221
|
+
mockRepairPlugin = mock(() => Promise.resolve());
|
|
222
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
223
|
+
mockGetAvailablePlugins = mock(() => Promise.resolve([makePlugin()]));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("calls saveGlobalInstalledPluginVersion once after successful CLI update", async () => {
|
|
227
|
+
// CLI is available, updatePlugin succeeds.
|
|
228
|
+
// The CLI does NOT update installedPluginVersions, so claudeup must save
|
|
229
|
+
// the version after a successful update.
|
|
230
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
231
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
232
|
+
|
|
233
|
+
await prerunClaude(["--help"], { force: true });
|
|
234
|
+
|
|
235
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The prerunner used to call `updatePlugin(id, "user")` unconditionally.
|
|
240
|
+
* `hasUpdate` is computed from the CURRENT PROJECT's resolved version, but
|
|
241
|
+
* installs live per project in installed_plugins.json — 495 of 496 magus
|
|
242
|
+
* installs on the reporter's machine are project-scoped. So it detected
|
|
243
|
+
* project drift, updated user scope, reported success, and left the project
|
|
244
|
+
* untouched, re-detecting the identical drift on every subsequent run.
|
|
245
|
+
*/
|
|
246
|
+
it("updates the PROJECT scope when that is where the outdated install is", async () => {
|
|
247
|
+
mockGetAvailablePlugins = mock(() =>
|
|
248
|
+
Promise.resolve([
|
|
249
|
+
makePlugin({
|
|
250
|
+
userScope: undefined,
|
|
251
|
+
projectScope: { enabled: true, version: "1.0.0" },
|
|
252
|
+
}),
|
|
253
|
+
]),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
await prerunClaude(["--help"], { force: true });
|
|
257
|
+
|
|
258
|
+
expect(mockUpdatePlugin).toHaveBeenCalledTimes(1);
|
|
259
|
+
expect(mockUpdatePlugin.mock.calls[0]?.[1]).toBe("project");
|
|
260
|
+
expect(mockSaveProject).toHaveBeenCalledTimes(1);
|
|
261
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("updates every scope that is outdated, not just the first", async () => {
|
|
265
|
+
mockGetAvailablePlugins = mock(() =>
|
|
266
|
+
Promise.resolve([
|
|
267
|
+
makePlugin({
|
|
268
|
+
userScope: { enabled: true, version: "1.0.0" },
|
|
269
|
+
projectScope: { enabled: true, version: "1.0.0" },
|
|
270
|
+
localScope: { enabled: true, version: "1.1.0" }, // already current
|
|
271
|
+
}),
|
|
272
|
+
]),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await prerunClaude(["--help"], { force: true });
|
|
276
|
+
|
|
277
|
+
const scopes = mockUpdatePlugin.mock.calls
|
|
278
|
+
.map((c: unknown[]) => c[1])
|
|
279
|
+
.sort();
|
|
280
|
+
expect(scopes).toEqual(["project", "user"]);
|
|
281
|
+
expect(mockSaveLocal).toHaveBeenCalledTimes(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A content-stale plugin has no version bump to install over, and
|
|
286
|
+
* `claude plugin install` no-ops on "already installed" (verified against the
|
|
287
|
+
* real marketplace in autotest/plugin-system/probe-05). The prerunner must
|
|
288
|
+
* therefore repair — uninstall+install — not update.
|
|
289
|
+
*/
|
|
290
|
+
it("repairs a content-stale plugin instead of updating it", async () => {
|
|
291
|
+
mockGetAvailablePlugins = mock(() =>
|
|
292
|
+
Promise.resolve([
|
|
293
|
+
makePlugin({
|
|
294
|
+
hasUpdate: false,
|
|
295
|
+
contentStale: true,
|
|
296
|
+
installedVersion: "1.0.0",
|
|
297
|
+
version: "1.0.0",
|
|
298
|
+
userScope: undefined,
|
|
299
|
+
projectScope: { enabled: true, version: "1.0.0" },
|
|
300
|
+
}),
|
|
301
|
+
]),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await prerunClaude(["--help"], { force: true });
|
|
305
|
+
|
|
306
|
+
expect(mockRepairPlugin).toHaveBeenCalledTimes(1);
|
|
307
|
+
expect(mockRepairPlugin.mock.calls[0]?.[1]).toBe("project");
|
|
308
|
+
expect(mockUpdatePlugin).toHaveBeenCalledTimes(0);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("prefers a real version update over a repair when both apply", async () => {
|
|
312
|
+
// A pending version bump reinstalls the files anyway, so repairing as well
|
|
313
|
+
// would double the work and the uninstall window.
|
|
314
|
+
mockGetAvailablePlugins = mock(() =>
|
|
315
|
+
Promise.resolve([makePlugin({ hasUpdate: true, contentStale: true })]),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
await prerunClaude(["--help"], { force: true });
|
|
319
|
+
|
|
320
|
+
expect(mockUpdatePlugin).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(mockRepairPlugin).toHaveBeenCalledTimes(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("does not repair when the CLI is unavailable", async () => {
|
|
325
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
326
|
+
mockGetAvailablePlugins = mock(() =>
|
|
327
|
+
Promise.resolve([
|
|
328
|
+
makePlugin({
|
|
329
|
+
hasUpdate: false,
|
|
330
|
+
contentStale: true,
|
|
331
|
+
version: "1.0.0",
|
|
332
|
+
projectScope: { enabled: true, version: "1.0.0" },
|
|
333
|
+
}),
|
|
334
|
+
]),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
await prerunClaude(["--help"], { force: true });
|
|
338
|
+
|
|
339
|
+
expect(mockRepairPlugin).toHaveBeenCalledTimes(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("does nothing when no scope holds an outdated install", async () => {
|
|
343
|
+
// hasUpdate can be true from a stale settings base while every real
|
|
344
|
+
// install is already current. Updating "somewhere" would be a guess.
|
|
345
|
+
mockGetAvailablePlugins = mock(() =>
|
|
346
|
+
Promise.resolve([
|
|
347
|
+
makePlugin({
|
|
348
|
+
userScope: { enabled: true, version: "1.1.0" },
|
|
349
|
+
projectScope: undefined,
|
|
350
|
+
}),
|
|
351
|
+
]),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await prerunClaude(["--help"], { force: true });
|
|
355
|
+
|
|
356
|
+
expect(mockUpdatePlugin).toHaveBeenCalledTimes(0);
|
|
357
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI is unavailable", async () => {
|
|
361
|
+
// CLI unavailable — we must NOT write phantom state.
|
|
362
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
363
|
+
|
|
364
|
+
await prerunClaude(["--help"], { force: true });
|
|
365
|
+
|
|
366
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI updatePlugin throws", async () => {
|
|
370
|
+
// CLI available but updatePlugin fails — must NOT write phantom state.
|
|
371
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
372
|
+
mockUpdatePlugin = mock(() =>
|
|
373
|
+
Promise.reject(new Error("CLI update failed")),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await prerunClaude(["--help"], { force: true });
|
|
377
|
+
|
|
378
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
379
|
+
});
|
|
385
380
|
});
|