claudeup 6.5.1 → 6.7.1

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 (85) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/active-closure.test.ts +184 -0
  3. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  4. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  5. package/src/__tests__/blackbox-parity.test.ts +684 -0
  6. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  7. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  8. package/src/__tests__/cli-apply-seams.test.ts +2 -0
  9. package/src/__tests__/cli-tool-commands.test.ts +1 -1
  10. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  11. package/src/__tests__/cli-update-view.test.ts +142 -2
  12. package/src/__tests__/content-drift.test.ts +60 -0
  13. package/src/__tests__/conventions-manager.test.ts +45 -45
  14. package/src/__tests__/enabled-not-installed.test.ts +2 -3
  15. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  16. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  17. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  18. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  19. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  20. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  21. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  22. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  23. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  24. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  25. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  26. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  27. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  28. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  29. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  30. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  31. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  32. package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
  33. package/src/__tests__/models-presets-marketplace.test.ts +141 -0
  34. package/src/__tests__/plugin-cli-argv.test.ts +1 -1
  35. package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
  36. package/src/__tests__/plugin-row-text.test.ts +336 -0
  37. package/src/__tests__/resolver.test.ts +3 -3
  38. package/src/__tests__/scope-action.test.ts +69 -19
  39. package/src/__tests__/shell-script-callers.test.ts +8 -8
  40. package/src/__tests__/tool-probe.test.ts +1400 -0
  41. package/src/__tests__/update-apply.test.ts +283 -1
  42. package/src/__tests__/update-plan.test.ts +773 -31
  43. package/src/__tests__/version-unknown-state.test.ts +388 -0
  44. package/src/cli/router.ts +0 -5
  45. package/src/cli/update-view.ts +92 -12
  46. package/src/cli/update.ts +114 -38
  47. package/src/data/models-presets.ts +10 -11
  48. package/src/data/predefined-profiles.ts +7 -7
  49. package/src/services/active-closure.ts +117 -0
  50. package/src/services/catalog-cache-store.ts +6 -2
  51. package/src/services/claude-settings.ts +2 -174
  52. package/src/services/community-styles.ts +1 -1
  53. package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
  54. package/src/services/gitignore-fixer.ts +1 -1
  55. package/src/services/marketplace-refresh.ts +30 -0
  56. package/src/services/plugin-manager.ts +200 -283
  57. package/src/services/resolver.ts +7 -2
  58. package/src/services/tool-probe.ts +1637 -0
  59. package/src/services/toolchain.ts +97 -0
  60. package/src/services/update-engine.ts +220 -32
  61. package/src/services/update-plan.ts +640 -160
  62. package/src/services/version-snapshot.ts +1 -1
  63. package/src/types/bun.d.ts +6 -0
  64. package/src/ui/App.tsx +2 -27
  65. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  66. package/src/ui/adapters/pluginsAdapter.ts +57 -0
  67. package/src/ui/components/modals/ModalContainer.tsx +0 -26
  68. package/src/ui/hooks/index.ts +0 -1
  69. package/src/ui/hooks/useGitignoreModal.ts +2 -3
  70. package/src/ui/renderers/cliToolRenderers.tsx +108 -67
  71. package/src/ui/renderers/pluginRenderers.tsx +157 -98
  72. package/src/ui/renderers/pluginRowText.ts +70 -0
  73. package/src/ui/renderers/pluginStatusText.ts +56 -0
  74. package/src/ui/screens/CliToolsScreen.tsx +154 -280
  75. package/src/ui/screens/PluginsScreen.tsx +156 -140
  76. package/src/ui/state/types.ts +0 -7
  77. package/src/__tests__/dual-write-prevention.test.ts +0 -380
  78. package/src/__tests__/gap-fill-versions.test.ts +0 -389
  79. package/src/__tests__/plugin-version-check.test.ts +0 -762
  80. package/src/cli/claude.ts +0 -21
  81. package/src/prerunner/index.ts +0 -500
  82. package/src/services/plugin-version-check.ts +0 -360
  83. package/src/services/update-cache.ts +0 -78
  84. package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
  85. package/src/ui/hooks/useMismatchModal.ts +0 -89
@@ -322,7 +322,7 @@ async function overlayRegistryVersions(
322
322
  // `readSettings(undefined)` already resolves to the current directory, so the
323
323
  // registry lookup has to as well. While it did not, the base map described one
324
324
  // project while the overlay refused to match any project's rows at all, and every
325
- // version was deleted as unbacked — which is why the prerunner and the global
325
+ // version was deleted as unbacked — which is why the global
326
326
  // plugin view, both of which pass no path, saw project-scope plugins as
327
327
  // uninstalled everywhere, not only in a worktree.
328
328
  const target = scope === "user" ? undefined : (projectPath ?? process.cwd());
@@ -347,7 +347,7 @@ async function overlayRegistryVersions(
347
347
  // binary, and `claude plugin install` writes it at no scope — so only
348
348
  // claudeup's own past writes can be in there, and they go stale silently.
349
349
  // Measured: user settings claimed browser-use 1.1.2 / terminal 4.0.2 /
350
- // code-analysis 5.1.0 while the registry said 1.4.0 / 4.1.4 / 5.3.1, and
350
+ // code-search 5.1.0 while the registry said 1.4.0 / 4.1.4 / 5.3.1, and
351
351
  // none of the claimed versions had a cache directory. Keeping the stale
352
352
  // value made every one of them read as a permanent pending update.
353
353
  if (entries && entries.length > 0) delete merged[pluginId];
@@ -677,98 +677,6 @@ export async function saveGlobalInstalledPluginVersion(
677
677
  await updateInstalledPluginsRegistry(pluginId, version, "user");
678
678
  }
679
679
 
680
- /**
681
- * Gap-fill: ensure global installedPluginVersions is at least as high as
682
- * project and local scopes. When a plugin is updated at project scope,
683
- * global may lag behind, causing Claude Code to resolve stale cache paths.
684
- *
685
- * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
686
- */
687
- export async function gapFillInstalledPluginVersions(
688
- projectPath?: string,
689
- ): Promise<
690
- Array<{ pluginId: string; oldVersion: string; newVersion: string }>
691
- > {
692
- const globalVersions = await getGlobalInstalledPluginVersions();
693
-
694
- // Collect versions from project and local scopes
695
- const allVersions: Record<string, string[]> = {};
696
-
697
- // Start with global versions
698
- for (const [id, ver] of Object.entries(globalVersions)) {
699
- allVersions[id] = [ver];
700
- }
701
-
702
- // Add project scope versions
703
- if (projectPath) {
704
- try {
705
- const settings = await readSettings(projectPath);
706
- for (const [id, ver] of Object.entries(
707
- settings.installedPluginVersions || {},
708
- )) {
709
- if (!allVersions[id]) allVersions[id] = [];
710
- allVersions[id].push(ver);
711
- }
712
- } catch {
713
- /* skip unreadable */
714
- }
715
-
716
- // Add local scope versions
717
- try {
718
- const localSettings = await readLocalSettings(projectPath);
719
- for (const [id, ver] of Object.entries(
720
- localSettings.installedPluginVersions || {},
721
- )) {
722
- if (!allVersions[id]) allVersions[id] = [];
723
- allVersions[id].push(ver);
724
- }
725
- } catch {
726
- /* skip unreadable */
727
- }
728
- }
729
-
730
- // Find plugins where global needs bumping
731
- const bumped: Array<{
732
- pluginId: string;
733
- oldVersion: string;
734
- newVersion: string;
735
- }> = [];
736
-
737
- for (const [pluginId, versions] of Object.entries(allVersions)) {
738
- // Filter to valid semver versions and sort descending
739
- const valid = versions.filter((v) => semver.valid(v));
740
- if (valid.length === 0) continue;
741
-
742
- const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
743
- const globalVer = globalVersions[pluginId];
744
-
745
- // If global is missing, invalid semver, or lower than the highest, bump it
746
- if (
747
- !globalVer ||
748
- !semver.valid(globalVer) ||
749
- semver.lt(globalVer, highest)
750
- ) {
751
- bumped.push({
752
- pluginId,
753
- oldVersion: globalVer || "(missing)",
754
- newVersion: highest,
755
- });
756
- }
757
- }
758
-
759
- // Apply bumps in a single write
760
- if (bumped.length > 0) {
761
- const settings = await readGlobalSettings();
762
- settings.installedPluginVersions = settings.installedPluginVersions || {};
763
- for (const { pluginId, newVersion } of bumped) {
764
- settings.installedPluginVersions[pluginId] = newVersion;
765
- }
766
- await writeGlobalSettings(settings);
767
- }
768
-
769
- return bumped;
770
- }
771
-
772
680
  export async function removeGlobalInstalledPluginVersion(
773
681
  pluginId: string,
774
682
  ): Promise<void> {
@@ -1362,86 +1270,6 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
1362
1270
  return result;
1363
1271
  }
1364
1272
 
1365
- /**
1366
- * Clean up extraKnownMarketplaces entries that are now properly registered
1367
- * in known_marketplaces.json (the official Claude Code marketplace registry).
1368
- *
1369
- * extraKnownMarketplaces was the old claudeup install mechanism. The official
1370
- * way is `claude plugin marketplace add` which writes to known_marketplaces.json.
1371
- * extraKnownMarketplaces should only be used in project settings for team
1372
- * marketplace suggestions — not in user settings as a primary install path.
1373
- *
1374
- * This function removes magus-related entries from extraKnownMarketplaces in
1375
- * both user and project settings when they're already in known_marketplaces.json.
1376
- */
1377
- export async function cleanupExtraKnownMarketplaces(
1378
- projectPath?: string,
1379
- ): Promise<string[]> {
1380
- const known = await readKnownMarketplaces();
1381
- const cleaned: string[] = [];
1382
-
1383
- // Clean user-level settings
1384
- try {
1385
- const globalSettings = await readGlobalSettings();
1386
- if (globalSettings.extraKnownMarketplaces) {
1387
- let changed = false;
1388
- for (const name of Object.keys(globalSettings.extraKnownMarketplaces)) {
1389
- if (known[name]) {
1390
- delete globalSettings.extraKnownMarketplaces[name];
1391
- cleaned.push(`user:${name}`);
1392
- changed = true;
1393
- }
1394
- }
1395
- // Remove the field entirely if empty
1396
- if (
1397
- changed &&
1398
- Object.keys(globalSettings.extraKnownMarketplaces).length === 0
1399
- ) {
1400
- // biome-ignore lint/performance/noDelete: the empty block is removed from settings.json entirely; an undefined value would survive Object.keys and `in` checks
1401
- delete globalSettings.extraKnownMarketplaces;
1402
- }
1403
- if (changed) {
1404
- await writeGlobalSettings(globalSettings);
1405
- }
1406
- }
1407
- } catch {
1408
- // Non-fatal: user settings cleanup is best-effort
1409
- }
1410
-
1411
- // Clean project-level settings
1412
- if (projectPath) {
1413
- try {
1414
- const projectSettings = await readSettings(projectPath);
1415
- if (projectSettings.extraKnownMarketplaces) {
1416
- let changed = false;
1417
- for (const name of Object.keys(
1418
- projectSettings.extraKnownMarketplaces,
1419
- )) {
1420
- if (known[name]) {
1421
- delete projectSettings.extraKnownMarketplaces[name];
1422
- cleaned.push(`project:${name}`);
1423
- changed = true;
1424
- }
1425
- }
1426
- if (
1427
- changed &&
1428
- Object.keys(projectSettings.extraKnownMarketplaces).length === 0
1429
- ) {
1430
- // biome-ignore lint/performance/noDelete: the empty block is removed from settings.json entirely; an undefined value would survive Object.keys and `in` checks
1431
- delete projectSettings.extraKnownMarketplaces;
1432
- }
1433
- if (changed) {
1434
- await writeSettings(projectSettings, projectPath);
1435
- }
1436
- }
1437
- } catch {
1438
- // Non-fatal: project settings cleanup is best-effort
1439
- }
1440
- }
1441
-
1442
- return cleaned;
1443
- }
1444
-
1445
1273
  /**
1446
1274
  * Read known_marketplaces.json to get marketplace source info
1447
1275
  */
@@ -727,7 +727,7 @@ export function describeCommunityFailure(
727
727
  // ─── Staleness: advisory, two-phase, never automatic ─────────────────────────
728
728
 
729
729
  /**
730
- * 24 hours, matching image-generate's staleness precedent in the root CLAUDE.md.
730
+ * 24 hours, matching the image plugin's staleness precedent in the root CLAUDE.md.
731
731
  *
732
732
  * The TTL is not only a "when to re-check" rule — it is also a "how long may we
733
733
  * keep SAYING up-to-date" rule. A check from three days ago is evidence about
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Prerunner integration for the gitignore manager.
2
+ * Read-only gitignore check for the gitignore manager.
3
3
  *
4
- * Called from `prerunner/index.ts` (Step 0.9) on every claudeup invocation.
5
- * Resolves the manifest, runs detection, and prints a concise warning when
6
- * violations exist. Does NOT auto-apply fixes — the user must open the
7
- * Git State tab and confirm. This mirrors the plugin-version-mismatch flow
8
- * where prerun *warns* and the TUI provides the interactive fix dialog.
4
+ * Called from the TUI on startup and from `doctor`. Resolves the manifest, runs
5
+ * detection, and returns a concise warning when violations exist. Does NOT
6
+ * auto-apply fixes — the user must open the Git State tab and confirm. This
7
+ * mirrors the plugin-version-mismatch flow, where the check only *reports* and
8
+ * the TUI provides the interactive fix dialog.
9
9
  */
10
10
 
11
11
  import type { Violation } from "../types/gitignore.js";
@@ -13,12 +13,14 @@ import { detectViolations } from "./gitignore-detector.js";
13
13
  import { formatViolationSummary } from "./gitignore-fixer.js";
14
14
  import { resolveGitignore } from "./gitignore-resolver.js";
15
15
 
16
- export interface PrerunResult {
16
+ export interface GitignoreCheckResult {
17
17
  violationCount: number;
18
18
  warnings: string[];
19
19
  }
20
20
 
21
- export async function checkGitignore(cwd: string): Promise<PrerunResult> {
21
+ export async function checkGitignore(
22
+ cwd: string,
23
+ ): Promise<GitignoreCheckResult> {
22
24
  const warnings: string[] = [];
23
25
  try {
24
26
  const resolved = await resolveGitignore(cwd, {
@@ -38,10 +40,12 @@ export async function checkGitignore(cwd: string): Promise<PrerunResult> {
38
40
  }
39
41
 
40
42
  /**
41
- * One-line summary suitable for prerun stderr. Returns null when there's
43
+ * One-line summary suitable for stderr. Returns null when there's
42
44
  * nothing to warn about.
43
45
  */
44
- export function formatPrerunWarning(result: PrerunResult): string | null {
46
+ export function formatGitignoreWarning(
47
+ result: GitignoreCheckResult,
48
+ ): string | null {
45
49
  if (result.violationCount === 0) return null;
46
50
  return `⚠ ${result.violationCount} git state issue(s) detected. Run: claudeup → Git State tab to review and fix`;
47
51
  }
@@ -12,7 +12,7 @@
12
12
  * Each call handles ONE violation; the caller is responsible for confirming
13
13
  * each one with the user.
14
14
  *
15
- * `formatViolationSummary` is a pure helper for prerun warning + TUI.
15
+ * `formatViolationSummary` is a pure helper for the warning line + TUI.
16
16
  */
17
17
 
18
18
  import { spawn } from "node:child_process";
@@ -173,9 +173,27 @@ function tap(fn: (() => void) | undefined): void {
173
173
  * one unreachable marketplace must not block launching Claude. Pulls run
174
174
  * concurrently, so a slow one doesn't serialize the rest onto the launch path.
175
175
  */
176
+ /**
177
+ * Select and report, writing nothing.
178
+ *
179
+ * Everything above the pull loop is a read — the marketplace registry and each
180
+ * entry's `autoUpdate` flag — so a read-only caller can have the full selection
181
+ * verdict for free. `--dry-run` and `--check` promise to write nothing and
182
+ * therefore cannot pull, but they must still be able to say "your magus clone
183
+ * has autoUpdate off, so its catalog will never advance". Gating the whole call
184
+ * off silently dropped that warning, which is the same "no signal reads as no
185
+ * problem" failure the flag exists to prevent.
186
+ */
187
+ export async function reportRegisteredMarketplaces(
188
+ skip: Iterable<string> = [],
189
+ ): Promise<RefreshResult> {
190
+ return refreshRegisteredMarketplaces(skip, {}, { reportOnly: true });
191
+ }
192
+
176
193
  export async function refreshRegisteredMarketplaces(
177
194
  skip: Iterable<string> = [],
178
195
  progress: RefreshProgress = {},
196
+ options: { reportOnly?: boolean } = {},
179
197
  ): Promise<RefreshResult> {
180
198
  const refreshed: string[] = [];
181
199
  const failed: string[] = [];
@@ -200,6 +218,18 @@ export async function refreshRegisteredMarketplaces(
200
218
  eligible.push(name);
201
219
  }
202
220
 
221
+ // A read-only caller stops here, with the selection verdict intact and not
222
+ // one clone touched. `eligible` becomes `skipped`: nothing was refreshed, so
223
+ // claiming otherwise would be the false "all current" this module guards.
224
+ if (options.reportOnly) {
225
+ return {
226
+ refreshed: [],
227
+ failed: [],
228
+ skipped: [...skipped, ...eligible],
229
+ autoUpdateDisabled,
230
+ };
231
+ }
232
+
203
233
  // Fast-forward every eligible clone in parallel so a slow one doesn't
204
234
  // serialize the rest onto the launch path. fastForwardCloneIfPresent never
205
235
  // throws, so the batch always resolves.