claudeup 4.19.0 → 4.22.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.
Files changed (180) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  18. package/src/__tests__/plugin-requires.test.ts +133 -0
  19. package/src/__tests__/plugin-setup.test.ts +7 -0
  20. package/src/__tests__/profile-command.test.ts +116 -0
  21. package/src/__tests__/profile-materializer.test.ts +82 -0
  22. package/src/__tests__/profile-sync.test.ts +136 -0
  23. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  24. package/src/__tests__/resolve-executable.test.ts +42 -0
  25. package/src/__tests__/resolver.test.ts +218 -0
  26. package/src/__tests__/symlink-manager.test.ts +99 -0
  27. package/src/__tests__/toolchain.test.ts +56 -0
  28. package/src/cli/claude.ts +21 -0
  29. package/src/cli/doctor.ts +132 -0
  30. package/src/cli/install.ts +360 -0
  31. package/src/cli/profile.ts +166 -0
  32. package/src/cli/router.ts +107 -0
  33. package/src/cli/update.ts +85 -0
  34. package/src/data/cli-tools.ts +7 -7
  35. package/src/data/gitignore-defaults.ts +4 -0
  36. package/src/data/marketplaces.ts +12 -0
  37. package/src/data/predefined-profiles.ts +3 -4
  38. package/src/main.tsx +11 -154
  39. package/src/prerunner/index.ts +62 -27
  40. package/src/services/claude-cli.ts +19 -22
  41. package/src/services/claude-settings.ts +99 -26
  42. package/src/services/conventions-manager.ts +865 -0
  43. package/src/services/doctor-bins.ts +49 -0
  44. package/src/services/doctor.ts +509 -0
  45. package/src/services/install-plan.ts +107 -0
  46. package/src/services/manifest.ts +166 -0
  47. package/src/services/marketplace-refresh.ts +173 -0
  48. package/src/services/plugin-manager.ts +7 -1
  49. package/src/services/plugin-requires.ts +215 -0
  50. package/src/services/plugin-version-check.ts +9 -4
  51. package/src/services/profile-materializer.ts +61 -0
  52. package/src/services/profile-sync.ts +150 -0
  53. package/src/services/resolver.ts +293 -0
  54. package/src/services/symlink-manager.ts +146 -0
  55. package/src/services/toolchain.ts +97 -0
  56. package/src/services/version-check.ts +10 -11
  57. package/src/types/index.ts +138 -31
  58. package/src/ui/App.tsx +0 -4
  59. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  60. package/src/ui/screens/index.ts +0 -1
  61. package/src/ui/state/reducer.ts +0 -101
  62. package/src/ui/state/types.ts +0 -35
  63. package/src/utils/command-utils.ts +23 -0
  64. package/src/utils/file-locking.ts +144 -0
  65. package/src/data/alias-flags.js +0 -205
  66. package/src/data/cli-tools.js +0 -123
  67. package/src/data/gitignore-defaults.js +0 -24
  68. package/src/data/gitignore-reasons.js +0 -97
  69. package/src/data/gitignore-templates.js +0 -21
  70. package/src/data/marketplaces.js +0 -138
  71. package/src/data/mcp-servers.js +0 -509
  72. package/src/data/predefined-profiles.js +0 -248
  73. package/src/data/settings-catalog.js +0 -625
  74. package/src/data/skill-repos.js +0 -160
  75. package/src/data/statuslines.js +0 -159
  76. package/src/data/statuslines.ts +0 -188
  77. package/src/index.js +0 -4
  78. package/src/index.ts +0 -5
  79. package/src/main.js +0 -170
  80. package/src/prerunner/index.js +0 -252
  81. package/src/services/alias-settings.js +0 -51
  82. package/src/services/alias-shell-writer.js +0 -1018
  83. package/src/services/alias-store.js +0 -129
  84. package/src/services/claude-cli.js +0 -189
  85. package/src/services/claude-runner.js +0 -28
  86. package/src/services/claude-settings.js +0 -1223
  87. package/src/services/gitignore-detector.js +0 -155
  88. package/src/services/gitignore-fixer.js +0 -231
  89. package/src/services/gitignore-prerun.js +0 -46
  90. package/src/services/gitignore-resolver.js +0 -143
  91. package/src/services/gitignore-service.js +0 -117
  92. package/src/services/local-marketplace.js +0 -339
  93. package/src/services/marketplace-fetcher.js +0 -96
  94. package/src/services/marketplace-sync.js +0 -94
  95. package/src/services/mcp-registry.js +0 -87
  96. package/src/services/plugin-manager.js +0 -473
  97. package/src/services/plugin-mcp-config.js +0 -177
  98. package/src/services/plugin-setup.js +0 -499
  99. package/src/services/plugin-version-check.js +0 -262
  100. package/src/services/profiles.js +0 -161
  101. package/src/services/settings-manager.js +0 -243
  102. package/src/services/skills-manager.js +0 -393
  103. package/src/services/skillsmp-client.js +0 -87
  104. package/src/services/update-cache.js +0 -52
  105. package/src/services/version-check.js +0 -99
  106. package/src/types/gitignore.js +0 -6
  107. package/src/types/index.js +0 -1
  108. package/src/ui/App.js +0 -355
  109. package/src/ui/adapters/pluginsAdapter.js +0 -139
  110. package/src/ui/adapters/settingsAdapter.js +0 -111
  111. package/src/ui/adapters/skillsAdapter.js +0 -154
  112. package/src/ui/components/CategoryHeader.js +0 -9
  113. package/src/ui/components/EmptyFilterState.js +0 -4
  114. package/src/ui/components/FlagDetailEditor.js +0 -0
  115. package/src/ui/components/ScopeIndicator.js +0 -30
  116. package/src/ui/components/ScrollableList.js +0 -36
  117. package/src/ui/components/SearchInput.js +0 -19
  118. package/src/ui/components/StyledText.js +0 -39
  119. package/src/ui/components/TabBar.js +0 -19
  120. package/src/ui/components/layout/FooterHints.js +0 -29
  121. package/src/ui/components/layout/Panel.js +0 -6
  122. package/src/ui/components/layout/ProgressBar.js +0 -14
  123. package/src/ui/components/layout/ScopeTabs.js +0 -6
  124. package/src/ui/components/layout/ScreenLayout.js +0 -16
  125. package/src/ui/components/layout/index.js +0 -5
  126. package/src/ui/components/modals/ConfirmModal.js +0 -14
  127. package/src/ui/components/modals/InputModal.js +0 -5
  128. package/src/ui/components/modals/LoadingModal.js +0 -17
  129. package/src/ui/components/modals/MessageModal.js +0 -16
  130. package/src/ui/components/modals/ModalContainer.js +0 -137
  131. package/src/ui/components/modals/SelectModal.js +0 -18
  132. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  133. package/src/ui/components/modals/index.js +0 -6
  134. package/src/ui/components/primitives/ActionHints.js +0 -13
  135. package/src/ui/components/primitives/DetailSection.js +0 -7
  136. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  137. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  138. package/src/ui/components/primitives/MetaText.js +0 -8
  139. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  140. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  141. package/src/ui/components/primitives/SelectableRow.js +0 -5
  142. package/src/ui/components/primitives/index.js +0 -8
  143. package/src/ui/hooks/index.js +0 -4
  144. package/src/ui/hooks/useAsyncData.js +0 -77
  145. package/src/ui/hooks/useGitignoreModal.js +0 -74
  146. package/src/ui/hooks/useKeyboard.js +0 -13
  147. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  148. package/src/ui/hooks/useMismatchModal.js +0 -77
  149. package/src/ui/registry.js +0 -1
  150. package/src/ui/renderers/cliToolRenderers.js +0 -54
  151. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  152. package/src/ui/renderers/mcpRenderers.js +0 -26
  153. package/src/ui/renderers/pluginRenderers.js +0 -124
  154. package/src/ui/renderers/profileRenderers.js +0 -177
  155. package/src/ui/renderers/settingsRenderers.js +0 -73
  156. package/src/ui/renderers/skillRenderers.js +0 -138
  157. package/src/ui/screens/AliasScreen.js +0 -1111
  158. package/src/ui/screens/CliToolsScreen.js +0 -338
  159. package/src/ui/screens/EnvVarsScreen.js +0 -152
  160. package/src/ui/screens/GitignoreScreen.js +0 -328
  161. package/src/ui/screens/McpRegistryScreen.js +0 -238
  162. package/src/ui/screens/McpScreen.js +0 -176
  163. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  164. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  165. package/src/ui/screens/PluginsScreen.js +0 -769
  166. package/src/ui/screens/ProfilesScreen.js +0 -298
  167. package/src/ui/screens/SkillsScreen.js +0 -549
  168. package/src/ui/screens/StatusLineScreen.js +0 -206
  169. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  170. package/src/ui/screens/index.js +0 -10
  171. package/src/ui/state/AnimationContext.js +0 -34
  172. package/src/ui/state/AppContext.js +0 -162
  173. package/src/ui/state/DimensionsContext.js +0 -71
  174. package/src/ui/state/reducer.js +0 -547
  175. package/src/ui/state/types.js +0 -1
  176. package/src/ui/theme.js +0 -47
  177. package/src/utils/clipboard.js +0 -56
  178. package/src/utils/command-utils.js +0 -19
  179. package/src/utils/fuzzy-search.js +0 -101
  180. package/src/utils/string-utils.js +0 -62
@@ -0,0 +1,107 @@
1
+ /**
2
+ * claudeup subcommand router.
3
+ *
4
+ * Parses argv and dispatches to a command handler. Commands run to completion
5
+ * and return an exit code; bare invocation (or an unknown token) falls through
6
+ * to the interactive TUI, preserving claudeup's historical behavior.
7
+ */
8
+
9
+ import { checkForUpdates } from "../services/version-check.js";
10
+ import { runClaudeCommand } from "./claude.js";
11
+ import { runUpdateCommand } from "./update.js";
12
+ import { runInstallCommand } from "./install.js";
13
+ import { runProfileCommand } from "./profile.js";
14
+ import { runDoctorCommand } from "./doctor.js";
15
+
16
+ export interface RouteOutcome {
17
+ /** A subcommand (or top-level flag) handled the invocation. */
18
+ handled: boolean;
19
+ /** Exit code to use when `handled` is true. */
20
+ exitCode?: number;
21
+ /** No subcommand matched — the caller should launch the TUI. */
22
+ launchTui: boolean;
23
+ }
24
+
25
+ const HANDLED = (exitCode: number): RouteOutcome => ({
26
+ handled: true,
27
+ exitCode,
28
+ launchTui: false,
29
+ });
30
+ const LAUNCH_TUI: RouteOutcome = { handled: false, launchTui: true };
31
+
32
+ export async function route(
33
+ argv: string[],
34
+ version: string,
35
+ ): Promise<RouteOutcome> {
36
+ // Top-level flags take precedence over any command token.
37
+ if (argv.includes("--version") || argv.includes("-v")) {
38
+ console.log(`claudeup v${version}`);
39
+ const result = await checkForUpdates();
40
+ if (result.updateAvailable) {
41
+ console.log(`\nUpdate available: v${result.latestVersion}`);
42
+ console.log("Run: claudeup update");
43
+ }
44
+ return HANDLED(0);
45
+ }
46
+
47
+ if (argv.includes("--help") || argv.includes("-h")) {
48
+ printHelp(version);
49
+ return HANDLED(0);
50
+ }
51
+
52
+ const [command, ...rest] = argv;
53
+
54
+ switch (command) {
55
+ case "claude":
56
+ return HANDLED(await runClaudeCommand(rest));
57
+ case "update":
58
+ return HANDLED(await runUpdateCommand());
59
+ case "install":
60
+ return HANDLED(await runInstallCommand(rest));
61
+ case "profile":
62
+ return HANDLED(await runProfileCommand(rest));
63
+ case "doctor":
64
+ return HANDLED(await runDoctorCommand(rest));
65
+ default:
66
+ // Bare `claudeup` or an unrecognized token → interactive TUI.
67
+ return LAUNCH_TUI;
68
+ }
69
+ }
70
+
71
+ function printHelp(version: string): void {
72
+ console.log(`claudeup v${version}
73
+
74
+ TUI tool for managing Claude Code plugins, MCPs, and configuration.
75
+
76
+ Usage: claudeup [options]
77
+ claudeup claude [args...] Run claude with auto-update prerunner
78
+ claudeup update Update claudeup to latest version
79
+
80
+ Options:
81
+ -v, --version Show version and check for updates
82
+ -h, --help Show this help message
83
+ --no-refresh Skip auto-refresh of marketplaces on startup
84
+
85
+ Commands:
86
+ claude [args...] Check for plugin updates (1h cache), then run claude
87
+ -f, --force Force update check (bypass 1h cache)
88
+ update Update claudeup itself to latest version
89
+
90
+ Experimental (in development):
91
+ install [profile] Install a team profile from .claude/profiles.json
92
+ profile <cmd> list | show | switch | sync team profiles
93
+ doctor Diagnose & repair plugin/marketplace/profile state
94
+
95
+ Navigation (TUI):
96
+ [1] Plugins [4] Settings [7] Git State
97
+ [2] Skills [5] Profiles [8] Alias
98
+ [3] MCP [6] CLI Tools
99
+
100
+ Keys:
101
+ ↑/↓ or j/k Navigate
102
+ Enter Select / Toggle
103
+ r Refresh current screen
104
+ ? Show help
105
+ q / Escape Back / Quit
106
+ `);
107
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `claudeup update` — self-update.
3
+ *
4
+ * Detects claudeup installs across bun + npm global, warns on duplicates
5
+ * (active vs shadowed), then re-installs `claudeup@latest` with whichever
6
+ * package manager owns the active binary. Resolves with the child's exit code.
7
+ */
8
+
9
+ import { spawn } from "node:child_process";
10
+
11
+ export async function runUpdateCommand(): Promise<number> {
12
+ const { execSync } = await import("node:child_process");
13
+ const { existsSync } = await import("node:fs");
14
+
15
+ // Detect all installations of claudeup across package managers
16
+ const installations: Array<{ manager: "bun" | "npm"; path: string }> = [];
17
+
18
+ // Check bun global
19
+ try {
20
+ const bunGlobalBin = execSync("bun pm -g bin", {
21
+ encoding: "utf-8",
22
+ timeout: 5000,
23
+ }).trim();
24
+ const bunPath = `${bunGlobalBin}/claudeup`;
25
+ if (existsSync(bunPath)) installations.push({ manager: "bun", path: bunPath });
26
+ } catch {
27
+ // bun not installed or no global claudeup
28
+ }
29
+
30
+ // Check npm global
31
+ try {
32
+ const npmPrefix = execSync("npm prefix -g", {
33
+ encoding: "utf-8",
34
+ timeout: 5000,
35
+ }).trim();
36
+ const npmPath = `${npmPrefix}/bin/claudeup`;
37
+ if (existsSync(npmPath)) installations.push({ manager: "npm", path: npmPath });
38
+ } catch {
39
+ // npm not installed or no global claudeup
40
+ }
41
+
42
+ // Warn about duplicate installations
43
+ if (installations.length > 1) {
44
+ const activePath = execSync("which claudeup", {
45
+ encoding: "utf-8",
46
+ timeout: 5000,
47
+ }).trim();
48
+
49
+ console.log(`⚠ claudeup is installed via multiple package managers:\n`);
50
+ for (const inst of installations) {
51
+ const tag = inst.path === activePath ? " (active)" : " (shadowed)";
52
+ console.log(` ${inst.manager}: ${inst.path}${tag}`);
53
+ }
54
+ console.log(`\nTo fix, keep one and remove the other:`);
55
+ for (const inst of installations) {
56
+ console.log(` ${inst.manager} uninstall -g claudeup`);
57
+ }
58
+ console.log();
59
+ }
60
+
61
+ // Determine which package manager to use for the update
62
+ let usesBun = false;
63
+ try {
64
+ const claudeupPath = execSync("which claudeup", {
65
+ encoding: "utf-8",
66
+ }).trim();
67
+ usesBun = claudeupPath.includes(".bun") || claudeupPath.includes("bun/bin");
68
+ } catch {
69
+ // If which fails, default to npm
70
+ }
71
+
72
+ const pkgManager = usesBun ? "bun" : "npm";
73
+ console.log(`Updating claudeup using ${pkgManager}...`);
74
+
75
+ const installArgs = ["install", "-g", "claudeup@latest"];
76
+
77
+ return new Promise<number>((resolve) => {
78
+ const proc = spawn(pkgManager, installArgs, {
79
+ stdio: "inherit",
80
+ shell: false, // Avoid shell for security (fixes DEP0190 warning)
81
+ });
82
+ proc.on("exit", (code) => resolve(code ?? 0));
83
+ proc.on("error", () => resolve(1));
84
+ });
85
+ }
@@ -6,7 +6,7 @@ export interface CliTool {
6
6
  checkCommand: string;
7
7
  website: string;
8
8
  category: "ai-coding" | "utility";
9
- packageManager: "npm" | "pip";
9
+ packageManager: "bun" | "npm" | "pip" | "brew" | "go";
10
10
  packageName: string;
11
11
  }
12
12
 
@@ -16,11 +16,11 @@ export const cliTools: CliTool[] = [
16
16
  displayName: "claudeup",
17
17
  description:
18
18
  "TUI tool for managing Claude Code plugins, MCPs, and configuration",
19
- installCommand: "npm install -g claudeup",
19
+ installCommand: "bun install -g claudeup",
20
20
  checkCommand: "claudeup --version",
21
21
  website: "https://github.com/MadAppGang/magus/tree/main/tools/claudeup",
22
22
  category: "ai-coding",
23
- packageManager: "npm",
23
+ packageManager: "bun",
24
24
  packageName: "claudeup",
25
25
  },
26
26
  {
@@ -28,22 +28,22 @@ export const cliTools: CliTool[] = [
28
28
  displayName: "Mnemex",
29
29
  description:
30
30
  "AST-aware code search with PageRank, callers/callees, and semantic embeddings",
31
- installCommand: "npm install -g mnemex",
31
+ installCommand: "bun install -g mnemex",
32
32
  checkCommand: "mnemex --version",
33
33
  website: "https://github.com/MadAppGang/mnemex",
34
34
  category: "ai-coding",
35
- packageManager: "npm",
35
+ packageManager: "bun",
36
36
  packageName: "mnemex",
37
37
  },
38
38
  {
39
39
  name: "claudish",
40
40
  displayName: "Claudish",
41
41
  description: "Run Claude Code with OpenRouter models (Grok, GPT-5, Gemini)",
42
- installCommand: "npm install -g claudish",
42
+ installCommand: "bun install -g claudish",
43
43
  checkCommand: "claudish --version",
44
44
  website: "https://github.com/MadAppGang/claudish",
45
45
  category: "ai-coding",
46
- packageManager: "npm",
46
+ packageManager: "bun",
47
47
  packageName: "claudish",
48
48
  },
49
49
  {
@@ -12,6 +12,8 @@ export const BUILTIN_DEFAULTS: GitignoreManifest = {
12
12
  ".claude/scheduled_tasks.lock",
13
13
  ".claude/cache/",
14
14
  ".claude/.statusline-worktree-*",
15
+ // Generated, per-developer profile build output (like node_modules).
16
+ ".claude/_profiles/",
15
17
  "ai-docs/sessions/",
16
18
  ".mnemex/",
17
19
  ".claudemem/",
@@ -19,6 +21,8 @@ export const BUILTIN_DEFAULTS: GitignoreManifest = {
19
21
  ".agents/",
20
22
  ],
21
23
  track: [
24
+ // The committed team source of truth when using profiles.
25
+ ".claude/profiles.json",
22
26
  ".claude/settings.json",
23
27
  ".mcp.json",
24
28
  "plugin.json",
@@ -39,6 +39,18 @@ export const defaultMarketplaces: Marketplace[] = [
39
39
  official: false,
40
40
  featured: true, // Always show plugins (expanded by default)
41
41
  },
42
+ {
43
+ name: "magus-marketing",
44
+ displayName: "Magus Marketing",
45
+ source: {
46
+ source: "github",
47
+ repo: "MadAppGang/magus-marketing",
48
+ },
49
+ description:
50
+ "SEO, cold email outreach, AI image generation, and video editing plugins",
51
+ official: false,
52
+ // Not featured: a niche channel, so keep it collapsed by default.
53
+ },
42
54
  {
43
55
  name: "claude-plugins-official",
44
56
  displayName: "Anthropic Official",
@@ -127,7 +127,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
127
127
  "code-analysis",
128
128
  "terminal",
129
129
  "statusline",
130
- "conductor",
131
130
  "multimodel",
132
131
  "gtd",
133
132
  ],
@@ -200,10 +199,10 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
200
199
  "code-analysis",
201
200
  "terminal",
202
201
  "statusline",
203
- "seo",
202
+ "seo@magus-marketing",
204
203
  "browser-use",
205
- "nanobanana",
206
- "video-editing",
204
+ "nanobanana@magus-marketing",
205
+ "video-editing@magus-marketing",
207
206
  ],
208
207
  anthropicPlugins: [
209
208
  "explanatory-output-style",
package/src/main.tsx CHANGED
@@ -2,15 +2,13 @@
2
2
 
3
3
  import { createCliRenderer } from "@opentui/core";
4
4
  import { createRoot } from "@opentui/react";
5
- import { spawn } from "node:child_process";
6
- import { createRequire } from "node:module";
7
5
  import { App } from "./ui/App.js";
8
- import { prerunClaude } from "./prerunner/index.js";
9
- import { checkForUpdates } from "./services/version-check.js";
6
+ import { route } from "./cli/router.js";
7
+ // Static import so `bun build --compile` embeds package.json into the binary;
8
+ // a dynamic require("../package.json") is not resolvable inside the bunfs root.
9
+ import pkg from "../package.json";
10
10
 
11
- const require = createRequire(import.meta.url);
12
- const pkg = require("../package.json") as { version: string };
13
- export const VERSION = pkg.version;
11
+ export const VERSION = (pkg as { version: string }).version;
14
12
 
15
13
  // Note: OpenTUI renderer handles alternate screen buffer automatically
16
14
  // No need for manual ANSI escape codes
@@ -18,153 +16,12 @@ export const VERSION = pkg.version;
18
16
  async function main(): Promise<void> {
19
17
  const args = process.argv.slice(2);
20
18
 
21
- // Handle "claudeup claude [args]" - prerunner mode
22
- if (args[0] === "claude") {
23
- const claudeArgs = args.slice(1);
24
-
25
- // Parse prerunner-specific flags
26
- const forceUpdate =
27
- claudeArgs.includes("--force") || claudeArgs.includes("-f");
28
-
29
- // Remove prerunner flags before passing to claude
30
- const filteredArgs = claudeArgs.filter(
31
- (arg) => arg !== "--force" && arg !== "-f",
32
- );
33
-
34
- const exitCode = await prerunClaude(filteredArgs, { force: forceUpdate });
35
- process.exit(exitCode);
36
- return;
37
- }
38
-
39
- // Handle "claudeup update" - self-update command
40
- if (args[0] === "update") {
41
- const { execSync } = await import("node:child_process");
42
- const { existsSync } = await import("node:fs");
43
-
44
- // Detect all installations of claudeup across package managers
45
- const installations: Array<{
46
- manager: "bun" | "npm";
47
- path: string;
48
- }> = [];
49
-
50
- // Check bun global
51
- try {
52
- const bunGlobalBin = execSync("bun pm -g bin", {
53
- encoding: "utf-8",
54
- timeout: 5000,
55
- }).trim();
56
- const bunPath = `${bunGlobalBin}/claudeup`;
57
- if (existsSync(bunPath)) installations.push({ manager: "bun", path: bunPath });
58
- } catch {
59
- // bun not installed or no global claudeup
60
- }
61
-
62
- // Check npm global
63
- try {
64
- const npmPrefix = execSync("npm prefix -g", {
65
- encoding: "utf-8",
66
- timeout: 5000,
67
- }).trim();
68
- const npmPath = `${npmPrefix}/bin/claudeup`;
69
- if (existsSync(npmPath)) installations.push({ manager: "npm", path: npmPath });
70
- } catch {
71
- // npm not installed or no global claudeup
72
- }
73
-
74
- // Warn about duplicate installations
75
- if (installations.length > 1) {
76
- const activePath = execSync("which claudeup", {
77
- encoding: "utf-8",
78
- timeout: 5000,
79
- }).trim();
80
-
81
- console.log(
82
- `⚠ claudeup is installed via multiple package managers:\n`,
83
- );
84
- for (const inst of installations) {
85
- const tag =
86
- inst.path === activePath ? " (active)" : " (shadowed)";
87
- console.log(` ${inst.manager}: ${inst.path}${tag}`);
88
- }
89
- console.log(`\nTo fix, keep one and remove the other:`);
90
- for (const inst of installations) {
91
- console.log(` ${inst.manager} uninstall -g claudeup`);
92
- }
93
- console.log();
94
- }
95
-
96
- // Determine which package manager to use for the update
97
- let usesBun = false;
98
- try {
99
- const claudeupPath = execSync("which claudeup", {
100
- encoding: "utf-8",
101
- }).trim();
102
- usesBun =
103
- claudeupPath.includes(".bun") || claudeupPath.includes("bun/bin");
104
- } catch {
105
- // If which fails, default to npm
106
- }
107
-
108
- const pkgManager = usesBun ? "bun" : "npm";
109
- console.log(`Updating claudeup using ${pkgManager}...`);
110
-
111
- const installArgs = ["install", "-g", "claudeup@latest"];
112
-
113
- const proc = spawn(pkgManager, installArgs, {
114
- stdio: "inherit",
115
- shell: false, // Avoid shell for security (fixes DEP0190 warning)
116
- });
117
- proc.on("exit", (code) => process.exit(code || 0));
118
- return;
119
- }
120
-
121
- // Handle --version flag - show version and check for updates
122
- if (args.includes("--version") || args.includes("-v")) {
123
- console.log(`claudeup v${VERSION}`);
124
- const result = await checkForUpdates();
125
- if (result.updateAvailable) {
126
- console.log(`\nUpdate available: v${result.latestVersion}`);
127
- console.log("Run: claudeup update");
128
- }
129
- process.exit(0);
130
- }
131
-
132
- // Handle --help flag
133
- if (args.includes("--help") || args.includes("-h")) {
134
- console.log(`claudeup v${VERSION}
135
-
136
- TUI tool for managing Claude Code plugins, MCPs, and configuration.
137
-
138
- Usage: claudeup [options]
139
- claudeup claude [args...] Run claude with auto-update prerunner
140
- claudeup update Update claudeup to latest version
141
-
142
- Options:
143
- -v, --version Show version and check for updates
144
- -h, --help Show this help message
145
- --no-refresh Skip auto-refresh of marketplaces on startup
146
-
147
- Commands:
148
- claude [args...] Check for plugin updates (1h cache), then run claude
149
- -f, --force Force update check (bypass 1h cache)
150
- update Update claudeup itself to latest version
151
-
152
- Navigation:
153
- [1] Plugins Manage plugin marketplaces and installed plugins
154
- [2] MCP Setup and manage MCP servers
155
- [3] Status Configure status line display
156
- [4] Env Environment variables for MCP servers
157
- [5] Tools Install and update AI coding CLI tools
158
-
159
- Keys:
160
- ↑/↓ or j/k Navigate
161
- Enter Select / Toggle
162
- g Toggle global/project scope (in Plugins)
163
- r Refresh current screen
164
- ? Show help
165
- q / Escape Back / Quit
166
- `);
167
- process.exit(0);
19
+ // Dispatch non-interactive subcommands (claude, update, install, …) and
20
+ // top-level flags (--version/--help). A bare invocation falls through to
21
+ // the interactive TUI below.
22
+ const outcome = await route(args, VERSION);
23
+ if (outcome.handled) {
24
+ process.exit(outcome.exitCode ?? 0);
168
25
  }
169
26
 
170
27
  // Create OpenTUI renderer (handles alternate screen buffer automatically)
@@ -1,31 +1,32 @@
1
- import fs from "fs-extra";
2
- import path from "node:path";
3
1
  import os from "node:os";
4
- import { UpdateCache } from "../services/update-cache.js";
5
- import {
6
- getAvailablePlugins,
7
- clearMarketplaceCache,
8
- } from "../services/plugin-manager.js";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ import { isClaudeAvailable, updatePlugin } from "../services/claude-cli.js";
9
5
  import { runClaude } from "../services/claude-runner.js";
10
6
  import {
11
- recoverMarketplaceSettings,
12
- migrateMarketplaceRename,
13
7
  cleanupExtraKnownMarketplaces,
8
+ gapFillInstalledPluginVersions,
9
+ migrateMarketplaceRename,
14
10
  readGlobalSettings,
15
- writeGlobalSettings,
11
+ recoverMarketplaceSettings,
16
12
  saveGlobalInstalledPluginVersion,
17
- gapFillInstalledPluginVersions,
13
+ writeGlobalSettings,
18
14
  } from "../services/claude-settings.js";
19
- import {
20
- checkPluginVersionMismatches,
21
- formatMismatchWarning,
22
- } from "../services/plugin-version-check.js";
23
- import { updatePlugin, isClaudeAvailable } from "../services/claude-cli.js";
24
- import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
25
15
  import {
26
16
  checkGitignore,
27
17
  formatPrerunWarning as formatGitignoreWarning,
28
18
  } from "../services/gitignore-prerun.js";
19
+ import { refreshRegisteredMarketplaces } from "../services/marketplace-refresh.js";
20
+ import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
21
+ import {
22
+ clearMarketplaceCache,
23
+ getAvailablePlugins,
24
+ } from "../services/plugin-manager.js";
25
+ import {
26
+ checkPluginVersionMismatches,
27
+ formatMismatchWarning,
28
+ } from "../services/plugin-version-check.js";
29
+ import { UpdateCache } from "../services/update-cache.js";
29
30
 
30
31
  export interface PrerunOptions {
31
32
  force?: boolean; // Bypass cache and force update check
@@ -133,9 +134,7 @@ export async function prerunClaude(
133
134
  // otherwise a freshly cloned repo whose only @magus references live
134
135
  // in project settings looks "clean" from the global-scope view.
135
136
  if (await isClaudeAvailable()) {
136
- const addedMarketplaces = await autoAddMissingMarketplaces(
137
- process.cwd(),
138
- );
137
+ const addedMarketplaces = await autoAddMissingMarketplaces(process.cwd());
139
138
  if (addedMarketplaces.length > 0) {
140
139
  console.log(
141
140
  `✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`,
@@ -238,20 +237,40 @@ export async function prerunClaude(
238
237
  console.log(
239
238
  `✓ Re-registered as GitHub source: ${recovery.reregistered.join(", ")}`,
240
239
  );
241
- // Trigger marketplace update for re-registered entries so the local
242
- // clone gets refreshed with the latest plugins.
240
+ // Re-registration only rewrites known_marketplaces.json (directory
241
+ // github source); it does NOT create the on-disk clone. Clone any whose
242
+ // directory is missing, using the NON-DESTRUCTIVE `marketplace add` —
243
+ // never `marketplace update`, which deletes the directory before
244
+ // re-cloning and can leave it permanently gone on failure (see
245
+ // marketplace-refresh.ts / marketplace-sync.ts). Clones that already
246
+ // exist are left to STEP 1.7's safe `git pull --ff-only`.
243
247
  const cliAvailable = await isClaudeAvailable();
244
248
  if (cliAvailable) {
245
- const { updateMarketplace } = await import(
246
- "../services/claude-cli.js"
249
+ const { addMarketplace } = await import("../services/claude-cli.js");
250
+ const { defaultMarketplaces } = await import(
251
+ "../data/marketplaces.js"
252
+ );
253
+ const marketplacesDir = path.join(
254
+ os.homedir(),
255
+ ".claude",
256
+ "plugins",
257
+ "marketplaces",
247
258
  );
248
259
  for (const mpName of recovery.reregistered) {
260
+ // Already on disk → STEP 1.7 fast-forwards it; nothing to clone.
261
+ if (await fs.pathExists(path.join(marketplacesDir, mpName))) {
262
+ continue;
263
+ }
264
+ const repo = defaultMarketplaces.find(
265
+ (m) => m.name === mpName,
266
+ )?.source.repo;
267
+ if (!repo) continue;
249
268
  try {
250
- await updateMarketplace(mpName);
251
- console.log(`✓ Updated marketplace: ${mpName}`);
269
+ await addMarketplace(repo);
270
+ console.log(`✓ Cloned re-registered marketplace: ${mpName}`);
252
271
  } catch (error) {
253
272
  console.warn(
254
- `⚠ Failed to update marketplace ${mpName}:`,
273
+ `⚠ Failed to clone marketplace ${mpName}:`,
255
274
  error instanceof Error ? error.message : "Unknown error",
256
275
  );
257
276
  }
@@ -259,6 +278,22 @@ export async function prerunClaude(
259
278
  }
260
279
  }
261
280
 
281
+ // STEP 1.7: Fast-forward the on-disk clones of every registered
282
+ // marketplace. Recovery above only *clones* re-registered ones that were
283
+ // missing; nothing else advances an existing healthy clone, so updated
284
+ // plugin setup metadata — e.g. a pinned dependency version — is never seen
285
+ // even though the fetched plugin list is current. Non-destructive
286
+ // `git pull --ff-only`: a failed pull leaves the clone stale, never gone.
287
+ const mpRefresh = await refreshRegisteredMarketplaces();
288
+ if (mpRefresh.refreshed.length > 0) {
289
+ console.log(
290
+ `✓ Refreshed marketplaces: ${mpRefresh.refreshed.join(", ")}`,
291
+ );
292
+ }
293
+ for (const name of mpRefresh.failed) {
294
+ console.warn(`⚠ Failed to refresh marketplace: ${name}`);
295
+ }
296
+
262
297
  // STEP 2: Clear cache to force fresh plugin info
263
298
  clearMarketplaceCache();
264
299