@danypops/pi-packed 0.27.3 → 0.27.5
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.
|
|
3
|
+
"version": "0.27.5",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@danypops/vehicle-client": "^0.10.0",
|
|
34
34
|
"@danypops/vehicle-client-pi": "^0.40.1",
|
|
35
35
|
"@danypops/vehicle-core": "^0.15.0",
|
|
36
|
-
"@danypops/vehicle-server": "^0.24.
|
|
36
|
+
"@danypops/vehicle-server": "^0.24.2",
|
|
37
37
|
"jiti": "^2.7.0",
|
|
38
38
|
"malevich-tui-components": "^0.21.1",
|
|
39
39
|
"publint": "0.3.22",
|
|
@@ -455,7 +455,13 @@ export function listManagedPackages(piHome: string, projectRoot?: string): Insta
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
export interface ReconcileAllResult {
|
|
458
|
-
|
|
458
|
+
/** `versionChanged` is true only when this pass's newly-resolved version genuinely differs
|
|
459
|
+
* from what Armada had registered before this sweep started -- `installed: true` alone means
|
|
460
|
+
* only "the register call itself succeeded", which is true on every healthy pass whether or
|
|
461
|
+
* not anything actually changed underneath (register()/reconcile() is a safe no-op against
|
|
462
|
+
* unchanged desired state). A caller wanting to know whether this pass genuinely restarted
|
|
463
|
+
* something (vs. just re-confirmed a healthy, unchanged fleet) must check this, not `installed`. */
|
|
464
|
+
reconciled: Array<{ packageName: string; vehicleName: string; installed: boolean; versionChanged: boolean; reason?: string }>;
|
|
459
465
|
skipped: number;
|
|
460
466
|
failed: Array<{ packageName: string; reason: string }>;
|
|
461
467
|
/** Every Vehicle unregistered because this sweep no longer discovers it at all -- see pruneStaleVehicles. */
|
|
@@ -555,6 +561,13 @@ export async function reconcileAllDaemonServices(
|
|
|
555
561
|
const failed: ReconcileAllResult["failed"] = [];
|
|
556
562
|
const seen = new Set<string>();
|
|
557
563
|
let skipped = 0;
|
|
564
|
+
// Snapshot BEFORE this sweep's own install() calls mutate Armada's registration -- the only
|
|
565
|
+
// way to tell "this pass's register() call genuinely changed something" from "register()
|
|
566
|
+
// succeeded against an already-current registration", since install()'s own ServiceInstallResult
|
|
567
|
+
// reports only the latter.
|
|
568
|
+
const previousVersions = new Map<string, string>(
|
|
569
|
+
(await installer.listRegisteredVehicles()).map((vehicle) => [String(vehicle.name), vehicle.version]),
|
|
570
|
+
);
|
|
558
571
|
for (const pkg of packages) {
|
|
559
572
|
// Re-registering the daemon from inside its own process makes Armada stop
|
|
560
573
|
// that process before registration can start the replacement.
|
|
@@ -577,6 +590,7 @@ export async function reconcileAllDaemonServices(
|
|
|
577
590
|
packageName: pkg.name,
|
|
578
591
|
vehicleName: resolved.spec.name,
|
|
579
592
|
installed: resolved.result.installed,
|
|
593
|
+
versionChanged: previousVersions.get(resolved.spec.name) !== resolved.spec.version,
|
|
580
594
|
...(resolved.result.installed ? {} : { reason: resolved.result.reason }),
|
|
581
595
|
});
|
|
582
596
|
}
|
|
@@ -127,6 +127,20 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
|
|
|
127
127
|
intervalMs: envMs(ENV.RECONCILE_SECS, RECONCILE_INTERVAL_DEFAULT_MS),
|
|
128
128
|
run: async () => {
|
|
129
129
|
const result = await reconcileAllDaemonServices(piHome, undefined, daemonServiceInstaller);
|
|
130
|
+
// A package updated through a route pi-packed never sees at all (e.g. the generic,
|
|
131
|
+
// daemon-unaware `pi update --extension` / pkg_update path -- see
|
|
132
|
+
// pkg-update-never-restarts-vehicle-daemon) leaves this as the ONLY thing that ever
|
|
133
|
+
// notices and restarts the stale daemon. Logging only on failure made every silent
|
|
134
|
+
// success indistinguishable from "this never ran" -- there was no way to confirm a
|
|
135
|
+
// restart this task performed actually happened, short of checking the process's own
|
|
136
|
+
// PID by hand.
|
|
137
|
+
const changed = result.reconciled.filter((entry) => entry.installed && entry.versionChanged);
|
|
138
|
+
if (changed.length > 0 || result.pruned.length > 0) {
|
|
139
|
+
logger.info("vehicle-reconcile applied changes", {
|
|
140
|
+
reconciled: changed.map((entry) => entry.vehicleName),
|
|
141
|
+
pruned: result.pruned.map((entry) => entry.vehicleName),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
130
144
|
if (result.failed.length > 0) {
|
|
131
145
|
logger.warn("vehicle-reconcile completed with failures", {
|
|
132
146
|
reconciled: result.reconciled.length,
|
|
@@ -171,28 +185,23 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
|
|
|
171
185
|
};
|
|
172
186
|
}
|
|
173
187
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
// startDaemon() itself now runs every maintenance task once immediately at startup (in
|
|
189
|
+
// addition to its own interval) -- see @danypops/vehicle-server's own daemon.ts. This used to
|
|
190
|
+
// be a bespoke wrapper here (runInitialMaintenance(), called after startDaemon() returned, or
|
|
191
|
+
// from serveMain()'s onListen) working around a bare setInterval() never firing until its full
|
|
192
|
+
// interval first elapsed; kept here would now double-run every task on every startup. Every
|
|
193
|
+
// OTHER vehicle-server-based daemon (lector, jittor, papyrus, pipes, tickets,
|
|
194
|
+
// web-spider-daemon) never had this workaround at all and now gets the same fix for free from
|
|
195
|
+
// the shared implementation, instead of needing to independently reinvent it.
|
|
182
196
|
export async function startPackedDaemon(options: StartPackedDaemonOptions = {}): Promise<RunningDaemon> {
|
|
183
|
-
|
|
184
|
-
const running = await startDaemon(configured);
|
|
185
|
-
runInitialMaintenance(configured.maintenanceTasks);
|
|
186
|
-
return running;
|
|
197
|
+
return startDaemon(daemonOptions(options));
|
|
187
198
|
}
|
|
188
199
|
|
|
189
200
|
export function serveMain(): void {
|
|
190
|
-
const configured = daemonOptions({});
|
|
191
201
|
runDaemonProcess({
|
|
192
|
-
...
|
|
202
|
+
...daemonOptions({}),
|
|
193
203
|
onListen: ({ host, port }) => {
|
|
194
204
|
logger.info("listening", { host, port });
|
|
195
|
-
runInitialMaintenance(configured.maintenanceTasks);
|
|
196
205
|
},
|
|
197
206
|
});
|
|
198
207
|
}
|
|
@@ -52,7 +52,17 @@ export const INDEX_OPERATION_TIMEOUT_MS = 10 * 60_000;
|
|
|
52
52
|
export const WATCH_INTERVAL_DEFAULT_MS = 30 * 60_000; // updates diff cadence
|
|
53
53
|
export const CATALOG_INTERVAL_DEFAULT_MS = 6 * 3_600_000; // full mirror TTL
|
|
54
54
|
export const INDEX_INTERVAL_DEFAULT_MS = 6 * 3_600_000; // static index regeneration TTL, same cadence as the catalog mirror it reads from
|
|
55
|
-
|
|
55
|
+
// Vehicle-service drift sweep cadence -- catches an out-of-band npm install/update a running
|
|
56
|
+
// daemon never picked up (e.g. a plain `pkg_update`/`pi update --extension`, which has no way to
|
|
57
|
+
// notify Armada/pi-packed at all -- see pkg-update-never-restarts-vehicle-daemon). Now that
|
|
58
|
+
// startDaemon() also runs every maintenance task once immediately at startup (see
|
|
59
|
+
// vehicle-server's own daemon.ts), a restart of pi-packed itself no longer waits out this
|
|
60
|
+
// interval at all -- this cadence now only bounds the OTHER case: an out-of-band update that
|
|
61
|
+
// lands while pi-packed's own daemon is already running and stays up. 30 minutes was tuned for
|
|
62
|
+
// a background safety net, not an interactive "I just updated something" workflow; 5 minutes
|
|
63
|
+
// keeps the same self-healing guarantee at a much more reasonable latency for a per-pass cost
|
|
64
|
+
// that's still just a handful of cheap native-service inspections for a fleet this size.
|
|
65
|
+
export const RECONCILE_INTERVAL_DEFAULT_MS = 5 * 60_000;
|
|
56
66
|
export const IDLE_BUDGET_DEFAULT_MS = 10 * 60_000; // on-demand self-exit
|
|
57
67
|
export const WATCHDOG_TICK_MS = 15_000;
|
|
58
68
|
|
package/service/test/cli.test.ts
CHANGED
|
@@ -120,7 +120,7 @@ class FakeDaemonServiceInstaller {
|
|
|
120
120
|
return {
|
|
121
121
|
ok: true,
|
|
122
122
|
output: "reconciled 1 Vehicle(s), skipped 0 non-daemon package(s)",
|
|
123
|
-
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
|
|
123
|
+
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true, versionChanged: true }],
|
|
124
124
|
skipped: 0,
|
|
125
125
|
failed: [],
|
|
126
126
|
pruned: [],
|
|
@@ -604,7 +604,7 @@ describe("CLI", () => {
|
|
|
604
604
|
expect(JSON.parse(json.out)).toEqual({
|
|
605
605
|
ok: true,
|
|
606
606
|
output: "reconciled 1 Vehicle(s), skipped 0 non-daemon package(s)",
|
|
607
|
-
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
|
|
607
|
+
reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true, versionChanged: true }],
|
|
608
608
|
skipped: 0,
|
|
609
609
|
failed: [],
|
|
610
610
|
pruned: [],
|
|
@@ -226,6 +226,33 @@ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle dr
|
|
|
226
226
|
}
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Regression guard for the double-execution risk this file's own daemon.ts (startPackedDaemon/
|
|
231
|
+
* serveMain) used to carry: a bespoke runInitialMaintenance() wrapper called explicitly, on top
|
|
232
|
+
* of what startDaemon() (vehicle-server) itself now already does since it started running every
|
|
233
|
+
* maintenance task once immediately at startup. Removed entirely -- this proves it stayed
|
|
234
|
+
* removed, not just that a task runs at least once (the test above already covers that).
|
|
235
|
+
*/
|
|
236
|
+
it("runs each maintenance task exactly once at startup, never twice", async () => {
|
|
237
|
+
const piHome = fakePiHome([]);
|
|
238
|
+
const paths = fakePaths();
|
|
239
|
+
let runs = 0;
|
|
240
|
+
const task: MaintenanceTask = {
|
|
241
|
+
name: "probe-exactly-once",
|
|
242
|
+
intervalMs: RECONCILE_INTERVAL_DEFAULT_MS,
|
|
243
|
+
run: () => {
|
|
244
|
+
runs++;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
const running = await startPackedDaemon({ paths, reg: registry, inst: installer, piHome, maintenanceTasks: [task] });
|
|
248
|
+
try {
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
250
|
+
expect(runs).toBe(1);
|
|
251
|
+
} finally {
|
|
252
|
+
await running.stop();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
229
256
|
it("does not reconcile Armada before the daemon has published its readiness handle", async () => {
|
|
230
257
|
const piHome = fakePiHome(["npm:@danypops/pi-packed", "npm:@danypops/probe"]);
|
|
231
258
|
const paths = fakePaths();
|
|
@@ -316,6 +343,46 @@ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle dr
|
|
|
316
343
|
});
|
|
317
344
|
});
|
|
318
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Real gap this closes: install()'s own ServiceInstallResult reports `installed: true`
|
|
348
|
+
* whenever register()/reconcile() SUCCEEDS -- which is true on every healthy pass whether or
|
|
349
|
+
* not anything actually changed underneath (register() is a safe no-op against unchanged
|
|
350
|
+
* desired state). A caller logging "applied changes" off `installed` alone would say so on
|
|
351
|
+
* literally every tick of a healthy, unchanged fleet -- see pkg-update-never-restarts-
|
|
352
|
+
* vehicle-daemon. versionChanged is the honest signal instead.
|
|
353
|
+
*/
|
|
354
|
+
describe("reconcileAllDaemonServices reports versionChanged honestly, not just installed", () => {
|
|
355
|
+
it("is true on a package's first-ever registration, false on an unchanged re-sweep, true again once its on-disk version actually moves", async () => {
|
|
356
|
+
const piHome = fakePiHome(["npm:@danypops/probe"]);
|
|
357
|
+
installMockDaemon(piHome, "@danypops/probe");
|
|
358
|
+
const harness = await createArmadaTestHarness();
|
|
359
|
+
try {
|
|
360
|
+
const installer = new RealDaemonServiceInstaller(harness.registrar);
|
|
361
|
+
|
|
362
|
+
const first = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
363
|
+
expect(first.reconciled).toEqual([
|
|
364
|
+
{ packageName: "@danypops/probe", vehicleName: "probe", installed: true, versionChanged: true },
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
const second = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
368
|
+
expect(second.reconciled).toEqual([
|
|
369
|
+
{ packageName: "@danypops/probe", vehicleName: "probe", installed: true, versionChanged: false },
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
writeFileSync(
|
|
373
|
+
join(piHome, "npm", "node_modules", "@danypops", "probe", "package.json"),
|
|
374
|
+
JSON.stringify({ name: "@danypops/probe", version: "1.1.0", packed: { daemonService: { binPath: "cli.ts", args: ["serve"] } } }),
|
|
375
|
+
);
|
|
376
|
+
const third = await reconcileAllDaemonServices(piHome, undefined, installer);
|
|
377
|
+
expect(third.reconciled).toEqual([
|
|
378
|
+
{ packageName: "@danypops/probe", vehicleName: "probe", installed: true, versionChanged: true },
|
|
379
|
+
]);
|
|
380
|
+
} finally {
|
|
381
|
+
await harness.dispose();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
319
386
|
describe("reconcileAllDaemonServices pruning (the-armada-registrar-jittor-web-spider-daemon-collision)", () => {
|
|
320
387
|
function stalePiHomeVehicle(piHome: string, name: string): VehicleSpec {
|
|
321
388
|
return {
|