@geminixiang/mikan 1.3.0-beta.1 → 1.4.0-beta.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 (106) hide show
  1. package/CHANGELOG.md +38 -10
  2. package/README.md +7 -8
  3. package/dist/adapters/commands/login.js +1 -1
  4. package/dist/adapters/commands/login.js.map +1 -1
  5. package/dist/adapters/commands/sandbox.js +2 -2
  6. package/dist/adapters/commands/sandbox.js.map +1 -1
  7. package/dist/cli/boot.js +1 -1
  8. package/dist/cli/boot.js.map +1 -1
  9. package/dist/cli/migrate.d.ts.map +1 -1
  10. package/dist/cli/migrate.js +3 -2
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/cli/onboard.d.ts.map +1 -1
  13. package/dist/cli/onboard.js +15 -13
  14. package/dist/cli/onboard.js.map +1 -1
  15. package/dist/cli/pm2-ecosystem.d.ts +1 -2
  16. package/dist/cli/pm2-ecosystem.d.ts.map +1 -1
  17. package/dist/cli/pm2-ecosystem.js +2 -3
  18. package/dist/cli/pm2-ecosystem.js.map +1 -1
  19. package/dist/env-manifest.d.ts.map +1 -1
  20. package/dist/env-manifest.js +11 -0
  21. package/dist/env-manifest.js.map +1 -1
  22. package/dist/harness/execution-resolver.d.ts +3 -2
  23. package/dist/harness/execution-resolver.d.ts.map +1 -1
  24. package/dist/harness/execution-resolver.js +12 -10
  25. package/dist/harness/execution-resolver.js.map +1 -1
  26. package/dist/harness/prompt.d.ts.map +1 -1
  27. package/dist/harness/prompt.js +3 -2
  28. package/dist/harness/prompt.js.map +1 -1
  29. package/dist/harness/runner.d.ts.map +1 -1
  30. package/dist/harness/runner.js +37 -62
  31. package/dist/harness/runner.js.map +1 -1
  32. package/dist/harness/session.js +1 -1
  33. package/dist/harness/session.js.map +1 -1
  34. package/dist/harness/tools/attach.d.ts +3 -2
  35. package/dist/harness/tools/attach.d.ts.map +1 -1
  36. package/dist/harness/tools/attach.js +9 -2
  37. package/dist/harness/tools/attach.js.map +1 -1
  38. package/dist/harness/tools/index.d.ts +3 -2
  39. package/dist/harness/tools/index.d.ts.map +1 -1
  40. package/dist/harness/tools/index.js +6 -4
  41. package/dist/harness/tools/index.js.map +1 -1
  42. package/dist/harness/tools/jev-browser.d.ts +2 -2
  43. package/dist/harness/tools/jev-browser.d.ts.map +1 -1
  44. package/dist/harness/tools/jev-browser.js +41 -33
  45. package/dist/harness/tools/jev-browser.js.map +1 -1
  46. package/dist/harness/tools/sandbox.js +2 -2
  47. package/dist/harness/tools/sandbox.js.map +1 -1
  48. package/dist/harness/tools/types.d.ts +4 -0
  49. package/dist/harness/tools/types.d.ts.map +1 -1
  50. package/dist/harness/tools/types.js.map +1 -1
  51. package/dist/harness/tools/web-fetch.d.ts +6 -0
  52. package/dist/harness/tools/web-fetch.d.ts.map +1 -0
  53. package/dist/harness/tools/web-fetch.js +136 -0
  54. package/dist/harness/tools/web-fetch.js.map +1 -0
  55. package/dist/harness/types.d.ts +4 -3
  56. package/dist/harness/types.d.ts.map +1 -1
  57. package/dist/harness/types.js.map +1 -1
  58. package/dist/main.js +10 -24
  59. package/dist/main.js.map +1 -1
  60. package/dist/migrations/index.js +2 -2
  61. package/dist/migrations/index.js.map +1 -1
  62. package/dist/package-version.d.ts +2 -0
  63. package/dist/package-version.d.ts.map +1 -0
  64. package/dist/package-version.js +19 -0
  65. package/dist/package-version.js.map +1 -0
  66. package/dist/sandbox/container.d.ts +3 -17
  67. package/dist/sandbox/container.d.ts.map +1 -1
  68. package/dist/sandbox/container.js +108 -129
  69. package/dist/sandbox/container.js.map +1 -1
  70. package/dist/sandbox/engine.d.ts +4 -0
  71. package/dist/sandbox/engine.d.ts.map +1 -0
  72. package/dist/sandbox/engine.js +30 -0
  73. package/dist/sandbox/engine.js.map +1 -0
  74. package/dist/sandbox/host.d.ts +1 -10
  75. package/dist/sandbox/host.d.ts.map +1 -1
  76. package/dist/sandbox/host.js +2 -79
  77. package/dist/sandbox/host.js.map +1 -1
  78. package/dist/sandbox/provisioner.d.ts +15 -1
  79. package/dist/sandbox/provisioner.d.ts.map +1 -1
  80. package/dist/sandbox/provisioner.js +159 -111
  81. package/dist/sandbox/provisioner.js.map +1 -1
  82. package/dist/sandbox/published-image.d.ts +3 -0
  83. package/dist/sandbox/published-image.d.ts.map +1 -0
  84. package/dist/sandbox/published-image.js +9 -0
  85. package/dist/sandbox/published-image.js.map +1 -0
  86. package/dist/sandbox/registry.d.ts +5 -3
  87. package/dist/sandbox/registry.d.ts.map +1 -1
  88. package/dist/sandbox/registry.js +36 -27
  89. package/dist/sandbox/registry.js.map +1 -1
  90. package/dist/sandbox/types.d.ts +14 -22
  91. package/dist/sandbox/types.d.ts.map +1 -1
  92. package/dist/sandbox/types.js.map +1 -1
  93. package/dist/sandbox/utils.d.ts +0 -15
  94. package/dist/sandbox/utils.d.ts.map +1 -1
  95. package/dist/sandbox/utils.js +0 -72
  96. package/dist/sandbox/utils.js.map +1 -1
  97. package/dist/types.d.ts +2 -0
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/vault/index.js +1 -1
  101. package/dist/vault/index.js.map +1 -1
  102. package/package.json +3 -1
  103. package/dist/harness/execution-env.d.ts +0 -4
  104. package/dist/harness/execution-env.d.ts.map +0 -1
  105. package/dist/harness/execution-env.js +0 -322
  106. package/dist/harness/execution-env.js.map +0 -1
@@ -4,29 +4,19 @@ import { readFileSync, statSync } from "node:fs";
4
4
  import { isAbsolute, relative } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import * as log from "../log.js";
7
+ import { isPublishedSandboxImage } from "./published-image.js";
7
8
  import { reportUserFacingError } from "../observability/index.js";
8
9
  const execFileAsync = promisify(execFile);
9
- function isDockerNotFoundError(err) {
10
- if (!err || typeof err !== "object")
11
- return false;
12
- const stderr = err.stderr;
13
- const message = err.message;
14
- const haystack = `${typeof stderr === "string" ? stderr : ""}\n${typeof message === "string" ? message : ""}`.toLowerCase();
15
- return (haystack.includes("no such network") ||
16
- haystack.includes("no such container") ||
17
- haystack.includes("no such object") ||
18
- haystack.includes("network not found") ||
19
- /network [^\n]+ not found/.test(haystack) ||
20
- /error: no such [^\n]+/.test(haystack));
21
- }
22
- import { errorMessage } from "../unknown-values.js";
23
- import { sweepOrphanedCommands } from "./container.js";
10
+ import { errorMessage, isRecord } from "../unknown-values.js";
11
+ import { containerEngine } from "./engine.js";
24
12
  export class DockerContainerManager {
25
13
  static { this.MANAGED_LABEL = "mikan.managed=true"; }
26
14
  static { this.IMAGE_MODE_LABEL = "mikan.sandbox=image"; }
27
15
  static { this.VAULT_ID_LABEL_KEY = "mikan.vault-id"; }
28
16
  static { this.CONVERSATION_ID_LABEL_KEY = "mikan.conversation-id"; }
29
17
  static { this.MOUNT_SIGNATURE_LABEL_KEY = "mikan.mount-signature"; }
18
+ static { this.NETWORK_LABEL_KEY = "mikan.network"; }
19
+ static { this.IMAGE_ID_LABEL_KEY = "mikan.image-id"; }
30
20
  constructor(image, options = {}) {
31
21
  this.image = image;
32
22
  this.state = new Map();
@@ -37,6 +27,14 @@ export class DockerContainerManager {
37
27
  this.limits = options.limits;
38
28
  this.boostLimits = options.boostLimits;
39
29
  this.execFileImpl = options.execFileImpl ?? execFileAsync;
30
+ this.configuredEngine = options.engine;
31
+ this.timeZone = options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
32
+ }
33
+ get engine() {
34
+ return this.configuredEngine ?? containerEngine();
35
+ }
36
+ run(args) {
37
+ return this.execFileImpl(this.engine, args);
40
38
  }
41
39
  static containerName(containerKey) {
42
40
  return `mikan-sandbox-${containerKey}`;
@@ -75,11 +73,14 @@ export class DockerContainerManager {
75
73
  : await this.runtimeDrift(containerKey, containerName, mounts, status);
76
74
  if (drift) {
77
75
  log.logInfo(`Container ${containerName} is out of date (${drift}); replacing container`);
76
+ const previousImage = drift === "image" ? await this.containerImage(containerName) : undefined;
78
77
  await this.replaceContainer(containerKey, containerName, mounts, options);
79
78
  log.logInfo(`Container ${containerName} replaced from image ${this.image}`);
79
+ if (previousImage)
80
+ await this.removeImageIfUnused(previousImage);
80
81
  }
81
82
  else if (status === "stopped") {
82
- await this.execFileImpl("docker", ["start", containerName]);
83
+ await this.run(["start", containerName]);
83
84
  log.logInfo(`Container ${containerName} started`);
84
85
  }
85
86
  else if (status === "missing") {
@@ -126,13 +127,18 @@ export class DockerContainerManager {
126
127
  getBoostLimits() {
127
128
  return this.boostLimits;
128
129
  }
130
+ markUsed(containerKey) {
131
+ const state = this.state.get(containerKey);
132
+ if (state)
133
+ state.lastUsed = Date.now();
134
+ }
129
135
  stop(containerKey) {
130
136
  return this.serialize(containerKey, () => this.stopInner(containerKey));
131
137
  }
132
138
  async stopInner(containerKey) {
133
139
  const containerName = this.getContainerName(containerKey);
134
140
  try {
135
- await this.execFileImpl("docker", ["stop", containerName]);
141
+ await this.run(["stop", containerName]);
136
142
  this.setState(containerKey, "stopped", containerName);
137
143
  this.boostedKeys.delete(containerKey);
138
144
  this.overrideLimits.delete(containerKey);
@@ -153,7 +159,7 @@ export class DockerContainerManager {
153
159
  throw new Error(`Failed to remove container ${containerName}`);
154
160
  }
155
161
  try {
156
- await this.execFileImpl("docker", ["network", "rm", networkName]);
162
+ await this.run(["network", "rm", networkName]);
157
163
  log.logInfo(`Network ${networkName} removed`);
158
164
  }
159
165
  catch (err) {
@@ -192,9 +198,6 @@ export class DockerContainerManager {
192
198
  const lastUsed = details.startedAtMs ?? Date.now();
193
199
  this.state.set(containerKey, { status, lastUsed, containerName });
194
200
  }
195
- await Promise.all(Array.from(this.state.values())
196
- .filter((state) => state.status === "running")
197
- .map((state) => sweepOrphanedCommands(state.containerName, this.execFileImpl)));
198
201
  const running = Array.from(this.state.values()).filter((s) => s.status === "running").length;
199
202
  const stopped = this.state.size - running;
200
203
  log.logInfo(`Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`);
@@ -229,7 +232,9 @@ export class DockerContainerManager {
229
232
  if (mounts.length > 0) {
230
233
  labels.push("--label", `${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}=${this.mountSignature(mounts)}`);
231
234
  }
232
- await this.execFileImpl("docker", [
235
+ labels.push("--label", `${DockerContainerManager.NETWORK_LABEL_KEY}=${networkName}`);
236
+ labels.push("--label", `${DockerContainerManager.IMAGE_ID_LABEL_KEY}=${await this.pulledImageId()}`);
237
+ await this.run([
233
238
  "run",
234
239
  "-d",
235
240
  "--name",
@@ -242,6 +247,8 @@ export class DockerContainerManager {
242
247
  "no-new-privileges",
243
248
  "--pids-limit",
244
249
  "1024",
250
+ "--env",
251
+ `TZ=${this.timeZone}`,
245
252
  ...labels,
246
253
  ...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
247
254
  ...this.mountArgs(mounts),
@@ -261,7 +268,7 @@ export class DockerContainerManager {
261
268
  resourceLimitArgs(limits) {
262
269
  const args = [];
263
270
  if (limits?.cpus)
264
- args.push("--cpus", limits.cpus);
271
+ args.push(...cpuQuotaArgs(limits.cpus));
265
272
  if (limits?.memory) {
266
273
  args.push("--memory", limits.memory);
267
274
  args.push("--memory-swap", limits.memory);
@@ -274,7 +281,7 @@ export class DockerContainerManager {
274
281
  return;
275
282
  const args = ["update", ...limitArgs, containerName];
276
283
  try {
277
- await this.execFileImpl("docker", args);
284
+ await this.run(args);
278
285
  }
279
286
  catch (err) {
280
287
  log.logWarning(`Failed to apply resource limits to container ${containerName}`, errorMessage(err));
@@ -284,7 +291,7 @@ export class DockerContainerManager {
284
291
  operation: "apply_resource_limits",
285
292
  severity: "warning",
286
293
  context: {
287
- sandboxType: "image",
294
+ sandboxType: "oci",
288
295
  containerKey,
289
296
  containerName,
290
297
  limitArgCount: limitArgs.length,
@@ -323,14 +330,8 @@ export class DockerContainerManager {
323
330
  if (mounts.length === 0)
324
331
  return false;
325
332
  const expected = this.mountSignature(mounts);
326
- const { stdout } = await this.execFileImpl("docker", [
327
- "inspect",
328
- "-f",
329
- `{{index .Config.Labels "${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}"}}`,
330
- containerName,
331
- ]);
332
- const actual = this.normalizeDockerValue(stdout.trim());
333
- return actual !== expected;
333
+ return ((await this.label(containerName, DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY)) !==
334
+ expected);
334
335
  }
335
336
  mountSignature(mounts) {
336
337
  const payload = mounts
@@ -354,50 +355,81 @@ export class DockerContainerManager {
354
355
  return "missing";
355
356
  }
356
357
  }
357
- async inspectBindMounts(containerName) {
358
- const { stdout } = await this.execFileImpl("docker", [
358
+ async label(containerName, key) {
359
+ const { stdout } = await this.run([
359
360
  "inspect",
360
361
  "-f",
361
- "{{json .HostConfig.Binds}}",
362
+ `{{index .Config.Labels "${key}"}}`,
362
363
  containerName,
363
364
  ]);
364
- const payload = stdout.trim();
365
- const parsed = JSON.parse(payload.length > 0 ? payload : "null");
366
- if (parsed === null) {
367
- return [];
368
- }
369
- if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== "string")) {
370
- throw new Error(`Unexpected docker bind mount payload for container "${containerName}"`);
371
- }
372
- return [...parsed].toSorted();
365
+ return this.normalizeDockerValue(stdout.trim());
366
+ }
367
+ async inspectBindMounts(containerName) {
368
+ const { stdout } = await this.run(["inspect", "-f", "{{json .Mounts}}", containerName]);
369
+ return parseBindMounts(stdout, containerName)
370
+ .map((mount) => `${mount.source}:${mount.target}${mount.readOnly ? ":ro" : ""}`)
371
+ .toSorted();
373
372
  }
374
373
  async hasImageDrift(containerName) {
375
374
  const desired = await this.localImageId();
376
375
  if (!desired)
377
376
  return false;
378
- const { stdout } = await this.execFileImpl("docker", [
377
+ return (await this.label(containerName, DockerContainerManager.IMAGE_ID_LABEL_KEY)) !== desired;
378
+ }
379
+ async localImageId() {
380
+ try {
381
+ const { stdout } = await this.run(["image", "inspect", "-f", "{{.Id}}", this.image]);
382
+ return this.normalizeDockerValue(stdout.trim());
383
+ }
384
+ catch {
385
+ return undefined;
386
+ }
387
+ }
388
+ prepareImage() {
389
+ this.imagePull ??= this.pullImage();
390
+ return this.imagePull;
391
+ }
392
+ async pullImage() {
393
+ try {
394
+ await this.run(["pull", this.image]);
395
+ log.logInfo(`Sandbox image ${this.image} is current`);
396
+ }
397
+ catch (err) {
398
+ log.logWarning(`Could not pull sandbox image ${this.image}; offices keep the local image if there is one`, errorMessage(err));
399
+ }
400
+ }
401
+ async containerImage(containerName) {
402
+ const { stdout } = await this.run([
379
403
  "inspect",
380
404
  "-f",
381
- "{{.Image}}",
405
+ `{{.Config.Image}}\t{{index .Config.Labels "${DockerContainerManager.IMAGE_ID_LABEL_KEY}"}}`,
382
406
  containerName,
383
407
  ]);
384
- return stdout.trim() !== desired;
408
+ const [reference = "", imageId] = stdout.trim().split("\t");
409
+ if (reference !== this.image && isPublishedSandboxImage(reference))
410
+ return reference;
411
+ return this.normalizeDockerValue(imageId);
385
412
  }
386
- async localImageId() {
413
+ async removeImageIfUnused(image) {
387
414
  try {
388
- const { stdout } = await this.execFileImpl("docker", [
389
- "image",
390
- "inspect",
391
- "-f",
392
- "{{.Id}}",
393
- this.image,
394
- ]);
395
- return this.normalizeDockerValue(stdout.trim());
415
+ await this.run(["rmi", image]);
416
+ log.logInfo(`Removed sandbox image ${image}, which no container uses`);
396
417
  }
397
418
  catch {
398
- return undefined;
419
+ return;
399
420
  }
400
421
  }
422
+ async pulledImageId() {
423
+ await this.imagePull;
424
+ const local = await this.localImageId();
425
+ if (local)
426
+ return local;
427
+ await this.run(["pull", this.image]);
428
+ const pulled = await this.localImageId();
429
+ if (!pulled)
430
+ throw new Error(`Image ${this.image} has no ID after pulling it`);
431
+ return pulled;
432
+ }
401
433
  async replaceContainer(containerKey, containerName, mounts, options) {
402
434
  const removed = await this.forceRemoveContainer(containerName, `Container ${containerName} removed for replacement`, `Failed to remove container ${containerName} for replacement`);
403
435
  if (!removed)
@@ -406,25 +438,21 @@ export class DockerContainerManager {
406
438
  }
407
439
  async hasNetworkModeDrift(containerKey, containerName) {
408
440
  const expected = DockerContainerManager.networkName(containerKey);
409
- const { stdout } = await this.execFileImpl("docker", [
410
- "inspect",
411
- "-f",
412
- "{{.HostConfig.NetworkMode}}",
413
- containerName,
414
- ]);
415
- return stdout.trim() !== expected;
441
+ return (await this.label(containerName, DockerContainerManager.NETWORK_LABEL_KEY)) !== expected;
416
442
  }
417
443
  async ensureNetwork(containerKey) {
418
444
  const networkName = DockerContainerManager.networkName(containerKey);
419
- try {
420
- await this.execFileImpl("docker", ["network", "inspect", networkName]);
445
+ const { stdout } = await this.run([
446
+ "network",
447
+ "ls",
448
+ "--filter",
449
+ `name=^${networkName}$`,
450
+ "--format",
451
+ "{{.Name}}",
452
+ ]);
453
+ if (this.parseNameLines(stdout).includes(networkName))
421
454
  return networkName;
422
- }
423
- catch (err) {
424
- if (!isDockerNotFoundError(err))
425
- throw err;
426
- }
427
- await this.execFileImpl("docker", [
455
+ await this.run([
428
456
  "network",
429
457
  "create",
430
458
  "--driver",
@@ -439,25 +467,26 @@ export class DockerContainerManager {
439
467
  ]);
440
468
  return networkName;
441
469
  }
470
+ async containerExists(containerName) {
471
+ const { stdout } = await this.run([
472
+ "ps",
473
+ "-a",
474
+ "--filter",
475
+ `name=^${containerName}$`,
476
+ "--format",
477
+ "{{.Names}}",
478
+ ]);
479
+ return this.parseNameLines(stdout).includes(containerName);
480
+ }
442
481
  async inspectStatus(containerName) {
443
- try {
444
- const { stdout } = await this.execFileImpl("docker", [
445
- "inspect",
446
- "-f",
447
- "{{.State.Running}}",
448
- containerName,
449
- ]);
450
- return stdout.trim() === "true" ? "running" : "stopped";
451
- }
452
- catch (err) {
453
- if (isDockerNotFoundError(err))
454
- return "missing";
455
- throw err;
456
- }
482
+ if (!(await this.containerExists(containerName)))
483
+ return "missing";
484
+ const { stdout } = await this.run(["inspect", "-f", "{{.State.Running}}", containerName]);
485
+ return stdout.trim() === "true" ? "running" : "stopped";
457
486
  }
458
487
  async listContainerNamesByLabel() {
459
488
  try {
460
- const { stdout } = await this.execFileImpl("docker", [
489
+ const { stdout } = await this.run([
461
490
  "ps",
462
491
  "-a",
463
492
  "--filter",
@@ -482,16 +511,17 @@ export class DockerContainerManager {
482
511
  }
483
512
  async inspectContainerDetails(containerName) {
484
513
  try {
485
- const { stdout } = await this.execFileImpl("docker", [
514
+ const { stdout } = await this.run([
486
515
  "inspect",
487
516
  "-f",
488
- `{{.State.Running}}\t{{.State.StartedAt}}\t{{json .HostConfig.Binds}}`,
517
+ `{{.State.Running}}\t{{.State.StartedAt}}\t{{json .Mounts}}`,
489
518
  containerName,
490
519
  ]);
491
- const [runningRaw, startedAtRaw, bindsRaw] = stdout.trim().split("\t");
520
+ const [runningRaw, startedAtRaw, mountsRaw] = stdout.trim().split("\t");
492
521
  const running = runningRaw === "true";
493
- const startedAtMs = this.parseDockerTimestamp(startedAtRaw);
494
- return { running, startedAtMs, bindSources: parseBindSources(bindsRaw) };
522
+ const startedAtMs = parseEngineTimestamp(this.normalizeDockerValue(startedAtRaw));
523
+ const bindSources = parseBindMounts(mountsRaw ?? "", containerName).map((mount) => mount.source);
524
+ return { running, startedAtMs, bindSources };
495
525
  }
496
526
  catch (err) {
497
527
  log.logWarning(`Failed to inspect container ${containerName} during reconcile`, errorMessage(err));
@@ -504,13 +534,6 @@ export class DockerContainerManager {
504
534
  const trimmed = value.trim();
505
535
  return trimmed.length > 0 ? trimmed : undefined;
506
536
  }
507
- parseDockerTimestamp(value) {
508
- const normalized = this.normalizeDockerValue(value);
509
- if (!normalized || normalized.startsWith("0001-"))
510
- return undefined;
511
- const parsed = Date.parse(normalized);
512
- return Number.isNaN(parsed) ? undefined : parsed;
513
- }
514
537
  containerKeyFromContainerName(containerName) {
515
538
  const prefix = DockerContainerManager.containerName("");
516
539
  if (!containerName.startsWith(prefix))
@@ -520,26 +543,51 @@ export class DockerContainerManager {
520
543
  }
521
544
  async forceRemoveContainer(containerName, successLog, failureLog) {
522
545
  try {
523
- await this.execFileImpl("docker", ["rm", "-f", containerName]);
546
+ if (await this.containerExists(containerName)) {
547
+ await this.run(["kill", containerName]).catch(() => undefined);
548
+ await this.run(["rm", "-f", containerName]);
549
+ }
524
550
  log.logInfo(successLog);
525
551
  return true;
526
552
  }
527
553
  catch (err) {
528
- const message = errorMessage(err);
529
- if (/no such container/i.test(message))
530
- return true;
531
- log.logWarning(failureLog, message);
554
+ log.logWarning(failureLog, errorMessage(err));
532
555
  return false;
533
556
  }
534
557
  }
535
558
  }
536
- function parseBindSources(raw) {
537
- const parsed = JSON.parse(raw && raw.length > 0 ? raw : "null");
538
- if (!Array.isArray(parsed))
559
+ const CPU_PERIOD_MICROSECONDS = 100_000;
560
+ function cpuQuotaArgs(cpus) {
561
+ const quota = Math.round(Number(cpus) * CPU_PERIOD_MICROSECONDS);
562
+ if (!Number.isFinite(quota) || quota <= 0)
563
+ throw new Error(`Invalid CPU limit: ${cpus}`);
564
+ return ["--cpu-period", String(CPU_PERIOD_MICROSECONDS), "--cpu-quota", String(quota)];
565
+ }
566
+ function parseBindMounts(raw, containerName) {
567
+ const payload = raw.trim();
568
+ const parsed = JSON.parse(payload.length > 0 ? payload : "null");
569
+ if (parsed === null)
539
570
  return [];
540
- return parsed
541
- .filter((bind) => typeof bind === "string")
542
- .map((bind) => bind.split(":", 1)[0] ?? bind);
571
+ if (!Array.isArray(parsed)) {
572
+ throw new Error(`Unexpected mount payload for container "${containerName}"`);
573
+ }
574
+ return parsed.flatMap((entry) => {
575
+ if (!isRecord(entry) || entry.Type !== "bind")
576
+ return [];
577
+ const { Source: source, Destination: target, RW: writable } = entry;
578
+ if (typeof source !== "string" || typeof target !== "string")
579
+ return [];
580
+ return [{ source, target, readOnly: writable === false }];
581
+ });
582
+ }
583
+ const ENGINE_LOCAL_TIMESTAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?) ([+-]\d{2})(\d{2})/;
584
+ function parseEngineTimestamp(value) {
585
+ if (!value || value.startsWith("0001-"))
586
+ return undefined;
587
+ const local = ENGINE_LOCAL_TIMESTAMP.exec(value);
588
+ const iso = local ? `${local[1]}T${local[2]}${local[3]}:${local[4]}` : value;
589
+ const parsed = Date.parse(iso);
590
+ return Number.isNaN(parsed) ? undefined : parsed;
543
591
  }
544
592
  function mountsWithin(sources, root) {
545
593
  return sources.some((source) => {