@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,546 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { bytesText, currentCwd, fsError, guestCwd, outputLines, resolveToolBinding, runExec, sessionWorkspaceSlug, sliceLines, unaryGuest, } from "./guest-rpc.js";
|
|
5
|
+
import { inferMountKind, mountsFromInput, projectMountDestinationReason } from "./mount-input.js";
|
|
6
|
+
import { publicContainer, publicDaemon, publicImage, publicMount } from "./public.js";
|
|
7
|
+
import { grpc } from "./grpc/runtime-client.js";
|
|
8
|
+
import { defaultMountMode, mountKindToProto, mountModeToProto } from "./mount-enums.js";
|
|
9
|
+
import { containerNotFound, } from "./workspace-binding.js";
|
|
10
|
+
// Cap on paths `container_glob` returns, matching the built-in glob tool's
|
|
11
|
+
// default result limit; `--sort=modified` makes the retained head the newest.
|
|
12
|
+
const GLOB_MAX_RESULTS = 100;
|
|
13
|
+
// VCS metadata directories ripgrep must never descend into for a discovery
|
|
14
|
+
// listing (`--no-ignore --hidden` would otherwise surface them).
|
|
15
|
+
const GLOB_VCS_EXCLUDES = [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"];
|
|
16
|
+
// failOnSearchError turns a ripgrep failure into a tool error: exit code 2 means
|
|
17
|
+
// ripgrep rejected the pattern or could not read the tree, and returning an
|
|
18
|
+
// empty result would present that as a successful search.
|
|
19
|
+
function failOnSearchError(tool, result) {
|
|
20
|
+
if (result.exitCode !== 2)
|
|
21
|
+
return;
|
|
22
|
+
const detail = result.stderr.trim();
|
|
23
|
+
throw new Error(detail === "" ? `${tool} search failed (ripgrep exit code 2)` : detail);
|
|
24
|
+
}
|
|
25
|
+
// The terminal environment the harness's shell executors impose on every
|
|
26
|
+
// command (`ENV_OVERRIDES` in dsh-bash-local), so a container command sees the
|
|
27
|
+
// same non-interactive terminal as the built-in `bash`.
|
|
28
|
+
const ENV_OVERRIDES = {
|
|
29
|
+
NO_COLOR: "1",
|
|
30
|
+
TERM: "dumb",
|
|
31
|
+
PAGER: "cat",
|
|
32
|
+
GIT_PAGER: "cat",
|
|
33
|
+
};
|
|
34
|
+
// managedEnv layers a container command's environment exactly like the
|
|
35
|
+
// harness's shell executors: the terminal overrides first, then the caller's
|
|
36
|
+
// entries, then the managed `DSH_*` snapshot last, so a caller cannot displace
|
|
37
|
+
// a harness-owned fact. Without the snapshot, `container_bash` would see a
|
|
38
|
+
// different command-visible environment than the built-in `bash`, which
|
|
39
|
+
// collects it through `ctx.shellEnv`.
|
|
40
|
+
function managedEnv(ctx, exec, env) {
|
|
41
|
+
const managed = ctx?.get?.("shellEnv")?.collect?.(exec) ?? {};
|
|
42
|
+
return { ...ENV_OVERRIDES, ...env, ...managed };
|
|
43
|
+
}
|
|
44
|
+
// containerTarget resolves a container-scoped file target through the plugin's
|
|
45
|
+
// own filesystem provider, so the container file tools share the harness's file
|
|
46
|
+
// contracts (the read-before-write guard, the binary rejection, and the same
|
|
47
|
+
// diagnostics) instead of calling the guest directly.
|
|
48
|
+
async function containerTarget(ctx, path, container, exec) {
|
|
49
|
+
return await ctx.fs.resolve(path, {
|
|
50
|
+
cwd: currentCwd(exec),
|
|
51
|
+
container: container ?? "",
|
|
52
|
+
signal: exec?.signal,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// identityFromInput validates the optional uid/gid/groups identity a tool
|
|
56
|
+
// accepts and returns it, or undefined when none was requested. Values are
|
|
57
|
+
// numeric only: the container's /etc/passwd is on the read-only rootfs, so
|
|
58
|
+
// there are no names to resolve.
|
|
59
|
+
function identityFromInput(input) {
|
|
60
|
+
const identity = {};
|
|
61
|
+
if (input.uid !== undefined) {
|
|
62
|
+
if (!Number.isInteger(input.uid) || input.uid < 0) {
|
|
63
|
+
throw new Error("uid must be an integer >= 0");
|
|
64
|
+
}
|
|
65
|
+
identity.uid = input.uid;
|
|
66
|
+
}
|
|
67
|
+
if (input.gid !== undefined) {
|
|
68
|
+
if (!Number.isInteger(input.gid) || input.gid < 0) {
|
|
69
|
+
throw new Error("gid must be an integer >= 0");
|
|
70
|
+
}
|
|
71
|
+
identity.gid = input.gid;
|
|
72
|
+
}
|
|
73
|
+
if (input.groups !== undefined) {
|
|
74
|
+
if (!Array.isArray(input.groups) ||
|
|
75
|
+
input.groups.some((group) => !Number.isInteger(group) || group < 0)) {
|
|
76
|
+
throw new Error("groups must be an array of integers >= 0");
|
|
77
|
+
}
|
|
78
|
+
identity.groups = input.groups;
|
|
79
|
+
}
|
|
80
|
+
return identity.uid === undefined &&
|
|
81
|
+
identity.gid === undefined &&
|
|
82
|
+
identity.groups === undefined
|
|
83
|
+
? undefined
|
|
84
|
+
: identity;
|
|
85
|
+
}
|
|
86
|
+
export const toolHandlers = {
|
|
87
|
+
image_list: async (resolver) => {
|
|
88
|
+
const result = await resolver.control("listImages", {});
|
|
89
|
+
return (result.images ?? []).map(publicImage);
|
|
90
|
+
},
|
|
91
|
+
image_get: async (resolver, input) => publicImage(await resolver.control("getImage", { imageId: input.imageId })),
|
|
92
|
+
image_build: async (resolver, input) => publicImage(await resolver.control("buildImage", {
|
|
93
|
+
imageId: input.imageId,
|
|
94
|
+
parent: input.parent,
|
|
95
|
+
packages: input.packages,
|
|
96
|
+
})),
|
|
97
|
+
image_rebuild: async (resolver, input) => publicImage(await resolver.control("rebuildImage", { imageId: input.imageId })),
|
|
98
|
+
image_rebuild_all: async (resolver) => {
|
|
99
|
+
const result = await resolver.control("rebuildAllImages", {});
|
|
100
|
+
return {
|
|
101
|
+
rebuilt: result.rebuilt ?? [],
|
|
102
|
+
skipped: result.skipped ?? [],
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
image_remove: async (resolver, input) => {
|
|
106
|
+
await resolver.control("removeImage", { imageId: input.imageId });
|
|
107
|
+
return { removed: input.imageId };
|
|
108
|
+
},
|
|
109
|
+
container_list: async (resolver, _input, exec) => {
|
|
110
|
+
const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
|
|
111
|
+
const result = await resolver.control("listContainers", {});
|
|
112
|
+
const rows = (result.containers ?? []).filter((row) => row.workspaceSlug === slug);
|
|
113
|
+
const defaultRow = rows.find((row) => row.containerName === "default");
|
|
114
|
+
const listed = defaultRow === undefined
|
|
115
|
+
? [{ containerName: "default", status: "not started" }]
|
|
116
|
+
: [publicContainer(defaultRow)];
|
|
117
|
+
return [
|
|
118
|
+
...listed,
|
|
119
|
+
...rows
|
|
120
|
+
.filter((row) => row.containerName !== "default")
|
|
121
|
+
.map(publicContainer),
|
|
122
|
+
];
|
|
123
|
+
},
|
|
124
|
+
container_start: async (resolver, input, exec) => {
|
|
125
|
+
const mounts = mountsFromInput(input.mounts, resolver.getConfig().projectsRoot);
|
|
126
|
+
const row = await resolver.control("startContainer", {
|
|
127
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
128
|
+
container: input.container,
|
|
129
|
+
imageId: input.image,
|
|
130
|
+
...(mounts === undefined ? {} : { mounts }),
|
|
131
|
+
...(input.env !== undefined ? { env: input.env } : {}),
|
|
132
|
+
...(input.secretEnv !== undefined ? { secretEnv: input.secretEnv } : {}),
|
|
133
|
+
...(input.paths !== undefined ? { paths: input.paths } : {}),
|
|
134
|
+
});
|
|
135
|
+
return publicContainer(row);
|
|
136
|
+
},
|
|
137
|
+
container_recreate: async (resolver, input, exec) => {
|
|
138
|
+
const mounts = mountsFromInput(input.mounts, resolver.getConfig().projectsRoot);
|
|
139
|
+
const row = await resolver.control("recreateContainer", {
|
|
140
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
141
|
+
container: input.container,
|
|
142
|
+
imageId: input.image ?? "",
|
|
143
|
+
...(mounts === undefined ? {} : { mounts }),
|
|
144
|
+
...(input.env !== undefined ? { env: input.env } : {}),
|
|
145
|
+
...(input.secretEnv !== undefined ? { secretEnv: input.secretEnv } : {}),
|
|
146
|
+
...(input.paths !== undefined ? { paths: input.paths } : {}),
|
|
147
|
+
});
|
|
148
|
+
return publicContainer(row);
|
|
149
|
+
},
|
|
150
|
+
container_remove: async (resolver, input, exec) => {
|
|
151
|
+
await resolver.control("removeContainer", {
|
|
152
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
153
|
+
container: input.container,
|
|
154
|
+
});
|
|
155
|
+
return { removed: input.container };
|
|
156
|
+
},
|
|
157
|
+
container_bash: async (resolver, input, exec, ctx) => {
|
|
158
|
+
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));
|
|
161
|
+
},
|
|
162
|
+
container_exec: async (resolver, input, exec, ctx) => {
|
|
163
|
+
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));
|
|
166
|
+
},
|
|
167
|
+
container_read: async (resolver, input, exec, ctx) => {
|
|
168
|
+
const target = await containerTarget(ctx, input.file_path, input.container, exec);
|
|
169
|
+
// Mirror the harness's read tool: stat first so a miss is FS_NOT_FOUND and
|
|
170
|
+
// a directory is FS_NOT_REGULAR_FILE, then record the read. The observation
|
|
171
|
+
// is what satisfies the read-before-write guard for a later
|
|
172
|
+
// container_write/container_edit on the same path.
|
|
173
|
+
const info = await ctx.fs.stat(target, exec?.signal);
|
|
174
|
+
if (info === undefined) {
|
|
175
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
176
|
+
throw fsError("FS_NOT_FOUND", `cannot read "${target.displayPath}": not found`);
|
|
177
|
+
}
|
|
178
|
+
if (info.type !== "file") {
|
|
179
|
+
throw fsError("FS_NOT_REGULAR_FILE", `cannot read "${target.displayPath}": not a regular file`);
|
|
180
|
+
}
|
|
181
|
+
const content = await ctx.fs.readText(target, exec?.signal);
|
|
182
|
+
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
183
|
+
return sliceLines(content, input.offset, input.limit);
|
|
184
|
+
},
|
|
185
|
+
container_write: async (resolver, input, exec, ctx) => {
|
|
186
|
+
const target = await containerTarget(ctx, input.file_path, input.container, exec);
|
|
187
|
+
// The harness's write tool computes the intent through this waterfall and
|
|
188
|
+
// the provider enforces it, so the container tool shares the same
|
|
189
|
+
// read-before-write guard and diagnostics.
|
|
190
|
+
const intent = await ctx.waterfall("fs/write-intent", target, exec, () => undefined);
|
|
191
|
+
const content = String(input.content ?? "");
|
|
192
|
+
const outcome = await ctx.fs.writeText(target, content, intent, exec?.signal);
|
|
193
|
+
ctx.emit("fs/observed", target, { kind: "present", version: outcome.version }, exec);
|
|
194
|
+
return { bytesWritten: Buffer.byteLength(content, "utf8") };
|
|
195
|
+
},
|
|
196
|
+
container_edit: async (resolver, input, exec, ctx) => {
|
|
197
|
+
const target = await containerTarget(ctx, input.file_path, input.container, exec);
|
|
198
|
+
const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => undefined);
|
|
199
|
+
const outcome = await ctx.fs.editText(target, {
|
|
200
|
+
oldString: input.old_string,
|
|
201
|
+
newString: input.new_string,
|
|
202
|
+
replaceAll: input.replace_all === true,
|
|
203
|
+
}, intent, exec?.signal);
|
|
204
|
+
ctx.emit("fs/observed", target, { kind: "present", version: outcome.version }, exec);
|
|
205
|
+
return { before: outcome.before, after: outcome.after };
|
|
206
|
+
},
|
|
207
|
+
container_glob: async (resolver, input, exec) => {
|
|
208
|
+
const sessionCwd = currentCwd(exec);
|
|
209
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
210
|
+
const argv = [
|
|
211
|
+
"rg",
|
|
212
|
+
"--files",
|
|
213
|
+
`--glob=${input.pattern}`,
|
|
214
|
+
"--sort=modified",
|
|
215
|
+
"--no-ignore",
|
|
216
|
+
"--hidden",
|
|
217
|
+
...GLOB_VCS_EXCLUDES.flatMap((name) => [
|
|
218
|
+
`--glob=!**/${name}`,
|
|
219
|
+
`--glob=!**/${name}/**`,
|
|
220
|
+
]),
|
|
221
|
+
];
|
|
222
|
+
const result = await runExec(binding, argv, guestCwd(input.path, sessionCwd, binding));
|
|
223
|
+
failOnSearchError("glob", result);
|
|
224
|
+
const files = outputLines(result.stdout);
|
|
225
|
+
const capped = files.length > GLOB_MAX_RESULTS;
|
|
226
|
+
return {
|
|
227
|
+
files: capped ? files.slice(0, GLOB_MAX_RESULTS) : files,
|
|
228
|
+
...(capped
|
|
229
|
+
? {
|
|
230
|
+
note: `showing ${GLOB_MAX_RESULTS} of ${files.length} files in modification-time order; narrow pattern or path to see more`,
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
container_grep: async (resolver, input, exec) => {
|
|
236
|
+
const sessionCwd = currentCwd(exec);
|
|
237
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
238
|
+
const cwd = guestCwd(undefined, sessionCwd, binding);
|
|
239
|
+
// The search root: the requested path, else the container's default (the
|
|
240
|
+
// session workspace when it is mounted). Ripgrep reads stdin instead of the
|
|
241
|
+
// working directory when it is given no path, so it is always explicit.
|
|
242
|
+
const root = guestCwd(input.path, sessionCwd, binding) ?? cwd;
|
|
243
|
+
const argv = ["rg", "-n"];
|
|
244
|
+
if (typeof input.include === "string" && input.include !== "") {
|
|
245
|
+
argv.push("--glob", input.include);
|
|
246
|
+
}
|
|
247
|
+
argv.push(input.pattern);
|
|
248
|
+
if (root !== undefined)
|
|
249
|
+
argv.push(root);
|
|
250
|
+
const result = await runExec(binding, argv, cwd);
|
|
251
|
+
failOnSearchError("grep", result);
|
|
252
|
+
return {
|
|
253
|
+
matches: outputLines(result.stdout),
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
container_mount_list: async (resolver, input, exec) => {
|
|
257
|
+
const slug = await sessionWorkspaceSlug(resolver, currentCwd(exec));
|
|
258
|
+
const result = await resolver.control("listContainers", {});
|
|
259
|
+
const row = (result.containers ?? []).find((candidate) => candidate.workspaceSlug === slug &&
|
|
260
|
+
candidate.containerName === input.container);
|
|
261
|
+
if (row === undefined) {
|
|
262
|
+
throw containerNotFound(input.container, slug);
|
|
263
|
+
}
|
|
264
|
+
const mounts = row.mounts ?? [];
|
|
265
|
+
return mounts.map(publicMount);
|
|
266
|
+
},
|
|
267
|
+
container_mount_add: async (resolver, input, exec) => {
|
|
268
|
+
const kind = inferMountKind(input);
|
|
269
|
+
const projectsRoot = resolver.getConfig().projectsRoot;
|
|
270
|
+
const destinationReason = projectMountDestinationReason(projectsRoot, input);
|
|
271
|
+
if (destinationReason !== undefined)
|
|
272
|
+
throw new Error(destinationReason);
|
|
273
|
+
if (kind === "secret" && input.mode === "read_write") {
|
|
274
|
+
throw new Error("secret mounts are read-only; omit mode or use read_only");
|
|
275
|
+
}
|
|
276
|
+
const protoKind = mountKindToProto(kind);
|
|
277
|
+
const mode = mountModeToProto(input.mode ?? defaultMountMode(kind));
|
|
278
|
+
const request = {
|
|
279
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
280
|
+
container: input.container,
|
|
281
|
+
kind: protoKind,
|
|
282
|
+
};
|
|
283
|
+
if (kind === "volume") {
|
|
284
|
+
request.volume = input.volume;
|
|
285
|
+
request.destination = input.destination;
|
|
286
|
+
request.mode = mode;
|
|
287
|
+
}
|
|
288
|
+
else if (kind === "tmpfs") {
|
|
289
|
+
request.destination = input.destination;
|
|
290
|
+
request.mode = mode;
|
|
291
|
+
}
|
|
292
|
+
else if (kind === "secret") {
|
|
293
|
+
request.secret = input.secret;
|
|
294
|
+
request.destination = input.destination;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
request.project = input.project;
|
|
298
|
+
request.mode = mode;
|
|
299
|
+
}
|
|
300
|
+
const row = await resolver.control("addContainerMount", request);
|
|
301
|
+
return publicContainer(row);
|
|
302
|
+
},
|
|
303
|
+
container_mount_remove: async (resolver, input, exec) => {
|
|
304
|
+
const kind = inferMountKind(input);
|
|
305
|
+
const protoKind = mountKindToProto(kind);
|
|
306
|
+
const request = {
|
|
307
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
308
|
+
container: input.container,
|
|
309
|
+
kind: protoKind,
|
|
310
|
+
};
|
|
311
|
+
if (kind === "project") {
|
|
312
|
+
request.project = input.project;
|
|
313
|
+
}
|
|
314
|
+
else if (kind === "volume") {
|
|
315
|
+
if (input.volume !== undefined)
|
|
316
|
+
request.volume = input.volume;
|
|
317
|
+
}
|
|
318
|
+
else if (kind === "secret") {
|
|
319
|
+
if (input.secret !== undefined)
|
|
320
|
+
request.secret = input.secret;
|
|
321
|
+
}
|
|
322
|
+
if (kind !== "project" && input.destination !== undefined) {
|
|
323
|
+
request.destination = input.destination;
|
|
324
|
+
}
|
|
325
|
+
const row = await resolver.control("removeContainerMount", request);
|
|
326
|
+
return publicContainer(row);
|
|
327
|
+
},
|
|
328
|
+
container_mount_update: async (resolver, input, exec) => {
|
|
329
|
+
const kind = inferMountKind(input);
|
|
330
|
+
if (kind !== "project" && kind !== "volume") {
|
|
331
|
+
throw new Error(`only project and volume mounts carry a mode; ${kind} mounts cannot be remounted`);
|
|
332
|
+
}
|
|
333
|
+
const projectsRoot = resolver.getConfig().projectsRoot;
|
|
334
|
+
const destinationReason = projectMountDestinationReason(projectsRoot, input);
|
|
335
|
+
if (destinationReason !== undefined)
|
|
336
|
+
throw new Error(destinationReason);
|
|
337
|
+
const request = {
|
|
338
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
339
|
+
container: input.container,
|
|
340
|
+
kind: mountKindToProto(kind),
|
|
341
|
+
mode: mountModeToProto(input.mode),
|
|
342
|
+
};
|
|
343
|
+
if (kind === "project") {
|
|
344
|
+
request.project = input.project;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
if (input.volume !== undefined)
|
|
348
|
+
request.volume = input.volume;
|
|
349
|
+
if (input.destination !== undefined) {
|
|
350
|
+
request.destination = input.destination;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const row = await resolver.control("updateContainerMount", request);
|
|
354
|
+
return publicContainer(row);
|
|
355
|
+
},
|
|
356
|
+
container_path_set: async (resolver, input, exec) => {
|
|
357
|
+
const sessionCwd = currentCwd(exec);
|
|
358
|
+
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
359
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
360
|
+
const paths = Array.isArray(input.paths) ? input.paths.map(String) : [];
|
|
361
|
+
return {
|
|
362
|
+
paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
container_path_add: async (resolver, input, exec) => {
|
|
366
|
+
const sessionCwd = currentCwd(exec);
|
|
367
|
+
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
368
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
369
|
+
const current = await containerPathState(binding);
|
|
370
|
+
// Prepending gives the new path the highest priority; an existing entry is
|
|
371
|
+
// moved to the front rather than duplicated.
|
|
372
|
+
const paths = [
|
|
373
|
+
input.path,
|
|
374
|
+
...current.paths.filter((path) => path !== input.path),
|
|
375
|
+
];
|
|
376
|
+
return {
|
|
377
|
+
paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
container_path_remove: async (resolver, input, exec) => {
|
|
381
|
+
const sessionCwd = currentCwd(exec);
|
|
382
|
+
const slug = await sessionWorkspaceSlug(resolver, sessionCwd);
|
|
383
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
384
|
+
const current = await containerPathState(binding);
|
|
385
|
+
if (!current.paths.includes(input.path)) {
|
|
386
|
+
if (current.defaultPaths.includes(input.path)) {
|
|
387
|
+
throw new Error(`path ${JSON.stringify(input.path)} is part of the container's default PATH and cannot be removed`);
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`path ${JSON.stringify(input.path)} is not an added path`);
|
|
390
|
+
}
|
|
391
|
+
const paths = current.paths.filter((path) => path !== input.path);
|
|
392
|
+
return {
|
|
393
|
+
paths: await applyContainerPaths(resolver, binding, slug, input.container, paths),
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
volume_list: async (resolver) => {
|
|
397
|
+
const result = await resolver.control("listVolumes", {});
|
|
398
|
+
return (result.volumes ?? []).map((volume) => ({ name: volume.name }));
|
|
399
|
+
},
|
|
400
|
+
volume_create: async (resolver, input) => {
|
|
401
|
+
await resolver.control("createVolume", { name: input.name });
|
|
402
|
+
return { name: input.name };
|
|
403
|
+
},
|
|
404
|
+
volume_remove: async (resolver, input) => {
|
|
405
|
+
await resolver.control("removeVolume", { name: input.name });
|
|
406
|
+
return { removed: input.name };
|
|
407
|
+
},
|
|
408
|
+
secret_list: async (resolver) => {
|
|
409
|
+
const result = await resolver.control("listSecrets", {});
|
|
410
|
+
return (result.secrets ?? []).map((secret) => ({ name: secret.name }));
|
|
411
|
+
},
|
|
412
|
+
secret_create: async (resolver, input) => {
|
|
413
|
+
await resolver.control("createSecret", {
|
|
414
|
+
name: input.name,
|
|
415
|
+
...(input.length ? { length: input.length } : {}),
|
|
416
|
+
...(input.charset ? { charset: input.charset } : {}),
|
|
417
|
+
});
|
|
418
|
+
return { name: input.name };
|
|
419
|
+
},
|
|
420
|
+
secret_remove: async (resolver, input) => {
|
|
421
|
+
await resolver.control("removeSecret", { name: input.name });
|
|
422
|
+
return { removed: input.name };
|
|
423
|
+
},
|
|
424
|
+
container_secret_add: async (resolver, input, exec) => {
|
|
425
|
+
const row = await resolver.control("addContainerSecret", {
|
|
426
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
427
|
+
container: input.container,
|
|
428
|
+
env: input.env,
|
|
429
|
+
secret: input.secret,
|
|
430
|
+
});
|
|
431
|
+
return publicContainer(row);
|
|
432
|
+
},
|
|
433
|
+
container_secret_remove: async (resolver, input, exec) => {
|
|
434
|
+
const row = await resolver.control("removeContainerSecret", {
|
|
435
|
+
workspaceSlug: await sessionWorkspaceSlug(resolver, currentCwd(exec)),
|
|
436
|
+
container: input.container,
|
|
437
|
+
env: input.env,
|
|
438
|
+
});
|
|
439
|
+
return publicContainer(row);
|
|
440
|
+
},
|
|
441
|
+
daemon_start: async (resolver, input, exec) => {
|
|
442
|
+
const sessionCwd = currentCwd(exec);
|
|
443
|
+
const binding = await resolveToolBinding(resolver, sessionCwd, input.container);
|
|
444
|
+
const request = {
|
|
445
|
+
argv: input.argv,
|
|
446
|
+
inheritEnv: { value: input.inheritEnv !== false },
|
|
447
|
+
};
|
|
448
|
+
if (input.name !== undefined)
|
|
449
|
+
request.name = input.name;
|
|
450
|
+
const cwd = guestCwd(input.cwd, sessionCwd, binding);
|
|
451
|
+
if (cwd !== undefined)
|
|
452
|
+
request.cwd = cwd;
|
|
453
|
+
if (input.env !== undefined)
|
|
454
|
+
request.env = input.env;
|
|
455
|
+
const identity = identityFromInput(input);
|
|
456
|
+
if (identity?.uid !== undefined)
|
|
457
|
+
request.uid = { value: identity.uid };
|
|
458
|
+
if (identity?.gid !== undefined)
|
|
459
|
+
request.gid = { value: identity.gid };
|
|
460
|
+
if (identity?.groups !== undefined)
|
|
461
|
+
request.groups = identity.groups;
|
|
462
|
+
const info = await unaryGuest({ binding }, "startDaemon", request);
|
|
463
|
+
return publicDaemon(info);
|
|
464
|
+
},
|
|
465
|
+
daemon_list: async (resolver, input, exec) => {
|
|
466
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
467
|
+
const result = (await unaryGuest({ binding }, "listDaemons", {}));
|
|
468
|
+
return (result.daemons ?? []).map(publicDaemon);
|
|
469
|
+
},
|
|
470
|
+
daemon_stop: async (resolver, input, exec) => {
|
|
471
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
472
|
+
await daemonCall(input.name, () => unaryGuest({ binding }, "stopDaemon", {
|
|
473
|
+
name: input.name,
|
|
474
|
+
signal: input.signal,
|
|
475
|
+
}));
|
|
476
|
+
return { stopped: input.name };
|
|
477
|
+
},
|
|
478
|
+
daemon_restart: async (resolver, input, exec) => {
|
|
479
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
480
|
+
const info = await daemonCall(input.name, () => unaryGuest({ binding }, "restartDaemon", { name: input.name }));
|
|
481
|
+
return publicDaemon(info);
|
|
482
|
+
},
|
|
483
|
+
daemon_logs: async (resolver, input, exec) => {
|
|
484
|
+
const binding = await resolveToolBinding(resolver, currentCwd(exec), input.container);
|
|
485
|
+
const result = (await daemonCall(input.name, () => unaryGuest({ binding }, "daemonLogs", {
|
|
486
|
+
name: input.name,
|
|
487
|
+
tailBytes: input.tailBytes,
|
|
488
|
+
})));
|
|
489
|
+
return {
|
|
490
|
+
stdout: bytesText(result.stdout),
|
|
491
|
+
stderr: bytesText(result.stderr),
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
function daemonNotFound(name) {
|
|
496
|
+
const error = new Error(`daemon ${JSON.stringify(name)} not found`);
|
|
497
|
+
error.code = "NOT_FOUND";
|
|
498
|
+
return error;
|
|
499
|
+
}
|
|
500
|
+
// containerPathState reads the guest's live PATH additions and the default PATH
|
|
501
|
+
// they are prepended to.
|
|
502
|
+
async function containerPathState(binding) {
|
|
503
|
+
const result = (await unaryGuest({ binding }, "getPaths", {}));
|
|
504
|
+
const paths = Array.isArray(result?.paths)
|
|
505
|
+
? result.paths.map((path) => String(path))
|
|
506
|
+
: [];
|
|
507
|
+
const defaultPath = typeof result?.defaultPath === "string" ? result.defaultPath : "";
|
|
508
|
+
return {
|
|
509
|
+
paths,
|
|
510
|
+
defaultPaths: defaultPath === "" ? [] : defaultPath.split(":"),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
// applyContainerPaths persists the list on the container and pushes it to the
|
|
514
|
+
// running guest agent, returning the applied (validated, deduplicated) list.
|
|
515
|
+
// The guest holds the live list; the container record is what a recreate
|
|
516
|
+
// restores it from.
|
|
517
|
+
async function applyContainerPaths(resolver, binding, slug, container, paths) {
|
|
518
|
+
const row = (await resolver.control("setContainerPaths", {
|
|
519
|
+
workspaceSlug: slug,
|
|
520
|
+
container,
|
|
521
|
+
paths: [...paths],
|
|
522
|
+
}));
|
|
523
|
+
const persisted = Array.isArray(row?.paths)
|
|
524
|
+
? row.paths.map((path) => String(path))
|
|
525
|
+
: [...paths];
|
|
526
|
+
const result = (await unaryGuest({ binding }, "setPaths", {
|
|
527
|
+
paths: persisted,
|
|
528
|
+
}));
|
|
529
|
+
return Array.isArray(result?.paths)
|
|
530
|
+
? result.paths.map((path) => String(path))
|
|
531
|
+
: persisted;
|
|
532
|
+
}
|
|
533
|
+
// daemonCall maps the guest agent's generic "unknown daemon" to a message that
|
|
534
|
+
// names the daemon the caller asked about.
|
|
535
|
+
async function daemonCall(name, call) {
|
|
536
|
+
try {
|
|
537
|
+
return await call();
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (error.code === grpc.status.NOT_FOUND &&
|
|
541
|
+
/unknown daemon/.test(error.message)) {
|
|
542
|
+
throw daemonNotFound(name);
|
|
543
|
+
}
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|