claudeup 6.5.1 → 6.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) 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__/enabled-not-installed.test.ts +1 -2
  14. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  15. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  16. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  17. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  18. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  19. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  20. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  21. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  22. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  23. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  24. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  25. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  26. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  27. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  28. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  29. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  30. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  31. package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
  32. package/src/__tests__/plugin-cli-argv.test.ts +1 -1
  33. package/src/__tests__/plugin-row-text.test.ts +336 -0
  34. package/src/__tests__/scope-action.test.ts +69 -19
  35. package/src/__tests__/shell-script-callers.test.ts +8 -8
  36. package/src/__tests__/tool-probe.test.ts +1400 -0
  37. package/src/__tests__/update-apply.test.ts +283 -1
  38. package/src/__tests__/update-plan.test.ts +773 -31
  39. package/src/__tests__/version-unknown-state.test.ts +388 -0
  40. package/src/cli/router.ts +0 -5
  41. package/src/cli/update-view.ts +92 -12
  42. package/src/cli/update.ts +114 -38
  43. package/src/services/active-closure.ts +117 -0
  44. package/src/services/catalog-cache-store.ts +6 -2
  45. package/src/services/claude-settings.ts +1 -173
  46. package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
  47. package/src/services/gitignore-fixer.ts +1 -1
  48. package/src/services/marketplace-refresh.ts +30 -0
  49. package/src/services/plugin-manager.ts +200 -283
  50. package/src/services/resolver.ts +7 -2
  51. package/src/services/tool-probe.ts +1637 -0
  52. package/src/services/toolchain.ts +97 -0
  53. package/src/services/update-engine.ts +220 -32
  54. package/src/services/update-plan.ts +640 -160
  55. package/src/services/version-snapshot.ts +1 -1
  56. package/src/types/bun.d.ts +6 -0
  57. package/src/ui/App.tsx +2 -27
  58. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  59. package/src/ui/adapters/pluginsAdapter.ts +57 -0
  60. package/src/ui/components/modals/ModalContainer.tsx +0 -26
  61. package/src/ui/hooks/index.ts +0 -1
  62. package/src/ui/hooks/useGitignoreModal.ts +2 -3
  63. package/src/ui/renderers/cliToolRenderers.tsx +108 -67
  64. package/src/ui/renderers/pluginRenderers.tsx +157 -98
  65. package/src/ui/renderers/pluginRowText.ts +70 -0
  66. package/src/ui/renderers/pluginStatusText.ts +56 -0
  67. package/src/ui/screens/CliToolsScreen.tsx +154 -280
  68. package/src/ui/screens/PluginsScreen.tsx +156 -140
  69. package/src/ui/state/types.ts +0 -7
  70. package/src/__tests__/dual-write-prevention.test.ts +0 -380
  71. package/src/__tests__/gap-fill-versions.test.ts +0 -389
  72. package/src/__tests__/plugin-version-check.test.ts +0 -762
  73. package/src/cli/claude.ts +0 -21
  74. package/src/prerunner/index.ts +0 -500
  75. package/src/services/plugin-version-check.ts +0 -360
  76. package/src/services/update-cache.ts +0 -78
  77. package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
  78. package/src/ui/hooks/useMismatchModal.ts +0 -89
package/src/cli/claude.ts DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * `claudeup claude [args...]` — prerunner mode.
3
- *
4
- * Runs plugin/marketplace update & migration checks (1h cache, or `-f`/`--force`
5
- * to bypass), then execs `claude` with the remaining args. Returns claude's exit
6
- * code so the caller can propagate it.
7
- */
8
-
9
- import { prerunClaude } from "../prerunner/index.js";
10
-
11
- export async function runClaudeCommand(claudeArgs: string[]): Promise<number> {
12
- const forceUpdate =
13
- claudeArgs.includes("--force") || claudeArgs.includes("-f");
14
-
15
- // Remove prerunner-specific flags before passing args to claude.
16
- const filteredArgs = claudeArgs.filter(
17
- (arg) => arg !== "--force" && arg !== "-f",
18
- );
19
-
20
- return prerunClaude(filteredArgs, { force: forceUpdate });
21
- }
@@ -1,500 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import fs from "fs-extra";
4
- import {
5
- type PluginScope,
6
- isClaudeAvailable,
7
- repairPlugin,
8
- updatePlugin,
9
- } from "../services/claude-cli.js";
10
- import { runClaude } from "../services/claude-runner.js";
11
- import {
12
- cleanupExtraKnownMarketplaces,
13
- gapFillInstalledPluginVersions,
14
- migrateMarketplaceRename,
15
- readGlobalSettings,
16
- recoverMarketplaceSettings,
17
- saveGlobalInstalledPluginVersion,
18
- saveLocalInstalledPluginVersion,
19
- writeGlobalSettings,
20
- } from "../services/claude-settings.js";
21
- import { clearContentDriftCache } from "../services/content-drift.js";
22
- import {
23
- checkGitignore,
24
- formatPrerunWarning as formatGitignoreWarning,
25
- } from "../services/gitignore-prerun.js";
26
- import { refreshRegisteredMarketplaces } from "../services/marketplace-refresh.js";
27
- import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
28
- import {
29
- clearMarketplaceCache,
30
- compareVersions,
31
- getAvailablePlugins,
32
- saveInstalledPluginVersionForScope,
33
- } from "../services/plugin-manager.js";
34
- import {
35
- checkPluginVersionMismatches,
36
- formatMismatchWarning,
37
- } from "../services/plugin-version-check.js";
38
- import { UpdateCache } from "../services/update-cache.js";
39
-
40
- export interface PrerunOptions {
41
- force?: boolean; // Bypass cache and force update check
42
- }
43
-
44
- const CONTINUITY_PLUGIN_SENTINEL = "tmux-claude-continuity";
45
- const CONTINUITY_PLUGIN_SCRIPT = path.join(
46
- os.homedir(),
47
- ".tmux",
48
- "plugins",
49
- "tmux-claude-continuity",
50
- "scripts",
51
- "on_session_start.sh",
52
- );
53
-
54
- /**
55
- * Ensure tmux-claude-continuity Claude Code hooks are present in global settings.
56
- * If the tmux plugin is installed but the hooks are missing, they are appended.
57
- * Returns a description of what was added, or null if nothing changed.
58
- */
59
- async function ensureTmuxContinuityHooks(): Promise<string | null> {
60
- // Plugin not installed — nothing to do
61
- if (!(await fs.pathExists(CONTINUITY_PLUGIN_SCRIPT))) {
62
- return null;
63
- }
64
-
65
- const settings = await readGlobalSettings();
66
-
67
- // Check if hooks are already configured by looking for the sentinel string
68
- const existingHooks = settings.hooks ?? {};
69
- for (const groups of Object.values(existingHooks)) {
70
- for (const group of groups) {
71
- for (const hook of group.hooks) {
72
- if (hook.command.includes(CONTINUITY_PLUGIN_SENTINEL)) {
73
- return null; // Already configured
74
- }
75
- }
76
- }
77
- }
78
-
79
- // Append hooks, preserving any existing entries in SessionStart and Stop
80
- const sessionStartGroups = existingHooks.SessionStart ?? [];
81
- const stopGroups = existingHooks.Stop ?? [];
82
-
83
- sessionStartGroups.push({
84
- hooks: [
85
- {
86
- type: "command",
87
- command:
88
- "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_session_start.sh",
89
- },
90
- ],
91
- });
92
-
93
- stopGroups.push({
94
- hooks: [
95
- {
96
- type: "command",
97
- command:
98
- "bash ~/.tmux/plugins/tmux-claude-continuity/scripts/on_stop.sh",
99
- },
100
- ],
101
- });
102
-
103
- settings.hooks = {
104
- ...existingHooks,
105
- SessionStart: sessionStartGroups,
106
- Stop: stopGroups,
107
- };
108
-
109
- await writeGlobalSettings(settings);
110
- return "SessionStart + Stop hooks";
111
- }
112
-
113
- /**
114
- * Prerun orchestration: Check for updates, apply them, then run claude
115
- * @param claudeArgs - Arguments to pass to claude CLI
116
- * @param options - Prerun options (force, etc.)
117
- * @returns Exit code from claude process
118
- */
119
- export async function prerunClaude(
120
- claudeArgs: string[],
121
- options: PrerunOptions = {},
122
- ): Promise<number> {
123
- const cache = new UpdateCache();
124
-
125
- try {
126
- // STEP 0: Migrate old marketplace names → magus (idempotent, no-ops if already migrated)
127
- const migration = await migrateMarketplaceRename();
128
- const migTotal =
129
- migration.projectMigrated +
130
- migration.globalMigrated +
131
- migration.localMigrated +
132
- migration.registryMigrated +
133
- (migration.knownMarketplacesMigrated ? 1 : 0);
134
- if (migTotal > 0) {
135
- console.log(`✓ Migrated ${migTotal} plugin reference(s) → magus`);
136
- }
137
-
138
- // STEP 0.5: Auto-add missing marketplaces
139
- // When plugins reference a marketplace that's not installed locally
140
- // (e.g., settings synced from another machine or committed by a
141
- // teammate in .claude/settings.json), add it automatically. Pass
142
- // process.cwd() so project and local enabledPlugins are scanned —
143
- // otherwise a freshly cloned repo whose only @magus references live
144
- // in project settings looks "clean" from the global-scope view.
145
- if (await isClaudeAvailable()) {
146
- const addedMarketplaces = await autoAddMissingMarketplaces(process.cwd());
147
- if (addedMarketplaces.length > 0) {
148
- console.log(
149
- `✓ Auto-added marketplace(s): ${addedMarketplaces.join(", ")}`,
150
- );
151
- }
152
- }
153
-
154
- // STEP 0.55: Clean up extraKnownMarketplaces entries that are now in known_marketplaces.json
155
- // extraKnownMarketplaces was the old install mechanism; the official way is known_marketplaces.json
156
- try {
157
- const cleaned = await cleanupExtraKnownMarketplaces();
158
- if (cleaned.length > 0) {
159
- console.log(
160
- `✓ Cleaned up legacy extraKnownMarketplaces: ${cleaned.join(", ")}`,
161
- );
162
- }
163
- } catch {
164
- // Non-fatal: cleanup is best-effort
165
- }
166
-
167
- // STEP 0.6: Ensure tmux-claude-continuity hooks are configured
168
- const addedHooks = await ensureTmuxContinuityHooks();
169
- if (addedHooks) {
170
- console.log(
171
- "✓ Added tmux-claude-continuity hooks to ~/.claude/settings.json",
172
- );
173
- }
174
-
175
- // STEP 0.7: Gap-fill installedPluginVersions across scopes
176
- // When a plugin is updated at project/local scope, global may lag behind.
177
- // Sync the highest version from any scope up to global so Claude Code
178
- // resolves the latest cached plugin paths at session startup.
179
- try {
180
- const gapFilled = await gapFillInstalledPluginVersions(process.cwd());
181
- if (gapFilled.length > 0) {
182
- console.log(
183
- `✓ Synced ${gapFilled.length} plugin version(s) to global:`,
184
- );
185
- for (const { pluginId, oldVersion, newVersion } of gapFilled) {
186
- console.log(` - ${pluginId}: ${oldVersion} → ${newVersion}`);
187
- }
188
- }
189
- } catch {
190
- // Non-fatal: gap-fill is best-effort
191
- }
192
-
193
- // STEP 0.8: Detect plugin version mismatches caused by the [0] index bug
194
- // Claude Code's pluginLoader.ts takes entries[0] regardless of project,
195
- // so if another project installed a different version first, this project
196
- // will silently load the wrong version.
197
- try {
198
- const cwd = process.cwd();
199
- const mismatches = await checkPluginVersionMismatches(cwd);
200
- if (mismatches.length > 0) {
201
- console.warn("");
202
- console.warn(formatMismatchWarning(mismatches));
203
- console.warn("");
204
- }
205
- } catch {
206
- // Non-fatal: mismatch detection is best-effort
207
- }
208
-
209
- // STEP 0.9: Detect gitignore manifest violations. Warn-only here — the
210
- // fix dialog lives in the TUI Gitignore tab, mirroring the plugin
211
- // version-mismatch flow (since destructive fixes need user confirmation).
212
- try {
213
- const cwd = process.cwd();
214
- const result = await checkGitignore(cwd);
215
- const warning = formatGitignoreWarning(result);
216
- if (warning) {
217
- console.warn("");
218
- console.warn(warning);
219
- console.warn("");
220
- }
221
- } catch {
222
- // Non-fatal: gitignore detection is best-effort
223
- }
224
-
225
- // STEP 1: Check if we should update (time-based cache, or forced)
226
- const shouldUpdate = options.force || (await cache.shouldCheckForUpdates());
227
-
228
- if (options.force) {
229
- console.log("⟳ Forcing plugin update check...");
230
- }
231
-
232
- if (shouldUpdate) {
233
- // STEP 1.5: Recover marketplace settings (enable autoUpdate, remove stale entries)
234
- const recovery = await recoverMarketplaceSettings();
235
- if (recovery.enabledAutoUpdate.length > 0) {
236
- console.log(
237
- `✓ Enabled auto-update for: ${recovery.enabledAutoUpdate.join(", ")}`,
238
- );
239
- }
240
- if (recovery.removed.length > 0) {
241
- console.log(
242
- `✓ Removed stale marketplaces: ${recovery.removed.join(", ")}`,
243
- );
244
- }
245
- if (recovery.reregistered.length > 0) {
246
- console.log(
247
- `✓ Re-registered as GitHub source: ${recovery.reregistered.join(", ")}`,
248
- );
249
- // Re-registration only rewrites known_marketplaces.json (directory →
250
- // github source); it does NOT create the on-disk clone. Clone any whose
251
- // directory is missing, using the NON-DESTRUCTIVE `marketplace add` —
252
- // never `marketplace update`, which deletes the directory before
253
- // re-cloning and can leave it permanently gone on failure (see
254
- // marketplace-refresh.ts / marketplace-sync.ts). Clones that already
255
- // exist are left to STEP 1.7's safe `git pull --ff-only`.
256
- const cliAvailable = await isClaudeAvailable();
257
- if (cliAvailable) {
258
- const { addMarketplace } = await import("../services/claude-cli.js");
259
- const { defaultMarketplaces } = await import(
260
- "../data/marketplaces.js"
261
- );
262
- const marketplacesDir = path.join(
263
- os.homedir(),
264
- ".claude",
265
- "plugins",
266
- "marketplaces",
267
- );
268
- for (const mpName of recovery.reregistered) {
269
- // Already on disk → STEP 1.7 fast-forwards it; nothing to clone.
270
- if (await fs.pathExists(path.join(marketplacesDir, mpName))) {
271
- continue;
272
- }
273
- const repo = defaultMarketplaces.find((m) => m.name === mpName)
274
- ?.source.repo;
275
- if (!repo) continue;
276
- try {
277
- await addMarketplace(repo);
278
- console.log(`✓ Cloned re-registered marketplace: ${mpName}`);
279
- } catch (error) {
280
- console.warn(
281
- `⚠ Failed to clone marketplace ${mpName}:`,
282
- error instanceof Error ? error.message : "Unknown error",
283
- );
284
- }
285
- }
286
- }
287
- }
288
-
289
- // STEP 1.7: Fast-forward the on-disk clones of every registered
290
- // marketplace. Recovery above only *clones* re-registered ones that were
291
- // missing; nothing else advances an existing healthy clone, so updated
292
- // plugin setup metadata — e.g. a pinned dependency version — is never seen
293
- // even though the fetched plugin list is current. Non-destructive
294
- // `git pull --ff-only`: a failed pull leaves the clone stale, never gone.
295
- const mpRefresh = await refreshRegisteredMarketplaces();
296
- if (mpRefresh.refreshed.length > 0) {
297
- console.log(
298
- `✓ Refreshed marketplaces: ${mpRefresh.refreshed.join(", ")}`,
299
- );
300
- }
301
- if (mpRefresh.autoUpdateDisabled.length > 0) {
302
- // Silence here is what let a clone sit 11 days behind: the catalog
303
- // stops moving, so every plugin from that marketplace reads as up
304
- // to date and no update is ever offered.
305
- for (const name of mpRefresh.autoUpdateDisabled) {
306
- console.log(
307
- `⚠ ${name}: auto-update disabled — its plugin catalog will not refresh, so updates stay hidden.`,
308
- );
309
- }
310
- console.log(
311
- ` Re-enable with: claude plugin marketplace update ${mpRefresh.autoUpdateDisabled[0]} (or set autoUpdate:true in ~/.claude/plugins/known_marketplaces.json)`,
312
- );
313
- }
314
- for (const name of mpRefresh.failed) {
315
- console.warn(`⚠ Failed to refresh marketplace: ${name}`);
316
- }
317
-
318
- // STEP 2: Clear cache to force fresh plugin info
319
- clearMarketplaceCache();
320
- // The marketplace refresh above may have moved each clone's HEAD, and
321
- // drift answers are relative to HEAD.
322
- clearContentDriftCache();
323
-
324
- // STEP 3: Get updated plugin info (to detect versions)
325
- const plugins = await getAvailablePlugins();
326
-
327
- // STEP 4: Auto-update enabled plugins via claude CLI
328
- const autoUpdatedPlugins: Array<{
329
- pluginId: string;
330
- oldVersion: string;
331
- newVersion: string;
332
- /** Scopes actually updated — never assume "user". */
333
- scopes: ReadonlyArray<PluginScope>;
334
- }> = [];
335
- /** Plugins whose files drifted under an unchanged version. */
336
- const repairedPlugins: Array<{
337
- pluginId: string;
338
- scope: PluginScope;
339
- }> = [];
340
-
341
- const cliAvailable = await isClaudeAvailable();
342
-
343
- for (const plugin of plugins) {
344
- // Only update if:
345
- // 1. Plugin is enabled
346
- // 2. Plugin has an update available
347
- // 3. Plugin has both installedVersion and version (newVersion)
348
- // A content-stale plugin has NO version bump to detect — its files
349
- // changed under an unchanged version — so it must be repaired
350
- // (uninstall+install) rather than updated. Auto-fixing broken state
351
- // without human interaction is claudeup's job; leaving a plugin
352
- // silently running deleted skills is exactly the state it exists
353
- // to prevent.
354
- if (
355
- plugin.enabled &&
356
- !plugin.hasUpdate &&
357
- plugin.contentStale &&
358
- plugin.installedVersion
359
- ) {
360
- if (!cliAvailable) continue;
361
- const staleScopes = (
362
- [
363
- ["user", plugin.userScope],
364
- ["project", plugin.projectScope],
365
- ["local", plugin.localScope],
366
- ] as const
367
- )
368
- .filter(([, status]) => !!status?.version)
369
- .map(([scope]) => scope);
370
-
371
- for (const scope of staleScopes) {
372
- try {
373
- await repairPlugin(plugin.id, scope, process.cwd());
374
- repairedPlugins.push({ pluginId: plugin.id, scope });
375
- } catch (error) {
376
- console.warn(
377
- `⚠ Failed to repair ${plugin.id} (${scope}):`,
378
- error instanceof Error ? error.message : "Unknown error",
379
- );
380
- }
381
- }
382
- continue;
383
- }
384
-
385
- if (
386
- plugin.enabled &&
387
- plugin.hasUpdate &&
388
- plugin.installedVersion &&
389
- plugin.version
390
- ) {
391
- try {
392
- if (!cliAvailable) {
393
- // CLI unavailable — skip update entirely, don't write phantom state
394
- continue;
395
- }
396
-
397
- // Update every scope that actually holds an outdated install.
398
- //
399
- // This used to be a hardcoded `updatePlugin(plugin.id, "user")`,
400
- // which was wrong for almost every real install: `hasUpdate` is
401
- // computed from the CURRENT PROJECT's resolved version, while
402
- // installs live per project in installed_plugins.json. On this
403
- // machine 495 of 496 magus installs are project-scoped, so the
404
- // prerunner detected project drift, "fixed" it at user scope,
405
- // reported success — and left the project row untouched. Next run
406
- // it detected the same drift again. The update never converged,
407
- // which is what "auto-update ran but an update is still available"
408
- // actually was.
409
- const outdatedScopes = (
410
- [
411
- ["user", plugin.userScope],
412
- ["project", plugin.projectScope],
413
- ["local", plugin.localScope],
414
- ] as const
415
- )
416
- .filter(
417
- ([, status]) =>
418
- !!status?.version &&
419
- compareVersions(plugin.version, status.version) > 0,
420
- )
421
- .map(([scope]) => scope);
422
-
423
- if (outdatedScopes.length === 0) continue;
424
-
425
- for (const scope of outdatedScopes) {
426
- await updatePlugin(plugin.id, scope);
427
- // The CLI does not maintain installedPluginVersions, so keep
428
- // claudeup's own copy in step for the scope we just touched.
429
- await saveInstalledPluginVersionForScope(
430
- plugin.id,
431
- plugin.version,
432
- scope,
433
- );
434
- }
435
-
436
- autoUpdatedPlugins.push({
437
- pluginId: plugin.id,
438
- oldVersion: plugin.installedVersion,
439
- newVersion: plugin.version,
440
- scopes: outdatedScopes,
441
- });
442
- } catch (error) {
443
- // Non-fatal: Log warning and continue
444
- console.warn(
445
- `⚠ Failed to auto-update ${plugin.id}:`,
446
- error instanceof Error ? error.message : "Unknown error",
447
- );
448
- }
449
- }
450
- }
451
-
452
- // STEP 5: Save cache
453
- await cache.saveCheck({
454
- lastUpdateCheck: new Date().toISOString(),
455
- lastUpdateResult: {
456
- updated: [],
457
- failed: [],
458
- autoUpdated: autoUpdatedPlugins,
459
- },
460
- });
461
-
462
- // STEP 6: Display auto-update summary
463
- if (autoUpdatedPlugins.length > 0) {
464
- console.log(`✓ Auto-updated ${autoUpdatedPlugins.length} plugin(s):`);
465
- for (const {
466
- pluginId,
467
- oldVersion,
468
- newVersion,
469
- scopes,
470
- } of autoUpdatedPlugins) {
471
- // Name the scope. "Auto-updated" that silently touched only user
472
- // scope, while the project stayed behind, is exactly how this became
473
- // untrustworthy.
474
- console.log(
475
- ` - ${pluginId}: ${oldVersion} → ${newVersion} (${scopes.join(", ")})`,
476
- );
477
- }
478
- }
479
-
480
- if (repairedPlugins.length > 0) {
481
- console.log(
482
- `✓ Repaired ${repairedPlugins.length} plugin(s) whose files changed without a version bump:`,
483
- );
484
- for (const { pluginId, scope } of repairedPlugins) {
485
- console.log(` - ${pluginId} (${scope})`);
486
- }
487
- }
488
- }
489
- // If !shouldUpdate → silent (no output)
490
- } catch (error) {
491
- // Non-fatal errors - warn and continue
492
- console.warn(
493
- "Warning: Plugin update check failed:",
494
- error instanceof Error ? error.message : "Unknown error",
495
- );
496
- }
497
-
498
- // STEP 8: Always run claude (even if update failed)
499
- return runClaude(claudeArgs);
500
- }