@cydm/happy-elves 0.1.0-beta.271 → 0.1.0-beta.272

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.
@@ -255,7 +255,10 @@ async function waitForDaemonLifecycle(client, machineId, requestId, action, opti
255
255
  catch (error) {
256
256
  lastError = error;
257
257
  const code = errorCode(error);
258
- if (code !== "MACHINE_OFFLINE" && code !== "COMMAND_TIMEOUT" && code !== "RELAY_CONNECT_TIMEOUT")
258
+ if (code !== "MACHINE_OFFLINE" &&
259
+ code !== "COMMAND_TIMEOUT" &&
260
+ code !== "RELAY_CONNECT_TIMEOUT" &&
261
+ code !== "RELAY_WEBSOCKET_ERROR")
259
262
  throw error;
260
263
  }
261
264
  await sleep(1000);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.271",
3
+ "version": "0.1.0-beta.272",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -238,6 +238,8 @@ import fs from "node:fs/promises";
238
238
  import path from "node:path";
239
239
 
240
240
  const input = ${inputJson};
241
+ let daemonStopAttempted = false;
242
+ let daemonStopped = false;
241
243
 
242
244
  main().catch(async (error) => {
243
245
  await fail(error);
@@ -263,6 +265,11 @@ async function main() {
263
265
  async function fail(error) {
264
266
  const message = error instanceof Error ? error.stack || error.message : String(error);
265
267
  await log("runner failed: " + message).catch(() => {});
268
+ if (daemonStopAttempted) {
269
+ await restartDaemonAfterFailure().catch(async (restartError) => {
270
+ await log("failure recovery restart failed: " + String(restartError instanceof Error ? restartError.message : restartError)).catch(() => {});
271
+ });
272
+ }
266
273
  await updateState({ status: "failed", finishedAt: new Date().toISOString(), error: String(error instanceof Error ? error.message : error) }).catch(() => {});
267
274
  }
268
275
 
@@ -280,11 +287,13 @@ async function log(line) {
280
287
  }
281
288
 
282
289
  async function stopDaemon() {
290
+ daemonStopAttempted = true;
283
291
  await updateState({ status: "stopping" });
284
292
  const pid = await readPid();
285
293
  if (!pid || pid === process.pid) {
286
294
  await fs.rm(input.pidPath, { force: true }).catch(() => {});
287
295
  await log("no managed daemon pid to stop");
296
+ daemonStopped = true;
288
297
  return;
289
298
  }
290
299
  await log("stopping daemon pid " + pid);
@@ -298,6 +307,7 @@ async function stopDaemon() {
298
307
  if (!isPidAlive(pid)) {
299
308
  await fs.rm(input.pidPath, { force: true }).catch(() => {});
300
309
  await log("daemon stopped");
310
+ daemonStopped = true;
301
311
  return;
302
312
  }
303
313
  await delay(200);
@@ -333,6 +343,22 @@ async function startDaemon() {
333
343
  await spawnDetached(commandName("happy-elves"), ["daemon", "start", "--json"]);
334
344
  }
335
345
 
346
+ async function restartDaemonAfterFailure() {
347
+ const currentlyRunning = await captureStatus().catch(() => undefined);
348
+ if (currentlyRunning?.ok === true && (currentlyRunning.data?.local?.running === true || currentlyRunning.data?.running === true)) {
349
+ await log("failure recovery skipped: daemon already running");
350
+ return;
351
+ }
352
+ if (!daemonStopped) {
353
+ await log("failure recovery skipped: daemon stop did not complete");
354
+ return;
355
+ }
356
+ await log("failure recovery attempting daemon restart");
357
+ await startDaemon();
358
+ await waitForDaemonRunning();
359
+ await log("failure recovery daemon running");
360
+ }
361
+
336
362
  async function waitForDaemonRunning() {
337
363
  const deadline = Date.now() + 45_000;
338
364
  while (Date.now() < deadline) {
@@ -369,7 +395,8 @@ function statusCommand() {
369
395
  }
370
396
 
371
397
  async function spawnDetached(command, args) {
372
- const child = spawn(command, args, {
398
+ const spec = spawnSpec(command, args);
399
+ const child = spawn(spec.command, spec.args, {
373
400
  cwd: input.configDir,
374
401
  detached: true,
375
402
  env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
@@ -377,13 +404,14 @@ async function spawnDetached(command, args) {
377
404
  windowsHide: true,
378
405
  });
379
406
  child.unref();
380
- await log("spawned " + command + " pid " + (child.pid || "unknown"));
407
+ await log("spawned " + spec.command + " " + spec.args.join(" ") + " pid " + (child.pid || "unknown"));
381
408
  }
382
409
 
383
410
  async function run(command, args, options = {}) {
384
- await log("run " + command + " " + args.join(" "));
411
+ const spec = spawnSpec(command, args);
412
+ await log("run " + spec.command + " " + spec.args.join(" "));
385
413
  return await new Promise((resolve, reject) => {
386
- const child = spawn(command, args, {
414
+ const child = spawn(spec.command, spec.args, {
387
415
  cwd: input.configDir,
388
416
  env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
389
417
  windowsHide: true,
@@ -450,7 +478,48 @@ function isPidAlive(pid) {
450
478
  }
451
479
 
452
480
  function commandName(name) {
453
- return process.platform === "win32" ? name + ".cmd" : name;
481
+ return runnerPlatform() === "win32" ? name + ".cmd" : name;
482
+ }
483
+
484
+ function runnerPlatform() {
485
+ return process.env.HAPPY_ELVES_TEST_RUNNER_PLATFORM || process.platform;
486
+ }
487
+
488
+ function spawnSpec(command, args) {
489
+ if (runnerPlatform() !== "win32" || !/\\.cmd$/i.test(command)) {
490
+ return { command, args };
491
+ }
492
+ return {
493
+ command: process.env.ComSpec || process.env.COMSPEC || "cmd.exe",
494
+ args: ["/d", "/s", "/c", windowsCommandLine([command, ...args])],
495
+ };
496
+ }
497
+
498
+ function windowsCommandLine(parts) {
499
+ return parts.map(quoteWindowsArg).join(" ");
500
+ }
501
+
502
+ function quoteWindowsArg(value) {
503
+ const text = String(value);
504
+ if (text.length === 0) return '""';
505
+ if (!/[\\s"]/u.test(text)) return text;
506
+ let result = '"';
507
+ let backslashes = 0;
508
+ for (const char of text) {
509
+ if (char === "\\\\") {
510
+ backslashes += 1;
511
+ continue;
512
+ }
513
+ if (char === '"') {
514
+ result += "\\\\".repeat(backslashes * 2 + 1) + '"';
515
+ backslashes = 0;
516
+ continue;
517
+ }
518
+ result += "\\\\".repeat(backslashes) + char;
519
+ backslashes = 0;
520
+ }
521
+ result += "\\\\".repeat(backslashes * 2) + '"';
522
+ return result;
454
523
  }
455
524
 
456
525
  function compact(text) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.271",
3
+ "version": "0.1.0-beta.272",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.271",
3
+ "version": "0.1.0-beta.272",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {