@pushpalsdev/cli 1.0.48 → 1.0.50

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.
@@ -3180,20 +3180,18 @@ async function ensureWorkerpalDockerImageReady(opts) {
3180
3180
  const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
3181
3181
  console.log(`[pushpals] Checking WorkerPal sandbox image ${opts.dockerImage} for runtimeTag=${runtimeTag}...`);
3182
3182
  const inspection = await inspectImageRuntimeTagFn(dockerExecutable, opts.dockerImage, sandbox.root, opts.env);
3183
- if (inspection.status === "failed") {
3184
- return {
3185
- ok: false,
3186
- detail: inspection.detail
3187
- };
3188
- }
3189
- const existingRuntimeTag = inspection.runtimeTag;
3190
- if (existingRuntimeTag === runtimeTag) {
3183
+ const inspectFailureDetail = inspection.status === "failed" ? inspection.detail : "";
3184
+ const existingRuntimeTag = inspection.status === "ok" ? inspection.runtimeTag : "";
3185
+ if (inspection.status === "ok" && existingRuntimeTag === runtimeTag) {
3191
3186
  return {
3192
3187
  ok: true,
3193
3188
  detail: `WorkerPal sandbox image is ready locally (${opts.dockerImage}, runtimeTag=${runtimeTag})`
3194
3189
  };
3195
3190
  }
3196
- console.log(existingRuntimeTag ? `[pushpals] WorkerPal sandbox image ${opts.dockerImage} is stale (runtimeTag=${existingRuntimeTag}); rebuilding locally...` : `[pushpals] WorkerPal sandbox image ${opts.dockerImage} is missing; building locally...`);
3191
+ if (inspectFailureDetail) {
3192
+ console.warn(`[pushpals] ${inspectFailureDetail}`);
3193
+ }
3194
+ console.log(inspectFailureDetail ? `[pushpals] WorkerPal sandbox image ${opts.dockerImage} could not be inspected; attempting local rebuild...` : existingRuntimeTag ? `[pushpals] WorkerPal sandbox image ${opts.dockerImage} is stale (runtimeTag=${existingRuntimeTag}); rebuilding locally...` : `[pushpals] WorkerPal sandbox image ${opts.dockerImage} is missing; building locally...`);
3197
3195
  const build = await runCommandWithEnvFn([
3198
3196
  dockerExecutable,
3199
3197
  "build",
@@ -3211,12 +3209,12 @@ async function ensureWorkerpalDockerImageReady(opts) {
3211
3209
  const detail = build.stderr || build.stdout || `docker build exited ${build.exitCode}`;
3212
3210
  return {
3213
3211
  ok: false,
3214
- detail: `failed to build local WorkerPal sandbox image ${opts.dockerImage}: ${detail}`
3212
+ detail: inspectFailureDetail ? `${inspectFailureDetail}; failed to build local WorkerPal sandbox image ${opts.dockerImage}: ${detail}` : `failed to build local WorkerPal sandbox image ${opts.dockerImage}: ${detail}`
3215
3213
  };
3216
3214
  }
3217
3215
  return {
3218
3216
  ok: true,
3219
- detail: `built local WorkerPal sandbox image ${opts.dockerImage} for runtimeTag=${runtimeTag}`
3217
+ detail: inspectFailureDetail ? `rebuilt local WorkerPal sandbox image ${opts.dockerImage} for runtimeTag=${runtimeTag} after image inspection failed` : `built local WorkerPal sandbox image ${opts.dockerImage} for runtimeTag=${runtimeTag}`
3220
3218
  };
3221
3219
  }
3222
3220
  async function prepareEmbeddedWorkerpalDockerImageIfNeeded(opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,9 @@ const DEFAULT_CONFIG = loadPushPalsConfig();
40
40
  const SHARED_CONTAINER_VENV_PYTHON = "/workspace/.venv/bin/python";
41
41
  const WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL = "pushpals.runtime_tag";
42
42
  const WORKERPAL_SANDBOX_COMPONENT_LABEL = "pushpals.component=workerpals-sandbox";
43
+ const DOCKER_IMAGE_INSPECT_TIMEOUT_MS = 15_000;
44
+ const DOCKER_IMAGE_BUILD_TIMEOUT_MS = 10 * 60_000;
45
+ const DOCKER_IMAGE_PULL_TIMEOUT_MS = 10 * 60_000;
43
46
 
44
47
  function parseClampedInt(value: unknown, defaultValue: number, min: number, max: number): number {
45
48
  const parsed =
@@ -99,6 +102,10 @@ function dockerBuildFileArg(root: string, dockerfilePath: string): string {
99
102
  return relativePath || "apps/workerpals/Dockerfile.sandbox";
100
103
  }
101
104
 
105
+ function isMissingDockerImageDetail(detail: string): boolean {
106
+ return /\b(no such object|no such image|not found)\b/i.test(String(detail ?? ""));
107
+ }
108
+
102
109
  type ParsedWorktreeRecord = {
103
110
  path: string;
104
111
  detached: boolean;
@@ -1530,6 +1537,45 @@ export class DockerExecutor {
1530
1537
  await new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
1531
1538
  }
1532
1539
 
1540
+ private async runDockerCommandCapture(
1541
+ command: string[],
1542
+ opts: { cwd?: string; timeoutMs?: number } = {},
1543
+ ): Promise<{ stdout: string; stderr: string; exitCode: number; timedOut: boolean }> {
1544
+ const proc = Bun.spawn(command, {
1545
+ cwd: opts.cwd,
1546
+ stdout: "pipe",
1547
+ stderr: "pipe",
1548
+ });
1549
+ let timedOut = false;
1550
+ let timer: ReturnType<typeof setTimeout> | null = null;
1551
+ if (
1552
+ typeof opts.timeoutMs === "number" &&
1553
+ Number.isFinite(opts.timeoutMs) &&
1554
+ opts.timeoutMs > 0
1555
+ ) {
1556
+ timer = setTimeout(() => {
1557
+ timedOut = true;
1558
+ try {
1559
+ proc.kill();
1560
+ } catch {
1561
+ // best-effort timeout termination only
1562
+ }
1563
+ }, opts.timeoutMs);
1564
+ }
1565
+ const [stdout, stderr, exitCode] = await Promise.all([
1566
+ new Response(proc.stdout).text(),
1567
+ new Response(proc.stderr).text(),
1568
+ proc.exited,
1569
+ ]);
1570
+ if (timer) clearTimeout(timer);
1571
+ return {
1572
+ stdout: stdout.trim(),
1573
+ stderr: stderr.trim(),
1574
+ exitCode,
1575
+ timedOut,
1576
+ };
1577
+ }
1578
+
1533
1579
  private compactError(err: unknown): string {
1534
1580
  const text = err instanceof Error ? err.message : String(err);
1535
1581
  const normalized = text.replace(/\s+/g, " ").trim();
@@ -1862,19 +1908,21 @@ export class DockerExecutor {
1862
1908
  console.log(
1863
1909
  `[DockerExecutor] Local image is unavailable or unsuitable. Pulling: ${this.options.imageName}`,
1864
1910
  );
1865
- const proc = Bun.spawn([resolveDockerExecutable(), "pull", this.options.imageName], {
1866
- stdout: "pipe",
1867
- stderr: "pipe",
1868
- });
1869
-
1870
- const exitCode = await proc.exited;
1871
- if (exitCode === 0) {
1911
+ const pull = await this.runDockerCommandCapture(
1912
+ [resolveDockerExecutable(), "pull", this.options.imageName],
1913
+ { timeoutMs: DOCKER_IMAGE_PULL_TIMEOUT_MS },
1914
+ );
1915
+ if (!pull.timedOut && pull.exitCode === 0) {
1872
1916
  console.log(`[DockerExecutor] Image pulled successfully`);
1873
1917
  return true;
1874
1918
  }
1875
1919
 
1876
- const stderr = (await new Response(proc.stderr).text()).trim();
1877
- console.error(`[DockerExecutor] Failed to pull image: ${stderr}`);
1920
+ const detail = pull.stderr || pull.stdout || `docker pull exited ${pull.exitCode}`;
1921
+ console.error(
1922
+ `[DockerExecutor] Failed to pull image: ${
1923
+ pull.timedOut ? `timed out after ${DOCKER_IMAGE_PULL_TIMEOUT_MS}ms` : detail
1924
+ }`,
1925
+ );
1878
1926
 
1879
1927
  // Another process may have built/pulled the image while this pull was running.
1880
1928
  if (await this.imageExists()) {
@@ -1891,19 +1939,21 @@ export class DockerExecutor {
1891
1939
  * Check if the Docker image exists locally
1892
1940
  */
1893
1941
  private async imageExists(): Promise<boolean> {
1894
- const proc = Bun.spawn(
1942
+ const result = await this.runDockerCommandCapture(
1895
1943
  [resolveDockerExecutable(), "image", "inspect", this.options.imageName],
1896
- {
1897
- stdout: "pipe",
1898
- stderr: "pipe",
1899
- },
1944
+ { timeoutMs: DOCKER_IMAGE_INSPECT_TIMEOUT_MS },
1900
1945
  );
1901
- const exitCode = await proc.exited;
1902
- return exitCode === 0;
1946
+ if (result.timedOut) {
1947
+ console.warn(
1948
+ `[DockerExecutor] Timed out checking local image ${this.options.imageName}; treating it as unavailable and attempting rebuild.`,
1949
+ );
1950
+ return false;
1951
+ }
1952
+ return result.exitCode === 0;
1903
1953
  }
1904
1954
 
1905
1955
  private async inspectImageRuntimeTag(): Promise<string> {
1906
- const proc = Bun.spawn(
1956
+ const result = await this.runDockerCommandCapture(
1907
1957
  [
1908
1958
  resolveDockerExecutable(),
1909
1959
  "image",
@@ -1912,14 +1962,24 @@ export class DockerExecutor {
1912
1962
  `{{ index .Config.Labels "${WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL}" }}`,
1913
1963
  this.options.imageName,
1914
1964
  ],
1915
- {
1916
- stdout: "pipe",
1917
- stderr: "pipe",
1918
- },
1965
+ { timeoutMs: DOCKER_IMAGE_INSPECT_TIMEOUT_MS },
1919
1966
  );
1920
- const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
1921
- if (exitCode !== 0) return "";
1922
- const value = stdout.trim();
1967
+ if (result.timedOut) {
1968
+ console.warn(
1969
+ `[DockerExecutor] Timed out inspecting runtime tag for ${this.options.imageName}; treating the local image as stale and attempting rebuild.`,
1970
+ );
1971
+ return "";
1972
+ }
1973
+ if (result.exitCode !== 0) {
1974
+ const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
1975
+ if (!isMissingDockerImageDetail(detail)) {
1976
+ console.warn(
1977
+ `[DockerExecutor] Failed to inspect runtime tag for ${this.options.imageName}: ${detail}`,
1978
+ );
1979
+ }
1980
+ return "";
1981
+ }
1982
+ const value = result.stdout.trim();
1923
1983
  return value === "<no value>" ? "" : value;
1924
1984
  }
1925
1985
 
@@ -1947,21 +2007,19 @@ export class DockerExecutor {
1947
2007
  this.options.imageName,
1948
2008
  ".",
1949
2009
  ];
1950
- const proc = Bun.spawn(args, {
2010
+ const build = await this.runDockerCommandCapture(args, {
1951
2011
  cwd: sandboxContext.root,
1952
- stdout: "pipe",
1953
- stderr: "pipe",
2012
+ timeoutMs: DOCKER_IMAGE_BUILD_TIMEOUT_MS,
1954
2013
  });
1955
- const [stdout, stderr, exitCode] = await Promise.all([
1956
- new Response(proc.stdout).text(),
1957
- new Response(proc.stderr).text(),
1958
- proc.exited,
1959
- ]);
1960
- if (exitCode === 0) {
2014
+ if (!build.timedOut && build.exitCode === 0) {
1961
2015
  return true;
1962
2016
  }
1963
- const detail = stderr.trim() || stdout.trim() || `docker build exited ${exitCode}`;
1964
- console.error(`[DockerExecutor] Failed to build local image: ${detail}`);
2017
+ const detail = build.stderr || build.stdout || `docker build exited ${build.exitCode}`;
2018
+ console.error(
2019
+ `[DockerExecutor] Failed to build local image: ${
2020
+ build.timedOut ? `timed out after ${DOCKER_IMAGE_BUILD_TIMEOUT_MS}ms` : detail
2021
+ }`,
2022
+ );
1965
2023
  return false;
1966
2024
  }
1967
2025
 
@@ -45,6 +45,7 @@
45
45
  "test:prompt-policy": "bun test tests/prompt-policy.enforcement.test.ts",
46
46
  "test:cli:integration": "bun test tests/cli.invocation-logging.test.ts tests/cli.runtime-bootstrap.test.ts tests/client.runtime-bootstrap.test.ts tests/shared.client-preflight.test.ts",
47
47
  "test:cli:e2e": "bun test ./tests/integration/cli.e2e.ts",
48
+ "test:workerpals:e2e": "bun test ./tests/integration/workerpals.control-plane.e2e.ts",
48
49
  "test:start:e2e": "bun test ./tests/integration/start.e2e.ts",
49
50
  "test:root": "bun test tests",
50
51
  "test:protocol": "bun run tests/protocol.integration.ts",