magus-cli 7.7.0 → 7.9.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 +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -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 +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- 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__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- 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 +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -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/cliToolRenderers.tsx +135 -2
- 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 +199 -98
- 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 +315 -259
- 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/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
|
@@ -573,6 +573,58 @@ export async function removeLocalInstalledPluginVersion(
|
|
|
573
573
|
);
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Take `pluginId` out of `installedPluginVersions` at `scope`, and touch
|
|
578
|
+
* nothing else.
|
|
579
|
+
*
|
|
580
|
+
* `installedPluginVersions` is magus's own field — Claude Code never writes it
|
|
581
|
+
* — so a plugin Claude Code uninstalled still has a claim there until magus
|
|
582
|
+
* clears it, and a stale claim is how an uninstalled plugin kept reading as
|
|
583
|
+
* installed. Unlike the `remove*InstalledPluginVersion` fallbacks above, this
|
|
584
|
+
* leaves `enabledPlugins` and the registry alone: those are Claude Code's, and
|
|
585
|
+
* the uninstall that just ran is what changed them. A file with no claim for
|
|
586
|
+
* the plugin is not rewritten.
|
|
587
|
+
*/
|
|
588
|
+
export async function forgetInstalledPluginVersion(
|
|
589
|
+
pluginId: string,
|
|
590
|
+
scope: "user" | "project" | "local",
|
|
591
|
+
projectPath?: string,
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
if (scope === "user") {
|
|
594
|
+
const settings = await readGlobalSettings();
|
|
595
|
+
const versions = writableMap(
|
|
596
|
+
settings,
|
|
597
|
+
"installedPluginVersions",
|
|
598
|
+
getGlobalSettingsPath(),
|
|
599
|
+
);
|
|
600
|
+
if (!versions || !Object.hasOwn(versions, pluginId)) return;
|
|
601
|
+
delete versions[pluginId];
|
|
602
|
+
await writeGlobalSettings(settings);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (scope === "local") {
|
|
606
|
+
const settings = await readLocalSettings(projectPath);
|
|
607
|
+
const versions = writableMap(
|
|
608
|
+
settings,
|
|
609
|
+
"installedPluginVersions",
|
|
610
|
+
getLocalSettingsPath(projectPath),
|
|
611
|
+
);
|
|
612
|
+
if (!versions || !Object.hasOwn(versions, pluginId)) return;
|
|
613
|
+
delete versions[pluginId];
|
|
614
|
+
await writeLocalSettings(settings, projectPath);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const settings = await readSettings(projectPath);
|
|
618
|
+
const versions = writableMap(
|
|
619
|
+
settings,
|
|
620
|
+
"installedPluginVersions",
|
|
621
|
+
getSettingsPath(projectPath),
|
|
622
|
+
);
|
|
623
|
+
if (!versions || !Object.hasOwn(versions, pluginId)) return;
|
|
624
|
+
delete versions[pluginId];
|
|
625
|
+
await writeSettings(settings, projectPath);
|
|
626
|
+
}
|
|
627
|
+
|
|
576
628
|
// Status line management
|
|
577
629
|
// Claude Code expects statusLine as an object: { type: "template", template: "..." }
|
|
578
630
|
// or { type: "command", command: "..." }. Writing a bare string causes a validation error
|
|
@@ -1000,359 +1052,15 @@ export interface MarketplaceRecoveryResult {
|
|
|
1000
1052
|
reregistered: string[];
|
|
1001
1053
|
}
|
|
1002
1054
|
|
|
1003
|
-
// =============================================================================
|
|
1004
|
-
// MARKETPLACE RENAME MIGRATION: mag-claude-plugins / MadAppGang-claude-code → magus
|
|
1005
|
-
// =============================================================================
|
|
1006
|
-
|
|
1007
|
-
const OLD_MARKETPLACE_NAMES = ["mag-claude-plugins", "MadAppGang-claude-code"];
|
|
1008
|
-
const NEW_MARKETPLACE_NAME = "magus";
|
|
1009
|
-
const NEW_MARKETPLACE_REPO = "MadAppGang/magus";
|
|
1010
|
-
const OLD_MARKETPLACE_REPOS = ["MadAppGang/claude-code"];
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Rename plugin keys in a Record from any old marketplace name to new.
|
|
1014
|
-
* e.g., "frontend@mag-claude-plugins" → "frontend@magus"
|
|
1015
|
-
* "dev@MadAppGang-claude-code" → "dev@magus"
|
|
1016
|
-
* Returns [migratedRecord, count] — count=0 means no changes.
|
|
1017
|
-
*/
|
|
1018
|
-
function migratePluginKeys<T>(
|
|
1019
|
-
record: Record<string, T> | undefined,
|
|
1020
|
-
): [Record<string, T>, number] {
|
|
1021
|
-
if (!record) return [{}, 0];
|
|
1022
|
-
const migrated: Record<string, T> = {};
|
|
1023
|
-
let count = 0;
|
|
1024
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1025
|
-
const oldName = OLD_MARKETPLACE_NAMES.find((n) => key.endsWith(`@${n}`));
|
|
1026
|
-
if (oldName) {
|
|
1027
|
-
const pluginName = key.slice(0, key.lastIndexOf("@"));
|
|
1028
|
-
const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
|
|
1029
|
-
// Don't overwrite if canonical key already exists
|
|
1030
|
-
if (!record[newKey]) {
|
|
1031
|
-
migrated[newKey] = value;
|
|
1032
|
-
}
|
|
1033
|
-
count++;
|
|
1034
|
-
} else {
|
|
1035
|
-
migrated[key] = value;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
return [migrated, count];
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/**
|
|
1042
|
-
* Migrate a single settings object in-place.
|
|
1043
|
-
* Returns number of keys renamed.
|
|
1044
|
-
*/
|
|
1045
|
-
function migrateSettingsObject(settings: ClaudeSettings): number {
|
|
1046
|
-
let total = 0;
|
|
1047
|
-
|
|
1048
|
-
const [ep, epCount] = migratePluginKeys(settings.enabledPlugins);
|
|
1049
|
-
if (epCount > 0) {
|
|
1050
|
-
settings.enabledPlugins = ep;
|
|
1051
|
-
total += epCount;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const [iv, ivCount] = migratePluginKeys(settings.installedPluginVersions);
|
|
1055
|
-
if (ivCount > 0) {
|
|
1056
|
-
settings.installedPluginVersions = iv;
|
|
1057
|
-
total += ivCount;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Migrate extraKnownMarketplaces keys
|
|
1061
|
-
for (const oldName of OLD_MARKETPLACE_NAMES) {
|
|
1062
|
-
if (settings.extraKnownMarketplaces?.[oldName]) {
|
|
1063
|
-
const entry = settings.extraKnownMarketplaces[oldName];
|
|
1064
|
-
delete settings.extraKnownMarketplaces[oldName];
|
|
1065
|
-
if (!settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME]) {
|
|
1066
|
-
settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = {
|
|
1067
|
-
...entry,
|
|
1068
|
-
source: {
|
|
1069
|
-
...entry.source,
|
|
1070
|
-
repo: NEW_MARKETPLACE_REPO,
|
|
1071
|
-
},
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
total++;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Fix stale repo URL on existing magus entry (e.g. key is "magus" but repo is still "MadAppGang/claude-code")
|
|
1079
|
-
const magusEntry = settings.extraKnownMarketplaces?.[NEW_MARKETPLACE_NAME];
|
|
1080
|
-
if (
|
|
1081
|
-
magusEntry?.source?.repo &&
|
|
1082
|
-
OLD_MARKETPLACE_REPOS.includes(magusEntry.source.repo)
|
|
1083
|
-
) {
|
|
1084
|
-
magusEntry.source.repo = NEW_MARKETPLACE_REPO;
|
|
1085
|
-
total++;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
return total;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
export interface MigrationResult {
|
|
1092
|
-
projectMigrated: number;
|
|
1093
|
-
globalMigrated: number;
|
|
1094
|
-
localMigrated: number;
|
|
1095
|
-
knownMarketplacesMigrated: boolean;
|
|
1096
|
-
registryMigrated: number;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Migrate all settings from mag-claude-plugins to magus.
|
|
1101
|
-
* Safe to call multiple times — no-ops if already migrated.
|
|
1102
|
-
* Runs across project settings, global settings, local settings,
|
|
1103
|
-
* known_marketplaces.json, and installed_plugins.json.
|
|
1104
|
-
*/
|
|
1105
|
-
/**
|
|
1106
|
-
* Decode a ~/.claude/projects/ directory name back to a filesystem path.
|
|
1107
|
-
* Claude Code encodes paths by replacing "/" with "-", which is lossy when
|
|
1108
|
-
* directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
|
|
1109
|
-
* Strategy: split on "-", then greedily recombine segments by checking which
|
|
1110
|
-
* combinations actually exist on disk.
|
|
1111
|
-
* Returns null if no valid path can be resolved.
|
|
1112
|
-
*/
|
|
1113
|
-
async function decodeProjectDirName(encoded: string): Promise<string | null> {
|
|
1114
|
-
// Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
|
|
1115
|
-
const segments = encoded.replace(/^-/, "").split("-");
|
|
1116
|
-
if (segments.length === 0) return null;
|
|
1117
|
-
|
|
1118
|
-
// Build path greedily: at each step, try joining the next segment with a dash first
|
|
1119
|
-
// (preserving directory names like "circl-infra"), fall back to slash (new path segment)
|
|
1120
|
-
let current = `/${segments[0]}`;
|
|
1121
|
-
for (let i = 1; i < segments.length; i++) {
|
|
1122
|
-
const withDash = `${current}-${segments[i]}`;
|
|
1123
|
-
const withSlash = `${current}/${segments[i]}`;
|
|
1124
|
-
// Prefer dash (keeps compound names intact) if that path prefix exists
|
|
1125
|
-
if (await fs.pathExists(withDash)) {
|
|
1126
|
-
current = withDash;
|
|
1127
|
-
} else {
|
|
1128
|
-
current = withSlash;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
return (await fs.pathExists(current)) ? current : null;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
export async function migrateMarketplaceRename(
|
|
1136
|
-
projectPath?: string,
|
|
1137
|
-
): Promise<MigrationResult> {
|
|
1138
|
-
const result: MigrationResult = {
|
|
1139
|
-
projectMigrated: 0,
|
|
1140
|
-
globalMigrated: 0,
|
|
1141
|
-
localMigrated: 0,
|
|
1142
|
-
knownMarketplacesMigrated: false,
|
|
1143
|
-
registryMigrated: 0,
|
|
1144
|
-
};
|
|
1145
|
-
|
|
1146
|
-
// 1. Project settings
|
|
1147
|
-
try {
|
|
1148
|
-
const settings = await readSettings(projectPath);
|
|
1149
|
-
const count = migrateSettingsObject(settings);
|
|
1150
|
-
if (count > 0) {
|
|
1151
|
-
await writeSettings(settings, projectPath);
|
|
1152
|
-
result.projectMigrated = count;
|
|
1153
|
-
}
|
|
1154
|
-
} catch {
|
|
1155
|
-
/* skip if unreadable */
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// 2. Global settings
|
|
1159
|
-
try {
|
|
1160
|
-
const settings = await readGlobalSettings();
|
|
1161
|
-
const count = migrateSettingsObject(settings);
|
|
1162
|
-
if (count > 0) {
|
|
1163
|
-
await writeGlobalSettings(settings);
|
|
1164
|
-
result.globalMigrated = count;
|
|
1165
|
-
}
|
|
1166
|
-
} catch {
|
|
1167
|
-
/* skip if unreadable */
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// 3. Local settings (settings.local.json)
|
|
1171
|
-
try {
|
|
1172
|
-
const local = await readLocalSettings(projectPath);
|
|
1173
|
-
let localCount = 0;
|
|
1174
|
-
const [ep, epCount] = migratePluginKeys(local.enabledPlugins);
|
|
1175
|
-
if (epCount > 0) {
|
|
1176
|
-
local.enabledPlugins = ep;
|
|
1177
|
-
localCount += epCount;
|
|
1178
|
-
}
|
|
1179
|
-
const [iv, ivCount] = migratePluginKeys(local.installedPluginVersions);
|
|
1180
|
-
if (ivCount > 0) {
|
|
1181
|
-
local.installedPluginVersions = iv;
|
|
1182
|
-
localCount += ivCount;
|
|
1183
|
-
}
|
|
1184
|
-
if (localCount > 0) {
|
|
1185
|
-
await writeLocalSettings(local, projectPath);
|
|
1186
|
-
result.localMigrated = localCount;
|
|
1187
|
-
}
|
|
1188
|
-
} catch {
|
|
1189
|
-
/* skip if unreadable */
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// 4. known_marketplaces.json — rename old keys + physical directory cleanup
|
|
1193
|
-
const marketplacesDir = path.join(
|
|
1194
|
-
os.homedir(),
|
|
1195
|
-
".claude",
|
|
1196
|
-
"plugins",
|
|
1197
|
-
"marketplaces",
|
|
1198
|
-
);
|
|
1199
|
-
const newDir = path.join(marketplacesDir, NEW_MARKETPLACE_NAME);
|
|
1200
|
-
|
|
1201
|
-
try {
|
|
1202
|
-
const known = await readKnownMarketplaces();
|
|
1203
|
-
let knownModified = false;
|
|
1204
|
-
|
|
1205
|
-
for (const oldName of OLD_MARKETPLACE_NAMES) {
|
|
1206
|
-
if (known[oldName]) {
|
|
1207
|
-
const oldEntry = known[oldName];
|
|
1208
|
-
// If canonical entry doesn't exist yet, create it
|
|
1209
|
-
if (!known[NEW_MARKETPLACE_NAME]) {
|
|
1210
|
-
known[NEW_MARKETPLACE_NAME] = {
|
|
1211
|
-
...oldEntry,
|
|
1212
|
-
source: {
|
|
1213
|
-
...oldEntry.source,
|
|
1214
|
-
repo: "MadAppGang/magus",
|
|
1215
|
-
},
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
delete known[oldName];
|
|
1219
|
-
knownModified = true;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// Ensure installLocation doesn't reference old directory names
|
|
1223
|
-
if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
|
|
1224
|
-
known[NEW_MARKETPLACE_NAME].installLocation = known[
|
|
1225
|
-
NEW_MARKETPLACE_NAME
|
|
1226
|
-
].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
|
|
1227
|
-
knownModified = true;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
if (knownModified) {
|
|
1232
|
-
await writeKnownMarketplaces(known);
|
|
1233
|
-
result.knownMarketplacesMigrated = true;
|
|
1234
|
-
}
|
|
1235
|
-
} catch {
|
|
1236
|
-
/* skip if unreadable */
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// 4b. Rename/remove old physical directories (runs even if keys were already migrated)
|
|
1240
|
-
for (const oldName of OLD_MARKETPLACE_NAMES) {
|
|
1241
|
-
const oldDir = path.join(marketplacesDir, oldName);
|
|
1242
|
-
try {
|
|
1243
|
-
if (await fs.pathExists(oldDir)) {
|
|
1244
|
-
if (!(await fs.pathExists(newDir))) {
|
|
1245
|
-
await fs.rename(oldDir, newDir);
|
|
1246
|
-
} else {
|
|
1247
|
-
// Both exist — remove the old one (magus dir is canonical)
|
|
1248
|
-
await fs.remove(oldDir);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
} catch {
|
|
1252
|
-
/* non-fatal: directory cleanup is best-effort */
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// 4c. Update git remote URL in the marketplace clone (old → new repo)
|
|
1257
|
-
try {
|
|
1258
|
-
if (await fs.pathExists(path.join(newDir, ".git"))) {
|
|
1259
|
-
const { execSync } = await import("node:child_process");
|
|
1260
|
-
const remote = execSync("git remote get-url origin", {
|
|
1261
|
-
cwd: newDir,
|
|
1262
|
-
encoding: "utf-8",
|
|
1263
|
-
timeout: 5000,
|
|
1264
|
-
}).trim();
|
|
1265
|
-
if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
|
|
1266
|
-
const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
|
|
1267
|
-
execSync(`git remote set-url origin "${newRemote}"`, {
|
|
1268
|
-
cwd: newDir,
|
|
1269
|
-
encoding: "utf-8",
|
|
1270
|
-
timeout: 5000,
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
} catch {
|
|
1275
|
-
/* non-fatal: git remote update is best-effort */
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// 5. installed_plugins.json — rename plugin ID keys
|
|
1279
|
-
try {
|
|
1280
|
-
let regCount = 0;
|
|
1281
|
-
await updateRegistryFile((registry) => {
|
|
1282
|
-
const newPlugins: typeof registry.plugins = {};
|
|
1283
|
-
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
1284
|
-
const oldName = OLD_MARKETPLACE_NAMES.find((n) =>
|
|
1285
|
-
pluginId.endsWith(`@${n}`),
|
|
1286
|
-
);
|
|
1287
|
-
if (oldName) {
|
|
1288
|
-
const pluginName = pluginId.slice(0, pluginId.lastIndexOf("@"));
|
|
1289
|
-
const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
|
|
1290
|
-
if (!newPlugins[newKey]) {
|
|
1291
|
-
newPlugins[newKey] = entries;
|
|
1292
|
-
}
|
|
1293
|
-
regCount++;
|
|
1294
|
-
} else {
|
|
1295
|
-
newPlugins[pluginId] = entries;
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
if (regCount === 0) return false;
|
|
1299
|
-
registry.plugins = newPlugins;
|
|
1300
|
-
return true;
|
|
1301
|
-
});
|
|
1302
|
-
// After the write, never inside the mutation: a refused write (a corrupt
|
|
1303
|
-
// file, or one another session changed) must not report a migration that
|
|
1304
|
-
// did not land.
|
|
1305
|
-
result.registryMigrated = regCount;
|
|
1306
|
-
} catch {
|
|
1307
|
-
/* skip if unreadable */
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
|
|
1311
|
-
try {
|
|
1312
|
-
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
1313
|
-
if (await fs.pathExists(projectsDir)) {
|
|
1314
|
-
const entries = await fs.readdir(projectsDir);
|
|
1315
|
-
const seenPaths = new Set<string>();
|
|
1316
|
-
// Current project (from step 1) already handled — skip it
|
|
1317
|
-
const currentProject = projectPath || process.cwd();
|
|
1318
|
-
seenPaths.add(currentProject);
|
|
1319
|
-
|
|
1320
|
-
for (const entry of entries) {
|
|
1321
|
-
const decoded = await decodeProjectDirName(entry);
|
|
1322
|
-
if (!decoded || seenPaths.has(decoded)) continue;
|
|
1323
|
-
seenPaths.add(decoded);
|
|
1324
|
-
|
|
1325
|
-
const settingsFile = path.join(decoded, ".claude", "settings.json");
|
|
1326
|
-
try {
|
|
1327
|
-
if (await fs.pathExists(settingsFile)) {
|
|
1328
|
-
const raw = await fs.readJson(settingsFile);
|
|
1329
|
-
const count = migrateSettingsObject(raw);
|
|
1330
|
-
if (count > 0) {
|
|
1331
|
-
await fs.writeJson(settingsFile, raw, { spaces: 2 });
|
|
1332
|
-
result.projectMigrated += count;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
} catch {
|
|
1336
|
-
/* skip individual projects that fail */
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
} catch {
|
|
1341
|
-
/* non-fatal: cross-project scan is best-effort */
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
return result;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
1055
|
/**
|
|
1348
1056
|
* Recover/sync marketplace settings:
|
|
1349
1057
|
* - Remove entries for marketplaces whose installLocation no longer exists
|
|
1350
1058
|
* - Fix stale "directory" source for known Magus marketplaces that should be "github"
|
|
1351
1059
|
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
* {@link getMarketplaceAutoUpdate}.
|
|
1060
|
+
* A write to Claude Code's registry, so it runs only inside an install the user
|
|
1061
|
+
* asked for, after Claude Code itself answered "not found in marketplace"
|
|
1062
|
+
* (claude-cli's recovery). Opening magus never calls it. It does not touch
|
|
1063
|
+
* `autoUpdate` — see {@link getMarketplaceAutoUpdate}.
|
|
1356
1064
|
*/
|
|
1357
1065
|
export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryResult> {
|
|
1358
1066
|
const known = await readKnownMarketplaces();
|
|
@@ -1992,9 +1700,8 @@ export async function removeFromInstalledPluginsRegistry(
|
|
|
1992
1700
|
* the reach it states, so those rows are never written here. When such a row is
|
|
1993
1701
|
* the one that WINS, the repair refuses outright and names the scope — silently
|
|
1994
1702
|
* rewriting nothing would be the worse answer of the two.
|
|
1995
|
-
* - A null repository root matches
|
|
1996
|
-
*
|
|
1997
|
-
* match here: "the same repository" cannot be true of two directories that are
|
|
1703
|
+
* - A null repository root matches nothing, here and (since Claude Code
|
|
1704
|
+
* 2.1.284) in the loader too: "the same repository" cannot be true of two directories that are
|
|
1998
1705
|
* in no repository at all, and treating it as true would have a repair run
|
|
1999
1706
|
* from a scratch directory rewrite another scratch directory's rows. Only the
|
|
2000
1707
|
* exact same directory qualifies without a root.
|
|
@@ -2209,6 +1916,15 @@ export async function loadedVersionHere(
|
|
|
2209
1916
|
}
|
|
2210
1917
|
}
|
|
2211
1918
|
|
|
1919
|
+
/**
|
|
1920
|
+
* {@link repairStaleLoad}'s refusal when `target` has no downloaded copy to
|
|
1921
|
+
* point Claude Code at. Exported so a caller that already names the version —
|
|
1922
|
+
* "installed v1.3.0, but …" — can say it without repeating it.
|
|
1923
|
+
*/
|
|
1924
|
+
export function notDownloadedReason(target: string): string {
|
|
1925
|
+
return `v${target} is not downloaded — run magus install`;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
2212
1928
|
/**
|
|
2213
1929
|
* What one {@link repairStaleLoad} attempt did, and whether it worked.
|
|
2214
1930
|
*
|
|
@@ -2326,9 +2042,7 @@ export async function repairStaleLoad(
|
|
|
2326
2042
|
root,
|
|
2327
2043
|
);
|
|
2328
2044
|
if (!(await fs.pathExists(cacheDir))) {
|
|
2329
|
-
return fail(
|
|
2330
|
-
`no cache directory for v${target} — ${cacheDir} does not exist`,
|
|
2331
|
-
);
|
|
2045
|
+
return fail(notDownloadedReason(target));
|
|
2332
2046
|
}
|
|
2333
2047
|
|
|
2334
2048
|
const file = installedPluginsFile(root);
|
|
@@ -2336,22 +2050,22 @@ export async function repairStaleLoad(
|
|
|
2336
2050
|
// precondition the write is checked against, so they have to describe one
|
|
2337
2051
|
// moment — see `writeInstalledPluginsRegistry`.
|
|
2338
2052
|
const before = await readRegistryBytes(file);
|
|
2339
|
-
if (before === null) return fail(
|
|
2053
|
+
if (before === null) return fail("no plugin is installed on this machine");
|
|
2340
2054
|
let registry: InstalledPluginsRegistry;
|
|
2341
2055
|
try {
|
|
2342
2056
|
registry = requireRegistry(file, before);
|
|
2343
|
-
} catch
|
|
2057
|
+
} catch {
|
|
2344
2058
|
// The same check every other reader makes; a repair reports it as a result
|
|
2345
2059
|
// rather than throwing, because it is one item in a plan of many.
|
|
2060
|
+
// Why it does not parse is `magus doctor`'s to show; here it is only the
|
|
2061
|
+
// reason nothing was changed.
|
|
2346
2062
|
return fail(
|
|
2347
|
-
|
|
2348
|
-
error instanceof Error ? error.message : String(error)
|
|
2349
|
-
}`,
|
|
2063
|
+
"Claude Code's list of installed plugins cannot be read, so nothing was changed — run magus doctor",
|
|
2350
2064
|
);
|
|
2351
2065
|
}
|
|
2352
2066
|
const rows = registry.plugins[pluginId];
|
|
2353
2067
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2354
|
-
return fail(
|
|
2068
|
+
return fail("it is not installed on this machine");
|
|
2355
2069
|
}
|
|
2356
2070
|
|
|
2357
2071
|
// One cache for the whole pass: the same project paths repeat across rows,
|
|
@@ -2374,13 +2088,9 @@ export async function repairStaleLoad(
|
|
|
2374
2088
|
// `LoaderScope` in plugin-resolution.ts.
|
|
2375
2089
|
const scope: string = winner.scope;
|
|
2376
2090
|
return fail(
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
scope === "managed"
|
|
2381
|
-
? "A managed record is administrator policy — raise it with whoever set that policy."
|
|
2382
|
-
: `Update that scope directly instead: claude plugin install ${pluginId} --scope user`
|
|
2383
|
-
}`,
|
|
2091
|
+
scope === "managed"
|
|
2092
|
+
? "Claude Code loads the install your administrator manages here — ask them to update it"
|
|
2093
|
+
: "Claude Code loads the USER install of it here — update it at USER scope",
|
|
2384
2094
|
);
|
|
2385
2095
|
}
|
|
2386
2096
|
|
|
@@ -2398,11 +2108,9 @@ export async function repairStaleLoad(
|
|
|
2398
2108
|
try {
|
|
2399
2109
|
backup = await registryBackupPath(file, pluginId);
|
|
2400
2110
|
await fs.copy(file, backup);
|
|
2401
|
-
} catch
|
|
2111
|
+
} catch {
|
|
2402
2112
|
return fail(
|
|
2403
|
-
|
|
2404
|
-
error instanceof Error ? error.message : String(error)
|
|
2405
|
-
}`,
|
|
2113
|
+
"could not save a backup before changing anything, so nothing was changed",
|
|
2406
2114
|
);
|
|
2407
2115
|
}
|
|
2408
2116
|
|
|
@@ -2428,10 +2136,8 @@ export async function repairStaleLoad(
|
|
|
2428
2136
|
if (backup) await fs.remove(backup).catch(() => {});
|
|
2429
2137
|
return fail(
|
|
2430
2138
|
error instanceof RegistryChangedError
|
|
2431
|
-
?
|
|
2432
|
-
:
|
|
2433
|
-
error instanceof Error ? error.message : String(error)
|
|
2434
|
-
}`,
|
|
2139
|
+
? "another Claude Code session changed its plugins at the same moment, so nothing was changed — run it again"
|
|
2140
|
+
: "the change could not be saved, so nothing was changed",
|
|
2435
2141
|
);
|
|
2436
2142
|
}
|
|
2437
2143
|
await pruneRegistryBackups(file);
|
|
@@ -2444,12 +2150,8 @@ export async function repairStaleLoad(
|
|
|
2444
2150
|
let after: InstalledPluginsRegistry;
|
|
2445
2151
|
try {
|
|
2446
2152
|
after = await readInstalledPluginsRegistry(root);
|
|
2447
|
-
} catch
|
|
2448
|
-
return fail(
|
|
2449
|
-
`could not re-read ${file} to confirm the repair: ${
|
|
2450
|
-
error instanceof Error ? error.message : String(error)
|
|
2451
|
-
}`,
|
|
2452
|
-
);
|
|
2153
|
+
} catch {
|
|
2154
|
+
return fail("the change was saved, but could not be checked afterwards");
|
|
2453
2155
|
}
|
|
2454
2156
|
const loaded =
|
|
2455
2157
|
pickLoadedEntry(after.plugins[pluginId], projectPath)?.version ?? null;
|
|
@@ -2470,8 +2172,10 @@ export async function repairStaleLoad(
|
|
|
2470
2172
|
backup,
|
|
2471
2173
|
loaded,
|
|
2472
2174
|
ok: false,
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2175
|
+
// The user's words, not the mechanism's: this reaches both front ends
|
|
2176
|
+
// verbatim as the reason an item failed, grouped by that reason.
|
|
2177
|
+
error: loaded
|
|
2178
|
+
? `Claude Code still loads v${loaded} in this folder`
|
|
2179
|
+
: "Claude Code loads no version of it in this folder",
|
|
2476
2180
|
};
|
|
2477
2181
|
}
|
|
@@ -19,14 +19,115 @@
|
|
|
19
19
|
* renderer.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
type CatalogueEntry,
|
|
24
|
+
type CliTool,
|
|
25
|
+
type OwnedCatalogueEntry,
|
|
26
|
+
cliTools,
|
|
27
|
+
isOwnedEntry,
|
|
28
|
+
} from "../data/cli-tools.js";
|
|
29
|
+
import type { ResolvedBin, SelectedBin } from "../types/index.js";
|
|
23
30
|
import type { Command } from "../utils/command-utils.js";
|
|
31
|
+
import { ownedRequirement } from "./plugin-requires.js";
|
|
24
32
|
import {
|
|
25
|
-
binInstallCommand,
|
|
26
33
|
binUninstallCommand,
|
|
27
34
|
binUpgradeCommand,
|
|
35
|
+
managedInstallCommand,
|
|
36
|
+
managedTool,
|
|
28
37
|
} from "./toolchain.js";
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A catalogue row ready to probe. For a tool a plugin owns, `owned` is that
|
|
41
|
+
* plugin's requirement — what an install runs through the deps executor — and
|
|
42
|
+
* the tool's check and installer are read from it.
|
|
43
|
+
*/
|
|
44
|
+
export interface CatalogueTool {
|
|
45
|
+
tool: CliTool;
|
|
46
|
+
owned?: ResolvedBin;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A catalogue tool a plugin owns and installs with an option magus does not
|
|
51
|
+
* version — a release, a Homebrew cask, apt. Shown read-only, as managed by
|
|
52
|
+
* that plugin: its presence is the dependency probe's answer, and its
|
|
53
|
+
* install, update and removal are the plugin's declaration, run by
|
|
54
|
+
* `magus doctor --fix`.
|
|
55
|
+
*/
|
|
56
|
+
export interface PluginManagedTool {
|
|
57
|
+
entry: OwnedCatalogueEntry;
|
|
58
|
+
owned: ResolvedBin;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One catalogue row: a tool magus versions, or one its plugin manages. */
|
|
62
|
+
export type CatalogueItem = CatalogueTool | PluginManagedTool;
|
|
63
|
+
|
|
64
|
+
export const isPluginManagedItem = (
|
|
65
|
+
item: CatalogueItem,
|
|
66
|
+
): item is PluginManagedTool => "entry" in item;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* An owned entry as a {@link CliTool}, from its plugin's requirement: null when
|
|
70
|
+
* that requirement is not installed by a version-managed kind, which the CLI
|
|
71
|
+
* Tools screen cannot track — such an entry is a {@link PluginManagedTool}.
|
|
72
|
+
*/
|
|
73
|
+
export function ownedCliTool(
|
|
74
|
+
entry: CatalogueEntry,
|
|
75
|
+
owned: SelectedBin,
|
|
76
|
+
): CliTool | null {
|
|
77
|
+
const managed = managedTool(owned);
|
|
78
|
+
if (!managed) return null;
|
|
79
|
+
const { name, displayName, description, website, category } = entry;
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
displayName,
|
|
83
|
+
description,
|
|
84
|
+
website,
|
|
85
|
+
category,
|
|
86
|
+
checkCommand: owned.check.join(" "),
|
|
87
|
+
packageManager: managed.via,
|
|
88
|
+
packageName:
|
|
89
|
+
managed.package ?? managed.formula ?? managed.module ?? owned.name,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The catalogue entries that need no lookup: every one no plugin owns. */
|
|
94
|
+
export function unownedCatalogue(): CatalogueTool[] {
|
|
95
|
+
return cliTools.flatMap((e) => (isOwnedEntry(e) ? [] : [{ tool: e }]));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The whole catalogue, in catalogue order, each owned entry resolved through
|
|
100
|
+
* its plugin's declaration (the copy Claude Code loads here, else the
|
|
101
|
+
* marketplace clone): a tracked tool when its chosen option is version-managed,
|
|
102
|
+
* else a {@link PluginManagedTool}. An owned entry whose declaration cannot be
|
|
103
|
+
* read here is left out: magus cannot say how to check or install it.
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveCatalogue(
|
|
106
|
+
projectPath: string,
|
|
107
|
+
owned: (
|
|
108
|
+
pluginId: string,
|
|
109
|
+
name: string,
|
|
110
|
+
projectPath: string,
|
|
111
|
+
) => Promise<SelectedBin | null> = ownedRequirement,
|
|
112
|
+
): Promise<CatalogueItem[]> {
|
|
113
|
+
const out: CatalogueItem[] = [];
|
|
114
|
+
for (const entry of cliTools) {
|
|
115
|
+
if (!isOwnedEntry(entry)) {
|
|
116
|
+
out.push({ tool: entry });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const requirement = await owned(entry.ownedBy, entry.name, projectPath);
|
|
120
|
+
if (!requirement) continue;
|
|
121
|
+
const resolved = {
|
|
122
|
+
...requirement,
|
|
123
|
+
sources: [`plugin:${entry.ownedBy}`],
|
|
124
|
+
};
|
|
125
|
+
const tool = ownedCliTool(entry, requirement);
|
|
126
|
+
out.push(tool ? { tool, owned: resolved } : { entry, owned: resolved });
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
30
131
|
/**
|
|
31
132
|
* How a tool turned out to be installed, as detected from its path on disk.
|
|
32
133
|
*
|
|
@@ -64,7 +165,7 @@ export type InstallMethod =
|
|
|
64
165
|
* disagreement unrepresentable rather than merely fixed.
|
|
65
166
|
*/
|
|
66
167
|
export function cliToolInstall(tool: CliTool): Command {
|
|
67
|
-
return
|
|
168
|
+
return managedInstallCommand({
|
|
68
169
|
name: tool.name,
|
|
69
170
|
via: tool.packageManager,
|
|
70
171
|
package: tool.packageName,
|