magus-cli 7.7.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 +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- 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 -57
- 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/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-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 +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 +90 -146
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- 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 -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/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 +65 -54
- 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 +313 -221
- 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/user-path.ts +60 -0
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- 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
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
installedScopeVersions,
|
|
40
40
|
isBehind,
|
|
41
41
|
isInstalledAnywhere,
|
|
42
|
+
isInstalledInScope,
|
|
42
43
|
isRepairablyBehind,
|
|
43
44
|
} from "./plugin-manager.js";
|
|
44
45
|
import {
|
|
@@ -77,6 +78,17 @@ export type PluginUpdateAction =
|
|
|
77
78
|
* every row, whichever scope or project each belongs to.
|
|
78
79
|
*/
|
|
79
80
|
| "stale-load"
|
|
81
|
+
/**
|
|
82
|
+
* DEPRECATED: a marketplace we READ does not list it. Claude Code refuses to
|
|
83
|
+
* load such a plugin ("Plugin X not found in marketplace Y" in its debug log,
|
|
84
|
+
* measured on 2.1.284), so it is dead weight and removing it loses nothing.
|
|
85
|
+
*
|
|
86
|
+
* Applied by uninstalling at every scope in `scopes` and dropping it from the
|
|
87
|
+
* profile, so the next activation does not add it back. `scopes` is empty for
|
|
88
|
+
* a plugin the profile declares and nothing installed: the profile edit is
|
|
89
|
+
* then the whole of the work.
|
|
90
|
+
*/
|
|
91
|
+
| "remove"
|
|
80
92
|
/** The catalog check failed; we cannot claim either way. */
|
|
81
93
|
| "unknown";
|
|
82
94
|
|
|
@@ -89,8 +101,19 @@ export type PluginReason =
|
|
|
89
101
|
| "absent"
|
|
90
102
|
/** install: installed at a scope, but no version resolves at any installed scope. */
|
|
91
103
|
| "files-missing"
|
|
92
|
-
/**
|
|
93
|
-
| "
|
|
104
|
+
/** remove: a marketplace we READ does not list it — no longer published. */
|
|
105
|
+
| "deprecated"
|
|
106
|
+
/**
|
|
107
|
+
* unknown: its marketplace no longer lists it, but another marketplace we
|
|
108
|
+
* read lists the same name — it moved, and removing it would lose it.
|
|
109
|
+
*/
|
|
110
|
+
| "moved"
|
|
111
|
+
/**
|
|
112
|
+
* unknown: no longer published, but it is in the plan only because another
|
|
113
|
+
* plugin the profile turns on requires it. Removing it would not stick: the
|
|
114
|
+
* next resolve derives it again.
|
|
115
|
+
*/
|
|
116
|
+
| "still-required"
|
|
94
117
|
/** unknown: installed, unlisted, and its marketplace contributed no listed ids. */
|
|
95
118
|
| "marketplace-unread"
|
|
96
119
|
/** unknown: the catalog fetch failed ("latest" pins only). */
|
|
@@ -243,6 +266,11 @@ export interface CatalogIndex {
|
|
|
243
266
|
isListed(pluginId: string): boolean;
|
|
244
267
|
/** Some OTHER listed id shares this id's "@marketplace" suffix. False for ids without "@". */
|
|
245
268
|
marketplaceIsKnown(pluginId: string): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* A LISTED plugin with the same name in a DIFFERENT marketplace, or
|
|
271
|
+
* undefined. The first in catalog order when several do.
|
|
272
|
+
*/
|
|
273
|
+
listedElsewhere(pluginId: string): PluginInfo | undefined;
|
|
246
274
|
}
|
|
247
275
|
|
|
248
276
|
export function indexCatalog(
|
|
@@ -269,6 +297,16 @@ export function indexCatalog(
|
|
|
269
297
|
return !!info && !info.isOrphaned;
|
|
270
298
|
};
|
|
271
299
|
|
|
300
|
+
// Listed plugins by bare name, for "did it move?".
|
|
301
|
+
const listedByName = new Map<string, PluginInfo[]>();
|
|
302
|
+
for (const [id, info] of byId) {
|
|
303
|
+
if (info.isOrphaned) continue;
|
|
304
|
+
const name = bareName(id);
|
|
305
|
+
const same = listedByName.get(name);
|
|
306
|
+
if (same) same.push(info);
|
|
307
|
+
else listedByName.set(name, [info]);
|
|
308
|
+
}
|
|
309
|
+
|
|
272
310
|
return {
|
|
273
311
|
get: (id) => byId.get(id),
|
|
274
312
|
isListed,
|
|
@@ -279,23 +317,223 @@ export function indexCatalog(
|
|
|
279
317
|
(listedPerSuffix.get(suffix) ?? 0) - (isListed(id) ? 1 : 0);
|
|
280
318
|
return others > 0;
|
|
281
319
|
},
|
|
320
|
+
listedElsewhere: (id) => {
|
|
321
|
+
const suffix = marketplaceSuffix(id);
|
|
322
|
+
if (!suffix) return undefined;
|
|
323
|
+
return listedByName
|
|
324
|
+
.get(bareName(id))
|
|
325
|
+
?.find((info) => marketplaceSuffix(info.id) !== suffix);
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** An id's name part: `kanban@magus` → `kanban`. */
|
|
331
|
+
function bareName(id: string): string {
|
|
332
|
+
const at = id.indexOf("@");
|
|
333
|
+
return at > 0 ? id.slice(0, at) : id;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const ALL_SCOPES: readonly PluginScope[] = ["user", "project", "local"];
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Every scope the plugin is installed at here — files missing included, since
|
|
340
|
+
* an install with no version is still an install Claude Code tries to load.
|
|
341
|
+
*/
|
|
342
|
+
function installedScopes(info: PluginInfo | undefined): PluginScope[] {
|
|
343
|
+
const byScope: Record<PluginScope, PluginInfo["userScope"]> = {
|
|
344
|
+
user: info?.userScope,
|
|
345
|
+
project: info?.projectScope,
|
|
346
|
+
local: info?.localScope,
|
|
347
|
+
};
|
|
348
|
+
return ALL_SCOPES.filter((scope) => isInstalledInScope(byScope[scope]));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The fields every classification carries, whatever its action. */
|
|
352
|
+
type ItemBase = Omit<
|
|
353
|
+
PluginUpdateItem,
|
|
354
|
+
"target" | "action" | "reason" | "scopes" | "note"
|
|
355
|
+
>;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Plugin id → the plugin(s) whose plugin.json requires it — `DependencyReport
|
|
359
|
+
* .requiredBy` for the profile's plugins, merged with the same edges read from
|
|
360
|
+
* every installed plugin (`requirersHere`). Every edge counts, a plugin the
|
|
361
|
+
* profile also names included.
|
|
362
|
+
*
|
|
363
|
+
* A single id is one requirer; a list is every requirer known. The list is
|
|
364
|
+
* what lets {@link planPluginUpdates} remove a deprecated plugin whose only
|
|
365
|
+
* requirers are being removed too, and keep one that anything else needs.
|
|
366
|
+
*/
|
|
367
|
+
export type RequiredBy = Readonly<Record<string, string | readonly string[]>>;
|
|
368
|
+
|
|
369
|
+
/** Every plugin `requiredBy` records as requiring `pluginId`, first first. */
|
|
370
|
+
export function requirersOf(
|
|
371
|
+
requiredBy: RequiredBy,
|
|
372
|
+
pluginId: string,
|
|
373
|
+
): string[] {
|
|
374
|
+
if (!Object.hasOwn(requiredBy, pluginId)) return [];
|
|
375
|
+
const value = requiredBy[pluginId];
|
|
376
|
+
if (value === undefined) return [];
|
|
377
|
+
return typeof value === "string" ? [value] : [...value];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Kept, not removed, although its marketplace no longer lists it: it MOVED to
|
|
382
|
+
* another marketplace, or another plugin STILL REQUIRES it. Both are facts,
|
|
383
|
+
* not a failed check — the CLI lists them under Deprecated as "kept" and never
|
|
384
|
+
* counts them as "could not be checked"; the TUI shows the note in a muted
|
|
385
|
+
* tone, not the amber "?" of a real unknown.
|
|
386
|
+
*/
|
|
387
|
+
export function isKeptDeprecated(item: PluginUpdateItem): boolean {
|
|
388
|
+
return (
|
|
389
|
+
item.action === "unknown" &&
|
|
390
|
+
(item.reason === "moved" || item.reason === "still-required")
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* No longer published: a marketplace we read does not list it. Removed at
|
|
396
|
+
* every scope it is installed at here, and from the profile — unless removing
|
|
397
|
+
* it would lose something or would not stick, which are both `unknown` and
|
|
398
|
+
* never applied:
|
|
399
|
+
*
|
|
400
|
+
* - MOVED: another marketplace we read lists the same name. The plugin still
|
|
401
|
+
* exists; uninstalling this copy is not the user's intent, and installing
|
|
402
|
+
* the other one is not magus's guess to make.
|
|
403
|
+
* - STILL REQUIRED: another plugin — one the profile turns on, or one
|
|
404
|
+
* installed here — requires this one. Removing it breaks that plugin, and
|
|
405
|
+
* when the profile never named it the next resolve derives it again, so
|
|
406
|
+
* `--check` would never clear. That holds whether or not the profile also
|
|
407
|
+
* names it.
|
|
408
|
+
*
|
|
409
|
+
* The notes are in the user's terms because they are the TUI's row text
|
|
410
|
+
* verbatim. Which marketplace stopped publishing it is the detail panel's to
|
|
411
|
+
* say, from the plugin's own display name.
|
|
412
|
+
*/
|
|
413
|
+
function notPublished(
|
|
414
|
+
base: ItemBase,
|
|
415
|
+
info: PluginInfo | undefined,
|
|
416
|
+
index: CatalogIndex,
|
|
417
|
+
requiredBy: RequiredBy,
|
|
418
|
+
): PluginUpdateItem {
|
|
419
|
+
const unknown = (reason: PluginReason, note: string): PluginUpdateItem => ({
|
|
420
|
+
...base,
|
|
421
|
+
target: null,
|
|
422
|
+
action: "unknown",
|
|
423
|
+
reason,
|
|
424
|
+
scopes: [],
|
|
425
|
+
note,
|
|
426
|
+
});
|
|
427
|
+
const elsewhere = index.listedElsewhere(base.pluginId);
|
|
428
|
+
if (elsewhere) {
|
|
429
|
+
return unknown(
|
|
430
|
+
"moved",
|
|
431
|
+
`moved to ${elsewhere.marketplaceDisplay || elsewhere.marketplace}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const [requirer] = requirersOf(requiredBy, base.pluginId);
|
|
435
|
+
if (requirer) {
|
|
436
|
+
return unknown("still-required", stillRequiredNote(requirer));
|
|
437
|
+
}
|
|
438
|
+
return deprecatedRemove(base, info);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** The kept row's words: which plugin still needs it. */
|
|
442
|
+
function stillRequiredNote(requirer: string): string {
|
|
443
|
+
return `no longer published, but ${bareName(requirer)} still requires it`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** A deprecated plugin, removed at every scope it is installed at here. */
|
|
447
|
+
function deprecatedRemove(
|
|
448
|
+
base: ItemBase,
|
|
449
|
+
info: PluginInfo | undefined,
|
|
450
|
+
): PluginUpdateItem {
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
target: null,
|
|
454
|
+
action: "remove",
|
|
455
|
+
reason: "deprecated",
|
|
456
|
+
scopes: installedScopes(info),
|
|
457
|
+
note: "no longer published",
|
|
282
458
|
};
|
|
283
459
|
}
|
|
284
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Deprecated plugins that require EACH OTHER are removed together.
|
|
463
|
+
*
|
|
464
|
+
* {@link classifyPlugin} judges one plugin at a time, so any recorded
|
|
465
|
+
* requirer keeps a deprecated plugin. That is right when the requirer stays
|
|
466
|
+
* and wrong when it is being removed too: a chain A → B of retired plugins
|
|
467
|
+
* removed A and kept B "because A still requires it", and a cycle A ↔ B kept
|
|
468
|
+
* both forever. Either way `magus update --check` right after `magus update`
|
|
469
|
+
* still reported work.
|
|
470
|
+
*
|
|
471
|
+
* So the removal set is settled as a whole. Start from every no-longer-
|
|
472
|
+
* published plugin that was not moved, and take out each one something OUTSIDE
|
|
473
|
+
* the set requires, until nothing changes. What is left — nobody who stays
|
|
474
|
+
* needs any of it — is removed in one pass. What was taken out stays, and its
|
|
475
|
+
* note names a plugin that genuinely stays and needs it.
|
|
476
|
+
*
|
|
477
|
+
* `moved` is never in the set: the plugin still exists elsewhere, and a
|
|
478
|
+
* plugin that requires it is a reason to keep, not to remove.
|
|
479
|
+
*/
|
|
480
|
+
function settleDeprecated(
|
|
481
|
+
items: readonly PluginUpdateItem[],
|
|
482
|
+
index: CatalogIndex,
|
|
483
|
+
requiredBy: RequiredBy,
|
|
484
|
+
): PluginUpdateItem[] {
|
|
485
|
+
const removable = new Set(
|
|
486
|
+
items
|
|
487
|
+
.filter((i) => i.reason === "deprecated" || i.reason === "still-required")
|
|
488
|
+
.map((i) => i.pluginId),
|
|
489
|
+
);
|
|
490
|
+
let changed = true;
|
|
491
|
+
while (changed) {
|
|
492
|
+
changed = false;
|
|
493
|
+
for (const id of removable) {
|
|
494
|
+
if (requirersOf(requiredBy, id).some((r) => !removable.has(r))) {
|
|
495
|
+
removable.delete(id);
|
|
496
|
+
changed = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return items.map((item) => {
|
|
501
|
+
if (item.reason !== "still-required") return item;
|
|
502
|
+
const {
|
|
503
|
+
target: _t,
|
|
504
|
+
action: _a,
|
|
505
|
+
reason: _r,
|
|
506
|
+
scopes: _s,
|
|
507
|
+
note: _n,
|
|
508
|
+
...base
|
|
509
|
+
} = item;
|
|
510
|
+
if (removable.has(item.pluginId)) {
|
|
511
|
+
return deprecatedRemove(base, index.get(item.pluginId));
|
|
512
|
+
}
|
|
513
|
+
const keeper = requirersOf(requiredBy, item.pluginId).find(
|
|
514
|
+
(r) => !removable.has(r),
|
|
515
|
+
);
|
|
516
|
+
return keeper ? { ...item, note: stillRequiredNote(keeper) } : item;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
285
520
|
/**
|
|
286
521
|
* THE plugin classifier. Pure.
|
|
287
522
|
*
|
|
288
523
|
* The rules are evaluated in order and the first match wins. Each branch below
|
|
289
524
|
* returns, so reading top to bottom is reading the precedence.
|
|
290
525
|
*
|
|
291
|
-
* @param pluginId
|
|
292
|
-
* @param pin
|
|
293
|
-
* @param index
|
|
526
|
+
* @param pluginId the plugin
|
|
527
|
+
* @param pin what it is judged against: an exact version, or "latest"
|
|
528
|
+
* @param index the catalog — see {@link indexCatalog}
|
|
529
|
+
* @param requiredBy the plugins the profile has only as dependencies — see
|
|
530
|
+
* {@link RequiredBy}
|
|
294
531
|
*/
|
|
295
532
|
export function classifyPlugin(
|
|
296
533
|
pluginId: string,
|
|
297
534
|
pin: string,
|
|
298
535
|
index: CatalogIndex,
|
|
536
|
+
requiredBy: RequiredBy = {},
|
|
299
537
|
): PluginUpdateItem {
|
|
300
538
|
const info = index.get(pluginId);
|
|
301
539
|
const listed = index.isListed(pluginId);
|
|
@@ -344,15 +582,13 @@ export function classifyPlugin(
|
|
|
344
582
|
// the marketplace. `marketplaceIsKnown` separates the two: holding other
|
|
345
583
|
// LISTED ids from that marketplace proves we genuinely read its catalog, so
|
|
346
584
|
// absence is an answer; holding none proves nothing.
|
|
585
|
+
//
|
|
586
|
+
// Such a pin is DEPRECATED, and removing it from the profile is the work:
|
|
587
|
+
// Claude Code will not load the plugin, so the pin only ever asks for an
|
|
588
|
+
// install that cannot happen. A plugin installed with its files missing lands
|
|
589
|
+
// here too (no installed scope has a version), and its scopes are uninstalled.
|
|
347
590
|
if (!listed && !installed && index.marketplaceIsKnown(pluginId)) {
|
|
348
|
-
return
|
|
349
|
-
...base,
|
|
350
|
-
target: null,
|
|
351
|
-
action: "unknown",
|
|
352
|
-
reason: "not-offered",
|
|
353
|
-
scopes: [],
|
|
354
|
-
note: "not offered by any configured marketplace",
|
|
355
|
-
};
|
|
591
|
+
return notPublished(base, info, index, requiredBy);
|
|
356
592
|
}
|
|
357
593
|
|
|
358
594
|
// No installed scope has a version — the "checked but not installed" case.
|
|
@@ -390,25 +626,22 @@ export function classifyPlugin(
|
|
|
390
626
|
// text: it moved to a marketplace that does publish it; a marketplace we
|
|
391
627
|
// did read no longer lists it; or its marketplace contributed nothing we
|
|
392
628
|
// could read, so its absence proves nothing either way.
|
|
629
|
+
//
|
|
630
|
+
// A marketplace we read that does not list it is a fact, not a guess: the
|
|
631
|
+
// plugin is DEPRECATED and is removed. A marketplace that contributed nothing
|
|
632
|
+
// readable proves nothing, so that case stays `unknown` and nothing is done.
|
|
393
633
|
if (!listed) {
|
|
394
|
-
const orphan = {
|
|
395
|
-
...base,
|
|
396
|
-
target: null,
|
|
397
|
-
action: "unknown" as const,
|
|
398
|
-
scopes: [],
|
|
399
|
-
};
|
|
400
634
|
if (index.marketplaceIsKnown(pluginId)) {
|
|
401
|
-
return
|
|
402
|
-
...orphan,
|
|
403
|
-
reason: "not-offered",
|
|
404
|
-
note: "not offered by any configured marketplace",
|
|
405
|
-
};
|
|
635
|
+
return notPublished(base, info, index, requiredBy);
|
|
406
636
|
}
|
|
407
637
|
const mp =
|
|
408
638
|
info?.marketplace || marketplaceSuffix(pluginId)?.slice(1) || pluginId;
|
|
409
639
|
return {
|
|
410
|
-
...
|
|
640
|
+
...base,
|
|
641
|
+
target: null,
|
|
642
|
+
action: "unknown",
|
|
411
643
|
reason: "marketplace-unread",
|
|
644
|
+
scopes: [],
|
|
412
645
|
note: `its marketplace ${mp} was not read, so whether it still offers this plugin is unknown`,
|
|
413
646
|
};
|
|
414
647
|
}
|
|
@@ -654,12 +887,15 @@ export function classifyPlugin(
|
|
|
654
887
|
* `magus update`, which then reported everything up to date. The owner asked
|
|
655
888
|
* for "absolutely the same code" on both sides; this is it.
|
|
656
889
|
*
|
|
657
|
-
* @param pinned
|
|
658
|
-
* @param catalog
|
|
890
|
+
* @param pinned closure.plugins — pluginId -> exact version | "latest"
|
|
891
|
+
* @param catalog getAvailablePlugins() output — keyed by id, or as a list
|
|
892
|
+
* @param requiredBy closure.dependencies?.requiredBy — which plugin needs each
|
|
893
|
+
* dependency the profile does not name itself
|
|
659
894
|
*/
|
|
660
895
|
export function planPluginUpdates(
|
|
661
896
|
pinned: Readonly<Record<string, string>>,
|
|
662
897
|
catalog: ReadonlyMap<string, PluginInfo> | readonly PluginInfo[],
|
|
898
|
+
requiredBy: RequiredBy = {},
|
|
663
899
|
): PluginUpdateItem[] {
|
|
664
900
|
const index = indexCatalog(catalog);
|
|
665
901
|
const plugins =
|
|
@@ -672,9 +908,13 @@ export function planPluginUpdates(
|
|
|
672
908
|
const pin = pinForRow(info, pinned);
|
|
673
909
|
if (pin !== null) pins.set(info.id, pin);
|
|
674
910
|
}
|
|
675
|
-
return
|
|
676
|
-
.map(([pluginId, pin]) =>
|
|
677
|
-
|
|
911
|
+
return settleDeprecated(
|
|
912
|
+
[...pins].map(([pluginId, pin]) =>
|
|
913
|
+
classifyPlugin(pluginId, pin, index, requiredBy),
|
|
914
|
+
),
|
|
915
|
+
index,
|
|
916
|
+
requiredBy,
|
|
917
|
+
).sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
678
918
|
}
|
|
679
919
|
|
|
680
920
|
/**
|
|
@@ -710,8 +950,11 @@ export function pinForRow(
|
|
|
710
950
|
export function classifyPluginRows(
|
|
711
951
|
plugins: readonly PluginInfo[],
|
|
712
952
|
pins: Readonly<Record<string, string>>,
|
|
953
|
+
requiredBy: RequiredBy = {},
|
|
713
954
|
): Map<string, PluginUpdateItem> {
|
|
714
|
-
return new Map(
|
|
955
|
+
return new Map(
|
|
956
|
+
planPluginUpdates(pins, plugins, requiredBy).map((i) => [i.pluginId, i]),
|
|
957
|
+
);
|
|
715
958
|
}
|
|
716
959
|
|
|
717
960
|
export interface PluginStateCounts {
|
|
@@ -725,6 +968,8 @@ export interface PluginStateCounts {
|
|
|
725
968
|
* header would report a clean bill of health for a session running old code.
|
|
726
969
|
*/
|
|
727
970
|
"stale-load": number;
|
|
971
|
+
/** Deprecated — no longer published, and removed by an apply. */
|
|
972
|
+
remove: number;
|
|
728
973
|
unknown: number;
|
|
729
974
|
}
|
|
730
975
|
|
|
@@ -738,6 +983,7 @@ export function countPluginStates(
|
|
|
738
983
|
repair: 0,
|
|
739
984
|
current: 0,
|
|
740
985
|
"stale-load": 0,
|
|
986
|
+
remove: 0,
|
|
741
987
|
unknown: 0,
|
|
742
988
|
};
|
|
743
989
|
for (const item of items) counts[item.action]++;
|
|
@@ -953,6 +1199,11 @@ export function planHasWork(plan: UpdatePlan): boolean {
|
|
|
953
1199
|
* (`summarizePlan`'s `unknownPlugins`/`unknownTools`, rendered by
|
|
954
1200
|
* `cli/update-view.ts`), and every unknown row carries its own reason above.
|
|
955
1201
|
*
|
|
1202
|
+
* A `remove` DOES count. It used to be an `unknown` ("not offered"), and the
|
|
1203
|
+
* argument above kept it out while nothing could act on it. `magus update` now
|
|
1204
|
+
* removes it, so the gate closes — and a deprecated plugin is not a guess: its
|
|
1205
|
+
* marketplace was read and does not list it, and Claude Code refuses to load it.
|
|
1206
|
+
*
|
|
956
1207
|
* Both unknowns are listed positively rather than as `!== "current"`, so a new
|
|
957
1208
|
* action has to be classified here deliberately instead of defaulting into the
|
|
958
1209
|
* gate.
|
|
@@ -964,7 +1215,8 @@ export function planIsBehind(plan: UpdatePlan): boolean {
|
|
|
964
1215
|
p.action === "install" ||
|
|
965
1216
|
p.action === "update" ||
|
|
966
1217
|
p.action === "repair" ||
|
|
967
|
-
p.action === "stale-load"
|
|
1218
|
+
p.action === "stale-load" ||
|
|
1219
|
+
p.action === "remove",
|
|
968
1220
|
) ||
|
|
969
1221
|
plan.bins.some((b) => b.action === "install" || b.action === "upgrade") ||
|
|
970
1222
|
plan.skills.some((s) => s.action === "install")
|
|
@@ -979,14 +1231,21 @@ export function summarizePlan(plan: UpdatePlan): {
|
|
|
979
1231
|
current: number;
|
|
980
1232
|
/** unknownPlugins + unknownTools. */
|
|
981
1233
|
unknown: number;
|
|
1234
|
+
/** Plugins that could not be checked — never a {@link isKeptDeprecated} one. */
|
|
982
1235
|
unknownPlugins: number;
|
|
983
1236
|
unknownTools: number;
|
|
1237
|
+
/**
|
|
1238
|
+
* No longer published by their marketplace, and kept on purpose: moved, or
|
|
1239
|
+
* still required. A determined fact, so not part of `unknown`.
|
|
1240
|
+
*/
|
|
1241
|
+
kept: number;
|
|
984
1242
|
} {
|
|
985
1243
|
const count = (a: PluginUpdateAction) =>
|
|
986
1244
|
plan.plugins.filter((p) => p.action === a).length;
|
|
987
1245
|
const bins = (a: BinUpdateAction) =>
|
|
988
1246
|
plan.bins.filter((b) => b.action === a).length;
|
|
989
|
-
const
|
|
1247
|
+
const kept = plan.plugins.filter(isKeptDeprecated).length;
|
|
1248
|
+
const unknownPlugins = count("unknown") - kept;
|
|
990
1249
|
const unknownTools = bins("unknown");
|
|
991
1250
|
return {
|
|
992
1251
|
installed: count("install") + bins("install"),
|
|
@@ -996,5 +1255,6 @@ export function summarizePlan(plan: UpdatePlan): {
|
|
|
996
1255
|
unknown: unknownPlugins + unknownTools,
|
|
997
1256
|
unknownPlugins,
|
|
998
1257
|
unknownTools,
|
|
1258
|
+
kept,
|
|
999
1259
|
};
|
|
1000
1260
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -486,6 +486,13 @@ export interface ResolvedClosure {
|
|
|
486
486
|
export interface DependencyReport {
|
|
487
487
|
/** Enabled only because an enabled plugin depends on them. Sorted. */
|
|
488
488
|
derived: string[];
|
|
489
|
+
/**
|
|
490
|
+
* For every dependency an enabled plugin declares — derived ones and ones
|
|
491
|
+
* the profile also names — the plugin whose plugin.json first asked for it,
|
|
492
|
+
* so a report can say which plugin still needs it. Optional because a
|
|
493
|
+
* hand-built report may carry no provenance.
|
|
494
|
+
*/
|
|
495
|
+
requiredBy?: Record<string, string>;
|
|
489
496
|
/** Enabled plugins whose plugin.json could not be read here. Sorted. */
|
|
490
497
|
unreadable: string[];
|
|
491
498
|
/** A dependency the profile itself turns off: `plugin` will not load. */
|