claudeup 4.19.0 → 4.23.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 (182) 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-badge.test.ts +91 -0
  18. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  19. package/src/__tests__/plugin-requires.test.ts +133 -0
  20. package/src/__tests__/plugin-setup.test.ts +7 -0
  21. package/src/__tests__/profile-command.test.ts +116 -0
  22. package/src/__tests__/profile-materializer.test.ts +82 -0
  23. package/src/__tests__/profile-sync.test.ts +136 -0
  24. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  25. package/src/__tests__/resolve-executable.test.ts +42 -0
  26. package/src/__tests__/resolver.test.ts +218 -0
  27. package/src/__tests__/symlink-manager.test.ts +99 -0
  28. package/src/__tests__/toolchain.test.ts +56 -0
  29. package/src/cli/claude.ts +21 -0
  30. package/src/cli/doctor.ts +132 -0
  31. package/src/cli/install.ts +360 -0
  32. package/src/cli/profile.ts +166 -0
  33. package/src/cli/router.ts +107 -0
  34. package/src/cli/update.ts +85 -0
  35. package/src/data/cli-tools.ts +7 -7
  36. package/src/data/gitignore-defaults.ts +4 -0
  37. package/src/data/marketplaces.ts +12 -0
  38. package/src/data/predefined-profiles.ts +3 -4
  39. package/src/main.tsx +11 -154
  40. package/src/prerunner/index.ts +62 -27
  41. package/src/services/claude-cli.ts +19 -22
  42. package/src/services/claude-settings.ts +99 -26
  43. package/src/services/conventions-manager.ts +865 -0
  44. package/src/services/doctor-bins.ts +49 -0
  45. package/src/services/doctor.ts +509 -0
  46. package/src/services/install-plan.ts +107 -0
  47. package/src/services/manifest.ts +166 -0
  48. package/src/services/marketplace-refresh.ts +173 -0
  49. package/src/services/plugin-manager.ts +7 -1
  50. package/src/services/plugin-requires.ts +215 -0
  51. package/src/services/plugin-version-check.ts +9 -4
  52. package/src/services/profile-materializer.ts +61 -0
  53. package/src/services/profile-sync.ts +150 -0
  54. package/src/services/resolver.ts +293 -0
  55. package/src/services/symlink-manager.ts +146 -0
  56. package/src/services/toolchain.ts +97 -0
  57. package/src/services/version-check.ts +10 -11
  58. package/src/types/index.ts +138 -31
  59. package/src/ui/App.tsx +0 -4
  60. package/src/ui/adapters/pluginsAdapter.ts +13 -0
  61. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  62. package/src/ui/screens/index.ts +0 -1
  63. package/src/ui/state/reducer.ts +0 -101
  64. package/src/ui/state/types.ts +0 -35
  65. package/src/utils/command-utils.ts +23 -0
  66. package/src/utils/file-locking.ts +144 -0
  67. package/src/data/alias-flags.js +0 -205
  68. package/src/data/cli-tools.js +0 -123
  69. package/src/data/gitignore-defaults.js +0 -24
  70. package/src/data/gitignore-reasons.js +0 -97
  71. package/src/data/gitignore-templates.js +0 -21
  72. package/src/data/marketplaces.js +0 -138
  73. package/src/data/mcp-servers.js +0 -509
  74. package/src/data/predefined-profiles.js +0 -248
  75. package/src/data/settings-catalog.js +0 -625
  76. package/src/data/skill-repos.js +0 -160
  77. package/src/data/statuslines.js +0 -159
  78. package/src/data/statuslines.ts +0 -188
  79. package/src/index.js +0 -4
  80. package/src/index.ts +0 -5
  81. package/src/main.js +0 -170
  82. package/src/prerunner/index.js +0 -252
  83. package/src/services/alias-settings.js +0 -51
  84. package/src/services/alias-shell-writer.js +0 -1018
  85. package/src/services/alias-store.js +0 -129
  86. package/src/services/claude-cli.js +0 -189
  87. package/src/services/claude-runner.js +0 -28
  88. package/src/services/claude-settings.js +0 -1223
  89. package/src/services/gitignore-detector.js +0 -155
  90. package/src/services/gitignore-fixer.js +0 -231
  91. package/src/services/gitignore-prerun.js +0 -46
  92. package/src/services/gitignore-resolver.js +0 -143
  93. package/src/services/gitignore-service.js +0 -117
  94. package/src/services/local-marketplace.js +0 -339
  95. package/src/services/marketplace-fetcher.js +0 -96
  96. package/src/services/marketplace-sync.js +0 -94
  97. package/src/services/mcp-registry.js +0 -87
  98. package/src/services/plugin-manager.js +0 -473
  99. package/src/services/plugin-mcp-config.js +0 -177
  100. package/src/services/plugin-setup.js +0 -499
  101. package/src/services/plugin-version-check.js +0 -262
  102. package/src/services/profiles.js +0 -161
  103. package/src/services/settings-manager.js +0 -243
  104. package/src/services/skills-manager.js +0 -393
  105. package/src/services/skillsmp-client.js +0 -87
  106. package/src/services/update-cache.js +0 -52
  107. package/src/services/version-check.js +0 -99
  108. package/src/types/gitignore.js +0 -6
  109. package/src/types/index.js +0 -1
  110. package/src/ui/App.js +0 -355
  111. package/src/ui/adapters/pluginsAdapter.js +0 -139
  112. package/src/ui/adapters/settingsAdapter.js +0 -111
  113. package/src/ui/adapters/skillsAdapter.js +0 -154
  114. package/src/ui/components/CategoryHeader.js +0 -9
  115. package/src/ui/components/EmptyFilterState.js +0 -4
  116. package/src/ui/components/FlagDetailEditor.js +0 -0
  117. package/src/ui/components/ScopeIndicator.js +0 -30
  118. package/src/ui/components/ScrollableList.js +0 -36
  119. package/src/ui/components/SearchInput.js +0 -19
  120. package/src/ui/components/StyledText.js +0 -39
  121. package/src/ui/components/TabBar.js +0 -19
  122. package/src/ui/components/layout/FooterHints.js +0 -29
  123. package/src/ui/components/layout/Panel.js +0 -6
  124. package/src/ui/components/layout/ProgressBar.js +0 -14
  125. package/src/ui/components/layout/ScopeTabs.js +0 -6
  126. package/src/ui/components/layout/ScreenLayout.js +0 -16
  127. package/src/ui/components/layout/index.js +0 -5
  128. package/src/ui/components/modals/ConfirmModal.js +0 -14
  129. package/src/ui/components/modals/InputModal.js +0 -5
  130. package/src/ui/components/modals/LoadingModal.js +0 -17
  131. package/src/ui/components/modals/MessageModal.js +0 -16
  132. package/src/ui/components/modals/ModalContainer.js +0 -137
  133. package/src/ui/components/modals/SelectModal.js +0 -18
  134. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  135. package/src/ui/components/modals/index.js +0 -6
  136. package/src/ui/components/primitives/ActionHints.js +0 -13
  137. package/src/ui/components/primitives/DetailSection.js +0 -7
  138. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  139. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  140. package/src/ui/components/primitives/MetaText.js +0 -8
  141. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  142. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  143. package/src/ui/components/primitives/SelectableRow.js +0 -5
  144. package/src/ui/components/primitives/index.js +0 -8
  145. package/src/ui/hooks/index.js +0 -4
  146. package/src/ui/hooks/useAsyncData.js +0 -77
  147. package/src/ui/hooks/useGitignoreModal.js +0 -74
  148. package/src/ui/hooks/useKeyboard.js +0 -13
  149. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  150. package/src/ui/hooks/useMismatchModal.js +0 -77
  151. package/src/ui/registry.js +0 -1
  152. package/src/ui/renderers/cliToolRenderers.js +0 -54
  153. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  154. package/src/ui/renderers/mcpRenderers.js +0 -26
  155. package/src/ui/renderers/pluginRenderers.js +0 -124
  156. package/src/ui/renderers/profileRenderers.js +0 -177
  157. package/src/ui/renderers/settingsRenderers.js +0 -73
  158. package/src/ui/renderers/skillRenderers.js +0 -138
  159. package/src/ui/screens/AliasScreen.js +0 -1111
  160. package/src/ui/screens/CliToolsScreen.js +0 -338
  161. package/src/ui/screens/EnvVarsScreen.js +0 -152
  162. package/src/ui/screens/GitignoreScreen.js +0 -328
  163. package/src/ui/screens/McpRegistryScreen.js +0 -238
  164. package/src/ui/screens/McpScreen.js +0 -176
  165. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  166. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  167. package/src/ui/screens/PluginsScreen.js +0 -769
  168. package/src/ui/screens/ProfilesScreen.js +0 -298
  169. package/src/ui/screens/SkillsScreen.js +0 -549
  170. package/src/ui/screens/StatusLineScreen.js +0 -206
  171. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  172. package/src/ui/screens/index.js +0 -10
  173. package/src/ui/state/AnimationContext.js +0 -34
  174. package/src/ui/state/AppContext.js +0 -162
  175. package/src/ui/state/DimensionsContext.js +0 -71
  176. package/src/ui/state/reducer.js +0 -547
  177. package/src/ui/state/types.js +0 -1
  178. package/src/ui/theme.js +0 -47
  179. package/src/utils/clipboard.js +0 -56
  180. package/src/utils/command-utils.js +0 -19
  181. package/src/utils/fuzzy-search.js +0 -101
  182. 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
+ "image-generate@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