magus-cli 7.6.0 → 7.8.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/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +29 -15
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind-780-cli-tools.test.ts +808 -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 +1203 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +920 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +724 -0
- package/src/__tests__/format-update-outcome.test.ts +9 -34
- package/src/__tests__/gitignore-profile.test.ts +0 -4
- package/src/__tests__/models-screen-state.test.ts +18 -40
- 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__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +23 -17
- package/src/__tests__/useStaleLoadModal.test.ts +1 -42
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/doctor.ts +151 -30
- package/src/cli/install.ts +5 -6
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +27 -5
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +84 -36
- package/src/services/gitignore-service.ts +0 -19
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +31 -19
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-absorb.ts +9 -0
- package/src/services/profile-activate.ts +1 -1
- package/src/services/profile-startup.ts +30 -27
- package/src/services/profile-state.ts +81 -2
- package/src/services/resolver.ts +95 -1
- package/src/services/update-engine.ts +326 -31
- package/src/services/update-plan.ts +293 -33
- package/src/types/index.ts +7 -0
- package/src/ui/App.tsx +186 -123
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +299 -28
- package/src/ui/adapters/startupReports.ts +282 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -13
- 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 +31 -31
- package/src/ui/hooks/useProfileStartupModal.ts +127 -122
- package/src/ui/hooks/useStaleLoadModal.ts +33 -53
- package/src/ui/keymap.ts +14 -1
- 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 -44
- package/src/ui/screens/CliToolsScreen.tsx +65 -51
- package/src/ui/screens/EnvVarsScreen.tsx +21 -6
- package/src/ui/screens/GitignoreScreen.tsx +49 -75
- package/src/ui/screens/McpRegistryScreen.tsx +72 -40
- package/src/ui/screens/McpScreen.tsx +55 -24
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +316 -245
- package/src/ui/screens/ProfilesScreen.tsx +88 -73
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +72 -7
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +25 -14
- package/src/ui/state/types.ts +61 -31
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/version-snapshot.ts +0 -189
|
@@ -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,13 @@ 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
32
|
import {
|
|
41
33
|
checkMissingDeps,
|
|
42
34
|
getPluginSetupFromSource,
|
|
43
35
|
installPluginDeps,
|
|
44
36
|
} from "../../services/plugin-setup.js";
|
|
45
37
|
import { saveProfile } from "../../services/profiles.js";
|
|
38
|
+
import { requirersHere } from "../../services/resolver.js";
|
|
46
39
|
import {
|
|
47
40
|
fetchPluginContents,
|
|
48
41
|
fetchPluginSkillDetail,
|
|
@@ -61,41 +54,54 @@ import {
|
|
|
61
54
|
// screen shows or acts on — row text, detail panel, header counts, U / a, the
|
|
62
55
|
// scope picker — reads the same map it builds.
|
|
63
56
|
import {
|
|
57
|
+
type PluginUpdateAction,
|
|
64
58
|
type PluginUpdateItem,
|
|
65
59
|
classifyPluginRows,
|
|
66
60
|
countPluginStates,
|
|
61
|
+
isKeptDeprecated,
|
|
67
62
|
scopeIsBehind,
|
|
68
63
|
} from "../../services/update-plan.js";
|
|
69
|
-
import {
|
|
70
|
-
diffVersions,
|
|
71
|
-
loadSeenVersions,
|
|
72
|
-
saveSeenVersions,
|
|
73
|
-
} from "../../services/version-snapshot.js";
|
|
74
64
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
75
65
|
import { createRequestGate } from "../../utils/request-gate.js";
|
|
76
66
|
import { buildCatalogNoticeText } from "../adapters/catalogNotice.js";
|
|
67
|
+
import { counted, failureReport } from "../adapters/failureReport.js";
|
|
77
68
|
import {
|
|
69
|
+
type FailureVerb,
|
|
78
70
|
type PluginBrowserItem,
|
|
79
71
|
type PluginComponentItem,
|
|
80
72
|
type PluginPluginItem,
|
|
81
73
|
buildPluginBrowserItems,
|
|
82
|
-
|
|
74
|
+
exceptionReason,
|
|
75
|
+
failureReasons,
|
|
76
|
+
failureVerb,
|
|
83
77
|
pluginContentsRepo,
|
|
78
|
+
pluginFailureReport,
|
|
84
79
|
} from "../adapters/pluginsAdapter.js";
|
|
80
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
85
81
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
86
82
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
87
83
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
88
84
|
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
89
|
-
import { type BindingId, moveDirection
|
|
85
|
+
import { type BindingId, moveDirection } from "../keymap.js";
|
|
90
86
|
import {
|
|
91
87
|
renderPluginDetail,
|
|
92
88
|
renderPluginRow,
|
|
93
89
|
} from "../renderers/pluginRenderers.js";
|
|
94
90
|
import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
95
91
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
96
|
-
import { asyncValue } from "../state/types.js";
|
|
92
|
+
import { type FailureReport, asyncValue } from "../state/types.js";
|
|
97
93
|
import { theme } from "../theme.js";
|
|
98
94
|
|
|
95
|
+
/** What a scope key did not do, for the failure popup's headline. */
|
|
96
|
+
const SCOPE_ACTION_VERB: Record<
|
|
97
|
+
"install" | "update" | "uninstall",
|
|
98
|
+
FailureVerb
|
|
99
|
+
> = {
|
|
100
|
+
install: "installed",
|
|
101
|
+
update: "updated",
|
|
102
|
+
uninstall: "removed",
|
|
103
|
+
};
|
|
104
|
+
|
|
99
105
|
/**
|
|
100
106
|
* The latest Plugins load wins.
|
|
101
107
|
*
|
|
@@ -150,10 +156,26 @@ export function PluginsScreen() {
|
|
|
150
156
|
const [pins, setPins] = useState<
|
|
151
157
|
Readonly<Record<string, string>> | null | undefined
|
|
152
158
|
>(undefined);
|
|
159
|
+
/**
|
|
160
|
+
* The profile those pins came from — the one a deprecated plugin is dropped
|
|
161
|
+
* from when `a` or `d` removes it. Same three states as `pins`; undefined
|
|
162
|
+
* lets the removal choose the active profile itself.
|
|
163
|
+
*/
|
|
164
|
+
const [activeProfileId, setActiveProfileId] = useState<
|
|
165
|
+
string | null | undefined
|
|
166
|
+
>(undefined);
|
|
167
|
+
/**
|
|
168
|
+
* For each plugin another one needs — the profile's or an installed one's —
|
|
169
|
+
* every plugin that needs it: the same map `magus update` plans with
|
|
170
|
+
* (`requirersHere`), so a deprecated dependency reads the same on both
|
|
171
|
+
* surfaces and is never removed from under a plugin that stays.
|
|
172
|
+
*/
|
|
173
|
+
const [requiredBy, setRequiredBy] = useState<
|
|
174
|
+
Readonly<Record<string, readonly string[]>>
|
|
175
|
+
>({});
|
|
153
176
|
|
|
154
177
|
// Fetch data (always fetches all scopes). Only the most recently started
|
|
155
|
-
// load may touch the screen
|
|
156
|
-
// load would rewind.
|
|
178
|
+
// load may touch the screen.
|
|
157
179
|
const fetchData = useCallback(async () => {
|
|
158
180
|
const token = pluginsLoadGate.begin();
|
|
159
181
|
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
@@ -173,7 +195,17 @@ export function PluginsScreen() {
|
|
|
173
195
|
}),
|
|
174
196
|
]);
|
|
175
197
|
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
198
|
+
// The same edges `magus update` plans with: the profile's and every
|
|
199
|
+
// installed plugin's, so a plugin one of them needs is never removed.
|
|
200
|
+
const required = await requirersHere(
|
|
201
|
+
active?.closure.dependencies?.requiredBy,
|
|
202
|
+
pluginData.filter(isInstalledAnywhere).map((p) => p.id),
|
|
203
|
+
state.projectPath || process.cwd(),
|
|
204
|
+
);
|
|
205
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
176
206
|
setPins(active?.closure.plugins ?? null);
|
|
207
|
+
setActiveProfileId(active?.profileId ?? null);
|
|
208
|
+
setRequiredBy(required);
|
|
177
209
|
|
|
178
210
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
179
211
|
// after the fetch, while the record still describes this attempt.
|
|
@@ -191,32 +223,6 @@ export function PluginsScreen() {
|
|
|
191
223
|
),
|
|
192
224
|
);
|
|
193
225
|
|
|
194
|
-
// Surface version changes made outside magus (Claude Code's own
|
|
195
|
-
// plugin update, a manual CLI call). Diffing against
|
|
196
|
-
// what we last rendered catches all of them; logging only what
|
|
197
|
-
// magus did would miss every one.
|
|
198
|
-
const current: Record<string, string> = {};
|
|
199
|
-
for (const p of pluginData) {
|
|
200
|
-
if (p.installedVersion) current[p.id] = p.installedVersion;
|
|
201
|
-
}
|
|
202
|
-
const projectPath = state.projectPath || process.cwd();
|
|
203
|
-
const previous = await loadSeenVersions(projectPath);
|
|
204
|
-
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
205
|
-
for (const change of diffVersions(previous, current)) {
|
|
206
|
-
const target = pluginData.find((p) => p.id === change.pluginId);
|
|
207
|
-
if (!target) continue;
|
|
208
|
-
if (change.kind === "updated") {
|
|
209
|
-
target.recentlyUpdatedFrom = change.from;
|
|
210
|
-
} else {
|
|
211
|
-
target.recentlyInstalled = true;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
// Await the write. Fire-and-forget raced the next fetchData() — a
|
|
215
|
-
// refresh or a post-action refetch could load the baseline before the
|
|
216
|
-
// previous save landed and re-report the same change.
|
|
217
|
-
await saveSeenVersions(projectPath, current);
|
|
218
|
-
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
219
|
-
|
|
220
226
|
dispatch({
|
|
221
227
|
type: "PLUGINS_DATA_SUCCESS",
|
|
222
228
|
marketplaces: allMarketplaces,
|
|
@@ -290,10 +296,53 @@ export function PluginsScreen() {
|
|
|
290
296
|
*/
|
|
291
297
|
const statuses = useMemo(
|
|
292
298
|
(): Map<string, PluginUpdateItem> =>
|
|
293
|
-
shownPlugins
|
|
294
|
-
|
|
299
|
+
shownPlugins
|
|
300
|
+
? classifyPluginRows(shownPlugins, pins ?? {}, requiredBy)
|
|
301
|
+
: new Map(),
|
|
302
|
+
[shownPlugins, pins, requiredBy],
|
|
295
303
|
);
|
|
296
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Installed plugins whose MCP server lacks variables, and how many — read
|
|
307
|
+
* from the current config on every load and after every attempt to set them
|
|
308
|
+
* (`envCheck`), so declining the question at install leaves a lasting state
|
|
309
|
+
* on that plugin, and setting them clears it. Nothing records the decline.
|
|
310
|
+
*/
|
|
311
|
+
const [mcpSettingsNeeded, setMcpSettingsNeeded] = useState<
|
|
312
|
+
ReadonlyMap<string, number>
|
|
313
|
+
>(new Map());
|
|
314
|
+
const [envCheck, setEnvCheck] = useState(0);
|
|
315
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: envCheck is the re-read trigger after the variables were asked for; the body never reads it
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (!shownPlugins) return;
|
|
318
|
+
let cancelled = false;
|
|
319
|
+
const installed = shownPlugins.filter(
|
|
320
|
+
(p) => !p.isOrphaned && isInstalledAnywhere(p),
|
|
321
|
+
);
|
|
322
|
+
void Promise.all(
|
|
323
|
+
installed.map(async (p) => {
|
|
324
|
+
try {
|
|
325
|
+
const missing = await missingPluginEnvVars(
|
|
326
|
+
p.marketplace,
|
|
327
|
+
p.name,
|
|
328
|
+
state.projectPath,
|
|
329
|
+
);
|
|
330
|
+
return [p.id, missing.length] as const;
|
|
331
|
+
} catch {
|
|
332
|
+
// Unreadable variables are reported where they are saved; a row
|
|
333
|
+
// that cannot tell claims nothing.
|
|
334
|
+
return [p.id, 0] as const;
|
|
335
|
+
}
|
|
336
|
+
}),
|
|
337
|
+
).then((counts) => {
|
|
338
|
+
if (cancelled) return;
|
|
339
|
+
setMcpSettingsNeeded(new Map(counts.filter(([, n]) => n > 0)));
|
|
340
|
+
});
|
|
341
|
+
return () => {
|
|
342
|
+
cancelled = true;
|
|
343
|
+
};
|
|
344
|
+
}, [shownPlugins, state.projectPath, envCheck]);
|
|
345
|
+
|
|
297
346
|
// Build list items (categories + plugins)
|
|
298
347
|
const allItems = useMemo((): PluginBrowserItem[] => {
|
|
299
348
|
if (!shownMarketplaces || !shownPlugins) return [];
|
|
@@ -304,6 +353,7 @@ export function PluginsScreen() {
|
|
|
304
353
|
expandedPlugins: pluginsState.expandedPlugins,
|
|
305
354
|
pluginContents: pluginsState.pluginContents,
|
|
306
355
|
statuses,
|
|
356
|
+
mcpSettingsNeeded,
|
|
307
357
|
});
|
|
308
358
|
}, [
|
|
309
359
|
shownMarketplaces,
|
|
@@ -312,6 +362,7 @@ export function PluginsScreen() {
|
|
|
312
362
|
pluginsState.expandedPlugins,
|
|
313
363
|
pluginsState.pluginContents,
|
|
314
364
|
statuses,
|
|
365
|
+
mcpSettingsNeeded,
|
|
315
366
|
]);
|
|
316
367
|
|
|
317
368
|
// Filter items by search query
|
|
@@ -517,6 +568,14 @@ export function PluginsScreen() {
|
|
|
517
568
|
remove: () => {
|
|
518
569
|
void handleRemoveDeprecated();
|
|
519
570
|
},
|
|
571
|
+
// The action behind the detail panel's "needs N settings — press e".
|
|
572
|
+
"set-env": () => {
|
|
573
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
574
|
+
if (item?.kind !== "plugin" || !item.mcpSettingsNeeded) return;
|
|
575
|
+
void collectPluginEnvVars(item.plugin.name, item.plugin.marketplace, {
|
|
576
|
+
ask: false,
|
|
577
|
+
});
|
|
578
|
+
},
|
|
520
579
|
"add-marketplace": () => {
|
|
521
580
|
void handleShowAddMarketplaceInstructions();
|
|
522
581
|
},
|
|
@@ -544,12 +603,91 @@ export function PluginsScreen() {
|
|
|
544
603
|
|
|
545
604
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
546
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Run the ONE apply engine over `items`, reload, and — only if something
|
|
608
|
+
* failed, or ran and changed nothing — say why in ONE popup, each plugin
|
|
609
|
+
* named with its reason in the same words the CLI prints. Shared by U, a
|
|
610
|
+
* and d, so the three say the same thing about the same outcome. A success
|
|
611
|
+
* says nothing: the reloaded rows show it. An item that ran and did not move
|
|
612
|
+
* is "not updated" — the fact the CLI lists as "ran but did not change" —
|
|
613
|
+
* because a row still offering the same update, with nothing said, reads as
|
|
614
|
+
* a key that did nothing. A throw from the engine itself — not from one
|
|
615
|
+
* item, which the engine catches and reports — names every item with its
|
|
616
|
+
* message.
|
|
617
|
+
*/
|
|
618
|
+
const applyAndReportFailures = async (
|
|
619
|
+
items: readonly PluginUpdateItem[],
|
|
620
|
+
report: (capture: ApplyReporter) => ApplyReporter = (capture) => capture,
|
|
621
|
+
): Promise<void> => {
|
|
622
|
+
const reasons = failureReasons();
|
|
623
|
+
const actions = new Map(items.map((i) => [i.pluginId, i.action]));
|
|
624
|
+
let failure: FailureReport | null;
|
|
625
|
+
try {
|
|
626
|
+
const result = await applyPlugins(
|
|
627
|
+
[...items],
|
|
628
|
+
state.projectPath || process.cwd(),
|
|
629
|
+
REAL_PLUGIN_DEPS,
|
|
630
|
+
report(reasons.reporter),
|
|
631
|
+
// The profile these rows were judged against — the active one — is
|
|
632
|
+
// the one a removed plugin leaves.
|
|
633
|
+
{ profile: activeProfileId },
|
|
634
|
+
);
|
|
635
|
+
failure = reasons.report(result.failed, actions, result.unchanged);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
638
|
+
failure = pluginFailureReport(
|
|
639
|
+
items.map((i) => ({
|
|
640
|
+
pluginId: i.pluginId,
|
|
641
|
+
verb: failureVerb(i.action),
|
|
642
|
+
detail: message,
|
|
643
|
+
exception: true,
|
|
644
|
+
})),
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
modal.hideModal();
|
|
648
|
+
fetchData();
|
|
649
|
+
if (failure) await modal.failures(failure);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* The same popup for a single plugin action outside the apply engine — a
|
|
654
|
+
* scope key, a plain uninstall — so it reads like every other failure here.
|
|
655
|
+
*/
|
|
656
|
+
const reportPluginFailure = async (
|
|
657
|
+
pluginId: string,
|
|
658
|
+
verb: FailureVerb,
|
|
659
|
+
error: unknown,
|
|
660
|
+
): Promise<void> => {
|
|
661
|
+
const report = pluginFailureReport([
|
|
662
|
+
{
|
|
663
|
+
pluginId,
|
|
664
|
+
verb,
|
|
665
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
666
|
+
exception: true,
|
|
667
|
+
},
|
|
668
|
+
]);
|
|
669
|
+
if (report) await modal.failures(report);
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* A marketplace that was not added. Magus runs the same command the user
|
|
674
|
+
* would, so running it by hand is the one real fix when magus cannot say
|
|
675
|
+
* why it failed.
|
|
676
|
+
*/
|
|
677
|
+
const addMarketplaceFailure = (repo: string, error: unknown) =>
|
|
678
|
+
stepFailureReport(
|
|
679
|
+
`Marketplace ${repo} was not added`,
|
|
680
|
+
repo,
|
|
681
|
+
error,
|
|
682
|
+
`Run it yourself: claude plugin marketplace add ${repo}`,
|
|
683
|
+
);
|
|
684
|
+
|
|
547
685
|
// ── Action handlers ────────────────────────────────────────────────────────
|
|
548
686
|
|
|
549
687
|
const handleRefresh = async () => {
|
|
550
688
|
progress.show("Refreshing cache...");
|
|
551
689
|
try {
|
|
552
|
-
|
|
690
|
+
await refreshAllMarketplaces((p) => {
|
|
553
691
|
progress.show(`${p.name}`, p.current, p.total);
|
|
554
692
|
});
|
|
555
693
|
clearMarketplaceCache();
|
|
@@ -557,26 +695,16 @@ export function PluginsScreen() {
|
|
|
557
695
|
// that moves HEAD invalidates every cached answer.
|
|
558
696
|
clearContentDriftCache();
|
|
559
697
|
progress.hide();
|
|
560
|
-
|
|
561
|
-
let message =
|
|
562
|
-
"Cache refreshed.\n\n" +
|
|
563
|
-
"To update marketplaces from GitHub, run in Claude Code:\n" +
|
|
564
|
-
" /plugin marketplace update\n\n" +
|
|
565
|
-
"Then refresh magus again with 'r'.";
|
|
566
|
-
|
|
567
|
-
if (result.repair.length > 0) {
|
|
568
|
-
const totalRepaired = result.repair.reduce(
|
|
569
|
-
(sum, r) => sum + r.repaired.length,
|
|
570
|
-
0,
|
|
571
|
-
);
|
|
572
|
-
message += `\n\nAuto-repaired ${totalRepaired} plugin(s) with missing agents/commands.`;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
await modal.message("Refreshed", message, "success");
|
|
576
698
|
fetchData();
|
|
577
699
|
} catch (error) {
|
|
578
700
|
progress.hide();
|
|
579
|
-
await modal.
|
|
701
|
+
await modal.failures(
|
|
702
|
+
stepFailureReport(
|
|
703
|
+
"The marketplaces were not refreshed",
|
|
704
|
+
"marketplaces",
|
|
705
|
+
error,
|
|
706
|
+
),
|
|
707
|
+
);
|
|
580
708
|
}
|
|
581
709
|
};
|
|
582
710
|
|
|
@@ -617,12 +745,7 @@ export function PluginsScreen() {
|
|
|
617
745
|
await cliAddMarketplace(choice);
|
|
618
746
|
await handleRefresh();
|
|
619
747
|
} 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
|
-
);
|
|
748
|
+
await modal.failures(addMarketplaceFailure(choice, e));
|
|
626
749
|
}
|
|
627
750
|
return;
|
|
628
751
|
}
|
|
@@ -634,7 +757,6 @@ export function PluginsScreen() {
|
|
|
634
757
|
" claude plugin marketplace add owner/repo\n\n" +
|
|
635
758
|
"Auto-update is enabled by default for new marketplaces.\n\n" +
|
|
636
759
|
"After adding, refresh magus with 'r' to see it.",
|
|
637
|
-
"info",
|
|
638
760
|
);
|
|
639
761
|
};
|
|
640
762
|
|
|
@@ -663,46 +785,37 @@ export function PluginsScreen() {
|
|
|
663
785
|
"NOTE: Individual plugin enablement is still project-specific.\n" +
|
|
664
786
|
'Use "Project" scope in magus to enable for the team.';
|
|
665
787
|
|
|
666
|
-
await modal.message("Team Configuration", helpText
|
|
788
|
+
await modal.message("Team Configuration", helpText);
|
|
667
789
|
};
|
|
668
790
|
|
|
669
791
|
/**
|
|
670
792
|
* Collect environment variables required by a plugin's MCP servers.
|
|
793
|
+
*
|
|
794
|
+
* After an install it first asks whether to configure them now; from the
|
|
795
|
+
* `e` key (`ask: false`) the user has already said yes. Declining, or
|
|
796
|
+
* leaving a value empty, is not remembered: the plugin's detail panel keeps
|
|
797
|
+
* saying what is missing, read from the config, until it is set.
|
|
671
798
|
*/
|
|
672
799
|
const collectPluginEnvVars = async (
|
|
673
800
|
pluginName: string,
|
|
674
801
|
marketplace: string,
|
|
802
|
+
{ ask = true }: { ask?: boolean } = {},
|
|
675
803
|
): Promise<boolean> => {
|
|
676
804
|
try {
|
|
677
|
-
const
|
|
678
|
-
if (!pluginSource) return true;
|
|
679
|
-
|
|
680
|
-
const requirements = await getPluginEnvRequirements(
|
|
805
|
+
const missingVars = await missingPluginEnvVars(
|
|
681
806
|
marketplace,
|
|
682
|
-
|
|
807
|
+
pluginName,
|
|
808
|
+
state.projectPath,
|
|
683
809
|
);
|
|
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
810
|
if (missingVars.length === 0) return true;
|
|
692
811
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (!wantToConfigure) {
|
|
700
|
-
await modal.message(
|
|
701
|
-
"Skipped Configuration",
|
|
702
|
-
`You can configure these variables later in the Settings tab (press ${tabKey("settings")}).`,
|
|
703
|
-
"info",
|
|
812
|
+
if (ask) {
|
|
813
|
+
const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
|
|
814
|
+
const wantToConfigure = await modal.confirm(
|
|
815
|
+
"Configure MCP Servers?",
|
|
816
|
+
`This plugin includes MCP servers (${serverNames.join(", ")}) that need ${missingVars.length} environment variable(s).\n\nConfigure now?`,
|
|
704
817
|
);
|
|
705
|
-
return true;
|
|
818
|
+
if (!wantToConfigure) return true;
|
|
706
819
|
}
|
|
707
820
|
|
|
708
821
|
for (const req of missingVars) {
|
|
@@ -724,14 +837,7 @@ export function PluginsScreen() {
|
|
|
724
837
|
"",
|
|
725
838
|
);
|
|
726
839
|
|
|
727
|
-
if (value === null)
|
|
728
|
-
await modal.message(
|
|
729
|
-
"Configuration Incomplete",
|
|
730
|
-
`Skipped remaining configuration.\nYou can configure these later in the Settings tab (press ${tabKey("settings")}).`,
|
|
731
|
-
"info",
|
|
732
|
-
);
|
|
733
|
-
return true;
|
|
734
|
-
}
|
|
840
|
+
if (value === null) return true;
|
|
735
841
|
|
|
736
842
|
if (value) {
|
|
737
843
|
await setMcpEnvVar(req.name, value, state.projectPath);
|
|
@@ -740,19 +846,21 @@ export function PluginsScreen() {
|
|
|
740
846
|
|
|
741
847
|
return true;
|
|
742
848
|
} catch (error) {
|
|
743
|
-
// The plugin itself is installed either way
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
849
|
+
// The plugin itself is installed either way, but its MCP server will
|
|
850
|
+
// not start without these — so it is said in the popup, never logged
|
|
851
|
+
// out of sight. A file that does not parse names itself and the fix.
|
|
852
|
+
modal.hideModal();
|
|
853
|
+
await modal.failures(
|
|
854
|
+
stepFailureReport(
|
|
855
|
+
`${pluginName} is installed, but its MCP server variables were not saved`,
|
|
856
|
+
pluginName,
|
|
857
|
+
error,
|
|
858
|
+
),
|
|
859
|
+
);
|
|
755
860
|
return true;
|
|
861
|
+
} finally {
|
|
862
|
+
// Re-read, so the panel's "needs N settings" matches what was saved.
|
|
863
|
+
setEnvCheck((n) => n + 1);
|
|
756
864
|
}
|
|
757
865
|
};
|
|
758
866
|
|
|
@@ -797,24 +905,28 @@ export function PluginsScreen() {
|
|
|
797
905
|
const result = await installPluginDeps(missing);
|
|
798
906
|
modal.hideModal();
|
|
799
907
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
"Dependencies Installed",
|
|
812
|
-
`Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`,
|
|
813
|
-
"success",
|
|
814
|
-
);
|
|
815
|
-
}
|
|
908
|
+
// Only what did not install, each with why; what did shows nowhere.
|
|
909
|
+
const report = failureReport(
|
|
910
|
+
`${counted(result.failed.length, "dependency", "dependencies")} of ${pluginName} ${
|
|
911
|
+
result.failed.length === 1 ? "was" : "were"
|
|
912
|
+
} not installed`,
|
|
913
|
+
result.failed.map((f) => ({
|
|
914
|
+
subject: f.pkg,
|
|
915
|
+
reason: exceptionReason(f.error).reason,
|
|
916
|
+
})),
|
|
917
|
+
);
|
|
918
|
+
if (report) await modal.failures(report);
|
|
816
919
|
} catch (error) {
|
|
817
|
-
|
|
920
|
+
// Hide the loading modal first: it would otherwise stay up forever,
|
|
921
|
+
// and the failure behind it would never be read.
|
|
922
|
+
modal.hideModal();
|
|
923
|
+
await modal.failures(
|
|
924
|
+
stepFailureReport(
|
|
925
|
+
`${pluginName} is installed, but its dependencies were not installed`,
|
|
926
|
+
pluginName,
|
|
927
|
+
error,
|
|
928
|
+
),
|
|
929
|
+
);
|
|
818
930
|
}
|
|
819
931
|
};
|
|
820
932
|
|
|
@@ -835,7 +947,6 @@ export function PluginsScreen() {
|
|
|
835
947
|
await modal.message(
|
|
836
948
|
`Remove ${mp.displayName}?`,
|
|
837
949
|
`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.`,
|
|
838
|
-
"info",
|
|
839
950
|
);
|
|
840
951
|
}
|
|
841
952
|
} else {
|
|
@@ -853,11 +964,7 @@ export function PluginsScreen() {
|
|
|
853
964
|
await cliAddMarketplace(repo);
|
|
854
965
|
await handleRefresh();
|
|
855
966
|
} catch (e) {
|
|
856
|
-
await modal.
|
|
857
|
-
"Could not add marketplace",
|
|
858
|
-
`${(e as Error).message}\n\nRun it yourself if this persists:\n\n claude plugin marketplace add ${repo}`,
|
|
859
|
-
"error",
|
|
860
|
-
);
|
|
967
|
+
await modal.failures(addMarketplaceFailure(repo, e));
|
|
861
968
|
}
|
|
862
969
|
}
|
|
863
970
|
}
|
|
@@ -984,7 +1091,7 @@ export function PluginsScreen() {
|
|
|
984
1091
|
fetchData();
|
|
985
1092
|
} catch (error) {
|
|
986
1093
|
modal.hideModal();
|
|
987
|
-
await
|
|
1094
|
+
await reportPluginFailure(plugin.id, SCOPE_ACTION_VERB[action], error);
|
|
988
1095
|
}
|
|
989
1096
|
}
|
|
990
1097
|
};
|
|
@@ -1014,51 +1121,20 @@ export function PluginsScreen() {
|
|
|
1014
1121
|
|
|
1015
1122
|
modal.loading(
|
|
1016
1123
|
// A broken plugin runs NO claude command at all, so naming one here would
|
|
1017
|
-
// misdescribe the work
|
|
1124
|
+
// misdescribe the work.
|
|
1018
1125
|
status.action === "stale-load"
|
|
1019
|
-
? `Repairing ${plugin.name}…\
|
|
1126
|
+
? `Repairing ${plugin.name}…\nmaking Claude Code load v${status.target ?? "?"} here`
|
|
1020
1127
|
: repairing
|
|
1021
1128
|
? `Repairing ${plugin.name}…\nclaude plugin uninstall ${plugin.id} --scope ${scope} && claude plugin install ${plugin.id} --scope ${scope}`
|
|
1022
1129
|
: `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
1023
1130
|
);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
// so what runs is exactly what the row claimed.
|
|
1032
|
-
const result = await applyPlugins(
|
|
1033
|
-
[status],
|
|
1034
|
-
state.projectPath || process.cwd(),
|
|
1035
|
-
REAL_PLUGIN_DEPS,
|
|
1036
|
-
reasons.reporter,
|
|
1037
|
-
);
|
|
1038
|
-
modal.hideModal();
|
|
1039
|
-
if (result.failed.length > 0) {
|
|
1040
|
-
await modal.message(
|
|
1041
|
-
"Error",
|
|
1042
|
-
`Failed to update ${plugin.name}`,
|
|
1043
|
-
"error",
|
|
1044
|
-
);
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
// Ran, exited 0, moved nothing — and the row will not have moved
|
|
1048
|
-
// either. Saying nothing here is what made "press U, press U,
|
|
1049
|
-
// forever": the CLI names the cause, so the screen does too.
|
|
1050
|
-
if (result.unchanged.length > 0) {
|
|
1051
|
-
await modal.message(
|
|
1052
|
-
"No change",
|
|
1053
|
-
reasons.lines(result.unchanged),
|
|
1054
|
-
"info",
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
fetchData();
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
modal.hideModal();
|
|
1060
|
-
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
1061
|
-
}
|
|
1131
|
+
// One plugin, but the SAME engine — so a single update records the
|
|
1132
|
+
// version that landed and touches every scope that is behind, exactly as
|
|
1133
|
+
// `magus update` does. The plan item IS the row's status: the classifier's
|
|
1134
|
+
// answer for this plugin under its real pin (the profile's, or "latest" for
|
|
1135
|
+
// an undeclared install — see pinForRow), so what runs is exactly what the
|
|
1136
|
+
// row claimed. A failure says why in a popup.
|
|
1137
|
+
await applyAndReportFailures([status]);
|
|
1062
1138
|
};
|
|
1063
1139
|
|
|
1064
1140
|
/**
|
|
@@ -1079,60 +1155,37 @@ export function PluginsScreen() {
|
|
|
1079
1155
|
if (pluginsState.plugins.status !== "success") return;
|
|
1080
1156
|
|
|
1081
1157
|
// Every item that needs work, by the same `pluginNeedsWork` the CLI's
|
|
1082
|
-
// apply uses: updates, content repairs, broken loads, missing files,
|
|
1083
|
-
// profile plugin not installed yet
|
|
1158
|
+
// apply uses: updates, content repairs, broken loads, missing files, a
|
|
1159
|
+
// profile plugin not installed yet, and deprecated plugins to remove —
|
|
1160
|
+
// exactly what `magus update` would do.
|
|
1084
1161
|
const items = [...statuses.values()]
|
|
1085
1162
|
.filter(pluginNeedsWork)
|
|
1086
1163
|
.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
1087
1164
|
if (items.length === 0) return;
|
|
1088
1165
|
|
|
1089
|
-
const
|
|
1090
|
-
const total = items.filter(pluginNeedsWork).length;
|
|
1166
|
+
const total = items.length;
|
|
1091
1167
|
|
|
1092
1168
|
// A broken plugin is repaired in the registry, not updated, and the progress
|
|
1093
1169
|
// line is the only place the user sees which one happened to which plugin.
|
|
1094
1170
|
const actionOf = new Map(items.map((i) => [i.pluginId, i.action]));
|
|
1171
|
+
const VERB: Partial<Record<PluginUpdateAction, string>> = {
|
|
1172
|
+
"stale-load": "Repairing",
|
|
1173
|
+
remove: "Removing",
|
|
1174
|
+
};
|
|
1095
1175
|
|
|
1096
1176
|
let done = 0;
|
|
1097
|
-
|
|
1098
|
-
|
|
1177
|
+
// One popup afterwards, and only for failures: each plugin that failed,
|
|
1178
|
+
// with why. The one this replaced listed six plugin names and not one
|
|
1179
|
+
// reason. Everything else shows its new state once the list reloads.
|
|
1180
|
+
await applyAndReportFailures(items, (capture) => ({
|
|
1099
1181
|
begin: (name) => {
|
|
1100
1182
|
done++;
|
|
1101
|
-
const verb =
|
|
1102
|
-
actionOf.get(name) === "stale-load" ? "Repairing" : "Updating";
|
|
1183
|
+
const verb = VERB[actionOf.get(name) ?? "update"] ?? "Updating";
|
|
1103
1184
|
modal.loading(`${verb} ${name} (${done}/${total})…`);
|
|
1104
1185
|
},
|
|
1105
|
-
finish:
|
|
1186
|
+
finish: capture.finish,
|
|
1106
1187
|
detach: (work) => work(),
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
try {
|
|
1110
|
-
const result = await applyPlugins(
|
|
1111
|
-
items,
|
|
1112
|
-
projectPath,
|
|
1113
|
-
REAL_PLUGIN_DEPS,
|
|
1114
|
-
reporter,
|
|
1115
|
-
);
|
|
1116
|
-
modal.hideModal();
|
|
1117
|
-
if (result.failed.length > 0) {
|
|
1118
|
-
await modal.message(
|
|
1119
|
-
"Some updates failed",
|
|
1120
|
-
result.failed.join(", "),
|
|
1121
|
-
"error",
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
if (result.unchanged.length > 0) {
|
|
1125
|
-
await modal.message(
|
|
1126
|
-
"No change",
|
|
1127
|
-
reasons.lines(result.unchanged),
|
|
1128
|
-
"info",
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
fetchData();
|
|
1132
|
-
} catch (error) {
|
|
1133
|
-
modal.hideModal();
|
|
1134
|
-
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
1135
|
-
}
|
|
1188
|
+
}));
|
|
1136
1189
|
};
|
|
1137
1190
|
|
|
1138
1191
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
@@ -1195,7 +1248,7 @@ export function PluginsScreen() {
|
|
|
1195
1248
|
fetchData();
|
|
1196
1249
|
} catch (error) {
|
|
1197
1250
|
modal.hideModal();
|
|
1198
|
-
await
|
|
1251
|
+
await reportPluginFailure(plugin.id, SCOPE_ACTION_VERB[action], error);
|
|
1199
1252
|
}
|
|
1200
1253
|
};
|
|
1201
1254
|
|
|
@@ -1212,6 +1265,17 @@ export function PluginsScreen() {
|
|
|
1212
1265
|
// was already installed. An orphan is removed; where else a name happens
|
|
1213
1266
|
// to appear is not this screen's guess to make.
|
|
1214
1267
|
|
|
1268
|
+
// A DEPRECATED one goes through the engine `a` and `magus update` use, so
|
|
1269
|
+
// `d` also drops it from the profile and reads the result back — the
|
|
1270
|
+
// uninstall-only path below left the profile declaring it, and the row
|
|
1271
|
+
// came straight back as "deprecated".
|
|
1272
|
+
const status = statuses.get(plugin.id);
|
|
1273
|
+
if (status?.action === "remove") {
|
|
1274
|
+
modal.loading(`Removing ${plugin.name}…`);
|
|
1275
|
+
await applyAndReportFailures([status]);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1215
1279
|
try {
|
|
1216
1280
|
// Remove from all scopes — try all to clean up stale references
|
|
1217
1281
|
const scopes: PluginScope[] = ["user", "project", "local"];
|
|
@@ -1233,7 +1297,7 @@ export function PluginsScreen() {
|
|
|
1233
1297
|
fetchData();
|
|
1234
1298
|
} catch (error) {
|
|
1235
1299
|
modal.hideModal();
|
|
1236
|
-
await
|
|
1300
|
+
await reportPluginFailure(plugin.id, "removed", error);
|
|
1237
1301
|
}
|
|
1238
1302
|
};
|
|
1239
1303
|
|
|
@@ -1245,10 +1309,12 @@ export function PluginsScreen() {
|
|
|
1245
1309
|
enabledPlugins =
|
|
1246
1310
|
(await readSettings(state.projectPath)).enabledPlugins ?? {};
|
|
1247
1311
|
} catch (error) {
|
|
1248
|
-
await modal.
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1312
|
+
await modal.failures(
|
|
1313
|
+
stepFailureReport(
|
|
1314
|
+
"No profile was saved",
|
|
1315
|
+
"this project's plugins",
|
|
1316
|
+
error,
|
|
1317
|
+
),
|
|
1252
1318
|
);
|
|
1253
1319
|
return;
|
|
1254
1320
|
}
|
|
@@ -1278,21 +1344,22 @@ export function PluginsScreen() {
|
|
|
1278
1344
|
try {
|
|
1279
1345
|
await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
|
|
1280
1346
|
modal.hideModal();
|
|
1281
|
-
await modal.message(
|
|
1282
|
-
"Saved",
|
|
1283
|
-
`Profile "${name.trim()}" saved.\nPress ${tabKey("profiles")} to manage profiles.`,
|
|
1284
|
-
"success",
|
|
1285
|
-
);
|
|
1286
1347
|
} catch (error) {
|
|
1287
1348
|
modal.hideModal();
|
|
1288
|
-
await modal.
|
|
1349
|
+
await modal.failures(
|
|
1350
|
+
stepFailureReport(
|
|
1351
|
+
`Profile "${name.trim()}" was not saved`,
|
|
1352
|
+
scope.toUpperCase(),
|
|
1353
|
+
error,
|
|
1354
|
+
),
|
|
1355
|
+
);
|
|
1289
1356
|
}
|
|
1290
1357
|
};
|
|
1291
1358
|
|
|
1292
1359
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
1293
1360
|
|
|
1294
1361
|
// Only a genuine first load takes over the screen. A reload keeps the list on
|
|
1295
|
-
// screen and says so in the notice
|
|
1362
|
+
// screen and says so in the header's notice row instead.
|
|
1296
1363
|
if (!shownMarketplaces || !shownPlugins) {
|
|
1297
1364
|
if (
|
|
1298
1365
|
pluginsState.marketplaces.status === "loading" ||
|
|
@@ -1344,6 +1411,9 @@ export function PluginsScreen() {
|
|
|
1344
1411
|
...(selectedItem?.kind === "plugin" && selectedItem.plugin.isOrphaned
|
|
1345
1412
|
? []
|
|
1346
1413
|
: ["remove" as const]),
|
|
1414
|
+
...(selectedItem?.kind === "plugin" && selectedItem.mcpSettingsNeeded
|
|
1415
|
+
? []
|
|
1416
|
+
: ["set-env" as const]),
|
|
1347
1417
|
];
|
|
1348
1418
|
|
|
1349
1419
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
@@ -1358,7 +1428,10 @@ export function PluginsScreen() {
|
|
|
1358
1428
|
// Cannot tell whether it is current. Counted and shown separately: folding
|
|
1359
1429
|
// it into `updateCount` would overstate, and folding it into silence is the
|
|
1360
1430
|
// "cannot tell rendered as fine" bug.
|
|
1361
|
-
|
|
1431
|
+
// Moved or still-required plugins are `unknown` to the planner but known to
|
|
1432
|
+
// the user — kept on purpose — so they are not counted as "unverified".
|
|
1433
|
+
const unknownCount =
|
|
1434
|
+
counts.unknown - [...statuses.values()].filter(isKeptDeprecated).length;
|
|
1362
1435
|
const installCount = counts.install;
|
|
1363
1436
|
// Broken: installed at the right version, loading an older one. Its own
|
|
1364
1437
|
// number, never folded into `updateCount` or into silence — a clean header
|
|
@@ -1368,8 +1441,11 @@ export function PluginsScreen() {
|
|
|
1368
1441
|
// a reader had to already know what "loads" versus "installed" means here
|
|
1369
1442
|
// before the count told them anything. `a` clears these.
|
|
1370
1443
|
const brokenCount = counts["stale-load"];
|
|
1371
|
-
|
|
1372
|
-
|
|
1444
|
+
// No longer published, and `a` removes them. Counted where the rows can be
|
|
1445
|
+
// seen to need work; they used to sit inside "unverified".
|
|
1446
|
+
const deprecatedCount = counts.remove;
|
|
1447
|
+
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` : ""}`;
|
|
1448
|
+
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""}${installCount > 0 ? ` │ ${installCount} +` : ""}${brokenCount + deprecatedCount > 0 ? ` │ ${brokenCount + deprecatedCount} ✗` : ""}${unknownCount > 0 ? ` │ ${unknownCount} ?` : ""} │ / to search`;
|
|
1373
1449
|
|
|
1374
1450
|
const catalogNotice = buildCatalogNotice({
|
|
1375
1451
|
failures: catalogFailures,
|
|
@@ -1379,18 +1455,13 @@ export function PluginsScreen() {
|
|
|
1379
1455
|
width: dimensions.terminalWidth,
|
|
1380
1456
|
now: nowTick,
|
|
1381
1457
|
});
|
|
1382
|
-
// Without a resolved profile
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1458
|
+
// Without a resolved profile every row is judged against "latest". That is
|
|
1459
|
+
// not said here: the startup question already tells the user there is no
|
|
1460
|
+
// active profile, and "profile not resolved" was mechanism, not state.
|
|
1385
1461
|
const notice = isReloading ? (
|
|
1386
1462
|
<text fg={theme.colors.muted}>⟳ Refreshing catalogs from GitHub…</text>
|
|
1387
1463
|
) : (
|
|
1388
|
-
|
|
1389
|
-
(pins === null ? (
|
|
1390
|
-
<text fg={theme.colors.muted}>
|
|
1391
|
-
profile not resolved — states judged against latest
|
|
1392
|
-
</text>
|
|
1393
|
-
) : null))
|
|
1464
|
+
catalogNotice
|
|
1394
1465
|
);
|
|
1395
1466
|
|
|
1396
1467
|
return (
|