@danypops/pi-packed 0.25.1 → 0.25.2

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.25.1",
3
+ "version": "0.25.2",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,6 +38,7 @@ import {
38
38
  } from "@danypops/vehicle-server/service";
39
39
  import { npmPackageName, readInstalledPackagesAcrossScopes, readPackageDeclarations } from "../packages/installed.ts";
40
40
  import type { InstalledPkg } from "../packages/package.ts";
41
+ import { versionAtLeast } from "../publish/publish.ts";
41
42
 
42
43
  export interface DaemonServiceManifest {
43
44
  /** Relative to the installed package's own root directory. */
@@ -125,6 +126,47 @@ function dependsOnVehicle(pkg: InstalledPackageJson): boolean {
125
126
  * web-spider, packed, enigma all expose a `serve` subcommand on the same
126
127
  * bin their `service install` command already points at).
127
128
  */
129
+ /**
130
+ * Resolves ONE candidate directory to a daemon entrypoint, using the same
131
+ * explicit-manifest-first-then-convention rule detectVehicleDaemonService's
132
+ * own doc comment describes -- factored out so the nested and hoisted
133
+ * candidates for the same dependency name can each be resolved
134
+ * independently and then compared, instead of the first one found
135
+ * unconditionally winning (see detectVehicleDaemonService's own comment on
136
+ * why iteration order alone is never a safe tie-break).
137
+ */
138
+ function resolveDependencyCandidate(depDir: string, depName: string): ResolvedDaemonEntrypoint | undefined {
139
+ const dep = readPackageJson(depDir);
140
+ if (!dep) return undefined;
141
+ // An explicit manifest on the dependency itself wins over convention detection here too --
142
+ // mirrors resolveDaemonServiceSpec's own explicit-manifest-first behavior for a directly
143
+ // installed package. Without this, a dependency's own correct, explicit
144
+ // handleFilename/name/etc. was silently discarded in favor of a guess whenever the daemon
145
+ // was discovered one level down instead of installed directly by name -- exactly the
146
+ // common case (a Pi extension like pi-papyrus has no daemon of its own). Confirmed live:
147
+ // papyrus's real handle file is "vehicle-handle.json", not the "daemon.json" convention
148
+ // guess, and papyrus is only ever discovered this way, never installed directly by name.
149
+ const depManifest = dep.packed?.daemonService;
150
+ if (depManifest && typeof depManifest.binPath === "string" && depManifest.binPath.length > 0 && dep.version) {
151
+ return {
152
+ binPath: join(depDir, depManifest.binPath),
153
+ args: depManifest.args,
154
+ name: depManifest.name ?? unscopedName(dep.name ?? depName),
155
+ displayName: depManifest.displayName,
156
+ handleFilename: depManifest.handleFilename,
157
+ workingDirectory: depManifest.workingDirectory,
158
+ restartOnFailure: depManifest.restartOnFailure,
159
+ restartSec: depManifest.restartSec,
160
+ version: dep.version,
161
+ };
162
+ }
163
+ const depBin = firstBinPath(dep);
164
+ if (depBin && dependsOnVehicle(dep) && dep.version) {
165
+ return { binPath: join(depDir, depBin), args: ["serve"], name: unscopedName(dep.name ?? depName), version: dep.version };
166
+ }
167
+ return undefined;
168
+ }
169
+
128
170
  export function detectVehicleDaemonService(
129
171
  packageDir: string,
130
172
  fallbackName: string,
@@ -143,36 +185,24 @@ export function detectVehicleDaemonService(
143
185
  join(packageDir, "node_modules", depName),
144
186
  ...(hoistedNodeModulesDir ? [join(hoistedNodeModulesDir, depName)] : []),
145
187
  ];
146
- for (const depDir of candidateDirs) {
147
- const dep = readPackageJson(depDir);
148
- if (!dep) continue;
149
- // An explicit manifest on the dependency itself wins over convention detection here too --
150
- // mirrors resolveDaemonServiceSpec's own explicit-manifest-first behavior for a directly
151
- // installed package. Without this, a dependency's own correct, explicit
152
- // handleFilename/name/etc. was silently discarded in favor of a guess whenever the daemon
153
- // was discovered one level down instead of installed directly by name -- exactly the
154
- // common case (a Pi extension like pi-papyrus has no daemon of its own). Confirmed live:
155
- // papyrus's real handle file is "vehicle-handle.json", not the "daemon.json" convention
156
- // guess, and papyrus is only ever discovered this way, never installed directly by name.
157
- const depManifest = dep.packed?.daemonService;
158
- if (depManifest && typeof depManifest.binPath === "string" && depManifest.binPath.length > 0 && dep.version) {
159
- return {
160
- binPath: join(depDir, depManifest.binPath),
161
- args: depManifest.args,
162
- name: depManifest.name ?? unscopedName(dep.name ?? depName),
163
- displayName: depManifest.displayName,
164
- handleFilename: depManifest.handleFilename,
165
- workingDirectory: depManifest.workingDirectory,
166
- restartOnFailure: depManifest.restartOnFailure,
167
- restartSec: depManifest.restartSec,
168
- version: dep.version,
169
- };
170
- }
171
- const depBin = firstBinPath(dep);
172
- if (depBin && dependsOnVehicle(dep) && dep.version) {
173
- return { binPath: join(depDir, depBin), args: ["serve"], name: unscopedName(dep.name ?? depName), version: dep.version };
174
- }
188
+ const resolved = candidateDirs
189
+ .map((depDir) => resolveDependencyCandidate(depDir, depName))
190
+ .filter((entry): entry is ResolvedDaemonEntrypoint => entry !== undefined);
191
+ if (resolved.length === 0) continue;
192
+ if (resolved.length === 1) return resolved[0];
193
+ // Both a nested copy (checked first, above) and a hoisted copy resolved for the exact same
194
+ // dependency name -- e.g. pi-papyrus's own nested @danypops/jittor@0.14.0 alongside the
195
+ // correctly-versioned, tree-wide hoisted @danypops/jittor@0.18.1. The real jittor incident:
196
+ // the nested copy always won here, unconditionally, purely because candidateDirs checks it
197
+ // first -- regardless of which was actually newer. Prefer the real higher semver version
198
+ // instead; a genuine tie keeps the first-found (nested) result, preserving papyrus's own
199
+ // intentional-nesting case (confirmed live, see resolveDependencyCandidate's own comment)
200
+ // exactly as before whenever there's nothing to disambiguate.
201
+ let best = resolved[0]!;
202
+ for (const candidate of resolved.slice(1)) {
203
+ if (versionAtLeast(candidate.version, best.version) && candidate.version !== best.version) best = candidate;
175
204
  }
205
+ return best;
176
206
  }
177
207
  return undefined;
178
208
  }
@@ -208,6 +208,60 @@ describe("resolveDaemonServiceSpec", () => {
208
208
  expect(result.spec.restartSec).toBe(2);
209
209
  });
210
210
 
211
+ it("prefers the higher-semver candidate when BOTH a nested and hoisted copy of the same dependency resolve via bin+dependency convention -- the real jittor incident: a stale nested copy must never win over a correctly-versioned hoisted one just because it's checked first", () => {
212
+ const piHome = fakePiHome();
213
+ const extDir = join(piHome, "npm", "node_modules", "@danypops/pi-papyrus");
214
+ writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": "^0.14.0" } });
215
+ // A stale nested copy pi-papyrus's own outdated floor forced npm to keep private, instead of
216
+ // deduping to the shared, correctly-versioned, hoisted copy every other consumer already uses.
217
+ const nestedDir = join(extDir, "node_modules", "@danypops/jittor");
218
+ writeRawPackage(nestedDir, {
219
+ name: "@danypops/jittor",
220
+ version: "0.14.0",
221
+ bin: { jittor: "src/cli.ts" },
222
+ dependencies: { "@danypops/vehicle-server": "^0.3.1" },
223
+ });
224
+ const hoistedDir = join(piHome, "npm", "node_modules", "@danypops/jittor");
225
+ writeRawPackage(hoistedDir, {
226
+ name: "@danypops/jittor",
227
+ version: "0.18.1",
228
+ bin: { jittor: "src/cli.ts" },
229
+ dependencies: { "@danypops/vehicle-server": "^0.17.0" },
230
+ });
231
+
232
+ const result = resolveDaemonServiceSpec(piHome, "npm:@danypops/pi-papyrus");
233
+ expect(result.ok).toBe(true);
234
+ if (!result.ok) throw new Error("expected ok");
235
+ expect(result.spec.binPath).toBe(join(hoistedDir, "src/cli.ts"));
236
+ expect(result.spec.version).toBe("0.18.1");
237
+ });
238
+
239
+ it("keeps the nested copy when it is genuinely the newer (or equal) one -- the tie-break only ever prefers a candidate that's actually newer, never blindly flips to hoisted", () => {
240
+ const piHome = fakePiHome();
241
+ const extDir = join(piHome, "npm", "node_modules", "@danypops/pi-papyrus");
242
+ writeRawPackage(extDir, { name: "@danypops/pi-papyrus", version: "1.0.0", dependencies: { "@danypops/jittor": "^0.18.0" } });
243
+ const nestedDir = join(extDir, "node_modules", "@danypops/jittor");
244
+ writeRawPackage(nestedDir, {
245
+ name: "@danypops/jittor",
246
+ version: "0.18.1",
247
+ bin: { jittor: "src/cli.ts" },
248
+ dependencies: { "@danypops/vehicle-server": "^0.17.0" },
249
+ });
250
+ const hoistedDir = join(piHome, "npm", "node_modules", "@danypops/jittor");
251
+ writeRawPackage(hoistedDir, {
252
+ name: "@danypops/jittor",
253
+ version: "0.14.0",
254
+ bin: { jittor: "src/cli.ts" },
255
+ dependencies: { "@danypops/vehicle-server": "^0.3.1" },
256
+ });
257
+
258
+ const result = resolveDaemonServiceSpec(piHome, "npm:@danypops/pi-papyrus");
259
+ expect(result.ok).toBe(true);
260
+ if (!result.ok) throw new Error("expected ok");
261
+ expect(result.spec.binPath).toBe(join(nestedDir, "src/cli.ts"));
262
+ expect(result.spec.version).toBe("0.18.1");
263
+ });
264
+
211
265
  it("also detects the legacy @danypops/daemon-kit dependency name, not just vehicle-server", () => {
212
266
  const piHome = fakePiHome();
213
267
  const dir = join(piHome, "npm", "node_modules", "@danypops/web-spider-daemon");