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
|
@@ -11,13 +11,9 @@ import {
|
|
|
11
11
|
repairPlugin as cliRepairPlugin,
|
|
12
12
|
uninstallPlugin as cliUninstallPlugin,
|
|
13
13
|
} from "../../services/claude-cli.js";
|
|
14
|
-
import {
|
|
15
|
-
SettingsParseError,
|
|
16
|
-
getMcpEnvVars,
|
|
17
|
-
readSettings,
|
|
18
|
-
setMcpEnvVar,
|
|
19
|
-
} from "../../services/claude-settings.js";
|
|
14
|
+
import { readSettings, setMcpEnvVar } from "../../services/claude-settings.js";
|
|
20
15
|
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
16
|
+
import { missingPluginEnvVars } from "../../services/plugin-env-needs.js";
|
|
21
17
|
import {
|
|
22
18
|
type MarketplaceFetchFailure,
|
|
23
19
|
type PluginInfo,
|
|
@@ -33,16 +29,9 @@ import {
|
|
|
33
29
|
scopeFilesMissing,
|
|
34
30
|
versionInScope,
|
|
35
31
|
} from "../../services/plugin-manager.js";
|
|
36
|
-
import {
|
|
37
|
-
getPluginEnvRequirements,
|
|
38
|
-
getPluginSourcePath,
|
|
39
|
-
} from "../../services/plugin-mcp-config.js";
|
|
40
|
-
import {
|
|
41
|
-
checkMissingDeps,
|
|
42
|
-
getPluginSetupFromSource,
|
|
43
|
-
installPluginDeps,
|
|
44
|
-
} from "../../services/plugin-setup.js";
|
|
32
|
+
import { resolvePluginBinRequirements } from "../../services/plugin-requires.js";
|
|
45
33
|
import { saveProfile } from "../../services/profiles.js";
|
|
34
|
+
import { requirersHere } from "../../services/resolver.js";
|
|
46
35
|
import {
|
|
47
36
|
fetchPluginContents,
|
|
48
37
|
fetchPluginSkillDetail,
|
|
@@ -61,46 +50,55 @@ import {
|
|
|
61
50
|
// screen shows or acts on — row text, detail panel, header counts, U / a, the
|
|
62
51
|
// scope picker — reads the same map it builds.
|
|
63
52
|
import {
|
|
53
|
+
type PluginUpdateAction,
|
|
64
54
|
type PluginUpdateItem,
|
|
65
55
|
classifyPluginRows,
|
|
66
56
|
countPluginStates,
|
|
57
|
+
isKeptDeprecated,
|
|
67
58
|
scopeIsBehind,
|
|
68
59
|
} from "../../services/update-plan.js";
|
|
69
|
-
import {
|
|
70
|
-
diffVersions,
|
|
71
|
-
loadSeenVersions,
|
|
72
|
-
saveSeenVersions,
|
|
73
|
-
} from "../../services/version-snapshot.js";
|
|
74
60
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
75
61
|
import { createRequestGate } from "../../utils/request-gate.js";
|
|
76
62
|
import { buildCatalogNoticeText } from "../adapters/catalogNotice.js";
|
|
63
|
+
import { installBinsInTui } from "../adapters/depsAdapter.js";
|
|
64
|
+
import { counted, failureReport } from "../adapters/failureReport.js";
|
|
77
65
|
import {
|
|
66
|
+
type FailureVerb,
|
|
78
67
|
type PluginBrowserItem,
|
|
79
68
|
type PluginComponentItem,
|
|
80
69
|
type PluginPluginItem,
|
|
81
70
|
buildPluginBrowserItems,
|
|
82
|
-
|
|
71
|
+
exceptionReason,
|
|
72
|
+
failureReasons,
|
|
73
|
+
failureVerb,
|
|
83
74
|
pluginContentsRepo,
|
|
75
|
+
pluginFailureReport,
|
|
84
76
|
} from "../adapters/pluginsAdapter.js";
|
|
77
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
85
78
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
86
79
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
87
80
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
88
81
|
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
89
|
-
import { type BindingId, moveDirection
|
|
82
|
+
import { type BindingId, moveDirection } from "../keymap.js";
|
|
90
83
|
import {
|
|
91
84
|
renderPluginDetail,
|
|
92
85
|
renderPluginRow,
|
|
93
86
|
} from "../renderers/pluginRenderers.js";
|
|
94
|
-
import {
|
|
95
|
-
useApp,
|
|
96
|
-
useModal,
|
|
97
|
-
useNotify,
|
|
98
|
-
useProgress,
|
|
99
|
-
} from "../state/AppContext.js";
|
|
87
|
+
import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
100
88
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
101
|
-
import { asyncValue } from "../state/types.js";
|
|
89
|
+
import { type FailureReport, asyncValue } from "../state/types.js";
|
|
102
90
|
import { theme } from "../theme.js";
|
|
103
91
|
|
|
92
|
+
/** What a scope key did not do, for the failure popup's headline. */
|
|
93
|
+
const SCOPE_ACTION_VERB: Record<
|
|
94
|
+
"install" | "update" | "uninstall",
|
|
95
|
+
FailureVerb
|
|
96
|
+
> = {
|
|
97
|
+
install: "installed",
|
|
98
|
+
update: "updated",
|
|
99
|
+
uninstall: "removed",
|
|
100
|
+
};
|
|
101
|
+
|
|
104
102
|
/**
|
|
105
103
|
* The latest Plugins load wins.
|
|
106
104
|
*
|
|
@@ -126,7 +124,6 @@ export function PluginsScreen() {
|
|
|
126
124
|
const { plugins: pluginsState } = state;
|
|
127
125
|
const modal = useModal();
|
|
128
126
|
const progress = useProgress();
|
|
129
|
-
const notify = useNotify();
|
|
130
127
|
const dimensions = useDimensions();
|
|
131
128
|
|
|
132
129
|
const isSearchActive =
|
|
@@ -156,10 +153,26 @@ export function PluginsScreen() {
|
|
|
156
153
|
const [pins, setPins] = useState<
|
|
157
154
|
Readonly<Record<string, string>> | null | undefined
|
|
158
155
|
>(undefined);
|
|
156
|
+
/**
|
|
157
|
+
* The profile those pins came from — the one a deprecated plugin is dropped
|
|
158
|
+
* from when `a` or `d` removes it. Same three states as `pins`; undefined
|
|
159
|
+
* lets the removal choose the active profile itself.
|
|
160
|
+
*/
|
|
161
|
+
const [activeProfileId, setActiveProfileId] = useState<
|
|
162
|
+
string | null | undefined
|
|
163
|
+
>(undefined);
|
|
164
|
+
/**
|
|
165
|
+
* For each plugin another one needs — the profile's or an installed one's —
|
|
166
|
+
* every plugin that needs it: the same map `magus update` plans with
|
|
167
|
+
* (`requirersHere`), so a deprecated dependency reads the same on both
|
|
168
|
+
* surfaces and is never removed from under a plugin that stays.
|
|
169
|
+
*/
|
|
170
|
+
const [requiredBy, setRequiredBy] = useState<
|
|
171
|
+
Readonly<Record<string, readonly string[]>>
|
|
172
|
+
>({});
|
|
159
173
|
|
|
160
174
|
// Fetch data (always fetches all scopes). Only the most recently started
|
|
161
|
-
// load may touch the screen
|
|
162
|
-
// load would rewind.
|
|
175
|
+
// load may touch the screen.
|
|
163
176
|
const fetchData = useCallback(async () => {
|
|
164
177
|
const token = pluginsLoadGate.begin();
|
|
165
178
|
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
@@ -179,7 +192,17 @@ export function PluginsScreen() {
|
|
|
179
192
|
}),
|
|
180
193
|
]);
|
|
181
194
|
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
195
|
+
// The same edges `magus update` plans with: the profile's and every
|
|
196
|
+
// installed plugin's, so a plugin one of them needs is never removed.
|
|
197
|
+
const required = await requirersHere(
|
|
198
|
+
active?.closure.dependencies?.requiredBy,
|
|
199
|
+
pluginData.filter(isInstalledAnywhere).map((p) => p.id),
|
|
200
|
+
state.projectPath || process.cwd(),
|
|
201
|
+
);
|
|
202
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
182
203
|
setPins(active?.closure.plugins ?? null);
|
|
204
|
+
setActiveProfileId(active?.profileId ?? null);
|
|
205
|
+
setRequiredBy(required);
|
|
183
206
|
|
|
184
207
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
185
208
|
// after the fetch, while the record still describes this attempt.
|
|
@@ -197,32 +220,6 @@ export function PluginsScreen() {
|
|
|
197
220
|
),
|
|
198
221
|
);
|
|
199
222
|
|
|
200
|
-
// Surface version changes made outside magus (Claude Code's own
|
|
201
|
-
// plugin update, a manual CLI call). Diffing against
|
|
202
|
-
// what we last rendered catches all of them; logging only what
|
|
203
|
-
// magus did would miss every one.
|
|
204
|
-
const current: Record<string, string> = {};
|
|
205
|
-
for (const p of pluginData) {
|
|
206
|
-
if (p.installedVersion) current[p.id] = p.installedVersion;
|
|
207
|
-
}
|
|
208
|
-
const projectPath = state.projectPath || process.cwd();
|
|
209
|
-
const previous = await loadSeenVersions(projectPath);
|
|
210
|
-
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
211
|
-
for (const change of diffVersions(previous, current)) {
|
|
212
|
-
const target = pluginData.find((p) => p.id === change.pluginId);
|
|
213
|
-
if (!target) continue;
|
|
214
|
-
if (change.kind === "updated") {
|
|
215
|
-
target.recentlyUpdatedFrom = change.from;
|
|
216
|
-
} else {
|
|
217
|
-
target.recentlyInstalled = true;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// Await the write. Fire-and-forget raced the next fetchData() — a
|
|
221
|
-
// refresh or a post-action refetch could load the baseline before the
|
|
222
|
-
// previous save landed and re-report the same change.
|
|
223
|
-
await saveSeenVersions(projectPath, current);
|
|
224
|
-
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
225
|
-
|
|
226
223
|
dispatch({
|
|
227
224
|
type: "PLUGINS_DATA_SUCCESS",
|
|
228
225
|
marketplaces: allMarketplaces,
|
|
@@ -296,10 +293,53 @@ export function PluginsScreen() {
|
|
|
296
293
|
*/
|
|
297
294
|
const statuses = useMemo(
|
|
298
295
|
(): Map<string, PluginUpdateItem> =>
|
|
299
|
-
shownPlugins
|
|
300
|
-
|
|
296
|
+
shownPlugins
|
|
297
|
+
? classifyPluginRows(shownPlugins, pins ?? {}, requiredBy)
|
|
298
|
+
: new Map(),
|
|
299
|
+
[shownPlugins, pins, requiredBy],
|
|
301
300
|
);
|
|
302
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Installed plugins whose MCP server lacks variables, and how many — read
|
|
304
|
+
* from the current config on every load and after every attempt to set them
|
|
305
|
+
* (`envCheck`), so declining the question at install leaves a lasting state
|
|
306
|
+
* on that plugin, and setting them clears it. Nothing records the decline.
|
|
307
|
+
*/
|
|
308
|
+
const [mcpSettingsNeeded, setMcpSettingsNeeded] = useState<
|
|
309
|
+
ReadonlyMap<string, number>
|
|
310
|
+
>(new Map());
|
|
311
|
+
const [envCheck, setEnvCheck] = useState(0);
|
|
312
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: envCheck is the re-read trigger after the variables were asked for; the body never reads it
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!shownPlugins) return;
|
|
315
|
+
let cancelled = false;
|
|
316
|
+
const installed = shownPlugins.filter(
|
|
317
|
+
(p) => !p.isOrphaned && isInstalledAnywhere(p),
|
|
318
|
+
);
|
|
319
|
+
void Promise.all(
|
|
320
|
+
installed.map(async (p) => {
|
|
321
|
+
try {
|
|
322
|
+
const missing = await missingPluginEnvVars(
|
|
323
|
+
p.marketplace,
|
|
324
|
+
p.name,
|
|
325
|
+
state.projectPath,
|
|
326
|
+
);
|
|
327
|
+
return [p.id, missing.length] as const;
|
|
328
|
+
} catch {
|
|
329
|
+
// Unreadable variables are reported where they are saved; a row
|
|
330
|
+
// that cannot tell claims nothing.
|
|
331
|
+
return [p.id, 0] as const;
|
|
332
|
+
}
|
|
333
|
+
}),
|
|
334
|
+
).then((counts) => {
|
|
335
|
+
if (cancelled) return;
|
|
336
|
+
setMcpSettingsNeeded(new Map(counts.filter(([, n]) => n > 0)));
|
|
337
|
+
});
|
|
338
|
+
return () => {
|
|
339
|
+
cancelled = true;
|
|
340
|
+
};
|
|
341
|
+
}, [shownPlugins, state.projectPath, envCheck]);
|
|
342
|
+
|
|
303
343
|
// Build list items (categories + plugins)
|
|
304
344
|
const allItems = useMemo((): PluginBrowserItem[] => {
|
|
305
345
|
if (!shownMarketplaces || !shownPlugins) return [];
|
|
@@ -310,6 +350,7 @@ export function PluginsScreen() {
|
|
|
310
350
|
expandedPlugins: pluginsState.expandedPlugins,
|
|
311
351
|
pluginContents: pluginsState.pluginContents,
|
|
312
352
|
statuses,
|
|
353
|
+
mcpSettingsNeeded,
|
|
313
354
|
});
|
|
314
355
|
}, [
|
|
315
356
|
shownMarketplaces,
|
|
@@ -318,6 +359,7 @@ export function PluginsScreen() {
|
|
|
318
359
|
pluginsState.expandedPlugins,
|
|
319
360
|
pluginsState.pluginContents,
|
|
320
361
|
statuses,
|
|
362
|
+
mcpSettingsNeeded,
|
|
321
363
|
]);
|
|
322
364
|
|
|
323
365
|
// Filter items by search query
|
|
@@ -523,6 +565,14 @@ export function PluginsScreen() {
|
|
|
523
565
|
remove: () => {
|
|
524
566
|
void handleRemoveDeprecated();
|
|
525
567
|
},
|
|
568
|
+
// The action behind the detail panel's "needs N settings — press e".
|
|
569
|
+
"set-env": () => {
|
|
570
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
571
|
+
if (item?.kind !== "plugin" || !item.mcpSettingsNeeded) return;
|
|
572
|
+
void collectPluginEnvVars(item.plugin.name, item.plugin.marketplace, {
|
|
573
|
+
ask: false,
|
|
574
|
+
});
|
|
575
|
+
},
|
|
526
576
|
"add-marketplace": () => {
|
|
527
577
|
void handleShowAddMarketplaceInstructions();
|
|
528
578
|
},
|
|
@@ -550,12 +600,91 @@ export function PluginsScreen() {
|
|
|
550
600
|
|
|
551
601
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
552
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Run the ONE apply engine over `items`, reload, and — only if something
|
|
605
|
+
* failed, or ran and changed nothing — say why in ONE popup, each plugin
|
|
606
|
+
* named with its reason in the same words the CLI prints. Shared by U, a
|
|
607
|
+
* and d, so the three say the same thing about the same outcome. A success
|
|
608
|
+
* says nothing: the reloaded rows show it. An item that ran and did not move
|
|
609
|
+
* is "not updated" — the fact the CLI lists as "ran but did not change" —
|
|
610
|
+
* because a row still offering the same update, with nothing said, reads as
|
|
611
|
+
* a key that did nothing. A throw from the engine itself — not from one
|
|
612
|
+
* item, which the engine catches and reports — names every item with its
|
|
613
|
+
* message.
|
|
614
|
+
*/
|
|
615
|
+
const applyAndReportFailures = async (
|
|
616
|
+
items: readonly PluginUpdateItem[],
|
|
617
|
+
report: (capture: ApplyReporter) => ApplyReporter = (capture) => capture,
|
|
618
|
+
): Promise<void> => {
|
|
619
|
+
const reasons = failureReasons();
|
|
620
|
+
const actions = new Map(items.map((i) => [i.pluginId, i.action]));
|
|
621
|
+
let failure: FailureReport | null;
|
|
622
|
+
try {
|
|
623
|
+
const result = await applyPlugins(
|
|
624
|
+
[...items],
|
|
625
|
+
state.projectPath || process.cwd(),
|
|
626
|
+
REAL_PLUGIN_DEPS,
|
|
627
|
+
report(reasons.reporter),
|
|
628
|
+
// The profile these rows were judged against — the active one — is
|
|
629
|
+
// the one a removed plugin leaves.
|
|
630
|
+
{ profile: activeProfileId },
|
|
631
|
+
);
|
|
632
|
+
failure = reasons.report(result.failed, actions, result.unchanged);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
635
|
+
failure = pluginFailureReport(
|
|
636
|
+
items.map((i) => ({
|
|
637
|
+
pluginId: i.pluginId,
|
|
638
|
+
verb: failureVerb(i.action),
|
|
639
|
+
detail: message,
|
|
640
|
+
exception: true,
|
|
641
|
+
})),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
modal.hideModal();
|
|
645
|
+
fetchData();
|
|
646
|
+
if (failure) await modal.failures(failure);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The same popup for a single plugin action outside the apply engine — a
|
|
651
|
+
* scope key, a plain uninstall — so it reads like every other failure here.
|
|
652
|
+
*/
|
|
653
|
+
const reportPluginFailure = async (
|
|
654
|
+
pluginId: string,
|
|
655
|
+
verb: FailureVerb,
|
|
656
|
+
error: unknown,
|
|
657
|
+
): Promise<void> => {
|
|
658
|
+
const report = pluginFailureReport([
|
|
659
|
+
{
|
|
660
|
+
pluginId,
|
|
661
|
+
verb,
|
|
662
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
663
|
+
exception: true,
|
|
664
|
+
},
|
|
665
|
+
]);
|
|
666
|
+
if (report) await modal.failures(report);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* A marketplace that was not added. Magus runs the same command the user
|
|
671
|
+
* would, so running it by hand is the one real fix when magus cannot say
|
|
672
|
+
* why it failed.
|
|
673
|
+
*/
|
|
674
|
+
const addMarketplaceFailure = (repo: string, error: unknown) =>
|
|
675
|
+
stepFailureReport(
|
|
676
|
+
`Marketplace ${repo} was not added`,
|
|
677
|
+
repo,
|
|
678
|
+
error,
|
|
679
|
+
`Run it yourself: claude plugin marketplace add ${repo}`,
|
|
680
|
+
);
|
|
681
|
+
|
|
553
682
|
// ── Action handlers ────────────────────────────────────────────────────────
|
|
554
683
|
|
|
555
684
|
const handleRefresh = async () => {
|
|
556
685
|
progress.show("Refreshing cache...");
|
|
557
686
|
try {
|
|
558
|
-
|
|
687
|
+
await refreshAllMarketplaces((p) => {
|
|
559
688
|
progress.show(`${p.name}`, p.current, p.total);
|
|
560
689
|
});
|
|
561
690
|
clearMarketplaceCache();
|
|
@@ -563,20 +692,16 @@ export function PluginsScreen() {
|
|
|
563
692
|
// that moves HEAD invalidates every cached answer.
|
|
564
693
|
clearContentDriftCache();
|
|
565
694
|
progress.hide();
|
|
566
|
-
|
|
567
|
-
const totalRepaired = result.repair.reduce(
|
|
568
|
-
(sum, r) => sum + r.repaired.length,
|
|
569
|
-
0,
|
|
570
|
-
);
|
|
571
|
-
notify(
|
|
572
|
-
totalRepaired > 0
|
|
573
|
-
? `Refreshed — repaired ${totalRepaired} plugin(s) with missing files`
|
|
574
|
-
: "Refreshed",
|
|
575
|
-
);
|
|
576
695
|
fetchData();
|
|
577
696
|
} catch (error) {
|
|
578
697
|
progress.hide();
|
|
579
|
-
await modal.
|
|
698
|
+
await modal.failures(
|
|
699
|
+
stepFailureReport(
|
|
700
|
+
"The marketplaces were not refreshed",
|
|
701
|
+
"marketplaces",
|
|
702
|
+
error,
|
|
703
|
+
),
|
|
704
|
+
);
|
|
580
705
|
}
|
|
581
706
|
};
|
|
582
707
|
|
|
@@ -617,12 +742,7 @@ export function PluginsScreen() {
|
|
|
617
742
|
await cliAddMarketplace(choice);
|
|
618
743
|
await handleRefresh();
|
|
619
744
|
} catch (e) {
|
|
620
|
-
await modal.
|
|
621
|
-
"Could not add marketplace",
|
|
622
|
-
`${(e as Error).message}\n\nRun it yourself if this persists:\n\n` +
|
|
623
|
-
` claude plugin marketplace add ${choice}`,
|
|
624
|
-
"error",
|
|
625
|
-
);
|
|
745
|
+
await modal.failures(addMarketplaceFailure(choice, e));
|
|
626
746
|
}
|
|
627
747
|
return;
|
|
628
748
|
}
|
|
@@ -634,7 +754,6 @@ export function PluginsScreen() {
|
|
|
634
754
|
" claude plugin marketplace add owner/repo\n\n" +
|
|
635
755
|
"Auto-update is enabled by default for new marketplaces.\n\n" +
|
|
636
756
|
"After adding, refresh magus with 'r' to see it.",
|
|
637
|
-
"info",
|
|
638
757
|
);
|
|
639
758
|
};
|
|
640
759
|
|
|
@@ -663,45 +782,37 @@ export function PluginsScreen() {
|
|
|
663
782
|
"NOTE: Individual plugin enablement is still project-specific.\n" +
|
|
664
783
|
'Use "Project" scope in magus to enable for the team.';
|
|
665
784
|
|
|
666
|
-
await modal.message("Team Configuration", helpText
|
|
785
|
+
await modal.message("Team Configuration", helpText);
|
|
667
786
|
};
|
|
668
787
|
|
|
669
788
|
/**
|
|
670
789
|
* Collect environment variables required by a plugin's MCP servers.
|
|
790
|
+
*
|
|
791
|
+
* After an install it first asks whether to configure them now; from the
|
|
792
|
+
* `e` key (`ask: false`) the user has already said yes. Declining, or
|
|
793
|
+
* leaving a value empty, is not remembered: the plugin's detail panel keeps
|
|
794
|
+
* saying what is missing, read from the config, until it is set.
|
|
671
795
|
*/
|
|
672
796
|
const collectPluginEnvVars = async (
|
|
673
797
|
pluginName: string,
|
|
674
798
|
marketplace: string,
|
|
799
|
+
{ ask = true }: { ask?: boolean } = {},
|
|
675
800
|
): Promise<boolean> => {
|
|
676
801
|
try {
|
|
677
|
-
const
|
|
678
|
-
if (!pluginSource) return true;
|
|
679
|
-
|
|
680
|
-
const requirements = await getPluginEnvRequirements(
|
|
802
|
+
const missingVars = await missingPluginEnvVars(
|
|
681
803
|
marketplace,
|
|
682
|
-
|
|
804
|
+
pluginName,
|
|
805
|
+
state.projectPath,
|
|
683
806
|
);
|
|
684
|
-
if (requirements.length === 0) return true;
|
|
685
|
-
|
|
686
|
-
const existingEnvVars = await getMcpEnvVars(state.projectPath);
|
|
687
|
-
const missingVars = requirements.filter(
|
|
688
|
-
(req) => !existingEnvVars[req.name] && !process.env[req.name],
|
|
689
|
-
);
|
|
690
|
-
|
|
691
807
|
if (missingVars.length === 0) return true;
|
|
692
808
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (!wantToConfigure) {
|
|
700
|
-
notify(
|
|
701
|
-
`${serverNames.join(", ")} not configured — set the variables in the Settings tab (press ${tabKey("settings")})`,
|
|
702
|
-
"warning",
|
|
809
|
+
if (ask) {
|
|
810
|
+
const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
|
|
811
|
+
const wantToConfigure = await modal.confirm(
|
|
812
|
+
"Configure MCP Servers?",
|
|
813
|
+
`This plugin includes MCP servers (${serverNames.join(", ")}) that need ${missingVars.length} environment variable(s).\n\nConfigure now?`,
|
|
703
814
|
);
|
|
704
|
-
return true;
|
|
815
|
+
if (!wantToConfigure) return true;
|
|
705
816
|
}
|
|
706
817
|
|
|
707
818
|
for (const req of missingVars) {
|
|
@@ -723,13 +834,7 @@ export function PluginsScreen() {
|
|
|
723
834
|
"",
|
|
724
835
|
);
|
|
725
836
|
|
|
726
|
-
if (value === null)
|
|
727
|
-
notify(
|
|
728
|
-
`${serverNames.join(", ")} partly configured — set the rest in the Settings tab (press ${tabKey("settings")})`,
|
|
729
|
-
"warning",
|
|
730
|
-
);
|
|
731
|
-
return true;
|
|
732
|
-
}
|
|
837
|
+
if (value === null) return true;
|
|
733
838
|
|
|
734
839
|
if (value) {
|
|
735
840
|
await setMcpEnvVar(req.name, value, state.projectPath);
|
|
@@ -738,77 +843,54 @@ export function PluginsScreen() {
|
|
|
738
843
|
|
|
739
844
|
return true;
|
|
740
845
|
} catch (error) {
|
|
741
|
-
// The plugin itself is installed either way
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
846
|
+
// The plugin itself is installed either way, but its MCP server will
|
|
847
|
+
// not start without these — so it is said in the popup, never logged
|
|
848
|
+
// out of sight. A file that does not parse names itself and the fix.
|
|
849
|
+
modal.hideModal();
|
|
850
|
+
await modal.failures(
|
|
851
|
+
stepFailureReport(
|
|
852
|
+
`${pluginName} is installed, but its MCP server variables were not saved`,
|
|
853
|
+
pluginName,
|
|
854
|
+
error,
|
|
855
|
+
),
|
|
856
|
+
);
|
|
753
857
|
return true;
|
|
858
|
+
} finally {
|
|
859
|
+
// Re-read, so the panel's "needs N settings" matches what was saved.
|
|
860
|
+
setEnvCheck((n) => n + 1);
|
|
754
861
|
}
|
|
755
862
|
};
|
|
756
863
|
|
|
757
864
|
/**
|
|
758
|
-
* Install
|
|
865
|
+
* Install what a just-installed plugin declares in `requires` and this
|
|
866
|
+
* machine lacks — the same plan and executor as `magus doctor --fix`.
|
|
759
867
|
*/
|
|
760
868
|
const installPluginSystemDeps = async (
|
|
761
869
|
pluginName: string,
|
|
762
870
|
marketplace: string,
|
|
763
871
|
): Promise<void> => {
|
|
872
|
+
const pluginId = `${pluginName}@${marketplace}`;
|
|
764
873
|
try {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
(missing.cargo?.length || 0) +
|
|
774
|
-
(missing.go?.length || 0) >
|
|
775
|
-
0;
|
|
776
|
-
|
|
777
|
-
if (!hasMissing) return;
|
|
778
|
-
|
|
779
|
-
const parts: string[] = [];
|
|
780
|
-
if (missing.pip?.length) parts.push(`pip: ${missing.pip.join(", ")}`);
|
|
781
|
-
if (missing.brew?.length) parts.push(`brew: ${missing.brew.join(", ")}`);
|
|
782
|
-
if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
|
|
783
|
-
if (missing.cargo?.length)
|
|
784
|
-
parts.push(`cargo: ${missing.cargo.join(", ")}`);
|
|
785
|
-
if (missing.go?.length) parts.push(`go: ${missing.go.join(", ")}`);
|
|
786
|
-
|
|
787
|
-
const wantInstall = await modal.confirm(
|
|
788
|
-
"Install Dependencies?",
|
|
789
|
-
`This plugin needs system dependencies:\n\n${parts.join("\n")}\n\nInstall now?`,
|
|
874
|
+
const bins = await resolvePluginBinRequirements(
|
|
875
|
+
pluginId,
|
|
876
|
+
state.projectPath || process.cwd(),
|
|
877
|
+
);
|
|
878
|
+
await installBinsInTui(
|
|
879
|
+
pluginId,
|
|
880
|
+
bins.map((bin) => ({ ...bin, sources: [`plugin:${pluginId}`] })),
|
|
881
|
+
modal,
|
|
790
882
|
);
|
|
791
|
-
|
|
792
|
-
if (!wantInstall) return;
|
|
793
|
-
|
|
794
|
-
modal.loading("Installing dependencies...");
|
|
795
|
-
const result = await installPluginDeps(missing);
|
|
796
|
-
modal.hideModal();
|
|
797
|
-
|
|
798
|
-
if (result.failed.length > 0) {
|
|
799
|
-
const failMsg = result.failed
|
|
800
|
-
.map((f) => `${f.pkg}: ${f.error}`)
|
|
801
|
-
.join("\n");
|
|
802
|
-
await modal.message(
|
|
803
|
-
"Partial Install",
|
|
804
|
-
`Installed: ${result.installed.length}\nFailed:\n${failMsg}`,
|
|
805
|
-
"error",
|
|
806
|
-
);
|
|
807
|
-
} else if (result.installed.length > 0) {
|
|
808
|
-
notify(`Installed ${result.installed.join(", ")}`);
|
|
809
|
-
}
|
|
810
883
|
} catch (error) {
|
|
811
|
-
|
|
884
|
+
// Hide the loading modal first: it would otherwise stay up forever,
|
|
885
|
+
// and the failure behind it would never be read.
|
|
886
|
+
modal.hideModal();
|
|
887
|
+
await modal.failures(
|
|
888
|
+
stepFailureReport(
|
|
889
|
+
`${pluginName} is installed, but its dependencies were not installed`,
|
|
890
|
+
pluginName,
|
|
891
|
+
error,
|
|
892
|
+
),
|
|
893
|
+
);
|
|
812
894
|
}
|
|
813
895
|
};
|
|
814
896
|
|
|
@@ -829,7 +911,6 @@ export function PluginsScreen() {
|
|
|
829
911
|
await modal.message(
|
|
830
912
|
`Remove ${mp.displayName}?`,
|
|
831
913
|
`To remove this marketplace, run in Claude Code:\n\n /plugin marketplace remove ${mp.name}\n\nAfter removing, refresh magus with 'r' to update the display.`,
|
|
832
|
-
"info",
|
|
833
914
|
);
|
|
834
915
|
}
|
|
835
916
|
} else {
|
|
@@ -847,11 +928,7 @@ export function PluginsScreen() {
|
|
|
847
928
|
await cliAddMarketplace(repo);
|
|
848
929
|
await handleRefresh();
|
|
849
930
|
} catch (e) {
|
|
850
|
-
await modal.
|
|
851
|
-
"Could not add marketplace",
|
|
852
|
-
`${(e as Error).message}\n\nRun it yourself if this persists:\n\n claude plugin marketplace add ${repo}`,
|
|
853
|
-
"error",
|
|
854
|
-
);
|
|
931
|
+
await modal.failures(addMarketplaceFailure(repo, e));
|
|
855
932
|
}
|
|
856
933
|
}
|
|
857
934
|
}
|
|
@@ -978,7 +1055,7 @@ export function PluginsScreen() {
|
|
|
978
1055
|
fetchData();
|
|
979
1056
|
} catch (error) {
|
|
980
1057
|
modal.hideModal();
|
|
981
|
-
await
|
|
1058
|
+
await reportPluginFailure(plugin.id, SCOPE_ACTION_VERB[action], error);
|
|
982
1059
|
}
|
|
983
1060
|
}
|
|
984
1061
|
};
|
|
@@ -1008,46 +1085,20 @@ export function PluginsScreen() {
|
|
|
1008
1085
|
|
|
1009
1086
|
modal.loading(
|
|
1010
1087
|
// A broken plugin runs NO claude command at all, so naming one here would
|
|
1011
|
-
// misdescribe the work
|
|
1088
|
+
// misdescribe the work.
|
|
1012
1089
|
status.action === "stale-load"
|
|
1013
|
-
? `Repairing ${plugin.name}…\
|
|
1090
|
+
? `Repairing ${plugin.name}…\nmaking Claude Code load v${status.target ?? "?"} here`
|
|
1014
1091
|
: repairing
|
|
1015
1092
|
? `Repairing ${plugin.name}…\nclaude plugin uninstall ${plugin.id} --scope ${scope} && claude plugin install ${plugin.id} --scope ${scope}`
|
|
1016
1093
|
: `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
1017
1094
|
);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
// so what runs is exactly what the row claimed.
|
|
1026
|
-
const result = await applyPlugins(
|
|
1027
|
-
[status],
|
|
1028
|
-
state.projectPath || process.cwd(),
|
|
1029
|
-
REAL_PLUGIN_DEPS,
|
|
1030
|
-
reasons.reporter,
|
|
1031
|
-
);
|
|
1032
|
-
modal.hideModal();
|
|
1033
|
-
if (result.failed.length > 0) {
|
|
1034
|
-
await modal.message(
|
|
1035
|
-
"Error",
|
|
1036
|
-
`Failed to update ${plugin.name}`,
|
|
1037
|
-
"error",
|
|
1038
|
-
);
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
// Ran, exited 0, moved nothing — and the row will not have moved
|
|
1042
|
-
// either. Saying nothing here is what made "press U, press U,
|
|
1043
|
-
// forever": the CLI names the cause, so the screen does too.
|
|
1044
|
-
const unchanged = reasons.notice(result.unchanged);
|
|
1045
|
-
if (unchanged) notify(unchanged, "warning");
|
|
1046
|
-
fetchData();
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
modal.hideModal();
|
|
1049
|
-
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
1050
|
-
}
|
|
1095
|
+
// One plugin, but the SAME engine — so a single update records the
|
|
1096
|
+
// version that landed and touches every scope that is behind, exactly as
|
|
1097
|
+
// `magus update` does. The plan item IS the row's status: the classifier's
|
|
1098
|
+
// answer for this plugin under its real pin (the profile's, or "latest" for
|
|
1099
|
+
// an undeclared install — see pinForRow), so what runs is exactly what the
|
|
1100
|
+
// row claimed. A failure says why in a popup.
|
|
1101
|
+
await applyAndReportFailures([status]);
|
|
1051
1102
|
};
|
|
1052
1103
|
|
|
1053
1104
|
/**
|
|
@@ -1068,57 +1119,37 @@ export function PluginsScreen() {
|
|
|
1068
1119
|
if (pluginsState.plugins.status !== "success") return;
|
|
1069
1120
|
|
|
1070
1121
|
// Every item that needs work, by the same `pluginNeedsWork` the CLI's
|
|
1071
|
-
// apply uses: updates, content repairs, broken loads, missing files,
|
|
1072
|
-
// profile plugin not installed yet
|
|
1122
|
+
// apply uses: updates, content repairs, broken loads, missing files, a
|
|
1123
|
+
// profile plugin not installed yet, and deprecated plugins to remove —
|
|
1124
|
+
// exactly what `magus update` would do.
|
|
1073
1125
|
const items = [...statuses.values()]
|
|
1074
1126
|
.filter(pluginNeedsWork)
|
|
1075
1127
|
.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
1076
1128
|
if (items.length === 0) return;
|
|
1077
1129
|
|
|
1078
|
-
const
|
|
1079
|
-
const total = items.filter(pluginNeedsWork).length;
|
|
1130
|
+
const total = items.length;
|
|
1080
1131
|
|
|
1081
1132
|
// A broken plugin is repaired in the registry, not updated, and the progress
|
|
1082
1133
|
// line is the only place the user sees which one happened to which plugin.
|
|
1083
1134
|
const actionOf = new Map(items.map((i) => [i.pluginId, i.action]));
|
|
1135
|
+
const VERB: Partial<Record<PluginUpdateAction, string>> = {
|
|
1136
|
+
"stale-load": "Repairing",
|
|
1137
|
+
remove: "Removing",
|
|
1138
|
+
};
|
|
1084
1139
|
|
|
1085
1140
|
let done = 0;
|
|
1086
|
-
|
|
1087
|
-
|
|
1141
|
+
// One popup afterwards, and only for failures: each plugin that failed,
|
|
1142
|
+
// with why. The one this replaced listed six plugin names and not one
|
|
1143
|
+
// reason. Everything else shows its new state once the list reloads.
|
|
1144
|
+
await applyAndReportFailures(items, (capture) => ({
|
|
1088
1145
|
begin: (name) => {
|
|
1089
1146
|
done++;
|
|
1090
|
-
const verb =
|
|
1091
|
-
actionOf.get(name) === "stale-load" ? "Repairing" : "Updating";
|
|
1147
|
+
const verb = VERB[actionOf.get(name) ?? "update"] ?? "Updating";
|
|
1092
1148
|
modal.loading(`${verb} ${name} (${done}/${total})…`);
|
|
1093
1149
|
},
|
|
1094
|
-
finish:
|
|
1150
|
+
finish: capture.finish,
|
|
1095
1151
|
detach: (work) => work(),
|
|
1096
|
-
};
|
|
1097
|
-
|
|
1098
|
-
try {
|
|
1099
|
-
const result = await applyPlugins(
|
|
1100
|
-
items,
|
|
1101
|
-
projectPath,
|
|
1102
|
-
REAL_PLUGIN_DEPS,
|
|
1103
|
-
reporter,
|
|
1104
|
-
);
|
|
1105
|
-
modal.hideModal();
|
|
1106
|
-
if (result.failed.length > 0) {
|
|
1107
|
-
await modal.message(
|
|
1108
|
-
"Some updates failed",
|
|
1109
|
-
result.failed.join(", "),
|
|
1110
|
-
"error",
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
// One line however many were left where they were: a line each
|
|
1114
|
-
// filled the banner and pushed out everything else it had to say.
|
|
1115
|
-
const unchanged = reasons.notice(result.unchanged);
|
|
1116
|
-
if (unchanged) notify(unchanged, "warning");
|
|
1117
|
-
fetchData();
|
|
1118
|
-
} catch (error) {
|
|
1119
|
-
modal.hideModal();
|
|
1120
|
-
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
1121
|
-
}
|
|
1152
|
+
}));
|
|
1122
1153
|
};
|
|
1123
1154
|
|
|
1124
1155
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
@@ -1181,7 +1212,7 @@ export function PluginsScreen() {
|
|
|
1181
1212
|
fetchData();
|
|
1182
1213
|
} catch (error) {
|
|
1183
1214
|
modal.hideModal();
|
|
1184
|
-
await
|
|
1215
|
+
await reportPluginFailure(plugin.id, SCOPE_ACTION_VERB[action], error);
|
|
1185
1216
|
}
|
|
1186
1217
|
};
|
|
1187
1218
|
|
|
@@ -1198,6 +1229,17 @@ export function PluginsScreen() {
|
|
|
1198
1229
|
// was already installed. An orphan is removed; where else a name happens
|
|
1199
1230
|
// to appear is not this screen's guess to make.
|
|
1200
1231
|
|
|
1232
|
+
// A DEPRECATED one goes through the engine `a` and `magus update` use, so
|
|
1233
|
+
// `d` also drops it from the profile and reads the result back — the
|
|
1234
|
+
// uninstall-only path below left the profile declaring it, and the row
|
|
1235
|
+
// came straight back as "deprecated".
|
|
1236
|
+
const status = statuses.get(plugin.id);
|
|
1237
|
+
if (status?.action === "remove") {
|
|
1238
|
+
modal.loading(`Removing ${plugin.name}…`);
|
|
1239
|
+
await applyAndReportFailures([status]);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1201
1243
|
try {
|
|
1202
1244
|
// Remove from all scopes — try all to clean up stale references
|
|
1203
1245
|
const scopes: PluginScope[] = ["user", "project", "local"];
|
|
@@ -1219,7 +1261,7 @@ export function PluginsScreen() {
|
|
|
1219
1261
|
fetchData();
|
|
1220
1262
|
} catch (error) {
|
|
1221
1263
|
modal.hideModal();
|
|
1222
|
-
await
|
|
1264
|
+
await reportPluginFailure(plugin.id, "removed", error);
|
|
1223
1265
|
}
|
|
1224
1266
|
};
|
|
1225
1267
|
|
|
@@ -1231,10 +1273,12 @@ export function PluginsScreen() {
|
|
|
1231
1273
|
enabledPlugins =
|
|
1232
1274
|
(await readSettings(state.projectPath)).enabledPlugins ?? {};
|
|
1233
1275
|
} catch (error) {
|
|
1234
|
-
await modal.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1276
|
+
await modal.failures(
|
|
1277
|
+
stepFailureReport(
|
|
1278
|
+
"No profile was saved",
|
|
1279
|
+
"this project's plugins",
|
|
1280
|
+
error,
|
|
1281
|
+
),
|
|
1238
1282
|
);
|
|
1239
1283
|
return;
|
|
1240
1284
|
}
|
|
@@ -1264,19 +1308,22 @@ export function PluginsScreen() {
|
|
|
1264
1308
|
try {
|
|
1265
1309
|
await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
|
|
1266
1310
|
modal.hideModal();
|
|
1267
|
-
notify(
|
|
1268
|
-
`Saved profile "${name.trim()}" at ${scope === "user" ? "USER" : "PROJECT"} scope`,
|
|
1269
|
-
);
|
|
1270
1311
|
} catch (error) {
|
|
1271
1312
|
modal.hideModal();
|
|
1272
|
-
await modal.
|
|
1313
|
+
await modal.failures(
|
|
1314
|
+
stepFailureReport(
|
|
1315
|
+
`Profile "${name.trim()}" was not saved`,
|
|
1316
|
+
scope.toUpperCase(),
|
|
1317
|
+
error,
|
|
1318
|
+
),
|
|
1319
|
+
);
|
|
1273
1320
|
}
|
|
1274
1321
|
};
|
|
1275
1322
|
|
|
1276
1323
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
1277
1324
|
|
|
1278
1325
|
// Only a genuine first load takes over the screen. A reload keeps the list on
|
|
1279
|
-
// screen and says so in the notice
|
|
1326
|
+
// screen and says so in the header's notice row instead.
|
|
1280
1327
|
if (!shownMarketplaces || !shownPlugins) {
|
|
1281
1328
|
if (
|
|
1282
1329
|
pluginsState.marketplaces.status === "loading" ||
|
|
@@ -1328,6 +1375,9 @@ export function PluginsScreen() {
|
|
|
1328
1375
|
...(selectedItem?.kind === "plugin" && selectedItem.plugin.isOrphaned
|
|
1329
1376
|
? []
|
|
1330
1377
|
: ["remove" as const]),
|
|
1378
|
+
...(selectedItem?.kind === "plugin" && selectedItem.mcpSettingsNeeded
|
|
1379
|
+
? []
|
|
1380
|
+
: ["set-env" as const]),
|
|
1331
1381
|
];
|
|
1332
1382
|
|
|
1333
1383
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
@@ -1342,7 +1392,10 @@ export function PluginsScreen() {
|
|
|
1342
1392
|
// Cannot tell whether it is current. Counted and shown separately: folding
|
|
1343
1393
|
// it into `updateCount` would overstate, and folding it into silence is the
|
|
1344
1394
|
// "cannot tell rendered as fine" bug.
|
|
1345
|
-
|
|
1395
|
+
// Moved or still-required plugins are `unknown` to the planner but known to
|
|
1396
|
+
// the user — kept on purpose — so they are not counted as "unverified".
|
|
1397
|
+
const unknownCount =
|
|
1398
|
+
counts.unknown - [...statuses.values()].filter(isKeptDeprecated).length;
|
|
1346
1399
|
const installCount = counts.install;
|
|
1347
1400
|
// Broken: installed at the right version, loading an older one. Its own
|
|
1348
1401
|
// number, never folded into `updateCount` or into silence — a clean header
|
|
@@ -1352,8 +1405,11 @@ export function PluginsScreen() {
|
|
|
1352
1405
|
// a reader had to already know what "loads" versus "installed" means here
|
|
1353
1406
|
// before the count told them anything. `a` clears these.
|
|
1354
1407
|
const brokenCount = counts["stale-load"];
|
|
1355
|
-
|
|
1356
|
-
|
|
1408
|
+
// No longer published, and `a` removes them. Counted where the rows can be
|
|
1409
|
+
// seen to need work; they used to sit inside "unverified".
|
|
1410
|
+
const deprecatedCount = counts.remove;
|
|
1411
|
+
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}${installCount > 0 ? ` │ ${installCount} to install` : ""}${brokenCount > 0 ? ` │ ${brokenCount} broken` : ""}${deprecatedCount > 0 ? ` │ ${deprecatedCount} deprecated` : ""}${unknownCount > 0 ? ` │ ${unknownCount} unverified` : ""}`;
|
|
1412
|
+
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""}${installCount > 0 ? ` │ ${installCount} +` : ""}${brokenCount + deprecatedCount > 0 ? ` │ ${brokenCount + deprecatedCount} ✗` : ""}${unknownCount > 0 ? ` │ ${unknownCount} ?` : ""} │ / to search`;
|
|
1357
1413
|
|
|
1358
1414
|
const catalogNotice = buildCatalogNotice({
|
|
1359
1415
|
failures: catalogFailures,
|