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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -17,6 +17,7 @@ function normalizeRepo(repo) {
17
17
  */
18
18
  export const deprecatedMarketplaces = {
19
19
  "mag-claude-plugins": "magus",
20
+ "MadAppGang-claude-code": "magus",
20
21
  };
21
22
  export const defaultMarketplaces = [
22
23
  {
@@ -22,6 +22,7 @@ function normalizeRepo(repo: string): string {
22
22
  */
23
23
  export const deprecatedMarketplaces: Record<string, string> = {
24
24
  "mag-claude-plugins": "magus",
25
+ "MadAppGang-claude-code": "magus",
25
26
  };
26
27
 
27
28
  export const defaultMarketplaces: Marketplace[] = [
@@ -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, saveInstalledPluginVersion, } from "../services/plugin-manager.js";
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 mag-claude-plugins → magus (idempotent, no-ops if already migrated)
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) from mag-claude-plugins → magus`);
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 with available updates
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
- // Save new version - this will:
54
- // 1. Update settings.json
55
- // 2. Call updateInstalledPluginsRegistry()
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,
@@ -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 { recoverMarketplaceSettings, migrateMarketplaceRename } from "../services/claude-settings.js";
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 mag-claude-plugins → magus (idempotent, no-ops if already migrated)
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) from mag-claude-plugins → magus`);
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 with available updates
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
- // Save new version - this will:
84
- // 1. Update settings.json
85
- // 2. Call updateInstalledPluginsRegistry()
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
+ }