@danypops/pi-packed 0.27.11 → 0.27.12

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.27.11",
3
+ "version": "0.27.12",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@
6
6
  * kind of collision at actual startup, one package at a time; doctor
7
7
  * answers the same question proactively, before pi ever runs.
8
8
  */
9
- import { existsSync } from "node:fs";
9
+ import { existsSync, statSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { createNodeServiceInstallDeps } from "@danypops/vehicle-server/service";
12
12
  import { listPackageResources, type PackageResources, resolveInstalledDir } from "../packages/resources.ts";
@@ -134,6 +134,13 @@ export async function runDoctor(piHome: string, projectRoot?: string, options: S
134
134
  const serviceReport = checkServiceUnitPaths(piHome, projectRoot, {
135
135
  ...createNodeServiceInstallDeps(),
136
136
  fileExists: existsSync,
137
+ getMtimeMs: (path) => {
138
+ try {
139
+ return statSync(path).mtimeMs;
140
+ } catch {
141
+ return undefined;
142
+ }
143
+ },
137
144
  });
138
145
 
139
146
  return {
@@ -1,9 +1,10 @@
1
+ import { join } from "node:path";
1
2
  import type { ServiceInstallDeps } from "@danypops/vehicle-server/service";
2
3
  import { readInstalledPackagesAcrossScopes } from "../packages/installed.ts";
3
4
  import { resolveDaemonServiceSpec } from "../daemon/daemon-service.ts";
4
5
 
5
6
  export interface ServiceUnitDiagnostic {
6
- code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE";
7
+ code: "SERVICE_EXEC_PATH_MISSING" | "SERVICE_NOT_RUNNING" | "SERVICE_STATUS_UNAVAILABLE" | "SERVICE_STALE_CODE";
7
8
  severity: "error" | "warning";
8
9
  package: string;
9
10
  unitName: string;
@@ -18,10 +19,23 @@ export interface ServiceDoctorReport {
18
19
 
19
20
  export interface ServiceDoctorDeps extends ServiceInstallDeps {
20
21
  fileExists(path: string): boolean;
22
+ /** Undefined (never throws) when the path doesn't exist or can't be stat'd. */
23
+ getMtimeMs(path: string): number | undefined;
21
24
  }
22
25
 
23
26
  interface ArmadaStatus {
24
- vehicles?: Array<{ name?: string; executable?: string; nativeStatus?: string; ready?: boolean }>;
27
+ vehicles?: Array<{ name?: string; executable?: string; nativeStatus?: string; ready?: boolean; nativePid?: number }>;
28
+ }
29
+
30
+ /** A boot/registration race can leave a process's own start time a few seconds behind its package.json's write; only a gap past this is treated as a genuinely stale running daemon. */
31
+ const STALE_CODE_GRACE_MS = 15_000;
32
+
33
+ /** Undefined (never throws) when `pid` isn't a running process this host can inspect, or `ps` isn't available. */
34
+ function processStartTimeMs(pid: number, deps: ServiceDoctorDeps): number | undefined {
35
+ const result = deps.runCommand("ps", ["-o", "lstart=", "-p", String(pid)]);
36
+ if (!result.ok) return undefined;
37
+ const parsed = Date.parse(result.output.trim());
38
+ return Number.isNaN(parsed) ? undefined : parsed;
25
39
  }
26
40
 
27
41
  export function checkServiceUnitPaths(
@@ -96,6 +110,20 @@ export function checkServiceUnitPaths(
96
110
  unitName: managedPackage.spec.name,
97
111
  message: `${managedPackage.spec.name}'s Armada service is ${vehicle.nativeStatus}; start it through Armada before connecting`,
98
112
  });
113
+ continue;
114
+ }
115
+ if (vehicle.nativePid !== undefined) {
116
+ const packageJsonMtime = deps.getMtimeMs(join(piHome, "npm", "node_modules", managedPackage.packageName, "package.json"));
117
+ const startedAt = processStartTimeMs(vehicle.nativePid, deps);
118
+ if (packageJsonMtime !== undefined && startedAt !== undefined && startedAt < packageJsonMtime - STALE_CODE_GRACE_MS) {
119
+ diagnostics.push({
120
+ code: "SERVICE_STALE_CODE",
121
+ severity: "warning",
122
+ package: managedPackage.packageName,
123
+ unitName: managedPackage.spec.name,
124
+ message: `${managedPackage.spec.name}'s process (pid ${vehicle.nativePid}) started before its own package.json was last written -- it is very likely still running old in-memory code; restart it through Armada`,
125
+ });
126
+ }
99
127
  }
100
128
  }
101
129
  return { ok: diagnostics.length === 0, diagnostics, checked };
@@ -37,6 +37,7 @@ function fakeDeps(status: unknown, fileExists = true): ServiceDoctorDeps {
37
37
  return {
38
38
  armadaCliPath: "/armada/cli.js",
39
39
  fileExists: () => fileExists,
40
+ getMtimeMs: () => undefined,
40
41
  runCommand: () => ({ ok: true, output: JSON.stringify(status) }),
41
42
  };
42
43
  }
@@ -125,6 +126,59 @@ describe("checkServiceUnitPaths", () => {
125
126
  });
126
127
  });
127
128
 
129
+ it("reports a running Vehicle whose process predates its own package.json as stale code, not an error", () => {
130
+ const home = piHome(["npm:fakedaemon"]);
131
+ const dir = installDaemonPackage(home, "fakedaemon");
132
+ const deps: ServiceDoctorDeps = {
133
+ armadaCliPath: "/armada/cli.js",
134
+ fileExists: () => true,
135
+ getMtimeMs: () => Date.now(),
136
+ runCommand: (command) =>
137
+ command === "ps"
138
+ ? { ok: true, output: new Date(Date.now() - 60_000).toString() }
139
+ : {
140
+ ok: true,
141
+ output: JSON.stringify({
142
+ vehicles: [{ name: "fakedaemon", executable: join(dir, "cli.ts"), nativeStatus: "running", nativePid: 4242 }],
143
+ }),
144
+ },
145
+ };
146
+ const report = checkServiceUnitPaths(home, undefined, deps);
147
+ expect(report).toEqual({
148
+ ok: false,
149
+ checked: 1,
150
+ diagnostics: [
151
+ {
152
+ code: "SERVICE_STALE_CODE",
153
+ severity: "warning",
154
+ package: "fakedaemon",
155
+ unitName: "fakedaemon",
156
+ message: expect.stringContaining("pid 4242"),
157
+ },
158
+ ],
159
+ });
160
+ });
161
+
162
+ it("never flags a running Vehicle whose process started after its own package.json was last written", () => {
163
+ const home = piHome(["npm:fakedaemon"]);
164
+ const dir = installDaemonPackage(home, "fakedaemon");
165
+ const deps: ServiceDoctorDeps = {
166
+ armadaCliPath: "/armada/cli.js",
167
+ fileExists: () => true,
168
+ getMtimeMs: () => Date.now() - 60_000,
169
+ runCommand: (command) =>
170
+ command === "ps"
171
+ ? { ok: true, output: new Date().toString() }
172
+ : {
173
+ ok: true,
174
+ output: JSON.stringify({
175
+ vehicles: [{ name: "fakedaemon", executable: join(dir, "cli.ts"), nativeStatus: "running", nativePid: 4242 }],
176
+ }),
177
+ },
178
+ };
179
+ expect(checkServiceUnitPaths(home, undefined, deps)).toEqual({ ok: true, diagnostics: [], checked: 1 });
180
+ });
181
+
128
182
  it("reports Armada status failure without guessing from native descriptors", () => {
129
183
  const home = piHome(["npm:fakedaemon"]);
130
184
  installDaemonPackage(home, "fakedaemon");