@williamthorsen/nmr 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.meta/changelog.json +105 -0
  2. package/.readyup/kits/default.js +1042 -137
  3. package/.readyup/manifest.json +151 -8
  4. package/CHANGELOG.md +33 -0
  5. package/README.md +20 -11
  6. package/dist/esm/commands/build-output.js +1 -1
  7. package/dist/esm/commands/build.js +3 -2
  8. package/dist/esm/commands/fmt-cache.d.ts +26 -0
  9. package/dist/esm/commands/fmt-cache.js +71 -0
  10. package/dist/esm/commands/fmt.d.ts +6 -1
  11. package/dist/esm/commands/fmt.js +24 -4
  12. package/dist/esm/config.js +28 -6
  13. package/dist/esm/default-scripts.d.ts +10 -1
  14. package/dist/esm/default-scripts.js +45 -39
  15. package/dist/esm/defineConfig.d.ts +1 -1
  16. package/dist/esm/help.d.ts +1 -1
  17. package/dist/esm/help.js +3 -3
  18. package/dist/esm/helpers/createOutputRecorder.d.ts +17 -0
  19. package/dist/esm/helpers/createOutputRecorder.js +22 -0
  20. package/dist/esm/resolve-scripts.d.ts +5 -3
  21. package/dist/esm/resolve-scripts.js +7 -3
  22. package/dist/esm/resolver.d.ts +5 -4
  23. package/dist/esm/resolver.js +11 -4
  24. package/dist/esm/runCli.js +5 -3
  25. package/dist/esm/runner.d.ts +7 -0
  26. package/dist/esm/runner.js +105 -15
  27. package/dist/esm/steps.d.ts +12 -5
  28. package/dist/esm/steps.js +5 -3
  29. package/dist/esm/taze.d.ts +5 -2
  30. package/dist/esm/taze.js +4 -1
  31. package/dist/esm/types.d.ts +10 -0
  32. package/dist/esm/vitest.js +9 -5
  33. package/dist/esm/workspace-globs.d.ts +5 -0
  34. package/dist/esm/workspace-globs.js +8 -0
  35. package/docs/check-cache.md +2 -0
  36. package/docs/scripts.md +26 -8
  37. package/docs/upgrades.md +1 -1
  38. package/docs/utilities.md +17 -3
  39. package/docs/vitest.md +8 -0
  40. package/package.json +6 -6
@@ -117,17 +117,17 @@ var require_visit = __commonJS({
117
117
  visit.BREAK = BREAK;
118
118
  visit.SKIP = SKIP;
119
119
  visit.REMOVE = REMOVE;
120
- function visit_(key, node, visitor, path2) {
121
- const ctrl = callVisitor(key, node, visitor, path2);
120
+ function visit_(key, node, visitor, path7) {
121
+ const ctrl = callVisitor(key, node, visitor, path7);
122
122
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
123
- replaceNode(key, path2, ctrl);
124
- return visit_(key, ctrl, visitor, path2);
123
+ replaceNode(key, path7, ctrl);
124
+ return visit_(key, ctrl, visitor, path7);
125
125
  }
126
126
  if (typeof ctrl !== "symbol") {
127
127
  if (identity.isCollection(node)) {
128
- path2 = Object.freeze(path2.concat(node));
128
+ path7 = Object.freeze(path7.concat(node));
129
129
  for (let i = 0; i < node.items.length; ++i) {
130
- const ci = visit_(i, node.items[i], visitor, path2);
130
+ const ci = visit_(i, node.items[i], visitor, path7);
131
131
  if (typeof ci === "number")
132
132
  i = ci - 1;
133
133
  else if (ci === BREAK)
@@ -138,13 +138,13 @@ var require_visit = __commonJS({
138
138
  }
139
139
  }
140
140
  } else if (identity.isPair(node)) {
141
- path2 = Object.freeze(path2.concat(node));
142
- const ck = visit_("key", node.key, visitor, path2);
141
+ path7 = Object.freeze(path7.concat(node));
142
+ const ck = visit_("key", node.key, visitor, path7);
143
143
  if (ck === BREAK)
144
144
  return BREAK;
145
145
  else if (ck === REMOVE)
146
146
  node.key = null;
147
- const cv = visit_("value", node.value, visitor, path2);
147
+ const cv = visit_("value", node.value, visitor, path7);
148
148
  if (cv === BREAK)
149
149
  return BREAK;
150
150
  else if (cv === REMOVE)
@@ -165,17 +165,17 @@ var require_visit = __commonJS({
165
165
  visitAsync.BREAK = BREAK;
166
166
  visitAsync.SKIP = SKIP;
167
167
  visitAsync.REMOVE = REMOVE;
168
- async function visitAsync_(key, node, visitor, path2) {
169
- const ctrl = await callVisitor(key, node, visitor, path2);
168
+ async function visitAsync_(key, node, visitor, path7) {
169
+ const ctrl = await callVisitor(key, node, visitor, path7);
170
170
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
171
- replaceNode(key, path2, ctrl);
172
- return visitAsync_(key, ctrl, visitor, path2);
171
+ replaceNode(key, path7, ctrl);
172
+ return visitAsync_(key, ctrl, visitor, path7);
173
173
  }
174
174
  if (typeof ctrl !== "symbol") {
175
175
  if (identity.isCollection(node)) {
176
- path2 = Object.freeze(path2.concat(node));
176
+ path7 = Object.freeze(path7.concat(node));
177
177
  for (let i = 0; i < node.items.length; ++i) {
178
- const ci = await visitAsync_(i, node.items[i], visitor, path2);
178
+ const ci = await visitAsync_(i, node.items[i], visitor, path7);
179
179
  if (typeof ci === "number")
180
180
  i = ci - 1;
181
181
  else if (ci === BREAK)
@@ -186,13 +186,13 @@ var require_visit = __commonJS({
186
186
  }
187
187
  }
188
188
  } else if (identity.isPair(node)) {
189
- path2 = Object.freeze(path2.concat(node));
190
- const ck = await visitAsync_("key", node.key, visitor, path2);
189
+ path7 = Object.freeze(path7.concat(node));
190
+ const ck = await visitAsync_("key", node.key, visitor, path7);
191
191
  if (ck === BREAK)
192
192
  return BREAK;
193
193
  else if (ck === REMOVE)
194
194
  node.key = null;
195
- const cv = await visitAsync_("value", node.value, visitor, path2);
195
+ const cv = await visitAsync_("value", node.value, visitor, path7);
196
196
  if (cv === BREAK)
197
197
  return BREAK;
198
198
  else if (cv === REMOVE)
@@ -219,23 +219,23 @@ var require_visit = __commonJS({
219
219
  }
220
220
  return visitor;
221
221
  }
222
- function callVisitor(key, node, visitor, path2) {
222
+ function callVisitor(key, node, visitor, path7) {
223
223
  if (typeof visitor === "function")
224
- return visitor(key, node, path2);
224
+ return visitor(key, node, path7);
225
225
  if (identity.isMap(node))
226
- return visitor.Map?.(key, node, path2);
226
+ return visitor.Map?.(key, node, path7);
227
227
  if (identity.isSeq(node))
228
- return visitor.Seq?.(key, node, path2);
228
+ return visitor.Seq?.(key, node, path7);
229
229
  if (identity.isPair(node))
230
- return visitor.Pair?.(key, node, path2);
230
+ return visitor.Pair?.(key, node, path7);
231
231
  if (identity.isScalar(node))
232
- return visitor.Scalar?.(key, node, path2);
232
+ return visitor.Scalar?.(key, node, path7);
233
233
  if (identity.isAlias(node))
234
- return visitor.Alias?.(key, node, path2);
234
+ return visitor.Alias?.(key, node, path7);
235
235
  return void 0;
236
236
  }
237
- function replaceNode(key, path2, node) {
238
- const parent = path2[path2.length - 1];
237
+ function replaceNode(key, path7, node) {
238
+ const parent = path7[path7.length - 1];
239
239
  if (identity.isCollection(parent)) {
240
240
  parent.items[key] = node;
241
241
  } else if (identity.isPair(parent)) {
@@ -847,10 +847,10 @@ var require_Collection = __commonJS({
847
847
  var createNode = require_createNode();
848
848
  var identity = require_identity();
849
849
  var Node = require_Node();
850
- function collectionFromPath(schema, path2, value) {
850
+ function collectionFromPath(schema, path7, value) {
851
851
  let v = value;
852
- for (let i = path2.length - 1; i >= 0; --i) {
853
- const k = path2[i];
852
+ for (let i = path7.length - 1; i >= 0; --i) {
853
+ const k = path7[i];
854
854
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
855
855
  const a = [];
856
856
  a[k] = v;
@@ -869,7 +869,7 @@ var require_Collection = __commonJS({
869
869
  sourceObjects: /* @__PURE__ */ new Map()
870
870
  });
871
871
  }
872
- var isEmptyPath = (path2) => path2 == null || typeof path2 === "object" && !!path2[Symbol.iterator]().next().done;
872
+ var isEmptyPath = (path7) => path7 == null || typeof path7 === "object" && !!path7[Symbol.iterator]().next().done;
873
873
  var Collection = class extends Node.NodeBase {
874
874
  constructor(type, schema) {
875
875
  super(type);
@@ -899,11 +899,11 @@ var require_Collection = __commonJS({
899
899
  * be a Pair instance or a `{ key, value }` object, which may not have a key
900
900
  * that already exists in the map.
901
901
  */
902
- addIn(path2, value) {
903
- if (isEmptyPath(path2))
902
+ addIn(path7, value) {
903
+ if (isEmptyPath(path7))
904
904
  this.add(value);
905
905
  else {
906
- const [key, ...rest] = path2;
906
+ const [key, ...rest] = path7;
907
907
  const node = this.get(key, true);
908
908
  if (identity.isCollection(node))
909
909
  node.addIn(rest, value);
@@ -917,8 +917,8 @@ var require_Collection = __commonJS({
917
917
  * Removes a value from the collection.
918
918
  * @returns `true` if the item was found and removed.
919
919
  */
920
- deleteIn(path2) {
921
- const [key, ...rest] = path2;
920
+ deleteIn(path7) {
921
+ const [key, ...rest] = path7;
922
922
  if (rest.length === 0)
923
923
  return this.delete(key);
924
924
  const node = this.get(key, true);
@@ -932,8 +932,8 @@ var require_Collection = __commonJS({
932
932
  * scalar values from their surrounding node; to disable set `keepScalar` to
933
933
  * `true` (collections are always returned intact).
934
934
  */
935
- getIn(path2, keepScalar) {
936
- const [key, ...rest] = path2;
935
+ getIn(path7, keepScalar) {
936
+ const [key, ...rest] = path7;
937
937
  const node = this.get(key, true);
938
938
  if (rest.length === 0)
939
939
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -951,8 +951,8 @@ var require_Collection = __commonJS({
951
951
  /**
952
952
  * Checks if the collection includes a value with the key `key`.
953
953
  */
954
- hasIn(path2) {
955
- const [key, ...rest] = path2;
954
+ hasIn(path7) {
955
+ const [key, ...rest] = path7;
956
956
  if (rest.length === 0)
957
957
  return this.has(key);
958
958
  const node = this.get(key, true);
@@ -962,8 +962,8 @@ var require_Collection = __commonJS({
962
962
  * Sets a value in this collection. For `!!set`, `value` needs to be a
963
963
  * boolean to add/remove the item from the set.
964
964
  */
965
- setIn(path2, value) {
966
- const [key, ...rest] = path2;
965
+ setIn(path7, value) {
966
+ const [key, ...rest] = path7;
967
967
  if (rest.length === 0) {
968
968
  this.set(key, value);
969
969
  } else {
@@ -3478,9 +3478,9 @@ var require_Document = __commonJS({
3478
3478
  this.contents.add(value);
3479
3479
  }
3480
3480
  /** Adds a value to the document. */
3481
- addIn(path2, value) {
3481
+ addIn(path7, value) {
3482
3482
  if (assertCollection(this.contents))
3483
- this.contents.addIn(path2, value);
3483
+ this.contents.addIn(path7, value);
3484
3484
  }
3485
3485
  /**
3486
3486
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -3555,14 +3555,14 @@ var require_Document = __commonJS({
3555
3555
  * Removes a value from the document.
3556
3556
  * @returns `true` if the item was found and removed.
3557
3557
  */
3558
- deleteIn(path2) {
3559
- if (Collection.isEmptyPath(path2)) {
3558
+ deleteIn(path7) {
3559
+ if (Collection.isEmptyPath(path7)) {
3560
3560
  if (this.contents == null)
3561
3561
  return false;
3562
3562
  this.contents = null;
3563
3563
  return true;
3564
3564
  }
3565
- return assertCollection(this.contents) ? this.contents.deleteIn(path2) : false;
3565
+ return assertCollection(this.contents) ? this.contents.deleteIn(path7) : false;
3566
3566
  }
3567
3567
  /**
3568
3568
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -3577,10 +3577,10 @@ var require_Document = __commonJS({
3577
3577
  * scalar values from their surrounding node; to disable set `keepScalar` to
3578
3578
  * `true` (collections are always returned intact).
3579
3579
  */
3580
- getIn(path2, keepScalar) {
3581
- if (Collection.isEmptyPath(path2))
3580
+ getIn(path7, keepScalar) {
3581
+ if (Collection.isEmptyPath(path7))
3582
3582
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3583
- return identity.isCollection(this.contents) ? this.contents.getIn(path2, keepScalar) : void 0;
3583
+ return identity.isCollection(this.contents) ? this.contents.getIn(path7, keepScalar) : void 0;
3584
3584
  }
3585
3585
  /**
3586
3586
  * Checks if the document includes a value with the key `key`.
@@ -3591,10 +3591,10 @@ var require_Document = __commonJS({
3591
3591
  /**
3592
3592
  * Checks if the document includes a value at `path`.
3593
3593
  */
3594
- hasIn(path2) {
3595
- if (Collection.isEmptyPath(path2))
3594
+ hasIn(path7) {
3595
+ if (Collection.isEmptyPath(path7))
3596
3596
  return this.contents !== void 0;
3597
- return identity.isCollection(this.contents) ? this.contents.hasIn(path2) : false;
3597
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path7) : false;
3598
3598
  }
3599
3599
  /**
3600
3600
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -3611,13 +3611,13 @@ var require_Document = __commonJS({
3611
3611
  * Sets a value in this document. For `!!set`, `value` needs to be a
3612
3612
  * boolean to add/remove the item from the set.
3613
3613
  */
3614
- setIn(path2, value) {
3615
- if (Collection.isEmptyPath(path2)) {
3614
+ setIn(path7, value) {
3615
+ if (Collection.isEmptyPath(path7)) {
3616
3616
  this.contents = value;
3617
3617
  } else if (this.contents == null) {
3618
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path2), value);
3618
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path7), value);
3619
3619
  } else if (assertCollection(this.contents)) {
3620
- this.contents.setIn(path2, value);
3620
+ this.contents.setIn(path7, value);
3621
3621
  }
3622
3622
  }
3623
3623
  /**
@@ -5578,9 +5578,9 @@ var require_cst_visit = __commonJS({
5578
5578
  visit.BREAK = BREAK;
5579
5579
  visit.SKIP = SKIP;
5580
5580
  visit.REMOVE = REMOVE;
5581
- visit.itemAtPath = (cst, path2) => {
5581
+ visit.itemAtPath = (cst, path7) => {
5582
5582
  let item = cst;
5583
- for (const [field, index] of path2) {
5583
+ for (const [field, index] of path7) {
5584
5584
  const tok = item?.[field];
5585
5585
  if (tok && "items" in tok) {
5586
5586
  item = tok.items[index];
@@ -5589,23 +5589,23 @@ var require_cst_visit = __commonJS({
5589
5589
  }
5590
5590
  return item;
5591
5591
  };
5592
- visit.parentCollection = (cst, path2) => {
5593
- const parent = visit.itemAtPath(cst, path2.slice(0, -1));
5594
- const field = path2[path2.length - 1][0];
5592
+ visit.parentCollection = (cst, path7) => {
5593
+ const parent = visit.itemAtPath(cst, path7.slice(0, -1));
5594
+ const field = path7[path7.length - 1][0];
5595
5595
  const coll = parent?.[field];
5596
5596
  if (coll && "items" in coll)
5597
5597
  return coll;
5598
5598
  throw new Error("Parent collection not found");
5599
5599
  };
5600
- function _visit(path2, item, visitor) {
5601
- let ctrl = visitor(item, path2);
5600
+ function _visit(path7, item, visitor) {
5601
+ let ctrl = visitor(item, path7);
5602
5602
  if (typeof ctrl === "symbol")
5603
5603
  return ctrl;
5604
5604
  for (const field of ["key", "value"]) {
5605
5605
  const token = item[field];
5606
5606
  if (token && "items" in token) {
5607
5607
  for (let i = 0; i < token.items.length; ++i) {
5608
- const ci = _visit(Object.freeze(path2.concat([[field, i]])), token.items[i], visitor);
5608
+ const ci = _visit(Object.freeze(path7.concat([[field, i]])), token.items[i], visitor);
5609
5609
  if (typeof ci === "number")
5610
5610
  i = ci - 1;
5611
5611
  else if (ci === BREAK)
@@ -5616,10 +5616,10 @@ var require_cst_visit = __commonJS({
5616
5616
  }
5617
5617
  }
5618
5618
  if (typeof ctrl === "function" && field === "key")
5619
- ctrl = ctrl(item, path2);
5619
+ ctrl = ctrl(item, path7);
5620
5620
  }
5621
5621
  }
5622
- return typeof ctrl === "function" ? ctrl(item, path2) : ctrl;
5622
+ return typeof ctrl === "function" ? ctrl(item, path7) : ctrl;
5623
5623
  }
5624
5624
  exports.visit = visit;
5625
5625
  }
@@ -7270,7 +7270,7 @@ var require_public_api = __commonJS({
7270
7270
  }
7271
7271
  return doc;
7272
7272
  }
7273
- function parse2(src, reviver, options) {
7273
+ function parse3(src, reviver, options) {
7274
7274
  let _reviver = void 0;
7275
7275
  if (typeof reviver === "function") {
7276
7276
  _reviver = reviver;
@@ -7311,7 +7311,7 @@ var require_public_api = __commonJS({
7311
7311
  return value.toString(options);
7312
7312
  return new Document.Document(value, _replacer, options).toString(options);
7313
7313
  }
7314
- exports.parse = parse2;
7314
+ exports.parse = parse3;
7315
7315
  exports.parseAllDocuments = parseAllDocuments;
7316
7316
  exports.parseDocument = parseDocument;
7317
7317
  exports.stringify = stringify;
@@ -7371,15 +7371,16 @@ var require_dist = __commonJS({
7371
7371
  });
7372
7372
 
7373
7373
  // .readyup/kits/default.ts
7374
- import { existsSync, globSync, readdirSync as readdirSync2 } from "node:fs";
7374
+ import { execFile } from "node:child_process";
7375
+ import { existsSync as existsSync3, globSync as globSync2, readdirSync as readdirSync2 } from "node:fs";
7375
7376
  import { basename, dirname, join, posix, sep } from "node:path";
7376
7377
 
7377
- // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.2/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
7378
+ // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.3/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/isError.js
7378
7379
  function isError(error) {
7379
7380
  return error instanceof Error || Object.prototype.toString.call(error) === "[object Error]";
7380
7381
  }
7381
7382
 
7382
- // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.2/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/describeError.js
7383
+ // ../../node_modules/.pnpm/@williamthorsen+toolbelt.errors@0.8.3/node_modules/@williamthorsen/toolbelt.errors/dist/esm/4-release/describeError.js
7383
7384
  function describeError(error) {
7384
7385
  try {
7385
7386
  if (isError(error) && typeof error.message === "string" && error.message !== "") {
@@ -7392,7 +7393,7 @@ function describeError(error) {
7392
7393
  }
7393
7394
 
7394
7395
  // .readyup/kits/default.ts
7395
- var import_yaml = __toESM(require_dist(), 1);
7396
+ var import_yaml2 = __toESM(require_dist(), 1);
7396
7397
  import { defineRdyKit } from "readyup";
7397
7398
  import {
7398
7399
  discoverWorkspaces,
@@ -7407,14 +7408,265 @@ import {
7407
7408
  readPackageJson
7408
7409
  } from "readyup/check-utils";
7409
7410
 
7411
+ // src/config.ts
7412
+ import { existsSync as existsSync2 } from "node:fs";
7413
+ import { pathToFileURL } from "node:url";
7414
+
7415
+ // src/helpers/config-path.ts
7416
+ import path from "node:path";
7417
+ var CONFIG_DIR = ".config";
7418
+ var CONFIG_FILENAME = "nmr.config.ts";
7419
+ var CONFIG_RELATIVE_PATH = path.join(CONFIG_DIR, CONFIG_FILENAME);
7420
+ function resolveConfigPath(baseDir) {
7421
+ return path.join(baseDir, CONFIG_DIR, CONFIG_FILENAME);
7422
+ }
7423
+
7424
+ // src/helpers/findClosestName.ts
7425
+ function findClosestName(name, candidates) {
7426
+ const ceiling = Math.max(1, Math.floor(name.length / 4));
7427
+ let closestName;
7428
+ let shortestDistance = Infinity;
7429
+ for (const candidate of candidates) {
7430
+ const distance = measureDistance(name, candidate);
7431
+ if (distance < shortestDistance) {
7432
+ shortestDistance = distance;
7433
+ closestName = candidate;
7434
+ }
7435
+ }
7436
+ return shortestDistance <= ceiling ? closestName : void 0;
7437
+ }
7438
+ function measureDistance(first, second) {
7439
+ if (first.length === 0 || second.length === 0) {
7440
+ return Math.max(first.length, second.length);
7441
+ }
7442
+ let previousRow = Array.from({ length: second.length + 1 }, (_unused, column) => column);
7443
+ let currentRow = Array.from({ length: second.length + 1 }, () => 0);
7444
+ for (let row = 1; row <= first.length; row += 1) {
7445
+ currentRow[0] = row;
7446
+ for (let column = 1; column <= second.length; column += 1) {
7447
+ const substitution = readCell(previousRow, column - 1) + (first[row - 1] === second[column - 1] ? 0 : 1);
7448
+ currentRow[column] = Math.min(
7449
+ readCell(currentRow, column - 1) + 1,
7450
+ readCell(previousRow, column) + 1,
7451
+ substitution
7452
+ );
7453
+ }
7454
+ const completedRow = currentRow;
7455
+ currentRow = previousRow;
7456
+ previousRow = completedRow;
7457
+ }
7458
+ return readCell(previousRow, second.length);
7459
+ }
7460
+ function readCell(row, index) {
7461
+ const value = row[index];
7462
+ if (value === void 0) {
7463
+ throw new RangeError(`No cell at index ${index} in a row of ${row.length}`);
7464
+ }
7465
+ return value;
7466
+ }
7467
+
7468
+ // src/helpers/hook-name.ts
7469
+ function isHookName(key) {
7470
+ return key.length > 4 && key.endsWith(":pre") || key.length > 5 && key.endsWith(":post");
7471
+ }
7472
+
7473
+ // src/helpers/type-guards.ts
7474
+ function isObject(value) {
7475
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7476
+ }
7477
+ function isStringRecord(value) {
7478
+ return isObject(value) && Object.values(value).every((entry) => typeof entry === "string");
7479
+ }
7480
+
7481
+ // src/resolver.ts
7482
+ import { readFileSync as readFileSync2 } from "node:fs";
7483
+
7484
+ // ../nmr-core/dist/esm/hasErrnoCode.js
7485
+ function hasErrnoCode(error, code) {
7486
+ return isError(error) && "code" in error && error.code === code;
7487
+ }
7488
+
7489
+ // src/helpers/package-json.ts
7490
+ import path4 from "node:path";
7491
+
7492
+ // src/UserError.ts
7493
+ var UserError = class extends Error {
7494
+ constructor(message) {
7495
+ super(message);
7496
+ this.name = "UserError";
7497
+ }
7498
+ };
7499
+
7500
+ // ../nmr-core/dist/esm/workspace.js
7501
+ import { existsSync, readFileSync } from "node:fs";
7502
+ import path3 from "node:path";
7503
+ var import_yaml = __toESM(require_dist(), 1);
7504
+
7505
+ // ../nmr-core/dist/esm/helpers/workspace-patterns.js
7506
+ import { globSync } from "node:fs";
7507
+ import path2 from "node:path";
7508
+ var MANIFEST = "package.json";
7509
+ var ALWAYS_EXCLUDED = ["**/node_modules/**"];
7510
+ function matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns) {
7511
+ const options = {
7512
+ cwd: monorepoRoot,
7513
+ exclude: [...excludedPatterns, ...ALWAYS_EXCLUDED],
7514
+ followSymlinks: true
7515
+ };
7516
+ const matches = globSync([...includedPatterns], options);
7517
+ const dirs = matches.map((match) => path2.dirname(path2.resolve(monorepoRoot, match)));
7518
+ return [...new Set(dirs)].toSorted();
7519
+ }
7520
+ function resolvePackageDirs(monorepoRoot, patterns) {
7521
+ const { excludedPatterns, includedPatterns } = splitWorkspacePatterns(patterns);
7522
+ if (includedPatterns.length === 0) {
7523
+ return [];
7524
+ }
7525
+ return matchPackageDirs(monorepoRoot, includedPatterns, excludedPatterns);
7526
+ }
7527
+ function splitWorkspacePatterns(patterns) {
7528
+ const includedPatterns = [];
7529
+ const excludedPatterns = [];
7530
+ for (const pattern of patterns) {
7531
+ if (pattern.trim() === "")
7532
+ continue;
7533
+ const isNegated = pattern.startsWith("!");
7534
+ const target = isNegated ? excludedPatterns : includedPatterns;
7535
+ target.push(buildManifestPattern(isNegated ? pattern.slice(1) : pattern));
7536
+ }
7537
+ return { excludedPatterns, includedPatterns };
7538
+ }
7539
+ function buildManifestPattern(pattern) {
7540
+ return `${pattern.replace(/\/?$/, "")}/${MANIFEST}`;
7541
+ }
7542
+
7543
+ // ../nmr-core/dist/esm/workspace.js
7544
+ var WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
7545
+ function isMonorepoRoot(dir) {
7546
+ return existsSync(path3.join(dir, WORKSPACE_MANIFEST));
7547
+ }
7548
+ function resolveWorkspace(monorepoRoot) {
7549
+ const manifestRead = readWorkspaceManifest(monorepoRoot);
7550
+ if (manifestRead.kind === "absent") {
7551
+ return { kind: "not-a-workspace" };
7552
+ }
7553
+ if (manifestRead.kind === "unreadable") {
7554
+ return { cause: "unreadable-manifest", kind: "empty", patterns: [] };
7555
+ }
7556
+ const packagesRead = readDeclaredPackages(manifestRead.value);
7557
+ if (packagesRead.kind !== "patterns") {
7558
+ const cause = packagesRead.kind === "absent" ? "no-packages-list" : "unreadable-packages";
7559
+ return { cause, kind: "empty", patterns: [] };
7560
+ }
7561
+ const { patterns } = packagesRead;
7562
+ const packageDirs = resolvePackageDirs(monorepoRoot, patterns);
7563
+ if (packageDirs.length > 0) {
7564
+ return { kind: "packages", packageDirs, patterns };
7565
+ }
7566
+ return { cause: diagnoseEmptyCause(monorepoRoot, patterns), kind: "empty", patterns };
7567
+ }
7568
+ function diagnoseEmptyCause(monorepoRoot, patterns) {
7569
+ const { excludedPatterns, includedPatterns } = splitWorkspacePatterns(patterns);
7570
+ if (includedPatterns.length === 0) {
7571
+ return "no-pattern";
7572
+ }
7573
+ if (excludedPatterns.length > 0 && matchPackageDirs(monorepoRoot, includedPatterns, []).length > 0) {
7574
+ return "all-excluded";
7575
+ }
7576
+ return "no-package";
7577
+ }
7578
+ function readDeclaredPackages(parsedManifest) {
7579
+ if (!isObject2(parsedManifest))
7580
+ return { kind: "absent" };
7581
+ const packages = parsedManifest["packages"];
7582
+ if (packages === void 0 || packages === null)
7583
+ return { kind: "absent" };
7584
+ if (!Array.isArray(packages))
7585
+ return { kind: "unreadable" };
7586
+ if (!packages.every((entry) => typeof entry === "string"))
7587
+ return { kind: "unreadable" };
7588
+ return packages.length === 0 ? { kind: "absent" } : { kind: "patterns", patterns: packages };
7589
+ }
7590
+ function isObject2(value) {
7591
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7592
+ }
7593
+ function readWorkspaceManifest(monorepoRoot) {
7594
+ const workspaceFile = path3.join(monorepoRoot, WORKSPACE_MANIFEST);
7595
+ if (!existsSync(workspaceFile)) {
7596
+ return { kind: "absent" };
7597
+ }
7598
+ try {
7599
+ return { kind: "parsed", value: (0, import_yaml.parse)(readFileSync(workspaceFile, "utf8")) };
7600
+ } catch {
7601
+ return { kind: "unreadable" };
7602
+ }
7603
+ }
7604
+
7605
+ // src/workspace.ts
7606
+ var WORKSPACE_MANIFEST2 = "pnpm-workspace.yaml";
7607
+ function getWorkspacePackageDirs(monorepoRoot) {
7608
+ const resolution = resolveWorkspace(monorepoRoot);
7609
+ if (resolution.kind === "not-a-workspace") {
7610
+ throw new UserError(`Not a monorepo root: no ${WORKSPACE_MANIFEST2} in ${monorepoRoot}`);
7611
+ }
7612
+ return resolution.kind === "packages" ? resolution.packageDirs : [];
7613
+ }
7614
+
7615
+ // src/helpers/package-json.ts
7616
+ function parsePackageJson(rawText, file) {
7617
+ try {
7618
+ return JSON.parse(rawText);
7619
+ } catch (error) {
7620
+ throw new UserError(`Invalid package.json at ${file}: ${describeError(error)}`);
7621
+ }
7622
+ }
7623
+ function readScriptRecord(dir, scripts) {
7624
+ const result = {};
7625
+ for (const [key, value] of Object.entries(scripts)) {
7626
+ if (typeof value !== "string") {
7627
+ throw new UserError(formatMalformedScript(dir, key, value));
7628
+ }
7629
+ result[key] = value;
7630
+ }
7631
+ return result;
7632
+ }
7633
+ function resolvePackageJsonPath(dir) {
7634
+ return path4.join(dir, "package.json");
7635
+ }
7636
+ function formatMalformedScript(dir, key, value) {
7637
+ const rejection = `Invalid package.json at ${resolvePackageJsonPath(dir)}: \`scripts.${key}\` must be a string.`;
7638
+ if (!Array.isArray(value)) {
7639
+ return rejection;
7640
+ }
7641
+ const field = isMonorepoRoot(dir) ? "rootScripts" : "workspaceScripts";
7642
+ return `${rejection} A step list belongs in \`${CONFIG_RELATIVE_PATH}\` under \`${field}\`.`;
7643
+ }
7644
+
7645
+ // src/steps.ts
7646
+ var SHELL_SAFE_TOKEN = /^[\w@%+=:,./-]+$/;
7647
+ function findUnexpressibleToken(element) {
7648
+ return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
7649
+ }
7650
+ function quoteToken(token) {
7651
+ if (SHELL_SAFE_TOKEN.test(token)) {
7652
+ return token;
7653
+ }
7654
+ return "'" + token.replaceAll("'", String.raw`'\''`) + "'";
7655
+ }
7656
+ function tokenize(element) {
7657
+ return element.split(/\s+/).filter((token) => token.length > 0);
7658
+ }
7659
+
7410
7660
  // src/default-scripts.ts
7411
7661
  var GATE_PROJECTS = "--project unit --project tool";
7412
7662
  var TYPECHECK_STEP = { run: "typecheck", shouldDeclineArguments: true };
7413
7663
  var ROOT_TYPECHECK_STEP = { run: "root:typecheck", shouldDeclineArguments: true };
7664
+ var LINT_CHECK_STEP = { run: "lint:check", shouldRunInBackground: true };
7665
+ var LINT_STRICT_STEP = { run: "lint:strict", shouldRunInBackground: true };
7414
7666
  var workspaceScripts = {
7415
7667
  build: ["compile"],
7416
- check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
7417
- "check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
7668
+ check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7669
+ "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7418
7670
  clean: "nmr-clean",
7419
7671
  compile: "nmr-compile",
7420
7672
  fix: ["lint", "fmt"],
@@ -7436,62 +7688,411 @@ var workspaceScripts = {
7436
7688
  upgrade: "nmr-report-catalog && nmr-taze",
7437
7689
  "view-coverage": "open coverage/index.html"
7438
7690
  };
7439
- var rootScripts = {
7440
- audit: ["audit:prod", "audit:dev"],
7441
- "audit:dev": "pnpm exec v11y --dev",
7442
- "audit:prod": "pnpm exec v11y --prod",
7443
- build: ["-R build"],
7444
- check: [TYPECHECK_STEP, "fmt:check", "lint:check", "test"],
7445
- "check:strict": [TYPECHECK_STEP, "fmt:check", "lint:strict", "test"],
7446
- // Excludes the audit, which in CI has a workflow of its own. The narrowed check runs against the build, so
7447
- // the build declines the arguments rather than being narrowed by them.
7448
- ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
7449
- clean: "nmr-clean",
7450
- fix: ["lint", "fmt"],
7451
- "fix:check": ["fmt:check", "lint:check"],
7452
- fmt: "nmr-fmt --write",
7453
- "fmt:check": "nmr-fmt --check",
7454
- lint: "eslint --fix .",
7455
- "lint:check": "eslint .",
7456
- "lint:strict": "strict-lint",
7457
- // The audit takes seconds and `ci` takes minutes, so the cheap gate fails first. The audit reads the
7458
- // dependency tree, and an argument narrowing the code under test does not say anything about that tree.
7459
- prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
7460
- "report-overrides": "nmr-report-overrides",
7461
- "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
7462
- "root:lint": "eslint --fix --ignore-pattern 'packages/**' .",
7463
- "root:lint:check": "eslint --ignore-pattern 'packages/**' .",
7464
- "root:lint:strict": "strict-lint --ignore-pattern 'packages/**' .",
7465
- "root:test": `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
7466
- "root:test:all": "vitest --config ./vitest.root.config.ts",
7467
- "root:test:tool": "vitest --config ./vitest.root.config.ts --project tool",
7468
- "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
7469
- "root:typecheck": "tsgo --noEmit",
7470
- // Includes the override report for the same reason `upgrade` does: Both end in the tool that rewrites a
7471
- // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
7472
- "root:upgrade": "nmr-report-overrides && nmr-taze",
7473
- test: ["root:test", "-R test"],
7474
- "test:all": ["root:test:all", "-R test:all"],
7475
- "test:coverage": ["root:test", "-R test:coverage"],
7476
- "test:tool": ["root:test:tool", "-R test:tool"],
7477
- "test:unit": ["root:test:unit", "-R test:unit"],
7478
- "test:watch": `vitest ${GATE_PROJECTS} --watch`,
7479
- // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
7480
- // default options at the root and searching for it in every package.
7481
- typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
7482
- // The command is a string because neither half names an nmr command: Both are binaries, and a composite
7483
- // element can name only a command.
7484
- upgrade: "nmr-report-overrides && nmr-taze --recursive"
7485
- };
7691
+ function buildRootScripts(workspaceGlobs) {
7692
+ const ignorePatterns = workspaceGlobs.map((glob) => `--ignore-pattern ${quoteToken(glob)} `).join("");
7693
+ return {
7694
+ audit: ["audit:prod", "audit:dev"],
7695
+ "audit:dev": "pnpm exec v11y --dev",
7696
+ "audit:prod": "pnpm exec v11y --prod",
7697
+ build: ["-R build"],
7698
+ check: [LINT_CHECK_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7699
+ "check:strict": [LINT_STRICT_STEP, TYPECHECK_STEP, "fmt:check", "test"],
7700
+ // Excludes the audit, which in CI has a workflow of its own. The narrowed check runs against the build, so
7701
+ // the build declines the arguments rather than being narrowed by them.
7702
+ ci: [{ run: "build", shouldDeclineArguments: true }, "check:strict"],
7703
+ clean: "nmr-clean",
7704
+ fix: ["lint", "fmt"],
7705
+ "fix:check": ["fmt:check", "lint:check"],
7706
+ fmt: "nmr-fmt --write",
7707
+ "fmt:check": "nmr-fmt --check",
7708
+ lint: "eslint --fix .",
7709
+ "lint:check": "eslint .",
7710
+ "lint:strict": "strict-lint",
7711
+ // The audit takes seconds and `ci` takes minutes, so the cheap gate fails first. The audit reads the
7712
+ // dependency tree, and an argument narrowing the code under test does not say anything about that tree.
7713
+ prepush: [{ run: "audit", shouldDeclineArguments: true }, "ci"],
7714
+ "report-overrides": "nmr-report-overrides",
7715
+ "root:check": [ROOT_TYPECHECK_STEP, "fmt:check", "root:lint:check", "root:test"],
7716
+ "root:lint": `eslint --fix ${ignorePatterns}.`,
7717
+ "root:lint:check": `eslint ${ignorePatterns}.`,
7718
+ "root:lint:strict": `strict-lint ${ignorePatterns}.`,
7719
+ "root:test": `vitest --config ./vitest.root.config.ts ${GATE_PROJECTS}`,
7720
+ "root:test:all": "vitest --config ./vitest.root.config.ts",
7721
+ "root:test:tool": "vitest --config ./vitest.root.config.ts --project tool",
7722
+ "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
7723
+ "root:typecheck": "tsgo --noEmit",
7724
+ // Includes the override report for the same reason `upgrade` does: Both end in the tool that rewrites a
7725
+ // `pnpm.overrides` block, so both need the reporter's rejection ahead of them.
7726
+ "root:upgrade": "nmr-report-overrides && nmr-taze",
7727
+ test: ["root:test", "-R test"],
7728
+ "test:all": ["root:test:all", "-R test:all"],
7729
+ "test:coverage": ["root:test", "-R test:coverage"],
7730
+ "test:tool": ["root:test:tool", "-R test:tool"],
7731
+ "test:unit": ["root:test:unit", "-R test:unit"],
7732
+ "test:watch": `vitest ${GATE_PROJECTS} --watch`,
7733
+ // Neither step is narrowable, so `nmr typecheck <file>` is rejected rather than checking that file under
7734
+ // default options at the root and searching for it in every package.
7735
+ typecheck: [ROOT_TYPECHECK_STEP, { run: "-R typecheck", shouldDeclineArguments: true }],
7736
+ // The command is a string because neither half names an nmr command: Both are binaries, and a composite
7737
+ // element can name only a command.
7738
+ upgrade: "nmr-report-overrides && nmr-taze --recursive"
7739
+ };
7740
+ }
7741
+
7742
+ // src/workspace-globs.ts
7743
+ import path5 from "node:path";
7744
+ function toWorkspacePackageGlobs(monorepoRoot, packageDirs) {
7745
+ return packageDirs.map((dir) => path5.relative(monorepoRoot, dir).split(path5.sep).join("/")).filter((relativeDir) => relativeDir !== "").map((relativeDir) => `${relativeDir}/**`).toSorted();
7746
+ }
7486
7747
 
7487
7748
  // src/resolve-scripts.ts
7488
- function getDefaultRootScripts() {
7489
- return { ...rootScripts };
7749
+ function getDefaultWorkspaceScripts() {
7750
+ return { ...workspaceScripts };
7751
+ }
7752
+ function getDefaultRootScripts(monorepoRoot) {
7753
+ const resolution = resolveWorkspace(monorepoRoot);
7754
+ const packageDirs = resolution.kind === "packages" ? resolution.packageDirs : [];
7755
+ return buildRootScripts(toWorkspacePackageGlobs(monorepoRoot, packageDirs));
7756
+ }
7757
+
7758
+ // src/resolver.ts
7759
+ function readPackageJsonScripts(packageDir) {
7760
+ const file = resolvePackageJsonPath(packageDir);
7761
+ let rawText;
7762
+ try {
7763
+ rawText = readFileSync2(file, "utf8");
7764
+ } catch (error) {
7765
+ if (hasErrnoCode(error, "ENOENT")) {
7766
+ return void 0;
7767
+ }
7768
+ throw error;
7769
+ }
7770
+ const parsedManifest = parsePackageJson(rawText, file);
7771
+ if (!isObject(parsedManifest)) return void 0;
7772
+ const scripts = parsedManifest["scripts"];
7773
+ if (!isObject(scripts)) return void 0;
7774
+ return readScriptRecord(packageDir, scripts);
7775
+ }
7776
+ function buildWorkspaceRegistry(config) {
7777
+ return {
7778
+ ...getDefaultWorkspaceScripts(),
7779
+ ...config.workspaceScripts
7780
+ };
7781
+ }
7782
+ function buildRootRegistry(config, monorepoRoot) {
7783
+ return {
7784
+ ...getDefaultRootScripts(monorepoRoot),
7785
+ ...config.rootScripts
7786
+ };
7787
+ }
7788
+
7789
+ // src/verbosity.ts
7790
+ var COMMAND_VERBOSITIES = ["full", "quiet"];
7791
+ function formatVerbosityRejection(source, value) {
7792
+ return `${source} is \`${value}\`, which is not one of: ${COMMAND_VERBOSITIES.join(", ")}`;
7793
+ }
7794
+ function isCommandVerbosity(value) {
7795
+ const names = COMMAND_VERBOSITIES;
7796
+ return names.includes(value);
7797
+ }
7798
+
7799
+ // src/config.ts
7800
+ var CONFIG_TIERS = {
7801
+ root: {
7802
+ label: "a monorepo-root config",
7803
+ honoredKeys: ["checkCache", "devBin", "output", "rootScripts", "selfHostedRunner", "workspaceScripts"],
7804
+ elsewhere: "the package's own config"
7805
+ },
7806
+ workspace: {
7807
+ label: "a package config",
7808
+ honoredKeys: ["build"],
7809
+ elsewhere: "the monorepo-root config"
7810
+ }
7811
+ };
7812
+ var RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
7813
+ var RECOGNIZED_BUILD_KEYS = ["extraIgnorePatterns"];
7814
+ var RECOGNIZED_CHECK_CACHE_KEYS = ["enabled", "excludeCommands", "extraCommands"];
7815
+ var RECOGNIZED_OUTPUT_KEYS = ["commandVerbosity", "extraAgentEnvVars"];
7816
+ var RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ["exempt"];
7817
+ var RECOGNIZED_STEP_KEYS = ["run", "shouldDeclineArguments", "shouldRunInBackground"];
7818
+ var RETIRED_STEP_KEYS = /* @__PURE__ */ new Map([["declinesArgs", "shouldDeclineArguments"]]);
7819
+ function isScriptRecord(value) {
7820
+ if (!isObject(value)) return false;
7821
+ for (const v of Object.values(value)) {
7822
+ if (typeof v === "string") continue;
7823
+ if (!Array.isArray(v) || !v.every(isScriptElement)) return false;
7824
+ }
7825
+ return true;
7826
+ }
7827
+ function isScriptElement(value) {
7828
+ if (typeof value === "string") return true;
7829
+ if (!isObject(value) || typeof value["run"] !== "string") return false;
7830
+ return isOptionalBoolean(value["shouldDeclineArguments"]) && isOptionalBoolean(value["shouldRunInBackground"]);
7831
+ }
7832
+ function validateScriptField(value, fieldName, configPath) {
7833
+ if (!Object.hasOwn(value, fieldName) || value[fieldName] === void 0) {
7834
+ return void 0;
7835
+ }
7836
+ const scripts = value[fieldName];
7837
+ if (!isScriptRecord(scripts)) {
7838
+ throw new UserError(
7839
+ `Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, where an element is a command string or \`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }\``
7840
+ );
7841
+ }
7842
+ assertValidElements(scripts, fieldName, configPath);
7843
+ return scripts;
7844
+ }
7845
+ function assertValidElements(scripts, fieldName, configPath) {
7846
+ for (const [command, script] of Object.entries(scripts)) {
7847
+ if (typeof script === "string") continue;
7848
+ for (const spec of script) {
7849
+ if (typeof spec !== "string") {
7850
+ assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
7851
+ assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
7852
+ }
7853
+ const element = typeof spec === "string" ? spec : spec.run;
7854
+ const token = findUnexpressibleToken(element);
7855
+ if (token === void 0) continue;
7856
+ throw new UserError(
7857
+ `Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains \`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and reference that name here.`
7858
+ );
7859
+ }
7860
+ }
7861
+ }
7862
+ function isOptionalBoolean(value) {
7863
+ return value === void 0 || typeof value === "boolean";
7864
+ }
7865
+ function isStringArray(value) {
7866
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
7867
+ }
7868
+ function validateBuildField(value, configPath) {
7869
+ const build = value["build"];
7870
+ if (build === void 0) {
7871
+ return void 0;
7872
+ }
7873
+ if (!isObject(build)) {
7874
+ throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
7875
+ }
7876
+ assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, "build.");
7877
+ const extraIgnorePatterns = build["extraIgnorePatterns"];
7878
+ if (extraIgnorePatterns !== void 0 && !isStringArray(extraIgnorePatterns)) {
7879
+ throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
7880
+ }
7881
+ return extraIgnorePatterns === void 0 ? {} : { extraIgnorePatterns };
7882
+ }
7883
+ function validateCheckCacheField(value, configPath) {
7884
+ const checkCache = value["checkCache"];
7885
+ if (checkCache === void 0) {
7886
+ return void 0;
7887
+ }
7888
+ if (!isObject(checkCache)) {
7889
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
7890
+ }
7891
+ assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, "checkCache.");
7892
+ const config = {};
7893
+ const enabledValue = checkCache["enabled"];
7894
+ if (enabledValue !== void 0) {
7895
+ if (typeof enabledValue !== "boolean") {
7896
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
7897
+ }
7898
+ config.enabled = enabledValue;
7899
+ }
7900
+ for (const field of ["excludeCommands", "extraCommands"]) {
7901
+ const commands = checkCache[field];
7902
+ if (commands === void 0) {
7903
+ continue;
7904
+ }
7905
+ if (!isStringArray(commands)) {
7906
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
7907
+ }
7908
+ config[field] = commands;
7909
+ }
7910
+ return config;
7911
+ }
7912
+ function validateSelfHostedRunnerField(value, configPath) {
7913
+ const selfHostedRunner = value["selfHostedRunner"];
7914
+ if (selfHostedRunner === void 0) {
7915
+ return void 0;
7916
+ }
7917
+ if (!isObject(selfHostedRunner)) {
7918
+ throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
7919
+ }
7920
+ assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, "selfHostedRunner.");
7921
+ const exempt = selfHostedRunner["exempt"];
7922
+ if (typeof exempt !== "string" || exempt.trim() === "") {
7923
+ throw new UserError(
7924
+ `Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`
7925
+ );
7926
+ }
7927
+ return { exempt };
7928
+ }
7929
+ function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
7930
+ const { checkCache } = config;
7931
+ if (checkCache === void 0 || !isMonorepoRoot(baseDir)) {
7932
+ return;
7933
+ }
7934
+ const fields = ["excludeCommands", "extraCommands"];
7935
+ const entries = fields.flatMap((field) => (checkCache[field] ?? []).map((command) => ({ command, field })));
7936
+ const hook = entries.find(({ command }) => isHookName(command));
7937
+ if (hook !== void 0) {
7938
+ throw new UserError(
7939
+ `Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, which is never gated on its own: It runs as part of the chain of the command that it wraps.`
7940
+ );
7941
+ }
7942
+ const registeredNames = /* @__PURE__ */ new Set([
7943
+ ...Object.keys(buildRootRegistry(config, baseDir)),
7944
+ ...Object.keys(buildWorkspaceRegistry(config))
7945
+ ]);
7946
+ const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
7947
+ if (missingEntries.length === 0) {
7948
+ return;
7949
+ }
7950
+ const declaredNames = readDeclaredScriptNames(baseDir);
7951
+ const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
7952
+ if (unresolvableEntry === void 0) {
7953
+ return;
7954
+ }
7955
+ const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
7956
+ throw new UserError(
7957
+ `Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: \`${unresolvableEntry.command}\`.${closestName === void 0 ? "" : ` Did you mean \`${closestName}\`?`}`
7958
+ );
7959
+ }
7960
+ function readDeclaredScriptNames(monorepoRoot) {
7961
+ let packageDirs;
7962
+ try {
7963
+ packageDirs = getWorkspacePackageDirs(monorepoRoot);
7964
+ } catch {
7965
+ packageDirs = [];
7966
+ }
7967
+ const names = /* @__PURE__ */ new Set();
7968
+ const dirs = [monorepoRoot, ...packageDirs];
7969
+ for (const dir of dirs) {
7970
+ const scripts = readPackageScripts(dir);
7971
+ for (const name of Object.keys(scripts)) {
7972
+ names.add(name);
7973
+ }
7974
+ }
7975
+ return names;
7976
+ }
7977
+ function readPackageScripts(packageDir) {
7978
+ try {
7979
+ return readPackageJsonScripts(packageDir) ?? {};
7980
+ } catch (error) {
7981
+ if (error instanceof UserError) {
7982
+ return {};
7983
+ }
7984
+ throw error;
7985
+ }
7986
+ }
7987
+ function validateOutputField(value, configPath) {
7988
+ const output = value["output"];
7989
+ if (output === void 0) {
7990
+ return void 0;
7991
+ }
7992
+ if (!isObject(output)) {
7993
+ throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
7994
+ }
7995
+ assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, "output.");
7996
+ const config = {};
7997
+ const commandVerbosity = output["commandVerbosity"];
7998
+ if (commandVerbosity !== void 0) {
7999
+ if (typeof commandVerbosity !== "string" || !isCommandVerbosity(commandVerbosity)) {
8000
+ const renderedValue = typeof commandVerbosity === "string" ? commandVerbosity : JSON.stringify(commandVerbosity);
8001
+ throw new UserError(
8002
+ `Invalid nmr config at ${configPath}: ${formatVerbosityRejection("`output.commandVerbosity`", renderedValue)}`
8003
+ );
8004
+ }
8005
+ config.commandVerbosity = commandVerbosity;
8006
+ }
8007
+ const extraAgentEnvVars = output["extraAgentEnvVars"];
8008
+ if (extraAgentEnvVars !== void 0) {
8009
+ if (!isStringArray(extraAgentEnvVars)) {
8010
+ throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
8011
+ }
8012
+ config.extraAgentEnvVars = extraAgentEnvVars;
8013
+ }
8014
+ return config;
8015
+ }
8016
+ function validateStringRecordField(value, fieldName, configPath) {
8017
+ if (!Object.hasOwn(value, fieldName) || value[fieldName] === void 0) {
8018
+ return void 0;
8019
+ }
8020
+ if (!isStringRecord(value[fieldName])) {
8021
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
8022
+ }
8023
+ return value[fieldName];
8024
+ }
8025
+ function validateConfig(value, configPath, baseDir) {
8026
+ if (!isObject(value)) {
8027
+ throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
8028
+ }
8029
+ assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
8030
+ const config = {};
8031
+ const build = validateBuildField(value, configPath);
8032
+ if (build) config.build = build;
8033
+ const checkCache = validateCheckCacheField(value, configPath);
8034
+ if (checkCache) config.checkCache = checkCache;
8035
+ const devBin = validateStringRecordField(value, "devBin", configPath);
8036
+ if (devBin) config.devBin = devBin;
8037
+ const output = validateOutputField(value, configPath);
8038
+ if (output) config.output = output;
8039
+ const workspaceScripts2 = validateScriptField(value, "workspaceScripts", configPath);
8040
+ if (workspaceScripts2) config.workspaceScripts = workspaceScripts2;
8041
+ const rootScripts = validateScriptField(value, "rootScripts", configPath);
8042
+ if (rootScripts) config.rootScripts = rootScripts;
8043
+ const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
8044
+ if (selfHostedRunner) config.selfHostedRunner = selfHostedRunner;
8045
+ assertResolvableCheckCacheCommands(config, configPath, baseDir);
8046
+ return config;
8047
+ }
8048
+ async function loadConfig(baseDir) {
8049
+ const configPath = resolveConfigPath(baseDir);
8050
+ if (!existsSync2(configPath)) {
8051
+ return {};
8052
+ }
8053
+ const importedModule = await import(pathToFileURL(configPath).href);
8054
+ const loadedConfig = isObject(importedModule) ? importedModule["default"] : void 0;
8055
+ return validateConfig(loadedConfig, configPath, baseDir);
8056
+ }
8057
+ async function loadRootConfig(monorepoRoot) {
8058
+ const config = await loadConfig(monorepoRoot);
8059
+ assertTierKeys(config, CONFIG_TIERS.root, resolveConfigPath(monorepoRoot));
8060
+ return config;
8061
+ }
8062
+ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
8063
+ for (const key of Object.keys(value)) {
8064
+ const replacement = retiredKeys.get(key);
8065
+ if (replacement === void 0) continue;
8066
+ throw new UserError(
8067
+ `Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`
8068
+ );
8069
+ }
8070
+ }
8071
+ function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = "") {
8072
+ const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
8073
+ if (unrecognizedKeys.length === 0) {
8074
+ return;
8075
+ }
8076
+ throw new UserError(
8077
+ `Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? "key" : "keys"} ${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`
8078
+ );
8079
+ }
8080
+ function assertTierKeys(config, tier, configPath) {
8081
+ const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
8082
+ if (unsupportedKeys.length === 0) {
8083
+ return;
8084
+ }
8085
+ throw new UserError(
8086
+ `Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(", ")} alone, not ${unsupportedKeys.toSorted().join(", ")}. Move those keys to ${tier.elsewhere}.`
8087
+ );
8088
+ }
8089
+ function formatKeyList(keys, prefix) {
8090
+ return keys.toSorted().map((key) => `\`${prefix}${key}\``).join(", ");
7490
8091
  }
7491
8092
 
7492
8093
  // src/tiers.ts
7493
8094
  import { readdirSync } from "node:fs";
7494
- import path from "node:path";
8095
+ import path6 from "node:path";
7495
8096
 
7496
8097
  // src/git-ignored-paths.ts
7497
8098
  import { spawnSync } from "node:child_process";
@@ -7522,7 +8123,7 @@ function findUntieredTestFiles(rootDir, options = {}) {
7522
8123
  }
7523
8124
  function hasTierInfix(filePath) {
7524
8125
  const tiers = TIER_NAMES;
7525
- return tiers.includes(path.basename(filePath).split(".").at(-3) ?? "");
8126
+ return tiers.includes(path6.basename(filePath).split(".").at(-3) ?? "");
7526
8127
  }
7527
8128
  var TEST_COLLECTION_EXCLUDE = [".git", "coverage", "dist", "node_modules"];
7528
8129
  var TIER_NAMES = ["unit", "tool", "localhost", "remote"];
@@ -7532,7 +8133,7 @@ function collectTestFiles(dir, relativeDir, isInTestDir, context) {
7532
8133
  const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
7533
8134
  if (entry.isDirectory()) {
7534
8135
  if (context.pruned.has(entry.name) || context.ignoredPaths.has(`${relativePath}/`)) continue;
7535
- collectTestFiles(path.join(dir, entry.name), relativePath, isInTestDir || entry.name === TEST_DIR, context);
8136
+ collectTestFiles(path6.join(dir, entry.name), relativePath, isInTestDir || entry.name === TEST_DIR, context);
7536
8137
  } else if (isInTestDir !== context.misplaced && TEST_FILE_PATTERN.test(entry.name) && !context.ignoredPaths.has(relativePath)) {
7537
8138
  context.foundPaths.push(relativePath);
7538
8139
  }
@@ -7780,6 +8381,32 @@ var default_default = defineRdyKit({
7780
8381
  fix: 'Remove "ws" script entries and replace any "pnpm run ws" invocations with nmr in each packages/*/package.json'
7781
8382
  }
7782
8383
  ]
8384
+ },
8385
+ {
8386
+ name: "self-hosted runner",
8387
+ checks: [
8388
+ {
8389
+ name: "GitHub reports the repo's runner settings",
8390
+ severity: "recommend",
8391
+ quiet: true,
8392
+ skip: () => describeSelfHostedRunnerSkip(),
8393
+ check: () => true,
8394
+ checks: [
8395
+ {
8396
+ name: "CI_RUNS_ON selects the self-hosted runner",
8397
+ severity: "recommend",
8398
+ skip: () => describeRecommendationSkip(),
8399
+ check: () => recommendSelfHostedRunner()
8400
+ },
8401
+ {
8402
+ name: "the self-hosted runner can run CI",
8403
+ severity: "warn",
8404
+ skip: () => describeReadinessSkip(),
8405
+ check: () => selfHostedRunnerCanRunCi()
8406
+ }
8407
+ ]
8408
+ }
8409
+ ]
7783
8410
  }
7784
8411
  ]
7785
8412
  });
@@ -7821,7 +8448,7 @@ var CATALOGUE_CHECKED_FIELDS = ["dependencies", "devDependencies", "optionalDepe
7821
8448
  var PROTOCOL_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
7822
8449
  function allWorkspacePackagesCanBuild() {
7823
8450
  const packagesDir = join(process.cwd(), "packages");
7824
- if (!existsSync(packagesDir)) return true;
8451
+ if (!existsSync3(packagesDir)) return true;
7825
8452
  const entries = readdirSync2(packagesDir, { withFileTypes: true });
7826
8453
  const failingPackages = [];
7827
8454
  for (const entry of entries) {
@@ -7830,7 +8457,7 @@ function allWorkspacePackagesCanBuild() {
7830
8457
  const content = readFile(pkgPath);
7831
8458
  if (!content) continue;
7832
8459
  const hasBuildOverride = /"build"\s*:/.test(content);
7833
- const hasDefaultBuildInputs = fileExists(`packages/${entry.name}/tsconfig.json`) && existsSync(join(packagesDir, entry.name, "src"));
8460
+ const hasDefaultBuildInputs = fileExists(`packages/${entry.name}/tsconfig.json`) && existsSync3(join(packagesDir, entry.name, "src"));
7834
8461
  if (!hasBuildOverride && !hasDefaultBuildInputs) {
7835
8462
  failingPackages.push(entry.name);
7836
8463
  }
@@ -8002,7 +8629,7 @@ function everyViteConfigHasVitestConfig() {
8002
8629
  return { ok: false, detail: formatPaths(unpairedConfigs) };
8003
8630
  }
8004
8631
  function findFiles(patterns, cwd) {
8005
- return globSync(patterns, { cwd, exclude: (path2) => SCAN_EXCLUDE_DIRS.has(basename(path2)) }).map((path2) => path2.split(sep).join("/")).toSorted();
8632
+ return globSync2(patterns, { cwd, exclude: (path7) => SCAN_EXCLUDE_DIRS.has(basename(path7)) }).map((path7) => path7.split(sep).join("/")).toSorted();
8006
8633
  }
8007
8634
  function findLiteralCataloguedSpecifiers(workspace, names) {
8008
8635
  const manifestPath = posix.join(workspace.dir, "package.json");
@@ -8019,10 +8646,10 @@ function findWorkspaceConfigs(workspace, pattern) {
8019
8646
  }
8020
8647
  function formatPaths(paths) {
8021
8648
  return `${paths.length} found:
8022
- ${paths.map((path2) => ` ${path2}`).join("\n")}`;
8649
+ ${paths.map((path7) => ` ${path7}`).join("\n")}`;
8023
8650
  }
8024
8651
  function getMinVersion() {
8025
- const pickedFields = { "version": "0.42.0" };
8652
+ const pickedFields = { "version": "0.44.0" };
8026
8653
  if (typeof pickedFields["version"] !== "string") {
8027
8654
  throw new TypeError("nmr/package.json: 'version' must be a string");
8028
8655
  }
@@ -8064,8 +8691,8 @@ function noPnpmFieldInPackageJson(cwd = process.cwd()) {
8064
8691
  return { ok: false, detail: formatPaths(declaringFiles) };
8065
8692
  }
8066
8693
  function noReExportOnlyVitestConfigs(cwd = process.cwd()) {
8067
- const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path2) => path2.includes("/"));
8068
- const reExports = nonRootConfigs.filter((path2) => isOwnedByReExportCheck(cwd, path2));
8694
+ const nonRootConfigs = findFiles([`**/${VITEST_CONFIG_PATTERN}`], cwd).filter((path7) => path7.includes("/"));
8695
+ const reExports = nonRootConfigs.filter((path7) => isOwnedByReExportCheck(cwd, path7));
8069
8696
  if (reExports.length === 0) return true;
8070
8697
  return { ok: false, detail: formatPaths(reExports) };
8071
8698
  }
@@ -8074,7 +8701,7 @@ function noRedundantRootScripts() {
8074
8701
  if (!pkg) return true;
8075
8702
  const scripts = pkg["scripts"];
8076
8703
  if (!isRecord(scripts)) return true;
8077
- const builtInNames = Object.keys(getDefaultRootScripts());
8704
+ const builtInNames = Object.keys(getDefaultRootScripts(process.cwd()));
8078
8705
  const redundantNames = Object.keys(scripts).filter((name) => builtInNames.includes(name));
8079
8706
  if (redundantNames.length === 0) return true;
8080
8707
  return {
@@ -8101,7 +8728,7 @@ function noUnguardedLefthookInstall(cwd = process.cwd()) {
8101
8728
  }
8102
8729
  function noWorkspaceRunScriptReferences() {
8103
8730
  const packagesDir = join(process.cwd(), "packages");
8104
- if (!existsSync(packagesDir)) return true;
8731
+ if (!existsSync3(packagesDir)) return true;
8105
8732
  const legacyPattern = /run-workspace-script|"pnpm\s+run\s+ws\b/;
8106
8733
  const entries = readdirSync2(packagesDir, { withFileTypes: true });
8107
8734
  const matches = [];
@@ -8139,7 +8766,7 @@ function readCataloguedNames(cwd) {
8139
8766
  if (content === void 0) return { ok: true, names: /* @__PURE__ */ new Set() };
8140
8767
  let workspaceManifest;
8141
8768
  try {
8142
- workspaceManifest = (0, import_yaml.parse)(content);
8769
+ workspaceManifest = (0, import_yaml2.parse)(content);
8143
8770
  } catch (error) {
8144
8771
  return { ok: false, detail: `cannot parse pnpm-workspace.yaml: ${describeError(error)}` };
8145
8772
  }
@@ -8244,14 +8871,288 @@ function vitestConfigBuildsOnSharedConfig() {
8244
8871
  function vitestRootConfigBuildsOnSharedConfig(cwd = process.cwd()) {
8245
8872
  return checkRootVitestConfig("vitest.root.config", "defineRootVitestConfig", cwd);
8246
8873
  }
8874
+ var SET_CI_RUNS_ON_COMMAND = `gh variable set CI_RUNS_ON --body '"self-hosted"'`;
8875
+ var CODE_QUALITY_WORKFLOW = "williamthorsen/.github/.github/workflows/code-quality-pnpm-workflow.yaml";
8876
+ var MIN_CODE_QUALITY_MAJOR = 8;
8877
+ var GH_TIMEOUT_MS = 15e3;
8878
+ var IMAGE_ABSENT_PROGRAM_PATTERN = /(?:^|[\s;&|(])(corepack|gh|node|npm|npx|pnpm)(?=\s|$)/gm;
8879
+ var NEVER_INSTALLED_PROGRAMS = /* @__PURE__ */ new Set(["corepack", "gh"]);
8880
+ var statusByCwd = /* @__PURE__ */ new Map();
8881
+ async function describeSelfHostedRunnerSkip(cwd = process.cwd(), readFacts) {
8882
+ const status = await getSelfHostedRunnerStatus(cwd, readFacts);
8883
+ return "skip" in status ? status.skip : false;
8884
+ }
8885
+ function findSelfHostedRunnerFailures(facts, cwd) {
8886
+ const failures = [];
8887
+ if (!facts.isPrivate) {
8888
+ failures.push(
8889
+ `${facts.nameWithOwner} is public: A self-hosted runner must never run code from a fork's pull request`
8890
+ );
8891
+ }
8892
+ if (facts.onlineSelfHostedRunners === 0) {
8893
+ failures.push(
8894
+ `${facts.nameWithOwner} does not have an online runner labelled self-hosted. When williamthorsen/hq allocates the repo slots in apps/runners/config/repos.yaml, the runners have not registered yet`
8895
+ );
8896
+ }
8897
+ const workflows = readWorkflows(cwd);
8898
+ failures.push(...findCodeQualityCallerDefects(workflows), ...findImageIncompatibleSteps(workflows));
8899
+ return failures;
8900
+ }
8901
+ async function readGitHubFacts(cwd, runGh = runGhCommand) {
8902
+ const repo = await runGh(["repo", "view", "--json", "nameWithOwner,visibility"], cwd);
8903
+ if (!repo.ok) return { skip: describeGhFailure(repo, "cannot resolve this repo on GitHub") };
8904
+ const repoView = parseJson(repo.stdout);
8905
+ if (!isRecord(repoView) || typeof repoView["nameWithOwner"] !== "string") {
8906
+ return { skip: "gh repo view returned an unexpected shape" };
8907
+ }
8908
+ const nameWithOwner = repoView["nameWithOwner"];
8909
+ const runners = await runGh(["api", `repos/${nameWithOwner}/actions/runners?per_page=100`], cwd);
8910
+ if (!runners.ok) {
8911
+ return {
8912
+ skip: describeGhFailure(runners, `cannot list the runners of ${nameWithOwner}, which needs admin access`)
8913
+ };
8914
+ }
8915
+ const repoVariable = await runGh(["api", `repos/${nameWithOwner}/actions/variables/CI_RUNS_ON`], cwd);
8916
+ if (!repoVariable.ok && !isNotFound(repoVariable)) {
8917
+ return { skip: describeGhFailure(repoVariable, `cannot read the CI_RUNS_ON variable of ${nameWithOwner}`) };
8918
+ }
8919
+ const repoValue = repoVariable.ok ? readVariableValue(parseJson(repoVariable.stdout)) : void 0;
8920
+ return {
8921
+ nameWithOwner,
8922
+ isPrivate: repoView["visibility"] !== "PUBLIC",
8923
+ onlineSelfHostedRunners: countOnlineSelfHostedRunners(parseJson(runners.stdout)),
8924
+ ciRunsOn: repoValue ?? await readOrganizationValue(nameWithOwner, cwd, runGh)
8925
+ };
8926
+ }
8927
+ function readRunnerSelection(value) {
8928
+ if (value === void 0 || value.trim() === "") return "unset";
8929
+ const parsed = parseJson(value);
8930
+ if (parsed === void 0) return "invalid";
8931
+ const labels = Array.isArray(parsed) ? parsed : [parsed];
8932
+ return labels.includes("self-hosted") ? "self-hosted" : "other";
8933
+ }
8934
+ async function recommendSelfHostedRunner(cwd = process.cwd(), readFacts) {
8935
+ const status = await getSelfHostedRunnerStatus(cwd, readFacts);
8936
+ if ("skip" in status) return true;
8937
+ const toolTests = status.toolTestFiles.length === 0 ? "The repo does not have any tool-tier tests." : `Before switching, confirm that the image provides every program that these tool-tier tests spawn: ${formatPaths(status.toolTestFiles)}`;
8938
+ return {
8939
+ ok: false,
8940
+ detail: `${status.facts.nameWithOwner} can run its CI on the self-hosted runner. ${toolTests}`,
8941
+ fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${status.facts.nameWithOwner}`
8942
+ };
8943
+ }
8944
+ async function describeRecommendationSkip(cwd = process.cwd(), readFacts) {
8945
+ const status = await getSelfHostedRunnerStatus(cwd, readFacts);
8946
+ if ("skip" in status) return status.skip;
8947
+ if (status.selection !== "unset") return `CI_RUNS_ON is already set to ${status.facts.ciRunsOn ?? ""}`;
8948
+ if (status.exempt !== void 0) return `the repo is exempt: ${status.exempt}`;
8949
+ if (status.failures.length > 0) return `not ready: ${status.failures.join("; ")}`;
8950
+ return false;
8951
+ }
8952
+ async function describeReadinessSkip(cwd = process.cwd(), readFacts) {
8953
+ const status = await getSelfHostedRunnerStatus(cwd, readFacts);
8954
+ if ("skip" in status) return status.skip;
8955
+ if (status.selection === "unset") return "CI_RUNS_ON is unset";
8956
+ if (status.selection === "other") return `CI_RUNS_ON selects ${status.facts.ciRunsOn ?? ""}`;
8957
+ return false;
8958
+ }
8959
+ async function selfHostedRunnerCanRunCi(cwd = process.cwd(), readFacts) {
8960
+ const status = await getSelfHostedRunnerStatus(cwd, readFacts);
8961
+ if ("skip" in status) return true;
8962
+ const { facts } = status;
8963
+ const conditions = status.exempt === void 0 ? status.failures : [`the repo is exempt: ${status.exempt}`, ...status.failures];
8964
+ if (status.selection !== "invalid" && conditions.length === 0) return true;
8965
+ const invalidValue = `CI_RUNS_ON is ${facts.ciRunsOn ?? ""}, which is not JSON, so every workflow that reads it with fromJSON fails`;
8966
+ if (conditions.length === 0) {
8967
+ return { ok: false, detail: invalidValue, fix: `${SET_CI_RUNS_ON_COMMAND} --repo ${facts.nameWithOwner}` };
8968
+ }
8969
+ const problems = status.selection === "invalid" ? [invalidValue, ...conditions] : conditions;
8970
+ return {
8971
+ ok: false,
8972
+ detail: problems.map((problem) => ` ${problem}`).join("\n"),
8973
+ fix: `Fix each condition, or return the repo's CI to ubuntu-latest: gh variable delete CI_RUNS_ON --repo ${facts.nameWithOwner}`
8974
+ };
8975
+ }
8976
+ function countOnlineSelfHostedRunners(response) {
8977
+ if (!isRecord(response) || !Array.isArray(response["runners"])) return 0;
8978
+ return response["runners"].filter(
8979
+ (runner) => isRecord(runner) && runner["status"] === "online" && Array.isArray(runner["labels"]) && runner["labels"].some((label) => isRecord(label) && label["name"] === "self-hosted")
8980
+ ).length;
8981
+ }
8982
+ function describeGhFailure(result, context) {
8983
+ if (result.isMissing) return "gh is not installed";
8984
+ if (/gh auth login|not logged in/i.test(result.stderr)) return "gh is not authenticated";
8985
+ const firstLine = result.stderr.trim().split("\n", 1)[0] ?? "";
8986
+ return firstLine === "" ? `gh ${context}` : `gh ${context}: ${firstLine}`;
8987
+ }
8988
+ function findCodeQualityCallerDefects(workflows) {
8989
+ const defects = [];
8990
+ for (const { file, jobs } of workflows) {
8991
+ for (const [jobName, job] of jobs) {
8992
+ const uses = job["uses"];
8993
+ if (typeof uses !== "string" || !uses.startsWith(`${CODE_QUALITY_WORKFLOW}@`)) continue;
8994
+ const ref = uses.slice(CODE_QUALITY_WORKFLOW.length + 1);
8995
+ const major = /^v(\d+)(?:\.\d+){0,2}$/.exec(ref)?.[1];
8996
+ if (major === void 0) {
8997
+ defects.push(
8998
+ `${file} job ${jobName} pins the code-quality workflow at ${ref}, whose release cannot be determined`
8999
+ );
9000
+ } else if (Number(major) < MIN_CODE_QUALITY_MAJOR) {
9001
+ defects.push(
9002
+ `${file} job ${jobName} pins the code-quality workflow at ${ref}, which predates CI_RUNS_ON; pin v${MIN_CODE_QUALITY_MAJOR} or later`
9003
+ );
9004
+ }
9005
+ const inputs = job["with"];
9006
+ if (isRecord(inputs) && inputs["runs-on"] !== void 0) {
9007
+ defects.push(`${file} job ${jobName} passes runs-on, which overrides CI_RUNS_ON`);
9008
+ }
9009
+ }
9010
+ }
9011
+ return defects;
9012
+ }
9013
+ function findImageIncompatibleSteps(workflows) {
9014
+ const defects = [];
9015
+ for (const { file, jobs } of workflows) {
9016
+ for (const [jobName, job] of jobs) {
9017
+ if (!/\bvars\.CI_RUNS_ON\b/.test(JSON.stringify(job["runs-on"] ?? ""))) continue;
9018
+ const steps = job["steps"];
9019
+ if (!Array.isArray(steps)) continue;
9020
+ const installed = /* @__PURE__ */ new Set();
9021
+ for (const [index, step] of steps.entries()) {
9022
+ if (!isRecord(step)) continue;
9023
+ const stepLabel = typeof step["name"] === "string" ? step["name"] : `step ${index + 1}`;
9024
+ const run = step["run"];
9025
+ if (typeof run === "string") {
9026
+ const missing = findUninstalledPrograms(run, installed);
9027
+ if (missing.length > 0) {
9028
+ defects.push(
9029
+ `${file} job ${jobName}, ${stepLabel}: runs ${missing.join(", ")}, which the runner image lacks at that step`
9030
+ );
9031
+ }
9032
+ }
9033
+ for (const program of readInstalledPrograms(step)) installed.add(program);
9034
+ }
9035
+ }
9036
+ }
9037
+ return defects;
9038
+ }
9039
+ function findUninstalledPrograms(run, installed) {
9040
+ const programs = /* @__PURE__ */ new Set();
9041
+ for (const match of run.matchAll(IMAGE_ABSENT_PROGRAM_PATTERN)) {
9042
+ const program = match[1];
9043
+ if (program === void 0) continue;
9044
+ const provider = program === "npm" || program === "npx" ? "node" : program;
9045
+ if (NEVER_INSTALLED_PROGRAMS.has(program) || !installed.has(provider)) programs.add(program);
9046
+ }
9047
+ return [...programs].toSorted();
9048
+ }
9049
+ function getSelfHostedRunnerStatus(cwd, readFacts) {
9050
+ if (readFacts !== void 0) return gatherSelfHostedRunnerStatus(cwd, readFacts);
9051
+ let status = statusByCwd.get(cwd);
9052
+ if (status === void 0) {
9053
+ status = gatherSelfHostedRunnerStatus(cwd, (dir) => readGitHubFacts(dir));
9054
+ statusByCwd.set(cwd, status);
9055
+ }
9056
+ return status;
9057
+ }
9058
+ async function gatherSelfHostedRunnerStatus(cwd, readFacts) {
9059
+ const facts = await readFacts(cwd);
9060
+ if ("skip" in facts) return facts;
9061
+ const failures = findSelfHostedRunnerFailures(facts, cwd);
9062
+ let exempt;
9063
+ try {
9064
+ exempt = (await loadRootConfig(cwd)).selfHostedRunner?.exempt;
9065
+ } catch (error) {
9066
+ failures.push(`the nmr config cannot be loaded: ${describeError(error)}`);
9067
+ }
9068
+ return {
9069
+ facts,
9070
+ selection: readRunnerSelection(facts.ciRunsOn),
9071
+ failures,
9072
+ exempt,
9073
+ toolTestFiles: findTestFiles(cwd).filter(
9074
+ (file) => file.endsWith(".tool.test.ts") || file.endsWith(".tool.test.tsx")
9075
+ )
9076
+ };
9077
+ }
9078
+ function isNotFound(result) {
9079
+ return /HTTP 404/.test(result.stderr);
9080
+ }
9081
+ function parseJson(text) {
9082
+ try {
9083
+ return JSON.parse(text);
9084
+ } catch {
9085
+ return void 0;
9086
+ }
9087
+ }
9088
+ function readInstalledPrograms(step) {
9089
+ const uses = step["uses"];
9090
+ if (typeof uses !== "string") return [];
9091
+ const action = uses.split("@", 1)[0];
9092
+ if (action === "actions/setup-node") return ["node"];
9093
+ if (action === "pnpm/action-setup") return ["pnpm"];
9094
+ if (action !== "pnpm/setup") return [];
9095
+ const inputs = isRecord(step["with"]) ? step["with"] : {};
9096
+ const runtime = inputs["runtime"];
9097
+ const installsNode = typeof runtime === "string" ? /^node\b/.test(runtime) : String(inputs["node-version-file"]) !== "false";
9098
+ return installsNode ? ["pnpm", "node"] : ["pnpm"];
9099
+ }
9100
+ async function readOrganizationValue(nameWithOwner, cwd, runGh) {
9101
+ const result = await runGh(["api", `repos/${nameWithOwner}/actions/organization-variables`], cwd);
9102
+ if (!result.ok) return void 0;
9103
+ const response = parseJson(result.stdout);
9104
+ if (!isRecord(response) || !Array.isArray(response["variables"])) return void 0;
9105
+ const variable = response["variables"].find(
9106
+ (entry) => isRecord(entry) && entry["name"] === "CI_RUNS_ON"
9107
+ );
9108
+ return readVariableValue(variable);
9109
+ }
9110
+ function readVariableValue(variable) {
9111
+ return isRecord(variable) && typeof variable["value"] === "string" ? variable["value"] : void 0;
9112
+ }
9113
+ function readWorkflows(cwd) {
9114
+ const dir = join(cwd, ".github", "workflows");
9115
+ if (!existsSync3(dir)) return [];
9116
+ const workflows = [];
9117
+ const files = readdirSync2(dir).filter((name) => /\.ya?ml$/.test(name)).toSorted();
9118
+ for (const file of files) {
9119
+ let document;
9120
+ try {
9121
+ document = (0, import_yaml2.parse)(readFile(join(dir, file)) ?? "");
9122
+ } catch {
9123
+ continue;
9124
+ }
9125
+ if (!isRecord(document) || !isRecord(document["jobs"])) continue;
9126
+ const jobs = Object.entries(document["jobs"]).filter(
9127
+ (entry) => isRecord(entry[1])
9128
+ );
9129
+ workflows.push({ file: posix.join(".github/workflows", file), jobs });
9130
+ }
9131
+ return workflows;
9132
+ }
9133
+ function runGhCommand(args, cwd) {
9134
+ return new Promise((resolve) => {
9135
+ void execFile("gh", args, { cwd, timeout: GH_TIMEOUT_MS }, (error, stdout, stderr) => {
9136
+ if (error === null) {
9137
+ resolve({ ok: true, stdout });
9138
+ return;
9139
+ }
9140
+ resolve({ ok: false, isMissing: "code" in error && error.code === "ENOENT", stderr: stderr || error.message });
9141
+ });
9142
+ });
9143
+ }
8247
9144
  export {
8248
9145
  cataloguedDependenciesUseCatalog,
8249
9146
  default_default as default,
9147
+ describeReadinessSkip,
9148
+ describeRecommendationSkip,
9149
+ describeSelfHostedRunnerSkip,
8250
9150
  everyBinTargetIsACommittedWrapper,
8251
9151
  everyBinWrapperTargetIsCoveredByFiles,
8252
9152
  everyTestFileNamesItsTier,
8253
9153
  everyTestFileSitsUnderTestsDir,
8254
9154
  everyViteConfigHasVitestConfig,
9155
+ findSelfHostedRunnerFailures,
8255
9156
  hasSupportedEslintVersion,
8256
9157
  hasSupportedStrictLintVersion,
8257
9158
  noPnpmFieldInPackageJson,
@@ -8260,6 +9161,10 @@ export {
8260
9161
  noUnguardedLefthookInstall,
8261
9162
  noWorkspacesFieldInPackageJson,
8262
9163
  prettierConfigBuildsOnSharedConfig,
9164
+ readGitHubFacts,
9165
+ readRunnerSelection,
9166
+ recommendSelfHostedRunner,
9167
+ selfHostedRunnerCanRunCi,
8263
9168
  tazeConfigAvoidsClobberedOptions,
8264
9169
  tazeConfigBuildsOnSharedConfig,
8265
9170
  testSuiteGatesTestFileConventions,