claudeup 3.5.0 → 3.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 +1 -1
- package/src/data/marketplaces.js +1 -0
- package/src/data/marketplaces.ts +1 -0
- package/src/prerunner/index.js +89 -11
- package/src/prerunner/index.ts +118 -11
- package/src/services/claude-cli.js +94 -0
- package/src/services/claude-cli.ts +132 -0
- package/src/services/claude-settings.js +65 -48
- package/src/services/claude-settings.ts +68 -52
- package/src/ui/App.js +1 -1
- package/src/ui/App.tsx +1 -1
- package/src/ui/screens/PluginsScreen.js +27 -95
- package/src/ui/screens/PluginsScreen.tsx +28 -117
package/package.json
CHANGED
package/src/data/marketplaces.js
CHANGED
package/src/data/marketplaces.ts
CHANGED
package/src/prerunner/index.js
CHANGED
|
@@ -1,7 +1,78 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
1
4
|
import { UpdateCache } from "../services/update-cache.js";
|
|
2
|
-
import { getAvailablePlugins, clearMarketplaceCache,
|
|
5
|
+
import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
|
|
3
6
|
import { runClaude } from "../services/claude-runner.js";
|
|
4
|
-
import { recoverMarketplaceSettings, migrateMarketplaceRename } from "../services/claude-settings.js";
|
|
7
|
+
import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, } from "../services/claude-settings.js";
|
|
8
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
9
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
10
|
+
import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
|
|
11
|
+
const MARKETPLACES_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
12
|
+
/**
|
|
13
|
+
* Collect all unique marketplace names from enabled plugins across all settings scopes.
|
|
14
|
+
* Returns a Set of marketplace names (e.g., "magus", "claude-plugins-official").
|
|
15
|
+
*/
|
|
16
|
+
async function getReferencedMarketplaces(projectPath) {
|
|
17
|
+
const marketplaceNames = new Set();
|
|
18
|
+
// Collect plugin IDs from all scopes
|
|
19
|
+
const allPluginIds = new Set();
|
|
20
|
+
try {
|
|
21
|
+
const global = await getGlobalEnabledPlugins();
|
|
22
|
+
for (const id of Object.keys(global))
|
|
23
|
+
allPluginIds.add(id);
|
|
24
|
+
}
|
|
25
|
+
catch { /* skip if unreadable */ }
|
|
26
|
+
if (projectPath) {
|
|
27
|
+
try {
|
|
28
|
+
const project = await getEnabledPlugins(projectPath);
|
|
29
|
+
for (const id of Object.keys(project))
|
|
30
|
+
allPluginIds.add(id);
|
|
31
|
+
}
|
|
32
|
+
catch { /* skip if unreadable */ }
|
|
33
|
+
try {
|
|
34
|
+
const local = await getLocalEnabledPlugins(projectPath);
|
|
35
|
+
for (const id of Object.keys(local))
|
|
36
|
+
allPluginIds.add(id);
|
|
37
|
+
}
|
|
38
|
+
catch { /* skip if unreadable */ }
|
|
39
|
+
}
|
|
40
|
+
// Parse marketplace names from plugin IDs
|
|
41
|
+
for (const pluginId of allPluginIds) {
|
|
42
|
+
const parsed = parsePluginId(pluginId);
|
|
43
|
+
if (parsed) {
|
|
44
|
+
marketplaceNames.add(parsed.marketplace);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return marketplaceNames;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
51
|
+
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
52
|
+
*/
|
|
53
|
+
async function autoAddMissingMarketplaces(projectPath) {
|
|
54
|
+
const referenced = await getReferencedMarketplaces(projectPath);
|
|
55
|
+
const added = [];
|
|
56
|
+
for (const mpName of referenced) {
|
|
57
|
+
// Check if marketplace directory exists locally
|
|
58
|
+
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
59
|
+
if (await fs.pathExists(mpDir))
|
|
60
|
+
continue;
|
|
61
|
+
// Look up the repo URL from default marketplaces
|
|
62
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
63
|
+
if (!defaultMp?.source.repo)
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
await addMarketplace(defaultMp.source.repo);
|
|
67
|
+
added.push(mpName);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Non-fatal: log and continue
|
|
71
|
+
console.warn(`⚠ Failed to auto-add marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return added;
|
|
75
|
+
}
|
|
5
76
|
/**
|
|
6
77
|
* Prerun orchestration: Check for updates, apply them, then run claude
|
|
7
78
|
* @param claudeArgs - Arguments to pass to claude CLI
|
|
@@ -11,13 +82,22 @@ import { recoverMarketplaceSettings, migrateMarketplaceRename } from "../service
|
|
|
11
82
|
export async function prerunClaude(claudeArgs, options = {}) {
|
|
12
83
|
const cache = new UpdateCache();
|
|
13
84
|
try {
|
|
14
|
-
// STEP 0: Migrate
|
|
85
|
+
// STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
|
|
15
86
|
const migration = await migrateMarketplaceRename();
|
|
16
87
|
const migTotal = migration.projectMigrated + migration.globalMigrated
|
|
17
88
|
+ migration.localMigrated + migration.registryMigrated
|
|
18
89
|
+ (migration.knownMarketplacesMigrated ? 1 : 0);
|
|
19
90
|
if (migTotal > 0) {
|
|
20
|
-
console.log(`✓ Migrated ${migTotal} plugin reference(s)
|
|
91
|
+
console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
|
|
92
|
+
}
|
|
93
|
+
// STEP 0.5: Auto-add missing marketplaces
|
|
94
|
+
// When plugins reference a marketplace that's not installed locally
|
|
95
|
+
// (e.g., settings synced from another machine), add it automatically.
|
|
96
|
+
if (await isClaudeAvailable()) {
|
|
97
|
+
const addedMarketplaces = await autoAddMissingMarketplaces();
|
|
98
|
+
if (addedMarketplaces.length > 0) {
|
|
99
|
+
console.log(`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`);
|
|
100
|
+
}
|
|
21
101
|
}
|
|
22
102
|
// STEP 1: Check if we should update (time-based cache, or forced)
|
|
23
103
|
const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
|
|
@@ -34,12 +114,12 @@ export async function prerunClaude(claudeArgs, options = {}) {
|
|
|
34
114
|
console.log(`✓ Removed stale marketplaces: ${recovery.removed.join(", ")}`);
|
|
35
115
|
}
|
|
36
116
|
// STEP 2: Clear cache to force fresh plugin info
|
|
37
|
-
// Note: Marketplace updates should be done via Claude Code's /plugin marketplace update
|
|
38
117
|
clearMarketplaceCache();
|
|
39
118
|
// STEP 3: Get updated plugin info (to detect versions)
|
|
40
119
|
const plugins = await getAvailablePlugins();
|
|
41
|
-
// STEP 4: Auto-update enabled plugins
|
|
120
|
+
// STEP 4: Auto-update enabled plugins via claude CLI
|
|
42
121
|
const autoUpdatedPlugins = [];
|
|
122
|
+
const cliAvailable = await isClaudeAvailable();
|
|
43
123
|
for (const plugin of plugins) {
|
|
44
124
|
// Only update if:
|
|
45
125
|
// 1. Plugin is enabled
|
|
@@ -50,11 +130,9 @@ export async function prerunClaude(claudeArgs, options = {}) {
|
|
|
50
130
|
plugin.installedVersion &&
|
|
51
131
|
plugin.version) {
|
|
52
132
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// 3. Copy plugin files to cache (via copyPluginToCache())
|
|
57
|
-
await saveInstalledPluginVersion(plugin.id, plugin.version);
|
|
133
|
+
if (cliAvailable) {
|
|
134
|
+
await updatePlugin(plugin.id, "user");
|
|
135
|
+
}
|
|
58
136
|
autoUpdatedPlugins.push({
|
|
59
137
|
pluginId: plugin.id,
|
|
60
138
|
oldVersion: plugin.installedVersion,
|
package/src/prerunner/index.ts
CHANGED
|
@@ -1,16 +1,112 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
1
4
|
import { UpdateCache } from "../services/update-cache.js";
|
|
2
5
|
import {
|
|
3
6
|
getAvailablePlugins,
|
|
4
7
|
clearMarketplaceCache,
|
|
5
|
-
saveInstalledPluginVersion,
|
|
6
8
|
} from "../services/plugin-manager.js";
|
|
7
9
|
import { runClaude } from "../services/claude-runner.js";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
recoverMarketplaceSettings,
|
|
12
|
+
migrateMarketplaceRename,
|
|
13
|
+
getGlobalEnabledPlugins,
|
|
14
|
+
getEnabledPlugins,
|
|
15
|
+
getLocalEnabledPlugins,
|
|
16
|
+
} from "../services/claude-settings.js";
|
|
17
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
18
|
+
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
19
|
+
import {
|
|
20
|
+
updatePlugin,
|
|
21
|
+
addMarketplace,
|
|
22
|
+
isClaudeAvailable,
|
|
23
|
+
} from "../services/claude-cli.js";
|
|
24
|
+
|
|
25
|
+
const MARKETPLACES_DIR = path.join(
|
|
26
|
+
os.homedir(),
|
|
27
|
+
".claude",
|
|
28
|
+
"plugins",
|
|
29
|
+
"marketplaces",
|
|
30
|
+
);
|
|
9
31
|
|
|
10
32
|
export interface PrerunOptions {
|
|
11
33
|
force?: boolean; // Bypass cache and force update check
|
|
12
34
|
}
|
|
13
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Collect all unique marketplace names from enabled plugins across all settings scopes.
|
|
38
|
+
* Returns a Set of marketplace names (e.g., "magus", "claude-plugins-official").
|
|
39
|
+
*/
|
|
40
|
+
async function getReferencedMarketplaces(
|
|
41
|
+
projectPath?: string,
|
|
42
|
+
): Promise<Set<string>> {
|
|
43
|
+
const marketplaceNames = new Set<string>();
|
|
44
|
+
|
|
45
|
+
// Collect plugin IDs from all scopes
|
|
46
|
+
const allPluginIds = new Set<string>();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const global = await getGlobalEnabledPlugins();
|
|
50
|
+
for (const id of Object.keys(global)) allPluginIds.add(id);
|
|
51
|
+
} catch { /* skip if unreadable */ }
|
|
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 { /* skip if unreadable */ }
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const local = await getLocalEnabledPlugins(projectPath);
|
|
61
|
+
for (const id of Object.keys(local)) allPluginIds.add(id);
|
|
62
|
+
} catch { /* skip if unreadable */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse marketplace names from plugin IDs
|
|
66
|
+
for (const pluginId of allPluginIds) {
|
|
67
|
+
const parsed = parsePluginId(pluginId);
|
|
68
|
+
if (parsed) {
|
|
69
|
+
marketplaceNames.add(parsed.marketplace);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return marketplaceNames;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check which referenced marketplaces are missing locally and auto-add them.
|
|
78
|
+
* Only adds marketplaces with known repos (from defaultMarketplaces).
|
|
79
|
+
*/
|
|
80
|
+
async function autoAddMissingMarketplaces(
|
|
81
|
+
projectPath?: string,
|
|
82
|
+
): Promise<string[]> {
|
|
83
|
+
const referenced = await getReferencedMarketplaces(projectPath);
|
|
84
|
+
const added: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const mpName of referenced) {
|
|
87
|
+
// Check if marketplace directory exists locally
|
|
88
|
+
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
89
|
+
if (await fs.pathExists(mpDir)) continue;
|
|
90
|
+
|
|
91
|
+
// Look up the repo URL from default marketplaces
|
|
92
|
+
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
93
|
+
if (!defaultMp?.source.repo) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await addMarketplace(defaultMp.source.repo);
|
|
97
|
+
added.push(mpName);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Non-fatal: log and continue
|
|
100
|
+
console.warn(
|
|
101
|
+
`⚠ Failed to auto-add marketplace ${mpName}:`,
|
|
102
|
+
error instanceof Error ? error.message : "Unknown error",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return added;
|
|
108
|
+
}
|
|
109
|
+
|
|
14
110
|
/**
|
|
15
111
|
* Prerun orchestration: Check for updates, apply them, then run claude
|
|
16
112
|
* @param claudeArgs - Arguments to pass to claude CLI
|
|
@@ -24,13 +120,25 @@ export async function prerunClaude(
|
|
|
24
120
|
const cache = new UpdateCache();
|
|
25
121
|
|
|
26
122
|
try {
|
|
27
|
-
// STEP 0: Migrate
|
|
123
|
+
// STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
|
|
28
124
|
const migration = await migrateMarketplaceRename();
|
|
29
125
|
const migTotal = migration.projectMigrated + migration.globalMigrated
|
|
30
126
|
+ migration.localMigrated + migration.registryMigrated
|
|
31
127
|
+ (migration.knownMarketplacesMigrated ? 1 : 0);
|
|
32
128
|
if (migTotal > 0) {
|
|
33
|
-
console.log(`✓ Migrated ${migTotal} plugin reference(s)
|
|
129
|
+
console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// STEP 0.5: Auto-add missing marketplaces
|
|
133
|
+
// When plugins reference a marketplace that's not installed locally
|
|
134
|
+
// (e.g., settings synced from another machine), add it automatically.
|
|
135
|
+
if (await isClaudeAvailable()) {
|
|
136
|
+
const addedMarketplaces = await autoAddMissingMarketplaces();
|
|
137
|
+
if (addedMarketplaces.length > 0) {
|
|
138
|
+
console.log(
|
|
139
|
+
`✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
34
142
|
}
|
|
35
143
|
|
|
36
144
|
// STEP 1: Check if we should update (time-based cache, or forced)
|
|
@@ -55,19 +163,20 @@ export async function prerunClaude(
|
|
|
55
163
|
}
|
|
56
164
|
|
|
57
165
|
// STEP 2: Clear cache to force fresh plugin info
|
|
58
|
-
// Note: Marketplace updates should be done via Claude Code's /plugin marketplace update
|
|
59
166
|
clearMarketplaceCache();
|
|
60
167
|
|
|
61
168
|
// STEP 3: Get updated plugin info (to detect versions)
|
|
62
169
|
const plugins = await getAvailablePlugins();
|
|
63
170
|
|
|
64
|
-
// STEP 4: Auto-update enabled plugins
|
|
171
|
+
// STEP 4: Auto-update enabled plugins via claude CLI
|
|
65
172
|
const autoUpdatedPlugins: Array<{
|
|
66
173
|
pluginId: string;
|
|
67
174
|
oldVersion: string;
|
|
68
175
|
newVersion: string;
|
|
69
176
|
}> = [];
|
|
70
177
|
|
|
178
|
+
const cliAvailable = await isClaudeAvailable();
|
|
179
|
+
|
|
71
180
|
for (const plugin of plugins) {
|
|
72
181
|
// Only update if:
|
|
73
182
|
// 1. Plugin is enabled
|
|
@@ -80,11 +189,9 @@ export async function prerunClaude(
|
|
|
80
189
|
plugin.version
|
|
81
190
|
) {
|
|
82
191
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// 3. Copy plugin files to cache (via copyPluginToCache())
|
|
87
|
-
await saveInstalledPluginVersion(plugin.id, plugin.version);
|
|
192
|
+
if (cliAvailable) {
|
|
193
|
+
await updatePlugin(plugin.id, "user");
|
|
194
|
+
}
|
|
88
195
|
|
|
89
196
|
autoUpdatedPlugins.push({
|
|
90
197
|
pluginId: plugin.id,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI wrapper service
|
|
3
|
+
*
|
|
4
|
+
* Delegates plugin and marketplace management to the `claude` CLI
|
|
5
|
+
* instead of manipulating settings JSON files directly.
|
|
6
|
+
*
|
|
7
|
+
* CLI commands used:
|
|
8
|
+
* - claude plugin install/uninstall/enable/disable/update
|
|
9
|
+
* - claude marketplace add
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { which } from "../utils/command-utils.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
/**
|
|
16
|
+
* Get the path to the claude CLI binary
|
|
17
|
+
* @throws Error if claude CLI is not found in PATH
|
|
18
|
+
*/
|
|
19
|
+
async function getClaudePath() {
|
|
20
|
+
const claudePath = await which("claude");
|
|
21
|
+
if (!claudePath) {
|
|
22
|
+
throw new Error("claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code");
|
|
23
|
+
}
|
|
24
|
+
return claudePath;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute a claude CLI command and return stdout
|
|
28
|
+
* @param args - Arguments to pass to claude
|
|
29
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30s)
|
|
30
|
+
* @returns stdout from the command
|
|
31
|
+
*/
|
|
32
|
+
async function execClaude(args, timeoutMs = 30000) {
|
|
33
|
+
const claudePath = await getClaudePath();
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync(claudePath, args, {
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
});
|
|
38
|
+
return stdout.trim();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const execError = error;
|
|
42
|
+
const msg = execError.stderr?.trim() ||
|
|
43
|
+
execError.stdout?.trim() ||
|
|
44
|
+
execError.message ||
|
|
45
|
+
"claude command failed";
|
|
46
|
+
throw new Error(msg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Install a plugin using claude CLI
|
|
51
|
+
* Handles enabling + version tracking + cache copy in one shot
|
|
52
|
+
*/
|
|
53
|
+
export async function installPlugin(pluginId, scope = "user") {
|
|
54
|
+
await execClaude(["plugin", "install", pluginId, "--scope", scope]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Uninstall a plugin using claude CLI
|
|
58
|
+
* Handles disabling + version removal in one shot
|
|
59
|
+
*/
|
|
60
|
+
export async function uninstallPlugin(pluginId, scope = "user") {
|
|
61
|
+
await execClaude(["plugin", "uninstall", pluginId, "--scope", scope]);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Enable a previously disabled plugin
|
|
65
|
+
*/
|
|
66
|
+
export async function enablePlugin(pluginId, scope = "user") {
|
|
67
|
+
await execClaude(["plugin", "enable", pluginId, "--scope", scope]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Disable a plugin without uninstalling it
|
|
71
|
+
*/
|
|
72
|
+
export async function disablePlugin(pluginId, scope = "user") {
|
|
73
|
+
await execClaude(["plugin", "disable", pluginId, "--scope", scope]);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Update a plugin to the latest version
|
|
77
|
+
*/
|
|
78
|
+
export async function updatePlugin(pluginId, scope = "user") {
|
|
79
|
+
await execClaude(["plugin", "update", pluginId, "--scope", scope], 60000);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Add a marketplace by GitHub repo (e.g., "MadAppGang/magus")
|
|
83
|
+
* Uses longer timeout since this involves cloning a git repo
|
|
84
|
+
*/
|
|
85
|
+
export async function addMarketplace(repo) {
|
|
86
|
+
await execClaude(["plugin", "marketplace", "add", repo], 60000);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if the claude CLI is available
|
|
90
|
+
* @returns true if claude CLI is found in PATH
|
|
91
|
+
*/
|
|
92
|
+
export async function isClaudeAvailable() {
|
|
93
|
+
return (await which("claude")) !== null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI wrapper service
|
|
3
|
+
*
|
|
4
|
+
* Delegates plugin and marketplace management to the `claude` CLI
|
|
5
|
+
* instead of manipulating settings JSON files directly.
|
|
6
|
+
*
|
|
7
|
+
* CLI commands used:
|
|
8
|
+
* - claude plugin install/uninstall/enable/disable/update
|
|
9
|
+
* - claude marketplace add
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { which } from "../utils/command-utils.js";
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
export type PluginScope = "user" | "project" | "local";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the path to the claude CLI binary
|
|
22
|
+
* @throws Error if claude CLI is not found in PATH
|
|
23
|
+
*/
|
|
24
|
+
async function getClaudePath(): Promise<string> {
|
|
25
|
+
const claudePath = await which("claude");
|
|
26
|
+
if (!claudePath) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"claude CLI not found in PATH. Install with: npm install -g @anthropic-ai/claude-code",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return claudePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute a claude CLI command and return stdout
|
|
36
|
+
* @param args - Arguments to pass to claude
|
|
37
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30s)
|
|
38
|
+
* @returns stdout from the command
|
|
39
|
+
*/
|
|
40
|
+
async function execClaude(
|
|
41
|
+
args: string[],
|
|
42
|
+
timeoutMs = 30000,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
const claudePath = await getClaudePath();
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await execFileAsync(claudePath, args, {
|
|
47
|
+
timeout: timeoutMs,
|
|
48
|
+
});
|
|
49
|
+
return stdout.trim();
|
|
50
|
+
} catch (error: unknown) {
|
|
51
|
+
const execError = error as {
|
|
52
|
+
stderr?: string;
|
|
53
|
+
stdout?: string;
|
|
54
|
+
message?: string;
|
|
55
|
+
code?: number;
|
|
56
|
+
};
|
|
57
|
+
const msg =
|
|
58
|
+
execError.stderr?.trim() ||
|
|
59
|
+
execError.stdout?.trim() ||
|
|
60
|
+
execError.message ||
|
|
61
|
+
"claude command failed";
|
|
62
|
+
throw new Error(msg);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Install a plugin using claude CLI
|
|
68
|
+
* Handles enabling + version tracking + cache copy in one shot
|
|
69
|
+
*/
|
|
70
|
+
export async function installPlugin(
|
|
71
|
+
pluginId: string,
|
|
72
|
+
scope: PluginScope = "user",
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
await execClaude(["plugin", "install", pluginId, "--scope", scope]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Uninstall a plugin using claude CLI
|
|
79
|
+
* Handles disabling + version removal in one shot
|
|
80
|
+
*/
|
|
81
|
+
export async function uninstallPlugin(
|
|
82
|
+
pluginId: string,
|
|
83
|
+
scope: PluginScope = "user",
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
await execClaude(["plugin", "uninstall", pluginId, "--scope", scope]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Enable a previously disabled plugin
|
|
90
|
+
*/
|
|
91
|
+
export async function enablePlugin(
|
|
92
|
+
pluginId: string,
|
|
93
|
+
scope: PluginScope = "user",
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await execClaude(["plugin", "enable", pluginId, "--scope", scope]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Disable a plugin without uninstalling it
|
|
100
|
+
*/
|
|
101
|
+
export async function disablePlugin(
|
|
102
|
+
pluginId: string,
|
|
103
|
+
scope: PluginScope = "user",
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
await execClaude(["plugin", "disable", pluginId, "--scope", scope]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update a plugin to the latest version
|
|
110
|
+
*/
|
|
111
|
+
export async function updatePlugin(
|
|
112
|
+
pluginId: string,
|
|
113
|
+
scope: PluginScope = "user",
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
await execClaude(["plugin", "update", pluginId, "--scope", scope], 60000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Add a marketplace by GitHub repo (e.g., "MadAppGang/magus")
|
|
120
|
+
* Uses longer timeout since this involves cloning a git repo
|
|
121
|
+
*/
|
|
122
|
+
export async function addMarketplace(repo: string): Promise<void> {
|
|
123
|
+
await execClaude(["plugin", "marketplace", "add", repo], 60000);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if the claude CLI is available
|
|
128
|
+
* @returns true if claude CLI is found in PATH
|
|
129
|
+
*/
|
|
130
|
+
export async function isClaudeAvailable(): Promise<boolean> {
|
|
131
|
+
return (await which("claude")) !== null;
|
|
132
|
+
}
|