claudeup 4.13.0 → 4.14.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 +1 -1
- package/src/prerunner/index.js +9 -87
- package/src/prerunner/index.ts +10 -109
- package/src/services/claude-cli.js +35 -17
- package/src/services/claude-cli.ts +41 -20
- package/src/services/claude-settings.js +14 -0
- package/src/services/claude-settings.ts +21 -0
- package/src/services/marketplace-sync.js +94 -0
- package/src/services/marketplace-sync.ts +114 -0
- package/src/ui/App.js +19 -0
- package/src/ui/App.tsx +18 -0
package/package.json
CHANGED
package/src/prerunner/index.js
CHANGED
|
@@ -4,92 +4,10 @@ import os from "node:os";
|
|
|
4
4
|
import { UpdateCache } from "../services/update-cache.js";
|
|
5
5
|
import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
|
|
6
6
|
import { runClaude } from "../services/claude-runner.js";
|
|
7
|
-
import { recoverMarketplaceSettings, migrateMarketplaceRename, cleanupExtraKnownMarketplaces,
|
|
7
|
+
import { recoverMarketplaceSettings, migrateMarketplaceRename, cleanupExtraKnownMarketplaces, readGlobalSettings, writeGlobalSettings, saveGlobalInstalledPluginVersion, gapFillInstalledPluginVersions, } from "../services/claude-settings.js";
|
|
8
8
|
import { checkPluginVersionMismatches, formatMismatchWarning, } from "../services/plugin-version-check.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
|
|
12
|
-
const MARKETPLACES_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
13
|
-
/**
|
|
14
|
-
* Collect all unique marketplace names from enabled plugins across all settings scopes.
|
|
15
|
-
* Returns a Set of marketplace names (e.g., "magus", "claude-plugins-official").
|
|
16
|
-
*/
|
|
17
|
-
async function getReferencedMarketplaces(projectPath) {
|
|
18
|
-
const marketplaceNames = new Set();
|
|
19
|
-
// Collect plugin IDs from all scopes
|
|
20
|
-
const allPluginIds = new Set();
|
|
21
|
-
try {
|
|
22
|
-
const global = await getGlobalEnabledPlugins();
|
|
23
|
-
for (const id of Object.keys(global))
|
|
24
|
-
allPluginIds.add(id);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
/* skip if unreadable */
|
|
28
|
-
}
|
|
29
|
-
if (projectPath) {
|
|
30
|
-
try {
|
|
31
|
-
const project = await getEnabledPlugins(projectPath);
|
|
32
|
-
for (const id of Object.keys(project))
|
|
33
|
-
allPluginIds.add(id);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
/* skip if unreadable */
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
const local = await getLocalEnabledPlugins(projectPath);
|
|
40
|
-
for (const id of Object.keys(local))
|
|
41
|
-
allPluginIds.add(id);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
/* skip if unreadable */
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Parse marketplace names from plugin IDs
|
|
48
|
-
for (const pluginId of allPluginIds) {
|
|
49
|
-
const parsed = parsePluginId(pluginId);
|
|
50
|
-
if (parsed) {
|
|
51
|
-
marketplaceNames.add(parsed.marketplace);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return marketplaceNames;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
58
|
-
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
59
|
-
*
|
|
60
|
-
* IMPORTANT: Only uses `marketplace add` (never `marketplace update`).
|
|
61
|
-
* Claude Code's `marketplace update` calls cacheMarketplaceFromGit() which
|
|
62
|
-
* deletes the marketplace directory before re-cloning. If the clone fails
|
|
63
|
-
* (network timeout, auth error), the directory stays permanently deleted
|
|
64
|
-
* and ALL plugins from that marketplace break. See:
|
|
65
|
-
* ai-docs/plugin-marketplace-bug-investigation.md
|
|
66
|
-
*
|
|
67
|
-
* Claude Code's own background autoupdate handles marketplace refreshing
|
|
68
|
-
* after session start — claudeup should only recover genuinely missing
|
|
69
|
-
* marketplaces, not trigger additional refresh cycles.
|
|
70
|
-
*/
|
|
71
|
-
async function autoAddMissingMarketplaces(projectPath) {
|
|
72
|
-
const referenced = await getReferencedMarketplaces(projectPath);
|
|
73
|
-
const added = [];
|
|
74
|
-
for (const mpName of referenced) {
|
|
75
|
-
// Check if marketplace directory exists locally
|
|
76
|
-
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
77
|
-
if (await fs.pathExists(mpDir))
|
|
78
|
-
continue;
|
|
79
|
-
// Look up the repo URL from default marketplaces
|
|
80
|
-
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
81
|
-
if (!defaultMp?.source.repo)
|
|
82
|
-
continue;
|
|
83
|
-
try {
|
|
84
|
-
await addMarketplace(defaultMp.source.repo);
|
|
85
|
-
added.push(mpName);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
console.warn(`⚠ Failed to auto-add marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return added;
|
|
92
|
-
}
|
|
9
|
+
import { updatePlugin, isClaudeAvailable } from "../services/claude-cli.js";
|
|
10
|
+
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
93
11
|
const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
|
|
94
12
|
const CONTINUITY_PLUGIN_SCRIPT = path.join(os.homedir(), ".tmux", "plugins", "tmux-claude-continuity", "scripts", "on_session_start.sh");
|
|
95
13
|
/**
|
|
@@ -162,9 +80,13 @@ export async function prerunClaude(claudeArgs, options = {}) {
|
|
|
162
80
|
}
|
|
163
81
|
// STEP 0.5: Auto-add missing marketplaces
|
|
164
82
|
// When plugins reference a marketplace that's not installed locally
|
|
165
|
-
// (e.g., settings synced from another machine
|
|
83
|
+
// (e.g., settings synced from another machine or committed by a
|
|
84
|
+
// teammate in .claude/settings.json), add it automatically. Pass
|
|
85
|
+
// process.cwd() so project and local enabledPlugins are scanned —
|
|
86
|
+
// otherwise a freshly cloned repo whose only @magus references live
|
|
87
|
+
// in project settings looks "clean" from the global-scope view.
|
|
166
88
|
if (await isClaudeAvailable()) {
|
|
167
|
-
const addedMarketplaces = await autoAddMissingMarketplaces();
|
|
89
|
+
const addedMarketplaces = await autoAddMissingMarketplaces(process.cwd());
|
|
168
90
|
if (addedMarketplaces.length > 0) {
|
|
169
91
|
console.log(`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`);
|
|
170
92
|
}
|
package/src/prerunner/index.ts
CHANGED
|
@@ -11,9 +11,6 @@ import {
|
|
|
11
11
|
recoverMarketplaceSettings,
|
|
12
12
|
migrateMarketplaceRename,
|
|
13
13
|
cleanupExtraKnownMarketplaces,
|
|
14
|
-
getGlobalEnabledPlugins,
|
|
15
|
-
getEnabledPlugins,
|
|
16
|
-
getLocalEnabledPlugins,
|
|
17
14
|
readGlobalSettings,
|
|
18
15
|
writeGlobalSettings,
|
|
19
16
|
saveGlobalInstalledPluginVersion,
|
|
@@ -23,115 +20,13 @@ import {
|
|
|
23
20
|
checkPluginVersionMismatches,
|
|
24
21
|
formatMismatchWarning,
|
|
25
22
|
} from "../services/plugin-version-check.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
updatePlugin,
|
|
30
|
-
addMarketplace,
|
|
31
|
-
isClaudeAvailable,
|
|
32
|
-
} from "../services/claude-cli.js";
|
|
33
|
-
|
|
34
|
-
const MARKETPLACES_DIR = path.join(
|
|
35
|
-
os.homedir(),
|
|
36
|
-
".claude",
|
|
37
|
-
"plugins",
|
|
38
|
-
"marketplaces",
|
|
39
|
-
);
|
|
23
|
+
import { updatePlugin, isClaudeAvailable } from "../services/claude-cli.js";
|
|
24
|
+
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
40
25
|
|
|
41
26
|
export interface PrerunOptions {
|
|
42
27
|
force?: boolean; // Bypass cache and force update check
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
/**
|
|
46
|
-
* Collect all unique marketplace names from enabled plugins across all settings scopes.
|
|
47
|
-
* Returns a Set of marketplace names (e.g., "magus", "claude-plugins-official").
|
|
48
|
-
*/
|
|
49
|
-
async function getReferencedMarketplaces(
|
|
50
|
-
projectPath?: string,
|
|
51
|
-
): Promise<Set<string>> {
|
|
52
|
-
const marketplaceNames = new Set<string>();
|
|
53
|
-
|
|
54
|
-
// Collect plugin IDs from all scopes
|
|
55
|
-
const allPluginIds = new Set<string>();
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
const global = await getGlobalEnabledPlugins();
|
|
59
|
-
for (const id of Object.keys(global)) allPluginIds.add(id);
|
|
60
|
-
} catch {
|
|
61
|
-
/* skip if unreadable */
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (projectPath) {
|
|
65
|
-
try {
|
|
66
|
-
const project = await getEnabledPlugins(projectPath);
|
|
67
|
-
for (const id of Object.keys(project)) allPluginIds.add(id);
|
|
68
|
-
} catch {
|
|
69
|
-
/* skip if unreadable */
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const local = await getLocalEnabledPlugins(projectPath);
|
|
74
|
-
for (const id of Object.keys(local)) allPluginIds.add(id);
|
|
75
|
-
} catch {
|
|
76
|
-
/* skip if unreadable */
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Parse marketplace names from plugin IDs
|
|
81
|
-
for (const pluginId of allPluginIds) {
|
|
82
|
-
const parsed = parsePluginId(pluginId);
|
|
83
|
-
if (parsed) {
|
|
84
|
-
marketplaceNames.add(parsed.marketplace);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return marketplaceNames;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
93
|
-
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
94
|
-
*
|
|
95
|
-
* IMPORTANT: Only uses `marketplace add` (never `marketplace update`).
|
|
96
|
-
* Claude Code's `marketplace update` calls cacheMarketplaceFromGit() which
|
|
97
|
-
* deletes the marketplace directory before re-cloning. If the clone fails
|
|
98
|
-
* (network timeout, auth error), the directory stays permanently deleted
|
|
99
|
-
* and ALL plugins from that marketplace break. See:
|
|
100
|
-
* ai-docs/plugin-marketplace-bug-investigation.md
|
|
101
|
-
*
|
|
102
|
-
* Claude Code's own background autoupdate handles marketplace refreshing
|
|
103
|
-
* after session start — claudeup should only recover genuinely missing
|
|
104
|
-
* marketplaces, not trigger additional refresh cycles.
|
|
105
|
-
*/
|
|
106
|
-
async function autoAddMissingMarketplaces(
|
|
107
|
-
projectPath?: string,
|
|
108
|
-
): Promise<string[]> {
|
|
109
|
-
const referenced = await getReferencedMarketplaces(projectPath);
|
|
110
|
-
const added: string[] = [];
|
|
111
|
-
|
|
112
|
-
for (const mpName of referenced) {
|
|
113
|
-
// Check if marketplace directory exists locally
|
|
114
|
-
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
115
|
-
if (await fs.pathExists(mpDir)) continue;
|
|
116
|
-
|
|
117
|
-
// Look up the repo URL from default marketplaces
|
|
118
|
-
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
119
|
-
if (!defaultMp?.source.repo) continue;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
await addMarketplace(defaultMp.source.repo);
|
|
123
|
-
added.push(mpName);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
console.warn(
|
|
126
|
-
`⚠ Failed to auto-add marketplace ${mpName}:`,
|
|
127
|
-
error instanceof Error ? error.message : "Unknown error",
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return added;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
30
|
const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
|
|
136
31
|
const CONTINUITY_PLUGIN_SCRIPT = path.join(
|
|
137
32
|
os.homedir(),
|
|
@@ -228,9 +123,15 @@ export async function prerunClaude(
|
|
|
228
123
|
|
|
229
124
|
// STEP 0.5: Auto-add missing marketplaces
|
|
230
125
|
// When plugins reference a marketplace that's not installed locally
|
|
231
|
-
// (e.g., settings synced from another machine
|
|
126
|
+
// (e.g., settings synced from another machine or committed by a
|
|
127
|
+
// teammate in .claude/settings.json), add it automatically. Pass
|
|
128
|
+
// process.cwd() so project and local enabledPlugins are scanned —
|
|
129
|
+
// otherwise a freshly cloned repo whose only @magus references live
|
|
130
|
+
// in project settings looks "clean" from the global-scope view.
|
|
232
131
|
if (await isClaudeAvailable()) {
|
|
233
|
-
const addedMarketplaces = await autoAddMissingMarketplaces(
|
|
132
|
+
const addedMarketplaces = await autoAddMissingMarketplaces(
|
|
133
|
+
process.cwd(),
|
|
134
|
+
);
|
|
234
135
|
if (addedMarketplaces.length > 0) {
|
|
235
136
|
console.log(
|
|
236
137
|
`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`,
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { which } from "../utils/command-utils.js";
|
|
14
|
-
import { removeGlobalInstalledPluginVersion, removeLocalInstalledPluginVersion, } from "./claude-settings.js";
|
|
14
|
+
import { isMarketplaceRegistered, removeGlobalInstalledPluginVersion, removeLocalInstalledPluginVersion, } from "./claude-settings.js";
|
|
15
15
|
import { removeInstalledPluginVersion } from "./plugin-manager.js";
|
|
16
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
/**
|
|
18
19
|
* Get the path to the claude CLI binary
|
|
@@ -48,13 +49,40 @@ async function execClaude(args, timeoutMs = 30000) {
|
|
|
48
49
|
throw new Error(msg);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Recover from a "not found in marketplace" error.
|
|
54
|
+
*
|
|
55
|
+
* Two distinct failure modes land in the same error string:
|
|
56
|
+
* 1. Marketplace is registered locally but stale (or uses the old
|
|
57
|
+
* "directory" source) — fix the registry and refresh the clone.
|
|
58
|
+
* 2. Marketplace is not registered at all — look up its repo in
|
|
59
|
+
* defaultMarketplaces and add it. This is the "new employee clones
|
|
60
|
+
* a project whose .claude/settings.json enables @magus plugins but
|
|
61
|
+
* magus was never registered on this machine" case.
|
|
62
|
+
*
|
|
63
|
+
* Returns true if recovery was attempted and a retry is worth trying.
|
|
64
|
+
*/
|
|
65
|
+
async function recoverMissingMarketplace(marketplace) {
|
|
66
|
+
if (await isMarketplaceRegistered(marketplace)) {
|
|
67
|
+
// Registered but stale — existing recovery path.
|
|
68
|
+
const { recoverMarketplaceSettings } = await import("./claude-settings.js");
|
|
69
|
+
await recoverMarketplaceSettings();
|
|
70
|
+
await updateMarketplace(marketplace);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === marketplace);
|
|
74
|
+
if (!defaultMp?.source.repo)
|
|
75
|
+
return false;
|
|
76
|
+
await addMarketplace(defaultMp.source.repo);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
51
79
|
/**
|
|
52
80
|
* Install a plugin using claude CLI
|
|
53
81
|
* Handles enabling + version tracking + cache copy in one shot.
|
|
54
82
|
*
|
|
55
83
|
* If the install fails because the plugin is "not found in marketplace",
|
|
56
|
-
*
|
|
57
|
-
*
|
|
84
|
+
* attempts to recover (add the marketplace if missing, or refresh it if
|
|
85
|
+
* stale) and retries once.
|
|
58
86
|
*/
|
|
59
87
|
export async function installPlugin(pluginId, scope = "user") {
|
|
60
88
|
try {
|
|
@@ -63,14 +91,8 @@ export async function installPlugin(pluginId, scope = "user") {
|
|
|
63
91
|
catch (error) {
|
|
64
92
|
const msg = error instanceof Error ? error.message : String(error);
|
|
65
93
|
if (msg.includes("not found in marketplace")) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (marketplace) {
|
|
69
|
-
// Fix known_marketplaces.json first (stale "directory" → "github"),
|
|
70
|
-
// then update, then retry
|
|
71
|
-
const { recoverMarketplaceSettings } = await import("./claude-settings.js");
|
|
72
|
-
await recoverMarketplaceSettings();
|
|
73
|
-
await updateMarketplace(marketplace);
|
|
94
|
+
const marketplace = pluginId.split("@")[1];
|
|
95
|
+
if (marketplace && (await recoverMissingMarketplace(marketplace))) {
|
|
74
96
|
await execClaude([
|
|
75
97
|
"plugin",
|
|
76
98
|
"install",
|
|
@@ -135,12 +157,8 @@ export async function updatePlugin(pluginId, scope = "user") {
|
|
|
135
157
|
catch (error) {
|
|
136
158
|
const msg = error instanceof Error ? error.message : String(error);
|
|
137
159
|
if (msg.includes("not found in marketplace")) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (marketplace) {
|
|
141
|
-
const { recoverMarketplaceSettings } = await import("./claude-settings.js");
|
|
142
|
-
await recoverMarketplaceSettings();
|
|
143
|
-
await updateMarketplace(marketplace);
|
|
160
|
+
const marketplace = pluginId.split("@")[1];
|
|
161
|
+
if (marketplace && (await recoverMissingMarketplace(marketplace))) {
|
|
144
162
|
await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
|
|
145
163
|
return;
|
|
146
164
|
}
|
|
@@ -13,10 +13,12 @@ import { execFile } from "node:child_process";
|
|
|
13
13
|
import { promisify } from "node:util";
|
|
14
14
|
import { which } from "../utils/command-utils.js";
|
|
15
15
|
import {
|
|
16
|
+
isMarketplaceRegistered,
|
|
16
17
|
removeGlobalInstalledPluginVersion,
|
|
17
18
|
removeLocalInstalledPluginVersion,
|
|
18
19
|
} from "./claude-settings.js";
|
|
19
20
|
import { removeInstalledPluginVersion } from "./plugin-manager.js";
|
|
21
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
20
22
|
|
|
21
23
|
const execFileAsync = promisify(execFile);
|
|
22
24
|
|
|
@@ -65,13 +67,46 @@ async function execClaude(args: string[], timeoutMs = 30000): Promise<string> {
|
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Recover from a "not found in marketplace" error.
|
|
72
|
+
*
|
|
73
|
+
* Two distinct failure modes land in the same error string:
|
|
74
|
+
* 1. Marketplace is registered locally but stale (or uses the old
|
|
75
|
+
* "directory" source) — fix the registry and refresh the clone.
|
|
76
|
+
* 2. Marketplace is not registered at all — look up its repo in
|
|
77
|
+
* defaultMarketplaces and add it. This is the "new employee clones
|
|
78
|
+
* a project whose .claude/settings.json enables @magus plugins but
|
|
79
|
+
* magus was never registered on this machine" case.
|
|
80
|
+
*
|
|
81
|
+
* Returns true if recovery was attempted and a retry is worth trying.
|
|
82
|
+
*/
|
|
83
|
+
async function recoverMissingMarketplace(
|
|
84
|
+
marketplace: string,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
if (await isMarketplaceRegistered(marketplace)) {
|
|
87
|
+
// Registered but stale — existing recovery path.
|
|
88
|
+
const { recoverMarketplaceSettings } = await import(
|
|
89
|
+
"./claude-settings.js"
|
|
90
|
+
);
|
|
91
|
+
await recoverMarketplaceSettings();
|
|
92
|
+
await updateMarketplace(marketplace);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === marketplace);
|
|
97
|
+
if (!defaultMp?.source.repo) return false;
|
|
98
|
+
|
|
99
|
+
await addMarketplace(defaultMp.source.repo);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
/**
|
|
69
104
|
* Install a plugin using claude CLI
|
|
70
105
|
* Handles enabling + version tracking + cache copy in one shot.
|
|
71
106
|
*
|
|
72
107
|
* If the install fails because the plugin is "not found in marketplace",
|
|
73
|
-
*
|
|
74
|
-
*
|
|
108
|
+
* attempts to recover (add the marketplace if missing, or refresh it if
|
|
109
|
+
* stale) and retries once.
|
|
75
110
|
*/
|
|
76
111
|
export async function installPlugin(
|
|
77
112
|
pluginId: string,
|
|
@@ -83,16 +118,8 @@ export async function installPlugin(
|
|
|
83
118
|
const msg =
|
|
84
119
|
error instanceof Error ? error.message : String(error);
|
|
85
120
|
if (msg.includes("not found in marketplace")) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
if (marketplace) {
|
|
89
|
-
// Fix known_marketplaces.json first (stale "directory" → "github"),
|
|
90
|
-
// then update, then retry
|
|
91
|
-
const { recoverMarketplaceSettings } = await import(
|
|
92
|
-
"./claude-settings.js"
|
|
93
|
-
);
|
|
94
|
-
await recoverMarketplaceSettings();
|
|
95
|
-
await updateMarketplace(marketplace);
|
|
121
|
+
const marketplace = pluginId.split("@")[1];
|
|
122
|
+
if (marketplace && (await recoverMissingMarketplace(marketplace))) {
|
|
96
123
|
await execClaude([
|
|
97
124
|
"plugin",
|
|
98
125
|
"install",
|
|
@@ -174,14 +201,8 @@ export async function updatePlugin(
|
|
|
174
201
|
const msg =
|
|
175
202
|
error instanceof Error ? error.message : String(error);
|
|
176
203
|
if (msg.includes("not found in marketplace")) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
if (marketplace) {
|
|
180
|
-
const { recoverMarketplaceSettings } = await import(
|
|
181
|
-
"./claude-settings.js"
|
|
182
|
-
);
|
|
183
|
-
await recoverMarketplaceSettings();
|
|
184
|
-
await updateMarketplace(marketplace);
|
|
204
|
+
const marketplace = pluginId.split("@")[1];
|
|
205
|
+
if (marketplace && (await recoverMissingMarketplace(marketplace))) {
|
|
185
206
|
await execClaude(
|
|
186
207
|
["plugin", "install", pluginId, "--scope", scope],
|
|
187
208
|
60000,
|
|
@@ -1014,6 +1014,20 @@ async function readKnownMarketplaces() {
|
|
|
1014
1014
|
}
|
|
1015
1015
|
return {};
|
|
1016
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Check whether a marketplace is registered locally. A marketplace is
|
|
1019
|
+
* considered registered when it has an entry in known_marketplaces.json
|
|
1020
|
+
* AND its clone directory exists on disk. Either condition alone is not
|
|
1021
|
+
* enough: an entry with a missing directory means the clone was lost; a
|
|
1022
|
+
* directory without an entry means Claude Code doesn't know about it.
|
|
1023
|
+
*/
|
|
1024
|
+
export async function isMarketplaceRegistered(name) {
|
|
1025
|
+
const known = await readKnownMarketplaces();
|
|
1026
|
+
if (!known[name])
|
|
1027
|
+
return false;
|
|
1028
|
+
const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
1029
|
+
return fs.pathExists(path.join(marketplacesDir, name));
|
|
1030
|
+
}
|
|
1017
1031
|
/**
|
|
1018
1032
|
* Get the source path for a plugin from its marketplace
|
|
1019
1033
|
* For directory-based marketplaces, returns the local directory path
|
|
@@ -1325,6 +1325,27 @@ async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
|
|
|
1325
1325
|
return {};
|
|
1326
1326
|
}
|
|
1327
1327
|
|
|
1328
|
+
/**
|
|
1329
|
+
* Check whether a marketplace is registered locally. A marketplace is
|
|
1330
|
+
* considered registered when it has an entry in known_marketplaces.json
|
|
1331
|
+
* AND its clone directory exists on disk. Either condition alone is not
|
|
1332
|
+
* enough: an entry with a missing directory means the clone was lost; a
|
|
1333
|
+
* directory without an entry means Claude Code doesn't know about it.
|
|
1334
|
+
*/
|
|
1335
|
+
export async function isMarketplaceRegistered(
|
|
1336
|
+
name: string,
|
|
1337
|
+
): Promise<boolean> {
|
|
1338
|
+
const known = await readKnownMarketplaces();
|
|
1339
|
+
if (!known[name]) return false;
|
|
1340
|
+
const marketplacesDir = path.join(
|
|
1341
|
+
os.homedir(),
|
|
1342
|
+
".claude",
|
|
1343
|
+
"plugins",
|
|
1344
|
+
"marketplaces",
|
|
1345
|
+
);
|
|
1346
|
+
return fs.pathExists(path.join(marketplacesDir, name));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1328
1349
|
/**
|
|
1329
1350
|
* Get the source path for a plugin from its marketplace
|
|
1330
1351
|
* For directory-based marketplaces, returns the local directory path
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace sync service
|
|
3
|
+
*
|
|
4
|
+
* Auto-registers marketplaces that are referenced by `enabledPlugins`
|
|
5
|
+
* in any settings scope (global, project, local) but are not installed
|
|
6
|
+
* locally. Fixes the "new employee clones repo with committed .claude/
|
|
7
|
+
* settings but has never run `claude plugin marketplace add`" scenario.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: Only uses `marketplace add` (never `marketplace update`).
|
|
10
|
+
* Claude Code's `marketplace update` calls cacheMarketplaceFromGit()
|
|
11
|
+
* which deletes the marketplace directory before re-cloning. If the
|
|
12
|
+
* clone fails (network, auth, timeout), the directory stays permanently
|
|
13
|
+
* deleted. See ai-docs/plugin-marketplace-bug-investigation.md.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "fs-extra";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, isMarketplaceRegistered, } from "./claude-settings.js";
|
|
19
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
20
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
21
|
+
import { addMarketplace } from "./claude-cli.js";
|
|
22
|
+
const MARKETPLACES_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
23
|
+
/**
|
|
24
|
+
* Collect unique marketplace names from enabled plugins across all settings
|
|
25
|
+
* scopes. When `projectPath` is provided, project and local scopes are
|
|
26
|
+
* included; otherwise only the global scope is scanned.
|
|
27
|
+
*/
|
|
28
|
+
export async function getReferencedMarketplaces(projectPath) {
|
|
29
|
+
const marketplaceNames = new Set();
|
|
30
|
+
const allPluginIds = new Set();
|
|
31
|
+
try {
|
|
32
|
+
const global = await getGlobalEnabledPlugins();
|
|
33
|
+
for (const id of Object.keys(global))
|
|
34
|
+
allPluginIds.add(id);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* skip if unreadable */
|
|
38
|
+
}
|
|
39
|
+
if (projectPath) {
|
|
40
|
+
try {
|
|
41
|
+
const project = await getEnabledPlugins(projectPath);
|
|
42
|
+
for (const id of Object.keys(project))
|
|
43
|
+
allPluginIds.add(id);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* skip if unreadable */
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const local = await getLocalEnabledPlugins(projectPath);
|
|
50
|
+
for (const id of Object.keys(local))
|
|
51
|
+
allPluginIds.add(id);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* skip if unreadable */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const pluginId of allPluginIds) {
|
|
58
|
+
const parsed = parsePluginId(pluginId);
|
|
59
|
+
if (parsed)
|
|
60
|
+
marketplaceNames.add(parsed.marketplace);
|
|
61
|
+
}
|
|
62
|
+
return marketplaceNames;
|
|
63
|
+
}
|
|
64
|
+
/** Re-exported for convenience — canonical implementation lives in claude-settings. */
|
|
65
|
+
export { isMarketplaceRegistered };
|
|
66
|
+
/**
|
|
67
|
+
* Register any referenced marketplaces that are missing locally.
|
|
68
|
+
* Looks up the GitHub repo in `defaultMarketplaces` and calls
|
|
69
|
+
* `claude plugin marketplace add` for each. Always writes to the global
|
|
70
|
+
* registry (`~/.claude/plugins/known_marketplaces.json`) — scope only
|
|
71
|
+
* controls which settings are scanned to build the reference list.
|
|
72
|
+
*
|
|
73
|
+
* Returns the names of marketplaces that were successfully added.
|
|
74
|
+
*/
|
|
75
|
+
export async function autoAddMissingMarketplaces(projectPath) {
|
|
76
|
+
const referenced = await getReferencedMarketplaces(projectPath);
|
|
77
|
+
const added = [];
|
|
78
|
+
for (const mpName of referenced) {
|
|
79
|
+
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
80
|
+
if (await fs.pathExists(mpDir))
|
|
81
|
+
continue;
|
|
82
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
83
|
+
if (!defaultMp?.source.repo)
|
|
84
|
+
continue;
|
|
85
|
+
try {
|
|
86
|
+
await addMarketplace(defaultMp.source.repo);
|
|
87
|
+
added.push(mpName);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.warn(`⚠ Failed to auto-add marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return added;
|
|
94
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace sync service
|
|
3
|
+
*
|
|
4
|
+
* Auto-registers marketplaces that are referenced by `enabledPlugins`
|
|
5
|
+
* in any settings scope (global, project, local) but are not installed
|
|
6
|
+
* locally. Fixes the "new employee clones repo with committed .claude/
|
|
7
|
+
* settings but has never run `claude plugin marketplace add`" scenario.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: Only uses `marketplace add` (never `marketplace update`).
|
|
10
|
+
* Claude Code's `marketplace update` calls cacheMarketplaceFromGit()
|
|
11
|
+
* which deletes the marketplace directory before re-cloning. If the
|
|
12
|
+
* clone fails (network, auth, timeout), the directory stays permanently
|
|
13
|
+
* deleted. See ai-docs/plugin-marketplace-bug-investigation.md.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "fs-extra";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import {
|
|
19
|
+
getGlobalEnabledPlugins,
|
|
20
|
+
getEnabledPlugins,
|
|
21
|
+
getLocalEnabledPlugins,
|
|
22
|
+
isMarketplaceRegistered,
|
|
23
|
+
} from "./claude-settings.js";
|
|
24
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
25
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
26
|
+
import { addMarketplace } from "./claude-cli.js";
|
|
27
|
+
|
|
28
|
+
const MARKETPLACES_DIR = path.join(
|
|
29
|
+
os.homedir(),
|
|
30
|
+
".claude",
|
|
31
|
+
"plugins",
|
|
32
|
+
"marketplaces",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Collect unique marketplace names from enabled plugins across all settings
|
|
37
|
+
* scopes. When `projectPath` is provided, project and local scopes are
|
|
38
|
+
* included; otherwise only the global scope is scanned.
|
|
39
|
+
*/
|
|
40
|
+
export async function getReferencedMarketplaces(
|
|
41
|
+
projectPath?: string,
|
|
42
|
+
): Promise<Set<string>> {
|
|
43
|
+
const marketplaceNames = new Set<string>();
|
|
44
|
+
const allPluginIds = new Set<string>();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const global = await getGlobalEnabledPlugins();
|
|
48
|
+
for (const id of Object.keys(global)) allPluginIds.add(id);
|
|
49
|
+
} catch {
|
|
50
|
+
/* skip if unreadable */
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (projectPath) {
|
|
54
|
+
try {
|
|
55
|
+
const project = await getEnabledPlugins(projectPath);
|
|
56
|
+
for (const id of Object.keys(project)) allPluginIds.add(id);
|
|
57
|
+
} catch {
|
|
58
|
+
/* skip if unreadable */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const local = await getLocalEnabledPlugins(projectPath);
|
|
63
|
+
for (const id of Object.keys(local)) allPluginIds.add(id);
|
|
64
|
+
} catch {
|
|
65
|
+
/* skip if unreadable */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const pluginId of allPluginIds) {
|
|
70
|
+
const parsed = parsePluginId(pluginId);
|
|
71
|
+
if (parsed) marketplaceNames.add(parsed.marketplace);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return marketplaceNames;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Re-exported for convenience — canonical implementation lives in claude-settings. */
|
|
78
|
+
export { isMarketplaceRegistered };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register any referenced marketplaces that are missing locally.
|
|
82
|
+
* Looks up the GitHub repo in `defaultMarketplaces` and calls
|
|
83
|
+
* `claude plugin marketplace add` for each. Always writes to the global
|
|
84
|
+
* registry (`~/.claude/plugins/known_marketplaces.json`) — scope only
|
|
85
|
+
* controls which settings are scanned to build the reference list.
|
|
86
|
+
*
|
|
87
|
+
* Returns the names of marketplaces that were successfully added.
|
|
88
|
+
*/
|
|
89
|
+
export async function autoAddMissingMarketplaces(
|
|
90
|
+
projectPath?: string,
|
|
91
|
+
): Promise<string[]> {
|
|
92
|
+
const referenced = await getReferencedMarketplaces(projectPath);
|
|
93
|
+
const added: string[] = [];
|
|
94
|
+
|
|
95
|
+
for (const mpName of referenced) {
|
|
96
|
+
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
97
|
+
if (await fs.pathExists(mpDir)) continue;
|
|
98
|
+
|
|
99
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
100
|
+
if (!defaultMp?.source.repo) continue;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await addMarketplace(defaultMp.source.repo);
|
|
104
|
+
added.push(mpName);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn(
|
|
107
|
+
`⚠ Failed to auto-add marketplace ${mpName}:`,
|
|
108
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return added;
|
|
114
|
+
}
|
package/src/ui/App.js
CHANGED
|
@@ -8,6 +8,8 @@ import { ModalContainer } from "./components/modals/index.js";
|
|
|
8
8
|
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
|
|
9
9
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
10
10
|
import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
|
|
11
|
+
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
12
|
+
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
11
13
|
import { checkPluginVersionMismatches, } from "../services/plugin-version-check.js";
|
|
12
14
|
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
13
15
|
import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
|
|
@@ -222,6 +224,23 @@ function AppContent({ onExit }) {
|
|
|
222
224
|
});
|
|
223
225
|
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
224
226
|
migrateMarketplaceRename().catch(() => { }); // non-blocking, best-effort
|
|
227
|
+
// Auto-add marketplaces referenced by enabledPlugins but missing from
|
|
228
|
+
// the local registry. Fixes the "fresh clone of a project whose
|
|
229
|
+
// .claude/settings.json enables @magus plugins but this machine has
|
|
230
|
+
// never run `claude plugin marketplace add`" case.
|
|
231
|
+
(async () => {
|
|
232
|
+
try {
|
|
233
|
+
if (!(await isClaudeAvailable()))
|
|
234
|
+
return;
|
|
235
|
+
const added = await autoAddMissingMarketplaces(process.cwd());
|
|
236
|
+
if (added.length > 0) {
|
|
237
|
+
setRecoveryReport(`added marketplace(s): ${added.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// non-fatal: user can still install marketplaces manually
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
225
244
|
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
226
245
|
recoverMarketplaceSettings()
|
|
227
246
|
.then(async (recovery) => {
|
package/src/ui/App.tsx
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
migrateMarketplaceRename,
|
|
29
29
|
recoverMarketplaceSettings,
|
|
30
30
|
} from "../services/claude-settings.js";
|
|
31
|
+
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
32
|
+
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
31
33
|
import {
|
|
32
34
|
checkPluginVersionMismatches,
|
|
33
35
|
type VersionMismatchInfo,
|
|
@@ -352,6 +354,22 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
352
354
|
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
353
355
|
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
354
356
|
|
|
357
|
+
// Auto-add marketplaces referenced by enabledPlugins but missing from
|
|
358
|
+
// the local registry. Fixes the "fresh clone of a project whose
|
|
359
|
+
// .claude/settings.json enables @magus plugins but this machine has
|
|
360
|
+
// never run `claude plugin marketplace add`" case.
|
|
361
|
+
(async () => {
|
|
362
|
+
try {
|
|
363
|
+
if (!(await isClaudeAvailable())) return;
|
|
364
|
+
const added = await autoAddMissingMarketplaces(process.cwd());
|
|
365
|
+
if (added.length > 0) {
|
|
366
|
+
setRecoveryReport(`added marketplace(s): ${added.join(", ")}`);
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// non-fatal: user can still install marketplaces manually
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
|
|
355
373
|
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
356
374
|
recoverMarketplaceSettings()
|
|
357
375
|
.then(async (recovery) => {
|