claudeup 4.18.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 (191) 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__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { execFile } from "node:child_process";
13
13
  import { promisify } from "node:util";
14
+ import { defaultMarketplaces } from "../data/marketplaces.js";
14
15
  import { which } from "../utils/command-utils.js";
15
16
  import {
16
17
  isMarketplaceRegistered,
@@ -18,7 +19,6 @@ import {
18
19
  removeLocalInstalledPluginVersion,
19
20
  } from "./claude-settings.js";
20
21
  import { removeInstalledPluginVersion } from "./plugin-manager.js";
21
- import { defaultMarketplaces } from "../data/marketplaces.js";
22
22
 
23
23
  const execFileAsync = promisify(execFile);
24
24
 
@@ -84,13 +84,21 @@ async function recoverMissingMarketplace(
84
84
  marketplace: string,
85
85
  ): Promise<boolean> {
86
86
  if (await isMarketplaceRegistered(marketplace)) {
87
- // Registered but stale existing recovery path.
88
- const { recoverMarketplaceSettings } = await import(
89
- "./claude-settings.js"
90
- );
87
+ // Registered but the plugin wasn't found → the registry may use a stale or
88
+ // "directory" source. Fix the registry, then refresh the clone
89
+ // NON-DESTRUCTIVELY (git pull --ff-only) — never `marketplace update`, which
90
+ // deletes the directory before re-cloning and can leave it permanently gone
91
+ // on failure (see marketplace-refresh.ts). If the clone is absent, fall
92
+ // through to the safe `marketplace add` below to create it.
93
+ const { recoverMarketplaceSettings } = await import("./claude-settings.js");
91
94
  await recoverMarketplaceSettings();
92
- await updateMarketplace(marketplace);
93
- return true;
95
+ const { fastForwardCloneIfPresent } = await import(
96
+ "./marketplace-refresh.js"
97
+ );
98
+ if ((await fastForwardCloneIfPresent(marketplace)) !== "absent") {
99
+ return true;
100
+ }
101
+ // Clone missing → fall through to `marketplace add`.
94
102
  }
95
103
 
96
104
  const defaultMp = defaultMarketplaces.find((m) => m.name === marketplace);
@@ -115,18 +123,11 @@ export async function installPlugin(
115
123
  try {
116
124
  await execClaude(["plugin", "install", pluginId, "--scope", scope]);
117
125
  } catch (error) {
118
- const msg =
119
- error instanceof Error ? error.message : String(error);
126
+ const msg = error instanceof Error ? error.message : String(error);
120
127
  if (msg.includes("not found in marketplace")) {
121
128
  const marketplace = pluginId.split("@")[1];
122
129
  if (marketplace && (await recoverMissingMarketplace(marketplace))) {
123
- await execClaude([
124
- "plugin",
125
- "install",
126
- pluginId,
127
- "--scope",
128
- scope,
129
- ]);
130
+ await execClaude(["plugin", "install", pluginId, "--scope", scope]);
130
131
  return;
131
132
  }
132
133
  }
@@ -193,13 +194,9 @@ export async function updatePlugin(
193
194
  scope: PluginScope = "user",
194
195
  ): Promise<void> {
195
196
  try {
196
- await execClaude(
197
- ["plugin", "install", pluginId, "--scope", scope],
198
- 60000,
199
- );
197
+ await execClaude(["plugin", "install", pluginId, "--scope", scope], 60000);
200
198
  } catch (error) {
201
- const msg =
202
- error instanceof Error ? error.message : String(error);
199
+ const msg = error instanceof Error ? error.message : String(error);
203
200
  if (msg.includes("not found in marketplace")) {
204
201
  const marketplace = pluginId.split("@")[1];
205
202
  if (marketplace && (await recoverMissingMarketplace(marketplace))) {
@@ -248,11 +248,85 @@ export async function getLocalEnabledPlugins(
248
248
  return settings.enabledPlugins || {};
249
249
  }
250
250
 
251
+ /**
252
+ * Overlay the installed_plugins.json registry onto a settings-derived version map.
253
+ *
254
+ * `settings.installedPluginVersions` is claudeup's own bookkeeping and is routinely
255
+ * incomplete: Claude Code's own `plugin install` writes the registry but not our
256
+ * settings key, so a plugin can be genuinely installed and still be absent here.
257
+ * Callers that only read settings then see `undefined` and — because the update
258
+ * check is `installedVersion && compare(...)` — silently treat the plugin as
259
+ * up to date instead of unknown. That hides real updates.
260
+ *
261
+ * The registry is what Claude Code actually loaded, so it wins on conflict.
262
+ *
263
+ * Scope resolution mirrors Claude Code: for a project scope, prefer an entry
264
+ * pinned to that exact projectPath (project or local), then fall back to the
265
+ * user-scope entry, which is what a project without its own install resolves to.
266
+ */
267
+ export function pickRegistryEntry(
268
+ entries: InstalledPluginEntry[] | undefined,
269
+ scope: "user" | "project" | "local",
270
+ projectPath?: string,
271
+ ): InstalledPluginEntry | undefined {
272
+ if (!Array.isArray(entries) || entries.length === 0) return undefined;
273
+
274
+ if (scope === "user") return entries.find((e) => e.scope === "user");
275
+
276
+ const target = projectPath ? path.resolve(projectPath) : undefined;
277
+ if (target) {
278
+ const pinned = entries.find(
279
+ (e) =>
280
+ (e.scope === scope || e.scope === "local") &&
281
+ e.projectPath !== undefined &&
282
+ path.resolve(e.projectPath) === target,
283
+ );
284
+ if (pinned) return pinned;
285
+ }
286
+ // A project with no install of its own resolves to the user-scope entry.
287
+ return entries.find((e) => e.scope === "user");
288
+ }
289
+
290
+ async function overlayRegistryVersions(
291
+ base: Record<string, string>,
292
+ scope: "user" | "project" | "local",
293
+ projectPath?: string,
294
+ ): Promise<Record<string, string>> {
295
+ let registry: InstalledPluginsRegistry;
296
+ try {
297
+ registry = await readInstalledPluginsRegistry();
298
+ } catch {
299
+ return base; // registry unreadable — settings-only is still better than nothing
300
+ }
301
+
302
+ const merged = { ...base };
303
+ for (const [pluginId, entries] of Object.entries(registry.plugins ?? {})) {
304
+ const pick = pickRegistryEntry(entries, scope, projectPath);
305
+ if (pick?.version) merged[pluginId] = pick.version;
306
+ }
307
+ return merged;
308
+ }
309
+
251
310
  export async function getLocalInstalledPluginVersions(
252
311
  projectPath?: string,
253
312
  ): Promise<Record<string, string>> {
254
313
  const settings = await readLocalSettings(projectPath);
255
- return settings.installedPluginVersions || {};
314
+ return overlayRegistryVersions(
315
+ settings.installedPluginVersions || {},
316
+ "local",
317
+ projectPath,
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Overlay the registry onto a project-scope settings map.
323
+ * Exported for plugin-manager, which reads project settings itself.
324
+ */
325
+ export async function getProjectInstalledPluginVersions(
326
+ base: Record<string, string>,
327
+ projectPath?: string,
328
+ ): Promise<Record<string, string>> {
329
+ return overlayRegistryVersions(base, "project", projectPath);
256
330
  }
257
331
 
258
332
  // Local plugin management (writes to settings.local.json)
@@ -529,7 +603,10 @@ export async function getGlobalInstalledPluginVersions(): Promise<
529
603
  Record<string, string>
530
604
  > {
531
605
  const settings = await readGlobalSettings();
532
- return settings.installedPluginVersions || {};
606
+ return overlayRegistryVersions(
607
+ settings.installedPluginVersions || {},
608
+ "user",
609
+ );
533
610
  }
534
611
 
535
612
  export async function saveGlobalInstalledPluginVersion(
@@ -744,20 +821,16 @@ export async function discoverAllGlobalMarketplaces(): Promise<
744
821
  }
745
822
  }
746
823
 
747
- // installed_plugins.json registry management
748
- const INSTALLED_PLUGINS_FILE = path.join(
749
- os.homedir(),
750
- ".claude",
751
- "plugins",
752
- "installed_plugins.json",
753
- );
824
+ // installed_plugins.json registry management.
825
+ // Resolved lazily (per-call) rather than cached at module load: caching
826
+ // os.homedir() in a module-level constant makes the path capture whatever
827
+ // HOME was set to when this module was first imported, which is fragile
828
+ // (import-order-dependent) and breaks any caller that changes HOME afterward.
829
+ const installedPluginsFile = (): string =>
830
+ path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json");
754
831
 
755
- const KNOWN_MARKETPLACES_FILE = path.join(
756
- os.homedir(),
757
- ".claude",
758
- "plugins",
759
- "known_marketplaces.json",
760
- );
832
+ const knownMarketplacesFile = (): string =>
833
+ path.join(os.homedir(), ".claude", "plugins", "known_marketplaces.json");
761
834
 
762
835
  interface KnownMarketplaceEntry {
763
836
  source: { source: string; path?: string; repo?: string };
@@ -774,8 +847,8 @@ type KnownMarketplaces = Record<string, KnownMarketplaceEntry>;
774
847
  async function writeKnownMarketplaces(
775
848
  marketplaces: KnownMarketplaces,
776
849
  ): Promise<void> {
777
- await fs.ensureDir(path.dirname(KNOWN_MARKETPLACES_FILE));
778
- await fs.writeJson(KNOWN_MARKETPLACES_FILE, marketplaces, { spaces: 2 });
850
+ await fs.ensureDir(path.dirname(knownMarketplacesFile()));
851
+ await fs.writeJson(knownMarketplacesFile(), marketplaces, { spaces: 2 });
779
852
  }
780
853
 
781
854
  /**
@@ -1316,8 +1389,8 @@ export async function cleanupExtraKnownMarketplaces(
1316
1389
  */
1317
1390
  async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
1318
1391
  try {
1319
- if (await fs.pathExists(KNOWN_MARKETPLACES_FILE)) {
1320
- return await fs.readJson(KNOWN_MARKETPLACES_FILE);
1392
+ if (await fs.pathExists(knownMarketplacesFile())) {
1393
+ return await fs.readJson(knownMarketplacesFile());
1321
1394
  }
1322
1395
  } catch {
1323
1396
  // Return empty if can't read
@@ -1434,8 +1507,8 @@ async function copyPluginToCache(
1434
1507
  */
1435
1508
  export async function readInstalledPluginsRegistry(): Promise<InstalledPluginsRegistry> {
1436
1509
  try {
1437
- if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
1438
- const content = await fs.readJson(INSTALLED_PLUGINS_FILE);
1510
+ if (await fs.pathExists(installedPluginsFile())) {
1511
+ const content = await fs.readJson(installedPluginsFile());
1439
1512
  // Validate structure
1440
1513
  if (!content.version || !content.plugins) {
1441
1514
  throw new Error("Invalid registry structure");
@@ -1444,10 +1517,10 @@ export async function readInstalledPluginsRegistry(): Promise<InstalledPluginsRe
1444
1517
  }
1445
1518
  } catch (error) {
1446
1519
  // Backup corrupted file
1447
- if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
1520
+ if (await fs.pathExists(installedPluginsFile())) {
1448
1521
  try {
1449
- const backup = `${INSTALLED_PLUGINS_FILE}.backup.${Date.now()}`;
1450
- await fs.copy(INSTALLED_PLUGINS_FILE, backup);
1522
+ const backup = `${installedPluginsFile()}.backup.${Date.now()}`;
1523
+ await fs.copy(installedPluginsFile(), backup);
1451
1524
  console.warn(`Corrupted registry backed up to: ${backup}`);
1452
1525
  } catch {
1453
1526
  // Ignore backup errors
@@ -1463,8 +1536,8 @@ export async function readInstalledPluginsRegistry(): Promise<InstalledPluginsRe
1463
1536
  export async function writeInstalledPluginsRegistry(
1464
1537
  registry: InstalledPluginsRegistry,
1465
1538
  ): Promise<void> {
1466
- await fs.ensureDir(path.dirname(INSTALLED_PLUGINS_FILE));
1467
- await fs.writeJson(INSTALLED_PLUGINS_FILE, registry, { spaces: 2 });
1539
+ await fs.ensureDir(path.dirname(installedPluginsFile()));
1540
+ await fs.writeJson(installedPluginsFile(), registry, { spaces: 2 });
1468
1541
  }
1469
1542
 
1470
1543
  /**