fyn 3.2.3 → 3.3.1

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;
@@ -1221,15 +1238,20 @@ const defaultPathFilter = [new RegExp(`/node_modules.*/(pirates/|isomorphic-load
1221
1238
  function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, ignorePathFilter = [] } = {}) {
1222
1239
  const stack = error && (error.stack || error.message);
1223
1240
  if (!stack) return String(stack);
1224
- return stack.split("\n").map((line) => {
1241
+ return stack.split(/\r?\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) {
1250
+ const prefix = path.startsWith(`file://${replacePath}`) ? `file://${replacePath}` : replacePath;
1251
+ if (path.startsWith(prefix)) path2 = path.slice(prefix.length);
1252
+ }
1253
+ return `${at}${name}${path2}${tail}`;
1254
+ }).filter((x) => x !== false).join("\n");
1233
1255
  }
1234
1256
  /**
1235
1257
  * Build stack of aggregate errors
@@ -1239,9 +1261,10 @@ function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, ignorePathF
1239
1261
  * @returns aggregate stack
1240
1262
  */
1241
1263
  function aggregateStack(stack, errors) {
1242
- return [stack].concat(errors && errors.map && errors.map((e) => {
1264
+ const stacks = errors && errors.map ? errors.map((e) => {
1243
1265
  return (e && (e.stack || e.message) || String(e)).replace(/^/gm, " ");
1244
- })).join("\n");
1266
+ }) : [];
1267
+ return [stack].concat(stacks).join("\n");
1245
1268
  }
1246
1269
  /**
1247
1270
  * build the aggregate stack of an AggregateError
@@ -1250,16 +1273,17 @@ function aggregateStack(stack, errors) {
1250
1273
  * @returns aggregate stack
1251
1274
  */
1252
1275
  function aggregateErrorStack(error) {
1253
- return aggregateStack(error.__stack || error.message || String(error), error.errors);
1276
+ return aggregateStack(error.__stack || (error instanceof AggregateError$1 ? "" : error.stack) || error.message || String(error), error.errors);
1254
1277
  }
1255
1278
  /**
1256
1279
  * AggregateError
1257
1280
  * - https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
1258
1281
  */
1259
1282
  var AggregateError$1 = class extends globalThis.AggregateError {
1260
- constructor(errors, msg) {
1283
+ constructor(errors, msg, options) {
1261
1284
  if (!errors || !(errors[Symbol.iterator] instanceof Function)) throw new TypeError(`input errors must be iterable but it's ${typeof errors}`);
1262
- super(errors, msg);
1285
+ const list = Array.from(errors);
1286
+ super(list, msg, options);
1263
1287
  Object.defineProperty(this, "name", { value: "AggregateError" });
1264
1288
  let aggStack;
1265
1289
  Object.defineProperties(this, {
@@ -1267,15 +1291,22 @@ var AggregateError$1 = class extends globalThis.AggregateError {
1267
1291
  configurable: true,
1268
1292
  enumerable: false,
1269
1293
  writable: true,
1270
- value: [].concat(errors)
1294
+ value: list
1271
1295
  },
1272
1296
  __stack: {
1273
1297
  enumerable: false,
1298
+ writable: true,
1274
1299
  value: this.stack
1275
1300
  },
1276
- stack: { get() {
1277
- return aggStack || (aggStack = aggregateErrorStack(this));
1278
- } }
1301
+ stack: {
1302
+ get() {
1303
+ return aggStack || (aggStack = aggregateErrorStack(this));
1304
+ },
1305
+ set(value) {
1306
+ this.__stack = value;
1307
+ aggStack = "";
1308
+ }
1309
+ }
1279
1310
  });
1280
1311
  }
1281
1312
  };
@@ -1540,6 +1571,22 @@ var AveAzul = class AveAzul extends Promise {
1540
1571
  constructor(executor) {
1541
1572
  super(executor);
1542
1573
  }
1574
+ catch(...args) {
1575
+ if (args.length <= 1) return super.catch(args[0]);
1576
+ const handler = args.pop();
1577
+ if (typeof handler !== "function") throw new TypeError("The last argument to .catch() must be a function, got " + String(handler));
1578
+ const filters = args;
1579
+ return super.catch((err) => {
1580
+ for (const f of filters) if (f === Error || f != null && f.prototype instanceof Error) {
1581
+ if (err instanceof f) return handler(err);
1582
+ } else if (typeof f === "function") {
1583
+ if (f(err)) return handler(err);
1584
+ } else if (err !== null && (typeof err === "object" || typeof err === "function")) {
1585
+ if (Object.keys(f).every((k) => f[k] == err[k])) return handler(err);
1586
+ }
1587
+ throw err;
1588
+ });
1589
+ }
1543
1590
  /**
1544
1591
  * Bluebird-style tap() method that lets you perform side effects in a chain
1545
1592
  * Similar to Bluebird's Promise.prototype.tap()
@@ -1559,7 +1606,16 @@ var AveAzul = class AveAzul extends Promise {
1559
1606
  * @returns Promise that resolves with the filtered array
1560
1607
  */
1561
1608
  filter(fn) {
1562
- return this.then((value) => filter$2(value, fn));
1609
+ return this.then(async (value) => {
1610
+ const arr = toArray$1(value);
1611
+ const len = arr.length;
1612
+ const result = [];
1613
+ for (let i = 0; i < len; i++) {
1614
+ const x = isPromise(arr[i]) ? await arr[i] : arr[i];
1615
+ if (await fn(x, i, len)) result.push(x);
1616
+ }
1617
+ return result;
1618
+ });
1563
1619
  }
1564
1620
  /**
1565
1621
  * Bluebird-style map() method for array operations
@@ -1568,7 +1624,11 @@ var AveAzul = class AveAzul extends Promise {
1568
1624
  * @returns Promise that resolves with the mapped array
1569
1625
  */
1570
1626
  map(fn, options = { concurrency: 50 }) {
1571
- return this.then((value) => map$1(value, fn, options));
1627
+ return this.then((value) => {
1628
+ const arr = toArray$1(value);
1629
+ const len = arr.length;
1630
+ return map$1(arr, (item, index) => fn(item, index, len), options);
1631
+ });
1572
1632
  }
1573
1633
  /**
1574
1634
  * Bluebird-style mapSeries() method for array operations
@@ -1605,7 +1665,7 @@ var AveAzul = class AveAzul extends Promise {
1605
1665
  */
1606
1666
  each(fn) {
1607
1667
  return this.then(async (value) => {
1608
- const arr = value;
1668
+ const arr = toArray$1(value);
1609
1669
  const result = [];
1610
1670
  for (let i = 0; i < arr.length; i++) {
1611
1671
  let x = arr[i];
@@ -1675,7 +1735,7 @@ var AveAzul = class AveAzul extends Promise {
1675
1735
  reduce(fn, initialValue) {
1676
1736
  const hasInitial = arguments.length > 1;
1677
1737
  return this.then(async (array) => {
1678
- const arr = array;
1738
+ const arr = toArray$1(array);
1679
1739
  const len = arr.length;
1680
1740
  let value;
1681
1741
  let idx;
@@ -1750,8 +1810,9 @@ var AveAzul = class AveAzul extends Promise {
1750
1810
  if (typeof fn !== "function") return AveAzul.reject(/* @__PURE__ */ new TypeError("expecting a function but got " + fn));
1751
1811
  return this.then(async (args) => {
1752
1812
  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);
1813
+ const values = args.slice();
1814
+ for (let i = 0; i < values.length; i++) if (isPromise(values[i])) values[i] = await values[i];
1815
+ return fn(...values);
1755
1816
  } else return fn(args);
1756
1817
  });
1757
1818
  }
@@ -1931,6 +1992,15 @@ var AveAzul = class AveAzul extends Promise {
1931
1992
  return AveAzul.resolve(items).each(fn);
1932
1993
  }
1933
1994
  /**
1995
+ * Bluebird-style filter() for array filtering
1996
+ * @param items - Array or iterable to filter
1997
+ * @param fn - Filter function (item, index, length)
1998
+ * @returns Promise that resolves with the items that passed the filter
1999
+ */
2000
+ static filter(items, fn) {
2001
+ return AveAzul.resolve(items).filter(fn);
2002
+ }
2003
+ /**
1934
2004
  * Bluebird-style reduce() for array reduction
1935
2005
  * @param array - Array to reduce
1936
2006
  * @param fn - Reducer function (value, item, index, length)
@@ -1990,7 +2060,7 @@ var AveAzul = class AveAzul extends Promise {
1990
2060
  * @returns Promise that resolves with handler result
1991
2061
  */
1992
2062
  static using(resources, ...args) {
1993
- if (args.length === 0) throw new TypeError("resrouces and handler function required");
2063
+ if (args.length === 0) throw new TypeError("resources and handler function required");
1994
2064
  if (Array.isArray(resources)) {
1995
2065
  if (args.length > 1) throw new TypeError("only two arguments are allowed when passing an array of resources");
1996
2066
  return using(resources, args[0], AveAzul, true);
@@ -2118,7 +2188,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2118
2188
  /* istanbul ignore if */
2119
2189
  if (!processOk(global.process)) return function() {};
2120
2190
  assert$3.equal(typeof cb, "function", "a callback must be provided for exit handler");
2121
- if (loaded === false) load$1();
2191
+ if (loaded === false) load$2();
2122
2192
  var ev = "exit";
2123
2193
  if (opts && opts.alwaysLast) ev = "afterexit";
2124
2194
  var remove = function() {
@@ -2168,7 +2238,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2168
2238
  return signals$1;
2169
2239
  };
2170
2240
  var loaded = false;
2171
- var load$1 = function load() {
2241
+ var load$2 = function load() {
2172
2242
  if (loaded || !processOk(global.process)) return;
2173
2243
  loaded = true;
2174
2244
  emitter.count += 1;
@@ -2183,7 +2253,7 @@ var require_signal_exit = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2183
2253
  process.emit = processEmit;
2184
2254
  process.reallyExit = processReallyExit;
2185
2255
  };
2186
- module.exports.load = load$1;
2256
+ module.exports.load = load$2;
2187
2257
  var originalProcessReallyExit = process.reallyExit;
2188
2258
  var processReallyExit = function processReallyExit(code) {
2189
2259
  /* istanbul ignore if */
@@ -21628,7 +21698,7 @@ var SignalExit = class extends SignalExitBase {
21628
21698
  }
21629
21699
  };
21630
21700
  const process$3 = globalThis.process;
21631
- const { onExit, load, unload } = signalExitWrap(processOk(process$3) ? new SignalExit(process$3) : new SignalExitFallback());
21701
+ const { onExit, load: load$1, unload } = signalExitWrap(processOk(process$3) ? new SignalExit(process$3) : new SignalExitFallback());
21632
21702
 
21633
21703
  //#endregion
21634
21704
  //#region node_modules/restore-cursor/index.js
@@ -24034,11 +24104,16 @@ const LogItemTypes = {
24034
24104
  none: 0
24035
24105
  };
24036
24106
  const DEFAULT_SPINNER_INTERVAL = 100;
24107
+ function levelValue(level) {
24108
+ if (!Object.prototype.hasOwnProperty.call(Levels, level)) throw new TypeError(`VisualLogger unknown log level: ${String(level)}`);
24109
+ return Levels[level];
24110
+ }
24037
24111
  const SPIN_OFF = 0;
24038
24112
  const SPIN_STARTED = 1;
24039
24113
  const SPIN_RUNNING = 2;
24040
24114
  var VisualLogger = class VisualLogger {
24041
24115
  constructor(options = {}) {
24116
+ var _a;
24042
24117
  options = {
24043
24118
  renderFps: 30,
24044
24119
  ...options
@@ -24047,7 +24122,7 @@ var VisualLogger = class VisualLogger {
24047
24122
  this._items = [];
24048
24123
  this._itemOptions = {};
24049
24124
  this._lines = [];
24050
- this._logLevel = Levels.info;
24125
+ this._logLevel = levelValue((_a = options.logLevel) !== null && _a !== void 0 ? _a : "info");
24051
24126
  this._itemType = LogItemTypes.normal;
24052
24127
  this._logData = [];
24053
24128
  this._output = options.output || defaultOutput$1;
@@ -24076,6 +24151,12 @@ var VisualLogger = class VisualLogger {
24076
24151
  this._color = enable;
24077
24152
  this.setPrefix();
24078
24153
  }
24154
+ get logLevel() {
24155
+ return Object.keys(Levels).find((k) => Levels[k] === this._logLevel);
24156
+ }
24157
+ set logLevel(level) {
24158
+ this._logLevel = levelValue(level);
24159
+ }
24079
24160
  static get spinners() {
24080
24161
  return [
24081
24162
  "|/-\\",
@@ -24210,7 +24291,10 @@ var VisualLogger = class VisualLogger {
24210
24291
  this._renderLineMsg(options, data);
24211
24292
  const updateData = typeof data === "string" ? {} : data;
24212
24293
  if (options.save !== false && updateData._save !== false) this._save(options._msg);
24213
- if (updateData._render === false) return this;
24294
+ if (updateData._render === false) {
24295
+ this._lines[itemIdx] = this._shouldLogItem() ? this._renderLine(options) : options._msg;
24296
+ return this;
24297
+ }
24214
24298
  }
24215
24299
  if (this._shouldLogItem()) {
24216
24300
  if (data === void 0 && !this._nextSpin(options)) return this;
@@ -26695,6 +26779,7 @@ var InflightStore = class {
26695
26779
  //#endregion
26696
26780
  //#region node_modules/item-queue/dist/item-queue.js
26697
26781
  const PAUSE_ITEM = Symbol("pause");
26782
+ const PAUSE_CALL = Symbol("pauseCall");
26698
26783
  const RESUME_ITEM = Symbol("resume");
26699
26784
  const NOOP_ITEM = Symbol("NOOP");
26700
26785
  const WATCH_PERIOD = 500;
@@ -26705,6 +26790,8 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26705
26790
  constructor(options) {
26706
26791
  assert(options && typeof options.processItem === "function", "ItemQueue: must provide options.processItem callback");
26707
26792
  super();
26793
+ /** bumped by an immediate pause(), so a resume() tick scheduled earlier skips itself */
26794
+ this._pauseSeq = 0;
26708
26795
  this.Promise = options.Promise || ItemQueue.Promise;
26709
26796
  assert(this.Promise, "ItemQueue: No Promise implementation available");
26710
26797
  this._pending = new InflightStore();
@@ -26714,7 +26801,7 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26714
26801
  this._processItem = options.processItem;
26715
26802
  this._stopOnError = options.stopOnError;
26716
26803
  this._failed = false;
26717
- this._timeout = options.timeout;
26804
+ this._timeout = options.timeout > 0 ? options.timeout : 0;
26718
26805
  this._watchPeriod = options.watchPeriod || WATCH_PERIOD;
26719
26806
  this._watchTime = options.watchTime;
26720
26807
  this._id = 1;
@@ -26731,19 +26818,18 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26731
26818
  * @returns promise that wait for queue to finish
26732
26819
  */
26733
26820
  wait() {
26734
- if (this._failed) return this.Promise.reject(this._failed);
26821
+ if (this._failed) return this.Promise.reject(this._failError);
26735
26822
  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
- }
26823
+ const onDone = (data) => {
26824
+ this.removeListener("fail", onFail);
26825
+ resolve(data);
26744
26826
  };
26745
- this.once("done", h);
26746
- this.once("fail", h);
26827
+ const onFail = (data) => {
26828
+ this.removeListener("done", onDone);
26829
+ reject(data.error);
26830
+ };
26831
+ this.once("done", onDone);
26832
+ this.once("fail", onFail);
26747
26833
  });
26748
26834
  return this.Promise.resolve();
26749
26835
  }
@@ -26831,7 +26917,15 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26831
26917
  * @returns instance self
26832
26918
  */
26833
26919
  pause() {
26834
- this._itemQ.unshift(this._wrap(ItemQueue.pauseItem));
26920
+ if (this._processing || !this._pending.isEmpty) this._itemQ.unshift({
26921
+ item: void 0,
26922
+ _control: PAUSE_CALL
26923
+ });
26924
+ else {
26925
+ this._pause = true;
26926
+ this._pauseSeq++;
26927
+ process.nextTick(() => this.emit("pause"));
26928
+ }
26835
26929
  return this;
26836
26930
  }
26837
26931
  /**
@@ -26854,7 +26948,10 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26854
26948
  * @returns instance self
26855
26949
  */
26856
26950
  resume() {
26951
+ this._itemQ = this._itemQ.filter((x) => x._control !== PAUSE_CALL);
26952
+ const pauseSeq = this._pauseSeq;
26857
26953
  process.nextTick(() => {
26954
+ if (pauseSeq !== this._pauseSeq) return;
26858
26955
  this.unpause();
26859
26956
  if (this._itemQ.length === 0) this._itemQ.push(this._wrap(RESUME_ITEM));
26860
26957
  this._process();
@@ -26872,7 +26969,7 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26872
26969
  return this.resume();
26873
26970
  }
26874
26971
  _wrap(item, stopOnError) {
26875
- if (typeof item === "symbol") return {
26972
+ if (item === PAUSE_ITEM || item === RESUME_ITEM) return {
26876
26973
  item: void 0,
26877
26974
  _control: item
26878
26975
  };
@@ -26884,14 +26981,15 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26884
26981
  _emit(evt, data) {
26885
26982
  this.emit(evt, data);
26886
26983
  }
26887
- _handleQueueItemDone(data) {
26984
+ _handleQueueItemDone(data, failed) {
26888
26985
  if (data.id > 0) this._pending.remove(data.id);
26889
26986
  if (this._failed) return;
26890
26987
  if (!data._control && data.id > 0) {
26891
- if (data.error) {
26988
+ if (failed) {
26892
26989
  this._emit("failItem", data);
26893
26990
  if (data.stopOnError !== false && this._stopOnError) {
26894
- this._failed = data.error;
26991
+ this._failed = true;
26992
+ this._failError = data.error;
26895
26993
  this._emit("fail", data);
26896
26994
  return;
26897
26995
  }
@@ -26963,25 +27061,60 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26963
27061
  }
26964
27062
  this._watchTimer = setTimeout(() => this._pendingWatcher(), this._watchPeriod).unref();
26965
27063
  }
27064
+ /**
27065
+ * Fail the item if it doesn't settle within `options.timeout`. The first outcome wins,
27066
+ * so a result that arrives after the timeout is ignored.
27067
+ */
27068
+ _withTimeout(promise, abort) {
27069
+ return new this.Promise((resolve, reject) => {
27070
+ const timer = setTimeout(() => {
27071
+ const error = Object.assign(/* @__PURE__ */ new Error(`item-queue: item timed out after ${this._timeout}ms`), { code: "ETIMEDOUT" });
27072
+ abort.abort(error);
27073
+ reject(error);
27074
+ }, this._timeout);
27075
+ promise.then((res) => {
27076
+ clearTimeout(timer);
27077
+ resolve(res);
27078
+ }, (err) => {
27079
+ clearTimeout(timer);
27080
+ reject(err);
27081
+ });
27082
+ });
27083
+ }
26966
27084
  _setupWatch() {
26967
27085
  if (!this._watchTimer && this._watchTime) process.nextTick(() => this._pendingWatcher());
26968
27086
  }
27087
+ /**
27088
+ * A failed queue never runs queued items. Report each one as a failed item
27089
+ * with the queue's error, so callers waiting on a `failItem` don't hang.
27090
+ */
27091
+ _failQueued() {
27092
+ const queued = this._itemQ;
27093
+ this._itemQ = [];
27094
+ for (const wrapped of queued) if (!wrapped._control) this._emit("failItem", {
27095
+ id: this._id++,
27096
+ error: this._failError,
27097
+ ...wrapped
27098
+ });
27099
+ return 0;
27100
+ }
26969
27101
  _process() {
26970
27102
  if (this._startTime === void 0) this._startTime = Date.now();
27103
+ if (this._failed) return this._failQueued();
26971
27104
  if (this._processing || this._pause || this._itemQ.length === 0) return 0;
26972
27105
  this._processing = true;
26973
27106
  let count = 0;
26974
27107
  let i = this._pending.count;
26975
27108
  for (; this._itemQ.length > 0 && i < this._concurrency; i++) {
26976
27109
  const wrapped = this._itemQ.shift();
26977
- if (wrapped._control === PAUSE_ITEM) {
27110
+ if (wrapped._control === PAUSE_ITEM || wrapped._control === PAUSE_CALL) {
26978
27111
  this._pause = true;
26979
27112
  if (this._pending.isEmpty) process.nextTick(() => {
26980
27113
  this._handleQueueItemDone({
26981
27114
  id: 0,
26982
27115
  item: void 0,
26983
27116
  _control: NOOP_ITEM
26984
- });
27117
+ }, false);
26985
27118
  });
26986
27119
  break;
26987
27120
  }
@@ -26989,12 +27122,16 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
26989
27122
  const id = this._id++;
26990
27123
  let promise;
26991
27124
  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);
27125
+ else {
27126
+ const abort = this._timeout ? new AbortController() : void 0;
27127
+ try {
27128
+ const res = this._processItem(wrapped.item, id, abort === null || abort === void 0 ? void 0 : abort.signal);
27129
+ if (res && res.then) promise = res;
27130
+ else promise = this.Promise.resolve(res);
27131
+ } catch (err) {
27132
+ promise = this.Promise.reject(err);
27133
+ }
27134
+ if (abort) promise = this._withTimeout(promise, abort);
26998
27135
  }
26999
27136
  this._pending.add(id, {
27000
27137
  item: wrapped.item,
@@ -27002,12 +27139,12 @@ var ItemQueue = class ItemQueue extends EventEmitter$1 {
27002
27139
  id,
27003
27140
  res,
27004
27141
  ...wrapped
27005
- }), (error) => {
27142
+ }, false), (error) => {
27006
27143
  this._handleQueueItemDone({
27007
27144
  id,
27008
27145
  error,
27009
27146
  ...wrapped
27010
- });
27147
+ }, true);
27011
27148
  })
27012
27149
  });
27013
27150
  }
@@ -27192,7 +27329,7 @@ function makeExtrasData(file, fullFile, path, stat, files, options) {
27192
27329
  * @param extras
27193
27330
  */
27194
27331
  function addResult(result, options, extras) {
27195
- const group = (options.grouping && typeof result === "string" ? result : result && result.group) || "files";
27332
+ const group = options.grouping && (typeof result === "string" ? result : result.group) || "files";
27196
27333
  if (!options.result[group]) options.result[group] = [];
27197
27334
  if (result.formatName !== void 0) options.result[group].push(result.formatName);
27198
27335
  else options.result[group].push(options.prependCwd ? extras.fullFile : extras.dirFile);
@@ -27234,10 +27371,10 @@ function processFile(options, extras) {
27234
27371
  }
27235
27372
  return false;
27236
27373
  }
27237
- function acceptEntry(options, entry, path, gitignore) {
27374
+ function acceptEntry(options, entry, path, gitignore, fsPath) {
27238
27375
  const isDirectory = entry.isDirectory();
27239
27376
  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;
27377
+ if (gitignore === null || gitignore === void 0 ? void 0 : gitignore.ignores(Path.resolve(options.dir, fsPath, entry.name), isDirectory)) return false;
27241
27378
  if (options.prefilter && !options.prefilter(entry.name, path, entry)) return false;
27242
27379
  if (isDirectory) return true;
27243
27380
  if (options.ignoreExt.length || options.filterExt.length) {
@@ -27280,7 +27417,7 @@ function releaseDirectory(options) {
27280
27417
  * @param level
27281
27418
  * @returns
27282
27419
  */
27283
- async function walk(path, options, level = 0, parentRules) {
27420
+ async function walk(path, options, level = 0, parentRules, fsPath = path) {
27284
27421
  let promises = [];
27285
27422
  let hasSlot = false;
27286
27423
  try {
@@ -27290,7 +27427,9 @@ async function walk(path, options, level = 0, parentRules) {
27290
27427
  hasSlot = true;
27291
27428
  }
27292
27429
  if (options._stopped) return void 0;
27293
- const dir = Path.join(options.dir, path);
27430
+ const dir = Path.join(options.dir, fsPath);
27431
+ const posixSep = options._sep === "/";
27432
+ const outDir = posixSep ? dir : join2(options._sep, options._outCwd, path);
27294
27433
  const gitignore = options.gitignore ? await GitignoreRules.load(Path.resolve(dir), options.gitignore, parentRules) : void 0;
27295
27434
  let files = await asyncReaddir(dir, options.readdirOpts);
27296
27435
  if (options.sortFiles) {
@@ -27301,16 +27440,16 @@ async function walk(path, options, level = 0, parentRules) {
27301
27440
  const extrasFiles = options.fullStat && options._earlyFilter && (options.filter || options.filterDir) ? files.map((entry) => entry.name) : files;
27302
27441
  for (let ix = 0; !options._stopped && ix < files.length; ix++) {
27303
27442
  const file = files[ix];
27304
- if (options._earlyFilter && !acceptEntry(options, file, path, gitignore)) continue;
27443
+ if (options._earlyFilter && !acceptEntry(options, file, path, gitignore, fsPath)) continue;
27305
27444
  let extras;
27306
27445
  if (options.fullStat) {
27307
27446
  const name = options._earlyFilter ? file.name : file;
27308
- const fullFile = join2(options._sep, dir, name);
27309
- const stat = await asyncLStat(fullFile);
27447
+ const fullFile = join2(options._sep, outDir, name);
27448
+ const stat = await asyncLStat(posixSep ? fullFile : join2("/", dir, name));
27310
27449
  if (options._stopped) break;
27311
27450
  extras = makeExtrasData(name, fullFile, path, stat, extrasFiles, options);
27312
27451
  } else {
27313
- const fullFile = join2(options._sep, dir, file.name);
27452
+ const fullFile = join2(options._sep, outDir, file.name);
27314
27453
  extras = makeExtrasData(file.name, fullFile, path, file, files, options);
27315
27454
  }
27316
27455
  if (extras.stat.isDirectory()) dirs.push(extras);
@@ -27328,7 +27467,8 @@ async function walk(path, options, level = 0, parentRules) {
27328
27467
  break;
27329
27468
  }
27330
27469
  if (!flags.skip && level < options.maxLevel) {
27331
- const walkP = walk(extras.dirFile, options, level + 1, gitignore);
27470
+ const childFsPath = posixSep ? extras.dirFile : join2("/", fsPath, extras.file);
27471
+ const walkP = walk(extras.dirFile, options, level + 1, gitignore, childFsPath);
27332
27472
  if (options.concurrency > 1) {
27333
27473
  promises.push(walkP);
27334
27474
  if (promises.length >= options.concurrency) {
@@ -27357,8 +27497,15 @@ async function walk(path, options, level = 0, parentRules) {
27357
27497
  * @returns
27358
27498
  */
27359
27499
  function makeOptions(opts) {
27360
- const options = typeof opts === "string" ? { cwd: opts } : opts;
27500
+ const input = typeof opts === "string" ? { cwd: opts } : opts;
27501
+ const options = {};
27502
+ for (const key of Object.keys(input)) if (input[key] !== void 0) options[key] = input[key];
27361
27503
  if (options.prefilter && options.fullStat === false) throw new TypeError("prefilter requires fullStat: true");
27504
+ for (const name of [
27505
+ "filter",
27506
+ "filterDir",
27507
+ "prefilter"
27508
+ ]) if (Object.prototype.toString.call(options[name]) === "[object AsyncFunction]") throw new TypeError(`${name} must return a result synchronously, not a promise`);
27362
27509
  const sep = options.pathSep || Path.posix.sep;
27363
27510
  let cwd = options.cwd || options.dir || process.cwd();
27364
27511
  if (!options.hasOwnProperty("pathSep") && cwd.includes("\\")) cwd = cwd.replace(/\\/g, "/");
@@ -27380,6 +27527,7 @@ function makeOptions(opts) {
27380
27527
  grouping: void 0
27381
27528
  }, options, {
27382
27529
  dir: cwd,
27530
+ _outCwd: Path.join(cwd),
27383
27531
  fullStat: options.fullStat === void 0 ? true : options.fullStat,
27384
27532
  result: Object.create(null),
27385
27533
  ignoreExt: [].concat(options.ignoreExt).map(cleanExt).filter((x) => x),
@@ -27469,6 +27617,8 @@ const commaPattern = /\\,/g;
27469
27617
  const periodPattern = /\\\./g;
27470
27618
  const EXPANSION_MAX = 1e5;
27471
27619
  const EXPANSION_MAX_LENGTH = 4e6;
27620
+ const EXPANSION_MAX_DEPTH = 1e3;
27621
+ const EXPANSION_MAX_REWRITES = 1e3;
27472
27622
  function numeric(str) {
27473
27623
  return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
27474
27624
  }
@@ -27478,32 +27628,43 @@ function escapeBraces(str) {
27478
27628
  function unescapeBraces(str) {
27479
27629
  return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
27480
27630
  }
27631
+ function pushAll(target, items) {
27632
+ for (let i = 0; i < items.length; i++) target.push(items[i]);
27633
+ }
27481
27634
  /**
27482
27635
  * Basically just str.split(","), but handling cases
27483
27636
  * where we have nested braced sections, which should be
27484
27637
  * treated as individual members, like {a,{b,c},d}
27485
27638
  */
27486
27639
  function parseCommaParts(str) {
27487
- if (!str) return [""];
27488
27640
  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;
27641
+ let carry = "";
27642
+ for (;;) {
27643
+ const m = balanced("{", "}", str);
27644
+ if (!m) {
27645
+ const tail = str.split(",");
27646
+ tail[0] = carry + tail[0];
27647
+ pushAll(parts, tail);
27648
+ return parts;
27649
+ }
27650
+ const { pre, body, post } = m;
27651
+ const p = pre.split(",");
27652
+ p[0] = carry + p[0];
27653
+ p[p.length - 1] += "{" + body + "}";
27654
+ if (!post.length) {
27655
+ pushAll(parts, p);
27656
+ return parts;
27657
+ }
27658
+ carry = p.pop();
27659
+ pushAll(parts, p);
27660
+ str = post;
27661
+ }
27501
27662
  }
27502
27663
  function expand(str, options = {}) {
27503
27664
  if (!str) return [];
27504
- const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH } = options;
27665
+ const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES } = options;
27505
27666
  if (str.slice(0, 2) === "{}") str = "\\{\\}" + str.slice(2);
27506
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
27667
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
27507
27668
  }
27508
27669
  function embrace(str) {
27509
27670
  return "{" + str + "}";
@@ -27569,8 +27730,10 @@ function expandSequence(body, isAlphaSequence, max, maxLength) {
27569
27730
  }
27570
27731
  return N;
27571
27732
  }
27572
- function expand_(str, max, maxLength, isTop) {
27733
+ function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
27734
+ if (depth > maxDepth) return [str];
27573
27735
  let acc = [""];
27736
+ let rewrites = 0;
27574
27737
  let dropEmpties = false;
27575
27738
  let firstGroup = true;
27576
27739
  for (;;) {
@@ -27589,7 +27752,8 @@ function expand_(str, max, maxLength, isTop) {
27589
27752
  const isSequence = isNumericSequence || isAlphaSequence;
27590
27753
  const isOptions = m.body.indexOf(",") >= 0;
27591
27754
  if (!isSequence && !isOptions) {
27592
- if (m.post.match(/,(?!,).*\}/)) {
27755
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
27756
+ rewrites++;
27593
27757
  str = m.pre + "{" + m.body + escClose + m.post;
27594
27758
  isTop = true;
27595
27759
  continue;
@@ -27605,7 +27769,7 @@ function expand_(str, max, maxLength, isTop) {
27605
27769
  else {
27606
27770
  let n = parseCommaParts(m.body);
27607
27771
  if (n.length === 1 && n[0] !== void 0) {
27608
- n = expand_(n[0], max, maxLength, false).map(embrace);
27772
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
27609
27773
  /* c8 ignore start */
27610
27774
  if (n.length === 1) {
27611
27775
  acc = combine(acc, pre + n[0], [""], max, maxLength, dropEmpties && !m.post.length);
@@ -27619,7 +27783,7 @@ function expand_(str, max, maxLength, isTop) {
27619
27783
  values = [];
27620
27784
  let valuesLength = 0;
27621
27785
  outer: for (let j = 0; j < n.length; j++) {
27622
- const expanded = expand_(n[j], max, maxLength, false);
27786
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
27623
27787
  for (let k = 0; k < expanded.length; k++) {
27624
27788
  const v = expanded[k];
27625
27789
  if (dropsEmpties && !v) continue;
@@ -29724,14 +29888,14 @@ var PackageRef = class {
29724
29888
  * @param ref package string reference
29725
29889
  */
29726
29890
  parseRef(ref) {
29727
- const parts = ref.split(":");
29728
- if (parts.length > 1) {
29729
- const tmp = parts[0].trim();
29891
+ const colonIx = ref.indexOf(":");
29892
+ if (colonIx >= 0) {
29893
+ const tmp = ref.substring(0, colonIx).trim();
29730
29894
  if (tmp === "id" || tmp === "path" || tmp === "name") this.type = tmp;
29731
29895
  else throw new Error(`package ref '${ref}' has unknown type '${tmp}' - must be 'id', 'path', or 'name'`);
29732
- this.value = parts[1].trim();
29896
+ this.value = ref.substring(colonIx + 1).trim();
29733
29897
  } else {
29734
- this.value = parts[0].trim();
29898
+ this.value = ref.trim();
29735
29899
  if (this.value.lastIndexOf("@") > 0) this.type = "id";
29736
29900
  else this.type = "name";
29737
29901
  }
@@ -29745,6 +29909,7 @@ var PackageRef = class {
29745
29909
  } else if (this.type === "path") this.mm = new Minimatch(this.value);
29746
29910
  }
29747
29911
  match(pkgInfo) {
29912
+ if (this.regex) this.regex.lastIndex = 0;
29748
29913
  if (this.type === "path") return this.regex ? this.regex.exec(pkgInfo.path) !== null : this.mm.match(pkgInfo.path);
29749
29914
  else if (this.type === "id") return this.regex ? this.regex.exec(pkgInfoId(pkgInfo)) !== null : this.value === pkgInfoId(pkgInfo);
29750
29915
  else return this.regex ? this.regex.exec(pkgInfo.name) !== null : this.value === pkgInfo.name;
@@ -29776,6 +29941,24 @@ function resolvePackage(semver, packages, fallbackToFirst = true) {
29776
29941
  if (fallbackToFirst) return packages[0];
29777
29942
  }
29778
29943
  /**
29944
+ * Widen a local dep's semver per `localDepAutoSemver`, same as fyn's getAutoSemver.
29945
+ *
29946
+ * @param semver semver string
29947
+ * @param autoSemver the `localDepAutoSemver` setting
29948
+ * @returns the semver to match local versions with
29949
+ */
29950
+ function localAutoSemver(semver, autoSemver) {
29951
+ if (autoSemver) {
29952
+ const parsedSv = import_semver.default.coerce(semver);
29953
+ if (parsedSv && parsedSv.raw) switch (autoSemver) {
29954
+ case "patch": return `~${parsedSv.raw}`;
29955
+ case "minor": return `^${parsedSv.raw}`;
29956
+ case "major": return "*";
29957
+ }
29958
+ }
29959
+ return semver;
29960
+ }
29961
+ /**
29779
29962
  * Make a package ID from basic info by joining name and version into a single string.
29780
29963
  *
29781
29964
  * @param info package basic info
@@ -29949,6 +30132,7 @@ var FynpoDepGraph = class {
29949
30132
  });
29950
30133
  files.push(scanned);
29951
30134
  }
30135
+ this.packages.byName = Object.create(null);
29952
30136
  const filesByDepth = [...[].concat(...files)].sort((a, b) => {
29953
30137
  const depth = (file) => posixify$2(Path.dirname(file)).split("/").length;
29954
30138
  return depth(a) - depth(b) || a.localeCompare(b);
@@ -30115,16 +30299,17 @@ var FynpoDepGraph = class {
30115
30299
  if (this.checkNoFynLocal(pkgInfo, name)) continue;
30116
30300
  const semId = pkgId$1(name, semver);
30117
30301
  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);
30302
+ if (resolveId === "") continue;
30303
+ let depPkg = resolveId ? byId[resolveId] : void 0;
30304
+ if (!depPkg) {
30305
+ const localSemver = localAutoSemver(semver, this._options.localDepAutoSemver);
30306
+ depPkg = resolvePackage(localSemver, byName[name], !import_semver.default.validRange(localSemver));
30307
+ this.resolvedCache[semId] = depPkg ? pkgInfoId(depPkg) : "";
30308
+ }
30309
+ if (depPkg) this.addDep(pkgInfo, depPkg, section);
30121
30310
  }
30122
30311
  };
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
- };
30312
+ for (const path in byPath) this.getDepData(byPath[path]);
30128
30313
  for (const path in byPath) {
30129
30314
  const pkgInfo = byPath[path];
30130
30315
  const depData = depMapByPath[pkgInfo.path];
@@ -30134,6 +30319,21 @@ var FynpoDepGraph = class {
30134
30319
  }
30135
30320
  }
30136
30321
  /**
30322
+ * Get the dep data of a package, creating an empty one if it doesn't exist yet
30323
+ *
30324
+ * @param pkgInfo package info
30325
+ * @returns dep data
30326
+ */
30327
+ getDepData(pkgInfo) {
30328
+ let depData = this.depMapByPath[pkgInfo.path];
30329
+ if (!depData) depData = this.depMapByPath[pkgInfo.path] = {
30330
+ pkgInfo,
30331
+ localDepsByPath: Object.create(null),
30332
+ dependentsByPath: Object.create(null)
30333
+ };
30334
+ return depData;
30335
+ }
30336
+ /**
30137
30337
  * Add a dependency relation by package paths
30138
30338
  *
30139
30339
  * @param from - path of package depend from
@@ -30198,8 +30398,8 @@ var FynpoDepGraph = class {
30198
30398
  */
30199
30399
  addDep(pkgInfo, depPkg, depSection, indirectSteps) {
30200
30400
  if (pkgInfo.path === depPkg.path) return false;
30201
- const dataPkg = this.depMapByPath[pkgInfo.path];
30202
- const dataDep = this.depMapByPath[depPkg.path];
30401
+ const dataPkg = this.getDepData(pkgInfo);
30402
+ const dataDep = this.getDepData(depPkg);
30203
30403
  if (dataPkg.localDepsByPath[depPkg.path]) return false;
30204
30404
  dataPkg.localDepsByPath[depPkg.path] = {
30205
30405
  name: depPkg.name,
@@ -30367,7 +30567,7 @@ let defaultLog$1 = consoleLog;
30367
30567
  const ERR_MODULE_NOT_FOUND = "ERR_MODULE_NOT_FOUND";
30368
30568
  const ERR_PACKAGE_PATH_NOT_EXPORTED = "ERR_PACKAGE_PATH_NOT_EXPORTED";
30369
30569
  function normalizeOpts(optsOrMsg) {
30370
- if (optsOrMsg === void 0) return {};
30570
+ if (optsOrMsg == null) return {};
30371
30571
  if (optsOrMsg === true || typeof optsOrMsg === "string") return { message: optsOrMsg };
30372
30572
  return optsOrMsg;
30373
30573
  }
@@ -30852,7 +31052,8 @@ const fyntil = {
30852
31052
  const graph = new FynpoDepGraph({
30853
31053
  cwd: fcm.topDir,
30854
31054
  packages: config.packages,
30855
- noFynLocal: config.noFynLocal
31055
+ noFynLocal: config.noFynLocal,
31056
+ localDepAutoSemver: config.localDepAutoSemver
30856
31057
  });
30857
31058
  await graph.resolve();
30858
31059
  return fyntil.fynpoConfig = {
@@ -30942,14 +31143,15 @@ const fyntil = {
30942
31143
  if (!Path.isAbsolute(rel) && !rel.startsWith(".")) return `.${Path.sep}${rel}`;
30943
31144
  return shouldPosixify ? posixify$2(rel) : rel;
30944
31145
  },
30945
- async readPkgJson(dirOrFile, keepRaw = false, packageFyn = false) {
31146
+ /** @param pkgJsonStr - package.json's content, when the caller has it already */
31147
+ async readPkgJson(dirOrFile, keepRaw = false, packageFyn = false, pkgJsonStr) {
30946
31148
  const dir = !dirOrFile.endsWith(".json") ? dirOrFile : Path.dirname(dirOrFile);
30947
31149
  const files = ["package.json", packageFyn && "package-fyn.json"].filter(Boolean);
30948
31150
  const finalJson = {};
30949
31151
  for (const fname of files) {
30950
31152
  const file = Path.join(dir, fname);
30951
31153
  try {
30952
- const str = await file_ops_default.readFile(file, "utf8");
31154
+ const str = fname === "package.json" && pkgJsonStr !== void 0 ? pkgJsonStr : await file_ops_default.readFile(file, "utf8");
30953
31155
  const json = JSON.parse(str.trim());
30954
31156
  merge(finalJson, json);
30955
31157
  if (keepRaw && fname !== "package-fyn.json") finalJson[PACKAGE_RAW_INFO] = {
@@ -35966,15 +36168,114 @@ var To = (s) => {
35966
36168
  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
36169
  };
35968
36170
 
36171
+ //#endregion
36172
+ //#region lib/util/fs-worker-pool.ts
36173
+ /** One worker less than the CPUs, for the main thread, and at most 8 */
36174
+ const POOL_SIZE = Math.max(1, Math.min(8, os.availableParallelism() - 1));
36175
+ /**
36176
+ * A downloaded Buffer is often a view into a much bigger ArrayBuffer, and postMessage copies the
36177
+ * whole ArrayBuffer. So the message gets a copy of just the view, transferred. The caller's
36178
+ * Buffer stays usable.
36179
+ */
36180
+ function postJob(worker, id, { op, job }) {
36181
+ const { data } = job;
36182
+ if (!data) {
36183
+ worker.postMessage({
36184
+ id,
36185
+ op,
36186
+ job
36187
+ });
36188
+ return;
36189
+ }
36190
+ const copy = new Uint8Array(data.byteLength);
36191
+ copy.set(data);
36192
+ worker.postMessage({
36193
+ id,
36194
+ op,
36195
+ job: {
36196
+ ...job,
36197
+ data: copy
36198
+ }
36199
+ }, [copy.buffer]);
36200
+ }
36201
+ var FsWorkerPool = class {
36202
+ _file;
36203
+ _workers = [];
36204
+ _queue = [];
36205
+ _nextId = 0;
36206
+ constructor(file) {
36207
+ this._file = file;
36208
+ }
36209
+ run(op, job) {
36210
+ return new Promise((resolve, reject) => {
36211
+ this._queue.push({
36212
+ job: {
36213
+ op,
36214
+ job
36215
+ },
36216
+ resolve,
36217
+ reject
36218
+ });
36219
+ this._dispatch();
36220
+ });
36221
+ }
36222
+ _dispatch() {
36223
+ while (this._queue.length > 0) {
36224
+ let pw = this._workers.find((w) => !w.busy);
36225
+ if (!pw) {
36226
+ if (this._workers.length >= POOL_SIZE) return;
36227
+ pw = this._start();
36228
+ }
36229
+ const pending = this._queue.shift();
36230
+ pw.busy = pending;
36231
+ pw.worker.ref();
36232
+ postJob(pw.worker, this._nextId++, pending.job);
36233
+ }
36234
+ }
36235
+ _start() {
36236
+ const pw = { worker: new Worker(this._file) };
36237
+ pw.worker.on("message", (msg) => {
36238
+ const pending = pw.busy;
36239
+ pw.busy = void 0;
36240
+ pw.worker.unref();
36241
+ if (msg.error) pending.reject(Object.assign(new Error(msg.error.message), msg.error));
36242
+ else pending.resolve(msg.result);
36243
+ this._dispatch();
36244
+ });
36245
+ pw.worker.on("error", (err) => {
36246
+ this._workers = this._workers.filter((w) => w !== pw);
36247
+ pw.busy?.reject(err);
36248
+ pw.busy = void 0;
36249
+ this._dispatch();
36250
+ });
36251
+ this._workers.push(pw);
36252
+ return pw;
36253
+ }
36254
+ };
36255
+ let pool;
36256
+ /**
36257
+ * The pool, or undefined when there's no built worker next to this module, which is the case
36258
+ * when running from source. Callers then do the work in-thread.
36259
+ */
36260
+ function getFsWorkerPool() {
36261
+ if (pool === void 0) {
36262
+ const file = fileURLToPath(new URL("./fs-worker.mjs", import.meta.url));
36263
+ pool = Br.existsSync(file) && new FsWorkerPool(file);
36264
+ logger$1.debug(pool ? `fs workers: up to ${POOL_SIZE} from ${file}` : "fs workers: none built, working in-thread");
36265
+ }
36266
+ return pool || void 0;
36267
+ }
36268
+
35969
36269
  //#endregion
35970
36270
  //#region lib/pkg-dist-extractor.ts
35971
36271
  const { retry, missPipe: missPipe$2 } = fyntil;
36272
+ const isStoreJob = (result) => typeof result?.store === "function";
35972
36273
  var PkgDistExtractor = class {
35973
36274
  _promiseQ;
35974
36275
  _fyn;
35975
36276
  constructor(options) {
35976
36277
  this._promiseQ = new promise_queue_default({
35977
- concurrency: 4,
36278
+ concurrency: options.fyn.extractConcurrency || Math.min(4, POOL_SIZE + 1),
35978
36279
  stopOnError: true,
35979
36280
  processItem: (x, id) => this.processItem(x, id)
35980
36281
  });
@@ -36024,6 +36325,7 @@ var PkgDistExtractor = class {
36024
36325
  const { pkg } = data;
36025
36326
  const promotedOpt = defaults$1({ promoted }, pick(pkg, "promoted"));
36026
36327
  const fullOutDir = this._fyn.getInstalledPkgDir(pkg.name, pkg.version, promotedOpt);
36328
+ let placed;
36027
36329
  if (pkg.extracted && pkg.extracted === fullOutDir) {
36028
36330
  logger$1.debug(`package ${pkg.name} ${pkg.version} has already been extracted to ${pkg.extracted}`);
36029
36331
  if (this._fyn.isNormalLayout && pkg.promoted && !promotedOpt.promoted) await this.movePromotedPkgFromFV(pkg, fullOutDir);
@@ -36036,18 +36338,24 @@ var PkgDistExtractor = class {
36036
36338
  }
36037
36339
  return json;
36038
36340
  }
36039
- await this._fyn.createPkgOutDir(fullOutDir);
36040
- const result = data.result;
36341
+ const job = data.result;
36342
+ if (isStoreJob(job) && !await job.store(!data.listener && !data.deferred)) {
36343
+ data.deferred = true;
36344
+ this._promiseQ.addItem(data);
36345
+ return;
36346
+ }
36347
+ const result = isStoreJob(job) ? job.integrity : job;
36041
36348
  let act;
36042
36349
  let retrieve;
36043
36350
  if (typeof result === "string") {
36044
36351
  act = "hardlink";
36045
- retrieve = () => {
36046
- return this._fyn.central.replicate(result, fullOutDir);
36352
+ retrieve = async () => {
36353
+ placed = await this._fyn.central.replicate(result, fullOutDir, () => this._fyn.createPkgOutDir(fullOutDir));
36047
36354
  };
36048
36355
  } else {
36049
36356
  act = "extract";
36050
- retrieve = () => {
36357
+ retrieve = async () => {
36358
+ await this._fyn.createPkgOutDir(fullOutDir);
36051
36359
  const untarStream = So({
36052
36360
  strip: 1,
36053
36361
  strict: true,
@@ -36063,7 +36371,7 @@ var PkgDistExtractor = class {
36063
36371
  logger$1.updateItem(LOAD_PACKAGE, `${act}ed ${msg}`);
36064
36372
  }
36065
36373
  let retries = 0;
36066
- return retry(() => this._fyn.loadJsonForPkg(pkg, fullOutDir), () => {
36374
+ return retry(() => this._fyn.loadJsonForPkg(pkg, fullOutDir, placed), () => {
36067
36375
  retries++;
36068
36376
  logger$1.warn(`retrying ${retries} reading package.json`, fullOutDir);
36069
36377
  return true;
@@ -38464,7 +38772,7 @@ var require_commonjs$8 = /* @__PURE__ */ __commonJSMin(((exports) => {
38464
38772
  //#region node_modules/brace-expansion/dist/commonjs/index.js
38465
38773
  var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38466
38774
  Object.defineProperty(exports, "__esModule", { value: true });
38467
- exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
38775
+ exports.EXPANSION_MAX_REWRITES = exports.EXPANSION_MAX_DEPTH = exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
38468
38776
  exports.expand = expand;
38469
38777
  const balanced_match_1 = require_commonjs$8();
38470
38778
  const escSlash = "\0SLASH" + Math.random() + "\0";
@@ -38484,6 +38792,8 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38484
38792
  const periodPattern = /\\\./g;
38485
38793
  exports.EXPANSION_MAX = 1e5;
38486
38794
  exports.EXPANSION_MAX_LENGTH = 4e6;
38795
+ exports.EXPANSION_MAX_DEPTH = 1e3;
38796
+ exports.EXPANSION_MAX_REWRITES = 1e3;
38487
38797
  function numeric(str) {
38488
38798
  return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
38489
38799
  }
@@ -38493,32 +38803,43 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38493
38803
  function unescapeBraces(str) {
38494
38804
  return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
38495
38805
  }
38806
+ function pushAll(target, items) {
38807
+ for (let i = 0; i < items.length; i++) target.push(items[i]);
38808
+ }
38496
38809
  /**
38497
38810
  * Basically just str.split(","), but handling cases
38498
38811
  * where we have nested braced sections, which should be
38499
38812
  * treated as individual members, like {a,{b,c},d}
38500
38813
  */
38501
38814
  function parseCommaParts(str) {
38502
- if (!str) return [""];
38503
38815
  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);
38816
+ let carry = "";
38817
+ for (;;) {
38818
+ const m = (0, balanced_match_1.balanced)("{", "}", str);
38819
+ if (!m) {
38820
+ const tail = str.split(",");
38821
+ tail[0] = carry + tail[0];
38822
+ pushAll(parts, tail);
38823
+ return parts;
38824
+ }
38825
+ const { pre, body, post } = m;
38826
+ const p = pre.split(",");
38827
+ p[0] = carry + p[0];
38828
+ p[p.length - 1] += "{" + body + "}";
38829
+ if (!post.length) {
38830
+ pushAll(parts, p);
38831
+ return parts;
38832
+ }
38833
+ carry = p.pop();
38834
+ pushAll(parts, p);
38835
+ str = post;
38513
38836
  }
38514
- parts.push.apply(parts, p);
38515
- return parts;
38516
38837
  }
38517
38838
  function expand(str, options = {}) {
38518
38839
  if (!str) return [];
38519
- const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
38840
+ const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH, maxDepth = exports.EXPANSION_MAX_DEPTH, maxRewrites = exports.EXPANSION_MAX_REWRITES } = options;
38520
38841
  if (str.slice(0, 2) === "{}") str = "\\{\\}" + str.slice(2);
38521
- return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
38842
+ return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
38522
38843
  }
38523
38844
  function embrace(str) {
38524
38845
  return "{" + str + "}";
@@ -38584,8 +38905,10 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38584
38905
  }
38585
38906
  return N;
38586
38907
  }
38587
- function expand_(str, max, maxLength, isTop) {
38908
+ function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
38909
+ if (depth > maxDepth) return [str];
38588
38910
  let acc = [""];
38911
+ let rewrites = 0;
38589
38912
  let dropEmpties = false;
38590
38913
  let firstGroup = true;
38591
38914
  for (;;) {
@@ -38604,7 +38927,8 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38604
38927
  const isSequence = isNumericSequence || isAlphaSequence;
38605
38928
  const isOptions = m.body.indexOf(",") >= 0;
38606
38929
  if (!isSequence && !isOptions) {
38607
- if (m.post.match(/,(?!,).*\}/)) {
38930
+ if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
38931
+ rewrites++;
38608
38932
  str = m.pre + "{" + m.body + escClose + m.post;
38609
38933
  isTop = true;
38610
38934
  continue;
@@ -38620,7 +38944,7 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38620
38944
  else {
38621
38945
  let n = parseCommaParts(m.body);
38622
38946
  if (n.length === 1 && n[0] !== void 0) {
38623
- n = expand_(n[0], max, maxLength, false).map(embrace);
38947
+ n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
38624
38948
  /* c8 ignore start */
38625
38949
  if (n.length === 1) {
38626
38950
  acc = combine(acc, pre + n[0], [""], max, maxLength, dropEmpties && !m.post.length);
@@ -38634,7 +38958,7 @@ var require_commonjs$7 = /* @__PURE__ */ __commonJSMin(((exports) => {
38634
38958
  values = [];
38635
38959
  let valuesLength = 0;
38636
38960
  outer: for (let j = 0; j < n.length; j++) {
38637
- const expanded = expand_(n[j], max, maxLength, false);
38961
+ const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
38638
38962
  for (let k = 0; k < expanded.length; k++) {
38639
38963
  const v = expanded[k];
38640
38964
  if (dropsEmpties && !v) continue;
@@ -45425,7 +45749,7 @@ var require_normalize_data = /* @__PURE__ */ __commonJSMin(((exports, module) =>
45425
45749
  }));
45426
45750
 
45427
45751
  //#endregion
45428
- //#region node_modules/.f/_/isexe/4.0.0/node_modules/isexe/dist/commonjs/index.min.js
45752
+ //#region node_modules/isexe/dist/commonjs/index.min.js
45429
45753
  var require_index_min$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
45430
45754
  var a = (t, e) => () => (e || t((e = { exports: {} }).exports, e), e.exports);
45431
45755
  var _ = a((i) => {
@@ -50375,12 +50699,18 @@ var require_cache_dir$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
50375
50699
  # This file is a cache directory tag created by cacache.
50376
50700
  # For information about cache directory tags, see https://bford.info/cachedir/
50377
50701
  `;
50378
- async function mkdir(cache) {
50379
- await fs$15.mkdir(cache, {
50380
- recursive: true,
50381
- owner: "inherit"
50382
- });
50383
- await writeTag(cache);
50702
+ const made = /* @__PURE__ */ new Map();
50703
+ function mkdir(cache) {
50704
+ let p = made.get(cache);
50705
+ if (!p) {
50706
+ p = fs$15.mkdir(cache, {
50707
+ recursive: true,
50708
+ owner: "inherit"
50709
+ }).then(() => writeTag(cache));
50710
+ made.set(cache, p);
50711
+ p.catch(() => made.delete(cache));
50712
+ }
50713
+ return p;
50384
50714
  }
50385
50715
  async function writeTag(cache) {
50386
50716
  try {
@@ -58967,11 +59297,14 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
58967
59297
  }
58968
59298
  /**
58969
59299
  * @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.
59300
+ * @returns {{response: HttpResponse, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object with a cached response headers and revalidation info.
58971
59301
  */
58972
59302
  _evaluateRequestHitResult(revalidation) {
58973
59303
  return {
58974
- response: { headers: this.responseHeaders() },
59304
+ response: {
59305
+ headers: this.responseHeaders(),
59306
+ status: this._status
59307
+ },
58975
59308
  revalidation
58976
59309
  };
58977
59310
  }
@@ -59015,9 +59348,9 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59015
59348
  * }
59016
59349
  * ```
59017
59350
  * @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:
59351
+ * @returns {{response: HttpResponse|undefined, revalidation: {headers: Record<string, string>, synchronous: boolean}|undefined}} An object containing keys:
59019
59352
  * - 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
59353
+ * - response: HttpResponse Set if you can respond to the client with these cached headers
59021
59354
  */
59022
59355
  evaluateRequest(req) {
59023
59356
  this._assertRequestHasHeaders(req);
@@ -59056,9 +59389,11 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59056
59389
  */
59057
59390
  _varyMatches(req) {
59058
59391
  if (!this._resHeaders.vary) return true;
59059
- if (this._resHeaders.vary === "*") return false;
59060
59392
  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;
59393
+ for (const name of fields) {
59394
+ if (name === "*") return false;
59395
+ if ((Object.prototype.hasOwnProperty.call(req.headers, name) && req.headers[name]) !== (Object.prototype.hasOwnProperty.call(this._reqHeaders, name) && this._reqHeaders[name])) return false;
59396
+ }
59062
59397
  return true;
59063
59398
  }
59064
59399
  /**
@@ -59100,6 +59435,13 @@ var require_http_cache_semantics = /* @__PURE__ */ __commonJSMin(((exports, modu
59100
59435
  return headers;
59101
59436
  }
59102
59437
  /**
59438
+ * Returns the status code of the cached response.
59439
+ * @returns {number} The response status code.
59440
+ */
59441
+ status() {
59442
+ return this._status;
59443
+ }
59444
+ /**
59103
59445
  * Returns the Date header value from the response or the current time if invalid.
59104
59446
  * @returns {number} Timestamp (in milliseconds) representing the Date header or response time.
59105
59447
  */
@@ -62205,8 +62547,14 @@ var require_common = /* @__PURE__ */ __commonJSMin(((exports) => {
62205
62547
  * {@link isInSubnet} when classifying a single address — notably when the
62206
62548
  * address came from untrusted input and the result backs a trust-boundary
62207
62549
  * decision such as an SSRF allow/deny filter.
62550
+ *
62551
+ * An address of one family is never inside a network of the other, so an
62552
+ * `Address4` against an `Address6` (or the reverse) is `false`. To compare
62553
+ * across families, convert first: `Address6.fromAddress4()`, `to4()`, or
62554
+ * `toAddress4Nat64()`.
62208
62555
  */
62209
62556
  function isHostInSubnet(address) {
62557
+ if (this.binaryZeroPad().length !== address.binaryZeroPad().length) return false;
62210
62558
  return this.mask(address.subnetMask) === address.mask();
62211
62559
  }
62212
62560
  /**
@@ -62493,7 +62841,9 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62493
62841
  */
62494
62842
  this.isCorrect = isCorrect4;
62495
62843
  /**
62496
- * Returns true if the given address is in the subnet of the current address
62844
+ * Returns true if the given address is in the subnet of the current address.
62845
+ * An `Address6` is never in the subnet of an `Address4`; convert with
62846
+ * `to4()` or `Address6.fromAddress4()` to compare across families.
62497
62847
  * @returns {boolean}
62498
62848
  */
62499
62849
  this.isInSubnet = common.isInSubnet;
@@ -62503,6 +62853,8 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62503
62853
  * when classifying a single address, so the answer doesn't change with the
62504
62854
  * CIDR suffix the caller happened to write — notably when the address came
62505
62855
  * from untrusted input and the result backs a trust-boundary decision.
62856
+ * An `Address6` is never in the subnet of an `Address4`; convert with
62857
+ * `to4()` or `Address6.fromAddress4()` to compare across families.
62506
62858
  * @returns {boolean}
62507
62859
  */
62508
62860
  this.isHostInSubnet = common.isHostInSubnet;
@@ -62515,6 +62867,8 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62515
62867
  if (this.subnetMask < 0 || this.subnetMask > constants.BITS) throw new address_error_1.AddressError("Invalid subnet mask.");
62516
62868
  address = address.replace(constants.RE_SUBNET_STRING, "");
62517
62869
  }
62870
+ const longest = constants.GROUPS * 4 - 1;
62871
+ if (address.length > longest) throw new address_error_1.AddressError(`IPv4 addresses are at most ${longest} characters.`);
62518
62872
  this.addressMinusSuffix = address;
62519
62873
  this.parsedAddress = this.parse(address);
62520
62874
  }
@@ -62631,15 +62985,20 @@ var require_ipv4 = /* @__PURE__ */ __commonJSMin(((exports) => {
62631
62985
  return Address4.fromHex(integer.toString(16).padStart(8, "0"));
62632
62986
  }
62633
62987
  /**
62634
- * Return an address from in-addr.arpa form
62988
+ * Return an address from in-addr.arpa form: the four octets reversed, with
62989
+ * or without the `.in-addr.arpa` suffix and root dot, in any case. Throws
62990
+ * `AddressError` unless the reversed labels form a valid IPv4 address, so
62991
+ * `fromArpa(x.reverseForm())` round-trips {@link reverseForm}.
62635
62992
  * @param {string} arpaFormAddress - an 'in-addr.arpa' form ipv4 address
62636
62993
  * @returns {Adress4}
62637
62994
  * @example
62638
- * var address = Address4.fromArpa(42.2.0.192.in-addr.arpa.)
62995
+ * var address = Address4.fromArpa('42.2.0.192.in-addr.arpa.')
62639
62996
  * address.correctForm(); // '192.0.2.42'
62640
62997
  */
62641
62998
  static fromArpa(arpaFormAddress) {
62642
- const address = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.$/, "").split(".").reverse().join(".");
62999
+ const longest = constants.GROUPS * 4 - 1 + 3 + 14;
63000
+ if (arpaFormAddress.length > longest) throw new address_error_1.AddressError(`in-addr.arpa names are at most ${longest} characters.`);
63001
+ const address = arpaFormAddress.replace(/(\.in-addr\.arpa)?\.?$/i, "").split(".").reverse().join(".");
62643
63002
  return new Address4(address);
62644
63003
  }
62645
63004
  /**
@@ -63420,7 +63779,9 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63420
63779
  this.v4 = false;
63421
63780
  this.zone = "";
63422
63781
  /**
63423
- * Returns true if the given address is in the subnet of the current address
63782
+ * Returns true if the given address is in the subnet of the current address.
63783
+ * An `Address4` is never in the subnet of an `Address6`; convert with
63784
+ * `Address6.fromAddress4()` or `to4()` to compare across families.
63424
63785
  * @returns {boolean}
63425
63786
  */
63426
63787
  this.isInSubnet = common.isInSubnet;
@@ -63430,6 +63791,8 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63430
63791
  * when classifying a single address, so the answer doesn't change with the
63431
63792
  * CIDR suffix the caller happened to write — notably when the address came
63432
63793
  * from untrusted input and the result backs a trust-boundary decision.
63794
+ * An `Address4` is never in the subnet of an `Address6`; convert with
63795
+ * `Address6.fromAddress4()` or `to4()` to compare across families.
63433
63796
  * @returns {boolean}
63434
63797
  */
63435
63798
  this.isHostInSubnet = common.isHostInSubnet;
@@ -63455,6 +63818,8 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63455
63818
  this.zone = zone[0];
63456
63819
  address = address.replace(constants6.RE_ZONE_STRING, "");
63457
63820
  }
63821
+ const longest = this.groups * 5 + 5;
63822
+ if (address.length > longest) throw new address_error_1.AddressError(`IPv6 addresses are at most ${longest} characters.`);
63458
63823
  this.addressMinusSuffix = address;
63459
63824
  this.parsedAddress = this.parse(this.addressMinusSuffix);
63460
63825
  }
@@ -63640,7 +64005,7 @@ var require_ipv6 = /* @__PURE__ */ __commonJSMin(((exports) => {
63640
64005
  * Address6.fromArpa('8.b.d.0.1.0.0.2.ip6.arpa.').networkForm(); // '2001:db8::/32'
63641
64006
  */
63642
64007
  static fromArpa(arpaFormAddress) {
63643
- const nibbles = arpaFormAddress.replace(/(\.ip6\.arpa)?\.?$/, "");
64008
+ const nibbles = arpaFormAddress.replace(/(\.ip6\.arpa)?\.?$/i, "");
63644
64009
  if (!/^[0-9a-f](\.[0-9a-f]){0,31}$/i.test(nibbles)) throw new address_error_1.AddressError("Invalid 'ip6.arpa' form.");
63645
64010
  const reversed = nibbles.split(".").reverse();
63646
64011
  const subnetMask = reversed.length * 4;
@@ -65801,6 +66166,7 @@ var require_lib$14 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
65801
66166
  });
65802
66167
  if (agentCache.has(cacheKey)) return agentCache.get(cacheKey);
65803
66168
  const newAgent = new Agent(normalizedOptions);
66169
+ newAgent.protocol = url.protocol;
65804
66170
  agentCache.set(cacheKey, newAgent);
65805
66171
  return newAgent;
65806
66172
  };
@@ -65892,7 +66258,7 @@ var require_package$5 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
65892
66258
  "publish": "true"
65893
66259
  },
65894
66260
  "_fyn": {},
65895
- "_from": "make-fetch-happen@^16.0.0",
66261
+ "_from": "make-fetch-happen@^16.0.1",
65896
66262
  "_id": "make-fetch-happen@16.0.1"
65897
66263
  };
65898
66264
  }));
@@ -86762,7 +87128,7 @@ var require_package_extensions = /* @__PURE__ */ __commonJSMin(((exports, module
86762
87128
  var require_npm_extension = /* @__PURE__ */ __commonJSMin(((exports, module) => {
86763
87129
  const { resolve: resolve$9, sep: sep$3 } = __require("node:path");
86764
87130
  const { readFileSync, existsSync: existsSync$3 } = __require("node:fs");
86765
- const { pathToFileURL } = __require("node:url");
87131
+ const { pathToFileURL: pathToFileURL$1 } = __require("node:url");
86766
87132
  const { isDeepStrictEqual } = __require("node:util");
86767
87133
  const { log } = require_lib$38();
86768
87134
  const ssri = require_lib$23();
@@ -86831,7 +87197,7 @@ var require_npm_extension = /* @__PURE__ */ __commonJSMin(((exports, module) =>
86831
87197
  async load() {
86832
87198
  if (!this.present) return;
86833
87199
  let mod;
86834
- if (this.format === "mjs") mod = await import(`${pathToFileURL(this.path).href}?h=${this.hash}`);
87200
+ if (this.format === "mjs") mod = await import(`${pathToFileURL$1(this.path).href}?h=${this.hash}`);
86835
87201
  else {
86836
87202
  delete __require.cache[__require.resolve(this.path)];
86837
87203
  mod = __require(this.path);
@@ -92175,10 +92541,11 @@ async function copyFile(srcFp, destFp) {
92175
92541
  *
92176
92542
  * @param {*} srcFp - source file path
92177
92543
  * @param {*} destFp - destination file path
92544
+ * @param {*} noCopy - fail instead of falling back to a copy (always fails on macOS)
92178
92545
  * @returns
92179
92546
  */
92180
- async function cloneFile(srcFp, destFp) {
92181
- return copyNew(srcFp, destFp, fs.constants.COPYFILE_FICLONE);
92547
+ async function cloneFile(srcFp, destFp, noCopy = false) {
92548
+ return copyNew(srcFp, destFp, noCopy ? fs.constants.COPYFILE_FICLONE_FORCE : fs.constants.COPYFILE_FICLONE);
92182
92549
  }
92183
92550
  async function prepDestDir(dest) {
92184
92551
  const statDest = await tryCatch(() => file_ops_default.lstat(dest));
@@ -92679,6 +93046,15 @@ function getBucketPath(cache, key) {
92679
93046
  return Path.join(cache, `index-v${indexV}`, hashed.slice(0, 2), hashed.slice(2, 4), hashed.slice(4));
92680
93047
  }
92681
93048
  /**
93049
+ * Get the content file path for an integrity that cacache matched, such as the `sri` from
93050
+ * `get.hasContent`. Same layout as cacache: the algorithm, then the hex digest in segments.
93051
+ */
93052
+ function getContentPath(cache, sri) {
93053
+ const hex = sri.hexDigest();
93054
+ const contentV = import_package.default["cache-version"].content;
93055
+ return Path.join(cache, `content-v${contentV}`, sri.algorithm, hex.slice(0, 2), hex.slice(2, 4), hex.slice(4));
93056
+ }
93057
+ /**
92682
93058
  * Update cache entry refresh timestamp by modifying bucket file mtime.
92683
93059
  * Does not modify file contents, only filesystem metadata.
92684
93060
  * Errors are silently ignored (cache refresh is non-critical).
@@ -92714,7 +93090,7 @@ async function getCacheInfoWithRefreshTime(cache, key) {
92714
93090
  //#endregion
92715
93091
  //#region lib/util/trimmed-packument.ts
92716
93092
  /** Bump when the kept fields change, so files from older versions are rebuilt. */
92717
- const TRIMMED_FORMAT = 1;
93093
+ const TRIMMED_FORMAT = 2;
92718
93094
  /**
92719
93095
  * The version fields fyn reads from a registry packument. Code that starts reading another
92720
93096
  * field from `meta.versions[v]` must add it here and bump TRIMMED_FORMAT.
@@ -92731,6 +93107,7 @@ const KEEP_VERSION_FIELDS = [
92731
93107
  "cpu",
92732
93108
  "deprecated",
92733
93109
  "_hasShrinkwrap",
93110
+ "_shrinkwrap",
92734
93111
  "fyn"
92735
93112
  ];
92736
93113
  /** Only the install hooks are read from `scripts`. */
@@ -92762,7 +93139,7 @@ function trimPackument(packument) {
92762
93139
  versions[version] = trimmed;
92763
93140
  }
92764
93141
  return {
92765
- $format: 1,
93142
+ $format: 2,
92766
93143
  name: packument.name,
92767
93144
  "dist-tags": packument["dist-tags"],
92768
93145
  time: packument.time,
@@ -92775,33 +93152,68 @@ function trimmedPackumentFile(dir, packumentUrl) {
92775
93152
  return Path.join(dir, `${hash}.json`);
92776
93153
  }
92777
93154
  /**
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.
93155
+ * Read a trimmed packument, its refresh time, and its validators. Resolves undefined when the
93156
+ * file is missing, unreadable, or from another format, so the caller falls back to the full copy.
92780
93157
  */
92781
93158
  async function readTrimmedPackument(file) {
92782
93159
  try {
92783
93160
  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,
93161
+ const parsed = parseTrimmed(data.toString());
93162
+ return parsed && {
93163
+ ...parsed,
92789
93164
  refreshTime: stat.mtimeMs
92790
93165
  };
92791
93166
  } catch {
92792
93167
  return;
92793
93168
  }
92794
93169
  }
93170
+ /** Parse a trimmed copy's JSON into the packument and its validators. Undefined for another format. */
93171
+ function parseTrimmed(json) {
93172
+ const packument = JSON.parse(json);
93173
+ if (packument.$format !== 2) return void 0;
93174
+ const { $etag: etag, $lastModified: lastModified, $corgi: corgi } = packument;
93175
+ delete packument.$format;
93176
+ delete packument.$etag;
93177
+ delete packument.$lastModified;
93178
+ delete packument.$corgi;
93179
+ return {
93180
+ packument,
93181
+ etag,
93182
+ lastModified,
93183
+ corgi
93184
+ };
93185
+ }
93186
+ /** Mark a trimmed packument as refreshed now, after the registry said it hasn't changed */
93187
+ async function touchTrimmedPackument(file) {
93188
+ const now = /* @__PURE__ */ new Date();
93189
+ await fs.promises.utimes(file, now, now).catch(() => void 0);
93190
+ }
92795
93191
  /**
92796
93192
  * Write a trimmed copy of a packument. The write goes to a temp file renamed over the target,
92797
93193
  * 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.
93194
+ * made from an existing cache entry rather than a fresh fetch. Failures are ignored: the next
93195
+ * install fetches the packument again.
92800
93196
  */
92801
- async function writeTrimmedPackument(file, packument, refreshTime) {
93197
+ async function writeTrimmedPackument(file, packument, refreshTime, validators) {
93198
+ let json;
93199
+ try {
93200
+ json = trimmedJson(trimPackument(packument), validators);
93201
+ } catch {
93202
+ return;
93203
+ }
93204
+ await writeTrimmed(file, json, refreshTime);
93205
+ }
93206
+ function trimmedJson(trimmed, validators) {
93207
+ return JSON.stringify({
93208
+ ...trimmed,
93209
+ $etag: validators?.etag,
93210
+ $lastModified: validators?.lastModified,
93211
+ $corgi: validators?.corgi || void 0
93212
+ });
93213
+ }
93214
+ async function writeTrimmed(file, data, refreshTime) {
92802
93215
  const tmp = `${file}.${process.pid}.${Crypto.randomBytes(4).toString("hex")}.tmp`;
92803
93216
  try {
92804
- const data = JSON.stringify(trimPackument(packument));
92805
93217
  try {
92806
93218
  await fs.promises.writeFile(tmp, data);
92807
93219
  } catch (err) {
@@ -92818,9 +93230,160 @@ async function writeTrimmedPackument(file, packument, refreshTime) {
92818
93230
  await fs.promises.rm(tmp, { force: true }).catch(() => void 0);
92819
93231
  }
92820
93232
  }
93233
+ /** Parse a registry response body, as sent with `encoding` */
93234
+ function decodePackument(data, encoding) {
93235
+ const json = encoding === "gzip" || encoding === "x-gzip" || encoding === "deflate" ? en.unzipSync(data) : encoding === "br" ? en.brotliDecompressSync(data) : data;
93236
+ return JSON.parse(Buffer.from(json.buffer, json.byteOffset, json.byteLength).toString());
93237
+ }
93238
+ /**
93239
+ * Decode a packument response and write its trimmed copy. The abbreviated form only flags that
93240
+ * a version has install scripts, and fyn records which ones, so one with any isn't saved. This
93241
+ * is the CPU-heavy part of a packument fetch, so an fs worker runs it when fyn has them built.
93242
+ */
93243
+ async function savePackument(job) {
93244
+ const packument = decodePackument(job.data, job.encoding);
93245
+ if (job.corgi && Object.values(packument.versions || {}).some((v) => v.hasInstallScript)) return { needFull: true };
93246
+ return { json: await saveTrimmed(job.file, packument, job) };
93247
+ }
93248
+ /** Trim a packument and write it with its validators. @returns the JSON written */
93249
+ async function saveTrimmed(file, packument, validators) {
93250
+ const text = trimmedJson(trimPackument(packument), validators);
93251
+ await writeTrimmed(file, text);
93252
+ return text;
93253
+ }
93254
+
93255
+ //#endregion
93256
+ //#region lib/util/registry-get.ts
93257
+ let load;
93258
+ 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]) => ({
93259
+ fetch: mfh.default,
93260
+ getAuth: nrf.default.getAuth
93261
+ }));
93262
+ /** The plain values of `opts`, which is all auth needs and all that can go to a worker */
93263
+ function pickRegistryOpts(opts) {
93264
+ const out = {};
93265
+ for (const [k, v] of Object.entries(opts)) if (v === void 0 || [
93266
+ "string",
93267
+ "number",
93268
+ "boolean"
93269
+ ].includes(typeof v)) out[k] = v;
93270
+ return out;
93271
+ }
93272
+ /** getAuth results, per opts, by authCacheKey */
93273
+ const authCaches = /* @__PURE__ */ new WeakMap();
93274
+ /**
93275
+ * What getAuth's result for `url` depends on. It walks up the URL's path for the longest
93276
+ * `//host/path` that has auth configured, and without one, falls back to the registry's auth.
93277
+ * So it's the longest configured prefix that matches, else the host.
93278
+ */
93279
+ function authCacheKey(url, prefixes) {
93280
+ const m = /^[a-z]+:(\/\/[^/?#]+)([^?#]*)/i.exec(url);
93281
+ if (!m) return void 0;
93282
+ const full = m[1] + m[2];
93283
+ let best = "";
93284
+ for (const p of prefixes) if (p.length > best.length && full.startsWith(p) && (p.endsWith("/") || full.length === p.length || full[p.length] === "/")) best = p;
93285
+ return best || `host ${m[1]}`;
93286
+ }
93287
+ function cachedAuth(getAuth, url, opts) {
93288
+ if (opts.forceAuth) return getAuth(url, opts);
93289
+ let cache = authCaches.get(opts);
93290
+ if (!cache) {
93291
+ cache = {
93292
+ prefixes: Object.keys(opts).map((k) => /^(\/\/.+):(_authToken|_auth|username|_password|certfile|keyfile)$/.exec(k)?.[1]).filter((p) => Boolean(p)),
93293
+ auths: /* @__PURE__ */ new Map()
93294
+ };
93295
+ authCaches.set(opts, cache);
93296
+ }
93297
+ const key = authCacheKey(url, cache.prefixes);
93298
+ if (key === void 0) return getAuth(url, opts);
93299
+ let auth = cache.auths.get(key);
93300
+ if (!auth) cache.auths.set(key, auth = getAuth(url, opts));
93301
+ return auth;
93302
+ }
93303
+ /** Statuses from 400 up throw */
93304
+ async function registryGet(url, extraHeaders, opts) {
93305
+ const { fetch, getAuth } = await loadRegistryFetch();
93306
+ const auth = cachedAuth(getAuth, url, opts);
93307
+ const headers = {
93308
+ "user-agent": `fyn node/${process.version}`,
93309
+ ...extraHeaders
93310
+ };
93311
+ if (auth.token) headers.authorization = `Bearer ${auth.token}`;
93312
+ else if (auth.auth) headers.authorization = `Basic ${auth.auth}`;
93313
+ const res = await fetch(url, {
93314
+ headers,
93315
+ compress: false,
93316
+ cert: auth.cert,
93317
+ key: auth.key,
93318
+ maxSockets: opts.maxSockets,
93319
+ noProxy: process.env.NOPROXY,
93320
+ retry: { retries: 3 },
93321
+ timeout: 3e5
93322
+ });
93323
+ if (res.status >= 400) {
93324
+ res.body.resume();
93325
+ throw Object.assign(/* @__PURE__ */ new Error(`${res.status} ${res.statusText} - GET ${url}`), {
93326
+ code: `E${res.status}`,
93327
+ statusCode: res.status
93328
+ });
93329
+ }
93330
+ return res;
93331
+ }
93332
+ /** npm's abbreviated install metadata, about a third the size of a full packument */
93333
+ const CORGI_DOC = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*";
93334
+ /**
93335
+ * The request headers for a packument, with validators so the registry can answer 304.
93336
+ *
93337
+ * @param full - ask for the full packument, not the abbreviated one
93338
+ */
93339
+ function packumentHeaders(validators, full) {
93340
+ const headers = {
93341
+ accept: full ? "application/json" : CORGI_DOC,
93342
+ "accept-encoding": "gzip"
93343
+ };
93344
+ if (validators?.etag) headers["if-none-match"] = validators.etag;
93345
+ if (validators?.lastModified) headers["if-modified-since"] = validators.lastModified;
93346
+ return headers;
93347
+ }
93348
+ /**
93349
+ * Fetch a packument and write fyn's trimmed copy of it. It starts with the abbreviated form, and
93350
+ * gets the full one when `save` finds that abbreviated isn't enough.
93351
+ *
93352
+ * @param get - does the GET, registryGet with the job's opts by default
93353
+ * @param save - decodes and writes the response; an fs worker can run it
93354
+ */
93355
+ async function fetchPackument(job, get, save = savePackument) {
93356
+ const doGet = get || ((url, headers) => registryGet(url, headers, job.opts || {}));
93357
+ const request = async (headers) => {
93358
+ const res = await doGet(job.url, headers);
93359
+ if (res.status === 304) {
93360
+ res.body?.resume();
93361
+ return;
93362
+ }
93363
+ const header = (name) => res.headers.get(name) || void 0;
93364
+ return {
93365
+ data: await res.buffer(),
93366
+ encoding: header("content-encoding"),
93367
+ etag: header("etag"),
93368
+ lastModified: header("last-modified"),
93369
+ corgi: /vnd\.npm\.install-v1/.test(header("content-type") || ""),
93370
+ file: job.file
93371
+ };
93372
+ };
93373
+ const first = await request(packumentHeaders(job, job.full));
93374
+ if (!first) return { status: 304 };
93375
+ let saved = await save(first);
93376
+ if ("needFull" in saved) saved = await save(await request(packumentHeaders(void 0, true)));
93377
+ if ("needFull" in saved) throw new Error(`no full packument from ${job.url}`);
93378
+ return {
93379
+ status: 200,
93380
+ json: saved.json
93381
+ };
93382
+ }
92821
93383
 
92822
93384
  //#endregion
92823
93385
  //#region lib/util/inflight.ts
93386
+ var import_lib = /* @__PURE__ */ __toESM(require_lib$23(), 1);
92824
93387
  var inflight_default = InflightStore;
92825
93388
 
92826
93389
  //#endregion
@@ -92855,7 +93418,10 @@ var PkgPreper = class {
92855
93418
  };
92856
93419
  return Promise.resolve((0, import_lib$2.default)(treeStub)).then((files) => {
92857
93420
  return Qn(tarOpt, files.map((f) => `./${f}`));
92858
- }).then(() => FsPromises.rename(tmpTarget, target)).then(() => void 0);
93421
+ }).then(() => FsPromises.rename(tmpTarget, target).catch((err) => {
93422
+ if (err.code !== "EXDEV") throw err;
93423
+ return FsPromises.copyFile(tmpTarget, target);
93424
+ })).then(() => void 0);
92859
93425
  });
92860
93426
  });
92861
93427
  }
@@ -92872,7 +93438,7 @@ var PkgPreper = class {
92872
93438
  });
92873
93439
  });
92874
93440
  }).catch((err) => {
92875
- stream.emit("error", err);
93441
+ stream.destroy(err);
92876
93442
  });
92877
93443
  return stream;
92878
93444
  }
@@ -92883,7 +93449,6 @@ var PkgPreper = class {
92883
93449
 
92884
93450
  //#endregion
92885
93451
  //#region node_modules/xsh/dist/util.js
92886
- var import_lib = /* @__PURE__ */ __toESM(require_lib$23(), 1);
92887
93452
  /**
92888
93453
  * Mutable holder for the Promise implementation xsh uses.
92889
93454
  *
@@ -93112,12 +93677,11 @@ const envPath = {
93112
93677
  //#endregion
93113
93678
  //#region node_modules/xsh/dist/mkcmd.js
93114
93679
  /**
93115
- * Make a command string: a single array of strings is joined with `" "`,
93116
- * otherwise all arguments are joined with `" "`.
93680
+ * Make a command string: every argument, string or array of strings,
93681
+ * contributes its words in order, joined with `" "`.
93117
93682
  */
93118
93683
  function mkCmd(...args) {
93119
- const a = args[0];
93120
- return Array.isArray(a) ? a.join(" ") : args.join(" ");
93684
+ return args.flat().join(" ");
93121
93685
  }
93122
93686
 
93123
93687
  //#endregion
@@ -93268,19 +93832,14 @@ const TEN_MB = 10485760;
93268
93832
  const DEFAULT_TIMEOUT_GRACE = 5e3;
93269
93833
  const DEFAULT_LAST_LINES = 10;
93270
93834
  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}`);
93835
+ function createTimeoutError(command, elapsed) {
93836
+ const err = /* @__PURE__ */ new Error(`Command timed out after ${elapsed}ms: ${command}`);
93273
93837
  err.name = "TimeoutError";
93274
- err.context = {
93275
- ...context,
93276
- duration: elapsed
93277
- };
93278
93838
  return err;
93279
93839
  }
93280
- function createAbortError(context) {
93281
- const err = /* @__PURE__ */ new Error(`Command aborted: ${context.command}`);
93840
+ function createAbortError(command) {
93841
+ const err = /* @__PURE__ */ new Error(`Command aborted: ${command}`);
93282
93842
  err.name = "AbortError";
93283
- err.context = context;
93284
93843
  return err;
93285
93844
  }
93286
93845
  function lastLines(text, n) {
@@ -93388,7 +93947,7 @@ var VisualExec = class {
93388
93947
  }
93389
93948
  _extractProgress(line) {
93390
93949
  const cfg = this._progress;
93391
- if (!cfg || !this._onProgress) return;
93950
+ if (!cfg || !(this._onProgress || cfg.format)) return;
93392
93951
  let progress = null;
93393
93952
  if (cfg.extract) progress = cfg.extract(line);
93394
93953
  else if (cfg.pattern) {
@@ -93399,7 +93958,24 @@ var VisualExec = class {
93399
93958
  percent: m.groups.percent ? parseInt(m.groups.percent, 10) : void 0
93400
93959
  };
93401
93960
  }
93402
- if (progress) this._onProgress(progress);
93961
+ if (!progress) return;
93962
+ if (cfg.format) {
93963
+ const text = cfg.format(progress);
93964
+ if (text !== this._progressText) {
93965
+ this._progressText = text;
93966
+ this._renderDigest(this._stdoutDigest);
93967
+ }
93968
+ }
93969
+ if (this._onProgress) this._onProgress(progress);
93970
+ }
93971
+ _renderDigest(item) {
93972
+ const display = item === this._stdoutDigest && this._progressText ? `=== ${this._title}\nstdout ${this._progressText}` : void 0;
93973
+ this._logger.updateItem(item.name, {
93974
+ msg: item.msg,
93975
+ display,
93976
+ _save: false,
93977
+ _render: false
93978
+ });
93403
93979
  }
93404
93980
  _updateDigest(item, buf) {
93405
93981
  const lines = (item.buf + sanitizeForDisplay(buf)).split("\n").map((x) => x && x.trim()).filter((x) => x);
@@ -93420,11 +93996,8 @@ var VisualExec = class {
93420
93996
  msgs = [item.buf];
93421
93997
  } else item.buf = msgs.join("\n");
93422
93998
  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
- });
93999
+ item.msg = msgs.join(chalk.blue.inverse("\\n"));
94000
+ this._renderDigest(item);
93428
94001
  }
93429
94002
  _createDataHandler(stream) {
93430
94003
  return (buf) => {
@@ -93459,6 +94032,9 @@ var VisualExec = class {
93459
94032
  * it defaults to `ExecOutput`, which is what a run without an `onComplete` produces.
93460
94033
  */
93461
94034
  show(child) {
94035
+ return this._show(child, this._command);
94036
+ }
94037
+ _show(child, command) {
93462
94038
  this._stdoutKey = Symbol("visual-exec-stdout");
93463
94039
  this._stderrKey = Symbol("visual-exec-stderr");
93464
94040
  this._rawChild = child.child;
@@ -93477,19 +94053,30 @@ var VisualExec = class {
93477
94053
  name: this._stdoutKey,
93478
94054
  buf: ""
93479
94055
  };
94056
+ this._stdoutDigest = stdoutDigest;
93480
94057
  const stderrDigest = {
93481
94058
  name: this._stderrKey,
93482
94059
  buf: ""
93483
94060
  };
93484
94061
  this._updateStdout = (buf) => this._updateDigest(stdoutDigest, buf);
93485
94062
  this._updateStderr = (buf) => this._updateDigest(stderrDigest, buf);
94063
+ const seen = {
94064
+ stdout: "",
94065
+ stderr: ""
94066
+ };
94067
+ const capture = (stream, data) => {
94068
+ const text = seen[stream] + data;
94069
+ seen[stream] = text.length > MAX_OUTPUT_IN_ERROR ? text.slice(-5e4) : text;
94070
+ };
93486
94071
  this._onStdoutData = (buf) => {
93487
94072
  const data = typeof buf === "string" ? buf : buf.toString();
94073
+ capture("stdout", data);
93488
94074
  this._updateStdout(data);
93489
94075
  this._createDataHandler("stdout")(buf);
93490
94076
  };
93491
94077
  this._onStderrData = (buf) => {
93492
94078
  const data = typeof buf === "string" ? buf : buf.toString();
94079
+ capture("stderr", data);
93493
94080
  this._updateStderr(data);
93494
94081
  this._createDataHandler("stderr")(buf);
93495
94082
  };
@@ -93508,7 +94095,9 @@ var VisualExec = class {
93508
94095
  stderr: ""
93509
94096
  };
93510
94097
  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);
94098
+ try {
94099
+ (_a = this._onComplete) === null || _a === void 0 || _a.call(this, output, exitCode);
94100
+ } catch (_e) {}
93512
94101
  this.logResult(err);
93513
94102
  throw err;
93514
94103
  }).then((output) => {
@@ -93517,6 +94106,20 @@ var VisualExec = class {
93517
94106
  const result = (_a = this._onComplete) === null || _a === void 0 ? void 0 : _a.call(this, output, 0);
93518
94107
  return result !== void 0 ? result : output;
93519
94108
  });
94109
+ const cancelError = (err, duration) => {
94110
+ const output = { ...seen };
94111
+ err.output = output;
94112
+ return enhanceError(err, {
94113
+ exitCode: -1,
94114
+ signal: "SIGTERM",
94115
+ cwd: this._cwd,
94116
+ command,
94117
+ duration,
94118
+ lastLines: lastLines(`${output.stdout}\n${output.stderr}`, DEFAULT_LAST_LINES),
94119
+ stdout: output.stdout,
94120
+ stderr: output.stderr
94121
+ });
94122
+ };
93520
94123
  if (this._timeout) {
93521
94124
  const timeoutPromise = new Promise((_, reject) => {
93522
94125
  timeoutId = setTimeout(() => {
@@ -93530,18 +94133,15 @@ var VisualExec = class {
93530
94133
  if ((_a = this._rawChild) === null || _a === void 0 ? void 0 : _a.pid) this._rawChild.kill("SIGKILL");
93531
94134
  }, this._timeoutGrace)).unref) === null || _d === void 0 || _d.call(_c);
93532
94135
  }
93533
- reject(createTimeoutError({
93534
- command: this._command,
93535
- cwd: this._cwd,
93536
- exitCode: -1,
93537
- signal: "SIGTERM"
93538
- }, this._timeout));
94136
+ reject(cancelError(createTimeoutError(command, this._timeout), this._timeout));
93539
94137
  }, this._timeout);
93540
94138
  });
93541
94139
  execPromise = Promise.race([execPromise, timeoutPromise]);
93542
94140
  }
94141
+ let handleAbort;
93543
94142
  const cleanup = () => {
93544
94143
  if (timeoutId) clearTimeout(timeoutId);
94144
+ if (handleAbort) this._signal.removeEventListener("abort", handleAbort);
93545
94145
  const stream = this._outputStream;
93546
94146
  this._outputStream = void 0;
93547
94147
  if (!(stream === null || stream === void 0 ? void 0 : stream.end)) return;
@@ -93554,15 +94154,9 @@ var VisualExec = class {
93554
94154
  };
93555
94155
  if (this._signal) {
93556
94156
  const abortPromise = new Promise((_, reject) => {
93557
- const handleAbort = () => {
94157
+ handleAbort = () => {
93558
94158
  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
- }));
94159
+ reject(cancelError(createAbortError(command), Date.now() - startTime));
93566
94160
  };
93567
94161
  if (this._signal.aborted) handleAbort();
93568
94162
  else this._signal.addEventListener("abort", handleAbort);
@@ -93667,7 +94261,7 @@ var VisualExec = class {
93667
94261
  };
93668
94262
  return Promise.reject(enhanceError(err, context));
93669
94263
  });
93670
- return this.show(child);
94264
+ return this._show(child, cmd);
93671
94265
  }
93672
94266
  };
93673
94267
 
@@ -93711,7 +94305,8 @@ function renameFromObj(obj, rename) {
93711
94305
  function removeFromObj(obj, fields) {
93712
94306
  for (const f of fields) if (typeof f === "string") deleteFields(f, obj);
93713
94307
  else Object.keys(f).forEach((f2) => {
93714
- removeFromObj(obj[f2], f[f2]);
94308
+ const sub = obj[f2];
94309
+ if (sub) removeFromObj(sub, f[f2]);
93715
94310
  });
93716
94311
  }
93717
94312
  function extractFromObj(obj, fields, output = {}) {
@@ -93783,6 +94378,7 @@ const keepStandardFields = [
93783
94378
 
93784
94379
  //#endregion
93785
94380
  //#region node_modules/publish-util/dist/prepack.js
94381
+ const runsPrePack = (script) => typeof script === "string" && script.includes("publish-util-prepack");
93786
94382
  function prePackObj(pkg, config = {}) {
93787
94383
  renameFromObj(pkg, config.rename);
93788
94384
  const keepObj = config.keep && extractFromObj(pkg, config.keep);
@@ -93800,7 +94396,8 @@ function prePackObj(pkg, config = {}) {
93800
94396
  if (!config.silent) console.log("scripts.postpack missing, adding it.\n To skip this, set publishUtil.autoPostPack to false");
93801
94397
  set(pkg, "scripts.postpack", "publish-util-postpack");
93802
94398
  }
93803
- if ((scripts === null || scripts === void 0 ? void 0 : scripts.prepack) === "publish-util-prepack") delete scripts.prepack;
94399
+ if (scripts && runsPrePack(scripts.prepack)) delete scripts.prepack;
94400
+ if ((scripts === null || scripts === void 0 ? void 0 : scripts.prepublishOnly) === "publish-util-prepublishonly") delete scripts.prepublishOnly;
93804
94401
  if (keepObj) merge(pkg, keepObj);
93805
94402
  }
93806
94403
 
@@ -94464,6 +95061,7 @@ var PkgSrcManager = class {
94464
95061
  /** trimmed packument copies, see util/trimmed-packument */
94465
95062
  _packumentDir;
94466
95063
  _lastMetaStatus;
95064
+ _registryOpts;
94467
95065
  _fetching;
94468
95066
  _fetchingMsg;
94469
95067
  constructor(options) {
@@ -94665,33 +95263,38 @@ var PkgSrcManager = class {
94665
95263
  this.updateFetchMetaStatus();
94666
95264
  }
94667
95265
  };
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}`;
95266
+ const registryRequest = async () => {
95267
+ logger$1.debug(`fetch packument ${qItem.packumentUrl}`);
95268
+ const { cached } = qItem;
95269
+ const file = trimmedPackumentFile(this._packumentDir, qItem.packumentUrl);
95270
+ try {
95271
+ const job = {
95272
+ url: qItem.packumentUrl,
95273
+ file,
95274
+ etag: cached?.etag,
95275
+ lastModified: cached?.lastModified,
95276
+ full: Boolean(this._fyn.lockTime) || Boolean(cached && !cached.corgi)
95277
+ };
95278
+ const pool = getFsWorkerPool();
95279
+ const res = await fetchPackument(job, (url, headers) => this.registryGet(url, headers), pool ? (saveJob) => pool.run("savePackument", saveJob) : void 0);
95280
+ if (res.status === 304) {
95281
+ if (cached.trimmed) touchTrimmedPackument(file);
95282
+ else writeTrimmedPackument(file, cached.packument, void 0, cached);
95283
+ updateItem("304");
95284
+ return cached.packument;
95285
+ }
95286
+ updateItem("200");
95287
+ return parseTrimmed(res.json).packument;
95288
+ } catch (err) {
95289
+ const msg = `failed fetching packument of ${pkgName}`;
94687
95290
  logger$1.error(chalk.yellow(msg), chalk.red(err.message));
94688
95291
  throw Object.assign(new AggregateError$1([err], msg), { expected: true });
94689
- });
95292
+ }
94690
95293
  };
94691
95294
  this._metaStat.wait--;
94692
95295
  this._metaStat.inTx++;
94693
95296
  this.updateFetchMetaStatus(false);
94694
- return (qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : pacoteRequest()).then((x) => {
95297
+ return (qItem.item.urlType ? this.fetchUrlSemverMeta(qItem.item) : dist_default.resolve(registryRequest())).then((x) => {
94695
95298
  this._metaStat.inTx--;
94696
95299
  const time = Date.now() - startTime;
94697
95300
  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 +95304,20 @@ var PkgSrcManager = class {
94701
95304
  qItem.defer.reject(Object.assign(new AggregateError$1([new Error(msg)], msg), { expected: true }));
94702
95305
  return;
94703
95306
  }
94704
- refreshCacheEntry(this._cacheDir, qItem.cacheKey).catch(() => {});
94705
- if (!qItem.item.urlType) writeTrimmedPackument(trimmedPackumentFile(this._packumentDir, qItem.packumentUrl), x);
94706
95307
  qItem.defer.resolve(x);
94707
95308
  }).catch((err) => {
94708
95309
  this._metaStat.inTx--;
94709
95310
  qItem.defer.reject(err);
94710
95311
  });
94711
95312
  }
95313
+ /** the plain values of the pacote options, for util/registry-get */
95314
+ get registryOpts() {
95315
+ return this._registryOpts ??= pickRegistryOpts(this._pacoteOpts);
95316
+ }
95317
+ /** GET from the registry, see util/registry-get */
95318
+ registryGet(url, extraHeaders) {
95319
+ return registryGet(url, extraHeaders, this.registryOpts);
95320
+ }
94712
95321
  hasMeta(item) {
94713
95322
  return Boolean(this._meta[item.resolutionName || item.name]);
94714
95323
  }
@@ -94882,16 +95491,35 @@ var PkgSrcManager = class {
94882
95491
  };
94883
95492
  };
94884
95493
  const readMemoizedPackument = async () => {
94885
- const memoized = await loadBestCachedPackument(false);
94886
- if (!(memoized && memoized.data)) return;
95494
+ const memoized = await readTrimmedPackument(trimmedFile) || await loadBestCachedPackument(false);
95495
+ if (!(memoized && (memoized.packument || memoized.data))) return;
94887
95496
  logger$1.debug(`using memoized packument cache for '${pkgName}'`);
94888
95497
  cacheMemoized = true;
94889
95498
  this._metaStat.wait--;
94890
- return JSON.parse(memoized.data.toString());
95499
+ return memoized.packument || JSON.parse(memoized.data.toString());
94891
95500
  };
94892
95501
  let fetchAttempted = false;
94893
95502
  let foundCache;
94894
95503
  let foundPackument;
95504
+ const revalidate = (packument) => {
95505
+ if (foundCache.packument) {
95506
+ const { etag, lastModified, corgi } = foundCache;
95507
+ return {
95508
+ packument,
95509
+ trimmed: true,
95510
+ etag,
95511
+ lastModified,
95512
+ corgi
95513
+ };
95514
+ }
95515
+ const resHeaders = foundCache.metadata?.resHeaders || {};
95516
+ return {
95517
+ packument,
95518
+ trimmed: false,
95519
+ etag: resHeaders.etag,
95520
+ lastModified: resHeaders["last-modified"]
95521
+ };
95522
+ };
94895
95523
  const queueMetaFetchRequest = (cached) => {
94896
95524
  fetchAttempted = true;
94897
95525
  const offline = this._fyn.remoteMetaDisabled;
@@ -94912,7 +95540,8 @@ var PkgSrcManager = class {
94912
95540
  cacheKey,
94913
95541
  item,
94914
95542
  packumentUrl,
94915
- defer: dist_default.defer()
95543
+ defer: dist_default.defer(),
95544
+ cached: cached && revalidate(cached)
94916
95545
  };
94917
95546
  this._netQ.addItem(netQItem);
94918
95547
  return netQItem.defer.promise;
@@ -94922,7 +95551,7 @@ var PkgSrcManager = class {
94922
95551
  let cacheLookup;
94923
95552
  if (item.urlType) cacheLookup = this._fyn.remoteMetaDisabled ? loadPreparedUrlMeta().then((preparedUrlMeta) => ({ preparedUrlMeta })) : dist_default.resolve();
94924
95553
  else if (forceRefresh) cacheLookup = dist_default.resolve();
94925
- else cacheLookup = readTrimmedPackument(trimmedFile).then((trimmed) => trimmed || loadBestCachedPackument(true));
95554
+ else cacheLookup = readTrimmedPackument(trimmedFile).then((trimmed) => trimmed && !(trimmed.corgi && this._fyn.lockTime) ? trimmed : loadBestCachedPackument(true));
94926
95555
  const promise = cacheLookup.then((cached) => {
94927
95556
  if (cached && cached.preparedUrlMeta) {
94928
95557
  cacheMemoized = true;
@@ -95053,10 +95682,21 @@ var PkgSrcManager = class {
95053
95682
  if (di && di.urlType) return `${di.name}@${di.semver}`;
95054
95683
  return `${pkgInfo.name}@${pkgInfo.version}`;
95055
95684
  }
95056
- async getCentralPackage(integrity, pkgInfo) {
95685
+ /**
95686
+ * A package not in the central store yet comes back as a store job, so the extractor writes
95687
+ * the store, not the download slot this runs in.
95688
+ *
95689
+ * @param data - the tarball's bytes, when it was downloaded for the central store and not
95690
+ * into cacache
95691
+ */
95692
+ async getCentralPackage(integrity, pkgInfo, data) {
95057
95693
  const { central, copy } = this._fyn;
95058
95694
  const tarId = this.tarballFetchId(pkgInfo);
95059
95695
  const tarStream = async () => {
95696
+ if (data) {
95697
+ if (!import_lib.default.checkData(data, integrity)) throw Object.assign(/* @__PURE__ */ new Error(`integrity check failed for ${tarId}`), { code: "EINTEGRITY" });
95698
+ return Readable.from([data]);
95699
+ }
95060
95700
  return integrity && await (await loadCacache()).get.hasContent(this._cacheDir, integrity) ? this.cacacheTarballStream(integrity) : this.pacoteTarballStream(tarId, pkgInfo, integrity);
95061
95701
  };
95062
95702
  if (integrity && central) {
@@ -95075,7 +95715,17 @@ var PkgSrcManager = class {
95075
95715
  hasCentral = false;
95076
95716
  }
95077
95717
  }
95078
- if (!hasCentral) await central.storeTarStream(tarId, integrity, tarStream);
95718
+ if (!hasCentral) {
95719
+ const tarSource = async () => {
95720
+ if (data) return { data };
95721
+ const content = await (await loadCacache()).get.hasContent(this._cacheDir, integrity);
95722
+ return content ? { file: getContentPath(this._cacheDir, content.sri) } : void 0;
95723
+ };
95724
+ return {
95725
+ integrity,
95726
+ store: (deferIfBusy) => central.storeTarStream(tarId, integrity, tarStream, deferIfBusy, tarSource)
95727
+ };
95728
+ }
95079
95729
  return integrity;
95080
95730
  }
95081
95731
  }
@@ -95085,7 +95735,14 @@ var PkgSrcManager = class {
95085
95735
  const startTime = Date.now();
95086
95736
  const pkgId = this.tarballFetchId(pkgInfo);
95087
95737
  const integrity = this.getIntegrity(pkgInfo);
95088
- const doFetch = () => {
95738
+ const tarballUrl = get$1(pkgInfo, "dist.tarball");
95739
+ const central = this._fyn.central;
95740
+ const centralEligible = Boolean(integrity && tarballUrl && central && !tarballUrl.startsWith("~url-spec~"));
95741
+ const copied = () => {
95742
+ const { copy } = this._fyn;
95743
+ return copy.indexOf(pkgInfo.name) >= 0 || copy.indexOf(`${pkgInfo.name}@${pkgInfo.version}`) >= 0;
95744
+ };
95745
+ const doFetch = (fetchForCentral) => {
95089
95746
  const fetchStartTime = Date.now();
95090
95747
  if (!this._fetching) {
95091
95748
  this._fetching = [];
@@ -95093,21 +95750,28 @@ var PkgSrcManager = class {
95093
95750
  }
95094
95751
  this._fetching.push(pkgId);
95095
95752
  logger$1.updateItem(FETCH_PACKAGE, `${this._fetching.length} ${this._fetchingMsg}`);
95096
- return this.pacotePrefetch(pkgId, pkgInfo, integrity).then(() => {
95753
+ return (fetchForCentral ? dist_default.resolve(this.registryGet(tarballUrl, {})).then((res) => res.buffer()) : this.pacotePrefetch(pkgId, pkgInfo, integrity)).then((data) => {
95097
95754
  const status = chalk.cyan(`200`);
95098
95755
  const time = log_format_default.time(Date.now() - fetchStartTime);
95099
95756
  const ix = this._fetching.indexOf(pkgId);
95100
95757
  this._fetching.splice(ix, 1);
95101
95758
  this._fetchingMsg = `${status} ${time} ${chalk.red.bgGreen(pkgInfo.name)}`;
95102
95759
  logger$1.updateItem(FETCH_PACKAGE, `${this._fetching.length} ${this._fetchingMsg}`);
95103
- return this.getCentralPackage(integrity, pkgInfo);
95760
+ return this.getCentralPackage(integrity, pkgInfo, data || void 0);
95104
95761
  });
95105
95762
  };
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);
95763
+ const fetchRemote = (fetchForCentral) => {
95108
95764
  const rd = this._fyn.remoteTgzDisabled;
95109
95765
  if (rd) throw new Error(`option ${rd} has disabled retrieving tarball from remote`);
95110
- return doFetch();
95766
+ return doFetch(fetchForCentral);
95767
+ };
95768
+ const promise = dist_default.resolve().then(async () => {
95769
+ if (centralEligible) {
95770
+ if (await central.has(integrity)) return this.getCentralPackage(integrity, pkgInfo);
95771
+ if (!this._fyn.remoteTgzDisabled && !copied() && await central.allow(integrity)) return fetchRemote(true);
95772
+ }
95773
+ if (await dist_default.resolve(loadCacache()).then((cacache) => cacache.get.hasContent(this._cacheDir, integrity)).catch(() => false)) return this.getCentralPackage(integrity, pkgInfo);
95774
+ return fetchRemote(false);
95111
95775
  });
95112
95776
  return {
95113
95777
  then: (r, e) => promise.then(r, e),
@@ -96872,6 +97536,51 @@ var PkgDepLocker = class {
96872
97536
  if (sorted) remove$3(sorted, (x) => x === item.resolved);
96873
97537
  }
96874
97538
  }
97539
+ /**
97540
+ * Locked versions by package name, newest first, from the serialized lock data
97541
+ */
97542
+ lockedVersions() {
97543
+ assert(this._isFynFormat, "lock data is no longer in fyn format");
97544
+ const result = {};
97545
+ for (const name in this._lockData) {
97546
+ if (name.startsWith("$")) continue;
97547
+ const versions = Object.keys(this._lockData[name]).filter((v) => !v.startsWith("_"));
97548
+ if (versions.length > 0) result[name] = versions.sort(simpleCompare);
97549
+ }
97550
+ return result;
97551
+ }
97552
+ /**
97553
+ * Drop lock data so the resolver picks the newest versions the ranges allow.
97554
+ * With no names, drop every package. With names, drop those packages and
97555
+ * everything they depend on, directly or not.
97556
+ *
97557
+ * @param names - packages to unlock, or none for all
97558
+ * @returns the unlocked package names
97559
+ */
97560
+ unlock(names = []) {
97561
+ assert(this._isFynFormat, "lock data is no longer in fyn format");
97562
+ const data = this._lockData;
97563
+ const pending = names.length > 0 ? [...names] : Object.keys(data).filter((n) => !n.startsWith("$"));
97564
+ const unlocked = /* @__PURE__ */ new Set();
97565
+ while (pending.length > 0) {
97566
+ const name = pending.pop();
97567
+ const pkgLocked = data[name];
97568
+ if (unlocked.has(name) || !pkgLocked) continue;
97569
+ unlocked.add(name);
97570
+ if (names.length === 0) continue;
97571
+ for (const version in pkgLocked) {
97572
+ if (version.startsWith("_")) continue;
97573
+ const vpkg = pkgLocked[version];
97574
+ const deps = {
97575
+ ...vpkg.dependencies,
97576
+ ...vpkg.optionalDependencies
97577
+ };
97578
+ for (const dep in deps) pending.push(parseNpmAlias(deps[dep], dep)?.name || dep);
97579
+ }
97580
+ }
97581
+ for (const name of unlocked) delete data[name];
97582
+ return [...unlocked].sort();
97583
+ }
96875
97584
  shasum(data) {
96876
97585
  return Crypto.createHash("sha1").update(data).digest("hex");
96877
97586
  }
@@ -96928,6 +97637,7 @@ var PkgDepLocker = class {
96928
97637
  */
96929
97638
  saveInstalled(filename) {
96930
97639
  if (!this._enable) return;
97640
+ file_ops_default.$.mkdirp(Path.dirname(filename));
96931
97641
  file_ops_default.writeFileSync(filename, this._stringify(Path.dirname(filename)));
96932
97642
  }
96933
97643
  /**
@@ -97055,7 +97765,8 @@ const typeGetters = {
97055
97765
  return lower === "true" || lower === "yes" || lower === "1" || lower === "on";
97056
97766
  },
97057
97767
  truthy: (x) => !!x,
97058
- json: (x) => JSON.parse(x)
97768
+ json: (x) => JSON.parse(x),
97769
+ array: (x) => JSON.parse(x)
97059
97770
  };
97060
97771
  const getEnvName = (optKey, opt, env) => {
97061
97772
  if (!Object.prototype.hasOwnProperty.call(opt, "env")) return void 0;
@@ -97078,9 +97789,12 @@ const xenvConfig = (spec, userConfig, options) => {
97078
97789
  userConfig = userConfig || {};
97079
97790
  const trace = {};
97080
97791
  const getType = (opt) => {
97081
- const tod = Object.prototype.hasOwnProperty.call(opt, "default") && typeof opt.default;
97082
- const type = opt.type || tod !== "function" && tod || "string";
97083
- return type === "object" ? "json" : type;
97792
+ if (opt.type) return opt.type === "object" ? "json" : opt.type;
97793
+ if (!Object.prototype.hasOwnProperty.call(opt, "default")) return "string";
97794
+ if (opt.default === null) return "string";
97795
+ if (Array.isArray(opt.default)) return "array";
97796
+ const tod = typeof opt.default;
97797
+ return tod === "object" ? "json" : tod === "function" ? "string" : tod;
97084
97798
  };
97085
97799
  const getGetter = (opt) => {
97086
97800
  const type = getType(opt);
@@ -97113,19 +97827,27 @@ const xenvConfig = (spec, userConfig, options) => {
97113
97827
  };
97114
97828
  }
97115
97829
  };
97116
- const sources = options && options.sources || ["env", "option"];
97117
- sources.push("default");
97830
+ const sources = [...options && options.sources || ["env", "option"], "default"];
97831
+ const badSource = sources.find((s) => !Object.prototype.hasOwnProperty.call(getters, s));
97832
+ if (badSource !== void 0) throw new Error(`xenv-config: unknown source "${badSource}"; valid sources are ${Object.keys(getters).join(", ")}`);
97118
97833
  const config = Object.keys(spec).reduce((cfg, k) => {
97834
+ var _a;
97119
97835
  const opt = spec[k];
97120
97836
  const type = getType(opt);
97121
97837
  let found;
97122
97838
  if (type !== "json") sources.find((s) => found = getters[s](opt, k));
97123
97839
  else {
97124
97840
  const mapped = sources.map((s) => getters[s](opt, k)).filter((x) => Boolean(x));
97125
- if (mapped.length > 0) found = {
97126
- trace: { src: mapped.map((x) => x.trace.src).join(",") },
97127
- value: merge.apply(null, [{}].concat(mapped.map((x) => x.value).reverse()))
97128
- };
97841
+ if (mapped.length > 0) {
97842
+ const name = (_a = mapped.find((x) => x.trace.name)) === null || _a === void 0 ? void 0 : _a.trace.name;
97843
+ found = {
97844
+ trace: {
97845
+ src: mapped.map((x) => x.trace.src).join(","),
97846
+ ...name && { name }
97847
+ },
97848
+ value: merge.apply(null, [{}].concat(mapped.map((x) => x.value).reverse()))
97849
+ };
97850
+ }
97129
97851
  }
97130
97852
  if (!found) return cfg;
97131
97853
  trace[k] = found.trace;
@@ -97143,18 +97865,12 @@ const xenvConfig = (spec, userConfig, options) => {
97143
97865
  //#endregion
97144
97866
  //#region lib/fyn-config.ts
97145
97867
  const spec = {
97146
- registry: {
97147
- env: "FYN_REGISTRY",
97148
- default: "http://localhost:4873"
97149
- },
97868
+ registry: { default: "http://localhost:4873" },
97150
97869
  pkgFile: {
97151
97870
  env: "FYN_PACKAGE_FILE",
97152
97871
  default: "package.json"
97153
97872
  },
97154
- targetDir: {
97155
- env: "FYN_TARGET_DIR",
97156
- default: "xout"
97157
- },
97873
+ targetDir: { default: "xout" },
97158
97874
  fynDir: {
97159
97875
  env: [
97160
97876
  "FYN_DIR",
@@ -97183,12 +97899,135 @@ function fynConfig(override) {
97183
97899
  }
97184
97900
 
97185
97901
  //#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";
97902
+ //#region lib/util/reflink.ts
97903
+ let nativeReflink;
97904
+ /**
97905
+ * @fynjs/reflink is a native addon, so it cannot be bundled into dist/. Load it at runtime from
97906
+ * an optional dependency, and resolve to undefined when it is absent or has no binary for
97907
+ * this platform.
97908
+ */
97909
+ function loadReflink() {
97910
+ nativeReflink ??= (async () => {
97911
+ try {
97912
+ const file = createRequire(import.meta.url).resolve("@fynjs/reflink");
97913
+ return await import(
97914
+ /* @vite-ignore */
97915
+ pathToFileURL(file).href
97916
+ );
97917
+ } catch (err) {
97918
+ logger$1.debug(`@fynjs/reflink not loaded, using JS clone: ${err.message}`);
97919
+ return;
97920
+ }
97921
+ })();
97922
+ return nativeReflink;
97923
+ }
97924
+ async function loadReflinkCloneFiles() {
97925
+ return (await loadReflink())?.cloneFiles;
97191
97926
  }
97927
+ /** undefined with no reflink, or an reflink older than cloneDir */
97928
+ async function loadReflinkCloneDir() {
97929
+ return (await loadReflink())?.cloneDir;
97930
+ }
97931
+
97932
+ //#endregion
97933
+ //#region lib/util/untar-tree.ts
97934
+ /** An `onentry` for Tar.x that builds the tree, and the result once extraction finishes */
97935
+ function treeCollector(strip) {
97936
+ const newDirObj = () => {
97937
+ const n = Object.create(null);
97938
+ n["/"] = Object.create(null);
97939
+ return n;
97940
+ };
97941
+ const result = {
97942
+ tree: newDirObj(),
97943
+ fromHeaders: true
97944
+ };
97945
+ const onentry = (entry) => {
97946
+ const isDir = entry.type === "Directory";
97947
+ const parts = entry.path.split(/\/|\\/).slice(strip).filter((p) => p && p !== ".");
97948
+ const wtree = (isDir ? parts : parts.slice(0, -1)).reduce((wt, dir) => {
97949
+ return wt[dir] || (wt[dir] = newDirObj());
97950
+ }, result.tree);
97951
+ if (isDir) return;
97952
+ if (!entry.mtime || ![
97953
+ "File",
97954
+ "OldFile",
97955
+ "ContiguousFile"
97956
+ ].includes(entry.type)) result.fromHeaders = false;
97957
+ const fname = parts[parts.length - 1];
97958
+ if (fname) {
97959
+ const m = Math.round((entry.mtime ? entry.mtime.getTime() : Date.now()) / 1e3);
97960
+ wtree["/"][fname] = {
97961
+ z: entry.size,
97962
+ m,
97963
+ $: entry.header.cksumValid && entry.header.cksum
97964
+ };
97965
+ }
97966
+ };
97967
+ return {
97968
+ onentry,
97969
+ result
97970
+ };
97971
+ }
97972
+ const hashList = (list) => Crypto$1.createHash("sha512").update(JSON.stringify(list.sort())).digest("base64");
97973
+ /** The shaSum of a tree from tar headers, in SUM_VERSION's format */
97974
+ function treeShasum(tree) {
97975
+ const files = [];
97976
+ const walk = (node, dir) => {
97977
+ 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}`);
97978
+ else walk(child, `${dir}${name}/`);
97979
+ };
97980
+ walk(tree, "");
97981
+ return hashList(files);
97982
+ }
97983
+ /** tree.json's content. An old entry keeps its sumVersion until validate() converts it. */
97984
+ function treeFileJson(info) {
97985
+ return JSON.stringify({
97986
+ $: info.tree,
97987
+ shaSum: info.shaSum,
97988
+ mutates: info.mutates,
97989
+ _: info.sumVersion ?? 1
97990
+ });
97991
+ }
97992
+
97993
+ //#endregion
97994
+ //#region lib/util/store-entry.ts
97995
+ /** an extraction marker or temp dir older than this was left by an install that died */
97996
+ const STALE_MS = 3e5;
97997
+ /** a name suffix no other install, or other call in this one, will pick */
97998
+ const uniqueSuffix = () => `${process.pid}-${Crypto$1.randomBytes(4).toString("hex")}`;
97999
+ /**
98000
+ * filterScanDir options that hash a package dir in one stat walk, in both formats: v1 for
98001
+ * checking an old entry, v2 for everything else (see SUM_VERSION). Call `sums` with the scan's
98002
+ * result.
98003
+ */
98004
+ function shasumScan(packageDir) {
98005
+ const v2 = [];
98006
+ const filter = (_file, _path, extras) => {
98007
+ const { stat, dirFile } = extras;
98008
+ const fullStat = stat;
98009
+ if (!fullStat.isDirectory()) v2.push(`${dirFile.replace(/\\/g, "/")}-${Math.floor(fullStat.mtimeMs / 1e3)}-${fullStat.size}`);
98010
+ return { formatName: `${dirFile}-${fullStat.mtimeMs}-${fullStat.size}` };
98011
+ };
98012
+ return {
98013
+ options: {
98014
+ cwd: packageDir,
98015
+ filter,
98016
+ filterDir: filter,
98017
+ fullStat: true,
98018
+ concurrency: 500,
98019
+ sortFiles: false,
98020
+ includeDir: true
98021
+ },
98022
+ sums: (files) => ({
98023
+ v1: hashList(files),
98024
+ v2: hashList(v2)
98025
+ })
98026
+ };
98027
+ }
98028
+
98029
+ //#endregion
98030
+ //#region lib/util/place-files.ts
97192
98031
  /** link errors that mean the filesystem can't hardlink from the store at all */
97193
98032
  const NO_LINK_CODES = [
97194
98033
  "EXDEV",
@@ -97197,6 +98036,14 @@ const NO_LINK_CODES = [
97197
98036
  "EOPNOTSUPP",
97198
98037
  "ENOSYS"
97199
98038
  ];
98039
+
98040
+ //#endregion
98041
+ //#region lib/fyn-central.ts
98042
+ const { missPipe } = fyntil;
98043
+ /** Type guard to check if parsed tree is new format with version */
98044
+ function isTreeFileContent(obj) {
98045
+ return typeof obj === "object" && obj !== null && "_" in obj && typeof obj._ === "number";
98046
+ }
97200
98047
  /**
97201
98048
  * Convert a directory tree structure to a flatten one like:
97202
98049
  * ```
@@ -97234,11 +98081,26 @@ function flattenTree(tree, output, baseDir) {
97234
98081
  var FynCentral = class {
97235
98082
  _centralDir;
97236
98083
  _map;
98084
+ /** entries found missing, so has() and allow() don't stat them again; storing re-checks */
98085
+ _missing = /* @__PURE__ */ new Map();
98086
+ /** store dirs this install has made, so each is made once */
98087
+ _madeDirs = /* @__PURE__ */ new Set();
97237
98088
  _hardlink;
97238
- constructor({ centralDir = ".fyn/_central-storage", hardlink = true } = {}) {
98089
+ _reflink;
98090
+ _copyFallback;
98091
+ /** cleared on the first dir clone the filesystem can't do, so later packages skip it */
98092
+ _cloneDirs = true;
98093
+ /**
98094
+ * cleared when a package's files hardlink but none clone, so later packages hardlink in the
98095
+ * fs workers. That's faster than @fynjs/reflink on a filesystem that can't clone (ext4).
98096
+ */
98097
+ _cloneFiles = true;
98098
+ constructor({ centralDir = ".fyn/_central-storage", hardlink = true, reflink = true, copyFallback = true } = {}) {
97239
98099
  this._centralDir = Path.resolve(centralDir);
97240
98100
  this._map = /* @__PURE__ */ new Map();
97241
98101
  this._hardlink = hardlink;
98102
+ this._reflink = reflink;
98103
+ this._copyFallback = copyFallback;
97242
98104
  }
97243
98105
  _analyze(integrity) {
97244
98106
  const sri = import_lib.default.parse(integrity, { single: true });
@@ -97272,14 +98134,16 @@ var FynCentral = class {
97272
98134
  }
97273
98135
  }
97274
98136
  try {
97275
- const stat = await file_ops_default.stat(info.contentPath);
97276
- info.exist = true;
97277
- if (stat.isDirectory()) {
98137
+ if ((await file_ops_default.stat(info.contentPath)).isDirectory()) {
97278
98138
  await this.readInfoTree(info);
97279
98139
  if (!noSet) this._map.set(integrity, info);
97280
98140
  }
98141
+ info.exist = true;
98142
+ this._missing.delete(integrity);
97281
98143
  return info;
97282
98144
  } catch (_err) {
98145
+ info.exist = false;
98146
+ if (!noSet) this._missing.set(integrity, info);
97283
98147
  return info;
97284
98148
  }
97285
98149
  }
@@ -97290,7 +98154,7 @@ var FynCentral = class {
97290
98154
  * @returns boolean
97291
98155
  */
97292
98156
  async has(integrity) {
97293
- const info = this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity);
98157
+ const info = this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity);
97294
98158
  return Boolean(info.tree);
97295
98159
  }
97296
98160
  /**
@@ -97300,7 +98164,7 @@ var FynCentral = class {
97300
98164
  * @returns boolean
97301
98165
  */
97302
98166
  async allow(integrity) {
97303
- return (this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity)).mutates ? false : true;
98167
+ return (this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity)).mutates ? false : true;
97304
98168
  }
97305
98169
  async getContentPath(integrity) {
97306
98170
  return (await this.getInfo(integrity)).contentPath;
@@ -97311,28 +98175,25 @@ var FynCentral = class {
97311
98175
  if (!info.tree) throw new Error(`fyn-central can't get package for integrity ${integrity}`);
97312
98176
  return info;
97313
98177
  }
97314
- async _calcContentShasum(info, pkgDir) {
98178
+ /**
98179
+ * Hash a package dir in one stat walk, in both formats: v1 for checking an old entry, v2 for
98180
+ * everything else (see SUM_VERSION).
98181
+ */
98182
+ async _scanShasums(packageDir) {
97315
98183
  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");
98184
+ const { options, sums } = shasumScan(packageDir);
98185
+ return sums(await filterScanDir(options));
97332
98186
  } catch (_err) {
97333
98187
  return;
97334
98188
  }
97335
98189
  }
98190
+ /** The v2 hash of a fresh extraction, from the tar header sizes and mtimes in its tree */
98191
+ _treeShasum(tree) {
98192
+ return treeShasum(tree);
98193
+ }
98194
+ async _calcContentShasum(info, pkgDir) {
98195
+ return (await this._scanShasums(pkgDir || Path.join(info.contentPath, "package")))?.v1;
98196
+ }
97336
98197
  /**
97337
98198
  * Get the hash of a npm package's extracted content.
97338
98199
  * - only consider the file names and their mtime and size because npm tar files
@@ -97351,16 +98212,41 @@ var FynCentral = class {
97351
98212
  }
97352
98213
  }
97353
98214
  async getMutation(integrity) {
97354
- return (this._map.has(integrity) ? this._map.get(integrity) : await this._loadTree(integrity)).mutates;
98215
+ return (this._map.get(integrity) ?? this._missing.get(integrity) ?? await this._loadTree(integrity)).mutates;
97355
98216
  }
98217
+ /**
98218
+ * Rename the entry away before removing it, so a concurrent install never finds it half
98219
+ * deleted, with tree.json present and package/ partly gone.
98220
+ */
97356
98221
  async delete(integrity) {
97357
98222
  const info = this._map.get(integrity);
97358
98223
  if (info && info.exist && info.contentPath) {
97359
- await file_ops_default.$.rimraf(info.contentPath);
98224
+ const trash = `${info.contentPath}.del-${uniqueSuffix()}`;
98225
+ try {
98226
+ await file_ops_default.rename(info.contentPath, trash);
98227
+ } catch (err) {
98228
+ if (err.code !== "ENOENT") throw err;
98229
+ }
97360
98230
  this._map.delete(integrity);
98231
+ await file_ops_default.$.rimraf(trash);
97361
98232
  }
97362
98233
  }
97363
98234
  /**
98235
+ * An entry with no readable tree.json is left from a broken write. Rename it away, then
98236
+ * remove it, the same way delete() does.
98237
+ */
98238
+ async _removeBrokenEntry(contentPath) {
98239
+ const trash = `${contentPath}.del-${uniqueSuffix()}`;
98240
+ try {
98241
+ await file_ops_default.rename(contentPath, trash);
98242
+ } catch (err) {
98243
+ if (err.code !== "ENOENT") throw err;
98244
+ return;
98245
+ }
98246
+ logger$1.warn(`fyn-central: replacing a broken store entry with no tree.json at ${contentPath}`);
98247
+ await file_ops_default.$.rimraf(trash);
98248
+ }
98249
+ /**
97364
98250
  * Read the content dir tree from file into info
97365
98251
  *
97366
98252
  * @param info - package info
@@ -97374,24 +98260,30 @@ var FynCentral = class {
97374
98260
  if (parsed.mutates !== void 0) info.mutates = parsed.mutates;
97375
98261
  info.tree = parsed.$;
97376
98262
  info.shaSum = parsed.shaSum;
98263
+ info.sumVersion = parsed._;
97377
98264
  } else info.tree = parsed;
97378
98265
  } catch (err) {
97379
98266
  throw new AggregateError$1([err], `fyn-central: reading tree file ${treeFile}`);
97380
98267
  }
97381
98268
  }
97382
98269
  /**
97383
- * Save the content dir tree from info to file
98270
+ * Save the content dir tree from info to file. In a live entry it's written to a temp file
98271
+ * and renamed over, so a concurrent install never reads half of it. A `path` is a temp dir
98272
+ * only this install uses, so it's written directly.
97384
98273
  *
97385
98274
  * @param info - package info
97386
98275
  * @param path - path to save the file
97387
98276
  */
97388
98277
  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
- }));
98278
+ const data = treeFileJson(info);
98279
+ if (path) {
98280
+ await file_ops_default.writeFile(Path.join(path, "tree.json"), data);
98281
+ return;
98282
+ }
98283
+ const treeFile = Path.join(info.contentPath, "tree.json");
98284
+ const tmpFile = `${treeFile}.${uniqueSuffix()}`;
98285
+ await file_ops_default.writeFile(tmpFile, data);
98286
+ await file_ops_default.rename(tmpFile, treeFile);
97395
98287
  }
97396
98288
  async setMutation(integrity, mutates = true) {
97397
98289
  const info = await this._loadTree(integrity);
@@ -97399,21 +98291,81 @@ var FynCentral = class {
97399
98291
  info.mutates = mutates;
97400
98292
  await this.saveInfoTree(info);
97401
98293
  }
97402
- async replicate(integrity, destDir) {
98294
+ /**
98295
+ * @param prepare - makes destDir, when the caller hasn't. An fs worker does it instead when
98296
+ * it places the files.
98297
+ * @returns package.json as placed, when an fs worker placed the files
98298
+ */
98299
+ async replicate(integrity, destDir, prepare) {
98300
+ let prepared = !prepare;
98301
+ const ensurePrepared = async () => {
98302
+ if (!prepared) {
98303
+ prepared = true;
98304
+ await prepare();
98305
+ }
98306
+ };
98307
+ const placing = [];
97403
98308
  try {
97404
98309
  const info = await this.getInfo(integrity);
97405
98310
  const list = flattenTree(info.tree, {
97406
98311
  dirs: [],
97407
98312
  files: []
97408
98313
  }, "");
97409
- for (const dir of list.dirs) await file_ops_default.$.mkdirp(Path.join(destDir, dir));
98314
+ list.dirs = [...new Set(list.dirs)];
98315
+ list.files = [...new Set(list.files)];
98316
+ const srcDir = Path.join(info.contentPath, "package");
98317
+ const cloneDir = this._reflink && this._cloneDirs && await loadReflinkCloneDir();
98318
+ if (cloneDir) {
98319
+ await ensurePrepared();
98320
+ if (await this._cloneWholeDir(cloneDir, srcDir, destDir)) return;
98321
+ }
98322
+ const reflinkCloneFiles = this._reflink && this._cloneFiles && await loadReflinkCloneFiles();
98323
+ const copy = this._copyFallback;
98324
+ const mkdirs = async () => {
98325
+ for (const dir of list.dirs) await file_ops_default.$.mkdirp(Path.join(destDir, dir));
98326
+ };
98327
+ if (reflinkCloneFiles) {
98328
+ await ensurePrepared();
98329
+ await mkdirs();
98330
+ const others = list.files.filter((f) => f !== "package.json");
98331
+ const [stats] = await Promise.all([reflinkCloneFiles(srcDir, destDir, others, this._hardlink, copy), others.length < list.files.length && reflinkCloneFiles(srcDir, destDir, ["package.json"], false, copy)]);
98332
+ if (this._cloneFiles && stats.cloned === 0 && stats.linked > 0) {
98333
+ this._cloneFiles = false;
98334
+ logger$1.debug(`fyn-central: can't clone files from ${this._centralDir}, hardlinking instead`);
98335
+ }
98336
+ return;
98337
+ }
98338
+ const pool = getFsWorkerPool();
98339
+ if (pool) {
98340
+ const { noLink, pkgJson } = await pool.run("place", {
98341
+ srcDir,
98342
+ destDir,
98343
+ dirs: list.dirs,
98344
+ files: list.files,
98345
+ hardlink: this._hardlink,
98346
+ reflink: this._reflink,
98347
+ copyFallback: copy,
98348
+ prepare: !prepared
98349
+ });
98350
+ if (noLink && this._hardlink) {
98351
+ this._hardlink = false;
98352
+ logger$1.info(`fyn-central: can't hardlink from ${this._centralDir} (${noLink}), copying instead`);
98353
+ }
98354
+ return pkgJson;
98355
+ }
98356
+ await ensurePrepared();
98357
+ await mkdirs();
97410
98358
  await map$1(list.files, (file) => {
97411
98359
  const src = Path.join(info.contentPath, "package", file);
97412
98360
  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);
98361
+ let placed;
98362
+ if (file === "package.json") placed = this._reflink && !copy ? cloneFile(src, dest, true) : copyFile(src, dest);
98363
+ else placed = this._hardlink ? this._linkFile(src, dest) : this._cloneOrCopy(src, dest);
98364
+ placing.push(placed);
98365
+ return placed;
97415
98366
  }, { concurrency: 5 });
97416
98367
  } catch (err) {
98368
+ await Promise.allSettled(placing);
97417
98369
  const msg = `fyn-central can't replicate package at ${destDir} for integrity ${integrity}`;
97418
98370
  throw new AggregateError$1([err], msg);
97419
98371
  }
@@ -97435,68 +98387,118 @@ var FynCentral = class {
97435
98387
  }
97436
98388
  } else if (code !== "EMLINK") throw err;
97437
98389
  }
97438
- await cloneFile(src, dest);
98390
+ await this._cloneOrCopy(src, dest);
98391
+ }
98392
+ /** Clone, else copy, as reflink and copyFallback allow. */
98393
+ async _cloneOrCopy(src, dest) {
98394
+ if (this._reflink) await cloneFile(src, dest, !this._copyFallback);
98395
+ else if (this._copyFallback) await copyFile(src, dest);
98396
+ else throw new Error(`fyn-central: can't place ${dest}: it can't be hardlinked, and reflink and copy-fallback are off`);
98397
+ }
98398
+ /**
98399
+ * Clone the store's package dir to `destDir` in one call. A dir clone needs a path that
98400
+ * doesn't exist, so it goes to a unique sibling that's then renamed over `destDir`. The
98401
+ * rename replaces `destDir` only while it's still the empty dir createPkgOutDir made. If
98402
+ * anything put files there meanwhile, the clone is dropped for the per-file path, which never
98403
+ * writes through an existing file. Resolves false, with `destDir` untouched, when not cloned.
98404
+ */
98405
+ async _cloneWholeDir(cloneDir, srcDir, destDir) {
98406
+ const tmp = `${destDir}.clone-${uniqueSuffix()}`;
98407
+ if (!await cloneDir(srcDir, tmp)) {
98408
+ this._cloneDirs = false;
98409
+ logger$1.debug(`fyn-central: can't clone dirs from ${this._centralDir}, cloning files instead`);
98410
+ return false;
98411
+ }
98412
+ try {
98413
+ await file_ops_default.rename(tmp, destDir);
98414
+ return true;
98415
+ } catch (err) {
98416
+ await file_ops_default.$.rimraf(tmp);
98417
+ const code = err.code;
98418
+ if (code === "ENOTEMPTY" || code === "EEXIST") return false;
98419
+ throw err;
98420
+ }
97439
98421
  }
98422
+ /** Extract a tarball and build its tree from the tar headers. */
97440
98423
  _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
98424
  const strip = 1;
98425
+ const { onentry, result } = treeCollector(strip);
97448
98426
  const untarStream = So({
97449
98427
  strip,
97450
98428
  strict: true,
97451
98429
  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
- }
98430
+ onentry
97469
98431
  });
97470
- return missPipe(tarStream, untarStream).then(() => dirTree);
98432
+ return missPipe(tarStream, untarStream).then(() => result);
97471
98433
  }
97472
- async _acquireTmpLock(info) {
97473
- const tmpLock = `${info.contentPath}.lock`;
98434
+ /**
98435
+ * Claim the `.extracting` marker that tells other installs this one is writing the entry.
98436
+ * mkdir is atomic, so only one install gets it. A marker older than STALE_MS was left by an
98437
+ * install that died, so it is taken over.
98438
+ *
98439
+ * @returns true when this install owns the marker
98440
+ */
98441
+ async _claimMarker(marker, retry = true) {
97474
98442
  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
- });
98443
+ await file_ops_default.mkdir(marker);
98444
+ return true;
97481
98445
  } 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}`);
98446
+ if (err.code !== "EEXIST") throw err;
98447
+ }
98448
+ const stat = await file_ops_default.stat(marker).catch(() => void 0);
98449
+ if (retry && (!stat || Date.now() - stat.mtimeMs > 3e5)) {
98450
+ await file_ops_default.$.rimraf(marker);
98451
+ return this._claimMarker(marker, false);
97485
98452
  }
97486
- return tmpLock;
98453
+ return false;
97487
98454
  }
97488
- async _storeTarStream(info, _integrity, _stream) {
98455
+ /** Remove temp and trash dirs that installs which died left next to the entry. */
98456
+ async _removeStaleTemps(contentPath) {
98457
+ const dir = Path.dirname(contentPath);
98458
+ const base = Path.basename(contentPath);
98459
+ for (const name of await file_ops_default.readdir(dir)) {
98460
+ if (!name.startsWith(`${base}.tmp`) && !name.startsWith(`${base}.del-`)) continue;
98461
+ const stat = await file_ops_default.stat(Path.join(dir, name)).catch(() => void 0);
98462
+ if (stat && Date.now() - stat.mtimeMs > 3e5) await file_ops_default.$.rimraf(Path.join(dir, name));
98463
+ }
98464
+ }
98465
+ /**
98466
+ * Extract into a temp dir only this call uses, then rename it into place. The rename is
98467
+ * atomic, so other installs see the entry complete or not at all. If another install renamed
98468
+ * its copy in first, that copy has the same integrity, so this one is dropped for it.
98469
+ *
98470
+ * With an fs worker, storeTarStream hands the whole job to _storeInWorker instead.
98471
+ */
98472
+ async _storeTarStream(info, _stream) {
97489
98473
  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);
98474
+ const tmp = `${info.contentPath}.tmp-${uniqueSuffix()}`;
98475
+ try {
98476
+ const targetDir = Path.join(tmp, "package");
98477
+ await file_ops_default.$.mkdirp(targetDir);
98478
+ if (typeof stream === "function") stream = stream();
98479
+ if (stream.then) stream = await stream;
98480
+ const { tree, fromHeaders } = await this._untarStream(stream, targetDir);
98481
+ info.tree = tree;
98482
+ info.shaSum = fromHeaders ? this._treeShasum(tree) : (await this._scanShasums(targetDir))?.v2;
98483
+ info.sumVersion = 2;
98484
+ await this.saveInfoTree(info, tmp);
98485
+ try {
98486
+ await file_ops_default.rename(tmp, info.contentPath);
98487
+ } catch (err) {
98488
+ const code = err.code;
98489
+ if (![
98490
+ "ENOTEMPTY",
98491
+ "EEXIST",
98492
+ "EPERM"
98493
+ ].includes(code) || !await file_ops_default.exists(info.contentPath)) throw err;
98494
+ logger$1.debug("fyn-central: another install stored it first", info.contentPath);
98495
+ await file_ops_default.$.rimraf(tmp);
98496
+ await this.readInfoTree(info);
98497
+ }
98498
+ } catch (err) {
98499
+ await file_ops_default.$.rimraf(tmp);
98500
+ throw err;
98501
+ }
97500
98502
  info.exist = true;
97501
98503
  }
97502
98504
  /**
@@ -97509,45 +98511,99 @@ var FynCentral = class {
97509
98511
  const info = this._map.get(integrity);
97510
98512
  if (info === void 0 || !info.exist) return false;
97511
98513
  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);
98514
+ const pkgDir = Path.join(info.contentPath, "package");
98515
+ const pool = getFsWorkerPool();
98516
+ const sums = pool ? await pool.run("scan", { dir: pkgDir }) : await this._scanShasums(pkgDir);
98517
+ if (info.sumVersion === 2) info.validated = Boolean(sums) && info.shaSum === sums.v2;
98518
+ else {
98519
+ info.validated = Boolean(sums) && (!info.shaSum || info.shaSum === sums.v1);
98520
+ if (info.validated) {
98521
+ info.shaSum = sums.v2;
98522
+ info.sumVersion = 2;
98523
+ await this.saveInfoTree(info);
98524
+ }
97516
98525
  }
97517
- info.validated = info.shaSum === sum;
97518
98526
  }
97519
98527
  return info.validated;
97520
98528
  }
97521
- async storeTarStream(pkgId, integrity, stream) {
97522
- let tmpLock = false;
98529
+ /**
98530
+ * Store a package's tarball in the central store. Installs never wait on each other: each
98531
+ * extracts into its own temp dir and renames it into place.
98532
+ *
98533
+ * While extracting, an install holds the entry's `.extracting` marker. With `deferIfBusy`, a
98534
+ * live marker from another install returns false without reading the stream, so the caller
98535
+ * can do its other packages first and come back to this one.
98536
+ *
98537
+ * @param tarSource - resolves the tarball's file on disk or its bytes, when it has either, so
98538
+ * an fs worker can untar it instead of the stream
98539
+ * @returns false when deferred, true when the package is in the store
98540
+ */
98541
+ async storeTarStream(pkgId, integrity, stream, deferIfBusy = false, tarSource) {
97523
98542
  let currentStream = stream;
98543
+ let marker;
97524
98544
  try {
97525
- let info = await this._loadTree(integrity);
98545
+ const pool = tarSource && getFsWorkerPool();
98546
+ const source = pool && await tarSource();
98547
+ if (source) return await this._storeInWorker(pool, source, pkgId, integrity, deferIfBusy);
98548
+ const info = await this._loadTree(integrity);
97526
98549
  if (info.exist) {
97527
98550
  logger$1.debug("fyn-central storeTarStream: already exist", info.contentPath);
97528
98551
  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
- }
98552
+ return true;
97546
98553
  }
98554
+ const entryDir = Path.dirname(info.contentPath);
98555
+ if (!this._madeDirs.has(entryDir)) {
98556
+ await file_ops_default.$.mkdirp(entryDir);
98557
+ this._madeDirs.add(entryDir);
98558
+ }
98559
+ const markerPath = `${info.contentPath}.extracting`;
98560
+ if (await this._claimMarker(markerPath)) {
98561
+ marker = markerPath;
98562
+ if ((await this._loadTree(integrity, info)).exist) return true;
98563
+ await this._removeStaleTemps(info.contentPath);
98564
+ } else if (deferIfBusy) {
98565
+ logger$1.debug("fyn-central: another install is storing it, deferring", pkgId);
98566
+ return false;
98567
+ }
98568
+ if (await file_ops_default.exists(info.contentPath)) await this._removeBrokenEntry(info.contentPath);
98569
+ logger$1.debug("storing tar to central store", pkgId, integrity);
98570
+ await this._storeTarStream(info, currentStream);
98571
+ currentStream = void 0;
98572
+ this._map.set(integrity, info);
98573
+ this._missing.delete(integrity);
98574
+ logger$1.debug("fyn-central storeTarStream: stored", pkgId, info.contentPath);
98575
+ return true;
97547
98576
  } finally {
97548
98577
  if (currentStream && currentStream.destroy !== void 0) currentStream.destroy();
97549
- if (tmpLock) await file_ops_default.$.releaseLock(tmpLock);
98578
+ if (marker) await file_ops_default.rmdir(marker).catch(() => void 0);
98579
+ }
98580
+ }
98581
+ /** storeTarStream's steps, run by an fs worker with sync calls, marker and all */
98582
+ async _storeInWorker(pool, source, pkgId, integrity, deferIfBusy) {
98583
+ const info = this._map.get(integrity) ?? this._missing.get(integrity) ?? this._analyze(integrity);
98584
+ const result = await pool.run("store", {
98585
+ ...source,
98586
+ integrity,
98587
+ contentPath: info.contentPath,
98588
+ deferIfBusy
98589
+ });
98590
+ if (result.busy) {
98591
+ logger$1.debug("fyn-central: another install is storing it, deferring", pkgId);
98592
+ return false;
98593
+ }
98594
+ if (result.stored) {
98595
+ info.tree = result.tree;
98596
+ info.shaSum = result.stored.shaSum;
98597
+ info.sumVersion = result.stored.sumVersion;
98598
+ info.exist = true;
98599
+ this._map.set(integrity, info);
98600
+ this._missing.delete(integrity);
98601
+ logger$1.debug("fyn-central storeTarStream: stored", pkgId, info.contentPath);
98602
+ } else {
98603
+ logger$1.debug("fyn-central: the entry is in place already", info.contentPath);
98604
+ if (!(await this._loadTree(integrity, info)).tree) logger$1.error(`fyn-central exist package missing tree.json`);
97550
98605
  }
98606
+ return true;
97551
98607
  }
97552
98608
  };
97553
98609
 
@@ -99499,19 +100555,10 @@ function applyChalkMethod(chalkInstance, name, values) {
99499
100555
  throw new TypeError(`${name} is not a chalk function`);
99500
100556
  }
99501
100557
  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;
100558
+ return str.replace(/&(?:#[xX]([0-9a-fA-F]+)|#([0-9]+)|\w+);/g, (m, hex, dec) => {
100559
+ if (hex === void 0 && dec === void 0) return Object.prototype.hasOwnProperty.call(htmlEntities, m) ? htmlEntities[m] : m;
100560
+ const p = hex !== void 0 ? parseInt(hex, 16) : parseInt(dec, 10);
100561
+ return p <= 1114111 ? String.fromCodePoint(p) : m;
99515
100562
  });
99516
100563
  }
99517
100564
  function applyChalkMarkers(markers, text, userChalk) {
@@ -99553,18 +100600,19 @@ function applyChalkMarkers(markers, text, userChalk) {
99553
100600
  co(typeof chalkify === "function", `final chalk value is not a function after applying ${markers}`);
99554
100601
  return chalkify(text);
99555
100602
  }
100603
+ const MARKERS_RE = /<[^>]*>/g;
99556
100604
  function remove(s, keepHtml) {
99557
- const r = s.replace(/<[^>]*>/g, "").trim();
100605
+ const r = s.replace(MARKERS_RE, "");
99558
100606
  return keepHtml ? r : decodeHtml(r);
99559
100607
  }
99560
100608
  function format$1(s, instance, userChalk) {
99561
100609
  userChalk = normalizeColors(userChalk || instance.CHALK);
99562
- if (userChalk.supportsColor === false) return remove(s);
99563
- const tks = s && s.match(/(<[^>]+>|[^<>]+)/g);
100610
+ const off = userChalk.supportsColor === false;
100611
+ const tks = s && s.match(/<[^<>]+>|[^<>]+|[<>]/g);
99564
100612
  if (!tks) return s || "";
99565
- return decodeHtml(tks.reduceRight((a, e, ix) => {
100613
+ const colorized = tks.reduceRight((a, e, ix) => {
99566
100614
  const lvl = a[a.length - 1];
99567
- if (e[0] !== "<") {
100615
+ if (e[0] !== "<" || e.length === 1) {
99568
100616
  lvl.s = e + lvl.s;
99569
100617
  return a;
99570
100618
  }
@@ -99577,20 +100625,27 @@ function format$1(s, instance, userChalk) {
99577
100625
  return a;
99578
100626
  }
99579
100627
  if (a.length === 1) {
100628
+ if (off) return a;
99580
100629
  const partial = tks.slice(0, ix).join("") + `[${tks[ix]}]` + (tks.length > ix + 1 ? "..." : "");
99581
100630
  throw new Error(`unbalanced open/close markers: ${partial}`);
99582
100631
  }
99583
100632
  const mk = e.substring(1, e.length - 1);
99584
- if (lvl.mk && mk !== lvl.mk) {
100633
+ if (lvl.mk && mk !== lvl.mk && !off) {
99585
100634
  const partial = tks.slice(0, ix).join("") + `[** ${tks[ix]} **]` + tks.slice(ix + 1, lvl.ix).join("") + `[** ${tks[lvl.ix]} **]`;
99586
100635
  throw new Error(`mismatch markers: ${partial}`);
99587
100636
  }
99588
- const t = applyChalkMarkers(mk, lvl.s, userChalk);
100637
+ const t = off ? lvl.s : applyChalkMarkers(mk, lvl.s, userChalk);
99589
100638
  a.pop();
99590
100639
  const nl = a.length - 1;
99591
100640
  a[nl].s = t + a[nl].s;
99592
100641
  return a;
99593
- }, [{ s: "" }])[0].s);
100642
+ }, [{ s: "" }]);
100643
+ while (colorized.length > 1) {
100644
+ const lvl = colorized.pop();
100645
+ const nl = colorized.length - 1;
100646
+ colorized[nl].s = lvl.s + tks[lvl.ix] + colorized[nl].s;
100647
+ }
100648
+ return decodeHtml(colorized[0].s);
99594
100649
  }
99595
100650
  /**
99596
100651
  * Build a chalker bound to a specific colors module.
@@ -101659,6 +102714,25 @@ async function syncLocalExports(options) {
101659
102714
  const createLock$1 = Util.promisify(import_lockfile.lock);
101660
102715
  const unlock$1 = Util.promisify(import_lockfile.unlock);
101661
102716
  const { posixify } = fyntil;
102717
+ /** FYN_SHORT_PKG_DIR is on only for "1" or "true" (any case); any other value is off. */
102718
+ function shortPkgDirFromEnv() {
102719
+ const v = (process.env.FYN_SHORT_PKG_DIR || "").toLowerCase();
102720
+ return v === "1" || v === "true";
102721
+ }
102722
+ /**
102723
+ * Whether an install lock was left by a process on this host that no longer runs. A lock
102724
+ * without an owner, from an older fyn, is left to lockfile's stale time.
102725
+ */
102726
+ async function installLockOwnerGone(fname) {
102727
+ try {
102728
+ const { pid, host } = JSON.parse(await file_ops_default.readFile(fname, "utf8"));
102729
+ if (host !== os$1.hostname() || !Number.isInteger(pid)) return false;
102730
+ process.kill(pid, 0);
102731
+ return false;
102732
+ } catch (err) {
102733
+ return err.code === "ESRCH";
102734
+ }
102735
+ }
101662
102736
  var Fyn = class Fyn {
101663
102737
  _cliSource;
101664
102738
  _shownMissingFiles;
@@ -101726,7 +102800,7 @@ var Fyn = class Fyn {
101726
102800
  logger$1.info("dep lock time set to", this._lockTime.toString());
101727
102801
  }
101728
102802
  this._installConfig = { time: 0 };
101729
- this._shortPkgDir = Boolean(process.env.FYN_SHORT_PKG_DIR);
102803
+ this._shortPkgDir = shortPkgDirFromEnv();
101730
102804
  if (!_fynpo) this._fynpo = {};
101731
102805
  else if (typeof _fynpo === "object") this._fynpo = _fynpo;
101732
102806
  }
@@ -101809,8 +102883,11 @@ var Fyn = class Fyn {
101809
102883
  } else if (options.centralStore) {
101810
102884
  centralDir = Path.join(this.fynDir, "_central-storage");
101811
102885
  logger$1.debug(`Enabling central store by CLI flag using dir ${centralDir}`);
101812
- } else if (!this._fynpo?.config) return this._central = false;
101813
- else {
102886
+ } else if (!this._fynpo?.config) {
102887
+ if (!await this._defaultCentralStore()) return this._central = false;
102888
+ centralDir = Path.join(this.fynDir, "_central-storage");
102889
+ logger$1.debug(`Enabling central store by default using dir ${centralDir}`);
102890
+ } else {
101814
102891
  centralDir = this._fynpo.config.centralDir;
101815
102892
  if (!centralDir) {
101816
102893
  const storeBaseDir = await fyntil.resolveGitMainWorktreeDir(this._fynpo.dir);
@@ -101827,15 +102904,31 @@ var Fyn = class Fyn {
101827
102904
  }
101828
102905
  return this._central = new FynCentral({
101829
102906
  centralDir,
101830
- hardlink: this.hardlink
102907
+ hardlink: this.hardlink,
102908
+ reflink: this.reflink,
102909
+ copyFallback: this.copyFallback
101831
102910
  });
101832
102911
  }
101833
102912
  /**
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.
102913
+ * A new install uses the central store on macOS when @fynjs/reflink can clone dirs, and on
102914
+ * Linux when it can hardlink or clone, since warm installs then run 2-3x faster than copying.
102915
+ * Files are cloned where possible, else hardlinked, else copied. A .fyn.json that saved
102916
+ * centralDir false keeps copying, and --no-central-store opts out.
102917
+ */
102918
+ async _defaultCentralStore() {
102919
+ if (this._options.centralStore !== void 0 || this._installConfig.centralDir !== void 0) return false;
102920
+ if (process.platform === "darwin") return this.reflink && Boolean(await loadReflinkCloneDir());
102921
+ if (process.platform === "linux") return this.hardlink || this.reflink && Boolean(await loadReflinkCloneFiles());
102922
+ return false;
102923
+ }
102924
+ /**
102925
+ * The central store only pays off when node_modules links or clones from it. Copying out of
102926
+ * it writes every file twice, so it's skipped when neither is known to be possible.
101836
102927
  */
101837
102928
  async _centralCopyOnlyReason(centralDir) {
101838
- if (!this.hardlink && ["darwin", "win32"].includes(process.platform)) return "hardlink is off";
102929
+ if (!this.copyFallback) return;
102930
+ if (!this.hardlink && !this.reflink) return "hardlink and reflink are off";
102931
+ if (!this.hardlink && ["darwin", "win32"].includes(process.platform) && !await loadReflinkCloneFiles()) return "hardlink is off and @fynjs/reflink isn't loaded";
101839
102932
  const deviceOf = async (dir) => {
101840
102933
  for (;;) try {
101841
102934
  return (await file_ops_default.stat(dir)).dev;
@@ -102425,9 +103518,6 @@ var Fyn = class Fyn {
102425
103518
  get refreshOptionals() {
102426
103519
  return this._options.refreshOptionals;
102427
103520
  }
102428
- get ignoreDist() {
102429
- return this._options.ignoreDist;
102430
- }
102431
103521
  get production() {
102432
103522
  return this._options.production;
102433
103523
  }
@@ -102441,9 +103531,20 @@ var Fyn = class Fyn {
102441
103531
  if (process.env.FYN_HARDLINK !== void 0) return fyntil.strToBool(process.env.FYN_HARDLINK);
102442
103532
  return this._installConfig.hardlink !== false;
102443
103533
  }
103534
+ /** --no-reflink never clones store files into node_modules, so @fynjs/reflink isn't used */
103535
+ get reflink() {
103536
+ return this._options.reflink !== false;
103537
+ }
103538
+ /** --no-copy-fallback fails the install on a store file that can't be cloned or hardlinked */
103539
+ get copyFallback() {
103540
+ return this._options.copyFallback !== false;
103541
+ }
102444
103542
  get concurrency() {
102445
103543
  return this._options.concurrency;
102446
103544
  }
103545
+ get extractConcurrency() {
103546
+ return this._options.extractConcurrency;
103547
+ }
102447
103548
  get deepResolve() {
102448
103549
  return this._options.deepResolve;
102449
103550
  }
@@ -102574,10 +103675,18 @@ var Fyn = class Fyn {
102574
103675
  async createInstallLock() {
102575
103676
  await this.createDir(this.getFvDir());
102576
103677
  const fname = this.getFvDir(".installing.lock");
103678
+ if (await installLockOwnerGone(fname)) {
103679
+ logger$1.debug(`taking over ${fname}, its install process is gone`);
103680
+ await file_ops_default.unlink(fname).catch(() => void 0);
103681
+ }
102577
103682
  await createLock$1(fname, {
102578
103683
  wait: 3e3,
102579
103684
  stale: 18e5
102580
103685
  });
103686
+ await file_ops_default.writeFile(fname, JSON.stringify({
103687
+ pid: process.pid,
103688
+ host: os$1.hostname()
103689
+ }));
102581
103690
  return true;
102582
103691
  }
102583
103692
  /**
@@ -102706,9 +103815,10 @@ var Fyn = class Fyn {
102706
103815
  }
102707
103816
  return null;
102708
103817
  }
102709
- async loadJsonForPkg(pkg, dir) {
103818
+ /** @param placed - package.json as an fs worker placed it, so it isn't read back */
103819
+ async loadJsonForPkg(pkg, dir, placed) {
102710
103820
  const fullOutDir = dir || this.getInstalledPkgDir(pkg.name, pkg.version, pkg);
102711
- const json = await fyntil.readPkgJson(fullOutDir, true);
103821
+ const json = await fyntil.readPkgJson(fullOutDir, true, false, placed?.str);
102712
103822
  const pkgId = `${pkg.name}@${pkg.version}`;
102713
103823
  const id = `${json.name}@${json.version}`;
102714
103824
  if (json.version !== pkg.version) {
@@ -102732,13 +103842,11 @@ var Fyn = class Fyn {
102732
103842
  }
102733
103843
  pkg.dir = json[PACKAGE_RAW_INFO].dir;
102734
103844
  pkg.str = json[PACKAGE_RAW_INFO].str;
102735
- try {
102736
- const gypFile = Path.join(fullOutDir, "binding.gyp");
102737
- await file_ops_default.lstat(gypFile);
103845
+ if (placed?.gyp ?? await file_ops_default.lstat(Path.join(fullOutDir, "binding.gyp")).then(() => true, () => false)) {
102738
103846
  json.gypfile = true;
102739
103847
  const scr = json.scripts;
102740
103848
  if (isEmpty(scr) || !scr.install && !scr.postinstall && !scr.postInstall) set(json, "scripts.install", "node-gyp rebuild");
102741
- } catch (err) {}
103849
+ }
102742
103850
  pkg.json = json;
102743
103851
  return json;
102744
103852
  }
@@ -104950,7 +106058,8 @@ var PkgOutdatedProvider = class {
104950
106058
  if (failed) throw failed.reason;
104951
106059
  return {
104952
106060
  records: settled.map((result) => result.status === "fulfilled" ? result.value : void 0).filter((record) => Boolean(record)),
104953
- skipped: skipped.sort()
106061
+ skipped: skipped.sort(),
106062
+ checked: selected.length - skipped.length
104954
106063
  };
104955
106064
  }
104956
106065
  };
@@ -104997,6 +106106,7 @@ const showOutdated = async (fyn, options = {}) => {
104997
106106
  if (output) await new Promise((resolve, reject) => {
104998
106107
  process.stdout.write(`${output}\n`, (error) => error ? reject(error) : resolve());
104999
106108
  });
106109
+ else if (!options.json) logger$1.info(result.checked === 1 ? "The only direct dependency is up to date" : result.checked > 1 ? `All ${result.checked} direct dependencies are up to date` : "No registry dependencies to check");
105000
106110
  if (!options.json && options.packages?.length) for (const name of result.skipped) logger$1.info(`Skipping non-registry dependency ${chalk.cyan(name)}`);
105001
106111
  return result;
105002
106112
  };
@@ -105006,13 +106116,15 @@ const showOutdated = async (fyn, options = {}) => {
105006
106116
  /**
105007
106117
  * Audit cache utilities for storing/retrieving npm security advisory data.
105008
106118
  *
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).
106119
+ * New advisories get published against versions that already exist, and a pinned lockfile
106120
+ * keeps the same cache key for a long time. So entries expire after AUDIT_CACHE_TTL.
105011
106121
  *
105012
106122
  * Cache key is generated from SHA256 hash of the sorted bulk request payload,
105013
106123
  * ensuring different dependency sets get different cache entries.
105014
106124
  */
105015
106125
  const AUDIT_CACHE_PREFIX = "fyn-audit-";
106126
+ /** How long a cached audit result stays fresh, in ms. */
106127
+ const AUDIT_CACHE_TTL = 18e5;
105016
106128
  /**
105017
106129
  * Generate a deterministic cache key from the bulk request payload.
105018
106130
  * Sorts package names and versions to ensure same dependencies = same key.
@@ -105036,12 +106148,17 @@ async function cacheAuditResult(cacheDir, key, result) {
105036
106148
  }
105037
106149
  /**
105038
106150
  * Retrieve cached audit result.
105039
- * Returns null if not found (cache miss).
106151
+ * Returns null if not found, or if `maxAge` (ms) is given and the entry is older than that.
105040
106152
  */
105041
- async function getCachedAuditResult(cacheDir, key) {
106153
+ async function getCachedAuditResult(cacheDir, key, maxAge) {
105042
106154
  const auditCacheDir = Path.join(cacheDir, "audit");
106155
+ const cacache = await loadCacache();
105043
106156
  try {
105044
- const { data } = await (await loadCacache()).get(auditCacheDir, key);
106157
+ if (maxAge !== void 0) {
106158
+ const info = await cacache.get.info(auditCacheDir, key);
106159
+ if (!info || Date.now() - info.time > maxAge) return null;
106160
+ }
106161
+ const { data } = await cacache.get(auditCacheDir, key);
105045
106162
  return JSON.parse(data.toString());
105046
106163
  } catch (err) {
105047
106164
  const code = err.code;
@@ -105057,7 +106174,7 @@ async function getCachedAuditResult(cacheDir, key) {
105057
106174
  *
105058
106175
  * Workflow:
105059
106176
  * 1. Build bulk payload from resolved DepData.pkgs
105060
- * 2. Check cache for existing advisories
106177
+ * 2. Check cache for advisories fetched within AUDIT_CACHE_TTL
105061
106178
  * 3. If miss, POST to /-/npm/v1/security/advisories/bulk
105062
106179
  * 4. Cache response and return
105063
106180
  */
@@ -105166,7 +106283,7 @@ var AuditReport = class {
105166
106283
  }
105167
106284
  const cacheKey = generateCacheKey(payload);
105168
106285
  if (!this._noCache) {
105169
- const cached = await getCachedAuditResult(this._cacheDir, cacheKey);
106286
+ const cached = await getCachedAuditResult(this._cacheDir, cacheKey, AUDIT_CACHE_TTL);
105170
106287
  if (cached) {
105171
106288
  logger$1.debug("Using cached audit result");
105172
106289
  return cached;
@@ -105306,6 +106423,12 @@ var AuditFormatter = class {
105306
106423
  return this.getSeverityLevel(severity) >= this.getSeverityLevel(this._auditLevel);
105307
106424
  }
105308
106425
  /**
106426
+ * Exit code like npm audit: 1 when any vulnerability meets the audit level.
106427
+ */
106428
+ exitCodeFor(filtered) {
106429
+ return filtered.length > 0 ? 1 : 0;
106430
+ }
106431
+ /**
105309
106432
  * Colorize text based on severity.
105310
106433
  */
105311
106434
  colorize(text, severity) {
@@ -105343,7 +106466,7 @@ var AuditFormatter = class {
105343
106466
  const total = filtered.length;
105344
106467
  return {
105345
106468
  output: `audited ${pkgCount} packages\n\n${total} ${total === 1 ? "vulnerability" : "vulnerabilities"} (${parts.join(", ")})\n\nRun \`fyn audit\` for details.`,
105346
- exitCode: 0
106469
+ exitCode: 1
105347
106470
  };
105348
106471
  }
105349
106472
  /**
@@ -105357,7 +106480,7 @@ var AuditFormatter = class {
105357
106480
  metadata: auditResult.metadata,
105358
106481
  advisories: auditResult.advisories
105359
106482
  }, null, 2),
105360
- exitCode: 0
106483
+ exitCode: this.exitCodeFor(filtered)
105361
106484
  };
105362
106485
  }
105363
106486
  /**
@@ -105404,7 +106527,7 @@ var AuditFormatter = class {
105404
106527
  lines.push("");
105405
106528
  return {
105406
106529
  output: lines.join("\n"),
105407
- exitCode: 0
106530
+ exitCode: this.exitCodeFor(filtered)
105408
106531
  };
105409
106532
  }
105410
106533
  /**
@@ -105481,6 +106604,9 @@ var ShowAudit = class {
105481
106604
  }
105482
106605
  /**
105483
106606
  * Run the audit workflow.
106607
+ *
106608
+ * @returns exit code like npm audit: 1 if a vulnerability meets the audit level
106609
+ * or the audit fails, else 0. Errors are logged, not thrown.
105484
106610
  */
105485
106611
  async runAudit() {
105486
106612
  const spinner = cli_logger_default.spinners[1];
@@ -105499,7 +106625,7 @@ var ShowAudit = class {
105499
106625
  }
105500
106626
  if (Object.keys(depData.pkgs).length === 0) {
105501
106627
  logger$1.info("No dependencies to audit");
105502
- return;
106628
+ return 0;
105503
106629
  }
105504
106630
  logger$1.addItem({
105505
106631
  name: FETCH_META,
@@ -105529,7 +106655,7 @@ var ShowAudit = class {
105529
106655
  statProvider
105530
106656
  });
105531
106657
  if (statProvider) await formatter.precomputeStats(vulnerabilities);
105532
- const { output } = formatter.format(auditResult, vulnerabilities);
106658
+ const { output, exitCode } = formatter.format(auditResult, vulnerabilities);
105533
106659
  if (output) console.log(output);
105534
106660
  if (this._opts.auditFile) {
105535
106661
  const { output: jsonOutput } = new AuditFormatter({
@@ -105541,10 +106667,12 @@ var ShowAudit = class {
105541
106667
  const targetPath = Path.isAbsolute(this._opts.auditFile) ? this._opts.auditFile : Path.join(baseDir, this._opts.auditFile);
105542
106668
  await fs.promises.writeFile(targetPath, jsonOutput, "utf8");
105543
106669
  }
106670
+ return exitCode;
105544
106671
  } catch (err) {
105545
106672
  logger$1.removeItem(FETCH_META);
105546
106673
  logger$1.error("Audit failed:", err.message);
105547
106674
  if (process.env.FYN_DEBUG) logger$1.error(err.stack);
106675
+ return 1;
105548
106676
  }
105549
106677
  }
105550
106678
  };
@@ -105558,6 +106686,28 @@ const showAudit = (fyn, opts) => {
105558
106686
  }).runAudit();
105559
106687
  };
105560
106688
 
106689
+ //#endregion
106690
+ //#region cli/show-updates.ts
106691
+ /**
106692
+ * Lines of `name old -> new` for every package whose locked versions changed
106693
+ */
106694
+ const formatUpdates = (before, after) => {
106695
+ const names = [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].sort();
106696
+ const lines = [];
106697
+ for (const name of names) {
106698
+ const oldVersions = (before[name] || []).join(", ");
106699
+ const newVersions = (after[name] || []).join(", ");
106700
+ if (oldVersions === newVersions) continue;
106701
+ lines.push(`${chalk.cyan(name)} ${chalk.red(oldVersions || "(none)")} -> ${chalk.green(newVersions || "(removed)")}`);
106702
+ }
106703
+ return lines;
106704
+ };
106705
+ const showUpdates = (before, after) => {
106706
+ const lines = formatUpdates(before, after);
106707
+ if (lines.length === 0) logger$1.info("All locked versions are already the newest their ranges allow");
106708
+ else logger$1.info(`Updated ${lines.length} packages:\n ${lines.join("\n ")}`);
106709
+ };
106710
+
105561
106711
  //#endregion
105562
106712
  //#region node_modules/.f/_/ini/4.1.1/node_modules/ini/lib/ini.js
105563
106713
  var require_ini = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -105845,7 +106995,7 @@ async function fetchDistTags2(baseUrl, _authToken, options) {
105845
106995
  try {
105846
106996
  return (await options.fetchJSON(pkUrl.toString(), { headers }))["dist-tags"];
105847
106997
  } catch (_a) {
105848
- return {};
106998
+ return;
105849
106999
  }
105850
107000
  }
105851
107001
  /**
@@ -105859,7 +107009,7 @@ async function fetchDistTags2(baseUrl, _authToken, options) {
105859
107009
  function getNpmRegistryInfo(npmConfig, pkg) {
105860
107010
  const pkgNames = pkg.name.split("/");
105861
107011
  const scope = pkgNames.length > 1 ? pkgNames[0] : "";
105862
- const regKey = Object.keys(npmConfig).find((k) => k.endsWith("registry") && (!scope || k.startsWith(scope)));
107012
+ const regKey = scope && Object.keys(npmConfig).find((k) => k.endsWith("registry") && k.startsWith(scope));
105863
107013
  const registry = regKey && npmConfig[regKey] || npmConfig.registry || "https://registry.npmjs.org/";
105864
107014
  const regHost = `//${new URL(registry).host}/`;
105865
107015
  const authKey = Object.keys(npmConfig).find((k) => k.endsWith("_authToken") && k.startsWith(regHost));
@@ -105920,6 +107070,7 @@ async function _internalCheck(options) {
105920
107070
  }));
105921
107071
  };
105922
107072
  if (shouldFetch) {
107073
+ if (!distTags || typeof distTags !== "object") return false;
105923
107074
  let notifiedVersion = typeof existMeta.notifiedVersion === "string" ? existMeta.notifiedVersion : "";
105924
107075
  let notifiedTime = existMeta.notifiedTime || 0;
105925
107076
  const checked = options.checkIsNewer(pkg, distTags, tag);
@@ -107097,6 +108248,20 @@ var FynCli = class {
107097
108248
  const start = Date.now();
107098
108249
  const runAudit = argv.opts?.audit !== false;
107099
108250
  const auditFile = argv.opts?.auditFile;
108251
+ const update = argv.opts?.update;
108252
+ let lockedBefore;
108253
+ const auditAfterInstall = async () => {
108254
+ if (!runAudit) return;
108255
+ try {
108256
+ await showAudit(this.fyn, {
108257
+ colors: this.fyn._options.colors !== false,
108258
+ summary: true,
108259
+ auditFile
108260
+ });
108261
+ } catch (err) {
108262
+ logger$1.warn("Security audit failed:", err.message);
108263
+ }
108264
+ };
107100
108265
  return dist_default.try(() => this.fyn._initializePkg()).then(async () => {
107101
108266
  checkNewVersion(this.fyn._options);
107102
108267
  if (!this.fyn._changeProdMode && !this.fyn._options.forceInstall && this.fyn._installConfig.fynlocal === Boolean(this.fyn.fynlocal) && this.fyn._installConfig.time) {
@@ -107111,6 +108276,7 @@ var FynCli = class {
107111
108276
  }
107112
108277
  installLocked = await this.fyn.createInstallLock();
107113
108278
  await this.fyn.readLockFiles();
108279
+ if (update) lockedBefore = this.unlockForUpdate(update);
107114
108280
  await this.fyn._startInstall();
107115
108281
  const pkg = this.fyn._pkg;
107116
108282
  if (get$1(pkg, "scripts.preinstall")) {
@@ -107211,19 +108377,14 @@ var FynCli = class {
107211
108377
  const end = Date.now();
107212
108378
  await this.fyn.saveFynpoIndirects();
107213
108379
  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.
108380
+ if (lockedBefore) showUpdates(lockedBefore, this.fyn.depLocker.lockedVersions());
108381
+ await auditAfterInstall();
108382
+ }).catch(async (err) => {
108383
+ if (err.message === "No Change") {
108384
+ logger$1.info(`No changes detected since last fyn install - nothing to be done.
107225
108385
  To force install, run 'fyn install --force-install' or 'fyn install --fi'`);
107226
- else failure = err;
108386
+ await auditAfterInstall();
108387
+ } else failure = err;
107227
108388
  }).finally(async () => {
107228
108389
  const localPkgBuild = this.fyn._localPkgBuilder?.waitForDone();
107229
108390
  if (localPkgBuild) try {
@@ -107241,6 +108402,21 @@ var FynCli = class {
107241
108402
  fyntil.exit(0);
107242
108403
  });
107243
108404
  }
108405
+ /**
108406
+ * Drop lock data for `fyn update` so the resolver picks the newest in-range versions
108407
+ *
108408
+ * @param names - packages to update, or empty for all
108409
+ * @returns the locked versions from before
108410
+ */
108411
+ unlockForUpdate(names) {
108412
+ const locker = this.fyn.depLocker;
108413
+ const before = locker.lockedVersions();
108414
+ const missing = names.filter((name) => !before[name]);
108415
+ if (missing.length > 0) throw new Error(`not found in lock data: ${missing.join(", ")}`);
108416
+ const unlocked = locker.unlock(names);
108417
+ logger$1.info(`updating ${unlocked.length} locked packages to the newest versions their ranges allow`);
108418
+ return before;
108419
+ }
107244
108420
  stat(argv) {
107245
108421
  return showStat(this.fyn, argv.args.packages).finally(() => {
107246
108422
  return this._opts.saveLogs && this.saveLogs(this._opts.saveLogs);
@@ -107288,16 +108464,16 @@ var FynCli = class {
107288
108464
  if (this._opts.saveLogs) await this.saveLogs(this._opts.saveLogs);
107289
108465
  }
107290
108466
  }
107291
- audit(argv) {
108467
+ async audit(argv) {
107292
108468
  const opts = {
107293
108469
  json: argv.opts?.json || false,
107294
108470
  omit: argv.opts?.omit || [],
107295
108471
  auditLevel: argv.opts?.auditLevel || "info",
107296
- noCache: argv.opts?.noCache || false
108472
+ noCache: argv.opts?.cache === false
107297
108473
  };
107298
- return showAudit(this.fyn, opts).finally(() => {
107299
- return this._opts.saveLogs && this.saveLogs(this._opts.saveLogs);
107300
- });
108474
+ const exitCode = await showAudit(this.fyn, opts);
108475
+ if (this._opts.saveLogs) await this.saveLogs(this._opts.saveLogs);
108476
+ if (exitCode) fyntil.exit(exitCode);
107301
108477
  }
107302
108478
  /**
107303
108479
  * Remove non-runtime files from a production node_modules. Only runs on demand,
@@ -107818,7 +108994,7 @@ var CliBase = class {
107818
108994
  if (!args) return;
107819
108995
  let seenOptional;
107820
108996
  args.replace(/([<[])([^>\]]*)([>\]])/g, (a, mark, xname) => {
107821
- assert(!this.variadic, `For args specifier of '${this.name}', only the last one can be variadic`);
108997
+ assert(!this.variadic, new InvalidArgSpecifierError(`For args specifier of '${this.name}', only the last one can be variadic`, args));
107822
108998
  const xm = xname.match(/([^\. ]+)?( +)?([^\.]+)?(\.\.+)?([^\.,]*)?( *, *)?([^\.]+)?$/);
107823
108999
  assert(xm, new InvalidArgSpecifierError(`Invalid args specifier '${xname}' for '${this.name}'.`));
107824
109000
  let variadic;
@@ -108026,6 +109202,7 @@ var Options = class {
108026
109202
  makeHelp() {
108027
109203
  const width = this.findLongestOptionHelpText();
108028
109204
  let help = [];
109205
+ setHelpZebra(this.command.ncConfig?.helpZebra !== false);
108029
109206
  resetZebraIndex();
108030
109207
  for (const opt of Object.values(this._options)) {
108031
109208
  const tail = [];
@@ -108249,6 +109426,7 @@ var CommandBase = class CommandBase extends CliBase {
108249
109426
  data.push(strs);
108250
109427
  }
108251
109428
  const cmdWidth = data.reduce((max, n) => n[0].length > max ? n[0].length : max, 0);
109429
+ setHelpZebra(this.ncConfig?.helpZebra !== false);
108252
109430
  resetZebraIndex();
108253
109431
  return data.reduce((help, strs) => {
108254
109432
  const lines = fitLines(strs, " ", " ", cmdWidth, getTerminalWidth());
@@ -108489,6 +109667,9 @@ var CommandNode = class extends ClapNode {
108489
109667
  * Allow you to apply extra config to the parsed object, overriding any `opts` with `source` not start with `cli`.
108490
109668
  *
108491
109669
  * For example, you can allow user to specify options in their `package.json` file, and apply those after the command line is parsed.
109670
+ *
109671
+ * Each key goes to this command and any parsed sub command that declares the option.
109672
+ * Keys that match no declared option are ignored.
108492
109673
  * @param config - Config object containing user options config
108493
109674
  * @param src - Name of the source that provided the config. Default to `user`
108494
109675
  * @returns
@@ -108515,8 +109696,9 @@ var CommandNode = class extends ClapNode {
108515
109696
  this.optCount[camelCaseKey] = this.optCount[matchOpt.name];
108516
109697
  }
108517
109698
  }
108518
- } else if (!this.optNodes[key]) new ClapNodeGenerator(this).addOptionWithArgs(key, [].concat(data.arg), void 0, src);
109699
+ }
108519
109700
  }
109701
+ for (const name in this.subCmdNodes) this.subCmdNodes[name].applyConfig(config, src);
108520
109702
  for (let command = this; command; command = command.getParent()) command._jsonMeta = void 0;
108521
109703
  }
108522
109704
  /**
@@ -108849,6 +110031,7 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108849
110031
  }
108850
110032
  return [null];
108851
110033
  }
110034
+ if (this.optNode && this.takesNegativeNumber(opt)) return this.consumeNonOptAsOption(opt);
108852
110035
  if (this.optNode) {
108853
110036
  this.complete();
108854
110037
  return [null].concat(this.parent.consumeOpt(opt));
@@ -108891,6 +110074,17 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108891
110074
  });
108892
110075
  }
108893
110076
  /**
110077
+ * Whether this open option takes a negative number like `-5` as its next value.
110078
+ * Boolean args don't, so `--flag -5` still reads `-5` as an option.
110079
+ *
110080
+ * @param arg - an argv entry that starts with '-'
110081
+ */
110082
+ takesNegativeNumber(arg) {
110083
+ if (!isNumber(arg)) return false;
110084
+ const args = this.optNode.option.args;
110085
+ return args[Math.min(this.node.argsList.length, args.length - 1)].type !== "boolean";
110086
+ }
110087
+ /**
108894
110088
  *
108895
110089
  * @param data
108896
110090
  * @returns
@@ -108918,7 +110112,11 @@ var ClapNodeGenerator = class ClapNodeGenerator {
108918
110112
  node.argsMap[argIx] = setValue;
108919
110113
  };
108920
110114
  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;
110115
+ else {
110116
+ const value = node.argsList[0];
110117
+ const isBool = isBoolean(value) || !opt.isUnknown && (value === "0" || value === "1");
110118
+ node.argsMap[0] = node.argsList.length > 0 ? this.convertValue(isBool ? "boolean" : isNumber(value) ? "number" : "string", value, opt) : true;
110119
+ }
108922
110120
  if (args.length > 0 && args[args.length - 1].array) {
108923
110121
  const lastIx = args.length - 1;
108924
110122
  if (node.argsList.length > lastIx) setArg(lastIx, args[lastIx], node.argsList.slice(lastIx));
@@ -109248,7 +110446,6 @@ var NixClap = class extends EventEmitter$1 {
109248
110446
  this._config = config;
109249
110447
  this._name = config.name;
109250
110448
  this._version = config.version || false;
109251
- setHelpZebra(config.helpZebra !== false);
109252
110449
  this._versionAlias = config.versionAlias;
109253
110450
  this._helpOpt = config.hasOwnProperty("help") ? config.help : {
109254
110451
  [HELP]: true,
@@ -109284,10 +110481,13 @@ var NixClap = class extends EventEmitter$1 {
109284
110481
  "parse-fail": (parsed) => this.showError(parsed.command.getErrorNodes()[0].error),
109285
110482
  "no-action": () => this.showHelp(/* @__PURE__ */ new Error("No command given"))
109286
110483
  };
109287
- if (!config.noDefaultHandlers) this.on("exit", defaultExit);
109288
110484
  const handlers = config.handlers || {};
109289
- for (const [name, handler] of Object.entries(this._evtHandlers)) {
109290
- const h = handlers.hasOwnProperty(name) ? handlers[name] : handler;
110485
+ const exitHandler = Object.hasOwn(handlers, "exit") ? handlers.exit : !config.noDefaultHandlers && defaultExit;
110486
+ if (typeof exitHandler === "function") this.on("exit", exitHandler);
110487
+ const names = /* @__PURE__ */ new Set([...Object.keys(this._evtHandlers), ...Object.keys(handlers)]);
110488
+ names.delete("exit");
110489
+ for (const name of names) {
110490
+ const h = Object.hasOwn(handlers, name) ? handlers[name] : this._evtHandlers[name];
109291
110491
  if (typeof h === "function") this.on(name, h);
109292
110492
  }
109293
110493
  this._skipExec = config.skipExec;
@@ -109356,6 +110556,23 @@ var NixClap = class extends EventEmitter$1 {
109356
110556
  return this;
109357
110557
  }
109358
110558
  /**
110559
+ * Make a copy of the help option without aliases that other options already use.
110560
+ */
110561
+ _withFreeHelpAliases(existingOptions) {
110562
+ const helpOpt = this._helpOpt;
110563
+ const usedAliases = /* @__PURE__ */ new Set();
110564
+ for (const optName in existingOptions) {
110565
+ const opt = existingOptions[optName];
110566
+ if (optName !== "help" && opt.alias) (Array.isArray(opt.alias) ? opt.alias : [opt.alias]).forEach((a) => usedAliases.add(a));
110567
+ }
110568
+ /* c8 ignore next 3 */
110569
+ const helpAliases = helpOpt.alias ? Array.isArray(helpOpt.alias) ? helpOpt.alias : [helpOpt.alias] : [];
110570
+ return {
110571
+ ...helpOpt,
110572
+ alias: helpAliases.filter((a) => !usedAliases.has(a))
110573
+ };
110574
+ }
110575
+ /**
109359
110576
  * Recursively adds help option to all subcommands.
109360
110577
  * Filters out help aliases that conflict with existing options in the subcommand.
109361
110578
  */
@@ -109364,18 +110581,9 @@ var NixClap = class extends EventEmitter$1 {
109364
110581
  for (const name in commands) {
109365
110582
  const cmd = commands[name];
109366
110583
  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
110584
  if (!existingOptions.hasOwnProperty("help")) cmd.options = {
109374
110585
  ...existingOptions,
109375
- help: {
109376
- ...this._helpOpt,
109377
- alias: filteredAliases
109378
- }
110586
+ help: this._withFreeHelpAliases(existingOptions)
109379
110587
  };
109380
110588
  if (cmd.subCommands) this._addHelpToSubCommands(cmd.subCommands);
109381
110589
  }
@@ -109396,13 +110604,13 @@ var NixClap = class extends EventEmitter$1 {
109396
110604
  let verAlias = ["V", "v"];
109397
110605
  Object.keys(options).forEach((k) => {
109398
110606
  const opt = options[k];
109399
- if (opt.alias) verAlias = verAlias.filter((x) => opt.alias.indexOf(x) < 0);
110607
+ if (opt.alias) verAlias = verAlias.filter((x) => ![].concat(opt.alias).includes(x));
109400
110608
  });
109401
110609
  options.version = this._getVersionOpt(verAlias);
109402
110610
  }
109403
110611
  if (this._helpOpt) {
109404
110612
  options = { ...options };
109405
- options["help"] = this._helpOpt;
110613
+ options["help"] = this._withFreeHelpAliases(options);
109406
110614
  this._addHelpToSubCommands(commands);
109407
110615
  }
109408
110616
  this._options = options;
@@ -109581,6 +110789,7 @@ var NixClap = class extends EventEmitter$1 {
109581
110789
  skipExec(skip = true) {
109582
110790
  this._skipExec = skip;
109583
110791
  this._skipExecDefault = skip;
110792
+ return this;
109584
110793
  }
109585
110794
  /**
109586
110795
  * Parses the given arguments and returns the result.
@@ -109671,9 +110880,10 @@ var NixClap = class extends EventEmitter$1 {
109671
110880
  this._rootCommand.name = this._name;
109672
110881
  }
109673
110882
  const { command, index } = new Parser(this).parse(argv, start);
110883
+ if (this._config.config) command.applyConfig(this._config.config(command) || {}, "user");
110884
+ command.applyDefaults();
109674
110885
  const missing = command.checkRequiredOptions();
109675
110886
  if (missing.length > 0) command.addError(/* @__PURE__ */ new Error("missing these required options " + missing.join(", ")));
109676
- command.applyDefaults();
109677
110887
  command.makeCamelCaseOptions();
109678
110888
  const errorNodes = command.getErrorNodes();
109679
110889
  return {
@@ -116550,8 +117760,7 @@ function setLogLevel(ll) {
116550
117760
  if (ll) {
116551
117761
  const levels = Object.keys(cli_logger_default.Levels);
116552
117762
  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;
117763
+ if ((real ? cli_logger_default.Levels[real] : void 0) !== void 0) logger$1.logLevel = real;
116555
117764
  else {
116556
117765
  logger$1.error(`Invalid log level "${ll}". Supported levels are: ${levels.join(", ")}`);
116557
117766
  fyntil.exit(1);
@@ -116691,12 +117900,21 @@ const options = {
116691
117900
  "central-store": {
116692
117901
  args: "<flag boolean>",
116693
117902
  alias: ["central", "cs"],
116694
- desc: "keep single copy of packages in central store",
116695
- argDefault: "false"
117903
+ 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
117904
  },
116697
117905
  hardlink: {
116698
117906
  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"
117907
+ 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"
117908
+ },
117909
+ reflink: {
117910
+ args: "<flag boolean>",
117911
+ 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",
117912
+ argDefault: "true"
117913
+ },
117914
+ "copy-fallback": {
117915
+ args: "<flag boolean>",
117916
+ desc: "copy central store files that can't be cloned or hardlinked (default). --no-copy-fallback fails the install instead",
117917
+ argDefault: "true"
116700
117918
  },
116701
117919
  copy: {
116702
117920
  args: "[packages string..]",
@@ -116776,11 +117994,6 @@ const options = {
116776
117994
  argDefault: "false",
116777
117995
  desc: "force refresh package meta from registry"
116778
117996
  },
116779
- "ignore-dist": {
116780
- alias: "i",
116781
- args: "<flag boolean>",
116782
- desc: "Ignore host in tarball URL from meta dist."
116783
- },
116784
117997
  "ignore-lock-url": {
116785
117998
  args: "<flag boolean>",
116786
117999
  argDefault: "false",
@@ -116828,6 +118041,11 @@ const options = {
116828
118041
  desc: "Max network concurrency (registry sockets)",
116829
118042
  argDefault: "32"
116830
118043
  },
118044
+ "extract-concurrency": {
118045
+ args: "<num number>",
118046
+ alias: "excc",
118047
+ desc: "Max packages extracted at once (default: CPU cores, at most 4)"
118048
+ },
116831
118049
  "auto-run": {
116832
118050
  args: "<flag boolean>",
116833
118051
  argDefault: "true",
@@ -116972,6 +118190,30 @@ const commands = {
116972
118190
  }
116973
118191
  }
116974
118192
  },
118193
+ update: {
118194
+ args: "[packages string..]",
118195
+ usage: "$0 $1 [packages..]",
118196
+ desc: "Update locked versions to the newest their package.json ranges allow",
118197
+ exec: async (cmd) => {
118198
+ const meta = cmd.jsonMeta;
118199
+ const config = await pickOptions(cmd);
118200
+ Object.assign(config.opts, {
118201
+ forceInstall: true,
118202
+ refreshMeta: true,
118203
+ npmLock: false
118204
+ });
118205
+ const update = meta.args.packages || [];
118206
+ return new FynCli(config).install({ opts: {
118207
+ audit: meta.opts.audit !== false,
118208
+ update
118209
+ } });
118210
+ },
118211
+ options: { audit: {
118212
+ desc: "run security audit after update (use --no-audit to skip)",
118213
+ args: "<flag boolean>",
118214
+ argDefault: "true"
118215
+ } }
118216
+ },
116975
118217
  stat: {
116976
118218
  desc: "Show stats of installed packages",
116977
118219
  usage: "$0 $1 <package-name>[@semver] [...]",
@@ -116986,14 +118228,14 @@ const commands = {
116986
118228
  args: "[packages string..]",
116987
118229
  exec: async (cmd) => {
116988
118230
  const json = Boolean(cmd.jsonMeta.opts.json);
116989
- const previousLogLevel = logger$1._logLevel;
118231
+ const previousLogLevel = logger$1.logLevel;
116990
118232
  if (json) cmd.jsonMeta.opts.logLevel = "none";
116991
118233
  try {
116992
118234
  const config = await pickOptions(cmd);
116993
118235
  config.noStartupInfo = json;
116994
118236
  return await new FynCli(config).outdated(cmd.jsonMeta);
116995
118237
  } finally {
116996
- if (json) logger$1._logLevel = previousLogLevel;
118238
+ if (json) logger$1.logLevel = previousLogLevel;
116997
118239
  }
116998
118240
  },
116999
118241
  options: { json: {
@@ -117022,9 +118264,10 @@ const commands = {
117022
118264
  desc: "Minimum severity level to report (info, low, moderate, high, critical)",
117023
118265
  argDefault: "info"
117024
118266
  },
117025
- "no-cache": {
118267
+ cache: {
117026
118268
  args: "<flag boolean>",
117027
- desc: "Bypass the audit cache and fetch fresh data"
118269
+ desc: "use the audit cache (use --no-cache to fetch fresh data)",
118270
+ argDefault: "true"
117028
118271
  }
117029
118272
  }
117030
118273
  },
@@ -117051,7 +118294,7 @@ const commands = {
117051
118294
  } else {
117052
118295
  logger$1.error(chalk.red("Script execution failed:"));
117053
118296
  logger$1.error(err.message || err.toString());
117054
- if (err.stack && logger$1._logLevel <= cli_logger_default.Levels.debug) logger$1.debug(err.stack);
118297
+ if (err.stack && cli_logger_default.Levels[logger$1.logLevel] <= cli_logger_default.Levels.debug) logger$1.debug(err.stack);
117055
118298
  }
117056
118299
  fyntil.exit(typeof exitCode === "number" ? exitCode : 1);
117057
118300
  }