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
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* and `required` setup keys.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
10
|
+
import * as actualChildProcess from "node:child_process";
|
|
11
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
12
|
import { tmpdir } from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
// Real modules captured before the mock.module() overrides, so the mocks spread
|
|
14
15
|
// the full surface instead of stripping exports (e.g. resolveExecutable) that
|
|
15
16
|
// bun's global, non-restored mock.module would otherwise remove for later files.
|
|
16
17
|
import * as actualCommandUtils from "../utils/command-utils.js";
|
|
17
|
-
import * as actualChildProcess from "node:child_process";
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
// Mocks — must be set up BEFORE any import from plugin-setup
|
|
@@ -86,6 +86,7 @@ const {
|
|
|
86
86
|
extractGoVersion,
|
|
87
87
|
extractBinaryName,
|
|
88
88
|
parseAtSpec,
|
|
89
|
+
parseGoSpec,
|
|
89
90
|
parsePipSpec,
|
|
90
91
|
goDepNeedsInstall,
|
|
91
92
|
goDepDecision,
|
|
@@ -119,14 +120,97 @@ describe("extractGoBinaryName", () => {
|
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
it("handles module with nested path and version", () => {
|
|
122
|
-
expect(
|
|
123
|
-
|
|
124
|
-
)
|
|
123
|
+
expect(extractGoBinaryName("github.com/org/repo/cmd/mycli@v2.0.0")).toBe(
|
|
124
|
+
"mycli",
|
|
125
|
+
);
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
it("handles empty string gracefully", () => {
|
|
128
129
|
expect(extractGoBinaryName("")).toBe("");
|
|
129
130
|
});
|
|
131
|
+
|
|
132
|
+
// Negative control for the /vN fix: a plain basename returns "v2" here.
|
|
133
|
+
// Without the go-module helper wired in, this test FAILS.
|
|
134
|
+
it("names the binary after the module, not its /v2 major-version suffix", () => {
|
|
135
|
+
expect(
|
|
136
|
+
extractGoBinaryName("github.com/MadAppGang/tmux-mcp/v2@v2.0.0"),
|
|
137
|
+
).toBe("tmux-mcp");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("strips the /v2 suffix from an unpinned module path too", () => {
|
|
141
|
+
expect(extractGoBinaryName("github.com/MadAppGang/tmux-mcp/v2")).toBe(
|
|
142
|
+
"tmux-mcp",
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("keeps a name that merely starts with v2", () => {
|
|
147
|
+
expect(extractGoBinaryName("github.com/x/v2tool@v1.0.0")).toBe("v2tool");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// 1b. parseGoSpec — the Go-specific view over parseAtSpec
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe("parseGoSpec", () => {
|
|
156
|
+
it("v1 module: name and pinned version", () => {
|
|
157
|
+
expect(parseGoSpec("github.com/MadAppGang/tmux-mcp@v1.7.1")).toEqual({
|
|
158
|
+
spec: "github.com/MadAppGang/tmux-mcp@v1.7.1",
|
|
159
|
+
name: "tmux-mcp",
|
|
160
|
+
version: "v1.7.1",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("v2 module: the /v2 suffix is not the name, the version survives", () => {
|
|
165
|
+
expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2@v2.0.0")).toEqual({
|
|
166
|
+
spec: "github.com/MadAppGang/tmux-mcp/v2@v2.0.0",
|
|
167
|
+
name: "tmux-mcp",
|
|
168
|
+
version: "v2.0.0",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("v2 module without a pin: name resolved, version null", () => {
|
|
173
|
+
expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2")).toEqual({
|
|
174
|
+
spec: "github.com/MadAppGang/tmux-mcp/v2",
|
|
175
|
+
name: "tmux-mcp",
|
|
176
|
+
version: null,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("a segment that starts with v2 is a real name", () => {
|
|
181
|
+
expect(parseGoSpec("github.com/x/v2tool@v1.0.0")).toEqual({
|
|
182
|
+
spec: "github.com/x/v2tool@v1.0.0",
|
|
183
|
+
name: "v2tool",
|
|
184
|
+
version: "v1.0.0",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("@latest is unpinned, same as parseAtSpec", () => {
|
|
189
|
+
expect(parseGoSpec("github.com/MadAppGang/tmux-mcp/v2@latest")).toEqual({
|
|
190
|
+
spec: "github.com/MadAppGang/tmux-mcp/v2@latest",
|
|
191
|
+
name: "tmux-mcp",
|
|
192
|
+
version: null,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Terminal 5.0.0 contract cases (test plan Area 3, C5 and C6). They live
|
|
197
|
+
// here rather than in their own file because any test file that imports
|
|
198
|
+
// plugin-setup.js ahead of the mock.module seam above bakes the real
|
|
199
|
+
// execFile into promisify() and fails the installPluginDeps suites.
|
|
200
|
+
it("C5: a /v10 suffix is stripped in full, not only its first digit", () => {
|
|
201
|
+
const { name, version } = parseGoSpec("github.com/x/tool/v10@v10.1.0");
|
|
202
|
+
expect({ name, version }).toEqual({ name: "tool", version: "v10.1.0" });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("C6: /v1 is never a Go major-version suffix, so it stays the name", () => {
|
|
206
|
+
const { name, version } = parseGoSpec("github.com/x/tool/v1@v1.0.0");
|
|
207
|
+
expect({ name, version }).toEqual({ name: "v1", version: "v1.0.0" });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("C6 control: the same shape at /v2 is a suffix and is stripped", () => {
|
|
211
|
+
const { name, version } = parseGoSpec("github.com/x/tool/v2@v2.0.0");
|
|
212
|
+
expect({ name, version }).toEqual({ name: "tool", version: "v2.0.0" });
|
|
213
|
+
});
|
|
130
214
|
});
|
|
131
215
|
|
|
132
216
|
// ---------------------------------------------------------------------------
|
|
@@ -283,9 +367,9 @@ describe("checkMissingDeps — go packages", () => {
|
|
|
283
367
|
|
|
284
368
|
describe("extractGoVersion", () => {
|
|
285
369
|
it("returns the pinned version", () => {
|
|
286
|
-
expect(
|
|
287
|
-
|
|
288
|
-
)
|
|
370
|
+
expect(extractGoVersion("github.com/MadAppGang/tmux-mcp@v1.6.2")).toBe(
|
|
371
|
+
"v1.6.2",
|
|
372
|
+
);
|
|
289
373
|
});
|
|
290
374
|
it("returns null for @latest (a moving target)", () => {
|
|
291
375
|
expect(
|
|
@@ -295,6 +379,14 @@ describe("extractGoVersion", () => {
|
|
|
295
379
|
it("returns null when unpinned", () => {
|
|
296
380
|
expect(extractGoVersion("github.com/user/tool")).toBeNull();
|
|
297
381
|
});
|
|
382
|
+
it("reads the pin behind a /v2 module path", () => {
|
|
383
|
+
expect(extractGoVersion("github.com/MadAppGang/tmux-mcp/v2@v2.0.0")).toBe(
|
|
384
|
+
"v2.0.0",
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
it("a /v2 module path with no pin is unpinned, not pinned to v2", () => {
|
|
388
|
+
expect(extractGoVersion("github.com/MadAppGang/tmux-mcp/v2")).toBeNull();
|
|
389
|
+
});
|
|
298
390
|
});
|
|
299
391
|
|
|
300
392
|
describe("goDepNeedsInstall — version awareness", () => {
|
|
@@ -666,6 +758,16 @@ describe("parseAtSpec", () => {
|
|
|
666
758
|
expect(parseAtSpec("tool@latest").version).toBeNull();
|
|
667
759
|
});
|
|
668
760
|
|
|
761
|
+
it("does NOT apply the Go /vN rule — an npm scope may be named v2", () => {
|
|
762
|
+
// The Go major-version strip lives in parseGoSpec only; folding it in
|
|
763
|
+
// here would turn this scoped npm package into "@scope".
|
|
764
|
+
expect(parseAtSpec("@scope/v2@1.0.0")).toEqual({
|
|
765
|
+
spec: "@scope/v2@1.0.0",
|
|
766
|
+
name: "v2",
|
|
767
|
+
version: "1.0.0",
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
669
771
|
it("treats a bare name as unpinned", () => {
|
|
670
772
|
expect(parseAtSpec("tool")).toEqual({
|
|
671
773
|
spec: "tool",
|
|
@@ -3,11 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import fs from "fs-extra";
|
|
6
|
-
import {
|
|
7
|
-
listProfiles,
|
|
8
|
-
showProfile,
|
|
9
|
-
switchProfile,
|
|
10
|
-
} from "../cli/profile.js";
|
|
6
|
+
import { listProfiles, showProfile, switchProfile } from "../cli/profile.js";
|
|
11
7
|
import { activeProfile } from "../services/symlink-manager.js";
|
|
12
8
|
import type { ProfileManifest } from "../types/index.js";
|
|
13
9
|
|
|
@@ -17,9 +13,7 @@ let errs: string[];
|
|
|
17
13
|
beforeEach(() => {
|
|
18
14
|
logs = [];
|
|
19
15
|
errs = [];
|
|
20
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
21
16
|
console.log = mock((...a: unknown[]) => logs.push(a.join(" "))) as any;
|
|
22
|
-
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
23
17
|
console.error = mock((...a: unknown[]) => errs.push(a.join(" "))) as any;
|
|
24
18
|
});
|
|
25
19
|
afterEach(() => mock.restore());
|
|
@@ -65,7 +65,9 @@ describe("buildProfileSettings", () => {
|
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
test("no union given -> no false entries (unchanged legacy shape)", () => {
|
|
68
|
-
const s = buildProfileSettings(
|
|
68
|
+
const s = buildProfileSettings(
|
|
69
|
+
closure({ plugins: { "dev@magus": "latest" } }),
|
|
70
|
+
);
|
|
69
71
|
expect(s.enabledPlugins).toEqual({ "dev@magus": true });
|
|
70
72
|
});
|
|
71
73
|
});
|
|
@@ -98,14 +100,28 @@ describe("materializeProfile", () => {
|
|
|
98
100
|
project,
|
|
99
101
|
);
|
|
100
102
|
expect(dir).toBe(profileDir("frontend", project));
|
|
101
|
-
expect(
|
|
102
|
-
|
|
103
|
+
expect(
|
|
104
|
+
(await fs.readJson(join(dir, "settings.json"))).enabledPlugins[
|
|
105
|
+
"dev@magus"
|
|
106
|
+
],
|
|
107
|
+
).toBe(true);
|
|
108
|
+
expect(
|
|
109
|
+
(await fs.readJson(join(dir, "mcp.json"))).mcpServers.x.command,
|
|
110
|
+
).toBe("y");
|
|
103
111
|
expect(await fs.pathExists(join(dir, "skills"))).toBe(true);
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
test("is idempotent (re-materialize overwrites cleanly)", async () => {
|
|
107
|
-
await materializeProfile(
|
|
108
|
-
|
|
115
|
+
await materializeProfile(
|
|
116
|
+
"p",
|
|
117
|
+
closure({ plugins: { "a@m": "latest" } }),
|
|
118
|
+
project,
|
|
119
|
+
);
|
|
120
|
+
const dir = await materializeProfile(
|
|
121
|
+
"p",
|
|
122
|
+
closure({ plugins: { "b@m": "latest" } }),
|
|
123
|
+
project,
|
|
124
|
+
);
|
|
109
125
|
const settings = await fs.readJson(join(dir, "settings.json"));
|
|
110
126
|
expect(settings.enabledPlugins).toEqual({ "b@m": true });
|
|
111
127
|
});
|
|
@@ -120,7 +136,10 @@ describe("materializeProfile", () => {
|
|
|
120
136
|
const dir = await materializeProfile("p", closure(), project);
|
|
121
137
|
|
|
122
138
|
expect(
|
|
123
|
-
await fs.readFile(
|
|
139
|
+
await fs.readFile(
|
|
140
|
+
join(dir, "skills", "systematic-debugging", "SKILL.md"),
|
|
141
|
+
"utf8",
|
|
142
|
+
),
|
|
124
143
|
).toContain("committed project skill");
|
|
125
144
|
});
|
|
126
145
|
|
|
@@ -157,11 +176,15 @@ describe("materializeProfile", () => {
|
|
|
157
176
|
project,
|
|
158
177
|
union,
|
|
159
178
|
);
|
|
160
|
-
expect(
|
|
179
|
+
expect(
|
|
180
|
+
(await fs.readJson(join(fe, "settings.json"))).enabledPlugins,
|
|
181
|
+
).toEqual({
|
|
161
182
|
"designer@magus": true,
|
|
162
183
|
"terminal@magus": false,
|
|
163
184
|
});
|
|
164
|
-
expect(
|
|
185
|
+
expect(
|
|
186
|
+
(await fs.readJson(join(be, "settings.json"))).enabledPlugins,
|
|
187
|
+
).toEqual({
|
|
165
188
|
"designer@magus": false,
|
|
166
189
|
"terminal@magus": true,
|
|
167
190
|
});
|
|
@@ -31,7 +31,10 @@ describe("computeSync (pure)", () => {
|
|
|
31
31
|
expect(next.plugins!["dev@magus"]).toBe("2.9.0");
|
|
32
32
|
// new plugin added as latest
|
|
33
33
|
expect(next.plugins!["designer@magus"]).toBe("latest");
|
|
34
|
-
expect(changes).toContainEqual({
|
|
34
|
+
expect(changes).toContainEqual({
|
|
35
|
+
kind: "plugin-added",
|
|
36
|
+
detail: "designer@magus",
|
|
37
|
+
});
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
test("removes a plugin that was disabled", () => {
|
|
@@ -41,7 +44,10 @@ describe("computeSync (pure)", () => {
|
|
|
41
44
|
{ mcpServers: {} },
|
|
42
45
|
);
|
|
43
46
|
expect(next.plugins!["terminal@magus"]).toBeUndefined();
|
|
44
|
-
expect(changes).toContainEqual({
|
|
47
|
+
expect(changes).toContainEqual({
|
|
48
|
+
kind: "plugin-removed",
|
|
49
|
+
detail: "terminal@magus",
|
|
50
|
+
});
|
|
45
51
|
});
|
|
46
52
|
|
|
47
53
|
test("captures a settings change and an mcp change", () => {
|
|
@@ -75,7 +81,14 @@ describe("computeSync (pure)", () => {
|
|
|
75
81
|
const withStamp: ProfileManifestEntry = { ...entry, updatedAt: "OLD" };
|
|
76
82
|
const { entry: changed } = computeSync(
|
|
77
83
|
withStamp,
|
|
78
|
-
{
|
|
84
|
+
{
|
|
85
|
+
enabledPlugins: {
|
|
86
|
+
"dev@magus": true,
|
|
87
|
+
"terminal@magus": true,
|
|
88
|
+
"new@magus": true,
|
|
89
|
+
},
|
|
90
|
+
effortLevel: "high",
|
|
91
|
+
},
|
|
79
92
|
{ mcpServers: {} },
|
|
80
93
|
"2026-07-27T00:00:00.000Z",
|
|
81
94
|
);
|
|
@@ -84,7 +97,10 @@ describe("computeSync (pure)", () => {
|
|
|
84
97
|
// Unchanged sync leaves updatedAt alone.
|
|
85
98
|
const { entry: same } = computeSync(
|
|
86
99
|
withStamp,
|
|
87
|
-
{
|
|
100
|
+
{
|
|
101
|
+
enabledPlugins: { "dev@magus": true, "terminal@magus": true },
|
|
102
|
+
effortLevel: "high",
|
|
103
|
+
},
|
|
88
104
|
{ mcpServers: {} },
|
|
89
105
|
"2026-07-27T00:00:00.000Z",
|
|
90
106
|
);
|
|
@@ -99,7 +115,9 @@ describe("syncProfile (io)", () => {
|
|
|
99
115
|
await fs.ensureDir(join(dir, ".claude"));
|
|
100
116
|
await fs.writeJson(join(dir, ".claude", "profiles.json"), {
|
|
101
117
|
version: 2,
|
|
102
|
-
profiles: {
|
|
118
|
+
profiles: {
|
|
119
|
+
frontend: { name: "Frontend", plugins: { "dev@magus": "2.9.0" } },
|
|
120
|
+
},
|
|
103
121
|
});
|
|
104
122
|
});
|
|
105
123
|
afterEach(async () => {
|
|
@@ -14,61 +14,75 @@ import type { InstalledPluginEntry } from "../types/index.js";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const entry = (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
scope: InstalledPluginEntry["scope"],
|
|
18
|
+
version: string,
|
|
19
|
+
projectPath?: string,
|
|
20
20
|
): InstalledPluginEntry => ({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
scope,
|
|
22
|
+
version,
|
|
23
|
+
projectPath,
|
|
24
|
+
installPath: `/cache/${version}`,
|
|
25
|
+
installedAt: "2026-01-01T00:00:00.000Z",
|
|
26
|
+
lastUpdated: "2026-01-01T00:00:00.000Z",
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
describe("pickRegistryEntry — scope resolution", () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
test("user scope takes the user entry, ignoring project installs", () => {
|
|
31
|
+
const entries = [
|
|
32
|
+
entry("project", "1.0.0", "/proj/a"),
|
|
33
|
+
entry("user", "2.0.0"),
|
|
34
|
+
entry("project", "3.0.0", "/proj/b"),
|
|
35
|
+
];
|
|
36
|
+
expect(pickRegistryEntry(entries, "user")?.version).toBe("2.0.0");
|
|
37
|
+
});
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
test("project scope prefers the entry pinned to that exact project", () => {
|
|
40
|
+
const entries = [
|
|
41
|
+
entry("user", "2.0.0"),
|
|
42
|
+
entry("project", "1.0.0", "/proj/a"),
|
|
43
|
+
entry("project", "3.0.0", "/proj/b"),
|
|
44
|
+
];
|
|
45
|
+
expect(pickRegistryEntry(entries, "project", "/proj/b")?.version).toBe(
|
|
46
|
+
"3.0.0",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
test("project scope falls back to the user entry when the project has no install", () => {
|
|
51
|
+
// This is the statusline case: installed at user scope, absent from project
|
|
52
|
+
// settings, previously surfaced as "no version" and skipped by update checks.
|
|
53
|
+
const entries = [
|
|
54
|
+
entry("user", "2.1.2"),
|
|
55
|
+
entry("project", "1.0.0", "/proj/other"),
|
|
56
|
+
];
|
|
57
|
+
expect(pickRegistryEntry(entries, "project", "/proj/mine")?.version).toBe(
|
|
58
|
+
"2.1.2",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
test("project paths are compared resolved, not as raw strings", () => {
|
|
63
|
+
const entries = [entry("project", "4.2.0", "/proj/a/../a")];
|
|
64
|
+
expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe(
|
|
65
|
+
"4.2.0",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
test("a local-scope entry satisfies a project lookup for the same path", () => {
|
|
70
|
+
const entries = [
|
|
71
|
+
entry("local", "5.0.0", "/proj/a"),
|
|
72
|
+
entry("user", "1.0.0"),
|
|
73
|
+
];
|
|
74
|
+
expect(pickRegistryEntry(entries, "project", "/proj/a")?.version).toBe(
|
|
75
|
+
"5.0.0",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
test("no project path and no user entry yields undefined, not a wrong project's version", () => {
|
|
80
|
+
const entries = [entry("project", "9.9.9", "/proj/somewhere-else")];
|
|
81
|
+
expect(pickRegistryEntry(entries, "project")).toBeUndefined();
|
|
82
|
+
});
|
|
69
83
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
test("empty and missing entry lists are handled", () => {
|
|
85
|
+
expect(pickRegistryEntry([], "user")).toBeUndefined();
|
|
86
|
+
expect(pickRegistryEntry(undefined, "user")).toBeUndefined();
|
|
87
|
+
});
|
|
74
88
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm, symlink, writeFile
|
|
2
|
+
import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { resolveExecutable } from "../utils/command-utils.js";
|
|
@@ -53,7 +53,11 @@ describe("dedupeBins — precedence by order", () => {
|
|
|
53
53
|
|
|
54
54
|
test("a later entry fills a version the winner left unset", () => {
|
|
55
55
|
const loose: BinRequirement = { name: "tmux", via: "brew" };
|
|
56
|
-
const pinned: BinRequirement = {
|
|
56
|
+
const pinned: BinRequirement = {
|
|
57
|
+
name: "tmux",
|
|
58
|
+
via: "brew",
|
|
59
|
+
version: "3.4",
|
|
60
|
+
};
|
|
57
61
|
const result = dedupeBins([
|
|
58
62
|
{ bin: loose, source: "plugin:a" },
|
|
59
63
|
{ bin: pinned, source: "plugin:b" },
|
|
@@ -150,9 +154,9 @@ describe("resolveProfile", () => {
|
|
|
150
154
|
});
|
|
151
155
|
|
|
152
156
|
test("throws for an unknown profile id", async () => {
|
|
153
|
-
await expect(
|
|
154
|
-
|
|
155
|
-
)
|
|
157
|
+
await expect(resolveProfile(manifestOf({}), "ghost")).rejects.toThrow(
|
|
158
|
+
'Profile "ghost" not found',
|
|
159
|
+
);
|
|
156
160
|
});
|
|
157
161
|
});
|
|
158
162
|
|
|
@@ -191,12 +195,22 @@ describe("resolveAllProfiles — union", () => {
|
|
|
191
195
|
|
|
192
196
|
test("conflicting explicit pins resolve to the higher semver, order-independent, and are reported", async () => {
|
|
193
197
|
const mk = (order: "ab" | "ba"): ProfileManifest => {
|
|
194
|
-
const a: ProfileManifestEntry = {
|
|
195
|
-
|
|
198
|
+
const a: ProfileManifestEntry = {
|
|
199
|
+
name: "A",
|
|
200
|
+
plugins: { "dev@magus": "2.9.0" },
|
|
201
|
+
};
|
|
202
|
+
const b: ProfileManifestEntry = {
|
|
203
|
+
name: "B",
|
|
204
|
+
plugins: { "dev@magus": "2.11.0" },
|
|
205
|
+
};
|
|
196
206
|
return manifestOf(order === "ab" ? { a, b } : { b, a });
|
|
197
207
|
};
|
|
198
|
-
const ab = await resolveAllProfiles(mk("ab"), {
|
|
199
|
-
|
|
208
|
+
const ab = await resolveAllProfiles(mk("ab"), {
|
|
209
|
+
binResolver: fakeBinResolver({}),
|
|
210
|
+
});
|
|
211
|
+
const ba = await resolveAllProfiles(mk("ba"), {
|
|
212
|
+
binResolver: fakeBinResolver({}),
|
|
213
|
+
});
|
|
200
214
|
|
|
201
215
|
// Deterministic: higher pin wins regardless of profile order.
|
|
202
216
|
expect(ab.plugins["dev@magus"]).toBe("2.11.0");
|
|
@@ -211,8 +225,12 @@ describe("resolveAllProfiles — union", () => {
|
|
|
211
225
|
a: { name: "A", cliTools: { claudish: "1.2.0" } },
|
|
212
226
|
b: { name: "B", cliTools: { claudish: "1.5.0" } },
|
|
213
227
|
});
|
|
214
|
-
const union = await resolveAllProfiles(manifest, {
|
|
215
|
-
|
|
228
|
+
const union = await resolveAllProfiles(manifest, {
|
|
229
|
+
binResolver: fakeBinResolver({}),
|
|
230
|
+
});
|
|
231
|
+
expect(union.bins.find((x) => x.name === "claudish")?.version).toBe(
|
|
232
|
+
"1.5.0",
|
|
233
|
+
);
|
|
216
234
|
expect(union.conflicts?.some((c) => c.includes("claudish"))).toBe(true);
|
|
217
235
|
});
|
|
218
236
|
});
|
|
@@ -255,7 +273,9 @@ describe("marketplaces derived from plugin ids", () => {
|
|
|
255
273
|
},
|
|
256
274
|
});
|
|
257
275
|
const c = await resolveProfile(manifest, "mixed", noBins);
|
|
258
|
-
expect(c.marketplaces["magus-marketing"]?.repo).toBe(
|
|
276
|
+
expect(c.marketplaces["magus-marketing"]?.repo).toBe(
|
|
277
|
+
"MadAppGang/magus-marketing",
|
|
278
|
+
);
|
|
259
279
|
expect(c.marketplaces["magus-alpha"]?.repo).toBe("MadAppGang/magus-alpha");
|
|
260
280
|
});
|
|
261
281
|
|
|
@@ -263,7 +283,9 @@ describe("marketplaces derived from plugin ids", () => {
|
|
|
263
283
|
const manifest = manifestOf({
|
|
264
284
|
pinned: {
|
|
265
285
|
name: "Pinned",
|
|
266
|
-
marketplaces: {
|
|
286
|
+
marketplaces: {
|
|
287
|
+
magus: { source: "github", repo: "MadAppGang/magus-fork" },
|
|
288
|
+
},
|
|
267
289
|
plugins: { "dev@magus": "latest" },
|
|
268
290
|
},
|
|
269
291
|
});
|
|
@@ -273,7 +295,10 @@ describe("marketplaces derived from plugin ids", () => {
|
|
|
273
295
|
|
|
274
296
|
test("an unknown marketplace is left for the manifest to declare", async () => {
|
|
275
297
|
const manifest = manifestOf({
|
|
276
|
-
third: {
|
|
298
|
+
third: {
|
|
299
|
+
name: "Third",
|
|
300
|
+
plugins: { "thing@someones-marketplace": "latest" },
|
|
301
|
+
},
|
|
277
302
|
});
|
|
278
303
|
const c = await resolveProfile(manifest, "third", noBins);
|
|
279
304
|
expect(c.marketplaces["someones-marketplace"]).toBeUndefined();
|