fyn 3.2.3 → 3.3.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.
package/dist/fyn.mjs CHANGED
@@ -17,7 +17,7 @@ import os$1 from "os";
17
17
  import * as util$5 from "util";
18
18
  import Util from "util";
19
19
  import EventEmitter$1, { EventEmitter, once } from "events";
20
- import { fileURLToPath } from "node:url";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import Pr, { basename, join, posix, win32 } from "node:path";
22
22
  import { pipeline } from "stream/promises";
23
23
  import * as Fs from "node:fs";
@@ -30,13 +30,14 @@ import { Buffer as Buffer$1 } from "buffer";
30
30
  import * as Ps$1 from "zlib";
31
31
  import en from "zlib";
32
32
  import co from "node:assert";
33
- import { randomBytes } from "node:crypto";
33
+ import Crypto$1, { randomBytes } from "node:crypto";
34
34
  import * as FsPromises from "node:fs/promises";
35
35
  import ro from "node:fs/promises";
36
+ import { Worker } from "node:worker_threads";
36
37
  import { URL as URL$1, fileURLToPath as fileURLToPath$1 } from "url";
37
38
  import childProcess from "child_process";
38
39
  import fs$1 from "fs/promises";
39
- import { PassThrough } from "stream";
40
+ import { PassThrough, Readable } from "stream";
40
41
  import * as dns from "dns";
41
42
  import * as net$3 from "net";
42
43
  import * as tls$1 from "tls";
@@ -680,7 +681,9 @@ var TimeoutRunner = class {
680
681
  this.defer = makeDefer(options.Promise);
681
682
  this.ThePromise = options.Promise;
682
683
  this.TimeoutError = options.TimeoutError;
683
- this.timeout = setTimeout(() => this.defer.reject(new this.TimeoutError(rejectMsg)), maxMs);
684
+ const msg = rejectMsg || "xaa TimeoutRunner operation timed out";
685
+ this.timeout = setTimeout(() => this.defer.reject(new this.TimeoutError(msg)), maxMs);
686
+ this.defer.promise.catch(() => {});
684
687
  }
685
688
  /**
686
689
  * check if runner has failed with error
@@ -787,6 +790,19 @@ function createMapContext(array) {
787
790
  };
788
791
  }
789
792
  /**
793
+ * attach partial results to a map error
794
+ *
795
+ * Thrown values can be primitives or frozen, so a failed assignment is ignored.
796
+ *
797
+ * @param err - the thrown value
798
+ * @param partial - results mapped so far
799
+ */
800
+ function addPartial(err, partial) {
801
+ try {
802
+ err.partial = partial;
803
+ } catch (_a) {}
804
+ }
805
+ /**
790
806
  * async map for array that supports concurrency
791
807
  *
792
808
  * Use by xaa.map internally.
@@ -798,7 +814,7 @@ function createMapContext(array) {
798
814
  */
799
815
  function multiMap(array, func, options) {
800
816
  const awaited = new Array(array.length);
801
- let error;
817
+ let failed = false;
802
818
  let completedCount = 0;
803
819
  let freeSlots = options.concurrency;
804
820
  let index = 0;
@@ -808,15 +824,15 @@ function multiMap(array, func, options) {
808
824
  const defer = makeDefer();
809
825
  const fail = (err) => {
810
826
  context.failed = true;
811
- if (!error) {
812
- error = err;
813
- error.partial = awaited;
814
- defer.reject(error);
827
+ if (!failed) {
828
+ failed = true;
829
+ addPartial(err, awaited);
830
+ defer.reject(err);
815
831
  }
816
832
  };
817
833
  const mapNext = () => {
818
- if (!error && completedCount === totalCount) return defer.resolve(awaited);
819
- if (error || freeSlots <= 0 || index >= totalCount) return null;
834
+ if (!failed && completedCount === totalCount) return defer.resolve(awaited);
835
+ if (failed || freeSlots <= 0 || index >= totalCount) return null;
820
836
  const ir = iterator && iterator.next();
821
837
  /* c8 ignore next 7 */
822
838
  if (ir && ir.done) {
@@ -840,15 +856,16 @@ function multiMap(array, func, options) {
840
856
  return mapNext();
841
857
  } else return save(res);
842
858
  };
859
+ const callFunc = (val) => {
860
+ try {
861
+ return handleRet(func.call(options.thisArg, val, pendingIx, context));
862
+ } catch (err) {
863
+ return fail(err);
864
+ }
865
+ };
843
866
  const item = ir ? ir.value : array[pendingIx];
844
- if (isPromise$1(item)) return item.then((val) => {
845
- return handleRet(func.call(options.thisArg, val, pendingIx, context));
846
- }, fail);
847
- else try {
848
- return handleRet(func.call(options.thisArg, item, pendingIx, context));
849
- } catch (err) {
850
- return fail(err);
851
- }
867
+ if (isPromise$1(item)) return item.then(callFunc, fail);
868
+ else return callFunc(item);
852
869
  };
853
870
  mapNext();
854
871
  return defer.promise;
@@ -865,7 +882,7 @@ async function mapSeries(array, func, options) {
865
882
  }
866
883
  } catch (err) {
867
884
  context.failed = true;
868
- err.partial = awaited;
885
+ addPartial(err, awaited);
869
886
  throw err;
870
887
  }
871
888
  return awaited;
@@ -1223,13 +1240,15 @@ function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, ignorePathF
1223
1240
  if (!stack) return String(stack);
1224
1241
  return stack.split("\n").map((line) => {
1225
1242
  if (!line.match(/ {4,}at/)) return line;
1226
- const match = line.match(/( {4,}at)([^\(]+\()([^\)]+\))(.*)/);
1227
- if (!match || !match[3].match(/[^:]+:\/\//) && !Path.isAbsolute(match[3])) return false;
1228
- const path = match[3].replace(/\\/g, "/");
1243
+ const match = line.match(/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/);
1244
+ const [, at, name = "", location = match === null || match === void 0 ? void 0 : match[5], tail = ""] = match || [];
1245
+ if (!location || !location.match(/[^:]+:\/\//) && !Path.isAbsolute(location)) return false;
1246
+ const path = location.replace(/\\/g, "/");
1229
1247
  if (defaultPathFilter.concat(ignorePathFilter).find((s) => s && (s instanceof RegExp ? path.match(s) : path.includes(s)))) return false;
1230
- const path2 = replacePath && replacePath.length > 1 ? path.replace(replacePath, "") : path;
1231
- return `${match[1]}${match[2]}${path2}${match[4]}`;
1232
- }).filter((x) => x).join("\n");
1248
+ let path2 = path;
1249
+ if (replacePath && replacePath.length > 1) path2 = path.startsWith(`file://${replacePath}`) ? path.slice(`file://${replacePath}`.length) : path.replace(replacePath, "");
1250
+ return `${at}${name}${path2}${tail}`;
1251
+ }).filter((x) => x !== false).join("\n");
1233
1252
  }
1234
1253
  /**
1235
1254
  * Build stack of aggregate errors
@@ -1250,16 +1269,17 @@ function aggregateStack(stack, errors) {
1250
1269
  * @returns aggregate stack
1251
1270
  */
1252
1271
  function aggregateErrorStack(error) {
1253
- return aggregateStack(error.__stack || error.message || String(error), error.errors);
1272
+ return aggregateStack(error.__stack || (error instanceof AggregateError$1 ? "" : error.stack) || error.message || String(error), error.errors);
1254
1273
  }
1255
1274
  /**
1256
1275
  * AggregateError
1257
1276
  * - https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
1258
1277
  */
1259
1278
  var AggregateError$1 = class extends globalThis.AggregateError {
1260
- constructor(errors, msg) {
1279
+ constructor(errors, msg, options) {
1261
1280
  if (!errors || !(errors[Symbol.iterator] instanceof Function)) throw new TypeError(`input errors must be iterable but it's ${typeof errors}`);
1262
- super(errors, msg);
1281
+ const list = Array.from(errors);
1282
+ super(list, msg, options);
1263
1283
  Object.defineProperty(this, "name", { value: "AggregateError" });
1264
1284
  let aggStack;
1265
1285
  Object.defineProperties(this, {
@@ -1267,7 +1287,7 @@ var AggregateError$1 = class extends globalThis.AggregateError {
1267
1287
  configurable: true,
1268
1288
  enumerable: false,
1269
1289
  writable: true,
1270
- value: [].concat(errors)
1290
+ value: list
1271
1291
  },
1272
1292
  __stack: {
1273
1293
  enumerable: false,
@@ -1540,6 +1560,22 @@ var AveAzul = class AveAzul extends Promise {
1540
1560
  constructor(executor) {
1541
1561
  super(executor);
1542
1562
  }
1563
+ catch(...args) {
1564
+ if (args.length <= 1) return super.catch(args[0]);
1565
+ const handler = args.pop();
1566
+ if (typeof handler !== "function") throw new TypeError("The last argument to .catch() must be a function, got " + String(handler));
1567
+ const filters = args;
1568
+ return super.catch((err) => {
1569
+ for (const f of filters) if (f === Error || f != null && f.prototype instanceof Error) {
1570
+ if (err instanceof f) return handler(err);
1571
+ } else if (typeof f === "function") {
1572
+ if (f(err)) return handler(err);
1573
+ } else if (err !== null && (typeof err === "object" || typeof err === "function")) {
1574
+ if (Object.keys(f).every((k) => f[k] == err[k])) return handler(err);
1575
+ }
1576
+ throw err;
1577
+ });
1578
+ }
1543
1579
  /**
1544
1580
  * Bluebird-style tap() method that lets you perform side effects in a chain
1545
1581
  * Similar to Bluebird's Promise.prototype.tap()
@@ -1559,7 +1595,16 @@ var AveAzul = class AveAzul extends Promise {
1559
1595
  * @returns Promise that resolves with the filtered array
1560
1596
  */
1561
1597
  filter(fn) {
1562
- return this.then((value) => filter$2(value, fn));
1598
+ return this.then(async (value) => {
1599
+ const arr = toArray$1(value);
1600
+ const len = arr.length;
1601
+ const result = [];
1602
+ for (let i = 0; i < len; i++) {
1603
+ const x = isPromise(arr[i]) ? await arr[i] : arr[i];
1604
+ if (await fn(x, i, len)) result.push(x);
1605
+ }
1606
+ return result;
1607
+ });
1563
1608
  }
1564
1609
  /**
1565
1610
  * Bluebird-style map() method for array operations
@@ -1568,7 +1613,11 @@ var AveAzul = class AveAzul extends Promise {
1568
1613
  * @returns Promise that resolves with the mapped array
1569
1614
  */
1570
1615
  map(fn, options = { concurrency: 50 }) {
1571
- return this.then((value) => map$1(value, fn, options));
1616
+ return this.then((value) => {
1617
+ const arr = toArray$1(value);
1618
+ const len = arr.length;
1619
+ return map$1(arr, (item, index) => fn(item, index, len), options);
1620
+ });
1572
1621
  }
1573
1622
  /**
1574
1623
  * Bluebird-style mapSeries() method for array operations
@@ -1605,7 +1654,7 @@ var AveAzul = class AveAzul extends Promise {
1605
1654
  */
1606
1655
  each(fn) {
1607
1656
  return this.then(async (value) => {
1608
- const arr = value;
1657
+ const arr = toArray$1(value);
1609
1658
  const result = [];
1610
1659
  for (let i = 0; i < arr.length; i++) {
1611
1660
  let x = arr[i];
@@ -1675,7 +1724,7 @@ var AveAzul = class AveAzul extends Promise {
1675
1724
  reduce(fn, initialValue) {
1676
1725
  const hasInitial = arguments.length > 1;
1677
1726
  return this.then(async (array) => {
1678
- const arr = array;
1727
+ const arr = toArray$1(array);
1679
1728
  const len = arr.length;
1680
1729
  let value;
1681
1730
  let idx;
@@ -1750,8 +1799,9 @@ var AveAzul = class AveAzul extends Promise {
1750
1799
  if (typeof fn !== "function") return AveAzul.reject(/* @__PURE__ */ new TypeError("expecting a function but got " + fn));
1751
1800
  return this.then(async (args) => {
1752
1801
  if (Array.isArray(args)) {
1753
- for (let i = 0; i < args.length; i++) if (isPromise(args[i])) args[i] = await args[i];
1754
- return fn(...args);
1802
+ const values = args.slice();
1803
+ for (let i = 0; i < values.length; i++) if (isPromise(values[i])) values[i] = await values[i];
1804
+ return fn(...values);
1755
1805
  } else return fn(args);
1756
1806
  });
1757
1807
  }
@@ -1931,6 +1981,15 @@ var AveAzul = class AveAzul extends Promise {
1931
1981
  return AveAzul.resolve(items).each(fn);
1932
1982
  }
1933
1983
  /**
1984
+ * Bluebird-style filter() for array filtering
1985
+ * @param items - Array or iterable to filter
1986
+ * @param fn - Filter function (item, index, length)
1987
+ * @returns Promise that resolves with the items that passed the filter
1988
+ */
1989
+ static filter(items, fn) {
1990
+ return AveAzul.resolve(items).filter(fn);
1991
+ }
1992
+ /**
1934
1993
  * Bluebird-style reduce() for array reduction
1935
1994
  * @param array - Array to reduce
1936
1995
  * @param fn - Reducer function (value, item, index, length)
@@ -1990,7 +2049,7 @@ var AveAzul = class AveAzul extends Promise {
1990
2049
  * @returns Promise that resolves with handler result
1991
2050
  */
1992
2051
  static using(resources, ...args) {
1993
- if (args.length === 0) throw new TypeError("resrouces and handler function required");
2052
+ if (args.length === 0) throw new TypeError("resources and handler function required");
1994
2053
  if (Array.isArray(resources)) {
1995
2054
  if (args.length > 1) throw new TypeError("only two arguments are allowed when passing an array of resources");
1996
2055
  return using(resources, args[0], AveAzul, true);
@@ -2118,7 +2177,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2118
2177
  /* istanbul ignore if */
2119
2178
  if (!processOk(global.process)) return function() {};
2120
2179
  assert$3.equal(typeof cb, "function", "a callback must be provided for exit handler");
2121
- if (loaded === false) load$1();
2180
+ if (loaded === false) load$2();
2122
2181
  var ev = "exit";
2123
2182
  if (opts && opts.alwaysLast) ev = "afterexit";
2124
2183
  var remove = function() {
@@ -2168,7 +2227,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2168
2227
  return signals$1;
2169
2228
  };
2170
2229
  var loaded = false;
2171
- var load$1 = function load() {
2230
+ var load$2 = function load() {
2172
2231
  if (loaded || !processOk(global.process)) return;
2173
2232
  loaded = true;
2174
2233
  emitter.count += 1;
@@ -2183,7 +2242,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2183
2242
  process.emit = processEmit;
2184
2243
  process.reallyExit = processReallyExit;
2185
2244
  };
2186
- module.exports.load = load$1;
2245
+ module.exports.load = load$2;
2187
2246
  var originalProcessReallyExit = process.reallyExit;
2188
2247
  var processReallyExit = function processReallyExit(code) {
2189
2248
  /* istanbul ignore if */
@@ -21628,7 +21687,7 @@ var SignalExit = class extends SignalExitBase {
21628
21687
  }
21629
21688
  };
21630
21689
  const process$3 = globalThis.process;
21631
- const { onExit, load, unload } = signalExitWrap(processOk(process$3) ? new SignalExit(process$3) : new SignalExitFallback());
21690
+ const { onExit, load: load$1, unload } = signalExitWrap(processOk(process$3) ? new SignalExit(process$3) : new SignalExitFallback());
21632
21691
 
21633
21692
  //#endregion
21634
21693
  //#region node_modules/restore-cursor/index.js
@@ -24034,11 +24093,16 @@ const LogItemTypes = {
24034
24093
  none: 0
24035
24094
  };
24036
24095
  const DEFAULT_SPINNER_INTERVAL = 100;
24096
+ function levelValue(level) {
24097
+ if (!Object.prototype.hasOwnProperty.call(Levels, level)) throw new TypeError(`VisualLogger unknown log level: ${String(level)}`);
24098
+ return Levels[level];
24099
+ }
24037
24100
  const SPIN_OFF = 0;
24038
24101
  const SPIN_STARTED = 1;
24039
24102
  const SPIN_RUNNING = 2;
24040
24103
  var VisualLogger = class VisualLogger {
24041
24104
  constructor(options = {}) {
24105
+ var _a;
24042
24106
  options = {
24043
24107
  renderFps: 30,
24044
24108
  ...options
@@ -24047,7 +24111,7 @@ var VisualLogger = class VisualLogger {
24047
24111
  this._items = [];
24048
24112
  this._itemOptions = {};
24049
24113
  this._lines = [];
24050
- this._logLevel = Levels.info;
24114
+ this._logLevel = levelValue((_a = options.logLevel) !== null && _a !== void 0 ? _a : "info");
24051
24115
  this._itemType = LogItemTypes.normal;
24052
24116
  this._logData = [];
24053
24117
  this._output = options.output || defaultOutput$1;
@@ -24076,6 +24140,12 @@ var VisualLogger = class VisualLogger {
24076
24140
  this._color = enable;
24077
24141
  this.setPrefix();
24078
24142
  }
24143
+ get logLevel() {
24144
+ return Object.keys(Levels).find((k) => Levels[k] === this._logLevel);
24145
+ }
24146
+ set logLevel(level) {
24147
+ this._logLevel = levelValue(level);
24148
+ }
24079
24149
  static get spinners() {
24080
24150
  return [
24081
24151
  "|/-\\",
@@ -24210,7 +24280,10 @@ var VisualLogger = class VisualLogger {
24210
24280
  this._renderLineMsg(options, data);
24211
24281
  const updateData = typeof data === "string" ? {} : data;
24212
24282
  if (options.save !== false && updateData._save !== false) this._save(options._msg);
24213
- if (updateData._render === false) return this;
24283
+ if (updateData._render === false) {
24284
+ this._lines[itemIdx] = this._shouldLogItem() ? this._renderLine(options) : options._msg;
24285
+ return this;
24286
+ }
24214
24287
  }
24215
24288
  if (this._shouldLogItem()) {
24216
24289
  if (data === void 0 && !this._nextSpin(options)) return this;
@@ -26695,6 +26768,7 @@ var InflightStore = class {
26695
26768
  //#endregion
26696
26769
  //#region node_modules/item-queue/dist/item-queue.js
26697
26770
  const PAUSE_ITEM = Symbol("pause");
26771
+ const PAUSE_CALL = Symbol("pauseCall");
26698
26772
  const RESUME_ITEM = Symbol("resume");
26699
26773
  const NOOP_ITEM = Symbol("NOOP");
26700
26774
  const WATCH_PERIOD = 500;
@@ -26705,6 +26779,8 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26705
26779
  constructor(options) {
26706
26780
  assert(options && typeof options.processItem === "function", "ItemQueue: must provide options.processItem callback");
26707
26781
  super();
26782
+ /** bumped by an immediate pause(), so a resume() tick scheduled earlier skips itself */
26783
+ this._pauseSeq = 0;
26708
26784
  this.Promise = options.Promise || ItemQueue.Promise;
26709
26785
  assert(this.Promise, "ItemQueue: No Promise implementation available");
26710
26786
  this._pending = new InflightStore();
@@ -26714,7 +26790,7 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26714
26790
  this._processItem = options.processItem;
26715
26791
  this._stopOnError = options.stopOnError;
26716
26792
  this._failed = false;
26717
- this._timeout = options.timeout;
26793
+ this._timeout = options.timeout > 0 ? options.timeout : 0;
26718
26794
  this._watchPeriod = options.watchPeriod || WATCH_PERIOD;
26719
26795
  this._watchTime = options.watchTime;
26720
26796
  this._id = 1;
@@ -26731,19 +26807,18 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26731
26807
  * @returns promise that wait for queue to finish
26732
26808
  */
26733
26809
  wait() {
26734
- if (this._failed) return this.Promise.reject(this._failed);
26810
+ if (this._failed) return this.Promise.reject(this._failError);
26735
26811
  if (this.isPending) return new this.Promise((resolve, reject) => {
26736
- const h = (data) => {
26737
- if (data.error) {
26738
- this.removeListener("done", h);
26739
- reject(data.error);
26740
- } else {
26741
- this.removeListener("fail", h);
26742
- resolve(data);
26743
- }
26812
+ const onDone = (data) => {
26813
+ this.removeListener("fail", onFail);
26814
+ resolve(data);
26815
+ };
26816
+ const onFail = (data) => {
26817
+ this.removeListener("done", onDone);
26818
+ reject(data.error);
26744
26819
  };
26745
- this.once("done", h);
26746
- this.once("fail", h);
26820
+ this.once("done", onDone);
26821
+ this.once("fail", onFail);
26747
26822
  });
26748
26823
  return this.Promise.resolve();
26749
26824
  }
@@ -26831,7 +26906,15 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26831
26906
  * @returns instance self
26832
26907
  */
26833
26908
  pause() {
26834
- this._itemQ.unshift(this._wrap(ItemQueue.pauseItem));
26909
+ if (this._processing || !this._pending.isEmpty) this._itemQ.unshift({
26910
+ item: void 0,
26911
+ _control: PAUSE_CALL
26912
+ });
26913
+ else {
26914
+ this._pause = true;
26915
+ this._pauseSeq++;
26916
+ process.nextTick(() => this.emit("pause"));
26917
+ }
26835
26918
  return this;
26836
26919
  }
26837
26920
  /**
@@ -26854,7 +26937,10 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26854
26937
  * @returns instance self
26855
26938
  */
26856
26939
  resume() {
26940
+ this._itemQ = this._itemQ.filter((x) => x._control !== PAUSE_CALL);
26941
+ const pauseSeq = this._pauseSeq;
26857
26942
  process.nextTick(() => {
26943
+ if (pauseSeq !== this._pauseSeq) return;
26858
26944
  this.unpause();
26859
26945
  if (this._itemQ.length === 0) this._itemQ.push(this._wrap(RESUME_ITEM));
26860
26946
  this._process();
@@ -26872,7 +26958,7 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26872
26958
  return this.resume();
26873
26959
  }
26874
26960
  _wrap(item, stopOnError) {
26875
- if (typeof item === "symbol") return {
26961
+ if (item === PAUSE_ITEM || item === RESUME_ITEM) return {
26876
26962
  item: void 0,
26877
26963
  _control: item
26878
26964
  };
@@ -26884,14 +26970,15 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26884
26970
  _emit(evt, data) {
26885
26971
  this.emit(evt, data);
26886
26972
  }
26887
- _handleQueueItemDone(data) {
26973
+ _handleQueueItemDone(data, failed) {
26888
26974
  if (data.id > 0) this._pending.remove(data.id);
26889
26975
  if (this._failed) return;
26890
26976
  if (!data._control && data.id > 0) {
26891
- if (data.error) {
26977
+ if (failed) {
26892
26978
  this._emit("failItem", data);
26893
26979
  if (data.stopOnError !== false && this._stopOnError) {
26894
- this._failed = data.error;
26980
+ this._failed = true;
26981
+ this._failError = data.error;
26895
26982
  this._emit("fail", data);
26896
26983
  return;
26897
26984
  }
@@ -26963,25 +27050,60 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26963
27050
  }
26964
27051
  this._watchTimer = setTimeout(() => this._pendingWatcher(), this._watchPeriod).unref();
26965
27052
  }
27053
+ /**
27054
+ * Fail the item if it doesn't settle within `options.timeout`. The first outcome wins,
27055
+ * so a result that arrives after the timeout is ignored.
27056
+ */
27057
+ _withTimeout(promise, abort) {
27058
+ return new this.Promise((resolve, reject) => {
27059
+ const timer = setTimeout(() => {
27060
+ const error = Object.assign(/* @__PURE__ */ new Error(`item-queue: item timed out after ${this._timeout}ms`), { code: "ETIMEDOUT" });
27061
+ abort.abort(error);
27062
+ reject(error);
27063
+ }, this._timeout);
27064
+ promise.then((res) => {
27065
+ clearTimeout(timer);
27066
+ resolve(res);
27067
+ }, (err) => {
27068
+ clearTimeout(timer);
27069
+ reject(err);
27070
+ });
27071
+ });
27072
+ }
26966
27073
  _setupWatch() {
26967
27074
  if (!this._watchTimer && this._watchTime) process.nextTick(() => this._pendingWatcher());
26968
27075
  }
27076
+ /**
27077
+ * A failed queue never runs queued items. Report each one as a failed item
27078
+ * with the queue's error, so callers waiting on a `failItem` don't hang.
27079
+ */
27080
+ _failQueued() {
27081
+ const queued = this._itemQ;
27082
+ this._itemQ = [];
27083
+ for (const wrapped of queued) if (!wrapped._control) this._emit("failItem", {
27084
+ id: this._id++,
27085
+ error: this._failError,
27086
+ ...wrapped
27087
+ });
27088
+ return 0;
27089
+ }
26969
27090
  _process() {
26970
27091
  if (this._startTime === void 0) this._startTime = Date.now();
27092
+ if (this._failed) return this._failQueued();
26971
27093
  if (this._processing || this._pause || this._itemQ.length === 0) return 0;
26972
27094
  this._processing = true;
26973
27095
  let count = 0;
26974
27096
  let i = this._pending.count;
26975
27097
  for (; this._itemQ.length > 0 && i < this._concurrency; i++) {
26976
27098
  const wrapped = this._itemQ.shift();
26977
- if (wrapped._control === PAUSE_ITEM) {
27099
+ if (wrapped._control === PAUSE_ITEM || wrapped._control === PAUSE_CALL) {
26978
27100
  this._pause = true;
26979
27101
  if (this._pending.isEmpty) process.nextTick(() => {
26980
27102
  this._handleQueueItemDone({
26981
27103
  id: 0,
26982
27104
  item: void 0,
26983
27105
  _control: NOOP_ITEM
26984
- });
27106
+ }, false);
26985
27107
  });
26986
27108
  break;
26987
27109
  }
@@ -26989,12 +27111,16 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26989
27111
  const id = this._id++;
26990
27112
  let promise;
26991
27113
  if (wrapped._control === RESUME_ITEM) promise = this.Promise.resolve({});
26992
- else try {
26993
- const res = this._processItem(wrapped.item, id);
26994
- if (res && res.then) promise = res;
26995
- else promise = this.Promise.resolve(res);
26996
- } catch (err) {
26997
- promise = this.Promise.reject(err);
27114
+ else {
27115
+ const abort = this._timeout ? new AbortController() : void 0;
27116
+ try {
27117
+ const res = this._processItem(wrapped.item, id, abort === null || abort === void 0 ? void 0 : abort.signal);
27118
+ if (res && res.then) promise = res;
27119
+ else promise = this.Promise.resolve(res);
27120
+ } catch (err) {
27121
+ promise = this.Promise.reject(err);
27122
+ }
27123
+ if (abort) promise = this._withTimeout(promise, abort);
26998
27124
  }
26999
27125
  this._pending.add(id, {
27000
27126
  item: wrapped.item,
@@ -27002,12 +27128,12 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
27002
27128
  id,
27003
27129
  res,
27004
27130
  ...wrapped
27005
- }), (error) => {
27131
+ }, false), (error) => {
27006
27132
  this._handleQueueItemDone({
27007
27133
  id,
27008
27134
  error,
27009
27135
  ...wrapped
27010
- });
27136
+ }, true);
27011
27137
  })
27012
27138
  });
27013
27139
  }
@@ -27192,7 +27318,7 @@ function makeExtrasData(file, fullFile, path, stat, files, options) {
27192
27318
  * @param extras
27193
27319
  */
27194
27320
  function addResult(result, options, extras) {
27195
- const group = (options.grouping && typeof result === "string" ? result : result && result.group) || "files";
27321
+ const group = options.grouping && (typeof result === "string" ? result : result.group) || "files";
27196
27322
  if (!options.result[group]) options.result[group] = [];
27197
27323
  if (result.formatName !== void 0) options.result[group].push(result.formatName);
27198
27324
  else options.result[group].push(options.prependCwd ? extras.fullFile : extras.dirFile);
@@ -27234,10 +27360,10 @@ function processFile(options, extras) {
27234
27360
  }
27235
27361
  return false;
27236
27362
  }
27237
- function acceptEntry(options, entry, path, gitignore) {
27363
+ function acceptEntry(options, entry, path, gitignore, fsPath) {
27238
27364
  const isDirectory = entry.isDirectory();
27239
27365
  if (isDirectory && options._ignoreDirs.has(entry.name)) return false;
27240
- if (gitignore === null || gitignore === void 0 ? void 0 : gitignore.ignores(Path.resolve(options.dir, path, entry.name), isDirectory)) return false;
27366
+ if (gitignore === null || gitignore === void 0 ? void 0 : gitignore.ignores(Path.resolve(options.dir, fsPath, entry.name), isDirectory)) return false;
27241
27367
  if (options.prefilter && !options.prefilter(entry.name, path, entry)) return false;
27242
27368
  if (isDirectory) return true;
27243
27369
  if (options.ignoreExt.length || options.filterExt.length) {
@@ -27280,7 +27406,7 @@ function releaseDirectory(options) {
27280
27406
  * @param level
27281
27407
  * @returns
27282
27408
  */
27283
- async function walk(path, options, level = 0, parentRules) {
27409
+ async function walk(path, options, level = 0, parentRules, fsPath = path) {
27284
27410
  let promises = [];
27285
27411
  let hasSlot = false;
27286
27412
  try {
@@ -27290,7 +27416,9 @@ async function walk(path, options, level = 0, parentRules) {
27290
27416
  hasSlot = true;
27291
27417
  }
27292
27418
  if (options._stopped) return void 0;
27293
- const dir = Path.join(options.dir, path);
27419
+ const dir = Path.join(options.dir, fsPath);
27420
+ const posixSep = options._sep === "/";
27421
+ const outDir = posixSep ? dir : join2(options._sep, options._outCwd, path);
27294
27422
  const gitignore = options.gitignore ? await GitignoreRules.load(Path.resolve(dir), options.gitignore, parentRules) : void 0;
27295
27423
  let files = await asyncReaddir(dir, options.readdirOpts);
27296
27424
  if (options.sortFiles) {
@@ -27301,16 +27429,16 @@ async function walk(path, options, level = 0, parentRules) {
27301
27429
  const extrasFiles = options.fullStat && options._earlyFilter && (options.filter || options.filterDir) ? files.map((entry) => entry.name) : files;
27302
27430
  for (let ix = 0; !options._stopped && ix < files.length; ix++) {
27303
27431
  const file = files[ix];
27304
- if (options._earlyFilter && !acceptEntry(options, file, path, gitignore)) continue;
27432
+ if (options._earlyFilter && !acceptEntry(options, file, path, gitignore, fsPath)) continue;
27305
27433
  let extras;
27306
27434
  if (options.fullStat) {
27307
27435
  const name = options._earlyFilter ? file.name : file;
27308
- const fullFile = join2(options._sep, dir, name);
27309
- const stat = await asyncLStat(fullFile);
27436
+ const fullFile = join2(options._sep, outDir, name);
27437
+ const stat = await asyncLStat(posixSep ? fullFile : join2("/", dir, name));
27310
27438
  if (options._stopped) break;
27311
27439
  extras = makeExtrasData(name, fullFile, path, stat, extrasFiles, options);
27312
27440
  } else {
27313
- const fullFile = join2(options._sep, dir, file.name);
27441
+ const fullFile = join2(options._sep, outDir, file.name);
27314
27442
  extras = makeExtrasData(file.name, fullFile, path, file, files, options);
27315
27443
  }
27316
27444
  if (extras.stat.isDirectory()) dirs.push(extras);
@@ -27328,7 +27456,8 @@ async function walk(path, options, level = 0, parentRules) {
27328
27456
  break;
27329
27457
  }
27330
27458
  if (!flags.skip && level < options.maxLevel) {
27331
- const walkP = walk(extras.dirFile, options, level + 1, gitignore);
27459
+ const childFsPath = posixSep ? extras.dirFile : join2("/", fsPath, extras.file);
27460
+ const walkP = walk(extras.dirFile, options, level + 1, gitignore, childFsPath);
27332
27461
  if (options.concurrency > 1) {
27333
27462
  promises.push(walkP);
27334
27463
  if (promises.length >= options.concurrency) {
@@ -27357,8 +27486,15 @@ async function walk(path, options, level = 0, parentRules) {
27357
27486
  * @returns
27358
27487
  */
27359
27488
  function makeOptions(opts) {
27360
- const options = typeof opts === "string" ? { cwd: opts } : opts;
27489
+ const input = typeof opts === "string" ? { cwd: opts } : opts;
27490
+ const options = {};
27491
+ for (const key of Object.keys(input)) if (input[key] !== void 0) options[key] = input[key];
27361
27492
  if (options.prefilter && options.fullStat === false) throw new TypeError("prefilter requires fullStat: true");
27493
+ for (const name of [
27494
+ "filter",
27495
+ "filterDir",
27496
+ "prefilter"
27497
+ ]) if (Object.prototype.toString.call(options[name]) === "[object AsyncFunction]") throw new TypeError(`${name} must return a result synchronously, not a promise`);
27362
27498
  const sep = options.pathSep || Path.posix.sep;
27363
27499
  let cwd = options.cwd || options.dir || process.cwd();
27364
27500
  if (!options.hasOwnProperty("pathSep") && cwd.includes("\\")) cwd = cwd.replace(/\\/g, "/");
@@ -27380,6 +27516,7 @@ function makeOptions(opts) {
27380
27516
  grouping: void 0
27381
27517
  }, options, {
27382
27518
  dir: cwd,
27519
+ _outCwd: Path.join(cwd),
27383
27520
  fullStat: options.fullStat === void 0 ? true : options.fullStat,
27384
27521
  result: Object.create(null),
27385
27522
  ignoreExt: [].concat(options.ignoreExt).map(cleanExt).filter((x) => x),
@@ -27469,6 +27606,8 @@ const commaPattern = /\\,/g;
27469
27606
  const periodPattern = /\\\./g;
27470
27607
  const EXPANSION_MAX = 1e5;
27471
27608
  const EXPANSION_MAX_LENGTH = 4e6;
27609
+ const EXPANSION_MAX_DEPTH = 1e3;
27610
+ const EXPANSION_MAX_REWRITES = 1e3;
27472
27611
  function numeric(str) {
27473
27612
  return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
27474
27613
  }
@@ -27478,32 +27617,43 @@ function escapeBraces(str) {
27478
27617
  function unescapeBraces(str) {
27479
27618
  return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
27480
27619
  }
27620
+ function pushAll(target, items) {
27621
+ for (let i = 0; i < items.length; i++) target.push(items[i]);
27622
+ }
27481
27623
  /**
27482
27624
  * Basically just str.split(","), but handling cases
27483
27625
  * where we have nested braced sections, which should be
27484
27626
  * treated as individual members, like {a,{b,c},d}
27485
27627
  */
27486
27628
  function parseCommaParts(str) {
27487
- if (!str) return [""];
27488
27629
  const parts = [];
27489
- const m = balanced("{", "}", str);
27490
- if (!m) return str.split(",");
27491
- const { pre, body, post } = m;
27492
- const p = pre.split(",");
27493
- p[p.length - 1] += "{" + body + "}";
27494
- const postParts = parseCommaParts(post);
27495
- if (post.length) {
27496
- p[p.length - 1] += postParts.shift();
27497
- p.push.apply(p, postParts);
27498
- }
27499
- parts.push.apply(parts, p);
27500
- return parts;
27630
+ let carry = "";
27631
+ for (;;) {
27632
+ const m = balanced("{", "}", str);
27633
+ if (!m) {
27634
+ const tail = str.split(",");
27635
+ tail[0] = carry + tail[0];
27636
+ pushAll(parts, tail);
27637
+ return parts;
27638
+ }
27639
+ const { pre, body, post } = m;
27640
+ const p = pre.split(",");
27641
+ p[0] = carry + p[0];
27642
+ p[p.length - 1] += "{" + body + "}";
27643
+ if (!post.length) {
27644
+ pushAll(parts, p);
27645
+ return parts;
27646
+ }
27647
+ carry = p.pop();
27648
+ pushAll(parts, p);
27649
+ str = post;
27650
+ }
27501
27651
  }
27502
27652
  function expand(str, options = {}) {
27503
27653
  if (!str) return [];
27504
- const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
27654
+ const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES } = options;
27505
27655
  if (str.slice(0, 2) === "{}") str = "\\{\\}" + str.slice(2);
27506
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
27656
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
27507
27657
  }
27508
27658
  function embrace(str) {
27509
27659
  return "{" + str + "}";
@@ -27569,8 +27719,10 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
27569
27719
  }
27570
27720
  return N;
27571
27721
  }
27572
- function expand_(str, max, maxLength, isTop) {
27722
+ function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
27723
+ if (depth > maxDepth) return [str];
27573
27724
  let acc = [""];
27725
+ let rewrites = 0;
27574
27726
  let dropEmpties = false;
27575
27727
  let firstGroup = true;
27576
27728
  for (;;) {
@@ -27589,7 +27741,8 @@ function expand_(str, max, maxLength, isTop) {
27589
27741
  const isSequence = isNumericSequence || isAlphaSequence;
27590
27742
  const isOptions = m.body.indexOf(",") >= 0;
27591
27743
  if (!isSequence && !isOptions) {
27592
- if (m.post.match(/,(?!,).*\}/)) {
27744
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
27745
+ rewrites++;
27593
27746
  str = m.pre + "{" + m.body + escClose + m.post;
27594
27747
  isTop = true;
27595
27748
  continue;
@@ -27605,7 +27758,7 @@ function expand_(str, max, maxLength, isTop) {
27605
27758
  else {
27606
27759
  let n = parseCommaParts(m.body);
27607
27760
  if (n.length === 1 && n[0] !== void 0) {
27608
- n = expand_(n[0], max, maxLength, false).map(embrace);
27761
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
27609
27762
  /* c8 ignore start */
27610
27763
  if (n.length === 1) {
27611
27764
  acc = combine(acc, pre + n[0], [""], max, maxLength, dropEmpties && !m.post.length);
@@ -27619,7 +27772,7 @@ function expand_(str, max, maxLength, isTop) {
27619
27772
  values = [];
27620
27773
  let valuesLength = 0;
27621
27774
  outer: for (let j = 0; j < n.length; j++) {
27622
- const expanded = expand_(n[j], max, maxLength, false);
27775
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
27623
27776
  for (let k = 0; k < expanded.length; k++) {
27624
27777
  const v = expanded[k];
27625
27778
  if (dropsEmpties && !v) continue;
@@ -29724,14 +29877,14 @@ var PackageRef = class {
29724
29877
  * @param ref package string reference
29725
29878
  */
29726
29879
  parseRef(ref) {
29727
- const parts = ref.split(":");
29728
- if (parts.length > 1) {
29729
- const tmp = parts[0].trim();
29880
+ const colonIx = ref.indexOf(":");
29881
+ if (colonIx >= 0) {
29882
+ const tmp = ref.substring(0, colonIx).trim();
29730
29883
  if (tmp === "id" || tmp === "path" || tmp === "name") this.type = tmp;
29731
29884
  else throw new Error(`package ref '${ref}' has unknown type '${tmp}' - must be 'id', 'path', or 'name'`);
29732
- this.value = parts[1].trim();
29885
+ this.value = ref.substring(colonIx + 1).trim();
29733
29886
  } else {
29734
- this.value = parts[0].trim();
29887
+ this.value = ref.trim();
29735
29888
  if (this.value.lastIndexOf("@") > 0) this.type = "id";
29736
29889
  else this.type = "name";
29737
29890
  }
@@ -29745,6 +29898,7 @@ var PackageRef = class {
29745
29898
  } else if (this.type === "path") this.mm = new Minimatch(this.value);
29746
29899
  }
29747
29900
  match(pkgInfo) {
29901
+ if (this.regex) this.regex.lastIndex = 0;
29748
29902
  if (this.type === "path") return this.regex ? this.regex.exec(pkgInfo.path) !== null : this.mm.match(pkgInfo.path);
29749
29903
  else if (this.type === "id") return this.regex ? this.regex.exec(pkgInfoId(pkgInfo)) !== null : this.value === pkgInfoId(pkgInfo);
29750
29904
  else return this.regex ? this.regex.exec(pkgInfo.name) !== null : this.value === pkgInfo.name;
@@ -29776,6 +29930,24 @@ function resolvePackage(semver, packages, fallbackToFirst = true) {
29776
29930
  if (fallbackToFirst) return packages[0];
29777
29931
  }
29778
29932
  /**
29933
+ * Widen a local dep's semver per `localDepAutoSemver`, same as fyn's getAutoSemver.
29934
+ *
29935
+ * @param semver semver string
29936
+ * @param autoSemver the `localDepAutoSemver` setting
29937
+ * @returns the semver to match local versions with
29938
+ */
29939
+ function localAutoSemver(semver, autoSemver) {
29940
+ if (autoSemver) {
29941
+ const parsedSv = import_semver.default.coerce(semver);
29942
+ if (parsedSv && parsedSv.raw) switch (autoSemver) {
29943
+ case "patch": return `~${parsedSv.raw}`;
29944
+ case "minor": return `^${parsedSv.raw}`;
29945
+ case "major": return "*";
29946
+ }
29947
+ }
29948
+ return semver;
29949
+ }
29950
+ /**
29779
29951
  * Make a package ID from basic info by joining name and version into a single string.
29780
29952
  *
29781
29953
  * @param info package basic info
@@ -29949,6 +30121,7 @@ var FynpoDepGraph = class {
29949
30121
  });
29950
30122
  files.push(scanned);
29951
30123
  }
30124
+ this.packages.byName = Object.create(null);
29952
30125
  const filesByDepth = [...[].concat(...files)].sort((a, b) => {
29953
30126
  const depth = (file) => posixify$2(Path.dirname(file)).split("/").length;
29954
30127
  return depth(a) - depth(b) || a.localeCompare(b);
@@ -30115,16 +30288,17 @@ var FynpoDepGraph = class {
30115
30288
  if (this.checkNoFynLocal(pkgInfo, name)) continue;
30116
30289
  const semId = pkgId$1(name, semver);
30117
30290
  const resolveId = this.resolvedCache[semId];
30118
- const depPkg = resolveId && byId[resolveId] || resolvePackage(semver, byName[name]);
30119
- if (!resolveId) this.resolvedCache[semId] = pkgInfoId(depPkg);
30120
- this.addDep(pkgInfo, depPkg, section);
30291
+ if (resolveId === "") continue;
30292
+ let depPkg = resolveId ? byId[resolveId] : void 0;
30293
+ if (!depPkg) {
30294
+ const localSemver = localAutoSemver(semver, this._options.localDepAutoSemver);
30295
+ depPkg = resolvePackage(localSemver, byName[name], !import_semver.default.validRange(localSemver));
30296
+ this.resolvedCache[semId] = depPkg ? pkgInfoId(depPkg) : "";
30297
+ }
30298
+ if (depPkg) this.addDep(pkgInfo, depPkg, section);
30121
30299
  }
30122
30300
  };
30123
- for (const path in byPath) if (!depMapByPath[path]) depMapByPath[path] = {
30124
- pkgInfo: byPath[path],
30125
- localDepsByPath: Object.create(null),
30126
- dependentsByPath: Object.create(null)
30127
- };
30301
+ for (const path in byPath) this.getDepData(byPath[path]);
30128
30302
  for (const path in byPath) {
30129
30303
  const pkgInfo = byPath[path];
30130
30304
  const depData = depMapByPath[pkgInfo.path];
@@ -30134,6 +30308,21 @@ var FynpoDepGraph = class {
30134
30308
  }
30135
30309
  }
30136
30310
  /**
30311
+ * Get the dep data of a package, creating an empty one if it doesn't exist yet
30312
+ *
30313
+ * @param pkgInfo package info
30314
+ * @returns dep data
30315
+ */
30316
+ getDepData(pkgInfo) {
30317
+ let depData = this.depMapByPath[pkgInfo.path];
30318
+ if (!depData) depData = this.depMapByPath[pkgInfo.path] = {
30319
+ pkgInfo,
30320
+ localDepsByPath: Object.create(null),
30321
+ dependentsByPath: Object.create(null)
30322
+ };
30323
+ return depData;
30324
+ }
30325
+ /**
30137
30326
  * Add a dependency relation by package paths
30138
30327
  *
30139
30328
  * @param from - path of package depend from
@@ -30198,8 +30387,8 @@ var FynpoDepGraph = class {
30198
30387
  */
30199
30388
  addDep(pkgInfo, depPkg, depSection, indirectSteps) {
30200
30389
  if (pkgInfo.path === depPkg.path) return false;
30201
- const dataPkg = this.depMapByPath[pkgInfo.path];
30202
- const dataDep = this.depMapByPath[depPkg.path];
30390
+ const dataPkg = this.getDepData(pkgInfo);
30391
+ const dataDep = this.getDepData(depPkg);
30203
30392
  if (dataPkg.localDepsByPath[depPkg.path]) return false;
30204
30393
  dataPkg.localDepsByPath[depPkg.path] = {
30205
30394
  name: depPkg.name,
@@ -30367,7 +30556,7 @@ let defaultLog$1 = consoleLog;
30367
30556
  const ERR_MODULE_NOT_FOUND = "ERR_MODULE_NOT_FOUND";
30368
30557
  const ERR_PACKAGE_PATH_NOT_EXPORTED = "ERR_PACKAGE_PATH_NOT_EXPORTED";
30369
30558
  function normalizeOpts(optsOrMsg) {
30370
- if (optsOrMsg === void 0) return {};
30559
+ if (optsOrMsg == null) return {};
30371
30560
  if (optsOrMsg === true || typeof optsOrMsg === "string") return { message: optsOrMsg };
30372
30561
  return optsOrMsg;
30373
30562
  }
@@ -30852,7 +31041,8 @@ const fyntil = {
30852
31041
  const graph = new FynpoDepGraph({
30853
31042
  cwd: fcm.topDir,
30854
31043
  packages: config.packages,
30855
- noFynLocal: config.noFynLocal
31044
+ noFynLocal: config.noFynLocal,
31045
+ localDepAutoSemver: config.localDepAutoSemver
30856
31046
  });
30857
31047
  await graph.resolve();
30858
31048
  return fyntil.fynpoConfig = {
@@ -30942,14 +31132,15 @@ const fyntil = {
30942
31132
  if (!Path.isAbsolute(rel) && !rel.startsWith(".")) return `.${Path.sep}${rel}`;
30943
31133
  return shouldPosixify ? posixify$2(rel) : rel;
30944
31134
  },
30945
- async readPkgJson(dirOrFile, keepRaw = false, packageFyn = false) {
31135
+ /** @param pkgJsonStr - package.json's content, when the caller has it already */
31136
+ async readPkgJson(dirOrFile, keepRaw = false, packageFyn = false, pkgJsonStr) {
30946
31137
  const dir = !dirOrFile.endsWith(".json") ? dirOrFile : Path.dirname(dirOrFile);
30947
31138
  const files = ["package.json", packageFyn && "package-fyn.json"].filter(Boolean);
30948
31139
  const finalJson = {};
30949
31140
  for (const fname of files) {
30950
31141
  const file = Path.join(dir, fname);
30951
31142
  try {
30952
- const str = await file_ops_default.readFile(file, "utf8");
31143
+ const str = fname === "package.json" && pkgJsonStr !== void 0 ? pkgJsonStr : await file_ops_default.readFile(file, "utf8");
30953
31144
  const json = JSON.parse(str.trim());
30954
31145
  merge(finalJson, json);
30955
31146
  if (keepRaw && fname !== "package-fyn.json") finalJson[PACKAGE_RAW_INFO] = {
@@ -35966,15 +36157,114 @@ var To = (s) => {
35966
36157
  s.mtimeCache || (s.mtimeCache = /* @__PURE__ */ new Map()), s.filter = t ? (e, i) => t(e, i) && !((s.mtimeCache?.get(e) ?? i.mtime ?? 0) > (i.mtime ?? 0)) : (e, i) => !((s.mtimeCache?.get(e) ?? i.mtime ?? 0) > (i.mtime ?? 0));
35967
36158
  };
35968
36159
 
36160
+ //#endregion
36161
+ //#region lib/util/fs-worker-pool.ts
36162
+ /** One worker less than the CPUs, for the main thread, and at most 8 */
36163
+ const POOL_SIZE = Math.max(1, Math.min(8, os.availableParallelism() - 1));
36164
+ /**
36165
+ * A downloaded Buffer is often a view into a much bigger ArrayBuffer, and postMessage copies the
36166
+ * whole ArrayBuffer. So the message gets a copy of just the view, transferred. The caller's
36167
+ * Buffer stays usable.
36168
+ */
36169
+ function postJob(worker, id, { op, job }) {
36170
+ const { data } = job;
36171
+ if (!data) {
36172
+ worker.postMessage({
36173
+ id,
36174
+ op,
36175
+ job
36176
+ });
36177
+ return;
36178
+ }
36179
+ const copy = new Uint8Array(data.byteLength);
36180
+ copy.set(data);
36181
+ worker.postMessage({
36182
+ id,
36183
+ op,
36184
+ job: {
36185
+ ...job,
36186
+ data: copy
36187
+ }
36188
+ }, [copy.buffer]);
36189
+ }
36190
+ var FsWorkerPool = class {
36191
+ _file;
36192
+ _workers = [];
36193
+ _queue = [];
36194
+ _nextId = 0;
36195
+ constructor(file) {
36196
+ this._file = file;
36197
+ }
36198
+ run(op, job) {
36199
+ return new Promise((resolve, reject) => {
36200
+ this._queue.push({
36201
+ job: {
36202
+ op,
36203
+ job
36204
+ },
36205
+ resolve,
36206
+ reject
36207
+ });
36208
+ this._dispatch();
36209
+ });
36210
+ }
36211
+ _dispatch() {
36212
+ while (this._queue.length > 0) {
36213
+ let pw = this._workers.find((w) => !w.busy);
36214
+ if (!pw) {
36215
+ if (this._workers.length >= POOL_SIZE) return;
36216
+ pw = this._start();
36217
+ }
36218
+ const pending = this._queue.shift();
36219
+ pw.busy = pending;
36220
+ pw.worker.ref();
36221
+ postJob(pw.worker, this._nextId++, pending.job);
36222
+ }
36223
+ }
36224
+ _start() {
36225
+ const pw = { worker: new Worker(this._file) };
36226
+ pw.worker.on("message", (msg) => {
36227
+ const pending = pw.busy;
36228
+ pw.busy = void 0;
36229
+ pw.worker.unref();
36230
+ if (msg.error) pending.reject(Object.assign(new Error(msg.error.message), msg.error));
36231
+ else pending.resolve(msg.result);
36232
+ this._dispatch();
36233
+ });
36234
+ pw.worker.on("error", (err) => {
36235
+ this._workers = this._workers.filter((w) => w !== pw);
36236
+ pw.busy?.reject(err);
36237
+ pw.busy = void 0;
36238
+ this._dispatch();
36239
+ });
36240
+ this._workers.push(pw);
36241
+ return pw;
36242
+ }
36243
+ };
36244
+ let pool;
36245
+ /**
36246
+ * The pool, or undefined when there's no built worker next to this module, which is the case
36247
+ * when running from source. Callers then do the work in-thread.
36248
+ */
36249
+ function getFsWorkerPool() {
36250
+ if (pool === void 0) {
36251
+ const file = fileURLToPath(new URL("./fs-worker.mjs", import.meta.url));
36252
+ pool = Br.existsSync(file) && new FsWorkerPool(file);
36253
+ logger$1.debug(pool ? `fs workers: up to ${POOL_SIZE} from ${file}` : "fs workers: none built, working in-thread");
36254
+ }
36255
+ return pool || void 0;
36256
+ }
36257
+
35969
36258
  //#endregion
35970
36259
  //#region lib/pkg-dist-extractor.ts
35971
36260
  const { retry, missPipe: missPipe$2 } = fyntil;
36261
+ const isStoreJob = (result) => typeof result?.store === "function";
35972
36262
  var PkgDistExtractor = class {
35973
36263
  _promiseQ;
35974
36264
  _fyn;
35975
36265
  constructor(options) {
35976
36266
  this._promiseQ = new promise_queue_default({
35977
- concurrency: 4,
36267
+ concurrency: options.fyn.extractConcurrency || Math.min(4, POOL_SIZE + 1),
35978
36268
  stopOnError: true,
35979
36269
  processItem: (x, id) => this.processItem(x, id)
35980
36270
  });
@@ -36024,6 +36314,7 @@ var PkgDistExtractor = class {
36024
36314
  const { pkg } = data;
36025
36315
  const promotedOpt = defaults$1({ promoted }, pick(pkg, "promoted"));
36026
36316
  const fullOutDir = this._fyn.getInstalledPkgDir(pkg.name, pkg.version, promotedOpt);
36317
+ let placed;
36027
36318
  if (pkg.extracted && pkg.extracted === fullOutDir) {
36028
36319
  logger$1.debug(`package ${pkg.name} ${pkg.version} has already been extracted to ${pkg.extracted}`);
36029
36320
  if (this._fyn.isNormalLayout && pkg.promoted && !promotedOpt.promoted) await this.movePromotedPkgFromFV(pkg, fullOutDir);
@@ -36036,18 +36327,24 @@ var PkgDistExtractor = class {
36036
36327
  }
36037
36328
  return json;
36038
36329
  }
36039
- await this._fyn.createPkgOutDir(fullOutDir);
36040
- const result = data.result;
36330
+ const job = data.result;
36331
+ if (isStoreJob(job) && !await job.store(!data.listener && !data.deferred)) {
36332
+ data.deferred = true;
36333
+ this._promiseQ.addItem(data);
36334
+ return;
36335
+ }
36336
+ const result = isStoreJob(job) ? job.integrity : job;
36041
36337
  let act;
36042
36338
  let retrieve;
36043
36339
  if (typeof result === "string") {
36044
36340
  act = "hardlink";
36045
- retrieve = () => {
36046
- return this._fyn.central.replicate(result, fullOutDir);
36341
+ retrieve = async () => {
36342
+ placed = await this._fyn.central.replicate(result, fullOutDir, () => this._fyn.createPkgOutDir(fullOutDir));
36047
36343
  };
36048
36344
  } else {
36049
36345
  act = "extract";
36050
- retrieve = () => {
36346
+ retrieve = async () => {
36347
+ await this._fyn.createPkgOutDir(fullOutDir);
36051
36348
  const untarStream = So({
36052
36349
  strip: 1,
36053
36350
  strict: true,
@@ -36063,7 +36360,7 @@ var PkgDistExtractor = class {
36063
36360
  logger$1.updateItem(LOAD_PACKAGE, `${act}ed ${msg}`);
36064
36361
  }
36065
36362
  let retries = 0;
36066
- return retry(() => this._fyn.loadJsonForPkg(pkg, fullOutDir), () => {
36363
+ return retry(() => this._fyn.loadJsonForPkg(pkg, fullOutDir, placed), () => {
36067
36364
  retries++;
36068
36365
  logger$1.warn(`retrying ${retries} reading package.json`, fullOutDir);
36069
36366
  return true;
@@ -38464,7 +38761,7 @@ var require_commonjs$8 = /* @__PURE__ */ __commonJSMin(((exports) => {
38464
38761
  //#region node_modules/brace-expansion/dist/commonjs/index.js
38465
38762
  var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38466
38763
  Object.defineProperty(exports, "__esModule", { value: true });
38467
- exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
38764
+ exports.EXPANSION_MAX_REWRITES = exports.EXPANSION_MAX_DEPTH = exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
38468
38765
  exports.expand = expand;
38469
38766
  const balanced_match_1 = require_commonjs$8();
38470
38767
  const escSlash = "\0SLASH" + Math.random() + "\0";
@@ -38484,6 +38781,8 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38484
38781
  const periodPattern = /\\\./g;
38485
38782
  exports.EXPANSION_MAX = 1e5;
38486
38783
  exports.EXPANSION_MAX_LENGTH = 4e6;
38784
+ exports.EXPANSION_MAX_DEPTH = 1e3;
38785
+ exports.EXPANSION_MAX_REWRITES = 1e3;
38487
38786
  function numeric(str) {
38488
38787
  return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
38489
38788
  }
@@ -38493,32 +38792,43 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38493
38792
  function unescapeBraces(str) {
38494
38793
  return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
38495
38794
  }
38795
+ function pushAll(target, items) {
38796
+ for (let i = 0; i < items.length; i++) target.push(items[i]);
38797
+ }
38496
38798
  /**
38497
38799
  * Basically just str.split(","), but handling cases
38498
38800
  * where we have nested braced sections, which should be
38499
38801
  * treated as individual members, like {a,{b,c},d}
38500
38802
  */
38501
38803
  function parseCommaParts(str) {
38502
- if (!str) return [""];
38503
38804
  const parts = [];
38504
- const m = (0, balanced_match_1.balanced)("{", "}", str);
38505
- if (!m) return str.split(",");
38506
- const { pre, body, post } = m;
38507
- const p = pre.split(",");
38508
- p[p.length - 1] += "{" + body + "}";
38509
- const postParts = parseCommaParts(post);
38510
- if (post.length) {
38511
- p[p.length - 1] += postParts.shift();
38512
- p.push.apply(p, postParts);
38805
+ let carry = "";
38806
+ for (;;) {
38807
+ const m = (0, balanced_match_1.balanced)("{", "}", str);
38808
+ if (!m) {
38809
+ const tail = str.split(",");
38810
+ tail[0] = carry + tail[0];
38811
+ pushAll(parts, tail);
38812
+ return parts;
38813
+ }
38814
+ const { pre, body, post } = m;
38815
+ const p = pre.split(",");
38816
+ p[0] = carry + p[0];
38817
+ p[p.length - 1] += "{" + body + "}";
38818
+ if (!post.length) {
38819
+ pushAll(parts, p);
38820
+ return parts;
38821
+ }
38822
+ carry = p.pop();
38823
+ pushAll(parts, p);
38824
+ str = post;
38513
38825
  }
38514
- parts.push.apply(parts, p);
38515
- return parts;
38516
38826
  }
38517
38827
  function expand(str, options = {}) {
38518
38828
  if (!str) return [];
38519
- const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
38829
+ const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH, maxDepth = exports.EXPANSION_MAX_DEPTH, maxRewrites = exports.EXPANSION_MAX_REWRITES } = options;
38520
38830
  if (str.slice(0, 2) === "{}") str = "\\{\\}" + str.slice(2);
38521
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
38831
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
38522
38832
  }
38523
38833
  function embrace(str) {
38524
38834
  return "{" + str + "}";
@@ -38584,8 +38894,10 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38584
38894
  }
38585
38895
  return N;
38586
38896
  }
38587
- function expand_(str, max, maxLength, isTop) {
38897
+ function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
38898
+ if (depth > maxDepth) return [str];
38588
38899
  let acc = [""];
38900
+ let rewrites = 0;
38589
38901
  let dropEmpties = false;
38590
38902
  let firstGroup = true;
38591
38903
  for (;;) {
@@ -38604,7 +38916,8 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38604
38916
  const isSequence = isNumericSequence || isAlphaSequence;
38605
38917
  const isOptions = m.body.indexOf(",") >= 0;
38606
38918
  if (!isSequence && !isOptions) {
38607
- if (m.post.match(/,(?!,).*\}/)) {
38919
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
38920
+ rewrites++;
38608
38921
  str = m.pre + "{" + m.body + escClose + m.post;
38609
38922
  isTop = true;
38610
38923
  continue;
@@ -38620,7 +38933,7 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38620
38933
  else {
38621
38934
  let n = parseCommaParts(m.body);
38622
38935
  if (n.length === 1 && n[0] !== void 0) {
38623
- n = expand_(n[0], max, maxLength, false).map(embrace);
38936
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
38624
38937
  /* c8 ignore start */
38625
38938
  if (n.length === 1) {
38626
38939
  acc = combine(acc, pre + n[0], [""], max, maxLength, dropEmpties && !m.post.length);
@@ -38634,7 +38947,7 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38634
38947
  values = [];
38635
38948
  let valuesLength = 0;
38636
38949
  outer: for (let j = 0; j < n.length; j++) {
38637
- const expanded = expand_(n[j], max, maxLength, false);
38950
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
38638
38951
  for (let k = 0; k < expanded.length; k++) {
38639
38952
  const v = expanded[k];
38640
38953
  if (dropsEmpties && !v) continue;
@@ -45425,7 +45738,7 @@ var require_normalize_data = /* @__PURE__ */ __commonJSMin(((exports, module) =>
45425
45738
  }));
45426
45739
 
45427
45740
  //#endregion
45428
- //#region node_modules/.f/_/isexe/4.0.0/node_modules/isexe/dist/commonjs/index.min.js
45741
+ //#region node_modules/isexe/dist/commonjs/index.min.js
45429
45742
  var require_index_min$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
45430
45743
  var a = (t, e) => () => (e || t((e = { exports: {} }).exports, e), e.exports);
45431
45744
  var _ = a((i) => {
@@ -50375,12 +50688,18 @@ var require_cache_dir$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
50375
50688
  # This file is a cache directory tag created by cacache.
50376
50689
  # For information about cache directory tags, see https://bford.info/cachedir/
50377
50690
  `;
50378
- async function mkdir(cache) {
50379
- await fs$15.mkdir(cache, {
50380
- recursive: true,
50381
- owner: "inherit"
50382
- });
50383
- await writeTag(cache);
50691
+ const made = /* @__PURE__ */ new Map();
50692
+ function mkdir(cache) {
50693
+ let p = made.get(cache);
50694
+ if (!p) {
50695
+ p = fs$15.mkdir(cache, {
50696
+ recursive: true,
50697
+ owner: "inherit"
50698
+ }).then(() => writeTag(cache));
50699
+ made.set(cache, p);
50700
+ p.catch(() => made.delete(cache));
50701
+ }
50702
+ return p;
50384
50703
  }
50385
50704
  async function writeTag(cache) {
50386
50705
  try {
@@ -58967,11 +59286,14 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
58967
59286
  }
58968
59287
  /**
58969
59288
  * @param {{headers: Record<string, string>, synchronous: boolean}|undefined} revalidation - Revalidation information, if any.
58970
- * @returns {{response: {headers: Record<string, string>}, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
59289
+ * @returns {{response: HttpResponse, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
58971
59290
  */
58972
59291
  _evaluateRequestHitResult(revalidation) {
58973
59292
  return {
58974
- response: { headers: this.responseHeaders() },
59293
+ response: {
59294
+ headers: this.responseHeaders(),
59295
+ status: this._status
59296
+ },
58975
59297
  revalidation
58976
59298
  };
58977
59299
  }
@@ -59015,9 +59337,9 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59015
59337
  * }
59016
59338
  * ```
59017
59339
  * @param {HttpRequest} req - new incoming HTTP request
59018
- * @returns {{response: {headers: Record<string, string>}|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
59340
+ * @returns {{response: HttpResponse|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
59019
59341
  * - revalidation: { headers: Record<string, string>, synchronous: boolean } Set if you should send this to the origin server
59020
- * - response: { headers: Record<string, string> } Set if you can respond to the client with these cached headers
59342
+ * - response: HttpResponse Set if you can respond to the client with these cached headers
59021
59343
  */
59022
59344
  evaluateRequest(req) {
59023
59345
  this._assertRequestHasHeaders(req);
@@ -59056,9 +59378,11 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59056
59378
  */
59057
59379
  _varyMatches(req) {
59058
59380
  if (!this._resHeaders.vary) return true;
59059
- if (this._resHeaders.vary === "*") return false;
59060
59381
  const fields = this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);
59061
- for (const name of fields) if (req.headers[name] !== this._reqHeaders[name]) return false;
59382
+ for (const name of fields) {
59383
+ if (name === "*") return false;
59384
+ if ((Object.prototype.hasOwnProperty.call(req.headers, name) && req.headers[name]) !== (Object.prototype.hasOwnProperty.call(this._reqHeaders, name) && this._reqHeaders[name])) return false;
59385
+ }
59062
59386
  return true;
59063
59387
  }
59064
59388
  /**
@@ -59100,6 +59424,13 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59100
59424
  return headers;
59101
59425
  }
59102
59426
  /**
59427
+ * Returns the status code of the cached response.
59428
+ * @returns {number} The response status code.
59429
+ */
59430
+ status() {
59431
+ return this._status;
59432
+ }
59433
+ /**
59103
59434
  * Returns the Date header value from the response or the current time if invalid.
59104
59435
  * @returns {number} Timestamp (in milliseconds) representing the Date header or response time.
59105
59436
  */
@@ -62205,8 +62536,14 @@ var require_common = /* @__PURE__ */ __commonJSMin(((exports) => {
62205
62536
  * {@link isInSubnet} when classifying a single address — notably when the
62206
62537
  * address came from untrusted input and the result backs a trust-boundary
62207
62538
  * decision such as an SSRF allow/deny filter.
62539
+ *
62540
+ * An address of one family is never inside a network of the other, so an
62541
+ * `Address4` against an `Address6` (or the reverse) is `false`. To compare
62542
+ * across families, convert first: `Address6.fromAddress4()`, `to4()`, or
62543
+ * `toAddress4Nat64()`.
62208
62544
  */
62209
62545
  function isHostInSubnet(address) {
62546
+ if (this.binaryZeroPad().length !== address.binaryZeroPad().length) return false;
62210
62547
  return this.mask(address.subnetMask) === address.mask();
62211
62548
  }
62212
62549
  /**
@@ -62493,7 +62830,9 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62493
62830
  */
62494
62831
  this.isCorrect = isCorrect4;
62495
62832
  /**
62496
- * Returns true if the given address is in the subnet of the current address
62833
+ * Returns true if the given address is in the subnet of the current address.
62834
+ * An `Address6` is never in the subnet of an `Address4`; convert with
62835
+ * `to4()` or `Address6.fromAddress4()` to compare across families.
62497
62836
  * @returns {boolean}
62498
62837
  */
62499
62838
  this.isInSubnet = common.isInSubnet;
@@ -62503,6 +62842,8 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62503
62842
  * when classifying a single address, so the answer doesn't change with the
62504
62843
  * CIDR suffix the caller happened to write — notably when the address came
62505
62844
  * from untrusted input and the result backs a trust-boundary decision.
62845
+ * An `Address6` is never in the subnet of an `Address4`; convert with
62846
+ * `to4()` or `Address6.fromAddress4()` to compare across families.
62506
62847
  * @returns {boolean}
62507
62848
  */
62508
62849
  this.isHostInSubnet = common.isHostInSubnet;
@@ -62515,6 +62856,8 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62515
62856
  if (this.subnetMask < 0 || this.subnetMask > constants.BITS) throw new address_error_1.AddressError("Invalid subnet mask.");
62516
62857
  address = address.replace(constants.RE_SUBNET_STRING, "");
62517
62858
  }
62859
+ const longest = constants.GROUPS * 4 - 1;
62860
+ if (address.length > longest) throw new address_error_1.AddressError(`IPv4 addresses are at most ${longest} characters.`);
62518
62861
  this.addressMinusSuffix = address;
62519
62862
  this.parsedAddress = this.parse(address);
62520
62863
  }
@@ -62631,15 +62974,20 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62631
62974
  return Address4.fromHex(integer.toString(16).padStart(8, "0"));
62632
62975
  }
62633
62976
  /**
62634
- * Return an address from in-addr.arpa form
62977
+ * Return an address from in-addr.arpa form: the four octets reversed, with
62978
+ * or without the `.in-addr.arpa` suffix and root dot, in any case. Throws
62979
+ * `AddressError` unless the reversed labels form a valid IPv4 address, so
62980
+ * `fromArpa(x.reverseForm())` round-trips {@link reverseForm}.
62635
62981
  * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address
62636
62982
  * @returns {Adress4}
62637
62983
  * @example
62638
- * var address = Address4.fromArpa(42.2.0.192.in-addr.arpa.)
62984
+ * var address = Address4.fromArpa('42.2.0.192.in-addr.arpa.')
62639
62985
  * address.correctForm(); // '192.0.2.42'
62640
62986
  */
62641
62987
  static fromArpa(arpaFormAddress) {
62642
- const address = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.$/, "").split(".").reverse().join(".");
62988
+ const longest = constants.GROUPS * 4 - 1 + 3 + 14;
62989
+ if (arpaFormAddress.length > longest) throw new address_error_1.AddressError(`in-addr.arpa names are at most ${longest} characters.`);
62990
+ const address = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.?$/i, "").split(".").reverse().join(".");
62643
62991
  return new Address4(address);
62644
62992
  }
62645
62993
  /**
@@ -63420,7 +63768,9 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63420
63768
  this.v4 = false;
63421
63769
  this.zone = "";
63422
63770
  /**
63423
- * Returns true if the given address is in the subnet of the current address
63771
+ * Returns true if the given address is in the subnet of the current address.
63772
+ * An `Address4` is never in the subnet of an `Address6`; convert with
63773
+ * `Address6.fromAddress4()` or `to4()` to compare across families.
63424
63774
  * @returns {boolean}
63425
63775
  */
63426
63776
  this.isInSubnet = common.isInSubnet;
@@ -63430,6 +63780,8 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63430
63780
  * when classifying a single address, so the answer doesn't change with the
63431
63781
  * CIDR suffix the caller happened to write — notably when the address came
63432
63782
  * from untrusted input and the result backs a trust-boundary decision.
63783
+ * An `Address4` is never in the subnet of an `Address6`; convert with
63784
+ * `Address6.fromAddress4()` or `to4()` to compare across families.
63433
63785
  * @returns {boolean}
63434
63786
  */
63435
63787
  this.isHostInSubnet = common.isHostInSubnet;
@@ -63455,6 +63807,8 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63455
63807
  this.zone = zone[0];
63456
63808
  address = address.replace(constants6.RE_ZONE_STRING, "");
63457
63809
  }
63810
+ const longest = this.groups * 5 + 5;
63811
+ if (address.length > longest) throw new address_error_1.AddressError(`IPv6 addresses are at most ${longest} characters.`);
63458
63812
  this.addressMinusSuffix = address;
63459
63813
  this.parsedAddress = this.parse(this.addressMinusSuffix);
63460
63814
  }
@@ -63640,7 +63994,7 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63640
63994
  * Address6.fromArpa('8.b.d.0.1.0.0.2.ip6.arpa.').networkForm(); // '2001:db8::/32'
63641
63995
  */
63642
63996
  static fromArpa(arpaFormAddress) {
63643
- const nibbles = arpaFormAddress.replace(/(\.ip6\.arpa)?\.?$/, "");
63997
+ const nibbles = arpaFormAddress.replace(/(\.ip6\.arpa)?\.?$/i, "");
63644
63998
  if (!/^[0-9a-f](\.[0-9a-f]){0,31}$/i.test(nibbles)) throw new address_error_1.AddressError("Invalid 'ip6.arpa' form.");
63645
63999
  const reversed = nibbles.split(".").reverse();
63646
64000
  const subnetMask = reversed.length * 4;
@@ -65801,6 +66155,7 @@ var require_lib$14 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
65801
66155
  });
65802
66156
  if (agentCache.has(cacheKey)) return agentCache.get(cacheKey);
65803
66157
  const newAgent = new Agent(normalizedOptions);
66158
+ newAgent.protocol = url.protocol;
65804
66159
  agentCache.set(cacheKey, newAgent);
65805
66160
  return newAgent;
65806
66161
  };
@@ -65892,7 +66247,7 @@ var require_package$5 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
65892
66247
  "publish": "true"
65893
66248
  },
65894
66249
  "_fyn": {},
65895
- "_from": "make-fetch-happen@^16.0.0",
66250
+ "_from": "make-fetch-happen@^16.0.1",
65896
66251
  "_id": "make-fetch-happen@16.0.1"
65897
66252
  };
65898
66253
  }));
@@ -86762,7 +87117,7 @@ var require_package_extensions = /* @__PURE__ */ __commonJSMin(((exports, module
86762
87117
  var require_npm_extension = /* @__PURE__ */ __commonJSMin(((exports, module) => {
86763
87118
  const { resolve: resolve$9, sep: sep$3 } = __require("node:path");
86764
87119
  const { readFileSync, existsSync: existsSync$3 } = __require("node:fs");
86765
- const { pathToFileURL } = __require("node:url");
87120
+ const { pathToFileURL: pathToFileURL$1 } = __require("node:url");
86766
87121
  const { isDeepStrictEqual } = __require("node:util");
86767
87122
  const { log } = require_lib$38();
86768
87123
  const ssri = require_lib$23();
@@ -86831,7 +87186,7 @@ var require_npm_extension = /* @__PURE__ */ __commonJSMin(((exports, module) =>
86831
87186
  async load() {
86832
87187
  if (!this.present) return;
86833
87188
  let mod;
86834
- if (this.format === "mjs") mod = await import(`${pathToFileURL(this.path).href}?h=${this.hash}`);
87189
+ if (this.format === "mjs") mod = await import(`${pathToFileURL$1(this.path).href}?h=${this.hash}`);
86835
87190
  else {
86836
87191
  delete __require.cache[__require.resolve(this.path)];
86837
87192
  mod = __require(this.path);
@@ -92175,10 +92530,11 @@ async function copyFile(srcFp, destFp) {
92175
92530
  *
92176
92531
  * @param {*} srcFp - source file path
92177
92532
  * @param {*} destFp - destination file path
92533
+ * @param {*} noCopy - fail instead of falling back to a copy (always fails on macOS)
92178
92534
  * @returns
92179
92535
  */
92180
- async function cloneFile(srcFp, destFp) {
92181
- return copyNew(srcFp, destFp, fs.constants.COPYFILE_FICLONE);
92536
+ async function cloneFile(srcFp, destFp, noCopy = false) {
92537
+ return copyNew(srcFp, destFp, noCopy ? fs.constants.COPYFILE_FICLONE_FORCE : fs.constants.COPYFILE_FICLONE);
92182
92538
  }
92183
92539
  async function prepDestDir(dest) {
92184
92540
  const statDest = await tryCatch(() => file_ops_default.lstat(dest));
@@ -92679,6 +93035,15 @@ function getBucketPath(cache, key) {
92679
93035
  return Path.join(cache, `index-v${indexV}`, hashed.slice(0, 2), hashed.slice(2, 4), hashed.slice(4));
92680
93036
  }
92681
93037
  /**
93038
+ * Get the content file path for an integrity that cacache matched, such as the `sri` from
93039
+ * `get.hasContent`. Same layout as cacache: the algorithm, then the hex digest in segments.
93040
+ */
93041
+ function getContentPath(cache, sri) {
93042
+ const hex = sri.hexDigest();
93043
+ const contentV = import_package.default["cache-version"].content;
93044
+ return Path.join(cache, `content-v${contentV}`, sri.algorithm, hex.slice(0, 2), hex.slice(2, 4), hex.slice(4));
93045
+ }
93046
+ /**
92682
93047
  * Update cache entry refresh timestamp by modifying bucket file mtime.
92683
93048
  * Does not modify file contents, only filesystem metadata.
92684
93049
  * Errors are silently ignored (cache refresh is non-critical).
@@ -92714,7 +93079,7 @@ async function getCacheInfoWithRefreshTime(cache, key) {
92714
93079
  //#endregion
92715
93080
  //#region lib/util/trimmed-packument.ts
92716
93081
  /** Bump when the kept fields change, so files from older versions are rebuilt. */
92717
- const TRIMMED_FORMAT = 1;
93082
+ const TRIMMED_FORMAT = 2;
92718
93083
  /**
92719
93084
  * The version fields fyn reads from a registry packument. Code that starts reading another
92720
93085
  * field from `meta.versions[v]` must add it here and bump TRIMMED_FORMAT.
@@ -92731,6 +93096,7 @@ const KEEP_VERSION_FIELDS = [
92731
93096
  "cpu",
92732
93097
  "deprecated",
92733
93098
  "_hasShrinkwrap",
93099
+ "_shrinkwrap",
92734
93100
  "fyn"
92735
93101
  ];
92736
93102
  /** Only the install hooks are read from `scripts`. */
@@ -92762,7 +93128,7 @@ function trimPackument(packument) {
92762
93128
  versions[version] = trimmed;
92763
93129
  }
92764
93130
  return {
92765
- $format: 1,
93131
+ $format: 2,
92766
93132
  name: packument.name,
92767
93133
  "dist-tags": packument["dist-tags"],
92768
93134
  time: packument.time,
@@ -92775,33 +93141,68 @@ function trimmedPackumentFile(dir, packumentUrl) {
92775
93141
  return Path.join(dir, `${hash}.json`);
92776
93142
  }
92777
93143
  /**
92778
- * Read a trimmed packument and its refresh time. Resolves undefined when the file is missing,
92779
- * unreadable, or from another format, so the caller falls back to the full copy.
93144
+ * Read a trimmed packument, its refresh time, and its validators. Resolves undefined when the
93145
+ * file is missing, unreadable, or from another format, so the caller falls back to the full copy.
92780
93146
  */
92781
93147
  async function readTrimmedPackument(file) {
92782
93148
  try {
92783
93149
  const [data, stat] = await Promise.all([fs.promises.readFile(file), fs.promises.stat(file)]);
92784
- const packument = JSON.parse(data.toString());
92785
- if (packument.$format !== 1) return void 0;
92786
- delete packument.$format;
92787
- return {
92788
- packument,
93150
+ const parsed = parseTrimmed(data.toString());
93151
+ return parsed && {
93152
+ ...parsed,
92789
93153
  refreshTime: stat.mtimeMs
92790
93154
  };
92791
93155
  } catch {
92792
93156
  return;
92793
93157
  }
92794
93158
  }
93159
+ /** Parse a trimmed copy's JSON into the packument and its validators. Undefined for another format. */
93160
+ function parseTrimmed(json) {
93161
+ const packument = JSON.parse(json);
93162
+ if (packument.$format !== 2) return void 0;
93163
+ const { $etag: etag, $lastModified: lastModified, $corgi: corgi } = packument;
93164
+ delete packument.$format;
93165
+ delete packument.$etag;
93166
+ delete packument.$lastModified;
93167
+ delete packument.$corgi;
93168
+ return {
93169
+ packument,
93170
+ etag,
93171
+ lastModified,
93172
+ corgi
93173
+ };
93174
+ }
93175
+ /** Mark a trimmed packument as refreshed now, after the registry said it hasn't changed */
93176
+ async function touchTrimmedPackument(file) {
93177
+ const now = /* @__PURE__ */ new Date();
93178
+ await fs.promises.utimes(file, now, now).catch(() => void 0);
93179
+ }
92795
93180
  /**
92796
93181
  * Write a trimmed copy of a packument. The write goes to a temp file renamed over the target,
92797
93182
  * so concurrent installs never read half a file. `refreshTime` sets the mtime when the copy is
92798
- * made from an existing cache entry rather than a fresh fetch. Failures are ignored: the full
92799
- * copy in cacache still works, just slower.
93183
+ * made from an existing cache entry rather than a fresh fetch. Failures are ignored: the next
93184
+ * install fetches the packument again.
92800
93185
  */
92801
- async function writeTrimmedPackument(file, packument, refreshTime) {
93186
+ async function writeTrimmedPackument(file, packument, refreshTime, validators) {
93187
+ let json;
93188
+ try {
93189
+ json = trimmedJson(trimPackument(packument), validators);
93190
+ } catch {
93191
+ return;
93192
+ }
93193
+ await writeTrimmed(file, json, refreshTime);
93194
+ }
93195
+ function trimmedJson(trimmed, validators) {
93196
+ return JSON.stringify({
93197
+ ...trimmed,
93198
+ $etag: validators?.etag,
93199
+ $lastModified: validators?.lastModified,
93200
+ $corgi: validators?.corgi || void 0
93201
+ });
93202
+ }
93203
+ async function writeTrimmed(file, data, refreshTime) {
92802
93204
  const tmp = `${file}.${process.pid}.${Crypto.randomBytes(4).toString("hex")}.tmp`;
92803
93205
  try {
92804
- const data = JSON.stringify(trimPackument(packument));
92805
93206
  try {
92806
93207
  await fs.promises.writeFile(tmp, data);
92807
93208
  } catch (err) {
@@ -92818,9 +93219,160 @@ async function writeTrimmedPackument(file, packument, refreshTime) {
92818
93219
  await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
92819
93220
  }
92820
93221
  }
93222
+ /** Parse a registry response body, as sent with `encoding` */
93223
+ function decodePackument(data, encoding) {
93224
+ const json = encoding === "gzip" || encoding === "x-gzip" || encoding === "deflate" ? en.unzipSync(data) : encoding === "br" ? en.brotliDecompressSync(data) : data;
93225
+ return JSON.parse(Buffer.from(json.buffer, json.byteOffset, json.byteLength).toString());
93226
+ }
93227
+ /**
93228
+ * Decode a packument response and write its trimmed copy. The abbreviated form only flags that
93229
+ * a version has install scripts, and fyn records which ones, so one with any isn't saved. This
93230
+ * is the CPU-heavy part of a packument fetch, so an fs worker runs it when fyn has them built.
93231
+ */
93232
+ async function savePackument(job) {
93233
+ const packument = decodePackument(job.data, job.encoding);
93234
+ if (job.corgi && Object.values(packument.versions || {}).some((v) => v.hasInstallScript)) return { needFull: true };
93235
+ return { json: await saveTrimmed(job.file, packument, job) };
93236
+ }
93237
+ /** Trim a packument and write it with its validators. @returns the JSON written */
93238
+ async function saveTrimmed(file, packument, validators) {
93239
+ const text = trimmedJson(trimPackument(packument), validators);
93240
+ await writeTrimmed(file, text);
93241
+ return text;
93242
+ }
93243
+
93244
+ //#endregion
93245
+ //#region lib/util/registry-get.ts
93246
+ let load;
93247
+ const loadRegistryFetch = () => load ??= Promise.all([Promise.resolve().then(() => /* @__PURE__ */ __toESM(require_lib$13(), 1)), Promise.resolve().then(() => /* @__PURE__ */ __toESM(require_lib$12(), 1))]).then(([mfh, nrf]) => ({
93248
+ fetch: mfh.default,
93249
+ getAuth: nrf.default.getAuth
93250
+ }));
93251
+ /** The plain values of `opts`, which is all auth needs and all that can go to a worker */
93252
+ function pickRegistryOpts(opts) {
93253
+ const out = {};
93254
+ for (const [k, v] of Object.entries(opts)) if (v === void 0 || [
93255
+ "string",
93256
+ "number",
93257
+ "boolean"
93258
+ ].includes(typeof v)) out[k] = v;
93259
+ return out;
93260
+ }
93261
+ /** getAuth results, per opts, by authCacheKey */
93262
+ const authCaches = /* @__PURE__ */ new WeakMap();
93263
+ /**
93264
+ * What getAuth's result for `url` depends on. It walks up the URL's path for the longest
93265
+ * `//host/path` that has auth configured, and without one, falls back to the registry's auth.
93266
+ * So it's the longest configured prefix that matches, else the host.
93267
+ */
93268
+ function authCacheKey(url, prefixes) {
93269
+ const m = /^[a-z]+:(\/\/[^/?#]+)([^?#]*)/i.exec(url);
93270
+ if (!m) return void 0;
93271
+ const full = m[1] + m[2];
93272
+ let best = "";
93273
+ for (const p of prefixes) if (p.length > best.length && full.startsWith(p) && (p.endsWith("/") || full.length === p.length || full[p.length] === "/")) best = p;
93274
+ return best || `host ${m[1]}`;
93275
+ }
93276
+ function cachedAuth(getAuth, url, opts) {
93277
+ if (opts.forceAuth) return getAuth(url, opts);
93278
+ let cache = authCaches.get(opts);
93279
+ if (!cache) {
93280
+ cache = {
93281
+ prefixes: Object.keys(opts).map((k) => /^(\/\/.+):(_authToken|_auth|username|_password|certfile|keyfile)$/.exec(k)?.[1]).filter((p) => Boolean(p)),
93282
+ auths: /* @__PURE__ */ new Map()
93283
+ };
93284
+ authCaches.set(opts, cache);
93285
+ }
93286
+ const key = authCacheKey(url, cache.prefixes);
93287
+ if (key === void 0) return getAuth(url, opts);
93288
+ let auth = cache.auths.get(key);
93289
+ if (!auth) cache.auths.set(key, auth = getAuth(url, opts));
93290
+ return auth;
93291
+ }
93292
+ /** Statuses from 400 up throw */
93293
+ async function registryGet(url, extraHeaders, opts) {
93294
+ const { fetch, getAuth } = await loadRegistryFetch();
93295
+ const auth = cachedAuth(getAuth, url, opts);
93296
+ const headers = {
93297
+ "user-agent": `fyn node/${process.version}`,
93298
+ ...extraHeaders
93299
+ };
93300
+ if (auth.token) headers.authorization = `Bearer ${auth.token}`;
93301
+ else if (auth.auth) headers.authorization = `Basic ${auth.auth}`;
93302
+ const res = await fetch(url, {
93303
+ headers,
93304
+ compress: false,
93305
+ cert: auth.cert,
93306
+ key: auth.key,
93307
+ maxSockets: opts.maxSockets,
93308
+ noProxy: process.env.NOPROXY,
93309
+ retry: { retries: 3 },
93310
+ timeout: 3e5
93311
+ });
93312
+ if (res.status >= 400) {
93313
+ res.body.resume();
93314
+ throw Object.assign(/* @__PURE__ */ new Error(`${res.status} ${res.statusText} - GET ${url}`), {
93315
+ code: `E${res.status}`,
93316
+ statusCode: res.status
93317
+ });
93318
+ }
93319
+ return res;
93320
+ }
93321
+ /** npm's abbreviated install metadata, about a third the size of a full packument */
93322
+ const CORGI_DOC = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
93323
+ /**
93324
+ * The request headers for a packument, with validators so the registry can answer 304.
93325
+ *
93326
+ * @param full - ask for the full packument, not the abbreviated one
93327
+ */
93328
+ function packumentHeaders(validators, full) {
93329
+ const headers = {
93330
+ accept: full ? "application/json" : CORGI_DOC,
93331
+ "accept-encoding": "gzip"
93332
+ };
93333
+ if (validators?.etag) headers["if-none-match"] = validators.etag;
93334
+ if (validators?.lastModified) headers["if-modified-since"] = validators.lastModified;
93335
+ return headers;
93336
+ }
93337
+ /**
93338
+ * Fetch a packument and write fyn's trimmed copy of it. It starts with the abbreviated form, and
93339
+ * gets the full one when `save` finds that abbreviated isn't enough.
93340
+ *
93341
+ * @param get - does the GET, registryGet with the job's opts by default
93342
+ * @param save - decodes and writes the response; an fs worker can run it
93343
+ */
93344
+ async function fetchPackument(job, get, save = savePackument) {
93345
+ const doGet = get || ((url, headers) => registryGet(url, headers, job.opts || {}));
93346
+ const request = async (headers) => {
93347
+ const res = await doGet(job.url, headers);
93348
+ if (res.status === 304) {
93349
+ res.body?.resume();
93350
+ return;
93351
+ }
93352
+ const header = (name) => res.headers.get(name) || void 0;
93353
+ return {
93354
+ data: await res.buffer(),
93355
+ encoding: header("content-encoding"),
93356
+ etag: header("etag"),
93357
+ lastModified: header("last-modified"),
93358
+ corgi: /vnd\.npm\.install-v1/.test(header("content-type") || ""),
93359
+ file: job.file
93360
+ };
93361
+ };
93362
+ const first = await request(packumentHeaders(job, job.full));
93363
+ if (!first) return { status: 304 };
93364
+ let saved = await save(first);
93365
+ if ("needFull" in saved) saved = await save(await request(packumentHeaders(void 0, true)));
93366
+ if ("needFull" in saved) throw new Error(`no full packument from ${job.url}`);
93367
+ return {
93368
+ status: 200,
93369
+ json: saved.json
93370
+ };
93371
+ }
92821
93372
 
92822
93373
  //#endregion
92823
93374
  //#region lib/util/inflight.ts
93375
+ var import_lib = /* @__PURE__ */ __toESM(require_lib$23(), 1);
92824
93376
  var inflight_default = InflightStore;
92825
93377
 
92826
93378
  //#endregion
@@ -92855,7 +93407,10 @@ var PkgPreper = class {
92855
93407
  };
92856
93408
  return Promise.resolve((0, import_lib$2.default)(treeStub)).then((files) => {
92857
93409
  return Qn(tarOpt, files.map((f) => `./${f}`));
92858
- }).then(() => FsPromises.rename(tmpTarget, target)).then(() => void 0);
93410
+ }).then(() => FsPromises.rename(tmpTarget, target).catch((err) => {
93411
+ if (err.code !== "EXDEV") throw err;
93412
+ return FsPromises.copyFile(tmpTarget, target);
93413
+ })).then(() => void 0);
92859
93414
  });
92860
93415
  });
92861
93416
  }
@@ -92872,7 +93427,7 @@ var PkgPreper = class {
92872
93427
  });
92873
93428
  });
92874
93429
  }).catch((err) => {
92875
- stream.emit("error", err);
93430
+ stream.destroy(err);
92876
93431
  });
92877
93432
  return stream;
92878
93433
  }
@@ -92883,7 +93438,6 @@ var PkgPreper = class {
92883
93438
 
92884
93439
  //#endregion
92885
93440
  //#region node_modules/xsh/dist/util.js
92886
- var import_lib = /* @__PURE__ */ __toESM(require_lib$23(), 1);
92887
93441
  /**
92888
93442
  * Mutable holder for the Promise implementation xsh uses.
92889
93443
  *
@@ -93112,12 +93666,11 @@ const envPath = {
93112
93666
  //#endregion
93113
93667
  //#region node_modules/xsh/dist/mkcmd.js
93114
93668
  /**
93115
- * Make a command string: a single array of strings is joined with `" "`,
93116
- * otherwise all arguments are joined with `" "`.
93669
+ * Make a command string: every argument, string or array of strings,
93670
+ * contributes its words in order, joined with `" "`.
93117
93671
  */
93118
93672
  function mkCmd(...args) {
93119
- const a = args[0];
93120
- return Array.isArray(a) ? a.join(" ") : args.join(" ");
93673
+ return args.flat().join(" ");
93121
93674
  }
93122
93675
 
93123
93676
  //#endregion
@@ -93268,19 +93821,14 @@ const TEN_MB = 10485760;
93268
93821
  const DEFAULT_TIMEOUT_GRACE = 5e3;
93269
93822
  const DEFAULT_LAST_LINES = 10;
93270
93823
  const MAX_OUTPUT_IN_ERROR = 5e4;
93271
- function createTimeoutError(context, elapsed) {
93272
- const err = /* @__PURE__ */ new Error(`Command timed out after ${elapsed}ms: ${context.command}`);
93824
+ function createTimeoutError(command, elapsed) {
93825
+ const err = /* @__PURE__ */ new Error(`Command timed out after ${elapsed}ms: ${command}`);
93273
93826
  err.name = "TimeoutError";
93274
- err.context = {
93275
- ...context,
93276
- duration: elapsed
93277
- };
93278
93827
  return err;
93279
93828
  }
93280
- function createAbortError(context) {
93281
- const err = /* @__PURE__ */ new Error(`Command aborted: ${context.command}`);
93829
+ function createAbortError(command) {
93830
+ const err = /* @__PURE__ */ new Error(`Command aborted: ${command}`);
93282
93831
  err.name = "AbortError";
93283
- err.context = context;
93284
93832
  return err;
93285
93833
  }
93286
93834
  function lastLines(text, n) {
@@ -93388,7 +93936,7 @@ var VisualExec = class {
93388
93936
  }
93389
93937
  _extractProgress(line) {
93390
93938
  const cfg = this._progress;
93391
- if (!cfg || !this._onProgress) return;
93939
+ if (!cfg || !(this._onProgress || cfg.format)) return;
93392
93940
  let progress = null;
93393
93941
  if (cfg.extract) progress = cfg.extract(line);
93394
93942
  else if (cfg.pattern) {
@@ -93399,7 +93947,24 @@ var VisualExec = class {
93399
93947
  percent: m.groups.percent ? parseInt(m.groups.percent, 10) : void 0
93400
93948
  };
93401
93949
  }
93402
- if (progress) this._onProgress(progress);
93950
+ if (!progress) return;
93951
+ if (cfg.format) {
93952
+ const text = cfg.format(progress);
93953
+ if (text !== this._progressText) {
93954
+ this._progressText = text;
93955
+ this._renderDigest(this._stdoutDigest);
93956
+ }
93957
+ }
93958
+ if (this._onProgress) this._onProgress(progress);
93959
+ }
93960
+ _renderDigest(item) {
93961
+ const display = item === this._stdoutDigest && this._progressText ? `=== ${this._title}\nstdout ${this._progressText}` : void 0;
93962
+ this._logger.updateItem(item.name, {
93963
+ msg: item.msg,
93964
+ display,
93965
+ _save: false,
93966
+ _render: false
93967
+ });
93403
93968
  }
93404
93969
  _updateDigest(item, buf) {
93405
93970
  const lines = (item.buf + sanitizeForDisplay(buf)).split("\n").map((x) => x && x.trim()).filter((x) => x);
@@ -93420,11 +93985,8 @@ var VisualExec = class {
93420
93985
  msgs = [item.buf];
93421
93986
  } else item.buf = msgs.join("\n");
93422
93987
  if (buf.endsWith("\n")) item.buf += "\n";
93423
- this._logger.updateItem(item.name, {
93424
- msg: msgs.join(chalk.blue.inverse("\\n")),
93425
- _save: false,
93426
- _render: false
93427
- });
93988
+ item.msg = msgs.join(chalk.blue.inverse("\\n"));
93989
+ this._renderDigest(item);
93428
93990
  }
93429
93991
  _createDataHandler(stream) {
93430
93992
  return (buf) => {
@@ -93459,6 +94021,9 @@ var VisualExec = class {
93459
94021
  * it defaults to `ExecOutput`, which is what a run without an `onComplete` produces.
93460
94022
  */
93461
94023
  show(child) {
94024
+ return this._show(child, this._command);
94025
+ }
94026
+ _show(child, command) {
93462
94027
  this._stdoutKey = Symbol("visual-exec-stdout");
93463
94028
  this._stderrKey = Symbol("visual-exec-stderr");
93464
94029
  this._rawChild = child.child;
@@ -93477,19 +94042,30 @@ var VisualExec = class {
93477
94042
  name: this._stdoutKey,
93478
94043
  buf: ""
93479
94044
  };
94045
+ this._stdoutDigest = stdoutDigest;
93480
94046
  const stderrDigest = {
93481
94047
  name: this._stderrKey,
93482
94048
  buf: ""
93483
94049
  };
93484
94050
  this._updateStdout = (buf) => this._updateDigest(stdoutDigest, buf);
93485
94051
  this._updateStderr = (buf) => this._updateDigest(stderrDigest, buf);
94052
+ const seen = {
94053
+ stdout: "",
94054
+ stderr: ""
94055
+ };
94056
+ const capture = (stream, data) => {
94057
+ const text = seen[stream] + data;
94058
+ seen[stream] = text.length > MAX_OUTPUT_IN_ERROR ? text.slice(-5e4) : text;
94059
+ };
93486
94060
  this._onStdoutData = (buf) => {
93487
94061
  const data = typeof buf === "string" ? buf : buf.toString();
94062
+ capture("stdout", data);
93488
94063
  this._updateStdout(data);
93489
94064
  this._createDataHandler("stdout")(buf);
93490
94065
  };
93491
94066
  this._onStderrData = (buf) => {
93492
94067
  const data = typeof buf === "string" ? buf : buf.toString();
94068
+ capture("stderr", data);
93493
94069
  this._updateStderr(data);
93494
94070
  this._createDataHandler("stderr")(buf);
93495
94071
  };
@@ -93508,7 +94084,9 @@ var VisualExec = class {
93508
94084
  stderr: ""
93509
94085
  };
93510
94086
  const exitCode = (_d = (_c = err.exitCode) !== null && _c !== void 0 ? _c : err.code) !== null && _d !== void 0 ? _d : 1;
93511
- (_a = this._onComplete) === null || _a === void 0 || _a.call(this, output, exitCode);
94087
+ try {
94088
+ (_a = this._onComplete) === null || _a === void 0 || _a.call(this, output, exitCode);
94089
+ } catch (_e) {}
93512
94090
  this.logResult(err);
93513
94091
  throw err;
93514
94092
  }).then((output) => {
@@ -93517,6 +94095,20 @@ var VisualExec = class {
93517
94095
  const result = (_a = this._onComplete) === null || _a === void 0 ? void 0 : _a.call(this, output, 0);
93518
94096
  return result !== void 0 ? result : output;
93519
94097
  });
94098
+ const cancelError = (err, duration) => {
94099
+ const output = { ...seen };
94100
+ err.output = output;
94101
+ return enhanceError(err, {
94102
+ exitCode: -1,
94103
+ signal: "SIGTERM",
94104
+ cwd: this._cwd,
94105
+ command,
94106
+ duration,
94107
+ lastLines: lastLines(`${output.stdout}\n${output.stderr}`, DEFAULT_LAST_LINES),
94108
+ stdout: output.stdout,
94109
+ stderr: output.stderr
94110
+ });
94111
+ };
93520
94112
  if (this._timeout) {
93521
94113
  const timeoutPromise = new Promise((_, reject) => {
93522
94114
  timeoutId = setTimeout(() => {
@@ -93530,18 +94122,15 @@ var VisualExec = class {
93530
94122
  if ((_a = this._rawChild) === null || _a === void 0 ? void 0 : _a.pid) this._rawChild.kill("SIGKILL");
93531
94123
  }, this._timeoutGrace)).unref) === null || _d === void 0 || _d.call(_c);
93532
94124
  }
93533
- reject(createTimeoutError({
93534
- command: this._command,
93535
- cwd: this._cwd,
93536
- exitCode: -1,
93537
- signal: "SIGTERM"
93538
- }, this._timeout));
94125
+ reject(cancelError(createTimeoutError(command, this._timeout), this._timeout));
93539
94126
  }, this._timeout);
93540
94127
  });
93541
94128
  execPromise = Promise.race([execPromise, timeoutPromise]);
93542
94129
  }
94130
+ let handleAbort;
93543
94131
  const cleanup = () => {
93544
94132
  if (timeoutId) clearTimeout(timeoutId);
94133
+ if (handleAbort) this._signal.removeEventListener("abort", handleAbort);
93545
94134
  const stream = this._outputStream;
93546
94135
  this._outputStream = void 0;
93547
94136
  if (!(stream === null || stream === void 0 ? void 0 : stream.end)) return;
@@ -93554,15 +94143,9 @@ var VisualExec = class {
93554
94143
  };
93555
94144
  if (this._signal) {
93556
94145
  const abortPromise = new Promise((_, reject) => {
93557
- const handleAbort = () => {
94146
+ handleAbort = () => {
93558
94147
  this.abort();
93559
- reject(createAbortError({
93560
- command: this._command,
93561
- cwd: this._cwd,
93562
- exitCode: -1,
93563
- signal: "SIGTERM",
93564
- duration: Date.now() - startTime
93565
- }));
94148
+ reject(cancelError(createAbortError(command), Date.now() - startTime));
93566
94149
  };
93567
94150
  if (this._signal.aborted) handleAbort();
93568
94151
  else this._signal.addEventListener("abort", handleAbort);
@@ -93667,7 +94250,7 @@ var VisualExec = class {
93667
94250
  };
93668
94251
  return Promise.reject(enhanceError(err, context));
93669
94252
  });
93670
- return this.show(child);
94253
+ return this._show(child, cmd);
93671
94254
  }
93672
94255
  };
93673
94256
 
@@ -93711,7 +94294,8 @@ function renameFromObj(obj, rename) {
93711
94294
  function removeFromObj(obj, fields) {
93712
94295
  for (const f of fields) if (typeof f === "string") deleteFields(f, obj);
93713
94296
  else Object.keys(f).forEach((f2) => {
93714
- removeFromObj(obj[f2], f[f2]);
94297
+ const sub = obj[f2];
94298
+ if (sub) removeFromObj(sub, f[f2]);
93715
94299
  });
93716
94300
  }
93717
94301
  function extractFromObj(obj, fields, output = {}) {
@@ -93783,6 +94367,7 @@ const keepStandardFields = [
93783
94367
 
93784
94368
  //#endregion
93785
94369
  //#region node_modules/publish-util/dist/prepack.js
94370
+ const runsPrePack = (script) => typeof script === "string" && script.includes("publish-util-prepack");
93786
94371
  function prePackObj(pkg, config = {}) {
93787
94372
  renameFromObj(pkg, config.rename);
93788
94373
  const keepObj = config.keep && extractFromObj(pkg, config.keep);
@@ -93800,7 +94385,8 @@ function prePackObj(pkg, config = {}) {
93800
94385
  if (!config.silent) console.log("scripts.postpack missing, adding it.\n To skip this, set publishUtil.autoPostPack to false");
93801
94386
  set(pkg, "scripts.postpack", "publish-util-postpack");
93802
94387
  }
93803
- if ((scripts === null || scripts === void 0 ? void 0 : scripts.prepack) === "publish-util-prepack") delete scripts.prepack;
94388
+ if (scripts && runsPrePack(scripts.prepack)) delete scripts.prepack;
94389
+ if ((scripts === null || scripts === void 0 ? void 0 : scripts.prepublishOnly) === "publish-util-prepublishonly") delete scripts.prepublishOnly;
93804
94390
  if (keepObj) merge(pkg, keepObj);
93805
94391
  }
93806
94392
 
@@ -94464,6 +95050,7 @@ var PkgSrcManager = class {
94464
95050
  /** trimmed packument copies, see util/trimmed-packument */
94465
95051
  _packumentDir;
94466
95052
  _lastMetaStatus;
95053
+ _registryOpts;
94467
95054
  _fetching;
94468
95055
  _fetchingMsg;
94469
95056
  constructor(options) {
@@ -94665,33 +95252,38 @@ var PkgSrcManager = class {
94665
95252
  this.updateFetchMetaStatus();
94666
95253
  }
94667
95254
  };
94668
- const pacoteRequest = () => {
94669
- logger$1.debug(`pacote.packument ${qItem.packumentUrl}`);
94670
- return loadPacote().then((pacote) => pacote.packument(pkgName, this.getPacoteOpts({
94671
- fullMetadata: true,
94672
- fetchRetries: 3,
94673
- preferOnline: true,
94674
- memoize: false
94675
- }))).then((x) => {
94676
- if (!x) {
94677
- const msg = `pacote returned null/undefined for packument of ${pkgName}`;
94678
- logger$1.error(chalk.yellow(msg));
94679
- throw Object.assign(new AggregateError$1([new Error(msg)], msg), { expected: true });
94680
- }
94681
- if (x.readme) delete x.readme;
94682
- if (x._contentLength) delete x._contentLength;
94683
- updateItem(x._cached ? "cached" : "200");
94684
- return x;
94685
- }).catch((err) => {
94686
- const msg = `pacote failed fetching packument of ${pkgName}`;
95255
+ const registryRequest = async () => {
95256
+ logger$1.debug(`fetch packument ${qItem.packumentUrl}`);
95257
+ const { cached } = qItem;
95258
+ const file = trimmedPackumentFile(this._packumentDir, qItem.packumentUrl);
95259
+ try {
95260
+ const job = {
95261
+ url: qItem.packumentUrl,
95262
+ file,
95263
+ etag: cached?.etag,
95264
+ lastModified: cached?.lastModified,
95265
+ full: Boolean(this._fyn.lockTime) || Boolean(cached && !cached.corgi)
95266
+ };
95267
+ const pool = getFsWorkerPool();
95268
+ const res = await fetchPackument(job, (url, headers) => this.registryGet(url, headers), pool ? (saveJob) => pool.run("savePackument", saveJob) : void 0);
95269
+ if (res.status === 304) {
95270
+ if (cached.trimmed) touchTrimmedPackument(file);
95271
+ else writeTrimmedPackument(file, cached.packument, void 0, cached);
95272
+ updateItem("304");
95273
+ return cached.packument;
95274
+ }
95275
+ updateItem("200");
95276
+ return parseTrimmed(res.json).packument;
95277
+ } catch (err) {
95278
+ const msg = `failed fetching packument of ${pkgName}`;
94687
95279
  logger$1.error(chalk.yellow(msg), chalk.red(err.message));
94688
95280
  throw Object.assign(new AggregateError$1([err], msg), { expected: true });
94689
- });
95281
+ }
94690
95282
  };
94691
95283
  this._metaStat.wait--;
94692
95284
  this._metaStat.inTx++;
94693
95285
  this.updateFetchMetaStatus(false);
94694
- return (qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : pacoteRequest()).then((x) => {
95286
+ return (qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : dist_default.resolve(registryRequest())).then((x) => {
94695
95287
  this._metaStat.inTx--;
94696
95288
  const time = Date.now() - startTime;
94697
95289
  if (time > 2e4) logger$1.info(chalk.red("Fetch meta of package"), log_format_default.pkgId(qItem.item), `took ${log_format_default.time(time)}!!!`);
@@ -94701,14 +95293,20 @@ var PkgSrcManager = class {
94701
95293
  qItem.defer.reject(Object.assign(new AggregateError$1([new Error(msg)], msg), { expected: true }));
94702
95294
  return;
94703
95295
  }
94704
- refreshCacheEntry(this._cacheDir, qItem.cacheKey).catch(() => {});
94705
- if (!qItem.item.urlType) writeTrimmedPackument(trimmedPackumentFile(this._packumentDir, qItem.packumentUrl), x);
94706
95296
  qItem.defer.resolve(x);
94707
95297
  }).catch((err) => {
94708
95298
  this._metaStat.inTx--;
94709
95299
  qItem.defer.reject(err);
94710
95300
  });
94711
95301
  }
95302
+ /** the plain values of the pacote options, for util/registry-get */
95303
+ get registryOpts() {
95304
+ return this._registryOpts ??= pickRegistryOpts(this._pacoteOpts);
95305
+ }
95306
+ /** GET from the registry, see util/registry-get */
95307
+ registryGet(url, extraHeaders) {
95308
+ return registryGet(url, extraHeaders, this.registryOpts);
95309
+ }
94712
95310
  hasMeta(item) {
94713
95311
  return Boolean(this._meta[item.resolutionName || item.name]);
94714
95312
  }
@@ -94882,16 +95480,35 @@ var PkgSrcManager = class {
94882
95480
  };
94883
95481
  };
94884
95482
  const readMemoizedPackument = async () => {
94885
- const memoized = await loadBestCachedPackument(false);
94886
- if (!(memoized && memoized.data)) return;
95483
+ const memoized = await readTrimmedPackument(trimmedFile) || await loadBestCachedPackument(false);
95484
+ if (!(memoized && (memoized.packument || memoized.data))) return;
94887
95485
  logger$1.debug(`using memoized packument cache for '${pkgName}'`);
94888
95486
  cacheMemoized = true;
94889
95487
  this._metaStat.wait--;
94890
- return JSON.parse(memoized.data.toString());
95488
+ return memoized.packument || JSON.parse(memoized.data.toString());
94891
95489
  };
94892
95490
  let fetchAttempted = false;
94893
95491
  let foundCache;
94894
95492
  let foundPackument;
95493
+ const revalidate = (packument) => {
95494
+ if (foundCache.packument) {
95495
+ const { etag, lastModified, corgi } = foundCache;
95496
+ return {
95497
+ packument,
95498
+ trimmed: true,
95499
+ etag,
95500
+ lastModified,
95501
+ corgi
95502
+ };
95503
+ }
95504
+ const resHeaders = foundCache.metadata?.resHeaders || {};
95505
+ return {
95506
+ packument,
95507
+ trimmed: false,
95508
+ etag: resHeaders.etag,
95509
+ lastModified: resHeaders["last-modified"]
95510
+ };
95511
+ };
94895
95512
  const queueMetaFetchRequest = (cached) => {
94896
95513
  fetchAttempted = true;
94897
95514
  const offline = this._fyn.remoteMetaDisabled;
@@ -94912,7 +95529,8 @@ var PkgSrcManager = class {
94912
95529
  cacheKey,
94913
95530
  item,
94914
95531
  packumentUrl,
94915
- defer: dist_default.defer()
95532
+ defer: dist_default.defer(),
95533
+ cached: cached && revalidate(cached)
94916
95534
  };
94917
95535
  this._netQ.addItem(netQItem);
94918
95536
  return netQItem.defer.promise;
@@ -94922,7 +95540,7 @@ var PkgSrcManager = class {
94922
95540
  let cacheLookup;
94923
95541
  if (item.urlType) cacheLookup = this._fyn.remoteMetaDisabled ? loadPreparedUrlMeta().then((preparedUrlMeta) => ({ preparedUrlMeta })) : dist_default.resolve();
94924
95542
  else if (forceRefresh) cacheLookup = dist_default.resolve();
94925
- else cacheLookup = readTrimmedPackument(trimmedFile).then((trimmed) => trimmed || loadBestCachedPackument(true));
95543
+ else cacheLookup = readTrimmedPackument(trimmedFile).then((trimmed) => trimmed && !(trimmed.corgi && this._fyn.lockTime) ? trimmed : loadBestCachedPackument(true));
94926
95544
  const promise = cacheLookup.then((cached) => {
94927
95545
  if (cached && cached.preparedUrlMeta) {
94928
95546
  cacheMemoized = true;
@@ -95053,10 +95671,21 @@ var PkgSrcManager = class {
95053
95671
  if (di && di.urlType) return `${di.name}@${di.semver}`;
95054
95672
  return `${pkgInfo.name}@${pkgInfo.version}`;
95055
95673
  }
95056
- async getCentralPackage(integrity, pkgInfo) {
95674
+ /**
95675
+ * A package not in the central store yet comes back as a store job, so the extractor writes
95676
+ * the store, not the download slot this runs in.
95677
+ *
95678
+ * @param data - the tarball's bytes, when it was downloaded for the central store and not
95679
+ * into cacache
95680
+ */
95681
+ async getCentralPackage(integrity, pkgInfo, data) {
95057
95682
  const { central, copy } = this._fyn;
95058
95683
  const tarId = this.tarballFetchId(pkgInfo);
95059
95684
  const tarStream = async () => {
95685
+ if (data) {
95686
+ if (!import_lib.default.checkData(data, integrity)) throw Object.assign(/* @__PURE__ */ new Error(`integrity check failed for ${tarId}`), { code: "EINTEGRITY" });
95687
+ return Readable.from([data]);
95688
+ }
95060
95689
  return integrity && await (await loadCacache()).get.hasContent(this._cacheDir, integrity) ? this.cacacheTarballStream(integrity) : this.pacoteTarballStream(tarId, pkgInfo, integrity);
95061
95690
  };
95062
95691
  if (integrity && central) {
@@ -95075,7 +95704,17 @@ var PkgSrcManager = class {
95075
95704
  hasCentral = false;
95076
95705
  }
95077
95706
  }
95078
- if (!hasCentral) await central.storeTarStream(tarId, integrity, tarStream);
95707
+ if (!hasCentral) {
95708
+ const tarSource = async () => {
95709
+ if (data) return { data };
95710
+ const content = await (await loadCacache()).get.hasContent(this._cacheDir, integrity);
95711
+ return content ? { file: getContentPath(this._cacheDir, content.sri) } : void 0;
95712
+ };
95713
+ return {
95714
+ integrity,
95715
+ store: (deferIfBusy) => central.storeTarStream(tarId, integrity, tarStream, deferIfBusy, tarSource)
95716
+ };
95717
+ }
95079
95718
  return integrity;
95080
95719
  }
95081
95720
  }
@@ -95085,7 +95724,14 @@ var PkgSrcManager = class {
95085
95724
  const startTime = Date.now();
95086
95725
  const pkgId = this.tarballFetchId(pkgInfo);
95087
95726
  const integrity = this.getIntegrity(pkgInfo);
95088
- const doFetch = () => {
95727
+ const tarballUrl = get$1(pkgInfo, "dist.tarball");
95728
+ const central = this._fyn.central;
95729
+ const centralEligible = Boolean(integrity && tarballUrl && central && !tarballUrl.startsWith("~url-spec~"));
95730
+ const copied = () => {
95731
+ const { copy } = this._fyn;
95732
+ return copy.indexOf(pkgInfo.name) >= 0 || copy.indexOf(`${pkgInfo.name}@${pkgInfo.version}`) >= 0;
95733
+ };
95734
+ const doFetch = (fetchForCentral) => {
95089
95735
  const fetchStartTime = Date.now();
95090
95736
  if (!this._fetching) {
95091
95737
  this._fetching = [];
@@ -95093,21 +95739,28 @@ var PkgSrcManager = class {
95093
95739
  }
95094
95740
  this._fetching.push(pkgId);
95095
95741
  logger$1.updateItem(FETCH_PACKAGE, `${this._fetching.length} ${this._fetchingMsg}`);
95096
- return this.pacotePrefetch(pkgId, pkgInfo, integrity).then(() => {
95742
+ return (fetchForCentral ? dist_default.resolve(this.registryGet(tarballUrl, {})).then((res) => res.buffer()) : this.pacotePrefetch(pkgId, pkgInfo, integrity)).then((data) => {
95097
95743
  const status = chalk.cyan(`200`);
95098
95744
  const time = log_format_default.time(Date.now() - fetchStartTime);
95099
95745
  const ix = this._fetching.indexOf(pkgId);
95100
95746
  this._fetching.splice(ix, 1);
95101
95747
  this._fetchingMsg = `${status} ${time} ${chalk.red.bgGreen(pkgInfo.name)}`;
95102
95748
  logger$1.updateItem(FETCH_PACKAGE, `${this._fetching.length} ${this._fetchingMsg}`);
95103
- return this.getCentralPackage(integrity, pkgInfo);
95749
+ return this.getCentralPackage(integrity, pkgInfo, data || void 0);
95104
95750
  });
95105
95751
  };
95106
- const promise = dist_default.resolve(loadCacache()).then((cacache) => cacache.get.hasContent(this._cacheDir, integrity)).catch(() => false).then((content) => {
95107
- if (content) return this.getCentralPackage(integrity, pkgInfo);
95752
+ const fetchRemote = (fetchForCentral) => {
95108
95753
  const rd = this._fyn.remoteTgzDisabled;
95109
95754
  if (rd) throw new Error(`option ${rd} has disabled retrieving tarball from remote`);
95110
- return doFetch();
95755
+ return doFetch(fetchForCentral);
95756
+ };
95757
+ const promise = dist_default.resolve().then(async () => {
95758
+ if (centralEligible) {
95759
+ if (await central.has(integrity)) return this.getCentralPackage(integrity, pkgInfo);
95760
+ if (!this._fyn.remoteTgzDisabled && !copied() && await central.allow(integrity)) return fetchRemote(true);
95761
+ }
95762
+ if (await dist_default.resolve(loadCacache()).then((cacache) => cacache.get.hasContent(this._cacheDir, integrity)).catch(() => false)) return this.getCentralPackage(integrity, pkgInfo);
95763
+ return fetchRemote(false);
95111
95764
  });
95112
95765
  return {
95113
95766
  then: (r, e) => promise.then(r, e),
@@ -96928,6 +97581,7 @@ var PkgDepLocker = class {
96928
97581
  */
96929
97582
  saveInstalled(filename) {
96930
97583
  if (!this._enable) return;
97584
+ file_ops_default.$.mkdirp(Path.dirname(filename));
96931
97585
  file_ops_default.writeFileSync(filename, this._stringify(Path.dirname(filename)));
96932
97586
  }
96933
97587
  /**
@@ -97113,8 +97767,7 @@ const xenvConfig = (spec, userConfig, options) => {
97113
97767
  };
97114
97768
  }
97115
97769
  };
97116
- const sources = options && options.sources || ["env", "option"];
97117
- sources.push("default");
97770
+ const sources = [...options && options.sources || ["env", "option"], "default"];
97118
97771
  const config = Object.keys(spec).reduce((cfg, k) => {
97119
97772
  const opt = spec[k];
97120
97773
  const type = getType(opt);
@@ -97143,18 +97796,12 @@ const xenvConfig = (spec, userConfig, options) => {
97143
97796
  //#endregion
97144
97797
  //#region lib/fyn-config.ts
97145
97798
  const spec = {
97146
- registry: {
97147
- env: "FYN_REGISTRY",
97148
- default: "http://localhost:4873"
97149
- },
97799
+ registry: { default: "http://localhost:4873" },
97150
97800
  pkgFile: {
97151
97801
  env: "FYN_PACKAGE_FILE",
97152
97802
  default: "package.json"
97153
97803
  },
97154
- targetDir: {
97155
- env: "FYN_TARGET_DIR",
97156
- default: "xout"
97157
- },
97804
+ targetDir: { default: "xout" },
97158
97805
  fynDir: {
97159
97806
  env: [
97160
97807
  "FYN_DIR",
@@ -97183,12 +97830,135 @@ function fynConfig(override) {
97183
97830
  }
97184
97831
 
97185
97832
  //#endregion
97186
- //#region lib/fyn-central.ts
97187
- const { missPipe } = fyntil;
97188
- /** Type guard to check if parsed tree is new format with version */
97189
- function isTreeFileContent(obj) {
97190
- return typeof obj === "object" && obj !== null && "_" in obj && typeof obj._ === "number";
97833
+ //#region lib/util/reflink.ts
97834
+ let nativeReflink;
97835
+ /**
97836
+ * @fynjs/reflink is a native addon, so it cannot be bundled into dist/. Load it at runtime from
97837
+ * an optional dependency, and resolve to undefined when it is absent or has no binary for
97838
+ * this platform.
97839
+ */
97840
+ function loadReflink() {
97841
+ nativeReflink ??= (async () => {
97842
+ try {
97843
+ const file = createRequire(import.meta.url).resolve("@fynjs/reflink");
97844
+ return await import(
97845
+ /* @vite-ignore */
97846
+ pathToFileURL(file).href
97847
+ );
97848
+ } catch (err) {
97849
+ logger$1.debug(`@fynjs/reflink not loaded, using JS clone: ${err.message}`);
97850
+ return;
97851
+ }
97852
+ })();
97853
+ return nativeReflink;
97191
97854
  }
97855
+ async function loadReflinkCloneFiles() {
97856
+ return (await loadReflink())?.cloneFiles;
97857
+ }
97858
+ /** undefined with no reflink, or an reflink older than cloneDir */
97859
+ async function loadReflinkCloneDir() {
97860
+ return (await loadReflink())?.cloneDir;
97861
+ }
97862
+
97863
+ //#endregion
97864
+ //#region lib/util/untar-tree.ts
97865
+ /** An `onentry` for Tar.x that builds the tree, and the result once extraction finishes */
97866
+ function treeCollector(strip) {
97867
+ const newDirObj = () => {
97868
+ const n = Object.create(null);
97869
+ n["/"] = Object.create(null);
97870
+ return n;
97871
+ };
97872
+ const result = {
97873
+ tree: newDirObj(),
97874
+ fromHeaders: true
97875
+ };
97876
+ const onentry = (entry) => {
97877
+ const parts = entry.path.split(/\/|\\/);
97878
+ const isDir = entry.type === "Directory";
97879
+ const wtree = parts.slice(strip, isDir ? parts.length : parts.length - 1).reduce((wt, dir) => {
97880
+ return wt[dir] || (wt[dir] = newDirObj());
97881
+ }, result.tree);
97882
+ if (isDir) return;
97883
+ if (!entry.mtime || ![
97884
+ "File",
97885
+ "OldFile",
97886
+ "ContiguousFile"
97887
+ ].includes(entry.type)) result.fromHeaders = false;
97888
+ const fname = parts[parts.length - 1];
97889
+ if (fname) {
97890
+ const m = Math.round((entry.mtime ? entry.mtime.getTime() : Date.now()) / 1e3);
97891
+ wtree["/"][fname] = {
97892
+ z: entry.size,
97893
+ m,
97894
+ $: entry.header.cksumValid && entry.header.cksum
97895
+ };
97896
+ }
97897
+ };
97898
+ return {
97899
+ onentry,
97900
+ result
97901
+ };
97902
+ }
97903
+ const hashList = (list) => Crypto$1.createHash("sha512").update(JSON.stringify(list.sort())).digest("base64");
97904
+ /** The shaSum of a tree from tar headers, in SUM_VERSION's format */
97905
+ function treeShasum(tree) {
97906
+ const files = [];
97907
+ const walk = (node, dir) => {
97908
+ for (const [name, child] of Object.entries(node)) if (name === "/") for (const [file, info] of Object.entries(child)) files.push(`${dir}${file}-${info.m}-${info.z}`);
97909
+ else walk(child, `${dir}${name}/`);
97910
+ };
97911
+ walk(tree, "");
97912
+ return hashList(files);
97913
+ }
97914
+ /** tree.json's content. An old entry keeps its sumVersion until validate() converts it. */
97915
+ function treeFileJson(info) {
97916
+ return JSON.stringify({
97917
+ $: info.tree,
97918
+ shaSum: info.shaSum,
97919
+ mutates: info.mutates,
97920
+ _: info.sumVersion ?? 1
97921
+ });
97922
+ }
97923
+
97924
+ //#endregion
97925
+ //#region lib/util/store-entry.ts
97926
+ /** an extraction marker or temp dir older than this was left by an install that died */
97927
+ const STALE_MS = 3e5;
97928
+ /** a name suffix no other install, or other call in this one, will pick */
97929
+ const uniqueSuffix = () => `${process.pid}-${Crypto$1.randomBytes(4).toString("hex")}`;
97930
+ /**
97931
+ * filterScanDir options that hash a package dir in one stat walk, in both formats: v1 for
97932
+ * checking an old entry, v2 for everything else (see SUM_VERSION). Call `sums` with the scan's
97933
+ * result.
97934
+ */
97935
+ function shasumScan(packageDir) {
97936
+ const v2 = [];
97937
+ const filter = (_file, _path, extras) => {
97938
+ const { stat, dirFile } = extras;
97939
+ const fullStat = stat;
97940
+ if (!fullStat.isDirectory()) v2.push(`${dirFile.replace(/\\/g, "/")}-${Math.floor(fullStat.mtimeMs / 1e3)}-${fullStat.size}`);
97941
+ return { formatName: `${dirFile}-${fullStat.mtimeMs}-${fullStat.size}` };
97942
+ };
97943
+ return {
97944
+ options: {
97945
+ cwd: packageDir,
97946
+ filter,
97947
+ filterDir: filter,
97948
+ fullStat: true,
97949
+ concurrency: 500,
97950
+ sortFiles: false,
97951
+ includeDir: true
97952
+ },
97953
+ sums: (files) => ({
97954
+ v1: hashList(files),
97955
+ v2: hashList(v2)
97956
+ })
97957
+ };
97958
+ }
97959
+
97960
+ //#endregion
97961
+ //#region lib/util/place-files.ts
97192
97962
  /** link errors that mean the filesystem can't hardlink from the store at all */
97193
97963
  const NO_LINK_CODES = [
97194
97964
  "EXDEV",
@@ -97197,6 +97967,14 @@ const NO_LINK_CODES = [
97197
97967
  "EOPNOTSUPP",
97198
97968
  "ENOSYS"
97199
97969
  ];
97970
+
97971
+ //#endregion
97972
+ //#region lib/fyn-central.ts
97973
+ const { missPipe } = fyntil;
97974
+ /** Type guard to check if parsed tree is new format with version */
97975
+ function isTreeFileContent(obj) {
97976
+ return typeof obj === "object" && obj !== null && "_" in obj && typeof obj._ === "number";
97977
+ }
97200
97978
  /**
97201
97979
  * Convert a directory tree structure to a flatten one like:
97202
97980
  * ```
@@ -97234,11 +98012,26 @@ function flattenTree(tree, output, baseDir) {
97234
98012
  var FynCentral = class {
97235
98013
  _centralDir;
97236
98014
  _map;
98015
+ /** entries found missing, so has() and allow() don't stat them again; storing re-checks */
98016
+ _missing = /* @__PURE__ */ new Map();
98017
+ /** store dirs this install has made, so each is made once */
98018
+ _madeDirs = /* @__PURE__ */ new Set();
97237
98019
  _hardlink;
97238
- constructor({ centralDir = ".fyn/_central-storage", hardlink = true } = {}) {
98020
+ _reflink;
98021
+ _copyFallback;
98022
+ /** cleared on the first dir clone the filesystem can't do, so later packages skip it */
98023
+ _cloneDirs = true;
98024
+ /**
98025
+ * cleared when a package's files hardlink but none clone, so later packages hardlink in the
98026
+ * fs workers. That's faster than @fynjs/reflink on a filesystem that can't clone (ext4).
98027
+ */
98028
+ _cloneFiles = true;
98029
+ constructor({ centralDir = ".fyn/_central-storage", hardlink = true, reflink = true, copyFallback = true } = {}) {
97239
98030
  this._centralDir = Path.resolve(centralDir);
97240
98031
  this._map = /* @__PURE__ */ new Map();
97241
98032
  this._hardlink = hardlink;
98033
+ this._reflink = reflink;
98034
+ this._copyFallback = copyFallback;
97242
98035
  }
97243
98036
  _analyze(integrity) {
97244
98037
  const sri = import_lib.default.parse(integrity, { single: true });
@@ -97272,14 +98065,16 @@ var FynCentral = class {
97272
98065
  }
97273
98066
  }
97274
98067
  try {
97275
- const stat = await file_ops_default.stat(info.contentPath);
97276
- info.exist = true;
97277
- if (stat.isDirectory()) {
98068
+ if ((await file_ops_default.stat(info.contentPath)).isDirectory()) {
97278
98069
  await this.readInfoTree(info);
97279
98070
  if (!noSet) this._map.set(integrity, info);
97280
98071
  }
98072
+ info.exist = true;
98073
+ this._missing.delete(integrity);
97281
98074
  return info;
97282
98075
  } catch (_err) {
98076
+ info.exist = false;
98077
+ if (!noSet) this._missing.set(integrity, info);
97283
98078
  return info;
97284
98079
  }
97285
98080
  }
@@ -97290,7 +98085,7 @@ var FynCentral = class {
97290
98085
  * @returns boolean
97291
98086
  */
97292
98087
  async has(integrity) {
97293
- const info = this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity);
98088
+ const info = this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity);
97294
98089
  return Boolean(info.tree);
97295
98090
  }
97296
98091
  /**
@@ -97300,7 +98095,7 @@ var FynCentral = class {
97300
98095
  * @returns boolean
97301
98096
  */
97302
98097
  async allow(integrity) {
97303
- return (this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity)).mutates ? false : true;
98098
+ return (this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity)).mutates ? false : true;
97304
98099
  }
97305
98100
  async getContentPath(integrity) {
97306
98101
  return (await this.getInfo(integrity)).contentPath;
@@ -97311,28 +98106,25 @@ var FynCentral = class {
97311
98106
  if (!info.tree) throw new Error(`fyn-central can't get package for integrity ${integrity}`);
97312
98107
  return info;
97313
98108
  }
97314
- async _calcContentShasum(info, pkgDir) {
98109
+ /**
98110
+ * Hash a package dir in one stat walk, in both formats: v1 for checking an old entry, v2 for
98111
+ * everything else (see SUM_VERSION).
98112
+ */
98113
+ async _scanShasums(packageDir) {
97315
98114
  try {
97316
- const packageDir = pkgDir || Path.join(info.contentPath, "package");
97317
- const filter = (_file, _path, extras) => {
97318
- const { stat, dirFile } = extras;
97319
- const fullStat = stat;
97320
- return { formatName: `${dirFile}-${fullStat.mtimeMs}-${fullStat.size}` };
97321
- };
97322
- const files = await filterScanDir({
97323
- cwd: packageDir,
97324
- filter,
97325
- filterDir: filter,
97326
- fullStat: true,
97327
- concurrency: 500,
97328
- sortFiles: false,
97329
- includeDir: true
97330
- });
97331
- return Crypto.createHash("sha512").update(JSON.stringify(files.sort())).digest("base64");
98115
+ const { options, sums } = shasumScan(packageDir);
98116
+ return sums(await filterScanDir(options));
97332
98117
  } catch (_err) {
97333
98118
  return;
97334
98119
  }
97335
98120
  }
98121
+ /** The v2 hash of a fresh extraction, from the tar header sizes and mtimes in its tree */
98122
+ _treeShasum(tree) {
98123
+ return treeShasum(tree);
98124
+ }
98125
+ async _calcContentShasum(info, pkgDir) {
98126
+ return (await this._scanShasums(pkgDir || Path.join(info.contentPath, "package")))?.v1;
98127
+ }
97336
98128
  /**
97337
98129
  * Get the hash of a npm package's extracted content.
97338
98130
  * - only consider the file names and their mtime and size because npm tar files
@@ -97351,16 +98143,41 @@ var FynCentral = class {
97351
98143
  }
97352
98144
  }
97353
98145
  async getMutation(integrity) {
97354
- return (this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity)).mutates;
98146
+ return (this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity)).mutates;
97355
98147
  }
98148
+ /**
98149
+ * Rename the entry away before removing it, so a concurrent install never finds it half
98150
+ * deleted, with tree.json present and package/ partly gone.
98151
+ */
97356
98152
  async delete(integrity) {
97357
98153
  const info = this._map.get(integrity);
97358
98154
  if (info && info.exist && info.contentPath) {
97359
- await file_ops_default.$.rimraf(info.contentPath);
98155
+ const trash = `${info.contentPath}.del-${uniqueSuffix()}`;
98156
+ try {
98157
+ await file_ops_default.rename(info.contentPath, trash);
98158
+ } catch (err) {
98159
+ if (err.code !== "ENOENT") throw err;
98160
+ }
97360
98161
  this._map.delete(integrity);
98162
+ await file_ops_default.$.rimraf(trash);
97361
98163
  }
97362
98164
  }
97363
98165
  /**
98166
+ * An entry with no readable tree.json is left from a broken write. Rename it away, then
98167
+ * remove it, the same way delete() does.
98168
+ */
98169
+ async _removeBrokenEntry(contentPath) {
98170
+ const trash = `${contentPath}.del-${uniqueSuffix()}`;
98171
+ try {
98172
+ await file_ops_default.rename(contentPath, trash);
98173
+ } catch (err) {
98174
+ if (err.code !== "ENOENT") throw err;
98175
+ return;
98176
+ }
98177
+ logger$1.warn(`fyn-central: replacing a broken store entry with no tree.json at ${contentPath}`);
98178
+ await file_ops_default.$.rimraf(trash);
98179
+ }
98180
+ /**
97364
98181
  * Read the content dir tree from file into info
97365
98182
  *
97366
98183
  * @param info - package info
@@ -97374,24 +98191,30 @@ var FynCentral = class {
97374
98191
  if (parsed.mutates !== void 0) info.mutates = parsed.mutates;
97375
98192
  info.tree = parsed.$;
97376
98193
  info.shaSum = parsed.shaSum;
98194
+ info.sumVersion = parsed._;
97377
98195
  } else info.tree = parsed;
97378
98196
  } catch (err) {
97379
98197
  throw new AggregateError$1([err], `fyn-central: reading tree file ${treeFile}`);
97380
98198
  }
97381
98199
  }
97382
98200
  /**
97383
- * Save the content dir tree from info to file
98201
+ * Save the content dir tree from info to file. In a live entry it's written to a temp file
98202
+ * and renamed over, so a concurrent install never reads half of it. A `path` is a temp dir
98203
+ * only this install uses, so it's written directly.
97384
98204
  *
97385
98205
  * @param info - package info
97386
98206
  * @param path - path to save the file
97387
98207
  */
97388
98208
  async saveInfoTree(info, path) {
97389
- await file_ops_default.writeFile(Path.join(path || info.contentPath, "tree.json"), JSON.stringify({
97390
- $: info.tree,
97391
- shaSum: info.shaSum,
97392
- mutates: info.mutates,
97393
- _: 1
97394
- }));
98209
+ const data = treeFileJson(info);
98210
+ if (path) {
98211
+ await file_ops_default.writeFile(Path.join(path, "tree.json"), data);
98212
+ return;
98213
+ }
98214
+ const treeFile = Path.join(info.contentPath, "tree.json");
98215
+ const tmpFile = `${treeFile}.${uniqueSuffix()}`;
98216
+ await file_ops_default.writeFile(tmpFile, data);
98217
+ await file_ops_default.rename(tmpFile, treeFile);
97395
98218
  }
97396
98219
  async setMutation(integrity, mutates = true) {
97397
98220
  const info = await this._loadTree(integrity);
@@ -97399,19 +98222,72 @@ var FynCentral = class {
97399
98222
  info.mutates = mutates;
97400
98223
  await this.saveInfoTree(info);
97401
98224
  }
97402
- async replicate(integrity, destDir) {
98225
+ /**
98226
+ * @param prepare - makes destDir, when the caller hasn't. An fs worker does it instead when
98227
+ * it places the files.
98228
+ * @returns package.json as placed, when an fs worker placed the files
98229
+ */
98230
+ async replicate(integrity, destDir, prepare) {
98231
+ let prepared = !prepare;
98232
+ const ensurePrepared = async () => {
98233
+ if (!prepared) {
98234
+ prepared = true;
98235
+ await prepare();
98236
+ }
98237
+ };
97403
98238
  try {
97404
98239
  const info = await this.getInfo(integrity);
97405
98240
  const list = flattenTree(info.tree, {
97406
98241
  dirs: [],
97407
98242
  files: []
97408
98243
  }, "");
97409
- for (const dir of list.dirs) await file_ops_default.$.mkdirp(Path.join(destDir, dir));
98244
+ const srcDir = Path.join(info.contentPath, "package");
98245
+ const cloneDir = this._reflink && this._cloneDirs && await loadReflinkCloneDir();
98246
+ if (cloneDir) {
98247
+ await ensurePrepared();
98248
+ if (await this._cloneWholeDir(cloneDir, srcDir, destDir)) return;
98249
+ }
98250
+ const reflinkCloneFiles = this._reflink && this._cloneFiles && await loadReflinkCloneFiles();
98251
+ const copy = this._copyFallback;
98252
+ const mkdirs = async () => {
98253
+ for (const dir of list.dirs) await file_ops_default.$.mkdirp(Path.join(destDir, dir));
98254
+ };
98255
+ if (reflinkCloneFiles) {
98256
+ await ensurePrepared();
98257
+ await mkdirs();
98258
+ const others = list.files.filter((f) => f !== "package.json");
98259
+ const [stats] = await Promise.all([reflinkCloneFiles(srcDir, destDir, others, this._hardlink, copy), others.length < list.files.length && reflinkCloneFiles(srcDir, destDir, ["package.json"], false, copy)]);
98260
+ if (this._cloneFiles && stats.cloned === 0 && stats.linked > 0) {
98261
+ this._cloneFiles = false;
98262
+ logger$1.debug(`fyn-central: can't clone files from ${this._centralDir}, hardlinking instead`);
98263
+ }
98264
+ return;
98265
+ }
98266
+ const pool = getFsWorkerPool();
98267
+ if (pool) {
98268
+ const { noLink, pkgJson } = await pool.run("place", {
98269
+ srcDir,
98270
+ destDir,
98271
+ dirs: list.dirs,
98272
+ files: list.files,
98273
+ hardlink: this._hardlink,
98274
+ reflink: this._reflink,
98275
+ copyFallback: copy,
98276
+ prepare: !prepared
98277
+ });
98278
+ if (noLink && this._hardlink) {
98279
+ this._hardlink = false;
98280
+ logger$1.info(`fyn-central: can't hardlink from ${this._centralDir} (${noLink}), copying instead`);
98281
+ }
98282
+ return pkgJson;
98283
+ }
98284
+ await ensurePrepared();
98285
+ await mkdirs();
97410
98286
  await map$1(list.files, (file) => {
97411
98287
  const src = Path.join(info.contentPath, "package", file);
97412
98288
  const dest = Path.join(destDir, file);
97413
- if (file === "package.json") return copyFile(src, dest);
97414
- return this._hardlink ? this._linkFile(src, dest) : cloneFile(src, dest);
98289
+ if (file === "package.json") return this._reflink && !copy ? cloneFile(src, dest, true) : copyFile(src, dest);
98290
+ return this._hardlink ? this._linkFile(src, dest) : this._cloneOrCopy(src, dest);
97415
98291
  }, { concurrency: 5 });
97416
98292
  } catch (err) {
97417
98293
  const msg = `fyn-central can't replicate package at ${destDir} for integrity ${integrity}`;
@@ -97435,68 +98311,118 @@ var FynCentral = class {
97435
98311
  }
97436
98312
  } else if (code !== "EMLINK") throw err;
97437
98313
  }
97438
- await cloneFile(src, dest);
98314
+ await this._cloneOrCopy(src, dest);
98315
+ }
98316
+ /** Clone, else copy, as reflink and copyFallback allow. */
98317
+ async _cloneOrCopy(src, dest) {
98318
+ if (this._reflink) await cloneFile(src, dest, !this._copyFallback);
98319
+ else if (this._copyFallback) await copyFile(src, dest);
98320
+ else throw new Error(`fyn-central: can't place ${dest}: it can't be hardlinked, and reflink and copy-fallback are off`);
98321
+ }
98322
+ /**
98323
+ * Clone the store's package dir to `destDir` in one call. A dir clone needs a path that
98324
+ * doesn't exist, so it goes to a unique sibling that's then renamed over `destDir`. The
98325
+ * rename replaces `destDir` only while it's still the empty dir createPkgOutDir made. If
98326
+ * anything put files there meanwhile, the clone is dropped for the per-file path, which never
98327
+ * writes through an existing file. Resolves false, with `destDir` untouched, when not cloned.
98328
+ */
98329
+ async _cloneWholeDir(cloneDir, srcDir, destDir) {
98330
+ const tmp = `${destDir}.clone-${uniqueSuffix()}`;
98331
+ if (!await cloneDir(srcDir, tmp)) {
98332
+ this._cloneDirs = false;
98333
+ logger$1.debug(`fyn-central: can't clone dirs from ${this._centralDir}, cloning files instead`);
98334
+ return false;
98335
+ }
98336
+ try {
98337
+ await file_ops_default.rename(tmp, destDir);
98338
+ return true;
98339
+ } catch (err) {
98340
+ await file_ops_default.$.rimraf(tmp);
98341
+ const code = err.code;
98342
+ if (code === "ENOTEMPTY" || code === "EEXIST") return false;
98343
+ throw err;
98344
+ }
97439
98345
  }
98346
+ /** Extract a tarball and build its tree from the tar headers. */
97440
98347
  _untarStream(tarStream, targetDir) {
97441
- const newDirObj = () => {
97442
- const n = Object.create(null);
97443
- n["/"] = Object.create(null);
97444
- return n;
97445
- };
97446
- const dirTree = newDirObj();
97447
98348
  const strip = 1;
98349
+ const { onentry, result } = treeCollector(strip);
97448
98350
  const untarStream = So({
97449
98351
  strip,
97450
98352
  strict: true,
97451
98353
  C: targetDir,
97452
- onentry: (entry) => {
97453
- const parts = entry.path.split(/\/|\\/);
97454
- const isDir = entry.type === "Directory";
97455
- const wtree = parts.slice(strip, isDir ? parts.length : parts.length - 1).reduce((wt, dir) => {
97456
- return wt[dir] || (wt[dir] = newDirObj());
97457
- }, dirTree);
97458
- if (isDir) return;
97459
- const fname = parts[parts.length - 1];
97460
- if (fname) {
97461
- const m = Math.round((entry.mtime ? entry.mtime.getTime() : Date.now()) / 1e3);
97462
- wtree["/"][fname] = {
97463
- z: entry.size,
97464
- m,
97465
- $: entry.header.cksumValid && entry.header.cksum
97466
- };
97467
- }
97468
- }
98354
+ onentry
97469
98355
  });
97470
- return missPipe(tarStream, untarStream).then(() => dirTree);
98356
+ return missPipe(tarStream, untarStream).then(() => result);
97471
98357
  }
97472
- async _acquireTmpLock(info) {
97473
- const tmpLock = `${info.contentPath}.lock`;
98358
+ /**
98359
+ * Claim the `.extracting` marker that tells other installs this one is writing the entry.
98360
+ * mkdir is atomic, so only one install gets it. A marker older than STALE_MS was left by an
98361
+ * install that died, so it is taken over.
98362
+ *
98363
+ * @returns true when this install owns the marker
98364
+ */
98365
+ async _claimMarker(marker, retry = true) {
97474
98366
  try {
97475
- await file_ops_default.$.mkdirp(Path.dirname(info.contentPath));
97476
- await file_ops_default.$.acquireLock(tmpLock, {
97477
- wait: 3e5,
97478
- pollPeriod: 500,
97479
- stale: 3e5
97480
- });
98367
+ await file_ops_default.mkdir(marker);
98368
+ return true;
97481
98369
  } catch (err) {
97482
- logger$1.error("fyn-central - unable to acquire tmp lock", tmpLock);
97483
- const msg = err.message && err.message.replace(tmpLock, "<lockfile>");
97484
- throw new Error(`Unable to acquire fyn-central tmp lock ${tmpLock} - ${msg}`);
98370
+ if (err.code !== "EEXIST") throw err;
97485
98371
  }
97486
- return tmpLock;
98372
+ const stat = await file_ops_default.stat(marker).catch(() => void 0);
98373
+ if (retry && (!stat || Date.now() - stat.mtimeMs > 3e5)) {
98374
+ await file_ops_default.$.rimraf(marker);
98375
+ return this._claimMarker(marker, false);
98376
+ }
98377
+ return false;
97487
98378
  }
97488
- async _storeTarStream(info, _integrity, _stream) {
98379
+ /** Remove temp and trash dirs that installs which died left next to the entry. */
98380
+ async _removeStaleTemps(contentPath) {
98381
+ const dir = Path.dirname(contentPath);
98382
+ const base = Path.basename(contentPath);
98383
+ for (const name of await file_ops_default.readdir(dir)) {
98384
+ if (!name.startsWith(`${base}.tmp`) && !name.startsWith(`${base}.del-`)) continue;
98385
+ const stat = await file_ops_default.stat(Path.join(dir, name)).catch(() => void 0);
98386
+ if (stat && Date.now() - stat.mtimeMs > 3e5) await file_ops_default.$.rimraf(Path.join(dir, name));
98387
+ }
98388
+ }
98389
+ /**
98390
+ * Extract into a temp dir only this call uses, then rename it into place. The rename is
98391
+ * atomic, so other installs see the entry complete or not at all. If another install renamed
98392
+ * its copy in first, that copy has the same integrity, so this one is dropped for it.
98393
+ *
98394
+ * With an fs worker, storeTarStream hands the whole job to _storeInWorker instead.
98395
+ */
98396
+ async _storeTarStream(info, _stream) {
97489
98397
  let stream = _stream;
97490
- const tmp = `${info.contentPath}.tmp`;
97491
- await file_ops_default.$.rimraf(tmp);
97492
- const targetDir = Path.join(tmp, "package");
97493
- await file_ops_default.$.mkdirp(targetDir);
97494
- if (typeof stream === "function") stream = stream();
97495
- if (stream.then) stream = await stream;
97496
- info.tree = await this._untarStream(stream, targetDir);
97497
- info.shaSum = await this._calcContentShasum(info, targetDir);
97498
- await this.saveInfoTree(info, tmp);
97499
- await file_ops_default.rename(tmp, info.contentPath);
98398
+ const tmp = `${info.contentPath}.tmp-${uniqueSuffix()}`;
98399
+ try {
98400
+ const targetDir = Path.join(tmp, "package");
98401
+ await file_ops_default.$.mkdirp(targetDir);
98402
+ if (typeof stream === "function") stream = stream();
98403
+ if (stream.then) stream = await stream;
98404
+ const { tree, fromHeaders } = await this._untarStream(stream, targetDir);
98405
+ info.tree = tree;
98406
+ info.shaSum = fromHeaders ? this._treeShasum(tree) : (await this._scanShasums(targetDir))?.v2;
98407
+ info.sumVersion = 2;
98408
+ await this.saveInfoTree(info, tmp);
98409
+ try {
98410
+ await file_ops_default.rename(tmp, info.contentPath);
98411
+ } catch (err) {
98412
+ const code = err.code;
98413
+ if (![
98414
+ "ENOTEMPTY",
98415
+ "EEXIST",
98416
+ "EPERM"
98417
+ ].includes(code) || !await file_ops_default.exists(info.contentPath)) throw err;
98418
+ logger$1.debug("fyn-central: another install stored it first", info.contentPath);
98419
+ await file_ops_default.$.rimraf(tmp);
98420
+ await this.readInfoTree(info);
98421
+ }
98422
+ } catch (err) {
98423
+ await file_ops_default.$.rimraf(tmp);
98424
+ throw err;
98425
+ }
97500
98426
  info.exist = true;
97501
98427
  }
97502
98428
  /**
@@ -97509,46 +98435,100 @@ var FynCentral = class {
97509
98435
  const info = this._map.get(integrity);
97510
98436
  if (info === void 0 || !info.exist) return false;
97511
98437
  if (info.validated === void 0) {
97512
- const sum = await this.getContentShasum(integrity);
97513
- if (!info.shaSum) {
97514
- info.shaSum = sum;
97515
- await this.saveInfoTree(info);
98438
+ const pkgDir = Path.join(info.contentPath, "package");
98439
+ const pool = getFsWorkerPool();
98440
+ const sums = pool ? await pool.run("scan", { dir: pkgDir }) : await this._scanShasums(pkgDir);
98441
+ if (info.sumVersion === 2) info.validated = Boolean(sums) && info.shaSum === sums.v2;
98442
+ else {
98443
+ info.validated = Boolean(sums) && (!info.shaSum || info.shaSum === sums.v1);
98444
+ if (info.validated) {
98445
+ info.shaSum = sums.v2;
98446
+ info.sumVersion = 2;
98447
+ await this.saveInfoTree(info);
98448
+ }
97516
98449
  }
97517
- info.validated = info.shaSum === sum;
97518
98450
  }
97519
98451
  return info.validated;
97520
98452
  }
97521
- async storeTarStream(pkgId, integrity, stream) {
97522
- let tmpLock = false;
98453
+ /**
98454
+ * Store a package's tarball in the central store. Installs never wait on each other: each
98455
+ * extracts into its own temp dir and renames it into place.
98456
+ *
98457
+ * While extracting, an install holds the entry's `.extracting` marker. With `deferIfBusy`, a
98458
+ * live marker from another install returns false without reading the stream, so the caller
98459
+ * can do its other packages first and come back to this one.
98460
+ *
98461
+ * @param tarSource - resolves the tarball's file on disk or its bytes, when it has either, so
98462
+ * an fs worker can untar it instead of the stream
98463
+ * @returns false when deferred, true when the package is in the store
98464
+ */
98465
+ async storeTarStream(pkgId, integrity, stream, deferIfBusy = false, tarSource) {
97523
98466
  let currentStream = stream;
98467
+ let marker;
97524
98468
  try {
97525
- let info = await this._loadTree(integrity);
98469
+ const pool = tarSource && getFsWorkerPool();
98470
+ const source = pool && await tarSource();
98471
+ if (source) return await this._storeInWorker(pool, source, pkgId, integrity, deferIfBusy);
98472
+ const info = await this._loadTree(integrity);
97526
98473
  if (info.exist) {
97527
98474
  logger$1.debug("fyn-central storeTarStream: already exist", info.contentPath);
97528
98475
  if (!info.tree) logger$1.error(`fyn-central exist package missing tree.json`);
97529
- } else {
97530
- tmpLock = await this._acquireTmpLock(info);
97531
- info = await this._loadTree(integrity, info, true);
97532
- if (info.exist) {
97533
- logger$1.debug("fyn-central storeTarStream: found after lock acquired", info.contentPath);
97534
- if (!info.tree) {
97535
- const msg = `fyn-central content exist but no tree.json ${info.contentPath}`;
97536
- logger$1.error(msg);
97537
- throw new Error(msg);
97538
- }
97539
- } else {
97540
- logger$1.debug("storing tar to central store", pkgId, integrity);
97541
- await this._storeTarStream(info, integrity, currentStream);
97542
- currentStream = void 0;
97543
- this._map.set(integrity, info);
97544
- logger$1.debug("fyn-central storeTarStream: stored", pkgId, info.contentPath);
97545
- }
98476
+ return true;
98477
+ }
98478
+ const entryDir = Path.dirname(info.contentPath);
98479
+ if (!this._madeDirs.has(entryDir)) {
98480
+ await file_ops_default.$.mkdirp(entryDir);
98481
+ this._madeDirs.add(entryDir);
98482
+ }
98483
+ const markerPath = `${info.contentPath}.extracting`;
98484
+ if (await this._claimMarker(markerPath)) {
98485
+ marker = markerPath;
98486
+ if ((await this._loadTree(integrity, info)).exist) return true;
98487
+ await this._removeStaleTemps(info.contentPath);
98488
+ } else if (deferIfBusy) {
98489
+ logger$1.debug("fyn-central: another install is storing it, deferring", pkgId);
98490
+ return false;
97546
98491
  }
98492
+ if (await file_ops_default.exists(info.contentPath)) await this._removeBrokenEntry(info.contentPath);
98493
+ logger$1.debug("storing tar to central store", pkgId, integrity);
98494
+ await this._storeTarStream(info, currentStream);
98495
+ currentStream = void 0;
98496
+ this._map.set(integrity, info);
98497
+ this._missing.delete(integrity);
98498
+ logger$1.debug("fyn-central storeTarStream: stored", pkgId, info.contentPath);
98499
+ return true;
97547
98500
  } finally {
97548
98501
  if (currentStream && currentStream.destroy !== void 0) currentStream.destroy();
97549
- if (tmpLock) await file_ops_default.$.releaseLock(tmpLock);
98502
+ if (marker) await file_ops_default.rmdir(marker).catch(() => void 0);
97550
98503
  }
97551
98504
  }
98505
+ /** storeTarStream's steps, run by an fs worker with sync calls, marker and all */
98506
+ async _storeInWorker(pool, source, pkgId, integrity, deferIfBusy) {
98507
+ const info = this._map.get(integrity) ?? this._missing.get(integrity) ?? this._analyze(integrity);
98508
+ const result = await pool.run("store", {
98509
+ ...source,
98510
+ integrity,
98511
+ contentPath: info.contentPath,
98512
+ deferIfBusy
98513
+ });
98514
+ if (result.busy) {
98515
+ logger$1.debug("fyn-central: another install is storing it, deferring", pkgId);
98516
+ return false;
98517
+ }
98518
+ if (result.stored) {
98519
+ info.tree = result.tree;
98520
+ info.shaSum = result.stored.shaSum;
98521
+ info.sumVersion = result.stored.sumVersion;
98522
+ info.exist = true;
98523
+ this._map.set(integrity, info);
98524
+ this._missing.delete(integrity);
98525
+ logger$1.debug("fyn-central storeTarStream: stored", pkgId, info.contentPath);
98526
+ } else {
98527
+ logger$1.debug("fyn-central: the entry is in place already", info.contentPath);
98528
+ if (!(await this._loadTree(integrity, info)).tree) logger$1.error(`fyn-central exist package missing tree.json`);
98529
+ }
98530
+ return true;
98531
+ }
97552
98532
  };
97553
98533
 
97554
98534
  //#endregion
@@ -99499,19 +100479,10 @@ function applyChalkMethod(chalkInstance, name, values) {
99499
100479
  throw new TypeError(`${name} is not a chalk function`);
99500
100480
  }
99501
100481
  function decodeHtml(str) {
99502
- return str.replace(/&[\w#]+;/g, (m) => {
99503
- if (Object.prototype.hasOwnProperty.call(htmlEntities, m)) return htmlEntities[m];
99504
- if (m.startsWith("&#x")) {
99505
- const s = m.substring(3, m.length - 1);
99506
- const p = parseInt(s, 16);
99507
- return String.fromCodePoint(p);
99508
- }
99509
- if (m.startsWith("&#")) {
99510
- const s = m.substring(2, m.length - 1);
99511
- const p = parseInt(s, 10);
99512
- return String.fromCodePoint(p);
99513
- }
99514
- return m;
100482
+ return str.replace(/&(?:#[xX]([0-9a-fA-F]+)|#([0-9]+)|\w+);/g, (m, hex, dec) => {
100483
+ if (hex === void 0 && dec === void 0) return Object.prototype.hasOwnProperty.call(htmlEntities, m) ? htmlEntities[m] : m;
100484
+ const p = hex !== void 0 ? parseInt(hex, 16) : parseInt(dec, 10);
100485
+ return p <= 1114111 ? String.fromCodePoint(p) : m;
99515
100486
  });
99516
100487
  }
99517
100488
  function applyChalkMarkers(markers, text, userChalk) {
@@ -99553,18 +100524,19 @@ function applyChalkMarkers(markers, text, userChalk) {
99553
100524
  co(typeof chalkify === "function", `final chalk value is not a function after applying ${markers}`);
99554
100525
  return chalkify(text);
99555
100526
  }
100527
+ const MARKERS_RE = /<[^>]*>/g;
99556
100528
  function remove(s, keepHtml) {
99557
- const r = s.replace(/<[^>]*>/g, "").trim();
100529
+ const r = s.replace(MARKERS_RE, "");
99558
100530
  return keepHtml ? r : decodeHtml(r);
99559
100531
  }
99560
100532
  function format$1(s, instance, userChalk) {
99561
100533
  userChalk = normalizeColors(userChalk || instance.CHALK);
99562
- if (userChalk.supportsColor === false) return remove(s);
99563
- const tks = s && s.match(/(<[^>]+>|[^<>]+)/g);
100534
+ const off = userChalk.supportsColor === false;
100535
+ const tks = s && s.match(/<[^<>]+>|[^<>]+|[<>]/g);
99564
100536
  if (!tks) return s || "";
99565
- return decodeHtml(tks.reduceRight((a, e, ix) => {
100537
+ const colorized = tks.reduceRight((a, e, ix) => {
99566
100538
  const lvl = a[a.length - 1];
99567
- if (e[0] !== "<") {
100539
+ if (e[0] !== "<" || e.length === 1) {
99568
100540
  lvl.s = e + lvl.s;
99569
100541
  return a;
99570
100542
  }
@@ -99577,20 +100549,27 @@ function format$1(s, instance, userChalk) {
99577
100549
  return a;
99578
100550
  }
99579
100551
  if (a.length === 1) {
100552
+ if (off) return a;
99580
100553
  const partial = tks.slice(0, ix).join("") + `[${tks[ix]}]` + (tks.length > ix + 1 ? "..." : "");
99581
100554
  throw new Error(`unbalanced open/close markers: ${partial}`);
99582
100555
  }
99583
100556
  const mk = e.substring(1, e.length - 1);
99584
- if (lvl.mk && mk !== lvl.mk) {
100557
+ if (lvl.mk && mk !== lvl.mk && !off) {
99585
100558
  const partial = tks.slice(0, ix).join("") + `[** ${tks[ix]} **]` + tks.slice(ix + 1, lvl.ix).join("") + `[** ${tks[lvl.ix]} **]`;
99586
100559
  throw new Error(`mismatch markers: ${partial}`);
99587
100560
  }
99588
- const t = applyChalkMarkers(mk, lvl.s, userChalk);
100561
+ const t = off ? lvl.s : applyChalkMarkers(mk, lvl.s, userChalk);
99589
100562
  a.pop();
99590
100563
  const nl = a.length - 1;
99591
100564
  a[nl].s = t + a[nl].s;
99592
100565
  return a;
99593
- }, [{ s: "" }])[0].s);
100566
+ }, [{ s: "" }]);
100567
+ while (colorized.length > 1) {
100568
+ const lvl = colorized.pop();
100569
+ const nl = colorized.length - 1;
100570
+ colorized[nl].s = lvl.s + tks[lvl.ix] + colorized[nl].s;
100571
+ }
100572
+ return decodeHtml(colorized[0].s);
99594
100573
  }
99595
100574
  /**
99596
100575
  * Build a chalker bound to a specific colors module.
@@ -101659,6 +102638,25 @@ async function syncLocalExports(options) {
101659
102638
  const createLock$1 = Util.promisify(import_lockfile.lock);
101660
102639
  const unlock$1 = Util.promisify(import_lockfile.unlock);
101661
102640
  const { posixify } = fyntil;
102641
+ /** FYN_SHORT_PKG_DIR is on only for "1" or "true" (any case); any other value is off. */
102642
+ function shortPkgDirFromEnv() {
102643
+ const v = (process.env.FYN_SHORT_PKG_DIR || "").toLowerCase();
102644
+ return v === "1" || v === "true";
102645
+ }
102646
+ /**
102647
+ * Whether an install lock was left by a process on this host that no longer runs. A lock
102648
+ * without an owner, from an older fyn, is left to lockfile's stale time.
102649
+ */
102650
+ async function installLockOwnerGone(fname) {
102651
+ try {
102652
+ const { pid, host } = JSON.parse(await file_ops_default.readFile(fname, "utf8"));
102653
+ if (host !== os$1.hostname() || !Number.isInteger(pid)) return false;
102654
+ process.kill(pid, 0);
102655
+ return false;
102656
+ } catch (err) {
102657
+ return err.code === "ESRCH";
102658
+ }
102659
+ }
101662
102660
  var Fyn = class Fyn {
101663
102661
  _cliSource;
101664
102662
  _shownMissingFiles;
@@ -101726,7 +102724,7 @@ var Fyn = class Fyn {
101726
102724
  logger$1.info("dep lock time set to", this._lockTime.toString());
101727
102725
  }
101728
102726
  this._installConfig = { time: 0 };
101729
- this._shortPkgDir = Boolean(process.env.FYN_SHORT_PKG_DIR);
102727
+ this._shortPkgDir = shortPkgDirFromEnv();
101730
102728
  if (!_fynpo) this._fynpo = {};
101731
102729
  else if (typeof _fynpo === "object") this._fynpo = _fynpo;
101732
102730
  }
@@ -101809,8 +102807,11 @@ var Fyn = class Fyn {
101809
102807
  } else if (options.centralStore) {
101810
102808
  centralDir = Path.join(this.fynDir, "_central-storage");
101811
102809
  logger$1.debug(`Enabling central store by CLI flag using dir ${centralDir}`);
101812
- } else if (!this._fynpo?.config) return this._central = false;
101813
- else {
102810
+ } else if (!this._fynpo?.config) {
102811
+ if (!await this._defaultCentralStore()) return this._central = false;
102812
+ centralDir = Path.join(this.fynDir, "_central-storage");
102813
+ logger$1.debug(`Enabling central store by default using dir ${centralDir}`);
102814
+ } else {
101814
102815
  centralDir = this._fynpo.config.centralDir;
101815
102816
  if (!centralDir) {
101816
102817
  const storeBaseDir = await fyntil.resolveGitMainWorktreeDir(this._fynpo.dir);
@@ -101827,15 +102828,31 @@ var Fyn = class Fyn {
101827
102828
  }
101828
102829
  return this._central = new FynCentral({
101829
102830
  centralDir,
101830
- hardlink: this.hardlink
102831
+ hardlink: this.hardlink,
102832
+ reflink: this.reflink,
102833
+ copyFallback: this.copyFallback
101831
102834
  });
101832
102835
  }
101833
102836
  /**
101834
- * The central store only pays off when node_modules links to it. Copying out of it writes
101835
- * every file twice, so it's skipped when linking is known to be impossible.
102837
+ * A new install uses the central store on macOS when @fynjs/reflink can clone dirs, and on
102838
+ * Linux when it can hardlink or clone, since warm installs then run 2-3x faster than copying.
102839
+ * Files are cloned where possible, else hardlinked, else copied. A .fyn.json that saved
102840
+ * centralDir false keeps copying, and --no-central-store opts out.
102841
+ */
102842
+ async _defaultCentralStore() {
102843
+ if (this._options.centralStore !== void 0 || this._installConfig.centralDir !== void 0) return false;
102844
+ if (process.platform === "darwin") return this.reflink && Boolean(await loadReflinkCloneDir());
102845
+ if (process.platform === "linux") return this.hardlink || this.reflink && Boolean(await loadReflinkCloneFiles());
102846
+ return false;
102847
+ }
102848
+ /**
102849
+ * The central store only pays off when node_modules links or clones from it. Copying out of
102850
+ * it writes every file twice, so it's skipped when neither is known to be possible.
101836
102851
  */
101837
102852
  async _centralCopyOnlyReason(centralDir) {
101838
- if (!this.hardlink && ["darwin", "win32"].includes(process.platform)) return "hardlink is off";
102853
+ if (!this.copyFallback) return;
102854
+ if (!this.hardlink && !this.reflink) return "hardlink and reflink are off";
102855
+ if (!this.hardlink && ["darwin", "win32"].includes(process.platform) && !await loadReflinkCloneFiles()) return "hardlink is off and @fynjs/reflink isn't loaded";
101839
102856
  const deviceOf = async (dir) => {
101840
102857
  for (;;) try {
101841
102858
  return (await file_ops_default.stat(dir)).dev;
@@ -102425,9 +103442,6 @@ var Fyn = class Fyn {
102425
103442
  get refreshOptionals() {
102426
103443
  return this._options.refreshOptionals;
102427
103444
  }
102428
- get ignoreDist() {
102429
- return this._options.ignoreDist;
102430
- }
102431
103445
  get production() {
102432
103446
  return this._options.production;
102433
103447
  }
@@ -102441,9 +103455,20 @@ var Fyn = class Fyn {
102441
103455
  if (process.env.FYN_HARDLINK !== void 0) return fyntil.strToBool(process.env.FYN_HARDLINK);
102442
103456
  return this._installConfig.hardlink !== false;
102443
103457
  }
103458
+ /** --no-reflink never clones store files into node_modules, so @fynjs/reflink isn't used */
103459
+ get reflink() {
103460
+ return this._options.reflink !== false;
103461
+ }
103462
+ /** --no-copy-fallback fails the install on a store file that can't be cloned or hardlinked */
103463
+ get copyFallback() {
103464
+ return this._options.copyFallback !== false;
103465
+ }
102444
103466
  get concurrency() {
102445
103467
  return this._options.concurrency;
102446
103468
  }
103469
+ get extractConcurrency() {
103470
+ return this._options.extractConcurrency;
103471
+ }
102447
103472
  get deepResolve() {
102448
103473
  return this._options.deepResolve;
102449
103474
  }
@@ -102574,10 +103599,18 @@ var Fyn = class Fyn {
102574
103599
  async createInstallLock() {
102575
103600
  await this.createDir(this.getFvDir());
102576
103601
  const fname = this.getFvDir(".installing.lock");
103602
+ if (await installLockOwnerGone(fname)) {
103603
+ logger$1.debug(`taking over ${fname}, its install process is gone`);
103604
+ await file_ops_default.unlink(fname).catch(() => void 0);
103605
+ }
102577
103606
  await createLock$1(fname, {
102578
103607
  wait: 3e3,
102579
103608
  stale: 18e5
102580
103609
  });
103610
+ await file_ops_default.writeFile(fname, JSON.stringify({
103611
+ pid: process.pid,
103612
+ host: os$1.hostname()
103613
+ }));
102581
103614
  return true;
102582
103615
  }
102583
103616
  /**
@@ -102706,9 +103739,10 @@ var Fyn = class Fyn {
102706
103739
  }
102707
103740
  return null;
102708
103741
  }
102709
- async loadJsonForPkg(pkg, dir) {
103742
+ /** @param placed - package.json as an fs worker placed it, so it isn't read back */
103743
+ async loadJsonForPkg(pkg, dir, placed) {
102710
103744
  const fullOutDir = dir || this.getInstalledPkgDir(pkg.name, pkg.version, pkg);
102711
- const json = await fyntil.readPkgJson(fullOutDir, true);
103745
+ const json = await fyntil.readPkgJson(fullOutDir, true, false, placed?.str);
102712
103746
  const pkgId = `${pkg.name}@${pkg.version}`;
102713
103747
  const id = `${json.name}@${json.version}`;
102714
103748
  if (json.version !== pkg.version) {
@@ -102732,13 +103766,11 @@ var Fyn = class Fyn {
102732
103766
  }
102733
103767
  pkg.dir = json[PACKAGE_RAW_INFO].dir;
102734
103768
  pkg.str = json[PACKAGE_RAW_INFO].str;
102735
- try {
102736
- const gypFile = Path.join(fullOutDir, "binding.gyp");
102737
- await file_ops_default.lstat(gypFile);
103769
+ if (placed?.gyp ?? await file_ops_default.lstat(Path.join(fullOutDir, "binding.gyp")).then(() => true, () => false)) {
102738
103770
  json.gypfile = true;
102739
103771
  const scr = json.scripts;
102740
103772
  if (isEmpty(scr) || !scr.install && !scr.postinstall && !scr.postInstall) set(json, "scripts.install", "node-gyp rebuild");
102741
- } catch (err) {}
103773
+ }
102742
103774
  pkg.json = json;
102743
103775
  return json;
102744
103776
  }
@@ -105006,13 +106038,15 @@ const showOutdated = async (fyn, options = {}) => {
105006
106038
  /**
105007
106039
  * Audit cache utilities for storing/retrieving npm security advisory data.
105008
106040
  *
105009
- * Advisories are immutable - once an advisory exists for a specific package@version,
105010
- * it doesn't change. Therefore we cache indefinitely (no TTL check needed).
106041
+ * New advisories get published against versions that already exist, and a pinned lockfile
106042
+ * keeps the same cache key for a long time. So entries expire after AUDIT_CACHE_TTL.
105011
106043
  *
105012
106044
  * Cache key is generated from SHA256 hash of the sorted bulk request payload,
105013
106045
  * ensuring different dependency sets get different cache entries.
105014
106046
  */
105015
106047
  const AUDIT_CACHE_PREFIX = "fyn-audit-";
106048
+ /** How long a cached audit result stays fresh, in ms. */
106049
+ const AUDIT_CACHE_TTL = 18e5;
105016
106050
  /**
105017
106051
  * Generate a deterministic cache key from the bulk request payload.
105018
106052
  * Sorts package names and versions to ensure same dependencies = same key.
@@ -105036,12 +106070,17 @@ async function cacheAuditResult(cacheDir, key, result) {
105036
106070
  }
105037
106071
  /**
105038
106072
  * Retrieve cached audit result.
105039
- * Returns null if not found (cache miss).
106073
+ * Returns null if not found, or if `maxAge` (ms) is given and the entry is older than that.
105040
106074
  */
105041
- async function getCachedAuditResult(cacheDir, key) {
106075
+ async function getCachedAuditResult(cacheDir, key, maxAge) {
105042
106076
  const auditCacheDir = Path.join(cacheDir, "audit");
106077
+ const cacache = await loadCacache();
105043
106078
  try {
105044
- const { data } = await (await loadCacache()).get(auditCacheDir, key);
106079
+ if (maxAge !== void 0) {
106080
+ const info = await cacache.get.info(auditCacheDir, key);
106081
+ if (!info || Date.now() - info.time > maxAge) return null;
106082
+ }
106083
+ const { data } = await cacache.get(auditCacheDir, key);
105045
106084
  return JSON.parse(data.toString());
105046
106085
  } catch (err) {
105047
106086
  const code = err.code;
@@ -105057,7 +106096,7 @@ async function getCachedAuditResult(cacheDir, key) {
105057
106096
  *
105058
106097
  * Workflow:
105059
106098
  * 1. Build bulk payload from resolved DepData.pkgs
105060
- * 2. Check cache for existing advisories
106099
+ * 2. Check cache for advisories fetched within AUDIT_CACHE_TTL
105061
106100
  * 3. If miss, POST to /-/npm/v1/security/advisories/bulk
105062
106101
  * 4. Cache response and return
105063
106102
  */
@@ -105166,7 +106205,7 @@ var AuditReport = class {
105166
106205
  }
105167
106206
  const cacheKey = generateCacheKey(payload);
105168
106207
  if (!this._noCache) {
105169
- const cached = await getCachedAuditResult(this._cacheDir, cacheKey);
106208
+ const cached = await getCachedAuditResult(this._cacheDir, cacheKey, AUDIT_CACHE_TTL);
105170
106209
  if (cached) {
105171
106210
  logger$1.debug("Using cached audit result");
105172
106211
  return cached;
@@ -105306,6 +106345,12 @@ var AuditFormatter = class {
105306
106345
  return this.getSeverityLevel(severity) >= this.getSeverityLevel(this._auditLevel);
105307
106346
  }
105308
106347
  /**
106348
+ * Exit code like npm audit: 1 when any vulnerability meets the audit level.
106349
+ */
106350
+ exitCodeFor(filtered) {
106351
+ return filtered.length > 0 ? 1 : 0;
106352
+ }
106353
+ /**
105309
106354
  * Colorize text based on severity.
105310
106355
  */
105311
106356
  colorize(text, severity) {
@@ -105343,7 +106388,7 @@ var AuditFormatter = class {
105343
106388
  const total = filtered.length;
105344
106389
  return {
105345
106390
  output: `audited ${pkgCount} packages\n\n${total} ${total === 1 ? "vulnerability" : "vulnerabilities"} (${parts.join(", ")})\n\nRun \`fyn audit\` for details.`,
105346
- exitCode: 0
106391
+ exitCode: 1
105347
106392
  };
105348
106393
  }
105349
106394
  /**
@@ -105357,7 +106402,7 @@ var AuditFormatter = class {
105357
106402
  metadata: auditResult.metadata,
105358
106403
  advisories: auditResult.advisories
105359
106404
  }, null, 2),
105360
- exitCode: 0
106405
+ exitCode: this.exitCodeFor(filtered)
105361
106406
  };
105362
106407
  }
105363
106408
  /**
@@ -105404,7 +106449,7 @@ var AuditFormatter = class {
105404
106449
  lines.push("");
105405
106450
  return {
105406
106451
  output: lines.join("\n"),
105407
- exitCode: 0
106452
+ exitCode: this.exitCodeFor(filtered)
105408
106453
  };
105409
106454
  }
105410
106455
  /**
@@ -105481,6 +106526,9 @@ var ShowAudit = class {
105481
106526
  }
105482
106527
  /**
105483
106528
  * Run the audit workflow.
106529
+ *
106530
+ * @returns exit code like npm audit: 1 if a vulnerability meets the audit level
106531
+ * or the audit fails, else 0. Errors are logged, not thrown.
105484
106532
  */
105485
106533
  async runAudit() {
105486
106534
  const spinner = cli_logger_default.spinners[1];
@@ -105499,7 +106547,7 @@ var ShowAudit = class {
105499
106547
  }
105500
106548
  if (Object.keys(depData.pkgs).length === 0) {
105501
106549
  logger$1.info("No dependencies to audit");
105502
- return;
106550
+ return 0;
105503
106551
  }
105504
106552
  logger$1.addItem({
105505
106553
  name: FETCH_META,
@@ -105529,7 +106577,7 @@ var ShowAudit = class {
105529
106577
  statProvider
105530
106578
  });
105531
106579
  if (statProvider) await formatter.precomputeStats(vulnerabilities);
105532
- const { output } = formatter.format(auditResult, vulnerabilities);
106580
+ const { output, exitCode } = formatter.format(auditResult, vulnerabilities);
105533
106581
  if (output) console.log(output);
105534
106582
  if (this._opts.auditFile) {
105535
106583
  const { output: jsonOutput } = new AuditFormatter({
@@ -105541,10 +106589,12 @@ var ShowAudit = class {
105541
106589
  const targetPath = Path.isAbsolute(this._opts.auditFile) ? this._opts.auditFile : Path.join(baseDir, this._opts.auditFile);
105542
106590
  await fs.promises.writeFile(targetPath, jsonOutput, "utf8");
105543
106591
  }
106592
+ return exitCode;
105544
106593
  } catch (err) {
105545
106594
  logger$1.removeItem(FETCH_META);
105546
106595
  logger$1.error("Audit failed:", err.message);
105547
106596
  if (process.env.FYN_DEBUG) logger$1.error(err.stack);
106597
+ return 1;
105548
106598
  }
105549
106599
  }
105550
106600
  };
@@ -105845,7 +106895,7 @@ async function fetchDistTags2(baseUrl, _authToken, options) {
105845
106895
  try {
105846
106896
  return (await options.fetchJSON(pkUrl.toString(), { headers }))["dist-tags"];
105847
106897
  } catch (_a) {
105848
- return {};
106898
+ return;
105849
106899
  }
105850
106900
  }
105851
106901
  /**
@@ -105859,7 +106909,7 @@ async function fetchDistTags2(baseUrl, _authToken, options) {
105859
106909
  function getNpmRegistryInfo(npmConfig, pkg) {
105860
106910
  const pkgNames = pkg.name.split("/");
105861
106911
  const scope = pkgNames.length > 1 ? pkgNames[0] : "";
105862
- const regKey = Object.keys(npmConfig).find((k) => k.endsWith("registry") && (!scope || k.startsWith(scope)));
106912
+ const regKey = scope && Object.keys(npmConfig).find((k) => k.endsWith("registry") && k.startsWith(scope));
105863
106913
  const registry = regKey && npmConfig[regKey] || npmConfig.registry || "https://registry.npmjs.org/";
105864
106914
  const regHost = `//${new URL(registry).host}/`;
105865
106915
  const authKey = Object.keys(npmConfig).find((k) => k.endsWith("_authToken") && k.startsWith(regHost));
@@ -105920,6 +106970,7 @@ async function _internalCheck(options) {
105920
106970
  }));
105921
106971
  };
105922
106972
  if (shouldFetch) {
106973
+ if (!distTags || typeof distTags !== "object") return false;
105923
106974
  let notifiedVersion = typeof existMeta.notifiedVersion === "string" ? existMeta.notifiedVersion : "";
105924
106975
  let notifiedTime = existMeta.notifiedTime || 0;
105925
106976
  const checked = options.checkIsNewer(pkg, distTags, tag);
@@ -107097,6 +108148,18 @@ var FynCli = class {
107097
108148
  const start = Date.now();
107098
108149
  const runAudit = argv.opts?.audit !== false;
107099
108150
  const auditFile = argv.opts?.auditFile;
108151
+ const auditAfterInstall = async () => {
108152
+ if (!runAudit) return;
108153
+ try {
108154
+ await showAudit(this.fyn, {
108155
+ colors: this.fyn._options.colors !== false,
108156
+ summary: true,
108157
+ auditFile
108158
+ });
108159
+ } catch (err) {
108160
+ logger$1.warn("Security audit failed:", err.message);
108161
+ }
108162
+ };
107100
108163
  return dist_default.try(() => this.fyn._initializePkg()).then(async () => {
107101
108164
  checkNewVersion(this.fyn._options);
107102
108165
  if (!this.fyn._changeProdMode && !this.fyn._options.forceInstall && this.fyn._installConfig.fynlocal === Boolean(this.fyn.fynlocal) && this.fyn._installConfig.time) {
@@ -107211,19 +108274,13 @@ var FynCli = class {
107211
108274
  const end = Date.now();
107212
108275
  await this.fyn.saveFynpoIndirects();
107213
108276
  logger$1.info(chalk.green("complete in total"), chalk.magenta(`${(end - start) / 1e3}`) + "secs");
107214
- if (runAudit) try {
107215
- await showAudit(this.fyn, {
107216
- colors: this.fyn._options.colors !== false,
107217
- summary: true,
107218
- auditFile
107219
- });
107220
- } catch (err) {
107221
- logger$1.warn("Security audit failed:", err.message);
107222
- }
107223
- }).catch((err) => {
107224
- if (err.message === "No Change") logger$1.info(`No changes detected since last fyn install - nothing to be done.
108277
+ await auditAfterInstall();
108278
+ }).catch(async (err) => {
108279
+ if (err.message === "No Change") {
108280
+ logger$1.info(`No changes detected since last fyn install - nothing to be done.
107225
108281
  To force install, run 'fyn install --force-install' or 'fyn install --fi'`);
107226
- else failure = err;
108282
+ await auditAfterInstall();
108283
+ } else failure = err;
107227
108284
  }).finally(async () => {
107228
108285
  const localPkgBuild = this.fyn._localPkgBuilder?.waitForDone();
107229
108286
  if (localPkgBuild) try {
@@ -107288,16 +108345,16 @@ var FynCli = class {
107288
108345
  if (this._opts.saveLogs) await this.saveLogs(this._opts.saveLogs);
107289
108346
  }
107290
108347
  }
107291
- audit(argv) {
108348
+ async audit(argv) {
107292
108349
  const opts = {
107293
108350
  json: argv.opts?.json || false,
107294
108351
  omit: argv.opts?.omit || [],
107295
108352
  auditLevel: argv.opts?.auditLevel || "info",
107296
- noCache: argv.opts?.noCache || false
108353
+ noCache: argv.opts?.cache === false
107297
108354
  };
107298
- return showAudit(this.fyn, opts).finally(() => {
107299
- return this._opts.saveLogs && this.saveLogs(this._opts.saveLogs);
107300
- });
108355
+ const exitCode = await showAudit(this.fyn, opts);
108356
+ if (this._opts.saveLogs) await this.saveLogs(this._opts.saveLogs);
108357
+ if (exitCode) fyntil.exit(exitCode);
107301
108358
  }
107302
108359
  /**
107303
108360
  * Remove non-runtime files from a production node_modules. Only runs on demand,
@@ -107818,7 +108875,7 @@ var CliBase = class {
107818
108875
  if (!args) return;
107819
108876
  let seenOptional;
107820
108877
  args.replace(/([<[])([^>\]]*)([>\]])/g, (a, mark, xname) => {
107821
- assert(!this.variadic, `For args specifier of '${this.name}', only the last one can be variadic`);
108878
+ assert(!this.variadic, new InvalidArgSpecifierError(`For args specifier of '${this.name}', only the last one can be variadic`, args));
107822
108879
  const xm = xname.match(/([^\. ]+)?( +)?([^\.]+)?(\.\.+)?([^\.,]*)?( *, *)?([^\.]+)?$/);
107823
108880
  assert(xm, new InvalidArgSpecifierError(`Invalid args specifier '${xname}' for '${this.name}'.`));
107824
108881
  let variadic;
@@ -108026,6 +109083,7 @@ var Options = class {
108026
109083
  makeHelp() {
108027
109084
  const width = this.findLongestOptionHelpText();
108028
109085
  let help = [];
109086
+ setHelpZebra(this.command.ncConfig?.helpZebra !== false);
108029
109087
  resetZebraIndex();
108030
109088
  for (const opt of Object.values(this._options)) {
108031
109089
  const tail = [];
@@ -108249,6 +109307,7 @@ var CommandBase = class CommandBase extends CliBase {
108249
109307
  data.push(strs);
108250
109308
  }
108251
109309
  const cmdWidth = data.reduce((max, n) => n[0].length > max ? n[0].length : max, 0);
109310
+ setHelpZebra(this.ncConfig?.helpZebra !== false);
108252
109311
  resetZebraIndex();
108253
109312
  return data.reduce((help, strs) => {
108254
109313
  const lines = fitLines(strs, " ", " ", cmdWidth, getTerminalWidth());
@@ -108489,6 +109548,9 @@ var CommandNode = class extends ClapNode {
108489
109548
  * Allow you to apply extra config to the parsed object, overriding any `opts` with `source` not start with `cli`.
108490
109549
  *
108491
109550
  * For example, you can allow user to specify options in their `package.json` file, and apply those after the command line is parsed.
109551
+ *
109552
+ * Each key goes to this command and any parsed sub command that declares the option.
109553
+ * Keys that match no declared option are ignored.
108492
109554
  * @param config - Config object containing user options config
108493
109555
  * @param src - Name of the source that provided the config. Default to `user`
108494
109556
  * @returns
@@ -108515,8 +109577,9 @@ var CommandNode = class extends ClapNode {
108515
109577
  this.optCount[camelCaseKey] = this.optCount[matchOpt.name];
108516
109578
  }
108517
109579
  }
108518
- } else if (!this.optNodes[key]) new ClapNodeGenerator(this).addOptionWithArgs(key, [].concat(data.arg), void 0, src);
109580
+ }
108519
109581
  }
109582
+ for (const name in this.subCmdNodes) this.subCmdNodes[name].applyConfig(config, src);
108520
109583
  for (let command = this; command; command = command.getParent()) command._jsonMeta = void 0;
108521
109584
  }
108522
109585
  /**
@@ -108849,6 +109912,7 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108849
109912
  }
108850
109913
  return [null];
108851
109914
  }
109915
+ if (this.optNode && this.takesNegativeNumber(opt)) return this.consumeNonOptAsOption(opt);
108852
109916
  if (this.optNode) {
108853
109917
  this.complete();
108854
109918
  return [null].concat(this.parent.consumeOpt(opt));
@@ -108891,6 +109955,17 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108891
109955
  });
108892
109956
  }
108893
109957
  /**
109958
+ * Whether this open option takes a negative number like `-5` as its next value.
109959
+ * Boolean args don't, so `--flag -5` still reads `-5` as an option.
109960
+ *
109961
+ * @param arg - an argv entry that starts with '-'
109962
+ */
109963
+ takesNegativeNumber(arg) {
109964
+ if (!isNumber(arg)) return false;
109965
+ const args = this.optNode.option.args;
109966
+ return args[Math.min(this.node.argsList.length, args.length - 1)].type !== "boolean";
109967
+ }
109968
+ /**
108894
109969
  *
108895
109970
  * @param data
108896
109971
  * @returns
@@ -108918,7 +109993,11 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108918
109993
  node.argsMap[argIx] = setValue;
108919
109994
  };
108920
109995
  if (opt.hasArgs) for (let i = 0; i < args.length && i < node.argsList.length; i++) setArg(i, args[i], node.argsList[i]);
108921
- else node.argsMap[0] = node.argsList.length > 0 ? this.convertValue(isBoolean(node.argsList[0]) ? "boolean" : isNumber(node.argsList[0]) ? "number" : "string", node.argsList[0], opt) : true;
109996
+ else {
109997
+ const value = node.argsList[0];
109998
+ const isBool = isBoolean(value) || !opt.isUnknown && (value === "0" || value === "1");
109999
+ node.argsMap[0] = node.argsList.length > 0 ? this.convertValue(isBool ? "boolean" : isNumber(value) ? "number" : "string", value, opt) : true;
110000
+ }
108922
110001
  if (args.length > 0 && args[args.length - 1].array) {
108923
110002
  const lastIx = args.length - 1;
108924
110003
  if (node.argsList.length > lastIx) setArg(lastIx, args[lastIx], node.argsList.slice(lastIx));
@@ -109248,7 +110327,6 @@ var NixClap = class extends EventEmitter$1 {
109248
110327
  this._config = config;
109249
110328
  this._name = config.name;
109250
110329
  this._version = config.version || false;
109251
- setHelpZebra(config.helpZebra !== false);
109252
110330
  this._versionAlias = config.versionAlias;
109253
110331
  this._helpOpt = config.hasOwnProperty("help") ? config.help : {
109254
110332
  [HELP]: true,
@@ -109284,10 +110362,13 @@ var NixClap = class extends EventEmitter$1 {
109284
110362
  "parse-fail": (parsed) => this.showError(parsed.command.getErrorNodes()[0].error),
109285
110363
  "no-action": () => this.showHelp(/* @__PURE__ */ new Error("No command given"))
109286
110364
  };
109287
- if (!config.noDefaultHandlers) this.on("exit", defaultExit);
109288
110365
  const handlers = config.handlers || {};
109289
- for (const [name, handler] of Object.entries(this._evtHandlers)) {
109290
- const h = handlers.hasOwnProperty(name) ? handlers[name] : handler;
110366
+ const exitHandler = Object.hasOwn(handlers, "exit") ? handlers.exit : !config.noDefaultHandlers && defaultExit;
110367
+ if (typeof exitHandler === "function") this.on("exit", exitHandler);
110368
+ const names = /* @__PURE__ */ new Set([...Object.keys(this._evtHandlers), ...Object.keys(handlers)]);
110369
+ names.delete("exit");
110370
+ for (const name of names) {
110371
+ const h = Object.hasOwn(handlers, name) ? handlers[name] : this._evtHandlers[name];
109291
110372
  if (typeof h === "function") this.on(name, h);
109292
110373
  }
109293
110374
  this._skipExec = config.skipExec;
@@ -109356,6 +110437,23 @@ var NixClap = class extends EventEmitter$1 {
109356
110437
  return this;
109357
110438
  }
109358
110439
  /**
110440
+ * Make a copy of the help option without aliases that other options already use.
110441
+ */
110442
+ _withFreeHelpAliases(existingOptions) {
110443
+ const helpOpt = this._helpOpt;
110444
+ const usedAliases = /* @__PURE__ */ new Set();
110445
+ for (const optName in existingOptions) {
110446
+ const opt = existingOptions[optName];
110447
+ if (optName !== "help" && opt.alias) (Array.isArray(opt.alias) ? opt.alias : [opt.alias]).forEach((a) => usedAliases.add(a));
110448
+ }
110449
+ /* c8 ignore next 3 */
110450
+ const helpAliases = helpOpt.alias ? Array.isArray(helpOpt.alias) ? helpOpt.alias : [helpOpt.alias] : [];
110451
+ return {
110452
+ ...helpOpt,
110453
+ alias: helpAliases.filter((a) => !usedAliases.has(a))
110454
+ };
110455
+ }
110456
+ /**
109359
110457
  * Recursively adds help option to all subcommands.
109360
110458
  * Filters out help aliases that conflict with existing options in the subcommand.
109361
110459
  */
@@ -109364,18 +110462,9 @@ var NixClap = class extends EventEmitter$1 {
109364
110462
  for (const name in commands) {
109365
110463
  const cmd = commands[name];
109366
110464
  const existingOptions = cmd.options || {};
109367
- const usedAliases = /* @__PURE__ */ new Set();
109368
- for (const optName in existingOptions) {
109369
- const opt = existingOptions[optName];
109370
- if (opt.alias) (Array.isArray(opt.alias) ? opt.alias : [opt.alias]).forEach((a) => usedAliases.add(a));
109371
- }
109372
- const filteredAliases = (this._helpOpt.alias ? Array.isArray(this._helpOpt.alias) ? this._helpOpt.alias : [this._helpOpt.alias] : []).filter((a) => !usedAliases.has(a));
109373
110465
  if (!existingOptions.hasOwnProperty("help")) cmd.options = {
109374
110466
  ...existingOptions,
109375
- help: {
109376
- ...this._helpOpt,
109377
- alias: filteredAliases
109378
- }
110467
+ help: this._withFreeHelpAliases(existingOptions)
109379
110468
  };
109380
110469
  if (cmd.subCommands) this._addHelpToSubCommands(cmd.subCommands);
109381
110470
  }
@@ -109396,13 +110485,13 @@ var NixClap = class extends EventEmitter$1 {
109396
110485
  let verAlias = ["V", "v"];
109397
110486
  Object.keys(options).forEach((k) => {
109398
110487
  const opt = options[k];
109399
- if (opt.alias) verAlias = verAlias.filter((x) => opt.alias.indexOf(x) < 0);
110488
+ if (opt.alias) verAlias = verAlias.filter((x) => ![].concat(opt.alias).includes(x));
109400
110489
  });
109401
110490
  options.version = this._getVersionOpt(verAlias);
109402
110491
  }
109403
110492
  if (this._helpOpt) {
109404
110493
  options = { ...options };
109405
- options["help"] = this._helpOpt;
110494
+ options["help"] = this._withFreeHelpAliases(options);
109406
110495
  this._addHelpToSubCommands(commands);
109407
110496
  }
109408
110497
  this._options = options;
@@ -109581,6 +110670,7 @@ var NixClap = class extends EventEmitter$1 {
109581
110670
  skipExec(skip = true) {
109582
110671
  this._skipExec = skip;
109583
110672
  this._skipExecDefault = skip;
110673
+ return this;
109584
110674
  }
109585
110675
  /**
109586
110676
  * Parses the given arguments and returns the result.
@@ -109671,9 +110761,10 @@ var NixClap = class extends EventEmitter$1 {
109671
110761
  this._rootCommand.name = this._name;
109672
110762
  }
109673
110763
  const { command, index } = new Parser(this).parse(argv, start);
110764
+ if (this._config.config) command.applyConfig(this._config.config(command) || {}, "user");
110765
+ command.applyDefaults();
109674
110766
  const missing = command.checkRequiredOptions();
109675
110767
  if (missing.length > 0) command.addError(/* @__PURE__ */ new Error("missing these required options " + missing.join(", ")));
109676
- command.applyDefaults();
109677
110768
  command.makeCamelCaseOptions();
109678
110769
  const errorNodes = command.getErrorNodes();
109679
110770
  return {
@@ -116550,8 +117641,7 @@ function setLogLevel(ll) {
116550
117641
  if (ll) {
116551
117642
  const levels = Object.keys(cli_logger_default.Levels);
116552
117643
  const real = find(levels, (l) => l.startsWith(ll));
116553
- const x = real ? cli_logger_default.Levels[real] : void 0;
116554
- if (x !== void 0) logger$1._logLevel = x;
117644
+ if ((real ? cli_logger_default.Levels[real] : void 0) !== void 0) logger$1.logLevel = real;
116555
117645
  else {
116556
117646
  logger$1.error(`Invalid log level "${ll}". Supported levels are: ${levels.join(", ")}`);
116557
117647
  fyntil.exit(1);
@@ -116691,12 +117781,21 @@ const options = {
116691
117781
  "central-store": {
116692
117782
  args: "<flag boolean>",
116693
117783
  alias: ["central", "cs"],
116694
- desc: "keep single copy of packages in central store",
116695
- argDefault: "false"
117784
+ desc: "keep single copy of packages in central store. On by default on macOS when @fynjs/reflink can clone, and on Linux when it can hardlink or clone, unless node_modules was installed without it. --no-central-store turns the default off"
116696
117785
  },
116697
117786
  hardlink: {
116698
117787
  args: "<flag boolean>",
116699
- desc: "hardlink central store files into node_modules (default). --no-hardlink is remembered, and skips the central store on macOS and Windows, where it would only copy"
117788
+ desc: "hardlink central store files into node_modules (default). --no-hardlink is remembered, and skips the central store on macOS and Windows, where it would only copy unless @fynjs/reflink can clone"
117789
+ },
117790
+ reflink: {
117791
+ args: "<flag boolean>",
117792
+ desc: "reflink (copy-on-write clone) central store files into node_modules where the filesystem can (default). --no-reflink never clones, so @fynjs/reflink isn't used",
117793
+ argDefault: "true"
117794
+ },
117795
+ "copy-fallback": {
117796
+ args: "<flag boolean>",
117797
+ desc: "copy central store files that can't be cloned or hardlinked (default). --no-copy-fallback fails the install instead",
117798
+ argDefault: "true"
116700
117799
  },
116701
117800
  copy: {
116702
117801
  args: "[packages string..]",
@@ -116776,11 +117875,6 @@ const options = {
116776
117875
  argDefault: "false",
116777
117876
  desc: "force refresh package meta from registry"
116778
117877
  },
116779
- "ignore-dist": {
116780
- alias: "i",
116781
- args: "<flag boolean>",
116782
- desc: "Ignore host in tarball URL from meta dist."
116783
- },
116784
117878
  "ignore-lock-url": {
116785
117879
  args: "<flag boolean>",
116786
117880
  argDefault: "false",
@@ -116828,6 +117922,11 @@ const options = {
116828
117922
  desc: "Max network concurrency (registry sockets)",
116829
117923
  argDefault: "32"
116830
117924
  },
117925
+ "extract-concurrency": {
117926
+ args: "<num number>",
117927
+ alias: "excc",
117928
+ desc: "Max packages extracted at once (default: CPU cores, at most 4)"
117929
+ },
116831
117930
  "auto-run": {
116832
117931
  args: "<flag boolean>",
116833
117932
  argDefault: "true",
@@ -116986,14 +118085,14 @@ const commands = {
116986
118085
  args: "[packages string..]",
116987
118086
  exec: async (cmd) => {
116988
118087
  const json = Boolean(cmd.jsonMeta.opts.json);
116989
- const previousLogLevel = logger$1._logLevel;
118088
+ const previousLogLevel = logger$1.logLevel;
116990
118089
  if (json) cmd.jsonMeta.opts.logLevel = "none";
116991
118090
  try {
116992
118091
  const config = await pickOptions(cmd);
116993
118092
  config.noStartupInfo = json;
116994
118093
  return await new FynCli(config).outdated(cmd.jsonMeta);
116995
118094
  } finally {
116996
- if (json) logger$1._logLevel = previousLogLevel;
118095
+ if (json) logger$1.logLevel = previousLogLevel;
116997
118096
  }
116998
118097
  },
116999
118098
  options: { json: {
@@ -117022,9 +118121,10 @@ const commands = {
117022
118121
  desc: "Minimum severity level to report (info, low, moderate, high, critical)",
117023
118122
  argDefault: "info"
117024
118123
  },
117025
- "no-cache": {
118124
+ cache: {
117026
118125
  args: "<flag boolean>",
117027
- desc: "Bypass the audit cache and fetch fresh data"
118126
+ desc: "use the audit cache (use --no-cache to fetch fresh data)",
118127
+ argDefault: "true"
117028
118128
  }
117029
118129
  }
117030
118130
  },
@@ -117051,7 +118151,7 @@ const commands = {
117051
118151
  } else {
117052
118152
  logger$1.error(chalk.red("Script execution failed:"));
117053
118153
  logger$1.error(err.message || err.toString());
117054
- if (err.stack && logger$1._logLevel <= cli_logger_default.Levels.debug) logger$1.debug(err.stack);
118154
+ if (err.stack && cli_logger_default.Levels[logger$1.logLevel] <= cli_logger_default.Levels.debug) logger$1.debug(err.stack);
117055
118155
  }
117056
118156
  fyntil.exit(typeof exitCode === "number" ? exitCode : 1);
117057
118157
  }