@danypops/pi-packed 0.26.0 → 0.27.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "typecheck": "bunx tsc --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@danypops/armada": "^0.4.8",
30
+ "@danypops/armada": "^0.4.9",
31
31
  "@danypops/packed": "^0.7.0",
32
32
  "@danypops/pi-extension-harness": "^0.7.0",
33
33
  "@danypops/vehicle-client": "^0.8.1",
@@ -25,8 +25,10 @@
25
25
  * step isn't a real convention, so detection reads what's already true
26
26
  * on disk instead of asking for one more declaration.
27
27
  */
28
+ import { spawn } from "node:child_process";
28
29
  import { readdirSync, readFileSync } from "node:fs";
29
30
  import { join } from "node:path";
31
+ import { fileURLToPath } from "node:url";
30
32
  import { createVehicleRegistrar, type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada";
31
33
  import { resolveDaemonPaths } from "@danypops/vehicle-server/paths";
32
34
  import {
@@ -439,6 +441,38 @@ const PACKED_PACKAGE_NAME = "@danypops/pi-packed";
439
441
  * daemonOptions() -- vehicleName: PACKED_VEHICLE_NAME). One canonical name, not two. */
440
442
  export const PACKED_VEHICLE_NAME = "pi-packed";
441
443
  const SELF_REGISTRATION_REASON = "Packed cannot replace its own Armada service from inside the running daemon";
444
+ const SELF_RESTART_SCHEDULED_REASON = "self-restart scheduled via a detached armada CLI process";
445
+ /** Long enough for the HTTP response carrying this reason back to the caller to flush before the
446
+ * detached child's `armada restart` stops this very process. */
447
+ const SELF_RESTART_DELAY_MS = 500;
448
+
449
+ /** Fire-and-forget: schedule Packed's own Vehicle to restart from OUTSIDE this process. Packed
450
+ * can't safely stop+start itself in-process (the `stop` step would kill the very process handling
451
+ * this request before it could `start` again) -- so this spawns a detached `armada restart
452
+ * pi-packed` a beat later, after the response reporting "scheduled" has had time to flush, and lets
453
+ * it outlive this process via detached+unref. Swallows a resolution failure silently: worst case is
454
+ * the pre-existing behavior (a human restarts the service by hand), never a crash. */
455
+ export type SelfRestartScheduler = (spec: ServiceSpec) => void;
456
+
457
+ export const scheduleSelfRestart: SelfRestartScheduler = (_spec) => {
458
+ let cliPath: string;
459
+ try {
460
+ cliPath = fileURLToPath(import.meta.resolve("@danypops/armada/cli"));
461
+ } catch {
462
+ return;
463
+ }
464
+ setTimeout(() => {
465
+ try {
466
+ const child = spawn(process.execPath, [cliPath, "restart", PACKED_VEHICLE_NAME, "--json"], {
467
+ detached: true,
468
+ stdio: "ignore",
469
+ });
470
+ child.unref();
471
+ } catch {
472
+ /* best-effort -- a human can still restart the service by hand */
473
+ }
474
+ }, SELF_RESTART_DELAY_MS).unref();
475
+ };
442
476
 
443
477
  /**
444
478
  * Sweeps every installed Packed package (global scope, plus a project's own
@@ -557,7 +591,10 @@ export interface DaemonServiceInstaller {
557
591
  * path/native controller are resolved once, not on every call.
558
592
  */
559
593
  export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
560
- constructor(private readonly registrar: VehicleRegistrar = createVehicleRegistrar()) {}
594
+ constructor(
595
+ private readonly registrar: VehicleRegistrar = createVehicleRegistrar(),
596
+ private readonly selfRestart: SelfRestartScheduler = scheduleSelfRestart,
597
+ ) {}
561
598
 
562
599
  async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
563
600
  return this.registrar.listRegistered();
@@ -600,7 +637,8 @@ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
600
637
  const resolved = resolveDaemonServiceSpec(piHome, source);
601
638
  if (!resolved.ok) return resolved;
602
639
  if (resolved.spec.name === PACKED_VEHICLE_NAME) {
603
- return { ok: true, restarted: false, reason: SELF_REGISTRATION_REASON, spec: resolved.spec };
640
+ this.selfRestart(resolved.spec);
641
+ return { ok: true, restarted: true, reason: SELF_RESTART_SCHEDULED_REASON, spec: resolved.spec };
604
642
  }
605
643
  if (!(await isVehicleServiceRegistered(resolved.spec.name, this.registrar))) {
606
644
  return { ok: true, restarted: false, reason: `no persistent service is registered for ${resolved.spec.name}`, spec: resolved.spec };
@@ -9,8 +9,11 @@ import {
9
9
  detectVehicleDaemonService,
10
10
  listManagedPackages,
11
11
  listUnconfiguredDaemonDependencies,
12
+ PACKED_VEHICLE_NAME,
12
13
  RealDaemonServiceInstaller,
13
14
  resolveDaemonServiceSpec,
15
+ scheduleSelfRestart,
16
+ type SelfRestartScheduler,
14
17
  } from "../src/daemon/daemon-service.ts";
15
18
 
16
19
  const roots: string[] = [];
@@ -470,6 +473,32 @@ describe("RealDaemonServiceInstaller -- Armada registration through the in-proce
470
473
  }
471
474
  });
472
475
 
476
+ it("restart() schedules an out-of-process self-restart for Packed's own Vehicle, never touching Armada in-process", async () => {
477
+ const piHome = fakePiHome();
478
+ writePackage(piHome, "@danypops/pi-packed", { binPath: "service/src/cli/cli.ts", args: ["serve"] });
479
+ const harness = await createArmadaTestHarness();
480
+ const scheduled: Array<{ name: string }> = [];
481
+ const selfRestart: SelfRestartScheduler = (spec) => {
482
+ scheduled.push({ name: spec.name });
483
+ };
484
+ try {
485
+ const installer = new RealDaemonServiceInstaller(harness.registrar, selfRestart);
486
+ const outcome = await installer.restart(piHome, "npm:@danypops/pi-packed");
487
+
488
+ expect(outcome).toMatchObject({ ok: true, restarted: true, spec: { name: "pi-packed" } });
489
+ expect(scheduled).toEqual([{ name: PACKED_VEHICLE_NAME }]);
490
+ // never asked Armada to stop/replace/start the very process running this test
491
+ expect(harness.events()).toEqual([]);
492
+ } finally {
493
+ await harness.dispose();
494
+ }
495
+ });
496
+
497
+ it("scheduleSelfRestart resolves armada's real CLI module -- proves the wiring is live, not just typechecked", () => {
498
+ expect(() => import.meta.resolve("@danypops/armada/cli")).not.toThrow();
499
+ expect(typeof scheduleSelfRestart).toBe("function");
500
+ });
501
+
473
502
  it("install() reports a non-daemon package without touching Armada", async () => {
474
503
  const piHome = fakePiHome();
475
504
  writePackage(piHome, "some-pkg", undefined);