magus-cli 7.7.0 → 7.9.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 +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -45
- package/src/ui/screens/CliToolsScreen.tsx +199 -98
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +315 -259
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
|
@@ -2,12 +2,13 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
PYTHON_CLI_INSTALLERS,
|
|
4
4
|
type PythonCliInstaller,
|
|
5
|
-
binInstallCommand,
|
|
6
5
|
binUpgradeCommand,
|
|
7
|
-
|
|
6
|
+
managedBins,
|
|
7
|
+
managedInstallCommand,
|
|
8
|
+
managedOption,
|
|
9
|
+
managedTool,
|
|
8
10
|
resolvePythonCliInstaller,
|
|
9
11
|
} from "../services/toolchain.js";
|
|
10
|
-
import type { ResolvedBin } from "../types/index.js";
|
|
11
12
|
import { formatCommand } from "../utils/command-utils.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -26,21 +27,21 @@ const UV = byProbe("uv");
|
|
|
26
27
|
const PIPX = byProbe("pipx");
|
|
27
28
|
const PY = byProbe("python3");
|
|
28
29
|
|
|
29
|
-
describe("
|
|
30
|
+
describe("managedInstallCommand (rendered by requires-probe's renderInstall)", () => {
|
|
30
31
|
test("bun with and without a version pin", () => {
|
|
31
32
|
expect(
|
|
32
33
|
formatCommand(
|
|
33
|
-
|
|
34
|
+
managedInstallCommand({
|
|
34
35
|
name: "claudish",
|
|
35
36
|
via: "bun",
|
|
36
37
|
package: "claudish",
|
|
37
38
|
version: "1.2.0",
|
|
38
39
|
}),
|
|
39
40
|
),
|
|
40
|
-
).toBe("bun
|
|
41
|
+
).toBe("bun add -g claudish@1.2.0");
|
|
41
42
|
expect(
|
|
42
|
-
formatCommand(
|
|
43
|
-
).toBe("bun
|
|
43
|
+
formatCommand(managedInstallCommand({ name: "claudish", via: "bun" })),
|
|
44
|
+
).toBe("bun add -g claudish");
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
test("the package name is ONE argv token, whatever is in it", () => {
|
|
@@ -51,18 +52,18 @@ describe("binInstallCommand", () => {
|
|
|
51
52
|
// words and a command separator. As argv it cannot be anything but a
|
|
52
53
|
// (nonexistent) package name, so the install fails instead of running.
|
|
53
54
|
const evil = "a b; rm -rf /";
|
|
54
|
-
const cmd =
|
|
55
|
+
const cmd = managedInstallCommand({ name: "x", via: "bun", package: evil });
|
|
55
56
|
expect(cmd.cmd).toBe("bun");
|
|
56
|
-
expect(cmd.args).toEqual(["
|
|
57
|
+
expect(cmd.args).toEqual(["add", "-g", evil]);
|
|
57
58
|
expect(cmd.args.at(-1)).toBe(evil); // unsplit: still exactly one token
|
|
58
59
|
// ...and rendering it for display quotes it rather than emitting syntax.
|
|
59
|
-
expect(formatCommand(cmd)).toBe("bun
|
|
60
|
+
expect(formatCommand(cmd)).toBe("bun add -g 'a b; rm -rf /'");
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
test("a version pin joins the package as one token, not two", () => {
|
|
63
64
|
// Splitting `pkg@version` would ask the installer for two packages, the
|
|
64
65
|
// second of which is a version number.
|
|
65
|
-
const cmd =
|
|
66
|
+
const cmd = managedInstallCommand({
|
|
66
67
|
name: "claudish",
|
|
67
68
|
via: "bun",
|
|
68
69
|
version: "1.2.0",
|
|
@@ -76,24 +77,32 @@ describe("binInstallCommand", () => {
|
|
|
76
77
|
// quotes nothing and the printed line is byte-identical to the string
|
|
77
78
|
// these builders returned before they returned argv.
|
|
78
79
|
expect(
|
|
79
|
-
formatCommand(
|
|
80
|
+
formatCommand(
|
|
81
|
+
managedInstallCommand({ name: "x", via: "npm", package: "x" }),
|
|
82
|
+
),
|
|
80
83
|
).toBe("npm install -g x");
|
|
81
84
|
expect(
|
|
82
85
|
formatCommand(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
managedInstallCommand({
|
|
87
|
+
name: "aider",
|
|
88
|
+
via: "pip",
|
|
89
|
+
package: "aider",
|
|
90
|
+
version: "0.1",
|
|
91
|
+
}),
|
|
87
92
|
),
|
|
88
93
|
).toBe("uv tool install aider==0.1");
|
|
89
94
|
expect(
|
|
90
95
|
formatCommand(
|
|
91
|
-
|
|
96
|
+
managedInstallCommand({ name: "tmux", via: "brew", formula: "tmux" }),
|
|
92
97
|
),
|
|
93
98
|
).toBe("brew install tmux");
|
|
94
99
|
expect(
|
|
95
100
|
formatCommand(
|
|
96
|
-
|
|
101
|
+
managedInstallCommand({
|
|
102
|
+
name: "t",
|
|
103
|
+
via: "go",
|
|
104
|
+
module: "github.com/x/t",
|
|
105
|
+
}),
|
|
97
106
|
),
|
|
98
107
|
).toBe("go install github.com/x/t@latest");
|
|
99
108
|
});
|
|
@@ -132,7 +141,7 @@ describe("binUpgradeCommand", () => {
|
|
|
132
141
|
// they are distinct references — so comparing them directly would be
|
|
133
142
|
// an assertion that cannot fail.
|
|
134
143
|
expect(formatCommand(binUpgradeCommand(bin, python))).not.toBe(
|
|
135
|
-
formatCommand(
|
|
144
|
+
formatCommand(managedInstallCommand(bin)),
|
|
136
145
|
);
|
|
137
146
|
}
|
|
138
147
|
});
|
|
@@ -172,20 +181,11 @@ describe("resolvePythonCliInstaller", () => {
|
|
|
172
181
|
});
|
|
173
182
|
|
|
174
183
|
test("a version pin is expressed in the installer's own syntax", () => {
|
|
175
|
-
expect(
|
|
176
|
-
formatCommand(
|
|
177
|
-
binInstallCommand({ name: "a", via: "pip", version: "1.2" }, UV),
|
|
178
|
-
),
|
|
179
|
-
).toBe("uv tool install a==1.2");
|
|
180
|
-
expect(
|
|
181
|
-
formatCommand(
|
|
182
|
-
binInstallCommand({ name: "a", via: "pip", version: "1.2" }, PIPX),
|
|
183
|
-
),
|
|
184
|
-
).toBe("pipx install a==1.2");
|
|
184
|
+
expect(PIPX.install("a", "1.2").args).toEqual(["install", "a==1.2"]);
|
|
185
185
|
// `a==1.2` is one token. Two would ask uv to install a package called
|
|
186
186
|
// "1.2".
|
|
187
187
|
expect(
|
|
188
|
-
|
|
188
|
+
managedInstallCommand({ name: "a", via: "pip", version: "1.2" }).args,
|
|
189
189
|
).toEqual(["tool", "install", "a==1.2"]);
|
|
190
190
|
});
|
|
191
191
|
|
|
@@ -207,26 +207,97 @@ describe("resolvePythonCliInstaller", () => {
|
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
describe("
|
|
211
|
-
test("
|
|
212
|
-
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
expect(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
210
|
+
describe("managedTool — the version-managed view of a requirement", () => {
|
|
211
|
+
test("bun/npm/pip/go/brew options map onto the managed shape, pin included", () => {
|
|
212
|
+
expect(
|
|
213
|
+
managedTool({
|
|
214
|
+
name: "claudish",
|
|
215
|
+
check: ["claudish", "--version"],
|
|
216
|
+
install: [],
|
|
217
|
+
chosen: { via: "bun", package: "claudish", version: "1.2.0" },
|
|
218
|
+
}),
|
|
219
|
+
).toEqual({
|
|
220
|
+
name: "claudish",
|
|
221
|
+
via: "bun",
|
|
222
|
+
package: "claudish",
|
|
223
|
+
check: "claudish --version",
|
|
224
|
+
version: "1.2.0",
|
|
225
|
+
});
|
|
226
|
+
expect(
|
|
227
|
+
managedTool({
|
|
228
|
+
name: "t",
|
|
229
|
+
check: ["t"],
|
|
230
|
+
install: [],
|
|
231
|
+
chosen: { via: "go", package: "github.com/x/t" },
|
|
232
|
+
}),
|
|
233
|
+
).toMatchObject({ via: "go", module: "github.com/x/t" });
|
|
234
|
+
expect(
|
|
235
|
+
managedTool({
|
|
236
|
+
name: "tmux",
|
|
237
|
+
check: ["tmux", "-V"],
|
|
238
|
+
install: [],
|
|
239
|
+
chosen: { via: "brew", formula: "tmux" },
|
|
240
|
+
}),
|
|
241
|
+
).toMatchObject({ via: "brew", formula: "tmux" });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("apt, release, bootstrap and the uv kinds have no managed version", () => {
|
|
245
|
+
const base = { name: "x", check: ["x"], install: [] };
|
|
246
|
+
for (const chosen of [
|
|
247
|
+
{ via: "apt" as const, packages: ["x"] },
|
|
248
|
+
{ via: "bootstrap" as const, tool: "uv" as const },
|
|
249
|
+
{ via: "uv-script" as const, script: "s.py", args: [] },
|
|
250
|
+
{ via: "uv-module" as const, script: "s.py", module: "m", args: [] },
|
|
251
|
+
]) {
|
|
252
|
+
expect(managedTool({ ...base, chosen })).toBeNull();
|
|
253
|
+
}
|
|
254
|
+
expect(managedTool({ ...base, chosen: null })).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("managedBins keeps provenance and drops what is not managed", () => {
|
|
258
|
+
const bins = managedBins([
|
|
259
|
+
{
|
|
260
|
+
name: "a",
|
|
261
|
+
check: ["a"],
|
|
262
|
+
install: [],
|
|
263
|
+
chosen: { via: "npm", package: "a" },
|
|
264
|
+
sources: ["plugin:p@m"],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "b",
|
|
268
|
+
check: ["b"],
|
|
269
|
+
install: [],
|
|
270
|
+
chosen: { via: "apt", packages: ["b"] },
|
|
271
|
+
sources: ["plugin:p@m"],
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
expect(bins).toEqual([
|
|
275
|
+
{
|
|
276
|
+
name: "a",
|
|
277
|
+
via: "npm",
|
|
278
|
+
package: "a",
|
|
279
|
+
check: "a",
|
|
280
|
+
sources: ["plugin:p@m"],
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("managedOption round-trips through managedTool", () => {
|
|
286
|
+
for (const t of [
|
|
287
|
+
{ name: "c", via: "bun" as const, package: "c", version: "1" },
|
|
288
|
+
{ name: "t", via: "brew" as const, formula: "tmux" },
|
|
289
|
+
{ name: "g", via: "go" as const, module: "github.com/x/g" },
|
|
290
|
+
{ name: "p", via: "pip" as const, package: "p" },
|
|
291
|
+
]) {
|
|
292
|
+
const option = managedOption(t);
|
|
293
|
+
expect(
|
|
294
|
+
managedTool({
|
|
295
|
+
name: t.name,
|
|
296
|
+
check: [t.name],
|
|
297
|
+
install: [option],
|
|
298
|
+
chosen: option,
|
|
299
|
+
}),
|
|
300
|
+
).toMatchObject(t);
|
|
301
|
+
}
|
|
231
302
|
});
|
|
232
303
|
});
|
|
@@ -26,7 +26,6 @@ import type {
|
|
|
26
26
|
UpdatePlan,
|
|
27
27
|
} from "../services/update-plan.js";
|
|
28
28
|
import type { InstalledPluginEntry, ResolvedBin } from "../types/index.js";
|
|
29
|
-
import { noMoveReasons } from "../ui/adapters/pluginsAdapter.js";
|
|
30
29
|
|
|
31
30
|
const PROJECT = "/tmp/project";
|
|
32
31
|
|
|
@@ -776,25 +775,6 @@ describe("applyPlugins — read-back against beforeVersions", () => {
|
|
|
776
775
|
expect(result.ok).toBe(1);
|
|
777
776
|
expect(result.unchanged).toEqual([]);
|
|
778
777
|
});
|
|
779
|
-
|
|
780
|
-
test("the TUI's collector names the same cause the CLI prints", async () => {
|
|
781
|
-
// The screen used to branch on `failed.length` alone: a no-op update
|
|
782
|
-
// closed with a success and a row that had not moved.
|
|
783
|
-
const { deps } = stubDeps("1.0.0");
|
|
784
|
-
const reasons = noMoveReasons();
|
|
785
|
-
const result = await applyPlugins(
|
|
786
|
-
[item({ beforeVersions: { project: "1.0.0" } })],
|
|
787
|
-
PROJECT,
|
|
788
|
-
deps,
|
|
789
|
-
reasons.reporter,
|
|
790
|
-
);
|
|
791
|
-
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
792
|
-
expect(reasons.lines(result.unchanged)).toEqual([
|
|
793
|
-
"dev@magus: the installer exited 0 but dev@magus is still 1.0.0",
|
|
794
|
-
]);
|
|
795
|
-
// An item with no recorded cause still gets a line.
|
|
796
|
-
expect(reasons.lines(["other@magus"])).toEqual(["other@magus"]);
|
|
797
|
-
});
|
|
798
778
|
});
|
|
799
779
|
|
|
800
780
|
// ── B4: installed must mean LOADED ──────────────────────────────────────────
|
|
@@ -847,8 +827,7 @@ describe("B4: an install or update ends with the loader on the new version", ()
|
|
|
847
827
|
{
|
|
848
828
|
ok: true,
|
|
849
829
|
name: "dev@magus",
|
|
850
|
-
detail:
|
|
851
|
-
"project → 2.0.0 · Claude Code now loads v2.0.0 here (3 older records moved forward)",
|
|
830
|
+
detail: "project → 2.0.0 · Claude Code now loads v2.0.0 in this folder",
|
|
852
831
|
},
|
|
853
832
|
]);
|
|
854
833
|
});
|
|
@@ -876,8 +855,10 @@ describe("B4: an install or update ends with the loader on the new version", ()
|
|
|
876
855
|
{
|
|
877
856
|
ok: false,
|
|
878
857
|
name: "dev@magus",
|
|
858
|
+
// The version still loading IS the reason; the repair's own message
|
|
859
|
+
// said it a second time, in the mechanism's words.
|
|
879
860
|
detail:
|
|
880
|
-
"installed v2.0.0, but Claude Code still loads v1.0.0
|
|
861
|
+
"installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
|
|
881
862
|
},
|
|
882
863
|
]);
|
|
883
864
|
});
|
|
@@ -917,7 +898,7 @@ describe("B4: an install or update ends with the loader on the new version", ()
|
|
|
917
898
|
await expect(
|
|
918
899
|
updatePluginInScope("dev@magus", "project", PROJECT, deps),
|
|
919
900
|
).rejects.toThrow(
|
|
920
|
-
"installed v2.0.0, but Claude Code still loads v1.0.0
|
|
901
|
+
"installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
|
|
921
902
|
);
|
|
922
903
|
});
|
|
923
904
|
|
|
@@ -938,7 +919,9 @@ describe("B4: an install or update ends with the loader on the new version", ()
|
|
|
938
919
|
const { deps } = loaderDeps("1.0.0", false);
|
|
939
920
|
await expect(
|
|
940
921
|
updatePluginInScope("dev@magus", "user", PROJECT, deps),
|
|
941
|
-
).rejects.toThrow(
|
|
922
|
+
).rejects.toThrow(
|
|
923
|
+
"installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
|
|
924
|
+
);
|
|
942
925
|
});
|
|
943
926
|
});
|
|
944
927
|
|
|
@@ -1067,7 +1050,8 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
|
|
|
1067
1050
|
{
|
|
1068
1051
|
ok: true,
|
|
1069
1052
|
name: "dev@magus",
|
|
1070
|
-
detail:
|
|
1053
|
+
detail:
|
|
1054
|
+
"project → 8.1.3 · Claude Code updated another folder of this repository instead of this one — this folder now loads v8.1.3 too",
|
|
1071
1055
|
},
|
|
1072
1056
|
]);
|
|
1073
1057
|
});
|
|
@@ -1084,7 +1068,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
|
|
|
1084
1068
|
// forward by the loader check (B4), and no misdirection is reported.
|
|
1085
1069
|
expect(repaired).toEqual(["8.1.3"]);
|
|
1086
1070
|
expect(rows[0]?.detail).toBe(
|
|
1087
|
-
"project → 8.1.3 · Claude Code now loads v8.1.3
|
|
1071
|
+
"project → 8.1.3 · Claude Code now loads v8.1.3 in this folder",
|
|
1088
1072
|
);
|
|
1089
1073
|
});
|
|
1090
1074
|
|
|
@@ -1097,7 +1081,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
|
|
|
1097
1081
|
expect(row(HERE).version).toBe("8.0.0");
|
|
1098
1082
|
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
1099
1083
|
expect(rows[0]?.detail).toBe(
|
|
1100
|
-
|
|
1084
|
+
"no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: no cache directory for v8.1.3)",
|
|
1101
1085
|
);
|
|
1102
1086
|
});
|
|
1103
1087
|
|
|
@@ -1125,7 +1109,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
|
|
|
1125
1109
|
expect(repaired).toEqual([]);
|
|
1126
1110
|
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
1127
1111
|
expect(rows[0]?.detail).toBe(
|
|
1128
|
-
|
|
1112
|
+
"no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: it went to v8.5.0, not the v8.1.3 this update asked for)",
|
|
1129
1113
|
);
|
|
1130
1114
|
});
|
|
1131
1115
|
|
|
@@ -430,19 +430,23 @@ describe("per-scope decisions — a stale scope is never masked", () => {
|
|
|
430
430
|
// No note: there is nothing for the reader to do about it.
|
|
431
431
|
expect(item.note).toBeUndefined();
|
|
432
432
|
// And it is not confused with the withdrawn case, which is the next test.
|
|
433
|
-
expect(item.reason).not.toBe("
|
|
433
|
+
expect(item.reason).not.toBe("deprecated");
|
|
434
434
|
});
|
|
435
435
|
|
|
436
|
-
test("
|
|
436
|
+
test("a declared plugin ABSENT from every catalog is deprecated", () => {
|
|
437
437
|
// The genuinely withdrawn case: no catalog entry at all, the installed
|
|
438
438
|
// version known only from the registry.
|
|
439
439
|
const [item] = planPluginUpdates(
|
|
440
440
|
{ "withdrawn@magus": "latest" },
|
|
441
441
|
catalog(info({ id: "dev@magus", version: "4.6.1" })),
|
|
442
442
|
);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
443
|
+
// Its marketplace was read and does not list it: no longer published, so
|
|
444
|
+
// the work is removing it — here from the profile alone, since nothing is
|
|
445
|
+
// installed to uninstall.
|
|
446
|
+
expect(item.action).toBe("remove");
|
|
447
|
+
expect(item.note).toBe("no longer published");
|
|
448
|
+
expect(item.reason).toBe("deprecated");
|
|
449
|
+
expect(item.scopes).toEqual([]);
|
|
446
450
|
});
|
|
447
451
|
});
|
|
448
452
|
|
|
@@ -855,7 +859,7 @@ describe("binNeedsWork — one predicate for apply, the meter and 'has work'", (
|
|
|
855
859
|
{ ...itemFor("upgrade"), name: "mnemex", target: "1.0.0" },
|
|
856
860
|
spec,
|
|
857
861
|
),
|
|
858
|
-
).toEqual({ cmd: "bun", args: ["
|
|
862
|
+
).toEqual({ cmd: "bun", args: ["add", "-g", "mnemex@1.0.0"] });
|
|
859
863
|
expect(
|
|
860
864
|
binCommandFor(
|
|
861
865
|
{ ...itemFor("upgrade"), target: "2.0.0" },
|
|
@@ -993,19 +997,20 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
|
|
|
993
997
|
info({ id: "dev@magus", version: "4.7.0" }),
|
|
994
998
|
),
|
|
995
999
|
);
|
|
996
|
-
expect(item.action).toBe("
|
|
1000
|
+
expect(item.action).toBe("remove");
|
|
997
1001
|
expect(item.action).not.toBe("current");
|
|
998
1002
|
expect(item.available).toBeNull();
|
|
999
1003
|
expect(item.target).toBeNull();
|
|
1000
|
-
|
|
1004
|
+
// A removal's scopes are where it is uninstalled: every scope installed here.
|
|
1005
|
+
expect(item.scopes).toEqual(["project"]);
|
|
1001
1006
|
});
|
|
1002
1007
|
|
|
1003
1008
|
// `moved` is GONE, and `movedTo` with it. It was inferred from a bare name
|
|
1004
1009
|
// match: a plugin published to two marketplaces — claudish is — reported
|
|
1005
1010
|
// "moved → magus" when nothing had moved and both copies were installed.
|
|
1006
1011
|
// The inference cannot tell a move from a dual publication, so it is not made
|
|
1007
|
-
// at all. An orphan is `
|
|
1008
|
-
test("an orphan is
|
|
1012
|
+
// at all. An orphan is `deprecated`, which is a fact rather than a guess.
|
|
1013
|
+
test("an orphan is deprecated, and never guesses where it went", () => {
|
|
1009
1014
|
const [item] = planPluginUpdates(
|
|
1010
1015
|
{ "seo@magus": "latest" },
|
|
1011
1016
|
catalog(
|
|
@@ -1014,13 +1019,13 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
|
|
|
1014
1019
|
),
|
|
1015
1020
|
);
|
|
1016
1021
|
expect(item).toMatchObject({
|
|
1017
|
-
action: "
|
|
1018
|
-
reason: "
|
|
1022
|
+
action: "remove",
|
|
1023
|
+
reason: "deprecated",
|
|
1019
1024
|
});
|
|
1020
1025
|
expect(item.note).not.toMatch(/moved/i);
|
|
1021
1026
|
});
|
|
1022
1027
|
|
|
1023
|
-
test("an orphan whose marketplace we DID read is
|
|
1028
|
+
test("an orphan whose marketplace we DID read is deprecated", () => {
|
|
1024
1029
|
const [item] = planPluginUpdates(
|
|
1025
1030
|
{ "seo@magus": "latest" },
|
|
1026
1031
|
catalog(
|
|
@@ -1029,9 +1034,9 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
|
|
|
1029
1034
|
),
|
|
1030
1035
|
);
|
|
1031
1036
|
expect(item).toMatchObject({
|
|
1032
|
-
action: "
|
|
1033
|
-
reason: "
|
|
1034
|
-
note: "
|
|
1037
|
+
action: "remove",
|
|
1038
|
+
reason: "deprecated",
|
|
1039
|
+
note: "no longer published",
|
|
1035
1040
|
});
|
|
1036
1041
|
});
|
|
1037
1042
|
|
|
@@ -1062,7 +1067,7 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
|
|
|
1062
1067
|
expect(item.reason).toBe("marketplace-unread");
|
|
1063
1068
|
});
|
|
1064
1069
|
|
|
1065
|
-
test("an orphan is
|
|
1070
|
+
test("an orphan is deprecated under an exact pin too", () => {
|
|
1066
1071
|
const [item] = planPluginUpdates(
|
|
1067
1072
|
{ "seo@magus": "1.8.0" },
|
|
1068
1073
|
catalog(
|
|
@@ -1070,7 +1075,7 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
|
|
|
1070
1075
|
info({ id: "dev@magus", version: "4.7.0" }),
|
|
1071
1076
|
),
|
|
1072
1077
|
);
|
|
1073
|
-
expect(item.action).toBe("
|
|
1078
|
+
expect(item.action).toBe("remove");
|
|
1074
1079
|
});
|
|
1075
1080
|
});
|
|
1076
1081
|
|
|
@@ -1359,6 +1364,7 @@ describe("countPluginStates", () => {
|
|
|
1359
1364
|
repair: 1,
|
|
1360
1365
|
current: 0,
|
|
1361
1366
|
"stale-load": 0,
|
|
1367
|
+
remove: 0,
|
|
1362
1368
|
unknown: 1,
|
|
1363
1369
|
});
|
|
1364
1370
|
});
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
buildStaleLoadModal,
|
|
4
|
-
staleLoadRepairNotices,
|
|
5
|
-
} from "../ui/hooks/useStaleLoadModal";
|
|
2
|
+
import { buildStaleLoadModal } from "../ui/hooks/useStaleLoadModal";
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* The startup popup for a broken plugin.
|
|
@@ -137,56 +134,3 @@ describe("buildStaleLoadModal", () => {
|
|
|
137
134
|
});
|
|
138
135
|
});
|
|
139
136
|
});
|
|
140
|
-
|
|
141
|
-
describe("staleLoadRepairNotices", () => {
|
|
142
|
-
it("says what loads NOW for the repaired plugins, in one line", () => {
|
|
143
|
-
expect(
|
|
144
|
-
staleLoadRepairNotices([
|
|
145
|
-
{ pluginId: "madbench@magus", target: "0.5.0", ok: true },
|
|
146
|
-
{ pluginId: "dev@magus", target: "7.4.0", ok: true },
|
|
147
|
-
]),
|
|
148
|
-
).toEqual([
|
|
149
|
-
{
|
|
150
|
-
text: "Now loading madbench@magus v0.5.0, dev@magus v7.4.0",
|
|
151
|
-
tone: "success",
|
|
152
|
-
},
|
|
153
|
-
]);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("reports a failed repair with its own reason, never as a success", () => {
|
|
157
|
-
expect(
|
|
158
|
-
staleLoadRepairNotices([
|
|
159
|
-
{
|
|
160
|
-
pluginId: "madbench@magus",
|
|
161
|
-
target: "0.5.0",
|
|
162
|
-
ok: false,
|
|
163
|
-
error: "the loader still resolves v0.3.0 here, not v0.5.0",
|
|
164
|
-
},
|
|
165
|
-
]),
|
|
166
|
-
).toEqual([
|
|
167
|
-
{
|
|
168
|
-
text: "madbench@magus not repaired: the loader still resolves v0.3.0 here, not v0.5.0",
|
|
169
|
-
tone: "warning",
|
|
170
|
-
},
|
|
171
|
-
]);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("falls back to a plain reason rather than an empty line", () => {
|
|
175
|
-
expect(
|
|
176
|
-
staleLoadRepairNotices([
|
|
177
|
-
{ pluginId: "a@magus", target: "1.0.0", ok: false },
|
|
178
|
-
]),
|
|
179
|
-
).toEqual([
|
|
180
|
-
{
|
|
181
|
-
text: "a@magus not repaired: repair did not take effect",
|
|
182
|
-
tone: "warning",
|
|
183
|
-
},
|
|
184
|
-
]);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("says so when there was nothing to repair", () => {
|
|
188
|
-
expect(staleLoadRepairNotices([])).toEqual([
|
|
189
|
-
{ text: "Nothing needed repairing", tone: "success" },
|
|
190
|
-
]);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
@@ -178,7 +178,7 @@ const magusCatalog = (...extra: PluginInfo[]) =>
|
|
|
178
178
|
);
|
|
179
179
|
|
|
180
180
|
describe("planPluginUpdates — a pin its own marketplace does not offer", () => {
|
|
181
|
-
test("declared, not installed, marketplace read, plugin absent →
|
|
181
|
+
test("declared, not installed, marketplace read, plugin absent → remove", () => {
|
|
182
182
|
// The bug: the !installed branch ran before any catalog guard, so
|
|
183
183
|
// `agentdev@magus` (retired) and `seo@magus` (moved to magus-marketing)
|
|
184
184
|
// planned as `install` with the literal pin as target on every run,
|
|
@@ -188,20 +188,20 @@ describe("planPluginUpdates — a pin its own marketplace does not offer", () =>
|
|
|
188
188
|
{ "agentdev@magus": "latest" },
|
|
189
189
|
magusCatalog(),
|
|
190
190
|
);
|
|
191
|
+
// Deprecated now, and the work is dropping the pin from the profile:
|
|
192
|
+
// still never an `install` of something nothing can fetch.
|
|
191
193
|
expect(items).toHaveLength(1);
|
|
192
|
-
expect(items[0]?.action).toBe("
|
|
194
|
+
expect(items[0]?.action).toBe("remove");
|
|
193
195
|
expect(items[0]?.target).toBeNull();
|
|
194
|
-
expect(items[0]?.note).
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
expect(items[0]?.reason).toBe("not-offered");
|
|
196
|
+
expect(items[0]?.note).toBe("no longer published");
|
|
197
|
+
expect(items[0]?.reason).toBe("deprecated");
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
test("an exact pin its marketplace does not offer is also
|
|
200
|
+
test("an exact pin its marketplace does not offer is also deprecated", () => {
|
|
201
201
|
const items = planPluginUpdates({ "gone@magus": "3.0.0" }, magusCatalog());
|
|
202
|
-
expect(items[0]?.action).toBe("
|
|
202
|
+
expect(items[0]?.action).toBe("remove");
|
|
203
203
|
expect(items[0]?.target).toBeNull();
|
|
204
|
-
expect(items[0]?.reason).toBe("
|
|
204
|
+
expect(items[0]?.reason).toBe("deprecated");
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
test("declared, not installed, and the catalog does offer it → install", () => {
|
|
@@ -345,14 +345,14 @@ describe("installed with no comparable catalog version — two different causes"
|
|
|
345
345
|
// The distinction this test exists for is between the two REASONS, and it
|
|
346
346
|
// still holds. What changed is the verdict: declaring no version is not a
|
|
347
347
|
// problem, so the row is `current` and says nothing. Being withdrawn IS a
|
|
348
|
-
// problem, so the next test's row is `
|
|
348
|
+
// problem, so the next test's row is `remove` and says so.
|
|
349
349
|
expect(items[0]?.reason).toBe("no-published-version");
|
|
350
|
-
expect(items[0]?.reason).not.toBe("
|
|
350
|
+
expect(items[0]?.reason).not.toBe("deprecated");
|
|
351
351
|
expect(items[0]?.action).toBe("current");
|
|
352
352
|
expect(items[0]?.note).toBeUndefined();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
test("absent from the catalog entirely IS
|
|
355
|
+
test("absent from the catalog entirely IS deprecated", () => {
|
|
356
356
|
const items = planPluginUpdates(
|
|
357
357
|
{ "gone@magus": "latest" },
|
|
358
358
|
new Map([
|
|
@@ -366,11 +366,9 @@ describe("installed with no comparable catalog version — two different causes"
|
|
|
366
366
|
],
|
|
367
367
|
]),
|
|
368
368
|
);
|
|
369
|
-
expect(items[0]?.action).toBe("
|
|
370
|
-
expect(items[0]?.note).
|
|
371
|
-
|
|
372
|
-
);
|
|
373
|
-
expect(items[0]?.reason).toBe("not-offered");
|
|
369
|
+
expect(items[0]?.action).toBe("remove");
|
|
370
|
+
expect(items[0]?.note).toBe("no longer published");
|
|
371
|
+
expect(items[0]?.reason).toBe("deprecated");
|
|
374
372
|
});
|
|
375
373
|
});
|
|
376
374
|
|
package/src/cli/ansi.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { barTrack } from "../ui/theme-mode.js";
|
|
25
25
|
import { brand } from "../ui/theme.js";
|
|
26
|
+
import { CSI, stripAnsi } from "../utils/string-utils.js";
|
|
26
27
|
|
|
27
28
|
// -- colour capability -------------------------------------------------------
|
|
28
29
|
|
|
@@ -192,13 +193,7 @@ export function dim(text: string): string {
|
|
|
192
193
|
|
|
193
194
|
// -- measuring ---------------------------------------------------------------
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC is the thing being matched.
|
|
197
|
-
const CSI = /\x1b\[[0-9;?]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
198
|
-
|
|
199
|
-
export function stripAnsi(text: string): string {
|
|
200
|
-
return text.replace(CSI, "");
|
|
201
|
-
}
|
|
196
|
+
export { stripAnsi };
|
|
202
197
|
|
|
203
198
|
/**
|
|
204
199
|
* Cells one codepoint occupies: 0, 1 or 2.
|