@danypops/pi-packed 0.27.4 → 0.27.6

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.4",
3
+ "version": "0.27.6",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,6 @@
31
31
  "@danypops/packed": "^0.7.0",
32
32
  "@danypops/pi-extension-harness": "^0.7.0",
33
33
  "@danypops/vehicle-client": "^0.10.0",
34
- "@danypops/vehicle-client-pi": "^0.40.1",
35
34
  "@danypops/vehicle-core": "^0.15.0",
36
35
  "@danypops/vehicle-server": "^0.24.2",
37
36
  "jiti": "^2.7.0",
@@ -41,9 +40,11 @@
41
40
  },
42
41
  "devDependencies": {
43
42
  "@danypops/pi-tui-harness": "^0.0.1",
43
+ "@danypops/vehicle-client-pi": "^0.41.3",
44
44
  "@types/semver": "^7.7.1"
45
45
  },
46
46
  "peerDependencies": {
47
+ "@danypops/vehicle-client-pi": "^0.41.3",
47
48
  "@earendil-works/pi-coding-agent": "*",
48
49
  "@earendil-works/pi-tui": "*",
49
50
  "typebox": "*"
@@ -566,8 +566,14 @@ function dependencyCheck(context: Context): void {
566
566
  path: file,
567
567
  message: `${name} must be declared as peerDependencies[${name}] = *`,
568
568
  });
569
- } else if (dependencies[name] === undefined && optional[name] === undefined) {
570
- const misplaced = dev[name] !== undefined ? "devDependency" : peers[name] !== undefined ? "peerDependency" : undefined;
569
+ } else if (dependencies[name] === undefined && optional[name] === undefined && peers[name] === undefined) {
570
+ // A real (non-"*") peerDependency range genuinely satisfies this -- npm's own
571
+ // documented mechanism for "a real runtime dependency the host is expected to
572
+ // already share a single instance of," distinct from CORE_PACKAGES' own stricter
573
+ // peers[name] === "*" contract (Pi's own always-exactly-one-host-version packages,
574
+ // checked separately above). A devDependency alone still doesn't count -- it
575
+ // guarantees nothing for a real consumer's own install.
576
+ const misplaced = dev[name] !== undefined ? "devDependency" : undefined;
571
577
  context.add({
572
578
  code: "RUNTIME_DEPENDENCY_MISSING",
573
579
  severity: "error",
@@ -455,7 +455,13 @@ export function listManagedPackages(piHome: string, projectRoot?: string): Insta
455
455
  }
456
456
 
457
457
  export interface ReconcileAllResult {
458
- reconciled: Array<{ packageName: string; vehicleName: string; installed: boolean; reason?: string }>;
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
  }
@@ -134,9 +134,10 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
134
134
  // success indistinguishable from "this never ran" -- there was no way to confirm a
135
135
  // restart this task performed actually happened, short of checking the process's own
136
136
  // PID by hand.
137
- if (result.reconciled.some((entry) => entry.installed) || result.pruned.length > 0) {
137
+ const changed = result.reconciled.filter((entry) => entry.installed && entry.versionChanged);
138
+ if (changed.length > 0 || result.pruned.length > 0) {
138
139
  logger.info("vehicle-reconcile applied changes", {
139
- reconciled: result.reconciled.filter((entry) => entry.installed).map((entry) => entry.vehicleName),
140
+ reconciled: changed.map((entry) => entry.vehicleName),
140
141
  pruned: result.pruned.map((entry) => entry.vehicleName),
141
142
  });
142
143
  }
@@ -441,4 +441,37 @@ describe("dependencyCheck false-positive regressions (packed check)", () => {
441
441
  const report = await checkPackage(root, { generic: false });
442
442
  expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
443
443
  });
444
+
445
+ it("a real (non-*) peerDependency range genuinely satisfies a shipped-code import -- npm's own documented mechanism for a shared-singleton runtime dependency, distinct from CORE_PACKAGES' own peers[name] === '*' contract", async () => {
446
+ const root = fixture(
447
+ {
448
+ ...base,
449
+ files: ["extensions", "README.md", "LICENSE"],
450
+ peerDependencies: { ...base.peerDependencies, "@scope/shared-singleton": "^1.2.3" },
451
+ devDependencies: { "@scope/shared-singleton": "^1.2.3" },
452
+ },
453
+ {
454
+ "extensions/index.ts": 'import { thing } from "@scope/shared-singleton";\nexport default function () { return thing; }\n',
455
+ "README.md": "# Example",
456
+ LICENSE: "MIT",
457
+ },
458
+ );
459
+ const report = await checkPackage(root, { generic: false });
460
+ expect(codes(report.diagnostics)).not.toContain("RUNTIME_DEPENDENCY_MISSING");
461
+ });
462
+
463
+ it("still flags a shipped-code import declared only as a devDependency, even though a peerDependency-shaped exception now exists", async () => {
464
+ const root = fixture(
465
+ { ...base, files: ["extensions", "README.md", "LICENSE"], devDependencies: { "leftover-dev-only": "1.0.0" } },
466
+ {
467
+ "extensions/index.ts": 'import thing from "leftover-dev-only";\nexport default function () { return thing; }\n',
468
+ "README.md": "# Example",
469
+ LICENSE: "MIT",
470
+ },
471
+ );
472
+ const report = await checkPackage(root, { generic: false });
473
+ expect(codes(report.diagnostics)).toContain("RUNTIME_DEPENDENCY_MISSING");
474
+ const diagnostic = report.diagnostics.find((d) => d.code === "RUNTIME_DEPENDENCY_MISSING")!;
475
+ expect(diagnostic.message).toContain("only a devDependency");
476
+ });
444
477
  });
@@ -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: [],
@@ -343,6 +343,46 @@ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle dr
343
343
  });
344
344
  });
345
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
+
346
386
  describe("reconcileAllDaemonServices pruning (the-armada-registrar-jittor-web-spider-daemon-collision)", () => {
347
387
  function stalePiHomeVehicle(piHome: string, name: string): VehicleSpec {
348
388
  return {