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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TUI CLI Tools row is built from the shared probe facts and the shared
|
|
3
|
+
* classifier — the row can no longer disagree with `claudeup update`.
|
|
4
|
+
*
|
|
5
|
+
* Pure: `buildToolRow` over `specForCatalogTool`, with facts supplied the way
|
|
6
|
+
* `probeTools` would report them. No React, no IO.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { cliTools } from "../data/cli-tools.js";
|
|
11
|
+
import { cliToolInstall } from "../services/cli-tool-commands.js";
|
|
12
|
+
import {
|
|
13
|
+
type ToolFacts,
|
|
14
|
+
classifyTool,
|
|
15
|
+
specForCatalogTool,
|
|
16
|
+
} from "../services/tool-probe.js";
|
|
17
|
+
import { binCommandFor } from "../services/update-engine.js";
|
|
18
|
+
import { planBinUpdates } from "../services/update-plan.js";
|
|
19
|
+
import type { ResolvedBin } from "../types/index.js";
|
|
20
|
+
import {
|
|
21
|
+
buildToolRow,
|
|
22
|
+
cliToolStatusText,
|
|
23
|
+
countToolRows,
|
|
24
|
+
} from "../ui/adapters/cliToolsAdapter.js";
|
|
25
|
+
|
|
26
|
+
const catalogue = (name: string) => {
|
|
27
|
+
const tool = cliTools.find((t) => t.name === name);
|
|
28
|
+
if (!tool) throw new Error(`no catalogue tool ${name}`);
|
|
29
|
+
return tool;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function facts(name: string, over: Partial<ToolFacts> = {}): ToolFacts {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
present: true,
|
|
36
|
+
resolvedPath: `/x/${name}`,
|
|
37
|
+
installed: "1.0.0",
|
|
38
|
+
latest: null,
|
|
39
|
+
behind: null,
|
|
40
|
+
managed: true,
|
|
41
|
+
sources: [],
|
|
42
|
+
...over,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("buildToolRow — which installer a catalogue row is judged through", () => {
|
|
47
|
+
test("catalogue says npm, detected bun: the spec is bun and the state is bun's", () => {
|
|
48
|
+
// codex and opencode on the reporter's machine: catalogued as npm,
|
|
49
|
+
// installed by bun into ~/.bun/install/global.
|
|
50
|
+
const codex = catalogue("codex");
|
|
51
|
+
expect(codex.packageManager).toBe("npm");
|
|
52
|
+
const install = { primary: "bun" as const, all: ["bun" as const] };
|
|
53
|
+
const spec = specForCatalogTool(codex, null, install);
|
|
54
|
+
expect(spec.via).toBe("bun");
|
|
55
|
+
|
|
56
|
+
const row = buildToolRow(
|
|
57
|
+
codex,
|
|
58
|
+
spec,
|
|
59
|
+
facts("codex", { installed: "0.153.2", latest: "0.154.0" }),
|
|
60
|
+
install,
|
|
61
|
+
);
|
|
62
|
+
expect(row.status).toMatchObject({
|
|
63
|
+
state: "upgrade",
|
|
64
|
+
target: "0.154.0",
|
|
65
|
+
});
|
|
66
|
+
// Enter names the same installer the row was probed through.
|
|
67
|
+
expect(row.updateCommand).toEqual({
|
|
68
|
+
cmd: "bun",
|
|
69
|
+
args: ["install", "-g", "@openai/codex@latest"],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("declared by the closure with via npm: the closure wins, and the row equals the CLI plan", () => {
|
|
74
|
+
const codex = catalogue("codex");
|
|
75
|
+
const declared: ResolvedBin = {
|
|
76
|
+
name: "codex",
|
|
77
|
+
via: "npm",
|
|
78
|
+
package: "@openai/codex",
|
|
79
|
+
sources: ["profile:default.cliTools"],
|
|
80
|
+
};
|
|
81
|
+
const install = { primary: "bun" as const, all: ["bun" as const] };
|
|
82
|
+
const spec = specForCatalogTool(codex, [declared], install);
|
|
83
|
+
expect(spec).toBe(declared);
|
|
84
|
+
|
|
85
|
+
// Through npm, the bun-installed file is not npm's: not-managed.
|
|
86
|
+
const f = facts("codex", {
|
|
87
|
+
managed: false,
|
|
88
|
+
resolvedPath:
|
|
89
|
+
"/Users/j/.bun/install/global/node_modules/@openai/codex/bin/codex.js",
|
|
90
|
+
owner: "npm (root /Users/j/.local/lib/node_modules)",
|
|
91
|
+
});
|
|
92
|
+
const row = buildToolRow(codex, spec, f, install);
|
|
93
|
+
const [cli] = planBinUpdates([declared], [f]);
|
|
94
|
+
expect(row.status).toMatchObject({
|
|
95
|
+
state: cli!.action,
|
|
96
|
+
reason: cli!.reason,
|
|
97
|
+
note: cli!.note,
|
|
98
|
+
target: cli!.target,
|
|
99
|
+
});
|
|
100
|
+
expect(row.status?.reason).toBe("not-managed");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("pinned and behind: the row's key installs the PIN, exactly what the CLI runs", () => {
|
|
104
|
+
// The defect this pins: the row read `→ v0.150.0` (the pin) while its
|
|
105
|
+
// key ran `npm install -g @openai/codex@latest`, which installs past the
|
|
106
|
+
// pin and leaves the tool `unknown / ahead-of-pin` for good.
|
|
107
|
+
const codex = catalogue("codex");
|
|
108
|
+
const declared: ResolvedBin = {
|
|
109
|
+
name: "codex",
|
|
110
|
+
via: "npm",
|
|
111
|
+
package: "@openai/codex",
|
|
112
|
+
version: "0.150.0",
|
|
113
|
+
sources: ["profile:default.cliTools"],
|
|
114
|
+
};
|
|
115
|
+
// Detected as bun — the installer the OLD command builder followed.
|
|
116
|
+
const install = { primary: "bun" as const, all: ["bun" as const] };
|
|
117
|
+
const spec = specForCatalogTool(codex, [declared], install);
|
|
118
|
+
const f = facts("codex", { installed: "0.149.0", managed: true });
|
|
119
|
+
|
|
120
|
+
const row = buildToolRow(codex, spec, f, install);
|
|
121
|
+
expect(row.status).toMatchObject({
|
|
122
|
+
state: "upgrade",
|
|
123
|
+
reason: "behind-pin",
|
|
124
|
+
target: "0.150.0",
|
|
125
|
+
});
|
|
126
|
+
expect(row.updateCommand).toEqual({
|
|
127
|
+
cmd: "npm",
|
|
128
|
+
args: ["install", "-g", "@openai/codex@0.150.0"],
|
|
129
|
+
});
|
|
130
|
+
// And it is the same command `claudeup update` would run for this row.
|
|
131
|
+
const [item] = planBinUpdates([declared], [f]);
|
|
132
|
+
expect(row.updateCommand).toEqual(binCommandFor(item!, declared)!);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("declared and absent: Enter installs at the pin, not the catalogue's @latest", () => {
|
|
136
|
+
const codex = catalogue("codex");
|
|
137
|
+
const declared: ResolvedBin = {
|
|
138
|
+
name: "codex",
|
|
139
|
+
via: "npm",
|
|
140
|
+
package: "@openai/codex",
|
|
141
|
+
version: "0.150.0",
|
|
142
|
+
sources: ["profile:default.cliTools"],
|
|
143
|
+
};
|
|
144
|
+
const row = buildToolRow(
|
|
145
|
+
codex,
|
|
146
|
+
specForCatalogTool(codex, [declared], undefined),
|
|
147
|
+
facts("codex", { present: false, installed: null }),
|
|
148
|
+
);
|
|
149
|
+
expect(row.status?.state).toBe("install");
|
|
150
|
+
expect(row.updateCommand).toBeUndefined();
|
|
151
|
+
expect(row.installCommand).toEqual({
|
|
152
|
+
cmd: "npm",
|
|
153
|
+
args: ["install", "-g", "@openai/codex@0.150.0"],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("declared, unpinned and behind: the closure's package, at latest", () => {
|
|
158
|
+
const codex = catalogue("codex");
|
|
159
|
+
const declared: ResolvedBin = {
|
|
160
|
+
name: "codex",
|
|
161
|
+
via: "npm",
|
|
162
|
+
package: "@openai/codex",
|
|
163
|
+
sources: ["plugin:dev@magus"],
|
|
164
|
+
};
|
|
165
|
+
const install = { primary: "bun" as const, all: ["bun" as const] };
|
|
166
|
+
const row = buildToolRow(
|
|
167
|
+
codex,
|
|
168
|
+
specForCatalogTool(codex, [declared], install),
|
|
169
|
+
facts("codex", { installed: "0.153.2", latest: "0.154.0" }),
|
|
170
|
+
install,
|
|
171
|
+
);
|
|
172
|
+
expect(row.status?.state).toBe("upgrade");
|
|
173
|
+
// npm, the PROFILE's installer — not bun, the detected one — because the
|
|
174
|
+
// row's state was judged through npm too.
|
|
175
|
+
expect(row.updateCommand).toEqual({
|
|
176
|
+
cmd: "npm",
|
|
177
|
+
args: ["install", "-g", "@openai/codex@latest"],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("undeclared: the screen's own rule survives — how it IS installed", () => {
|
|
182
|
+
const codex = catalogue("codex");
|
|
183
|
+
const install = { primary: "bun" as const, all: ["bun" as const] };
|
|
184
|
+
const row = buildToolRow(
|
|
185
|
+
codex,
|
|
186
|
+
specForCatalogTool(codex, null, install),
|
|
187
|
+
facts("codex", { installed: "0.153.2", latest: "0.154.0" }),
|
|
188
|
+
install,
|
|
189
|
+
);
|
|
190
|
+
expect(row.updateCommand).toEqual({
|
|
191
|
+
cmd: "bun",
|
|
192
|
+
args: ["install", "-g", "@openai/codex@latest"],
|
|
193
|
+
});
|
|
194
|
+
expect(row.installCommand).toEqual(cliToolInstall(codex));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("detected pnpm → the catalogue's via, and a not-managed row naming the real path", () => {
|
|
198
|
+
const opencode = catalogue("opencode");
|
|
199
|
+
const install = { primary: "pnpm" as const, all: ["pnpm" as const] };
|
|
200
|
+
const spec = specForCatalogTool(opencode, null, install);
|
|
201
|
+
expect(spec.via).toBe(opencode.packageManager);
|
|
202
|
+
|
|
203
|
+
const f = facts("opencode", {
|
|
204
|
+
managed: false,
|
|
205
|
+
resolvedPath:
|
|
206
|
+
"/Users/j/.local/share/pnpm/global/5/node_modules/opencode-ai/bin/opencode",
|
|
207
|
+
owner: "npm (root /Users/j/.local/lib/node_modules)",
|
|
208
|
+
});
|
|
209
|
+
const row = buildToolRow(opencode, spec, f, install);
|
|
210
|
+
expect(row.status?.reason).toBe("not-managed");
|
|
211
|
+
expect(row.status?.note).toContain("/Users/j/.local/share/pnpm/");
|
|
212
|
+
// Enter still follows how it IS installed (pnpm).
|
|
213
|
+
expect(row.updateCommand?.cmd).toBe("pnpm");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("no facts yet → checking, no status, no command", () => {
|
|
217
|
+
const tool = catalogue("claudish");
|
|
218
|
+
const row = buildToolRow(
|
|
219
|
+
tool,
|
|
220
|
+
specForCatalogTool(tool, null, undefined),
|
|
221
|
+
undefined,
|
|
222
|
+
);
|
|
223
|
+
expect(row).toMatchObject({ checking: true, status: null });
|
|
224
|
+
expect(row.updateCommand).toBeUndefined();
|
|
225
|
+
expect(cliToolStatusText(row).suffix).toBe(" ...");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("absent → install, with no update command", () => {
|
|
229
|
+
const tool = catalogue("cline");
|
|
230
|
+
const spec = specForCatalogTool(tool, null, undefined);
|
|
231
|
+
const row = buildToolRow(
|
|
232
|
+
tool,
|
|
233
|
+
spec,
|
|
234
|
+
facts("cline", { present: false, installed: null }),
|
|
235
|
+
{ primary: "unknown", all: [] },
|
|
236
|
+
);
|
|
237
|
+
expect(row.status?.state).toBe("install");
|
|
238
|
+
expect(row.updateCommand).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("cliToolStatusText — the row's words", () => {
|
|
243
|
+
test("unknown renders as ? <note>, the note verbatim", () => {
|
|
244
|
+
const tool = catalogue("crush");
|
|
245
|
+
const spec = specForCatalogTool(tool, null, {
|
|
246
|
+
primary: "npm",
|
|
247
|
+
all: ["npm"],
|
|
248
|
+
});
|
|
249
|
+
const f = facts("crush", { managed: false, owner: "npm (root /l)" });
|
|
250
|
+
const row = buildToolRow(tool, spec, f);
|
|
251
|
+
const text = cliToolStatusText(row);
|
|
252
|
+
expect(text.tone).toBe("warning");
|
|
253
|
+
expect(text.suffix).toBe(` ? ${classifyTool(spec, f).note}`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("upgrade names the target once, with one v", () => {
|
|
257
|
+
const tool = catalogue("mnemex");
|
|
258
|
+
const spec = specForCatalogTool(tool, null, {
|
|
259
|
+
primary: "bun",
|
|
260
|
+
all: ["bun"],
|
|
261
|
+
});
|
|
262
|
+
const row = buildToolRow(tool, spec, facts("mnemex", { latest: "v1.2.0" }));
|
|
263
|
+
expect(cliToolStatusText(row).suffix).toBe(" → v1.2.0");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("current and install say nothing extra", () => {
|
|
267
|
+
const tool = catalogue("mnemex");
|
|
268
|
+
const spec = specForCatalogTool(tool, null, undefined);
|
|
269
|
+
expect(
|
|
270
|
+
cliToolStatusText(
|
|
271
|
+
buildToolRow(tool, spec, facts("mnemex", { behind: false })),
|
|
272
|
+
).suffix,
|
|
273
|
+
).toBe("");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("countToolRows — header counts from states", () => {
|
|
278
|
+
test("installed, updates and unverified", () => {
|
|
279
|
+
const tool = catalogue("mnemex");
|
|
280
|
+
const spec = specForCatalogTool(tool, null, undefined);
|
|
281
|
+
const rows = [
|
|
282
|
+
buildToolRow(tool, spec, facts("mnemex", { behind: false })),
|
|
283
|
+
buildToolRow(tool, spec, facts("mnemex", { behind: true })),
|
|
284
|
+
buildToolRow(tool, spec, facts("mnemex", { managed: false })),
|
|
285
|
+
buildToolRow(tool, spec, facts("mnemex", { present: false })),
|
|
286
|
+
buildToolRow(tool, spec, undefined),
|
|
287
|
+
];
|
|
288
|
+
expect(countToolRows(rows)).toEqual({
|
|
289
|
+
installed: 3,
|
|
290
|
+
updates: 1,
|
|
291
|
+
unverified: 1,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -19,10 +19,14 @@ import {
|
|
|
19
19
|
applyFrame,
|
|
20
20
|
applyRow,
|
|
21
21
|
header,
|
|
22
|
+
nothingToApplyNotice,
|
|
22
23
|
planReport,
|
|
23
24
|
stepFrame,
|
|
24
25
|
stepRecord,
|
|
25
26
|
summaryLine,
|
|
27
|
+
uncheckedGateNotice,
|
|
28
|
+
unknownPluginsNotice,
|
|
29
|
+
unknownToolsNotice,
|
|
26
30
|
} from "../cli/update-view.js";
|
|
27
31
|
import type { UpdatePlan } from "../services/update-plan.js";
|
|
28
32
|
|
|
@@ -73,8 +77,26 @@ function plan(over: Partial<UpdatePlan> = {}): UpdatePlan {
|
|
|
73
77
|
},
|
|
74
78
|
],
|
|
75
79
|
bins: [
|
|
76
|
-
{
|
|
77
|
-
|
|
80
|
+
{
|
|
81
|
+
name: "tmux-mcp",
|
|
82
|
+
action: "current",
|
|
83
|
+
reason: "at-pin",
|
|
84
|
+
version: "v1.7.1",
|
|
85
|
+
installed: "v1.7.1",
|
|
86
|
+
latest: null,
|
|
87
|
+
target: null,
|
|
88
|
+
sources: [],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "tmux",
|
|
92
|
+
action: "upgrade",
|
|
93
|
+
reason: "behind",
|
|
94
|
+
installed: "3.7b",
|
|
95
|
+
latest: "3.7c",
|
|
96
|
+
target: "3.7c",
|
|
97
|
+
note: "3.7c is available",
|
|
98
|
+
sources: [],
|
|
99
|
+
},
|
|
78
100
|
],
|
|
79
101
|
skills: [
|
|
80
102
|
{
|
|
@@ -271,6 +293,15 @@ describe("bars", () => {
|
|
|
271
293
|
const line = summaryLine(0, 0, 0, 0, 100)[1]!;
|
|
272
294
|
expect(line.match(/[█░]+/)![0]).toHaveLength(36);
|
|
273
295
|
});
|
|
296
|
+
|
|
297
|
+
test("unchanged is its own count, never folded into changed", () => {
|
|
298
|
+
setColorEnabled(false);
|
|
299
|
+
const line = summaryLine(1, 4, 0, 0, 100, 2)[1]!;
|
|
300
|
+
expect(line).toContain("1 changed");
|
|
301
|
+
expect(line).toContain("2 unchanged");
|
|
302
|
+
// Omitted when zero, and the parameter defaults to zero.
|
|
303
|
+
expect(summaryLine(1, 4, 0, 0, 100)[1]).not.toContain("unchanged");
|
|
304
|
+
});
|
|
274
305
|
});
|
|
275
306
|
|
|
276
307
|
// -- misc ---------------------------------------------------------------------
|
|
@@ -301,5 +332,114 @@ describe("sections", () => {
|
|
|
301
332
|
(l) => l.includes("tmux ") && l.includes("UPGRADE"),
|
|
302
333
|
)!;
|
|
303
334
|
expect(row).toContain("brew upgrade tmux");
|
|
335
|
+
expect(row).toContain("3.7b → 3.7c");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("an unpinned CURRENT tool shows its version, not 'pinned —' (F8)", () => {
|
|
339
|
+
setColorEnabled(false);
|
|
340
|
+
const p = plan();
|
|
341
|
+
p.bins = [
|
|
342
|
+
{
|
|
343
|
+
name: "tmux",
|
|
344
|
+
action: "current",
|
|
345
|
+
reason: "up-to-date",
|
|
346
|
+
installed: "3.7c",
|
|
347
|
+
latest: null,
|
|
348
|
+
target: null,
|
|
349
|
+
sources: [],
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
const row = planReport(p).find(
|
|
353
|
+
(l) => l.includes("tmux ") && l.includes("CURRENT"),
|
|
354
|
+
)!;
|
|
355
|
+
expect(row).toContain("3.7c");
|
|
356
|
+
expect(row).not.toContain("pinned");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("a pinned current tool still names its pin", () => {
|
|
360
|
+
setColorEnabled(false);
|
|
361
|
+
const row = planReport(plan()).find((l) => l.includes("tmux-mcp"))!;
|
|
362
|
+
expect(row).toContain("pinned v1.7.1");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("an UNKNOWN tool shows its reason and no command", () => {
|
|
366
|
+
setColorEnabled(false);
|
|
367
|
+
const p = plan();
|
|
368
|
+
p.bins = [
|
|
369
|
+
{
|
|
370
|
+
name: "crush",
|
|
371
|
+
action: "unknown",
|
|
372
|
+
reason: "not-managed",
|
|
373
|
+
note: "on PATH at /n/crush.js, which npm (root /l) does not own",
|
|
374
|
+
installed: "0.7.1",
|
|
375
|
+
latest: null,
|
|
376
|
+
target: null,
|
|
377
|
+
sources: [],
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
p.binSpecs = new Map([
|
|
381
|
+
[
|
|
382
|
+
"crush",
|
|
383
|
+
{ name: "crush", via: "npm", package: "@charmland/crush", sources: [] },
|
|
384
|
+
],
|
|
385
|
+
]);
|
|
386
|
+
const row = planReport(p).find((l) => l.includes("crush"))!;
|
|
387
|
+
expect(row).toContain("0.7.1");
|
|
388
|
+
expect(row).toContain("which npm (root /l) does not own");
|
|
389
|
+
expect(row).not.toContain("npm install");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// -- unverified notices -------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
describe("unverified notices", () => {
|
|
396
|
+
// A plugin is `unknown` for several reasons, each printed on its own row. The
|
|
397
|
+
// closing notices must hold for every one of them. They used to say "re-run
|
|
398
|
+
// once the catalog is reachable", which is false advice for a plugin whose
|
|
399
|
+
// marketplace publishes no version — the case a real dry run showed five of.
|
|
400
|
+
const notices = () => [
|
|
401
|
+
unknownPluginsNotice(5),
|
|
402
|
+
nothingToApplyNotice(5),
|
|
403
|
+
unknownToolsNotice(1, ["crush"]),
|
|
404
|
+
uncheckedGateNotice(6),
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
test("never advise a re-run, never blame the catalog", () => {
|
|
408
|
+
setColorEnabled(false);
|
|
409
|
+
for (const line of notices()) {
|
|
410
|
+
expect(line).not.toMatch(/re-run/i);
|
|
411
|
+
expect(line).not.toContain("catalog is reachable");
|
|
412
|
+
expect(line).toContain("Reasons are shown above.");
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("an unverified plugin is never reported as up to date", () => {
|
|
417
|
+
setColorEnabled(false);
|
|
418
|
+
const under = unknownPluginsNotice(5);
|
|
419
|
+
expect(under).toContain("5 plugin(s) could not be checked");
|
|
420
|
+
expect(under).toContain("They are NOT reported as up to date.");
|
|
421
|
+
const nothing = nothingToApplyNotice(5);
|
|
422
|
+
expect(nothing.trimStart()).toStartWith("⚠ Nothing to apply.");
|
|
423
|
+
expect(nothing).toContain("5 plugin(s) could not be checked");
|
|
424
|
+
expect(nothing).toContain("They are NOT reported as up to date.");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("the tool notice names the tools it could not check", () => {
|
|
428
|
+
setColorEnabled(false);
|
|
429
|
+
expect(unknownToolsNotice(2, ["crush", "tmux"])).toContain(
|
|
430
|
+
"2 tool(s) could not be checked: crush, tmux — not applied.",
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("the gate notice says the unknowns passed without being verified", () => {
|
|
435
|
+
// `--check` exits 0 with unknowns present, so this line carries the whole
|
|
436
|
+
// difference between "nothing is known to be behind" and "all verified".
|
|
437
|
+
setColorEnabled(false);
|
|
438
|
+
const line = uncheckedGateNotice(6);
|
|
439
|
+
expect(line).toContain("6 item(s) could not be checked");
|
|
440
|
+
expect(line).toContain("they do not fail this gate");
|
|
441
|
+
expect(line).toContain("They are NOT reported as up to date.");
|
|
442
|
+
// A warning, not a failure: the gate is about to pass.
|
|
443
|
+
expect(line.trimStart()).toStartWith("⚠");
|
|
304
444
|
});
|
|
305
445
|
});
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
hasContentDrift,
|
|
17
17
|
repoHeadSha,
|
|
18
18
|
} from "../services/content-drift.js";
|
|
19
|
+
import {
|
|
20
|
+
type PluginInfo,
|
|
21
|
+
contentDriftCandidate,
|
|
22
|
+
} from "../services/plugin-manager.js";
|
|
19
23
|
|
|
20
24
|
let configDir: string;
|
|
21
25
|
let prevConfigDir: string | undefined;
|
|
@@ -291,3 +295,59 @@ describe("repoHeadSha", () => {
|
|
|
291
295
|
).toBeUndefined();
|
|
292
296
|
});
|
|
293
297
|
});
|
|
298
|
+
|
|
299
|
+
describe("contentDriftCandidate — which plugins are even asked", () => {
|
|
300
|
+
const info = (over: Partial<PluginInfo> = {}): PluginInfo => ({
|
|
301
|
+
id: "dev@magus",
|
|
302
|
+
name: "dev",
|
|
303
|
+
version: "3.0.0",
|
|
304
|
+
description: "",
|
|
305
|
+
marketplace: "magus",
|
|
306
|
+
marketplaceDisplay: "magus",
|
|
307
|
+
enabled: true,
|
|
308
|
+
installedVersion: "3.0.0",
|
|
309
|
+
...over,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("same version, provably: asked", () => {
|
|
313
|
+
expect(contentDriftCandidate(info())).toBe(true);
|
|
314
|
+
// The comparator, not string equality: "3.0" IS 3.0.0.
|
|
315
|
+
expect(contentDriftCandidate(info({ installedVersion: "3.0" }))).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("the pre-classifier `hasUpdate` no longer decides", () => {
|
|
319
|
+
// It was the gate's first clause. It could only ever be true for a pair
|
|
320
|
+
// the version compare already rejects, and reading it here made the
|
|
321
|
+
// input the shared classifier consumes depend on the rule it replaced.
|
|
322
|
+
// A stale flag must not suppress the question.
|
|
323
|
+
expect(contentDriftCandidate(info({ hasUpdate: true }))).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("behind the catalog: NOT asked — that is an update, not drift", () => {
|
|
327
|
+
// hasContentDrift diffs the installed commit against the clone HEAD, so
|
|
328
|
+
// a plugin that is merely behind always "differs". Calling that drift
|
|
329
|
+
// would make it a repair, and a repair reinstalls whatever the clone
|
|
330
|
+
// holds — past a pin.
|
|
331
|
+
expect(contentDriftCandidate(info({ installedVersion: "2.9.0" }))).toBe(
|
|
332
|
+
false,
|
|
333
|
+
);
|
|
334
|
+
expect(
|
|
335
|
+
contentDriftCandidate(
|
|
336
|
+
info({ hasUpdate: false, installedVersion: "2.9.0" }),
|
|
337
|
+
),
|
|
338
|
+
).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("unorderable, not installed, no catalog version, orphaned: not asked", () => {
|
|
342
|
+
expect(
|
|
343
|
+
contentDriftCandidate(
|
|
344
|
+
info({ version: "517b2fcd1b60", installedVersion: "b819188d2eea" }),
|
|
345
|
+
),
|
|
346
|
+
).toBe(false);
|
|
347
|
+
expect(contentDriftCandidate(info({ installedVersion: undefined }))).toBe(
|
|
348
|
+
false,
|
|
349
|
+
);
|
|
350
|
+
expect(contentDriftCandidate(info({ version: null }))).toBe(false);
|
|
351
|
+
expect(contentDriftCandidate(info({ isOrphaned: true }))).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -8,8 +8,7 @@ import type { PluginInfo } from "../services/plugin-manager.js";
|
|
|
8
8
|
*
|
|
9
9
|
* It is also invisible to every update path, because the check is
|
|
10
10
|
* `installedVersion && compare(latest, installedVersion) > 0` — with no
|
|
11
|
-
* installed version that is false, so
|
|
12
|
-
* prerunner's auto-update will ever touch it.
|
|
11
|
+
* installed version that is false, so no update path will ever touch it.
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
const plugin = (over: Partial<PluginInfo>): PluginInfo =>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# NOT from this machine: nothing brew manages here is outdated, so no real non-empty output could be captured read-only.
|
|
2
|
+
# This is Homebrew's OWN expected output for `brew outdated --json=v2`, copied verbatim from
|
|
3
|
+
# /opt/homebrew/Library/Homebrew/test/cmd/outdated_spec.rb ("outputs JSON for outdated formulae and casks")
|
|
4
|
+
# at Homebrew commit 088d2f79849d1b08792459b65026207a91c545d6 (JSON.pretty_generate of that hash).
|
|
5
|
+
# The generator is cmd/outdated.rb json_info: { name: f.full_name, installed_versions, current_version, pinned, pinned_version }.
|
|
6
|
+
{
|
|
7
|
+
"formulae": [
|
|
8
|
+
{
|
|
9
|
+
"name": "testball",
|
|
10
|
+
"installed_versions": [
|
|
11
|
+
"0.0.1"
|
|
12
|
+
],
|
|
13
|
+
"current_version": "0.1",
|
|
14
|
+
"pinned": false,
|
|
15
|
+
"pinned_version": null
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"casks": [
|
|
19
|
+
{
|
|
20
|
+
"name": "local-caffeine",
|
|
21
|
+
"installed_versions": [
|
|
22
|
+
"1.2.2"
|
|
23
|
+
],
|
|
24
|
+
"current_version": "1.2.3",
|
|
25
|
+
"pinned": false,
|
|
26
|
+
"pinned_version": null
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# captured 2026-09-11 23:12 AEST and again 2026-09-12 00:40 AEST on the reporter's machine (Homebrew 6.0.22), read-only:
|
|
2
|
+
# HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 NO_COLOR=1 brew outdated --json=v2 tmux
|
|
3
|
+
# exit=0 (tmux 3.7c installed, current)
|
|
4
|
+
{
|
|
5
|
+
"formulae": [],
|
|
6
|
+
"casks": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# captured 2026-09-12 00:40 AEST, read-only:
|
|
2
|
+
# HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 NO_COLOR=1 brew outdated --json=v2 cowsay-not-a-formula-xyz
|
|
3
|
+
# exit=1, stdout empty, stderr below. (Assumption A5 was half wrong: a formula brew KNOWS but has not
|
|
4
|
+
# installed — wget was tried before it turned out to be installed — exits 0 with formulae: [], so ownership
|
|
5
|
+
# must be decided before brew's silence is believed.)
|
|
6
|
+
Error: No available formula with the name "cowsay-not-a-formula-xyz".
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# captured 2026-09-12 00:58 AEST, read-only: go version -m /Users/jack/go/bin/tmux-mcp (go 1.27.1, exit=0)
|
|
2
|
+
/Users/jack/go/bin/tmux-mcp: go1.27.1
|
|
3
|
+
path github.com/MadAppGang/tmux-mcp/v2
|
|
4
|
+
mod github.com/MadAppGang/tmux-mcp/v2 v2.0.0 h1:DhieVeG/e6K/+++7XrP9ruhCnywVatf8e+bnjzjMLXE=
|
|
5
|
+
dep github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
|
6
|
+
dep github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
|
7
|
+
dep github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
|
8
|
+
dep github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
9
|
+
dep github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
|
|
10
|
+
dep github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
|
11
|
+
dep github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
|
|
12
|
+
dep github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
|
13
|
+
dep github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
|
14
|
+
dep github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
|
15
|
+
dep golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
|
16
|
+
dep gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
17
|
+
build -buildmode=exe
|
|
18
|
+
build -compiler=gc
|
|
19
|
+
build DefaultGODEBUG=tracebacklabels=0,x509sslcertoverrideplatform=0
|
|
20
|
+
build CGO_ENABLED=1
|
|
21
|
+
build CGO_CFLAGS=
|
|
22
|
+
build CGO_CPPFLAGS=
|
|
23
|
+
build CGO_CXXFLAGS=
|
|
24
|
+
build CGO_LDFLAGS=
|
|
25
|
+
build GOARCH=arm64
|
|
26
|
+
build GOOS=darwin
|
|
27
|
+
build GOARM64=v8.0
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# captured 2026-09-12 00:50 AEST, read-only GET: curl -sS 'https://registry.npmjs.org/-/package/%40nope-xyz%2Fdoes-not-exist-zz/dist-tags'
|
|
2
|
+
# http 401 — the registry answers 401, not 404, for a scoped name that does not exist (status trailer removed)
|
|
3
|
+
"Unauthorized"
|
|
4
|
+
# http=401
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# captured 2026-09-12 00:50 AEST, read-only GET (verifies A3: a scoped name with / encoded as %2F answers):
|
|
2
|
+
# curl -sS 'https://registry.npmjs.org/-/package/%40openai%2Fcodex/dist-tags' (http 200; curl's status trailer removed)
|
|
3
|
+
{"beta":"0.1.2505172116","native":"0.1.2505291658","linux-arm64":"0.154.0-linux-arm64","latest":"0.154.0","linux-x64":"0.154.0-linux-x64","win32-arm64":"0.154.0-win32-arm64","darwin-arm64":"0.154.0-darwin-arm64","darwin-x64":"0.154.0-darwin-x64","win32-x64":"0.154.0-win32-x64","alpha-darwin-arm64":"0.155.0-alpha.3.9-darwin-arm64","alpha":"0.155.0-alpha.3.9","alpha-linux-arm64":"0.155.0-alpha.3.9-linux-arm64","alpha-darwin-x64":"0.155.0-alpha.3.9-darwin-x64","alpha-linux-x64":"0.155.0-alpha.3.9-linux-x64","alpha-win32-x64":"0.155.0-alpha.3.9-win32-x64","alpha-win32-arm64":"0.155.0-alpha.3.9-win32-arm64"}
|
|
4
|
+
# http=200
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# captured 2026-09-12 00:42 AEST, read-only: cat ~/.local/share/uv/tools/browser-use/uv-receipt.toml (uv 0.11.15)
|
|
2
|
+
[tool]
|
|
3
|
+
requirements = [{ name = "browser-use" }]
|
|
4
|
+
entrypoints = [
|
|
5
|
+
{ name = "browser", install-path = "/Users/jack/.local/bin/browser", from = "browser-use" },
|
|
6
|
+
{ name = "browser-use", install-path = "/Users/jack/.local/bin/browser-use", from = "browser-use" },
|
|
7
|
+
{ name = "browser-use-tui", install-path = "/Users/jack/.local/bin/browser-use-tui", from = "browser-use" },
|
|
8
|
+
{ name = "browseruse", install-path = "/Users/jack/.local/bin/browseruse", from = "browser-use" },
|
|
9
|
+
{ name = "bu", install-path = "/Users/jack/.local/bin/bu", from = "browser-use" },
|
|
10
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# captured 2026-09-12 00:55 AEST: UV_TOOL_DIR=$SESSION/validation/captures/uv-scratch/tools UV_TOOL_BIN_DIR=$SESSION/validation/captures/uv-scratch/bin UV_CACHE_DIR=$SESSION/validation/captures/uv-scratch/cache UV_PYTHON_INSTALL_DIR=$SESSION/validation/captures/uv-scratch/python NO_COLOR=1 uv tool install --force git+https://github.com/VaasuDevanS/cowsay-python@v6.1
|
|
2
|
+
# then: cat $SESSION/validation/captures/uv-scratch/tools/cowsay/uv-receipt.toml
|
|
3
|
+
# $SESSION = claudish worktree update-fix2 ai-docs/sessions/dev-feature-claudeup-unify-20260911-2240 (the user's real uv tools were never touched)
|
|
4
|
+
[tool]
|
|
5
|
+
requirements = [{ name = "cowsay", git = "https://github.com/VaasuDevanS/cowsay-python?rev=v6.1" }]
|
|
6
|
+
entrypoints = [
|
|
7
|
+
{ name = "cowsay", install-path = "/Users/jack/mag/claudish/.claude/worktrees/update-fix2/ai-docs/sessions/dev-feature-claudeup-unify-20260911-2240/validation/captures/uv-scratch/bin/cowsay", from = "cowsay" },
|
|
8
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# captured 2026-09-12 00:54 AEST: UV_TOOL_DIR=$SESSION/validation/captures/uv-scratch/tools UV_TOOL_BIN_DIR=$SESSION/validation/captures/uv-scratch/bin UV_CACHE_DIR=$SESSION/validation/captures/uv-scratch/cache UV_PYTHON_INSTALL_DIR=$SESSION/validation/captures/uv-scratch/python NO_COLOR=1 uv tool install 'cowsay==6.0'
|
|
2
|
+
# then: cat $SESSION/validation/captures/uv-scratch/tools/cowsay/uv-receipt.toml
|
|
3
|
+
# $SESSION = claudish worktree update-fix2 ai-docs/sessions/dev-feature-claudeup-unify-20260911-2240 (the user's real uv tools were never touched)
|
|
4
|
+
[tool]
|
|
5
|
+
requirements = [{ name = "cowsay", specifier = "==6.0" }]
|
|
6
|
+
entrypoints = [
|
|
7
|
+
{ name = "cowsay", install-path = "/Users/jack/mag/claudish/.claude/worktrees/update-fix2/ai-docs/sessions/dev-feature-claudeup-unify-20260911-2240/validation/captures/uv-scratch/bin/cowsay", from = "cowsay" },
|
|
8
|
+
]
|