@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.
Files changed (191) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +14 -2
  3. package/cordis.patch.yml +58 -0
  4. package/dist/approval-reasons.d.ts +3 -4
  5. package/dist/approval-reasons.js +40 -49
  6. package/dist/approval-reasons.test.js +43 -15
  7. package/dist/approval.js +27 -10
  8. package/dist/approval.test.js +165 -34
  9. package/dist/card-route.d.ts +3 -2
  10. package/dist/card-route.js +154 -42
  11. package/dist/card-route.test.js +85 -43
  12. package/dist/card-test-support.js +39 -66
  13. package/dist/client/ContainerCard.d.ts +1 -1
  14. package/dist/client/ContainerCard.js +92 -50
  15. package/dist/client/card-protocol.d.ts +6 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +16 -18
  18. package/dist/client/container-card-controller.js +92 -40
  19. package/dist/client/container-card-create-modal.d.ts +2 -2
  20. package/dist/client/container-card-create-modal.js +13 -5
  21. package/dist/client/container-card-default-env.d.ts +10 -0
  22. package/dist/client/container-card-default-env.js +69 -0
  23. package/dist/client/container-card-directory.d.ts +2 -2
  24. package/dist/client/container-card-directory.js +12 -4
  25. package/dist/client/container-card-editors.d.ts +3 -3
  26. package/dist/client/container-card-editors.js +104 -32
  27. package/dist/client/container-card-images.d.ts +4 -4
  28. package/dist/client/container-card-images.js +19 -6
  29. package/dist/client/container-card-paths.d.ts +3 -3
  30. package/dist/client/container-card-paths.js +18 -11
  31. package/dist/client/container-card-row.d.ts +10 -6
  32. package/dist/client/container-card-row.js +82 -13
  33. package/dist/client/container-card-secrets.d.ts +3 -3
  34. package/dist/client/container-card-secrets.js +8 -6
  35. package/dist/client/container-card-shared.d.ts +4 -17
  36. package/dist/client/container-card-shared.js +9 -25
  37. package/dist/client/container-card-styles.d.ts +2 -8
  38. package/dist/client/container-card-styles.js +14 -52
  39. package/dist/client/container-card-volumes.d.ts +2 -2
  40. package/dist/client/container-card-volumes.js +9 -5
  41. package/dist/client/container-card-workspace.d.ts +8 -6
  42. package/dist/client/container-card-workspace.js +9 -3
  43. package/dist/client/directory-picker.js +2 -1
  44. package/dist/client/index.js +13141 -2036
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +106 -38
  47. package/dist/client/podman-terminal-guide.d.ts +11 -0
  48. package/dist/client/podman-terminal-guide.js +208 -0
  49. package/dist/client/podman-terminal-title.d.ts +7 -0
  50. package/dist/client/podman-terminal-title.js +30 -0
  51. package/dist/client/podman-terminal.d.ts +26 -0
  52. package/dist/client/podman-terminal.js +412 -0
  53. package/dist/client/read-only-approval.d.ts +2 -2
  54. package/dist/client/read-only-approval.js +13 -3
  55. package/dist/client/slot-contract.d.ts +0 -11
  56. package/dist/client/terminal-preference.d.ts +26 -0
  57. package/dist/client/terminal-preference.js +67 -0
  58. package/dist/client/terminal-protocol.d.ts +85 -0
  59. package/dist/client/terminal-protocol.js +14 -0
  60. package/dist/client/terminal-tab.d.ts +60 -0
  61. package/dist/client/terminal-tab.js +35 -0
  62. package/dist/client/terminal-targets.d.ts +33 -0
  63. package/dist/client/terminal-targets.js +65 -0
  64. package/dist/client/terminal-titles.d.ts +23 -0
  65. package/dist/client/terminal-titles.js +52 -0
  66. package/dist/client/terminal-transport.d.ts +63 -0
  67. package/dist/client/terminal-transport.js +207 -0
  68. package/dist/client/tool-views.js +215 -49
  69. package/dist/container-env.d.ts +11 -0
  70. package/dist/container-env.js +96 -0
  71. package/dist/container-env.test.d.ts +1 -0
  72. package/dist/container-env.test.js +93 -0
  73. package/dist/containers.test.js +196 -18
  74. package/dist/daemons.test.js +18 -4
  75. package/dist/dsh-version.d.ts +1 -0
  76. package/dist/dsh-version.js +46 -0
  77. package/dist/env-rows.d.ts +14 -0
  78. package/dist/env-rows.js +52 -0
  79. package/dist/env-rows.test.d.ts +1 -0
  80. package/dist/env-rows.test.js +43 -0
  81. package/dist/fs-provider.d.ts +1 -0
  82. package/dist/fs-provider.js +26 -12
  83. package/dist/fs-provider.test.js +51 -10
  84. package/dist/generated/version.d.ts +2 -2
  85. package/dist/generated/version.js +2 -2
  86. package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
  87. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  88. package/dist/grpc/runtime-client.d.ts +3 -1
  89. package/dist/grpc/runtime-client.js +90 -17
  90. package/dist/guest-rpc.d.ts +15 -19
  91. package/dist/guest-rpc.js +289 -70
  92. package/dist/guest-rpc.test.d.ts +1 -0
  93. package/dist/guest-rpc.test.js +241 -0
  94. package/dist/guest-terminal.d.ts +30 -0
  95. package/dist/guest-terminal.js +196 -0
  96. package/dist/images.test.js +10 -3
  97. package/dist/index.d.ts +11 -16
  98. package/dist/index.js +42 -36
  99. package/dist/locales.test.d.ts +1 -0
  100. package/dist/locales.test.js +34 -0
  101. package/dist/misc.test.js +38 -10
  102. package/dist/mount-enums.js +2 -1
  103. package/dist/mount-input.d.ts +2 -0
  104. package/dist/mount-input.js +38 -22
  105. package/dist/mounts.test.js +81 -11
  106. package/dist/naming.test.d.ts +1 -0
  107. package/dist/naming.test.js +28 -0
  108. package/dist/output-reader.js +36 -4
  109. package/dist/package-deps.test.d.ts +1 -0
  110. package/dist/package-deps.test.js +49 -0
  111. package/dist/paths.test.js +4 -2
  112. package/dist/plugin-meta.test.d.ts +1 -0
  113. package/dist/plugin-meta.test.js +29 -0
  114. package/dist/project-path.js +33 -7
  115. package/dist/project-path.test.js +14 -0
  116. package/dist/prompts.d.ts +0 -4
  117. package/dist/prompts.js +15 -84
  118. package/dist/read-only-shell.js +3 -1
  119. package/dist/read-only-shell.test.js +80 -25
  120. package/dist/runtime-client.test.d.ts +1 -0
  121. package/dist/runtime-client.test.js +53 -0
  122. package/dist/secrets.test.js +21 -6
  123. package/dist/settings-commands.test.js +61 -14
  124. package/dist/settings-create.test.js +32 -10
  125. package/dist/settings-default-env.test.d.ts +1 -0
  126. package/dist/settings-default-env.test.js +175 -0
  127. package/dist/settings-mounts.test.js +46 -7
  128. package/dist/settings-schema.d.ts +6 -11
  129. package/dist/settings-schema.js +4 -8
  130. package/dist/settings-schema.test.d.ts +1 -0
  131. package/dist/settings-schema.test.js +35 -0
  132. package/dist/settings-workspaces.test.js +38 -63
  133. package/dist/spill-store.d.ts +1 -0
  134. package/dist/spill-store.js +17 -4
  135. package/dist/spill-store.test.js +31 -8
  136. package/dist/subprocess.d.ts +4 -0
  137. package/dist/subprocess.js +148 -162
  138. package/dist/subprocess.test.js +118 -8
  139. package/dist/terminal-preference.test.d.ts +1 -0
  140. package/dist/terminal-preference.test.js +62 -0
  141. package/dist/terminal-route.d.ts +10 -0
  142. package/dist/terminal-route.js +270 -0
  143. package/dist/terminal-route.test.d.ts +1 -0
  144. package/dist/terminal-route.test.js +265 -0
  145. package/dist/terminal-sessions.d.ts +69 -0
  146. package/dist/terminal-sessions.js +252 -0
  147. package/dist/terminal-shells.d.ts +22 -0
  148. package/dist/terminal-shells.js +99 -0
  149. package/dist/terminal-tab.test.d.ts +1 -0
  150. package/dist/terminal-tab.test.js +58 -0
  151. package/dist/terminal-targets.test.d.ts +1 -0
  152. package/dist/terminal-targets.test.js +43 -0
  153. package/dist/terminal-titles.test.d.ts +1 -0
  154. package/dist/terminal-titles.test.js +36 -0
  155. package/dist/test-support.d.ts +35 -5
  156. package/dist/test-support.js +91 -29
  157. package/dist/tool-defs.js +19 -4
  158. package/dist/tool-handlers.js +80 -28
  159. package/dist/tool-params.js +19 -5
  160. package/dist/version-compat.d.ts +4 -0
  161. package/dist/version-compat.js +29 -0
  162. package/dist/version-compat.test.d.ts +1 -0
  163. package/dist/version-compat.test.js +27 -0
  164. package/dist/volumes.test.js +1 -1
  165. package/dist/workspace-binding.d.ts +19 -10
  166. package/dist/workspace-binding.js +252 -78
  167. package/dist/workspace-binding.test.js +202 -27
  168. package/docs/architecture.md +71 -20
  169. package/docs/architecture.zh.md +62 -12
  170. package/docs/configuration.md +61 -20
  171. package/docs/configuration.zh.md +53 -19
  172. package/docs/development-prompt.md +72 -0
  173. package/docs/development-prompt.zh.md +69 -0
  174. package/docs/development.md +184 -29
  175. package/docs/development.zh.md +165 -26
  176. package/docs/install-dsh-and-dsh-podman.md +24 -21
  177. package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
  178. package/docs/uninstall.md +83 -0
  179. package/docs/uninstall.zh.md +79 -0
  180. package/docs/update.md +120 -0
  181. package/docs/update.zh.md +114 -0
  182. package/docs/usage.md +108 -61
  183. package/docs/usage.zh.md +76 -38
  184. package/locale/en.json +6 -0
  185. package/locale/zh.json +6 -0
  186. package/package.json +58 -25
  187. package/quadlet/dsh-podman-orchestrator.container +46 -0
  188. package/quadlet/dsh.container +35 -0
  189. package/LICENSE.pkg +0 -17103
  190. package/dist/preferences.d.ts +0 -6
  191. package/dist/preferences.js +0 -27
@@ -3,7 +3,7 @@
3
3
  // SPDX-License-Identifier: MIT
4
4
  import test from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { editGuest, editProvider, fakeGuest, providerFor, stubResolver } from "./test-support.js";
6
+ import { editGuest, editProvider, fakeGuest, providerFor, stubResolver, } from "./test-support.js";
7
7
  import { createFilesystemProvider } from "./index.js";
8
8
  test("filesystem provider resolves absolute safe paths", async () => {
9
9
  const provider = createFilesystemProvider(stubResolver);
@@ -71,7 +71,9 @@ test("filesystem provider lstat maps entry types without following links", async
71
71
  token: "t",
72
72
  }),
73
73
  });
74
- const info = await provider.lstat("/projects/team/link", { cwd: "/projects/team" });
74
+ const info = await provider.lstat("/projects/team/link", {
75
+ cwd: "/projects/team",
76
+ });
75
77
  assert.equal(info.type, "symlink");
76
78
  assert.equal(info.size, 3);
77
79
  assert.deepEqual(requests, [{ path: "/projects/team/link", noFollow: true }]);
@@ -100,7 +102,8 @@ test("filesystem provider streamText decodes UTF-8 and rejects binary", async ()
100
102
  for await (const _chunk of await binary.streamText(binaryTarget)) {
101
103
  // Drain the stream so the binary sample is inspected.
102
104
  }
103
- }, (error) => error instanceof Error && error.code === "FS_NOT_TEXT");
105
+ }, (error) => error instanceof Error &&
106
+ error.code === "FS_NOT_TEXT");
104
107
  });
105
108
  test("filesystem provider readBytes returns content and caps at maxBytes", async () => {
106
109
  const fake = fakeGuest(Buffer.from("hello"));
@@ -109,7 +112,8 @@ test("filesystem provider readBytes returns content and caps at maxBytes", async
109
112
  const bytes = await provider.readBytes(target, undefined, 10);
110
113
  assert.equal(Buffer.from(bytes).toString(), "hello");
111
114
  assert.equal(fake.requests[0].length, 11);
112
- await assert.rejects(() => provider.readBytes(target, undefined, 3), (error) => error instanceof Error && error.code === "FS_TOO_LARGE");
115
+ await assert.rejects(() => provider.readBytes(target, undefined, 3), (error) => error instanceof Error &&
116
+ error.code === "FS_TOO_LARGE");
113
117
  });
114
118
  test("filesystem provider readByteRange returns the window and empty for zero length", async () => {
115
119
  const fake = fakeGuest(Buffer.from("abcdef"));
@@ -152,7 +156,10 @@ test("filesystem provider writeText reports the harness error codes", async () =
152
156
  const existing = editProvider(editGuest({}, Buffer.from("hello")));
153
157
  const target = await existing.resolve("/a", { cwd: "/x" });
154
158
  await assert.rejects(() => existing.writeText(target, "x", { kind: "createIfAbsent" }), (error) => error.code === "FS_NOT_OBSERVED");
155
- await assert.rejects(() => existing.writeText(target, "x", { kind: "replaceIfVersion", version: "other" }), (error) => error.code === "FS_STALE_VERSION");
159
+ await assert.rejects(() => existing.writeText(target, "x", {
160
+ kind: "replaceIfVersion",
161
+ version: "other",
162
+ }), (error) => error.code === "FS_STALE_VERSION");
156
163
  const dir = editProvider(editGuest({ isDir: true }));
157
164
  const dirTarget = await dir.resolve("/a", { cwd: "/x" });
158
165
  await assert.rejects(() => dir.writeText(dirTarget, "x"), (error) => error.code === "FS_NOT_REGULAR_FILE");
@@ -215,7 +222,9 @@ test("filesystem provider listDir returns resolved child targets", async () => {
215
222
  token: "t",
216
223
  }),
217
224
  });
218
- const target = await provider.resolve("/projects/team", { cwd: "/projects/team" });
225
+ const target = await provider.resolve("/projects/team", {
226
+ cwd: "/projects/team",
227
+ });
219
228
  const entries = await provider.listDir(target);
220
229
  assert.deepEqual(requests, [{ path: "/projects/team" }]);
221
230
  assert.deepEqual(entries.map((entry) => ({
@@ -224,9 +233,24 @@ test("filesystem provider listDir returns resolved child targets", async () => {
224
233
  path: entry.target.displayPath,
225
234
  binding: entry.target.binding.token,
226
235
  })), [
227
- { name: "src", type: "directory", path: "/projects/team/src", binding: "t" },
228
- { name: "a.txt", type: "file", path: "/projects/team/a.txt", binding: "t" },
229
- { name: "link", type: "other", path: "/projects/team/link", binding: "t" },
236
+ {
237
+ name: "src",
238
+ type: "directory",
239
+ path: "/projects/team/src",
240
+ binding: "t",
241
+ },
242
+ {
243
+ name: "a.txt",
244
+ type: "file",
245
+ path: "/projects/team/a.txt",
246
+ binding: "t",
247
+ },
248
+ {
249
+ name: "link",
250
+ type: "other",
251
+ path: "/projects/team/link",
252
+ binding: "t",
253
+ },
230
254
  ]);
231
255
  assert.equal(entries[1].size, 3);
232
256
  assert.equal(entries[0].size, undefined);
@@ -247,7 +271,10 @@ test("filesystem provider listDir falls back to the directory bit", async () =>
247
271
  });
248
272
  const target = await provider.resolve("/w", { cwd: "/w" });
249
273
  const entries = await provider.listDir(target);
250
- assert.deepEqual(entries.map((entry) => entry.type), ["directory", "file"]);
274
+ assert.deepEqual(entries.map((entry) => entry.type), [
275
+ "directory",
276
+ "file",
277
+ ]);
251
278
  });
252
279
  test("filesystem provider writeText normalizes the diff basis", async () => {
253
280
  const binding = editGuest({}, Buffer.from("a\r\nb\r\n"));
@@ -258,3 +285,17 @@ test("filesystem provider writeText normalizes the diff basis", async () => {
258
285
  assert.equal(outcome.after, "c\nd\n");
259
286
  assert.equal(Buffer.concat(binding.writes).toString(), "c\r\nd\r\n");
260
287
  });
288
+ test("filesystem provider reports watching as unsupported", async () => {
289
+ const provider = createFilesystemProvider(stubResolver);
290
+ const target = await provider.resolve("/projects/team/app", { cwd: "/x" });
291
+ let notified = false;
292
+ await assert.rejects(() => provider.watch(target, () => {
293
+ notified = true;
294
+ }, new AbortController().signal), (error) => error.code === "FS_IO_ERROR" &&
295
+ error.message ===
296
+ "Filesystem watching is not supported by this provider.");
297
+ assert.equal(notified, false, "changed must not fire when watching is unsupported");
298
+ const aborted = new AbortController();
299
+ aborted.abort();
300
+ assert.throws(() => provider.watch(target, () => { }, aborted.signal), (error) => error.code === "FS_ABORTED");
301
+ });
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.2.0-rc.3";
2
- export declare const GIT_COMMIT = "f61b296";
1
+ export declare const VERSION = "0.2.0";
2
+ export declare const GIT_COMMIT = "276d50e";
@@ -2,5 +2,5 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
  // Generated by scripts/generate-version.mjs; do not edit.
5
- export const VERSION = "0.2.0-rc.3";
6
- export const GIT_COMMIT = "f61b296";
5
+ export const VERSION = "0.2.0";
6
+ export const GIT_COMMIT = "276d50e";
@@ -7,9 +7,11 @@ package dshctl.v1;
7
7
  option go_package = "github.com/Exagone313/dsh-podman/internal/genproto/dshctl/v1;dshctlv1";
8
8
 
9
9
  service OrchestratorControl {
10
+ rpc GetVersion(GetVersionRequest) returns (GetVersionResponse);
10
11
  rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse);
11
12
  rpc CreateWorkspace(CreateWorkspaceRequest) returns (Workspace);
12
13
  rpc DescribeWorkspace(DescribeWorkspaceRequest) returns (Workspace);
14
+ rpc EnsureContainer(EnsureContainerRequest) returns (Container);
13
15
  rpc ListWorkspaces(ListWorkspacesRequest) returns (ListWorkspacesResponse);
14
16
  rpc RemoveWorkspace(RemoveWorkspaceRequest) returns (RemoveWorkspaceResponse);
15
17
  rpc ListImages(ListImagesRequest) returns (ListImagesResponse);
@@ -41,6 +43,8 @@ service OrchestratorControl {
41
43
  rpc RemoveContainerSecret(RemoveContainerSecretRequest) returns (Container);
42
44
  }
43
45
 
46
+ message GetVersionRequest {}
47
+ message GetVersionResponse { string version = 1; string commit = 2; }
44
48
  message ListProjectsRequest {}
45
49
  message ListProjectsResponse { repeated Project projects = 1; }
46
50
  message Project { string name = 1; string host_path = 2; }
@@ -79,6 +83,11 @@ message ListImagesResponse { repeated Image images = 1; }
79
83
  message Image { string image_id = 1; string parent = 2; repeated string packages = 3; string image_tag = 4; string built_at = 5; bool is_base = 6; string status = 7; string primitive = 8; string package_manager = 9; bool base_public = 10; }
80
84
  message RebuildImageRequest { string image_id = 1; }
81
85
  message RebuildAllImagesRequest {}
86
+ // The images one RebuildAllImages call rebuilt or could not provide. `rebuilt`
87
+ // names the stored custom images that were rebuilt, in dependency order; a base
88
+ // image ensured beforehand is not listed. `skipped` names every image the call
89
+ // could not make available: base short names first, then the custom images
90
+ // whose rebuild failed and the images that depend on them.
82
91
  message RebuildAllImagesResponse { repeated string rebuilt = 1; repeated string skipped = 2; }
83
92
  message RemoveImageRequest { string image_id = 1; }
84
93
  message RemoveImageResponse {}
@@ -109,7 +118,8 @@ message Container {
109
118
  repeated string paths = 12;
110
119
  }
111
120
  message StartContainerRequest { string workspace_slug = 1; string container = 2; string image_id = 3; repeated ProjectMount mounts = 4; map<string, string> env = 5; map<string, string> secret_env = 6; repeated string paths = 7; }
112
- message RecreateContainerRequest { string workspace_slug = 1; string image_id = 2; string container = 3; repeated ProjectMount mounts = 4; map<string, string> env = 5; repeated string paths = 6; }
121
+ message EnsureContainerRequest { string workspace_slug = 1; string container = 2; }
122
+ message RecreateContainerRequest { string workspace_slug = 1; string image_id = 2; string container = 3; repeated ProjectMount mounts = 4; map<string, string> env = 5; repeated string paths = 6; map<string, string> secret_env = 7; }
113
123
  message RemoveContainerRequest { string workspace_slug = 1; string container = 2; }
114
124
  message RemoveContainerResponse {}
115
125
  message AddContainerMountRequest { string workspace_slug = 1; string container = 2; string project = 3; reserved 4; string destination = 5; MountMode mode = 6; MountKind kind = 7; string volume = 8; string secret = 9; }
@@ -9,6 +9,9 @@ option go_package = "github.com/Exagone313/dsh-podman/internal/genproto/dshguest
9
9
  import "google/protobuf/wrappers.proto";
10
10
 
11
11
  service WorkspaceGuestAgent {
12
+ // Ping reports that the agent is reachable and accepts the caller's
13
+ // credential. Callers use it as a readiness probe before issuing work.
14
+ rpc Ping(PingRequest) returns (PingResponse);
12
15
  rpc Exec(stream ExecInput) returns (stream ExecOutput);
13
16
  rpc Signal(SignalRequest) returns (SignalResponse);
14
17
  rpc ReadFile(ReadFileRequest) returns (stream ReadFileChunk);
@@ -27,6 +30,8 @@ service WorkspaceGuestAgent {
27
30
  rpc DaemonLogs(DaemonLogsRequest) returns (DaemonLogsResponse);
28
31
  rpc Terminal(stream TerminalInput) returns (stream TerminalOutput);
29
32
  }
33
+ message PingRequest {}
34
+ message PingResponse { string version = 1; string commit = 2; }
30
35
  message ExecInput { oneof payload { ExecStart start = 1; bytes stdin_chunk = 2; } }
31
36
  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
37
  message ExecOutput { oneof payload { bytes stdout_chunk = 1; bytes stderr_chunk = 2; ExecExit exit = 3; } string process_id = 4; }
@@ -1,5 +1,7 @@
1
1
  import grpc from "@grpc/grpc-js";
2
2
  export declare function controlClient(socket: string): grpc.Client;
3
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>;
4
+ export declare function closeClients(): void;
5
+ export declare function abortError(label: string): Error;
6
+ export declare function unary<T>(client: grpc.Client, method: string, request: unknown, metadata?: grpc.Metadata, signal?: AbortSignal): Promise<T>;
5
7
  export { grpc };
@@ -7,27 +7,55 @@ import { fileURLToPath } from "node:url";
7
7
  import { dirname, resolve } from "node:path";
8
8
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "proto");
9
9
  const definitions = new Map();
10
+ // Live channels, keyed like the constructors: one client per service and
11
+ // socket for the plugin's lifetime. A channel holds a unix-socket connection
12
+ // and worker threads, so constructing one per call leaks both; the resolver
13
+ // disposes this cache on shutdown.
14
+ const clients = new Map();
15
+ // Keepalive lets a half-open channel fail fast instead of leaving a call
16
+ // pending forever, which is what backs the "hung orchestrator" symptom. Calls
17
+ // themselves carry no deadline: several control operations (image pull/build,
18
+ // cache clean) are legitimately long, so cancellation is driven by the turn's
19
+ // AbortSignal instead.
20
+ //
21
+ // Both servers permit exactly this policy (internal/grpcopts.KeepalivePolicy);
22
+ // grpc-go's default (5 minutes, no pings without streams) answers these pings
23
+ // with GOAWAY "too many pings", which grpc-js logs as "rejected by server
24
+ // because of excess pings". Keep the interval in sync with that policy.
25
+ const CHANNEL_OPTIONS = {
26
+ "grpc.keepalive_time_ms": 30_000,
27
+ "grpc.keepalive_timeout_ms": 10_000,
28
+ "grpc.keepalive_permit_without_calls": 1,
29
+ };
10
30
  function unixTarget(socket) {
11
31
  return socket.startsWith("unix:") ? socket : `unix:${socket}`;
12
32
  }
33
+ function constructorFor(service, proto) {
34
+ const cached = definitions.get(service);
35
+ if (cached !== undefined)
36
+ return cached;
37
+ const packageDefinition = loader.loadSync(resolve(root, proto), {
38
+ longs: String,
39
+ enums: String,
40
+ defaults: true,
41
+ });
42
+ const loaded = grpc.loadPackageDefinition(packageDefinition);
43
+ const namespace = service.startsWith("dshctl")
44
+ ? loaded.dshctl
45
+ : loaded.dshguest;
46
+ const version = namespace.v1;
47
+ const Constructor = version[service.split(".").at(-1)];
48
+ definitions.set(service, Constructor);
49
+ return Constructor;
50
+ }
13
51
  function client(service, proto, socket) {
14
52
  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);
53
+ let existing = clients.get(key);
54
+ if (existing === undefined) {
55
+ existing = new (constructorFor(service, proto))(unixTarget(socket), grpc.credentials.createInsecure(), CHANNEL_OPTIONS);
56
+ clients.set(key, existing);
29
57
  }
30
- return new Constructor(unixTarget(socket), grpc.credentials.createInsecure());
58
+ return existing;
31
59
  }
32
60
  export function controlClient(socket) {
33
61
  return client("dshctl.v1.OrchestratorControl", "dshctl/v1/control.proto", socket);
@@ -35,9 +63,54 @@ export function controlClient(socket) {
35
63
  export function guestClient(socket) {
36
64
  return client("dshguest.v1.WorkspaceGuestAgent", "dshguest/v1/guest.proto", socket);
37
65
  }
38
- export function unary(client, method, request, metadata) {
66
+ // closeClients closes and forgets every cached channel. Called when the plugin
67
+ // is disposed; a later call would create a fresh client.
68
+ export function closeClients() {
69
+ for (const client of clients.values()) {
70
+ client.close();
71
+ }
72
+ clients.clear();
73
+ }
74
+ // abortError is the stable shape an aborted call rejects with, so callers can
75
+ // tell a cancellation apart from a transport failure without parsing text.
76
+ export function abortError(label) {
77
+ const error = new Error(`${label} aborted`);
78
+ error.code = "ABORTED";
79
+ return error;
80
+ }
81
+ // onAbortCancel cancels a pending call when the signal aborts, and returns the
82
+ // detach function that removes the listener once the call settles.
83
+ function onAbortCancel(call, signal) {
84
+ if (signal === undefined)
85
+ return () => { };
86
+ const onAbort = () => {
87
+ try {
88
+ call.cancel();
89
+ }
90
+ catch {
91
+ // A finished call cannot be cancelled again; nothing to do.
92
+ }
93
+ };
94
+ signal.addEventListener("abort", onAbort, { once: true });
95
+ return () => signal.removeEventListener("abort", onAbort);
96
+ }
97
+ export function unary(client, method, request, metadata, signal) {
39
98
  return new Promise((resolvePromise, reject) => {
40
- client[method](request, metadata ?? new grpc.Metadata(), (error, value) => error ? reject(error) : resolvePromise(value));
99
+ if (signal?.aborted) {
100
+ reject(abortError(method));
101
+ return;
102
+ }
103
+ let detach = () => { };
104
+ const call = client[method](request, metadata ?? new grpc.Metadata(), (error, value) => {
105
+ detach();
106
+ if (error) {
107
+ reject(signal?.aborted ? abortError(method) : error);
108
+ }
109
+ else {
110
+ resolvePromise(value);
111
+ }
112
+ });
113
+ detach = onAbortCancel(call, signal);
41
114
  });
42
115
  }
43
116
  export { grpc };
@@ -1,4 +1,4 @@
1
- import { WorkspaceResolver } from "./workspace-binding.js";
1
+ import { type WorkspaceBinding, WorkspaceResolver } from "./workspace-binding.js";
2
2
  export declare function resolveGuestPath(path: string, sessionCwd: unknown): string;
3
3
  export declare function resolveGuestCwd(cwd: unknown, sessionCwd: unknown): string | undefined;
4
4
  export declare function guestCwd(requested: unknown, sessionCwd: unknown, binding: {
@@ -11,6 +11,7 @@ export declare function detectLineEndings(raw: string): "CRLF" | "LF";
11
11
  export declare function restoreLineEndings(content: string, endings: "CRLF" | "LF"): string;
12
12
  export declare function fsError(code: string, message: string): Error;
13
13
  export declare function throwIfAborted(signal: AbortSignal | undefined, verb: string): void;
14
+ export declare function withGuestAuth<T>(binding: WorkspaceBinding, run: (guest: any, token: string) => Promise<T>): Promise<T>;
14
15
  export declare function guestChunks(target: any, range: {
15
16
  offset?: number;
16
17
  length?: number;
@@ -19,42 +20,37 @@ export declare function guestTextChunks(target: any, signal: AbortSignal | undef
19
20
  export declare function readGuestText(target: any, signal: AbortSignal | undefined): Promise<string>;
20
21
  export declare function readDiffBasis(target: any, signal: AbortSignal | undefined): Promise<string | null>;
21
22
  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: {
23
+ export declare function writeGuestFile(binding: WorkspaceBinding, path: string, content: string, opts: {
26
24
  create: boolean;
27
25
  truncate: boolean;
28
26
  }, signal?: AbortSignal): Promise<number>;
29
- export declare function readGuestFile(binding: {
30
- guest: any;
31
- token: string;
32
- }, path: string): Promise<string>;
27
+ export declare function readGuestFile(binding: WorkspaceBinding, path: string): Promise<string>;
33
28
  export interface ExecIdentity {
34
29
  uid?: number;
35
30
  gid?: number;
36
31
  groups?: number[];
37
32
  }
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<{
33
+ export interface RunExecOptions {
34
+ signal?: AbortSignal;
35
+ maxBytes?: number;
36
+ spillBytes?: number;
37
+ killGraceMs?: number;
38
+ }
39
+ export declare function runExec(binding: WorkspaceBinding, argv: readonly string[], cwd?: string, env?: Record<string, string>, timeoutMs?: number, identity?: ExecIdentity, options?: RunExecOptions): Promise<{
42
40
  exitCode: number;
43
41
  signal: string | null;
44
42
  stdout: string;
45
43
  stderr: string;
44
+ stdoutSpillPath?: string;
45
+ stderrSpillPath?: string;
46
46
  }>;
47
47
  export declare function sliceLines(content: string, offset: unknown, limit: unknown): string;
48
+ export declare function streamLines(stream: AsyncIterable<string>, offset: unknown, limit: unknown): Promise<string>;
48
49
  export declare function outputLines(text: string): string[];
49
50
  export declare function bytesText(value: unknown): string;
50
51
  export declare function currentCwd(exec: any): unknown;
51
52
  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
- }>;
53
+ export declare function resolveToolBinding(resolver: WorkspaceResolver, cwd: unknown, container: string, signal?: AbortSignal): Promise<WorkspaceBinding>;
58
54
  export declare function guestStat(target: any, signal?: AbortSignal): Promise<any>;
59
55
  export declare function guestStatResponse(target: any, signal?: AbortSignal): Promise<any>;
60
56
  export declare function unaryGuest(target: any, method: string, request: unknown, signal?: AbortSignal): Promise<unknown>;