@pushpalsdev/cli 1.0.17 → 1.0.18

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 (2) hide show
  1. package/dist/pushpals-cli.js +270 -16
  2. package/package.json +1 -1
@@ -25,6 +25,41 @@ import { relative, resolve as resolve2 } from "path";
25
25
  import { existsSync, readFileSync } from "fs";
26
26
  import { join, resolve, isAbsolute } from "path";
27
27
 
28
+ // ../shared/src/autonomy_policy.ts
29
+ var DRIVE_RE = /^[A-Za-z]:\//;
30
+ var SLASH_RE = /\/+/g;
31
+ function normalizeAutonomyComponentArea(value) {
32
+ const normalized = normalizeRepoRelativePath(value);
33
+ if (!normalized)
34
+ return null;
35
+ return normalized;
36
+ }
37
+ function normalizeRepoRelativePath(value) {
38
+ if (typeof value !== "string")
39
+ return null;
40
+ let path = value.trim();
41
+ if (!path)
42
+ return null;
43
+ path = path.normalize("NFC").replace(/\\/g, "/");
44
+ if (path.startsWith("/"))
45
+ return null;
46
+ if (DRIVE_RE.test(path))
47
+ return null;
48
+ path = path.replace(SLASH_RE, "/");
49
+ const out = [];
50
+ for (const rawSegment of path.split("/")) {
51
+ const segment = rawSegment.trim();
52
+ if (!segment || segment === ".")
53
+ continue;
54
+ if (segment === "..")
55
+ return null;
56
+ out.push(segment);
57
+ }
58
+ if (out.length === 0)
59
+ return null;
60
+ return out.join("/");
61
+ }
62
+
28
63
  // ../shared/src/local_network.ts
29
64
  var DEFAULT_LOCAL_LOOPBACK_HOST = "127.0.0.1";
30
65
  function isLoopbackHost(hostname) {
@@ -398,14 +433,26 @@ function loadPushPalsConfig(options = {}) {
398
433
  "tests/unit": 2
399
434
  };
400
435
  const remoteAutonomyDispatchByComponentRaw = asStringNumberRecord(remoteAutonomyNode.max_dispatch_per_hour_by_component);
401
- const remoteAutonomyDispatchByComponent = {
402
- ...remoteAutonomyDispatchByComponentCfg
436
+ const legacyAutonomyComponentAliasMap = new Map(Object.keys(remoteAutonomyDispatchByComponentCfg).flatMap((key) => {
437
+ const direct = normalizeAutonomyComponentArea(key);
438
+ const legacyUnderscore = normalizeAutonomyComponentArea(key.replace(/\//g, "_"));
439
+ const legacyHyphen = normalizeAutonomyComponentArea(key.replace(/\//g, "-"));
440
+ return [direct, legacyUnderscore, legacyHyphen].filter((value) => Boolean(value)).map((value) => [value, key]);
441
+ }));
442
+ const coerceAutonomyComponentConfigKey = (value) => {
443
+ const direct = normalizeAutonomyComponentArea(value);
444
+ const legacyAliasCandidate = normalizeAutonomyComponentArea(value.trim().toLowerCase().replace(/\\/g, "/").replace(/_+/g, "/").replace(/-+/g, "/").replace(/\/+/g, "/"));
445
+ if (legacyAliasCandidate && legacyAutonomyComponentAliasMap.has(legacyAliasCandidate)) {
446
+ return legacyAutonomyComponentAliasMap.get(legacyAliasCandidate) ?? legacyAliasCandidate;
447
+ }
448
+ return direct;
403
449
  };
404
- const normalizeAutonomyComponentKey = (value) => value.trim().toLowerCase().replace(/\\/g, "/").replace(/_+/g, "/").replace(/-+/g, "/").replace(/\/+/g, "/");
405
- const canonicalComponentByNormalized = new Map(Object.keys(remoteAutonomyDispatchByComponentCfg).map((key) => [normalizeAutonomyComponentKey(key), key]));
450
+ const remoteAutonomyDispatchByComponent = Object.fromEntries(Object.entries(remoteAutonomyDispatchByComponentCfg).map(([key, value]) => [
451
+ coerceAutonomyComponentConfigKey(key) ?? key,
452
+ value
453
+ ]));
406
454
  for (const [rawKey, rawValue] of Object.entries(remoteAutonomyDispatchByComponentRaw)) {
407
- const normalized = normalizeAutonomyComponentKey(rawKey);
408
- const canonical = canonicalComponentByNormalized.get(normalized);
455
+ const canonical = coerceAutonomyComponentConfigKey(rawKey);
409
456
  if (!canonical)
410
457
  continue;
411
458
  const parsed = typeof rawValue === "number" ? rawValue : typeof rawValue === "string" ? Number.parseInt(rawValue.trim(), 10) : Number.NaN;
@@ -1369,9 +1416,9 @@ function parsePositiveInt(value, fallback) {
1369
1416
  function jsonHtmlBootstrap(value) {
1370
1417
  return JSON.stringify(value).replace(/</g, "\\u003c");
1371
1418
  }
1372
- async function runGitWithEnv(args, cwd, env) {
1419
+ async function runCommandWithEnv(command, cwd, env) {
1373
1420
  try {
1374
- const proc = Bun.spawn(["git", ...args], {
1421
+ const proc = Bun.spawn(command, {
1375
1422
  cwd,
1376
1423
  env,
1377
1424
  stdout: "pipe",
@@ -1392,6 +1439,9 @@ async function runGitWithEnv(args, cwd, env) {
1392
1439
  };
1393
1440
  }
1394
1441
  }
1442
+ async function runGitWithEnv(args, cwd, env) {
1443
+ return await runCommandWithEnv(["git", ...args], cwd, env);
1444
+ }
1395
1445
  async function runGit(args, cwd) {
1396
1446
  return await runGitWithEnv(args, cwd, {
1397
1447
  ...process.env,
@@ -1744,7 +1794,9 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1744
1794
  PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
1745
1795
  ...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
1746
1796
  ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {},
1747
- ...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {}
1797
+ ...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {},
1798
+ ...typeof env.PUSHPALS_DOCKER_BIN === "string" && env.PUSHPALS_DOCKER_BIN.trim() ? { PUSHPALS_DOCKER_BIN: env.PUSHPALS_DOCKER_BIN.trim() } : {},
1799
+ ...typeof env.PUSHPALS_DOCKER_BIN_ABSOLUTE === "string" && env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() ? { PUSHPALS_DOCKER_BIN_ABSOLUTE: env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() } : {}
1748
1800
  };
1749
1801
  }
1750
1802
  function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
@@ -2020,6 +2072,19 @@ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = p
2020
2072
  }
2021
2073
  return env;
2022
2074
  }
2075
+ function applyResolvedDockerBinaryToRuntimeEnv(env, resolvedDockerBinary, platform = process.platform) {
2076
+ const resolvedPath = String(resolvedDockerBinary ?? "").trim();
2077
+ if (!resolvedPath)
2078
+ return env;
2079
+ prependExecutableDirToPath(env, resolvedPath, platform);
2080
+ env.PUSHPALS_DOCKER_BIN = basename(resolvedPath);
2081
+ if (resolvedPath.includes("/") || resolvedPath.includes("\\")) {
2082
+ env.PUSHPALS_DOCKER_BIN_ABSOLUTE = resolvedPath;
2083
+ } else {
2084
+ delete env.PUSHPALS_DOCKER_BIN_ABSOLUTE;
2085
+ }
2086
+ return env;
2087
+ }
2023
2088
  function resolveRuntimeGitExecutableCandidates(env, platform = process.platform) {
2024
2089
  const candidates = [];
2025
2090
  const seen = new Set;
@@ -2039,6 +2104,25 @@ function resolveRuntimeGitExecutableCandidates(env, platform = process.platform)
2039
2104
  pushCandidate("git");
2040
2105
  return candidates;
2041
2106
  }
2107
+ function resolveRuntimeDockerExecutableCandidates(env, platform = process.platform) {
2108
+ const candidates = [];
2109
+ const seen = new Set;
2110
+ const pushCandidate = (value) => {
2111
+ const trimmed = String(value ?? "").trim();
2112
+ if (!trimmed)
2113
+ return;
2114
+ const key = platform === "win32" ? trimmed.toLowerCase() : trimmed;
2115
+ if (seen.has(key))
2116
+ return;
2117
+ seen.add(key);
2118
+ candidates.push(trimmed);
2119
+ };
2120
+ pushCandidate(env.PUSHPALS_DOCKER_BIN ?? "");
2121
+ pushCandidate(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? "");
2122
+ pushCandidate(platform === "win32" ? "docker.exe" : "docker");
2123
+ pushCandidate("docker");
2124
+ return candidates;
2125
+ }
2042
2126
  function resolveWindowsShellExecutableCandidatesForEnv(env, platform = process.platform) {
2043
2127
  if (platform !== "win32")
2044
2128
  return [];
@@ -2153,6 +2237,32 @@ async function resolveSourceControlManagerGitProbe(cwd, env, platform = process.
2153
2237
  detail: candidates.join(", ") || "git"
2154
2238
  };
2155
2239
  }
2240
+ async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform) {
2241
+ const resolvedDockerBinary = await resolveCommandPath(platform === "win32" ? "docker.exe" : "docker", cwd, env);
2242
+ if (resolvedDockerBinary) {
2243
+ prependExecutableDirToPath(env, resolvedDockerBinary, platform);
2244
+ env.PUSHPALS_DOCKER_BIN = basename(resolvedDockerBinary);
2245
+ env.PUSHPALS_DOCKER_BIN_ABSOLUTE = resolvedDockerBinary;
2246
+ }
2247
+ const candidates = resolveRuntimeDockerExecutableCandidates(env, platform);
2248
+ const failures = [];
2249
+ for (const candidate of candidates) {
2250
+ const result = await runCommandWithEnv([candidate, "version", "--format", "{{.Server.Version}}"], cwd, env);
2251
+ if (result.ok) {
2252
+ const version = result.stdout.trim();
2253
+ return {
2254
+ ok: true,
2255
+ detail: version ? `${candidate} (${version})` : candidate
2256
+ };
2257
+ }
2258
+ const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
2259
+ failures.push(`${candidate}: ${detail}`);
2260
+ }
2261
+ return {
2262
+ ok: false,
2263
+ detail: failures.join(" | ") || "docker"
2264
+ };
2265
+ }
2156
2266
  async function precheckSourceControlManagerGitAvailability(opts) {
2157
2267
  const platform = opts.platform ?? process.platform;
2158
2268
  const env = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
@@ -2161,8 +2271,9 @@ async function precheckSourceControlManagerGitAvailability(opts) {
2161
2271
  useRuntimeConfig: opts.preflightUsesEmbeddedRuntime,
2162
2272
  sessionId: opts.sessionId
2163
2273
  });
2164
- if (env.PUSHPALS_GIT_BIN) {
2165
- applyResolvedGitBinaryToRuntimeEnv(env, env.PUSHPALS_GIT_BIN, platform);
2274
+ const preconfiguredGitBinary = env.PUSHPALS_GIT_BIN_ABSOLUTE ?? env.PUSHPALS_GIT_BIN;
2275
+ if (preconfiguredGitBinary) {
2276
+ applyResolvedGitBinaryToRuntimeEnv(env, preconfiguredGitBinary, platform);
2166
2277
  }
2167
2278
  const remoteStatus = opts.gitRemoteCheckFn ? await opts.gitRemoteCheckFn(opts.repoRoot, opts.remote, env) : opts.repoHasRemoteFn ? await opts.repoHasRemoteFn(opts.repoRoot, opts.remote) ? { status: "ok", remote: opts.remote } : { status: "missing_remote", remote: opts.remote } : await checkGitRemoteConfigured(opts.repoRoot, opts.remote, env);
2168
2279
  if (remoteStatus.status === "missing_remote") {
@@ -2198,6 +2309,55 @@ async function precheckSourceControlManagerGitAvailability(opts) {
2198
2309
  env
2199
2310
  };
2200
2311
  }
2312
+ async function precheckWorkerpalDockerAvailability(opts) {
2313
+ const env = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
2314
+ repoRoot: opts.repoRoot,
2315
+ runtimeRoot: opts.runtimeRoot,
2316
+ useRuntimeConfig: opts.preflightUsesEmbeddedRuntime,
2317
+ sessionId: opts.sessionId
2318
+ });
2319
+ const preconfiguredDockerBinary = env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN;
2320
+ if (preconfiguredDockerBinary) {
2321
+ applyResolvedDockerBinaryToRuntimeEnv(env, preconfiguredDockerBinary, opts.platform ?? process.platform);
2322
+ }
2323
+ if (!opts.autoSpawnWorkerpals) {
2324
+ return {
2325
+ status: "skipped",
2326
+ detail: "WorkerPal auto-spawn is disabled",
2327
+ env
2328
+ };
2329
+ }
2330
+ if (!opts.dockerEnabled) {
2331
+ return {
2332
+ status: "skipped",
2333
+ detail: "WorkerPal docker mode is disabled",
2334
+ env
2335
+ };
2336
+ }
2337
+ if (!opts.requireDocker) {
2338
+ return {
2339
+ status: "skipped",
2340
+ detail: "WorkerPal docker mode is optional",
2341
+ env
2342
+ };
2343
+ }
2344
+ const dockerProbe = await (opts.dockerProbeFn ?? resolveWorkerpalDockerProbe)(opts.repoRoot, env, opts.platform ?? process.platform);
2345
+ if (!dockerProbe.ok) {
2346
+ return {
2347
+ status: "failed",
2348
+ detail: dockerProbe.detail,
2349
+ env
2350
+ };
2351
+ }
2352
+ return {
2353
+ status: "ok",
2354
+ detail: dockerProbe.detail,
2355
+ env
2356
+ };
2357
+ }
2358
+ function resolveWorkerpalCapacityTimeoutMs(config) {
2359
+ return Math.max(config.remotebuddy.waitForWorkerpalMs, config.remotebuddy.workerpalStartupTimeoutMs, config.remotebuddy.workerpalDocker ? config.workerpals.dockerAgentStartupTimeoutMs + 15000 : 0, 1e4);
2360
+ }
2201
2361
  async function checkGitRemoteConfigured(repoRoot, remote, env) {
2202
2362
  const normalizedRemote = String(remote ?? "").trim();
2203
2363
  if (!normalizedRemote) {
@@ -2539,6 +2699,42 @@ async function probeSourceControlManager(port) {
2539
2699
  return false;
2540
2700
  }
2541
2701
  }
2702
+ async function fetchWorkerStatusRows(serverUrl, ttlMs) {
2703
+ const payload = await fetchJsonWithTimeout(`${serverUrl}/workers?ttlMs=${Math.max(1000, Math.floor(ttlMs))}`, {}, 1e4);
2704
+ if (!payload?.ok || !Array.isArray(payload.workers)) {
2705
+ return [];
2706
+ }
2707
+ return payload.workers;
2708
+ }
2709
+ async function waitForWorkerpalCapacity(opts) {
2710
+ const deadline = Date.now() + Math.max(1000, opts.timeoutMs);
2711
+ let lastObservedOnline = 0;
2712
+ while (Date.now() < deadline) {
2713
+ const workers = await (opts.fetchWorkersFn ?? fetchWorkerStatusRows)(opts.serverUrl, opts.ttlMs);
2714
+ const onlineWorkers = workers.filter((worker) => Boolean(worker?.isOnline) && String(worker?.status ?? "").trim().toLowerCase() !== "offline");
2715
+ const idleWorkers = onlineWorkers.filter((worker) => Number(worker?.activeJobCount ?? 0) <= 0);
2716
+ if (onlineWorkers.length > 0) {
2717
+ lastObservedOnline = Math.max(lastObservedOnline, onlineWorkers.length);
2718
+ }
2719
+ if (idleWorkers.length > 0) {
2720
+ return {
2721
+ ok: true,
2722
+ detail: `${idleWorkers.length} idle / ${onlineWorkers.length} online`
2723
+ };
2724
+ }
2725
+ await (opts.sleepFn ?? Bun.sleep)(DEFAULT_RUNTIME_BOOT_POLL_MS);
2726
+ }
2727
+ if (lastObservedOnline > 0) {
2728
+ return {
2729
+ ok: false,
2730
+ detail: `${lastObservedOnline} online WorkerPal(s) reported but none became idle within ${Math.max(1000, opts.timeoutMs)}ms`
2731
+ };
2732
+ }
2733
+ return {
2734
+ ok: false,
2735
+ detail: `no online WorkerPal reported within ${Math.max(1000, opts.timeoutMs)}ms`
2736
+ };
2737
+ }
2542
2738
  async function fetchWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
2543
2739
  const controller = new AbortController;
2544
2740
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -2624,15 +2820,20 @@ async function autoStartRuntimeServices(opts) {
2624
2820
  }
2625
2821
  await ensureRuntimeAssets(runtimeRoot, runtimeTag);
2626
2822
  const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
2627
- const runtimeEnv = buildEmbeddedRuntimeEnv(process.env, {
2823
+ const runtimeEnv = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
2628
2824
  repoRoot: opts.repoRoot,
2629
2825
  runtimeRoot,
2630
2826
  useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
2631
2827
  sessionId: opts.sessionId
2632
2828
  });
2633
2829
  runtimeEnv.PUSHPALS_WORKERPALS_BIN = runtimeBinaries.workerpals;
2634
- if (runtimeEnv.PUSHPALS_GIT_BIN) {
2635
- applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
2830
+ const preconfiguredRuntimeGitBinary = runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE ?? runtimeEnv.PUSHPALS_GIT_BIN;
2831
+ if (preconfiguredRuntimeGitBinary) {
2832
+ applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, preconfiguredRuntimeGitBinary);
2833
+ }
2834
+ const preconfiguredRuntimeDockerBinary = runtimeEnv.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? runtimeEnv.PUSHPALS_DOCKER_BIN;
2835
+ if (preconfiguredRuntimeDockerBinary) {
2836
+ applyResolvedDockerBinaryToRuntimeEnv(runtimeEnv, preconfiguredRuntimeDockerBinary);
2636
2837
  }
2637
2838
  const gitLookupCommand = typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? runtimeEnv.PUSHPALS_GIT_BIN.trim() : "git";
2638
2839
  const resolvedGitBinary = await resolveCommandPath(gitLookupCommand, opts.repoRoot, runtimeEnv);
@@ -2720,6 +2921,24 @@ ${tail}` : ""}`);
2720
2921
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded remotebuddy autonomous engine is disabled (remotebuddy.autonomy.enabled=false).");
2721
2922
  };
2722
2923
  reportRemoteBuddyAutonomousEngineState();
2924
+ if (runtimePreflight.config.remotebuddy.autoSpawnWorkerpals) {
2925
+ const workerpalReadyTimeoutMs = resolveWorkerpalCapacityTimeoutMs(runtimePreflight.config);
2926
+ const workerpalCapacity = await waitForWorkerpalCapacity({
2927
+ serverUrl: opts.serverUrl,
2928
+ timeoutMs: workerpalReadyTimeoutMs,
2929
+ ttlMs: runtimePreflight.config.remotebuddy.workerpalOnlineTtlMs
2930
+ });
2931
+ if (!workerpalCapacity.ok) {
2932
+ const tail = readLogTail(remotebuddyService.logPath);
2933
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity did not become available within ${workerpalReadyTimeoutMs}ms.`);
2934
+ stopRuntimeServices(services);
2935
+ throw new Error(`Embedded WorkerPal capacity did not become available within ${workerpalReadyTimeoutMs}ms (${workerpalCapacity.detail}). ` + `See ${remotebuddyService.logPath}${tail ? `
2936
+ --- remotebuddy log tail ---
2937
+ ${tail}` : ""}`);
2938
+ }
2939
+ console.log(`[pushpals] Embedded WorkerPal capacity is ready (${workerpalCapacity.detail}).`);
2940
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity ready (${workerpalCapacity.detail}).`);
2941
+ }
2723
2942
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
2724
2943
  const scmGitProbe = await resolveSourceControlManagerGitProbe(opts.repoRoot, runtimeEnv, process.platform);
2725
2944
  const scmRemoteStatus = await checkGitRemoteConfigured(opts.repoRoot, opts.sourceControlManagerRemote, runtimeEnv);
@@ -3396,6 +3615,15 @@ async function main() {
3396
3615
  console.error(`[pushpals] Precheck failed: embedded SourceControlManager git command is unavailable (${scmGitPrecheck.detail}).`);
3397
3616
  process.exit(1);
3398
3617
  }
3618
+ const workerpalDockerPrecheck = await precheckWorkerpalDockerAvailability({
3619
+ repoRoot,
3620
+ runtimeRoot: preparedRuntime.runtimeRoot,
3621
+ preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
3622
+ autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
3623
+ dockerEnabled: Boolean(config.remotebuddy.workerpalDocker),
3624
+ requireDocker: Boolean(config.remotebuddy.workerpalRequireDocker),
3625
+ baseEnv: scmGitPrecheck.env
3626
+ });
3399
3627
  const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
3400
3628
  if (!precheckPassed) {
3401
3629
  process.exit(1);
@@ -3428,6 +3656,11 @@ async function main() {
3428
3656
  };
3429
3657
  if (!serverHealthy) {
3430
3658
  if (!parsed.noAutoStart) {
3659
+ if (workerpalDockerPrecheck.status === "failed") {
3660
+ console.error(`[pushpals] Precheck failed: Docker-backed WorkerPal auto-spawn is required but Docker is unavailable (${workerpalDockerPrecheck.detail}).`);
3661
+ console.error("[pushpals] Precheck failed: start Docker Desktop or the Docker daemon, then retry pushpals.");
3662
+ process.exit(1);
3663
+ }
3431
3664
  try {
3432
3665
  const startedRuntime = await autoStartRuntimeServices({
3433
3666
  repoRoot,
@@ -3438,7 +3671,8 @@ async function main() {
3438
3671
  sourceControlManagerRemote: config.sourceControlManager.remote,
3439
3672
  preparedRuntime,
3440
3673
  requestedRuntimeTag: parsed.runtimeTag,
3441
- startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled))
3674
+ startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled)),
3675
+ baseEnv: workerpalDockerPrecheck.env
3442
3676
  });
3443
3677
  autoStartedServices = startedRuntime.services;
3444
3678
  pushpalsLogPath = startedRuntime.pushpalsLogPath;
@@ -3493,6 +3727,22 @@ async function main() {
3493
3727
  }
3494
3728
  process.exit(1);
3495
3729
  }
3730
+ const workerpalCapacity = await waitForWorkerpalCapacity({
3731
+ serverUrl,
3732
+ timeoutMs: resolveWorkerpalCapacityTimeoutMs(config),
3733
+ ttlMs: config.remotebuddy.workerpalOnlineTtlMs
3734
+ });
3735
+ if (!workerpalCapacity.ok) {
3736
+ stopAutoStartedServices();
3737
+ console.error(`[pushpals] WorkerPal capacity is not ready for repo ${repoRoot}: ${workerpalCapacity.detail}.`);
3738
+ if (workerpalDockerPrecheck.status === "failed") {
3739
+ console.error(`[pushpals] Docker precheck detail: ${workerpalDockerPrecheck.detail}`);
3740
+ } else if (serverWasAlreadyHealthy) {
3741
+ console.error("[pushpals] A PushPals runtime is already serving this repo, but it does not currently have an idle WorkerPal available.");
3742
+ console.error("[pushpals] Wait for a worker to become idle or restart the runtime after fixing WorkerPal startup.");
3743
+ }
3744
+ process.exit(1);
3745
+ }
3496
3746
  const saved = statePath ? readCliState(statePath) : {};
3497
3747
  pushpalsLogPath = pushpalsLogPath || (typeof saved.pushpalsLogPath === "string" ? saved.pushpalsLogPath : undefined);
3498
3748
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -3667,10 +3917,12 @@ if (import.meta.main) {
3667
3917
  });
3668
3918
  }
3669
3919
  export {
3920
+ waitForWorkerpalCapacity,
3670
3921
  startEmbeddedMonitoringHub,
3671
3922
  resolveWindowsWhereExecutableCandidatesForEnv,
3672
3923
  resolveWindowsShellExecutableCandidatesForEnv,
3673
3924
  resolveRuntimeGitExecutableCandidates,
3925
+ resolveRuntimeDockerExecutableCandidates,
3674
3926
  resolvePreferredRuntimeReleaseTag,
3675
3927
  resolveCommandPath,
3676
3928
  resolveCliStatePath,
@@ -3678,6 +3930,7 @@ export {
3678
3930
  resolveBundledRuntimeAssetSource,
3679
3931
  resolveBundledMonitoringHubRoot,
3680
3932
  prepareCliRuntime,
3933
+ precheckWorkerpalDockerAvailability,
3681
3934
  precheckSourceControlManagerGitAvailability,
3682
3935
  normalizeRepoPathForComparison,
3683
3936
  normalizeCliInteractiveMessage,
@@ -3695,5 +3948,6 @@ export {
3695
3948
  buildEmbeddedRuntimeEnv,
3696
3949
  buildEmbeddedMonitoringHubHtml,
3697
3950
  buildCliClearTargets,
3698
- applyResolvedGitBinaryToRuntimeEnv
3951
+ applyResolvedGitBinaryToRuntimeEnv,
3952
+ applyResolvedDockerBinaryToRuntimeEnv
3699
3953
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {