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.
- package/package.json +4 -4
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -0
- package/src/__tests__/cli-tool-commands.test.ts +1 -1
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +142 -2
- package/src/__tests__/content-drift.test.ts +60 -0
- package/src/__tests__/conventions-manager.test.ts +45 -45
- package/src/__tests__/enabled-not-installed.test.ts +2 -3
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
- package/src/__tests__/models-presets-marketplace.test.ts +141 -0
- package/src/__tests__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/resolver.test.ts +3 -3
- package/src/__tests__/scope-action.test.ts +69 -19
- package/src/__tests__/shell-script-callers.test.ts +8 -8
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/update-apply.test.ts +283 -1
- package/src/__tests__/update-plan.test.ts +773 -31
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/cli/router.ts +0 -5
- package/src/cli/update-view.ts +92 -12
- package/src/cli/update.ts +114 -38
- package/src/data/models-presets.ts +10 -11
- package/src/data/predefined-profiles.ts +7 -7
- package/src/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +2 -174
- package/src/services/community-styles.ts +1 -1
- package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
- package/src/services/gitignore-fixer.ts +1 -1
- package/src/services/marketplace-refresh.ts +30 -0
- package/src/services/plugin-manager.ts +200 -283
- package/src/services/resolver.ts +7 -2
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/update-engine.ts +220 -32
- package/src/services/update-plan.ts +640 -160
- package/src/services/version-snapshot.ts +1 -1
- package/src/types/bun.d.ts +6 -0
- package/src/ui/App.tsx +2 -27
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/pluginsAdapter.ts +57 -0
- package/src/ui/components/modals/ModalContainer.tsx +0 -26
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +2 -3
- package/src/ui/renderers/cliToolRenderers.tsx +108 -67
- package/src/ui/renderers/pluginRenderers.tsx +157 -98
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/screens/CliToolsScreen.tsx +154 -280
- package/src/ui/screens/PluginsScreen.tsx +156 -140
- package/src/ui/state/types.ts +0 -7
- package/src/__tests__/dual-write-prevention.test.ts +0 -380
- package/src/__tests__/gap-fill-versions.test.ts +0 -389
- package/src/__tests__/plugin-version-check.test.ts +0 -762
- package/src/cli/claude.ts +0 -21
- package/src/prerunner/index.ts +0 -500
- package/src/services/plugin-version-check.ts +0 -360
- package/src/services/update-cache.ts +0 -78
- package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
- 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
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Read-only gitignore check for the gitignore manager.
|
|
3
3
|
*
|
|
4
|
-
* Called from
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
16
|
+
export interface GitignoreCheckResult {
|
|
17
17
|
violationCount: number;
|
|
18
18
|
warnings: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export async function checkGitignore(
|
|
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
|
|
43
|
+
* One-line summary suitable for stderr. Returns null when there's
|
|
42
44
|
* nothing to warn about.
|
|
43
45
|
*/
|
|
44
|
-
export function
|
|
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
|
|
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.
|