@exagone313/dsh-podman 0.2.0-rc.2
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/LICENSE +21 -0
- package/README.md +41 -0
- package/README.zh.md +41 -0
- package/cordis.patch.yml +17 -0
- package/dist/approval-reasons.d.ts +129 -0
- package/dist/approval-reasons.js +253 -0
- package/dist/approval-reasons.test.d.ts +1 -0
- package/dist/approval-reasons.test.js +115 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.js +412 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/approval.test.js +462 -0
- package/dist/card-route.d.ts +15 -0
- package/dist/card-route.js +478 -0
- package/dist/card-route.test.d.ts +1 -0
- package/dist/card-route.test.js +181 -0
- package/dist/card-test-support.d.ts +15 -0
- package/dist/card-test-support.js +160 -0
- package/dist/client/ContainerCard.d.ts +6 -0
- package/dist/client/ContainerCard.js +79 -0
- package/dist/client/card-client.d.ts +6 -0
- package/dist/client/card-client.js +47 -0
- package/dist/client/card-protocol.d.ts +101 -0
- package/dist/client/card-protocol.js +10 -0
- package/dist/client/container-card-caches.d.ts +10 -0
- package/dist/client/container-card-caches.js +31 -0
- package/dist/client/container-card-controller.d.ts +107 -0
- package/dist/client/container-card-controller.js +207 -0
- package/dist/client/container-card-create-modal.d.ts +24 -0
- package/dist/client/container-card-create-modal.js +109 -0
- package/dist/client/container-card-directory-styles.d.ts +5 -0
- package/dist/client/container-card-directory-styles.js +246 -0
- package/dist/client/container-card-directory.d.ts +12 -0
- package/dist/client/container-card-directory.js +144 -0
- package/dist/client/container-card-editors.d.ts +30 -0
- package/dist/client/container-card-editors.js +116 -0
- package/dist/client/container-card-images.d.ts +37 -0
- package/dist/client/container-card-images.js +35 -0
- package/dist/client/container-card-paths.d.ts +20 -0
- package/dist/client/container-card-paths.js +88 -0
- package/dist/client/container-card-row.d.ts +30 -0
- package/dist/client/container-card-row.js +54 -0
- package/dist/client/container-card-secrets.d.ts +21 -0
- package/dist/client/container-card-secrets.js +71 -0
- package/dist/client/container-card-shared.d.ts +55 -0
- package/dist/client/container-card-shared.js +152 -0
- package/dist/client/container-card-styles.d.ts +21 -0
- package/dist/client/container-card-styles.js +143 -0
- package/dist/client/container-card-volumes.d.ts +12 -0
- package/dist/client/container-card-volumes.js +37 -0
- package/dist/client/container-card-workspace.d.ts +31 -0
- package/dist/client/container-card-workspace.js +24 -0
- package/dist/client/directory-picker.d.ts +12 -0
- package/dist/client/directory-picker.js +30 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4419 -0
- package/dist/client/locales.d.ts +9 -0
- package/dist/client/locales.js +370 -0
- package/dist/client/read-only-approval.d.ts +8 -0
- package/dist/client/read-only-approval.js +63 -0
- package/dist/client/slot-contract.d.ts +12 -0
- package/dist/client/slot-contract.js +4 -0
- package/dist/client/terminal-styles.d.ts +5 -0
- package/dist/client/terminal-styles.js +31 -0
- package/dist/client/tool-views.d.ts +8 -0
- package/dist/client/tool-views.js +241 -0
- package/dist/containers.test.d.ts +1 -0
- package/dist/containers.test.js +443 -0
- package/dist/daemons.test.d.ts +1 -0
- package/dist/daemons.test.js +153 -0
- package/dist/fs-provider.d.ts +23 -0
- package/dist/fs-provider.js +184 -0
- package/dist/fs-provider.test.d.ts +1 -0
- package/dist/fs-provider.test.js +260 -0
- package/dist/generated/version.d.ts +2 -0
- package/dist/generated/version.js +6 -0
- package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
- package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
- package/dist/grpc/runtime-client.d.ts +5 -0
- package/dist/grpc/runtime-client.js +43 -0
- package/dist/guest-rpc.d.ts +62 -0
- package/dist/guest-rpc.js +391 -0
- package/dist/images.test.d.ts +1 -0
- package/dist/images.test.js +79 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +140 -0
- package/dist/misc.test.d.ts +1 -0
- package/dist/misc.test.js +141 -0
- package/dist/mount-enums.d.ts +22 -0
- package/dist/mount-enums.js +81 -0
- package/dist/mount-input.d.ts +14 -0
- package/dist/mount-input.js +64 -0
- package/dist/mounts.test.d.ts +1 -0
- package/dist/mounts.test.js +408 -0
- package/dist/output-reader.d.ts +34 -0
- package/dist/output-reader.js +87 -0
- package/dist/paths.test.d.ts +1 -0
- package/dist/paths.test.js +74 -0
- package/dist/preferences.d.ts +6 -0
- package/dist/preferences.js +27 -0
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.js +73 -0
- package/dist/project-path.test.d.ts +1 -0
- package/dist/project-path.test.js +52 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +149 -0
- package/dist/public.d.ts +4 -0
- package/dist/public.js +63 -0
- package/dist/read-only-shell.d.ts +14 -0
- package/dist/read-only-shell.js +150 -0
- package/dist/read-only-shell.test.d.ts +1 -0
- package/dist/read-only-shell.test.js +127 -0
- package/dist/secrets.test.d.ts +1 -0
- package/dist/secrets.test.js +181 -0
- package/dist/settings-commands.test.d.ts +1 -0
- package/dist/settings-commands.test.js +549 -0
- package/dist/settings-create.test.d.ts +1 -0
- package/dist/settings-create.test.js +394 -0
- package/dist/settings-mounts.test.d.ts +1 -0
- package/dist/settings-mounts.test.js +467 -0
- package/dist/settings-paths.test.d.ts +1 -0
- package/dist/settings-paths.test.js +65 -0
- package/dist/settings-schema.d.ts +14 -0
- package/dist/settings-schema.js +14 -0
- package/dist/settings-workspaces.test.d.ts +1 -0
- package/dist/settings-workspaces.test.js +265 -0
- package/dist/spill-store.d.ts +17 -0
- package/dist/spill-store.js +54 -0
- package/dist/spill-store.test.d.ts +1 -0
- package/dist/spill-store.test.js +59 -0
- package/dist/subprocess.d.ts +9 -0
- package/dist/subprocess.js +369 -0
- package/dist/subprocess.test.d.ts +1 -0
- package/dist/subprocess.test.js +234 -0
- package/dist/test-support.d.ts +328 -0
- package/dist/test-support.js +437 -0
- package/dist/tool-defs.d.ts +26 -0
- package/dist/tool-defs.js +188 -0
- package/dist/tool-handlers.d.ts +2 -0
- package/dist/tool-handlers.js +546 -0
- package/dist/tool-params.d.ts +803 -0
- package/dist/tool-params.js +494 -0
- package/dist/tool-schemas.d.ts +2 -0
- package/dist/tool-schemas.js +5 -0
- package/dist/tool-views.d.ts +2 -0
- package/dist/tool-views.js +56 -0
- package/dist/views.test.d.ts +1 -0
- package/dist/views.test.js +23 -0
- package/dist/volumes.test.d.ts +1 -0
- package/dist/volumes.test.js +40 -0
- package/dist/workspace-binding.d.ts +40 -0
- package/dist/workspace-binding.js +275 -0
- package/dist/workspace-binding.test.d.ts +1 -0
- package/dist/workspace-binding.test.js +337 -0
- package/docs/architecture.md +173 -0
- package/docs/architecture.zh.md +129 -0
- package/docs/configuration.md +160 -0
- package/docs/configuration.zh.md +147 -0
- package/docs/development.md +142 -0
- package/docs/development.zh.md +135 -0
- package/docs/install-dsh-and-dsh-podman.md +165 -0
- package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
- package/docs/usage.md +393 -0
- package/docs/usage.zh.md +315 -0
- package/package.json +119 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
syntax = "proto3";
|
|
6
|
+
package dshguest.v1;
|
|
7
|
+
option go_package = "github.com/Exagone313/dsh-podman/internal/genproto/dshguest/v1;dshguestv1";
|
|
8
|
+
|
|
9
|
+
import "google/protobuf/wrappers.proto";
|
|
10
|
+
|
|
11
|
+
service WorkspaceGuestAgent {
|
|
12
|
+
rpc Exec(stream ExecInput) returns (stream ExecOutput);
|
|
13
|
+
rpc Signal(SignalRequest) returns (SignalResponse);
|
|
14
|
+
rpc ReadFile(ReadFileRequest) returns (stream ReadFileChunk);
|
|
15
|
+
rpc WriteFile(stream WriteFileChunk) returns (WriteFileResponse);
|
|
16
|
+
rpc Stat(StatRequest) returns (StatResponse);
|
|
17
|
+
rpc ReadDir(ReadDirRequest) returns (ReadDirResponse);
|
|
18
|
+
rpc Mkdir(MkdirRequest) returns (MkdirResponse);
|
|
19
|
+
rpc Delete(DeleteRequest) returns (DeleteResponse);
|
|
20
|
+
rpc SetPaths(SetPathsRequest) returns (PathsResponse);
|
|
21
|
+
rpc GetPaths(GetPathsRequest) returns (PathsResponse);
|
|
22
|
+
rpc StartDaemon(StartDaemonRequest) returns (DaemonInfo);
|
|
23
|
+
rpc ListDaemons(ListDaemonsRequest) returns (ListDaemonsResponse);
|
|
24
|
+
rpc StopDaemon(StopDaemonRequest) returns (StopDaemonResponse);
|
|
25
|
+
rpc StopAllDaemons(StopAllDaemonsRequest) returns (StopAllDaemonsResponse);
|
|
26
|
+
rpc RestartDaemon(RestartDaemonRequest) returns (DaemonInfo);
|
|
27
|
+
rpc DaemonLogs(DaemonLogsRequest) returns (DaemonLogsResponse);
|
|
28
|
+
rpc Terminal(stream TerminalInput) returns (stream TerminalOutput);
|
|
29
|
+
}
|
|
30
|
+
message ExecInput { oneof payload { ExecStart start = 1; bytes stdin_chunk = 2; } }
|
|
31
|
+
message ExecStart { repeated string argv = 1; string cwd = 2; map<string, string> env = 3; repeated string unset_env = 4; SpillTarget spill_stdout = 5; SpillTarget spill_stderr = 6; google.protobuf.Int32Value uid = 7; google.protobuf.Int32Value gid = 8; repeated uint32 groups = 9; bool stdin_pipe = 10; }
|
|
32
|
+
message ExecOutput { oneof payload { bytes stdout_chunk = 1; bytes stderr_chunk = 2; ExecExit exit = 3; } string process_id = 4; }
|
|
33
|
+
message ExecExit { int32 exit_code = 1; bool signaled = 2; string signal = 3; bool stdout_spill_valid = 4; bool stderr_spill_valid = 5; }
|
|
34
|
+
// SpillTarget is a bounded full-stream copy of one output stream, written in
|
|
35
|
+
// the guest's execution world so the caller can read the complete output after
|
|
36
|
+
// an in-memory truncation.
|
|
37
|
+
message SpillTarget { int64 max_bytes = 1; string path = 2; }
|
|
38
|
+
message SignalRequest { string process_id = 1; string signal = 2; }
|
|
39
|
+
message SignalResponse {}
|
|
40
|
+
message ReadFileRequest { string path = 1; int64 offset = 2; int64 length = 3; }
|
|
41
|
+
message ReadFileChunk { bytes data = 1; }
|
|
42
|
+
message WriteFileChunk { oneof payload { WriteFileStart start = 1; bytes data_chunk = 2; } }
|
|
43
|
+
message WriteFileStart { string path = 1; bool create = 2; bool truncate = 3; }
|
|
44
|
+
message WriteFileResponse { int64 bytes_written = 1; }
|
|
45
|
+
message StatRequest { string path = 1; bool no_follow = 2; }
|
|
46
|
+
message StatResponse { bool exists = 1; bool is_dir = 2; int64 size = 3; string mode = 4; string modified_at = 5; bool is_symlink = 6; }
|
|
47
|
+
message ReadDirRequest { string path = 1; }
|
|
48
|
+
message ReadDirResponse { repeated DirEntry entries = 1; }
|
|
49
|
+
message DirEntry { string name = 1; bool is_dir = 2; int64 size = 3; string type = 4; }
|
|
50
|
+
message MkdirRequest { string path = 1; bool parents = 2; }
|
|
51
|
+
message MkdirResponse {}
|
|
52
|
+
message DeleteRequest { string path = 1; bool recursive = 2; }
|
|
53
|
+
message DeleteResponse {}
|
|
54
|
+
// Path additions the agent prepends to every child's PATH, highest priority
|
|
55
|
+
// first. default_path is the agent's own PATH (the container image's), which
|
|
56
|
+
// the additions are prepended to.
|
|
57
|
+
message SetPathsRequest { repeated string paths = 1; }
|
|
58
|
+
message GetPathsRequest {}
|
|
59
|
+
message PathsResponse { repeated string paths = 1; string default_path = 2; }
|
|
60
|
+
message StartDaemonRequest { string name = 1; repeated string argv = 2; string cwd = 3; map<string, string> env = 4; google.protobuf.Int32Value uid = 5; google.protobuf.Int32Value gid = 6; repeated uint32 groups = 7; google.protobuf.BoolValue inherit_env = 8; }
|
|
61
|
+
message DaemonInfo { string name = 1; repeated string argv = 2; bool running = 3; int32 exit_code = 4; string started_at = 5; string stopped_at = 6; uint32 uid = 7; uint32 gid = 8; repeated uint32 groups = 9; }
|
|
62
|
+
message ListDaemonsRequest {}
|
|
63
|
+
message ListDaemonsResponse { repeated DaemonInfo daemons = 1; }
|
|
64
|
+
message StopDaemonRequest { string name = 1; string signal = 2; }
|
|
65
|
+
message StopDaemonResponse {}
|
|
66
|
+
message StopAllDaemonsRequest {}
|
|
67
|
+
message StopAllDaemonsResponse { repeated string daemons = 1; }
|
|
68
|
+
message RestartDaemonRequest { string name = 1; }
|
|
69
|
+
message DaemonLogsRequest { string name = 1; int32 tail_bytes = 2; }
|
|
70
|
+
message DaemonLogsResponse { bytes stdout = 1; bytes stderr = 2; }
|
|
71
|
+
message TerminalInput { oneof payload { TerminalStart start = 1; bytes stdin_chunk = 2; TerminalResize resize = 3; TerminalSignal signal = 4; string inspect_request_id = 5; bool close = 6; } }
|
|
72
|
+
message TerminalStart { repeated string argv = 1; string cwd = 2; map<string, string> env = 3; uint32 rows = 4; uint32 cols = 5; }
|
|
73
|
+
message TerminalResize { uint32 rows = 1; uint32 cols = 2; }
|
|
74
|
+
message TerminalSignal { string signal = 1; string request_id = 2; }
|
|
75
|
+
message TerminalOutput { oneof payload { TerminalStarted started = 1; bytes stdout_chunk = 2; TerminalExit exit = 3; TerminalForeground foreground = 4; TerminalSignalled signalled = 5; } }
|
|
76
|
+
message TerminalStarted { int32 pid = 1; }
|
|
77
|
+
message TerminalExit { int32 exit_code = 1; bool signaled = 2; string signal = 3; }
|
|
78
|
+
message TerminalForeground { string request_id = 1; bool found = 2; int32 process_group_id = 3; bool input_waiting = 4; }
|
|
79
|
+
message TerminalSignalled { string request_id = 1; bool found = 2; int32 process_group_id = 3; }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import grpc from "@grpc/grpc-js";
|
|
2
|
+
export declare function controlClient(socket: string): grpc.Client;
|
|
3
|
+
export declare function guestClient(socket: string): grpc.Client;
|
|
4
|
+
export declare function unary<T>(client: grpc.Client, method: string, request: unknown, metadata?: grpc.Metadata): Promise<T>;
|
|
5
|
+
export { grpc };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import grpc from "@grpc/grpc-js";
|
|
5
|
+
import loader from "@grpc/proto-loader";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "proto");
|
|
9
|
+
const definitions = new Map();
|
|
10
|
+
function unixTarget(socket) {
|
|
11
|
+
return socket.startsWith("unix:") ? socket : `unix:${socket}`;
|
|
12
|
+
}
|
|
13
|
+
function client(service, proto, socket) {
|
|
14
|
+
const key = `${service}:${socket}`;
|
|
15
|
+
let Constructor = definitions.get(key);
|
|
16
|
+
if (Constructor === undefined) {
|
|
17
|
+
const packageDefinition = loader.loadSync(resolve(root, proto), {
|
|
18
|
+
longs: String,
|
|
19
|
+
enums: String,
|
|
20
|
+
defaults: true,
|
|
21
|
+
});
|
|
22
|
+
const loaded = grpc.loadPackageDefinition(packageDefinition);
|
|
23
|
+
const namespace = service.startsWith("dshctl")
|
|
24
|
+
? loaded.dshctl
|
|
25
|
+
: loaded.dshguest;
|
|
26
|
+
const version = namespace.v1;
|
|
27
|
+
Constructor = version[service.split(".").at(-1)];
|
|
28
|
+
definitions.set(key, Constructor);
|
|
29
|
+
}
|
|
30
|
+
return new Constructor(unixTarget(socket), grpc.credentials.createInsecure());
|
|
31
|
+
}
|
|
32
|
+
export function controlClient(socket) {
|
|
33
|
+
return client("dshctl.v1.OrchestratorControl", "dshctl/v1/control.proto", socket);
|
|
34
|
+
}
|
|
35
|
+
export function guestClient(socket) {
|
|
36
|
+
return client("dshguest.v1.WorkspaceGuestAgent", "dshguest/v1/guest.proto", socket);
|
|
37
|
+
}
|
|
38
|
+
export function unary(client, method, request, metadata) {
|
|
39
|
+
return new Promise((resolvePromise, reject) => {
|
|
40
|
+
client[method](request, metadata ?? new grpc.Metadata(), (error, value) => error ? reject(error) : resolvePromise(value));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export { grpc };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { WorkspaceResolver } from "./workspace-binding.js";
|
|
2
|
+
export declare function resolveGuestPath(path: string, sessionCwd: unknown): string;
|
|
3
|
+
export declare function resolveGuestCwd(cwd: unknown, sessionCwd: unknown): string | undefined;
|
|
4
|
+
export declare function guestCwd(requested: unknown, sessionCwd: unknown, binding: {
|
|
5
|
+
defaultCwd?: string;
|
|
6
|
+
}): string | undefined;
|
|
7
|
+
export declare function approvalPath(path: string, sessionCwd: unknown): string;
|
|
8
|
+
export declare const DIFF_BASIS_MAX_BYTES: number;
|
|
9
|
+
export declare function normalizeLineEndings(content: string): string;
|
|
10
|
+
export declare function detectLineEndings(raw: string): "CRLF" | "LF";
|
|
11
|
+
export declare function restoreLineEndings(content: string, endings: "CRLF" | "LF"): string;
|
|
12
|
+
export declare function fsError(code: string, message: string): Error;
|
|
13
|
+
export declare function throwIfAborted(signal: AbortSignal | undefined, verb: string): void;
|
|
14
|
+
export declare function guestChunks(target: any, range: {
|
|
15
|
+
offset?: number;
|
|
16
|
+
length?: number;
|
|
17
|
+
} | undefined, signal: AbortSignal | undefined): AsyncIterable<Buffer>;
|
|
18
|
+
export declare function guestTextChunks(target: any, signal: AbortSignal | undefined): AsyncIterable<string>;
|
|
19
|
+
export declare function readGuestText(target: any, signal: AbortSignal | undefined): Promise<string>;
|
|
20
|
+
export declare function readDiffBasis(target: any, signal: AbortSignal | undefined): Promise<string | null>;
|
|
21
|
+
export declare function guestVersion(result: any): string;
|
|
22
|
+
export declare function writeGuestFile(binding: {
|
|
23
|
+
guest: any;
|
|
24
|
+
token: string;
|
|
25
|
+
}, path: string, content: string, opts: {
|
|
26
|
+
create: boolean;
|
|
27
|
+
truncate: boolean;
|
|
28
|
+
}, signal?: AbortSignal): Promise<number>;
|
|
29
|
+
export declare function readGuestFile(binding: {
|
|
30
|
+
guest: any;
|
|
31
|
+
token: string;
|
|
32
|
+
}, path: string): Promise<string>;
|
|
33
|
+
export interface ExecIdentity {
|
|
34
|
+
uid?: number;
|
|
35
|
+
gid?: number;
|
|
36
|
+
groups?: number[];
|
|
37
|
+
}
|
|
38
|
+
export declare function runExec(binding: {
|
|
39
|
+
guest: any;
|
|
40
|
+
token: string;
|
|
41
|
+
}, argv: readonly string[], cwd?: string, env?: Record<string, string>, timeoutMs?: number, identity?: ExecIdentity): Promise<{
|
|
42
|
+
exitCode: number;
|
|
43
|
+
signal: string | null;
|
|
44
|
+
stdout: string;
|
|
45
|
+
stderr: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function sliceLines(content: string, offset: unknown, limit: unknown): string;
|
|
48
|
+
export declare function outputLines(text: string): string[];
|
|
49
|
+
export declare function bytesText(value: unknown): string;
|
|
50
|
+
export declare function currentCwd(exec: any): unknown;
|
|
51
|
+
export declare function sessionWorkspaceSlug(resolver: WorkspaceResolver, cwd: unknown): Promise<string>;
|
|
52
|
+
export declare function resolveToolBinding(resolver: WorkspaceResolver, cwd: unknown, container: string): Promise<{
|
|
53
|
+
guest: any;
|
|
54
|
+
token: string;
|
|
55
|
+
socket: string;
|
|
56
|
+
defaultCwd?: string;
|
|
57
|
+
}>;
|
|
58
|
+
export declare function guestStat(target: any, signal?: AbortSignal): Promise<any>;
|
|
59
|
+
export declare function guestStatResponse(target: any, signal?: AbortSignal): Promise<any>;
|
|
60
|
+
export declare function unaryGuest(target: any, method: string, request: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
61
|
+
export declare function globCwd(argv: readonly string[], cwd: unknown): string | undefined;
|
|
62
|
+
export declare function remoteArgv(argv: readonly string[]): readonly string[];
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { metadata, workspaceSlug } from "./workspace-binding.js";
|
|
5
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
6
|
+
// Resolve a container-side path against the session working directory.
|
|
7
|
+
// Absolute paths pass through unchanged. The session cwd and the container's
|
|
8
|
+
// view of the project coincide because the orchestrator mounts each project at
|
|
9
|
+
// its mirrored path under DSH_PODMAN_PROJECTS_ROOT.
|
|
10
|
+
function resolveAgainstSession(path, sessionCwd) {
|
|
11
|
+
if (isAbsolute(path))
|
|
12
|
+
return path;
|
|
13
|
+
const base = typeof sessionCwd === "string" && sessionCwd !== "" ? sessionCwd : undefined;
|
|
14
|
+
if (base === undefined) {
|
|
15
|
+
throw new Error("relative paths need a session working directory; pass an absolute path");
|
|
16
|
+
}
|
|
17
|
+
return resolvePath(base, path);
|
|
18
|
+
}
|
|
19
|
+
// Resolve a guest filesystem path against the session working directory.
|
|
20
|
+
//
|
|
21
|
+
// A ".." segment is refused before resolution: afterwards it would have been
|
|
22
|
+
// normalised away, and the guest agent would reject the result with a less
|
|
23
|
+
// helpful error. The guest agent confines these paths to the projects root, so
|
|
24
|
+
// this is a clearer error rather than the boundary itself.
|
|
25
|
+
export function resolveGuestPath(path, sessionCwd) {
|
|
26
|
+
if (path.split("/").includes("..")) {
|
|
27
|
+
throw new Error("path must not escape the workspace");
|
|
28
|
+
}
|
|
29
|
+
return resolveAgainstSession(path, sessionCwd);
|
|
30
|
+
}
|
|
31
|
+
// Resolve a working directory, or a path handed to a command, against the
|
|
32
|
+
// session working directory. An unset value stays unset, leaving the guest
|
|
33
|
+
// agent's own working directory in place.
|
|
34
|
+
//
|
|
35
|
+
// Unlike resolveGuestPath this does not refuse "..": commands are not confined
|
|
36
|
+
// to the projects root, so rejecting it would be theatre while breaking a
|
|
37
|
+
// legitimate "../sibling" working directory.
|
|
38
|
+
export function resolveGuestCwd(cwd, sessionCwd) {
|
|
39
|
+
if (typeof cwd !== "string" || cwd === "")
|
|
40
|
+
return undefined;
|
|
41
|
+
return resolveAgainstSession(cwd, sessionCwd);
|
|
42
|
+
}
|
|
43
|
+
// guestCwd resolves a command's working directory: an explicit value is
|
|
44
|
+
// resolved against the session, otherwise the container's default (the session
|
|
45
|
+
// directory when it is mounted) is used. Undefined leaves the guest agent's own
|
|
46
|
+
// working directory in place.
|
|
47
|
+
export function guestCwd(requested, sessionCwd, binding) {
|
|
48
|
+
return resolveGuestCwd(requested, sessionCwd) ?? binding.defaultCwd;
|
|
49
|
+
}
|
|
50
|
+
// The path to show in an approval prompt: the resolved target when it can be
|
|
51
|
+
// worked out, else the value as given. Never throws — a bad path is reported
|
|
52
|
+
// by the handler, and the prompt must still render.
|
|
53
|
+
export function approvalPath(path, sessionCwd) {
|
|
54
|
+
try {
|
|
55
|
+
return resolveGuestPath(path, sessionCwd);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return path;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Binary detection samples only the head of a file, matching the harness's
|
|
62
|
+
// local backend (`fs-local`).
|
|
63
|
+
const BINARY_SAMPLE_BYTES = 8192;
|
|
64
|
+
// Cap on the contextual-diff basis read before a write; a larger existing file
|
|
65
|
+
// yields `before: null`, matching `fs-local`'s default (10 MiB).
|
|
66
|
+
export const DIFF_BASIS_MAX_BYTES = 10 * 1024 * 1024;
|
|
67
|
+
// Line-ending handling for edits, mirroring `fs-local`: match on LF-normalized
|
|
68
|
+
// text, then restore the file's original style on write-back.
|
|
69
|
+
export function normalizeLineEndings(content) {
|
|
70
|
+
return content.replaceAll("\r\n", "\n");
|
|
71
|
+
}
|
|
72
|
+
export function detectLineEndings(raw) {
|
|
73
|
+
const sample = raw.slice(0, 4096);
|
|
74
|
+
const crlfCount = sample.split("\r\n").length - 1;
|
|
75
|
+
const lfCount = sample.split("\n").length - 1 - crlfCount;
|
|
76
|
+
return crlfCount > lfCount ? "CRLF" : "LF";
|
|
77
|
+
}
|
|
78
|
+
export function restoreLineEndings(content, endings) {
|
|
79
|
+
return endings === "LF"
|
|
80
|
+
? content
|
|
81
|
+
: normalizeLineEndings(content).split("\n").join("\r\n");
|
|
82
|
+
}
|
|
83
|
+
export function fsError(code, message) {
|
|
84
|
+
const error = new Error(message);
|
|
85
|
+
error.code = code;
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
// The harness's FS_ABORTED: a caller that already cancelled must fail before
|
|
89
|
+
// any I/O.
|
|
90
|
+
export function throwIfAborted(signal, verb) {
|
|
91
|
+
if (signal?.aborted)
|
|
92
|
+
throw fsError("FS_ABORTED", `${verb} aborted`);
|
|
93
|
+
}
|
|
94
|
+
// Cancel a gRPC call when the caller aborts; returns a detach function.
|
|
95
|
+
function onAbortCancel(call, signal) {
|
|
96
|
+
if (signal === undefined)
|
|
97
|
+
return () => { };
|
|
98
|
+
const cancel = () => {
|
|
99
|
+
try {
|
|
100
|
+
call.cancel?.();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A finished call cannot be cancelled; nothing to do.
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
cancel();
|
|
108
|
+
return () => { };
|
|
109
|
+
}
|
|
110
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
111
|
+
return () => signal.removeEventListener("abort", cancel);
|
|
112
|
+
}
|
|
113
|
+
// Stream raw bytes from the guest's ReadFile. A zero/absent length reads to
|
|
114
|
+
// EOF. Cancelling the signal cancels the call and reports FS_ABORTED.
|
|
115
|
+
export async function* guestChunks(target, range, signal) {
|
|
116
|
+
throwIfAborted(signal, "read");
|
|
117
|
+
const request = { path: target.targetKey };
|
|
118
|
+
if (range?.offset !== undefined && range.offset > 0) {
|
|
119
|
+
request.offset = range.offset;
|
|
120
|
+
}
|
|
121
|
+
if (range?.length !== undefined && range.length > 0) {
|
|
122
|
+
request.length = range.length;
|
|
123
|
+
}
|
|
124
|
+
const call = target.binding.guest.readFile(request, metadata(target.binding.token));
|
|
125
|
+
const detach = onAbortCancel(call, signal);
|
|
126
|
+
try {
|
|
127
|
+
for await (const chunk of call) {
|
|
128
|
+
yield Buffer.from(chunk.data);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (signal?.aborted)
|
|
133
|
+
throw fsError("FS_ABORTED", "read aborted");
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
detach();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Stream the guest file as decoded UTF-8 text, rejecting binary content (a NUL
|
|
141
|
+
// byte in the head, or invalid UTF-8) with the harness's FS_NOT_TEXT code.
|
|
142
|
+
export async function* guestTextChunks(target, signal) {
|
|
143
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
144
|
+
let sampled = 0;
|
|
145
|
+
try {
|
|
146
|
+
for await (const buffer of guestChunks(target, undefined, signal)) {
|
|
147
|
+
if (sampled < BINARY_SAMPLE_BYTES) {
|
|
148
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, BINARY_SAMPLE_BYTES - sampled));
|
|
149
|
+
if (sample.includes(0)) {
|
|
150
|
+
throw fsError("FS_NOT_TEXT", `cannot read "${target.displayPath}": binary file`);
|
|
151
|
+
}
|
|
152
|
+
sampled += sample.length;
|
|
153
|
+
}
|
|
154
|
+
const text = decoder.decode(buffer, { stream: true });
|
|
155
|
+
if (text !== "")
|
|
156
|
+
yield text;
|
|
157
|
+
}
|
|
158
|
+
const tail = decoder.decode();
|
|
159
|
+
if (tail !== "")
|
|
160
|
+
yield tail;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
// A fatal TextDecoder failure is a TypeError; anything else (our own
|
|
164
|
+
// FS_NOT_TEXT/FS_ABORTED, or a transport error) keeps its identity.
|
|
165
|
+
if (error instanceof TypeError) {
|
|
166
|
+
throw fsError("FS_NOT_TEXT", `cannot read "${target.displayPath}": invalid UTF-8 text`);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export async function readGuestText(target, signal) {
|
|
172
|
+
let text = "";
|
|
173
|
+
for await (const chunk of guestTextChunks(target, signal))
|
|
174
|
+
text += chunk;
|
|
175
|
+
return text;
|
|
176
|
+
}
|
|
177
|
+
// The contextual-diff basis before a write: absent for a create, binary, or
|
|
178
|
+
// unreadable prior file (presentation only, never a correctness input).
|
|
179
|
+
export async function readDiffBasis(target, signal) {
|
|
180
|
+
try {
|
|
181
|
+
return await readGuestText(target, signal);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.code === "FS_NOT_TEXT")
|
|
185
|
+
return null;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function guestVersion(result) {
|
|
190
|
+
return `agent:${result.modifiedAt ?? ""}:${result.size ?? 0}:${result.mode ?? ""}`;
|
|
191
|
+
}
|
|
192
|
+
export async function writeGuestFile(binding, path, content, opts, signal) {
|
|
193
|
+
throwIfAborted(signal, "write");
|
|
194
|
+
return new Promise((resolveDone, reject) => {
|
|
195
|
+
let detach = () => { };
|
|
196
|
+
const call = binding.guest.writeFile(metadata(binding.token), {}, (error, result) => {
|
|
197
|
+
detach();
|
|
198
|
+
error
|
|
199
|
+
? reject(signal?.aborted ? fsError("FS_ABORTED", "write aborted") : error)
|
|
200
|
+
: resolveDone(Number(result?.bytesWritten ?? 0));
|
|
201
|
+
});
|
|
202
|
+
detach = onAbortCancel(call, signal);
|
|
203
|
+
call.write({
|
|
204
|
+
start: { path, create: opts.create, truncate: opts.truncate },
|
|
205
|
+
});
|
|
206
|
+
call.write({ dataChunk: Buffer.from(content) });
|
|
207
|
+
call.end();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
export async function readGuestFile(binding, path) {
|
|
211
|
+
const chunks = [];
|
|
212
|
+
await new Promise((resolveDone, reject) => {
|
|
213
|
+
const call = binding.guest.readFile({ path }, metadata(binding.token));
|
|
214
|
+
call.on("data", (chunk) => chunks.push(Buffer.from(chunk.data)));
|
|
215
|
+
call.on("error", reject);
|
|
216
|
+
call.on("end", resolveDone);
|
|
217
|
+
});
|
|
218
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
219
|
+
}
|
|
220
|
+
const READ_LIMIT = 2000;
|
|
221
|
+
export async function runExec(binding, argv, cwd, env, timeoutMs, identity) {
|
|
222
|
+
const stdout = [];
|
|
223
|
+
const stderr = [];
|
|
224
|
+
return new Promise((resolveDone, reject) => {
|
|
225
|
+
const stream = binding.guest.exec(metadata(binding.token));
|
|
226
|
+
let processId;
|
|
227
|
+
let timer;
|
|
228
|
+
const stopTimer = () => {
|
|
229
|
+
if (timer !== undefined) {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
timer = undefined;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
235
|
+
timer = setTimeout(() => {
|
|
236
|
+
timer = undefined;
|
|
237
|
+
if (processId !== undefined) {
|
|
238
|
+
// Best effort: ask the guest to terminate the running process.
|
|
239
|
+
unaryGuest({ binding }, "signal", {
|
|
240
|
+
processId,
|
|
241
|
+
signal: "SIGTERM",
|
|
242
|
+
}).catch(() => { });
|
|
243
|
+
}
|
|
244
|
+
reject(new Error(`command timed out after ${timeoutMs} ms`));
|
|
245
|
+
}, timeoutMs);
|
|
246
|
+
}
|
|
247
|
+
stream.on("data", (output) => {
|
|
248
|
+
if (output.processId)
|
|
249
|
+
processId = String(output.processId);
|
|
250
|
+
if (output.stdoutChunk)
|
|
251
|
+
stdout.push(Buffer.from(output.stdoutChunk));
|
|
252
|
+
if (output.stderrChunk)
|
|
253
|
+
stderr.push(Buffer.from(output.stderrChunk));
|
|
254
|
+
if (output.exit) {
|
|
255
|
+
stopTimer();
|
|
256
|
+
resolveDone({
|
|
257
|
+
exitCode: output.exit.exitCode,
|
|
258
|
+
signal: output.exit.signaled ? output.exit.signal : null,
|
|
259
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
260
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
stream.on("error", (error) => {
|
|
265
|
+
stopTimer();
|
|
266
|
+
reject(error);
|
|
267
|
+
});
|
|
268
|
+
stream.write({
|
|
269
|
+
start: {
|
|
270
|
+
argv: remoteArgv(argv),
|
|
271
|
+
cwd,
|
|
272
|
+
env: env ?? {},
|
|
273
|
+
...(identity?.uid !== undefined ? { uid: { value: identity.uid } } : {}),
|
|
274
|
+
...(identity?.gid !== undefined ? { gid: { value: identity.gid } } : {}),
|
|
275
|
+
...(identity?.groups !== undefined ? { groups: identity.groups } : {}),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
stream.end();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// sliceLines returns the requested 1-based line range of a file's content,
|
|
282
|
+
// defaulting to the first READ_LIMIT lines like the harness's read tool.
|
|
283
|
+
export function sliceLines(content, offset, limit) {
|
|
284
|
+
const start = typeof offset === "number" && Number.isInteger(offset) && offset > 0
|
|
285
|
+
? offset
|
|
286
|
+
: 1;
|
|
287
|
+
const max = typeof limit === "number" && Number.isInteger(limit) && limit > 0
|
|
288
|
+
? limit
|
|
289
|
+
: READ_LIMIT;
|
|
290
|
+
return content.split("\n").slice(start - 1, start - 1 + max).join("\n");
|
|
291
|
+
}
|
|
292
|
+
export function outputLines(text) {
|
|
293
|
+
return text
|
|
294
|
+
.split("\n")
|
|
295
|
+
.map((line) => line.trimEnd())
|
|
296
|
+
.filter((line) => line !== "");
|
|
297
|
+
}
|
|
298
|
+
export function bytesText(value) {
|
|
299
|
+
if (value === undefined)
|
|
300
|
+
return "";
|
|
301
|
+
return Buffer.isBuffer(value) ? value.toString("utf8") : String(value);
|
|
302
|
+
}
|
|
303
|
+
export function currentCwd(exec) {
|
|
304
|
+
return exec?.agent?.session?.header?.cwd;
|
|
305
|
+
}
|
|
306
|
+
export async function sessionWorkspaceSlug(resolver, cwd) {
|
|
307
|
+
const workspace = await resolver.registry?.resolveByPath?.(String(cwd));
|
|
308
|
+
if (workspace === undefined)
|
|
309
|
+
return "default";
|
|
310
|
+
return workspaceSlug(workspace.id);
|
|
311
|
+
}
|
|
312
|
+
export async function resolveToolBinding(resolver, cwd, container) {
|
|
313
|
+
if (container === "default")
|
|
314
|
+
return resolver.resolve(cwd);
|
|
315
|
+
return resolver.containerBinding(cwd, container);
|
|
316
|
+
}
|
|
317
|
+
export async function guestStat(target, signal) {
|
|
318
|
+
const result = await guestStatResponse(target, signal);
|
|
319
|
+
if (!result.exists)
|
|
320
|
+
return undefined;
|
|
321
|
+
return {
|
|
322
|
+
version: guestVersion(result),
|
|
323
|
+
type: result.isDir ? "directory" : "file",
|
|
324
|
+
...(result.isDir ? {} : { size: Number(result.size ?? 0) }),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
export function guestStatResponse(target, signal) {
|
|
328
|
+
throwIfAborted(signal, "stat");
|
|
329
|
+
return new Promise((resolveDone, reject) => {
|
|
330
|
+
let detach = () => { };
|
|
331
|
+
const call = target.binding.guest.stat({ path: target.targetKey }, metadata(target.binding.token), (error, value) => {
|
|
332
|
+
detach();
|
|
333
|
+
error
|
|
334
|
+
? reject(signal?.aborted ? fsError("FS_ABORTED", "stat aborted") : error)
|
|
335
|
+
: resolveDone(value);
|
|
336
|
+
});
|
|
337
|
+
detach = onAbortCancel(call, signal);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
export async function unaryGuest(target, method, request, signal) {
|
|
341
|
+
throwIfAborted(signal, method);
|
|
342
|
+
return new Promise((resolveDone, reject) => {
|
|
343
|
+
let detach = () => { };
|
|
344
|
+
const call = target.binding.guest[method](request, metadata(target.binding.token), (error, result) => {
|
|
345
|
+
detach();
|
|
346
|
+
error
|
|
347
|
+
? reject(signal?.aborted ? fsError("FS_ABORTED", `${method} aborted`) : error)
|
|
348
|
+
: resolveDone(result);
|
|
349
|
+
});
|
|
350
|
+
detach = onAbortCancel(call, signal);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// globCwd returns the working directory a ripgrep discovery listing must run
|
|
354
|
+
// in, or undefined to keep the caller's. The harness's glob tool passes its
|
|
355
|
+
// search path as an absolute positional root, but ripgrep anchors a `--glob`
|
|
356
|
+
// pattern containing "/" to the process cwd, so a pattern like "sub/b.txt"
|
|
357
|
+
// could never match an absolute root. Running the listing from the root anchors
|
|
358
|
+
// the pattern to `path` while keeping the printed paths absolute, which is what
|
|
359
|
+
// the harness renders against the session working directory.
|
|
360
|
+
export function globCwd(argv, cwd) {
|
|
361
|
+
const runner = argv[0];
|
|
362
|
+
if (runner === undefined || !/(?:^|\/)rg(?:\.exe)?$/.test(runner))
|
|
363
|
+
return undefined;
|
|
364
|
+
if (!argv.includes("--files"))
|
|
365
|
+
return undefined;
|
|
366
|
+
const separator = argv.indexOf("--");
|
|
367
|
+
if (separator < 0)
|
|
368
|
+
return undefined;
|
|
369
|
+
const roots = argv.slice(separator + 1);
|
|
370
|
+
if (roots.length !== 1)
|
|
371
|
+
return undefined;
|
|
372
|
+
const root = roots[0];
|
|
373
|
+
if (typeof cwd !== "string" || cwd === "" || !isAbsolute(root))
|
|
374
|
+
return undefined;
|
|
375
|
+
return root;
|
|
376
|
+
}
|
|
377
|
+
// remoteArgv rewrites an argv for the guest's execution world: the harness's
|
|
378
|
+
// bundled ripgrep becomes the guest's /usr/bin/rg, and a landlock-run sandbox
|
|
379
|
+
// wrapper is unwrapped to the command it guards.
|
|
380
|
+
export function remoteArgv(argv) {
|
|
381
|
+
const runner = argv[0];
|
|
382
|
+
if (runner !== undefined && /(?:^|\/)rg(?:\.exe)?$/.test(runner)) {
|
|
383
|
+
return ["/usr/bin/rg", ...argv.slice(1)];
|
|
384
|
+
}
|
|
385
|
+
if (runner !== undefined && /(?:^|\/)landlock-run(?:$|\/)/.test(runner)) {
|
|
386
|
+
const separator = argv.indexOf("--");
|
|
387
|
+
if (separator >= 0)
|
|
388
|
+
return remoteArgv(argv.slice(separator + 1));
|
|
389
|
+
}
|
|
390
|
+
return argv;
|
|
391
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { EXPECTED_TOOLS } from "./test-support.js";
|
|
7
|
+
import { TOOLS, imageRemoveParameters, toolHandlers } from "./index.js";
|
|
8
|
+
test("tool set covers the image and container surface", () => {
|
|
9
|
+
assert.deepEqual(TOOLS.map((tool) => tool.name).sort(), [...EXPECTED_TOOLS].sort());
|
|
10
|
+
});
|
|
11
|
+
test("image_build requires imageId, parent and packages", () => {
|
|
12
|
+
const tool = TOOLS.find((entry) => entry.name === "image_build");
|
|
13
|
+
assert.ok(tool, "image_build registered");
|
|
14
|
+
assert.equal(tool.approval, true, "image_build must require approval");
|
|
15
|
+
assert.deepEqual(tool.parameters.required, ["imageId", "parent", "packages"]);
|
|
16
|
+
assert.equal(tool.parameters.properties.imageId.description, "Image short name.");
|
|
17
|
+
assert.ok(tool.parameters.properties.parent.description.includes("Short name of the parent image"), "parent must be documented as a short name");
|
|
18
|
+
});
|
|
19
|
+
test("image_remove is registered, requires approval and requires imageId", () => {
|
|
20
|
+
const tool = TOOLS.find((entry) => entry.name === "image_remove");
|
|
21
|
+
assert.ok(tool, "image_remove registered");
|
|
22
|
+
assert.equal(tool.approval, true, "image_remove must require approval");
|
|
23
|
+
assert.equal(tool.parameters.type, "object", "image_remove type");
|
|
24
|
+
assert.equal(typeof tool.parameters.properties, "object");
|
|
25
|
+
assert.deepEqual(tool.parameters.required, ["imageId"]);
|
|
26
|
+
assert.equal(tool.parameters.properties.imageId.type, "string");
|
|
27
|
+
assert.deepEqual(imageRemoveParameters, tool.parameters);
|
|
28
|
+
});
|
|
29
|
+
test("image_list returns image objects", async () => {
|
|
30
|
+
const resolver = {
|
|
31
|
+
control: async () => ({
|
|
32
|
+
images: [
|
|
33
|
+
{
|
|
34
|
+
imageId: "archlinux",
|
|
35
|
+
isBase: true,
|
|
36
|
+
status: "built",
|
|
37
|
+
primitive: "docker.io/library/archlinux:latest",
|
|
38
|
+
packageManager: "pacman",
|
|
39
|
+
basePublic: false,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
imageId: "dev",
|
|
43
|
+
parent: "ubuntu",
|
|
44
|
+
status: "built",
|
|
45
|
+
packages: ["git"],
|
|
46
|
+
packageManager: "apt",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
const out = await toolHandlers.image_list(resolver, {}, {});
|
|
52
|
+
assert.deepEqual(out, [
|
|
53
|
+
{
|
|
54
|
+
imageId: "archlinux",
|
|
55
|
+
isBase: true,
|
|
56
|
+
status: "built",
|
|
57
|
+
primitive: "docker.io/library/archlinux:latest",
|
|
58
|
+
packageManager: "pacman",
|
|
59
|
+
basePublic: false,
|
|
60
|
+
packages: [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
imageId: "dev",
|
|
64
|
+
parent: "ubuntu",
|
|
65
|
+
isBase: false,
|
|
66
|
+
status: "built",
|
|
67
|
+
packageManager: "apt",
|
|
68
|
+
basePublic: false,
|
|
69
|
+
packages: ["git"],
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
test("image_rebuild_all returns rebuilt and skipped arrays", async () => {
|
|
74
|
+
const resolver = {
|
|
75
|
+
control: async () => ({ rebuilt: ["archlinux", "dev"], skipped: ["broken"] }),
|
|
76
|
+
};
|
|
77
|
+
const out = await toolHandlers.image_rebuild_all(resolver, {}, {});
|
|
78
|
+
assert.deepEqual(out, { rebuilt: ["archlinux", "dev"], skipped: ["broken"] });
|
|
79
|
+
});
|