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.
- 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__/enabled-not-installed.test.ts +1 -2
- 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__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- 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/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +1 -173
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* version-snapshot.ts — detect plugin changes that happened outside claudeup.
|
|
3
3
|
*
|
|
4
4
|
* Plugins get updated by things claudeup does not control: Claude Code's own
|
|
5
|
-
* `plugin install`/`update`,
|
|
5
|
+
* `plugin install`/`update`, or a direct CLI
|
|
6
6
|
* call. All of them write `installed_plugins.json`; none of them tell the user
|
|
7
7
|
* anything the next time claudeup opens, so versions move underfoot silently.
|
|
8
8
|
*
|
package/src/types/bun.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* payload from `Bun.stdin.stream()` because that is the fastest path on the
|
|
8
8
|
* hottest code claudeup has (a PreToolUse hook runs on every Agent call), so
|
|
9
9
|
* the global is declared here rather than the call being rewritten.
|
|
10
|
+
* `services/tool-probe.ts` reads uv's `uv-receipt.toml` with `Bun.TOML.parse`
|
|
11
|
+
* (the receipt decides whether uv's silence may be read as "current"); a
|
|
12
|
+
* missing parser degrades to "cannot tell", so the member is optional.
|
|
10
13
|
*
|
|
11
14
|
* Keep this MINIMAL. Every member added here is a member nobody checks against
|
|
12
15
|
* the real runtime; a full mirror of Bun's API would be a second, worse copy of
|
|
@@ -18,4 +21,7 @@ declare namespace Bun {
|
|
|
18
21
|
const stdin: {
|
|
19
22
|
stream(): AsyncIterable<Uint8Array>;
|
|
20
23
|
};
|
|
24
|
+
const TOML: {
|
|
25
|
+
parse(text: string): unknown;
|
|
26
|
+
};
|
|
21
27
|
}
|
package/src/ui/App.tsx
CHANGED
|
@@ -6,17 +6,13 @@ import {
|
|
|
6
6
|
migrateMarketplaceRename,
|
|
7
7
|
recoverMarketplaceSettings,
|
|
8
8
|
} from "../services/claude-settings.js";
|
|
9
|
-
import { checkGitignore } from "../services/gitignore-
|
|
9
|
+
import { checkGitignore } from "../services/gitignore-check.js";
|
|
10
10
|
import {
|
|
11
11
|
applyAllSafeFixes,
|
|
12
12
|
loadGitignoreState,
|
|
13
13
|
} from "../services/gitignore-service.js";
|
|
14
14
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
15
15
|
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
16
|
-
import {
|
|
17
|
-
type VersionMismatchInfo,
|
|
18
|
-
checkPluginVersionMismatches,
|
|
19
|
-
} from "../services/plugin-version-check.js";
|
|
20
16
|
import {
|
|
21
17
|
type VersionCheckResult,
|
|
22
18
|
checkForUpdates,
|
|
@@ -26,7 +22,6 @@ import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
|
26
22
|
import { ModalContainer } from "./components/modals/index.js";
|
|
27
23
|
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
28
24
|
import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
|
|
29
|
-
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
30
25
|
import {
|
|
31
26
|
AliasScreen,
|
|
32
27
|
CliToolsScreen,
|
|
@@ -365,10 +360,6 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
365
360
|
const [showDebug, setShowDebug] = useState(false);
|
|
366
361
|
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
367
362
|
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
368
|
-
const [mismatchData, setMismatchData] = useState<
|
|
369
|
-
VersionMismatchInfo[] | null
|
|
370
|
-
>(null);
|
|
371
|
-
const mismatchModal = useMismatchModal();
|
|
372
363
|
const [gitignoreData, setGitignoreData] = useState<{
|
|
373
364
|
violationCount: number;
|
|
374
365
|
safeCount: number;
|
|
@@ -400,15 +391,8 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
400
391
|
return () => clearTimeout(timer);
|
|
401
392
|
}, [recoveryReport]);
|
|
402
393
|
|
|
403
|
-
// Show mismatch modal when data arrives
|
|
404
|
-
useEffect(() => {
|
|
405
|
-
if (!mismatchData || mismatchData.length === 0) return;
|
|
406
|
-
mismatchModal.show(mismatchData);
|
|
407
|
-
setMismatchData(null);
|
|
408
|
-
}, [mismatchData, mismatchModal]);
|
|
409
|
-
|
|
410
394
|
// Show gitignore modal — wait for the modal slot to be free so we don't
|
|
411
|
-
// stomp on
|
|
395
|
+
// stomp on another modal that may have just been shown.
|
|
412
396
|
useEffect(() => {
|
|
413
397
|
if (!gitignoreData) return;
|
|
414
398
|
if (state.modal) return;
|
|
@@ -482,15 +466,6 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
482
466
|
})
|
|
483
467
|
.catch(() => {}); // non-fatal
|
|
484
468
|
|
|
485
|
-
// Check for plugin version mismatches (the [0] index bug)
|
|
486
|
-
checkPluginVersionMismatches(process.cwd())
|
|
487
|
-
.then((mismatches) => {
|
|
488
|
-
if (mismatches.length > 0) {
|
|
489
|
-
setMismatchData(mismatches);
|
|
490
|
-
}
|
|
491
|
-
})
|
|
492
|
-
.catch(() => {}); // non-fatal
|
|
493
|
-
|
|
494
469
|
// Check for gitignore manifest violations and surface a popup if any.
|
|
495
470
|
// Loads twice (once for count, once when user picks "fix safe") because
|
|
496
471
|
// the second load happens after edits — keeps detection cheap on startup.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CLI Tools row, built from the SAME probe facts and the SAME classifier
|
|
3
|
+
* `claudeup update` plans with.
|
|
4
|
+
*
|
|
5
|
+
* The screen used to carry its own probe — a shell `which`, `npm view` per
|
|
6
|
+
* tool, and a private comparator that read "1.2.3-beta" as "1.2.3" — so the
|
|
7
|
+
* row and the CLI could disagree about the same tool. Now the row's state is
|
|
8
|
+
* `classifyTool(spec, facts)`, and for a tool the active profile declares the
|
|
9
|
+
* spec IS the profile's (`specForCatalogTool`), so the two cannot differ.
|
|
10
|
+
*
|
|
11
|
+
* The same goes for the COMMANDS: a declared tool's row builds them with
|
|
12
|
+
* `binCommandForState`, the rule `claudeup update`'s apply path uses, so the
|
|
13
|
+
* row's target and the row's key can never aim at different versions.
|
|
14
|
+
*
|
|
15
|
+
* Pure: no IO, no React. The screen gathers facts; this only assembles.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CliTool } from "../../data/cli-tools.js";
|
|
19
|
+
import {
|
|
20
|
+
cliToolInstall,
|
|
21
|
+
cliToolUpdate,
|
|
22
|
+
} from "../../services/cli-tool-commands.js";
|
|
23
|
+
import {
|
|
24
|
+
type InstallInfo,
|
|
25
|
+
type ToolFacts,
|
|
26
|
+
type ToolStatus,
|
|
27
|
+
classifyTool,
|
|
28
|
+
} from "../../services/tool-probe.js";
|
|
29
|
+
import { binCommandForState } from "../../services/toolchain.js";
|
|
30
|
+
import type { BinRequirement, ResolvedBin } from "../../types/index.js";
|
|
31
|
+
import type { Command } from "../../utils/command-utils.js";
|
|
32
|
+
import type { StatusTone } from "../renderers/pluginStatusText.js";
|
|
33
|
+
|
|
34
|
+
export interface CliToolRow {
|
|
35
|
+
tool: CliTool;
|
|
36
|
+
/** The requirement the tool was probed through (closure → detected → catalogue). */
|
|
37
|
+
spec: BinRequirement;
|
|
38
|
+
/** True until facts arrive. */
|
|
39
|
+
checking: boolean;
|
|
40
|
+
/** Null while checking. */
|
|
41
|
+
status: ToolStatus | null;
|
|
42
|
+
install?: InstallInfo;
|
|
43
|
+
/**
|
|
44
|
+
* What Enter runs for an installed tool. For a tool the active profile
|
|
45
|
+
* declares this is the SAME command `claudeup update` would run for the
|
|
46
|
+
* same row (pin included); otherwise the DETECTED method's updater, so it
|
|
47
|
+
* names the installer the row was probed through.
|
|
48
|
+
*/
|
|
49
|
+
updateCommand?: Command;
|
|
50
|
+
/** What Enter runs for a tool that is not installed. */
|
|
51
|
+
installCommand: Command;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Did this spec come from the resolver's closure — i.e. does the active profile
|
|
56
|
+
* declare this tool?
|
|
57
|
+
*
|
|
58
|
+
* `ResolvedBin` is `BinRequirement` plus `sources`, and only the resolver adds
|
|
59
|
+
* them, so the provenance travels with the spec itself rather than being
|
|
60
|
+
* re-derived here (which would be a second copy of `specForCatalogTool`'s
|
|
61
|
+
* branch 1, free to disagree with it).
|
|
62
|
+
*/
|
|
63
|
+
function declaredSpec(spec: BinRequirement): ResolvedBin | null {
|
|
64
|
+
const candidate = spec as ResolvedBin;
|
|
65
|
+
return Array.isArray(candidate.sources) ? candidate : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildToolRow(
|
|
69
|
+
tool: CliTool,
|
|
70
|
+
spec: BinRequirement,
|
|
71
|
+
facts: ToolFacts | undefined,
|
|
72
|
+
install?: InstallInfo,
|
|
73
|
+
): CliToolRow {
|
|
74
|
+
const declared = declaredSpec(spec);
|
|
75
|
+
// A declared tool installs at its pin, the way `claudeup update` does; the
|
|
76
|
+
// catalogue entry has no pin to honour.
|
|
77
|
+
const installCommand =
|
|
78
|
+
(declared && binCommandForState("install", null, declared)) ||
|
|
79
|
+
cliToolInstall(tool);
|
|
80
|
+
const base = { tool, spec, installCommand, ...(install ? { install } : {}) };
|
|
81
|
+
if (!facts) return { ...base, checking: true, status: null };
|
|
82
|
+
const status = classifyTool(spec, facts);
|
|
83
|
+
const updateCommand = updateCommandFor(tool, declared, status, install);
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
checking: false,
|
|
87
|
+
status,
|
|
88
|
+
...(updateCommand ? { updateCommand } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The command the row's key runs, from the same rule as the row's state.
|
|
94
|
+
*
|
|
95
|
+
* A declared tool goes through `binCommandForState` — the CLI apply path's own
|
|
96
|
+
* builder over the PROFILE's requirement — so a row whose target is the pin
|
|
97
|
+
* installs the pin. Building `@latest` here (what this used to do, always) let
|
|
98
|
+
* the TUI overshoot a pin the CLI hits exactly, and the tool then classified
|
|
99
|
+
* `unknown / ahead-of-pin` for good: claudeup never downgrades.
|
|
100
|
+
*
|
|
101
|
+
* An undeclared catalogue tool keeps the screen's own rule, "follow how it IS
|
|
102
|
+
* installed": there is no pin to honour, and `pnpm`/`yarn` have no
|
|
103
|
+
* `BinInstaller` counterpart to hand the shared builder.
|
|
104
|
+
*/
|
|
105
|
+
function updateCommandFor(
|
|
106
|
+
tool: CliTool,
|
|
107
|
+
declared: ResolvedBin | null,
|
|
108
|
+
status: ToolStatus,
|
|
109
|
+
install: InstallInfo | undefined,
|
|
110
|
+
): Command | undefined {
|
|
111
|
+
if (!isInstalled(status)) return undefined;
|
|
112
|
+
if (declared) {
|
|
113
|
+
return binCommandForState("upgrade", status.target, declared) ?? undefined;
|
|
114
|
+
}
|
|
115
|
+
if (!install) return undefined;
|
|
116
|
+
return cliToolUpdate(tool, install.primary, install.brewFormula) ?? undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** On PATH, as far as the probe could tell. */
|
|
120
|
+
export function isInstalled(status: ToolStatus | null): boolean {
|
|
121
|
+
return status !== null && status.state !== "install";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A version for display, with exactly one leading "v". */
|
|
125
|
+
export function displayVersion(v: string): string {
|
|
126
|
+
return `v${v.replace(/^v/i, "")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CliToolStatusText {
|
|
130
|
+
/** Text after the version, with its leading space; "" for nothing to say. */
|
|
131
|
+
suffix: string;
|
|
132
|
+
tone: StatusTone;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The row's status words. An `unknown` shows its note verbatim — the same
|
|
137
|
+
* string `claudeup update` prints for that tool.
|
|
138
|
+
*/
|
|
139
|
+
export function cliToolStatusText(row: CliToolRow): CliToolStatusText {
|
|
140
|
+
const { status } = row;
|
|
141
|
+
if (row.checking || !status) return { suffix: " ...", tone: "muted" };
|
|
142
|
+
switch (status.state) {
|
|
143
|
+
case "unknown":
|
|
144
|
+
return {
|
|
145
|
+
suffix: ` ? ${status.note ?? "cannot tell whether it is current"}`,
|
|
146
|
+
tone: "warning",
|
|
147
|
+
};
|
|
148
|
+
case "upgrade":
|
|
149
|
+
return {
|
|
150
|
+
suffix: status.target
|
|
151
|
+
? ` → ${displayVersion(status.target)}`
|
|
152
|
+
: " update available",
|
|
153
|
+
tone: "warning",
|
|
154
|
+
};
|
|
155
|
+
case "install":
|
|
156
|
+
case "current":
|
|
157
|
+
return { suffix: "", tone: "muted" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Header counts, from the rows' states. */
|
|
162
|
+
export function countToolRows(rows: readonly CliToolRow[]): {
|
|
163
|
+
installed: number;
|
|
164
|
+
updates: number;
|
|
165
|
+
unverified: number;
|
|
166
|
+
} {
|
|
167
|
+
let installed = 0;
|
|
168
|
+
let updates = 0;
|
|
169
|
+
let unverified = 0;
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
if (isInstalled(row.status)) installed++;
|
|
172
|
+
if (row.status?.state === "upgrade") updates++;
|
|
173
|
+
if (row.status?.state === "unknown") unverified++;
|
|
174
|
+
}
|
|
175
|
+
return { installed, updates, unverified };
|
|
176
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
2
2
|
import type { PluginComponent } from "../../services/skills-manager.js";
|
|
3
|
+
import type { ApplyReporter } from "../../services/update-engine.js";
|
|
4
|
+
import type { PluginUpdateItem } from "../../services/update-plan.js";
|
|
3
5
|
import type { Marketplace } from "../../types/index.js";
|
|
4
6
|
import type { PluginContentsEntry } from "../state/types.js";
|
|
5
7
|
|
|
@@ -37,6 +39,12 @@ export interface PluginPluginItem {
|
|
|
37
39
|
isExpandable: boolean;
|
|
38
40
|
/** True when its component rows follow below. */
|
|
39
41
|
isExpanded: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The classifier's answer for this plugin — the same item `claudeup update`
|
|
44
|
+
* plans with. Null for a catalog entry the user is only browsing (neither
|
|
45
|
+
* declared by the profile, installed, nor enabled): it has no state to claim.
|
|
46
|
+
*/
|
|
47
|
+
status: PluginUpdateItem | null;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -87,6 +95,8 @@ export interface BuildPluginBrowserItemsArgs {
|
|
|
87
95
|
collapsedMarketplaces: Set<string>;
|
|
88
96
|
expandedPlugins?: Set<string>;
|
|
89
97
|
pluginContents?: Map<string, PluginContentsEntry>;
|
|
98
|
+
/** `classifyPluginRows` output, by plugin id. A plugin absent from it gets no status. */
|
|
99
|
+
statuses?: ReadonlyMap<string, PluginUpdateItem>;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/**
|
|
@@ -111,6 +121,7 @@ function pushPluginItems(
|
|
|
111
121
|
marketplace: Marketplace,
|
|
112
122
|
expandedPlugins: Set<string>,
|
|
113
123
|
pluginContents: Map<string, PluginContentsEntry>,
|
|
124
|
+
statuses: ReadonlyMap<string, PluginUpdateItem>,
|
|
114
125
|
): void {
|
|
115
126
|
const repo = pluginContentsRepo(plugin, marketplace);
|
|
116
127
|
const isExpandable = !plugin.isOrphaned && !!repo;
|
|
@@ -123,6 +134,7 @@ function pushPluginItems(
|
|
|
123
134
|
plugin,
|
|
124
135
|
isExpandable,
|
|
125
136
|
isExpanded,
|
|
137
|
+
status: statuses.get(plugin.id) ?? null,
|
|
126
138
|
});
|
|
127
139
|
|
|
128
140
|
if (!isExpanded) return;
|
|
@@ -214,6 +226,47 @@ function isMadAppGang(mp: Marketplace): boolean {
|
|
|
214
226
|
return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
|
|
215
227
|
}
|
|
216
228
|
|
|
229
|
+
// ─── "Ran, but nothing moved" ─────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Collect the engine's no-move reasons so the screen can name them.
|
|
233
|
+
*
|
|
234
|
+
* `ApplyResult.unchanged` is the whole point of the read-back: an item that
|
|
235
|
+
* ran, exited 0, and left the machine exactly as it was. The CLI prints
|
|
236
|
+
* `N item(s) ran but did not change:` with each cause; the TUI used to branch
|
|
237
|
+
* on `failed.length` alone, show a success, and refetch a row that had not
|
|
238
|
+
* moved — "press U, press U, forever", which is the experience the read-back
|
|
239
|
+
* was added to remove.
|
|
240
|
+
*
|
|
241
|
+
* The cause itself only ever reaches the reporter (`applyPlugins` formats it as
|
|
242
|
+
* `no change — <why>`), so this captures it there, exactly as `cli/update.ts`
|
|
243
|
+
* does with `finishedDetail`.
|
|
244
|
+
*/
|
|
245
|
+
export function noMoveReasons(): {
|
|
246
|
+
reporter: ApplyReporter;
|
|
247
|
+
/** One line per unchanged item, with its cause when one was reported. */
|
|
248
|
+
lines(unchanged: readonly string[]): string;
|
|
249
|
+
} {
|
|
250
|
+
const why = new Map<string, string>();
|
|
251
|
+
return {
|
|
252
|
+
reporter: {
|
|
253
|
+
begin: () => {},
|
|
254
|
+
finish: (_ok, name, detail) => {
|
|
255
|
+
const reason = detail.replace(/^no change — /, "");
|
|
256
|
+
if (reason !== detail) why.set(name, reason);
|
|
257
|
+
},
|
|
258
|
+
detach: (work) => work(),
|
|
259
|
+
},
|
|
260
|
+
lines: (unchanged) =>
|
|
261
|
+
unchanged
|
|
262
|
+
.map((name) => {
|
|
263
|
+
const reason = why.get(name);
|
|
264
|
+
return reason ? `${name}: ${reason}` : name;
|
|
265
|
+
})
|
|
266
|
+
.join("\n"),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
/**
|
|
218
271
|
* Builds the flat list of items for the PluginsScreen list panel.
|
|
219
272
|
* Extracted from PluginsScreen so it can be tested independently.
|
|
@@ -224,6 +277,7 @@ export function buildPluginBrowserItems({
|
|
|
224
277
|
collapsedMarketplaces,
|
|
225
278
|
expandedPlugins = new Set(),
|
|
226
279
|
pluginContents = new Map(),
|
|
280
|
+
statuses = new Map(),
|
|
227
281
|
}: BuildPluginBrowserItemsArgs): PluginBrowserItem[] {
|
|
228
282
|
const pluginsByMarketplace = new Map<string, PluginInfo[]>();
|
|
229
283
|
for (const plugin of plugins) {
|
|
@@ -279,6 +333,7 @@ export function buildPluginBrowserItems({
|
|
|
279
333
|
marketplace,
|
|
280
334
|
expandedPlugins,
|
|
281
335
|
pluginContents,
|
|
336
|
+
statuses,
|
|
282
337
|
);
|
|
283
338
|
}
|
|
284
339
|
}
|
|
@@ -316,6 +371,7 @@ export function buildPluginBrowserItems({
|
|
|
316
371
|
communityVirtualMp,
|
|
317
372
|
expandedPlugins,
|
|
318
373
|
pluginContents,
|
|
374
|
+
statuses,
|
|
319
375
|
);
|
|
320
376
|
}
|
|
321
377
|
}
|
|
@@ -347,6 +403,7 @@ export function buildPluginBrowserItems({
|
|
|
347
403
|
marketplace,
|
|
348
404
|
expandedPlugins,
|
|
349
405
|
pluginContents,
|
|
406
|
+
statuses,
|
|
350
407
|
);
|
|
351
408
|
}
|
|
352
409
|
}
|
|
@@ -6,7 +6,6 @@ import { InputModal } from "./InputModal.js";
|
|
|
6
6
|
import { LoadingModal } from "./LoadingModal.js";
|
|
7
7
|
import { MessageModal } from "./MessageModal.js";
|
|
8
8
|
import { SelectModal } from "./SelectModal.js";
|
|
9
|
-
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Container that renders the active modal as an overlay
|
|
@@ -29,8 +28,6 @@ export function ModalContainer() {
|
|
|
29
28
|
modalRef.current = modal;
|
|
30
29
|
if (modal?.type === "select") {
|
|
31
30
|
setSelectIndex(modal.defaultIndex ?? 0);
|
|
32
|
-
} else if (modal?.type === "version-mismatch") {
|
|
33
|
-
setSelectIndex(0);
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
|
|
@@ -45,7 +42,6 @@ export function ModalContainer() {
|
|
|
45
42
|
else if (modal.type === "input") modal.onCancel();
|
|
46
43
|
else if (modal.type === "select") modal.onCancel();
|
|
47
44
|
else if (modal.type === "message") modal.onDismiss();
|
|
48
|
-
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
49
45
|
return;
|
|
50
46
|
}
|
|
51
47
|
|
|
@@ -54,7 +50,6 @@ export function ModalContainer() {
|
|
|
54
50
|
if (modal.type === "confirm") modal.onCancel();
|
|
55
51
|
else if (modal.type === "select") modal.onCancel();
|
|
56
52
|
else if (modal.type === "message") modal.onDismiss();
|
|
57
|
-
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
58
53
|
return;
|
|
59
54
|
}
|
|
60
55
|
|
|
@@ -75,19 +70,6 @@ export function ModalContainer() {
|
|
|
75
70
|
return;
|
|
76
71
|
}
|
|
77
72
|
|
|
78
|
-
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
79
|
-
if (modal.type === "version-mismatch") {
|
|
80
|
-
if (key.name === "return" || key.name === "enter") {
|
|
81
|
-
if (selectIndex === 0) modal.onFix();
|
|
82
|
-
else modal.onDismiss();
|
|
83
|
-
} else if (key.name === "up" || key.name === "k") {
|
|
84
|
-
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
85
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
86
|
-
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
87
|
-
}
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
73
|
// Message modal — Enter to dismiss
|
|
92
74
|
if (modal.type === "message") {
|
|
93
75
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -155,14 +137,6 @@ export function ModalContainer() {
|
|
|
155
137
|
case "loading":
|
|
156
138
|
return <LoadingModal message={modal.message} />;
|
|
157
139
|
|
|
158
|
-
case "version-mismatch":
|
|
159
|
-
return (
|
|
160
|
-
<VersionMismatchModal
|
|
161
|
-
mismatches={modal.mismatches}
|
|
162
|
-
defaultIndex={selectIndex}
|
|
163
|
-
/>
|
|
164
|
-
);
|
|
165
|
-
|
|
166
140
|
default:
|
|
167
141
|
return null;
|
|
168
142
|
}
|
package/src/ui/hooks/index.ts
CHANGED
|
@@ -3,9 +3,8 @@ import { useApp, useNavigation } from "../state/AppContext.js";
|
|
|
3
3
|
import type { ModalState } from "../state/types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Startup gitignore-violation modal
|
|
7
|
-
*
|
|
8
|
-
* a problem, asks "add missing ignores / open the tab / dismiss".
|
|
6
|
+
* Startup gitignore-violation modal: claudeup launches, detects a problem, and
|
|
7
|
+
* asks "add missing ignores / open the tab / dismiss".
|
|
9
8
|
*
|
|
10
9
|
* Pure builder is exported separately for tests.
|
|
11
10
|
*/
|