@agentshouse/kit 0.1.0-alpha.38 → 0.1.0-alpha.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,13 +13,13 @@ The Skill set Kit installs is Matt Pocock's
13
13
  Linux (`amd64`, `arm64`) or macOS (Apple silicon, Intel):
14
14
 
15
15
  ```sh
16
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-linux.sh)
16
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)
17
17
  ```
18
18
 
19
19
  Windows 11 (`amd64`), in PowerShell:
20
20
 
21
21
  ```powershell
22
- & ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-windows.ps1)))
22
+ & ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))
23
23
  ```
24
24
 
25
25
  The command sets up Docker if it is missing, creates `~/.house-kit` (Kit home)
@@ -559,6 +559,7 @@ if kit_installed; then
559
559
  else
560
560
  resume_kit
561
561
  fi
562
+ ((NATIVE == 0)) || native_kit kit workspace update
562
563
  exit 0
563
564
  fi
564
565
 
package/bin/kit.js CHANGED
@@ -13,7 +13,7 @@ import { runResident, runWorkers } from "../src/worker.js";
13
13
  import { openSession, releaseControl, requestDrain } from "../src/house.js";
14
14
  import { reportHost } from "../src/host.js";
15
15
  import { signInAgent, updateAgents } from "../src/agents.js";
16
- import { serveWorkspaces } from "../src/workspace.js";
16
+ import { serveWorkspaces, updateWorkspaces } from "../src/workspace.js";
17
17
  import { captureRepository } from "../src/repository.js";
18
18
  import { bootstrap, pull, rebootstrap, submit } from "../src/sync.js";
19
19
  import { formatMount, formatUnmount, mountPackage, unmountSubtree } from "../src/mount.js";
@@ -35,7 +35,7 @@ const LOGIN_USAGE =
35
35
  const HOST_USAGE = "usage: kit host report [--config <path>]";
36
36
  const AGENTS_USAGE =
37
37
  "usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
38
- const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
38
+ const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>] | kit workspace update";
39
39
  const SOURCE_USAGE =
40
40
  "usage: kit source <capture|status> <account|instance> [service] [--config <path>]\n" +
41
41
  " kit source <reset|retry-files> <instance> [service] [--config <path>]\n" +
@@ -895,6 +895,21 @@ async function runWorkspaceHost(args, directory) {
895
895
  }
896
896
  }
897
897
 
898
+ async function runWorkspaceUpdate(args, directory) {
899
+ declared(args, [], 0, { exact: true });
900
+ const answered = await updateWorkspaces({
901
+ house: await storedHouse(directory),
902
+ configuration: join(directory, "kit.json"),
903
+ });
904
+ for (const { workspace, running } of answered.updated) {
905
+ write(running ? `Updated workspace ${workspace}.` : `Updated workspace ${workspace}; it stays stopped.`);
906
+ }
907
+ for (const { workspace, cause } of answered.unchanged) {
908
+ write(`Workspace ${workspace} is unchanged: ${cause}`);
909
+ }
910
+ if (answered.unchanged.length > 0) process.exitCode = 1;
911
+ }
912
+
898
913
  async function runTelegramConnect(args, directory) {
899
914
  declared(args, [], 0, { exact: true });
900
915
  const house = await storedHouse(directory);
@@ -1027,6 +1042,7 @@ async function main() {
1027
1042
  if (command === "workspace") {
1028
1043
  const [action, ...rest] = args;
1029
1044
  if (action === "host") return runWorkspaceHost(rest, directory);
1045
+ if (action === "update") return runWorkspaceUpdate(rest, directory);
1030
1046
  throw new Error(WORKSPACE_USAGE);
1031
1047
  }
1032
1048
  if (command === "telegram") {
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.38",
3
+ "version": "0.1.0-alpha.39",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
@@ -14,10 +14,10 @@
14
14
  }
15
15
  },
16
16
  "windows": {
17
- "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-windows.ps1)))",
17
+ "command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))",
18
18
  "asset": {
19
19
  "name": "connect-windows.ps1",
20
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.38/connect-windows.ps1"
20
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1"
21
21
  },
22
22
  "workspace": {
23
23
  "argument": "--workspace",
package/docs/linux.md CHANGED
@@ -69,3 +69,9 @@ The server connects as a continuous Environment. Put `--linux` before `kit` or
69
69
  `house` to run a Kit command, as in
70
70
  `bash <(curl -fsSL …/connect-linux.sh) --linux kit apply`, and run a newer
71
71
  release's command with `--linux` to update it, as above.
72
+
73
+ The same run updates the container Environments the server hosts to that
74
+ release. A running one waits, and is named, until its own Agent sessions end;
75
+ run the command again then. A stopped one is updated and stays stopped. Each
76
+ keeps its files, sign-ins and CPU, memory and disk limits; only what was
77
+ installed outside its workspace and Kit home is not kept.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.38",
3
+ "version": "0.1.0-alpha.39",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -160,12 +160,11 @@ function maximaOf(value, declaration) {
160
160
 
161
161
  function hostOf(value, declaration) {
162
162
  if (value === null) return null;
163
- const host = closed(value, ["workspace", "maxima", "image"], declaration);
163
+ const host = closed(value, ["workspace", "maxima"], declaration);
164
164
  if (host.maxima === undefined) refuse(`${declaration}.maxima`);
165
165
  return {
166
166
  workspace: absolutePath(host.workspace, `${declaration}.workspace`),
167
167
  maxima: maximaOf(host.maxima, `${declaration}.maxima`),
168
- image: host.image === undefined || host.image === null ? null : text(host.image, `${declaration}.image`, 255),
169
168
  };
170
169
  }
171
170
 
package/src/contract.js CHANGED
@@ -276,16 +276,20 @@ export const KIT_WORKSPACES_ACQUIRE_PATH = "/kit/workspaces/acquire";
276
276
  export const WORKSPACE_ACQUIRE_MAX = 10;
277
277
  export const WORKSPACE_POLL_MS = 2_000;
278
278
  export const WORKSPACE_LABEL = "house-kit.workspace";
279
- export const WORKSPACE_MOUNT = "/workspace";
280
- export const WORKSPACE_KIT_HOME = "/workspace/.house-kit";
279
+ export const WORKSPACE_KIT_HOME = "/kit-home";
281
280
  export const WORKSPACE_DISK_IMAGE = "disk.img";
282
281
  export const WORKSPACE_DISK_MOUNT = "mnt";
282
+ export const WORKSPACE_DISK_KIT_HOME = "kit-home";
283
+ export const WORKSPACE_DISK_HOUSE = "house";
284
+ export const WORKSPACE_SELECTED_IMAGE = "/opt/house-kit/image";
285
+ export const WORKSPACE_RELEASE_IMAGE = /^ghcr\.io\/agentshouse\/kit@sha256:[0-9a-f]{64}$/;
283
286
  export const WORKSPACE_MOUNT_OPTIONS = "loop,nosuid,nodev";
284
287
  export const HOST_MOUNT_INFO = "/proc/self/mountinfo";
285
288
  export const WORKSPACE_SYSTEM_RESERVE_BYTES = 512 * 1024 * 1024;
286
289
  export const WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
287
290
  export const WORKSPACE_READY_ATTEMPTS = 60;
288
291
  export const WORKSPACE_READY_POLL_MS = 1_000;
292
+ export const WORKSPACE_LOCK_POLL_MS = 1_000;
289
293
  export const WORKSPACE_STOP_TIMEOUT_SECONDS = 10;
290
294
  export const WORKSPACE_OPERATION_WITHDRAWN = "hosted_environment_operation_not_found";
291
295
  export const CGROUP_CONTROLLERS = "/sys/fs/cgroup/cgroup.controllers";
package/src/workspace.js CHANGED
@@ -1,22 +1,28 @@
1
1
  import { access, mkdir, readFile, rm } from "node:fs/promises";
2
+ import { createServer } from "node:net";
2
3
  import { join } from "node:path";
3
4
  import { readConfiguration } from "./configuration.js";
4
5
  import {
5
6
  CGROUP_CONTROLLERS,
6
7
  HOST_MOUNT_INFO,
8
+ LAUNCH_DIRECTORY_BASE,
7
9
  LOOP_CONTROL,
8
10
  WORKSPACE_ACQUIRE_MAX,
9
11
  WORKSPACE_COMMAND_TIMEOUT_MS,
12
+ WORKSPACE_DISK_HOUSE,
10
13
  WORKSPACE_DISK_IMAGE,
14
+ WORKSPACE_DISK_KIT_HOME,
11
15
  WORKSPACE_DISK_MOUNT,
12
16
  WORKSPACE_KIT_HOME,
13
17
  WORKSPACE_MOUNT_OPTIONS,
14
18
  WORKSPACE_LABEL,
15
- WORKSPACE_MOUNT,
19
+ WORKSPACE_LOCK_POLL_MS,
16
20
  WORKSPACE_OPERATION_WITHDRAWN,
17
21
  WORKSPACE_POLL_MS,
18
22
  WORKSPACE_READY_ATTEMPTS,
19
23
  WORKSPACE_READY_POLL_MS,
24
+ WORKSPACE_RELEASE_IMAGE,
25
+ WORKSPACE_SELECTED_IMAGE,
20
26
  WORKSPACE_STOP_TIMEOUT_SECONDS,
21
27
  WORKSPACE_SYSTEM_RESERVE_BYTES,
22
28
  } from "./contract.js";
@@ -27,10 +33,13 @@ import {
27
33
  openSession,
28
34
  releaseControl,
29
35
  reportWorkspaceOutcome,
36
+ updateDrain,
30
37
  } from "./house.js";
31
38
  import { houseFailed, KitRefusal } from "./refusal.js";
32
39
 
33
40
  const REQUIRED_CONTROLLERS = ["cpu", "memory"];
41
+ const PROVISIONED_DISK = "/disk";
42
+ const STOPPED_STATES = ["created", "exited"];
34
43
  const MEMINFO = "/proc/meminfo";
35
44
  const MEM_AVAILABLE = /^MemAvailable:\s+(\d+)\s+kB$/m;
36
45
 
@@ -106,30 +115,35 @@ export function mountedPaths(document) {
106
115
  );
107
116
  }
108
117
 
109
- export function workspaceCreateArguments(identity, operation, image, house, root) {
118
+ export function workspaceCreateArguments(identity, workspace, image, house, root) {
119
+ const disk = workspaceMountPath(root, identity);
110
120
  return [
111
121
  "create",
112
122
  "--name",
113
123
  identity,
114
124
  "--label",
115
- `${WORKSPACE_LABEL}=${operation.environment}`,
125
+ `${WORKSPACE_LABEL}=${workspace.environment}`,
116
126
  "--cpus",
117
- (operation.maxima.cpu_millicores / 1000).toFixed(3),
127
+ (workspace.maxima.cpu_millicores / 1000).toFixed(3),
118
128
  "--memory",
119
- `${operation.maxima.memory_bytes}b`,
129
+ `${workspace.maxima.memory_bytes}b`,
120
130
  "--memory-swap",
121
- `${operation.maxima.memory_bytes}b`,
131
+ `${workspace.maxima.memory_bytes}b`,
122
132
  "--restart",
123
133
  "no",
134
+ ...(new URL(house).protocol === "https:" ? [] : ["--network", "host"]),
124
135
  "--mount",
125
- `type=bind,source=${workspaceMountPath(root, identity)},target=${WORKSPACE_MOUNT}`,
136
+ `type=bind,source=${disk}/${WORKSPACE_DISK_KIT_HOME},target=${WORKSPACE_KIT_HOME}`,
137
+ "--mount",
138
+ `type=bind,source=${disk}/${WORKSPACE_DISK_HOUSE},target=${LAUNCH_DIRECTORY_BASE}`,
139
+ "--env",
140
+ `HOME=${WORKSPACE_KIT_HOME}`,
126
141
  "--env",
127
142
  `HOUSE_KIT_HOME=${WORKSPACE_KIT_HOME}`,
128
143
  "--env",
129
- `HOUSE_KIT_HOUSE=${house}`,
130
- "--workdir",
131
- WORKSPACE_MOUNT,
144
+ "HOUSE_KIT_CONTAINER=1",
132
145
  image,
146
+ "resident",
133
147
  ];
134
148
  }
135
149
 
@@ -198,7 +212,7 @@ export async function workspacePrerequisites(host, image) {
198
212
  if (formatter.status !== 0) {
199
213
  return {
200
214
  supported: false,
201
- refusal: `the declared workspace image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
215
+ refusal: `the selected Kit image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
202
216
  };
203
217
  }
204
218
  return { supported: true, runtime: `docker ${version.stdout.trim()}` };
@@ -215,34 +229,45 @@ function bootstrapConfiguration() {
215
229
  });
216
230
  }
217
231
 
218
- function provisionArguments(root, identity, image, target, mode) {
232
+ function diskArguments(root, identity, image, script) {
219
233
  return [
220
234
  "run",
221
235
  "--rm",
222
236
  "--interactive",
223
237
  "--mount",
224
- `type=bind,source=${workspaceMountPath(root, identity)},target=${WORKSPACE_MOUNT}`,
238
+ `type=bind,source=${workspaceMountPath(root, identity)},target=${PROVISIONED_DISK}`,
225
239
  "--entrypoint",
226
240
  "sh",
227
241
  image,
228
242
  "-c",
229
- `set -e; mkdir -p ${WORKSPACE_KIT_HOME}; chmod 700 ${WORKSPACE_KIT_HOME}; cat > ${target}; chmod ${mode} ${target}`,
243
+ script,
230
244
  ];
231
245
  }
232
246
 
233
- async function provisionWorkspaceState(host, root, identity, image, authority) {
234
- for (const [target, mode, content] of [
235
- [`${WORKSPACE_KIT_HOME}/credential`, "600", `${authority.credential}\n`],
236
- [`${WORKSPACE_KIT_HOME}/kit.json`, "600", `${bootstrapConfiguration()}\n`],
237
- ]) {
238
- const written = await docker(
247
+ async function onDisk(host, settings, identity, script, stdin = "") {
248
+ const written = await docker(host, diskArguments(settings.root, identity, settings.image, script), stdin);
249
+ if (written.status !== 0) {
250
+ throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
251
+ }
252
+ }
253
+
254
+ async function provisionWorkspaceState(host, settings, identity, operation) {
255
+ const home = `${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`;
256
+ const house = `${PROVISIONED_DISK}/${WORKSPACE_DISK_HOUSE}`;
257
+ const files = [
258
+ ...(operation.authority === undefined ? [] : [["credential", operation.authority.credential]]),
259
+ ["kit.json", bootstrapConfiguration()],
260
+ ["house", settings.house],
261
+ ["environment", operation.environment],
262
+ ];
263
+ for (const [name, content] of files) {
264
+ await onDisk(
239
265
  host,
240
- provisionArguments(root, identity, image, target, mode),
241
- content,
266
+ settings,
267
+ identity,
268
+ `set -e; mkdir -p ${home} ${house}; chmod 700 ${home} ${house}; cat > ${home}/${name}; chmod 600 ${home}/${name}`,
269
+ `${content}\n`,
242
270
  );
243
- if (written.status !== 0) {
244
- throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
245
- }
246
271
  }
247
272
  }
248
273
 
@@ -289,9 +314,7 @@ export async function createWorkspace(host, settings, operation) {
289
314
  }
290
315
  const mountRefusal = await mountWorkspace(host, settings, identity);
291
316
  if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
292
- if (operation.authority !== undefined) {
293
- await provisionWorkspaceState(host, settings.root, identity, settings.image, operation.authority);
294
- }
317
+ await provisionWorkspaceState(host, settings, identity, operation);
295
318
  const created = await docker(
296
319
  host,
297
320
  workspaceCreateArguments(identity, operation, settings.image, settings.house, settings.root),
@@ -312,9 +335,42 @@ async function workspacePresent(host, identity) {
312
335
  return inspected.status === 0 ? inspected.stdout.trim() : null;
313
336
  }
314
337
 
338
+ async function heldWorkspace(host, identity) {
339
+ const inspected = await docker(host, [
340
+ "container",
341
+ "inspect",
342
+ "--format",
343
+ `{{.State.Status}} {{.Config.Image}} {{index .Config.Labels "${WORKSPACE_LABEL}"}} {{.HostConfig.NanoCpus}} {{.HostConfig.Memory}}`,
344
+ identity,
345
+ ]);
346
+ if (inspected.status !== 0) return null;
347
+ const [state, image, environment, nanoCpus, memory] = inspected.stdout.trim().split(/\s+/);
348
+ return {
349
+ state,
350
+ image,
351
+ environment,
352
+ maxima: { cpu_millicores: Number(nanoCpus) / 1_000_000, memory_bytes: Number(memory) },
353
+ };
354
+ }
355
+
356
+ async function replaceWorkspace(host, settings, identity, held) {
357
+ const mountRefusal = await mountWorkspace(host, settings, identity);
358
+ if (mountRefusal !== null) return mountRefusal;
359
+ await onDisk(host, settings, identity, `rm -f ${updateDrain(`${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`)}`);
360
+ const removed = await docker(host, ["rm", "--force", identity]);
361
+ if (removed.status !== 0) return `the workspace was not replaced: ${removed.stderr.trim()}`;
362
+ const created = await docker(
363
+ host,
364
+ workspaceCreateArguments(identity, held, settings.image, settings.house, settings.root),
365
+ );
366
+ if (created.status !== 0) return `the workspace was not recreated: ${created.stderr.trim()}`;
367
+ return null;
368
+ }
369
+
315
370
  export async function startWorkspace(host, settings, operation) {
316
371
  const identity = operation.workspace ?? workspaceIdentity(operation);
317
- if ((await workspacePresent(host, identity)) === null) return { outcome: "absent" };
372
+ const held = await heldWorkspace(host, identity);
373
+ if (held === null) return { outcome: "absent" };
318
374
  const available = memoryAvailable(await host.read(MEMINFO));
319
375
  if (available - operation.maxima.memory_bytes < WORKSPACE_SYSTEM_RESERVE_BYTES) {
320
376
  return {
@@ -324,6 +380,10 @@ export async function startWorkspace(host, settings, operation) {
324
380
  }
325
381
  const mountRefusal = await mountWorkspace(host, settings, identity);
326
382
  if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
383
+ if (STOPPED_STATES.includes(held.state) && held.image !== settings.image) {
384
+ const refusal = await replaceWorkspace(host, settings, identity, held);
385
+ if (refusal !== null) return { outcome: "failed", refusal };
386
+ }
327
387
  const started = await docker(host, ["start", identity]);
328
388
  if (started.status !== 0) {
329
389
  return { outcome: "failed", refusal: `the workspace did not start: ${started.stderr.trim()}` };
@@ -425,37 +485,123 @@ export async function reconcileWorkspaces(host, authorized) {
425
485
  );
426
486
  const stopped = [];
427
487
  for (const managed of await managedWorkspaces(host)) {
428
- if (managed.state !== "running") continue;
429
488
  if (expected.get(managed.environment) === "running") continue;
430
- const settled = await docker(host, [
431
- "stop",
432
- "--timeout",
433
- String(WORKSPACE_STOP_TIMEOUT_SECONDS),
434
- managed.name,
435
- ]);
436
- if (settled.status === 0) stopped.push(managed.name);
489
+ const settled = await exclusively(host, managed.name, async () => {
490
+ if ((await workspacePresent(host, managed.name)) !== "running") return false;
491
+ const stopping = await docker(host, [
492
+ "stop",
493
+ "--timeout",
494
+ String(WORKSPACE_STOP_TIMEOUT_SECONDS),
495
+ managed.name,
496
+ ]);
497
+ return stopping.status === 0;
498
+ });
499
+ if (settled) stopped.push(managed.name);
437
500
  }
438
501
  return stopped;
439
502
  }
440
503
 
504
+ function workspaceLock(identity) {
505
+ return new Promise((resolve, reject) => {
506
+ const lock = createServer();
507
+ lock.once("error", reject);
508
+ lock.listen({ path: `\0${WORKSPACE_LABEL}/${identity}` }, () => resolve(lock));
509
+ });
510
+ }
511
+
512
+ async function exclusively(host, identity, act) {
513
+ let lock = null;
514
+ while (lock === null) {
515
+ try {
516
+ lock = await workspaceLock(identity);
517
+ } catch (error) {
518
+ if (error.code !== "EADDRINUSE") throw error;
519
+ await host.wait(WORKSPACE_LOCK_POLL_MS);
520
+ }
521
+ }
522
+ try {
523
+ return await act();
524
+ } finally {
525
+ await new Promise((resolve) => lock.close(resolve));
526
+ }
527
+ }
528
+
441
529
  export async function performOperation(host, settings, operation) {
442
- if (operation.kind === "create") return createWorkspace(host, settings, operation);
443
- if (operation.kind === "start") return startWorkspace(host, settings, operation);
444
- if (operation.kind === "stop") return stopWorkspace(host, operation);
445
- return deleteWorkspace(host, settings, operation);
530
+ const selected = operation.kind === "stop" ? settings : { ...settings, image: await selectedImage(host) };
531
+ return exclusively(host, operation.workspace ?? workspaceIdentity(operation), () => {
532
+ if (operation.kind === "stop") return stopWorkspace(host, operation);
533
+ if (operation.kind === "create") return createWorkspace(host, selected, operation);
534
+ if (operation.kind === "start") return startWorkspace(host, selected, operation);
535
+ return deleteWorkspace(host, selected, operation);
536
+ });
446
537
  }
447
538
 
448
- export async function serveWorkspaces(options, dependencies = {}) {
539
+ async function selectedImage(host) {
540
+ let image;
541
+ try {
542
+ image = (await host.read(WORKSPACE_SELECTED_IMAGE)).trim();
543
+ } catch {
544
+ image = null;
545
+ }
546
+ if (image === null || !WORKSPACE_RELEASE_IMAGE.test(image)) {
547
+ throw new KitRefusal("kit_workspace_host_unconfigured", WORKSPACE_SELECTED_IMAGE);
548
+ }
549
+ return image;
550
+ }
551
+
552
+ async function hostSettings(options) {
449
553
  const configuration = await readConfiguration(options.configuration);
450
- if (configuration.host === null || configuration.host.image === null) {
554
+ if (configuration.host === null) {
451
555
  throw new KitRefusal("kit_workspace_host_unconfigured", options.configuration);
452
556
  }
557
+ return { root: configuration.host.workspace, house: options.house };
558
+ }
559
+
560
+ async function updateWorkspace(host, settings, name) {
561
+ try {
562
+ const held = await heldWorkspace(host, name);
563
+ if (held === null || held.image === settings.image) return null;
564
+ const running = !STOPPED_STATES.includes(held.state);
565
+ if (running) {
566
+ const drained = await docker(host, ["exec", name, "kit", "resident", "drain"]);
567
+ if (drained.status !== 0) {
568
+ return { workspace: name, cause: drained.stdout.trim() || drained.stderr.trim() };
569
+ }
570
+ }
571
+ const refusal = await replaceWorkspace(host, settings, name, held);
572
+ if (refusal !== null) return { workspace: name, cause: refusal };
573
+ if (running) {
574
+ const started = await docker(host, ["start", name]);
575
+ if (started.status !== 0) {
576
+ return { workspace: name, cause: `the workspace did not start: ${started.stderr.trim()}` };
577
+ }
578
+ }
579
+ return { workspace: name, running };
580
+ } catch (error) {
581
+ if (!(error instanceof KitRefusal)) throw error;
582
+ return { workspace: name, cause: error.message };
583
+ }
584
+ }
585
+
586
+ export async function updateWorkspaces(options, dependencies = {}) {
453
587
  const host = adapter(dependencies);
454
- const settings = {
455
- root: configuration.host.workspace,
456
- image: configuration.host.image,
457
- house: options.house,
458
- };
588
+ const updated = [];
589
+ const unchanged = [];
590
+ const managed = await managedWorkspaces(host);
591
+ if (managed.length === 0) return { updated, unchanged };
592
+ const settings = { ...(await hostSettings(options)), image: await selectedImage(host) };
593
+ for (const { name } of managed) {
594
+ const answer = await exclusively(host, name, () => updateWorkspace(host, settings, name));
595
+ if (answer === null) continue;
596
+ if (answer.cause === undefined) updated.push(answer);
597
+ else unchanged.push(answer);
598
+ }
599
+ return { updated, unchanged };
600
+ }
601
+
602
+ export async function serveWorkspaces(options, dependencies = {}) {
603
+ const host = adapter(dependencies);
604
+ const settings = await hostSettings(options);
459
605
  const session = await openSession(options, dependencies);
460
606
  const settled = [];
461
607
  const withdrawn = [];