@patchstack/connect 0.3.29 → 0.3.30

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 +37 -21
  2. package/README.md +24 -10
  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 +1062 -189
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.cjs +419 -104
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +28 -9
  10. package/dist/index.d.ts +28 -9
  11. package/dist/index.js +412 -97
  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 +1039 -235
  30. package/dist/protect.cjs.map +1 -1
  31. package/dist/protect.d.ts +39 -11
  32. package/dist/protect.edge.js +663 -132
  33. package/dist/protect.edge.js.map +4 -4
  34. package/dist/protect.js +665 -134
  35. package/dist/protect.js.map +1 -1
  36. package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
  37. package/dist/refresh-manifest-ZSP76JWQ.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,122 @@ 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));
362
532
  }
363
- async function walk(dir, acc, depth) {
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);
540
+ }
541
+ var CONCURRENCY = 32;
542
+ async function walk(dir, depth, seen) {
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);
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);
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);
572
+ });
573
+ return perEntry.flat();
574
+ }
575
+ async function packageAndNested(pkgDir, resolved, depth, seen) {
576
+ const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
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;
401
590
  }
402
- async function readPackage(pkgDir, depth, acc) {
591
+ async function readPackage(pkgDir, depth) {
403
592
  let raw;
404
593
  try {
405
- raw = await readFile4(path4.join(pkgDir, "package.json"), "utf8");
594
+ raw = await readFile5(path4.join(pkgDir, "package.json"), "utf8");
406
595
  } catch {
407
- return;
596
+ return null;
408
597
  }
409
598
  let parsed;
410
599
  try {
411
600
  parsed = JSON.parse(raw);
412
601
  } catch {
413
- return;
602
+ return null;
414
603
  }
415
604
  if (typeof parsed.name !== "string" || parsed.name.length === 0) {
416
- return;
605
+ return null;
417
606
  }
418
607
  if (typeof parsed.version !== "string" || parsed.version.length === 0) {
419
- return;
608
+ return null;
420
609
  }
421
- acc.push({
422
- name: parsed.name,
423
- version: parsed.version,
424
- direct: depth === 0
425
- });
610
+ return { name: parsed.name, version: parsed.version, direct: depth === 0 };
426
611
  }
427
- async function walkNested(pkgDir, acc, depth) {
612
+ async function walkNested(pkgDir, depth, seen) {
428
613
  const nested = path4.join(pkgDir, "node_modules");
429
- if (!await isPlainDirectory(nested)) {
430
- return;
614
+ if (await claim(nested, seen, depth + 1) === null) {
615
+ return [];
431
616
  }
432
- await walk(nested, acc, depth + 1);
617
+ return walk(nested, depth + 1, seen);
433
618
  }
434
- async function isPlainDirectory(dir) {
619
+ async function claim(dir, seen, depth) {
620
+ let resolved;
435
621
  try {
436
622
  const info = await lstat(dir);
437
- return info.isDirectory() && !info.isSymbolicLink();
623
+ if (!info.isDirectory() && !info.isSymbolicLink()) return null;
624
+ resolved = await realpath(dir);
625
+ const target = await stat(resolved);
626
+ if (!target.isDirectory()) return null;
438
627
  } catch {
439
- return false;
628
+ return null;
440
629
  }
630
+ const claimed = seen.get(resolved);
631
+ if (claimed !== void 0 && claimed <= depth) return null;
632
+ seen.set(resolved, depth);
633
+ return resolved;
441
634
  }
442
635
 
443
636
  // src/parsers/pnpm.ts
444
- import { readFile as readFile5 } from "fs/promises";
445
- async function parsePnpmLockfile(lockfilePath) {
637
+ import { readFile as readFile6 } from "fs/promises";
638
+ async function parsePnpmLockfile(lockfilePath, report) {
446
639
  let raw;
447
640
  try {
448
- raw = await readFile5(lockfilePath, "utf8");
641
+ raw = await readFile6(lockfilePath, "utf8");
449
642
  } catch (cause) {
450
643
  throw new PatchstackError(
451
644
  `Could not read lockfile at ${lockfilePath}`,
@@ -466,6 +659,7 @@ async function parsePnpmLockfile(lockfilePath) {
466
659
  for (const key of packageKeys) {
467
660
  const parsed = parsePackageKey(key);
468
661
  if (parsed === null) {
662
+ recordUnreadable(report, key);
469
663
  continue;
470
664
  }
471
665
  entries.push({
@@ -693,12 +887,12 @@ function extractLeafName(trimmed) {
693
887
  }
694
888
 
695
889
  // src/parsers/yarn.ts
696
- import { readFile as readFile6 } from "fs/promises";
890
+ import { readFile as readFile7 } from "fs/promises";
697
891
  import path5 from "path";
698
- async function parseYarnLockfile(lockfilePath) {
892
+ async function parseYarnLockfile(lockfilePath, report) {
699
893
  let raw;
700
894
  try {
701
- raw = await readFile6(lockfilePath, "utf8");
895
+ raw = await readFile7(lockfilePath, "utf8");
702
896
  } catch (cause) {
703
897
  throw new PatchstackError(
704
898
  `Could not read lockfile at ${lockfilePath}`,
@@ -706,7 +900,7 @@ async function parseYarnLockfile(lockfilePath) {
706
900
  cause
707
901
  );
708
902
  }
709
- const blocks = parseBlocks(raw);
903
+ const blocks = parseBlocks(raw, report);
710
904
  if (blocks.length === 0) {
711
905
  throw new PatchstackError(
712
906
  `Lockfile at ${lockfilePath} contains no package entries`,
@@ -735,13 +929,19 @@ async function parseYarnLockfile(lockfilePath) {
735
929
  }
736
930
  return entries;
737
931
  }
738
- function parseBlocks(raw) {
932
+ var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
933
+ function parseBlocks(raw, report) {
739
934
  const lines = raw.split(/\r?\n/);
740
935
  const blocks = [];
741
936
  let current = null;
742
937
  const finalize = () => {
743
- if (current !== null && current.version.length > 0 && current.names.size > 0) {
938
+ if (current === null) {
939
+ return;
940
+ }
941
+ if (current.version.length > 0 && current.names.size > 0) {
744
942
  blocks.push(current);
943
+ } else if (current.unreadable) {
944
+ recordUnreadable(report, current.key);
745
945
  }
746
946
  current = null;
747
947
  };
@@ -758,13 +958,13 @@ function parseBlocks(raw) {
758
958
  }
759
959
  const keyLine = trimmed.slice(0, -1);
760
960
  const names = /* @__PURE__ */ new Set();
961
+ let unreadable = false;
761
962
  for (const spec of splitDescriptors(keyLine)) {
762
- const name = extractName(spec);
763
- if (name !== null) {
764
- names.add(name);
765
- }
963
+ const classified = classifyDescriptor(spec);
964
+ if (classified.name !== void 0) names.add(classified.name);
965
+ else if (!classified.excluded) unreadable = true;
766
966
  }
767
- current = { names, version: "" };
967
+ current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
768
968
  continue;
769
969
  }
770
970
  if (current === null) {
@@ -819,20 +1019,37 @@ function splitDescriptors(keyLine) {
819
1019
  }
820
1020
  return parts;
821
1021
  }
822
- function extractName(rawSpec) {
1022
+ var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
1023
+ function classifyDescriptor(rawSpec) {
823
1024
  let s = rawSpec.trim();
824
1025
  if (s.length === 0) {
825
- return null;
1026
+ return { excluded: true };
826
1027
  }
827
1028
  if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
828
1029
  s = s.slice(1, -1);
829
1030
  }
830
- const atIdx = s.lastIndexOf("@");
1031
+ if (NON_PACKAGE_KEYS.has(s)) {
1032
+ return { excluded: true };
1033
+ }
1034
+ const atIdx = s.indexOf("@", 1);
831
1035
  if (atIdx <= 0) {
832
- return null;
1036
+ return {};
833
1037
  }
834
1038
  const name = s.slice(0, atIdx);
835
- return name.length > 0 ? name : null;
1039
+ const range = s.slice(atIdx + 1);
1040
+ if (name.length === 0) {
1041
+ return {};
1042
+ }
1043
+ if (NON_REGISTRY_PROTOCOLS.test(range)) {
1044
+ return { excluded: true };
1045
+ }
1046
+ const alias = /^npm:(.+)$/i.exec(range);
1047
+ if (alias !== null) {
1048
+ const target = alias[1] ?? "";
1049
+ const targetAt = target.indexOf("@", 1);
1050
+ return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
1051
+ }
1052
+ return { name };
836
1053
  }
837
1054
  function parseVersionField(content) {
838
1055
  if (!content.startsWith("version")) {
@@ -857,7 +1074,7 @@ async function readDirectDepNames(cwd) {
857
1074
  const names = /* @__PURE__ */ new Set();
858
1075
  let raw;
859
1076
  try {
860
- raw = await readFile6(path5.join(cwd, "package.json"), "utf8");
1077
+ raw = await readFile7(path5.join(cwd, "package.json"), "utf8");
861
1078
  } catch {
862
1079
  return names;
863
1080
  }
@@ -886,7 +1103,10 @@ async function readDirectDepNames(cwd) {
886
1103
  // src/parsers/index.ts
887
1104
  var LOCKFILE_CANDIDATES = [
888
1105
  { filename: "package-lock.json", strategy: "npm-lockfile" },
889
- { filename: "bun.lock", strategy: "node-modules-walk" },
1106
+ // Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
1107
+ // top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
1108
+ // for new workspaces.
1109
+ { filename: "bun.lock", strategy: "bun-lockfile" },
890
1110
  { filename: "bun.lockb", strategy: "node-modules-walk" },
891
1111
  { filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
892
1112
  { filename: "yarn.lock", strategy: "yarn-lockfile" }
@@ -921,33 +1141,57 @@ async function scanLockfile(cwd) {
921
1141
  const declared = await readDeclaredDependencyNames(cwd);
922
1142
  const warnings = [];
923
1143
  let firstParsed = null;
924
- let walkTried = false;
1144
+ let walked = null;
1145
+ let walkWasCandidate = false;
1146
+ const parsedSources = [];
925
1147
  for (const candidate of candidates) {
926
1148
  let packages;
1149
+ const report = newReport();
927
1150
  try {
928
- packages = await runStrategy(candidate, cwd);
1151
+ packages = await runStrategy(candidate, cwd, report);
929
1152
  } catch {
930
1153
  continue;
931
1154
  }
932
- walkTried ||= candidate.strategy === "node-modules-walk";
1155
+ const unreadable = unreadableWarning(candidate.filename, report);
1156
+ if (unreadable !== null) warnings.push(unreadable);
1157
+ if (candidate.strategy === "node-modules-walk") {
1158
+ walked = packages;
1159
+ walkWasCandidate = true;
1160
+ }
933
1161
  firstParsed ??= { packages, filename: candidate.filename };
934
- const missing = missingDependencies(declared, packages);
935
- if (missing.length === 0) {
936
- return manifestWith(packages, warnings);
1162
+ parsedSources.push({ packages, filename: candidate.filename });
1163
+ }
1164
+ for (const source of parsedSources) {
1165
+ const missing = missingDependencies(declared, source.packages);
1166
+ if (missing.length > 0) {
1167
+ warnings.push(staleWarning(source.filename, missing));
1168
+ continue;
1169
+ }
1170
+ const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
1171
+ (other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
1172
+ );
1173
+ if (conflicts.length === 0) {
1174
+ return manifestWith(source.packages, warnings);
937
1175
  }
938
- warnings.push(staleWarning(candidate.filename, missing));
1176
+ warnings.push(conflictWarning(source.filename, conflicts));
1177
+ break;
939
1178
  }
940
- if (!walkTried) {
1179
+ if (walked === null) {
941
1180
  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);
1181
+ walked = await walkNodeModules(cwd);
949
1182
  } catch {
1183
+ walked = null;
1184
+ }
1185
+ }
1186
+ if (walked !== null) {
1187
+ const missing = missingDependencies(declared, walked);
1188
+ if (missing.length > 0) {
1189
+ warnings.push(staleWarning("node_modules/", missing));
950
1190
  }
1191
+ warnings.push(
1192
+ walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
1193
+ );
1194
+ return manifestWith(walked, warnings);
951
1195
  }
952
1196
  if (firstParsed === null) {
953
1197
  const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
@@ -964,6 +1208,15 @@ async function scanLockfile(cwd) {
964
1208
  function manifestWith(packages, warnings) {
965
1209
  return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
966
1210
  }
1211
+ function otherCompleteSources(all, current, declared) {
1212
+ return all.filter(
1213
+ (source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
1214
+ );
1215
+ }
1216
+ function conflictWarning(filename, conflicts) {
1217
+ const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
1218
+ 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.`;
1219
+ }
967
1220
  function staleWarning(source, missing) {
968
1221
  const sample = missing.slice(0, 3).join(", ");
969
1222
  const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
@@ -978,14 +1231,18 @@ async function presentLockfiles(cwd) {
978
1231
  );
979
1232
  return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
980
1233
  }
981
- async function runStrategy(detected, cwd) {
1234
+ async function runStrategy(detected, cwd, report) {
982
1235
  switch (detected.strategy) {
983
1236
  case "npm-lockfile":
984
1237
  return parseNpmLockfile(detected.filePath);
1238
+ case "bun-lockfile":
1239
+ return parseBunLockfile(detected.filePath, report);
1240
+ // The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
1241
+ // many it dropped, because a manifest short a package reads exactly like a project without it.
985
1242
  case "pnpm-lockfile":
986
- return parsePnpmLockfile(detected.filePath);
1243
+ return parsePnpmLockfile(detected.filePath, report);
987
1244
  case "yarn-lockfile":
988
- return parseYarnLockfile(detected.filePath);
1245
+ return parseYarnLockfile(detected.filePath, report);
989
1246
  case "node-modules-walk":
990
1247
  return walkNodeModules(cwd);
991
1248
  }
@@ -1114,4 +1371,4 @@ async function reportManifest(cwd) {
1114
1371
  export {
1115
1372
  reportManifest
1116
1373
  };
1117
- //# sourceMappingURL=refresh-manifest-VRBE6RH6.js.map
1374
+ //# sourceMappingURL=refresh-manifest-ZSP76JWQ.js.map