@patchstack/connect 0.3.29 → 0.3.31

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.
Files changed (40) hide show
  1. package/AGENT-INSTALL.md +42 -23
  2. package/README.md +40 -11
  3. package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
  4. package/dist/chunk-LLKP5EJS.js.map +1 -0
  5. package/dist/cli.js +1265 -287
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +456 -111
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +76 -10
  10. package/dist/index.d.ts +76 -10
  11. package/dist/index.js +449 -104
  12. package/dist/index.js.map +1 -1
  13. package/dist/protect/templates/astro-middleware.ts +33 -13
  14. package/dist/protect/templates/demo-rules.json +2 -3
  15. package/dist/protect/templates/express-guard.cjs +26 -15
  16. package/dist/protect/templates/express-guard.js +26 -15
  17. package/dist/protect/templates/express-guard.ts +33 -16
  18. package/dist/protect/templates/fastify-plugin.cjs +33 -11
  19. package/dist/protect/templates/fastify-plugin.js +33 -11
  20. package/dist/protect/templates/fastify-plugin.ts +40 -12
  21. package/dist/protect/templates/generic-guard.cjs +28 -12
  22. package/dist/protect/templates/generic-guard.js +28 -13
  23. package/dist/protect/templates/generic-guard.ts +41 -20
  24. package/dist/protect/templates/guard.ts +47 -27
  25. package/dist/protect/templates/next-middleware.ts +29 -12
  26. package/dist/protect/templates/nuxt-middleware.ts +29 -12
  27. package/dist/protect/templates/rules.json +2 -2
  28. package/dist/protect/templates/sveltekit-hooks.ts +33 -13
  29. package/dist/protect.cjs +1204 -252
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.ts +39 -11
  32. package/dist/protect.edge.js +792 -143
  33. package/dist/protect.edge.js.map +4 -4
  34. package/dist/protect.js +794 -145
  35. package/dist/protect.js.map +1 -1
  36. package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-KNZQBC4V.js} +387 -100
  37. package/dist/refresh-manifest-KNZQBC4V.js.map +1 -0
  38. package/package.json +5 -2
  39. package/dist/chunk-MJOTFUDE.js.map +0 -1
  40. package/dist/refresh-manifest-VRBE6RH6.js.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  pulseFetch
3
- } from "./chunk-MJOTFUDE.js";
3
+ } from "./chunk-LLKP5EJS.js";
4
4
 
5
5
  // src/config.ts
6
- import { readFile, writeFile } from "fs/promises";
6
+ import { readFile, writeFile, chmod } from "fs/promises";
7
7
  import path from "path";
8
8
 
9
9
  // src/types.ts
@@ -110,9 +110,11 @@ function isTimeoutError(cause) {
110
110
 
111
111
  // src/config.ts
112
112
  var CONFIG_FILENAME = ".patchstackrc.json";
113
+ var SECRET_FILENAME = ".patchstackrc.local.json";
113
114
  var DEFAULT_ENVIRONMENT = "production";
114
115
  async function resolveConfig(options) {
115
116
  const fromFile = await readConfigFile(options.cwd);
117
+ const fromSecretFile = await readSecretFile(options.cwd);
116
118
  const fromEnv = readEnv();
117
119
  const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
118
120
  const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
@@ -137,8 +139,8 @@ async function resolveConfig(options) {
137
139
  "CONFIG_MISSING"
138
140
  );
139
141
  }
140
- const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
141
- const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
142
+ const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
143
+ const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
142
144
  return {
143
145
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
144
146
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
@@ -149,8 +151,15 @@ async function resolveConfig(options) {
149
151
  widget: fromFile.widget !== false
150
152
  };
151
153
  }
154
+ async function readSecretFile(cwd) {
155
+ const file = await readJsonFile(cwd, SECRET_FILENAME);
156
+ return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
157
+ }
152
158
  async function readConfigFile(cwd) {
153
- const target = path.join(cwd, CONFIG_FILENAME);
159
+ return readJsonFile(cwd, CONFIG_FILENAME);
160
+ }
161
+ async function readJsonFile(cwd, filename) {
162
+ const target = path.join(cwd, filename);
154
163
  let raw;
155
164
  try {
156
165
  raw = await readFile(target, "utf8");
@@ -228,7 +237,7 @@ async function readDeclaredDependencyNames(cwd) {
228
237
  return [];
229
238
  }
230
239
  const names = /* @__PURE__ */ new Set();
231
- for (const group of ["dependencies", "devDependencies"]) {
240
+ for (const group of ["dependencies", "devDependencies", "optionalDependencies"]) {
232
241
  const deps = parsed[group];
233
242
  if (typeof deps !== "object" || deps === null) {
234
243
  continue;
@@ -246,14 +255,171 @@ function missingDependencies(declared, packages) {
246
255
  const present = new Set(packages.map((entry) => entry.name));
247
256
  return declared.filter((name) => !present.has(name));
248
257
  }
258
+ function disagreements(packages, other) {
259
+ const otherVersions = /* @__PURE__ */ new Map();
260
+ for (const entry of other) {
261
+ if (!otherVersions.has(entry.name)) otherVersions.set(entry.name, entry.version);
262
+ }
263
+ const seen = /* @__PURE__ */ new Set();
264
+ const conflicts = [];
265
+ for (const entry of packages) {
266
+ if (seen.has(entry.name)) continue;
267
+ seen.add(entry.name);
268
+ const otherVersion = otherVersions.get(entry.name);
269
+ if (otherVersion !== void 0 && otherVersion !== entry.version) {
270
+ conflicts.push({ name: entry.name, version: entry.version, otherVersion });
271
+ }
272
+ }
273
+ return conflicts;
274
+ }
249
275
 
250
- // src/parsers/npm.ts
276
+ // src/parsers/bun.ts
251
277
  import { readFile as readFile3 } from "fs/promises";
278
+
279
+ // src/parsers/report.ts
280
+ var MAX_SAMPLES = 3;
281
+ function newReport() {
282
+ return { unreadable: 0, samples: [] };
283
+ }
284
+ function recordUnreadable(report, sample) {
285
+ if (!report) return;
286
+ report.unreadable++;
287
+ if (report.samples.length < MAX_SAMPLES) report.samples.push(sample);
288
+ }
289
+ function unreadableWarning(filename, report) {
290
+ if (report.unreadable === 0) return null;
291
+ const shown = report.samples.join(", ");
292
+ const one = report.unreadable === 1;
293
+ return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
294
+ }
295
+
296
+ // src/parsers/bun.ts
297
+ async function parseBunLockfile(lockfilePath, report) {
298
+ let raw;
299
+ try {
300
+ raw = await readFile3(lockfilePath, "utf8");
301
+ } catch (cause) {
302
+ throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
303
+ }
304
+ let parsed;
305
+ try {
306
+ parsed = JSON.parse(stripTrailingCommas(raw));
307
+ } catch (cause) {
308
+ throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
309
+ }
310
+ const packages = parsed?.packages;
311
+ if (typeof packages !== "object" || packages === null) {
312
+ throw new PatchstackError(`Lockfile at ${lockfilePath} has no "packages" entries`, "LOCKFILE_PARSE_ERROR");
313
+ }
314
+ const direct = directDependencyNames(parsed);
315
+ const entries = [];
316
+ let skipped = 0;
317
+ for (const [key, value] of Object.entries(packages)) {
318
+ const descriptor = Array.isArray(value) ? value[0] : value;
319
+ if (typeof descriptor !== "string") {
320
+ skipped++;
321
+ recordUnreadable(report, key);
322
+ continue;
323
+ }
324
+ const split = splitDescriptor(descriptor);
325
+ if (split === null) {
326
+ skipped++;
327
+ if (!isDeliberatelyExcluded(descriptor)) recordUnreadable(report, key);
328
+ continue;
329
+ }
330
+ entries.push({
331
+ name: split.name,
332
+ version: split.version,
333
+ ...direct.has(split.name) ? { direct: true } : {},
334
+ ...key === split.name ? {} : { path: key }
335
+ });
336
+ }
337
+ if (entries.length === 0) {
338
+ throw new PatchstackError(
339
+ `Lockfile at ${lockfilePath} lists no registry packages${skipped > 0 ? ` (${skipped} non-registry entr${skipped === 1 ? "y" : "ies"} skipped)` : ""}`,
340
+ "LOCKFILE_PARSE_ERROR"
341
+ );
342
+ }
343
+ return entries;
344
+ }
345
+ function splitDescriptor(descriptor) {
346
+ const at = descriptor.lastIndexOf("@");
347
+ if (at <= 0) return null;
348
+ const name = descriptor.slice(0, at);
349
+ const version = descriptor.slice(at + 1);
350
+ if (name === "" || version === "") return null;
351
+ if (NON_REGISTRY_VERSION.test(version)) return null;
352
+ return { name, version };
353
+ }
354
+ var NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
355
+ function isDeliberatelyExcluded(descriptor) {
356
+ const at = descriptor.lastIndexOf("@");
357
+ if (at <= 0) return false;
358
+ return NON_REGISTRY_VERSION.test(descriptor.slice(at + 1));
359
+ }
360
+ function directDependencyNames(parsed) {
361
+ const names = /* @__PURE__ */ new Set();
362
+ const workspaces = parsed?.workspaces;
363
+ if (typeof workspaces !== "object" || workspaces === null) return names;
364
+ for (const workspace of Object.values(workspaces)) {
365
+ if (typeof workspace !== "object" || workspace === null) continue;
366
+ for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
367
+ const deps = workspace[group];
368
+ if (typeof deps !== "object" || deps === null) continue;
369
+ for (const name of Object.keys(deps)) names.add(name);
370
+ }
371
+ }
372
+ return names;
373
+ }
374
+ function stripTrailingCommas(source) {
375
+ const out = [];
376
+ let inString = false;
377
+ let pendingComma = -1;
378
+ for (let i = 0; i < source.length; i++) {
379
+ const ch = source[i];
380
+ if (inString) {
381
+ out.push(ch);
382
+ if (ch === "\\") {
383
+ const next = source[i + 1];
384
+ if (next !== void 0) {
385
+ out.push(next);
386
+ i++;
387
+ }
388
+ } else if (ch === '"') {
389
+ inString = false;
390
+ }
391
+ continue;
392
+ }
393
+ if (ch === '"') {
394
+ inString = true;
395
+ pendingComma = -1;
396
+ out.push(ch);
397
+ continue;
398
+ }
399
+ if (ch === ",") {
400
+ pendingComma = out.length;
401
+ out.push(ch);
402
+ continue;
403
+ }
404
+ if (ch === "}" || ch === "]") {
405
+ if (pendingComma !== -1) out[pendingComma] = " ";
406
+ pendingComma = -1;
407
+ out.push(ch);
408
+ continue;
409
+ }
410
+ if (!/\s/.test(ch)) pendingComma = -1;
411
+ out.push(ch);
412
+ }
413
+ return out.join("");
414
+ }
415
+
416
+ // src/parsers/npm.ts
417
+ import { readFile as readFile4 } from "fs/promises";
252
418
  import path3 from "path";
253
419
  async function parseNpmLockfile(lockfilePath) {
254
420
  let raw;
255
421
  try {
256
- raw = await readFile3(lockfilePath, "utf8");
422
+ raw = await readFile4(lockfilePath, "utf8");
257
423
  } catch (cause) {
258
424
  throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
259
425
  }
@@ -334,8 +500,9 @@ function isDirectV2(pkgPath) {
334
500
  }
335
501
 
336
502
  // src/parsers/node_modules.ts
337
- import { lstat, readFile as readFile4, readdir, stat } from "fs/promises";
503
+ import { lstat, readFile as readFile5, readdir, realpath, stat } from "fs/promises";
338
504
  import path4 from "path";
505
+ var PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
339
506
  async function walkNodeModules(cwd) {
340
507
  const root = path4.join(cwd, "node_modules");
341
508
  try {
@@ -356,96 +523,125 @@ async function walkNodeModules(cwd) {
356
523
  cause
357
524
  );
358
525
  }
359
- const entries = [];
360
- await walk(root, entries, 0);
361
- return entries;
526
+ const seen = /* @__PURE__ */ new Map();
527
+ try {
528
+ seen.set(await realpath(cwd), -1);
529
+ } catch {
530
+ }
531
+ return dedupeByShallowest(await walk(root, 0, seen, cwd));
532
+ }
533
+ function dedupeByShallowest(found) {
534
+ const best = /* @__PURE__ */ new Map();
535
+ for (const item of found) {
536
+ const current = best.get(item.realpath);
537
+ if (current === void 0 || item.depth < current.depth) best.set(item.realpath, item);
538
+ }
539
+ return [...best.values()].map(({ entry }) => entry);
362
540
  }
363
- async function walk(dir, acc, depth) {
541
+ var CONCURRENCY = 32;
542
+ async function walk(dir, depth, seen, root) {
364
543
  let names;
365
544
  try {
366
545
  names = await readdir(dir);
367
546
  } catch {
368
- return;
547
+ return [];
369
548
  }
370
- for (const name of names) {
371
- if (name.startsWith(".")) {
372
- continue;
373
- }
549
+ const perEntry = await mapLimit(names, async (name) => {
550
+ if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
374
551
  const fullPath = path4.join(dir, name);
375
- if (!await isPlainDirectory(fullPath)) {
376
- continue;
377
- }
552
+ const resolved = await claim(fullPath, seen, depth);
553
+ if (resolved === null) return [];
554
+ if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
378
555
  if (name.startsWith("@")) {
379
556
  let subNames;
380
557
  try {
381
558
  subNames = await readdir(fullPath);
382
559
  } catch {
383
- continue;
560
+ return [];
384
561
  }
385
- for (const sub of subNames) {
386
- if (sub.startsWith(".")) {
387
- continue;
388
- }
562
+ const scoped = await mapLimit(subNames, async (sub) => {
563
+ if (sub.startsWith(".")) return [];
389
564
  const scopedDir = path4.join(fullPath, sub);
390
- if (!await isPlainDirectory(scopedDir)) {
391
- continue;
392
- }
393
- await readPackage(scopedDir, depth, acc);
394
- await walkNested(scopedDir, acc, depth);
395
- }
396
- continue;
565
+ const scopedReal = await claim(scopedDir, seen, depth);
566
+ if (scopedReal === null) return [];
567
+ return packageAndNested(scopedDir, scopedReal, depth, seen, root);
568
+ });
569
+ return scoped.flat();
397
570
  }
398
- await readPackage(fullPath, depth, acc);
399
- await walkNested(fullPath, acc, depth);
400
- }
571
+ return packageAndNested(fullPath, resolved, depth, seen, root);
572
+ });
573
+ return perEntry.flat();
401
574
  }
402
- async function readPackage(pkgDir, depth, acc) {
575
+ async function packageAndNested(pkgDir, resolved, depth, seen, root) {
576
+ const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
577
+ return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
578
+ }
579
+ async function mapLimit(items, fn) {
580
+ const results = new Array(items.length);
581
+ let next = 0;
582
+ const workers = Array.from({ length: Math.min(CONCURRENCY, items.length) }, async () => {
583
+ while (next < items.length) {
584
+ const index = next++;
585
+ results[index] = await fn(items[index]);
586
+ }
587
+ });
588
+ await Promise.all(workers);
589
+ return results;
590
+ }
591
+ function installLocation(root, pkgDir, impl = path4) {
592
+ return impl.relative(root, pkgDir).split(impl.sep).join("/");
593
+ }
594
+ async function readPackage(pkgDir, depth, root) {
403
595
  let raw;
404
596
  try {
405
- raw = await readFile4(path4.join(pkgDir, "package.json"), "utf8");
597
+ raw = await readFile5(path4.join(pkgDir, "package.json"), "utf8");
406
598
  } catch {
407
- return;
599
+ return null;
408
600
  }
409
601
  let parsed;
410
602
  try {
411
603
  parsed = JSON.parse(raw);
412
604
  } catch {
413
- return;
605
+ return null;
414
606
  }
415
607
  if (typeof parsed.name !== "string" || parsed.name.length === 0) {
416
- return;
608
+ return null;
417
609
  }
418
610
  if (typeof parsed.version !== "string" || parsed.version.length === 0) {
419
- return;
611
+ return null;
420
612
  }
421
- acc.push({
422
- name: parsed.name,
423
- version: parsed.version,
424
- direct: depth === 0
425
- });
613
+ return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
426
614
  }
427
- async function walkNested(pkgDir, acc, depth) {
615
+ async function walkNested(pkgDir, depth, seen, root) {
428
616
  const nested = path4.join(pkgDir, "node_modules");
429
- if (!await isPlainDirectory(nested)) {
430
- return;
617
+ if (await claim(nested, seen, depth + 1) === null) {
618
+ return [];
431
619
  }
432
- await walk(nested, acc, depth + 1);
620
+ return walk(nested, depth + 1, seen, root);
433
621
  }
434
- async function isPlainDirectory(dir) {
622
+ async function claim(dir, seen, depth) {
623
+ let resolved;
435
624
  try {
436
625
  const info = await lstat(dir);
437
- return info.isDirectory() && !info.isSymbolicLink();
626
+ if (!info.isDirectory() && !info.isSymbolicLink()) return null;
627
+ resolved = await realpath(dir);
628
+ const target = await stat(resolved);
629
+ if (!target.isDirectory()) return null;
438
630
  } catch {
439
- return false;
631
+ return null;
440
632
  }
633
+ const claimed = seen.get(resolved);
634
+ if (claimed !== void 0 && claimed <= depth) return null;
635
+ seen.set(resolved, depth);
636
+ return resolved;
441
637
  }
442
638
 
443
639
  // src/parsers/pnpm.ts
444
- import { readFile as readFile5 } from "fs/promises";
445
- async function parsePnpmLockfile(lockfilePath) {
640
+ import { readFile as readFile6 } from "fs/promises";
641
+ async function parsePnpmLockfile(lockfilePath, report) {
446
642
  let raw;
447
643
  try {
448
- raw = await readFile5(lockfilePath, "utf8");
644
+ raw = await readFile6(lockfilePath, "utf8");
449
645
  } catch (cause) {
450
646
  throw new PatchstackError(
451
647
  `Could not read lockfile at ${lockfilePath}`,
@@ -466,6 +662,7 @@ async function parsePnpmLockfile(lockfilePath) {
466
662
  for (const key of packageKeys) {
467
663
  const parsed = parsePackageKey(key);
468
664
  if (parsed === null) {
665
+ recordUnreadable(report, key);
469
666
  continue;
470
667
  }
471
668
  entries.push({
@@ -693,12 +890,12 @@ function extractLeafName(trimmed) {
693
890
  }
694
891
 
695
892
  // src/parsers/yarn.ts
696
- import { readFile as readFile6 } from "fs/promises";
893
+ import { readFile as readFile7 } from "fs/promises";
697
894
  import path5 from "path";
698
- async function parseYarnLockfile(lockfilePath) {
895
+ async function parseYarnLockfile(lockfilePath, report) {
699
896
  let raw;
700
897
  try {
701
- raw = await readFile6(lockfilePath, "utf8");
898
+ raw = await readFile7(lockfilePath, "utf8");
702
899
  } catch (cause) {
703
900
  throw new PatchstackError(
704
901
  `Could not read lockfile at ${lockfilePath}`,
@@ -706,7 +903,7 @@ async function parseYarnLockfile(lockfilePath) {
706
903
  cause
707
904
  );
708
905
  }
709
- const blocks = parseBlocks(raw);
906
+ const blocks = parseBlocks(raw, report);
710
907
  if (blocks.length === 0) {
711
908
  throw new PatchstackError(
712
909
  `Lockfile at ${lockfilePath} contains no package entries`,
@@ -735,13 +932,19 @@ async function parseYarnLockfile(lockfilePath) {
735
932
  }
736
933
  return entries;
737
934
  }
738
- function parseBlocks(raw) {
935
+ var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
936
+ function parseBlocks(raw, report) {
739
937
  const lines = raw.split(/\r?\n/);
740
938
  const blocks = [];
741
939
  let current = null;
742
940
  const finalize = () => {
743
- if (current !== null && current.version.length > 0 && current.names.size > 0) {
941
+ if (current === null) {
942
+ return;
943
+ }
944
+ if (current.version.length > 0 && current.names.size > 0) {
744
945
  blocks.push(current);
946
+ } else if (current.unreadable) {
947
+ recordUnreadable(report, current.key);
745
948
  }
746
949
  current = null;
747
950
  };
@@ -758,13 +961,13 @@ function parseBlocks(raw) {
758
961
  }
759
962
  const keyLine = trimmed.slice(0, -1);
760
963
  const names = /* @__PURE__ */ new Set();
964
+ let unreadable = false;
761
965
  for (const spec of splitDescriptors(keyLine)) {
762
- const name = extractName(spec);
763
- if (name !== null) {
764
- names.add(name);
765
- }
966
+ const classified = classifyDescriptor(spec);
967
+ if (classified.name !== void 0) names.add(classified.name);
968
+ else if (!classified.excluded) unreadable = true;
766
969
  }
767
- current = { names, version: "" };
970
+ current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
768
971
  continue;
769
972
  }
770
973
  if (current === null) {
@@ -819,20 +1022,37 @@ function splitDescriptors(keyLine) {
819
1022
  }
820
1023
  return parts;
821
1024
  }
822
- function extractName(rawSpec) {
1025
+ var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
1026
+ function classifyDescriptor(rawSpec) {
823
1027
  let s = rawSpec.trim();
824
1028
  if (s.length === 0) {
825
- return null;
1029
+ return { excluded: true };
826
1030
  }
827
1031
  if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
828
1032
  s = s.slice(1, -1);
829
1033
  }
830
- const atIdx = s.lastIndexOf("@");
1034
+ if (NON_PACKAGE_KEYS.has(s)) {
1035
+ return { excluded: true };
1036
+ }
1037
+ const atIdx = s.indexOf("@", 1);
831
1038
  if (atIdx <= 0) {
832
- return null;
1039
+ return {};
833
1040
  }
834
1041
  const name = s.slice(0, atIdx);
835
- return name.length > 0 ? name : null;
1042
+ const range = s.slice(atIdx + 1);
1043
+ if (name.length === 0) {
1044
+ return {};
1045
+ }
1046
+ if (NON_REGISTRY_PROTOCOLS.test(range)) {
1047
+ return { excluded: true };
1048
+ }
1049
+ const alias = /^npm:(.+)$/i.exec(range);
1050
+ if (alias !== null) {
1051
+ const target = alias[1] ?? "";
1052
+ const targetAt = target.indexOf("@", 1);
1053
+ return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
1054
+ }
1055
+ return { name };
836
1056
  }
837
1057
  function parseVersionField(content) {
838
1058
  if (!content.startsWith("version")) {
@@ -857,7 +1077,7 @@ async function readDirectDepNames(cwd) {
857
1077
  const names = /* @__PURE__ */ new Set();
858
1078
  let raw;
859
1079
  try {
860
- raw = await readFile6(path5.join(cwd, "package.json"), "utf8");
1080
+ raw = await readFile7(path5.join(cwd, "package.json"), "utf8");
861
1081
  } catch {
862
1082
  return names;
863
1083
  }
@@ -886,7 +1106,10 @@ async function readDirectDepNames(cwd) {
886
1106
  // src/parsers/index.ts
887
1107
  var LOCKFILE_CANDIDATES = [
888
1108
  { filename: "package-lock.json", strategy: "npm-lockfile" },
889
- { filename: "bun.lock", strategy: "node-modules-walk" },
1109
+ // Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
1110
+ // top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
1111
+ // for new workspaces.
1112
+ { filename: "bun.lock", strategy: "bun-lockfile" },
890
1113
  { filename: "bun.lockb", strategy: "node-modules-walk" },
891
1114
  { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
892
1115
  { filename: "yarn.lock", strategy: "yarn-lockfile" }
@@ -921,33 +1144,57 @@ async function scanLockfile(cwd) {
921
1144
  const declared = await readDeclaredDependencyNames(cwd);
922
1145
  const warnings = [];
923
1146
  let firstParsed = null;
924
- let walkTried = false;
1147
+ let walked = null;
1148
+ let walkWasCandidate = false;
1149
+ const parsedSources = [];
925
1150
  for (const candidate of candidates) {
926
1151
  let packages;
1152
+ const report = newReport();
927
1153
  try {
928
- packages = await runStrategy(candidate, cwd);
1154
+ packages = await runStrategy(candidate, cwd, report);
929
1155
  } catch {
930
1156
  continue;
931
1157
  }
932
- walkTried ||= candidate.strategy === "node-modules-walk";
1158
+ const unreadable = unreadableWarning(candidate.filename, report);
1159
+ if (unreadable !== null) warnings.push(unreadable);
1160
+ if (candidate.strategy === "node-modules-walk") {
1161
+ walked = packages;
1162
+ walkWasCandidate = true;
1163
+ }
933
1164
  firstParsed ??= { packages, filename: candidate.filename };
934
- const missing = missingDependencies(declared, packages);
935
- if (missing.length === 0) {
936
- return manifestWith(packages, warnings);
1165
+ parsedSources.push({ packages, filename: candidate.filename });
1166
+ }
1167
+ for (const source of parsedSources) {
1168
+ const missing = missingDependencies(declared, source.packages);
1169
+ if (missing.length > 0) {
1170
+ warnings.push(staleWarning(source.filename, missing));
1171
+ continue;
1172
+ }
1173
+ const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
1174
+ (other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
1175
+ );
1176
+ if (conflicts.length === 0) {
1177
+ return manifestWith(source.packages, warnings);
937
1178
  }
938
- warnings.push(staleWarning(candidate.filename, missing));
1179
+ warnings.push(conflictWarning(source.filename, conflicts));
1180
+ break;
939
1181
  }
940
- if (!walkTried) {
1182
+ if (walked === null) {
941
1183
  try {
942
- const packages = await walkNodeModules(cwd);
943
- const missing = missingDependencies(declared, packages);
944
- if (missing.length > 0) {
945
- warnings.push(staleWarning("node_modules/", missing));
946
- }
947
- warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
948
- return manifestWith(packages, warnings);
1184
+ walked = await walkNodeModules(cwd);
949
1185
  } catch {
1186
+ walked = null;
1187
+ }
1188
+ }
1189
+ if (walked !== null) {
1190
+ const missing = missingDependencies(declared, walked);
1191
+ if (missing.length > 0) {
1192
+ warnings.push(staleWarning("node_modules/", missing));
950
1193
  }
1194
+ warnings.push(
1195
+ walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
1196
+ );
1197
+ return manifestWith(walked, warnings);
951
1198
  }
952
1199
  if (firstParsed === null) {
953
1200
  const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
@@ -964,6 +1211,15 @@ async function scanLockfile(cwd) {
964
1211
  function manifestWith(packages, warnings) {
965
1212
  return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
966
1213
  }
1214
+ function otherCompleteSources(all, current, declared) {
1215
+ return all.filter(
1216
+ (source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
1217
+ );
1218
+ }
1219
+ function conflictWarning(filename, conflicts) {
1220
+ const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
1221
+ return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
1222
+ }
967
1223
  function staleWarning(source, missing) {
968
1224
  const sample = missing.slice(0, 3).join(", ");
969
1225
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
@@ -978,14 +1234,18 @@ async function presentLockfiles(cwd) {
978
1234
  );
979
1235
  return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
980
1236
  }
981
- async function runStrategy(detected, cwd) {
1237
+ async function runStrategy(detected, cwd, report) {
982
1238
  switch (detected.strategy) {
983
1239
  case "npm-lockfile":
984
1240
  return parseNpmLockfile(detected.filePath);
1241
+ case "bun-lockfile":
1242
+ return parseBunLockfile(detected.filePath, report);
1243
+ // The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
1244
+ // many it dropped, because a manifest short a package reads exactly like a project without it.
985
1245
  case "pnpm-lockfile":
986
- return parsePnpmLockfile(detected.filePath);
1246
+ return parsePnpmLockfile(detected.filePath, report);
987
1247
  case "yarn-lockfile":
988
- return parseYarnLockfile(detected.filePath);
1248
+ return parseYarnLockfile(detected.filePath, report);
989
1249
  case "node-modules-walk":
990
1250
  return walkNodeModules(cwd);
991
1251
  }
@@ -1000,21 +1260,33 @@ async function exists(filePath) {
1000
1260
  }
1001
1261
 
1002
1262
  // src/normalize.ts
1003
- function buildWirePayload(manifest) {
1263
+ function buildWirePayload(manifest, options = {}) {
1264
+ const withPaths = options.installPaths === true;
1004
1265
  const seen = /* @__PURE__ */ new Map();
1005
1266
  const wirePackages = [];
1267
+ const byIdentity = /* @__PURE__ */ new Map();
1268
+ let entriesWithoutPath = 0;
1006
1269
  for (const entry of manifest.packages) {
1270
+ const identity = `${entry.name}@${entry.version}`;
1271
+ const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
1272
+ if (location === void 0) entriesWithoutPath++;
1273
+ const existing = byIdentity.get(identity);
1274
+ if (existing) {
1275
+ if (location !== void 0) addPath(existing, location);
1276
+ continue;
1277
+ }
1007
1278
  const versions = seen.get(entry.name);
1008
1279
  if (versions) {
1009
- if (versions.has(entry.version)) {
1010
- continue;
1011
- }
1012
1280
  versions.add(entry.version);
1013
1281
  } else {
1014
1282
  seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
1015
1283
  }
1016
- wirePackages.push({ name: entry.name, version: entry.version });
1284
+ const wire = { name: entry.name, version: entry.version };
1285
+ if (location !== void 0) addPath(wire, location);
1286
+ byIdentity.set(identity, wire);
1287
+ wirePackages.push(wire);
1017
1288
  }
1289
+ for (const wire of wirePackages) wire.paths?.sort();
1018
1290
  wirePackages.sort((a, b) => {
1019
1291
  if (a.name === b.name) {
1020
1292
  return compareVersions(a.version, b.version);
@@ -1028,7 +1300,18 @@ function buildWirePayload(manifest) {
1028
1300
  }
1029
1301
  }
1030
1302
  return {
1031
- payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
1303
+ payload: {
1304
+ ecosystem: manifest.ecosystem,
1305
+ packages: wirePackages,
1306
+ // Every entry had a location, so a missing path anywhere means the package is not installed there.
1307
+ // One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
1308
+ // no way to tell which entry was the incomplete one.
1309
+ //
1310
+ // No separate check for "locations were not requested": with them off every entry counts as without
1311
+ // one, so this is already false — which is the default, and the safe direction. An explicit
1312
+ // `withPaths &&` here was a guard no test could distinguish from its absence.
1313
+ installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
1314
+ },
1032
1315
  stats: {
1033
1316
  uniqueNames: seen.size,
1034
1317
  duplicateNames,
@@ -1036,6 +1319,10 @@ function buildWirePayload(manifest) {
1036
1319
  }
1037
1320
  };
1038
1321
  }
1322
+ function addPath(wire, installPath) {
1323
+ if (wire.paths === void 0) wire.paths = [installPath];
1324
+ else if (!wire.paths.includes(installPath)) wire.paths.push(installPath);
1325
+ }
1039
1326
  function compareVersions(a, b) {
1040
1327
  if (a === b) {
1041
1328
  return 0;
@@ -1114,4 +1401,4 @@ async function reportManifest(cwd) {
1114
1401
  export {
1115
1402
  reportManifest
1116
1403
  };
1117
- //# sourceMappingURL=refresh-manifest-VRBE6RH6.js.map
1404
+ //# sourceMappingURL=refresh-manifest-KNZQBC4V.js.map