@pushpalsdev/cli 1.0.24 → 1.0.27

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.
@@ -637,7 +637,7 @@ function loadPushPalsConfig(options = {}) {
637
637
  workerpalOnlineTtlMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_ONLINE_TTL_MS") ?? remoteNode.workerpal_online_ttl_ms, 15000)),
638
638
  waitForWorkerpalMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_WAIT_FOR_WORKERPAL_MS") ?? remoteNode.wait_for_workerpal_ms, 15000)),
639
639
  autoSpawnWorkerpals: parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ?? asBoolean(remoteNode.auto_spawn_workerpals, true),
640
- maxWorkerpals: Math.max(1, asInt(remoteNode.max_workerpals, 20)),
640
+ maxWorkerpals: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20)),
641
641
  workerpalStartupTimeoutMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS") ?? remoteNode.workerpal_startup_timeout_ms, 1e4)),
642
642
  workerpalDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_DOCKER") ?? asBoolean(remoteNode.workerpal_docker, true),
643
643
  workerpalRequireDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_REQUIRE_DOCKER") ?? asBoolean(remoteNode.workerpal_require_docker, true),
@@ -1743,6 +1743,21 @@ function writeTextFileIfMissing(pathValue, text) {
1743
1743
  mkdirSync(dirname(pathValue), { recursive: true });
1744
1744
  writeFileSync(pathValue, text, "utf8");
1745
1745
  }
1746
+ function migrateEmbeddedRuntimeLocalToml(localTomlPath) {
1747
+ if (!existsSync4(localTomlPath))
1748
+ return;
1749
+ let original;
1750
+ try {
1751
+ original = readFileSync4(localTomlPath, "utf8");
1752
+ } catch {
1753
+ return;
1754
+ }
1755
+ const updated = original.replace(/^(\[remotebuddy\.autonomy\]\r?\n)(enabled\s*=\s*false\s*\r?\n)/m, `$1enabled = true
1756
+ `);
1757
+ if (updated !== original) {
1758
+ writeFileSync(localTomlPath, updated, "utf8");
1759
+ }
1760
+ }
1746
1761
  function copyRuntimeAssetBundle(source, runtimeRoot, force) {
1747
1762
  mkdirSync(runtimeRoot, { recursive: true });
1748
1763
  cpSync(source.envExamplePath, join2(runtimeRoot, ".env.example"), {
@@ -1782,12 +1797,14 @@ function seedRuntimePreflightAssets(runtimeRoot) {
1782
1797
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1783
1798
  `);
1784
1799
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1800
+ const localTomlPath = join2(runtimeRoot, "configs", "local.toml");
1785
1801
  if (existsSync4(localExamplePath)) {
1786
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1802
+ writeTextFileIfMissing(localTomlPath, readFileSync4(localExamplePath, "utf8"));
1787
1803
  } else {
1788
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1804
+ writeTextFileIfMissing(localTomlPath, `# Local PushPals runtime overrides
1789
1805
  `);
1790
1806
  }
1807
+ migrateEmbeddedRuntimeLocalToml(localTomlPath);
1791
1808
  }
1792
1809
  async function fetchTextFromUrl(url, timeoutMs = 20000) {
1793
1810
  const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, timeoutMs);
@@ -1845,12 +1862,14 @@ async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
1845
1862
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1846
1863
  `);
1847
1864
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1865
+ const localTomlPath = join2(runtimeRoot, "configs", "local.toml");
1848
1866
  if (existsSync4(localExamplePath)) {
1849
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync4(localExamplePath, "utf8"));
1867
+ writeTextFileIfMissing(localTomlPath, readFileSync4(localExamplePath, "utf8"));
1850
1868
  } else {
1851
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1869
+ writeTextFileIfMissing(localTomlPath, `# Local PushPals runtime overrides
1852
1870
  `);
1853
1871
  }
1872
+ migrateEmbeddedRuntimeLocalToml(localTomlPath);
1854
1873
  console.log("[pushpals] Embedded runtime assets are ready.");
1855
1874
  }
1856
1875
  function resolveDeferredRuntimeTagHint(explicitTag) {
@@ -1895,6 +1914,35 @@ function runtimeBinaryFilename(serviceName, platformKey) {
1895
1914
  const extension = platformKey.startsWith("windows-") ? ".exe" : "";
1896
1915
  return `pushpals-runtime-${serviceToken}-${platformKey}${extension}`;
1897
1916
  }
1917
+ function resolveRuntimeBinaryInstallState(runtimeRoot, platformKey) {
1918
+ const binDir = join2(runtimeRoot, "bin", platformKey);
1919
+ const tagMarkerPath = join2(binDir, ".runtime-tag");
1920
+ const installedTag = existsSync4(tagMarkerPath) ? readFileSync4(tagMarkerPath, "utf8").trim() : "";
1921
+ return { binDir, tagMarkerPath, installedTag };
1922
+ }
1923
+ function cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, activeBinDir) {
1924
+ const legacyRoot = join2(runtimeRoot, "bin");
1925
+ if (!existsSync4(legacyRoot))
1926
+ return;
1927
+ let entries;
1928
+ try {
1929
+ entries = readdirSync(legacyRoot, { withFileTypes: true });
1930
+ } catch {
1931
+ return;
1932
+ }
1933
+ for (const entry of entries) {
1934
+ if (!entry.isDirectory())
1935
+ continue;
1936
+ const candidateDir = join2(legacyRoot, entry.name);
1937
+ if (candidateDir === activeBinDir)
1938
+ continue;
1939
+ if (!entry.name.endsWith(`-${platformKey}`))
1940
+ continue;
1941
+ try {
1942
+ rmSync(candidateDir, { recursive: true, force: true });
1943
+ } catch {}
1944
+ }
1945
+ }
1898
1946
  function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1899
1947
  const env = normalizeChildProcessEnv(baseEnv);
1900
1948
  const useRuntimeConfig = opts.useRuntimeConfig !== false;
@@ -2032,7 +2080,8 @@ async function downloadBinaryAsset(tag, assetName, outPath) {
2032
2080
  async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
2033
2081
  const platformKey = resolveRuntimePlatformKey();
2034
2082
  console.log(`[pushpals] Preparing embedded runtime binaries for ${runtimeTag} (${platformKey})...`);
2035
- const binDir = join2(runtimeRoot, "bin", `${runtimeTag}-${platformKey}`);
2083
+ const installState = resolveRuntimeBinaryInstallState(runtimeRoot, platformKey);
2084
+ const { binDir, tagMarkerPath, installedTag } = installState;
2036
2085
  mkdirSync(binDir, { recursive: true });
2037
2086
  const runtimeBinaries = {
2038
2087
  server: join2(binDir, runtimeBinaryFilename("server", platformKey)),
@@ -2048,14 +2097,18 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
2048
2097
  runtimeBinaries.workerpals,
2049
2098
  runtimeBinaries.sourceControlManager
2050
2099
  ];
2100
+ const shouldRefreshAll = installedTag !== runtimeTag;
2051
2101
  let downloadedCount = 0;
2052
2102
  for (const binaryPath of requiredAssets) {
2053
- if (existsSync4(binaryPath))
2103
+ if (!shouldRefreshAll && existsSync4(binaryPath))
2054
2104
  continue;
2055
2105
  const assetName = binaryPath.split(/[\\/]/).pop() || "";
2056
2106
  await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
2057
2107
  downloadedCount++;
2058
2108
  }
2109
+ writeFileSync(tagMarkerPath, `${runtimeTag}
2110
+ `, "utf8");
2111
+ cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, binDir);
2059
2112
  if (process.platform !== "win32") {
2060
2113
  for (const binaryPath of requiredAssets) {
2061
2114
  try {
@@ -2430,10 +2483,54 @@ async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform
2430
2483
  }
2431
2484
  var WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL = "pushpals.runtime_tag";
2432
2485
  var WORKERPAL_SANDBOX_COMPONENT_LABEL = "pushpals.component=workerpals-sandbox";
2486
+ var WORKERPAL_WARM_COMPONENT_LABEL = "pushpals.component=workerpals-warm";
2433
2487
  function resolveConfiguredDockerExecutable(env, platform = process.platform) {
2434
2488
  const configured = String(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN ?? (platform === "win32" ? "docker.exe" : "docker")).trim();
2435
2489
  return configured || (platform === "win32" ? "docker.exe" : "docker");
2436
2490
  }
2491
+ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2492
+ const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
2493
+ const dockerExecutable = resolveConfiguredDockerExecutable(opts.env, opts.platform ?? process.platform);
2494
+ const list = await runCommandWithEnvFn([
2495
+ dockerExecutable,
2496
+ "ps",
2497
+ "-aq",
2498
+ "--filter",
2499
+ `label=${WORKERPAL_WARM_COMPONENT_LABEL}`,
2500
+ "--filter",
2501
+ `label=pushpals.repo=${opts.repoRoot}`
2502
+ ], opts.repoRoot, opts.env);
2503
+ if (!list.ok) {
2504
+ const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
2505
+ return {
2506
+ ok: false,
2507
+ detail: `failed to inspect lingering WorkerPal warm containers: ${detail}`,
2508
+ removed: 0
2509
+ };
2510
+ }
2511
+ const containerIds = list.stdout.split(/\s+/).map((value) => value.trim()).filter(Boolean);
2512
+ if (containerIds.length === 0) {
2513
+ return {
2514
+ ok: true,
2515
+ detail: "no lingering WorkerPal warm containers found",
2516
+ removed: 0
2517
+ };
2518
+ }
2519
+ const remove = await runCommandWithEnvFn([dockerExecutable, "rm", "-f", ...containerIds], opts.repoRoot, opts.env);
2520
+ if (!remove.ok) {
2521
+ const detail = remove.stderr || remove.stdout || `exit ${remove.exitCode}`;
2522
+ return {
2523
+ ok: false,
2524
+ detail: `failed to remove lingering WorkerPal warm containers: ${detail}`,
2525
+ removed: 0
2526
+ };
2527
+ }
2528
+ return {
2529
+ ok: true,
2530
+ detail: `removed ${containerIds.length} lingering WorkerPal warm container(s)`,
2531
+ removed: containerIds.length
2532
+ };
2533
+ }
2437
2534
  async function inspectDockerImageRuntimeTag(dockerExecutable, imageName, cwd, env) {
2438
2535
  const inspect = await runCommandWithEnv([
2439
2536
  dockerExecutable,
@@ -3919,6 +4016,24 @@ async function main() {
3919
4016
  let autoStartedServices = [];
3920
4017
  let pushpalsLogPath;
3921
4018
  let resolvedRuntimeTagForAutoStart = preparedRuntime.runtimeTag || parsed.runtimeTag || "";
4019
+ const cleanupWorkerpalWarmContainersIfNeeded = async (phase) => {
4020
+ if (workerpalDockerPrecheck.status === "failed")
4021
+ return;
4022
+ if (!config.remotebuddy.autoSpawnWorkerpals || !config.remotebuddy.workerpalDocker || !config.remotebuddy.workerpalRequireDocker) {
4023
+ return;
4024
+ }
4025
+ const cleanup = await cleanupLingeringWorkerpalWarmContainers({
4026
+ repoRoot,
4027
+ env: workerpalDockerPrecheck.env
4028
+ });
4029
+ if (!cleanup.ok) {
4030
+ console.warn(`[pushpals] WorkerPal warm-container cleanup warning (${phase}): ${cleanup.detail}`);
4031
+ return;
4032
+ }
4033
+ if (cleanup.removed > 0) {
4034
+ console.log(`[pushpals] ${cleanup.detail} (${phase}).`);
4035
+ }
4036
+ };
3922
4037
  const stopAutoStartedServices = () => {
3923
4038
  if (autoStartedServices.length === 0)
3924
4039
  return;
@@ -3940,6 +4055,7 @@ async function main() {
3940
4055
  console.warn(`[pushpals] ${shutdown.detail}`);
3941
4056
  }
3942
4057
  await stopRuntimeServicesGracefully(services);
4058
+ await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
3943
4059
  };
3944
4060
  let serverHealthy = await probeServer(serverUrl);
3945
4061
  const serverWasAlreadyHealthy = serverHealthy;
@@ -3968,6 +4084,7 @@ async function main() {
3968
4084
  detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
3969
4085
  };
3970
4086
  if (!serverHealthy) {
4087
+ await cleanupWorkerpalWarmContainersIfNeeded("startup preflight");
3971
4088
  if (!parsed.noAutoStart) {
3972
4089
  try {
3973
4090
  const startedRuntime = await autoStartRuntimeServices({
@@ -4262,8 +4379,10 @@ export {
4262
4379
  extractRemoteBuddySessionConsumerHealth,
4263
4380
  extractRemoteBuddyAutonomousEngineState,
4264
4381
  ensureWorkerpalDockerImageReady,
4382
+ ensureRuntimeBinaries,
4265
4383
  downloadRuntimeAssetsFromSourceTag,
4266
4384
  copyTrackedRepoPath,
4385
+ cleanupLingeringWorkerpalWarmContainers,
4267
4386
  bundledMonitoringHubNeedsRefresh,
4268
4387
  buildWorkerpalSandboxPaths,
4269
4388
  buildServiceStopCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.27",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,7 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 10
43
+ max_workerpals = 3
44
44
  workerpal_startup_timeout_ms = 10000
45
45
  workerpal_docker = true
46
46
  workerpal_require_docker = true
@@ -143,8 +143,8 @@ docker_warm_max_attempts = 3
143
143
  docker_warm_retry_backoff_ms = 2000
144
144
  docker_job_max_attempts = 2
145
145
  docker_job_retry_backoff_ms = 3000
146
- docker_warm_memory_mb = 2048
147
- docker_warm_cpus = 2
146
+ docker_warm_memory_mb = 1024
147
+ docker_warm_cpus = 1
148
148
  file_modifying_jobs = ["task.execute"]
149
149
  output_max_chars = 196608
150
150
  output_max_lines = 600
@@ -23,7 +23,7 @@ codex_timeout_ms = 120000
23
23
  reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 10
26
+ max_workerpals = 3
27
27
  crash_restart_enabled = true
28
28
  crash_restart_max_restarts = 3
29
29
  crash_restart_backoff_ms = 3000
@@ -59,8 +59,8 @@ openhands_stuck_guard_explore_limit = 18
59
59
  openhands_stuck_guard_min_elapsed_ms = 180000
60
60
  openhands_stuck_guard_broad_scan_limit = 2
61
61
  openhands_stuck_guard_no_progress_max_ms = 300000
62
- docker_warm_memory_mb = 2048
63
- docker_warm_cpus = 2
62
+ docker_warm_memory_mb = 1024
63
+ docker_warm_cpus = 1
64
64
  file_modifying_jobs = ["task.execute"]
65
65
  output_max_chars = 196608
66
66
  output_max_lines = 600
@@ -40,7 +40,7 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 10
43
+ max_workerpals = 3
44
44
  workerpal_startup_timeout_ms = 10000
45
45
  workerpal_docker = true
46
46
  workerpal_require_docker = true
@@ -143,8 +143,8 @@ docker_warm_max_attempts = 3
143
143
  docker_warm_retry_backoff_ms = 2000
144
144
  docker_job_max_attempts = 2
145
145
  docker_job_retry_backoff_ms = 3000
146
- docker_warm_memory_mb = 2048
147
- docker_warm_cpus = 2
146
+ docker_warm_memory_mb = 1024
147
+ docker_warm_cpus = 1
148
148
  file_modifying_jobs = ["task.execute"]
149
149
  output_max_chars = 196608
150
150
  output_max_lines = 600
@@ -23,7 +23,7 @@ codex_timeout_ms = 120000
23
23
  reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 10
26
+ max_workerpals = 3
27
27
  crash_restart_enabled = true
28
28
  crash_restart_max_restarts = 3
29
29
  crash_restart_backoff_ms = 3000
@@ -59,8 +59,8 @@ openhands_stuck_guard_explore_limit = 18
59
59
  openhands_stuck_guard_min_elapsed_ms = 180000
60
60
  openhands_stuck_guard_broad_scan_limit = 2
61
61
  openhands_stuck_guard_no_progress_max_ms = 300000
62
- docker_warm_memory_mb = 2048
63
- docker_warm_cpus = 2
62
+ docker_warm_memory_mb = 1024
63
+ docker_warm_cpus = 1
64
64
  file_modifying_jobs = ["task.execute"]
65
65
  output_max_chars = 196608
66
66
  output_max_lines = 600
@@ -1508,7 +1508,10 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
1508
1508
  asBoolean(remoteNode.auto_spawn_workerpals, true),
1509
1509
  maxWorkerpals: Math.max(
1510
1510
  1,
1511
- asInt(remoteNode.max_workerpals, 20),
1511
+ asInt(
1512
+ parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals,
1513
+ 20,
1514
+ ),
1512
1515
  ),
1513
1516
  workerpalStartupTimeoutMs: Math.max(
1514
1517
  1_000,