magus-cli 7.11.3 → 7.11.5
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__/cli-profile-prompts.test.ts +9 -9
- package/src/__tests__/deprecated-remove-all-profiles.test.ts +171 -0
- package/src/__tests__/not-installed-answer-wrong-scope.test.ts +46 -0
- package/src/__tests__/profile-adopt.test.ts +63 -1
- package/src/__tests__/update-removes-deprecated-other-profiles.test.ts +401 -0
- package/src/cli/bootstrap.ts +4 -20
- package/src/cli/update.ts +14 -2
- package/src/services/active-closure.ts +12 -2
- package/src/services/claude-cli.ts +12 -1
- package/src/services/profile-adopt.ts +13 -28
- package/src/services/profile-state.ts +2 -12
- package/src/services/resolver.ts +31 -0
- package/src/services/update-engine.ts +146 -12
- package/src/services/update-plan.ts +42 -2
- package/src/ui/screens/PluginsScreen.tsx +24 -38
package/src/services/resolver.ts
CHANGED
|
@@ -285,6 +285,37 @@ export function resolveExtends(
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/** Whether a profile entry, `extends` included, turns the plugin on. */
|
|
289
|
+
export function profileLists(
|
|
290
|
+
entry: ProfileManifestEntry | undefined,
|
|
291
|
+
pluginId: string,
|
|
292
|
+
): boolean {
|
|
293
|
+
if (!entry) return false;
|
|
294
|
+
const value = resolveExtends(entry).plugins?.[pluginId];
|
|
295
|
+
return value !== undefined && value !== false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Every plugin id some profile in the manifest turns on, `extends` included —
|
|
300
|
+
* by {@link profileLists}, the test a removal uses to find the profiles it
|
|
301
|
+
* must edit.
|
|
302
|
+
*
|
|
303
|
+
* The planner's view of the profiles that are NOT active. The render writes
|
|
304
|
+
* `false` for each of these into the live settings.json, so a deprecated one
|
|
305
|
+
* shows as a row on the Plugins screen even though the active profile never
|
|
306
|
+
* names it; `magus update` and the screen both pass this set to the planner,
|
|
307
|
+
* which is how such a plugin gets planned for removal at all.
|
|
308
|
+
*/
|
|
309
|
+
export function pluginsAnyProfileLists(manifest: ProfileManifest): string[] {
|
|
310
|
+
const out = new Set<string>();
|
|
311
|
+
for (const entry of Object.values(manifest.profiles)) {
|
|
312
|
+
for (const pluginId of Object.keys(resolveExtends(entry).plugins ?? {})) {
|
|
313
|
+
if (profileLists(entry, pluginId)) out.add(pluginId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return [...out];
|
|
317
|
+
}
|
|
318
|
+
|
|
288
319
|
/** The pin a selected requirement installs at, if its chosen option carries one. */
|
|
289
320
|
export function pinOfBin(bin: SelectedBin): string | undefined {
|
|
290
321
|
const option = bin.chosen;
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
inheritedRowsOf,
|
|
58
58
|
loadedVersionHere,
|
|
59
59
|
notDownloadedReason,
|
|
60
|
+
readInheritedRegistryRows,
|
|
60
61
|
readInstalledPluginsRegistry,
|
|
61
62
|
repairStaleLoad,
|
|
62
63
|
rowBelongsToRepository,
|
|
@@ -69,9 +70,15 @@ import {
|
|
|
69
70
|
readInstalledVersionForScope,
|
|
70
71
|
saveInstalledPluginVersionForScope,
|
|
71
72
|
} from "./plugin-manager.js";
|
|
72
|
-
import {
|
|
73
|
+
import { reconcileActiveProfile, refuseOnProblems } from "./profile-absorb.js";
|
|
74
|
+
import { regenerateLiveFiles } from "./profile-activate.js";
|
|
75
|
+
import {
|
|
76
|
+
activeProfile,
|
|
77
|
+
hasCurrentMarker,
|
|
78
|
+
readActiveMarker,
|
|
79
|
+
} from "./profile-live.js";
|
|
73
80
|
import { removePluginFromProfile } from "./profile-state.js";
|
|
74
|
-
import { profileDependentsOf } from "./resolver.js";
|
|
81
|
+
import { profileDependentsOf, profileLists } from "./resolver.js";
|
|
75
82
|
import { installSkill } from "./skills-manager.js";
|
|
76
83
|
import {
|
|
77
84
|
type ToolFacts,
|
|
@@ -296,6 +303,17 @@ export interface PluginRemoveDeps {
|
|
|
296
303
|
projectPath: string,
|
|
297
304
|
profile: string | null | undefined,
|
|
298
305
|
) => Promise<boolean>;
|
|
306
|
+
/**
|
|
307
|
+
* Stop EVERY profile listing a deprecated plugin, the live files absorbed
|
|
308
|
+
* first (`unlistFromEveryProfile`); answers whether none lists it, read
|
|
309
|
+
* back, and throws the first failure. What {@link applyRemove} runs in
|
|
310
|
+
* place of `dropFromProfile` when present. Optional so a double that models
|
|
311
|
+
* one profile keeps doing it through `dropFromProfile`.
|
|
312
|
+
*/
|
|
313
|
+
unlistEverywhere?: (
|
|
314
|
+
pluginId: string,
|
|
315
|
+
projectPath: string,
|
|
316
|
+
) => Promise<boolean>;
|
|
299
317
|
/**
|
|
300
318
|
* Clear magus's OWN `installedPluginVersions` claim for the plugin at a
|
|
301
319
|
* scope it was removed from (`forgetInstalledPluginVersion`). Claude Code
|
|
@@ -338,6 +356,12 @@ export const REAL_REMOVE_DEPS: PluginRemoveDeps = {
|
|
|
338
356
|
(await enabledAt(scope, projectPath))[pluginId] === true,
|
|
339
357
|
dropFromProfile: (pluginId, projectPath, profile) =>
|
|
340
358
|
removePluginFromProfile(projectPath, pluginId, profile),
|
|
359
|
+
unlistEverywhere: async (pluginId, projectPath) => {
|
|
360
|
+
const { problems } = await unlistFromEveryProfile(pluginId, projectPath);
|
|
361
|
+
if (problems.length > 0) throw problems[0];
|
|
362
|
+
const { profiles } = await readManifest(projectPath);
|
|
363
|
+
return !Object.values(profiles).some((e) => profileLists(e, pluginId));
|
|
364
|
+
},
|
|
341
365
|
forgetVersion: forgetInstalledPluginVersion,
|
|
342
366
|
// The profile picked as `removePluginFromProfile` picks it, so the check and
|
|
343
367
|
// the drop judge the same profile. A parse error throws, as it does there.
|
|
@@ -856,13 +880,14 @@ async function applyStaleLoad(
|
|
|
856
880
|
* Remove ONE deprecated plugin — the `remove` half of {@link applyPlugins}.
|
|
857
881
|
*
|
|
858
882
|
* Uninstalls it at every scope the plan found it installed at, through the
|
|
859
|
-
* Claude Code CLI wrapper, then stops
|
|
860
|
-
* even when the first fails part-way: a
|
|
861
|
-
* marketplace publishes only ever asks for an
|
|
883
|
+
* Claude Code CLI wrapper, then stops every profile listing it
|
|
884
|
+
* (`unlistEverywhere`). Both halves run even when the first fails part-way: a
|
|
885
|
+
* profile that keeps a plugin no marketplace publishes only ever asks for an
|
|
886
|
+
* install that cannot happen.
|
|
862
887
|
*
|
|
863
888
|
* Success is READ BACK, never "the calls returned": installed at no scope here
|
|
864
|
-
* afterwards — all three are checked, not just the planned ones — and
|
|
865
|
-
* profile
|
|
889
|
+
* afterwards — all three are checked, not just the planned ones — and no
|
|
890
|
+
* profile lists it.
|
|
866
891
|
*/
|
|
867
892
|
async function applyRemove(
|
|
868
893
|
item: PluginUpdateItem,
|
|
@@ -914,11 +939,11 @@ async function applyRemove(
|
|
|
914
939
|
inheritedError ??= error;
|
|
915
940
|
}
|
|
916
941
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
942
|
+
// Every profile that lists it, or one left listing it brings the row back
|
|
943
|
+
// on the next render.
|
|
944
|
+
const unlisted = remove.unlistEverywhere
|
|
945
|
+
? await remove.unlistEverywhere(item.pluginId, projectPath)
|
|
946
|
+
: await remove.dropFromProfile(item.pluginId, projectPath, profile);
|
|
922
947
|
|
|
923
948
|
const still: PluginScope[] = [];
|
|
924
949
|
for (const scope of REMOVE_SCOPES) {
|
|
@@ -1007,6 +1032,115 @@ async function applyRemove(
|
|
|
1007
1032
|
/** Every scope a removal must leave the plugin uninstalled at. */
|
|
1008
1033
|
const REMOVE_SCOPES: readonly PluginScope[] = ["user", "project", "local"];
|
|
1009
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Remove an orphan the plan holds no `remove` item for — the Plugins screen's
|
|
1037
|
+
* `d` on a row the planner keeps rather than removes: it moved to another
|
|
1038
|
+
* marketplace, another plugin still requires it, or its marketplace was not
|
|
1039
|
+
* read. The user asked for it gone, so it goes.
|
|
1040
|
+
*
|
|
1041
|
+
* Uninstalls at every scope here, then each inherited row from its own folder,
|
|
1042
|
+
* then {@link unlistFromEveryProfile}: a key the render writes `false` for is
|
|
1043
|
+
* cleared only once no profile lists the id.
|
|
1044
|
+
*
|
|
1045
|
+
* `problems` holds every real failure; the CLI's "nothing at this scope here"
|
|
1046
|
+
* answer is not one. `unlistedFrom` names the profiles that listed it.
|
|
1047
|
+
*/
|
|
1048
|
+
export async function removeDeprecatedPlugin(
|
|
1049
|
+
pluginId: string,
|
|
1050
|
+
projectPath: string,
|
|
1051
|
+
deps: {
|
|
1052
|
+
uninstall?: (id: string, scope: PluginScope, cwd: string) => Promise<void>;
|
|
1053
|
+
inheritedRows?: (
|
|
1054
|
+
id: string,
|
|
1055
|
+
projectPath: string,
|
|
1056
|
+
) => Promise<InstalledPluginEntry[]>;
|
|
1057
|
+
} = {},
|
|
1058
|
+
): Promise<{ problems: unknown[]; unlistedFrom: string[] }> {
|
|
1059
|
+
const uninstall = deps.uninstall ?? uninstallPluginViaCli;
|
|
1060
|
+
const inheritedRows = deps.inheritedRows ?? readInheritedRegistryRows;
|
|
1061
|
+
const problems: unknown[] = [];
|
|
1062
|
+
const unlistedFrom: string[] = [];
|
|
1063
|
+
|
|
1064
|
+
for (const scope of REMOVE_SCOPES) {
|
|
1065
|
+
try {
|
|
1066
|
+
await uninstall(pluginId, scope, projectPath);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (!isNotInstalledAnswer(error)) problems.push(error);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// A folder of the same repository may hold the install this one only
|
|
1072
|
+
// shows: the CLI removes it only when run from that folder.
|
|
1073
|
+
for (const row of await inheritedRows(pluginId, projectPath)) {
|
|
1074
|
+
if (!row.projectPath) continue;
|
|
1075
|
+
try {
|
|
1076
|
+
await uninstall(pluginId, row.scope, row.projectPath);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
problems.push(error);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const unlisted = await unlistFromEveryProfile(pluginId, projectPath);
|
|
1083
|
+
problems.push(...unlisted.problems);
|
|
1084
|
+
return { problems, unlistedFrom: unlisted.unlistedFrom };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Stop EVERY profile in the project listing a deprecated plugin, keeping the
|
|
1089
|
+
* user's live edits — the profile half of both removals, {@link applyRemove}
|
|
1090
|
+
* (`magus update`, the Plugins screen's `a` and `d`) and
|
|
1091
|
+
* {@link removeDeprecatedPlugin}.
|
|
1092
|
+
*
|
|
1093
|
+
* Every profile, not only the active one: the render writes `false` for each
|
|
1094
|
+
* id ANY profile lists, so one profile left listing it brings the row back on
|
|
1095
|
+
* the next render. A deprecated plugin can never be installed, so no profile
|
|
1096
|
+
* loses anything it could use.
|
|
1097
|
+
*
|
|
1098
|
+
* The live files are absorbed into the active profile BEFORE any profile is
|
|
1099
|
+
* written, and rendered again after, so what Claude Code or the user wrote
|
|
1100
|
+
* since the last render survives and the id's key is gone. Under
|
|
1101
|
+
* `withCliBookkeeping` (`magus update`) both are left to the wrapper, whose
|
|
1102
|
+
* reconcile absorbs before it renders: reconciling here would write a fresh
|
|
1103
|
+
* marker mid-run, clearing the bookkeeping flag, so the installs that follow
|
|
1104
|
+
* would read as the user's own edits.
|
|
1105
|
+
*
|
|
1106
|
+
* `unlistedFrom` names the profiles that listed it, each READ BACK as no longer
|
|
1107
|
+
* listing it; `problems` holds every failure, a refused reconcile included,
|
|
1108
|
+
* in which case nothing was written.
|
|
1109
|
+
*/
|
|
1110
|
+
async function unlistFromEveryProfile(
|
|
1111
|
+
pluginId: string,
|
|
1112
|
+
projectPath: string,
|
|
1113
|
+
): Promise<{ problems: unknown[]; unlistedFrom: string[] }> {
|
|
1114
|
+
const problems: unknown[] = [];
|
|
1115
|
+
const unlistedFrom: string[] = [];
|
|
1116
|
+
try {
|
|
1117
|
+
const marker = (await hasCurrentMarker(projectPath))
|
|
1118
|
+
? await readActiveMarker(projectPath)
|
|
1119
|
+
: null;
|
|
1120
|
+
const wrapperReconciles = marker?.cliBookkeeping === true;
|
|
1121
|
+
// Refuses, writing nothing, on an unreadable live file.
|
|
1122
|
+
if (!wrapperReconciles) {
|
|
1123
|
+
refuseOnProblems(await reconcileActiveProfile(projectPath));
|
|
1124
|
+
}
|
|
1125
|
+
const manifest = await readManifest(projectPath);
|
|
1126
|
+
for (const [id, entry] of Object.entries(manifest.profiles)) {
|
|
1127
|
+
if (!profileLists(entry, pluginId)) continue;
|
|
1128
|
+
if (await removePluginFromProfile(projectPath, pluginId, id)) {
|
|
1129
|
+
unlistedFrom.push(id);
|
|
1130
|
+
} else {
|
|
1131
|
+
problems.push(
|
|
1132
|
+
new Error(`the ${entry.name ?? id} profile still lists it`),
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// Renders nothing, and answers false, when no profile is active.
|
|
1137
|
+
if (!wrapperReconciles) await regenerateLiveFiles(projectPath);
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
problems.push(error);
|
|
1140
|
+
}
|
|
1141
|
+
return { problems, unlistedFrom };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1010
1144
|
/** Options for {@link applyPlugins}. */
|
|
1011
1145
|
export interface ApplyPluginsOptions {
|
|
1012
1146
|
/**
|
|
@@ -879,6 +879,8 @@ export function classifyPlugin(
|
|
|
879
879
|
* Every plugin the active profile declares, under its pin, and every plugin
|
|
880
880
|
* installed in any scope (user, project, local), under "latest" — the rule
|
|
881
881
|
* {@link pinForRow} states — each classified by {@link classifyPlugin}, sorted.
|
|
882
|
+
* A plugin only other profiles list joins the plan only to be removed, when it
|
|
883
|
+
* is deprecated ({@link onlyOtherProfilesList}).
|
|
882
884
|
*
|
|
883
885
|
* ONE list, read by both surfaces: the screen's rows, header counts and
|
|
884
886
|
* "update all", and the CLI's report and apply. It used to be two — the CLI
|
|
@@ -891,11 +893,16 @@ export function classifyPlugin(
|
|
|
891
893
|
* @param catalog getAvailablePlugins() output — keyed by id, or as a list
|
|
892
894
|
* @param requiredBy closure.dependencies?.requiredBy — which plugin needs each
|
|
893
895
|
* dependency the profile does not name itself
|
|
896
|
+
* @param listedByProfiles every plugin id ANY profile in the manifest lists
|
|
897
|
+
* (`pluginsAnyProfileLists`). One neither pinned nor
|
|
898
|
+
* installed is planned only as a deprecated `remove`; see
|
|
899
|
+
* {@link onlyOtherProfilesList}.
|
|
894
900
|
*/
|
|
895
901
|
export function planPluginUpdates(
|
|
896
902
|
pinned: Readonly<Record<string, string>>,
|
|
897
903
|
catalog: ReadonlyMap<string, PluginInfo> | readonly PluginInfo[],
|
|
898
904
|
requiredBy: RequiredBy = {},
|
|
905
|
+
listedByProfiles: readonly string[] = [],
|
|
899
906
|
): PluginUpdateItem[] {
|
|
900
907
|
const index = indexCatalog(catalog);
|
|
901
908
|
const plugins =
|
|
@@ -908,13 +915,42 @@ export function planPluginUpdates(
|
|
|
908
915
|
const pin = pinForRow(info, pinned);
|
|
909
916
|
if (pin !== null) pins.set(info.id, pin);
|
|
910
917
|
}
|
|
918
|
+
const othersOnly = new Set<string>();
|
|
919
|
+
for (const pluginId of listedByProfiles) {
|
|
920
|
+
if (pins.has(pluginId)) continue;
|
|
921
|
+
othersOnly.add(pluginId);
|
|
922
|
+
pins.set(pluginId, "latest");
|
|
923
|
+
}
|
|
911
924
|
return settleDeprecated(
|
|
912
925
|
[...pins].map(([pluginId, pin]) =>
|
|
913
926
|
classifyPlugin(pluginId, pin, index, requiredBy),
|
|
914
927
|
),
|
|
915
928
|
index,
|
|
916
929
|
requiredBy,
|
|
917
|
-
)
|
|
930
|
+
)
|
|
931
|
+
.filter(
|
|
932
|
+
(item) => !othersOnly.has(item.pluginId) || onlyOtherProfilesList(item),
|
|
933
|
+
)
|
|
934
|
+
.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* The one item a plugin gets when only profiles that are NOT active list it,
|
|
939
|
+
* and nothing here installs it: a deprecated `remove`.
|
|
940
|
+
*
|
|
941
|
+
* The render writes `false` for it into the live settings.json, so the Plugins
|
|
942
|
+
* screen shows it as a row, and a profile that keeps a plugin no marketplace
|
|
943
|
+
* publishes only ever asks for an install that cannot happen. Removing it here
|
|
944
|
+
* drops it from every profile that lists it, which is what clears the row.
|
|
945
|
+
*
|
|
946
|
+
* Anything else — published, moved, still required, a marketplace we did not
|
|
947
|
+
* read — is no work of this profile's: it is not active, so nothing is
|
|
948
|
+
* installed, updated or kept for it, and it gets no item at all. Judged after
|
|
949
|
+
* {@link settleDeprecated}, so retired plugins that only require each other are
|
|
950
|
+
* still removed together.
|
|
951
|
+
*/
|
|
952
|
+
function onlyOtherProfilesList(item: PluginUpdateItem): boolean {
|
|
953
|
+
return item.action === "remove" && item.reason === "deprecated";
|
|
918
954
|
}
|
|
919
955
|
|
|
920
956
|
/**
|
|
@@ -951,9 +987,13 @@ export function classifyPluginRows(
|
|
|
951
987
|
plugins: readonly PluginInfo[],
|
|
952
988
|
pins: Readonly<Record<string, string>>,
|
|
953
989
|
requiredBy: RequiredBy = {},
|
|
990
|
+
listedByProfiles: readonly string[] = [],
|
|
954
991
|
): Map<string, PluginUpdateItem> {
|
|
955
992
|
return new Map(
|
|
956
|
-
planPluginUpdates(pins, plugins, requiredBy).map((i) => [
|
|
993
|
+
planPluginUpdates(pins, plugins, requiredBy, listedByProfiles).map((i) => [
|
|
994
|
+
i.pluginId,
|
|
995
|
+
i,
|
|
996
|
+
]),
|
|
957
997
|
);
|
|
958
998
|
}
|
|
959
999
|
|
|
@@ -9,14 +9,8 @@ import {
|
|
|
9
9
|
type PluginScope,
|
|
10
10
|
addMarketplace as cliAddMarketplace,
|
|
11
11
|
repairPlugin as cliRepairPlugin,
|
|
12
|
-
isNotInstalledAnswer,
|
|
13
|
-
uninstallPluginViaCli,
|
|
14
12
|
} from "../../services/claude-cli.js";
|
|
15
|
-
import {
|
|
16
|
-
readInheritedRegistryRows,
|
|
17
|
-
readSettings,
|
|
18
|
-
setMcpEnvVar,
|
|
19
|
-
} from "../../services/claude-settings.js";
|
|
13
|
+
import { readSettings, setMcpEnvVar } from "../../services/claude-settings.js";
|
|
20
14
|
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
21
15
|
import { missingPluginEnvVars } from "../../services/plugin-env-needs.js";
|
|
22
16
|
import {
|
|
@@ -49,6 +43,7 @@ import {
|
|
|
49
43
|
applyPlugins,
|
|
50
44
|
installPluginInScope,
|
|
51
45
|
pluginNeedsWork,
|
|
46
|
+
removeDeprecatedPlugin,
|
|
52
47
|
uninstallPluginInScope,
|
|
53
48
|
updatePluginInScope,
|
|
54
49
|
} from "../../services/update-engine.js";
|
|
@@ -176,6 +171,14 @@ export function PluginsScreen() {
|
|
|
176
171
|
const [requiredBy, setRequiredBy] = useState<
|
|
177
172
|
Readonly<Record<string, readonly string[]>>
|
|
178
173
|
>({});
|
|
174
|
+
/**
|
|
175
|
+
* Every plugin any profile lists — the same set `magus update` plans with,
|
|
176
|
+
* so a deprecated plugin only another profile keeps is a `remove` row here
|
|
177
|
+
* and a removal there.
|
|
178
|
+
*/
|
|
179
|
+
const [listedByProfiles, setListedByProfiles] = useState<readonly string[]>(
|
|
180
|
+
[],
|
|
181
|
+
);
|
|
179
182
|
|
|
180
183
|
// Fetch data (always fetches all scopes). Only the most recently started
|
|
181
184
|
// load may touch the screen.
|
|
@@ -209,6 +212,7 @@ export function PluginsScreen() {
|
|
|
209
212
|
setPins(active?.closure.plugins ?? null);
|
|
210
213
|
setActiveProfileId(active?.profileId ?? null);
|
|
211
214
|
setRequiredBy(required);
|
|
215
|
+
setListedByProfiles(active?.listedByProfiles ?? []);
|
|
212
216
|
|
|
213
217
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
214
218
|
// after the fetch, while the record still describes this attempt.
|
|
@@ -300,9 +304,14 @@ export function PluginsScreen() {
|
|
|
300
304
|
const statuses = useMemo(
|
|
301
305
|
(): Map<string, PluginUpdateItem> =>
|
|
302
306
|
shownPlugins
|
|
303
|
-
? classifyPluginRows(
|
|
307
|
+
? classifyPluginRows(
|
|
308
|
+
shownPlugins,
|
|
309
|
+
pins ?? {},
|
|
310
|
+
requiredBy,
|
|
311
|
+
listedByProfiles,
|
|
312
|
+
)
|
|
304
313
|
: new Map(),
|
|
305
|
-
[shownPlugins, pins, requiredBy],
|
|
314
|
+
[shownPlugins, pins, requiredBy, listedByProfiles],
|
|
306
315
|
);
|
|
307
316
|
|
|
308
317
|
/**
|
|
@@ -1265,39 +1274,16 @@ export function PluginsScreen() {
|
|
|
1265
1274
|
return;
|
|
1266
1275
|
}
|
|
1267
1276
|
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1277
|
+
// An orphan the plan keeps rather than removes — it moved, another plugin
|
|
1278
|
+
// still requires it, or its marketplace was not read — and the user asked
|
|
1279
|
+
// for it gone. The engine uninstalls everywhere, drops it from every
|
|
1280
|
+
// profile and renders the active one again, so the row does not come back.
|
|
1271
1281
|
try {
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
for (const scope of scopes) {
|
|
1275
|
-
try {
|
|
1276
|
-
modal.loading(
|
|
1277
|
-
`Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`,
|
|
1278
|
-
);
|
|
1279
|
-
await uninstallPluginViaCli(plugin.id, scope, state.projectPath);
|
|
1280
|
-
} catch (error) {
|
|
1281
|
-
if (!isNotInstalledAnswer(error)) problems.push(error);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
// A folder of the same repository may hold the install this one only
|
|
1285
|
-
// shows: the CLI removes it only when run from that folder.
|
|
1286
|
-
const inherited = await readInheritedRegistryRows(
|
|
1282
|
+
modal.loading(`Removing ${plugin.name}…`);
|
|
1283
|
+
const { problems } = await removeDeprecatedPlugin(
|
|
1287
1284
|
plugin.id,
|
|
1288
1285
|
state.projectPath || process.cwd(),
|
|
1289
1286
|
);
|
|
1290
|
-
for (const row of inherited) {
|
|
1291
|
-
if (!row.projectPath) continue;
|
|
1292
|
-
try {
|
|
1293
|
-
modal.loading(
|
|
1294
|
-
`Removing ${plugin.name} from the repository's main folder, which this folder shares…\nclaude plugin uninstall ${plugin.id} --scope ${row.scope}`,
|
|
1295
|
-
);
|
|
1296
|
-
await uninstallPluginViaCli(plugin.id, row.scope, row.projectPath);
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
problems.push(error);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
1287
|
|
|
1302
1288
|
modal.hideModal();
|
|
1303
1289
|
fetchData();
|