knarr 0.0.2 → 0.1.0

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 (111) hide show
  1. package/README.md +1 -0
  2. package/dist/add-4Z3V5722.mjs +3 -0
  3. package/dist/bell-SIN4ERLS.mjs +2 -0
  4. package/dist/{check-YVEJEI2G.mjs → check-7CJHAF6Y.mjs} +1 -1
  5. package/dist/{chokidar-LVDD2IK4.mjs → chokidar-2G63V464.mjs} +1 -1
  6. package/dist/chunk-3SXN6MQL.mjs +4 -0
  7. package/dist/chunk-7GCKCCHU.mjs +3 -0
  8. package/dist/chunk-7ROQJISX.mjs +15 -0
  9. package/dist/chunk-AA43FODB.mjs +3 -0
  10. package/dist/chunk-ABG6UD3X.mjs +22 -0
  11. package/dist/chunk-BH2WXCLS.mjs +4 -0
  12. package/dist/chunk-COI7GWH3.mjs +4 -0
  13. package/dist/chunk-E3HZCU3S.mjs +5 -0
  14. package/dist/chunk-FSVETLZK.mjs +4 -0
  15. package/dist/{chunk-3KNUBUPH.mjs → chunk-G7HFTV5Y.mjs} +1 -1
  16. package/dist/chunk-GAWQLCRB.mjs +3 -0
  17. package/dist/{chunk-MBKCCWSD.mjs → chunk-HVN6HBWZ.mjs} +1 -1
  18. package/dist/{chunk-SYADAYF4.mjs → chunk-ICU4V73X.mjs} +1 -1
  19. package/dist/{chunk-XQPVRRTN.mjs → chunk-IP5ROSRE.mjs} +1 -1
  20. package/dist/chunk-J6HZHQVL.mjs +3 -0
  21. package/dist/chunk-L7O2RSTN.mjs +3 -0
  22. package/dist/chunk-M3TRMEJV.mjs +3 -0
  23. package/dist/chunk-MN4DV2NO.mjs +3 -0
  24. package/dist/chunk-N3B2JO4H.mjs +13 -0
  25. package/dist/{chunk-7HVPEBK5.mjs → chunk-NGPAGRK4.mjs} +1 -1
  26. package/dist/{chunk-B3DZ5HVQ.mjs → chunk-OQDZJP55.mjs} +1 -1
  27. package/dist/chunk-QEH5VHRO.mjs +13 -0
  28. package/dist/chunk-QFHYSC3K.mjs +3 -0
  29. package/dist/{chunk-CTJF2EWO.mjs → chunk-QHNHLR6T.mjs} +2 -2
  30. package/dist/chunk-RJSBXX2Q.mjs +3 -0
  31. package/dist/{chunk-NBSJGM2X.mjs → chunk-SPHWZS5P.mjs} +1 -1
  32. package/dist/chunk-U6O35NZ7.mjs +3 -0
  33. package/dist/chunk-UCSYKFEZ.mjs +3 -0
  34. package/dist/chunk-W2MMKBWU.mjs +3 -0
  35. package/dist/chunk-WBTSRLB6.mjs +7 -0
  36. package/dist/chunk-WN3DOKIM.mjs +3 -0
  37. package/dist/{chunk-TEFMLGCB.mjs → chunk-XDWZZBCM.mjs} +1 -1
  38. package/dist/{chunk-2GDRDQA5.mjs → chunk-Z5K6DFA6.mjs} +1 -1
  39. package/dist/clean-EJIYYQZM.mjs +3 -0
  40. package/dist/cli.mjs +4 -4
  41. package/dist/dev-PUNHH4MS.mjs +3 -0
  42. package/dist/doctor-ZK4OPSD4.mjs +6 -0
  43. package/dist/explain-BDTONX7H.mjs +5 -0
  44. package/dist/fs-NU36NNEG.mjs +2 -0
  45. package/dist/history-MLJHRV3Q.mjs +2 -0
  46. package/dist/index.d.ts +79 -18
  47. package/dist/index.mjs +1524 -443
  48. package/dist/init-X4F3XKVC.mjs +16 -0
  49. package/dist/{list-GLSA6I67.mjs → list-4PTHMIQ4.mjs} +2 -2
  50. package/dist/migrate-VHDLW6I7.mjs +8 -0
  51. package/dist/nextjs-config-T5D6LOJE.mjs +2 -0
  52. package/dist/preflight-IXICTR6G.mjs +2 -0
  53. package/dist/{publish-TAWTHPSD.mjs → publish-UGDDG6OT.mjs} +2 -2
  54. package/dist/push-V575ASM4.mjs +3 -0
  55. package/dist/remove-EPVDMWCS.mjs +2 -0
  56. package/dist/reset-5M6AU4FU.mjs +3 -0
  57. package/dist/restore-QPISOGEK.mjs +12 -0
  58. package/dist/rollback-CTGK7SXG.mjs +3 -0
  59. package/dist/{status-FA6UEHEF.mjs → status-3ZXM3CRH.mjs} +2 -2
  60. package/dist/{tailwind-source-RIWWXW2Y.mjs → tailwind-source-HCTY2XDU.mjs} +2 -2
  61. package/dist/topo-sort-ZNGR6D7M.mjs +2 -0
  62. package/dist/{tracker-JJEYXX45.mjs → tracker-QAKEXYUJ.mjs} +1 -1
  63. package/dist/update-RSPDDAJK.mjs +12 -0
  64. package/dist/use-LZ2GIXYZ.mjs +3 -0
  65. package/dist/vite-config-XOGAKYGQ.mjs +2 -0
  66. package/dist/watch-orchestrator-N6HZUDS3.mjs +3 -0
  67. package/dist/watcher-KOLIXRUG.mjs +3 -0
  68. package/dist/workspace-T6PNO4L3.mjs +2 -0
  69. package/dist/{xxhash-wasm-DTW44IIQ.mjs → xxhash-wasm-I6Q3T2SL.mjs} +1 -1
  70. package/package.json +10 -4
  71. package/dist/add-S4U56IVA.mjs +0 -3
  72. package/dist/bell-YD6IWNXO.mjs +0 -2
  73. package/dist/chunk-2VCW5RWI.mjs +0 -3
  74. package/dist/chunk-37AAX47E.mjs +0 -19
  75. package/dist/chunk-5BZD55UB.mjs +0 -3
  76. package/dist/chunk-6QHABEBL.mjs +0 -14
  77. package/dist/chunk-7JG555TZ.mjs +0 -3
  78. package/dist/chunk-7SDPRKFT.mjs +0 -13
  79. package/dist/chunk-BS4VKVYH.mjs +0 -3
  80. package/dist/chunk-EE2UYGFD.mjs +0 -4
  81. package/dist/chunk-GO6F6AGH.mjs +0 -3
  82. package/dist/chunk-GQYG5FCW.mjs +0 -5
  83. package/dist/chunk-KOHUNKHP.mjs +0 -3
  84. package/dist/chunk-LXGALE74.mjs +0 -13
  85. package/dist/chunk-OPLSUHCD.mjs +0 -4
  86. package/dist/chunk-QGLOGD5G.mjs +0 -3
  87. package/dist/chunk-SFLWVTJC.mjs +0 -3
  88. package/dist/chunk-SN4TOUQW.mjs +0 -7
  89. package/dist/chunk-UBGMLVMB.mjs +0 -3
  90. package/dist/chunk-XKO24LUM.mjs +0 -3
  91. package/dist/chunk-ZJEEAMB3.mjs +0 -3
  92. package/dist/clean-ZD5GPGXQ.mjs +0 -3
  93. package/dist/dev-TMWMIT7W.mjs +0 -3
  94. package/dist/doctor-AXP7GVBM.mjs +0 -4
  95. package/dist/fs-2NITBGIO.mjs +0 -2
  96. package/dist/history-SKKGT225.mjs +0 -2
  97. package/dist/init-XK4B7XYG.mjs +0 -7
  98. package/dist/migrate-MW4BVLL2.mjs +0 -8
  99. package/dist/preflight-TVJFHRI2.mjs +0 -2
  100. package/dist/push-65ZZFZJY.mjs +0 -3
  101. package/dist/remove-LOBXHBMC.mjs +0 -2
  102. package/dist/reset-EAN5NVGM.mjs +0 -3
  103. package/dist/restore-YHZYT54B.mjs +0 -11
  104. package/dist/rollback-U3LQTWH5.mjs +0 -3
  105. package/dist/topo-sort-WEIVPJKN.mjs +0 -2
  106. package/dist/update-VOUKMOLK.mjs +0 -3
  107. package/dist/use-DUZYEUVU.mjs +0 -3
  108. package/dist/vite-config-UWCLPTOZ.mjs +0 -2
  109. package/dist/watch-orchestrator-I2623SMT.mjs +0 -3
  110. package/dist/watcher-PTPUN2HE.mjs +0 -3
  111. package/dist/workspace-S3TAUSS3.mjs +0 -2
package/dist/index.mjs CHANGED
@@ -1,7 +1,12 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
5
10
  };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
@@ -293,7 +298,7 @@ var init_logger = __esm({
293
298
  // src/utils/hash.ts
294
299
  import { createHash } from "crypto";
295
300
  import { readFile as readFile2, stat } from "fs/promises";
296
- import { relative as relative2 } from "path";
301
+ import { join as join3, relative as relative2 } from "path";
297
302
  import { availableParallelism } from "os";
298
303
  function getXXHash() {
299
304
  if (!_xxhash) {
@@ -304,18 +309,28 @@ function getXXHash() {
304
309
  }
305
310
  return _xxhash;
306
311
  }
307
- async function computeContentHash(files, baseDir) {
308
- const sorted = [...files].sort((a, b) => {
309
- const relA = normalizePath(relative2(baseDir, a));
310
- const relB = normalizePath(relative2(baseDir, b));
311
- return relA.localeCompare(relB);
312
- });
313
- const currentFiles = new Set(sorted);
312
+ async function computeContentHash(files, baseDir, contentOverrides = /* @__PURE__ */ new Map()) {
313
+ const fileByRel = new Map(
314
+ files.map((file) => [normalizePath(relative2(baseDir, file)), file])
315
+ );
316
+ for (const rel of contentOverrides.keys()) {
317
+ if (!fileByRel.has(rel)) fileByRel.set(rel, join3(baseDir, rel));
318
+ }
319
+ const sorted = [...fileByRel.entries()].sort(
320
+ ([relA], [relB]) => relA.localeCompare(relB)
321
+ );
322
+ const currentFiles = new Set(fileByRel.values());
314
323
  let cacheHits = 0;
315
324
  const contents = await Promise.all(
316
325
  sorted.map(
317
- (file) => limit(async () => {
318
- const rel = normalizePath(relative2(baseDir, file));
326
+ ([rel, file]) => limit(async () => {
327
+ const override = contentOverrides.get(rel);
328
+ if (override !== void 0) {
329
+ return {
330
+ rel,
331
+ content: Buffer.isBuffer(override) ? override : Buffer.from(override)
332
+ };
333
+ }
319
334
  const s = await stat(file);
320
335
  const cached = _contentCache.get(file);
321
336
  if (cached && cached.mtimeMs === s.mtimeMs && cached.size === s.size) {
@@ -331,7 +346,7 @@ async function computeContentHash(files, baseDir) {
331
346
  for (const key of _contentCache.keys()) {
332
347
  if (!currentFiles.has(key)) _contentCache.delete(key);
333
348
  }
334
- verbose(`[hash] Computing content hash for ${files.length} files (${cacheHits} cached)`);
349
+ verbose(`[hash] Computing content hash for ${sorted.length} files (${cacheHits} cached)`);
335
350
  const hash = createHash("sha256");
336
351
  const lenBuf = Buffer.alloc(4);
337
352
  for (const { rel, content } of contents) {
@@ -382,13 +397,21 @@ var init_hash = __esm({
382
397
  function recordMutation(mutation) {
383
398
  mutations.push(mutation);
384
399
  }
400
+ function getMutations() {
401
+ return [...mutations];
402
+ }
403
+ function markDryRunJsonReportPrinted() {
404
+ jsonReportPrinted = true;
405
+ }
385
406
  function printDryRunReport() {
386
407
  if (mutations.length === 0) {
387
408
  consola.info("[dry-run] No mutations would be performed");
388
409
  return;
389
410
  }
390
411
  if (isJsonOutput()) {
412
+ if (jsonReportPrinted) return;
391
413
  console.log(JSON.stringify({ dryRun: true, mutations }, null, 2));
414
+ jsonReportPrinted = true;
392
415
  return;
393
416
  }
394
417
  const grouped = /* @__PURE__ */ new Map();
@@ -430,14 +453,16 @@ function printDryRunReport() {
430
453
  }
431
454
  function resetMutations() {
432
455
  mutations.length = 0;
456
+ jsonReportPrinted = false;
433
457
  }
434
- var mutations;
458
+ var mutations, jsonReportPrinted;
435
459
  var init_dry_run = __esm({
436
460
  "src/utils/dry-run.ts"() {
437
461
  "use strict";
438
462
  init_logger();
439
463
  init_console();
440
464
  mutations = [];
465
+ jsonReportPrinted = false;
441
466
  }
442
467
  });
443
468
 
@@ -469,7 +494,7 @@ import {
469
494
  writeFile,
470
495
  constants
471
496
  } from "fs/promises";
472
- import { join as join3, dirname, relative as relative3, parse as parsePath } from "path";
497
+ import { join as join4, dirname, relative as relative3, parse as parsePath } from "path";
473
498
  import { availableParallelism as availableParallelism2 } from "os";
474
499
  function isNodeError(err) {
475
500
  return err instanceof Error && "code" in err;
@@ -508,7 +533,39 @@ async function copyWithCoW(src, dest, options) {
508
533
  }
509
534
  async function collectFiles(dir) {
510
535
  const entries = await readdir(dir, { recursive: true, withFileTypes: true });
511
- return entries.filter((e) => e.isFile()).map((e) => join3(e.parentPath, e.name));
536
+ return entries.filter((e) => e.isFile()).map((e) => join4(e.parentPath, e.name));
537
+ }
538
+ async function removeDestinationConflicts(destDir, relPath) {
539
+ const parts = relPath.split(/[\\/]/).filter(Boolean);
540
+ let current = destDir;
541
+ for (let i = 0; i < parts.length; i++) {
542
+ current = join4(current, parts[i]);
543
+ try {
544
+ const s = await stat2(current);
545
+ const isLeaf = i === parts.length - 1;
546
+ if (s.isDirectory()) {
547
+ if (isLeaf) {
548
+ if (isDryRun()) {
549
+ recordMutation({ type: "remove", path: current });
550
+ } else {
551
+ await rm(current, { recursive: true, force: true });
552
+ }
553
+ }
554
+ continue;
555
+ }
556
+ if (!isLeaf) {
557
+ if (isDryRun()) {
558
+ recordMutation({ type: "remove", path: current });
559
+ return;
560
+ } else {
561
+ await rm(current, { force: true });
562
+ }
563
+ }
564
+ } catch (err) {
565
+ if (isNodeError(err) && err.code === "ENOENT") return;
566
+ throw err;
567
+ }
568
+ }
512
569
  }
513
570
  async function incrementalCopy(srcDir, destDir, options = {}) {
514
571
  const srcFilesPromise = collectFiles(srcDir);
@@ -524,7 +581,7 @@ async function incrementalCopy(srcDir, destDir, options = {}) {
524
581
  srcFiles.map(
525
582
  (srcFile) => ioLimit(async () => {
526
583
  const rel = relative3(srcDir, srcFile);
527
- const destFile = join3(destDir, rel);
584
+ const destFile = join4(destDir, rel);
528
585
  let needsCopy = true;
529
586
  let srcTimes = null;
530
587
  if (options.force) {
@@ -554,7 +611,7 @@ async function incrementalCopy(srcDir, destDir, options = {}) {
554
611
  }
555
612
  }
556
613
  } catch (err) {
557
- if (isNodeError(err) && err.code === "ENOENT") {
614
+ if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
558
615
  verbose(`[copy] ${rel} (new file)`);
559
616
  } else {
560
617
  throw err;
@@ -562,7 +619,9 @@ async function incrementalCopy(srcDir, destDir, options = {}) {
562
619
  }
563
620
  }
564
621
  if (needsCopy) {
622
+ await removeDestinationConflicts(destDir, rel);
565
623
  await copyWithCoW(srcFile, destFile);
624
+ if (isDryRun()) return "copied";
566
625
  if (!srcTimes) {
567
626
  const s = await stat2(srcFile);
568
627
  srcTimes = { atime: s.atime, mtime: s.mtime };
@@ -589,7 +648,12 @@ async function incrementalCopy(srcDir, destDir, options = {}) {
589
648
  if (isDryRun()) {
590
649
  recordMutation({ type: "remove", path: destFile });
591
650
  } else {
592
- await rm(destFile);
651
+ await rm(destFile, { force: true }).catch((err) => {
652
+ if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR" || err.code === "EISDIR" || err.code === "ERR_FS_EISDIR")) {
653
+ return;
654
+ }
655
+ throw err;
656
+ });
593
657
  }
594
658
  })
595
659
  )
@@ -691,17 +755,17 @@ var init_fs = __esm({
691
755
 
692
756
  // src/utils/pack-list.ts
693
757
  import { readFile as readFile3, readdir as readdir2, stat as stat3 } from "fs/promises";
694
- import { join as join4, relative as relative4, resolve as resolve2, sep } from "path";
758
+ import { join as join5, relative as relative4, resolve as resolve2, sep } from "path";
695
759
  import picomatch from "picomatch";
696
760
  async function resolvePackFiles(packageDir, pkg) {
697
761
  const files = [];
698
762
  const absDir = resolve2(packageDir);
699
- files.push(join4(absDir, "package.json"));
763
+ files.push(join5(absDir, "package.json"));
700
764
  const allFiles = await collectAllFiles(absDir, absDir);
701
765
  const allRelPaths = allFiles.map((f) => normalizePath(relative4(absDir, f)));
702
766
  if (pkg.files && pkg.files.length > 0) {
703
767
  for (const pattern of pkg.files) {
704
- const target = join4(absDir, pattern);
768
+ const target = join5(absDir, pattern);
705
769
  const resolved = resolve2(target);
706
770
  if (!resolved.startsWith(absDir + sep) && resolved !== absDir) {
707
771
  consola.warn(`files pattern "${pattern}" escapes package directory, skipping`);
@@ -754,7 +818,7 @@ async function resolvePackFiles(packageDir, pkg) {
754
818
  const fileSet = new Set(files);
755
819
  const allFileSet = new Set(allFiles);
756
820
  for (const name of ["README.md", "README", "LICENSE", "LICENCE", "CHANGELOG.md"]) {
757
- const p = join4(absDir, name);
821
+ const p = join5(absDir, name);
758
822
  if (fileSet.has(p)) continue;
759
823
  if (!allFileSet.has(p)) continue;
760
824
  files.push(p);
@@ -778,28 +842,38 @@ function shouldIgnore(relPath, matchers) {
778
842
  return false;
779
843
  }
780
844
  async function loadNpmIgnore(dir) {
781
- const matchers = { literals: /* @__PURE__ */ new Set(), patterns: [], negations: [] };
845
+ const npmIgnore = await readIgnoreFile(dir, ".npmignore");
846
+ if (npmIgnore !== null) return parseIgnoreFile(npmIgnore);
847
+ const gitIgnore = await readIgnoreFile(dir, ".gitignore");
848
+ return gitIgnore === null ? createIgnoreMatchers() : parseIgnoreFile(gitIgnore);
849
+ }
850
+ async function readIgnoreFile(dir, filename) {
782
851
  try {
783
- const content = await readFile3(join4(dir, ".npmignore"), "utf-8");
784
- for (const line of content.split("\n")) {
785
- const trimmed = line.trim();
786
- if (!trimmed || trimmed.startsWith("#")) continue;
787
- if (trimmed.startsWith("!")) {
788
- const pat = trimmed.slice(1);
789
- if (hasGlobChars(pat)) {
790
- matchers.negations.push(picomatch(pat, { dot: true }));
791
- } else {
792
- matchers.negations.push(picomatch(pat, { dot: true }));
793
- }
794
- } else if (hasGlobChars(trimmed)) {
795
- matchers.patterns.push(picomatch(trimmed, { dot: true }));
852
+ return await readFile3(join5(dir, filename), "utf-8");
853
+ } catch (err) {
854
+ if (isNodeError(err) && err.code === "ENOENT") return null;
855
+ throw err;
856
+ }
857
+ }
858
+ function createIgnoreMatchers() {
859
+ return { literals: /* @__PURE__ */ new Set(), patterns: [], negations: [] };
860
+ }
861
+ function parseIgnoreFile(content) {
862
+ const matchers = { literals: /* @__PURE__ */ new Set(), patterns: [], negations: [] };
863
+ for (const line of content.split("\n")) {
864
+ const trimmed = line.trim();
865
+ if (!trimmed || trimmed.startsWith("#")) continue;
866
+ if (trimmed.startsWith("!")) {
867
+ const pat = trimmed.slice(1);
868
+ if (hasGlobChars(pat)) {
869
+ matchers.negations.push(picomatch(pat, { dot: true }));
796
870
  } else {
797
- matchers.literals.add(trimmed.replace(/\/$/, ""));
871
+ matchers.negations.push(picomatch(pat, { dot: true }));
798
872
  }
799
- }
800
- } catch (err) {
801
- if (isNodeError(err) && err.code !== "ENOENT") {
802
- throw err;
873
+ } else if (hasGlobChars(trimmed)) {
874
+ matchers.patterns.push(picomatch(trimmed, { dot: true }));
875
+ } else {
876
+ matchers.literals.add(trimmed.replace(/\/$/, ""));
803
877
  }
804
878
  }
805
879
  return matchers;
@@ -812,7 +886,7 @@ async function collectAllFiles(dir, rootDir) {
812
886
  try {
813
887
  const entries = await readdir2(dir, { withFileTypes: true });
814
888
  for (const entry of entries) {
815
- const full = join4(dir, entry.name);
889
+ const full = join5(dir, entry.name);
816
890
  if (entry.isDirectory()) {
817
891
  if (entry.name === ".git") continue;
818
892
  if (dir === rootDir && entry.name === "node_modules") continue;
@@ -844,6 +918,7 @@ var init_pack_list = __esm({
844
918
  ".hg",
845
919
  ".DS_Store",
846
920
  ".npmrc",
921
+ ".gitignore",
847
922
  ".knarr",
848
923
  "test",
849
924
  "tests",
@@ -1028,6 +1103,208 @@ var init_lockfile = __esm({
1028
1103
  }
1029
1104
  });
1030
1105
 
1106
+ // src/utils/pm-detect.ts
1107
+ import { readFile as readFile5, stat as stat5 } from "fs/promises";
1108
+ import { join as join7, dirname as dirname3 } from "path";
1109
+ function parsePackageManagerSpec(value) {
1110
+ if (typeof value !== "string") return null;
1111
+ const match = value.trim().match(/^([^@\s]+)(?:@(.+))?$/);
1112
+ if (!match) return null;
1113
+ const name = match[1];
1114
+ if (!VALID_PMS.has(name)) return null;
1115
+ return {
1116
+ name,
1117
+ version: match[2]?.trim() ?? ""
1118
+ };
1119
+ }
1120
+ async function readPackageManagerSpec(dir) {
1121
+ try {
1122
+ const raw = await readFile5(join7(dir, "package.json"), "utf-8");
1123
+ const pkg = JSON.parse(raw);
1124
+ return parsePackageManagerSpec(pkg.packageManager);
1125
+ } catch {
1126
+ return null;
1127
+ }
1128
+ }
1129
+ async function findPackageManagerSpec(projectDir) {
1130
+ let dir = projectDir;
1131
+ for (; ; ) {
1132
+ const spec = await readPackageManagerSpec(dir);
1133
+ if (spec) return spec;
1134
+ const parent = dirname3(dir);
1135
+ if (parent === dir) return null;
1136
+ dir = parent;
1137
+ }
1138
+ }
1139
+ function yarnDefaultsToPnp(version) {
1140
+ const normalized = version.trim().replace(/^npm:/, "");
1141
+ if (normalized === "classic") return false;
1142
+ const major = normalized.match(/^v?(\d+)(?:[.+-]|$)/);
1143
+ if (major) return Number(major[1]) >= 2;
1144
+ return normalized.length > 0;
1145
+ }
1146
+ async function detectPackageManager(projectDir) {
1147
+ return (await detectPackageManagerInfo(projectDir)).packageManager;
1148
+ }
1149
+ async function detectPackageManagerInfo(projectDir) {
1150
+ let dir = projectDir;
1151
+ for (; ; ) {
1152
+ const fromField = await readPackageManagerSpec(dir);
1153
+ if (fromField) {
1154
+ return {
1155
+ packageManager: fromField.name,
1156
+ source: "packageManager",
1157
+ dir,
1158
+ file: join7(dir, "package.json")
1159
+ };
1160
+ }
1161
+ const results = await Promise.all(
1162
+ LOCKFILES.map(async ([lockfile, packageManager]) => {
1163
+ try {
1164
+ await stat5(join7(dir, lockfile));
1165
+ return { lockfile, packageManager };
1166
+ } catch {
1167
+ return null;
1168
+ }
1169
+ })
1170
+ );
1171
+ const found = results.find((result) => result !== null);
1172
+ if (found) {
1173
+ return {
1174
+ packageManager: found.packageManager,
1175
+ source: "lockfile",
1176
+ dir,
1177
+ file: join7(dir, found.lockfile)
1178
+ };
1179
+ }
1180
+ const yarnArtifact = await findExistingFile(dir, YARN_PROJECT_ARTIFACTS);
1181
+ if (yarnArtifact) {
1182
+ return {
1183
+ packageManager: "yarn",
1184
+ source: "yarnArtifact",
1185
+ dir,
1186
+ file: join7(dir, yarnArtifact)
1187
+ };
1188
+ }
1189
+ const parent = dirname3(dir);
1190
+ if (parent === dir) {
1191
+ return {
1192
+ packageManager: "npm",
1193
+ source: "default",
1194
+ dir: projectDir
1195
+ };
1196
+ }
1197
+ dir = parent;
1198
+ }
1199
+ }
1200
+ async function findExistingFile(dir, filenames) {
1201
+ for (const filename of filenames) {
1202
+ try {
1203
+ await stat5(join7(dir, filename));
1204
+ return filename;
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ return null;
1209
+ }
1210
+ function readYarnrcScalar(content, key) {
1211
+ for (const line of content.split("\n")) {
1212
+ const trimmed = line.trim();
1213
+ if (trimmed.startsWith("#") || !trimmed.includes(key)) continue;
1214
+ const match = trimmed.match(new RegExp(`^${key}:\\s*(.+)$`));
1215
+ if (!match) continue;
1216
+ const value = match[1].trim().replace(/\s+#.*$/, "").trim().replace(/^["']|["']$/g, "");
1217
+ if (value) return value;
1218
+ }
1219
+ return null;
1220
+ }
1221
+ async function findYarnrcScalar(projectDir, key) {
1222
+ let dir = projectDir;
1223
+ for (; ; ) {
1224
+ try {
1225
+ const content = await readFile5(join7(dir, ".yarnrc.yml"), "utf-8");
1226
+ const value = readYarnrcScalar(content, key);
1227
+ if (value) return value;
1228
+ } catch {
1229
+ }
1230
+ const parent = dirname3(dir);
1231
+ if (parent === dir) return null;
1232
+ dir = parent;
1233
+ }
1234
+ }
1235
+ async function detectYarnNodeLinker(projectDir) {
1236
+ const value = await findYarnrcScalar(projectDir, "nodeLinker");
1237
+ if (value === "node-modules" || value === "pnpm" || value === "pnp") {
1238
+ return value;
1239
+ }
1240
+ return null;
1241
+ }
1242
+ async function detectYarnPnpmStoreFolder(projectDir) {
1243
+ return findYarnrcScalar(projectDir, "pnpmStoreFolder");
1244
+ }
1245
+ async function hasYarnrcYml(projectDir) {
1246
+ let dir = projectDir;
1247
+ for (; ; ) {
1248
+ try {
1249
+ await stat5(join7(dir, ".yarnrc.yml"));
1250
+ return true;
1251
+ } catch {
1252
+ const parent = dirname3(dir);
1253
+ if (parent === dir) return false;
1254
+ dir = parent;
1255
+ }
1256
+ }
1257
+ }
1258
+ async function hasYarnPnpManifest(projectDir) {
1259
+ let dir = projectDir;
1260
+ for (; ; ) {
1261
+ const results = await Promise.all(
1262
+ YARN_PNP_MANIFESTS.map(async (manifest) => {
1263
+ try {
1264
+ await stat5(join7(dir, manifest));
1265
+ return true;
1266
+ } catch {
1267
+ return false;
1268
+ }
1269
+ })
1270
+ );
1271
+ if (results.some(Boolean)) return true;
1272
+ const parent = dirname3(dir);
1273
+ if (parent === dir) return false;
1274
+ dir = parent;
1275
+ }
1276
+ }
1277
+ async function hasYarnPnpMarkers(projectDir) {
1278
+ if (await hasYarnPnpManifest(projectDir)) return true;
1279
+ return await detectYarnNodeLinker(projectDir) === "pnp";
1280
+ }
1281
+ async function isYarnPnpProject(projectDir) {
1282
+ const linker = await detectYarnNodeLinker(projectDir);
1283
+ if (linker === "pnp") return true;
1284
+ if (linker === "node-modules" || linker === "pnpm") return false;
1285
+ if (await hasYarnPnpManifest(projectDir)) return true;
1286
+ if (await hasYarnrcYml(projectDir)) return true;
1287
+ const spec = await findPackageManagerSpec(projectDir);
1288
+ return spec?.name === "yarn" && yarnDefaultsToPnp(spec.version);
1289
+ }
1290
+ var VALID_PMS, LOCKFILES, YARN_PNP_MANIFESTS, YARN_PROJECT_ARTIFACTS;
1291
+ var init_pm_detect = __esm({
1292
+ "src/utils/pm-detect.ts"() {
1293
+ "use strict";
1294
+ VALID_PMS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
1295
+ LOCKFILES = [
1296
+ ["pnpm-lock.yaml", "pnpm"],
1297
+ ["bun.lockb", "bun"],
1298
+ ["bun.lock", "bun"],
1299
+ ["yarn.lock", "yarn"],
1300
+ ["package-lock.json", "npm"],
1301
+ ["npm-shrinkwrap.json", "npm"]
1302
+ ];
1303
+ YARN_PNP_MANIFESTS = [".pnp.cjs", ".pnp.js", ".pnp.loader.mjs"];
1304
+ YARN_PROJECT_ARTIFACTS = [".yarnrc.yml", ...YARN_PNP_MANIFESTS];
1305
+ }
1306
+ });
1307
+
1031
1308
  // src/core/history.ts
1032
1309
  var history_exports = {};
1033
1310
  __export(history_exports, {
@@ -1039,13 +1316,13 @@ __export(history_exports, {
1039
1316
  resolveHistoryLimit: () => resolveHistoryLimit,
1040
1317
  restoreHistoryEntry: () => restoreHistoryEntry
1041
1318
  });
1042
- import { readdir as readdir4, readFile as readFile5 } from "fs/promises";
1043
- import { join as join6 } from "path";
1319
+ import { readdir as readdir4, readFile as readFile6 } from "fs/promises";
1320
+ import { join as join8 } from "path";
1044
1321
  async function captureHistory(name, version, oldEntryDir, historyLimit) {
1045
- const metaPath = join6(oldEntryDir, ".knarr-meta.json");
1322
+ const metaPath = join8(oldEntryDir, ".knarr-meta.json");
1046
1323
  let meta;
1047
1324
  try {
1048
- meta = JSON.parse(await readFile5(metaPath, "utf-8"));
1325
+ meta = JSON.parse(await readFile6(metaPath, "utf-8"));
1049
1326
  } catch {
1050
1327
  verbose(`[history] Could not read meta from ${metaPath}, skipping history capture`);
1051
1328
  return;
@@ -1065,12 +1342,12 @@ async function captureHistory(name, version, oldEntryDir, historyLimit) {
1065
1342
  const tmpHistoryEntry = entryDir + `.tmp-${process.pid}`;
1066
1343
  try {
1067
1344
  await ensureDir(tmpHistoryEntry);
1068
- const oldPkgDir = join6(oldEntryDir, "package");
1345
+ const oldPkgDir = join8(oldEntryDir, "package");
1069
1346
  if (await exists(oldPkgDir)) {
1070
- await moveDir(oldPkgDir, join6(tmpHistoryEntry, "package"));
1347
+ await moveDir(oldPkgDir, join8(tmpHistoryEntry, "package"));
1071
1348
  }
1072
1349
  await atomicWriteFile(
1073
- join6(tmpHistoryEntry, ".knarr-meta.json"),
1350
+ join8(tmpHistoryEntry, ".knarr-meta.json"),
1074
1351
  JSON.stringify(meta, null, 2)
1075
1352
  );
1076
1353
  await moveDir(tmpHistoryEntry, entryDir);
@@ -1093,16 +1370,16 @@ async function listHistory(name, version) {
1093
1370
  }
1094
1371
  const result = [];
1095
1372
  for (const buildId of entries) {
1096
- const entryDir = join6(historyDir, buildId);
1097
- const metaPath = join6(entryDir, ".knarr-meta.json");
1373
+ const entryDir = join8(historyDir, buildId);
1374
+ const metaPath = join8(entryDir, ".knarr-meta.json");
1098
1375
  try {
1099
- const meta = JSON.parse(await readFile5(metaPath, "utf-8"));
1376
+ const meta = JSON.parse(await readFile6(metaPath, "utf-8"));
1100
1377
  result.push({
1101
1378
  buildId: meta.buildId ?? buildId,
1102
1379
  contentHash: meta.contentHash,
1103
1380
  publishedAt: meta.publishedAt,
1104
1381
  sourcePath: meta.sourcePath,
1105
- packageDir: join6(entryDir, "package")
1382
+ packageDir: join8(entryDir, "package")
1106
1383
  });
1107
1384
  } catch {
1108
1385
  }
@@ -1112,15 +1389,15 @@ async function listHistory(name, version) {
1112
1389
  }
1113
1390
  async function getHistoryEntry(name, version, buildId) {
1114
1391
  const entryDir = getHistoryEntryPath(name, version, buildId);
1115
- const metaPath = join6(entryDir, ".knarr-meta.json");
1392
+ const metaPath = join8(entryDir, ".knarr-meta.json");
1116
1393
  try {
1117
- const meta = JSON.parse(await readFile5(metaPath, "utf-8"));
1394
+ const meta = JSON.parse(await readFile6(metaPath, "utf-8"));
1118
1395
  return {
1119
1396
  buildId: meta.buildId ?? buildId,
1120
1397
  contentHash: meta.contentHash,
1121
1398
  publishedAt: meta.publishedAt,
1122
1399
  sourcePath: meta.sourcePath,
1123
- packageDir: join6(entryDir, "package")
1400
+ packageDir: join8(entryDir, "package")
1124
1401
  };
1125
1402
  } catch {
1126
1403
  return null;
@@ -1131,17 +1408,17 @@ async function restoreHistoryEntry(name, version, buildId, historyLimit) {
1131
1408
  if (!entry) return null;
1132
1409
  const storeEntryDir = getStoreEntryPath(name, version);
1133
1410
  const historyEntryDir = getHistoryEntryPath(name, version, buildId);
1134
- const historyPkg = join6(historyEntryDir, "package");
1135
- const historyMeta = join6(historyEntryDir, ".knarr-meta.json");
1136
- const metaContent = await readFile5(historyMeta, "utf-8");
1137
- const storePkg = join6(storeEntryDir, "package");
1138
- const storeMeta = join6(storeEntryDir, ".knarr-meta.json");
1411
+ const historyPkg = join8(historyEntryDir, "package");
1412
+ const historyMeta = join8(historyEntryDir, ".knarr-meta.json");
1413
+ const metaContent = await readFile6(historyMeta, "utf-8");
1414
+ const storePkg = join8(storeEntryDir, "package");
1415
+ const storeMeta = join8(storeEntryDir, ".knarr-meta.json");
1139
1416
  if (!await exists(historyPkg)) {
1140
1417
  throw new Error(`History entry ${buildId} is missing its package directory`);
1141
1418
  }
1142
- const currentMetaContent = await readFile5(storeMeta, "utf-8").catch(() => null);
1419
+ const currentMetaContent = await readFile6(storeMeta, "utf-8").catch(() => null);
1143
1420
  const oldEntryDir = storeEntryDir + `.restore-old-${process.pid}-${Date.now()}`;
1144
- const oldPkg = join6(oldEntryDir, "package");
1421
+ const oldPkg = join8(oldEntryDir, "package");
1145
1422
  let stagedOld = false;
1146
1423
  let historyMovedToStore = false;
1147
1424
  try {
@@ -1151,7 +1428,7 @@ async function restoreHistoryEntry(name, version, buildId, historyLimit) {
1151
1428
  await moveDir(storePkg, oldPkg);
1152
1429
  }
1153
1430
  if (currentMetaContent) {
1154
- await atomicWriteFile(join6(oldEntryDir, ".knarr-meta.json"), currentMetaContent);
1431
+ await atomicWriteFile(join8(oldEntryDir, ".knarr-meta.json"), currentMetaContent);
1155
1432
  }
1156
1433
  stagedOld = true;
1157
1434
  }
@@ -1227,30 +1504,35 @@ var workspace_exports = {};
1227
1504
  __export(workspace_exports, {
1228
1505
  buildReverseAdjacency: () => buildReverseAdjacency,
1229
1506
  buildWorkspaceGraph: () => buildWorkspaceGraph,
1507
+ filterPublishableWorkspaceGraph: () => filterPublishableWorkspaceGraph,
1508
+ findWorkspacePackageRoot: () => findWorkspacePackageRoot,
1230
1509
  findWorkspacePackages: () => findWorkspacePackages,
1231
1510
  findWorkspaceRoot: () => findWorkspaceRoot,
1232
1511
  parseCatalogs: () => parseCatalogs
1233
1512
  });
1234
- import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
1235
- import { join as join7, dirname as dirname3, resolve as resolve3, relative as relative5 } from "path";
1513
+ import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
1514
+ import { join as join9, dirname as dirname4, resolve as resolve3, relative as relative5 } from "path";
1236
1515
  import picomatch2 from "picomatch";
1237
1516
  async function findWorkspaceRoot(startDir) {
1238
1517
  let dir = startDir;
1239
1518
  for (; ; ) {
1240
- if (await exists(join7(dir, "pnpm-workspace.yaml"))) {
1519
+ if (await exists(join9(dir, "pnpm-workspace.yaml"))) {
1241
1520
  return dir;
1242
1521
  }
1243
- const parent = dirname3(dir);
1522
+ const parent = dirname4(dir);
1244
1523
  if (parent === dir) return null;
1245
1524
  dir = parent;
1246
1525
  }
1247
1526
  }
1527
+ async function findWorkspacePackageRoot(startDir) {
1528
+ return await findWorkspaceRoot(startDir) ?? await findPackageJsonWorkspaceRoot(startDir);
1529
+ }
1248
1530
  async function parseCatalogs(workspaceRoot) {
1249
1531
  const result = { default: {}, named: {} };
1250
- const filePath = join7(workspaceRoot, "pnpm-workspace.yaml");
1532
+ const filePath = join9(workspaceRoot, "pnpm-workspace.yaml");
1251
1533
  let content;
1252
1534
  try {
1253
- content = await readFile6(filePath, "utf-8");
1535
+ content = await readFile7(filePath, "utf-8");
1254
1536
  } catch {
1255
1537
  return result;
1256
1538
  }
@@ -1319,7 +1601,7 @@ async function findWorkspacePackages(startDir) {
1319
1601
  const rootDir = pnpmRoot ?? await findPackageJsonWorkspaceRoot(startDir);
1320
1602
  if (!rootDir) return [];
1321
1603
  try {
1322
- const rootPkg = JSON.parse(await readFile6(join7(rootDir, "package.json"), "utf-8"));
1604
+ const rootPkg = JSON.parse(await readFile7(join9(rootDir, "package.json"), "utf-8"));
1323
1605
  const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
1324
1606
  if (workspaces.length === 0) return [];
1325
1607
  const positive = workspaces.filter((p) => !p.startsWith("!"));
@@ -1330,10 +1612,10 @@ async function findWorkspacePackages(startDir) {
1330
1612
  }
1331
1613
  }
1332
1614
  async function parsePnpmWorkspacePackages(workspaceRoot) {
1333
- const filePath = join7(workspaceRoot, "pnpm-workspace.yaml");
1615
+ const filePath = join9(workspaceRoot, "pnpm-workspace.yaml");
1334
1616
  let content;
1335
1617
  try {
1336
- content = await readFile6(filePath, "utf-8");
1618
+ content = await readFile7(filePath, "utf-8");
1337
1619
  } catch {
1338
1620
  return [];
1339
1621
  }
@@ -1360,11 +1642,11 @@ async function findPackageJsonWorkspaceRoot(startDir) {
1360
1642
  let dir = startDir;
1361
1643
  for (; ; ) {
1362
1644
  try {
1363
- const pkg = JSON.parse(await readFile6(join7(dir, "package.json"), "utf-8"));
1645
+ const pkg = JSON.parse(await readFile7(join9(dir, "package.json"), "utf-8"));
1364
1646
  if (pkg.workspaces) return dir;
1365
1647
  } catch {
1366
1648
  }
1367
- const parent = dirname3(dir);
1649
+ const parent = dirname4(dir);
1368
1650
  if (parent === dir) return null;
1369
1651
  dir = parent;
1370
1652
  }
@@ -1382,7 +1664,7 @@ async function resolveWorkspaceGlobs(rootDir, patterns, negations = []) {
1382
1664
  foundGlob = true;
1383
1665
  globParts.push(part);
1384
1666
  } else {
1385
- staticPrefix = join7(staticPrefix, part);
1667
+ staticPrefix = join9(staticPrefix, part);
1386
1668
  }
1387
1669
  }
1388
1670
  if (globParts.length === 1 && globParts[0] === "*") {
@@ -1390,8 +1672,8 @@ async function resolveWorkspaceGlobs(rootDir, patterns, negations = []) {
1390
1672
  const entries = await readdir5(staticPrefix, { withFileTypes: true });
1391
1673
  for (const entry of entries) {
1392
1674
  if (entry.isDirectory()) {
1393
- const pkgDir = join7(staticPrefix, entry.name);
1394
- if (await exists(join7(pkgDir, "package.json"))) {
1675
+ const pkgDir = join9(staticPrefix, entry.name);
1676
+ if (await exists(join9(pkgDir, "package.json"))) {
1395
1677
  results.push(pkgDir);
1396
1678
  }
1397
1679
  }
@@ -1403,14 +1685,14 @@ async function resolveWorkspaceGlobs(rootDir, patterns, negations = []) {
1403
1685
  const candidates = await collectDirs(rootDir, 8);
1404
1686
  for (const candidate of candidates) {
1405
1687
  const rel = normalizePath(relative5(rootDir, candidate));
1406
- if (isMatch(rel) && await exists(join7(candidate, "package.json"))) {
1688
+ if (isMatch(rel) && await exists(join9(candidate, "package.json"))) {
1407
1689
  results.push(candidate);
1408
1690
  }
1409
1691
  }
1410
1692
  }
1411
1693
  } else {
1412
1694
  const pkgDir = resolve3(rootDir, pattern);
1413
- if (await exists(join7(pkgDir, "package.json"))) {
1695
+ if (await exists(join9(pkgDir, "package.json"))) {
1414
1696
  results.push(pkgDir);
1415
1697
  }
1416
1698
  }
@@ -1430,7 +1712,7 @@ async function collectDirs(dir, maxDepth) {
1430
1712
  const entries = await readdir5(dir, { withFileTypes: true });
1431
1713
  for (const entry of entries) {
1432
1714
  if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git") continue;
1433
- const full = join7(dir, entry.name);
1715
+ const full = join9(dir, entry.name);
1434
1716
  results.push(full);
1435
1717
  results.push(...await collectDirs(full, maxDepth - 1));
1436
1718
  }
@@ -1444,7 +1726,7 @@ async function buildWorkspaceGraph(startDir) {
1444
1726
  for (const dir of dirs) {
1445
1727
  try {
1446
1728
  const pkg = JSON.parse(
1447
- await readFile6(join7(dir, "package.json"), "utf-8")
1729
+ await readFile7(join9(dir, "package.json"), "utf-8")
1448
1730
  );
1449
1731
  if (pkg.name && pkg.version) {
1450
1732
  packages.push({ name: pkg.name, version: pkg.version, dir, pkg });
@@ -1469,6 +1751,19 @@ async function buildWorkspaceGraph(startDir) {
1469
1751
  }
1470
1752
  return { packages, adjacency };
1471
1753
  }
1754
+ function filterPublishableWorkspaceGraph(graph) {
1755
+ const packages = graph.packages.filter((pkg) => !pkg.pkg.private);
1756
+ const names = new Set(packages.map((pkg) => pkg.name));
1757
+ const adjacency = /* @__PURE__ */ new Map();
1758
+ for (const pkg of packages) {
1759
+ const deps = graph.adjacency.get(pkg.name) ?? /* @__PURE__ */ new Set();
1760
+ adjacency.set(
1761
+ pkg.name,
1762
+ new Set([...deps].filter((dep) => names.has(dep)))
1763
+ );
1764
+ }
1765
+ return { packages, adjacency };
1766
+ }
1472
1767
  function buildReverseAdjacency(adjacency) {
1473
1768
  const reverse = /* @__PURE__ */ new Map();
1474
1769
  for (const name of adjacency.keys()) {
@@ -1491,10 +1786,25 @@ function parseKeyValue(line) {
1491
1786
  const colonIdx = trimmed.indexOf(":");
1492
1787
  if (colonIdx <= 0) return null;
1493
1788
  const key = trimmed.slice(0, colonIdx).trim();
1494
- const value = trimmed.slice(colonIdx + 1).trim();
1789
+ const value = stripYamlInlineComment(trimmed.slice(colonIdx + 1)).trim();
1495
1790
  if (!key || !value) return null;
1791
+ const unquotedKey = key.replace(/^["']|["']$/g, "");
1496
1792
  const unquoted = value.replace(/^["']|["']$/g, "");
1497
- return [key, unquoted];
1793
+ return [unquotedKey, unquoted];
1794
+ }
1795
+ function stripYamlInlineComment(value) {
1796
+ let quote = null;
1797
+ for (let i = 0; i < value.length; i++) {
1798
+ const char = value[i];
1799
+ if ((char === '"' || char === "'") && value[i - 1] !== "\\") {
1800
+ quote = quote === char ? null : quote ?? char;
1801
+ continue;
1802
+ }
1803
+ if (!quote && char === "#" && (i === 0 || /\s/.test(value[i - 1]))) {
1804
+ return value.slice(0, i);
1805
+ }
1806
+ }
1807
+ return value;
1498
1808
  }
1499
1809
  var init_workspace = __esm({
1500
1810
  "src/utils/workspace.ts"() {
@@ -1505,16 +1815,16 @@ var init_workspace = __esm({
1505
1815
  });
1506
1816
 
1507
1817
  // src/core/publisher.ts
1508
- import { readFile as readFile7, stat as stat5 } from "fs/promises";
1509
- import { join as join8, relative as relative6, dirname as dirname4, resolve as resolve4 } from "path";
1818
+ import { readFile as readFile8, stat as stat6 } from "fs/promises";
1819
+ import { delimiter, join as join10, relative as relative6, dirname as dirname5, resolve as resolve4 } from "path";
1510
1820
  import { spawn } from "child_process";
1511
1821
  import { platform } from "os";
1512
1822
  import { availableParallelism as availableParallelism3 } from "os";
1513
1823
  async function publish(packageDir, options = {}) {
1514
- const pkgPath = join8(packageDir, "package.json");
1824
+ const pkgPath = join10(packageDir, "package.json");
1515
1825
  let pkgContent;
1516
1826
  try {
1517
- pkgContent = await readFile7(pkgPath, "utf-8");
1827
+ pkgContent = await readFile8(pkgPath, "utf-8");
1518
1828
  } catch {
1519
1829
  throw new Error(`No package.json found in ${packageDir}`);
1520
1830
  }
@@ -1522,11 +1832,6 @@ async function publish(packageDir, options = {}) {
1522
1832
  if (!pkg.name) throw new Error("package.json missing 'name' field");
1523
1833
  if (!pkg.version) throw new Error("package.json missing 'version' field");
1524
1834
  validatePackageIdentity(pkg.name, pkg.version);
1525
- if (pkg.private && !options.allowPrivate) {
1526
- throw new Error(
1527
- `Package "${pkg.name}" is private. Use --private flag to publish private packages.`
1528
- );
1529
- }
1530
1835
  await runLifecycleHook(packageDir, pkg, "preknarr");
1531
1836
  if (options.runScripts !== false) {
1532
1837
  await runLifecycleHook(packageDir, pkg, "prepack");
@@ -1535,7 +1840,7 @@ async function publish(packageDir, options = {}) {
1535
1840
  if (pkg.publishConfig?.directory) {
1536
1841
  publishDir = resolve4(packageDir, pkg.publishConfig.directory);
1537
1842
  try {
1538
- const s = await stat5(publishDir);
1843
+ const s = await stat6(publishDir);
1539
1844
  if (!s.isDirectory()) {
1540
1845
  throw new Error(`publishConfig.directory "${pkg.publishConfig.directory}" is not a directory`);
1541
1846
  }
@@ -1547,22 +1852,46 @@ async function publish(packageDir, options = {}) {
1547
1852
  }
1548
1853
  verbose(`[publish] Using publishConfig.directory: ${publishDir}`);
1549
1854
  }
1550
- const filePkg = publishDir !== packageDir ? JSON.parse(await readFile7(join8(publishDir, "package.json"), "utf-8").catch(() => JSON.stringify(pkg))) : pkg;
1551
- const files = await resolvePackFiles(publishDir, filePkg);
1855
+ const publishPkgPath = join10(publishDir, "package.json");
1856
+ const publishDirHasPackageJson = publishDir === packageDir || await exists(publishPkgPath);
1857
+ const filePkg = publishDirHasPackageJson ? JSON.parse(await readFile8(publishPkgPath, "utf-8")) : pkg;
1858
+ if (!filePkg.name) throw new Error("publish package.json missing 'name' field");
1859
+ if (!filePkg.version) throw new Error("publish package.json missing 'version' field");
1860
+ validatePackageIdentity(filePkg.name, filePkg.version);
1861
+ if (filePkg.private && !options.allowPrivate) {
1862
+ throw new Error(
1863
+ `Package "${filePkg.name}" is private. Use --private flag to publish private packages.`
1864
+ );
1865
+ }
1866
+ const packListPkg = publishDirHasPackageJson ? filePkg : { ...filePkg, files: void 0 };
1867
+ const files = await resolvePackFiles(publishDir, packListPkg);
1552
1868
  if (files.length === 0) {
1553
1869
  throw new Error("No publishable files found");
1554
1870
  }
1555
- verbose(`[publish] Resolved ${files.length} files for ${pkg.name}@${pkg.version}`);
1556
- const contentHash = await computeContentHash(files, publishDir);
1557
- await preloadWorkspaceVersions(pkg, packageDir);
1558
- await preloadCatalogs(pkg, packageDir);
1871
+ verbose(`[publish] Resolved ${files.length} files for ${filePkg.name}@${filePkg.version}`);
1872
+ await preloadWorkspaceVersions(filePkg, packageDir);
1873
+ await preloadCatalogs(filePkg, packageDir);
1874
+ let processedPkg = rewriteProtocolVersions(filePkg);
1875
+ processedPkg = applyPublishConfig(processedPkg);
1876
+ const contentOverrides = /* @__PURE__ */ new Map();
1877
+ if (processedPkg !== filePkg || !publishDirHasPackageJson) {
1878
+ contentOverrides.set("package.json", JSON.stringify(processedPkg, null, 2));
1879
+ }
1880
+ const hashFiles = [...files];
1881
+ const hashFileRels = new Set(
1882
+ files.map((file) => relative6(publishDir, file).replace(/\\/g, "/"))
1883
+ );
1884
+ for (const rel of contentOverrides.keys()) {
1885
+ if (!hashFileRels.has(rel)) hashFiles.push(join10(publishDir, rel));
1886
+ }
1887
+ const contentHash = await computeContentHash(hashFiles, publishDir, contentOverrides);
1559
1888
  if (!options.force) {
1560
- const existingMeta = await readMeta(pkg.name, pkg.version);
1889
+ const existingMeta = await readMeta(filePkg.name, filePkg.version);
1561
1890
  if (existingMeta && existingMeta.contentHash === contentHash) {
1562
- consola.info(`${pkg.name}@${pkg.version} already up to date (no changes since last publish)`);
1891
+ consola.info(`${filePkg.name}@${filePkg.version} already up to date (no changes since last publish)`);
1563
1892
  return {
1564
- name: pkg.name,
1565
- version: pkg.version,
1893
+ name: filePkg.name,
1894
+ version: filePkg.version,
1566
1895
  fileCount: files.length,
1567
1896
  skipped: true,
1568
1897
  contentHash,
@@ -1570,17 +1899,17 @@ async function publish(packageDir, options = {}) {
1570
1899
  };
1571
1900
  }
1572
1901
  }
1573
- const storeEntryDir = getStoreEntryPath(pkg.name, pkg.version);
1902
+ const storeEntryDir = getStoreEntryPath(filePkg.name, filePkg.version);
1574
1903
  const result = await withFileLock(
1575
1904
  storeEntryDir + ".lock",
1576
1905
  async () => {
1577
1906
  if (!options.force) {
1578
- const metaUnderLock = await readMeta(pkg.name, pkg.version);
1907
+ const metaUnderLock = await readMeta(filePkg.name, filePkg.version);
1579
1908
  if (metaUnderLock && metaUnderLock.contentHash === contentHash) {
1580
- consola.info(`${pkg.name}@${pkg.version} already up to date (no changes since last publish)`);
1909
+ consola.info(`${filePkg.name}@${filePkg.version} already up to date (no changes since last publish)`);
1581
1910
  return {
1582
- name: pkg.name,
1583
- version: pkg.version,
1911
+ name: filePkg.name,
1912
+ version: filePkg.version,
1584
1913
  fileCount: files.length,
1585
1914
  skipped: true,
1586
1915
  contentHash,
@@ -1589,33 +1918,36 @@ async function publish(packageDir, options = {}) {
1589
1918
  }
1590
1919
  }
1591
1920
  const tmpDir = storeEntryDir + `.tmp-${process.pid}-${Date.now()}`;
1592
- const tmpPackageDir = join8(tmpDir, "package");
1921
+ const tmpPackageDir = join10(tmpDir, "package");
1593
1922
  const buildId = contentHash.slice(9, 17);
1594
1923
  try {
1595
1924
  await ensurePrivateDir(tmpPackageDir);
1596
- let processedPkg = rewriteProtocolVersions(pkg);
1597
- processedPkg = applyPublishConfig(processedPkg);
1598
1925
  verbose(`[publish] Copying files to temp store...`);
1599
1926
  const uniqueDirs = new Set(
1600
- files.map((file) => dirname4(join8(tmpPackageDir, relative6(publishDir, file))))
1927
+ files.map((file) => dirname5(join10(tmpPackageDir, relative6(publishDir, file))))
1601
1928
  );
1602
1929
  await Promise.all([...uniqueDirs].map((d) => ensureDir(d)));
1603
1930
  await Promise.all(
1604
1931
  files.map(
1605
1932
  (file) => copyLimit(async () => {
1606
1933
  const rel = relative6(publishDir, file);
1607
- const dest = join8(tmpPackageDir, rel);
1608
- if (rel === "package.json" && processedPkg !== pkg) {
1609
- await atomicWriteFile(dest, JSON.stringify(processedPkg, null, 2));
1934
+ const normalizedRel = rel.replace(/\\/g, "/");
1935
+ const dest = join10(tmpPackageDir, rel);
1936
+ const override = contentOverrides.get(normalizedRel);
1937
+ if (override !== void 0) {
1938
+ await atomicWriteFile(
1939
+ dest,
1940
+ Buffer.isBuffer(override) ? override.toString("utf-8") : override
1941
+ );
1610
1942
  } else {
1611
1943
  await copyWithCoW(file, dest, { ensureParent: false });
1612
1944
  }
1613
1945
  })
1614
1946
  )
1615
1947
  );
1616
- if (publishDir !== packageDir) {
1948
+ if (contentOverrides.has("package.json")) {
1617
1949
  await atomicWriteFile(
1618
- join8(tmpPackageDir, "package.json"),
1950
+ join10(tmpPackageDir, "package.json"),
1619
1951
  JSON.stringify(processedPkg, null, 2)
1620
1952
  );
1621
1953
  }
@@ -1627,7 +1959,7 @@ async function publish(packageDir, options = {}) {
1627
1959
  buildId
1628
1960
  };
1629
1961
  await atomicWriteFile(
1630
- join8(tmpDir, ".knarr-meta.json"),
1962
+ join10(tmpDir, ".knarr-meta.json"),
1631
1963
  JSON.stringify(meta, null, 2)
1632
1964
  );
1633
1965
  const hadOld = await exists(storeEntryDir);
@@ -1651,7 +1983,7 @@ async function publish(packageDir, options = {}) {
1651
1983
  if (hadOld && !isDryRun()) {
1652
1984
  try {
1653
1985
  const { captureHistory: captureHistory2 } = await Promise.resolve().then(() => (init_history(), history_exports));
1654
- await captureHistory2(pkg.name, pkg.version, oldDir, options.historyLimit);
1986
+ await captureHistory2(filePkg.name, filePkg.version, oldDir, options.historyLimit);
1655
1987
  } catch (err) {
1656
1988
  verbose(`[publish] History capture failed: ${err instanceof Error ? err.message : String(err)}`);
1657
1989
  }
@@ -1663,8 +1995,8 @@ async function publish(packageDir, options = {}) {
1663
1995
  throw err;
1664
1996
  }
1665
1997
  return {
1666
- name: pkg.name,
1667
- version: pkg.version,
1998
+ name: filePkg.name,
1999
+ version: filePkg.version,
1668
2000
  fileCount: files.length,
1669
2001
  skipped: false,
1670
2002
  contentHash,
@@ -1679,10 +2011,46 @@ async function publish(packageDir, options = {}) {
1679
2011
  }
1680
2012
  await runLifecycleHook(packageDir, pkg, "postknarr");
1681
2013
  consola.success(
1682
- `Published ${pkg.name}@${pkg.version} (${files.length} files) [${result.buildId}]`
2014
+ `Published ${filePkg.name}@${filePkg.version} (${files.length} files) [${result.buildId}]`
1683
2015
  );
1684
2016
  return result;
1685
2017
  }
2018
+ function pathEnvKey(env) {
2019
+ if (platform() !== "win32") return "PATH";
2020
+ return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "Path";
2021
+ }
2022
+ function setEnvVar(env, key, value) {
2023
+ if (platform() === "win32") {
2024
+ const lowered = key.toLowerCase();
2025
+ for (const existingKey of Object.keys(env)) {
2026
+ if (existingKey.toLowerCase() === lowered) {
2027
+ delete env[existingKey];
2028
+ }
2029
+ }
2030
+ }
2031
+ env[key] = value;
2032
+ }
2033
+ function buildLifecycleEnv(packageDir, pkg, hookName, script) {
2034
+ const env = { ...process.env };
2035
+ const key = pathEnvKey(env);
2036
+ const existingPath = env[key] ?? "";
2037
+ setEnvVar(
2038
+ env,
2039
+ key,
2040
+ [
2041
+ join10(packageDir, "node_modules", ".bin"),
2042
+ existingPath
2043
+ ].filter(Boolean).join(delimiter)
2044
+ );
2045
+ if (!("INIT_CWD" in env) || !env.INIT_CWD) {
2046
+ setEnvVar(env, "INIT_CWD", packageDir);
2047
+ }
2048
+ setEnvVar(env, "npm_lifecycle_event", hookName);
2049
+ setEnvVar(env, "npm_lifecycle_script", script);
2050
+ setEnvVar(env, "npm_package_name", pkg.name);
2051
+ setEnvVar(env, "npm_package_version", pkg.version);
2052
+ return env;
2053
+ }
1686
2054
  async function runLifecycleHook(packageDir, pkg, hookName) {
1687
2055
  const script = pkg.scripts?.[hookName];
1688
2056
  if (!script) return;
@@ -1694,13 +2062,16 @@ async function runLifecycleHook(packageDir, pkg, hookName) {
1694
2062
  });
1695
2063
  return;
1696
2064
  }
1697
- verbose(`[lifecycle] Running ${hookName}: ${script}`);
2065
+ const command = await resolveLifecycleCommand(packageDir, hookName, script);
2066
+ verbose(`[lifecycle] Running ${hookName}: ${command}`);
1698
2067
  return new Promise((resolve8, reject) => {
1699
2068
  const isWin = platform() === "win32";
1700
2069
  const shell = isWin ? "cmd" : "sh";
1701
2070
  const shellFlag = isWin ? "/c" : "-c";
1702
- const child = spawn(shell, [shellFlag, script], {
2071
+ const env = buildLifecycleEnv(packageDir, pkg, hookName, script);
2072
+ const child = spawn(shell, [shellFlag, command], {
1703
2073
  cwd: packageDir,
2074
+ env,
1704
2075
  stdio: "inherit"
1705
2076
  });
1706
2077
  const timer = setTimeout(() => {
@@ -1721,6 +2092,10 @@ async function runLifecycleHook(packageDir, pkg, hookName) {
1721
2092
  });
1722
2093
  });
1723
2094
  }
2095
+ async function resolveLifecycleCommand(packageDir, hookName, script) {
2096
+ const shouldUseYarn = await hasYarnPnpMarkers(packageDir) || (await detectPackageManagerInfo(packageDir)).packageManager === "yarn" && await isYarnPnpProject(packageDir);
2097
+ return shouldUseYarn ? `yarn run ${hookName}` : script;
2098
+ }
1724
2099
  function applyPublishConfig(pkg) {
1725
2100
  if (!pkg.publishConfig) return pkg;
1726
2101
  const result = { ...pkg };
@@ -1749,13 +2124,9 @@ function rewriteProtocolVersions(pkg) {
1749
2124
  const newDeps = { ...deps };
1750
2125
  for (const [name, version] of Object.entries(deps)) {
1751
2126
  if (version.startsWith("workspace:")) {
1752
- const versionPart = version.slice("workspace:".length);
1753
- if (versionPart === "*" || versionPart === "^" || versionPart === "~") {
1754
- const depVersion = _cachedWorkspaceVersions?.versions.get(name) ?? pkg.version;
1755
- newDeps[name] = versionPart === "*" ? depVersion : versionPart + depVersion;
1756
- } else {
1757
- newDeps[name] = versionPart;
1758
- }
2127
+ const resolved = resolveWorkspaceSpecifier(name, version);
2128
+ if (!resolved) continue;
2129
+ newDeps[name] = resolved;
1759
2130
  fieldChanged = true;
1760
2131
  changed = true;
1761
2132
  } else if (version.startsWith("catalog:")) {
@@ -1783,6 +2154,32 @@ function rewriteProtocolVersions(pkg) {
1783
2154
  }
1784
2155
  return changed ? result : pkg;
1785
2156
  }
2157
+ function resolveWorkspaceSpecifier(depName, specifier) {
2158
+ const raw = specifier.slice("workspace:".length);
2159
+ const alias = parseWorkspaceAlias(raw);
2160
+ const targetName = alias?.name ?? depName;
2161
+ const versionPart = alias?.range ?? raw;
2162
+ if (versionPart === "*" || versionPart === "^" || versionPart === "~") {
2163
+ const depVersion = _cachedWorkspaceVersions?.versions.get(targetName);
2164
+ if (!depVersion) {
2165
+ consola.warn(
2166
+ `workspace: specifier for "${targetName}" could not be resolved \u2014 published package.json will contain "${specifier}" which may cause install failures`
2167
+ );
2168
+ return null;
2169
+ }
2170
+ const resolved = versionPart === "*" ? depVersion : versionPart + depVersion;
2171
+ return alias && targetName !== depName ? `npm:${targetName}@${resolved}` : resolved;
2172
+ }
2173
+ return alias && targetName !== depName ? `npm:${targetName}@${versionPart}` : versionPart;
2174
+ }
2175
+ function parseWorkspaceAlias(specifierBody) {
2176
+ const atIndex = specifierBody.lastIndexOf("@");
2177
+ if (atIndex <= 0) return null;
2178
+ const name = specifierBody.slice(0, atIndex);
2179
+ const range = specifierBody.slice(atIndex + 1);
2180
+ if (!name || !range) return null;
2181
+ return { name, range };
2182
+ }
1786
2183
  function resolveCatalogVersion(specifier, depName, catalogs) {
1787
2184
  const catalogRef = specifier.slice("catalog:".length);
1788
2185
  if (catalogRef === "" || catalogRef === "default") {
@@ -1792,8 +2189,8 @@ function resolveCatalogVersion(specifier, depName, catalogs) {
1792
2189
  }
1793
2190
  async function getWorkspaceRoot(packageDir) {
1794
2191
  if (_cachedWorkspaceRoot?.dir === packageDir) return _cachedWorkspaceRoot.root;
1795
- const { findWorkspaceRoot: findWorkspaceRoot2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
1796
- const root = await findWorkspaceRoot2(packageDir);
2192
+ const { findWorkspacePackageRoot: findWorkspacePackageRoot2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
2193
+ const root = await findWorkspacePackageRoot2(packageDir);
1797
2194
  _cachedWorkspaceRoot = { dir: packageDir, root };
1798
2195
  return root;
1799
2196
  }
@@ -1813,7 +2210,6 @@ async function preloadWorkspaceVersions(pkg, packageDir) {
1813
2210
  _cachedWorkspaceVersions = null;
1814
2211
  return;
1815
2212
  }
1816
- if (_cachedWorkspaceVersions?.root === root) return;
1817
2213
  const { findWorkspacePackages: findWorkspacePackages2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
1818
2214
  const pkgDirs = await findWorkspacePackages2(root);
1819
2215
  const versions = /* @__PURE__ */ new Map();
@@ -1821,7 +2217,7 @@ async function preloadWorkspaceVersions(pkg, packageDir) {
1821
2217
  pkgDirs.map(async (dir) => {
1822
2218
  try {
1823
2219
  const depPkg = JSON.parse(
1824
- await readFile7(join8(dir, "package.json"), "utf-8")
2220
+ await readFile8(join10(dir, "package.json"), "utf-8")
1825
2221
  );
1826
2222
  if (depPkg.name && depPkg.version) {
1827
2223
  versions.set(depPkg.name, depPkg.version);
@@ -1848,8 +2244,8 @@ async function preloadCatalogs(pkg, packageDir) {
1848
2244
  _cachedCatalogs = null;
1849
2245
  return;
1850
2246
  }
1851
- const workspaceFile = join8(root, "pnpm-workspace.yaml");
1852
- const mtimeMs = (await stat5(workspaceFile).catch(() => null))?.mtimeMs ?? 0;
2247
+ const workspaceFile = join10(root, "pnpm-workspace.yaml");
2248
+ const mtimeMs = (await stat6(workspaceFile).catch(() => null))?.mtimeMs ?? 0;
1853
2249
  if (_cachedCatalogs?.root === root && _cachedCatalogs.mtimeMs === mtimeMs) return;
1854
2250
  const { parseCatalogs: parseCatalogs2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
1855
2251
  const catalogs = await parseCatalogs2(root);
@@ -1870,6 +2266,7 @@ var init_publisher = __esm({
1870
2266
  init_logger();
1871
2267
  init_dry_run();
1872
2268
  init_validators();
2269
+ init_pm_detect();
1873
2270
  copyLimit = pLimit(Math.max(availableParallelism3(), 8));
1874
2271
  HOOK_TIMEOUT = parseInt(process.env.KNARR_HOOK_TIMEOUT ?? "30000", 10);
1875
2272
  PUBLISH_CONFIG_OVERRIDES = [
@@ -1888,9 +2285,54 @@ var init_publisher = __esm({
1888
2285
  });
1889
2286
 
1890
2287
  // src/utils/bin-linker.ts
1891
- import { mkdir as mkdir3, symlink, writeFile as writeFile2, chmod, rm as rm3 } from "fs/promises";
1892
- import { join as join9, relative as relative7, resolve as resolve5, sep as sep2 } from "path";
2288
+ import {
2289
+ mkdir as mkdir3,
2290
+ symlink,
2291
+ writeFile as writeFile2,
2292
+ chmod,
2293
+ rm as rm3,
2294
+ lstat,
2295
+ readFile as readFile9,
2296
+ readlink
2297
+ } from "fs/promises";
2298
+ import { join as join11, relative as relative7, resolve as resolve5, sep as sep2 } from "path";
1893
2299
  import { platform as platform2 } from "os";
2300
+ function safeInterpreter(value) {
2301
+ return value && SAFE_INTERPRETER_RE.test(value) ? value : "node";
2302
+ }
2303
+ function parseShebangInterpreter(content) {
2304
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trim();
2305
+ if (!firstLine?.startsWith("#!")) return "node";
2306
+ const command = firstLine.slice(2).trim();
2307
+ const parts = command.split(/\s+/).filter(Boolean);
2308
+ if (parts.length === 0) return "node";
2309
+ const executable = parts[0].replace(/\\/g, "/").split("/").pop();
2310
+ if (executable === "env") {
2311
+ const args = parts.slice(1);
2312
+ const interpreter = args[0] === "-S" ? args[1] : args[0];
2313
+ return safeInterpreter(interpreter);
2314
+ }
2315
+ return safeInterpreter(executable);
2316
+ }
2317
+ async function readBinInterpreter(targetAbsolute) {
2318
+ try {
2319
+ return parseShebangInterpreter(await readFile9(targetAbsolute, "utf-8"));
2320
+ } catch {
2321
+ return "node";
2322
+ }
2323
+ }
2324
+ function resolveBinPath(binDir, binName, suffix = "") {
2325
+ if (!binName || binName === "." || binName === ".." || CONTROL_CHARS_RE2.test(binName) || binName.includes("/") || binName.includes("\\") || binName.includes(":")) {
2326
+ return null;
2327
+ }
2328
+ const path = join11(binDir, `${binName}${suffix}`);
2329
+ const resolvedBinDir = resolve5(binDir);
2330
+ const resolvedPath = resolve5(path);
2331
+ if (resolvedPath !== resolvedBinDir && resolvedPath.startsWith(resolvedBinDir + sep2)) {
2332
+ return path;
2333
+ }
2334
+ return null;
2335
+ }
1894
2336
  function resolveBinEntries(pkg) {
1895
2337
  if (!pkg.bin) return {};
1896
2338
  if (typeof pkg.bin === "string") {
@@ -1902,28 +2344,46 @@ function resolveBinEntries(pkg) {
1902
2344
  async function createBinLinks(consumerPath, packageName, pkg) {
1903
2345
  const entries = resolveBinEntries(pkg);
1904
2346
  if (Object.keys(entries).length === 0) return 0;
2347
+ const binDir = join11(consumerPath, "node_modules", ".bin");
2348
+ const safeEntries = Object.entries(entries).map(([binName, binPath]) => ({
2349
+ binName,
2350
+ binPath,
2351
+ linkPath: resolveBinPath(binDir, binName),
2352
+ cmdPath: resolveBinPath(binDir, binName, ".cmd"),
2353
+ ps1Path: resolveBinPath(binDir, binName, ".ps1")
2354
+ })).filter((entry) => {
2355
+ if (!entry.linkPath || !entry.cmdPath || !entry.ps1Path) {
2356
+ consola.warn(`bin name "${entry.binName}" is not safe, skipping`);
2357
+ return false;
2358
+ }
2359
+ return true;
2360
+ });
2361
+ const packageRoot = join11(consumerPath, "node_modules", packageName);
2362
+ const resolvedPackageRoot = resolve5(packageRoot);
2363
+ const validEntries = safeEntries.filter(({ binName, binPath }) => {
2364
+ const resolvedTarget = resolve5(join11(packageRoot, binPath));
2365
+ if (resolvedTarget.startsWith(resolvedPackageRoot + sep2) || resolvedTarget === resolvedPackageRoot) {
2366
+ return true;
2367
+ }
2368
+ consola.warn(`bin "${binName}" points outside package directory, skipping`);
2369
+ return false;
2370
+ });
2371
+ if (validEntries.length === 0) return 0;
1905
2372
  if (isDryRun()) {
1906
- for (const binName of Object.keys(entries)) {
1907
- recordMutation({ type: "bin-link", path: join9(consumerPath, "node_modules", ".bin", binName), detail: packageName });
2373
+ for (const { linkPath } of validEntries) {
2374
+ recordMutation({ type: "bin-link", path: linkPath, detail: packageName });
1908
2375
  }
1909
- verbose(`[dry-run] would create ${Object.keys(entries).length} bin link(s) for ${packageName}`);
1910
- return Object.keys(entries).length;
2376
+ verbose(`[dry-run] would create ${validEntries.length} bin link(s) for ${packageName}`);
2377
+ return validEntries.length;
1911
2378
  }
1912
- const binDir = join9(consumerPath, "node_modules", ".bin");
1913
2379
  await mkdir3(binDir, { recursive: true });
1914
2380
  const isWindows = platform2() === "win32";
1915
2381
  let count = 0;
1916
- for (const [binName, binPath] of Object.entries(entries)) {
1917
- const packageRoot = join9(consumerPath, "node_modules", packageName);
1918
- const targetAbsolute = join9(packageRoot, binPath);
1919
- const resolvedTarget = resolve5(targetAbsolute);
1920
- if (!resolvedTarget.startsWith(resolve5(packageRoot) + sep2) && resolvedTarget !== resolve5(packageRoot)) {
1921
- consola.warn(`bin "${binName}" points outside package directory, skipping`);
1922
- continue;
1923
- }
2382
+ for (const { binName, binPath, linkPath, cmdPath, ps1Path } of validEntries) {
2383
+ const targetAbsolute = join11(packageRoot, binPath);
1924
2384
  const targetRelative = normalizePath(relative7(binDir, targetAbsolute));
2385
+ const interpreter = await readBinInterpreter(targetAbsolute);
1925
2386
  if (isWindows) {
1926
- const cmdPath = join9(binDir, `${binName}.cmd`);
1927
2387
  const targetWindows = targetRelative.replace(/\//g, "\\");
1928
2388
  const cmdContent = `@ECHO off\r
1929
2389
  GOTO start\r
@@ -1932,23 +2392,21 @@ SET dp0=%~dp0\r
1932
2392
  EXIT /b\r
1933
2393
  :start\r
1934
2394
  CALL :find_dp0\r
1935
- node "%dp0%\\${targetWindows}" %*\r
2395
+ ${interpreter} "%dp0%\\${targetWindows}" %*\r
1936
2396
  `;
1937
2397
  await writeFile2(cmdPath, cmdContent);
1938
- const ps1Path = join9(binDir, `${binName}.ps1`);
1939
2398
  const ps1Content = `#!/usr/bin/env pwsh
1940
2399
  $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
1941
- & node "$basedir/${targetRelative}" $args
2400
+ & ${interpreter} "$basedir/${targetRelative}" $args
1942
2401
  exit $LASTEXITCODE
1943
2402
  `;
1944
2403
  await writeFile2(ps1Path, ps1Content);
1945
- const shPath = join9(binDir, binName);
1946
2404
  const shContent = `#!/bin/sh
1947
- exec node "${targetRelative}" "$@"
2405
+ basedir=$(dirname "$0")
2406
+ exec ${interpreter} "$basedir/${targetRelative}" "$@"
1948
2407
  `;
1949
- await writeFile2(shPath, shContent);
2408
+ await writeFile2(linkPath, shContent);
1950
2409
  } else {
1951
- const linkPath = join9(binDir, binName);
1952
2410
  try {
1953
2411
  await rm3(linkPath, { force: true });
1954
2412
  } catch {
@@ -1960,7 +2418,8 @@ exec node "${targetRelative}" "$@"
1960
2418
  if (isNodeError(err) && (err.code === "EPERM" || err.code === "EACCES")) {
1961
2419
  verbose(`[bin-linker] Symlink failed (${err.code}), using shell wrapper for ${binName}`);
1962
2420
  const shContent = `#!/bin/sh
1963
- exec node "${targetRelative}" "$@"
2421
+ basedir=$(dirname "$0")
2422
+ exec ${interpreter} "$basedir/${targetRelative}" "$@"
1964
2423
  `;
1965
2424
  await writeFile2(linkPath, shContent);
1966
2425
  await chmod(linkPath, 493);
@@ -1975,26 +2434,62 @@ exec node "${targetRelative}" "$@"
1975
2434
  }
1976
2435
  async function removeBinLinks(consumerPath, pkg) {
1977
2436
  const entries = resolveBinEntries(pkg);
2437
+ const binDir = join11(consumerPath, "node_modules", ".bin");
2438
+ const safeEntries = Object.keys(entries).map((binName) => ({
2439
+ binName,
2440
+ linkPath: resolveBinPath(binDir, binName),
2441
+ cmdPath: resolveBinPath(binDir, binName, ".cmd"),
2442
+ ps1Path: resolveBinPath(binDir, binName, ".ps1")
2443
+ })).filter(
2444
+ (entry) => !!entry.linkPath && !!entry.cmdPath && !!entry.ps1Path
2445
+ );
1978
2446
  if (isDryRun()) {
1979
- for (const binName of Object.keys(entries)) {
1980
- recordMutation({ type: "bin-unlink", path: join9(consumerPath, "node_modules", ".bin", binName) });
2447
+ for (const { linkPath } of safeEntries) {
2448
+ recordMutation({ type: "bin-unlink", path: linkPath });
1981
2449
  }
1982
- verbose(`[dry-run] would remove ${Object.keys(entries).length} bin link(s)`);
2450
+ verbose(`[dry-run] would remove ${safeEntries.length} bin link(s)`);
1983
2451
  return;
1984
2452
  }
1985
- const binDir = join9(consumerPath, "node_modules", ".bin");
1986
2453
  const isWindows = platform2() === "win32";
1987
- for (const binName of Object.keys(entries)) {
2454
+ const packageRoot = join11(consumerPath, "node_modules", pkg.name);
2455
+ for (const { linkPath, cmdPath, ps1Path } of safeEntries) {
1988
2456
  try {
1989
- await rm3(join9(binDir, binName), { force: true });
2457
+ if (await binFileBelongsToPackage(binDir, linkPath, packageRoot)) {
2458
+ await rm3(linkPath, { force: true });
2459
+ }
1990
2460
  if (isWindows) {
1991
- await rm3(join9(binDir, `${binName}.cmd`), { force: true });
1992
- await rm3(join9(binDir, `${binName}.ps1`), { force: true });
2461
+ if (await binFileBelongsToPackage(binDir, cmdPath, packageRoot)) {
2462
+ await rm3(cmdPath, { force: true });
2463
+ }
2464
+ if (await binFileBelongsToPackage(binDir, ps1Path, packageRoot)) {
2465
+ await rm3(ps1Path, { force: true });
2466
+ }
1993
2467
  }
1994
2468
  } catch {
1995
2469
  }
1996
2470
  }
1997
2471
  }
2472
+ async function binFileBelongsToPackage(binDir, linkPath, packageRoot) {
2473
+ let fileStat;
2474
+ try {
2475
+ fileStat = await lstat(linkPath);
2476
+ } catch (err) {
2477
+ if (isNodeError(err) && err.code === "ENOENT") return false;
2478
+ throw err;
2479
+ }
2480
+ const resolvedPackageRoot = resolve5(packageRoot);
2481
+ if (fileStat.isSymbolicLink()) {
2482
+ const target = await readlink(linkPath);
2483
+ const resolvedTarget = resolve5(binDir, target);
2484
+ return resolvedTarget === resolvedPackageRoot || resolvedTarget.startsWith(resolvedPackageRoot + sep2);
2485
+ }
2486
+ if (!fileStat.isFile()) return false;
2487
+ const content = await readFile9(linkPath, "utf-8");
2488
+ const relPackageRoot = normalizePath(relative7(binDir, packageRoot));
2489
+ const relWindowsPackageRoot = relPackageRoot.replace(/\//g, "\\");
2490
+ return content.includes(`${relPackageRoot}/`) || content.includes(`${relPackageRoot}"`) || content.includes(`${relWindowsPackageRoot}\\`) || content.includes(`${relWindowsPackageRoot}"`);
2491
+ }
2492
+ var CONTROL_CHARS_RE2, SAFE_INTERPRETER_RE;
1998
2493
  var init_bin_linker = __esm({
1999
2494
  "src/utils/bin-linker.ts"() {
2000
2495
  "use strict";
@@ -2003,106 +2498,31 @@ var init_bin_linker = __esm({
2003
2498
  init_logger();
2004
2499
  init_paths();
2005
2500
  init_dry_run();
2501
+ CONTROL_CHARS_RE2 = /[\x00-\x1F\x7F]/;
2502
+ SAFE_INTERPRETER_RE = /^[A-Za-z0-9._+-]+$/;
2006
2503
  }
2007
2504
  });
2008
2505
 
2009
- // src/utils/pm-detect.ts
2010
- import { readFile as readFile8, stat as stat6 } from "fs/promises";
2011
- import { join as join10, dirname as dirname5 } from "path";
2012
- async function readPackageManagerField(dir) {
2013
- try {
2014
- const raw = await readFile8(join10(dir, "package.json"), "utf-8");
2015
- const pkg = JSON.parse(raw);
2016
- if (typeof pkg.packageManager !== "string") return null;
2017
- const name = pkg.packageManager.split("@")[0];
2018
- return VALID_PMS.has(name) ? name : null;
2019
- } catch {
2020
- return null;
2506
+ // src/utils/bundler-detect.ts
2507
+ import { join as join12 } from "path";
2508
+ async function detectAllBundlers(projectDir) {
2509
+ const checks = BUNDLER_CONFIGS.flatMap(
2510
+ ([type, configFiles]) => configFiles.map(async (configFile) => ({
2511
+ type,
2512
+ configFile: join12(projectDir, configFile),
2513
+ found: await exists(join12(projectDir, configFile))
2514
+ }))
2515
+ );
2516
+ const all = await Promise.all(checks);
2517
+ const seen = /* @__PURE__ */ new Set();
2518
+ const results = [];
2519
+ for (const { type, configFile, found } of all) {
2520
+ if (found && !seen.has(type)) {
2521
+ seen.add(type);
2522
+ results.push({ type, configFile });
2523
+ }
2021
2524
  }
2022
- }
2023
- async function detectPackageManager(projectDir) {
2024
- let dir = projectDir;
2025
- for (; ; ) {
2026
- const fromField = await readPackageManagerField(dir);
2027
- if (fromField) return fromField;
2028
- const results = await Promise.all(
2029
- LOCKFILES.map(async ([lockfile, pm]) => {
2030
- try {
2031
- await stat6(join10(dir, lockfile));
2032
- return pm;
2033
- } catch {
2034
- return null;
2035
- }
2036
- })
2037
- );
2038
- const found = results.find((pm) => pm !== null);
2039
- if (found) return found;
2040
- const parent = dirname5(dir);
2041
- if (parent === dir) return "npm";
2042
- dir = parent;
2043
- }
2044
- }
2045
- async function detectYarnNodeLinker(projectDir) {
2046
- let dir = projectDir;
2047
- for (; ; ) {
2048
- let content;
2049
- try {
2050
- content = await readFile8(join10(dir, ".yarnrc.yml"), "utf-8");
2051
- } catch {
2052
- const parent = dirname5(dir);
2053
- if (parent === dir) return null;
2054
- dir = parent;
2055
- continue;
2056
- }
2057
- for (const line of content.split("\n")) {
2058
- const trimmed = line.trim();
2059
- if (trimmed.startsWith("#") || !trimmed.includes("nodeLinker")) continue;
2060
- const match = trimmed.match(/^nodeLinker:\s*(.+)$/);
2061
- if (match) {
2062
- const value = match[1].trim().replace(/^["']|["']$/g, "");
2063
- if (value === "node-modules" || value === "pnpm" || value === "pnp") {
2064
- return value;
2065
- }
2066
- }
2067
- }
2068
- return null;
2069
- }
2070
- }
2071
- var VALID_PMS, LOCKFILES;
2072
- var init_pm_detect = __esm({
2073
- "src/utils/pm-detect.ts"() {
2074
- "use strict";
2075
- VALID_PMS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
2076
- LOCKFILES = [
2077
- ["pnpm-lock.yaml", "pnpm"],
2078
- ["bun.lockb", "bun"],
2079
- ["bun.lock", "bun"],
2080
- ["yarn.lock", "yarn"],
2081
- ["package-lock.json", "npm"]
2082
- ];
2083
- }
2084
- });
2085
-
2086
- // src/utils/bundler-detect.ts
2087
- import { join as join11 } from "path";
2088
- async function detectAllBundlers(projectDir) {
2089
- const checks = BUNDLER_CONFIGS.flatMap(
2090
- ([type, configFiles]) => configFiles.map(async (configFile) => ({
2091
- type,
2092
- configFile: join11(projectDir, configFile),
2093
- found: await exists(join11(projectDir, configFile))
2094
- }))
2095
- );
2096
- const all = await Promise.all(checks);
2097
- const seen = /* @__PURE__ */ new Set();
2098
- const results = [];
2099
- for (const { type, configFile, found } of all) {
2100
- if (found && !seen.has(type)) {
2101
- seen.add(type);
2102
- results.push({ type, configFile });
2103
- }
2104
- }
2105
- return results;
2525
+ return results;
2106
2526
  }
2107
2527
  var BUNDLER_CONFIGS;
2108
2528
  var init_bundler_detect = __esm({
@@ -2120,27 +2540,29 @@ var init_bundler_detect = __esm({
2120
2540
  });
2121
2541
 
2122
2542
  // src/utils/bundler-cache.ts
2123
- import { join as join12 } from "path";
2543
+ import { join as join13 } from "path";
2124
2544
  async function invalidateBundlerCache(consumerPath) {
2125
2545
  if (isDryRun()) {
2126
2546
  verbose(`[dry-run] would invalidate bundler caches for ${consumerPath}`);
2127
2547
  recordMutation({ type: "cache-invalidate", path: consumerPath });
2128
- return;
2548
+ return 0;
2129
2549
  }
2130
2550
  let bundlers = bundlerCache.get(consumerPath);
2131
2551
  if (!bundlers) {
2132
2552
  bundlers = await detectAllBundlers(consumerPath);
2133
2553
  bundlerCache.set(consumerPath, bundlers);
2134
2554
  }
2555
+ let invalidated = 0;
2135
2556
  for (const bundler of bundlers) {
2136
2557
  if (!bundler.type) continue;
2137
2558
  const dirs = CACHE_DIRS[bundler.type];
2138
2559
  if (!dirs) continue;
2139
2560
  for (const dir of dirs) {
2140
- const cacheDir = join12(consumerPath, dir);
2561
+ const cacheDir = join13(consumerPath, dir);
2141
2562
  if (await exists(cacheDir)) {
2142
2563
  try {
2143
2564
  await removeDir(cacheDir);
2565
+ invalidated++;
2144
2566
  verbose(`[inject] Invalidated ${bundler.type} cache: ${dir}`);
2145
2567
  } catch {
2146
2568
  verbose(`[inject] Could not clear ${bundler.type} cache: ${dir} (locked?)`);
@@ -2148,6 +2570,7 @@ async function invalidateBundlerCache(consumerPath) {
2148
2570
  }
2149
2571
  }
2150
2572
  }
2573
+ return invalidated;
2151
2574
  }
2152
2575
  var CACHE_DIRS, bundlerCache;
2153
2576
  var init_bundler_cache = __esm({
@@ -2166,59 +2589,78 @@ var init_bundler_cache = __esm({
2166
2589
  });
2167
2590
 
2168
2591
  // src/core/injector.ts
2169
- import { readFile as readFile9, readdir as readdir6, realpath, stat as stat7 } from "fs/promises";
2170
- import { join as join13, resolve as resolve6 } from "path";
2592
+ import { lstat as lstat2, readFile as readFile10, readdir as readdir6, realpath, rm as rm4, symlink as symlink2 } from "fs/promises";
2593
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join14, relative as relative8, resolve as resolve6 } from "path";
2594
+ import { platform as platform3 } from "os";
2171
2595
  async function inject(storeEntry, consumerPath, pm, options = {}) {
2172
- const targetDir = await resolveTargetDir(
2596
+ const targetDir = await resolveInjectionTarget(
2173
2597
  consumerPath,
2174
2598
  storeEntry.name,
2175
2599
  pm,
2176
- storeEntry.version
2600
+ storeEntry.version,
2601
+ { repairMissingLink: true }
2177
2602
  );
2178
2603
  verbose(`[inject] ${storeEntry.name}@${storeEntry.version} \u2192 ${targetDir}`);
2179
2604
  await ensureDir(targetDir);
2605
+ const previousPkg = await readPackageJson(targetDir);
2180
2606
  const { copied, removed, skipped } = await incrementalCopy(
2181
2607
  storeEntry.packageDir,
2182
2608
  targetDir,
2183
2609
  { force: options.force }
2184
2610
  );
2185
2611
  verbose(`[inject] ${copied} copied, ${removed} removed, ${skipped} skipped`);
2186
- if (copied > 0 || removed > 0) {
2187
- await invalidateBundlerCache(consumerPath);
2188
- }
2612
+ const cacheInvalidations = copied > 0 || removed > 0 ? await invalidateBundlerCache(consumerPath) : 0;
2189
2613
  const pkg = await readPackageJson(storeEntry.packageDir);
2614
+ if (previousPkg) {
2615
+ await removeBinLinks(consumerPath, previousPkg);
2616
+ }
2190
2617
  const binLinks = pkg ? await createBinLinks(consumerPath, storeEntry.name, pkg) : 0;
2191
2618
  if (binLinks > 0) {
2192
2619
  verbose(`[inject] Created ${binLinks} bin link(s)`);
2193
2620
  }
2194
- return { copied, removed, skipped, binLinks };
2621
+ return { copied, removed, skipped, binLinks, cacheInvalidations };
2195
2622
  }
2196
2623
  async function backupExisting(consumerPath, packageName, pm) {
2197
- const installedDir = await resolveTargetDir(consumerPath, packageName, pm);
2624
+ const installedDir = await resolveInjectionTarget(consumerPath, packageName, pm);
2198
2625
  if (!await exists(installedDir)) return false;
2199
2626
  const backupDir = getConsumerBackupPath(consumerPath, packageName);
2200
2627
  await removeDir(backupDir);
2201
2628
  await copyDir(installedDir, backupDir);
2202
2629
  return true;
2203
2630
  }
2204
- async function restoreBackup(consumerPath, packageName, pm) {
2631
+ async function restoreBackup(consumerPath, packageName, pm, version) {
2205
2632
  const backupDir = getConsumerBackupPath(consumerPath, packageName);
2206
2633
  if (!await exists(backupDir)) return false;
2207
- const targetDir = await resolveTargetDir(consumerPath, packageName, pm);
2634
+ const targetDir = await resolveInjectionTarget(consumerPath, packageName, pm, version);
2635
+ const currentPkg = await readPackageJson(targetDir);
2636
+ if (currentPkg) {
2637
+ await removeBinLinks(consumerPath, currentPkg);
2638
+ }
2208
2639
  await removeDir(targetDir);
2209
2640
  await copyDir(backupDir, targetDir);
2641
+ const restoredPkg = await readPackageJson(isDryRun() ? backupDir : targetDir);
2642
+ if (restoredPkg) {
2643
+ const binLinks = await createBinLinks(consumerPath, packageName, restoredPkg);
2644
+ if (binLinks > 0) {
2645
+ verbose(`[restore] Restored ${binLinks} original bin link(s) for ${packageName}`);
2646
+ }
2647
+ }
2210
2648
  await removeDir(backupDir);
2211
2649
  return true;
2212
2650
  }
2213
- async function removeInjected(consumerPath, packageName, pm) {
2214
- const targetDir = await resolveTargetDir(consumerPath, packageName, pm);
2651
+ async function removeInjected(consumerPath, packageName, pm, version) {
2652
+ const directPath = getNodeModulesPackagePath(consumerPath, packageName);
2653
+ const targetDir = await resolveInjectionTarget(consumerPath, packageName, pm, version);
2215
2654
  const pkg = await readPackageJson(targetDir);
2216
2655
  if (pkg) {
2217
2656
  await removeBinLinks(consumerPath, pkg);
2218
2657
  }
2219
2658
  await removeDir(targetDir);
2659
+ if (resolve6(targetDir) !== resolve6(directPath)) {
2660
+ await removeDir(directPath);
2661
+ }
2220
2662
  }
2221
- async function checkMissingDeps(storeEntry, consumerPath) {
2663
+ async function checkMissingDeps(storeEntry, consumerPath, pm) {
2222
2664
  const pkg = await readPackageJson(storeEntry.packageDir);
2223
2665
  if (!pkg) return [];
2224
2666
  const allDeps = {
@@ -2234,75 +2676,455 @@ async function checkMissingDeps(storeEntry, consumerPath) {
2234
2676
  const results = await Promise.all(
2235
2677
  depNames.map(async (dep) => ({
2236
2678
  dep,
2237
- installed: await exists(join13(consumerPath, "node_modules", dep))
2679
+ installed: await isDependencyInstalledForPackage(dep, storeEntry, consumerPath, pm)
2238
2680
  }))
2239
2681
  );
2240
2682
  return results.filter((r) => !r.installed).map((r) => r.dep);
2241
2683
  }
2242
- async function resolveTargetDir(consumerPath, packageName, pm, version) {
2684
+ async function isDependencyInstalledForPackage(dependencyName, storeEntry, consumerPath, pm) {
2685
+ if (await exists(join14(consumerPath, "node_modules", dependencyName))) {
2686
+ return true;
2687
+ }
2688
+ if (!pm) return false;
2689
+ const targetDir = await resolveInjectionTarget(
2690
+ consumerPath,
2691
+ storeEntry.name,
2692
+ pm,
2693
+ storeEntry.version,
2694
+ { warnOnFallback: false }
2695
+ );
2696
+ return dependencyVisibleFromDirectory(targetDir, dependencyName);
2697
+ }
2698
+ async function dependencyVisibleFromDirectory(startDir, dependencyName) {
2699
+ const realStart = await realpath(startDir).catch((err) => {
2700
+ if (isNodeError(err) && err.code === "ENOENT") return startDir;
2701
+ throw err;
2702
+ });
2703
+ let dir = realStart;
2704
+ for (; ; ) {
2705
+ if (await exists(join14(dir, "node_modules", dependencyName))) {
2706
+ return true;
2707
+ }
2708
+ const parent = dirname6(dir);
2709
+ if (parent === dir) return false;
2710
+ dir = parent;
2711
+ }
2712
+ }
2713
+ async function resolveInjectionTarget(consumerPath, packageName, pm, version, options = {}) {
2243
2714
  const directPath = getNodeModulesPackagePath(consumerPath, packageName);
2244
- const needsSymlinkResolution = pm === "pnpm" || pm === "yarn" && await detectYarnNodeLinker(consumerPath) === "pnpm";
2245
- if (!needsSymlinkResolution) {
2715
+ const currentPm = await detectPackageManagerInfo(consumerPath);
2716
+ const useTrackedPm = currentPm.source === "default";
2717
+ const effectiveYarn = currentPm.packageManager === "yarn" || useTrackedPm && pm === "yarn";
2718
+ if (await hasYarnPnpMarkers(consumerPath) || effectiveYarn && await isYarnPnpProject(consumerPath)) {
2719
+ throw new Error(
2720
+ "Yarn PnP mode is not compatible with Knarr. Set `nodeLinker: node-modules` or `nodeLinker: pnpm` in .yarnrc.yml, then run `yarn install`."
2721
+ );
2722
+ }
2723
+ const yarnLinker = effectiveYarn ? await detectYarnNodeLinker(consumerPath) : null;
2724
+ const storeKind = getEffectiveStoreKind(
2725
+ pm,
2726
+ currentPm.packageManager,
2727
+ currentPm.source,
2728
+ yarnLinker
2729
+ );
2730
+ const effectiveBun = currentPm.packageManager === "bun" || useTrackedPm && pm === "bun";
2731
+ if (!storeKind) {
2732
+ if (effectiveBun) {
2733
+ return resolveBunTarget(consumerPath, directPath, packageName, version);
2734
+ }
2246
2735
  return directPath;
2247
2736
  }
2737
+ const virtualStoreDirs = storeKind === "pnpm" ? await getPnpmVirtualStoreDirs(consumerPath) : await getYarnPnpmStoreDirs(consumerPath);
2248
2738
  try {
2249
- const realPath = await resolveRealPath(directPath);
2250
- if (realPath !== resolve6(directPath)) {
2251
- verbose(`[inject] pnpm: resolved symlink \u2192 ${realPath}`);
2739
+ const realPath = await resolvePackageEntrySymlink(directPath);
2740
+ if (realPath) {
2741
+ const valid = storeKind === "pnpm" ? await isPnpmVirtualStorePackageRealPath(virtualStoreDirs, packageName, realPath) : await isYarnPnpmStorePackageRealPath(virtualStoreDirs, realPath);
2742
+ if (!valid) {
2743
+ throw new Error(
2744
+ `Refusing to inject ${packageName}: node_modules entry resolves outside a configured ${storeKindLabel(storeKind)} virtual store (${realPath})`
2745
+ );
2746
+ }
2747
+ await validateResolvedPackageIdentity(realPath, packageName, version, storeKind);
2748
+ verbose(`[inject] ${storeKindLabel(storeKind)}: resolved symlink \u2192 ${realPath}`);
2252
2749
  return realPath;
2253
2750
  }
2254
2751
  } catch (err) {
2255
- if (isNodeError(err) && err.code !== "ENOENT") {
2256
- consola.debug(`pnpm symlink resolution error: ${err instanceof Error ? err.message : String(err)}`);
2257
- }
2258
- }
2259
- const pnpmDir = join13(consumerPath, "node_modules", ".pnpm");
2260
- if (await exists(pnpmDir)) {
2261
- verbose(`[inject] pnpm: scanning .pnpm/ for ${packageName}`);
2262
- const encodedName = packageName.replaceAll("/", "+");
2263
- if (version) {
2264
- const exactEntry = `${encodedName}@${version}`;
2265
- const candidate = join13(pnpmDir, exactEntry, "node_modules", packageName);
2266
- if (await exists(candidate)) {
2267
- verbose(`[inject] pnpm: exact version match in .pnpm/ \u2192 ${candidate}`);
2268
- return candidate;
2752
+ if (isNodeError(err) && err.code === "ENOENT") {
2753
+ } else {
2754
+ if (isNodeError(err)) {
2755
+ verbose(`${storeKindLabel(storeKind)} symlink resolution error: ${err instanceof Error ? err.message : String(err)}`);
2269
2756
  }
2757
+ throw err;
2270
2758
  }
2271
- const entries = await readdir6(pnpmDir);
2272
- for (const entry of entries) {
2273
- if (entry.startsWith(encodedName + "@")) {
2274
- const candidate = join13(
2275
- pnpmDir,
2276
- entry,
2277
- "node_modules",
2278
- packageName
2759
+ }
2760
+ const existingStoreDirs = [];
2761
+ for (const storeDir of virtualStoreDirs) {
2762
+ if (await exists(storeDir)) {
2763
+ existingStoreDirs.push(storeDir);
2764
+ }
2765
+ }
2766
+ if (existingStoreDirs.length > 0) {
2767
+ if (!await isDeclaredConsumerDependency(consumerPath, packageName)) {
2768
+ verbose(
2769
+ `[inject] ${storeKindLabel(storeKind)}: ${packageName} is not declared by the consumer, skipping virtual store scan`
2770
+ );
2771
+ return directPath;
2772
+ }
2773
+ if (storeKind === "yarn-pnpm") {
2774
+ const matches = [];
2775
+ for (const storeDir of existingStoreDirs) {
2776
+ verbose(`[inject] yarn-pnpm: scanning ${relative8(consumerPath, storeDir) || "."} for ${packageName}`);
2777
+ const candidate2 = await resolveYarnPnpmCandidate(
2778
+ storeDir,
2779
+ packageName,
2780
+ version
2279
2781
  );
2280
- if (await exists(candidate)) {
2281
- verbose(`[inject] pnpm: found in .pnpm/ \u2192 ${candidate}`);
2782
+ if (candidate2) {
2783
+ matches.push(candidate2);
2784
+ }
2785
+ }
2786
+ const candidate = requireSingleVirtualStoreCandidate(packageName, storeKind, matches);
2787
+ if (candidate) {
2788
+ verbose(`[inject] yarn-pnpm: found in virtual store \u2192 ${candidate}`);
2789
+ if (options.repairMissingLink) {
2790
+ await repairMissingVirtualStoreLink(directPath, candidate, packageName, storeKind);
2791
+ }
2792
+ return candidate;
2793
+ }
2794
+ }
2795
+ if (storeKind === "pnpm") {
2796
+ const encodedName = packageName.replaceAll("/", "+");
2797
+ for (const pnpmDir of existingStoreDirs) {
2798
+ verbose(`[inject] pnpm: scanning ${relative8(consumerPath, pnpmDir) || "."} for ${packageName}`);
2799
+ if (version) {
2800
+ const exactEntry = `${encodedName}@${version}`;
2801
+ const candidate2 = await resolvePnpmCandidate(
2802
+ pnpmDir,
2803
+ packageName,
2804
+ version,
2805
+ join14(pnpmDir, exactEntry, "node_modules", packageName)
2806
+ );
2807
+ if (candidate2) {
2808
+ verbose(`[inject] pnpm: exact version match in virtual store \u2192 ${candidate2}`);
2809
+ if (options.repairMissingLink) {
2810
+ await repairMissingVirtualStoreLink(directPath, candidate2, packageName, storeKind);
2811
+ }
2812
+ return candidate2;
2813
+ }
2814
+ }
2815
+ const entries = await readdir6(pnpmDir);
2816
+ const matches = [];
2817
+ for (const entry of entries) {
2818
+ if (matchesPnpmPackageEntry(encodedName, version, entry)) {
2819
+ const candidate2 = await resolvePnpmCandidate(
2820
+ pnpmDir,
2821
+ packageName,
2822
+ version,
2823
+ join14(pnpmDir, entry, "node_modules", packageName)
2824
+ );
2825
+ if (candidate2) {
2826
+ matches.push(candidate2);
2827
+ }
2828
+ }
2829
+ }
2830
+ const candidate = requireSingleVirtualStoreCandidate(packageName, storeKind, matches);
2831
+ if (candidate) {
2832
+ verbose(`[inject] pnpm: found in virtual store \u2192 ${candidate}`);
2833
+ if (options.repairMissingLink) {
2834
+ await repairMissingVirtualStoreLink(directPath, candidate, packageName, storeKind);
2835
+ }
2282
2836
  return candidate;
2283
2837
  }
2284
2838
  }
2285
2839
  }
2286
2840
  }
2287
- consola.warn(
2288
- `pnpm: Could not find ${packageName} in .pnpm/ virtual store, using direct node_modules path. If this causes issues, run 'pnpm install' to rebuild the virtual store, then 'knarr add' again.`
2289
- );
2841
+ if (options.warnOnFallback !== false) {
2842
+ consola.warn(
2843
+ `${storeKindLabel(storeKind)}: Could not find ${packageName} in a configured virtual store, using direct node_modules path. If this causes issues, run your package manager's install command to rebuild the virtual store, then 'knarr add' again.`
2844
+ );
2845
+ }
2290
2846
  return directPath;
2291
2847
  }
2292
- async function resolveRealPath(linkPath) {
2848
+ function storeKindLabel(kind) {
2849
+ return kind === "pnpm" ? "pnpm" : "Yarn pnpm-linker";
2850
+ }
2851
+ async function resolveBunTarget(consumerPath, directPath, packageName, version) {
2293
2852
  try {
2294
- await stat7(linkPath);
2295
- return await realpath(linkPath);
2853
+ const linkStat = await lstat2(directPath);
2854
+ if (!linkStat.isSymbolicLink()) return directPath;
2296
2855
  } catch (err) {
2297
- if (isNodeError(err) && err.code === "ENOENT") {
2298
- return resolve6(linkPath);
2856
+ if (isNodeError(err) && err.code === "ENOENT") return directPath;
2857
+ throw err;
2858
+ }
2859
+ const realPath = await realpath(directPath);
2860
+ const bunStoreDir = join14(consumerPath, "node_modules", ".bun");
2861
+ const bunStoreRoot = await realpath(bunStoreDir).catch((err) => {
2862
+ if (isNodeError(err) && err.code === "ENOENT") return bunStoreDir;
2863
+ throw err;
2864
+ });
2865
+ if (!isPathInside(bunStoreRoot, realPath)) {
2866
+ throw new Error(
2867
+ `Refusing to inject ${packageName}: Bun target resolves outside the local node_modules/.bun store (${realPath})`
2868
+ );
2869
+ }
2870
+ await validateResolvedPackageIdentity(realPath, packageName, version, "Bun isolated");
2871
+ verbose(`[inject] Bun isolated: resolved symlink \u2192 ${realPath}`);
2872
+ return realPath;
2873
+ }
2874
+ function isPathInside(parentDir, childPath) {
2875
+ const rel = relative8(resolve6(parentDir), resolve6(childPath));
2876
+ return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute2(rel);
2877
+ }
2878
+ function getEffectiveStoreKind(storedPm, currentPm, currentSource, yarnLinker) {
2879
+ if (currentSource !== "default") {
2880
+ if (currentPm === "yarn") {
2881
+ return yarnLinker === "pnpm" ? "yarn-pnpm" : null;
2299
2882
  }
2883
+ return currentPm === "pnpm" ? "pnpm" : null;
2884
+ }
2885
+ if (storedPm === "yarn") {
2886
+ return yarnLinker === "pnpm" ? "yarn-pnpm" : null;
2887
+ }
2888
+ return storedPm === "pnpm" ? "pnpm" : null;
2889
+ }
2890
+ async function resolvePackageEntrySymlink(linkPath) {
2891
+ const linkStat = await lstat2(linkPath);
2892
+ if (!linkStat.isSymbolicLink()) return null;
2893
+ return realpath(linkPath);
2894
+ }
2895
+ async function validateResolvedPackageIdentity(targetDir, packageName, version, storeKind) {
2896
+ const label = storeKind === "Bun isolated" ? storeKind : storeKindLabel(storeKind);
2897
+ const pkg = await readPackageJson(targetDir);
2898
+ if (!pkg) return;
2899
+ if (pkg.name && pkg.name !== packageName) {
2900
+ throw new Error(
2901
+ `Refusing to inject ${packageName}: ${label} target contains package "${pkg.name}"`
2902
+ );
2903
+ }
2904
+ if (version && pkg.version && pkg.version !== version) {
2905
+ throw new Error(
2906
+ `Refusing to inject ${packageName}@${version}: ${label} target contains version ${pkg.version}`
2907
+ );
2908
+ }
2909
+ }
2910
+ async function repairMissingVirtualStoreLink(directPath, targetDir, packageName, storeKind) {
2911
+ let removeDanglingSymlink = false;
2912
+ try {
2913
+ const directStat = await lstat2(directPath);
2914
+ if (!directStat.isSymbolicLink()) return;
2915
+ removeDanglingSymlink = true;
2916
+ } catch (err) {
2917
+ if (!isNodeError(err) || err.code !== "ENOENT") throw err;
2918
+ }
2919
+ const linkParent = dirname6(directPath);
2920
+ if (isDryRun()) {
2921
+ const targetRelative2 = relative8(linkParent, targetDir);
2922
+ verbose(
2923
+ `[inject] ${storeKindLabel(storeKind)}: would restore node_modules symlink for ${packageName} \u2192 ${targetRelative2}`
2924
+ );
2925
+ recordMutation({
2926
+ type: "write",
2927
+ path: directPath,
2928
+ dest: targetDir,
2929
+ detail: `${storeKindLabel(storeKind)} package symlink`
2930
+ });
2931
+ return;
2932
+ }
2933
+ const isWindows = platform3() === "win32";
2934
+ await ensureDir(linkParent);
2935
+ const linkParentReal = await realpath(linkParent).catch(() => linkParent);
2936
+ const targetRelative = relative8(linkParentReal, targetDir);
2937
+ verbose(
2938
+ `[inject] ${storeKindLabel(storeKind)}: restoring node_modules symlink for ${packageName} \u2192 ${targetRelative}`
2939
+ );
2940
+ if (removeDanglingSymlink) {
2941
+ await rm4(directPath, { force: true });
2942
+ }
2943
+ await symlink2(
2944
+ isWindows ? targetDir : targetRelative,
2945
+ directPath,
2946
+ isWindows ? "junction" : "dir"
2947
+ );
2948
+ }
2949
+ async function resolvePnpmCandidate(pnpmDir, packageName, version, candidatePath) {
2950
+ if (!isPnpmVirtualStorePackagePathFromRoot(pnpmDir, packageName, candidatePath)) {
2951
+ return null;
2952
+ }
2953
+ try {
2954
+ const [pnpmRoot, realPath] = await Promise.all([
2955
+ realpath(pnpmDir),
2956
+ realpath(candidatePath)
2957
+ ]);
2958
+ if (!isPnpmVirtualStorePackagePathFromRoot(pnpmRoot, packageName, realPath)) {
2959
+ throw new Error(
2960
+ `Refusing to inject ${packageName}: virtual store candidate resolves outside a configured pnpm virtual store (${realPath})`
2961
+ );
2962
+ }
2963
+ await validateResolvedPackageIdentity(realPath, packageName, version, "pnpm");
2964
+ return realPath;
2965
+ } catch (err) {
2966
+ if (isNodeError(err) && err.code === "ENOENT") return null;
2300
2967
  throw err;
2301
2968
  }
2302
2969
  }
2970
+ async function resolveYarnPnpmCandidate(storeDir, packageName, version) {
2971
+ const entries = await readdir6(storeDir);
2972
+ const matches = [];
2973
+ for (const entry of entries) {
2974
+ const candidatePath = join14(storeDir, entry, "package");
2975
+ const pkg = await readPackageJson(candidatePath);
2976
+ if (!pkg || pkg.name !== packageName) continue;
2977
+ if (version && pkg.version !== version) continue;
2978
+ const [storeRoot, realPath] = await Promise.all([
2979
+ realpath(storeDir),
2980
+ realpath(candidatePath)
2981
+ ]);
2982
+ if (!isYarnPnpmStorePackagePathFromRoot(storeRoot, realPath)) {
2983
+ throw new Error(
2984
+ `Refusing to inject ${packageName}: virtual store candidate resolves outside a configured Yarn pnpm-linker virtual store (${realPath})`
2985
+ );
2986
+ }
2987
+ matches.push(realPath);
2988
+ }
2989
+ return requireSingleVirtualStoreCandidate(packageName, "yarn-pnpm", matches);
2990
+ }
2991
+ function matchesPnpmPackageEntry(encodedName, version, entry) {
2992
+ if (!version) return entry.startsWith(`${encodedName}@`);
2993
+ const exactEntry = `${encodedName}@${version}`;
2994
+ return entry === exactEntry || entry.startsWith(`${exactEntry}_`);
2995
+ }
2996
+ function isInside(root, target) {
2997
+ const rel = relative8(resolve6(root), resolve6(target));
2998
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
2999
+ }
3000
+ function requireSingleVirtualStoreCandidate(packageName, storeKind, matches) {
3001
+ if (matches.length === 0) return null;
3002
+ if (matches.length === 1) return matches[0];
3003
+ throw new Error(
3004
+ `Refusing to repair ${packageName}: multiple ${storeKindLabel(storeKind)} virtual-store entries match. Run your package manager's install command, then try again.`
3005
+ );
3006
+ }
3007
+ async function getPnpmVirtualStoreDirs(consumerPath) {
3008
+ const nodeModulesDir = join14(consumerPath, "node_modules");
3009
+ const dirs = [];
3010
+ const defaultDir = join14(nodeModulesDir, ".pnpm");
3011
+ if (await isLocalVirtualStoreDir(consumerPath, defaultDir)) {
3012
+ dirs.push(defaultDir);
3013
+ }
3014
+ const configured = await readConfiguredPnpmVirtualStoreDir(nodeModulesDir);
3015
+ if (!configured) return dirs;
3016
+ const configuredDir = isAbsolute2(configured) ? configured : resolve6(nodeModulesDir, configured);
3017
+ if (!await isLocalVirtualStoreDir(consumerPath, configuredDir)) {
3018
+ verbose(`[inject] pnpm: ignoring external virtualStoreDir ${configuredDir}`);
3019
+ return dirs;
3020
+ }
3021
+ if (!dirs.some((dir) => resolve6(dir) === resolve6(configuredDir))) {
3022
+ dirs.push(configuredDir);
3023
+ }
3024
+ return dirs;
3025
+ }
3026
+ async function getYarnPnpmStoreDirs(consumerPath) {
3027
+ const nodeModulesDir = join14(consumerPath, "node_modules");
3028
+ const dirs = [];
3029
+ const defaultDir = join14(nodeModulesDir, ".store");
3030
+ if (await isLocalVirtualStoreDir(consumerPath, defaultDir)) {
3031
+ dirs.push(defaultDir);
3032
+ }
3033
+ const configured = await detectYarnPnpmStoreFolder(consumerPath);
3034
+ if (!configured) return dirs;
3035
+ const configuredDir = isAbsolute2(configured) ? configured : resolve6(consumerPath, configured);
3036
+ if (!await isLocalVirtualStoreDir(consumerPath, configuredDir)) {
3037
+ verbose(`[inject] yarn-pnpm: ignoring external pnpmStoreFolder ${configuredDir}`);
3038
+ return dirs;
3039
+ }
3040
+ if (!dirs.some((dir) => resolve6(dir) === resolve6(configuredDir))) {
3041
+ dirs.push(configuredDir);
3042
+ }
3043
+ return dirs;
3044
+ }
3045
+ async function isLocalVirtualStoreDir(consumerPath, storeDir) {
3046
+ const consumerRoot = await realpath(consumerPath).catch(() => resolve6(consumerPath));
3047
+ const storeRoot = await realpath(storeDir).catch((err) => {
3048
+ if (isNodeError(err) && err.code === "ENOENT") return resolve6(storeDir);
3049
+ throw err;
3050
+ });
3051
+ return isInside(consumerRoot, storeRoot);
3052
+ }
3053
+ async function readConfiguredPnpmVirtualStoreDir(nodeModulesDir) {
3054
+ try {
3055
+ const content = await readFile10(join14(nodeModulesDir, ".modules.yaml"), "utf-8");
3056
+ return parsePnpmVirtualStoreDir(content);
3057
+ } catch (err) {
3058
+ if (isNodeError(err) && err.code === "ENOENT") return null;
3059
+ throw err;
3060
+ }
3061
+ }
3062
+ function parsePnpmVirtualStoreDir(content) {
3063
+ try {
3064
+ const parsed = JSON.parse(content);
3065
+ if (typeof parsed.virtualStoreDir === "string" && parsed.virtualStoreDir.trim()) {
3066
+ return parsed.virtualStoreDir.trim();
3067
+ }
3068
+ } catch {
3069
+ }
3070
+ const match = content.match(/^virtualStoreDir:\s*(.+?)\s*$/m);
3071
+ if (!match) return null;
3072
+ let value = match[1].trim().replace(/\s+#.*$/, "");
3073
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
3074
+ value = value.slice(1, -1);
3075
+ }
3076
+ return value || null;
3077
+ }
3078
+ async function isPnpmVirtualStorePackageRealPath(pnpmDirs, packageName, targetPath) {
3079
+ for (const pnpmDir of pnpmDirs) {
3080
+ try {
3081
+ const pnpmRoot = await realpath(pnpmDir);
3082
+ if (isPnpmVirtualStorePackagePathFromRoot(pnpmRoot, packageName, targetPath)) {
3083
+ return true;
3084
+ }
3085
+ } catch (err) {
3086
+ if (isNodeError(err) && err.code === "ENOENT") continue;
3087
+ throw err;
3088
+ }
3089
+ }
3090
+ return false;
3091
+ }
3092
+ async function isYarnPnpmStorePackageRealPath(storeDirs, targetPath) {
3093
+ for (const storeDir of storeDirs) {
3094
+ try {
3095
+ const storeRoot = await realpath(storeDir);
3096
+ if (isYarnPnpmStorePackagePathFromRoot(storeRoot, targetPath)) {
3097
+ return true;
3098
+ }
3099
+ } catch (err) {
3100
+ if (isNodeError(err) && err.code === "ENOENT") continue;
3101
+ throw err;
3102
+ }
3103
+ }
3104
+ return false;
3105
+ }
3106
+ function isPnpmVirtualStorePackagePathFromRoot(pnpmDir, packageName, targetPath) {
3107
+ if (!isInside(pnpmDir, targetPath)) return false;
3108
+ const rel = relative8(pnpmDir, targetPath).replace(/\\/g, "/");
3109
+ const suffix = `node_modules/${packageName}`.replace(/\\/g, "/");
3110
+ return rel === suffix || rel.endsWith(`/${suffix}`);
3111
+ }
3112
+ function isYarnPnpmStorePackagePathFromRoot(storeDir, targetPath) {
3113
+ if (!isInside(storeDir, targetPath)) return false;
3114
+ const rel = relative8(storeDir, targetPath).replace(/\\/g, "/");
3115
+ const parts = rel.split("/");
3116
+ return parts.length === 2 && parts[1] === "package";
3117
+ }
3118
+ async function isDeclaredConsumerDependency(consumerPath, packageName) {
3119
+ const pkg = await readPackageJson(consumerPath);
3120
+ if (!pkg) return false;
3121
+ return Boolean(
3122
+ pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName] || pkg.optionalDependencies?.[packageName] || pkg.peerDependencies?.[packageName]
3123
+ );
3124
+ }
2303
3125
  async function readPackageJson(dir) {
2304
3126
  try {
2305
- const content = await readFile9(join13(dir, "package.json"), "utf-8");
3127
+ const content = await readFile10(join14(dir, "package.json"), "utf-8");
2306
3128
  return JSON.parse(content);
2307
3129
  } catch (err) {
2308
3130
  if (isNodeError(err) && err.code !== "ENOENT") {
@@ -2319,14 +3141,15 @@ var init_injector = __esm({
2319
3141
  init_fs();
2320
3142
  init_bin_linker();
2321
3143
  init_logger();
3144
+ init_dry_run();
2322
3145
  init_pm_detect();
2323
3146
  init_bundler_cache();
2324
3147
  }
2325
3148
  });
2326
3149
 
2327
3150
  // src/core/tracker.ts
2328
- import { readFile as readFile10 } from "fs/promises";
2329
- import { dirname as dirname6 } from "path";
3151
+ import { readFile as readFile11 } from "fs/promises";
3152
+ import { dirname as dirname7 } from "path";
2330
3153
  async function readConsumerState(consumerPath) {
2331
3154
  const { state } = await readConsumerStateSafe(consumerPath);
2332
3155
  return state;
@@ -2334,7 +3157,7 @@ async function readConsumerState(consumerPath) {
2334
3157
  async function readConsumerStateSafe(consumerPath) {
2335
3158
  const statePath = getConsumerStatePath(consumerPath);
2336
3159
  try {
2337
- const content = await readFile10(statePath, "utf-8");
3160
+ const content = await readFile11(statePath, "utf-8");
2338
3161
  const parsed = JSON.parse(content);
2339
3162
  if (!isConsumerState(parsed)) {
2340
3163
  consola.warn(`Invalid consumer state in ${statePath}, using defaults`);
@@ -2387,7 +3210,7 @@ async function getLink(consumerPath, packageName) {
2387
3210
  async function readConsumersRegistry() {
2388
3211
  const regPath = getConsumersPath();
2389
3212
  try {
2390
- const content = await readFile10(regPath, "utf-8");
3213
+ const content = await readFile11(regPath, "utf-8");
2391
3214
  const parsed = JSON.parse(content);
2392
3215
  if (!isConsumersRegistry(parsed)) {
2393
3216
  consola.warn(`Invalid consumers registry, using empty registry`);
@@ -2403,7 +3226,7 @@ async function readConsumersRegistry() {
2403
3226
  }
2404
3227
  async function writeConsumersRegistry(registry) {
2405
3228
  const regPath = getConsumersPath();
2406
- await ensurePrivateDir(dirname6(getConsumersPath()));
3229
+ await ensurePrivateDir(dirname7(getConsumersPath()));
2407
3230
  await atomicWriteFile(regPath, JSON.stringify(registry, null, 2));
2408
3231
  }
2409
3232
  async function registerConsumer(packageName, consumerPath) {
@@ -2439,22 +3262,37 @@ async function getConsumers(packageName) {
2439
3262
  const registry = await readConsumersRegistry();
2440
3263
  return registry[packageName] ?? [];
2441
3264
  }
2442
- async function cleanStaleConsumers() {
3265
+ async function cleanStaleConsumers(options = {}) {
2443
3266
  const regPath = getConsumersPath();
2444
3267
  let removedConsumers = 0;
2445
3268
  let removedPackages = 0;
3269
+ let removedMissingLinks = 0;
3270
+ let skippedUnreliableConsumers = 0;
2446
3271
  await withFileLock(regPath, async () => {
2447
3272
  const registry = await readConsumersRegistry();
2448
3273
  const updated = {};
2449
3274
  for (const [pkgName, consumers] of Object.entries(registry)) {
2450
3275
  const results = await Promise.all(
2451
- consumers.map(async (consumerPath) => ({
2452
- consumerPath,
2453
- valid: await exists(consumerPath)
2454
- }))
3276
+ consumers.map(async (consumerPath) => {
3277
+ if (!await exists(consumerPath)) {
3278
+ return { consumerPath, valid: false, reason: "missing-dir" };
3279
+ }
3280
+ if (options.removeMissingLinks) {
3281
+ const { state, reliable } = await readConsumerStateSafe(consumerPath);
3282
+ if (!reliable) {
3283
+ skippedUnreliableConsumers++;
3284
+ return { consumerPath, valid: true, reason: "unreliable-state" };
3285
+ }
3286
+ if (!state.links[pkgName]) {
3287
+ return { consumerPath, valid: false, reason: "missing-link" };
3288
+ }
3289
+ }
3290
+ return { consumerPath, valid: true, reason: null };
3291
+ })
2455
3292
  );
2456
3293
  const validConsumers = results.filter((r) => r.valid).map((r) => r.consumerPath);
2457
3294
  removedConsumers += consumers.length - validConsumers.length;
3295
+ removedMissingLinks += results.filter((r) => r.reason === "missing-link").length;
2458
3296
  if (validConsumers.length > 0) {
2459
3297
  updated[pkgName] = validConsumers;
2460
3298
  } else {
@@ -2463,7 +3301,12 @@ async function cleanStaleConsumers() {
2463
3301
  }
2464
3302
  await writeConsumersRegistry(updated);
2465
3303
  });
2466
- return { removedConsumers, removedPackages };
3304
+ return {
3305
+ removedConsumers,
3306
+ removedPackages,
3307
+ removedMissingLinks,
3308
+ skippedUnreliableConsumers
3309
+ };
2467
3310
  }
2468
3311
  var init_tracker = __esm({
2469
3312
  "src/core/tracker.ts"() {
@@ -2477,13 +3320,13 @@ var init_tracker = __esm({
2477
3320
  });
2478
3321
 
2479
3322
  // src/utils/build-detect.ts
2480
- import { readFile as readFile11 } from "fs/promises";
2481
- import { join as join14 } from "path";
3323
+ import { readFile as readFile12 } from "fs/promises";
3324
+ import { join as join15 } from "path";
2482
3325
  async function detectBuildCommand(packageDir, pm) {
2483
- const runPrefix = pm === "npm" ? "npm run " : `${pm} `;
3326
+ const runPrefix = pm === "npm" || pm === "bun" ? `${pm} run ` : `${pm} `;
2484
3327
  try {
2485
3328
  const pkg = JSON.parse(
2486
- await readFile11(join14(packageDir, "package.json"), "utf-8")
3329
+ await readFile12(join15(packageDir, "package.json"), "utf-8")
2487
3330
  );
2488
3331
  const scripts = pkg.scripts || {};
2489
3332
  for (const name of ["build", "compile", "bundle", "tsc"]) {
@@ -2502,11 +3345,11 @@ var init_build_detect = __esm({
2502
3345
  });
2503
3346
 
2504
3347
  // src/utils/config.ts
2505
- import { readFile as readFile12 } from "fs/promises";
2506
- import { join as join15 } from "path";
3348
+ import { readFile as readFile13 } from "fs/promises";
3349
+ import { join as join16 } from "path";
2507
3350
  async function loadKnarrConfig(projectDir) {
2508
3351
  try {
2509
- const raw = await readFile12(join15(projectDir, "package.json"), "utf-8");
3352
+ const raw = await readFile13(join16(projectDir, "package.json"), "utf-8");
2510
3353
  const pkg = JSON.parse(raw);
2511
3354
  const source = pkg.knarr;
2512
3355
  if (!source || typeof source !== "object") return {};
@@ -2569,6 +3412,19 @@ var init_timer = __esm({
2569
3412
  // src/utils/output.ts
2570
3413
  function output(data) {
2571
3414
  if (isJsonOutput()) {
3415
+ if (isDryRun()) {
3416
+ markDryRunJsonReportPrinted();
3417
+ if (data && typeof data === "object" && !Array.isArray(data)) {
3418
+ console.log(JSON.stringify({
3419
+ ...data,
3420
+ dryRun: true,
3421
+ mutations: getMutations()
3422
+ }, null, 2));
3423
+ } else {
3424
+ console.log(JSON.stringify({ data, dryRun: true, mutations: getMutations() }, null, 2));
3425
+ }
3426
+ return;
3427
+ }
2572
3428
  console.log(JSON.stringify(data, null, 2));
2573
3429
  }
2574
3430
  }
@@ -2576,6 +3432,7 @@ var init_output = __esm({
2576
3432
  "src/utils/output.ts"() {
2577
3433
  "use strict";
2578
3434
  init_logger();
3435
+ init_dry_run();
2579
3436
  }
2580
3437
  });
2581
3438
 
@@ -2669,8 +3526,8 @@ var init_errors = __esm({
2669
3526
  });
2670
3527
 
2671
3528
  // src/core/push-engine.ts
2672
- import { readFile as readFile13 } from "fs/promises";
2673
- import { join as join16 } from "path";
3529
+ import { readFile as readFile14 } from "fs/promises";
3530
+ import { join as join17 } from "path";
2674
3531
  async function doPush(packageDir, options = {}) {
2675
3532
  const timer = new Timer();
2676
3533
  const result = await publish(packageDir, {
@@ -2679,96 +3536,240 @@ async function doPush(packageDir, options = {}) {
2679
3536
  historyLimit: options.historyLimit
2680
3537
  });
2681
3538
  if (result.skipped) {
2682
- consola.info("No changes to push");
2683
- return;
3539
+ const entry2 = await getStoreEntry(result.name, result.version);
3540
+ if (entry2) {
3541
+ return pushStoreEntry(entry2, {
3542
+ force: options.force,
3543
+ timer,
3544
+ noConsumersStatus: "available",
3545
+ noChange: true,
3546
+ skippedReason: "content unchanged"
3547
+ });
3548
+ } else {
3549
+ const summary = createEmptySummary(result.name, result.version, result.buildId, timer.elapsedMs());
3550
+ summary.noChange = true;
3551
+ summary.skippedReason = "content unchanged";
3552
+ consola.info(
3553
+ `No changes to push for ${result.name}@${result.version}` + (result.buildId ? ` [${result.buildId}]` : "")
3554
+ );
3555
+ output(summary);
3556
+ return summary;
3557
+ }
3558
+ }
3559
+ if (isDryRun()) {
3560
+ const consumers = await getConsumers(result.name);
3561
+ const summary = createEmptySummary(result.name, result.version, result.buildId, timer.elapsedMs());
3562
+ summary.skippedReason = "dry-run";
3563
+ summary.consumers = consumers.length;
3564
+ summary.skippedConsumers = consumers.length;
3565
+ summary.consumerResults = consumers.map((consumerPath) => ({
3566
+ consumerPath,
3567
+ status: "skipped",
3568
+ copied: 0,
3569
+ removed: 0,
3570
+ skipped: 0,
3571
+ binLinks: 0,
3572
+ cacheInvalidations: 0,
3573
+ reason: "dry-run publish preview; store entry was not written"
3574
+ }));
3575
+ consola.info(
3576
+ `Dry-run: would publish ${result.name}@${result.version}` + (result.buildId ? ` [${result.buildId}]` : "") + ". Skipping consumer injection preview because the store entry was not written."
3577
+ );
3578
+ output(summary);
3579
+ return summary;
2684
3580
  }
2685
3581
  const entry = await getStoreEntry(result.name, result.version);
2686
3582
  if (!entry) {
2687
3583
  errorWithSuggestion(
2688
3584
  `Failed to read store entry for ${result.name}@${result.version} after publish`
2689
3585
  );
2690
- return;
3586
+ const summary = createEmptySummary(result.name, result.version, result.buildId, timer.elapsedMs());
3587
+ summary.failedConsumers = 1;
3588
+ summary.consumerResults = [
3589
+ {
3590
+ consumerPath: packageDir,
3591
+ status: "failed",
3592
+ copied: 0,
3593
+ removed: 0,
3594
+ skipped: 0,
3595
+ binLinks: 0,
3596
+ cacheInvalidations: 0,
3597
+ error: "store entry missing after publish"
3598
+ }
3599
+ ];
3600
+ return summary;
2691
3601
  }
2692
- const consumers = await getConsumers(result.name);
3602
+ return pushStoreEntry(entry, {
3603
+ force: options.force,
3604
+ timer,
3605
+ noConsumersStatus: "published"
3606
+ });
3607
+ }
3608
+ async function pushStoreEntry(entry, options = {}) {
3609
+ const timer = options.timer ?? new Timer();
3610
+ const consumers = await getConsumers(entry.name);
2693
3611
  if (consumers.length === 0) {
2694
- consola.success(
2695
- `Published ${result.name}@${result.version} to store`
2696
- );
3612
+ const status = options.noConsumersStatus ?? "available";
3613
+ consola.success(`${entry.name}@${entry.version} ${status} in store`);
2697
3614
  consola.info(
2698
- "No consumers registered yet. Run 'knarr add " + result.name + "' in a consumer project to start receiving pushes."
3615
+ "No consumers registered yet. Run 'knarr add " + entry.name + "' in a consumer project to start receiving pushes."
2699
3616
  );
2700
- output({
2701
- name: result.name,
2702
- version: result.version,
2703
- buildId: result.buildId,
2704
- consumers: 0,
2705
- failedConsumers: 0,
2706
- copied: 0,
2707
- skipped: 0,
2708
- elapsed: timer.elapsedMs()
2709
- });
2710
- return;
3617
+ const summary2 = createEmptySummary(
3618
+ entry.name,
3619
+ entry.version,
3620
+ entry.meta.buildId ?? "",
3621
+ timer.elapsedMs()
3622
+ );
3623
+ summary2.noChange = options.noChange ?? false;
3624
+ summary2.skippedReason = options.skippedReason;
3625
+ if (options.emitOutput !== false) output(summary2);
3626
+ return summary2;
2711
3627
  }
2712
3628
  let totalCopied = 0;
3629
+ let totalRemoved = 0;
2713
3630
  let totalSkipped = 0;
2714
- let pushCount = 0;
3631
+ let totalBinLinks = 0;
3632
+ let totalCacheInvalidations = 0;
3633
+ let updatedCount = 0;
3634
+ let skippedCount = 0;
2715
3635
  let failedCount = 0;
2716
3636
  const results = await Promise.all(
2717
3637
  consumers.map(
2718
3638
  (consumerPath) => consumerLimit(async () => {
2719
- const link = await getLink(consumerPath, result.name);
3639
+ const link = await getLink(consumerPath, entry.name);
2720
3640
  if (!link) {
2721
3641
  verbose(
2722
- `[push] No link found for ${result.name} in ${consumerPath}, skipping`
3642
+ `[push] No link found for ${entry.name} in ${consumerPath}, skipping`
2723
3643
  );
2724
- return null;
3644
+ return {
3645
+ consumerPath,
3646
+ status: "skipped",
3647
+ copied: 0,
3648
+ removed: 0,
3649
+ skipped: 0,
3650
+ binLinks: 0,
3651
+ cacheInvalidations: 0,
3652
+ reason: "not linked in consumer state"
3653
+ };
2725
3654
  }
2726
3655
  try {
3656
+ const currentPm = await detectPackageManagerInfo(consumerPath);
3657
+ const packageManager = currentPm.source === "default" ? link.packageManager : currentPm.packageManager;
2727
3658
  const injectResult = await inject(
2728
3659
  entry,
2729
3660
  consumerPath,
2730
- link.packageManager,
3661
+ packageManager,
2731
3662
  { force: options.force }
2732
3663
  );
2733
- await addLink(consumerPath, result.name, {
3664
+ await addLink(consumerPath, entry.name, {
2734
3665
  ...link,
2735
3666
  contentHash: entry.meta.contentHash,
2736
3667
  linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2737
- buildId: entry.meta.buildId ?? ""
3668
+ buildId: entry.meta.buildId ?? "",
3669
+ packageManager
2738
3670
  });
2739
- return injectResult;
3671
+ return {
3672
+ consumerPath,
3673
+ status: "updated",
3674
+ copied: injectResult.copied,
3675
+ removed: injectResult.removed,
3676
+ skipped: injectResult.skipped,
3677
+ binLinks: injectResult.binLinks,
3678
+ cacheInvalidations: injectResult.cacheInvalidations
3679
+ };
2740
3680
  } catch (err) {
3681
+ const message2 = err instanceof Error ? err.message : String(err);
2741
3682
  consola.warn(
2742
- `Failed to push to ${consumerPath}: ${err instanceof Error ? err.message : String(err)}`
3683
+ `Failed to push to ${consumerPath}: ${message2}`
2743
3684
  );
2744
- return null;
3685
+ return {
3686
+ consumerPath,
3687
+ status: "failed",
3688
+ copied: 0,
3689
+ removed: 0,
3690
+ skipped: 0,
3691
+ binLinks: 0,
3692
+ cacheInvalidations: 0,
3693
+ error: message2
3694
+ };
2745
3695
  }
2746
3696
  })
2747
3697
  )
2748
3698
  );
2749
3699
  for (const r of results) {
2750
- if (r) {
3700
+ if (r.status === "updated") {
2751
3701
  totalCopied += r.copied;
3702
+ totalRemoved += r.removed;
2752
3703
  totalSkipped += r.skipped;
2753
- pushCount++;
3704
+ totalBinLinks += r.binLinks;
3705
+ totalCacheInvalidations += r.cacheInvalidations;
3706
+ updatedCount++;
3707
+ } else if (r.status === "skipped") {
3708
+ skippedCount++;
2754
3709
  } else {
2755
3710
  failedCount++;
2756
3711
  }
2757
3712
  }
2758
- const buildTag = result.buildId ? ` [${result.buildId}]` : "";
2759
- consola.success(
2760
- `Pushed ${result.name}@${result.version}${buildTag} to ${pushCount} consumer(s) in ${timer.elapsed()} (${totalCopied} files changed, ${totalSkipped} unchanged)`
2761
- );
2762
- output({
2763
- name: result.name,
2764
- version: result.version,
2765
- buildId: result.buildId,
2766
- consumers: pushCount,
3713
+ const buildId = entry.meta.buildId ?? "";
3714
+ const buildTag = buildId ? ` [${buildId}]` : "";
3715
+ const detailParts = [
3716
+ `${totalCopied} copied`,
3717
+ `${totalRemoved} removed`,
3718
+ `${totalSkipped} unchanged`
3719
+ ];
3720
+ if (totalBinLinks > 0) detailParts.push(`${totalBinLinks} bin links`);
3721
+ if (totalCacheInvalidations > 0) {
3722
+ detailParts.push(`${totalCacheInvalidations} cache invalidations`);
3723
+ }
3724
+ if (skippedCount > 0) detailParts.push(`${skippedCount} consumer(s) skipped`);
3725
+ if (failedCount > 0) detailParts.push(`${failedCount} failed`);
3726
+ const consumerLabel = failedCount > 0 || skippedCount > 0 ? `${updatedCount}/${consumers.length} consumer(s)` : `${updatedCount} consumer(s)`;
3727
+ const message = `Pushed ${entry.name}@${entry.version}${buildTag} to ${consumerLabel} in ${timer.elapsed()} (${detailParts.join(", ")})`;
3728
+ if (failedCount > 0) {
3729
+ consola.warn(message);
3730
+ } else {
3731
+ consola.success(message);
3732
+ }
3733
+ const summary = {
3734
+ name: entry.name,
3735
+ version: entry.version,
3736
+ buildId,
3737
+ noChange: false,
3738
+ consumers: consumers.length,
3739
+ updatedConsumers: updatedCount,
2767
3740
  failedConsumers: failedCount,
3741
+ skippedConsumers: skippedCount,
2768
3742
  copied: totalCopied,
3743
+ removed: totalRemoved,
2769
3744
  skipped: totalSkipped,
2770
- elapsed: timer.elapsedMs()
2771
- });
3745
+ binLinks: totalBinLinks,
3746
+ cacheInvalidations: totalCacheInvalidations,
3747
+ elapsed: timer.elapsedMs(),
3748
+ consumerResults: results
3749
+ };
3750
+ summary.noChange = options.noChange ?? false;
3751
+ summary.skippedReason = options.skippedReason;
3752
+ if (options.emitOutput !== false) output(summary);
3753
+ return summary;
3754
+ }
3755
+ function createEmptySummary(name, version, buildId, elapsed) {
3756
+ return {
3757
+ name,
3758
+ version,
3759
+ buildId,
3760
+ noChange: false,
3761
+ consumers: 0,
3762
+ updatedConsumers: 0,
3763
+ failedConsumers: 0,
3764
+ skippedConsumers: 0,
3765
+ copied: 0,
3766
+ removed: 0,
3767
+ skipped: 0,
3768
+ binLinks: 0,
3769
+ cacheInvalidations: 0,
3770
+ elapsed,
3771
+ consumerResults: []
3772
+ };
2772
3773
  }
2773
3774
  async function resolveWatchConfig(packageDir, args, config) {
2774
3775
  let buildCmd = args.build;
@@ -2786,22 +3787,24 @@ async function resolveWatchConfig(packageDir, args, config) {
2786
3787
  consola.info(`Auto-detected build command: ${detected}`);
2787
3788
  }
2788
3789
  }
2789
- if (buildCmd) {
3790
+ if (buildCmd && !patterns) {
2790
3791
  const { exists: exists3 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2791
3792
  const candidates = ["src", "lib", "source", "app", "pages", "components"];
2792
3793
  const existing = (await Promise.all(
2793
3794
  candidates.map(async (dir) => ({
2794
3795
  dir,
2795
- exists: await exists3(join16(packageDir, dir))
3796
+ exists: await exists3(join17(packageDir, dir))
2796
3797
  }))
2797
3798
  )).filter((c) => c.exists).map((c) => c.dir);
2798
3799
  patterns = existing.length > 0 ? existing : ["src", "lib"];
2799
3800
  verbose(`[watch] Using source patterns with build command: ${patterns.join(", ")}`);
3801
+ } else if (buildCmd && patterns) {
3802
+ verbose(`[watch] Using configured watch patterns with build command: ${patterns.join(", ")}`);
2800
3803
  } else {
2801
3804
  consola.info("No build command detected \u2014 watching output directories directly");
2802
3805
  try {
2803
3806
  const pkg = JSON.parse(
2804
- await readFile13(join16(packageDir, "package.json"), "utf-8")
3807
+ await readFile14(join17(packageDir, "package.json"), "utf-8")
2805
3808
  );
2806
3809
  if (pkg.files && pkg.files.length > 0) {
2807
3810
  patterns = pkg.files;
@@ -2920,14 +3923,16 @@ __export(watcher_exports, {
2920
3923
  startWatcher: () => startWatcher
2921
3924
  });
2922
3925
  import { spawn as spawn2 } from "child_process";
2923
- import { readdir as readdir7, stat as stat8 } from "fs/promises";
2924
- import { platform as platform3 } from "os";
2925
- import { join as join17 } from "path";
3926
+ import { readdir as readdir7, stat as stat7 } from "fs/promises";
3927
+ import { platform as platform4 } from "os";
3928
+ import { join as join18 } from "path";
2926
3929
  function killActiveBuild() {
2927
- if (activeChild && !activeChild.killed) {
2928
- activeChild.kill("SIGTERM");
2929
- activeChild = null;
3930
+ for (const child of activeChildren) {
3931
+ if (!child.killed) {
3932
+ child.kill("SIGTERM");
3933
+ }
2930
3934
  }
3935
+ activeChildren.clear();
2931
3936
  }
2932
3937
  async function walkDir(dir, snapshot) {
2933
3938
  let entries;
@@ -2938,12 +3943,12 @@ async function walkDir(dir, snapshot) {
2938
3943
  }
2939
3944
  for (const entry of entries) {
2940
3945
  if (IGNORED_DIRS.has(entry.name)) continue;
2941
- const fullPath = join17(dir, entry.name);
3946
+ const fullPath = join18(dir, entry.name);
2942
3947
  if (entry.isDirectory()) {
2943
3948
  await walkDir(fullPath, snapshot);
2944
3949
  } else {
2945
3950
  try {
2946
- const st = await stat8(fullPath);
3951
+ const st = await stat7(fullPath);
2947
3952
  snapshot.set(fullPath, st.mtimeMs);
2948
3953
  } catch {
2949
3954
  }
@@ -2954,7 +3959,7 @@ async function buildSnapshot(watchPaths) {
2954
3959
  const snapshot = /* @__PURE__ */ new Map();
2955
3960
  for (const p of watchPaths) {
2956
3961
  try {
2957
- const st = await stat8(p);
3962
+ const st = await stat7(p);
2958
3963
  if (st.isDirectory()) {
2959
3964
  await walkDir(p, snapshot);
2960
3965
  } else {
@@ -2977,6 +3982,8 @@ async function startWatcher(watchDir, options, onChange) {
2977
3982
  let closed = false;
2978
3983
  let running = false;
2979
3984
  let lastBuildEndTime = 0;
3985
+ let cycle = 0;
3986
+ let lastSuccessfulPush = null;
2980
3987
  let hasPendingChanges = false;
2981
3988
  const doBuild = async () => {
2982
3989
  if (closed || running) return;
@@ -2992,11 +3999,15 @@ async function startWatcher(watchDir, options, onChange) {
2992
3999
  }
2993
4000
  running = true;
2994
4001
  hasPendingChanges = false;
4002
+ cycle++;
2995
4003
  try {
4004
+ consola.info(`Change cycle #${cycle} started`);
2996
4005
  if (options.buildCmd) {
2997
4006
  const success = await runBuildCommand(options.buildCmd, watchDir);
2998
4007
  if (!success) {
2999
- consola.warn("Build failed (see output above), skipping push");
4008
+ consola.warn(
4009
+ "Build failed (see output above), skipping push" + (lastSuccessfulPush ? `. Last successful push: ${lastSuccessfulPush.toLocaleTimeString()}` : ". No successful push yet.")
4010
+ );
3000
4011
  if (options.notify) {
3001
4012
  const { ringBell: ringBell2 } = await Promise.resolve().then(() => (init_bell(), bell_exports));
3002
4013
  ringBell2(true);
@@ -3005,6 +4016,7 @@ async function startWatcher(watchDir, options, onChange) {
3005
4016
  }
3006
4017
  }
3007
4018
  await onChange();
4019
+ lastSuccessfulPush = /* @__PURE__ */ new Date();
3008
4020
  if (options.notify) ringBell(true);
3009
4021
  } catch (err) {
3010
4022
  consola.error(`Push failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -3110,7 +4122,7 @@ async function startWatcher(watchDir, options, onChange) {
3110
4122
  }
3111
4123
  function runBuildCommand(cmd, cwd) {
3112
4124
  return new Promise((resolve8) => {
3113
- const isWin = platform3() === "win32";
4125
+ const isWin = platform4() === "win32";
3114
4126
  const shell = isWin ? "cmd" : "sh";
3115
4127
  const shellFlag = isWin ? "/c" : "-c";
3116
4128
  consola.start(`Running: ${cmd}`);
@@ -3118,9 +4130,9 @@ function runBuildCommand(cmd, cwd) {
3118
4130
  cwd,
3119
4131
  stdio: "inherit"
3120
4132
  });
3121
- activeChild = child;
4133
+ activeChildren.add(child);
3122
4134
  child.on("close", (code) => {
3123
- activeChild = null;
4135
+ activeChildren.delete(child);
3124
4136
  if (code === 0) {
3125
4137
  consola.success("Build succeeded");
3126
4138
  resolve8(true);
@@ -3130,35 +4142,39 @@ function runBuildCommand(cmd, cwd) {
3130
4142
  }
3131
4143
  });
3132
4144
  child.on("error", (err) => {
3133
- activeChild = null;
4145
+ activeChildren.delete(child);
3134
4146
  consola.error(`Build error: ${err.message}`);
3135
4147
  resolve8(false);
3136
4148
  });
3137
4149
  });
3138
4150
  }
3139
- var activeChild, activeWatcher, IGNORED_DIRS;
4151
+ var activeChildren, activeWatcher, IGNORED_DIRS;
3140
4152
  var init_watcher = __esm({
3141
4153
  "src/core/watcher.ts"() {
3142
4154
  "use strict";
3143
4155
  init_console();
3144
4156
  init_bell();
3145
- activeChild = null;
4157
+ activeChildren = /* @__PURE__ */ new Set();
3146
4158
  activeWatcher = null;
3147
4159
  IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".knarr"]);
3148
4160
  }
3149
4161
  });
3150
4162
 
3151
4163
  // src/core/watch-orchestrator.ts
3152
- var cascadeLimit, WatchOrchestrator;
4164
+ function assertPushSucceeded(summary, packageName) {
4165
+ if (summary.failedConsumers === 0) return;
4166
+ throw new Error(
4167
+ `Push for ${packageName} failed for ${summary.failedConsumers} consumer(s)`
4168
+ );
4169
+ }
4170
+ var WatchOrchestrator;
3153
4171
  var init_watch_orchestrator = __esm({
3154
4172
  "src/core/watch-orchestrator.ts"() {
3155
4173
  "use strict";
3156
- init_concurrency();
3157
4174
  init_console();
3158
4175
  init_logger();
3159
4176
  init_config();
3160
4177
  init_push_engine();
3161
- cascadeLimit = pLimit(2);
3162
4178
  WatchOrchestrator = class {
3163
4179
  packages = /* @__PURE__ */ new Map();
3164
4180
  dependents = /* @__PURE__ */ new Map();
@@ -3169,14 +4185,23 @@ var init_watch_orchestrator = __esm({
3169
4185
  }
3170
4186
  async start(startDir, args, pushOptions) {
3171
4187
  this.pushOptions = pushOptions;
3172
- const { buildWorkspaceGraph: buildWorkspaceGraph2, buildReverseAdjacency: buildReverseAdjacency2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
4188
+ const {
4189
+ buildWorkspaceGraph: buildWorkspaceGraph2,
4190
+ buildReverseAdjacency: buildReverseAdjacency2,
4191
+ filterPublishableWorkspaceGraph: filterPublishableWorkspaceGraph2
4192
+ } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
3173
4193
  const { topoSort: topoSort2, CycleError: CycleError2 } = await Promise.resolve().then(() => (init_topo_sort(), topo_sort_exports));
3174
4194
  const { startWatcher: startWatcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
3175
- const graph = await buildWorkspaceGraph2(startDir);
4195
+ const discovered = await buildWorkspaceGraph2(startDir);
4196
+ const graph = filterPublishableWorkspaceGraph2(discovered);
3176
4197
  if (graph.packages.length === 0) {
3177
- consola.warn("No workspace packages found");
4198
+ consola.warn("No publishable workspace packages found");
3178
4199
  return;
3179
4200
  }
4201
+ const privateCount = discovered.packages.length - graph.packages.length;
4202
+ if (privateCount > 0) {
4203
+ consola.info(`Skipping ${privateCount} private workspace package(s)`);
4204
+ }
3180
4205
  let ordered;
3181
4206
  try {
3182
4207
  ordered = topoSort2(graph.adjacency);
@@ -3199,14 +4224,25 @@ var init_watch_orchestrator = __esm({
3199
4224
  const { buildCmd, patterns } = await resolveWatchConfig(dir, args, config);
3200
4225
  const notify = args.notify ?? config.notify ?? false;
3201
4226
  const wrappedOnChange = async () => {
3202
- await doPush(dir, pushOptions);
3203
- await this.onPackagePushed(name);
4227
+ await this.runExclusive(name, async () => {
4228
+ const summary = await doPush(dir, pushOptions);
4229
+ assertPushSucceeded(summary, name);
4230
+ await this.onPackagePushed(name);
4231
+ });
3204
4232
  };
3205
4233
  const parseMs = (v) => {
3206
4234
  if (!v) return void 0;
3207
4235
  const n = parseInt(v, 10);
3208
4236
  return Number.isFinite(n) ? n : void 0;
3209
4237
  };
4238
+ const entry = {
4239
+ dir,
4240
+ buildCmd,
4241
+ state: "idle",
4242
+ watcher: { close: async () => {
4243
+ } }
4244
+ };
4245
+ this.packages.set(name, entry);
3210
4246
  const watcher = await startWatcher2(
3211
4247
  dir,
3212
4248
  {
@@ -3218,7 +4254,7 @@ var init_watch_orchestrator = __esm({
3218
4254
  },
3219
4255
  wrappedOnChange
3220
4256
  );
3221
- this.packages.set(name, { dir, buildCmd, state: "idle", watcher });
4257
+ entry.watcher = watcher;
3222
4258
  }
3223
4259
  consola.info(`Watching ${this.packages.size} workspace packages`);
3224
4260
  await new Promise((resolve8) => {
@@ -3236,12 +4272,11 @@ var init_watch_orchestrator = __esm({
3236
4272
  const deps = this.dependents.get(name);
3237
4273
  if (!deps || deps.size === 0) return;
3238
4274
  verbose(`[cascade] ${name} pushed, triggering dependents: ${[...deps].join(", ")}`);
3239
- const tasks = [...deps].map(
3240
- (depName) => cascadeLimit(() => this.requestRebuild(depName))
3241
- );
3242
- await Promise.all(tasks);
4275
+ for (const depName of deps) {
4276
+ await this.requestRebuild(depName);
4277
+ }
3243
4278
  }
3244
- async requestRebuild(name) {
4279
+ async runExclusive(name, task) {
3245
4280
  const entry = this.packages.get(name);
3246
4281
  if (!entry) return;
3247
4282
  if (entry.state === "queued") {
@@ -3254,30 +4289,35 @@ var init_watch_orchestrator = __esm({
3254
4289
  return;
3255
4290
  }
3256
4291
  entry.state = "building";
3257
- verbose(`[cascade] Rebuilding ${name}`);
3258
4292
  try {
4293
+ await task(entry);
4294
+ } finally {
4295
+ const wasQueued = entry.state === "queued";
4296
+ entry.state = "idle";
4297
+ if (wasQueued) {
4298
+ await this.requestRebuild(name);
4299
+ }
4300
+ }
4301
+ }
4302
+ async requestRebuild(name) {
4303
+ await this.runExclusive(name, async (entry) => {
4304
+ verbose(`[cascade] Rebuilding ${name}`);
3259
4305
  if (entry.buildCmd) {
3260
4306
  const { runBuildCommand: runBuildCommand2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
3261
4307
  const success = await runBuildCommand2(entry.buildCmd, entry.dir);
3262
4308
  if (!success) {
3263
4309
  consola.warn(`[cascade] Build failed for ${name}, skipping dependents`);
3264
- entry.state = "idle";
3265
4310
  return;
3266
4311
  }
3267
4312
  }
3268
- await doPush(entry.dir, this.pushOptions);
4313
+ const summary = await doPush(entry.dir, this.pushOptions);
4314
+ assertPushSucceeded(summary, name);
3269
4315
  await this.onPackagePushed(name);
3270
- } catch (err) {
4316
+ }).catch((err) => {
3271
4317
  consola.warn(
3272
4318
  `[cascade] Push failed for ${name}: ${err instanceof Error ? err.message : String(err)}`
3273
4319
  );
3274
- } finally {
3275
- const wasQueued = entry.state === "queued";
3276
- entry.state = "idle";
3277
- if (wasQueued) {
3278
- await this.requestRebuild(name);
3279
- }
3280
- }
4320
+ });
3281
4321
  }
3282
4322
  async close() {
3283
4323
  await Promise.all(
@@ -3305,42 +4345,82 @@ init_timer();
3305
4345
  init_logger();
3306
4346
  async function doPushAll(startDir, options = {}) {
3307
4347
  const timer = new Timer();
3308
- const graph = await buildWorkspaceGraph(startDir);
4348
+ const discovered = await buildWorkspaceGraph(startDir);
4349
+ const graph = filterPublishableWorkspaceGraph(discovered);
3309
4350
  if (graph.packages.length === 0) {
3310
- consola.warn("No workspace packages found");
4351
+ consola.warn("No publishable workspace packages found");
3311
4352
  return;
3312
4353
  }
4354
+ const privateCount = discovered.packages.length - graph.packages.length;
4355
+ if (privateCount > 0) {
4356
+ consola.info(`Skipping ${privateCount} private workspace package(s)`);
4357
+ }
3313
4358
  let ordered;
3314
4359
  try {
3315
4360
  ordered = topoSort(graph.adjacency);
3316
4361
  } catch (err) {
3317
4362
  if (err instanceof CycleError) {
3318
4363
  consola.error(`Cannot push: ${err.message}`);
3319
- return;
4364
+ throw err;
3320
4365
  }
3321
4366
  throw err;
3322
4367
  }
3323
4368
  const nameToDir = new Map(graph.packages.map((p) => [p.name, p.dir]));
3324
4369
  consola.info(`Pushing ${ordered.length} packages in dependency order`);
3325
4370
  verbose(`[batch-push] Order: ${ordered.join(" \u2192 ")}`);
4371
+ const reverseAdjacency = buildReverseAdjacency(graph.adjacency);
4372
+ const blocked = new Set(options.skipPackages ?? []);
4373
+ for (const name of [...blocked]) {
4374
+ markTransitiveDependentsBlocked(name, reverseAdjacency, blocked);
4375
+ }
3326
4376
  let success = 0;
3327
4377
  let failed = 0;
4378
+ let skipped = 0;
3328
4379
  for (const name of ordered) {
3329
4380
  const dir = nameToDir.get(name);
3330
4381
  if (!dir) continue;
4382
+ if (blocked.has(name)) {
4383
+ verbose(`[batch-push] Skipping ${name}: dependency build/push failed`);
4384
+ skipped++;
4385
+ continue;
4386
+ }
3331
4387
  try {
3332
- await doPush(dir, options);
3333
- success++;
4388
+ const summary = await doPush(dir, options);
4389
+ if (summary.failedConsumers > 0) {
4390
+ failed++;
4391
+ markTransitiveDependentsBlocked(name, reverseAdjacency, blocked);
4392
+ } else {
4393
+ success++;
4394
+ }
3334
4395
  } catch (err) {
3335
4396
  consola.warn(
3336
4397
  `Failed to push ${name}: ${err instanceof Error ? err.message : String(err)}`
3337
4398
  );
3338
4399
  failed++;
4400
+ markTransitiveDependentsBlocked(name, reverseAdjacency, blocked);
3339
4401
  }
3340
4402
  }
3341
- consola.success(
3342
- `Pushed ${success}/${ordered.length} packages in ${timer.elapsed()}${failed > 0 ? ` (${failed} failed)` : ""}`
3343
- );
4403
+ const details = [
4404
+ failed > 0 ? `${failed} failed` : null,
4405
+ skipped > 0 ? `${skipped} skipped` : null
4406
+ ].filter((part) => part !== null);
4407
+ const summaryMessage = `Pushed ${success}/${ordered.length} packages in ${timer.elapsed()}` + (details.length > 0 ? ` (${details.join(", ")})` : "");
4408
+ if (failed > 0) {
4409
+ consola.warn(summaryMessage);
4410
+ throw new Error(`Failed to push ${failed} workspace package(s)`);
4411
+ }
4412
+ if (skipped > 0) {
4413
+ consola.warn(summaryMessage);
4414
+ return;
4415
+ }
4416
+ consola.success(summaryMessage);
4417
+ }
4418
+ function markTransitiveDependentsBlocked(name, reverseAdjacency, blocked) {
4419
+ for (const dependent of reverseAdjacency.get(name) ?? []) {
4420
+ if (blocked.has(dependent)) continue;
4421
+ blocked.add(dependent);
4422
+ markTransitiveDependentsBlocked(dependent, reverseAdjacency, blocked);
4423
+ }
3344
4424
  }
3345
4425
 
3346
4426
  // src/index.ts
@@ -3354,13 +4434,13 @@ init_workspace();
3354
4434
 
3355
4435
  // src/utils/preflight.ts
3356
4436
  init_logger();
3357
- import { readFile as readFile14, stat as stat9 } from "fs/promises";
3358
- import { join as join18, resolve as resolve7 } from "path";
4437
+ import { readFile as readFile15, stat as stat8 } from "fs/promises";
4438
+ import { join as join19, resolve as resolve7 } from "path";
3359
4439
  async function runPreflightChecks(packageDir) {
3360
4440
  const issues = [];
3361
4441
  let pkgContent;
3362
4442
  try {
3363
- pkgContent = await readFile14(join18(packageDir, "package.json"), "utf-8");
4443
+ pkgContent = await readFile15(join19(packageDir, "package.json"), "utf-8");
3364
4444
  } catch {
3365
4445
  issues.push({
3366
4446
  code: "NO_PACKAGE_JSON",
@@ -3418,7 +4498,7 @@ async function runPreflightChecks(packageDir) {
3418
4498
  }
3419
4499
  async function fileExists(filePath) {
3420
4500
  try {
3421
- const s = await stat9(filePath);
4501
+ const s = await stat8(filePath);
3422
4502
  return s.isFile();
3423
4503
  } catch {
3424
4504
  return false;
@@ -3506,7 +4586,7 @@ init_paths();
3506
4586
  init_dry_run();
3507
4587
 
3508
4588
  // src/utils/vite-config.ts
3509
- import { readFile as readFile15 } from "fs/promises";
4589
+ import { readFile as readFile16 } from "fs/promises";
3510
4590
  function defineConfigUsesTernary(content) {
3511
4591
  const callRegex = /(^|[^A-Za-z0-9_$])defineConfig\s*\(/g;
3512
4592
  let match;
@@ -3592,6 +4672,7 @@ export {
3592
4672
  cleanStaleConsumers,
3593
4673
  clearHistory,
3594
4674
  detectPackageManager,
4675
+ detectPackageManagerInfo,
3595
4676
  doPush,
3596
4677
  doPushAll,
3597
4678
  findStoreEntry,