@pushpalsdev/cli 1.1.29 → 1.1.31

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,21 @@
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 { dirname, 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 = "";
19
+ let resolvedBunCommand = "";
12
20
  if (existsSync(packageJsonPath)) {
13
21
  try {
14
22
  const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
@@ -25,6 +33,84 @@ function fail(lines) {
25
33
  process.exit(1);
26
34
  }
27
35
 
36
+ function parseBoundedTimeoutMs(envName, defaultValue, maxValue) {
37
+ const raw = String(process.env[envName] ?? "").trim();
38
+ if (raw === "0") return 0;
39
+ const parsed = Number.parseInt(raw, 10);
40
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultValue;
41
+ return Math.max(1_000, Math.min(maxValue, parsed));
42
+ }
43
+
44
+ function parseBunProbeTimeoutMs() {
45
+ return parseBoundedTimeoutMs(BUN_PROBE_TIMEOUT_ENV, DEFAULT_BUN_PROBE_TIMEOUT_MS, 60 * 1000);
46
+ }
47
+
48
+ function parseBootstrapTimeoutMs() {
49
+ return parseBoundedTimeoutMs(
50
+ BOOTSTRAP_TIMEOUT_ENV,
51
+ DEFAULT_BOOTSTRAP_TIMEOUT_MS,
52
+ 30 * 60 * 1000,
53
+ );
54
+ }
55
+
56
+ function createReadyMarkerPath() {
57
+ const root = join(tmpdir(), "pushpals-cli-ready");
58
+ mkdirSync(root, { recursive: true });
59
+ return join(root, `ready-${process.pid}-${Date.now()}.txt`);
60
+ }
61
+
62
+ function killChildTree(child) {
63
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
64
+ try {
65
+ if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) {
66
+ spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
67
+ stdio: "ignore",
68
+ timeout: 10_000,
69
+ });
70
+ return;
71
+ }
72
+ child.kill("SIGKILL");
73
+ } catch {
74
+ // best-effort watchdog cleanup only
75
+ }
76
+ }
77
+
78
+ function resolveWindowsBunCommand() {
79
+ try {
80
+ const where = spawnSync("where.exe", ["bun"], {
81
+ encoding: "utf8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ timeout: 5_000,
84
+ });
85
+ if (where.status !== 0) return "bun";
86
+ const candidates = String(where.stdout ?? "")
87
+ .split(/\r?\n/g)
88
+ .map((line) => line.trim())
89
+ .filter(Boolean);
90
+
91
+ for (const candidate of candidates) {
92
+ const lower = candidate.toLowerCase();
93
+ if (lower.endsWith(".exe") && existsSync(candidate)) return candidate;
94
+
95
+ // Bun installed through npm/nvm on Windows commonly exposes shell shims:
96
+ // <node-dir>\bun
97
+ // <node-dir>\bun.cmd
98
+ // Those shims delegate to <node-dir>\node_modules\bun\bin\bun.exe.
99
+ const shimTarget = join(dirname(candidate), "node_modules", "bun", "bin", "bun.exe");
100
+ if (existsSync(shimTarget)) return shimTarget;
101
+ }
102
+ } catch {
103
+ // Fall back to PATH/shell command resolution below.
104
+ }
105
+ return "bun";
106
+ }
107
+
108
+ function resolveBunCommand() {
109
+ if (resolvedBunCommand) return resolvedBunCommand;
110
+ resolvedBunCommand = process.platform === "win32" ? resolveWindowsBunCommand() : "bun";
111
+ return resolvedBunCommand;
112
+ }
113
+
28
114
  if (!existsSync(bundledCliPath)) {
29
115
  fail([
30
116
  "[pushpals] CLI bundle is missing in this package install.",
@@ -33,16 +119,26 @@ if (!existsSync(bundledCliPath)) {
33
119
  ]);
34
120
  }
35
121
 
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;
122
+ function probeBunRuntime() {
123
+ const timeout = parseBunProbeTimeoutMs();
124
+ const options = { stdio: "ignore", timeout };
125
+ const result = spawnSync(resolveBunCommand(), ["--version"], options);
126
+ return {
127
+ ok: result.status === 0,
128
+ timedOut: Boolean(result.error && result.error.code === "ETIMEDOUT"),
129
+ };
43
130
  }
44
131
 
45
- if (!hasBunRuntime()) {
132
+ const bunRuntime = probeBunRuntime();
133
+ if (!bunRuntime.ok) {
134
+ if (bunRuntime.timedOut) {
135
+ fail([
136
+ `[pushpals] Bun runtime probe timed out after ${parseBunProbeTimeoutMs()}ms.`,
137
+ "[pushpals] This usually means the Bun process wedged during startup; the CLI refused to continue so it does not freeze the shell.",
138
+ `[pushpals] Set ${BUN_PROBE_TIMEOUT_ENV}=0 to disable this probe timeout, or use a direct binary release:`,
139
+ `[pushpals] ${releaseUrl}`,
140
+ ]);
141
+ }
46
142
  fail([
47
143
  "[pushpals] Bun runtime is required for the npm package entrypoint.",
48
144
  "[pushpals] Install Bun from https://bun.sh, or use a direct binary release:",
@@ -51,38 +147,105 @@ if (!hasBunRuntime()) {
51
147
  }
52
148
 
53
149
  function spawnBunCli() {
150
+ readyMarkerPath = process.env[BOOTSTRAP_READY_MARKER_ENV] || createReadyMarkerPath();
54
151
  const childEnv = {
55
152
  ...process.env,
56
153
  PUSHPALS_CLI_PACKAGE_VERSION: packageVersion || process.env.PUSHPALS_CLI_PACKAGE_VERSION || "",
154
+ [BOOTSTRAP_READY_MARKER_ENV]: readyMarkerPath,
57
155
  };
58
156
 
59
- if (process.platform !== "win32") {
60
- return spawn("bun", [bundledCliPath, ...process.argv.slice(2)], {
157
+ const bunCommand = resolveBunCommand();
158
+ if (process.platform === "win32" && bunCommand === "bun") {
159
+ const quoteWindows = (value) => `"${String(value).replace(/"/g, '\\"')}"`;
160
+ const commandLine = [
161
+ bunCommand,
162
+ quoteWindows(bundledCliPath),
163
+ ...process.argv.slice(2).map(quoteWindows),
164
+ ].join(" ");
165
+ return spawn(commandLine, {
166
+ shell: true,
61
167
  stdio: "inherit",
62
168
  env: childEnv,
63
169
  });
64
170
  }
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,
171
+ return spawn(bunCommand, [bundledCliPath, ...process.argv.slice(2)], {
74
172
  stdio: "inherit",
75
173
  env: childEnv,
76
174
  });
77
175
  }
78
176
 
79
177
  const child = spawnBunCli();
178
+ const bootstrapTimeoutMs = parseBootstrapTimeoutMs();
179
+ let watchdogTimer = null;
180
+ let markerPollTimer = null;
181
+ let watchdogFired = false;
182
+ let parentSignalExit = false;
183
+
184
+ function cleanupReadyMarker() {
185
+ if (!readyMarkerPath) return;
186
+ try {
187
+ rmSync(readyMarkerPath, { force: true });
188
+ } catch {
189
+ // best-effort cleanup only
190
+ }
191
+ }
192
+
193
+ function clearBootstrapWatchdog() {
194
+ if (watchdogTimer) {
195
+ clearTimeout(watchdogTimer);
196
+ watchdogTimer = null;
197
+ }
198
+ if (markerPollTimer) {
199
+ clearInterval(markerPollTimer);
200
+ markerPollTimer = null;
201
+ }
202
+ }
203
+
204
+ if (bootstrapTimeoutMs > 0) {
205
+ markerPollTimer = setInterval(() => {
206
+ if (readyMarkerPath && existsSync(readyMarkerPath)) {
207
+ clearBootstrapWatchdog();
208
+ }
209
+ }, 1_000);
210
+ watchdogTimer = setTimeout(() => {
211
+ if (readyMarkerPath && existsSync(readyMarkerPath)) {
212
+ clearBootstrapWatchdog();
213
+ return;
214
+ }
215
+ watchdogFired = true;
216
+ process.stderr.write(
217
+ `[pushpals] Bun runtime did not finish CLI bootstrap within ${bootstrapTimeoutMs}ms; terminating Bun process tree. ` +
218
+ `Set ${BOOTSTRAP_TIMEOUT_ENV}=0 to disable this watchdog.\n`,
219
+ );
220
+ killChildTree(child);
221
+ }, bootstrapTimeoutMs);
222
+ }
223
+
224
+ function terminateChildAndExit(signal) {
225
+ if (parentSignalExit) return;
226
+ parentSignalExit = true;
227
+ clearBootstrapWatchdog();
228
+ killChildTree(child);
229
+ cleanupReadyMarker();
230
+ process.exit(signal === "SIGINT" ? 130 : 143);
231
+ }
232
+
233
+ process.once("SIGINT", () => terminateChildAndExit("SIGINT"));
234
+ process.once("SIGTERM", () => terminateChildAndExit("SIGTERM"));
80
235
 
81
236
  child.on("error", (err) => {
237
+ clearBootstrapWatchdog();
238
+ cleanupReadyMarker();
82
239
  fail([`[pushpals] Failed to launch Bun runtime: ${String(err?.message ?? err)}`]);
83
240
  });
84
241
 
85
242
  child.on("exit", (code, signal) => {
243
+ clearBootstrapWatchdog();
244
+ cleanupReadyMarker();
245
+ if (watchdogFired) {
246
+ process.exit(124);
247
+ return;
248
+ }
86
249
  if (signal) {
87
250
  process.kill(process.pid, signal);
88
251
  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.31",
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;