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
|
@@ -35,12 +35,14 @@ import type { MarketplacePlugin } from "./marketplace-catalog.js";
|
|
|
35
35
|
/**
|
|
36
36
|
* How long a cached catalog is served before we ask again.
|
|
37
37
|
*
|
|
38
|
-
* This is the ONLY refresh rhythm magus has, and it
|
|
39
|
-
* `magus update`
|
|
40
|
-
* `update`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* This is the ONLY refresh rhythm magus has, and it applies to both surfaces:
|
|
39
|
+
* `magus update` and the TUI read a catalog written here within the hour
|
|
40
|
+
* before they go to the network. `update` pulls the marketplace clones and
|
|
41
|
+
* drops its in-process memo before planning, but it does NOT clear this file,
|
|
42
|
+
* so a release published inside the TTL is not seen by `update` either. Only
|
|
43
|
+
* an explicit refresh (`r` in the TUI) clears it. Anything that reports "no
|
|
44
|
+
* update" — or decides a plugin is no longer published — off a cached catalog
|
|
45
|
+
* is working from an answer up to an hour old, by design.
|
|
44
46
|
*/
|
|
45
47
|
export const CATALOG_TTL_MS = 60 * 60 * 1000;
|
|
46
48
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execFile } from "node:child_process";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
13
15
|
import { promisify } from "node:util";
|
|
14
16
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
15
17
|
import { which } from "../utils/command-utils.js";
|
|
@@ -28,13 +30,12 @@ export type PluginScope = "user" | "project" | "local";
|
|
|
28
30
|
* Get the path to the claude CLI binary
|
|
29
31
|
* @throws Error if claude CLI is not found in PATH
|
|
30
32
|
*/
|
|
33
|
+
const CLAUDE_NOT_FOUND =
|
|
34
|
+
"claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code";
|
|
35
|
+
|
|
31
36
|
async function getClaudePath(): Promise<string> {
|
|
32
37
|
const claudePath = await which("claude");
|
|
33
|
-
if (!claudePath)
|
|
34
|
-
throw new Error(
|
|
35
|
-
"claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code",
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
+
if (!claudePath) throw new Error(CLAUDE_NOT_FOUND);
|
|
38
39
|
return claudePath;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -42,29 +43,84 @@ async function getClaudePath(): Promise<string> {
|
|
|
42
43
|
* Execute a claude CLI command and return stdout
|
|
43
44
|
* @param args - Arguments to pass to claude
|
|
44
45
|
* @param timeoutMs - Timeout in milliseconds (default: 30s)
|
|
46
|
+
* @param cwd - Working directory; Claude Code resolves project and local
|
|
47
|
+
* scope from it. Defaults to this process's.
|
|
45
48
|
* @returns stdout from the command
|
|
46
49
|
*/
|
|
47
|
-
async function execClaude(
|
|
50
|
+
async function execClaude(
|
|
51
|
+
args: string[],
|
|
52
|
+
timeoutMs = 30000,
|
|
53
|
+
cwd?: string,
|
|
54
|
+
): Promise<string> {
|
|
48
55
|
const claudePath = await getClaudePath();
|
|
49
56
|
try {
|
|
50
57
|
const { stdout } = await execFileAsync(claudePath, args, {
|
|
51
58
|
timeout: timeoutMs,
|
|
59
|
+
...(cwd ? { cwd } : {}),
|
|
52
60
|
});
|
|
53
61
|
return stdout.trim();
|
|
54
62
|
} catch (error: unknown) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
throw new Error(claudeFailureMessage(error, args, timeoutMs, cwd));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a failed `claude` command says, in the words a user reads.
|
|
69
|
+
*
|
|
70
|
+
* Claude Code's own output first — stderr, then stdout — since that is the
|
|
71
|
+
* reason. Only when it printed nothing does Node's error message stand in, and
|
|
72
|
+
* that one opens with `Command failed: /absolute/path/to/claude plugin …`:
|
|
73
|
+
* the path and the argument list are the mechanism, not the reason, so that
|
|
74
|
+
* line is dropped and what follows it kept. A run that was killed for taking
|
|
75
|
+
* too long says so.
|
|
76
|
+
*
|
|
77
|
+
* A spawn that fails with ENOENT is not always a missing `claude`: Node and
|
|
78
|
+
* Bun both report a working directory that does not exist the same way,
|
|
79
|
+
* naming the binary. So when the run was given a `cwd` that is gone, that is
|
|
80
|
+
* what is said — by the folder's name, never its absolute path — rather than
|
|
81
|
+
* telling someone with Claude Code installed to install it.
|
|
82
|
+
*/
|
|
83
|
+
export function claudeFailureMessage(
|
|
84
|
+
error: unknown,
|
|
85
|
+
args: readonly string[],
|
|
86
|
+
timeoutMs: number,
|
|
87
|
+
cwd?: string,
|
|
88
|
+
): string {
|
|
89
|
+
const execError = error as {
|
|
90
|
+
code?: string | number;
|
|
91
|
+
stderr?: string;
|
|
92
|
+
stdout?: string;
|
|
93
|
+
message?: string;
|
|
94
|
+
killed?: boolean;
|
|
95
|
+
signal?: string | null;
|
|
96
|
+
};
|
|
97
|
+
const output = execError.stderr?.trim() || execError.stdout?.trim();
|
|
98
|
+
if (output) return output;
|
|
99
|
+
if (execError.killed) {
|
|
100
|
+
return `claude ${args.slice(0, 2).join(" ")} did not finish within ${Math.round(timeoutMs / 1000)}s`;
|
|
101
|
+
}
|
|
102
|
+
const message = execError.message ?? "";
|
|
103
|
+
// The binary vanished between the PATH lookup and the spawn (Node says
|
|
104
|
+
// `spawn <path> ENOENT`, Bun `Executable not found in $PATH`). Same fact as
|
|
105
|
+
// the lookup failing, so the same words — and never the absolute path.
|
|
106
|
+
if (
|
|
107
|
+
execError.code === "ENOENT" ||
|
|
108
|
+
/\bENOENT\b|Executable not found in \$PATH/.test(message)
|
|
109
|
+
) {
|
|
110
|
+
if (cwd && !existsSync(cwd)) {
|
|
111
|
+
return `the folder ${path.basename(cwd) || cwd} no longer exists`;
|
|
112
|
+
}
|
|
113
|
+
return CLAUDE_NOT_FOUND;
|
|
114
|
+
}
|
|
115
|
+
if (execError.code === "EACCES" || /\bEACCES\b/.test(message)) {
|
|
116
|
+
return "claude could not be started: permission denied";
|
|
67
117
|
}
|
|
118
|
+
const rest = message
|
|
119
|
+
.replace(/^Command failed: [^\n]*\n?/, "")
|
|
120
|
+
// Any leftover mention of the binary by absolute path is mechanism.
|
|
121
|
+
.replace(/(?:\/[^\s/:]+)+\/claude\b/g, "claude")
|
|
122
|
+
.trim();
|
|
123
|
+
return rest || `claude ${args.slice(0, 2).join(" ")} failed`;
|
|
68
124
|
}
|
|
69
125
|
|
|
70
126
|
/**
|
|
@@ -186,6 +242,32 @@ export async function uninstallPlugin(
|
|
|
186
242
|
}
|
|
187
243
|
}
|
|
188
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Uninstall through `claude plugin uninstall` and NOTHING else: its error is
|
|
247
|
+
* thrown, never absorbed.
|
|
248
|
+
*
|
|
249
|
+
* {@link uninstallPlugin} falls back to editing the settings files directly
|
|
250
|
+
* when the command fails. That fallback is a direct writer of Claude Code's
|
|
251
|
+
* files, and it turns every failure into an apparent success — so a caller
|
|
252
|
+
* that reads the result back, as removing a deprecated plugin does, could
|
|
253
|
+
* never see why the plugin was still there. This one lets the command's own
|
|
254
|
+
* words reach the caller.
|
|
255
|
+
*
|
|
256
|
+
* Runs in `projectPath` when given, because Claude Code resolves project and
|
|
257
|
+
* local scope from its working directory.
|
|
258
|
+
*/
|
|
259
|
+
export async function uninstallPluginViaCli(
|
|
260
|
+
pluginId: string,
|
|
261
|
+
scope: PluginScope,
|
|
262
|
+
projectPath?: string,
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
await execClaude(
|
|
265
|
+
["plugin", "uninstall", pluginId, "--scope", scope],
|
|
266
|
+
30000,
|
|
267
|
+
projectPath,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
189
271
|
/**
|
|
190
272
|
* Repair a plugin whose files drifted from the marketplace under an unchanged
|
|
191
273
|
* version — uninstall, then install again at the same scope.
|
|
@@ -270,7 +352,12 @@ export async function disablePlugin(
|
|
|
270
352
|
* {@link installPlugin}, and content drift under an unchanged version still
|
|
271
353
|
* needs {@link repairPlugin} — `update` has no version bump to act on there.
|
|
272
354
|
*
|
|
273
|
-
*
|
|
355
|
+
* On a "not found in marketplace" error it recovers once and retries, exactly
|
|
356
|
+
* as {@link installPlugin} does (see `recoverMissingMarketplace`): a registered
|
|
357
|
+
* marketplace has its source repaired and its clone fast-forwarded with
|
|
358
|
+
* `git pull --ff-only` — never `claude plugin marketplace update`, which
|
|
359
|
+
* deletes the clone before re-cloning — and a missing clone or an unregistered
|
|
360
|
+
* default marketplace is added with `claude plugin marketplace add`.
|
|
274
361
|
*/
|
|
275
362
|
export async function updatePlugin(
|
|
276
363
|
pluginId: string,
|