claudeup 4.30.0 → 4.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -92
- package/package.json +4 -4
- package/src/__tests__/content-drift.test.ts +152 -0
- package/src/__tests__/dual-write-prevention.test.ts +151 -8
- package/src/__tests__/marketplace-refresh.test.ts +7 -2
- package/src/__tests__/moved-marketplace.test.ts +87 -0
- package/src/__tests__/profile-materializer.test.ts +87 -0
- package/src/__tests__/version-snapshot.test.ts +85 -0
- package/src/cli/install.ts +36 -5
- package/src/cli/router.ts +17 -9
- package/src/prerunner/index.ts +152 -6
- package/src/services/claude-cli.ts +31 -0
- package/src/services/claude-settings.ts +17 -1
- package/src/services/content-drift.ts +121 -0
- package/src/services/marketplace-refresh.ts +15 -3
- package/src/services/plugin-manager.ts +102 -2
- package/src/services/profile-materializer.ts +55 -4
- package/src/services/version-snapshot.ts +57 -8
- package/src/ui/renderers/pluginRenderers.tsx +20 -4
- package/src/ui/screens/PluginsScreen.tsx +82 -10
|
@@ -7,6 +7,7 @@ import { ScrollableList } from "../components/ScrollableList.js";
|
|
|
7
7
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
8
8
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
9
9
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
10
|
+
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
10
11
|
import {
|
|
11
12
|
diffVersions,
|
|
12
13
|
loadSeenVersions,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
import { saveProfile } from "../../services/profiles.js";
|
|
31
32
|
import {
|
|
32
33
|
installPlugin as cliInstallPlugin,
|
|
34
|
+
repairPlugin as cliRepairPlugin,
|
|
33
35
|
uninstallPlugin as cliUninstallPlugin,
|
|
34
36
|
updatePlugin as cliUpdatePlugin,
|
|
35
37
|
type PluginScope,
|
|
@@ -87,7 +89,8 @@ export function PluginsScreen() {
|
|
|
87
89
|
for (const p of pluginData) {
|
|
88
90
|
if (p.installedVersion) current[p.id] = p.installedVersion;
|
|
89
91
|
}
|
|
90
|
-
const
|
|
92
|
+
const projectPath = state.projectPath || process.cwd();
|
|
93
|
+
const previous = await loadSeenVersions(projectPath);
|
|
91
94
|
for (const change of diffVersions(previous, current)) {
|
|
92
95
|
const target = pluginData.find((p) => p.id === change.pluginId);
|
|
93
96
|
if (!target) continue;
|
|
@@ -97,8 +100,10 @@ export function PluginsScreen() {
|
|
|
97
100
|
target.recentlyInstalled = true;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
103
|
+
// Await the write. Fire-and-forget raced the next fetchData() — a
|
|
104
|
+
// refresh or a post-action refetch could load the baseline before the
|
|
105
|
+
// previous save landed and re-report the same change.
|
|
106
|
+
await saveSeenVersions(projectPath, current);
|
|
102
107
|
|
|
103
108
|
dispatch({
|
|
104
109
|
type: "PLUGINS_DATA_SUCCESS",
|
|
@@ -352,6 +357,9 @@ export function PluginsScreen() {
|
|
|
352
357
|
progress.show(`${p.name}`, p.current, p.total);
|
|
353
358
|
});
|
|
354
359
|
clearMarketplaceCache();
|
|
360
|
+
// Drift is answered by diffing against the clone's HEAD, so a refresh
|
|
361
|
+
// that moves HEAD invalidates every cached answer.
|
|
362
|
+
clearContentDriftCache();
|
|
355
363
|
progress.hide();
|
|
356
364
|
|
|
357
365
|
let message =
|
|
@@ -705,17 +713,29 @@ export function PluginsScreen() {
|
|
|
705
713
|
|
|
706
714
|
const handleUpdate = async () => {
|
|
707
715
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
708
|
-
if (!item || item.kind !== "plugin"
|
|
716
|
+
if (!item || item.kind !== "plugin") return;
|
|
717
|
+
if (!item.plugin.hasUpdate && !item.plugin.contentStale) return;
|
|
709
718
|
|
|
710
719
|
const plugin = item.plugin;
|
|
711
720
|
const scope: PluginScope =
|
|
712
721
|
pluginsState.scope === "global" ? "user" : "project";
|
|
713
722
|
|
|
723
|
+
// No version bump to install over — the files changed underneath an
|
|
724
|
+
// unchanged version, and `plugin install` no-ops on "already installed".
|
|
725
|
+
// Only uninstall+install re-copies. See claude-cli.ts repairPlugin.
|
|
726
|
+
const repairing = !plugin.hasUpdate && plugin.contentStale;
|
|
727
|
+
|
|
714
728
|
modal.loading(
|
|
715
|
-
|
|
729
|
+
repairing
|
|
730
|
+
? `Repairing ${plugin.name}…\nclaude plugin uninstall ${plugin.id} --scope ${scope} && claude plugin install ${plugin.id} --scope ${scope}`
|
|
731
|
+
: `Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
716
732
|
);
|
|
717
733
|
try {
|
|
718
|
-
|
|
734
|
+
if (repairing) {
|
|
735
|
+
await cliRepairPlugin(plugin.id, scope, state.projectPath);
|
|
736
|
+
} else {
|
|
737
|
+
await cliUpdatePlugin(plugin.id, scope);
|
|
738
|
+
}
|
|
719
739
|
if (plugin.version) {
|
|
720
740
|
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
721
741
|
}
|
|
@@ -731,7 +751,11 @@ export function PluginsScreen() {
|
|
|
731
751
|
const handleUpdateAll = async () => {
|
|
732
752
|
if (pluginsState.plugins.status !== "success") return;
|
|
733
753
|
|
|
734
|
-
|
|
754
|
+
// Content-stale plugins are included: they need repairing, and excluding
|
|
755
|
+
// them meant "update all" left the machine with silently outdated files.
|
|
756
|
+
const updatable = pluginsState.plugins.data.filter(
|
|
757
|
+
(p) => p.hasUpdate || p.contentStale,
|
|
758
|
+
);
|
|
735
759
|
if (updatable.length === 0) return;
|
|
736
760
|
|
|
737
761
|
const scope: PluginScope =
|
|
@@ -741,10 +765,15 @@ export function PluginsScreen() {
|
|
|
741
765
|
try {
|
|
742
766
|
for (let i = 0; i < updatable.length; i++) {
|
|
743
767
|
const plugin = updatable[i];
|
|
768
|
+
const repairing = !plugin.hasUpdate && plugin.contentStale;
|
|
744
769
|
modal.loading(
|
|
745
|
-
|
|
770
|
+
`${repairing ? "Repairing" : "Updating"} ${plugin.name} (${i + 1}/${updatable.length})…\nclaude plugin install ${plugin.id} --scope ${scope}`,
|
|
746
771
|
);
|
|
747
|
-
|
|
772
|
+
if (repairing) {
|
|
773
|
+
await cliRepairPlugin(plugin.id, scope, state.projectPath);
|
|
774
|
+
} else {
|
|
775
|
+
await cliUpdatePlugin(plugin.id, scope);
|
|
776
|
+
}
|
|
748
777
|
if (plugin.version) {
|
|
749
778
|
await saveVersionForScope(plugin.id, plugin.version, scope);
|
|
750
779
|
}
|
|
@@ -872,19 +901,62 @@ export function PluginsScreen() {
|
|
|
872
901
|
if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned) return;
|
|
873
902
|
|
|
874
903
|
const plugin = item.plugin;
|
|
904
|
+
|
|
905
|
+
// A plugin that moved marketplace is not deprecated — it is still
|
|
906
|
+
// published under a new namespace. Offering only "remove" silently drops
|
|
907
|
+
// something the user still wants (the magus → magus-marketing split left
|
|
908
|
+
// seo, instantly, video-editing and image-generate in this state).
|
|
909
|
+
let migrateTo: string | undefined;
|
|
910
|
+
if (plugin.movedTo) {
|
|
911
|
+
const choice = await modal.select(
|
|
912
|
+
"Plugin moved",
|
|
913
|
+
`${plugin.name} is no longer published by "${plugin.marketplace}", but "${plugin.movedTo}" still publishes it.`,
|
|
914
|
+
[
|
|
915
|
+
{
|
|
916
|
+
label: `Migrate to ${plugin.name}@${plugin.movedTo}`,
|
|
917
|
+
value: "migrate",
|
|
918
|
+
},
|
|
919
|
+
{ label: "Remove it entirely", value: "remove" },
|
|
920
|
+
],
|
|
921
|
+
);
|
|
922
|
+
if (choice === null) return;
|
|
923
|
+
if (choice === "migrate") migrateTo = plugin.movedTo;
|
|
924
|
+
}
|
|
925
|
+
|
|
875
926
|
try {
|
|
876
927
|
// Remove from all scopes — try all to clean up stale references
|
|
877
928
|
const scopes: PluginScope[] = ["user", "project", "local"];
|
|
929
|
+
const removedFrom: PluginScope[] = [];
|
|
878
930
|
for (const scope of scopes) {
|
|
879
931
|
try {
|
|
880
932
|
modal.loading(
|
|
881
933
|
`Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`,
|
|
882
934
|
);
|
|
935
|
+
const wasInstalled =
|
|
936
|
+
scope === "user"
|
|
937
|
+
? plugin.userScope?.enabled
|
|
938
|
+
: scope === "project"
|
|
939
|
+
? plugin.projectScope?.enabled
|
|
940
|
+
: plugin.localScope?.enabled;
|
|
883
941
|
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
942
|
+
if (wasInstalled) removedFrom.push(scope);
|
|
884
943
|
} catch {
|
|
885
944
|
// Ignore errors for scopes where it doesn't exist
|
|
886
945
|
}
|
|
887
946
|
}
|
|
947
|
+
|
|
948
|
+
if (migrateTo) {
|
|
949
|
+
// Reinstall under the new namespace in every scope it occupied, so
|
|
950
|
+
// the migration does not quietly change where the plugin is enabled.
|
|
951
|
+
const newId = `${plugin.name}@${migrateTo}`;
|
|
952
|
+
for (const scope of removedFrom.length > 0 ? removedFrom : ["project" as PluginScope]) {
|
|
953
|
+
modal.loading(
|
|
954
|
+
`Installing ${newId} (${scope})…\nclaude plugin install ${newId} --scope ${scope}`,
|
|
955
|
+
);
|
|
956
|
+
await cliInstallPlugin(newId, scope);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
888
960
|
modal.hideModal();
|
|
889
961
|
fetchData();
|
|
890
962
|
} catch (error) {
|
|
@@ -986,7 +1058,7 @@ export function PluginsScreen() {
|
|
|
986
1058
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
987
1059
|
const updateCount = plugins.filter(
|
|
988
1060
|
(p) =>
|
|
989
|
-
p.hasUpdate &&
|
|
1061
|
+
(p.hasUpdate || p.contentStale) &&
|
|
990
1062
|
(p.userScope?.enabled ||
|
|
991
1063
|
p.projectScope?.enabled ||
|
|
992
1064
|
p.localScope?.enabled),
|