@exagone313/dsh-podman 0.2.0-rc.3 → 0.2.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/README.md +14 -2
- package/README.zh.md +14 -2
- package/cordis.patch.yml +58 -0
- package/dist/approval-reasons.d.ts +3 -4
- package/dist/approval-reasons.js +40 -49
- package/dist/approval-reasons.test.js +43 -15
- package/dist/approval.js +27 -10
- package/dist/approval.test.js +165 -34
- package/dist/card-route.d.ts +3 -2
- package/dist/card-route.js +154 -42
- package/dist/card-route.test.js +85 -43
- package/dist/card-test-support.js +39 -66
- package/dist/client/ContainerCard.d.ts +1 -1
- package/dist/client/ContainerCard.js +92 -50
- package/dist/client/card-protocol.d.ts +6 -3
- package/dist/client/container-card-caches.js +11 -6
- package/dist/client/container-card-controller.d.ts +16 -18
- package/dist/client/container-card-controller.js +92 -40
- package/dist/client/container-card-create-modal.d.ts +2 -2
- package/dist/client/container-card-create-modal.js +13 -5
- package/dist/client/container-card-default-env.d.ts +10 -0
- package/dist/client/container-card-default-env.js +69 -0
- package/dist/client/container-card-directory.d.ts +2 -2
- package/dist/client/container-card-directory.js +12 -4
- package/dist/client/container-card-editors.d.ts +3 -3
- package/dist/client/container-card-editors.js +104 -32
- package/dist/client/container-card-images.d.ts +4 -4
- package/dist/client/container-card-images.js +19 -6
- package/dist/client/container-card-paths.d.ts +3 -3
- package/dist/client/container-card-paths.js +18 -11
- package/dist/client/container-card-row.d.ts +10 -6
- package/dist/client/container-card-row.js +82 -13
- package/dist/client/container-card-secrets.d.ts +3 -3
- package/dist/client/container-card-secrets.js +8 -6
- package/dist/client/container-card-shared.d.ts +4 -17
- package/dist/client/container-card-shared.js +9 -25
- package/dist/client/container-card-styles.d.ts +2 -8
- package/dist/client/container-card-styles.js +14 -52
- package/dist/client/container-card-volumes.d.ts +2 -2
- package/dist/client/container-card-volumes.js +9 -5
- package/dist/client/container-card-workspace.d.ts +8 -6
- package/dist/client/container-card-workspace.js +9 -3
- package/dist/client/directory-picker.js +2 -1
- package/dist/client/index.js +13141 -2036
- package/dist/client/locales.d.ts +6 -1
- package/dist/client/locales.js +106 -38
- package/dist/client/podman-terminal-guide.d.ts +11 -0
- package/dist/client/podman-terminal-guide.js +208 -0
- package/dist/client/podman-terminal-title.d.ts +7 -0
- package/dist/client/podman-terminal-title.js +30 -0
- package/dist/client/podman-terminal.d.ts +26 -0
- package/dist/client/podman-terminal.js +412 -0
- package/dist/client/read-only-approval.d.ts +2 -2
- package/dist/client/read-only-approval.js +13 -3
- package/dist/client/slot-contract.d.ts +0 -11
- package/dist/client/terminal-preference.d.ts +26 -0
- package/dist/client/terminal-preference.js +67 -0
- package/dist/client/terminal-protocol.d.ts +85 -0
- package/dist/client/terminal-protocol.js +14 -0
- package/dist/client/terminal-tab.d.ts +60 -0
- package/dist/client/terminal-tab.js +35 -0
- package/dist/client/terminal-targets.d.ts +33 -0
- package/dist/client/terminal-targets.js +65 -0
- package/dist/client/terminal-titles.d.ts +23 -0
- package/dist/client/terminal-titles.js +52 -0
- package/dist/client/terminal-transport.d.ts +63 -0
- package/dist/client/terminal-transport.js +207 -0
- package/dist/client/tool-views.js +215 -49
- package/dist/container-env.d.ts +11 -0
- package/dist/container-env.js +96 -0
- package/dist/container-env.test.d.ts +1 -0
- package/dist/container-env.test.js +93 -0
- package/dist/containers.test.js +196 -18
- package/dist/daemons.test.js +18 -4
- package/dist/dsh-version.d.ts +1 -0
- package/dist/dsh-version.js +46 -0
- package/dist/env-rows.d.ts +14 -0
- package/dist/env-rows.js +52 -0
- package/dist/env-rows.test.d.ts +1 -0
- package/dist/env-rows.test.js +43 -0
- package/dist/fs-provider.d.ts +1 -0
- package/dist/fs-provider.js +26 -12
- package/dist/fs-provider.test.js +51 -10
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +2 -2
- package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
- package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
- package/dist/grpc/runtime-client.d.ts +3 -1
- package/dist/grpc/runtime-client.js +90 -17
- package/dist/guest-rpc.d.ts +15 -19
- package/dist/guest-rpc.js +289 -70
- package/dist/guest-rpc.test.d.ts +1 -0
- package/dist/guest-rpc.test.js +241 -0
- package/dist/guest-terminal.d.ts +30 -0
- package/dist/guest-terminal.js +196 -0
- package/dist/images.test.js +10 -3
- package/dist/index.d.ts +11 -16
- package/dist/index.js +42 -36
- package/dist/locales.test.d.ts +1 -0
- package/dist/locales.test.js +34 -0
- package/dist/misc.test.js +38 -10
- package/dist/mount-enums.js +2 -1
- package/dist/mount-input.d.ts +2 -0
- package/dist/mount-input.js +38 -22
- package/dist/mounts.test.js +81 -11
- package/dist/naming.test.d.ts +1 -0
- package/dist/naming.test.js +28 -0
- package/dist/output-reader.js +36 -4
- package/dist/package-deps.test.d.ts +1 -0
- package/dist/package-deps.test.js +49 -0
- package/dist/paths.test.js +4 -2
- package/dist/plugin-meta.test.d.ts +1 -0
- package/dist/plugin-meta.test.js +29 -0
- package/dist/project-path.js +33 -7
- package/dist/project-path.test.js +14 -0
- package/dist/prompts.d.ts +0 -4
- package/dist/prompts.js +15 -84
- package/dist/read-only-shell.js +3 -1
- package/dist/read-only-shell.test.js +80 -25
- package/dist/runtime-client.test.d.ts +1 -0
- package/dist/runtime-client.test.js +53 -0
- package/dist/secrets.test.js +21 -6
- package/dist/settings-commands.test.js +61 -14
- package/dist/settings-create.test.js +32 -10
- package/dist/settings-default-env.test.d.ts +1 -0
- package/dist/settings-default-env.test.js +175 -0
- package/dist/settings-mounts.test.js +46 -7
- package/dist/settings-schema.d.ts +6 -11
- package/dist/settings-schema.js +4 -8
- package/dist/settings-schema.test.d.ts +1 -0
- package/dist/settings-schema.test.js +35 -0
- package/dist/settings-workspaces.test.js +38 -63
- package/dist/spill-store.d.ts +1 -0
- package/dist/spill-store.js +17 -4
- package/dist/spill-store.test.js +31 -8
- package/dist/subprocess.d.ts +4 -0
- package/dist/subprocess.js +148 -162
- package/dist/subprocess.test.js +118 -8
- package/dist/terminal-preference.test.d.ts +1 -0
- package/dist/terminal-preference.test.js +62 -0
- package/dist/terminal-route.d.ts +10 -0
- package/dist/terminal-route.js +270 -0
- package/dist/terminal-route.test.d.ts +1 -0
- package/dist/terminal-route.test.js +265 -0
- package/dist/terminal-sessions.d.ts +69 -0
- package/dist/terminal-sessions.js +252 -0
- package/dist/terminal-shells.d.ts +22 -0
- package/dist/terminal-shells.js +99 -0
- package/dist/terminal-tab.test.d.ts +1 -0
- package/dist/terminal-tab.test.js +58 -0
- package/dist/terminal-targets.test.d.ts +1 -0
- package/dist/terminal-targets.test.js +43 -0
- package/dist/terminal-titles.test.d.ts +1 -0
- package/dist/terminal-titles.test.js +36 -0
- package/dist/test-support.d.ts +35 -5
- package/dist/test-support.js +91 -29
- package/dist/tool-defs.js +19 -4
- package/dist/tool-handlers.js +80 -28
- package/dist/tool-params.js +19 -5
- package/dist/version-compat.d.ts +4 -0
- package/dist/version-compat.js +29 -0
- package/dist/version-compat.test.d.ts +1 -0
- package/dist/version-compat.test.js +27 -0
- package/dist/volumes.test.js +1 -1
- package/dist/workspace-binding.d.ts +19 -10
- package/dist/workspace-binding.js +252 -78
- package/dist/workspace-binding.test.js +202 -27
- package/docs/architecture.md +71 -20
- package/docs/architecture.zh.md +62 -12
- package/docs/configuration.md +61 -20
- package/docs/configuration.zh.md +53 -19
- package/docs/development-prompt.md +72 -0
- package/docs/development-prompt.zh.md +69 -0
- package/docs/development.md +184 -29
- package/docs/development.zh.md +165 -26
- package/docs/install-dsh-and-dsh-podman.md +24 -21
- package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
- package/docs/uninstall.md +83 -0
- package/docs/uninstall.zh.md +79 -0
- package/docs/update.md +120 -0
- package/docs/update.zh.md +114 -0
- package/docs/usage.md +108 -61
- package/docs/usage.zh.md +76 -38
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -25
- package/quadlet/dsh-podman-orchestrator.container +46 -0
- package/quadlet/dsh.container +35 -0
- package/LICENSE.pkg +0 -17103
- package/dist/preferences.d.ts +0 -6
- package/dist/preferences.js +0 -27
package/dist/test-support.js
CHANGED
|
@@ -8,11 +8,27 @@ import { createFilesystemProvider } from "./index.js";
|
|
|
8
8
|
export const WORKSPACE_ID = "2c573001-4171-4900-904b-12a5cc02737a";
|
|
9
9
|
export class FakeTerminalCall extends EventEmitter {
|
|
10
10
|
stdinChunks = [];
|
|
11
|
+
resizes = [];
|
|
12
|
+
start;
|
|
13
|
+
closed = false;
|
|
14
|
+
ended = false;
|
|
11
15
|
write(message) {
|
|
16
|
+
if (message.close) {
|
|
17
|
+
this.closed = true;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
12
20
|
if (message.start) {
|
|
21
|
+
this.start = message.start;
|
|
13
22
|
queueMicrotask(() => this.emit("data", { started: { pid: 42 } }));
|
|
14
23
|
return;
|
|
15
24
|
}
|
|
25
|
+
if (message.resize) {
|
|
26
|
+
this.resizes.push({
|
|
27
|
+
rows: message.resize.rows,
|
|
28
|
+
cols: message.resize.cols,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
16
32
|
if (message.stdinChunk) {
|
|
17
33
|
this.stdinChunks.push(Buffer.from(message.stdinChunk));
|
|
18
34
|
return;
|
|
@@ -20,7 +36,12 @@ export class FakeTerminalCall extends EventEmitter {
|
|
|
20
36
|
if (message.inspectRequestId !== undefined) {
|
|
21
37
|
const requestId = message.inspectRequestId;
|
|
22
38
|
queueMicrotask(() => this.emit("data", {
|
|
23
|
-
foreground: {
|
|
39
|
+
foreground: {
|
|
40
|
+
requestId,
|
|
41
|
+
found: true,
|
|
42
|
+
processGroupId: 7,
|
|
43
|
+
inputWaiting: true,
|
|
44
|
+
},
|
|
24
45
|
}));
|
|
25
46
|
return;
|
|
26
47
|
}
|
|
@@ -31,7 +52,9 @@ export class FakeTerminalCall extends EventEmitter {
|
|
|
31
52
|
}));
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
|
-
end() {
|
|
55
|
+
end() {
|
|
56
|
+
this.ended = true;
|
|
57
|
+
}
|
|
35
58
|
cancel() { }
|
|
36
59
|
emitStdout(data) {
|
|
37
60
|
this.emit("data", { stdoutChunk: data });
|
|
@@ -98,7 +121,9 @@ export const sessionExec = (name, facts = { mode: "read-only" }) => ({
|
|
|
98
121
|
export const presetExec = (name, preset, args, facts = {}) => ({
|
|
99
122
|
name,
|
|
100
123
|
arguments: args,
|
|
101
|
-
agent: {
|
|
124
|
+
agent: {
|
|
125
|
+
session: { header: { agentPreset: preset }, facts: { ...facts, preset } },
|
|
126
|
+
},
|
|
102
127
|
});
|
|
103
128
|
export const MOUNT_TOOLS = [
|
|
104
129
|
"container_mount_list",
|
|
@@ -146,7 +171,11 @@ export const SECRET_BEARING_CONTAINER = {
|
|
|
146
171
|
agentSocketPath: `/run/dsh-podman/${WORKSPACE_ID}-default/guest.sock`,
|
|
147
172
|
agentToken: "super-secret-token",
|
|
148
173
|
mounts: [
|
|
149
|
-
{
|
|
174
|
+
{
|
|
175
|
+
projectName: "team",
|
|
176
|
+
mode: "MOUNT_MODE_READ_WRITE",
|
|
177
|
+
kind: "MOUNT_KIND_PROJECT",
|
|
178
|
+
},
|
|
150
179
|
],
|
|
151
180
|
env: { PATH: "/bin", DB_PASSWORD: "hunter2" },
|
|
152
181
|
secretEnv: { DB_PASS: "db-pass" },
|
|
@@ -200,8 +229,9 @@ export function guestFileRecorder(content = "hello", exists = true) {
|
|
|
200
229
|
write(message) {
|
|
201
230
|
if (message.start !== undefined)
|
|
202
231
|
path = message.start.path;
|
|
203
|
-
if (message.dataChunk !== undefined)
|
|
232
|
+
if (message.dataChunk !== undefined) {
|
|
204
233
|
body += String(message.dataChunk);
|
|
234
|
+
}
|
|
205
235
|
},
|
|
206
236
|
end() {
|
|
207
237
|
writes.push({ path, content: body });
|
|
@@ -264,8 +294,9 @@ export function guestExecRecorder(defaultCwd, stdout) {
|
|
|
264
294
|
},
|
|
265
295
|
end() {
|
|
266
296
|
for (const handler of handlers.data ?? []) {
|
|
267
|
-
if (stdout !== undefined)
|
|
297
|
+
if (stdout !== undefined) {
|
|
268
298
|
handler({ stdoutChunk: Buffer.from(stdout) });
|
|
299
|
+
}
|
|
269
300
|
handler({ exit: { exitCode: 0, signaled: false } });
|
|
270
301
|
}
|
|
271
302
|
},
|
|
@@ -381,36 +412,61 @@ export function editProvider(binding) {
|
|
|
381
412
|
resolveForPath: async () => binding,
|
|
382
413
|
});
|
|
383
414
|
}
|
|
415
|
+
// The guest Exec bidi stream the subprocess provider drives, with the
|
|
416
|
+
// backpressure and failure controls its tests need. `write()` records the start
|
|
417
|
+
// message; `end()` replays the process id, the configured stdout and (unless
|
|
418
|
+
// disabled) the exit message, mirroring a guest that answered immediately.
|
|
419
|
+
export class FakeExecStream extends EventEmitter {
|
|
420
|
+
starts;
|
|
421
|
+
options;
|
|
422
|
+
paused = 0;
|
|
423
|
+
resumed = 0;
|
|
424
|
+
cancelled = false;
|
|
425
|
+
constructor(starts, options) {
|
|
426
|
+
super();
|
|
427
|
+
this.starts = starts;
|
|
428
|
+
this.options = options;
|
|
429
|
+
}
|
|
430
|
+
write(message) {
|
|
431
|
+
this.starts.push(message);
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
end() {
|
|
435
|
+
this.emitData({ processId: "7" });
|
|
436
|
+
if (this.options.stdout !== undefined) {
|
|
437
|
+
this.emitData({ stdoutChunk: Buffer.from(this.options.stdout) });
|
|
438
|
+
}
|
|
439
|
+
if (this.options.emitExit !== false) {
|
|
440
|
+
this.emitData({
|
|
441
|
+
exit: { exitCode: 0, signaled: false, ...this.options.exit },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
pause() {
|
|
446
|
+
this.paused++;
|
|
447
|
+
}
|
|
448
|
+
resume() {
|
|
449
|
+
this.resumed++;
|
|
450
|
+
}
|
|
451
|
+
cancel() {
|
|
452
|
+
this.cancelled = true;
|
|
453
|
+
}
|
|
454
|
+
emitData(message) {
|
|
455
|
+
this.emit("data", message);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
384
458
|
// A fake guest for the subprocess provider: exec emits a process id (and
|
|
385
459
|
// optionally an exit), and the unary signal/delete RPCs are recorded.
|
|
386
460
|
export function spawnGuest(options = {}) {
|
|
387
461
|
const signals = [];
|
|
388
462
|
const starts = [];
|
|
389
463
|
const deletes = [];
|
|
464
|
+
const streams = [];
|
|
390
465
|
const guest = {
|
|
391
466
|
exec: () => {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
(handlers[event] ??= []).push(handler);
|
|
396
|
-
},
|
|
397
|
-
write(message) {
|
|
398
|
-
starts.push(message);
|
|
399
|
-
},
|
|
400
|
-
end() {
|
|
401
|
-
for (const handler of handlers.data ?? []) {
|
|
402
|
-
handler({ processId: "7" });
|
|
403
|
-
if (options.stdout !== undefined) {
|
|
404
|
-
handler({ stdoutChunk: Buffer.from(options.stdout) });
|
|
405
|
-
}
|
|
406
|
-
if (options.emitExit !== false) {
|
|
407
|
-
handler({
|
|
408
|
-
exit: { exitCode: 0, signaled: false, ...options.exit },
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
},
|
|
413
|
-
};
|
|
467
|
+
const stream = new FakeExecStream(starts, options);
|
|
468
|
+
streams.push(stream);
|
|
469
|
+
return stream;
|
|
414
470
|
},
|
|
415
471
|
signal: (request, _metadata, callback) => {
|
|
416
472
|
signals.push(request);
|
|
@@ -425,13 +481,19 @@ export function spawnGuest(options = {}) {
|
|
|
425
481
|
signals,
|
|
426
482
|
starts,
|
|
427
483
|
deletes,
|
|
484
|
+
streams,
|
|
485
|
+
guest,
|
|
428
486
|
resolver: { resolve: async () => ({ guest, token: "t" }) },
|
|
429
487
|
};
|
|
430
488
|
}
|
|
431
489
|
export const spawnSpec = (overrides = {}) => ({
|
|
432
490
|
argv: ["sleep", "1"],
|
|
433
491
|
cwd: "/projects/team",
|
|
434
|
-
stdio: {
|
|
492
|
+
stdio: {
|
|
493
|
+
stdin: "ignore",
|
|
494
|
+
stdout: { maxBytes: 10 },
|
|
495
|
+
stderr: { maxBytes: 10 },
|
|
496
|
+
},
|
|
435
497
|
graceMs: 50,
|
|
436
498
|
...overrides,
|
|
437
499
|
});
|
package/dist/tool-defs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
|
-
import { containerBashParameters, containerEditParameters, containerExecParameters, containerGlobParameters, containerGrepParameters, containerListParameters, containerMountAddParameters, containerMountListParameters, containerMountRemoveParameters, containerMountUpdateParameters, containerPathAddParameters, containerPathRemoveParameters, containerPathSetParameters, containerReadParameters, containerRecreateParameters, containerRemoveParameters, containerSecretAddParameters, containerSecretRemoveParameters, containerStartParameters, containerWriteParameters, daemonListParameters, daemonLogsParameters, daemonRestartParameters, daemonStartParameters, daemonStopParameters, imageBuildParameters, imageGetParameters, imageListParameters, imageRebuildAllParameters, imageRebuildParameters, imageRemoveParameters, secretCreateParameters, secretListParameters, secretRemoveParameters, volumeCreateParameters, volumeListParameters, volumeRemoveParameters } from "./tool-params.js";
|
|
4
|
+
import { containerBashParameters, containerEditParameters, containerExecParameters, containerGlobParameters, containerGrepParameters, containerListParameters, containerMountAddParameters, containerMountListParameters, containerMountRemoveParameters, containerMountUpdateParameters, containerPathAddParameters, containerPathRemoveParameters, containerPathSetParameters, containerReadParameters, containerRecreateParameters, containerRemoveParameters, containerSecretAddParameters, containerSecretRemoveParameters, containerStartParameters, containerWriteParameters, daemonListParameters, daemonLogsParameters, daemonRestartParameters, daemonStartParameters, daemonStopParameters, imageBuildParameters, imageGetParameters, imageListParameters, imageRebuildAllParameters, imageRebuildParameters, imageRemoveParameters, secretCreateParameters, secretListParameters, secretRemoveParameters, volumeCreateParameters, volumeListParameters, volumeRemoveParameters, } from "./tool-params.js";
|
|
5
5
|
export function defineTool(definition) {
|
|
6
6
|
return definition;
|
|
7
7
|
}
|
|
@@ -9,6 +9,16 @@ export const toolOutput = {
|
|
|
9
9
|
schema: { type: "string" },
|
|
10
10
|
render: (_args, value) => [{ type: "text", text: value }],
|
|
11
11
|
};
|
|
12
|
+
// hasSecretEnv reports whether a call attaches secrets to a container. Unlike
|
|
13
|
+
// plain `env`, a secret env var injects a value the caller never sees, so
|
|
14
|
+
// container_start must be approved for it exactly like container_secret_add.
|
|
15
|
+
function hasSecretEnv(args) {
|
|
16
|
+
const secretEnv = args.secretEnv;
|
|
17
|
+
return (typeof secretEnv === "object" &&
|
|
18
|
+
secretEnv !== null &&
|
|
19
|
+
!Array.isArray(secretEnv) &&
|
|
20
|
+
Object.keys(secretEnv).length > 0);
|
|
21
|
+
}
|
|
12
22
|
export const TOOLS = [
|
|
13
23
|
{ name: "image_list", parameters: imageListParameters },
|
|
14
24
|
{ name: "image_get", parameters: imageGetParameters },
|
|
@@ -28,14 +38,19 @@ export const TOOLS = [
|
|
|
28
38
|
{
|
|
29
39
|
name: "container_start",
|
|
30
40
|
parameters: containerStartParameters,
|
|
31
|
-
approvalWhen: (args) => Array.isArray(args.mounts) && args.mounts.length > 0
|
|
41
|
+
approvalWhen: (args) => (Array.isArray(args.mounts) && args.mounts.length > 0) ||
|
|
42
|
+
hasSecretEnv(args),
|
|
32
43
|
},
|
|
33
44
|
{
|
|
34
45
|
name: "container_recreate",
|
|
35
46
|
parameters: containerRecreateParameters,
|
|
36
47
|
approval: true,
|
|
37
48
|
},
|
|
38
|
-
{
|
|
49
|
+
{
|
|
50
|
+
name: "container_remove",
|
|
51
|
+
parameters: containerRemoveParameters,
|
|
52
|
+
approval: true,
|
|
53
|
+
},
|
|
39
54
|
{ name: "container_bash", parameters: containerBashParameters },
|
|
40
55
|
{ name: "container_exec", parameters: containerExecParameters },
|
|
41
56
|
{ name: "container_read", parameters: containerReadParameters },
|
|
@@ -122,7 +137,7 @@ export const TOOL_DESCRIPTIONS = {
|
|
|
122
137
|
container_read: "Read a file inside a container of the current workspace.",
|
|
123
138
|
container_write: "Write a file inside a container of the current workspace.",
|
|
124
139
|
container_edit: "Edit a file inside a container of the current workspace.",
|
|
125
|
-
container_glob:
|
|
140
|
+
container_glob: 'List files inside a container of the current workspace matching a glob pattern. A pattern with no "/" matches basenames at any depth. Results are files only, include hidden and ignored files, and exclude VCS metadata directories. Defaults to the session working directory when it is mounted; pass path to override.',
|
|
126
141
|
container_grep: "Search file contents inside a container of the current workspace. Defaults to the session working directory when it is mounted; pass cwd to override.",
|
|
127
142
|
container_mount_list: "List the mounts of a container in the current workspace, with the exact kind, project, volume, secret, and destination values to pass to container_mount_remove.",
|
|
128
143
|
container_mount_add: "Add a mount to a container in the current workspace: a project bind (project, a path under the projects root), a tmpfs, a named volume, or a secret, selected by kind (default project). Recreates the container, terminating its running processes; bind-mounted volume data persists. Requires approval: adding a mount changes the container filesystem view.",
|
package/dist/tool-handlers.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
|
-
import { bytesText, currentCwd, fsError, guestCwd, outputLines, resolveToolBinding, runExec, sessionWorkspaceSlug,
|
|
5
|
-
import { inferMountKind, mountsFromInput, projectMountDestinationReason } from "./mount-input.js";
|
|
6
|
-
import {
|
|
4
|
+
import { bytesText, currentCwd, fsError, guestCwd, outputLines, resolveToolBinding, runExec, sessionWorkspaceSlug, streamLines, unaryGuest, } from "./guest-rpc.js";
|
|
5
|
+
import { inferMountKind, mountsFromInput, projectMountDestinationReason, } from "./mount-input.js";
|
|
6
|
+
import { mergeDefaultEnv } from "./container-env.js";
|
|
7
|
+
import { publicContainer, publicDaemon, publicImage, publicMount, } from "./public.js";
|
|
7
8
|
import { grpc } from "./grpc/runtime-client.js";
|
|
8
|
-
import { defaultMountMode, mountKindToProto, mountModeToProto } from "./mount-enums.js";
|
|
9
|
+
import { defaultMountMode, mountKindToProto, mountModeToProto, } from "./mount-enums.js";
|
|
9
10
|
import { containerNotFound, } from "./workspace-binding.js";
|
|
10
11
|
// Cap on paths `container_glob` returns, matching the built-in glob tool's
|
|
11
12
|
// default result limit; `--sort=modified` makes the retained head the newest.
|
|
@@ -22,6 +23,18 @@ function failOnSearchError(tool, result) {
|
|
|
22
23
|
const detail = result.stderr.trim();
|
|
23
24
|
throw new Error(detail === "" ? `${tool} search failed (ripgrep exit code 2)` : detail);
|
|
24
25
|
}
|
|
26
|
+
// withSpillNote points the caller at the guest spill file when a command's
|
|
27
|
+
// output was truncated in memory, so the full stream stays reachable with
|
|
28
|
+
// container_read instead of silently vanishing.
|
|
29
|
+
function withSpillNote(result) {
|
|
30
|
+
const paths = [result.stdoutSpillPath, result.stderrSpillPath].filter((path) => typeof path === "string");
|
|
31
|
+
if (paths.length === 0)
|
|
32
|
+
return result;
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
note: `output was truncated; the full stream is at ${paths.join(", ")} (read it with container_read)`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
25
38
|
// The terminal environment the harness's shell executors impose on every
|
|
26
39
|
// command (`ENV_OVERRIDES` in dsh-bash-local), so a container command sees the
|
|
27
40
|
// same non-interactive terminal as the built-in `bash`.
|
|
@@ -39,7 +52,32 @@ const ENV_OVERRIDES = {
|
|
|
39
52
|
// collects it through `ctx.shellEnv`.
|
|
40
53
|
function managedEnv(ctx, exec, env) {
|
|
41
54
|
const managed = ctx?.get?.("shellEnv")?.collect?.(exec) ?? {};
|
|
42
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
...ENV_OVERRIDES,
|
|
57
|
+
...env,
|
|
58
|
+
...managed,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// startContainerEnv resolves the environment a `container_start` request
|
|
62
|
+
// carries. The caller's map wins over the defaults; a container that does not
|
|
63
|
+
// exist yet is a creation and gets the defaults alone; one that already exists
|
|
64
|
+
// keeps the environment it has (the orchestrator preserves a stored env when
|
|
65
|
+
// the request omits one, so nothing has to be sent).
|
|
66
|
+
async function startContainerEnv(resolver, slug, input) {
|
|
67
|
+
const defaults = resolver.getConfig().containerEnv;
|
|
68
|
+
if (input.env !== undefined) {
|
|
69
|
+
return mergeDefaultEnv(defaults, input.env);
|
|
70
|
+
}
|
|
71
|
+
if (defaults === undefined || Object.keys(defaults).length === 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const result = await resolver.control("listContainers", {});
|
|
75
|
+
const name = input.container === "" ? "default" : input.container;
|
|
76
|
+
const exists = (result.containers ?? []).some((row) => row.workspaceSlug === slug && row.containerName === name);
|
|
77
|
+
if (exists)
|
|
78
|
+
return undefined;
|
|
79
|
+
const env = mergeDefaultEnv(defaults, undefined);
|
|
80
|
+
return Object.keys(env).length === 0 ? undefined : env;
|
|
43
81
|
}
|
|
44
82
|
// containerTarget resolves a container-scoped file target through the plugin's
|
|
45
83
|
// own filesystem provider, so the container file tools share the harness's file
|
|
@@ -123,12 +161,14 @@ export const toolHandlers = {
|
|
|
123
161
|
},
|
|
124
162
|
container_start: async (resolver, input, exec) => {
|
|
125
163
|
const mounts = mountsFromInput(input.mounts, resolver.getConfig().projectsRoot);
|
|
164
|
+
const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
|
|
165
|
+
const env = await startContainerEnv(resolver, slug, input);
|
|
126
166
|
const row = await resolver.control("startContainer", {
|
|
127
|
-
workspaceSlug:
|
|
167
|
+
workspaceSlug: slug,
|
|
128
168
|
container: input.container,
|
|
129
169
|
imageId: input.image,
|
|
130
170
|
...(mounts === undefined ? {} : { mounts }),
|
|
131
|
-
...(
|
|
171
|
+
...(env === undefined ? {} : { env }),
|
|
132
172
|
...(input.secretEnv !== undefined ? { secretEnv: input.secretEnv } : {}),
|
|
133
173
|
...(input.paths !== undefined ? { paths: input.paths } : {}),
|
|
134
174
|
});
|
|
@@ -156,13 +196,13 @@ export const toolHandlers = {
|
|
|
156
196
|
},
|
|
157
197
|
container_bash: async (resolver, input, exec, ctx) => {
|
|
158
198
|
const sessionCwd = currentCwd(exec);
|
|
159
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
160
|
-
return runExec(binding, ["bash", "-c", input.command], guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input));
|
|
199
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
200
|
+
return withSpillNote(await runExec(binding, ["bash", "-c", input.command], guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input), { signal: exec?.signal }));
|
|
161
201
|
},
|
|
162
202
|
container_exec: async (resolver, input, exec, ctx) => {
|
|
163
203
|
const sessionCwd = currentCwd(exec);
|
|
164
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
165
|
-
return runExec(binding, input.argv, guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input));
|
|
204
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
205
|
+
return withSpillNote(await runExec(binding, input.argv, guestCwd(input.workdir, sessionCwd, binding), managedEnv(ctx, exec, input.env), input.timeoutMs, identityFromInput(input), { signal: exec?.signal }));
|
|
166
206
|
},
|
|
167
207
|
container_read: async (resolver, input, exec, ctx) => {
|
|
168
208
|
const target = await containerTarget(ctx, input.file_path, input.container, exec);
|
|
@@ -178,9 +218,11 @@ export const toolHandlers = {
|
|
|
178
218
|
if (info.type !== "file") {
|
|
179
219
|
throw fsError("FS_NOT_REGULAR_FILE", `cannot read "${target.displayPath}": not a regular file`);
|
|
180
220
|
}
|
|
181
|
-
|
|
221
|
+
// Stream the requested line window instead of buffering the whole file:
|
|
222
|
+
// a read of a few lines from a huge file must not read it all into memory.
|
|
223
|
+
const lines = await streamLines(await ctx.fs.streamText(target, exec?.signal), input.offset, input.limit);
|
|
182
224
|
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
183
|
-
return
|
|
225
|
+
return lines;
|
|
184
226
|
},
|
|
185
227
|
container_write: async (resolver, input, exec, ctx) => {
|
|
186
228
|
const target = await containerTarget(ctx, input.file_path, input.container, exec);
|
|
@@ -190,7 +232,10 @@ export const toolHandlers = {
|
|
|
190
232
|
const intent = await ctx.waterfall("fs/write-intent", target, exec, () => undefined);
|
|
191
233
|
const content = String(input.content ?? "");
|
|
192
234
|
const outcome = await ctx.fs.writeText(target, content, intent, exec?.signal);
|
|
193
|
-
ctx.emit("fs/observed", target, {
|
|
235
|
+
ctx.emit("fs/observed", target, {
|
|
236
|
+
kind: "present",
|
|
237
|
+
version: outcome.version,
|
|
238
|
+
}, exec);
|
|
194
239
|
return { bytesWritten: Buffer.byteLength(content, "utf8") };
|
|
195
240
|
},
|
|
196
241
|
container_edit: async (resolver, input, exec, ctx) => {
|
|
@@ -201,12 +246,15 @@ export const toolHandlers = {
|
|
|
201
246
|
newString: input.new_string,
|
|
202
247
|
replaceAll: input.replace_all === true,
|
|
203
248
|
}, intent, exec?.signal);
|
|
204
|
-
ctx.emit("fs/observed", target, {
|
|
249
|
+
ctx.emit("fs/observed", target, {
|
|
250
|
+
kind: "present",
|
|
251
|
+
version: outcome.version,
|
|
252
|
+
}, exec);
|
|
205
253
|
return { before: outcome.before, after: outcome.after };
|
|
206
254
|
},
|
|
207
255
|
container_glob: async (resolver, input, exec) => {
|
|
208
256
|
const sessionCwd = currentCwd(exec);
|
|
209
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
257
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
210
258
|
const argv = [
|
|
211
259
|
"rg",
|
|
212
260
|
"--files",
|
|
@@ -219,7 +267,7 @@ export const toolHandlers = {
|
|
|
219
267
|
`--glob=!**/${name}/**`,
|
|
220
268
|
]),
|
|
221
269
|
];
|
|
222
|
-
const result = await runExec(binding, argv, guestCwd(input.path, sessionCwd, binding));
|
|
270
|
+
const result = await runExec(binding, argv, guestCwd(input.path, sessionCwd, binding), undefined, undefined, undefined, { signal: exec?.signal });
|
|
223
271
|
failOnSearchError("glob", result);
|
|
224
272
|
const files = outputLines(result.stdout);
|
|
225
273
|
const capped = files.length > GLOB_MAX_RESULTS;
|
|
@@ -234,7 +282,7 @@ export const toolHandlers = {
|
|
|
234
282
|
},
|
|
235
283
|
container_grep: async (resolver, input, exec) => {
|
|
236
284
|
const sessionCwd = currentCwd(exec);
|
|
237
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
285
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
238
286
|
const cwd = guestCwd(undefined, sessionCwd, binding);
|
|
239
287
|
// The search root: the requested path, else the container's default (the
|
|
240
288
|
// session workspace when it is mounted). Ripgrep reads stdin instead of the
|
|
@@ -247,7 +295,9 @@ export const toolHandlers = {
|
|
|
247
295
|
argv.push(input.pattern);
|
|
248
296
|
if (root !== undefined)
|
|
249
297
|
argv.push(root);
|
|
250
|
-
const result = await runExec(binding, argv, cwd
|
|
298
|
+
const result = await runExec(binding, argv, cwd, undefined, undefined, undefined, {
|
|
299
|
+
signal: exec?.signal,
|
|
300
|
+
});
|
|
251
301
|
failOnSearchError("grep", result);
|
|
252
302
|
return {
|
|
253
303
|
matches: outputLines(result.stdout),
|
|
@@ -356,7 +406,7 @@ export const toolHandlers = {
|
|
|
356
406
|
container_path_set: async (resolver, input, exec) => {
|
|
357
407
|
const sessionCwd = currentCwd(exec);
|
|
358
408
|
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
359
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
409
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
360
410
|
const paths = Array.isArray(input.paths) ? input.paths.map(String) : [];
|
|
361
411
|
return {
|
|
362
412
|
paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
|
|
@@ -365,7 +415,7 @@ export const toolHandlers = {
|
|
|
365
415
|
container_path_add: async (resolver, input, exec) => {
|
|
366
416
|
const sessionCwd = currentCwd(exec);
|
|
367
417
|
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
368
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
418
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
369
419
|
const current = await containerPathState(binding);
|
|
370
420
|
// Prepending gives the new path the highest priority; an existing entry is
|
|
371
421
|
// moved to the front rather than duplicated.
|
|
@@ -380,7 +430,7 @@ export const toolHandlers = {
|
|
|
380
430
|
container_path_remove: async (resolver, input, exec) => {
|
|
381
431
|
const sessionCwd = currentCwd(exec);
|
|
382
432
|
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
383
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
433
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
384
434
|
const current = await containerPathState(binding);
|
|
385
435
|
if (!current.paths.includes(input.path)) {
|
|
386
436
|
if (current.defaultPaths.includes(input.path)) {
|
|
@@ -440,7 +490,7 @@ export const toolHandlers = {
|
|
|
440
490
|
},
|
|
441
491
|
daemon_start: async (resolver, input, exec) => {
|
|
442
492
|
const sessionCwd = currentCwd(exec);
|
|
443
|
-
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
493
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container, exec?.signal);
|
|
444
494
|
const request = {
|
|
445
495
|
argv: input.argv,
|
|
446
496
|
inheritEnv: { value: input.inheritEnv !== false },
|
|
@@ -463,12 +513,12 @@ export const toolHandlers = {
|
|
|
463
513
|
return publicDaemon(info);
|
|
464
514
|
},
|
|
465
515
|
daemon_list: async (resolver, input, exec) => {
|
|
466
|
-
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
516
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container, exec?.signal);
|
|
467
517
|
const result = (await unaryGuest({ binding }, "listDaemons", {}));
|
|
468
518
|
return (result.daemons ?? []).map(publicDaemon);
|
|
469
519
|
},
|
|
470
520
|
daemon_stop: async (resolver, input, exec) => {
|
|
471
|
-
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
521
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container, exec?.signal);
|
|
472
522
|
await daemonCall(input.name, () => unaryGuest({ binding }, "stopDaemon", {
|
|
473
523
|
name: input.name,
|
|
474
524
|
signal: input.signal,
|
|
@@ -476,12 +526,12 @@ export const toolHandlers = {
|
|
|
476
526
|
return { stopped: input.name };
|
|
477
527
|
},
|
|
478
528
|
daemon_restart: async (resolver, input, exec) => {
|
|
479
|
-
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
529
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container, exec?.signal);
|
|
480
530
|
const info = await daemonCall(input.name, () => unaryGuest({ binding }, "restartDaemon", { name: input.name }));
|
|
481
531
|
return publicDaemon(info);
|
|
482
532
|
},
|
|
483
533
|
daemon_logs: async (resolver, input, exec) => {
|
|
484
|
-
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
534
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container, exec?.signal);
|
|
485
535
|
const result = (await daemonCall(input.name, () => unaryGuest({ binding }, "daemonLogs", {
|
|
486
536
|
name: input.name,
|
|
487
537
|
tailBytes: input.tailBytes,
|
|
@@ -504,7 +554,9 @@ async function containerPathState(binding) {
|
|
|
504
554
|
const paths = Array.isArray(result?.paths)
|
|
505
555
|
? result.paths.map((path) => String(path))
|
|
506
556
|
: [];
|
|
507
|
-
const defaultPath = typeof result?.defaultPath === "string"
|
|
557
|
+
const defaultPath = typeof result?.defaultPath === "string"
|
|
558
|
+
? result.defaultPath
|
|
559
|
+
: "";
|
|
508
560
|
return {
|
|
509
561
|
paths,
|
|
510
562
|
defaultPaths: defaultPath === "" ? [] : defaultPath.split(":"),
|
package/dist/tool-params.js
CHANGED
|
@@ -136,7 +136,11 @@ export const containerStartParameters = {
|
|
|
136
136
|
},
|
|
137
137
|
mounts: mountsParam,
|
|
138
138
|
env: envParam,
|
|
139
|
-
secretEnv: {
|
|
139
|
+
secretEnv: {
|
|
140
|
+
type: "object",
|
|
141
|
+
additionalProperties: { type: "string" },
|
|
142
|
+
description: "Secret environment variables (env var name to secret short name).",
|
|
143
|
+
},
|
|
140
144
|
paths: pathsParam,
|
|
141
145
|
},
|
|
142
146
|
required: ["container"],
|
|
@@ -151,7 +155,11 @@ export const containerRecreateParameters = {
|
|
|
151
155
|
},
|
|
152
156
|
mounts: mountsParam,
|
|
153
157
|
env: envParam,
|
|
154
|
-
secretEnv: {
|
|
158
|
+
secretEnv: {
|
|
159
|
+
type: "object",
|
|
160
|
+
additionalProperties: { type: "string" },
|
|
161
|
+
description: "Secret environment variables (env var name to secret short name).",
|
|
162
|
+
},
|
|
155
163
|
paths: pathsParam,
|
|
156
164
|
},
|
|
157
165
|
required: ["container"],
|
|
@@ -272,12 +280,16 @@ export const volumeListParameters = {
|
|
|
272
280
|
};
|
|
273
281
|
export const volumeCreateParameters = {
|
|
274
282
|
type: "object",
|
|
275
|
-
properties: {
|
|
283
|
+
properties: {
|
|
284
|
+
name: { type: "string", description: "Volume name to create." },
|
|
285
|
+
},
|
|
276
286
|
required: ["name"],
|
|
277
287
|
};
|
|
278
288
|
export const volumeRemoveParameters = {
|
|
279
289
|
type: "object",
|
|
280
|
-
properties: {
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: "string", description: "Volume name to remove." },
|
|
292
|
+
},
|
|
281
293
|
required: ["name"],
|
|
282
294
|
};
|
|
283
295
|
export const secretListParameters = {
|
|
@@ -304,7 +316,9 @@ export const secretCreateParameters = {
|
|
|
304
316
|
};
|
|
305
317
|
export const secretRemoveParameters = {
|
|
306
318
|
type: "object",
|
|
307
|
-
properties: {
|
|
319
|
+
properties: {
|
|
320
|
+
name: { type: "string", description: "Secret name to remove." },
|
|
321
|
+
},
|
|
308
322
|
required: ["name"],
|
|
309
323
|
};
|
|
310
324
|
export const containerSecretAddParameters = {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
// core returns the leading x.y.z of a version string, or "" when there is
|
|
5
|
+
// none. A release build is exactly the tag; a build from a later commit carries
|
|
6
|
+
// a git-describe suffix (`1.2.3-4-gabc123`), and a local build may be `dev`.
|
|
7
|
+
export function core(value) {
|
|
8
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(value ?? ""));
|
|
9
|
+
return match === null ? "" : `${match[1]}.${match[2]}.${match[3]}`;
|
|
10
|
+
}
|
|
11
|
+
// major returns the major component of a version's core, or -1 when the value
|
|
12
|
+
// has no parseable core.
|
|
13
|
+
export function major(value) {
|
|
14
|
+
const parsed = core(value);
|
|
15
|
+
return parsed === "" ? -1 : Number(parsed.slice(0, parsed.indexOf(".")));
|
|
16
|
+
}
|
|
17
|
+
// versionState mirrors the orchestrator's own compatibility rule, so the card
|
|
18
|
+
// reports exactly what the control plane enforces.
|
|
19
|
+
export function versionState(plugin, orchestrator) {
|
|
20
|
+
const orchestratorCore = core(orchestrator);
|
|
21
|
+
if (orchestratorCore === "")
|
|
22
|
+
return "ok";
|
|
23
|
+
const pluginCore = core(plugin);
|
|
24
|
+
if (pluginCore === "")
|
|
25
|
+
return "major-mismatch";
|
|
26
|
+
if (major(plugin) !== major(orchestrator))
|
|
27
|
+
return "major-mismatch";
|
|
28
|
+
return pluginCore === orchestratorCore ? "ok" : "minor-mismatch";
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { core, major, versionState } from "./version-compat.js";
|
|
7
|
+
test("core keeps the leading x.y.z", () => {
|
|
8
|
+
assert.equal(core("1.2.3"), "1.2.3");
|
|
9
|
+
assert.equal(core("0.2.0-rc.3"), "0.2.0");
|
|
10
|
+
assert.equal(core("1.2.3-4-gabc123"), "1.2.3");
|
|
11
|
+
assert.equal(core("dev"), "");
|
|
12
|
+
assert.equal(core(""), "");
|
|
13
|
+
});
|
|
14
|
+
test("major reports the leading component", () => {
|
|
15
|
+
assert.equal(major("1.2.3"), 1);
|
|
16
|
+
assert.equal(major("0.2.0-rc.3"), 0);
|
|
17
|
+
assert.equal(major("dev"), -1);
|
|
18
|
+
});
|
|
19
|
+
test("versionState mirrors the orchestrator's compatibility rule", () => {
|
|
20
|
+
assert.equal(versionState("1.2.3", "1.2.3"), "ok");
|
|
21
|
+
assert.equal(versionState("1.2.3-4-gabc", "1.2.3"), "ok");
|
|
22
|
+
assert.equal(versionState("1.3.0", "1.2.3"), "minor-mismatch");
|
|
23
|
+
assert.equal(versionState("1.2.4", "1.2.3"), "minor-mismatch");
|
|
24
|
+
assert.equal(versionState("2.0.0", "1.9.0"), "major-mismatch");
|
|
25
|
+
assert.equal(versionState("1.9.0", "2.0.0"), "major-mismatch");
|
|
26
|
+
assert.equal(versionState("1.2.3", "dev"), "ok", "an orchestrator with no comparable version accepts any plugin");
|
|
27
|
+
});
|
package/dist/volumes.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
import { VOLUME_TOOLS } from "./test-support.js";
|
|
7
|
-
import {
|
|
7
|
+
import { toolHandlers, TOOLS } from "./index.js";
|
|
8
8
|
test("volume tools are registered with the expected schemas", () => {
|
|
9
9
|
for (const name of VOLUME_TOOLS) {
|
|
10
10
|
const tool = TOOLS.find((entry) => entry.name === name);
|