claudeup 6.5.0 → 6.6.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__/active-closure.test.ts +184 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/catalog-notice.test.ts +3 -3
- package/src/__tests__/cli-apply-seams.test.ts +2 -0
- package/src/__tests__/cli-tool-commands.test.ts +1 -1
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +142 -2
- package/src/__tests__/content-drift.test.ts +60 -0
- package/src/__tests__/enabled-not-installed.test.ts +1 -2
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
- package/src/__tests__/marketplace-badge.test.ts +1 -1
- package/src/__tests__/marketplaces.test.ts +1 -0
- package/src/__tests__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/scope-action.test.ts +69 -19
- package/src/__tests__/shell-script-callers.test.ts +8 -8
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/update-apply.test.ts +283 -1
- package/src/__tests__/update-plan.test.ts +773 -31
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/cli/router.ts +0 -5
- package/src/cli/update-view.ts +92 -12
- package/src/cli/update.ts +114 -38
- package/src/data/marketplaces.ts +13 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +1 -173
- package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
- package/src/services/gitignore-fixer.ts +1 -1
- package/src/services/marketplace-refresh.ts +30 -0
- package/src/services/plugin-manager.ts +200 -283
- package/src/services/resolver.ts +7 -2
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/update-engine.ts +220 -32
- package/src/services/update-plan.ts +640 -160
- package/src/services/version-snapshot.ts +1 -1
- package/src/types/bun.d.ts +6 -0
- package/src/ui/App.tsx +2 -27
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/pluginsAdapter.ts +58 -1
- package/src/ui/components/modals/ModalContainer.tsx +0 -26
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +2 -3
- package/src/ui/renderers/cliToolRenderers.tsx +108 -67
- package/src/ui/renderers/pluginRenderers.tsx +157 -98
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/screens/CliToolsScreen.tsx +154 -280
- package/src/ui/screens/PluginsScreen.tsx +156 -140
- package/src/ui/state/types.ts +0 -7
- package/src/__tests__/dual-write-prevention.test.ts +0 -380
- package/src/__tests__/gap-fill-versions.test.ts +0 -389
- package/src/__tests__/plugin-version-check.test.ts +0 -762
- package/src/cli/claude.ts +0 -21
- package/src/prerunner/index.ts +0 -500
- package/src/services/plugin-version-check.ts +0 -360
- package/src/services/update-cache.ts +0 -78
- package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
- package/src/ui/hooks/useMismatchModal.ts +0 -89
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
defaultMarketplaces,
|
|
5
5
|
getAllMarketplaces,
|
|
6
6
|
} from "../../data/marketplaces.js";
|
|
7
|
+
import { loadActiveClosure } from "../../services/active-closure.js";
|
|
7
8
|
import {
|
|
8
9
|
type PluginScope,
|
|
9
10
|
addMarketplace as cliAddMarketplace,
|
|
@@ -37,11 +38,6 @@ import {
|
|
|
37
38
|
getPluginSetupFromSource,
|
|
38
39
|
installPluginDeps,
|
|
39
40
|
} from "../../services/plugin-setup.js";
|
|
40
|
-
import {
|
|
41
|
-
type VersionMismatchInfo,
|
|
42
|
-
checkPluginVersionMismatches,
|
|
43
|
-
checkSinglePluginMismatch,
|
|
44
|
-
} from "../../services/plugin-version-check.js";
|
|
45
41
|
import { saveProfile } from "../../services/profiles.js";
|
|
46
42
|
import {
|
|
47
43
|
fetchPluginContents,
|
|
@@ -57,7 +53,15 @@ import {
|
|
|
57
53
|
pluginNeedsWork,
|
|
58
54
|
updatePluginInScope,
|
|
59
55
|
} from "../../services/update-engine.js";
|
|
60
|
-
|
|
56
|
+
// The ONE plugin classifier, shared with `claudeup update`. Every state this
|
|
57
|
+
// screen shows or acts on — row text, detail panel, header counts, U / a, the
|
|
58
|
+
// scope picker — reads the same map it builds.
|
|
59
|
+
import {
|
|
60
|
+
type PluginUpdateItem,
|
|
61
|
+
classifyPluginRows,
|
|
62
|
+
countPluginStates,
|
|
63
|
+
scopeIsBehind,
|
|
64
|
+
} from "../../services/update-plan.js";
|
|
61
65
|
import {
|
|
62
66
|
diffVersions,
|
|
63
67
|
loadSeenVersions,
|
|
@@ -70,13 +74,13 @@ import {
|
|
|
70
74
|
type PluginComponentItem,
|
|
71
75
|
type PluginPluginItem,
|
|
72
76
|
buildPluginBrowserItems,
|
|
77
|
+
noMoveReasons,
|
|
73
78
|
pluginContentsRepo,
|
|
74
79
|
} from "../adapters/pluginsAdapter.js";
|
|
75
80
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
76
81
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
77
82
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
78
83
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
79
|
-
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
80
84
|
import {
|
|
81
85
|
renderPluginDetail,
|
|
82
86
|
renderPluginRow,
|
|
@@ -101,7 +105,6 @@ export function PluginsScreen() {
|
|
|
101
105
|
const modal = useModal();
|
|
102
106
|
const progress = useProgress();
|
|
103
107
|
const dimensions = useDimensions();
|
|
104
|
-
const mismatchModal = useMismatchModal();
|
|
105
108
|
|
|
106
109
|
const isSearchActive =
|
|
107
110
|
state.isSearching &&
|
|
@@ -122,13 +125,32 @@ export function PluginsScreen() {
|
|
|
122
125
|
/** Re-render tick so the retry countdown in the notice actually counts down. */
|
|
123
126
|
const [nowTick, setNowTick] = useState(() => Date.now());
|
|
124
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The active profile's pins — the same values `claudeup update` plans with.
|
|
130
|
+
*
|
|
131
|
+
* `undefined` until the first fetch answers; `null` when no profile could be
|
|
132
|
+
* resolved (no manifest, an invalid one, or several with none active). In
|
|
133
|
+
* that case every row is judged against "latest" and the notice says so.
|
|
134
|
+
*/
|
|
135
|
+
const [pins, setPins] = useState<
|
|
136
|
+
Readonly<Record<string, string>> | null | undefined
|
|
137
|
+
>(undefined);
|
|
138
|
+
|
|
125
139
|
// Fetch data (always fetches all scopes)
|
|
126
140
|
const fetchData = useCallback(async () => {
|
|
127
141
|
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
128
142
|
try {
|
|
129
143
|
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
130
144
|
const allMarketplaces = getAllMarketplaces(localMarketplaces);
|
|
131
|
-
|
|
145
|
+
// Read-only, never throws. `resolveBins: false`: this screen needs only
|
|
146
|
+
// the plugin pins, not every declared plugin's binary requirements.
|
|
147
|
+
const [pluginData, active] = await Promise.all([
|
|
148
|
+
getAvailablePlugins(state.projectPath),
|
|
149
|
+
loadActiveClosure(state.projectPath || process.cwd(), {
|
|
150
|
+
resolveBins: false,
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
setPins(active?.closure.plugins ?? null);
|
|
132
154
|
|
|
133
155
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
134
156
|
// after the fetch, while the record still describes this attempt.
|
|
@@ -147,7 +169,7 @@ export function PluginsScreen() {
|
|
|
147
169
|
);
|
|
148
170
|
|
|
149
171
|
// Surface version changes made outside claudeup (Claude Code's own
|
|
150
|
-
// plugin update,
|
|
172
|
+
// plugin update, a manual CLI call). Diffing against
|
|
151
173
|
// what we last rendered catches all of them; logging only what
|
|
152
174
|
// claudeup did would miss every one.
|
|
153
175
|
const current: Record<string, string> = {};
|
|
@@ -234,6 +256,18 @@ export function PluginsScreen() {
|
|
|
234
256
|
const isReloading =
|
|
235
257
|
pluginsState.plugins.status === "loading" && shownPlugins !== undefined;
|
|
236
258
|
|
|
259
|
+
/**
|
|
260
|
+
* One status per plugin that has a pin, computed ONCE per data/pins change.
|
|
261
|
+
* The rows, the detail panel, the header counts, U / a and the scope picker
|
|
262
|
+
* all read this map, so none of them can disagree with another — or with
|
|
263
|
+
* `claudeup update`, which runs the same classifier over the same pins.
|
|
264
|
+
*/
|
|
265
|
+
const statuses = useMemo(
|
|
266
|
+
(): Map<string, PluginUpdateItem> =>
|
|
267
|
+
shownPlugins ? classifyPluginRows(shownPlugins, pins ?? {}) : new Map(),
|
|
268
|
+
[shownPlugins, pins],
|
|
269
|
+
);
|
|
270
|
+
|
|
237
271
|
// Build list items (categories + plugins)
|
|
238
272
|
const allItems = useMemo((): PluginBrowserItem[] => {
|
|
239
273
|
if (!shownMarketplaces || !shownPlugins) return [];
|
|
@@ -243,6 +277,7 @@ export function PluginsScreen() {
|
|
|
243
277
|
collapsedMarketplaces: pluginsState.collapsedMarketplaces,
|
|
244
278
|
expandedPlugins: pluginsState.expandedPlugins,
|
|
245
279
|
pluginContents: pluginsState.pluginContents,
|
|
280
|
+
statuses,
|
|
246
281
|
});
|
|
247
282
|
}, [
|
|
248
283
|
shownMarketplaces,
|
|
@@ -250,6 +285,7 @@ export function PluginsScreen() {
|
|
|
250
285
|
pluginsState.collapsedMarketplaces,
|
|
251
286
|
pluginsState.expandedPlugins,
|
|
252
287
|
pluginsState.pluginContents,
|
|
288
|
+
statuses,
|
|
253
289
|
]);
|
|
254
290
|
|
|
255
291
|
// Filter items by search query
|
|
@@ -517,32 +553,11 @@ export function PluginsScreen() {
|
|
|
517
553
|
else if (event.name === "l") handleScopeToggle("local");
|
|
518
554
|
else if (event.name === "d") handleRemoveDeprecated();
|
|
519
555
|
else if (event.name === "a") handleUpdateAll();
|
|
520
|
-
else if (event.name === "m") handleCheckMismatches();
|
|
521
556
|
else if (event.name === "s") handleSaveAsProfile();
|
|
522
557
|
});
|
|
523
558
|
|
|
524
559
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
525
560
|
|
|
526
|
-
/**
|
|
527
|
-
* Check for version mismatch after a plugin update and show the
|
|
528
|
-
* interactive mismatch modal with a "Fix all projects" button.
|
|
529
|
-
*
|
|
530
|
-
* The [0] index bug in Claude Code means the update may not actually
|
|
531
|
-
* take effect if another project's entry is at index 0. The shared
|
|
532
|
-
* useMismatchModal hook provides the same fix flow available at startup.
|
|
533
|
-
*/
|
|
534
|
-
const warnIfVersionMismatch = async (pluginId: string): Promise<void> => {
|
|
535
|
-
try {
|
|
536
|
-
const projectPath = state.projectPath || process.cwd();
|
|
537
|
-
const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
538
|
-
if (mismatch) {
|
|
539
|
-
mismatchModal.show([mismatch]);
|
|
540
|
-
}
|
|
541
|
-
} catch {
|
|
542
|
-
// Non-fatal: mismatch check is best-effort
|
|
543
|
-
}
|
|
544
|
-
};
|
|
545
|
-
|
|
546
561
|
// ── Action handlers ────────────────────────────────────────────────────────
|
|
547
562
|
|
|
548
563
|
const handleRefresh = async () => {
|
|
@@ -851,24 +866,24 @@ export function PluginsScreen() {
|
|
|
851
866
|
}
|
|
852
867
|
} else if (item.kind === "plugin") {
|
|
853
868
|
const plugin = item.plugin;
|
|
854
|
-
const
|
|
869
|
+
const status = statuses.get(plugin.id);
|
|
855
870
|
|
|
871
|
+
// The label's arrow and what the key then does are ONE rule —
|
|
872
|
+
// `scopeIsBehind` — so the picker can never show no arrow and then
|
|
873
|
+
// update, or show an arrow and then uninstall.
|
|
856
874
|
const buildScopeLabel = (
|
|
857
875
|
name: string,
|
|
876
|
+
scopeKey: PluginScope,
|
|
858
877
|
scope: ScopeStatus | undefined,
|
|
859
878
|
desc: string,
|
|
860
879
|
) => {
|
|
861
880
|
const installed = isInstalledInScope(scope);
|
|
862
881
|
const ver = scope?.version;
|
|
863
|
-
const hasUpdate =
|
|
864
|
-
ver &&
|
|
865
|
-
latestVersion &&
|
|
866
|
-
ver !== latestVersion &&
|
|
867
|
-
latestVersion !== "0.0.0";
|
|
868
882
|
let label = installed ? `● ${name}` : `○ ${name}`;
|
|
869
883
|
label += ` (${desc})`;
|
|
870
884
|
if (ver) label += ` v${ver}`;
|
|
871
|
-
if (
|
|
885
|
+
if (scopeIsBehind(status, scopeKey) && status?.target)
|
|
886
|
+
label += ` → v${status.target}`;
|
|
872
887
|
// Enabled in settings with nothing installed. Without saying so the
|
|
873
888
|
// row reads exactly like a scope that was never enabled, and the
|
|
874
889
|
// user cannot tell that picking it repairs rather than adds.
|
|
@@ -878,15 +893,25 @@ export function PluginsScreen() {
|
|
|
878
893
|
|
|
879
894
|
const scopeOptions = [
|
|
880
895
|
{
|
|
881
|
-
label: buildScopeLabel("User", plugin.userScope, "global"),
|
|
896
|
+
label: buildScopeLabel("User", "user", plugin.userScope, "global"),
|
|
882
897
|
value: "user",
|
|
883
898
|
},
|
|
884
899
|
{
|
|
885
|
-
label: buildScopeLabel(
|
|
900
|
+
label: buildScopeLabel(
|
|
901
|
+
"Project",
|
|
902
|
+
"project",
|
|
903
|
+
plugin.projectScope,
|
|
904
|
+
"team",
|
|
905
|
+
),
|
|
886
906
|
value: "project",
|
|
887
907
|
},
|
|
888
908
|
{
|
|
889
|
-
label: buildScopeLabel(
|
|
909
|
+
label: buildScopeLabel(
|
|
910
|
+
"Local",
|
|
911
|
+
"local",
|
|
912
|
+
plugin.localScope,
|
|
913
|
+
"private",
|
|
914
|
+
),
|
|
890
915
|
value: "local",
|
|
891
916
|
},
|
|
892
917
|
];
|
|
@@ -912,8 +937,12 @@ export function PluginsScreen() {
|
|
|
912
937
|
: "Local";
|
|
913
938
|
|
|
914
939
|
// Decided from the registry-backed version, not the `enabledPlugins`
|
|
915
|
-
// flag alone — see resolveScopeAction.
|
|
916
|
-
|
|
940
|
+
// flag alone — see resolveScopeAction. Update-versus-uninstall is the
|
|
941
|
+
// same `scopeIsBehind` the label above used.
|
|
942
|
+
const action = resolveScopeAction(
|
|
943
|
+
selectedScope,
|
|
944
|
+
scopeIsBehind(status, scopeValue as PluginScope),
|
|
945
|
+
);
|
|
917
946
|
|
|
918
947
|
try {
|
|
919
948
|
const scope = scopeValue as PluginScope;
|
|
@@ -928,7 +957,7 @@ export function PluginsScreen() {
|
|
|
928
957
|
);
|
|
929
958
|
// Scope is the user's explicit choice here, so this does NOT go
|
|
930
959
|
// through applyPlugins (which advances every scope behind). It
|
|
931
|
-
// still records the version READ BACK, not
|
|
960
|
+
// still records the version READ BACK, not the catalog version.
|
|
932
961
|
await updatePluginInScope(
|
|
933
962
|
plugin.id,
|
|
934
963
|
scope,
|
|
@@ -939,7 +968,7 @@ export function PluginsScreen() {
|
|
|
939
968
|
`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
940
969
|
);
|
|
941
970
|
// Same read-back as the update branch above. A fresh install is
|
|
942
|
-
// no more able to guarantee it got
|
|
971
|
+
// no more able to guarantee it got the catalog version than an
|
|
943
972
|
// update is — the marketplace serves whatever it publishes at
|
|
944
973
|
// the moment of the call — so recording the version we asked
|
|
945
974
|
// for made `install --check` compare that number against a copy
|
|
@@ -956,9 +985,6 @@ export function PluginsScreen() {
|
|
|
956
985
|
if (action !== "install") {
|
|
957
986
|
modal.hideModal();
|
|
958
987
|
}
|
|
959
|
-
if (action === "update") {
|
|
960
|
-
await warnIfVersionMismatch(plugin.id);
|
|
961
|
-
}
|
|
962
988
|
fetchData();
|
|
963
989
|
} catch (error) {
|
|
964
990
|
modal.hideModal();
|
|
@@ -970,7 +996,9 @@ export function PluginsScreen() {
|
|
|
970
996
|
const handleUpdate = async () => {
|
|
971
997
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
972
998
|
if (!item || item.kind !== "plugin") return;
|
|
973
|
-
|
|
999
|
+
// The row said "update" or "stale — reinstall"; nothing else is work.
|
|
1000
|
+
const status = statuses.get(item.plugin.id);
|
|
1001
|
+
if (status?.action !== "update" && status?.action !== "repair") return;
|
|
974
1002
|
|
|
975
1003
|
const plugin = item.plugin;
|
|
976
1004
|
const scope: PluginScope =
|
|
@@ -979,28 +1007,26 @@ export function PluginsScreen() {
|
|
|
979
1007
|
// No version bump to install over — the files changed underneath an
|
|
980
1008
|
// unchanged version, and `plugin install` no-ops on "already installed".
|
|
981
1009
|
// Only uninstall+install re-copies. See claude-cli.ts repairPlugin.
|
|
982
|
-
const repairing =
|
|
1010
|
+
const repairing = status.action === "repair";
|
|
983
1011
|
|
|
984
1012
|
modal.loading(
|
|
985
1013
|
repairing
|
|
986
1014
|
? `Repairing ${plugin.name}…\nclaude plugin uninstall ${plugin.id} --scope ${scope} && claude plugin install ${plugin.id} --scope ${scope}`
|
|
987
1015
|
: `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
988
1016
|
);
|
|
1017
|
+
const reasons = noMoveReasons();
|
|
989
1018
|
try {
|
|
990
1019
|
// One plugin, but the SAME engine — so a single update records the
|
|
991
1020
|
// version that landed and touches every scope that is behind, exactly
|
|
992
|
-
// as `claudeup update` does.
|
|
993
|
-
//
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
? pluginsState.plugins.data.map((p) => [p.id, p] as const)
|
|
997
|
-
: [[plugin.id, plugin] as const],
|
|
998
|
-
);
|
|
999
|
-
const items = planPluginUpdates({ [plugin.id]: "latest" }, catalog);
|
|
1021
|
+
// as `claudeup update` does. The plan item IS the row's status: the
|
|
1022
|
+
// classifier's answer for this plugin under its real pin (the
|
|
1023
|
+
// profile's, or "latest" for an undeclared install — see pinForRow),
|
|
1024
|
+
// so what runs is exactly what the row claimed.
|
|
1000
1025
|
const result = await applyPlugins(
|
|
1001
|
-
|
|
1026
|
+
[status],
|
|
1002
1027
|
state.projectPath || process.cwd(),
|
|
1003
1028
|
REAL_PLUGIN_DEPS,
|
|
1029
|
+
reasons.reporter,
|
|
1004
1030
|
);
|
|
1005
1031
|
modal.hideModal();
|
|
1006
1032
|
if (result.failed.length > 0) {
|
|
@@ -1011,7 +1037,16 @@ export function PluginsScreen() {
|
|
|
1011
1037
|
);
|
|
1012
1038
|
return;
|
|
1013
1039
|
}
|
|
1014
|
-
|
|
1040
|
+
// Ran, exited 0, moved nothing — and the row will not have moved
|
|
1041
|
+
// either. Saying nothing here is what made "press U, press U,
|
|
1042
|
+
// forever": the CLI names the cause, so the screen does too.
|
|
1043
|
+
if (result.unchanged.length > 0) {
|
|
1044
|
+
await modal.message(
|
|
1045
|
+
"No change",
|
|
1046
|
+
reasons.lines(result.unchanged),
|
|
1047
|
+
"info",
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1015
1050
|
fetchData();
|
|
1016
1051
|
} catch (error) {
|
|
1017
1052
|
modal.hideModal();
|
|
@@ -1034,29 +1069,26 @@ export function PluginsScreen() {
|
|
|
1034
1069
|
const handleUpdateAll = async () => {
|
|
1035
1070
|
if (pluginsState.plugins.status !== "success") return;
|
|
1036
1071
|
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
)
|
|
1042
|
-
|
|
1072
|
+
// Exactly the rows that say "update" or "stale — reinstall", each planned
|
|
1073
|
+
// under its real pin. Content-stale plugins are included: they need
|
|
1074
|
+
// repairing, and excluding them meant "update all" left the machine with
|
|
1075
|
+
// silently outdated files.
|
|
1076
|
+
const items = [...statuses.values()]
|
|
1077
|
+
.filter((s) => s.action === "update" || s.action === "repair")
|
|
1078
|
+
.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
1079
|
+
if (items.length === 0) return;
|
|
1043
1080
|
|
|
1044
1081
|
const projectPath = state.projectPath || process.cwd();
|
|
1045
|
-
// The screen's own list IS the catalog the planner needs.
|
|
1046
|
-
const catalog = new Map(pluginsState.plugins.data.map((p) => [p.id, p]));
|
|
1047
|
-
const items = planPluginUpdates(
|
|
1048
|
-
Object.fromEntries(updatable.map((p) => [p.id, "latest"])),
|
|
1049
|
-
catalog,
|
|
1050
|
-
);
|
|
1051
1082
|
const total = items.filter(pluginNeedsWork).length;
|
|
1052
1083
|
|
|
1053
1084
|
let done = 0;
|
|
1085
|
+
const reasons = noMoveReasons();
|
|
1054
1086
|
const reporter: ApplyReporter = {
|
|
1055
1087
|
begin: (name) => {
|
|
1056
1088
|
done++;
|
|
1057
1089
|
modal.loading(`Updating ${name} (${done}/${total})…`);
|
|
1058
1090
|
},
|
|
1059
|
-
finish:
|
|
1091
|
+
finish: reasons.reporter.finish,
|
|
1060
1092
|
detach: (work) => work(),
|
|
1061
1093
|
};
|
|
1062
1094
|
|
|
@@ -1067,10 +1099,6 @@ export function PluginsScreen() {
|
|
|
1067
1099
|
REAL_PLUGIN_DEPS,
|
|
1068
1100
|
reporter,
|
|
1069
1101
|
);
|
|
1070
|
-
const updatedPluginIds = items
|
|
1071
|
-
.filter(pluginNeedsWork)
|
|
1072
|
-
.map((i) => i.pluginId)
|
|
1073
|
-
.filter((id) => !result.failed.includes(id));
|
|
1074
1102
|
modal.hideModal();
|
|
1075
1103
|
if (result.failed.length > 0) {
|
|
1076
1104
|
await modal.message(
|
|
@@ -1079,18 +1107,12 @@ export function PluginsScreen() {
|
|
|
1079
1107
|
"error",
|
|
1080
1108
|
);
|
|
1081
1109
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
} catch {
|
|
1089
|
-
// best-effort
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
if (allMismatches.length > 0) {
|
|
1093
|
-
mismatchModal.show(allMismatches);
|
|
1110
|
+
if (result.unchanged.length > 0) {
|
|
1111
|
+
await modal.message(
|
|
1112
|
+
"No change",
|
|
1113
|
+
reasons.lines(result.unchanged),
|
|
1114
|
+
"info",
|
|
1115
|
+
);
|
|
1094
1116
|
}
|
|
1095
1117
|
fetchData();
|
|
1096
1118
|
} catch (error) {
|
|
@@ -1099,39 +1121,11 @@ export function PluginsScreen() {
|
|
|
1099
1121
|
}
|
|
1100
1122
|
};
|
|
1101
1123
|
|
|
1102
|
-
/**
|
|
1103
|
-
* Manual on-demand check for plugin version mismatches across projects.
|
|
1104
|
-
* Bound to the `m` keybind. Shows a success message if everything is
|
|
1105
|
-
* aligned, or the interactive fix modal if any mismatches are found.
|
|
1106
|
-
*/
|
|
1107
|
-
const handleCheckMismatches = async () => {
|
|
1108
|
-
try {
|
|
1109
|
-
const projectPath = state.projectPath || process.cwd();
|
|
1110
|
-
const mismatches = await checkPluginVersionMismatches(projectPath);
|
|
1111
|
-
if (mismatches.length === 0) {
|
|
1112
|
-
await modal.message(
|
|
1113
|
-
"No Mismatches",
|
|
1114
|
-
"All enabled plugins in this project will load their expected versions. No action needed.",
|
|
1115
|
-
"success",
|
|
1116
|
-
);
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
mismatchModal.show(mismatches);
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
await modal.message(
|
|
1122
|
-
"Error",
|
|
1123
|
-
`Failed to check mismatches: ${error}`,
|
|
1124
|
-
"error",
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
1124
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
1130
1125
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
1131
1126
|
if (!item || item.kind !== "plugin") return;
|
|
1132
1127
|
|
|
1133
1128
|
const plugin = item.plugin;
|
|
1134
|
-
const latestVersion = plugin.version || "0.0.0";
|
|
1135
1129
|
const scopeLabel =
|
|
1136
1130
|
scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
|
|
1137
1131
|
|
|
@@ -1145,8 +1139,13 @@ export function PluginsScreen() {
|
|
|
1145
1139
|
// This used to key off `scopeData.enabled` alone, so a plugin the list
|
|
1146
1140
|
// had just labelled "not installed" answered its own scope key by
|
|
1147
1141
|
// uninstalling itself — the `enabledPlugins` flag was set, the registry
|
|
1148
|
-
// entry was missing, and only the flag was consulted.
|
|
1149
|
-
|
|
1142
|
+
// entry was missing, and only the flag was consulted. Update-versus-
|
|
1143
|
+
// uninstall is the classifier's `scopeIsBehind`, the same rule the Enter
|
|
1144
|
+
// scope picker labels with.
|
|
1145
|
+
const action = resolveScopeAction(
|
|
1146
|
+
scopeData,
|
|
1147
|
+
scopeIsBehind(statuses.get(plugin.id), scope),
|
|
1148
|
+
);
|
|
1150
1149
|
|
|
1151
1150
|
try {
|
|
1152
1151
|
if (action === "uninstall") {
|
|
@@ -1181,9 +1180,6 @@ export function PluginsScreen() {
|
|
|
1181
1180
|
if (action !== "install") {
|
|
1182
1181
|
modal.hideModal();
|
|
1183
1182
|
}
|
|
1184
|
-
if (action === "update") {
|
|
1185
|
-
await warnIfVersionMismatch(plugin.id);
|
|
1186
|
-
}
|
|
1187
1183
|
fetchData();
|
|
1188
1184
|
} catch (error) {
|
|
1189
1185
|
modal.hideModal();
|
|
@@ -1369,33 +1365,53 @@ export function PluginsScreen() {
|
|
|
1369
1365
|
{ keys: ["u", "/", "p", "/", "l"], label: "toggle" },
|
|
1370
1366
|
{ keys: ["U"], label: "update" },
|
|
1371
1367
|
{ keys: ["a"], label: "all" },
|
|
1372
|
-
{ keys: ["m"], label: "mismatches" },
|
|
1373
1368
|
{ keys: ["s"], label: "profile" },
|
|
1374
1369
|
{ keys: ["/"], label: "search" },
|
|
1375
1370
|
];
|
|
1376
1371
|
|
|
1377
1372
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
1378
1373
|
const plugins: PluginInfo[] = shownPlugins ?? [];
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1374
|
+
// "installed" must mean the same thing here as in the rows beside it. This
|
|
1375
|
+
// counted `p.enabled` — the settings flag — so the header read "11 installed"
|
|
1376
|
+
// on a screen whose every row said "not installed". Same predicate as the
|
|
1377
|
+
// row and the detail panel now.
|
|
1378
|
+
const isInstalledAnywhere = (p: PluginInfo) =>
|
|
1379
|
+
isInstalledInScope(p.userScope) ||
|
|
1380
|
+
isInstalledInScope(p.projectScope) ||
|
|
1381
|
+
isInstalledInScope(p.localScope);
|
|
1382
|
+
const installedCount = plugins.filter(isInstalledAnywhere).length;
|
|
1383
|
+
// Every state count is the classifier's, over the same map the rows read,
|
|
1384
|
+
// so a header can never count a row the list beside it does not show.
|
|
1385
|
+
const counts = countPluginStates(statuses.values());
|
|
1386
|
+
const updateCount = counts.update + counts.repair;
|
|
1387
|
+
// Cannot tell whether it is current. Counted and shown separately: folding
|
|
1388
|
+
// it into `updateCount` would overstate, and folding it into silence is the
|
|
1389
|
+
// "cannot tell rendered as fine" bug.
|
|
1390
|
+
const unknownCount = counts.unknown;
|
|
1391
|
+
const installCount = counts.install;
|
|
1392
|
+
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}${installCount > 0 ? ` │ ${installCount} to install` : ""}${unknownCount > 0 ? ` │ ${unknownCount} unverified` : ""}`;
|
|
1393
|
+
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""}${installCount > 0 ? ` │ ${installCount} +` : ""}${unknownCount > 0 ? ` │ ${unknownCount} ?` : ""} │ / to search`;
|
|
1394
|
+
|
|
1395
|
+
const catalogNotice = buildCatalogNotice({
|
|
1396
|
+
failures: catalogFailures,
|
|
1397
|
+
unverifiedPlugins: [...statuses.values()].filter(
|
|
1398
|
+
(s) => s.reason === "catalog-unverified",
|
|
1399
|
+
).length,
|
|
1400
|
+
width: dimensions.terminalWidth,
|
|
1401
|
+
now: nowTick,
|
|
1402
|
+
});
|
|
1403
|
+
// Without a resolved profile, exact pins are unknown here and every row is
|
|
1404
|
+
// judged against "latest". Saying so is what keeps a row that disagrees with
|
|
1405
|
+
// `claudeup update <profile>` from reading as a contradiction.
|
|
1390
1406
|
const notice = isReloading ? (
|
|
1391
1407
|
<text fg={theme.colors.muted}>⟳ Refreshing catalogs from GitHub…</text>
|
|
1392
1408
|
) : (
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1409
|
+
(catalogNotice ??
|
|
1410
|
+
(pins === null ? (
|
|
1411
|
+
<text fg={theme.colors.muted}>
|
|
1412
|
+
profile not resolved — states judged against latest
|
|
1413
|
+
</text>
|
|
1414
|
+
) : null))
|
|
1399
1415
|
);
|
|
1400
1416
|
|
|
1401
1417
|
return (
|
package/src/ui/state/types.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ModelsConfig, ModelsStatus } from "../../services/models-core.js";
|
|
2
2
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
3
|
-
import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
|
|
4
3
|
import type {
|
|
5
4
|
PluginComponent,
|
|
6
5
|
PluginSkillDetail,
|
|
@@ -113,12 +112,6 @@ export type ModalState =
|
|
|
113
112
|
| {
|
|
114
113
|
type: "loading";
|
|
115
114
|
message: string;
|
|
116
|
-
}
|
|
117
|
-
| {
|
|
118
|
-
type: "version-mismatch";
|
|
119
|
-
mismatches: VersionMismatchInfo[];
|
|
120
|
-
onFix: () => void;
|
|
121
|
-
onDismiss: () => void;
|
|
122
115
|
};
|
|
123
116
|
|
|
124
117
|
// ============================================================================
|