signalk-container 1.7.0 → 1.8.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 (88) hide show
  1. package/dist/containers.d.ts +53 -6
  2. package/dist/containers.d.ts.map +1 -1
  3. package/dist/containers.js +278 -165
  4. package/dist/containers.js.map +1 -1
  5. package/dist/doctor.d.ts +42 -0
  6. package/dist/doctor.d.ts.map +1 -0
  7. package/dist/doctor.js +65 -0
  8. package/dist/doctor.js.map +1 -0
  9. package/dist/index.d.ts +117 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +234 -87
  12. package/dist/index.js.map +1 -1
  13. package/dist/jobs.d.ts +2 -41
  14. package/dist/jobs.d.ts.map +1 -1
  15. package/dist/jobs.js +23 -104
  16. package/dist/jobs.js.map +1 -1
  17. package/dist/log-stream-broker.d.ts +2 -2
  18. package/dist/log-stream-broker.d.ts.map +1 -1
  19. package/dist/log-stream-broker.js +3 -6
  20. package/dist/log-stream-broker.js.map +1 -1
  21. package/dist/manifest/atomicWrite.d.ts +16 -0
  22. package/dist/manifest/atomicWrite.d.ts.map +1 -0
  23. package/dist/manifest/atomicWrite.js +100 -0
  24. package/dist/manifest/atomicWrite.js.map +1 -0
  25. package/dist/manifest/resolver.d.ts +33 -0
  26. package/dist/manifest/resolver.d.ts.map +1 -0
  27. package/dist/manifest/resolver.js +52 -0
  28. package/dist/manifest/resolver.js.map +1 -0
  29. package/dist/manifest/schema.d.ts +53 -0
  30. package/dist/manifest/schema.d.ts.map +1 -0
  31. package/dist/manifest/schema.js +47 -0
  32. package/dist/manifest/schema.js.map +1 -0
  33. package/dist/manifest/store.d.ts +45 -0
  34. package/dist/manifest/store.d.ts.map +1 -0
  35. package/dist/manifest/store.js +220 -0
  36. package/dist/manifest/store.js.map +1 -0
  37. package/dist/resources.d.ts +1 -1
  38. package/dist/resources.d.ts.map +1 -1
  39. package/dist/resources.js +9 -19
  40. package/dist/resources.js.map +1 -1
  41. package/dist/runtime.d.ts +66 -1
  42. package/dist/runtime.d.ts.map +1 -1
  43. package/dist/runtime.js +107 -26
  44. package/dist/runtime.js.map +1 -1
  45. package/dist/types.d.ts +187 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js +1 -2
  48. package/dist/updates/cache.d.ts +1 -1
  49. package/dist/updates/cache.d.ts.map +1 -1
  50. package/dist/updates/cache.js +8 -13
  51. package/dist/updates/cache.js.map +1 -1
  52. package/dist/updates/offline.js +1 -4
  53. package/dist/updates/offline.js.map +1 -1
  54. package/dist/updates/routes.d.ts +1 -1
  55. package/dist/updates/routes.d.ts.map +1 -1
  56. package/dist/updates/routes.js +1 -4
  57. package/dist/updates/routes.js.map +1 -1
  58. package/dist/updates/semver.js +1 -4
  59. package/dist/updates/semver.js.map +1 -1
  60. package/dist/updates/service.d.ts +5 -5
  61. package/dist/updates/service.d.ts.map +1 -1
  62. package/dist/updates/service.js +12 -17
  63. package/dist/updates/service.js.map +1 -1
  64. package/dist/updates/sources.d.ts +1 -1
  65. package/dist/updates/sources.d.ts.map +1 -1
  66. package/dist/updates/sources.js +2 -6
  67. package/dist/updates/sources.js.map +1 -1
  68. package/dist/updates/tagClassifier.d.ts +1 -1
  69. package/dist/updates/tagClassifier.d.ts.map +1 -1
  70. package/dist/updates/tagClassifier.js +1 -4
  71. package/dist/updates/tagClassifier.js.map +1 -1
  72. package/dist/updates/types.d.ts +1 -1
  73. package/dist/updates/types.d.ts.map +1 -1
  74. package/dist/updates/types.js +1 -2
  75. package/doc/plugin-developer-guide.md +114 -0
  76. package/package.json +12 -10
  77. package/public/chunks/_virtual_mf-localSharedImportMap___mfe_internal__signalk_container-BmrRsGF5.js +1 -0
  78. package/public/chunks/dist-oj8URvh5.js +4 -0
  79. package/public/chunks/hostInit-qNQhWMu1.js +1 -0
  80. package/public/chunks/preload-helper-CM6grPkN.js +1 -0
  81. package/public/chunks/virtualExposes-CjK-c_Lg.js +1 -0
  82. package/public/configpanel-entry.js +5 -0
  83. package/public/remoteEntry.js +1 -1
  84. package/public/520.js +0 -1
  85. package/public/540.js +0 -2
  86. package/public/540.js.LICENSE.txt +0 -9
  87. package/public/main.js +0 -1
  88. package/webpack.config.js +0 -51
@@ -1,83 +1,7 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.MAX_TAIL = void 0;
37
- exports.volumeArg = volumeArg;
38
- exports.classifyVolumeSources = classifyVolumeSources;
39
- exports.collectRecoveredVolumes = collectRecoveredVolumes;
40
- exports.safeInvokeVolumeIssue = safeInvokeVolumeIssue;
41
- exports.safeInvokeContainerLog = safeInvokeContainerLog;
42
- exports.tailContainerLogs = tailContainerLogs;
43
- exports.parsePositiveIntQuery = parsePositiveIntQuery;
44
- exports.getContainerLogs = getContainerLogs;
45
- exports.qualifyImage = qualifyImage;
46
- exports.imageExists = imageExists;
47
- exports.getImageDigest = getImageDigest;
48
- exports.pullImage = pullImage;
49
- exports.getContainerState = getContainerState;
50
- exports.getLiveResources = getLiveResources;
51
- exports.parsePortBindings = parsePortBindings;
52
- exports.getActualPortBindings = getActualPortBindings;
53
- exports.getLiveContainerConfig = getLiveContainerConfig;
54
- exports.diffContainerConfig = diffContainerConfig;
55
- exports.ensureRunning = ensureRunning;
56
- exports.startContainer = startContainer;
57
- exports.stopContainer = stopContainer;
58
- exports.removeContainer = removeContainer;
59
- exports.listContainers = listContainers;
60
- exports.pruneImages = pruneImages;
61
- exports.execInContainer = execInContainer;
62
- exports.ensureNetwork = ensureNetwork;
63
- exports.removeNetwork = removeNetwork;
64
- exports.connectToNetwork = connectToNetwork;
65
- exports.disconnectFromNetwork = disconnectFromNetwork;
66
- exports.parseSelfContainerIdFromCgroup = parseSelfContainerIdFromCgroup;
67
- exports.parseSelfContainerIdsFromCgroupFile = parseSelfContainerIdsFromCgroupFile;
68
- exports.readSelfContainerIdsFromCgroup = readSelfContainerIdsFromCgroup;
69
- exports.findSelfContainerId = findSelfContainerId;
70
- exports.resolveSignalkDataSource = resolveSignalkDataSource;
71
- exports.resolveHostPathFromMounts = resolveHostPathFromMounts;
72
- exports.resolveHostPath = resolveHostPath;
73
- exports.releaseReservedPort = releaseReservedPort;
74
- exports.findAvailablePort = findAvailablePort;
75
- exports.resolveSignalkNetworks = resolveSignalkNetworks;
76
- exports.waitForReady = waitForReady;
77
- const net = __importStar(require("net"));
78
- const node_fs_1 = require("node:fs");
79
- const runtime_1 = require("./runtime");
80
- const resources_1 = require("./resources");
1
+ import * as net from "node:net";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { execRuntime, execRuntimeLong, isContainerized, spawnRuntimeStreaming, userMappingFlags, } from "./runtime.js";
4
+ import { resourceFlagsForRun } from "./resources.js";
81
5
  const CONTAINER_PREFIX = "sk-";
82
6
  function prefixedName(name) {
83
7
  return name.startsWith(CONTAINER_PREFIX)
@@ -95,7 +19,7 @@ function prefixedName(name) {
95
19
  * Used by both ContainerConfig.volumes (containers.ts) and JobConfig
96
20
  * inputs/outputs (jobs.ts) so the named-volume guard stays in one place.
97
21
  */
98
- function volumeArg(hostPath, containerPath, runtime, readOnly = false) {
22
+ export function volumeArg(hostPath, containerPath, runtime, readOnly = false) {
99
23
  const isNamedVolume = !hostPath.startsWith("/") && !hostPath.startsWith(".");
100
24
  const flags = [];
101
25
  if (readOnly)
@@ -140,7 +64,7 @@ function volumeSource(raw) {
140
64
  * `probe` is the host-path existence check; defaults to `existsSync`.
141
65
  * Tests inject a stub so they don't touch the filesystem.
142
66
  */
143
- function classifyVolumeSources(volumes, probe = node_fs_1.existsSync) {
67
+ export function classifyVolumeSources(volumes, probe = existsSync) {
144
68
  const kept = {};
145
69
  const skipped = [];
146
70
  const aborted = [];
@@ -195,7 +119,7 @@ function classifyVolumeSources(volumes, probe = node_fs_1.existsSync) {
195
119
  *
196
120
  * Pure function — no I/O.
197
121
  */
198
- function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
122
+ export function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
199
123
  if (!prior)
200
124
  return [];
201
125
  const stillMissing = new Set([...currentSkipped, ...currentAborted].map((v) => v.containerPath));
@@ -228,7 +152,7 @@ function collectRecoveredVolumes(prior, currentSkipped, currentAborted, kept) {
228
152
  * Pure-by-design: `reportError` is injected so tests can capture
229
153
  * the message instead of writing to `app.error`.
230
154
  */
231
- function safeInvokeVolumeIssue(handler, event, reportError) {
155
+ export function safeInvokeVolumeIssue(handler, event, reportError) {
232
156
  if (!handler)
233
157
  return;
234
158
  try {
@@ -249,7 +173,7 @@ function safeInvokeVolumeIssue(handler, event, reportError) {
249
173
  * doc for why the `try { Promise.resolve(...).catch(...) }` dance
250
174
  * is needed.
251
175
  */
252
- function safeInvokeContainerLog(handler, line, reportError) {
176
+ export function safeInvokeContainerLog(handler, line, reportError) {
253
177
  if (!handler)
254
178
  return;
255
179
  try {
@@ -271,8 +195,8 @@ function safeInvokeContainerLog(handler, line, reportError) {
271
195
  *
272
196
  * `spawn` is exposed for tests (defaults to `spawnRuntimeStreaming`).
273
197
  */
274
- function tailContainerLogs(runtime, name, onLine, options) {
275
- const spawnFn = options?.spawn ?? runtime_1.spawnRuntimeStreaming;
198
+ export function tailContainerLogs(runtime, name, onLine, options) {
199
+ const spawnFn = options?.spawn ?? spawnRuntimeStreaming;
276
200
  const fullName = prefixedName(name);
277
201
  const tail = String(normalizeTail(options?.startTail, 0));
278
202
  return spawnFn(runtime, ["logs", "-f", "--tail", tail, fullName], onLine, {
@@ -289,7 +213,7 @@ function tailContainerLogs(runtime, name, onLine, options) {
289
213
  });
290
214
  }
291
215
  /** Upper bound for `--tail N` accepted at the public boundary. */
292
- exports.MAX_TAIL = 10000;
216
+ export const MAX_TAIL = 10000;
293
217
  /**
294
218
  * Coerce an optional caller-supplied tail count to a finite,
295
219
  * non-negative integer so a buggy or hostile caller can never
@@ -301,7 +225,7 @@ exports.MAX_TAIL = 10000;
301
225
  function normalizeTail(value, fallback) {
302
226
  if (value === undefined || !Number.isFinite(value))
303
227
  return fallback;
304
- return Math.min(Math.max(0, Math.floor(value)), exports.MAX_TAIL);
228
+ return Math.min(Math.max(0, Math.floor(value)), MAX_TAIL);
305
229
  }
306
230
  /**
307
231
  * Validate an optional unsigned-integer query parameter (e.g.
@@ -314,7 +238,7 @@ function normalizeTail(value, fallback) {
314
238
  * instead of forwarding silently-coerced values to runtime-facing
315
239
  * logic. Exported so the parser is testable in isolation.
316
240
  */
317
- function parsePositiveIntQuery(raw, field) {
241
+ export function parsePositiveIntQuery(raw, field) {
318
242
  if (raw === undefined || raw === "")
319
243
  return { value: undefined };
320
244
  if (typeof raw !== "string")
@@ -356,7 +280,7 @@ function parsePositiveIntQuery(raw, field) {
356
280
  * Used both by the `GET /containers/:name/logs` REST route and by
357
281
  * `ContainerManagerApi.getLogs` for in-process consumer-plugin calls.
358
282
  */
359
- async function getContainerLogs(runtime, name, options, exec = runtime_1.execRuntime) {
283
+ export async function getContainerLogs(runtime, name, options, exec = execRuntime) {
360
284
  const tail = normalizeTail(options?.tail, 200);
361
285
  const args = ["logs", "--tail", String(tail)];
362
286
  if (options?.since !== undefined &&
@@ -384,15 +308,31 @@ async function getContainerLogs(runtime, name, options, exec = runtime_1.execRun
384
308
  };
385
309
  return [...toLines(result.stdout), ...toLines(result.stderr)];
386
310
  }
387
- function qualifyImage(image, runtime) {
311
+ /**
312
+ * Bare image id: 64 hex chars, optionally with the `sha256:` prefix
313
+ * podman/docker print for `Config.Image`. Such ids must never be
314
+ * qualified with a registry — they're addressable by content, not by
315
+ * name.
316
+ */
317
+ const BARE_IMAGE_ID = /^(?:sha256:)?[a-f0-9]{64}$/;
318
+ export function qualifyImage(image, runtime) {
388
319
  // Podman requires fully qualified image names when unqualified-search
389
320
  // registries are not configured. Prefix docker.io/ if missing.
390
321
  if (runtime.runtime === "podman") {
391
- const parts = image.split("/");
322
+ // Bare image ids (e.g. `sha256:abc…` or `abc…`) are content-addressed
323
+ // and never need a registry prefix; pass through.
324
+ if (BARE_IMAGE_ID.test(image))
325
+ return image;
326
+ // Strip any `@sha256:...` digest before splitting — for an
327
+ // unqualified ref like `alpine@sha256:abc...` the digest's `:`
328
+ // would otherwise make us think the first segment is `host:port`.
329
+ const refWithoutDigest = image.split("@", 1)[0];
330
+ const parts = refWithoutDigest.split("/");
392
331
  // Treat first component as a registry only if it has a dot, a colon
393
- // (port), or is exactly "localhost". Otherwise, prefix docker.io/.
332
+ // (port) appearing in a multi-segment ref, or is exactly "localhost".
333
+ // Otherwise, prefix docker.io/.
394
334
  const looksLikeRegistry = parts[0].includes(".") ||
395
- parts[0].includes(":") ||
335
+ (parts.length > 1 && parts[0].includes(":")) ||
396
336
  parts[0] === "localhost";
397
337
  if (parts.length <= 2 && !looksLikeRegistry) {
398
338
  return `docker.io/${image}`;
@@ -400,7 +340,7 @@ function qualifyImage(image, runtime) {
400
340
  }
401
341
  return image;
402
342
  }
403
- async function imageExists(runtime, image, exec = runtime_1.execRuntime) {
343
+ export async function imageExists(runtime, image, exec = execRuntime) {
404
344
  const result = await exec(runtime, ["image", "inspect", image]);
405
345
  return result.exitCode === 0;
406
346
  }
@@ -413,10 +353,10 @@ async function imageExists(runtime, image, exec = runtime_1.execRuntime) {
413
353
  * pulled image, or a container name to inspect the image a running
414
354
  * container is using.
415
355
  */
416
- async function getImageDigest(runtime, imageOrContainer) {
356
+ export async function getImageDigest(runtime, imageOrContainer, exec = execRuntime) {
417
357
  // Try image inspect first; fall back to container inspect for names.
418
358
  const qualified = qualifyImage(imageOrContainer, runtime);
419
- const imgResult = await (0, runtime_1.execRuntime)(runtime, [
359
+ const imgResult = await exec(runtime, [
420
360
  "image",
421
361
  "inspect",
422
362
  "--format",
@@ -427,7 +367,7 @@ async function getImageDigest(runtime, imageOrContainer) {
427
367
  return imgResult.stdout.trim();
428
368
  }
429
369
  // Maybe it's a container name; .Image on a container returns the image ID.
430
- const ctrResult = await (0, runtime_1.execRuntime)(runtime, [
370
+ const ctrResult = await exec(runtime, [
431
371
  "inspect",
432
372
  "--format",
433
373
  "{{.Image}}",
@@ -438,13 +378,74 @@ async function getImageDigest(runtime, imageOrContainer) {
438
378
  }
439
379
  return null;
440
380
  }
441
- async function pullImage(runtime, image, onProgress) {
442
- const { exitCode, log } = await (0, runtime_1.execRuntimeLong)(runtime, ["pull", image], onProgress, 300000);
381
+ /**
382
+ * Return the first RepoDigest for an image reference, as
383
+ * `sha256:<hex>` (without the `image@` prefix). This is the
384
+ * *manifest* digest — what consumer plugins and CI tools speak —
385
+ * distinct from the local image ID returned by `getImageDigest`.
386
+ *
387
+ * Returns null when the image has no RepoDigests, which happens for:
388
+ * - locally-built images never pushed to a registry
389
+ * - images side-loaded via `podman load` from a tarball
390
+ *
391
+ * The resolver falls back to a `local:<image-id>` synthetic identity
392
+ * in that case so the manifest entry still round-trips.
393
+ */
394
+ export async function getRepoDigest(runtime, image, exec = execRuntime) {
395
+ const qualified = qualifyImage(image, runtime);
396
+ // The {{if .RepoDigests}}{{end}} guard avoids `[<no value>]` output
397
+ // that some podman versions emit when RepoDigests is empty.
398
+ const result = await exec(runtime, [
399
+ "image",
400
+ "inspect",
401
+ "--format",
402
+ "{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}",
403
+ qualified,
404
+ ]);
405
+ if (result.exitCode !== 0)
406
+ return null;
407
+ const raw = result.stdout.trim();
408
+ if (!raw)
409
+ return null;
410
+ const at = raw.lastIndexOf("@");
411
+ if (at < 0)
412
+ return null;
413
+ const digest = raw.slice(at + 1);
414
+ return /^sha256:[a-f0-9]{64}$/.test(digest) ? digest : null;
415
+ }
416
+ /**
417
+ * Return the digest of the image a *running* container is actually
418
+ * using, in the form `sha256:<hex>` or `local:<image-id>`.
419
+ *
420
+ * Distinct from "what `image:tag` resolves to right now": if someone
421
+ * `podman pull`'d the image after the container started, the local
422
+ * tag moved but the container is still on the old bits. This walks
423
+ * from the container's image-id (immutable for its lifetime) to its
424
+ * RepoDigests.
425
+ *
426
+ * Returns null if the container doesn't exist or the runtime can't
427
+ * read its image-id.
428
+ */
429
+ export async function getLiveContainerDigest(runtime, containerName, exec = execRuntime) {
430
+ // The caller passes the unprefixed name from `ensureRunning`; the
431
+ // running container always carries the `sk-` prefix.
432
+ const imageId = await getImageDigest(runtime, prefixedName(containerName), exec);
433
+ if (!imageId)
434
+ return null;
435
+ const repoDigest = await getRepoDigest(runtime, imageId, exec);
436
+ if (repoDigest)
437
+ return repoDigest;
438
+ // Locally-built image with no RepoDigests — return the local id
439
+ // under the same `local:` namespace the resolver uses.
440
+ return `local:${imageId}`;
441
+ }
442
+ export async function pullImage(runtime, image, onProgress) {
443
+ const { exitCode, log } = await execRuntimeLong(runtime, ["pull", image], onProgress, 300000);
443
444
  if (exitCode !== 0) {
444
445
  throw new Error(`Failed to pull ${image}: ${log.slice(-5).join("\n")}`);
445
446
  }
446
447
  }
447
- async function getContainerState(runtime, name, exec = runtime_1.execRuntime) {
448
+ export async function getContainerState(runtime, name, exec = execRuntime) {
448
449
  const fullName = prefixedName(name);
449
450
  // Query multiple state fields and treat the container as running if
450
451
  // ANY of them indicate running. Rationale: rootless podman on some
@@ -502,7 +503,7 @@ async function getContainerState(runtime, name, exec = runtime_1.execRuntime) {
502
503
  *
503
504
  * `exec` defaults to the production execRuntime; tests pass a stub.
504
505
  */
505
- async function getLiveResources(runtime, name, exec = runtime_1.execRuntime) {
506
+ export async function getLiveResources(runtime, name, exec = execRuntime) {
506
507
  const fullName = prefixedName(name);
507
508
  // Use Go-template format for reliable parsing across podman/docker.
508
509
  // Each line is one numeric or string value; empty/zero means "unset".
@@ -599,7 +600,7 @@ function bytesToString(bytes) {
599
600
  * already handle "no binding found" so a parse error degrades gracefully into
600
601
  * "leave the existing cache alone".
601
602
  */
602
- function parsePortBindings(json) {
603
+ export function parsePortBindings(json) {
603
604
  const out = new Map();
604
605
  if (!json || json === "null")
605
606
  return out;
@@ -648,7 +649,7 @@ function parsePortBindings(json) {
648
649
  * Returns an empty map on inspect failure; callers fall back to whatever they
649
650
  * had pre-validation.
650
651
  */
651
- async function getActualPortBindings(runtime, name, exec = runtime_1.execRuntime) {
652
+ export async function getActualPortBindings(runtime, name, exec = execRuntime) {
652
653
  const fullName = prefixedName(name);
653
654
  const result = await exec(runtime, [
654
655
  "inspect",
@@ -669,7 +670,7 @@ async function getActualPortBindings(runtime, name, exec = runtime_1.execRuntime
669
670
  * what the container was actually created with, so a recreate can fire
670
671
  * automatically instead of silently ignoring the new config until restart.
671
672
  */
672
- async function getLiveContainerConfig(runtime, name, exec = runtime_1.execRuntime) {
673
+ export async function getLiveContainerConfig(runtime, name, exec = execRuntime) {
673
674
  const fullName = prefixedName(name);
674
675
  // Sentinel between sections — `\x1f` (ASCII unit separator) avoids any
675
676
  // collision with shell-meta or path characters that might appear inside
@@ -685,28 +686,40 @@ async function getLiveContainerConfig(runtime, name, exec = runtime_1.execRuntim
685
686
  SEP +
686
687
  "{{json .Config.Env}}" +
687
688
  SEP +
688
- "{{json .HostConfig.PortBindings}}";
689
+ "{{json .HostConfig.PortBindings}}" +
690
+ SEP +
691
+ "{{json .HostConfig.ExtraHosts}}" +
692
+ SEP +
693
+ "{{.Config.User}}";
689
694
  const result = await exec(runtime, ["inspect", "--format", fmt, fullName]);
690
695
  if (result.exitCode !== 0)
691
696
  return null;
692
697
  const parts = result.stdout.split(SEP);
693
- if (parts.length !== 6)
698
+ if (parts.length !== 8)
694
699
  return null;
695
- const [rawImage, rawCmd, rawNetworkMode, rawBinds, rawEnv, rawPortBindings] = parts;
696
- // Split image into image+tag on the LAST colon (registries can carry
697
- // ports like `localhost:5000/foo:tag`). If no tag, default to "latest"
698
- // matches what `qualifyImage` would have produced.
700
+ const [rawImage, rawCmd, rawNetworkMode, rawBinds, rawEnv, rawPortBindings, rawExtraHosts, rawUser,] = parts;
701
+ // Split image into image+tag (and optional digest). Config.Image can
702
+ // be `repo:tag`, `repo@sha256:...`, or `repo:tag@sha256:...`.
703
+ // Registries with ports (`localhost:5000/foo:tag`) push the
704
+ // image-vs-tag colon past the last slash.
699
705
  const imageRef = rawImage.trim();
700
- const lastColon = imageRef.lastIndexOf(":");
701
- const lastSlash = imageRef.lastIndexOf("/");
706
+ let imageAndTag = imageRef;
707
+ let digest = null;
708
+ const atIdx = imageRef.indexOf("@sha256:");
709
+ if (atIdx >= 0) {
710
+ imageAndTag = imageRef.slice(0, atIdx);
711
+ digest = imageRef.slice(atIdx + 1);
712
+ }
713
+ const lastColon = imageAndTag.lastIndexOf(":");
714
+ const lastSlash = imageAndTag.lastIndexOf("/");
702
715
  let image;
703
716
  let tag;
704
717
  if (lastColon > lastSlash) {
705
- image = imageRef.slice(0, lastColon);
706
- tag = imageRef.slice(lastColon + 1);
718
+ image = imageAndTag.slice(0, lastColon);
719
+ tag = imageAndTag.slice(lastColon + 1);
707
720
  }
708
721
  else {
709
- image = imageRef;
722
+ image = imageAndTag;
710
723
  tag = "latest";
711
724
  }
712
725
  let command = null;
@@ -770,7 +783,39 @@ async function getLiveContainerConfig(runtime, name, exec = runtime_1.execRuntim
770
783
  // leave empty
771
784
  }
772
785
  const portBindings = parsePortBindingsFromJsonString(rawPortBindings);
773
- return { image, tag, command, networkMode, env, binds, portBindings };
786
+ const extraHosts = new Map();
787
+ try {
788
+ const parsed = JSON.parse(rawExtraHosts);
789
+ if (Array.isArray(parsed)) {
790
+ for (const entry of parsed) {
791
+ if (typeof entry !== "string")
792
+ continue;
793
+ // ExtraHosts format: "hostname:ipaddress"
794
+ const colon = entry.indexOf(":");
795
+ if (colon < 0)
796
+ continue;
797
+ const hostname = entry.slice(0, colon);
798
+ const ip = entry.slice(colon + 1);
799
+ extraHosts.set(hostname, ip);
800
+ }
801
+ }
802
+ }
803
+ catch {
804
+ // leave empty
805
+ }
806
+ const user = (rawUser ?? "").trim();
807
+ return {
808
+ image,
809
+ tag,
810
+ digest,
811
+ command,
812
+ networkMode,
813
+ env,
814
+ binds,
815
+ portBindings,
816
+ extraHosts,
817
+ user,
818
+ };
774
819
  }
775
820
  /**
776
821
  * Wrapper around `parsePortBindings` that keeps the container-port key as
@@ -897,10 +942,12 @@ function bindingsEqual(a, b) {
897
942
  * - ports: container-port key compared as runtime-emitted (`9000/tcp`).
898
943
  * Multiple host bindings per container port compared as a sorted set.
899
944
  */
900
- function diffContainerConfig(requested, live, runtime, prior) {
945
+ export function diffContainerConfig(requested, live, runtime, prior) {
901
946
  const drifted = [];
902
- const requestedImageRef = qualifyImage(`${requested.image}:${requested.tag}`, runtime);
903
- const liveImageRef = qualifyImage(`${live.image}:${live.tag}`, runtime);
947
+ const requestedImageRef = qualifyImage(requested.digest
948
+ ? `${requested.image}@${requested.digest}`
949
+ : `${requested.image}:${requested.tag}`, runtime);
950
+ const liveImageRef = qualifyImage(live.digest ? `${live.image}@${live.digest}` : `${live.image}:${live.tag}`, runtime);
904
951
  if (requestedImageRef !== liveImageRef)
905
952
  drifted.push("image+tag");
906
953
  if (requested.command !== undefined) {
@@ -989,11 +1036,55 @@ function diffContainerConfig(requested, live, runtime, prior) {
989
1036
  }
990
1037
  if (portsDrift)
991
1038
  drifted.push("ports");
1039
+ // ExtraHosts: build canonical Map<hostname, ip> for each side.
1040
+ const requestedExtraHosts = new Map();
1041
+ if (requested.extraHosts) {
1042
+ for (const [hostname, ip] of Object.entries(requested.extraHosts)) {
1043
+ requestedExtraHosts.set(hostname, ip);
1044
+ }
1045
+ }
1046
+ // This plugin injects host.containers.internal:host-gateway in
1047
+ // buildRunArgs() for Docker so containers can reach the host the
1048
+ // same way Podman does natively. Mirror that here so the live
1049
+ // ExtraHosts (which records the flag) doesn't fire false drift —
1050
+ // but only when the user didn't supply their own override for the
1051
+ // same key.
1052
+ if (runtime.runtime === "docker" &&
1053
+ !requestedExtraHosts.has("host.containers.internal")) {
1054
+ requestedExtraHosts.set("host.containers.internal", "host-gateway");
1055
+ }
1056
+ let extraHostsDrift = requestedExtraHosts.size !== live.extraHosts.size;
1057
+ if (!extraHostsDrift) {
1058
+ for (const [hostname, ip] of requestedExtraHosts) {
1059
+ if (live.extraHosts.get(hostname) !== ip) {
1060
+ extraHostsDrift = true;
1061
+ break;
1062
+ }
1063
+ }
1064
+ }
1065
+ if (extraHostsDrift)
1066
+ drifted.push("extraHosts");
1067
+ // User/ownership drift. Compute the `--user uid:gid` form the
1068
+ // translator would emit and compare to live `Config.User`. The
1069
+ // `--userns=keep-id` flag (rootless Podman) doesn't surface in
1070
+ // `Config.User`, so we only fire drift when the expected form is
1071
+ // a `--user` flag — that is, opt-out, rootless-Podman, and
1072
+ // unavailable-hostUser all suppress drift on this field by design.
1073
+ const expectedUserFlags = userMappingFlags(runtime, requested.user);
1074
+ const userIdx = expectedUserFlags.indexOf("--user");
1075
+ const expectedUser = userIdx >= 0 && userIdx + 1 < expectedUserFlags.length
1076
+ ? expectedUserFlags[userIdx + 1]
1077
+ : null;
1078
+ if (expectedUser !== null && expectedUser !== live.user) {
1079
+ drifted.push("user");
1080
+ }
992
1081
  return { drifted };
993
1082
  }
994
1083
  function buildRunArgs(name, config, runtime) {
995
1084
  const fullName = prefixedName(name);
996
- const imageRef = qualifyImage(`${config.image}:${config.tag}`, runtime);
1085
+ const imageRef = qualifyImage(config.digest
1086
+ ? `${config.image}@${config.digest}`
1087
+ : `${config.image}:${config.tag}`, runtime);
997
1088
  const args = ["run", "-d", "--name", fullName];
998
1089
  if (config.restart && config.restart !== "no") {
999
1090
  args.push("--restart", config.restart);
@@ -1001,6 +1092,11 @@ function buildRunArgs(name, config, runtime) {
1001
1092
  if (config.networkMode) {
1002
1093
  args.push("--network", config.networkMode);
1003
1094
  }
1095
+ // UID/GID alignment so files created inside the container on bind-
1096
+ // mounted host paths land owned by the host caller. Same decision
1097
+ // matrix as `runJob` (see `userMappingFlags`). `config.user === false`
1098
+ // opts out; the in-image UID/GID defaults to 0 when not declared.
1099
+ args.push(...userMappingFlags(runtime, config.user));
1004
1100
  if (config.ports) {
1005
1101
  for (const [containerPort, hostBind] of Object.entries(config.ports)) {
1006
1102
  const port = containerPort.replace(/\/tcp$/, "");
@@ -1017,17 +1113,32 @@ function buildRunArgs(name, config, runtime) {
1017
1113
  args.push("-e", `${key}=${value}`);
1018
1114
  }
1019
1115
  }
1116
+ // Add extra hosts: user-provided + (for Docker) the
1117
+ // host.containers.internal:host-gateway mapping Podman provides
1118
+ // natively. Skip the Docker injection if the user already supplied
1119
+ // their own value for the same key to avoid duplicate /etc/hosts
1120
+ // entries and the implicit first-match-wins override.
1121
+ const userHasInternalOverride = !!config.extraHosts &&
1122
+ Object.prototype.hasOwnProperty.call(config.extraHosts, "host.containers.internal");
1123
+ if (config.extraHosts) {
1124
+ for (const [hostname, ip] of Object.entries(config.extraHosts)) {
1125
+ args.push("--add-host", `${hostname}:${ip}`);
1126
+ }
1127
+ }
1128
+ if (runtime.runtime === "docker" && !userHasInternalOverride) {
1129
+ args.push("--add-host", "host.containers.internal:host-gateway");
1130
+ }
1020
1131
  // Resource limits (--cpus, --memory, --pids-limit, etc.)
1021
1132
  // Fields whose backing cgroup controller is unavailable on this
1022
1133
  // runtime are silently dropped.
1023
- args.push(...(0, resources_1.resourceFlagsForRun)(config.resources, runtime));
1134
+ args.push(...resourceFlagsForRun(config.resources, runtime));
1024
1135
  args.push(imageRef);
1025
1136
  if (config.command) {
1026
1137
  args.push(...config.command);
1027
1138
  }
1028
1139
  return args;
1029
1140
  }
1030
- async function ensureRunning(runtime, name, config, debug, options, exec = runtime_1.execRuntime,
1141
+ export async function ensureRunning(runtime, name, config, debug, options, exec = execRuntime,
1031
1142
  /**
1032
1143
  * Prior `ContainerConfig` from the previous `ensureRunning` call within
1033
1144
  * this signalk-container lifetime, if any. Used to detect "unset" drift
@@ -1039,7 +1150,9 @@ async function ensureRunning(runtime, name, config, debug, options, exec = runti
1039
1150
  prior, _postRecreate = false) {
1040
1151
  const state = await getContainerState(runtime, name, exec);
1041
1152
  const fullName = prefixedName(name);
1042
- const imageRef = qualifyImage(`${config.image}:${config.tag}`, runtime);
1153
+ const imageRef = qualifyImage(config.digest
1154
+ ? `${config.image}@${config.digest}`
1155
+ : `${config.image}:${config.tag}`, runtime);
1043
1156
  // Drift detection is needed for both "running" and "stopped" — a stopped
1044
1157
  // container with stale env/volumes/ports/command would otherwise be
1045
1158
  // re-started with the OLD config. Inspect works on both states.
@@ -1106,14 +1219,14 @@ prior, _postRecreate = false) {
1106
1219
  }
1107
1220
  }
1108
1221
  }
1109
- async function startContainer(runtime, name) {
1222
+ export async function startContainer(runtime, name) {
1110
1223
  const fullName = prefixedName(name);
1111
- const result = await (0, runtime_1.execRuntime)(runtime, ["start", fullName]);
1224
+ const result = await execRuntime(runtime, ["start", fullName]);
1112
1225
  if (result.exitCode !== 0) {
1113
1226
  throw new Error(`Failed to start ${fullName}: ${result.stderr}`);
1114
1227
  }
1115
1228
  }
1116
- async function fixVolumePermissions(runtime, name, exec = runtime_1.execRuntime) {
1229
+ async function fixVolumePermissions(runtime, name, exec = execRuntime) {
1117
1230
  const fullName = prefixedName(name);
1118
1231
  const state = await getContainerState(runtime, name, exec);
1119
1232
  if (state !== "running")
@@ -1134,10 +1247,10 @@ async function fixVolumePermissions(runtime, name, exec = runtime_1.execRuntime)
1134
1247
  // back silently if chmod isn't available in the image (distroless etc.).
1135
1248
  await exec(runtime, ["exec", fullName, "chmod", "-R", "o+rwX", ...mounts]);
1136
1249
  }
1137
- async function stopContainer(runtime, name) {
1250
+ export async function stopContainer(runtime, name) {
1138
1251
  const fullName = prefixedName(name);
1139
1252
  await fixVolumePermissions(runtime, name).catch(() => { });
1140
- const result = await (0, runtime_1.execRuntime)(runtime, ["stop", fullName]);
1253
+ const result = await execRuntime(runtime, ["stop", fullName]);
1141
1254
  if (result.exitCode !== 0) {
1142
1255
  const state = await getContainerState(runtime, name);
1143
1256
  if (state !== "stopped" && state !== "missing") {
@@ -1145,7 +1258,7 @@ async function stopContainer(runtime, name) {
1145
1258
  }
1146
1259
  }
1147
1260
  }
1148
- async function removeContainer(runtime, name, exec = runtime_1.execRuntime) {
1261
+ export async function removeContainer(runtime, name, exec = execRuntime) {
1149
1262
  const fullName = prefixedName(name);
1150
1263
  await fixVolumePermissions(runtime, name, exec).catch(() => { });
1151
1264
  await exec(runtime, ["stop", fullName]);
@@ -1154,8 +1267,8 @@ async function removeContainer(runtime, name, exec = runtime_1.execRuntime) {
1154
1267
  throw new Error(`Failed to remove ${fullName}: ${result.stderr}`);
1155
1268
  }
1156
1269
  }
1157
- async function listContainers(runtime) {
1158
- const result = await (0, runtime_1.execRuntime)(runtime, [
1270
+ export async function listContainers(runtime) {
1271
+ const result = await execRuntime(runtime, [
1159
1272
  "ps",
1160
1273
  "-a",
1161
1274
  "--filter",
@@ -1183,8 +1296,8 @@ async function listContainers(runtime) {
1183
1296
  };
1184
1297
  });
1185
1298
  }
1186
- async function pruneImages(runtime) {
1187
- const result = await (0, runtime_1.execRuntime)(runtime, ["image", "prune", "-f"]);
1299
+ export async function pruneImages(runtime) {
1300
+ const result = await execRuntime(runtime, ["image", "prune", "-f"]);
1188
1301
  if (result.exitCode !== 0) {
1189
1302
  throw new Error(`Prune failed: ${result.stderr}`);
1190
1303
  }
@@ -1195,28 +1308,28 @@ async function pruneImages(runtime) {
1195
1308
  spaceReclaimed: reclaimedMatch?.[1] ?? "0 B",
1196
1309
  };
1197
1310
  }
1198
- async function execInContainer(runtime, name, command) {
1311
+ export async function execInContainer(runtime, name, command) {
1199
1312
  const fullName = prefixedName(name);
1200
- return (0, runtime_1.execRuntime)(runtime, ["exec", fullName, ...command]);
1313
+ return execRuntime(runtime, ["exec", fullName, ...command]);
1201
1314
  }
1202
- async function ensureNetwork(runtime, name) {
1203
- const inspect = await (0, runtime_1.execRuntime)(runtime, ["network", "inspect", name]);
1315
+ export async function ensureNetwork(runtime, name) {
1316
+ const inspect = await execRuntime(runtime, ["network", "inspect", name]);
1204
1317
  if (inspect.exitCode !== 0) {
1205
- const create = await (0, runtime_1.execRuntime)(runtime, ["network", "create", name]);
1318
+ const create = await execRuntime(runtime, ["network", "create", name]);
1206
1319
  if (create.exitCode !== 0 && !create.stderr.includes("already exists")) {
1207
1320
  throw new Error(`Failed to create network ${name}: ${create.stderr}`);
1208
1321
  }
1209
1322
  }
1210
1323
  }
1211
- async function removeNetwork(runtime, name) {
1212
- const result = await (0, runtime_1.execRuntime)(runtime, ["network", "rm", name]);
1324
+ export async function removeNetwork(runtime, name) {
1325
+ const result = await execRuntime(runtime, ["network", "rm", name]);
1213
1326
  if (result.exitCode !== 0 && !result.stderr.includes("not found")) {
1214
1327
  throw new Error(`Failed to remove network ${name}: ${result.stderr}`);
1215
1328
  }
1216
1329
  }
1217
- async function connectToNetwork(runtime, containerName, networkName) {
1330
+ export async function connectToNetwork(runtime, containerName, networkName) {
1218
1331
  const fullName = prefixedName(containerName);
1219
- const result = await (0, runtime_1.execRuntime)(runtime, [
1332
+ const result = await execRuntime(runtime, [
1220
1333
  "network",
1221
1334
  "connect",
1222
1335
  networkName,
@@ -1230,9 +1343,9 @@ async function connectToNetwork(runtime, containerName, networkName) {
1230
1343
  throw new Error(`Failed to connect ${fullName} to ${networkName}: ${result.stderr}`);
1231
1344
  }
1232
1345
  }
1233
- async function disconnectFromNetwork(runtime, containerName, networkName) {
1346
+ export async function disconnectFromNetwork(runtime, containerName, networkName) {
1234
1347
  const fullName = prefixedName(containerName);
1235
- const result = await (0, runtime_1.execRuntime)(runtime, [
1348
+ const result = await execRuntime(runtime, [
1236
1349
  "network",
1237
1350
  "disconnect",
1238
1351
  networkName,
@@ -1266,7 +1379,7 @@ async function disconnectFromNetwork(runtime, containerName, networkName) {
1266
1379
  * 12+ hex char run; `docker inspect` will reject false positives
1267
1380
  * (e.g. systemd slice names that happen to be hex).
1268
1381
  */
1269
- function parseSelfContainerIdFromCgroup(line) {
1382
+ export function parseSelfContainerIdFromCgroup(line) {
1270
1383
  // 1) cgroup v1 path: `/<runtime>/<id>` where runtime is `docker`,
1271
1384
  // `kubepods/...`, etc.
1272
1385
  const v1 = line.match(/[/]([0-9a-f]{12,64})(?:[/.]|$)/i);
@@ -1287,7 +1400,7 @@ function parseSelfContainerIdFromCgroup(line) {
1287
1400
  * tests can drive the multi-line / dedup logic without touching
1288
1401
  * `/proc/self/cgroup` (which varies across test hosts).
1289
1402
  */
1290
- function parseSelfContainerIdsFromCgroupFile(content) {
1403
+ export function parseSelfContainerIdsFromCgroupFile(content) {
1291
1404
  const seen = new Set();
1292
1405
  const ids = [];
1293
1406
  for (const line of content.split("\n")) {
@@ -1316,10 +1429,10 @@ function parseSelfContainerIdsFromCgroupFile(content) {
1316
1429
  *
1317
1430
  * Skipped in production paths when `isContainerized()` is false.
1318
1431
  */
1319
- function readSelfContainerIdsFromCgroup() {
1432
+ export function readSelfContainerIdsFromCgroup() {
1320
1433
  let content;
1321
1434
  try {
1322
- content = (0, node_fs_1.readFileSync)("/proc/self/cgroup", "utf8");
1435
+ content = readFileSync("/proc/self/cgroup", "utf8");
1323
1436
  }
1324
1437
  catch {
1325
1438
  return [];
@@ -1337,7 +1450,7 @@ function readSelfContainerIdsFromCgroup() {
1337
1450
  async function tryInspect(runtime, candidateId, debug, source) {
1338
1451
  if (!candidateId)
1339
1452
  return null;
1340
- const result = await (0, runtime_1.execRuntime)(runtime, [
1453
+ const result = await execRuntime(runtime, [
1341
1454
  "inspect",
1342
1455
  "--format",
1343
1456
  "{{.Id}}",
@@ -1371,7 +1484,7 @@ async function tryInspect(runtime, candidateId, debug, source) {
1371
1484
  * unset" behaviour — fall back to bare-metal-style handling, or
1372
1485
  * return null to the caller (depends on the consumer).
1373
1486
  */
1374
- async function findSelfContainerId(runtime, debug = () => { }) {
1487
+ export async function findSelfContainerId(runtime, debug = () => { }) {
1375
1488
  // 1. Explicit override — no `inspect` validation needed since the
1376
1489
  // operator chose this deliberately. An invalid value here
1377
1490
  // surfaces as the same downstream error the user would have
@@ -1413,8 +1526,8 @@ async function findSelfContainerId(runtime, debug = () => { }) {
1413
1526
  * a ContainerConfig. The content visible at mountPoint inside the managed
1414
1527
  * container will always correspond to the root of dataDir.
1415
1528
  */
1416
- async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
1417
- if (!(0, runtime_1.isContainerized)()) {
1529
+ export async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
1530
+ if (!isContainerized()) {
1418
1531
  // Running bare-metal: dataDir is already a host filesystem path.
1419
1532
  return dataDir;
1420
1533
  }
@@ -1427,7 +1540,7 @@ async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
1427
1540
  debug(`resolveSignalkDataSource: could not detect self container id; falling back to dataDir=${dataDir}`);
1428
1541
  return dataDir;
1429
1542
  }
1430
- const result = await (0, runtime_1.execRuntime)(runtime, [
1543
+ const result = await execRuntime(runtime, [
1431
1544
  "inspect",
1432
1545
  "--format",
1433
1546
  "{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}\n{{end}}",
@@ -1477,7 +1590,7 @@ async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
1477
1590
  * logic can be unit-tested independently of the runtime. Returns null
1478
1591
  * when no mount covers `absPath`.
1479
1592
  */
1480
- function resolveHostPathFromMounts(absPath, mounts) {
1593
+ export function resolveHostPathFromMounts(absPath, mounts) {
1481
1594
  // Longest-prefix match. Same logic as resolveSignalkDataSource — handles
1482
1595
  // exact matches and parent-directory mounts uniformly.
1483
1596
  let best = null;
@@ -1531,8 +1644,8 @@ function resolveHostPathFromMounts(absPath, mounts) {
1531
1644
  * should surface an actionable error (not silently fall through, as
1532
1645
  * `resolveSignalkDataSource` does for backwards-compat).
1533
1646
  */
1534
- async function resolveHostPath(absPath, runtime, debug = () => { }) {
1535
- if (!(0, runtime_1.isContainerized)()) {
1647
+ export async function resolveHostPath(absPath, runtime, debug = () => { }) {
1648
+ if (!isContainerized()) {
1536
1649
  // Bare-metal: the absolute path IS the host path. No subpath needed.
1537
1650
  return { source: absPath, subPath: "" };
1538
1651
  }
@@ -1544,7 +1657,7 @@ async function resolveHostPath(absPath, runtime, debug = () => { }) {
1544
1657
  debug(`resolveHostPath: could not detect self container id; cannot resolve ${absPath}`);
1545
1658
  return null;
1546
1659
  }
1547
- const result = await (0, runtime_1.execRuntime)(runtime, [
1660
+ const result = await execRuntime(runtime, [
1548
1661
  "inspect",
1549
1662
  "--format",
1550
1663
  "{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}\n{{end}}",
@@ -1584,7 +1697,7 @@ const reservedPorts = new Set();
1584
1697
  * (so the OS-level bind now prevents collisions), or when the container
1585
1698
  * creation failed (so the next attempt can re-probe freely).
1586
1699
  */
1587
- function releaseReservedPort(port) {
1700
+ export function releaseReservedPort(port) {
1588
1701
  reservedPorts.delete(port);
1589
1702
  }
1590
1703
  /**
@@ -1614,7 +1727,7 @@ function isPortAvailable(port) {
1614
1727
  * Used by the `signalkAccessiblePorts` bare-metal path to prefer the
1615
1728
  * declared port number while gracefully stepping over conflicts.
1616
1729
  */
1617
- async function findAvailablePort(preferred) {
1730
+ export async function findAvailablePort(preferred) {
1618
1731
  for (let port = preferred; port <= 65535; port++) {
1619
1732
  if (reservedPorts.has(port))
1620
1733
  continue;
@@ -1645,8 +1758,8 @@ async function findAvailablePort(preferred) {
1645
1758
  * back to `networkMode: container:<self-container-id>`. A non-empty
1646
1759
  * array contains the user-defined network names to attach to.
1647
1760
  */
1648
- async function resolveSignalkNetworks(runtime, debug = () => { }) {
1649
- if (!(0, runtime_1.isContainerized)())
1761
+ export async function resolveSignalkNetworks(runtime, debug = () => { }) {
1762
+ if (!isContainerized())
1650
1763
  return null;
1651
1764
  // Cascade detection — see `findSelfContainerId`. Critically this fixes
1652
1765
  // `network_mode: host` deployments where HOSTNAME is the host machine
@@ -1656,7 +1769,7 @@ async function resolveSignalkNetworks(runtime, debug = () => { }) {
1656
1769
  debug("resolveSignalkNetworks: could not detect self container id, returning null");
1657
1770
  return null;
1658
1771
  }
1659
- const result = await (0, runtime_1.execRuntime)(runtime, [
1772
+ const result = await execRuntime(runtime, [
1660
1773
  "inspect",
1661
1774
  "--format",
1662
1775
  "{{range $k,$v := .NetworkSettings.Networks}}{{$k}}\n{{end}}",
@@ -1673,7 +1786,7 @@ async function resolveSignalkNetworks(runtime, debug = () => { }) {
1673
1786
  debug(`resolveSignalkNetworks: all=${all.join(",")} userDefined=${userDefined.join(",")}`);
1674
1787
  return userDefined;
1675
1788
  }
1676
- async function waitForReady(url, timeoutMs = 30000, intervalMs = 500) {
1789
+ export async function waitForReady(url, timeoutMs = 30000, intervalMs = 500) {
1677
1790
  const deadline = Date.now() + timeoutMs;
1678
1791
  while (Date.now() < deadline) {
1679
1792
  try {