claudeup 4.10.1 → 4.11.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 +1 -1
- package/src/__tests__/plugin-setup.test.ts +300 -0
- package/src/__tests__/plugin-version-check.test.ts +760 -0
- package/src/prerunner/index.js +17 -0
- package/src/prerunner/index.ts +20 -0
- package/src/services/plugin-setup.js +88 -1
- package/src/services/plugin-setup.ts +99 -1
- package/src/services/plugin-version-check.js +248 -0
- package/src/services/plugin-version-check.ts +340 -0
- package/src/ui/App.js +52 -27
- package/src/ui/App.tsx +102 -68
- package/src/ui/screens/PluginsScreen.js +86 -13
- package/src/ui/screens/PluginsScreen.tsx +135 -24
- package/src/ui/state/DimensionsContext.js +8 -6
- package/src/ui/state/DimensionsContext.tsx +10 -1
package/package.json
CHANGED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plugin-setup.ts: go package manager and required binary validator.
|
|
3
|
+
*
|
|
4
|
+
* Tests extractGoBinaryName (pure function), and uses module mocking to
|
|
5
|
+
* verify installPluginDeps() and checkMissingDeps() behavior for `go`
|
|
6
|
+
* and `required` setup keys.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Mocks — must be set up BEFORE any import from plugin-setup
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// Track which commands are "available" and what execFile calls return
|
|
16
|
+
let availableBinaries: Set<string>;
|
|
17
|
+
let execResults: Map<
|
|
18
|
+
string,
|
|
19
|
+
{ stdout: string; stderr: string; error?: boolean }
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
// Mock command-utils BEFORE importing plugin-setup
|
|
23
|
+
mock.module("../utils/command-utils.js", () => ({
|
|
24
|
+
which: async (cmd: string) => {
|
|
25
|
+
return availableBinaries.has(cmd) ? `/usr/bin/${cmd}` : null;
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock child_process execFile — promisify wraps this callback-based API
|
|
30
|
+
mock.module("node:child_process", () => ({
|
|
31
|
+
execFile: (
|
|
32
|
+
cmd: string,
|
|
33
|
+
args: string[],
|
|
34
|
+
opts: unknown,
|
|
35
|
+
cb?: (
|
|
36
|
+
err: Error | null,
|
|
37
|
+
result: { stdout: string; stderr: string },
|
|
38
|
+
) => void,
|
|
39
|
+
) => {
|
|
40
|
+
const key = `${cmd} ${args.join(" ")}`;
|
|
41
|
+
|
|
42
|
+
// Find matching result by prefix
|
|
43
|
+
let result = execResults.get(key);
|
|
44
|
+
if (!result) {
|
|
45
|
+
for (const [k, v] of execResults.entries()) {
|
|
46
|
+
if (key.startsWith(k)) {
|
|
47
|
+
result = v;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!result) {
|
|
54
|
+
result = { stdout: "", stderr: "", error: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof cb === "function") {
|
|
58
|
+
if (result.error) {
|
|
59
|
+
const err = new Error(result.stderr) as Error & {
|
|
60
|
+
stdout: string;
|
|
61
|
+
stderr: string;
|
|
62
|
+
};
|
|
63
|
+
err.stdout = result.stdout;
|
|
64
|
+
err.stderr = result.stderr;
|
|
65
|
+
cb(err, { stdout: result.stdout, stderr: result.stderr });
|
|
66
|
+
} else {
|
|
67
|
+
cb(null, { stdout: result.stdout, stderr: result.stderr });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// Import AFTER mocks are set up
|
|
74
|
+
const {
|
|
75
|
+
extractGoBinaryName,
|
|
76
|
+
installPluginDeps,
|
|
77
|
+
checkMissingDeps,
|
|
78
|
+
} = await import("../services/plugin-setup.js");
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// 1. Pure function tests — extractGoBinaryName
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
describe("extractGoBinaryName", () => {
|
|
85
|
+
it("extracts binary name from module@version", () => {
|
|
86
|
+
expect(extractGoBinaryName("github.com/MadAppGang/tmux-mcp@latest")).toBe(
|
|
87
|
+
"tmux-mcp",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("extracts binary name from module@semver", () => {
|
|
92
|
+
expect(extractGoBinaryName("github.com/user/tool@v1.2.3")).toBe("tool");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("extracts binary name from module without version", () => {
|
|
96
|
+
expect(extractGoBinaryName("github.com/user/tool")).toBe("tool");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles single-segment package name", () => {
|
|
100
|
+
expect(extractGoBinaryName("mytool")).toBe("mytool");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("handles module with nested path and version", () => {
|
|
104
|
+
expect(
|
|
105
|
+
extractGoBinaryName("github.com/org/repo/cmd/mycli@v2.0.0"),
|
|
106
|
+
).toBe("mycli");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("handles empty string gracefully", () => {
|
|
110
|
+
expect(extractGoBinaryName("")).toBe("");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// 2. Integration tests — installPluginDeps with go packages
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe("installPluginDeps — go packages", () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
availableBinaries = new Set(["go"]); // go is available by default
|
|
121
|
+
execResults = new Map();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("skips go packages when binary already exists", async () => {
|
|
125
|
+
availableBinaries.add("tmux-mcp");
|
|
126
|
+
|
|
127
|
+
const result = await installPluginDeps({
|
|
128
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.skipped).toContain(
|
|
132
|
+
"go:github.com/MadAppGang/tmux-mcp@latest",
|
|
133
|
+
);
|
|
134
|
+
expect(result.installed).toHaveLength(0);
|
|
135
|
+
expect(result.failed).toHaveLength(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("installs go packages when binary not found", async () => {
|
|
139
|
+
// go is available but tmux-mcp is not
|
|
140
|
+
execResults.set("/usr/bin/go install", {
|
|
141
|
+
stdout: "",
|
|
142
|
+
stderr: "",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await installPluginDeps({
|
|
146
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.installed).toContain(
|
|
150
|
+
"go:github.com/MadAppGang/tmux-mcp@latest",
|
|
151
|
+
);
|
|
152
|
+
expect(result.failed).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("reports failure when go is not available", async () => {
|
|
156
|
+
availableBinaries.delete("go");
|
|
157
|
+
|
|
158
|
+
const result = await installPluginDeps({
|
|
159
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.failed).toHaveLength(1);
|
|
163
|
+
expect(result.failed[0].pkg).toBe(
|
|
164
|
+
"go:github.com/MadAppGang/tmux-mcp@latest",
|
|
165
|
+
);
|
|
166
|
+
expect(result.failed[0].error).toBe("go not found");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("reports failure when go install fails", async () => {
|
|
170
|
+
execResults.set("/usr/bin/go install", {
|
|
171
|
+
stdout: "",
|
|
172
|
+
stderr: "build error: something went wrong",
|
|
173
|
+
error: true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await installPluginDeps({
|
|
177
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.failed).toHaveLength(1);
|
|
181
|
+
expect(result.failed[0].error).toContain("something went wrong");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 3. Integration tests — installPluginDeps with required binaries
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
describe("installPluginDeps — required binaries", () => {
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
availableBinaries = new Set();
|
|
192
|
+
execResults = new Map();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("skips required binaries that are present", async () => {
|
|
196
|
+
availableBinaries.add("tmux");
|
|
197
|
+
|
|
198
|
+
const result = await installPluginDeps({
|
|
199
|
+
required: ["tmux"],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.skipped).toContain("required:tmux");
|
|
203
|
+
expect(result.failed).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("reports failure for missing required binaries", async () => {
|
|
207
|
+
const result = await installPluginDeps({
|
|
208
|
+
required: ["tmux"],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(result.failed).toHaveLength(1);
|
|
212
|
+
expect(result.failed[0].pkg).toBe("required:tmux");
|
|
213
|
+
expect(result.failed[0].error).toContain(
|
|
214
|
+
"Required binary 'tmux' not found in PATH",
|
|
215
|
+
);
|
|
216
|
+
expect(result.failed[0].error).toContain("install it manually");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("handles multiple required binaries with mixed availability", async () => {
|
|
220
|
+
availableBinaries.add("tmux");
|
|
221
|
+
// ffmpeg is not available
|
|
222
|
+
|
|
223
|
+
const result = await installPluginDeps({
|
|
224
|
+
required: ["tmux", "ffmpeg"],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result.skipped).toContain("required:tmux");
|
|
228
|
+
expect(result.failed).toHaveLength(1);
|
|
229
|
+
expect(result.failed[0].pkg).toBe("required:ffmpeg");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// 4. checkMissingDeps tests — go packages
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
describe("checkMissingDeps — go packages", () => {
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
availableBinaries = new Set();
|
|
240
|
+
execResults = new Map();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("detects missing go packages when binary not in PATH", async () => {
|
|
244
|
+
const missing = await checkMissingDeps({
|
|
245
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(missing.go).toEqual(["github.com/MadAppGang/tmux-mcp@latest"]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("does not report go packages when binary exists", async () => {
|
|
252
|
+
availableBinaries.add("tmux-mcp");
|
|
253
|
+
|
|
254
|
+
const missing = await checkMissingDeps({
|
|
255
|
+
go: ["github.com/MadAppGang/tmux-mcp@latest"],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(missing.go).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// 5. checkMissingDeps tests — required binaries
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
describe("checkMissingDeps — required binaries", () => {
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
availableBinaries = new Set();
|
|
269
|
+
execResults = new Map();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("detects missing required binaries", async () => {
|
|
273
|
+
const missing = await checkMissingDeps({
|
|
274
|
+
required: ["tmux", "ffmpeg"],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(missing.required).toEqual(["tmux", "ffmpeg"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("does not report present required binaries", async () => {
|
|
281
|
+
availableBinaries.add("tmux");
|
|
282
|
+
availableBinaries.add("ffmpeg");
|
|
283
|
+
|
|
284
|
+
const missing = await checkMissingDeps({
|
|
285
|
+
required: ["tmux", "ffmpeg"],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(missing.required).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("reports only missing required binaries", async () => {
|
|
292
|
+
availableBinaries.add("tmux");
|
|
293
|
+
|
|
294
|
+
const missing = await checkMissingDeps({
|
|
295
|
+
required: ["tmux", "ffmpeg"],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(missing.required).toEqual(["ffmpeg"]);
|
|
299
|
+
});
|
|
300
|
+
});
|