@pushpalsdev/cli 1.0.35 → 1.0.37
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/dist/pushpals-cli.js +499 -175
- package/package.json +1 -1
- package/runtime/sandbox/package.json +1 -0
package/dist/pushpals-cli.js
CHANGED
|
@@ -6,23 +6,323 @@ import {
|
|
|
6
6
|
appendFileSync,
|
|
7
7
|
chmodSync,
|
|
8
8
|
cpSync,
|
|
9
|
-
existsSync as
|
|
9
|
+
existsSync as existsSync5,
|
|
10
10
|
lstatSync,
|
|
11
11
|
mkdirSync,
|
|
12
12
|
readdirSync,
|
|
13
13
|
readFileSync as readFileSync4,
|
|
14
|
-
rmSync,
|
|
14
|
+
rmSync as rmSync2,
|
|
15
15
|
writeFileSync
|
|
16
16
|
} from "fs";
|
|
17
17
|
import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4, win32 as pathWin32 } from "path";
|
|
18
18
|
import { createInterface } from "readline";
|
|
19
19
|
|
|
20
|
+
// ../shared/src/localbuddy_runtime.ts
|
|
21
|
+
var TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
22
|
+
var FALSY = new Set(["0", "false", "no", "off"]);
|
|
23
|
+
|
|
24
|
+
// ../../scripts/start_runtime_services.ts
|
|
25
|
+
var DEFAULT_SERVICE_MANAGER_POLL_MS = 1000;
|
|
26
|
+
var DEFAULT_SERVICE_MANAGER_MAX_RESTART_ATTEMPTS = 4;
|
|
27
|
+
var DEFAULT_SERVICE_MANAGER_STABLE_WINDOW_MS = 60000;
|
|
28
|
+
var DEFAULT_SERVICE_MANAGER_BASE_BACKOFF_MS = 2000;
|
|
29
|
+
var DEFAULT_SERVICE_MANAGER_MAX_BACKOFF_MS = 30000;
|
|
30
|
+
function formatEmbeddedRuntimeHealthLines(health) {
|
|
31
|
+
if (!health)
|
|
32
|
+
return [];
|
|
33
|
+
const lines = [`[pushpals] embeddedRuntime=${health.state} detail=${health.detail}`];
|
|
34
|
+
if (health.action) {
|
|
35
|
+
lines.push(`[pushpals] embeddedRuntimeAction=${health.action}`);
|
|
36
|
+
}
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
39
|
+
function computeServiceRestartBackoffMs(attempt) {
|
|
40
|
+
const boundedAttempt = Math.max(1, Math.floor(attempt));
|
|
41
|
+
const exponential = DEFAULT_SERVICE_MANAGER_BASE_BACKOFF_MS * Math.pow(2, boundedAttempt - 1);
|
|
42
|
+
return Math.max(DEFAULT_SERVICE_MANAGER_BASE_BACKOFF_MS, Math.min(DEFAULT_SERVICE_MANAGER_MAX_BACKOFF_MS, Math.floor(exponential)));
|
|
43
|
+
}
|
|
44
|
+
function shouldRestartService(attempts, maxAttempts = DEFAULT_SERVICE_MANAGER_MAX_RESTART_ATTEMPTS) {
|
|
45
|
+
const normalizedAttempts = Math.max(0, Math.floor(attempts));
|
|
46
|
+
const normalizedMax = Math.max(1, Math.floor(maxAttempts));
|
|
47
|
+
return normalizedAttempts < normalizedMax;
|
|
48
|
+
}
|
|
49
|
+
function pipeProcessStreamToLines(stream, onLine) {
|
|
50
|
+
if (!stream || typeof stream === "number" || typeof stream.getReader !== "function")
|
|
51
|
+
return;
|
|
52
|
+
const reader = stream.getReader();
|
|
53
|
+
const decoder = new TextDecoder;
|
|
54
|
+
let pending = "";
|
|
55
|
+
(async () => {
|
|
56
|
+
try {
|
|
57
|
+
while (true) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
pending += decoder.decode(value, { stream: true });
|
|
62
|
+
const lines = pending.split(/\r?\n/);
|
|
63
|
+
pending = lines.pop() ?? "";
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const trimmed = line.trimEnd();
|
|
66
|
+
if (!trimmed)
|
|
67
|
+
continue;
|
|
68
|
+
onLine?.(trimmed);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const rest = decoder.decode();
|
|
72
|
+
if (rest)
|
|
73
|
+
pending += rest;
|
|
74
|
+
const tail = pending.trimEnd();
|
|
75
|
+
if (tail)
|
|
76
|
+
onLine?.(tail);
|
|
77
|
+
} catch {} finally {
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
}
|
|
82
|
+
function spawnManagedService(spec) {
|
|
83
|
+
const env = { ...spec.env ?? {} };
|
|
84
|
+
const proc = Bun.spawn(spec.command, {
|
|
85
|
+
cwd: spec.cwd,
|
|
86
|
+
env,
|
|
87
|
+
stdout: "pipe",
|
|
88
|
+
stderr: "pipe"
|
|
89
|
+
});
|
|
90
|
+
pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
|
|
91
|
+
pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
|
|
92
|
+
const service = {
|
|
93
|
+
name: spec.name,
|
|
94
|
+
proc,
|
|
95
|
+
command: [...spec.command],
|
|
96
|
+
cwd: spec.cwd,
|
|
97
|
+
env,
|
|
98
|
+
exited: false,
|
|
99
|
+
exitCode: null,
|
|
100
|
+
launchedAtMs: Date.now(),
|
|
101
|
+
logPath: spec.logPath
|
|
102
|
+
};
|
|
103
|
+
proc.exited.then((code) => {
|
|
104
|
+
service.exited = true;
|
|
105
|
+
service.exitCode = code;
|
|
106
|
+
});
|
|
107
|
+
return service;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class ServiceManager {
|
|
111
|
+
services = new Map;
|
|
112
|
+
launchSpecs = new Map;
|
|
113
|
+
stateByService = new Map;
|
|
114
|
+
degradedServiceReasons = new Map;
|
|
115
|
+
pollMs;
|
|
116
|
+
maxRestartAttempts;
|
|
117
|
+
stableWindowMs;
|
|
118
|
+
computeRestartBackoffMs;
|
|
119
|
+
degradedAction;
|
|
120
|
+
spawnService;
|
|
121
|
+
onHealthChange;
|
|
122
|
+
onServiceDegraded;
|
|
123
|
+
onEvent;
|
|
124
|
+
timer;
|
|
125
|
+
stopped = false;
|
|
126
|
+
constructor(options = {}) {
|
|
127
|
+
this.pollMs = Math.max(50, Math.floor(options.pollMs ?? DEFAULT_SERVICE_MANAGER_POLL_MS));
|
|
128
|
+
this.maxRestartAttempts = Math.max(1, Math.floor(options.maxRestartAttempts ?? DEFAULT_SERVICE_MANAGER_MAX_RESTART_ATTEMPTS));
|
|
129
|
+
this.stableWindowMs = Math.max(1000, Math.floor(options.stableWindowMs ?? DEFAULT_SERVICE_MANAGER_STABLE_WINDOW_MS));
|
|
130
|
+
this.computeRestartBackoffMs = options.computeRestartBackoffMs ?? computeServiceRestartBackoffMs;
|
|
131
|
+
this.degradedAction = options.degradedAction ?? "Inspect the affected service logs or restart the runtime after fixing the failure.";
|
|
132
|
+
this.spawnService = options.spawnService ?? spawnManagedService;
|
|
133
|
+
this.onHealthChange = options.onHealthChange;
|
|
134
|
+
this.onServiceDegraded = options.onServiceDegraded;
|
|
135
|
+
this.onEvent = options.onEvent;
|
|
136
|
+
this.timer = setInterval(() => this.tick(), this.pollMs);
|
|
137
|
+
}
|
|
138
|
+
startService(spec) {
|
|
139
|
+
this.launchSpecs.set(spec.name, {
|
|
140
|
+
...spec,
|
|
141
|
+
command: [...spec.command],
|
|
142
|
+
env: { ...spec.env ?? {} }
|
|
143
|
+
});
|
|
144
|
+
const service = this.spawnService(spec);
|
|
145
|
+
this.services.set(spec.name, service);
|
|
146
|
+
return service;
|
|
147
|
+
}
|
|
148
|
+
getServices() {
|
|
149
|
+
return Array.from(this.services.values());
|
|
150
|
+
}
|
|
151
|
+
getService(name) {
|
|
152
|
+
return this.services.get(name) ?? null;
|
|
153
|
+
}
|
|
154
|
+
getHealth() {
|
|
155
|
+
if (this.degradedServiceReasons.size === 0)
|
|
156
|
+
return null;
|
|
157
|
+
const detail = Array.from(this.degradedServiceReasons.entries()).map(([name, reason]) => `${name}: ${reason}`).join(" | ");
|
|
158
|
+
return {
|
|
159
|
+
state: "degraded",
|
|
160
|
+
detail,
|
|
161
|
+
action: this.degradedAction
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
stop() {
|
|
165
|
+
if (this.stopped)
|
|
166
|
+
return;
|
|
167
|
+
this.stopped = true;
|
|
168
|
+
clearInterval(this.timer);
|
|
169
|
+
for (const state of this.stateByService.values()) {
|
|
170
|
+
if (!state.pendingRestartTimer)
|
|
171
|
+
continue;
|
|
172
|
+
clearTimeout(state.pendingRestartTimer);
|
|
173
|
+
state.pendingRestartTimer = null;
|
|
174
|
+
}
|
|
175
|
+
for (const service of this.services.values()) {
|
|
176
|
+
try {
|
|
177
|
+
const pid = service.proc.pid;
|
|
178
|
+
if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
|
|
179
|
+
Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
180
|
+
stdin: "ignore",
|
|
181
|
+
stdout: "ignore",
|
|
182
|
+
stderr: "ignore"
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
service.proc.kill();
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
ensureState(name) {
|
|
191
|
+
const existing = this.stateByService.get(name);
|
|
192
|
+
if (existing)
|
|
193
|
+
return existing;
|
|
194
|
+
const created = {
|
|
195
|
+
attempts: 0,
|
|
196
|
+
nextRestartAtMs: 0,
|
|
197
|
+
lastRestartReason: "",
|
|
198
|
+
pendingRestartTimer: null
|
|
199
|
+
};
|
|
200
|
+
this.stateByService.set(name, created);
|
|
201
|
+
return created;
|
|
202
|
+
}
|
|
203
|
+
emitHealthChange() {
|
|
204
|
+
this.onHealthChange?.(this.getHealth());
|
|
205
|
+
}
|
|
206
|
+
emitEvent(level, line) {
|
|
207
|
+
this.onEvent?.(level, line);
|
|
208
|
+
}
|
|
209
|
+
tick() {
|
|
210
|
+
if (this.stopped)
|
|
211
|
+
return;
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
for (const [name, service] of this.services.entries()) {
|
|
214
|
+
const launchSpec = this.launchSpecs.get(name);
|
|
215
|
+
if (!launchSpec)
|
|
216
|
+
continue;
|
|
217
|
+
const state = this.ensureState(name);
|
|
218
|
+
if (!service.exited) {
|
|
219
|
+
if (state.attempts > 0 && now - service.launchedAtMs >= this.stableWindowMs) {
|
|
220
|
+
state.attempts = 0;
|
|
221
|
+
state.nextRestartAtMs = 0;
|
|
222
|
+
state.lastRestartReason = "";
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (state.pendingRestartTimer)
|
|
227
|
+
continue;
|
|
228
|
+
if (state.nextRestartAtMs > now)
|
|
229
|
+
continue;
|
|
230
|
+
const reason = `exit code ${service.exitCode ?? "unknown"}`;
|
|
231
|
+
if (!shouldRestartService(state.attempts, this.maxRestartAttempts)) {
|
|
232
|
+
this.emitEvent("error", `Managed ${name} exited (${reason}) and reached restart limit (${state.attempts}/${this.maxRestartAttempts}).`);
|
|
233
|
+
this.launchSpecs.delete(name);
|
|
234
|
+
if (!this.degradedServiceReasons.has(name)) {
|
|
235
|
+
const degradationReason = `reached restart limit after ${reason} (${state.attempts}/${this.maxRestartAttempts})`;
|
|
236
|
+
this.degradedServiceReasons.set(name, degradationReason);
|
|
237
|
+
const health = this.getHealth();
|
|
238
|
+
if (health) {
|
|
239
|
+
this.onHealthChange?.(health);
|
|
240
|
+
this.onServiceDegraded?.(name, degradationReason, health);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const nextAttempt = state.attempts + 1;
|
|
246
|
+
state.lastRestartReason = reason;
|
|
247
|
+
const backoffMs = Math.max(1, Math.floor(this.computeRestartBackoffMs(nextAttempt)));
|
|
248
|
+
this.emitEvent("warn", `Managed ${name} exited (${reason}); restarting attempt ${nextAttempt}/${this.maxRestartAttempts} in ${backoffMs}ms.`);
|
|
249
|
+
state.nextRestartAtMs = now + backoffMs;
|
|
250
|
+
state.pendingRestartTimer = setTimeout(() => {
|
|
251
|
+
state.pendingRestartTimer = null;
|
|
252
|
+
state.nextRestartAtMs = 0;
|
|
253
|
+
if (this.stopped)
|
|
254
|
+
return;
|
|
255
|
+
const current = this.services.get(name);
|
|
256
|
+
if (!current || !current.exited)
|
|
257
|
+
return;
|
|
258
|
+
const spec = this.launchSpecs.get(name);
|
|
259
|
+
if (!spec)
|
|
260
|
+
return;
|
|
261
|
+
if (!shouldRestartService(state.attempts, this.maxRestartAttempts))
|
|
262
|
+
return;
|
|
263
|
+
state.attempts += 1;
|
|
264
|
+
const restarted = this.spawnService(spec);
|
|
265
|
+
this.services.set(name, restarted);
|
|
266
|
+
this.emitEvent("log", `Restarted managed ${name}.`);
|
|
267
|
+
}, backoffMs);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../../apps/workerpals/src/common/worktree_cleanup.ts
|
|
273
|
+
import { existsSync, rmSync } from "fs";
|
|
274
|
+
function defaultSleep(ms) {
|
|
275
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
276
|
+
}
|
|
277
|
+
function windowsDeletionCandidates(worktreePath) {
|
|
278
|
+
const seen = new Set;
|
|
279
|
+
const out = [];
|
|
280
|
+
const add = (value) => {
|
|
281
|
+
if (!value || seen.has(value))
|
|
282
|
+
return;
|
|
283
|
+
seen.add(value);
|
|
284
|
+
out.push(value);
|
|
285
|
+
};
|
|
286
|
+
add(worktreePath);
|
|
287
|
+
if (process.platform === "win32" && /^[A-Za-z]:[\\/]/.test(worktreePath)) {
|
|
288
|
+
add(`\\\\?\\${worktreePath}`);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
async function forceDeleteWorktreePath(worktreePath, options = {}) {
|
|
293
|
+
const retries = Math.max(1, Math.floor(options.retries ?? 5));
|
|
294
|
+
const delayMs = Math.max(0, Math.floor(options.delayMs ?? 120));
|
|
295
|
+
const sleep = options.sleepFn ?? defaultSleep;
|
|
296
|
+
const removePath = options.removeFn ?? ((targetPath) => rmSync(targetPath, { recursive: true, force: true }));
|
|
297
|
+
const pathExists = options.existsFn ?? ((targetPath) => existsSync(targetPath));
|
|
298
|
+
let lastError = "";
|
|
299
|
+
for (let attempt = 1;attempt <= retries; attempt++) {
|
|
300
|
+
if (!pathExists(worktreePath))
|
|
301
|
+
return { removed: true };
|
|
302
|
+
for (const candidate of windowsDeletionCandidates(worktreePath)) {
|
|
303
|
+
try {
|
|
304
|
+
removePath(candidate);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
lastError = String(error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!pathExists(worktreePath))
|
|
310
|
+
return { removed: true };
|
|
311
|
+
if (attempt < retries)
|
|
312
|
+
await sleep(delayMs * attempt);
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
removed: !pathExists(worktreePath),
|
|
316
|
+
...lastError ? { lastError } : {}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
20
320
|
// ../shared/src/client_preflight.ts
|
|
21
|
-
import { existsSync as
|
|
321
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
22
322
|
import { relative, resolve as resolve2 } from "path";
|
|
23
323
|
|
|
24
324
|
// ../shared/src/config.ts
|
|
25
|
-
import { existsSync, readFileSync } from "fs";
|
|
325
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
26
326
|
import { join, resolve, isAbsolute } from "path";
|
|
27
327
|
|
|
28
328
|
// ../shared/src/autonomy_policy.ts
|
|
@@ -101,8 +401,8 @@ function normalizeLoopbackHttpUrl(value, fallbackPort) {
|
|
|
101
401
|
// ../shared/src/config.ts
|
|
102
402
|
var PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "..");
|
|
103
403
|
var DEFAULT_CONFIG_DIR = "configs";
|
|
104
|
-
var
|
|
105
|
-
var
|
|
404
|
+
var TRUTHY2 = new Set(["1", "true", "yes", "on"]);
|
|
405
|
+
var FALSY2 = new Set(["0", "false", "no", "off"]);
|
|
106
406
|
var DEFAULT_WORKERPALS_QUALITY_CRITIC_MIN_SCORE = 8;
|
|
107
407
|
var DEFAULT_WORKERPALS_QUALITY_MAX_AUTO_REVISIONS = 1;
|
|
108
408
|
var DEFAULT_WORKERPALS_FILE_MODIFYING_JOBS = ["task.execute"];
|
|
@@ -133,9 +433,9 @@ function parseBoolEnv(name) {
|
|
|
133
433
|
const raw = (process.env[name] ?? "").trim().toLowerCase();
|
|
134
434
|
if (!raw)
|
|
135
435
|
return null;
|
|
136
|
-
if (
|
|
436
|
+
if (TRUTHY2.has(raw))
|
|
137
437
|
return true;
|
|
138
|
-
if (
|
|
438
|
+
if (FALSY2.has(raw))
|
|
139
439
|
return false;
|
|
140
440
|
return null;
|
|
141
441
|
}
|
|
@@ -147,7 +447,7 @@ function parseIntEnv(name) {
|
|
|
147
447
|
return Number.isFinite(parsed) ? parsed : null;
|
|
148
448
|
}
|
|
149
449
|
function parseTomlFile(path) {
|
|
150
|
-
if (!
|
|
450
|
+
if (!existsSync2(path))
|
|
151
451
|
return {};
|
|
152
452
|
const raw = readFileSync(path, "utf-8");
|
|
153
453
|
const parsed = Bun.TOML.parse(raw);
|
|
@@ -156,7 +456,7 @@ function parseTomlFile(path) {
|
|
|
156
456
|
return parsed;
|
|
157
457
|
}
|
|
158
458
|
function parseRequiredTomlFile(path) {
|
|
159
|
-
if (!
|
|
459
|
+
if (!existsSync2(path)) {
|
|
160
460
|
throw new Error(`Missing required runtime config file: ${path}`);
|
|
161
461
|
}
|
|
162
462
|
return parseTomlFile(path);
|
|
@@ -192,9 +492,9 @@ function asBoolean(value, fallback) {
|
|
|
192
492
|
return value;
|
|
193
493
|
if (typeof value === "string") {
|
|
194
494
|
const lowered = value.trim().toLowerCase();
|
|
195
|
-
if (
|
|
495
|
+
if (TRUTHY2.has(lowered))
|
|
196
496
|
return true;
|
|
197
|
-
if (
|
|
497
|
+
if (FALSY2.has(lowered))
|
|
198
498
|
return false;
|
|
199
499
|
}
|
|
200
500
|
return fallback;
|
|
@@ -949,7 +1249,7 @@ function validateVisionDocStructure(markdown) {
|
|
|
949
1249
|
// ../shared/src/client_preflight.ts
|
|
950
1250
|
function runtimeHasConfigDir(runtimeRoot, dirName) {
|
|
951
1251
|
const dirPath = resolve2(runtimeRoot, dirName);
|
|
952
|
-
return
|
|
1252
|
+
return existsSync3(resolve2(dirPath, "default.toml")) || existsSync3(resolve2(dirPath, "local.example.toml")) || existsSync3(resolve2(dirPath, "local.toml"));
|
|
953
1253
|
}
|
|
954
1254
|
function resolveClientConfigDir(projectRoot, runtimeRoot, explicitConfigDir) {
|
|
955
1255
|
if (explicitConfigDir && explicitConfigDir.trim()) {
|
|
@@ -1003,21 +1303,21 @@ function evaluateClientRuntimePreflight(options) {
|
|
|
1003
1303
|
});
|
|
1004
1304
|
const issues = [];
|
|
1005
1305
|
const envPath = resolve2(runtimeRoot, ".env");
|
|
1006
|
-
if (!
|
|
1306
|
+
if (!existsSync3(envPath)) {
|
|
1007
1307
|
const envExamplePath = resolve2(runtimeRoot, ".env.example");
|
|
1008
1308
|
issues.push({
|
|
1009
1309
|
code: "missing_env_file",
|
|
1010
1310
|
message: `Missing required local env file: ${toDisplayPath(projectRoot, envPath)}.`,
|
|
1011
|
-
copyCommands:
|
|
1311
|
+
copyCommands: existsSync3(envExamplePath) ? buildCopyCommands(projectRoot, envExamplePath, envPath) : undefined
|
|
1012
1312
|
});
|
|
1013
1313
|
}
|
|
1014
1314
|
const localTomlPath = resolve2(runtimeRoot, "configs", "local.toml");
|
|
1015
|
-
if (!
|
|
1315
|
+
if (!existsSync3(localTomlPath)) {
|
|
1016
1316
|
const localExamplePath = resolve2(runtimeRoot, "configs", "local.example.toml");
|
|
1017
1317
|
issues.push({
|
|
1018
1318
|
code: "missing_local_toml",
|
|
1019
1319
|
message: `Missing required local config file: ${toDisplayPath(projectRoot, localTomlPath)}.`,
|
|
1020
|
-
copyCommands:
|
|
1320
|
+
copyCommands: existsSync3(localExamplePath) ? buildCopyCommands(projectRoot, localExamplePath, localTomlPath) : undefined
|
|
1021
1321
|
});
|
|
1022
1322
|
}
|
|
1023
1323
|
const autonomyEnabled = Boolean(config.remotebuddy.autonomy.enabled);
|
|
@@ -1034,11 +1334,11 @@ function evaluateClientRuntimePreflight(options) {
|
|
|
1034
1334
|
}
|
|
1035
1335
|
const visionPath = resolve2(projectRoot, "vision.md");
|
|
1036
1336
|
const visionTemplatePath = resolve2(visionTemplateRoot, "vision.example.md");
|
|
1037
|
-
if (!
|
|
1337
|
+
if (!existsSync3(visionPath)) {
|
|
1038
1338
|
issues.push({
|
|
1039
1339
|
code: "missing_vision_doc",
|
|
1040
1340
|
message: "Missing required autonomy vision file: vision.md " + "(required when remotebuddy.autonomy.enabled=true).",
|
|
1041
|
-
copyCommands:
|
|
1341
|
+
copyCommands: existsSync3(visionTemplatePath) ? buildCopyCommands(projectRoot, visionTemplatePath, visionPath) : undefined
|
|
1042
1342
|
});
|
|
1043
1343
|
return {
|
|
1044
1344
|
ok: false,
|
|
@@ -1152,14 +1452,14 @@ function normalizePresenceLookupToken(value) {
|
|
|
1152
1452
|
}
|
|
1153
1453
|
|
|
1154
1454
|
// ../shared/src/repo.ts
|
|
1155
|
-
import { existsSync as
|
|
1455
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
|
|
1156
1456
|
import { resolve as resolve3 } from "path";
|
|
1157
1457
|
function resolveDotGitEntry(repoRoot) {
|
|
1158
1458
|
return resolve3(repoRoot, ".git");
|
|
1159
1459
|
}
|
|
1160
1460
|
function resolveGitMetadataDir(repoRoot) {
|
|
1161
1461
|
const dotGitPath = resolveDotGitEntry(repoRoot);
|
|
1162
|
-
if (!
|
|
1462
|
+
if (!existsSync4(dotGitPath))
|
|
1163
1463
|
return null;
|
|
1164
1464
|
try {
|
|
1165
1465
|
const stat = statSync(dotGitPath);
|
|
@@ -1178,7 +1478,7 @@ function resolveGitMetadataDir(repoRoot) {
|
|
|
1178
1478
|
if (!match)
|
|
1179
1479
|
return null;
|
|
1180
1480
|
const gitDir = resolve3(repoRoot, match[1].trim());
|
|
1181
|
-
return
|
|
1481
|
+
return existsSync4(gitDir) ? gitDir : null;
|
|
1182
1482
|
} catch {
|
|
1183
1483
|
return null;
|
|
1184
1484
|
}
|
|
@@ -1217,6 +1517,7 @@ var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
|
1217
1517
|
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
1218
1518
|
var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
|
|
1219
1519
|
var DEFAULT_SERVICE_STABILITY_GRACE_MS = 4000;
|
|
1520
|
+
var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
|
|
1220
1521
|
var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
|
|
1221
1522
|
var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
|
|
1222
1523
|
var EMBEDDED_RUNTIME_WINDOWS_SAFETY_CAPS = {
|
|
@@ -1292,6 +1593,9 @@ function formatWorkerExecutionReadinessLines(readiness) {
|
|
|
1292
1593
|
}
|
|
1293
1594
|
return lines;
|
|
1294
1595
|
}
|
|
1596
|
+
function formatEmbeddedRuntimeHealthLines2(health) {
|
|
1597
|
+
return formatEmbeddedRuntimeHealthLines(health);
|
|
1598
|
+
}
|
|
1295
1599
|
function summarizeWorkerStatusRows(workers) {
|
|
1296
1600
|
const onlineWorkers = workers.filter((worker) => Boolean(worker?.isOnline) && String(worker?.status ?? "").trim().toLowerCase() !== "offline");
|
|
1297
1601
|
const idleWorkers = onlineWorkers.filter((worker) => Number(worker?.activeJobCount ?? 0) <= 0);
|
|
@@ -1600,7 +1904,7 @@ function copyTrackedRepoPath(repoRoot, sourcePath, destinationPath, force = true
|
|
|
1600
1904
|
throw new Error("sourcePath is required");
|
|
1601
1905
|
}
|
|
1602
1906
|
const absoluteSource = resolve4(repoRoot, normalizedSource);
|
|
1603
|
-
if (!
|
|
1907
|
+
if (!existsSync5(absoluteSource)) {
|
|
1604
1908
|
throw new Error(`tracked repo source is missing: ${absoluteSource}`);
|
|
1605
1909
|
}
|
|
1606
1910
|
const trackedFiles = listTrackedRepoFilesForPath(repoRoot, normalizedSource);
|
|
@@ -1633,7 +1937,7 @@ function copyTrackedRepoPath(repoRoot, sourcePath, destinationPath, force = true
|
|
|
1633
1937
|
}
|
|
1634
1938
|
}
|
|
1635
1939
|
function isCompleteWorkerpalSandboxRoot(root) {
|
|
1636
|
-
return
|
|
1940
|
+
return existsSync5(join2(root, "package.json")) && existsSync5(join2(root, "apps", "workerpals", "Dockerfile.sandbox")) && existsSync5(join2(root, "packages", "shared", "package.json")) && existsSync5(join2(root, "packages", "protocol", "package.json")) && existsSync5(join2(root, "configs", "default.toml")) && existsSync5(join2(root, "prompts", "workerpals")) && existsSync5(join2(root, "protocol", "schemas", "envelope.schema.json")) && existsSync5(join2(root, "protocol", "schemas", "events.schema.json"));
|
|
1637
1941
|
}
|
|
1638
1942
|
function populateWorkerpalSandboxRuntimeAssets(runtimeRoot, force) {
|
|
1639
1943
|
const sandbox = buildWorkerpalSandboxPaths(runtimeRoot);
|
|
@@ -1664,7 +1968,7 @@ function copySourceCheckoutWorkerpalSandboxBuildContext(sourceRoot, runtimeRoot,
|
|
|
1664
1968
|
for (const [fromPath, toPath] of copyPairs) {
|
|
1665
1969
|
copyTrackedRepoPath(sourceRoot, fromPath, toPath, force);
|
|
1666
1970
|
}
|
|
1667
|
-
if (
|
|
1971
|
+
if (existsSync5(join2(sourceRoot, "bun.lock"))) {
|
|
1668
1972
|
copyTrackedRepoPath(sourceRoot, "bun.lock", join2(sandbox.root, "bun.lock"), force);
|
|
1669
1973
|
}
|
|
1670
1974
|
populateWorkerpalSandboxRuntimeAssets(runtimeRoot, force);
|
|
@@ -1682,7 +1986,7 @@ function copyWorkerpalSandboxBuildContext(source, runtimeRoot, force) {
|
|
|
1682
1986
|
copySourceCheckoutWorkerpalSandboxBuildContext(source.root, runtimeRoot, force);
|
|
1683
1987
|
}
|
|
1684
1988
|
function isCompleteRuntimeAssetSource(source) {
|
|
1685
|
-
return
|
|
1989
|
+
return existsSync5(source.envExamplePath) && existsSync5(source.visionExamplePath) && existsSync5(join2(source.configsDir, "default.toml")) && existsSync5(source.promptsDir) && existsSync5(join2(source.protocolSchemasDir, "envelope.schema.json")) && existsSync5(join2(source.protocolSchemasDir, "events.schema.json"));
|
|
1686
1990
|
}
|
|
1687
1991
|
function resolveBundledRuntimeAssetSource() {
|
|
1688
1992
|
const candidates = [
|
|
@@ -1697,10 +2001,10 @@ function resolveBundledRuntimeAssetSource() {
|
|
|
1697
2001
|
return null;
|
|
1698
2002
|
}
|
|
1699
2003
|
function looksLikeMonitoringHubBuild(root) {
|
|
1700
|
-
return
|
|
2004
|
+
return existsSync5(join2(root, "index.html")) && existsSync5(join2(root, "_expo"));
|
|
1701
2005
|
}
|
|
1702
2006
|
function latestPathMtimeMs(pathValue) {
|
|
1703
|
-
if (!
|
|
2007
|
+
if (!existsSync5(pathValue))
|
|
1704
2008
|
return 0;
|
|
1705
2009
|
const stat = lstatSync(pathValue);
|
|
1706
2010
|
let latest = stat.mtimeMs;
|
|
@@ -1753,7 +2057,7 @@ function resolveCliSourceCheckoutRoot() {
|
|
|
1753
2057
|
resolve4(import.meta.dir, "..", "..", "..")
|
|
1754
2058
|
];
|
|
1755
2059
|
for (const candidate of candidates) {
|
|
1756
|
-
if (
|
|
2060
|
+
if (existsSync5(join2(candidate, "package.json")) && existsSync5(join2(candidate, "apps", "client", "app.json")) && existsSync5(join2(candidate, "scripts", "sync-cli-monitor-ui.ts"))) {
|
|
1757
2061
|
return candidate;
|
|
1758
2062
|
}
|
|
1759
2063
|
}
|
|
@@ -1787,7 +2091,7 @@ async function ensureBundledMonitoringHubRoot() {
|
|
|
1787
2091
|
return resolveBundledMonitoringHubRoot();
|
|
1788
2092
|
}
|
|
1789
2093
|
function repoLooksLikePushPalsSourceCheckout(repoRoot) {
|
|
1790
|
-
return
|
|
2094
|
+
return existsSync5(join2(repoRoot, "configs", "default.toml"));
|
|
1791
2095
|
}
|
|
1792
2096
|
function parseSemverFromPackageVersion(value) {
|
|
1793
2097
|
const raw = String(value ?? "").trim();
|
|
@@ -1837,13 +2141,13 @@ async function resolveRuntimeReleaseTag(explicitTag) {
|
|
|
1837
2141
|
return await fetchLatestReleaseTag();
|
|
1838
2142
|
}
|
|
1839
2143
|
function writeTextFileIfMissing(pathValue, text) {
|
|
1840
|
-
if (
|
|
2144
|
+
if (existsSync5(pathValue))
|
|
1841
2145
|
return;
|
|
1842
2146
|
mkdirSync(dirname(pathValue), { recursive: true });
|
|
1843
2147
|
writeFileSync(pathValue, text, "utf8");
|
|
1844
2148
|
}
|
|
1845
2149
|
function migrateEmbeddedRuntimeLocalToml(localTomlPath) {
|
|
1846
|
-
if (!
|
|
2150
|
+
if (!existsSync5(localTomlPath))
|
|
1847
2151
|
return;
|
|
1848
2152
|
let original;
|
|
1849
2153
|
try {
|
|
@@ -1893,8 +2197,8 @@ function copyBundledRuntimeAssets(runtimeRoot, force = true) {
|
|
|
1893
2197
|
}
|
|
1894
2198
|
function hasSeededRuntimePreflightAssets(runtimeRoot) {
|
|
1895
2199
|
const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
|
|
1896
|
-
const hasProtocolSchemas =
|
|
1897
|
-
return
|
|
2200
|
+
const hasProtocolSchemas = existsSync5(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync5(join2(protocolSchemasDir, "events.schema.json"));
|
|
2201
|
+
return existsSync5(join2(runtimeRoot, ".env.example")) && existsSync5(join2(runtimeRoot, "vision.example.md")) && existsSync5(join2(runtimeRoot, "configs", "default.toml")) && existsSync5(join2(runtimeRoot, "prompts")) && hasProtocolSchemas && isCompleteWorkerpalSandboxRoot(join2(runtimeRoot, "sandbox"));
|
|
1898
2202
|
}
|
|
1899
2203
|
function seedRuntimePreflightAssets(runtimeRoot) {
|
|
1900
2204
|
if (!hasSeededRuntimePreflightAssets(runtimeRoot)) {
|
|
@@ -1904,7 +2208,7 @@ function seedRuntimePreflightAssets(runtimeRoot) {
|
|
|
1904
2208
|
`);
|
|
1905
2209
|
const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
|
|
1906
2210
|
const localTomlPath = join2(runtimeRoot, "configs", "local.toml");
|
|
1907
|
-
if (
|
|
2211
|
+
if (existsSync5(localExamplePath)) {
|
|
1908
2212
|
writeTextFileIfMissing(localTomlPath, readFileSync4(localExamplePath, "utf8"));
|
|
1909
2213
|
} else {
|
|
1910
2214
|
writeTextFileIfMissing(localTomlPath, `# Local PushPals runtime overrides
|
|
@@ -1949,15 +2253,15 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
|
|
|
1949
2253
|
async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
|
|
1950
2254
|
console.log(`[pushpals] Preparing embedded runtime assets for ${runtimeTag}...`);
|
|
1951
2255
|
const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
|
|
1952
|
-
const currentTag =
|
|
2256
|
+
const currentTag = existsSync5(markerPath) ? readFileSync4(markerPath, "utf8").trim() : "";
|
|
1953
2257
|
const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
|
|
1954
|
-
const hasProtocolSchemas =
|
|
1955
|
-
const hasAssets =
|
|
2258
|
+
const hasProtocolSchemas = existsSync5(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync5(join2(protocolSchemasDir, "events.schema.json"));
|
|
2259
|
+
const hasAssets = existsSync5(join2(runtimeRoot, ".env.example")) && existsSync5(join2(runtimeRoot, "vision.example.md")) && existsSync5(join2(runtimeRoot, "configs", "default.toml")) && existsSync5(join2(runtimeRoot, "prompts")) && hasProtocolSchemas && isCompleteWorkerpalSandboxRoot(join2(runtimeRoot, "sandbox"));
|
|
1956
2260
|
if (!hasAssets || currentTag !== runtimeTag) {
|
|
1957
2261
|
console.log(`[pushpals] Embedded runtime assets ${hasAssets ? "are stale" : "are missing"}; refreshing bundle...`);
|
|
1958
2262
|
copyBundledRuntimeAssets(runtimeRoot);
|
|
1959
|
-
const hasProtocolSchemasAfterCopy =
|
|
1960
|
-
const hasAssetsAfterCopy =
|
|
2263
|
+
const hasProtocolSchemasAfterCopy = existsSync5(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync5(join2(protocolSchemasDir, "events.schema.json"));
|
|
2264
|
+
const hasAssetsAfterCopy = existsSync5(join2(runtimeRoot, ".env.example")) && existsSync5(join2(runtimeRoot, "vision.example.md")) && existsSync5(join2(runtimeRoot, "configs", "default.toml")) && existsSync5(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy && isCompleteWorkerpalSandboxRoot(join2(runtimeRoot, "sandbox"));
|
|
1961
2265
|
if (!hasAssetsAfterCopy) {
|
|
1962
2266
|
console.log("[pushpals] Bundled runtime assets are incomplete; falling back to release source downloads...");
|
|
1963
2267
|
await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
|
|
@@ -1969,7 +2273,7 @@ async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
|
|
|
1969
2273
|
`);
|
|
1970
2274
|
const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
|
|
1971
2275
|
const localTomlPath = join2(runtimeRoot, "configs", "local.toml");
|
|
1972
|
-
if (
|
|
2276
|
+
if (existsSync5(localExamplePath)) {
|
|
1973
2277
|
writeTextFileIfMissing(localTomlPath, readFileSync4(localExamplePath, "utf8"));
|
|
1974
2278
|
} else {
|
|
1975
2279
|
writeTextFileIfMissing(localTomlPath, `# Local PushPals runtime overrides
|
|
@@ -2023,12 +2327,12 @@ function runtimeBinaryFilename(serviceName, platformKey) {
|
|
|
2023
2327
|
function resolveRuntimeBinaryInstallState(runtimeRoot, platformKey) {
|
|
2024
2328
|
const binDir = join2(runtimeRoot, "bin", platformKey);
|
|
2025
2329
|
const tagMarkerPath = join2(binDir, ".runtime-tag");
|
|
2026
|
-
const installedTag =
|
|
2330
|
+
const installedTag = existsSync5(tagMarkerPath) ? readFileSync4(tagMarkerPath, "utf8").trim() : "";
|
|
2027
2331
|
return { binDir, tagMarkerPath, installedTag };
|
|
2028
2332
|
}
|
|
2029
2333
|
function cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, activeBinDir) {
|
|
2030
2334
|
const legacyRoot = join2(runtimeRoot, "bin");
|
|
2031
|
-
if (!
|
|
2335
|
+
if (!existsSync5(legacyRoot))
|
|
2032
2336
|
return;
|
|
2033
2337
|
let entries;
|
|
2034
2338
|
try {
|
|
@@ -2045,7 +2349,7 @@ function cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, activeBinDi
|
|
|
2045
2349
|
if (!entry.name.endsWith(`-${platformKey}`))
|
|
2046
2350
|
continue;
|
|
2047
2351
|
try {
|
|
2048
|
-
|
|
2352
|
+
rmSync2(candidateDir, { recursive: true, force: true });
|
|
2049
2353
|
} catch {}
|
|
2050
2354
|
}
|
|
2051
2355
|
}
|
|
@@ -2117,7 +2421,7 @@ function resolveEmbeddedBunExecutableFromEnv(env, platform = process.platform, c
|
|
|
2117
2421
|
continue;
|
|
2118
2422
|
for (const candidate of candidates) {
|
|
2119
2423
|
const fullPath = join2(dir, candidate);
|
|
2120
|
-
if (
|
|
2424
|
+
if (existsSync5(fullPath)) {
|
|
2121
2425
|
return fullPath;
|
|
2122
2426
|
}
|
|
2123
2427
|
}
|
|
@@ -2190,7 +2494,7 @@ function appendRuntimeServicesLogLine(logPath, line) {
|
|
|
2190
2494
|
} catch {}
|
|
2191
2495
|
}
|
|
2192
2496
|
function readLogTail(logPath, maxLines = 40) {
|
|
2193
|
-
if (!
|
|
2497
|
+
if (!existsSync5(logPath))
|
|
2194
2498
|
return "";
|
|
2195
2499
|
const raw = readFileSync4(logPath, "utf8");
|
|
2196
2500
|
const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
|
|
@@ -2216,7 +2520,7 @@ function extractRemoteBuddyAutonomousEngineState(logText) {
|
|
|
2216
2520
|
return state;
|
|
2217
2521
|
}
|
|
2218
2522
|
function readRemoteBuddyAutonomousEngineState(logPath) {
|
|
2219
|
-
if (!
|
|
2523
|
+
if (!existsSync5(logPath))
|
|
2220
2524
|
return "unknown";
|
|
2221
2525
|
try {
|
|
2222
2526
|
return extractRemoteBuddyAutonomousEngineState(readFileSync4(logPath, "utf8"));
|
|
@@ -2258,7 +2562,7 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
|
|
|
2258
2562
|
const shouldRefreshAll = installedTag !== runtimeTag;
|
|
2259
2563
|
let downloadedCount = 0;
|
|
2260
2564
|
for (const binaryPath of requiredAssets) {
|
|
2261
|
-
if (!shouldRefreshAll &&
|
|
2565
|
+
if (!shouldRefreshAll && existsSync5(binaryPath))
|
|
2262
2566
|
continue;
|
|
2263
2567
|
const assetName = binaryPath.split(/[\\/]/).pop() || "";
|
|
2264
2568
|
await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
|
|
@@ -2282,71 +2586,6 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
|
|
|
2282
2586
|
console.log("[pushpals] Embedded runtime binaries are ready.");
|
|
2283
2587
|
return runtimeBinaries;
|
|
2284
2588
|
}
|
|
2285
|
-
function spawnRuntimeService(name, command, cwd, env, logPath, runtimeServicesLogPath) {
|
|
2286
|
-
const header = `[pushpals] service=${name} command=${command.join(" ")} cwd=${cwd}`;
|
|
2287
|
-
writeFileSync(logPath, `${header}
|
|
2288
|
-
`, "utf8");
|
|
2289
|
-
if (runtimeServicesLogPath) {
|
|
2290
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath, header);
|
|
2291
|
-
}
|
|
2292
|
-
const proc = Bun.spawn(command, {
|
|
2293
|
-
cwd,
|
|
2294
|
-
env,
|
|
2295
|
-
stdout: "pipe",
|
|
2296
|
-
stderr: "pipe"
|
|
2297
|
-
});
|
|
2298
|
-
const pipeToLog = async (stream, channel) => {
|
|
2299
|
-
if (!stream)
|
|
2300
|
-
return;
|
|
2301
|
-
const reader = stream.getReader();
|
|
2302
|
-
const decoder = new TextDecoder;
|
|
2303
|
-
let pending = "";
|
|
2304
|
-
while (true) {
|
|
2305
|
-
const { done, value } = await reader.read();
|
|
2306
|
-
if (done)
|
|
2307
|
-
break;
|
|
2308
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
2309
|
-
if (!chunk)
|
|
2310
|
-
continue;
|
|
2311
|
-
pending += chunk;
|
|
2312
|
-
const lines = pending.split(/\r?\n/);
|
|
2313
|
-
pending = lines.pop() ?? "";
|
|
2314
|
-
for (const line of lines) {
|
|
2315
|
-
const serviceLine = `[${channel}] ${line}`;
|
|
2316
|
-
appendFileSync(logPath, `${serviceLine}
|
|
2317
|
-
`, "utf8");
|
|
2318
|
-
if (runtimeServicesLogPath) {
|
|
2319
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
const rest = decoder.decode();
|
|
2324
|
-
if (rest)
|
|
2325
|
-
pending += rest;
|
|
2326
|
-
if (pending.trim().length > 0) {
|
|
2327
|
-
const serviceLine = `[${channel}] ${pending.trimEnd()}`;
|
|
2328
|
-
appendFileSync(logPath, `${serviceLine}
|
|
2329
|
-
`, "utf8");
|
|
2330
|
-
if (runtimeServicesLogPath) {
|
|
2331
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
};
|
|
2335
|
-
pipeToLog(proc.stdout, "stdout");
|
|
2336
|
-
pipeToLog(proc.stderr, "stderr");
|
|
2337
|
-
const service = {
|
|
2338
|
-
name,
|
|
2339
|
-
proc,
|
|
2340
|
-
logPath,
|
|
2341
|
-
exited: false,
|
|
2342
|
-
exitCode: null
|
|
2343
|
-
};
|
|
2344
|
-
proc.exited.then((code) => {
|
|
2345
|
-
service.exited = true;
|
|
2346
|
-
service.exitCode = code;
|
|
2347
|
-
});
|
|
2348
|
-
return service;
|
|
2349
|
-
}
|
|
2350
2589
|
function buildServiceStopCommand(pid, platform = process.platform) {
|
|
2351
2590
|
if (platform === "win32" && typeof pid === "number" && pid > 0) {
|
|
2352
2591
|
return ["taskkill", "/PID", String(pid), "/T", "/F"];
|
|
@@ -2560,6 +2799,12 @@ function quoteWindowsCmdArg(value) {
|
|
|
2560
2799
|
function isOptionalEmbeddedService(name) {
|
|
2561
2800
|
return name === "source_control_manager";
|
|
2562
2801
|
}
|
|
2802
|
+
function computeEmbeddedServiceRestartBackoffMs(attempt) {
|
|
2803
|
+
return computeServiceRestartBackoffMs(attempt);
|
|
2804
|
+
}
|
|
2805
|
+
function shouldRestartEmbeddedService(attempts, maxAttempts = EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS) {
|
|
2806
|
+
return shouldRestartService(attempts, maxAttempts);
|
|
2807
|
+
}
|
|
2563
2808
|
async function canSpawnCommand(command, cwd, env) {
|
|
2564
2809
|
try {
|
|
2565
2810
|
const proc = Bun.spawn(command, {
|
|
@@ -2721,6 +2966,7 @@ async function cleanupLingeringWorkerpalWarmContainers(opts) {
|
|
|
2721
2966
|
}
|
|
2722
2967
|
async function cleanupLingeringPushPalsGitWorktrees(opts) {
|
|
2723
2968
|
const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
|
|
2969
|
+
const forceDeleteWorktreePathFn = opts.forceDeleteWorktreePathFn ?? forceDeleteWorktreePath;
|
|
2724
2970
|
const list = await runCommandWithEnvFn(["git", "worktree", "list", "--porcelain"], opts.repoRoot, opts.env);
|
|
2725
2971
|
if (!list.ok) {
|
|
2726
2972
|
const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
|
|
@@ -2748,7 +2994,13 @@ async function cleanupLingeringPushPalsGitWorktrees(opts) {
|
|
|
2748
2994
|
removed += 1;
|
|
2749
2995
|
continue;
|
|
2750
2996
|
}
|
|
2751
|
-
|
|
2997
|
+
const forced = await forceDeleteWorktreePathFn(entry.path);
|
|
2998
|
+
if (forced.removed) {
|
|
2999
|
+
removed += 1;
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
const removeDetail = remove.stderr || remove.stdout || `exit ${remove.exitCode}`;
|
|
3003
|
+
failures.push(`${entry.path}: ${removeDetail}${forced.lastError ? ` | fallback: ${forced.lastError}` : ""}`);
|
|
2752
3004
|
}
|
|
2753
3005
|
const prune = await runCommandWithEnvFn(["git", "worktree", "prune"], opts.repoRoot, opts.env);
|
|
2754
3006
|
if (!prune.ok) {
|
|
@@ -3103,10 +3355,10 @@ function buildCliClearTargets(opts) {
|
|
|
3103
3355
|
return targets;
|
|
3104
3356
|
}
|
|
3105
3357
|
function removeCliClearTarget(target) {
|
|
3106
|
-
if (!
|
|
3358
|
+
if (!existsSync5(target.path))
|
|
3107
3359
|
return "missing";
|
|
3108
3360
|
try {
|
|
3109
|
-
|
|
3361
|
+
rmSync2(target.path, { recursive: true, force: true });
|
|
3110
3362
|
return "removed";
|
|
3111
3363
|
} catch (err) {
|
|
3112
3364
|
return {
|
|
@@ -3482,7 +3734,6 @@ async function autoStartRuntimeServices(opts) {
|
|
|
3482
3734
|
if (resolvedGitBinary) {
|
|
3483
3735
|
applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, resolvedGitBinary);
|
|
3484
3736
|
}
|
|
3485
|
-
const services = [];
|
|
3486
3737
|
const startupStartedAt = Date.now();
|
|
3487
3738
|
const startupPhases = [];
|
|
3488
3739
|
const recordStartupPhase = (name, startedAt, status) => {
|
|
@@ -3521,23 +3772,70 @@ async function autoStartRuntimeServices(opts) {
|
|
|
3521
3772
|
console.log(`[pushpals] service log (localbuddy)=${serviceLogPaths.localbuddy}`);
|
|
3522
3773
|
console.log(`[pushpals] service log (remotebuddy)=${serviceLogPaths.remotebuddy}`);
|
|
3523
3774
|
console.log(`[pushpals] service log (source_control_manager)=${serviceLogPaths.source_control_manager}`);
|
|
3775
|
+
const serviceManager = new ServiceManager({
|
|
3776
|
+
degradedAction: "Inspect the embedded service log or restart pushpals after fixing the runtime failure.",
|
|
3777
|
+
onEvent: (level, line) => {
|
|
3778
|
+
const cliLine = `[pushpals] ${line.replace(/^Managed /, "Embedded ").replace(/^Restarted managed /, "Restarted embedded ")}`;
|
|
3779
|
+
if (level === "error") {
|
|
3780
|
+
console.error(cliLine);
|
|
3781
|
+
} else if (level === "warn") {
|
|
3782
|
+
console.warn(cliLine);
|
|
3783
|
+
} else {
|
|
3784
|
+
console.log(cliLine);
|
|
3785
|
+
}
|
|
3786
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, cliLine);
|
|
3787
|
+
},
|
|
3788
|
+
onHealthChange: (health) => {
|
|
3789
|
+
for (const line of formatEmbeddedRuntimeHealthLines2(health)) {
|
|
3790
|
+
console.error(line);
|
|
3791
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, line);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
const launchService = (name, command) => {
|
|
3796
|
+
const logPath = serviceLogPaths[name];
|
|
3797
|
+
const header = `[pushpals] service=${name} command=${command.join(" ")} cwd=${opts.repoRoot}`;
|
|
3798
|
+
writeFileSync(logPath, `${header}
|
|
3799
|
+
`, "utf8");
|
|
3800
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, header);
|
|
3801
|
+
return serviceManager.startService({
|
|
3802
|
+
name,
|
|
3803
|
+
color: "",
|
|
3804
|
+
command,
|
|
3805
|
+
cwd: opts.repoRoot,
|
|
3806
|
+
env: runtimeEnv,
|
|
3807
|
+
logPath,
|
|
3808
|
+
onStdoutLine: (line) => {
|
|
3809
|
+
const serviceLine = `[stdout] ${line}`;
|
|
3810
|
+
appendFileSync(logPath, `${serviceLine}
|
|
3811
|
+
`, "utf8");
|
|
3812
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
|
|
3813
|
+
},
|
|
3814
|
+
onStderrLine: (line) => {
|
|
3815
|
+
const serviceLine = `[stderr] ${line}`;
|
|
3816
|
+
appendFileSync(logPath, `${serviceLine}
|
|
3817
|
+
`, "utf8");
|
|
3818
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
};
|
|
3524
3822
|
const serverHealthy = await probeServer(opts.serverUrl);
|
|
3525
3823
|
if (!serverHealthy) {
|
|
3526
3824
|
const serverPhaseStartedAt = Date.now();
|
|
3527
3825
|
console.log("[pushpals] Starting embedded server...");
|
|
3528
|
-
const serverService =
|
|
3529
|
-
|
|
3530
|
-
console.log(`[pushpals] server log: ${
|
|
3826
|
+
const serverService = launchService("server", [runtimeBinaries.server]);
|
|
3827
|
+
const serverLogPath = serverService.logPath ?? serviceLogPaths.server;
|
|
3828
|
+
console.log(`[pushpals] server log: ${serverLogPath}`);
|
|
3531
3829
|
const serverDeadline = Date.now() + DEFAULT_SERVER_BOOT_TIMEOUT_MS;
|
|
3532
3830
|
let serverIsReady = false;
|
|
3533
3831
|
while (Date.now() < serverDeadline) {
|
|
3534
3832
|
if (serverService.exited) {
|
|
3535
|
-
const tail = readLogTail(
|
|
3833
|
+
const tail = readLogTail(serverLogPath);
|
|
3536
3834
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}).`);
|
|
3537
3835
|
recordStartupPhase("server", serverPhaseStartedAt, "exited");
|
|
3538
3836
|
emitStartupTimingSummary("failed", "server exited during bootstrap");
|
|
3539
|
-
stopRuntimeServices(
|
|
3540
|
-
throw new Error(`Embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}). ` + `See ${
|
|
3837
|
+
stopRuntimeServices(serviceManager.getServices());
|
|
3838
|
+
throw new Error(`Embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}). ` + `See ${serverLogPath}${tail ? `
|
|
3541
3839
|
--- server log tail ---
|
|
3542
3840
|
${tail}` : ""}`);
|
|
3543
3841
|
}
|
|
@@ -3548,12 +3846,12 @@ ${tail}` : ""}`);
|
|
|
3548
3846
|
await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
3549
3847
|
}
|
|
3550
3848
|
if (!serverIsReady) {
|
|
3551
|
-
const tail = readLogTail(
|
|
3849
|
+
const tail = readLogTail(serverLogPath);
|
|
3552
3850
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms.`);
|
|
3553
3851
|
recordStartupPhase("server", serverPhaseStartedAt, "timeout");
|
|
3554
3852
|
emitStartupTimingSummary("failed", "server health timeout");
|
|
3555
|
-
stopRuntimeServices(
|
|
3556
|
-
throw new Error(`Embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms. ` + `See ${
|
|
3853
|
+
stopRuntimeServices(serviceManager.getServices());
|
|
3854
|
+
throw new Error(`Embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms. ` + `See ${serverLogPath}${tail ? `
|
|
3557
3855
|
--- server log tail ---
|
|
3558
3856
|
${tail}` : ""}`);
|
|
3559
3857
|
}
|
|
@@ -3567,9 +3865,8 @@ ${tail}` : ""}`);
|
|
|
3567
3865
|
if (localBuddyEnabled) {
|
|
3568
3866
|
const localBuddyPhaseStartedAt = Date.now();
|
|
3569
3867
|
console.log("[pushpals] Starting embedded LocalBuddy...");
|
|
3570
|
-
const localbuddyService =
|
|
3571
|
-
|
|
3572
|
-
console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
|
|
3868
|
+
const localbuddyService = launchService("localbuddy", [runtimeBinaries.localbuddy]);
|
|
3869
|
+
console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath ?? serviceLogPaths.localbuddy}`);
|
|
3573
3870
|
recordStartupPhase("localbuddy", localBuddyPhaseStartedAt, "started");
|
|
3574
3871
|
} else {
|
|
3575
3872
|
recordStartupPhase("localbuddy", Date.now(), "skipped");
|
|
@@ -3578,13 +3875,13 @@ ${tail}` : ""}`);
|
|
|
3578
3875
|
}
|
|
3579
3876
|
const remoteBuddyPhaseStartedAt = Date.now();
|
|
3580
3877
|
console.log("[pushpals] Starting embedded RemoteBuddy...");
|
|
3581
|
-
const remotebuddyService =
|
|
3582
|
-
|
|
3583
|
-
console.log(`[pushpals] remotebuddy log: ${
|
|
3878
|
+
const remotebuddyService = launchService("remotebuddy", [runtimeBinaries.remotebuddy]);
|
|
3879
|
+
const remotebuddyLogPath = remotebuddyService.logPath ?? serviceLogPaths.remotebuddy;
|
|
3880
|
+
console.log(`[pushpals] remotebuddy log: ${remotebuddyLogPath}`);
|
|
3584
3881
|
recordStartupPhase("remotebuddy", remoteBuddyPhaseStartedAt, "started");
|
|
3585
3882
|
let lastReportedRemoteBuddyAutonomyState = "unknown";
|
|
3586
3883
|
const reportRemoteBuddyAutonomousEngineState = () => {
|
|
3587
|
-
const autonomyState = readRemoteBuddyAutonomousEngineState(
|
|
3884
|
+
const autonomyState = readRemoteBuddyAutonomousEngineState(remotebuddyLogPath);
|
|
3588
3885
|
if (autonomyState === "unknown" || autonomyState === lastReportedRemoteBuddyAutonomyState) {
|
|
3589
3886
|
return;
|
|
3590
3887
|
}
|
|
@@ -3635,9 +3932,11 @@ ${tail}` : ""}`);
|
|
|
3635
3932
|
} else if (scmRemoteStatus.status === "ok") {
|
|
3636
3933
|
console.log(`[pushpals] Embedded SourceControlManager git=${scmGitProbe.detail}`);
|
|
3637
3934
|
console.log("[pushpals] Starting embedded SourceControlManager...");
|
|
3638
|
-
const sourceControlManagerService =
|
|
3639
|
-
|
|
3640
|
-
|
|
3935
|
+
const sourceControlManagerService = launchService("source_control_manager", [
|
|
3936
|
+
runtimeBinaries.sourceControlManager,
|
|
3937
|
+
"--skip-clean-check"
|
|
3938
|
+
]);
|
|
3939
|
+
console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath ?? serviceLogPaths.source_control_manager}`);
|
|
3641
3940
|
recordStartupPhase("source_control_manager", scmPhaseStartedAt, "started");
|
|
3642
3941
|
} else {
|
|
3643
3942
|
console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
|
|
@@ -3651,30 +3950,36 @@ ${tail}` : ""}`);
|
|
|
3651
3950
|
}
|
|
3652
3951
|
const deadline = Date.now() + DEFAULT_RUNTIME_BOOT_TIMEOUT_MS;
|
|
3653
3952
|
const readinessPhaseStartedAt = Date.now();
|
|
3953
|
+
const optionalServiceExitWarned = new Set;
|
|
3654
3954
|
while (Date.now() < deadline) {
|
|
3655
3955
|
reportRemoteBuddyAutonomousEngineState();
|
|
3656
|
-
for (
|
|
3657
|
-
const service = services[i];
|
|
3956
|
+
for (const service of serviceManager.getServices()) {
|
|
3658
3957
|
if (service.exited) {
|
|
3659
3958
|
if (isOptionalEmbeddedService(service.name)) {
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3959
|
+
const runtimeServiceName2 = service.name;
|
|
3960
|
+
const serviceLogPath2 = service.logPath ?? serviceLogPaths[runtimeServiceName2];
|
|
3961
|
+
if (!optionalServiceExitWarned.has(runtimeServiceName2)) {
|
|
3962
|
+
console.warn(`[pushpals] Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); startup will continue and host supervisor will attempt recovery.`);
|
|
3963
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); startup will continue and host supervisor will attempt recovery.`);
|
|
3964
|
+
const tail2 = readLogTail(serviceLogPath2);
|
|
3965
|
+
if (tail2) {
|
|
3966
|
+
console.warn(`[pushpals] ${service.name} log tail:
|
|
3665
3967
|
${tail2}`);
|
|
3666
|
-
|
|
3968
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] ${service.name} log tail:
|
|
3667
3969
|
${tail2}`);
|
|
3970
|
+
}
|
|
3971
|
+
optionalServiceExitWarned.add(runtimeServiceName2);
|
|
3668
3972
|
}
|
|
3669
|
-
services.splice(i, 1);
|
|
3670
3973
|
continue;
|
|
3671
3974
|
}
|
|
3672
|
-
const
|
|
3975
|
+
const runtimeServiceName = service.name;
|
|
3976
|
+
const serviceLogPath = service.logPath ?? serviceLogPaths[runtimeServiceName];
|
|
3977
|
+
const tail = readLogTail(serviceLogPath);
|
|
3673
3978
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}).`);
|
|
3674
3979
|
recordStartupPhase("readiness", readinessPhaseStartedAt, "failed");
|
|
3675
3980
|
emitStartupTimingSummary("failed", `${service.name} exited during startup`);
|
|
3676
|
-
stopRuntimeServices(
|
|
3677
|
-
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}). ` + `See ${
|
|
3981
|
+
stopRuntimeServices(serviceManager.getServices());
|
|
3982
|
+
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}). ` + `See ${serviceLogPath}${tail ? `
|
|
3678
3983
|
--- ${service.name} log tail ---
|
|
3679
3984
|
${tail}` : ""}`);
|
|
3680
3985
|
}
|
|
@@ -3686,29 +3991,34 @@ ${tail}` : ""}`);
|
|
|
3686
3991
|
const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
|
|
3687
3992
|
while (Date.now() < stabilityDeadline) {
|
|
3688
3993
|
reportRemoteBuddyAutonomousEngineState();
|
|
3689
|
-
for (
|
|
3690
|
-
const service = services[i];
|
|
3994
|
+
for (const service of serviceManager.getServices()) {
|
|
3691
3995
|
if (!service.exited)
|
|
3692
3996
|
continue;
|
|
3693
3997
|
if (isOptionalEmbeddedService(service.name)) {
|
|
3694
|
-
const
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
console.warn(`[pushpals] ${service.name}
|
|
3998
|
+
const runtimeServiceName2 = service.name;
|
|
3999
|
+
const serviceLogPath2 = service.logPath ?? serviceLogPaths[runtimeServiceName2];
|
|
4000
|
+
if (!optionalServiceExitWarned.has(runtimeServiceName2)) {
|
|
4001
|
+
const tail2 = readLogTail(serviceLogPath2);
|
|
4002
|
+
console.warn(`[pushpals] Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}); startup will continue and host supervisor will attempt recovery.`);
|
|
4003
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}); startup will continue and host supervisor will attempt recovery.`);
|
|
4004
|
+
if (tail2) {
|
|
4005
|
+
console.warn(`[pushpals] ${service.name} log tail:
|
|
3699
4006
|
${tail2}`);
|
|
3700
|
-
|
|
4007
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] ${service.name} log tail:
|
|
3701
4008
|
${tail2}`);
|
|
4009
|
+
}
|
|
4010
|
+
optionalServiceExitWarned.add(runtimeServiceName2);
|
|
3702
4011
|
}
|
|
3703
|
-
services.splice(i, 1);
|
|
3704
4012
|
continue;
|
|
3705
4013
|
}
|
|
3706
|
-
const
|
|
4014
|
+
const runtimeServiceName = service.name;
|
|
4015
|
+
const serviceLogPath = service.logPath ?? serviceLogPaths[runtimeServiceName];
|
|
4016
|
+
const tail = readLogTail(serviceLogPath);
|
|
3707
4017
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}).`);
|
|
3708
4018
|
recordStartupPhase("readiness", readinessPhaseStartedAt, "failed");
|
|
3709
4019
|
emitStartupTimingSummary("failed", `${service.name} exited immediately after bootstrap`);
|
|
3710
|
-
stopRuntimeServices(
|
|
3711
|
-
throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${
|
|
4020
|
+
stopRuntimeServices(serviceManager.getServices());
|
|
4021
|
+
throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${serviceLogPath}${tail ? `
|
|
3712
4022
|
--- ${service.name} log tail ---
|
|
3713
4023
|
${tail}` : ""}`);
|
|
3714
4024
|
}
|
|
@@ -3719,13 +4029,13 @@ ${tail}` : ""}`);
|
|
|
3719
4029
|
emitStartupTimingSummary("ready");
|
|
3720
4030
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded runtime is ready.");
|
|
3721
4031
|
return {
|
|
3722
|
-
|
|
4032
|
+
serviceManager,
|
|
3723
4033
|
pushpalsLogPath: runtimeServicesLogPath
|
|
3724
4034
|
};
|
|
3725
4035
|
}
|
|
3726
4036
|
await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
3727
4037
|
}
|
|
3728
|
-
stopRuntimeServices(
|
|
4038
|
+
stopRuntimeServices(serviceManager.getServices());
|
|
3729
4039
|
const remoteBuddyHealth = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
|
|
3730
4040
|
if (!localBuddyEnabled && !remoteBuddyHealth.ok) {
|
|
3731
4041
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail}).`);
|
|
@@ -3745,7 +4055,7 @@ ${tail}` : ""}`);
|
|
|
3745
4055
|
throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
|
|
3746
4056
|
}
|
|
3747
4057
|
function readCliState(pathValue) {
|
|
3748
|
-
if (!
|
|
4058
|
+
if (!existsSync5(pathValue))
|
|
3749
4059
|
return {};
|
|
3750
4060
|
try {
|
|
3751
4061
|
const raw = readFileSync4(pathValue, "utf8");
|
|
@@ -3848,11 +4158,11 @@ function resolveMonitoringHubAssetPath(assetRoot, pathname) {
|
|
|
3848
4158
|
const candidatePath = resolve4(root, relativePath);
|
|
3849
4159
|
if (candidatePath !== root && !candidatePath.startsWith(rootPrefix))
|
|
3850
4160
|
return null;
|
|
3851
|
-
if (
|
|
4161
|
+
if (existsSync5(candidatePath))
|
|
3852
4162
|
return candidatePath;
|
|
3853
4163
|
if (!extname(relativePath)) {
|
|
3854
4164
|
const nestedIndexPath = resolve4(root, relativePath, "index.html");
|
|
3855
|
-
if ((nestedIndexPath === root || nestedIndexPath.startsWith(rootPrefix)) &&
|
|
4165
|
+
if ((nestedIndexPath === root || nestedIndexPath.startsWith(rootPrefix)) && existsSync5(nestedIndexPath)) {
|
|
3856
4166
|
return nestedIndexPath;
|
|
3857
4167
|
}
|
|
3858
4168
|
return join2(root, "index.html");
|
|
@@ -3861,7 +4171,7 @@ function resolveMonitoringHubAssetPath(assetRoot, pathname) {
|
|
|
3861
4171
|
}
|
|
3862
4172
|
async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
|
|
3863
4173
|
const assetPath = resolveMonitoringHubAssetPath(assetRoot, pathname);
|
|
3864
|
-
if (!assetPath || !
|
|
4174
|
+
if (!assetPath || !existsSync5(assetPath))
|
|
3865
4175
|
return null;
|
|
3866
4176
|
if (assetPath.endsWith("index.html")) {
|
|
3867
4177
|
const html = injectMonitoringHubBootstrap(readFileSync4(assetPath, "utf8"), bootstrap);
|
|
@@ -4405,7 +4715,7 @@ async function main() {
|
|
|
4405
4715
|
platform: `${process.platform}/${process.arch}`,
|
|
4406
4716
|
repoRoot
|
|
4407
4717
|
};
|
|
4408
|
-
let
|
|
4718
|
+
let autoStartedServiceManager = null;
|
|
4409
4719
|
let pushpalsLogPath;
|
|
4410
4720
|
let resolvedRuntimeTagForAutoStart = preparedRuntime.runtimeTag || parsed.runtimeTag || "";
|
|
4411
4721
|
const cleanupWorkerpalWarmContainersIfNeeded = async (phase) => {
|
|
@@ -4464,17 +4774,24 @@ async function main() {
|
|
|
4464
4774
|
}
|
|
4465
4775
|
return readiness;
|
|
4466
4776
|
};
|
|
4777
|
+
const reportEmbeddedRuntimeHealth = () => {
|
|
4778
|
+
const health = autoStartedServiceManager?.getHealth() ?? null;
|
|
4779
|
+
for (const line of formatEmbeddedRuntimeHealthLines2(health)) {
|
|
4780
|
+
console.log(line);
|
|
4781
|
+
}
|
|
4782
|
+
return health;
|
|
4783
|
+
};
|
|
4467
4784
|
const stopAutoStartedServices = () => {
|
|
4468
|
-
if (
|
|
4785
|
+
if (!autoStartedServiceManager)
|
|
4469
4786
|
return;
|
|
4470
|
-
|
|
4471
|
-
|
|
4787
|
+
autoStartedServiceManager.stop();
|
|
4788
|
+
autoStartedServiceManager = null;
|
|
4472
4789
|
};
|
|
4473
4790
|
const stopAutoStartedServicesGracefully = async (reason) => {
|
|
4474
|
-
if (
|
|
4791
|
+
if (!autoStartedServiceManager)
|
|
4475
4792
|
return;
|
|
4476
|
-
const
|
|
4477
|
-
|
|
4793
|
+
const serviceManager = autoStartedServiceManager;
|
|
4794
|
+
autoStartedServiceManager = null;
|
|
4478
4795
|
const shutdown = await requestLocalRuntimeShutdown(serverUrl, repoRoot, reason);
|
|
4479
4796
|
if (shutdown.attempted && shutdown.accepted) {
|
|
4480
4797
|
console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
|
|
@@ -4484,6 +4801,8 @@ async function main() {
|
|
|
4484
4801
|
} else if (shutdown.detail) {
|
|
4485
4802
|
console.warn(`[pushpals] ${shutdown.detail}`);
|
|
4486
4803
|
}
|
|
4804
|
+
serviceManager.stop();
|
|
4805
|
+
const services = serviceManager.getServices();
|
|
4487
4806
|
await stopRuntimeServicesGracefully(services);
|
|
4488
4807
|
await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
|
|
4489
4808
|
await cleanupPushPalsGitWorktreesIfNeeded("cli shutdown");
|
|
@@ -4531,7 +4850,7 @@ async function main() {
|
|
|
4531
4850
|
startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled)),
|
|
4532
4851
|
baseEnv: workerpalDockerPrecheck.env
|
|
4533
4852
|
});
|
|
4534
|
-
|
|
4853
|
+
autoStartedServiceManager = startedRuntime.serviceManager;
|
|
4535
4854
|
pushpalsLogPath = startedRuntime.pushpalsLogPath;
|
|
4536
4855
|
serverHealthy = await probeServer(serverUrl);
|
|
4537
4856
|
} catch (err) {
|
|
@@ -4655,6 +4974,7 @@ async function main() {
|
|
|
4655
4974
|
console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
|
|
4656
4975
|
console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
|
|
4657
4976
|
reportWorkerExecutionReadinessFromSnapshot(startupWorkerExecutionReadiness);
|
|
4977
|
+
reportEmbeddedRuntimeHealth();
|
|
4658
4978
|
if (parsed.runtimeOnly) {
|
|
4659
4979
|
console.log("[pushpals] runtimeOnly=true");
|
|
4660
4980
|
} else {
|
|
@@ -4689,7 +5009,7 @@ ${line}
|
|
|
4689
5009
|
try {
|
|
4690
5010
|
monitoringHub?.stop();
|
|
4691
5011
|
} catch {}
|
|
4692
|
-
if (
|
|
5012
|
+
if (autoStartedServiceManager) {
|
|
4693
5013
|
console.log("[pushpals] Stopping embedded runtime services...");
|
|
4694
5014
|
}
|
|
4695
5015
|
await stopAutoStartedServicesGracefully("pushpals CLI exit");
|
|
@@ -4767,6 +5087,7 @@ ${line}
|
|
|
4767
5087
|
console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
|
|
4768
5088
|
console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
|
|
4769
5089
|
await reportWorkerExecutionReadiness();
|
|
5090
|
+
reportEmbeddedRuntimeHealth();
|
|
4770
5091
|
rl.prompt();
|
|
4771
5092
|
continue;
|
|
4772
5093
|
}
|
|
@@ -4806,6 +5127,7 @@ export {
|
|
|
4806
5127
|
waitForWorkerpalCapacity,
|
|
4807
5128
|
startEmbeddedMonitoringHub,
|
|
4808
5129
|
shouldRunEmbeddedRuntimeStartupPrechecks,
|
|
5130
|
+
shouldRestartEmbeddedService,
|
|
4809
5131
|
resolveWorkerExecutionReadiness,
|
|
4810
5132
|
resolveWindowsWhereExecutableCandidatesForEnv,
|
|
4811
5133
|
resolveWindowsShellExecutableCandidatesForEnv,
|
|
@@ -4832,6 +5154,7 @@ export {
|
|
|
4832
5154
|
formatTimestampedCliLine,
|
|
4833
5155
|
formatSessionEventLine,
|
|
4834
5156
|
formatRuntimeStartupTimingSummary,
|
|
5157
|
+
formatEmbeddedRuntimeHealthLines2 as formatEmbeddedRuntimeHealthLines,
|
|
4835
5158
|
extractRemoteBuddySessionConsumerHealth,
|
|
4836
5159
|
extractRemoteBuddyAutonomousEngineState,
|
|
4837
5160
|
ensureWorkerpalDockerImageReady,
|
|
@@ -4840,6 +5163,7 @@ export {
|
|
|
4840
5163
|
describeWorkerExecutionReadiness,
|
|
4841
5164
|
createSessionEventReplayFilter,
|
|
4842
5165
|
copyTrackedRepoPath,
|
|
5166
|
+
computeEmbeddedServiceRestartBackoffMs,
|
|
4843
5167
|
cleanupLingeringWorkerpalWarmContainers,
|
|
4844
5168
|
cleanupLingeringPushPalsGitWorktrees,
|
|
4845
5169
|
bundledMonitoringHubNeedsRefresh,
|
package/package.json
CHANGED
|
@@ -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:start:e2e": "bun test ./tests/integration/start.e2e.ts",
|
|
48
49
|
"test:root": "bun test tests",
|
|
49
50
|
"test:protocol": "bun run tests/protocol.integration.ts",
|
|
50
51
|
"test:integration": "python -u tests/integration/integration_controller.py --mode integration",
|