@pushpalsdev/cli 1.1.29 → 1.1.30

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.
package/bin/pushpals.cjs CHANGED
@@ -2,13 +2,20 @@
2
2
  "use strict";
3
3
 
4
4
  const { spawn, spawnSync } = require("node:child_process");
5
- const { existsSync, readFileSync } = require("node:fs");
6
- const { resolve } = require("node:path");
5
+ const { existsSync, mkdirSync, readFileSync, rmSync } = require("node:fs");
6
+ const { tmpdir } = require("node:os");
7
+ const { join, resolve } = require("node:path");
7
8
 
8
9
  const bundledCliPath = resolve(__dirname, "..", "dist", "pushpals-cli.js");
9
10
  const packageJsonPath = resolve(__dirname, "..", "package.json");
10
11
  const releaseUrl = "https://github.com/PushPalsDev/pushpals/releases";
12
+ const DEFAULT_BUN_PROBE_TIMEOUT_MS = 10_000;
13
+ const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 5 * 60 * 1000;
14
+ const BUN_PROBE_TIMEOUT_ENV = "PUSHPALS_BUN_PROBE_TIMEOUT_MS";
15
+ const BOOTSTRAP_TIMEOUT_ENV = "PUSHPALS_CLI_BOOTSTRAP_TIMEOUT_MS";
16
+ const BOOTSTRAP_READY_MARKER_ENV = "PUSHPALS_CLI_READY_MARKER";
11
17
  let packageVersion = "";
18
+ let readyMarkerPath = "";
12
19
  if (existsSync(packageJsonPath)) {
13
20
  try {
14
21
  const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
@@ -25,6 +32,48 @@ function fail(lines) {
25
32
  process.exit(1);
26
33
  }
27
34
 
35
+ function parseBoundedTimeoutMs(envName, defaultValue, maxValue) {
36
+ const raw = String(process.env[envName] ?? "").trim();
37
+ if (raw === "0") return 0;
38
+ const parsed = Number.parseInt(raw, 10);
39
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultValue;
40
+ return Math.max(1_000, Math.min(maxValue, parsed));
41
+ }
42
+
43
+ function parseBunProbeTimeoutMs() {
44
+ return parseBoundedTimeoutMs(BUN_PROBE_TIMEOUT_ENV, DEFAULT_BUN_PROBE_TIMEOUT_MS, 60 * 1000);
45
+ }
46
+
47
+ function parseBootstrapTimeoutMs() {
48
+ return parseBoundedTimeoutMs(
49
+ BOOTSTRAP_TIMEOUT_ENV,
50
+ DEFAULT_BOOTSTRAP_TIMEOUT_MS,
51
+ 30 * 60 * 1000,
52
+ );
53
+ }
54
+
55
+ function createReadyMarkerPath() {
56
+ const root = join(tmpdir(), "pushpals-cli-ready");
57
+ mkdirSync(root, { recursive: true });
58
+ return join(root, `ready-${process.pid}-${Date.now()}.txt`);
59
+ }
60
+
61
+ function killChildTree(child) {
62
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
63
+ try {
64
+ if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
65
+ spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
66
+ stdio: "ignore",
67
+ timeout: 10_000,
68
+ });
69
+ return;
70
+ }
71
+ child.kill("SIGKILL");
72
+ } catch {
73
+ // best-effort watchdog cleanup only
74
+ }
75
+ }
76
+
28
77
  if (!existsSync(bundledCliPath)) {
29
78
  fail([
30
79
  "[pushpals] CLI bundle is missing in this package install.",
@@ -33,16 +82,26 @@ if (!existsSync(bundledCliPath)) {
33
82
  ]);
34
83
  }
35
84
 
36
- function hasBunRuntime() {
37
- if (process.platform === "win32") {
38
- const probe = spawnSync("bun --version", { shell: true, stdio: "ignore" });
39
- return probe.status === 0;
40
- }
41
- const probe = spawnSync("bun", ["--version"], { stdio: "ignore" });
42
- return probe.status === 0;
85
+ function probeBunRuntime() {
86
+ const timeout = parseBunProbeTimeoutMs();
87
+ const options = { stdio: "ignore", timeout };
88
+ const result = spawnSync("bun", ["--version"], options);
89
+ return {
90
+ ok: result.status === 0,
91
+ timedOut: Boolean(result.error && result.error.code === "ETIMEDOUT"),
92
+ };
43
93
  }
44
94
 
45
- if (!hasBunRuntime()) {
95
+ const bunRuntime = probeBunRuntime();
96
+ if (!bunRuntime.ok) {
97
+ if (bunRuntime.timedOut) {
98
+ fail([
99
+ `[pushpals] Bun runtime probe timed out after ${parseBunProbeTimeoutMs()}ms.`,
100
+ "[pushpals] This usually means the Bun process wedged during startup; the CLI refused to continue so it does not freeze the shell.",
101
+ `[pushpals] Set ${BUN_PROBE_TIMEOUT_ENV}=0 to disable this probe timeout, or use a direct binary release:`,
102
+ `[pushpals] ${releaseUrl}`,
103
+ ]);
104
+ }
46
105
  fail([
47
106
  "[pushpals] Bun runtime is required for the npm package entrypoint.",
48
107
  "[pushpals] Install Bun from https://bun.sh, or use a direct binary release:",
@@ -51,38 +110,104 @@ if (!hasBunRuntime()) {
51
110
  }
52
111
 
53
112
  function spawnBunCli() {
113
+ readyMarkerPath = process.env[BOOTSTRAP_READY_MARKER_ENV] || createReadyMarkerPath();
54
114
  const childEnv = {
55
115
  ...process.env,
56
116
  PUSHPALS_CLI_PACKAGE_VERSION: packageVersion || process.env.PUSHPALS_CLI_PACKAGE_VERSION || "",
117
+ [BOOTSTRAP_READY_MARKER_ENV]: readyMarkerPath,
57
118
  };
58
119
 
59
- if (process.platform !== "win32") {
60
- return spawn("bun", [bundledCliPath, ...process.argv.slice(2)], {
120
+ if (process.platform === "win32") {
121
+ const quoteWindows = (value) => `"${String(value).replace(/"/g, '\\"')}"`;
122
+ const commandLine = [
123
+ "bun",
124
+ quoteWindows(bundledCliPath),
125
+ ...process.argv.slice(2).map(quoteWindows),
126
+ ].join(" ");
127
+ return spawn(commandLine, {
128
+ shell: true,
61
129
  stdio: "inherit",
62
130
  env: childEnv,
63
131
  });
64
132
  }
65
-
66
- const quoteWindows = (value) => `"${String(value).replace(/"/g, '\\"')}"`;
67
- const commandLine = [
68
- "bun",
69
- quoteWindows(bundledCliPath),
70
- ...process.argv.slice(2).map(quoteWindows),
71
- ].join(" ");
72
- return spawn(commandLine, {
73
- shell: true,
133
+ return spawn("bun", [bundledCliPath, ...process.argv.slice(2)], {
74
134
  stdio: "inherit",
75
135
  env: childEnv,
76
136
  });
77
137
  }
78
138
 
79
139
  const child = spawnBunCli();
140
+ const bootstrapTimeoutMs = parseBootstrapTimeoutMs();
141
+ let watchdogTimer = null;
142
+ let markerPollTimer = null;
143
+ let watchdogFired = false;
144
+ let parentSignalExit = false;
145
+
146
+ function cleanupReadyMarker() {
147
+ if (!readyMarkerPath) return;
148
+ try {
149
+ rmSync(readyMarkerPath, { force: true });
150
+ } catch {
151
+ // best-effort cleanup only
152
+ }
153
+ }
154
+
155
+ function clearBootstrapWatchdog() {
156
+ if (watchdogTimer) {
157
+ clearTimeout(watchdogTimer);
158
+ watchdogTimer = null;
159
+ }
160
+ if (markerPollTimer) {
161
+ clearInterval(markerPollTimer);
162
+ markerPollTimer = null;
163
+ }
164
+ }
165
+
166
+ if (bootstrapTimeoutMs > 0) {
167
+ markerPollTimer = setInterval(() => {
168
+ if (readyMarkerPath && existsSync(readyMarkerPath)) {
169
+ clearBootstrapWatchdog();
170
+ }
171
+ }, 1_000);
172
+ watchdogTimer = setTimeout(() => {
173
+ if (readyMarkerPath && existsSync(readyMarkerPath)) {
174
+ clearBootstrapWatchdog();
175
+ return;
176
+ }
177
+ watchdogFired = true;
178
+ process.stderr.write(
179
+ `[pushpals] Bun runtime did not finish CLI bootstrap within ${bootstrapTimeoutMs}ms; terminating Bun process tree. ` +
180
+ `Set ${BOOTSTRAP_TIMEOUT_ENV}=0 to disable this watchdog.\n`,
181
+ );
182
+ killChildTree(child);
183
+ }, bootstrapTimeoutMs);
184
+ }
185
+
186
+ function terminateChildAndExit(signal) {
187
+ if (parentSignalExit) return;
188
+ parentSignalExit = true;
189
+ clearBootstrapWatchdog();
190
+ killChildTree(child);
191
+ cleanupReadyMarker();
192
+ process.exit(signal === "SIGINT" ? 130 : 143);
193
+ }
194
+
195
+ process.once("SIGINT", () => terminateChildAndExit("SIGINT"));
196
+ process.once("SIGTERM", () => terminateChildAndExit("SIGTERM"));
80
197
 
81
198
  child.on("error", (err) => {
199
+ clearBootstrapWatchdog();
200
+ cleanupReadyMarker();
82
201
  fail([`[pushpals] Failed to launch Bun runtime: ${String(err?.message ?? err)}`]);
83
202
  });
84
203
 
85
204
  child.on("exit", (code, signal) => {
205
+ clearBootstrapWatchdog();
206
+ cleanupReadyMarker();
207
+ if (watchdogFired) {
208
+ process.exit(124);
209
+ return;
210
+ }
86
211
  if (signal) {
87
212
  process.kill(process.pid, signal);
88
213
  return;
@@ -1652,6 +1652,8 @@ var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
1652
1652
  var DEFAULT_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 5000;
1653
1653
  var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS_ENV = "PUSHPALS_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS";
1654
1654
  var BLOCKING_WORKERPAL_IMAGE_BUILD_ENV = "PUSHPALS_BLOCKING_WORKERPAL_IMAGE_BUILD";
1655
+ var WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS_ENV = "PUSHPALS_WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS";
1656
+ var DEFAULT_WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS = 30000;
1655
1657
  var CLI_SESSION_JOB_LOG_MAX_CHARS = 700;
1656
1658
  var CLI_SESSION_SHOW_JOB_EVENTS_ENV = "PUSHPALS_CLI_SHOW_JOB_EVENTS";
1657
1659
  var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
@@ -3112,6 +3114,7 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
3112
3114
  console.log(`[pushpals] Embedded runtime binaries downloaded: ${downloadedCount}.`);
3113
3115
  }
3114
3116
  console.log("[pushpals] Embedded runtime binaries are ready.");
3117
+ runtimeBinaries.freshlyInstalled = downloadedCount > 0;
3115
3118
  return runtimeBinaries;
3116
3119
  }
3117
3120
  function buildServiceStopCommand(pid, platform = process.platform) {
@@ -3915,6 +3918,14 @@ function resolveWorkerpalStartupReadinessProbeMaxMs(env = process.env) {
3915
3918
  function resolveWorkerpalStartupReadinessProbeTimeoutMs(config) {
3916
3919
  return Math.max(1000, Math.min(resolveWorkerpalCapacityTimeoutMs(config), resolveWorkerpalStartupReadinessProbeMaxMs()));
3917
3920
  }
3921
+ function resolveWindowsFreshRuntimeWorkerpalPrewarmDelayMs(env = process.env, platform = process.platform) {
3922
+ if (platform !== "win32")
3923
+ return 0;
3924
+ const raw = String(env[WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS_ENV] ?? "").trim();
3925
+ if (raw === "0")
3926
+ return 0;
3927
+ return clampPositiveInt(parsePositiveInt(raw, DEFAULT_WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS), 0, 5 * 60000);
3928
+ }
3918
3929
  function shouldPrepareEmbeddedWorkerpalDockerImageBlocking(opts = {}) {
3919
3930
  const env = opts.env ?? process.env;
3920
3931
  const explicit = String(env[BLOCKING_WORKERPAL_IMAGE_BUILD_ENV] ?? "").trim();
@@ -4457,6 +4468,13 @@ async function autoStartRuntimeServices(opts) {
4457
4468
  runtimeTag
4458
4469
  });
4459
4470
  runtimeEnv.PUSHPALS_WORKERPALS_BIN = runtimeBinaries.workerpals;
4471
+ if (runtimeBinaries.freshlyInstalled && process.platform === "win32" && runtimePreflight.config.remotebuddy.autoSpawnWorkerpals && !runtimeEnv.PUSHPALS_REMOTEBUDDY_WORKERPAL_PREWARM_DELAY_MS) {
4472
+ const delayMs = resolveWindowsFreshRuntimeWorkerpalPrewarmDelayMs(runtimeEnv, process.platform);
4473
+ if (delayMs > 0) {
4474
+ runtimeEnv.PUSHPALS_REMOTEBUDDY_WORKERPAL_PREWARM_DELAY_MS = String(delayMs);
4475
+ console.log(`[pushpals] Fresh Windows runtime binaries detected; delaying WorkerPal prewarm by ${delayMs}ms so security software can finish first-run binary checks. Set ${WINDOWS_FRESH_RUNTIME_WORKERPAL_PREWARM_DELAY_MS_ENV}=0 to disable.`);
4476
+ }
4477
+ }
4460
4478
  const preconfiguredRuntimeGitBinary = runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE ?? runtimeEnv.PUSHPALS_GIT_BIN;
4461
4479
  if (preconfiguredRuntimeGitBinary) {
4462
4480
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, preconfiguredRuntimeGitBinary);
@@ -4939,6 +4957,17 @@ function writeCliState(pathValue, state) {
4939
4957
  writeFileSync(pathValue, `${JSON.stringify(payload, null, 2)}
4940
4958
  `, "utf8");
4941
4959
  }
4960
+ function markCliBootstrapReadyFromEnv(env = process.env) {
4961
+ const markerPath = String(env.PUSHPALS_CLI_READY_MARKER ?? "").trim();
4962
+ if (!markerPath)
4963
+ return;
4964
+ try {
4965
+ mkdirSync(dirname(markerPath), { recursive: true });
4966
+ writeFileSync(markerPath, `${process.pid}
4967
+ ${new Date().toISOString()}
4968
+ `, "utf8");
4969
+ } catch {}
4970
+ }
4942
4971
  function resolveCliStatePath(repoRoot) {
4943
4972
  return resolveGitStateFilePath(repoRoot, "pushpals-cli-state.json");
4944
4973
  }
@@ -5950,6 +5979,7 @@ async function main() {
5950
5979
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
5951
5980
  reportWorkerExecutionReadinessFromSnapshot(startupWorkerExecutionReadiness);
5952
5981
  reportEmbeddedRuntimeHealth();
5982
+ markCliBootstrapReadyFromEnv();
5953
5983
  if (parsed.runtimeOnly) {
5954
5984
  console.log("[pushpals] runtimeOnly=true");
5955
5985
  } else {
@@ -6124,6 +6154,7 @@ export {
6124
6154
  resolveWorkerExecutionReadiness,
6125
6155
  resolveWindowsWhereExecutableCandidatesForEnv,
6126
6156
  resolveWindowsShellExecutableCandidatesForEnv,
6157
+ resolveWindowsFreshRuntimeWorkerpalPrewarmDelayMs,
6127
6158
  resolveRuntimeGitExecutableCandidates,
6128
6159
  resolveRuntimeDockerExecutableCandidates,
6129
6160
  resolvePreferredRuntimeReleaseTag,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.29",
3
+ "version": "1.1.30",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8396,6 +8396,12 @@ function parseEnabledFlag(raw, defaultValue) {
8396
8396
  return defaultValue;
8397
8397
  return !["0", "false", "no", "off"].includes(text);
8398
8398
  }
8399
+ function parseNonNegativeMs(raw, defaultValue = 0) {
8400
+ const parsed = Number.parseInt(String(raw ?? "").trim(), 10);
8401
+ if (!Number.isFinite(parsed) || parsed < 0)
8402
+ return Math.max(0, defaultValue);
8403
+ return Math.floor(parsed);
8404
+ }
8399
8405
  function isCodexUnavailableFailureSignal(message, detail) {
8400
8406
  const text = `${message}
8401
8407
  ${detail}`.toLowerCase();
@@ -8968,6 +8974,7 @@ class RemoteBuddyOrchestrator {
8968
8974
  workerSpawnCooldownUntil = 0;
8969
8975
  workerSpawnBackoffMs;
8970
8976
  workerAutoscalePollMs;
8977
+ workerPrewarmDelayMs;
8971
8978
  lastWorkerAutoscaleAt = 0;
8972
8979
  comm;
8973
8980
  statusHeartbeatTimer = null;
@@ -9030,6 +9037,7 @@ class RemoteBuddyOrchestrator {
9030
9037
  this.workerpalsUnavailableReason = null;
9031
9038
  this.workerSpawnBackoffMs = Math.max(1000, Number.isFinite(remoteCfg.crashRestartBackoffMs) && remoteCfg.crashRestartBackoffMs > 0 ? remoteCfg.crashRestartBackoffMs : 3000);
9032
9039
  this.workerAutoscalePollMs = Math.max(1000, remoteCfg.pollMs);
9040
+ this.workerPrewarmDelayMs = Math.min(5 * 60000, parseNonNegativeMs(process.env.PUSHPALS_REMOTEBUDDY_WORKERPAL_PREWARM_DELAY_MS, 0));
9033
9041
  this.statusHeartbeatMs = Math.max(0, remoteCfg.statusHeartbeatMs);
9034
9042
  this.fetchFailureLogsOnJobFailure = parseEnabledFlag(process.env.REMOTEBUDDY_FETCH_FAILURE_LOGS, true);
9035
9043
  this.executionBudgetInteractiveMs = Math.max(60000, remoteCfg.executionBudgetInteractiveMs);
@@ -9081,6 +9089,9 @@ class RemoteBuddyOrchestrator {
9081
9089
  this.autonomousEngine.setRuntimeEnabled(this.autonomyRuntimeEnabled);
9082
9090
  console.log(`[RemoteBuddy] Detected repo root: ${this.repo}`);
9083
9091
  console.log(`[RemoteBuddy] Worker scheduler: min=${this.minWorkers} max=${this.maxWorkers} autoSpawn=${this.autoSpawnWorkers ? "on" : "off"} wait=${this.waitForWorkerMs}ms`);
9092
+ if (this.workerPrewarmDelayMs > 0) {
9093
+ console.log(`[RemoteBuddy] WorkerPal startup prewarm delayed by ${this.workerPrewarmDelayMs}ms to reduce first-run binary scan contention.`);
9094
+ }
9084
9095
  console.log(`[RemoteBuddy] Budgets: interactive=${this.executionBudgetInteractiveMs}ms normal=${this.executionBudgetNormalMs}ms background=${this.executionBudgetBackgroundMs}ms finalization=${this.finalizationBudgetMs}ms`);
9085
9096
  console.log(`[RemoteBuddy] Failure log fetch on job failures: ${this.fetchFailureLogsOnJobFailure ? "on" : "off"}`);
9086
9097
  console.log(`[RemoteBuddy] Persistent memory: ${this.memoryEnabled ? "on" : "off"} crossSession=${this.memoryIncludeCrossSession ? "on" : "off"} recallItems=${this.memoryMaxRecallItems} recallChars=${this.memoryMaxRecallChars} retentionDays=${this.memoryRetentionDays}`);
@@ -10039,6 +10050,12 @@ Please reply with the missing details and I will enqueue a follow-up request.` :
10039
10050
  }
10040
10051
  }
10041
10052
  async ensureWorkerCapacityOnStartup() {
10053
+ if (this.workerPrewarmDelayMs > 0) {
10054
+ console.log(`[RemoteBuddy] Waiting ${this.workerPrewarmDelayMs}ms before WorkerPal startup prewarm.`);
10055
+ await Bun.sleep(this.workerPrewarmDelayMs);
10056
+ if (this.disposed)
10057
+ return;
10058
+ }
10042
10059
  const workers = await this.fetchWorkers();
10043
10060
  if (this.pickIdleWorker(workers)) {
10044
10061
  return;