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
|
@@ -5,14 +5,18 @@ import {
|
|
|
5
5
|
cliToolUninstall,
|
|
6
6
|
uninstallBlocker,
|
|
7
7
|
} from "../../services/cli-tool-commands.js";
|
|
8
|
+
import { doctorFixCommand } from "../../shared/plugin/hooks/lib/requires-probe.js";
|
|
8
9
|
import { formatCommand } from "../../utils/command-utils.js";
|
|
9
10
|
import {
|
|
10
11
|
type CliToolEnterAction,
|
|
11
12
|
type CliToolRow,
|
|
13
|
+
type CliToolsRow,
|
|
14
|
+
type PluginManagedRow,
|
|
12
15
|
cliToolEnterAction,
|
|
13
16
|
cliToolStatusText,
|
|
14
17
|
displayVersion,
|
|
15
18
|
isInstalled,
|
|
19
|
+
isPluginManagedRow,
|
|
16
20
|
} from "../adapters/cliToolsAdapter.js";
|
|
17
21
|
import { rowLead } from "../components/primitives/index.js";
|
|
18
22
|
import { type UiColor, theme } from "../theme.js";
|
|
@@ -94,13 +98,139 @@ function toneColor(tone: StatusTone): UiColor {
|
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// ─── Plugin-managed rows ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** The mark and words for a plugin-managed row, from the dependency probe. */
|
|
104
|
+
function managedStatus(presence: PluginManagedRow["presence"]): {
|
|
105
|
+
icon: string;
|
|
106
|
+
color: UiColor;
|
|
107
|
+
text: string;
|
|
108
|
+
} {
|
|
109
|
+
switch (presence?.status) {
|
|
110
|
+
case undefined:
|
|
111
|
+
return { icon: "○", color: theme.colors.muted, text: "● Checking..." };
|
|
112
|
+
case "present":
|
|
113
|
+
return { icon: "●", color: theme.colors.success, text: "● Installed" };
|
|
114
|
+
case "off-path":
|
|
115
|
+
return {
|
|
116
|
+
icon: "!",
|
|
117
|
+
color: theme.colors.warning,
|
|
118
|
+
text: `! Installed in ${presence.prefix}, which is not on the PATH Claude Code starts with`,
|
|
119
|
+
};
|
|
120
|
+
case "missing":
|
|
121
|
+
return { icon: "○", color: theme.colors.muted, text: "○ Not installed" };
|
|
122
|
+
case "unknown":
|
|
123
|
+
return {
|
|
124
|
+
icon: "?",
|
|
125
|
+
color: theme.colors.warning,
|
|
126
|
+
text: `? Could not tell: ${presence.reason}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderPluginManagedRow(
|
|
132
|
+
row: PluginManagedRow,
|
|
133
|
+
isSelected: boolean,
|
|
134
|
+
): React.ReactNode {
|
|
135
|
+
const { icon, color } = managedStatus(row.presence);
|
|
136
|
+
const suffix =
|
|
137
|
+
row.presence === null ? " ..." : ` · managed by ${row.managedBy}`;
|
|
138
|
+
if (isSelected) {
|
|
139
|
+
return (
|
|
140
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
141
|
+
{rowLead(true)}
|
|
142
|
+
{icon} {row.entry.displayName}
|
|
143
|
+
{suffix}{" "}
|
|
144
|
+
</text>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return (
|
|
148
|
+
<text fg={theme.colors.text}>
|
|
149
|
+
<span fg={color}>
|
|
150
|
+
{rowLead(false)}
|
|
151
|
+
{icon}
|
|
152
|
+
</span>
|
|
153
|
+
<span fg={theme.colors.text}> {row.entry.displayName}</span>
|
|
154
|
+
<span fg={theme.colors.dim}>{suffix}</span>
|
|
155
|
+
</text>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderPluginManagedDetail(
|
|
160
|
+
row: PluginManagedRow,
|
|
161
|
+
width: number,
|
|
162
|
+
): React.ReactNode {
|
|
163
|
+
const { entry, managedBy, presence } = row;
|
|
164
|
+
const status = managedStatus(presence);
|
|
165
|
+
const here =
|
|
166
|
+
presence?.status === "present" || presence?.status === "off-path";
|
|
167
|
+
return (
|
|
168
|
+
<box flexDirection="column" width={width}>
|
|
169
|
+
<box flexDirection="row" marginBottom={1}>
|
|
170
|
+
<text fg={theme.colors.info}>
|
|
171
|
+
<strong>{entry.displayName}</strong>
|
|
172
|
+
</text>
|
|
173
|
+
</box>
|
|
174
|
+
|
|
175
|
+
<text fg={theme.colors.muted}>{entry.description}</text>
|
|
176
|
+
|
|
177
|
+
<box marginTop={1} flexDirection="column">
|
|
178
|
+
<box flexDirection="row">
|
|
179
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
180
|
+
{"Status "}
|
|
181
|
+
</text>
|
|
182
|
+
<text fg={status.color} minWidth={0}>
|
|
183
|
+
{wrapValue(status.text, width)}
|
|
184
|
+
</text>
|
|
185
|
+
</box>
|
|
186
|
+
<box flexDirection="row">
|
|
187
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
188
|
+
{"Managed "}
|
|
189
|
+
</text>
|
|
190
|
+
<text fg={theme.colors.text}>by {managedBy}</text>
|
|
191
|
+
</box>
|
|
192
|
+
{!here && presence !== null && row.installText ? (
|
|
193
|
+
<box flexDirection="row">
|
|
194
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
195
|
+
{"Install "}
|
|
196
|
+
</text>
|
|
197
|
+
<text fg={theme.colors.info} minWidth={0}>
|
|
198
|
+
{wrapValue(row.installText, width)}
|
|
199
|
+
</text>
|
|
200
|
+
</box>
|
|
201
|
+
) : null}
|
|
202
|
+
<box flexDirection="row">
|
|
203
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
204
|
+
{"Website "}
|
|
205
|
+
</text>
|
|
206
|
+
<box flexGrow={1} minWidth={0}>
|
|
207
|
+
<text fg={theme.colors.info} wrapMode="char">
|
|
208
|
+
{entry.website}
|
|
209
|
+
</text>
|
|
210
|
+
</box>
|
|
211
|
+
</box>
|
|
212
|
+
</box>
|
|
213
|
+
|
|
214
|
+
<box marginTop={2}>
|
|
215
|
+
<text fg={theme.colors.muted}>
|
|
216
|
+
{wrapText(
|
|
217
|
+
`${managedBy} installs ${entry.name} and decides its version; it cannot be installed, updated or removed from here. ${doctorFixCommand(null)} installs it when it is missing.`,
|
|
218
|
+
width,
|
|
219
|
+
).join("\n")}
|
|
220
|
+
</text>
|
|
221
|
+
</box>
|
|
222
|
+
</box>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
97
226
|
// ─── Row renderer ──────────────────────────────────────────────────────────────
|
|
98
227
|
|
|
99
228
|
export function renderCliToolRow(
|
|
100
|
-
row:
|
|
229
|
+
row: CliToolsRow,
|
|
101
230
|
_index: number,
|
|
102
231
|
isSelected: boolean,
|
|
103
232
|
): React.ReactNode {
|
|
233
|
+
if (isPluginManagedRow(row)) return renderPluginManagedRow(row, isSelected);
|
|
104
234
|
const { tool, status, install } = row;
|
|
105
235
|
const installed = isInstalled(status);
|
|
106
236
|
const allMethods = install?.all ?? [];
|
|
@@ -167,10 +297,13 @@ export function renderCliToolRow(
|
|
|
167
297
|
// ─── Detail renderer ───────────────────────────────────────────────────────────
|
|
168
298
|
|
|
169
299
|
export function renderCliToolDetail(
|
|
170
|
-
row:
|
|
300
|
+
row: CliToolsRow | undefined,
|
|
171
301
|
/** Usable detail-panel columns (`detailPanelWidth`); the scrollbox does not constrain width. */
|
|
172
302
|
width: number,
|
|
173
303
|
): React.ReactNode {
|
|
304
|
+
if (row && isPluginManagedRow(row)) {
|
|
305
|
+
return renderPluginManagedDetail(row, width);
|
|
306
|
+
}
|
|
174
307
|
if (!row) {
|
|
175
308
|
return (
|
|
176
309
|
<box
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
import type { McpServer } from "../../types/index.js";
|
|
2
|
+
import type { McpRegistryServer, McpServer } from "../../types/index.js";
|
|
3
3
|
import { rowLead } from "../components/primitives/index.js";
|
|
4
4
|
import { theme } from "../theme.js";
|
|
5
5
|
|
|
@@ -10,11 +10,27 @@ import { theme } from "../theme.js";
|
|
|
10
10
|
*/
|
|
11
11
|
const SERVER_ICON = "⬢";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A server added or removed this magus session, on its row: a running Claude
|
|
15
|
+
* Code picks the change up only after it restarts. One string for the curated
|
|
16
|
+
* and the registry rows alike.
|
|
17
|
+
*/
|
|
18
|
+
const RESTART_MARK = " ↻ restart Claude Code";
|
|
19
|
+
|
|
13
20
|
// ─── List item types ───────────────────────────────────────────────────────────
|
|
14
21
|
|
|
15
22
|
export type McpListItem =
|
|
16
23
|
| { kind: "category"; label: string }
|
|
17
|
-
| {
|
|
24
|
+
| {
|
|
25
|
+
kind: "server";
|
|
26
|
+
server: McpServer;
|
|
27
|
+
isInstalled: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Added or removed in this magus session: a running Claude Code picks
|
|
30
|
+
* it up only after it restarts. Absent means false.
|
|
31
|
+
*/
|
|
32
|
+
needsRestart?: boolean;
|
|
33
|
+
};
|
|
18
34
|
|
|
19
35
|
// ─── Row renderers ─────────────────────────────────────────────────────────────
|
|
20
36
|
|
|
@@ -39,13 +55,15 @@ export function renderMcpRow(
|
|
|
39
55
|
const { server, isInstalled } = item;
|
|
40
56
|
const icon = isInstalled ? "●" : "○";
|
|
41
57
|
const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
|
|
58
|
+
const restart = item.needsRestart ? RESTART_MARK : "";
|
|
42
59
|
|
|
43
60
|
if (isSelected) {
|
|
44
61
|
return (
|
|
45
62
|
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
46
63
|
{rowLead(true)}
|
|
47
64
|
{icon} {server.name}
|
|
48
|
-
{server.requiresConfig ? " ⚙" : ""}
|
|
65
|
+
{server.requiresConfig ? " ⚙" : ""}
|
|
66
|
+
{restart}{" "}
|
|
49
67
|
</text>
|
|
50
68
|
);
|
|
51
69
|
}
|
|
@@ -58,10 +76,91 @@ export function renderMcpRow(
|
|
|
58
76
|
</span>
|
|
59
77
|
<span fg={theme.colors.text}> {server.name}</span>
|
|
60
78
|
{server.requiresConfig ? <span fg={theme.colors.warning}> ⚙</span> : null}
|
|
79
|
+
{restart ? <span fg={theme.colors.warning}>{restart}</span> : null}
|
|
61
80
|
</text>
|
|
62
81
|
);
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
// ─── Registry rows ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One server on the MCP Registry screen, with the same two states a curated
|
|
88
|
+
* row on the MCP screen carries: installed in this project, and — when it was
|
|
89
|
+
* added this magus session — that a running Claude Code picks it up only after
|
|
90
|
+
* it restarts.
|
|
91
|
+
*
|
|
92
|
+
* Adding from the registry used to show nothing at all: the restart state was
|
|
93
|
+
* drawn only on curated rows, and a registry server is not one, so the promise
|
|
94
|
+
* "shown on the MCP screen" had no row to be shown on.
|
|
95
|
+
*/
|
|
96
|
+
export interface McpRegistryListItem {
|
|
97
|
+
server: McpRegistryServer;
|
|
98
|
+
isInstalled: boolean;
|
|
99
|
+
needsRestart: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderMcpRegistryRow(
|
|
103
|
+
item: McpRegistryListItem,
|
|
104
|
+
_index: number,
|
|
105
|
+
isSelected: boolean,
|
|
106
|
+
): React.ReactNode {
|
|
107
|
+
const { server, isInstalled, needsRestart } = item;
|
|
108
|
+
const version = server.version ? ` v${server.version}` : "";
|
|
109
|
+
const icon = isInstalled ? "● " : "";
|
|
110
|
+
const restart = needsRestart ? RESTART_MARK : "";
|
|
111
|
+
if (isSelected) {
|
|
112
|
+
return (
|
|
113
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
114
|
+
{rowLead(true)}
|
|
115
|
+
{icon}
|
|
116
|
+
{server.name}
|
|
117
|
+
{version}
|
|
118
|
+
{restart}{" "}
|
|
119
|
+
</text>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
<text fg={theme.colors.text}>
|
|
124
|
+
<span>{rowLead(false)}</span>
|
|
125
|
+
{icon ? <span fg={theme.colors.success}>{icon}</span> : null}
|
|
126
|
+
<span>
|
|
127
|
+
<strong>{server.name}</strong>
|
|
128
|
+
</span>
|
|
129
|
+
<span fg={theme.colors.info}>{version}</span>
|
|
130
|
+
{restart ? <span fg={theme.colors.warning}>{restart}</span> : null}
|
|
131
|
+
</text>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The registry detail panel's state lines — installed or not, and the
|
|
137
|
+
* restart — in the same words and colours as a curated server's detail.
|
|
138
|
+
*/
|
|
139
|
+
export function renderMcpRegistryStatus(
|
|
140
|
+
item: McpRegistryListItem,
|
|
141
|
+
): React.ReactNode {
|
|
142
|
+
return (
|
|
143
|
+
<box flexDirection="column">
|
|
144
|
+
<box flexDirection="row">
|
|
145
|
+
<text fg={theme.colors.muted}>{"Status "}</text>
|
|
146
|
+
{item.isInstalled ? (
|
|
147
|
+
<text fg={theme.colors.success}>{"● Installed"}</text>
|
|
148
|
+
) : (
|
|
149
|
+
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
150
|
+
)}
|
|
151
|
+
</box>
|
|
152
|
+
{item.needsRestart ? (
|
|
153
|
+
<box flexDirection="row">
|
|
154
|
+
<text fg={theme.colors.muted}>{"Takes effect "}</text>
|
|
155
|
+
<text fg={theme.colors.warning} wrapMode="word">
|
|
156
|
+
{"↻ after restarting Claude Code"}
|
|
157
|
+
</text>
|
|
158
|
+
</box>
|
|
159
|
+
) : null}
|
|
160
|
+
</box>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
65
164
|
// ─── Detail renderer ───────────────────────────────────────────────────────────
|
|
66
165
|
|
|
67
166
|
export function renderMcpDetail(
|
|
@@ -112,6 +211,14 @@ export function renderMcpDetail(
|
|
|
112
211
|
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
113
212
|
)}
|
|
114
213
|
</box>
|
|
214
|
+
{item.needsRestart ? (
|
|
215
|
+
<box flexDirection="row">
|
|
216
|
+
<text fg={theme.colors.muted}>{"Takes effect "}</text>
|
|
217
|
+
<text fg={theme.colors.warning} wrapMode="word">
|
|
218
|
+
{"↻ after restarting Claude Code"}
|
|
219
|
+
</text>
|
|
220
|
+
</box>
|
|
221
|
+
) : null}
|
|
115
222
|
<box flexDirection="row">
|
|
116
223
|
<text fg={theme.colors.muted}>{"Type "}</text>
|
|
117
224
|
<text fg={theme.colors.text}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
|
|
3
|
+
import { mcpSettingsNeededText } from "../../services/plugin-env-needs.js";
|
|
3
4
|
import {
|
|
4
5
|
compareVersions,
|
|
5
6
|
hasMissingFiles,
|
|
@@ -10,11 +11,11 @@ import {
|
|
|
10
11
|
} from "../../services/plugin-manager.js";
|
|
11
12
|
import type { ScopeStatus } from "../../services/plugin-manager.js";
|
|
12
13
|
import type { PluginComponentKind } from "../../services/skills-manager.js";
|
|
13
|
-
import
|
|
14
|
-
PluginUpdateAction,
|
|
15
|
-
PluginUpdateItem,
|
|
14
|
+
import {
|
|
15
|
+
type PluginUpdateAction,
|
|
16
|
+
type PluginUpdateItem,
|
|
17
|
+
isKeptDeprecated,
|
|
16
18
|
} from "../../services/update-plan.js";
|
|
17
|
-
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
18
19
|
import type { PluginRelease } from "../../types/index.js";
|
|
19
20
|
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
20
21
|
import type {
|
|
@@ -43,7 +44,11 @@ import type {
|
|
|
43
44
|
import { componentChip } from "../theme-mode.js";
|
|
44
45
|
import { type UiColor, theme } from "../theme.js";
|
|
45
46
|
import { renderMarkdown } from "./markdownRenderer.js";
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
type PluginRowSegment,
|
|
49
|
+
isKnownVersion,
|
|
50
|
+
pluginRowText,
|
|
51
|
+
} from "./pluginRowText.js";
|
|
47
52
|
import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
|
|
48
53
|
|
|
49
54
|
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
@@ -285,6 +290,7 @@ const ACTION_LABEL: Record<PluginUpdateAction, string> = {
|
|
|
285
290
|
// The DETAIL panel's label, not the row's — this is where the mechanism is
|
|
286
291
|
// allowed to be named, and the note beside it spells out both versions.
|
|
287
292
|
"stale-load": "BROKEN — OLDER VERSION LOADS",
|
|
293
|
+
remove: "DEPRECATED",
|
|
288
294
|
unknown: "UNKNOWN",
|
|
289
295
|
};
|
|
290
296
|
|
|
@@ -321,12 +327,14 @@ function StatusLine({
|
|
|
321
327
|
: status.action === "update" && status.target
|
|
322
328
|
? `→ v${status.target}`
|
|
323
329
|
: "";
|
|
330
|
+
// Moved or still required: kept on purpose, which is known — never UNKNOWN.
|
|
331
|
+
const label = isKeptDeprecated(status) ? "KEPT" : ACTION_LABEL[status.action];
|
|
324
332
|
return (
|
|
325
333
|
<KeyValueLine
|
|
326
334
|
label="Status"
|
|
327
335
|
value={
|
|
328
336
|
<span>
|
|
329
|
-
<span fg={toneColor(tone)}>{
|
|
337
|
+
<span fg={toneColor(tone)}>{label}</span>
|
|
330
338
|
{detail ? <span fg={theme.colors.text}> {detail}</span> : null}
|
|
331
339
|
</span>
|
|
332
340
|
}
|
|
@@ -349,6 +357,57 @@ function pluginDetail(
|
|
|
349
357
|
|
|
350
358
|
// Published by no marketplace we read. The status line says which of the
|
|
351
359
|
// three things that means — moved, withdrawn, or its marketplace unread.
|
|
360
|
+
//
|
|
361
|
+
// Deprecated — a marketplace we read no longer lists it — is the one of the
|
|
362
|
+
// three that is a fact, and gets one plain line in the user's terms. The row
|
|
363
|
+
// already says `a` removes it; `d` removes just this one.
|
|
364
|
+
if (plugin.isOrphaned && status?.action === "remove") {
|
|
365
|
+
return (
|
|
366
|
+
<box flexDirection="column">
|
|
367
|
+
<box justifyContent="center">
|
|
368
|
+
<text bg={theme.colors.danger} fg={theme.hints.fg}>
|
|
369
|
+
<strong> {plugin.name} — DEPRECATED </strong>
|
|
370
|
+
</text>
|
|
371
|
+
</box>
|
|
372
|
+
{/* No Status line: the heading says DEPRECATED and the line below
|
|
373
|
+
says why, so the note would be the same fact a third time. */}
|
|
374
|
+
<box marginTop={1}>
|
|
375
|
+
<text fg={theme.colors.text}>
|
|
376
|
+
No longer published by{" "}
|
|
377
|
+
{plugin.marketplaceDisplay || plugin.marketplace}. Claude Code does
|
|
378
|
+
not load it.
|
|
379
|
+
</text>
|
|
380
|
+
</box>
|
|
381
|
+
<ActionHints hints={[{ key: "d", label: "Remove", tone: "danger" }]} />
|
|
382
|
+
</box>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// Kept on purpose: it moved to another marketplace, or a plugin that stays
|
|
386
|
+
// still requires it. The status line's note says which; nothing is wrong
|
|
387
|
+
// enough for amber, and `d` would only undo a deliberate keep.
|
|
388
|
+
if (plugin.isOrphaned && status && isKeptDeprecated(status)) {
|
|
389
|
+
return (
|
|
390
|
+
<box flexDirection="column">
|
|
391
|
+
<box justifyContent="center">
|
|
392
|
+
<text bg={theme.colors.muted} fg={theme.hints.fg}>
|
|
393
|
+
<strong> {plugin.name} — KEPT </strong>
|
|
394
|
+
</text>
|
|
395
|
+
</box>
|
|
396
|
+
<box marginTop={1}>
|
|
397
|
+
<StatusLine status={status} />
|
|
398
|
+
</box>
|
|
399
|
+
<box marginTop={1}>
|
|
400
|
+
<text fg={theme.colors.muted}>
|
|
401
|
+
No longer published by{" "}
|
|
402
|
+
{plugin.marketplaceDisplay || plugin.marketplace}.{" "}
|
|
403
|
+
{status.reason === "moved"
|
|
404
|
+
? "magus leaves this copy installed; the plugin is still published elsewhere."
|
|
405
|
+
: "magus keeps it installed while a plugin that needs it stays."}
|
|
406
|
+
</text>
|
|
407
|
+
</box>
|
|
408
|
+
</box>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
352
411
|
if (plugin.isOrphaned) {
|
|
353
412
|
return (
|
|
354
413
|
<box flexDirection="column">
|
|
@@ -466,6 +525,18 @@ function pluginDetail(
|
|
|
466
525
|
</text>
|
|
467
526
|
</box>
|
|
468
527
|
) : null}
|
|
528
|
+
{/*
|
|
529
|
+
* A lasting state, not a message: derived from the current config each
|
|
530
|
+
* load, so it is here for as long as the variables are unset — however
|
|
531
|
+
* the install question was answered — and gone once they are set.
|
|
532
|
+
*/}
|
|
533
|
+
{item.mcpSettingsNeeded ? (
|
|
534
|
+
<box>
|
|
535
|
+
<text fg={theme.colors.warning}>
|
|
536
|
+
⚙ {mcpSettingsNeededText(item.mcpSettingsNeeded)}
|
|
537
|
+
</text>
|
|
538
|
+
</box>
|
|
539
|
+
) : null}
|
|
469
540
|
{/*
|
|
470
541
|
* THE panel where the mechanism is spelled out in full. The row above
|
|
471
542
|
* says only "BROKEN — press a to fix", on purpose; a reader who wants to
|
|
@@ -12,27 +12,43 @@
|
|
|
12
12
|
* - an orphan's text depended on the cursor — the selected row said
|
|
13
13
|
* "moved → X", the unselected one always said "deprecated".
|
|
14
14
|
*
|
|
15
|
+
* A row shows the current version and the state, nothing about history. It
|
|
16
|
+
* used to badge what changed since magus last looked ("v1.8.0 → v1.7.4
|
|
17
|
+
* updated"), from a snapshot of versions seen, and rendered a downgrade as
|
|
18
|
+
* "updated" beside an offer to update again.
|
|
19
|
+
*
|
|
15
20
|
* Pure and `.ts`, so a test can assert on exactly what the row prints.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
19
23
|
import type { PluginPluginItem } from "../adapters/pluginsAdapter.js";
|
|
20
24
|
import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Sentinel recorded by Claude Code for a plugin that is installed but whose
|
|
28
|
+
* version is unknown — every plugin in the official Anthropic marketplace uses
|
|
29
|
+
* it. It is not a real version and must never be rendered as one.
|
|
30
|
+
*/
|
|
31
|
+
export const UNKNOWN_VERSION = "0.0.0";
|
|
32
|
+
|
|
33
|
+
/** True when a version string carries information worth printing. */
|
|
34
|
+
export function isKnownVersion(v: string | undefined): v is string {
|
|
35
|
+
return !!v && v !== UNKNOWN_VERSION;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export interface PluginRowSegment {
|
|
23
39
|
text: string;
|
|
24
40
|
tone: StatusTone | "text";
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface PluginRowText {
|
|
28
|
-
/** After the name: the version,
|
|
44
|
+
/** After the name: the version, then the status suffix. */
|
|
29
45
|
segments: PluginRowSegment[];
|
|
30
46
|
/** The segments joined — exactly what the row renders after the name. */
|
|
31
47
|
text: string;
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
export function pluginRowText(item: PluginPluginItem): PluginRowText {
|
|
35
|
-
const {
|
|
51
|
+
const { status } = item;
|
|
36
52
|
const segments: PluginRowSegment[] = [];
|
|
37
53
|
|
|
38
54
|
// A version is shown only when it carries information. "0.0.0" means
|
|
@@ -43,32 +59,16 @@ export function pluginRowText(item: PluginPluginItem): PluginRowText {
|
|
|
43
59
|
// `plugin.installedVersion` printed a version where the classifier had none —
|
|
44
60
|
// "v0.8.1 files missing", with 0.8.1 read off a scope not installed here.
|
|
45
61
|
const installed = status?.installed ?? undefined;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// "What just changed" and "what state it is in now" are independent facts
|
|
49
|
-
// and both can be true at once: an external update can move a plugin
|
|
50
|
-
// 2.9.0 → 3.0.1 while the marketplace already offers 3.3.1. So the badge
|
|
51
|
-
// replaces only the version, and the status suffix is always appended.
|
|
52
|
-
if (plugin.recentlyUpdatedFrom && shown) {
|
|
53
|
-
// Show what actually happened, not just where it landed — the update may
|
|
54
|
-
// have come from Claude Code or a direct CLI call without telling us.
|
|
55
|
-
segments.push({
|
|
56
|
-
text: ` v${plugin.recentlyUpdatedFrom} → v${shown} updated`,
|
|
57
|
-
tone: "success",
|
|
58
|
-
});
|
|
59
|
-
} else if (plugin.recentlyInstalled) {
|
|
60
|
-
// Newly seen. A plugin can be genuinely installed with no version we can
|
|
61
|
-
// name, so the version is optional here.
|
|
62
|
-
segments.push({
|
|
63
|
-
text: shown ? ` v${shown} new` : " new",
|
|
64
|
-
tone: "success",
|
|
65
|
-
});
|
|
66
|
-
} else if (shown) {
|
|
67
|
-
segments.push({ text: ` v${shown}`, tone: "muted" });
|
|
62
|
+
if (isKnownVersion(installed)) {
|
|
63
|
+
segments.push({ text: ` v${installed}`, tone: "muted" });
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
const suffix = pluginStatusText(status);
|
|
71
67
|
if (suffix.suffix) segments.push({ text: suffix.suffix, tone: suffix.tone });
|
|
72
68
|
|
|
69
|
+
// Its MCP server lacks variables. The same ⚙ the MCP screen puts on a
|
|
70
|
+
// server that needs configuring; the detail panel says how many, and the key.
|
|
71
|
+
if (item.mcpSettingsNeeded) segments.push({ text: " ⚙", tone: "warning" });
|
|
72
|
+
|
|
73
73
|
return { segments, text: segments.map((s) => s.text).join("") };
|
|
74
74
|
}
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* which is what makes the two front ends say the same thing.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
type PluginUpdateItem,
|
|
16
|
+
isKeptDeprecated,
|
|
17
|
+
} from "../../services/update-plan.js";
|
|
15
18
|
|
|
16
19
|
export type StatusTone = "muted" | "warning" | "danger" | "success" | "info";
|
|
17
20
|
|
|
@@ -27,6 +30,16 @@ export function pluginStatusText(
|
|
|
27
30
|
if (!status) return { suffix: "", tone: "muted" };
|
|
28
31
|
const withNote = status.note ? ` (${status.note})` : "";
|
|
29
32
|
|
|
33
|
+
// No longer published here, and kept on purpose — it moved, or a plugin
|
|
34
|
+
// that stays still requires it. A fact, not "cannot tell": muted, with the
|
|
35
|
+
// note ("moved to Magus Marketing"), never the amber "?".
|
|
36
|
+
if (isKeptDeprecated(status)) {
|
|
37
|
+
return {
|
|
38
|
+
suffix: ` ${status.note ?? "no longer published"}`,
|
|
39
|
+
tone: "muted",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
switch (status.action) {
|
|
31
44
|
case "unknown":
|
|
32
45
|
// Amber, not red: "cannot tell" is not a failure.
|
|
@@ -55,6 +68,10 @@ export function pluginStatusText(
|
|
|
55
68
|
// whose session runs a version nobody installed is broken, and there is
|
|
56
69
|
// now a key that fixes it.
|
|
57
70
|
return { suffix: " BROKEN — press a to fix", tone: "danger" };
|
|
71
|
+
case "remove":
|
|
72
|
+
// No longer published. Red, like BROKEN: Claude Code refuses to load it,
|
|
73
|
+
// and `a` removes it. Which marketplace dropped it is the detail panel's.
|
|
74
|
+
return { suffix: " deprecated — press a to remove", tone: "danger" };
|
|
58
75
|
case "install":
|
|
59
76
|
// `absent` only ever comes from a profile pin (see `pinForRow`), so it
|
|
60
77
|
// may say so. An undeclared row whose files are missing must not.
|