@hublo/sentinel 1.2.0-alpha.1 → 1.2.0-alpha.10

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.
@@ -1,9 +1,3 @@
1
- import {
2
- REACT_APP_DEFAULTS,
3
- REQUIREMENTS,
4
- isPlainObject
5
- } from "./chunk-XVDOQ3G3.js";
6
-
7
1
  // src/core/registry.ts
8
2
  var adapters = [];
9
3
  var PresetUnsupportedError = class extends Error {
@@ -88,7 +82,7 @@ var BaseAdapter = class {
88
82
 
89
83
  // src/shared/package-json.ts
90
84
  import { existsSync, readFileSync } from "fs";
91
- import { dirname, join } from "path";
85
+ import { basename, dirname, join } from "path";
92
86
  import { fileURLToPath } from "url";
93
87
  function readOwnPackage() {
94
88
  let dir = dirname(fileURLToPath(import.meta.url));
@@ -133,6 +127,10 @@ function readNxProjectName(dir) {
133
127
  return void 0;
134
128
  }
135
129
  }
130
+ function moduleName(dir) {
131
+ const declared = readProjectPackageJson(dir).name;
132
+ return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
133
+ }
136
134
 
137
135
  // src/core/domain.ts
138
136
  var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
@@ -207,9 +205,9 @@ function binFromOwnInstall(packageName, binName) {
207
205
  try {
208
206
  const manifest = require2.resolve(`${packageName}/package.json`);
209
207
  const bin = require2(manifest).bin;
210
- const relative4 = typeof bin === "string" ? bin : bin?.[binName];
211
- if (!relative4) return void 0;
212
- const executable = join2(dirname2(manifest), relative4);
208
+ const relative6 = typeof bin === "string" ? bin : bin?.[binName];
209
+ if (!relative6) return void 0;
210
+ const executable = join2(dirname2(manifest), relative6);
213
211
  return existsSync3(executable) ? executable : void 0;
214
212
  } catch {
215
213
  return void 0;
@@ -222,8 +220,8 @@ function binSearchPath(cwd) {
222
220
  }
223
221
 
224
222
  // src/roles/build/plan.ts
225
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
226
- import { join as join7 } from "path";
223
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
224
+ import { join as join8 } from "path";
227
225
 
228
226
  // src/core/config/existing-command.ts
229
227
  import { readFileSync as readFileSync3 } from "fs";
@@ -243,7 +241,7 @@ function parseJsonc(text, source = "config") {
243
241
 
244
242
  // src/core/config/manifest.ts
245
243
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
246
- import { basename, join as join3 } from "path";
244
+ import { basename as basename2, join as join3 } from "path";
247
245
  function moduleScripts(cwd) {
248
246
  try {
249
247
  const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
@@ -252,7 +250,7 @@ function moduleScripts(cwd) {
252
250
  return {};
253
251
  }
254
252
  }
255
- function moduleName(cwd) {
253
+ function moduleName2(cwd) {
256
254
  try {
257
255
  const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
258
256
  return pkg.name;
@@ -261,7 +259,7 @@ function moduleName(cwd) {
261
259
  }
262
260
  }
263
261
  function isSelfAdoption(cwd) {
264
- return moduleName(cwd) === readOwnPackage().name;
262
+ return moduleName2(cwd) === readOwnPackage().name;
265
263
  }
266
264
  function selfCommand(cwd, flags) {
267
265
  const own = readOwnPackage();
@@ -280,7 +278,7 @@ function manifestOperation(cwd, scripts) {
280
278
  return {
281
279
  kind: "merge-json",
282
280
  path: "package.json",
283
- value: { name: readNxProjectName(cwd) ?? basename(cwd), private: true, ...value }
281
+ value: { name: readNxProjectName(cwd) ?? basename2(cwd), private: true, ...value }
284
282
  };
285
283
  }
286
284
 
@@ -384,16 +382,98 @@ function keepsOtherCommands(script, sentinelCommand) {
384
382
  return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
385
383
  }
386
384
 
385
+ // src/roles/build/presets/requirements.json
386
+ var requirements_default = {
387
+ svelte: {
388
+ why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
389
+ requires: {
390
+ "@sveltejs/vite-plugin-svelte": "7.0.0",
391
+ svelte: "5.46.4"
392
+ }
393
+ }
394
+ };
395
+
396
+ // src/roles/build/presets/toolchain.json
397
+ var toolchain_default = {
398
+ $comment: "The build toolchain sentinel owns. One table, because three places used to hold parts of it: the list to remove from a module's package.json, the list of import specifiers to rewrite, and the map from a default import to the name sentinel re-exports it under. A package added to one and not the others gives a removal with no rewrite, or an import pointing at a package the module no longer declares.",
399
+ owned: [
400
+ {
401
+ package: "vite",
402
+ specifier: "vite",
403
+ named: ["defineConfig", "loadEnv", "mergeConfig"],
404
+ types: [
405
+ "Plugin",
406
+ "PluginOption",
407
+ "SassPreprocessorOptions",
408
+ "UserConfig",
409
+ "UserConfigExport"
410
+ ],
411
+ why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
412
+ },
413
+ {
414
+ package: "@vitejs/plugin-react",
415
+ specifier: "@vitejs/plugin-react",
416
+ default: "react",
417
+ why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
418
+ },
419
+ {
420
+ package: "@tailwindcss/vite",
421
+ specifier: "@tailwindcss/vite",
422
+ default: "tailwindcss",
423
+ why: "the Tailwind pipeline, identical in all three front apps."
424
+ },
425
+ {
426
+ package: "vite-plugin-svgr",
427
+ specifier: "vite-plugin-svgr",
428
+ default: "svgr",
429
+ why: "SVG as components, identical in all three front apps."
430
+ },
431
+ {
432
+ package: "nitro",
433
+ specifier: "nitro/vite",
434
+ named: ["nitro"],
435
+ why: "the SSR server. Note the package and the import specifier differ, which is exactly the kind of split that made three separate lists disagree."
436
+ }
437
+ ],
438
+ add: [
439
+ {
440
+ package: "@hublo/sentinel",
441
+ why: "the module now gets its toolchain from here, so it declares it. Pinned by the installer rather than by us: `--init` writes the dependency, `pnpm install` resolves it."
442
+ }
443
+ ],
444
+ $notOwned: {
445
+ "@rolldown/plugin-babel": "declared by career and host-admin themselves, not at the root. It supports LocatorJS, a development convenience gated on LOCATORJS=true, not shared build infrastructure. Removing it would remove a feature from two teams, which is their call and must not ride along inside an adoption.",
446
+ "@locator/babel-jsx": "same, and same owners.",
447
+ "@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
448
+ "@tanstack/router-plugin": "generates the app's own route tree.",
449
+ "vite-bundle-analyzer": "at the root at 1.3.7 and imported nowhere, the only mention being a commented-out line in host-admin. Not a candidate to re-export, a candidate to delete from the root."
450
+ }
451
+ };
452
+
453
+ // src/roles/build/preset-data.ts
454
+ var REQUIREMENTS = requirements_default;
455
+ var OWNED_PACKAGES = toolchain_default.owned;
456
+
387
457
  // src/roles/build/config-policy.ts
458
+ var COMPANION_CONFIG_FILES = [
459
+ "vitest.config.ts",
460
+ "vitest.config.mts",
461
+ "vitest.config.js",
462
+ "vitest.config.mjs"
463
+ ];
388
464
  var BUILD_CONFIG_FILES = [
389
465
  "vite.config.ts",
390
466
  "vite.config.mts",
391
467
  "vite.config.js",
392
468
  "vite.config.mjs"
393
469
  ];
394
- var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
470
+ var BUILD_PRESET_SPECIFIERS = {
471
+ react: "@hublo/sentinel/build/react",
472
+ nest: "@hublo/sentinel/build/nest"
473
+ };
474
+ var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
395
475
  var BUILD_SCRIPT_NAME = "build";
396
- var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
476
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
397
477
  var DEV_SCRIPT_NAME = "serve";
398
478
  var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
399
479
  function buildTarget() {
@@ -404,13 +484,9 @@ function buildTarget() {
404
484
  }
405
485
  };
406
486
  }
407
- var SENTINEL_OWNED_BUILD_PACKAGES = [
408
- "vite",
409
- "@vitejs/plugin-react",
410
- "@tailwindcss/vite",
411
- "vite-plugin-svgr",
412
- "nitro"
413
- ];
487
+ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
488
+ (entry) => entry.package
489
+ );
414
490
 
415
491
  // src/roles/build/dev-script.ts
416
492
  var CHAIN = " -- ";
@@ -418,23 +494,143 @@ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
418
494
  var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
419
495
  function appOptions(rest) {
420
496
  const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
421
- const kept = tokens.filter((token, at2) => !(at2 === 0 && DEV_SUBCOMMANDS.has(token)));
497
+ const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
422
498
  return kept.join(" ");
423
499
  }
424
500
  function composeDevScript(existing, command) {
425
501
  if (existing === void 0 || existing.trim() === "") return command;
426
502
  const chunks = existing.trim().split(CHAIN);
427
- const at2 = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
428
- if (at2 === -1) return existing;
429
- const rest = (chunks[at2] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
503
+ const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
504
+ if (at === -1) return existing;
505
+ const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
430
506
  const options = appOptions(rest);
431
- chunks[at2] = options === "" ? command : `${command} -- ${options}`;
507
+ chunks[at] = options === "" ? command : `${command} -- ${options}`;
432
508
  return chunks.join(CHAIN);
433
509
  }
434
510
 
435
- // src/roles/build/read-adoption.ts
511
+ // src/roles/build/nest/adopt.ts
512
+ import { relative as relative2 } from "path";
513
+
514
+ // src/roles/build/nest/nx-target.ts
436
515
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
437
- import { join as join6 } from "path";
516
+ import { join as join6, relative } from "path";
517
+ var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
518
+ var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
519
+ var readProjectJson = (cwd) => {
520
+ const path = join6(cwd, "project.json");
521
+ if (!existsSync6(path)) return void 0;
522
+ try {
523
+ return JSON.parse(readFileSync4(path, "utf8"));
524
+ } catch {
525
+ return void 0;
526
+ }
527
+ };
528
+ function webpackBuildTarget(cwd) {
529
+ const project = readProjectJson(cwd);
530
+ const target = project?.targets?.build;
531
+ if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
532
+ const { outputPath, main, webpackConfig } = target.options ?? {};
533
+ if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
534
+ return void 0;
535
+ }
536
+ return {
537
+ project: project.name,
538
+ outputPath,
539
+ main,
540
+ webpackConfig,
541
+ configurations: target.configurations
542
+ };
543
+ }
544
+ function refusalFor(cwd, target) {
545
+ if (workspaceRootFor(cwd, target) === void 0) {
546
+ return `this service's build target declares \`main: ${target.main}\`, which does not place this directory inside a workspace. Every value the config needs is derived from that path, so a wrong one would resolve \`workspaceRoot\` to the service itself and leave the alias table resolving nothing. Nothing was written.`;
547
+ }
548
+ if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
549
+ return `this service builds with ${target.webpackConfig}, not the shared ${TRANSLATABLE_WEBPACK_CONFIG} this preset reproduces. Its own config adds loaders or rules the preset has no equivalent for, and translating it anyway would produce a build that succeeds and a service that fails later. Read that file, decide what the Vite config needs, and write it by hand. Nothing was written.`;
550
+ }
551
+ const live = liveFileReplacements(cwd, target);
552
+ if (live.length > 0) {
553
+ return `this service's production configuration replaces ${live.join(", ")}, and those files exist. The preset has no equivalent, so translating the target would drop a real substitution. Nothing was written.`;
554
+ }
555
+ return void 0;
556
+ }
557
+ function liveFileReplacements(cwd, target) {
558
+ const workspaceRoot = workspaceRootFor(cwd, target);
559
+ if (workspaceRoot === void 0) return [];
560
+ const configurations = target.configurations ?? {};
561
+ const live = [];
562
+ for (const configuration of Object.values(configurations)) {
563
+ for (const replacement of configuration.fileReplacements ?? []) {
564
+ const exists = existsSync6(join6(workspaceRoot, replacement.replace)) || existsSync6(join6(workspaceRoot, replacement.with));
565
+ if (exists) live.push(replacement.replace);
566
+ }
567
+ }
568
+ return live;
569
+ }
570
+ function workspaceRootFor(cwd, target) {
571
+ const moduleRoot = target.main.replace(/\/src\/.*$/, "");
572
+ if (moduleRoot === target.main || !cwd.endsWith(moduleRoot)) return void 0;
573
+ return cwd.slice(0, cwd.length - moduleRoot.length - 1);
574
+ }
575
+ function workspaceRootHops(cwd, target) {
576
+ const root = workspaceRootFor(cwd, target);
577
+ if (root === void 0) {
578
+ throw new Error(`sentinel build(nest): cannot place ${cwd} in a workspace from ${target.main}`);
579
+ }
580
+ const rel = relative(root, cwd);
581
+ return rel === "" ? 0 : rel.split(/[\\/]/).length;
582
+ }
583
+
584
+ // src/roles/build/nest/adopt.ts
585
+ var NEST_CONFIG_FILE = "vite.config.mts";
586
+ function nestConfigSource(cwd, target) {
587
+ const hops = workspaceRootHops(cwd, target);
588
+ const up = Array.from({ length: hops }, () => "..").join("/");
589
+ return `import path from 'node:path'
590
+
591
+ import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
592
+
593
+ export default nestService({
594
+ project: '${target.project}',
595
+ root: __dirname,
596
+ workspaceRoot: path.resolve(__dirname, '${up}'),
597
+ outDir: '${target.outputPath}',
598
+ })
599
+ `;
600
+ }
601
+ function nestBuildTarget(cwd, target) {
602
+ const workspaceRoot = workspaceRootFor(cwd, target);
603
+ if (workspaceRoot === void 0) {
604
+ throw new Error(
605
+ `sentinel build(nest): cannot place ${cwd} inside a workspace from ${target.main}`
606
+ );
607
+ }
608
+ const moduleRoot = relative2(workspaceRoot, cwd);
609
+ return {
610
+ executor: "nx:run-commands",
611
+ outputs: [`{workspaceRoot}/${target.outputPath}`],
612
+ options: {
613
+ cwd: moduleRoot,
614
+ command: "pnpm exec sentinel --run --build",
615
+ forwardAllArgs: false
616
+ }
617
+ };
618
+ }
619
+ function nestAdoptionOperations(cwd, target) {
620
+ return [
621
+ { kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) },
622
+ { kind: "remove-json-keys", path: "project.json", keys: [["targets", "build"]] },
623
+ {
624
+ kind: "merge-json",
625
+ path: "project.json",
626
+ value: { targets: { build: nestBuildTarget(cwd, target) } }
627
+ }
628
+ ];
629
+ }
630
+
631
+ // src/roles/build/read-adoption.ts
632
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
633
+ import { join as join7 } from "path";
438
634
  var NOT_ADOPTED = (configFile, unreadable = null) => ({
439
635
  configFile,
440
636
  preset: null,
@@ -445,26 +641,32 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
445
641
  unreadable
446
642
  });
447
643
  function buildConfigFile(cwd) {
448
- return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
644
+ return BUILD_CONFIG_FILES.find((name) => existsSync7(join7(cwd, name)));
449
645
  }
450
646
  function importsSpecifier(source, specifier) {
451
647
  const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
452
648
  return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
453
649
  }
454
650
  function importsPreset(source) {
455
- return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
651
+ return Object.values(BUILD_PRESET_SPECIFIERS).some(
652
+ (specifier) => importsSpecifier(source, specifier)
653
+ );
456
654
  }
457
655
  var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
458
656
  function declaredBuildPreset(cwd) {
459
657
  const source = readBuildConfigSource(cwd);
460
- if (source === void 0) return void 0;
658
+ if (source === void 0) {
659
+ return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
660
+ }
661
+ if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
662
+ if (importsPreset(source)) return "react";
461
663
  return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
462
664
  }
463
665
  function readBuildConfigSource(cwd) {
464
666
  const configFile = buildConfigFile(cwd);
465
667
  if (!configFile) return void 0;
466
668
  try {
467
- return readFileSync4(join6(cwd, configFile), "utf8");
669
+ return readFileSync5(join7(cwd, configFile), "utf8");
468
670
  } catch {
469
671
  return void 0;
470
672
  }
@@ -474,7 +676,7 @@ function readBuildAdoption(cwd) {
474
676
  if (!configFile) return NOT_ADOPTED(null);
475
677
  let source;
476
678
  try {
477
- source = readFileSync4(join6(cwd, configFile), "utf8");
679
+ source = readFileSync5(join7(cwd, configFile), "utf8");
478
680
  } catch (error) {
479
681
  return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
480
682
  }
@@ -486,7 +688,9 @@ function readBuildAdoption(cwd) {
486
688
  );
487
689
  return {
488
690
  configFile,
489
- preset: "react",
691
+ // Read from the config rather than assumed: the two presets are adopted the same way and
692
+ // reporting the wrong one would make `--inspect` lie about what a module builds.
693
+ preset: declaredBuildPreset(cwd) ?? "react",
490
694
  adopted: true,
491
695
  conformant: ownDeclarations.length === 0,
492
696
  drift: ownDeclarations.map(
@@ -502,9 +706,9 @@ function atLeast(declared, needed) {
502
706
  const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
503
707
  const left = parse2(declared);
504
708
  const right = parse2(needed);
505
- for (let at2 = 0; at2 < Math.max(left.length, right.length); at2++) {
506
- const a = left[at2] ?? 0;
507
- const b = right[at2] ?? 0;
709
+ for (let at = 0; at < Math.max(left.length, right.length); at++) {
710
+ const a = left[at] ?? 0;
711
+ const b = right[at] ?? 0;
508
712
  if (a !== b) return a > b;
509
713
  }
510
714
  return true;
@@ -525,43 +729,103 @@ function describeUnmet(preset, unmet) {
525
729
  return `the build role does not apply to this module yet: ${list}. ${entry?.why ?? ""}. Nothing was written. Raise those versions, check the module still builds and its tests still pass, then re-run \`sentinel --init --build\`.`;
526
730
  }
527
731
 
528
- // src/roles/build/plan.ts
529
- function scaffold() {
530
- return `import { defineConfig, reactApp } from '${BUILD_PRESET_SPECIFIER}'
531
- import { tanstackStart } from '@tanstack/react-start/plugin/vite'
532
- import { tanstackRouter } from '@tanstack/router-plugin/vite'
533
- import path from 'node:path'
534
-
535
- /*
536
- * Composition comes from sentinel: which plugins run under test and which under a real build,
537
- * the order they run in, and how the env is read. The values below are this app's own.
538
- *
539
- * TanStack is passed IN rather than imported by sentinel, because the app codes against it
540
- * directly. \`alias\` is passed through verbatim and never read, so nothing in it can be lost.
541
- * Anything sentinel does not set goes in \`overrides\`, which merges over the preset last.
542
- */
543
- export default defineConfig(({ mode }) =>
544
- reactApp({
545
- root: __dirname,
546
- mode,
547
- base: '/',
548
- port: 3000,
549
- // Explicit, NOT port + 1: the three apps in this repo disagree, one of them goes down.
550
- hmrPort: 3001,
551
- router: {
552
- routesDirectory: path.resolve(__dirname, 'src/routes'),
553
- generatedRouteTree: path.resolve(__dirname, 'src/routeTree.gen.ts'),
554
- },
555
- tanstack: { start: tanstackStart, router: tanstackRouter },
556
- alias: [],
557
- }),
558
- )
559
-
560
- // If this app reads environment variables, use \`appEnv(__dirname, mode)\` rather than Vite's
561
- // \`loadEnv\`: it applies the production -> prd normalisation this repo deploys under, which
562
- // every app was otherwise retyping.
563
- `;
732
+ // src/roles/build/rewrite-imports.ts
733
+ import { parseSync } from "oxc-parser";
734
+ var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
735
+ function esmBlocker(fileName, packageType) {
736
+ if (/\.(mts|mjs)$/.test(fileName)) return void 0;
737
+ if (packageType === "module") return void 0;
738
+ return `sentinel is ESM only, and this module does not declare \`"type": "module"\`, so Vite would load ${fileName} as CommonJS and fail to require it. Add \`"type": "module"\` to this module's package.json, or rename the config to \`${fileName.replace(/\.(ts|js)$/, ".m$1")}\`, then run this again.`;
739
+ }
740
+ var DEFAULT_EXPORT_NAMES = Object.fromEntries(
741
+ OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
742
+ entry.specifier,
743
+ entry.default
744
+ ])
745
+ );
746
+ function renderImport(bindings) {
747
+ const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
748
+ const values = bindings.filter((b) => !b.typeOnly);
749
+ const types = bindings.filter((b) => b.typeOnly);
750
+ const lines = [];
751
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
752
+ if (types.length > 0) {
753
+ lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
754
+ }
755
+ return lines.join("\n");
756
+ }
757
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
758
+ const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
759
+ if (errors.length > 0) {
760
+ return {
761
+ kind: "blocked",
762
+ why: `${fileName} does not parse: ${errors[0]?.message ?? "unknown"}`
763
+ };
764
+ }
765
+ const imports = program.body.filter(
766
+ (node) => node.type === "ImportDeclaration"
767
+ );
768
+ if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
769
+ return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
770
+ }
771
+ const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
772
+ if (moving.length === 0) {
773
+ return {
774
+ kind: "unchanged",
775
+ why: "this config imports none of the packages sentinel owns, so there is nothing to move"
776
+ };
777
+ }
778
+ const bindings = [];
779
+ for (const node of moving) {
780
+ const from = node.source.value;
781
+ const specifiers = node.specifiers ?? [];
782
+ if (specifiers.length === 0) {
783
+ return { kind: "blocked", why: `\`import '${from}'\` has no bindings to move` };
784
+ }
785
+ for (const spec of specifiers) {
786
+ const typeOnly = node.importKind === "type" || spec.importKind === "type";
787
+ if (spec.type === "ImportDefaultSpecifier") {
788
+ const exported = DEFAULT_EXPORT_NAMES[from];
789
+ if (!exported) {
790
+ return {
791
+ kind: "blocked",
792
+ why: `no sentinel export for the default import of \`${from}\``
793
+ };
794
+ }
795
+ bindings.push({ exported, local: spec.local.name, typeOnly });
796
+ continue;
797
+ }
798
+ if (spec.type === "ImportSpecifier") {
799
+ bindings.push({
800
+ exported: spec.imported?.name ?? spec.imported?.value ?? spec.local.name,
801
+ local: spec.local.name,
802
+ typeOnly
803
+ });
804
+ continue;
805
+ }
806
+ return {
807
+ kind: "blocked",
808
+ why: `\`import * as ${spec.local.name} from '${from}'\` cannot be mapped onto sentinel's named exports`
809
+ };
810
+ }
811
+ }
812
+ const spans = moving.map((node) => ({ start: node.start, end: node.end })).sort((left, right) => left.start - right.start);
813
+ const first = spans[0];
814
+ const rest = spans.slice(1);
815
+ let out = source;
816
+ for (const span of rest.slice().reverse()) {
817
+ const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
818
+ out = out.slice(0, span.start) + out.slice(span.end + after);
819
+ }
820
+ out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
821
+ return {
822
+ kind: "rewritten",
823
+ source: out,
824
+ moved: [...new Set(moving.map((node) => node.source.value))].sort()
825
+ };
564
826
  }
827
+
828
+ // src/roles/build/plan.ts
565
829
  function ownedBuildDependencies(cwd) {
566
830
  const manifest = readProjectPackageJson(cwd);
567
831
  const keys = [];
@@ -590,9 +854,12 @@ function buildScripts(cwd) {
590
854
  };
591
855
  const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
592
856
  if (existingDev !== void 0) {
857
+ const own2 = moduleName(cwd);
593
858
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
594
859
  existingDev,
595
- selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND
860
+ // Trailing `--` for the same reason as the build script: whatever the caller appends
861
+ // reaches Vite, not sentinel's parser. It goes after `--module`, which is sentinel's own.
862
+ `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2} --`
596
863
  );
597
864
  }
598
865
  return scripts;
@@ -615,15 +882,65 @@ function plan(context) {
615
882
  const notes = [];
616
883
  const operations = [];
617
884
  const configFile = buildConfigFile(context.cwd);
885
+ const webpackTarget = webpackBuildTarget(context.cwd);
886
+ const isNest = declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0;
887
+ if (isNest) {
888
+ if (webpackTarget === void 0) {
889
+ return {
890
+ operations: [],
891
+ skipped: `already building through sentinel: ${configFile ?? NEST_CONFIG_FILE} imports the nest preset and the nx target runs it. Nothing to change.`
892
+ };
893
+ }
894
+ const refusal = refusalFor(context.cwd, webpackTarget);
895
+ if (refusal !== void 0) return { operations: [], blocked: refusal };
896
+ const dropped = Object.keys(webpackTarget.configurations ?? {});
897
+ return {
898
+ operations: nestAdoptionOperations(context.cwd, webpackTarget),
899
+ notes: [
900
+ `wrote ${NEST_CONFIG_FILE} and pointed the nx build target at sentinel. The target stays \`nx:run-commands\` with \`forwardAllArgs: false\`, so the image's \`--generatePackageJson\` still works and the Dockerfile needs no change.`,
901
+ ...dropped.length > 0 ? [
902
+ `dropped the \`${dropped.join("`, `")}\` configuration${dropped.length > 1 ? "s" : ""} from the build target: its fileReplacements point at files that do not exist, so it replaced nothing. A live one would have blocked this instead.`
903
+ ] : [],
904
+ `verify with \`nx run ${webpackTarget.project}:generate-swagger-file\` and \`git diff --exit-code\`: it runs the built bundle, so it proves the service boots AND that its published contract did not move.`
905
+ ]
906
+ };
907
+ }
618
908
  if (configFile === void 0) {
619
- operations.push({ kind: "write", path: BUILD_CONFIG_FILES[0], contents: scaffold() });
620
- notes.push(
621
- `wrote ${BUILD_CONFIG_FILES[0]} as a starting point. Fill in this app's own values (base, ports, routes); sentinel owns the composition, not the data.`
622
- );
623
- } else if (!readsPreset(context.cwd, configFile)) {
624
- notes.push(
625
- `${configFile} is this app's own, so it was left alone. To adopt: import \`reactApp\` from \`${BUILD_PRESET_SPECIFIER}\` and pass it this app's values, keeping \`alias\` and your own plugins verbatim. \`sentinel --inspect --build\` reports what the resolved config departs from and what it adds, before and after.`
626
- );
909
+ return {
910
+ operations: [],
911
+ skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (36 services here build through \`@nx/webpack\`), the build role does not apply to it.`
912
+ };
913
+ } else {
914
+ const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
915
+ if (notEsm !== void 0) return { operations: [], skipped: notEsm };
916
+ const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
917
+ if (rewrite.kind === "blocked") {
918
+ return {
919
+ operations: [],
920
+ skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
921
+ };
922
+ }
923
+ if (rewrite.kind === "rewritten") {
924
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
925
+ }
926
+ for (const name of COMPANION_CONFIG_FILES) {
927
+ const source = readConfigSource(context.cwd, name);
928
+ if (source === "") continue;
929
+ const companion = rewriteBuildImports(source, name);
930
+ if (companion.kind === "rewritten") {
931
+ operations.push({ kind: "write", path: name, contents: companion.source });
932
+ notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
933
+ } else if (companion.kind === "blocked") {
934
+ notes.push(
935
+ `${name} imports the toolchain and could NOT be pointed at sentinel: ${companion.why}. It will stop resolving when the workspace root drops these packages, and it will fail your TESTS rather than your build.`
936
+ );
937
+ }
938
+ }
939
+ if (rewrite.kind === "rewritten") {
940
+ notes.push(
941
+ `${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
942
+ );
943
+ }
627
944
  }
628
945
  operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
629
946
  operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
@@ -636,12 +953,12 @@ function plan(context) {
636
953
  }
637
954
  return { operations, notes };
638
955
  }
639
- function readsPreset(cwd, configFile) {
640
- if (!existsSync7(join7(cwd, configFile))) return false;
956
+ function readConfigSource(cwd, configFile) {
957
+ if (!existsSync8(join8(cwd, configFile))) return "";
641
958
  try {
642
- return readFileSync5(join7(cwd, configFile), "utf8").includes(BUILD_PRESET_SPECIFIER);
959
+ return readFileSync6(join8(cwd, configFile), "utf8");
643
960
  } catch {
644
- return false;
961
+ return "";
645
962
  }
646
963
  }
647
964
 
@@ -679,11 +996,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
679
996
  * resolution let it run at all — filtering here would give "no adapter for preset svelte",
680
997
  * which tells nobody what to do about it.
681
998
  *
682
- * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
683
- * would change that.
999
+ * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
1000
+ * grounds that "there is nothing to bundle", which was never true: those 36 services bundle
1001
+ * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
1002
+ * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
1003
+ * migration generator refuses every one of them because they use `@nx/js:node`
1004
+ * (nrwl/nx#36389). So the way out runs through here.
1005
+ *
1006
+ * `node` and `tools` remain genuinely declined: nothing under them bundles at all.
684
1007
  */
685
1008
  appliesTo(preset) {
686
- return preset === "react" || preset === "svelte";
1009
+ return preset === "react" || preset === "svelte" || preset === "nest";
687
1010
  }
688
1011
  /**
689
1012
  * `react`, read from the module's Vite config rather than from its dependencies.
@@ -773,6 +1096,26 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
773
1096
  get label() {
774
1097
  return "dev";
775
1098
  }
1099
+ /**
1100
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
1101
+ *
1102
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1103
+ * because adopting one without the other leaves the module half-migrated. What was missing
1104
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
1105
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
1106
+ * wrong correction, it added friction to a command that already produced the right result.
1107
+ */
1108
+ plan(context) {
1109
+ const planned = super.plan(context);
1110
+ if (planned.operations.length === 0) return planned;
1111
+ return {
1112
+ ...planned,
1113
+ notes: [
1114
+ ...planned.notes ?? [],
1115
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1116
+ ]
1117
+ };
1118
+ }
776
1119
  /**
777
1120
  * Start the dev server. It does not return until stopped, so there is no verdict to report
778
1121
  * beyond the exit code the developer's own Ctrl-C produces.
@@ -791,48 +1134,6 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
791
1134
  }
792
1135
  };
793
1136
 
794
- // src/roles/build/owned-paths.ts
795
- var PRESET_OWNED_KEYS = [
796
- "base",
797
- "root",
798
- "define",
799
- "server",
800
- "build",
801
- "nitro",
802
- "resolve",
803
- "plugins"
804
- ];
805
- function describeDepartures(config, defaults) {
806
- const departures = [];
807
- const build = asRecord(config.build);
808
- const target = build?.target;
809
- if (typeof target === "string" && target !== defaults.target) {
810
- departures.push({
811
- rule: "build.target",
812
- reason: `${target}, where the convention is ${defaults.target}`
813
- });
814
- }
815
- const server = asRecord(config.server);
816
- const hosts = server?.allowedHosts;
817
- if (Array.isArray(hosts) && !sameStrings(hosts, defaults.allowedHosts)) {
818
- departures.push({
819
- rule: "server.allowedHosts",
820
- reason: `${JSON.stringify(hosts)}, where the convention is ${JSON.stringify(defaults.allowedHosts)}`
821
- });
822
- }
823
- return departures;
824
- }
825
- function appAdditions(config) {
826
- const owned = new Set(PRESET_OWNED_KEYS);
827
- return Object.keys(config).filter((key) => !owned.has(key)).sort();
828
- }
829
- function asRecord(value) {
830
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
831
- }
832
- function sameStrings(a, b) {
833
- return a.length === b.length && a.every((value, at2) => value === b[at2]);
834
- }
835
-
836
1137
  // src/roles/build/prerequisite.ts
837
1138
  import { spawnSync as spawnSync2 } from "child_process";
838
1139
  var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
@@ -859,23 +1160,6 @@ function runPrerequisite(cwd, env = process.env) {
859
1160
  return { kind: "ran" };
860
1161
  }
861
1162
 
862
- // src/roles/build/resolve-config.ts
863
- async function resolveBuildConfig(cwd) {
864
- try {
865
- const { loadConfigFromFile } = await import("vite");
866
- const loaded = await loadConfigFromFile(
867
- { command: "build", mode: "production" },
868
- void 0,
869
- cwd,
870
- "silent"
871
- );
872
- if (!loaded) return { kind: "failed", reason: "Vite found no config file in this module" };
873
- return { kind: "loaded", config: loaded.config, from: loaded.path };
874
- } catch (error) {
875
- return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
876
- }
877
- }
878
-
879
1163
  // src/roles/build/adapters/vite/vite.adapter.ts
880
1164
  var ViteAdapter = class extends ViteRoleAdapter {
881
1165
  target = "build";
@@ -901,25 +1185,30 @@ var ViteAdapter = class extends ViteRoleAdapter {
901
1185
  return this.spawnVite(ctx, ready.vite, ["build"], 1);
902
1186
  }
903
1187
  /**
904
- * What this module builds with, without building it.
1188
+ * What this module builds with, WITHOUT loading its config.
905
1189
  *
906
- * `--inspect` reads the committed config for every role, and the moment `--build` was wired
907
- * it joined the sweep — so leaving this to throw would have broken `sentinel --inspect` on
908
- * every React module, for a role that had only just arrived. Cheap and honest is the bar.
1190
+ * Everything reported here is read from the manifest and from the config's text: whether the
1191
+ * module imports its toolchain from sentinel, which Vite install would answer, what it still
1192
+ * declares of its own, and whether it has a `prebuild` step.
909
1193
  *
910
- * `vite` is the interesting field and the one nothing else reports: an adopted config takes
911
- * its plugins from sentinel, so a module answering `module` here is one build away from the
912
- * two-copies failure, and this is where that is visible before it happens.
1194
+ * It used to also load the config through Vite and report where it departed from a preset
1195
+ * and what it added on top. Both were dropped with the preset itself. Adoption changes a
1196
+ * config's IMPORTS and nothing else, so there is no preset the app signed up to and no
1197
+ * distance from it to measure; and computing it meant EXECUTING an app's config, which is
1198
+ * expensive and can fail on its own terms (career throws without `VITE_BRAND_ID`). Reading
1199
+ * a file to report on it must not be able to break.
913
1200
  *
914
- * The resolved config comes from Vite's own loader rather than from parsing the file. See
915
- * `resolve-config` for why that is the only honest answer here, and what it costs.
1201
+ * They come back with the shared base, when there is again something an app has chosen to
1202
+ * adopt and can therefore depart from.
916
1203
  */
917
1204
  async inspect(ctx) {
918
1205
  const adoption = readBuildAdoption(ctx.cwd);
919
- const base = {
1206
+ return {
920
1207
  runner: "vite",
921
1208
  configFile: adoption.configFile,
922
1209
  ...adoption.unreadable ? { unreadable: adoption.unreadable } : {},
1210
+ // The field nothing else reports: an adopted config takes its plugins from sentinel, so
1211
+ // a module answering `module` here is one build away from the two-copies failure.
923
1212
  vite: viteOrigin(ctx.cwd),
924
1213
  // Rendered as a labelled block by the shared renderer, the same shape the lint role's
925
1214
  // parked rules and the format role's overrides already use.
@@ -929,36 +1218,6 @@ var ViteAdapter = class extends ViteRoleAdapter {
929
1218
  })),
930
1219
  prerequisite: moduleScripts(ctx.cwd)[PREBUILD_SCRIPT_NAME] ?? null
931
1220
  };
932
- if (adoption.configFile === null) return base;
933
- const resolved = await resolveBuildConfig(ctx.cwd);
934
- if (resolved.kind === "failed") {
935
- return { ...base, configError: resolved.reason };
936
- }
937
- const config = resolved.config;
938
- return {
939
- ...base,
940
- // What the app departs from, and what it adds, are different facts. A DEPARTURE is a
941
- // disagreement with an opinion the preset holds; an ADDITION is the app needing
942
- // something the preset never claimed, which is the preset working rather than being
943
- // worked around.
944
- departures: describeDepartures(config, REACT_APP_DEFAULTS),
945
- additions: appAdditions(config),
946
- resolved: {
947
- base: at(config, "base") ?? null,
948
- target: at(config, "build", "target") ?? null,
949
- sourcemap: at(config, "build", "sourcemap") ?? null,
950
- port: at(config, "server", "port") ?? null,
951
- // The one number nothing else reports and that no formula predicts: career goes DOWN
952
- // to 9998 where the others go up, which is why it is data rather than `port + 1`.
953
- hmrPort: at(config, "server", "hmr", "port") ?? null,
954
- // Counted, not listed. It is 56-67% of every config today, and printing 296 entries
955
- // would bury everything above it. The count is what tells you whether it moved.
956
- aliases: countOf(at(config, "resolve", "alias")),
957
- // Flattened without a depth limit: Vite lets a plugin be an arbitrarily nested array,
958
- // and a magic number here would silently undercount the day someone nests one deeper.
959
- plugins: countOf(at(config, "plugins"))
960
- }
961
- };
962
1221
  }
963
1222
  /**
964
1223
  * Adoption as data, WITHOUT building.
@@ -984,17 +1243,6 @@ var ViteAdapter = class extends ViteRoleAdapter {
984
1243
  };
985
1244
  }
986
1245
  };
987
- function at(config, ...path) {
988
- let value = config;
989
- for (const key of path) {
990
- if (typeof value !== "object" || value === null) return void 0;
991
- value = value[key];
992
- }
993
- return value;
994
- }
995
- function countOf(value) {
996
- return Array.isArray(value) ? value.flat(Number.POSITIVE_INFINITY).length : 0;
997
- }
998
1246
 
999
1247
  // src/roles/build/register.ts
1000
1248
  function registerBuild() {
@@ -1006,12 +1254,12 @@ function registerBuild() {
1006
1254
 
1007
1255
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1008
1256
  import { spawnSync as spawnSync3 } from "child_process";
1009
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1010
- import { join as join14 } from "path";
1257
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
1258
+ import { join as join15 } from "path";
1011
1259
 
1012
1260
  // src/core/config/has-source.ts
1013
1261
  import { readdirSync } from "fs";
1014
- import { extname, join as join8, relative } from "path";
1262
+ import { extname, join as join9, relative as relative3 } from "path";
1015
1263
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1016
1264
  "node_modules",
1017
1265
  "dist",
@@ -1062,7 +1310,7 @@ function hasSourceFiles(cwd, extensions) {
1062
1310
  }
1063
1311
  for (const entry of entries) {
1064
1312
  if (entry.isDirectory()) {
1065
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
1313
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join9(dir, entry.name));
1066
1314
  continue;
1067
1315
  }
1068
1316
  if (wanted.has(extname(entry.name))) return true;
@@ -1083,12 +1331,12 @@ function listSourceFiles(cwd, extensions) {
1083
1331
  continue;
1084
1332
  }
1085
1333
  for (const entry of entries) {
1086
- const full = join8(dir, entry.name);
1334
+ const full = join9(dir, entry.name);
1087
1335
  if (entry.isDirectory()) {
1088
1336
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1089
1337
  continue;
1090
1338
  }
1091
- if (wanted.has(extname(entry.name))) found.push(relative(cwd, full));
1339
+ if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1092
1340
  }
1093
1341
  }
1094
1342
  return found;
@@ -1096,8 +1344,9 @@ function listSourceFiles(cwd, extensions) {
1096
1344
 
1097
1345
  // src/core/named-scope.ts
1098
1346
  var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1099
- function scopeFlags(paths) {
1100
- return paths.length > 0 ? [TOLERATE_EMPTY_FLAG] : [];
1347
+ function scopeFlags(paths, options = []) {
1348
+ if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1349
+ return [TOLERATE_EMPTY_FLAG];
1101
1350
  }
1102
1351
 
1103
1352
  // src/core/settings.ts
@@ -1105,8 +1354,8 @@ var WORKSPACE_ROOT_MARKER = "nx.json";
1105
1354
  var DEFAULT_MAX_DIAGNOSTICS = 100;
1106
1355
 
1107
1356
  // src/core/workspace-prep.ts
1108
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1109
- import { dirname as dirname3, join as join9, relative as relative2 } from "path";
1357
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync } from "fs";
1358
+ import { dirname as dirname3, join as join10, relative as relative4 } from "path";
1110
1359
  var OVERRIDE_KEY = "i18next>typescript";
1111
1360
  var NATIVE_TS_ALIAS = "@typescript/native";
1112
1361
  var WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -1115,7 +1364,7 @@ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1115
1364
  function findWorkspaceRoot(startDir) {
1116
1365
  let dir = startDir;
1117
1366
  for (; ; ) {
1118
- if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
1367
+ if (existsSync9(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
1119
1368
  const parent = dirname3(dir);
1120
1369
  if (parent === dir) return void 0;
1121
1370
  dir = parent;
@@ -1128,9 +1377,9 @@ function declaredNativeTs(pkg) {
1128
1377
  return version || void 0;
1129
1378
  }
1130
1379
  function ensureI18nextSingleton(root, dryRun) {
1131
- const pkgPath = join9(root, "package.json");
1132
- if (!existsSync8(pkgPath)) return void 0;
1133
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1380
+ const pkgPath = join10(root, "package.json");
1381
+ if (!existsSync9(pkgPath)) return void 0;
1382
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1134
1383
  const want = declaredNativeTs(pkg);
1135
1384
  if (!want) return void 0;
1136
1385
  const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
@@ -1143,10 +1392,10 @@ function ensureI18nextSingleton(root, dryRun) {
1143
1392
  return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1144
1393
  }
1145
1394
  function ensureReleaseAgeAllowList(root, dryRun) {
1146
- const yamlPath = join9(root, WORKSPACE_YAML);
1147
- if (!existsSync8(yamlPath)) return void 0;
1395
+ const yamlPath = join10(root, WORKSPACE_YAML);
1396
+ if (!existsSync9(yamlPath)) return void 0;
1148
1397
  const own = readOwnPackage().name;
1149
- const lines = readFileSync6(yamlPath, "utf8").split("\n");
1398
+ const lines = readFileSync7(yamlPath, "utf8").split("\n");
1150
1399
  const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1151
1400
  if (keyIdx === -1) return void 0;
1152
1401
  let lastItemIdx = keyIdx;
@@ -1179,13 +1428,13 @@ var ROOT_PRETTIER_CONFIGS = [
1179
1428
  "prettier.config.mjs"
1180
1429
  ];
1181
1430
  function ensureFormatterExclusion(root, moduleDir, dryRun) {
1182
- const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1431
+ const rel = relative4(root, moduleDir).replaceAll("\\", "/");
1183
1432
  if (rel === "" || rel.startsWith("..")) return void 0;
1184
- const ignorePath = join9(root, PRETTIER_IGNORE);
1185
- const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
1433
+ const ignorePath = join10(root, PRETTIER_IGNORE);
1434
+ const hasPrettier = existsSync9(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync9(join10(root, name)));
1186
1435
  if (!hasPrettier) return void 0;
1187
1436
  const pattern = `/${rel}/`;
1188
- const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
1437
+ const existing = existsSync9(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1189
1438
  const lines = existing.split("\n");
1190
1439
  if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1191
1440
  return void 0;
@@ -1221,10 +1470,10 @@ function ensureWorkspacePrep(opts) {
1221
1470
  function inspectWorkspacePrep(root) {
1222
1471
  const entries = [];
1223
1472
  let pkg = {};
1224
- const pkgPath = join9(root, "package.json");
1225
- if (existsSync8(pkgPath)) {
1473
+ const pkgPath = join10(root, "package.json");
1474
+ if (existsSync9(pkgPath)) {
1226
1475
  try {
1227
- pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1476
+ pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1228
1477
  } catch {
1229
1478
  pkg = {};
1230
1479
  }
@@ -1238,9 +1487,9 @@ function inspectWorkspacePrep(root) {
1238
1487
  });
1239
1488
  }
1240
1489
  const own = readOwnPackage().name;
1241
- const yamlPath = join9(root, WORKSPACE_YAML);
1242
- if (existsSync8(yamlPath)) {
1243
- const yaml = readFileSync6(yamlPath, "utf8");
1490
+ const yamlPath = join10(root, WORKSPACE_YAML);
1491
+ if (existsSync9(yamlPath)) {
1492
+ const yaml = readFileSync7(yamlPath, "utf8");
1244
1493
  const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1245
1494
  if (listed) {
1246
1495
  const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
@@ -1327,8 +1576,8 @@ function writesWhenRewritten(name, command) {
1327
1576
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1328
1577
 
1329
1578
  // src/roles/format/inherited-ignores.ts
1330
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1331
- import { join as join10 } from "path";
1579
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
1580
+ import { join as join11 } from "path";
1332
1581
  var ROOT_IGNORE_FILE = ".prettierignore";
1333
1582
  function isPattern(line) {
1334
1583
  const trimmed = line.trim();
@@ -1343,11 +1592,11 @@ function toModulePattern(pattern) {
1343
1592
  }
1344
1593
  function inheritedIgnorePatterns(workspaceRoot) {
1345
1594
  if (!workspaceRoot) return [];
1346
- const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1347
- if (!existsSync9(path)) return [];
1595
+ const path = join11(workspaceRoot, ROOT_IGNORE_FILE);
1596
+ if (!existsSync10(path)) return [];
1348
1597
  let contents;
1349
1598
  try {
1350
- contents = readFileSync7(path, "utf8");
1599
+ contents = readFileSync8(path, "utf8");
1351
1600
  } catch {
1352
1601
  return [];
1353
1602
  }
@@ -1434,13 +1683,13 @@ function formatPresetFor(preset) {
1434
1683
  }
1435
1684
 
1436
1685
  // src/roles/format/prettier-config.ts
1437
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1438
- import { join as join12 } from "path";
1686
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1687
+ import { join as join13 } from "path";
1439
1688
 
1440
1689
  // src/roles/format/resolve-oxfmt.ts
1441
- import { readFileSync as readFileSync8 } from "fs";
1690
+ import { readFileSync as readFileSync9 } from "fs";
1442
1691
  import { createRequire as createRequire2 } from "module";
1443
- import { dirname as dirname4, join as join11 } from "path";
1692
+ import { dirname as dirname4, join as join12 } from "path";
1444
1693
  function resolveOxfmt(cwd) {
1445
1694
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1446
1695
  }
@@ -1469,8 +1718,8 @@ function configSchema() {
1469
1718
  function readConfigSchema() {
1470
1719
  try {
1471
1720
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
1472
- const schemaPath = join11(dirname4(manifest), "configuration_schema.json");
1473
- return JSON.parse(readFileSync8(schemaPath, "utf8"));
1721
+ const schemaPath = join12(dirname4(manifest), "configuration_schema.json");
1722
+ return JSON.parse(readFileSync9(schemaPath, "utf8"));
1474
1723
  } catch {
1475
1724
  return void 0;
1476
1725
  }
@@ -1501,14 +1750,14 @@ function toOxfmtOverrides(value) {
1501
1750
  return { overrides, unresolved };
1502
1751
  }
1503
1752
  function readPrettierSettings(cwd) {
1504
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
1753
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync11(join13(cwd, name)));
1505
1754
  if (!file) return { options: {}, unresolved: [] };
1506
1755
  if (/\.(js|cjs|mjs)$/.test(file)) {
1507
1756
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1508
1757
  }
1509
1758
  let parsed;
1510
1759
  try {
1511
- parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
1760
+ parsed = parseJsonc(readFileSync10(join13(cwd, file), "utf8"), file);
1512
1761
  } catch {
1513
1762
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1514
1763
  }
@@ -1538,8 +1787,8 @@ function readPrettierSettings(cwd) {
1538
1787
  }
1539
1788
 
1540
1789
  // src/roles/format/read-adoption.ts
1541
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1542
- import { join as join13 } from "path";
1790
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1791
+ import { join as join14 } from "path";
1543
1792
  var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
1544
1793
  configFile,
1545
1794
  preset: null,
@@ -1555,11 +1804,11 @@ function sameValue(a, b) {
1555
1804
  return JSON.stringify(a) === JSON.stringify(b);
1556
1805
  }
1557
1806
  function readFormatAdoption(cwd) {
1558
- const path = join13(cwd, FORMAT_CONFIG_FILE);
1559
- if (!existsSync11(path)) return NOT_ADOPTED2(null);
1807
+ const path = join14(cwd, FORMAT_CONFIG_FILE);
1808
+ if (!existsSync12(path)) return NOT_ADOPTED2(null);
1560
1809
  let parsed;
1561
1810
  try {
1562
- parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
1811
+ parsed = parseJsonc(readFileSync11(path, "utf8"), FORMAT_CONFIG_FILE);
1563
1812
  } catch (error) {
1564
1813
  const reason = error instanceof Error ? error.message : String(error);
1565
1814
  return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -1684,7 +1933,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1684
1933
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1685
1934
  ];
1686
1935
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1687
- (name) => existsSync12(join14(context.cwd, name))
1936
+ (name) => existsSync13(join15(context.cwd, name))
1688
1937
  );
1689
1938
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1690
1939
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1808,7 +2057,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1808
2057
  });
1809
2058
  const result = spawnSync3(
1810
2059
  oxfmt,
1811
- [mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
2060
+ [mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
1812
2061
  {
1813
2062
  cwd: ctx.cwd,
1814
2063
  stdio: "inherit"
@@ -1924,7 +2173,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1924
2173
  modulePrettierDependencies(cwd) {
1925
2174
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1926
2175
  try {
1927
- const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
2176
+ const manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
1928
2177
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1929
2178
  } catch {
1930
2179
  return [];
@@ -2006,8 +2255,8 @@ function registerFormat() {
2006
2255
 
2007
2256
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2008
2257
  import { spawnSync as spawnSync4 } from "child_process";
2009
- import { existsSync as existsSync20, readFileSync as readFileSync19, rmSync, writeFileSync as writeFileSync2 } from "fs";
2010
- import { join as join24 } from "path";
2258
+ import { existsSync as existsSync21, readFileSync as readFileSync20, rmSync, writeFileSync as writeFileSync2 } from "fs";
2259
+ import { join as join25 } from "path";
2011
2260
 
2012
2261
  // src/core/config/deferred-rules.ts
2013
2262
  function deferredRuleNames(rules) {
@@ -2016,7 +2265,7 @@ function deferredRuleNames(rules) {
2016
2265
 
2017
2266
  // src/core/fix-report.ts
2018
2267
  import { statSync } from "fs";
2019
- import { join as join15 } from "path";
2268
+ import { join as join16 } from "path";
2020
2269
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2021
2270
  function readFailure(output) {
2022
2271
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2036,7 +2285,7 @@ function fingerprint(path) {
2036
2285
  function snapshotFiles(cwd, extensions) {
2037
2286
  const snapshot = /* @__PURE__ */ new Map();
2038
2287
  for (const file of listSourceFiles(cwd, extensions)) {
2039
- const mark = fingerprint(join15(cwd, file));
2288
+ const mark = fingerprint(join16(cwd, file));
2040
2289
  if (mark !== void 0) snapshot.set(file, mark);
2041
2290
  }
2042
2291
  return snapshot;
@@ -2044,7 +2293,7 @@ function snapshotFiles(cwd, extensions) {
2044
2293
  function changedSince(cwd, before, extensions) {
2045
2294
  const changed = [];
2046
2295
  for (const file of listSourceFiles(cwd, extensions)) {
2047
- const now = fingerprint(join15(cwd, file));
2296
+ const now = fingerprint(join16(cwd, file));
2048
2297
  if (now === void 0) continue;
2049
2298
  if (before.get(file) !== now) changed.push(file);
2050
2299
  }
@@ -2063,8 +2312,8 @@ function describeFixOutcome(outcome, toolLabel) {
2063
2312
  }
2064
2313
 
2065
2314
  // src/roles/lint/module-baseline.ts
2066
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2067
- import { join as join16 } from "path";
2315
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
2316
+ import { join as join17 } from "path";
2068
2317
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2069
2318
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2070
2319
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2108,11 +2357,11 @@ function describeHolds(holds) {
2108
2357
  return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
2109
2358
  }
2110
2359
  function heldRules(cwd) {
2111
- const path = join16(cwd, LINT_BASELINE_FILE);
2112
- if (!existsSync13(path)) return [];
2360
+ const path = join17(cwd, LINT_BASELINE_FILE);
2361
+ if (!existsSync14(path)) return [];
2113
2362
  try {
2114
2363
  const parsed = parseJsonc(
2115
- readFileSync12(path, "utf8"),
2364
+ readFileSync13(path, "utf8"),
2116
2365
  LINT_BASELINE_FILE
2117
2366
  );
2118
2367
  return Object.keys(parsed.rules ?? {});
@@ -3935,14 +4184,14 @@ function downgradedRulesFor(preset) {
3935
4184
  }
3936
4185
 
3937
4186
  // src/roles/lint/extra-layers.ts
3938
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3939
- import { isAbsolute as isAbsolute2, join as join17, resolve as resolve3 } from "path";
4187
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4188
+ import { isAbsolute as isAbsolute2, join as join18, resolve as resolve3 } from "path";
3940
4189
  function ruleCount(cwd, specifier) {
3941
4190
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3942
- if (!existsSync14(path)) return void 0;
4191
+ if (!existsSync15(path)) return void 0;
3943
4192
  try {
3944
4193
  const parsed = parseJsonc(
3945
- readFileSync13(path, "utf8"),
4194
+ readFileSync14(path, "utf8"),
3946
4195
  specifier
3947
4196
  );
3948
4197
  return Object.keys(parsed.rules ?? {}).length;
@@ -3962,7 +4211,7 @@ function extraLayers(cwd, extendsList) {
3962
4211
  function committedExtendsList(cwd, configFile) {
3963
4212
  try {
3964
4213
  const parsed = parseJsonc(
3965
- readFileSync13(join17(cwd, configFile), "utf8"),
4214
+ readFileSync14(join18(cwd, configFile), "utf8"),
3966
4215
  configFile
3967
4216
  );
3968
4217
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4021,8 +4270,8 @@ function errorCountsByConfigRule(stdout) {
4021
4270
  }
4022
4271
 
4023
4272
  // src/core/config/read-adoption.ts
4024
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4025
- import { join as join19 } from "path";
4273
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4274
+ import { join as join20 } from "path";
4026
4275
 
4027
4276
  // src/core/config/owned-keys.ts
4028
4277
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4037,12 +4286,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4037
4286
  }
4038
4287
 
4039
4288
  // src/core/config/resolve-config-target.ts
4040
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4041
- import { join as join18 } from "path";
4289
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4290
+ import { join as join19 } from "path";
4042
4291
  function readExtends(absolutePath) {
4043
4292
  let parsed;
4044
4293
  try {
4045
- parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
4294
+ parsed = parseJsonc(readFileSync15(absolutePath, "utf8"), absolutePath);
4046
4295
  } catch {
4047
4296
  return [];
4048
4297
  }
@@ -4056,8 +4305,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4056
4305
  let existing;
4057
4306
  let existingExtendsSomething = false;
4058
4307
  for (const candidate of candidates) {
4059
- const absolutePath = join18(moduleDir, candidate);
4060
- if (!existsSync15(absolutePath)) continue;
4308
+ const absolutePath = join19(moduleDir, candidate);
4309
+ if (!existsSync16(absolutePath)) continue;
4061
4310
  const chain = readExtends(absolutePath);
4062
4311
  if (existing === void 0) {
4063
4312
  existing = candidate;
@@ -4090,12 +4339,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4090
4339
  });
4091
4340
  function readAdoption(cwd, options) {
4092
4341
  const target = resolveConfigTarget(cwd, options);
4093
- if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
4342
+ if (target.reason === "none" || !existsSync17(join20(cwd, target.path))) {
4094
4343
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4095
4344
  }
4096
4345
  let parsed;
4097
4346
  try {
4098
- parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
4347
+ parsed = parseJsonc(readFileSync16(join20(cwd, target.path), "utf8"), target.path);
4099
4348
  } catch (error) {
4100
4349
  const reason = error instanceof Error ? error.message : String(error);
4101
4350
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4129,9 +4378,9 @@ function readLintAdoption(cwd) {
4129
4378
  }
4130
4379
 
4131
4380
  // src/roles/lint/resolve-oxlint.ts
4132
- import { existsSync as existsSync17 } from "fs";
4381
+ import { existsSync as existsSync18 } from "fs";
4133
4382
  import { createRequire as createRequire3 } from "module";
4134
- import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
4383
+ import { delimiter as delimiter2, dirname as dirname5, join as join21 } from "path";
4135
4384
  import { fileURLToPath as fileURLToPath2 } from "url";
4136
4385
  var PACKAGE_OF = {
4137
4386
  oxlint: "oxlint",
@@ -4156,32 +4405,31 @@ function tsgolintShim(cwd) {
4156
4405
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4157
4406
  try {
4158
4407
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4159
- candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4160
- candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
4408
+ candidates.push(join21(packageDir, "node_modules", ".bin", "tsgolint"));
4409
+ candidates.push(join21(packageDir, "..", ".bin", "tsgolint"));
4161
4410
  } catch {
4162
4411
  }
4163
4412
  }
4164
4413
  try {
4165
4414
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4166
- candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
4415
+ candidates.push(join21(ownRoot, "node_modules", ".bin", "tsgolint"));
4167
4416
  } catch {
4168
4417
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4169
4418
  }
4170
- return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
4419
+ return candidates.find((candidate) => candidate !== "" && existsSync18(candidate));
4171
4420
  }
4172
4421
  function oxlintSearchPath(cwd) {
4173
4422
  return binSearchPath(cwd);
4174
4423
  }
4175
4424
 
4176
4425
  // src/roles/lint/adapters/oxlint/plan.ts
4177
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4178
- import { join as join23 } from "path";
4426
+ import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
4427
+ import { join as join24 } from "path";
4179
4428
 
4180
4429
  // src/roles/lint/eslint-ignores.ts
4181
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4182
- import { join as join21 } from "path";
4183
- var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
4184
- var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
4430
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4431
+ import { join as join22 } from "path";
4432
+ import { parseSync as parseSync2 } from "oxc-parser";
4185
4433
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4186
4434
  function toOxlintPattern(pattern) {
4187
4435
  return pattern.replace(/^\.\//, "");
@@ -4192,24 +4440,71 @@ function readRootEslintIgnores(root) {
4192
4440
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4193
4441
  }
4194
4442
  function readEslintIgnores(cwd) {
4195
- const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
4443
+ const config = ESLINT_CONFIG_FILES.map((name) => join22(cwd, name)).find((path) => existsSync19(path));
4196
4444
  if (!config) return { patterns: [], unresolved: [] };
4197
4445
  let source;
4198
4446
  try {
4199
- source = readFileSync16(config, "utf8");
4447
+ source = readFileSync17(config, "utf8");
4200
4448
  } catch {
4201
4449
  return { patterns: [], unresolved: [] };
4202
4450
  }
4203
4451
  const patterns = /* @__PURE__ */ new Set();
4204
- for (const block of IGNORE_BLOCKS) {
4205
- for (const [, body] of source.matchAll(block)) {
4206
- for (const [, literal] of (body ?? "").matchAll(STRING_LITERAL)) {
4207
- if (literal) patterns.add(toOxlintPattern(literal));
4452
+ const unresolved = new Set([...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]));
4453
+ const { program, errors } = parseSync2(config, source, { sourceType: "module" });
4454
+ if (errors.length > 0) {
4455
+ return { patterns: [], unresolved: [`${config} could not be parsed`] };
4456
+ }
4457
+ walk(program, (node) => {
4458
+ if (node.type === "CallExpression" && calleeName(node) === "globalIgnores") {
4459
+ for (const literal of stringLiterals(firstArrayArgument(node))) {
4460
+ patterns.add(toOxlintPattern(literal));
4208
4461
  }
4462
+ return;
4209
4463
  }
4464
+ if (node.type !== "ObjectExpression") return;
4465
+ const keys = objectKeys(node);
4466
+ if (!keys.includes("ignores")) return;
4467
+ const values = arrayValues(node, "ignores");
4468
+ if (keys.length > 1) {
4469
+ unresolved.add(
4470
+ `ignores scoped to one config block (with ${keys.filter((k) => k !== "ignores").join(", ")}): ${values.join(", ")} \u2014 exempt from those RULES, not excluded from linting`
4471
+ );
4472
+ return;
4473
+ }
4474
+ for (const literal of values) patterns.add(toOxlintPattern(literal));
4475
+ });
4476
+ return { patterns: [...patterns], unresolved: [...unresolved] };
4477
+ }
4478
+ function walk(node, visit) {
4479
+ if (!node || typeof node !== "object") return;
4480
+ const current = node;
4481
+ if (typeof current.type === "string") visit(current);
4482
+ for (const key of Object.keys(current)) {
4483
+ const value = current[key];
4484
+ if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
4485
+ else if (value && typeof value === "object") walk(value, visit);
4210
4486
  }
4211
- const unresolved = [...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]);
4212
- return { patterns: [...patterns], unresolved: [...new Set(unresolved)] };
4487
+ }
4488
+ function calleeName(node) {
4489
+ const callee = node.callee;
4490
+ return callee?.type === "Identifier" ? callee.name : void 0;
4491
+ }
4492
+ function firstArrayArgument(node) {
4493
+ const args = node.arguments ?? [];
4494
+ return args.find((argument) => argument?.type === "ArrayExpression");
4495
+ }
4496
+ function stringLiterals(array) {
4497
+ const elements = array?.elements ?? [];
4498
+ return elements.filter((element) => element?.type === "Literal" && typeof element.value === "string").map((element) => element.value);
4499
+ }
4500
+ function objectKeys(node) {
4501
+ const properties = node.properties ?? [];
4502
+ return properties.filter((property) => property?.type === "Property" && !property.computed).map((property) => property.key?.name ?? String(property.key?.value ?? "")).filter(Boolean);
4503
+ }
4504
+ function arrayValues(node, name) {
4505
+ const properties = node.properties ?? [];
4506
+ const found = properties.find((property) => property.key?.name === name);
4507
+ return stringLiterals(found?.value);
4213
4508
  }
4214
4509
 
4215
4510
  // src/roles/lint/lint-script.ts
@@ -4250,8 +4545,8 @@ function lintPresetFor(preset) {
4250
4545
  }
4251
4546
 
4252
4547
  // src/roles/lint/rename-suppressions.ts
4253
- import { readdirSync as readdirSync2, readFileSync as readFileSync17, statSync as statSync2 } from "fs";
4254
- import { join as join22, relative as relative3 } from "path";
4548
+ import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
4549
+ import { join as join23, relative as relative5 } from "path";
4255
4550
  var SOURCE_EXTENSIONS = [
4256
4551
  ".ts",
4257
4552
  ".tsx",
@@ -4298,7 +4593,7 @@ function* sourceFiles(dir) {
4298
4593
  return;
4299
4594
  }
4300
4595
  for (const entry of entries) {
4301
- const full = join22(dir, entry);
4596
+ const full = join23(dir, entry);
4302
4597
  let isDirectory;
4303
4598
  try {
4304
4599
  isDirectory = statSync2(full).isDirectory();
@@ -4318,7 +4613,7 @@ function findSuppressionRenames(cwd, renames) {
4318
4613
  for (const file of sourceFiles(cwd)) {
4319
4614
  let content;
4320
4615
  try {
4321
- content = readFileSync17(file, "utf8");
4616
+ content = readFileSync18(file, "utf8");
4322
4617
  } catch {
4323
4618
  continue;
4324
4619
  }
@@ -4342,7 +4637,7 @@ function findSuppressionRenames(cwd, renames) {
4342
4637
  }
4343
4638
  if (hits.length === 0) return;
4344
4639
  found.push({
4345
- file: relative3(cwd, file),
4640
+ file: relative5(cwd, file),
4346
4641
  line: index + 1,
4347
4642
  from: line,
4348
4643
  to: line.replace(rules, renamed.join(", ")),
@@ -4446,7 +4741,7 @@ function plan2(context) {
4446
4741
  keys: removableDeps
4447
4742
  });
4448
4743
  }
4449
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
4744
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync20(join24(context.cwd, name)));
4450
4745
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4451
4746
  operations.push(
4452
4747
  ...nxTargetOperations({
@@ -4544,7 +4839,7 @@ function lintScripts(cwd) {
4544
4839
  function committedExtends(cwd) {
4545
4840
  try {
4546
4841
  const parsed = parseJsonc(
4547
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4842
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4548
4843
  LINT_CONFIG_FILE
4549
4844
  );
4550
4845
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4555,7 +4850,7 @@ function committedExtends(cwd) {
4555
4850
  function committedIgnorePatterns(cwd) {
4556
4851
  try {
4557
4852
  const parsed = parseJsonc(
4558
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4853
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4559
4854
  LINT_CONFIG_FILE
4560
4855
  );
4561
4856
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4567,7 +4862,7 @@ function moduleEslintDependencies(cwd) {
4567
4862
  const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
4568
4863
  let manifest;
4569
4864
  try {
4570
- manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
4865
+ manifest = JSON.parse(readFileSync19(join24(cwd, "package.json"), "utf8"));
4571
4866
  } catch {
4572
4867
  return [];
4573
4868
  }
@@ -4701,20 +4996,20 @@ ${result.stderr ?? ""}`);
4701
4996
  * build the developer can see, rather than a silent half-adoption they cannot.
4702
4997
  */
4703
4998
  writeModuleBaseline(ctx, oxlint, env) {
4704
- const configPath = join24(ctx.cwd, LINT_CONFIG_FILE);
4705
- const baselinePath = join24(ctx.cwd, LINT_BASELINE_FILE);
4706
- if (!existsSync20(configPath)) return;
4999
+ const configPath = join25(ctx.cwd, LINT_CONFIG_FILE);
5000
+ const baselinePath = join25(ctx.cwd, LINT_BASELINE_FILE);
5001
+ if (!existsSync21(configPath)) return;
4707
5002
  let config;
4708
5003
  try {
4709
5004
  config = parseJsonc(
4710
- readFileSync19(configPath, "utf8"),
5005
+ readFileSync20(configPath, "utf8"),
4711
5006
  LINT_CONFIG_FILE
4712
5007
  );
4713
5008
  } catch {
4714
5009
  return;
4715
5010
  }
4716
5011
  const current = Array.isArray(config.extends) ? config.extends : [];
4717
- const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
5012
+ const measurePath = join25(ctx.cwd, LINT_MEASURE_FILE);
4718
5013
  let measured;
4719
5014
  try {
4720
5015
  writeFileSync2(
@@ -4755,14 +5050,14 @@ ${result.stderr ?? ""}`);
4755
5050
  } else {
4756
5051
  rmSync(baselinePath, { force: true });
4757
5052
  }
4758
- if (next.length !== current.length || next.some((entry, at2) => entry !== current[at2])) {
5053
+ if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
4759
5054
  config.extends = next;
4760
5055
  writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
4761
5056
  `);
4762
5057
  }
4763
5058
  }
4764
5059
  async run(ctx) {
4765
- if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
5060
+ if (!existsSync21(join25(ctx.cwd, LINT_CONFIG_FILE))) {
4766
5061
  process.stderr.write(
4767
5062
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
4768
5063
  `
@@ -4807,7 +5102,7 @@ ${result.stderr ?? ""}`);
4807
5102
  valueFlags: OXLINT_VALUE_FLAGS
4808
5103
  });
4809
5104
  const targets = paths.length > 0 ? paths : ["."];
4810
- const emptyScope = scopeFlags(paths);
5105
+ const emptyScope = scopeFlags(paths, passedOptions);
4811
5106
  const lint = (extra) => {
4812
5107
  const passed = [...extra, ...emptyScope, ...passedOptions];
4813
5108
  const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
@@ -4910,7 +5205,7 @@ ${result.stderr ?? ""}`);
4910
5205
  let stub;
4911
5206
  try {
4912
5207
  stub = parseJsonc(
4913
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5208
+ readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
4914
5209
  LINT_CONFIG_FILE
4915
5210
  );
4916
5211
  } catch {
@@ -4920,7 +5215,7 @@ ${result.stderr ?? ""}`);
4920
5215
  for (const entry of stub.extends ?? []) {
4921
5216
  try {
4922
5217
  const preset = parseJsonc(
4923
- readFileSync19(join24(cwd, entry), "utf8"),
5218
+ readFileSync20(join25(cwd, entry), "utf8"),
4924
5219
  entry
4925
5220
  );
4926
5221
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4990,14 +5285,14 @@ ${result.stderr ?? ""}`);
4990
5285
  let parsed;
4991
5286
  try {
4992
5287
  parsed = parseJsonc(
4993
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5288
+ readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
4994
5289
  LINT_CONFIG_FILE
4995
5290
  );
4996
5291
  } catch {
4997
5292
  return void 0;
4998
5293
  }
4999
5294
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5000
- return targets.find((target) => !existsSync20(join24(cwd, target)));
5295
+ return targets.find((target) => !existsSync21(join25(cwd, target)));
5001
5296
  }
5002
5297
  /** Announce what is not enforced, so reduced coverage is never silent. */
5003
5298
  announceDisabled(preset) {
@@ -5029,9 +5324,9 @@ function registerLint() {
5029
5324
 
5030
5325
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5031
5326
  import { spawnSync as spawnSync5 } from "child_process";
5032
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
5327
+ import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
5033
5328
  import { createRequire as createRequire4 } from "module";
5034
- import { join as join26 } from "path";
5329
+ import { join as join27 } from "path";
5035
5330
 
5036
5331
  // src/roles/typescript/presets/nest.json
5037
5332
  var nest_default2 = {
@@ -5212,8 +5507,8 @@ function readTsconfigAdoption(cwd) {
5212
5507
  }
5213
5508
 
5214
5509
  // src/roles/typescript/adapters/tsc/plan.ts
5215
- import { readFileSync as readFileSync20 } from "fs";
5216
- import { join as join25 } from "path";
5510
+ import { readFileSync as readFileSync21 } from "fs";
5511
+ import { join as join26 } from "path";
5217
5512
 
5218
5513
  // src/roles/typescript/typecheck-script.ts
5219
5514
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5306,7 +5601,7 @@ function planAdoption(context) {
5306
5601
  };
5307
5602
  }
5308
5603
  const existing = parseJsonc(
5309
- readFileSync20(join25(context.cwd, target.path), "utf8"),
5604
+ readFileSync21(join26(context.cwd, target.path), "utf8"),
5310
5605
  target.path
5311
5606
  );
5312
5607
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5444,7 +5739,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5444
5739
  let chain;
5445
5740
  try {
5446
5741
  const parsed = parseJsonc(
5447
- readFileSync21(join26(cwd, target.path), "utf8"),
5742
+ readFileSync22(join27(cwd, target.path), "utf8"),
5448
5743
  target.path
5449
5744
  );
5450
5745
  chain = parsed.extends;
@@ -5457,7 +5752,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5457
5752
  );
5458
5753
  if (preset === void 0) return void 0;
5459
5754
  try {
5460
- createRequire4(join26(cwd, "noop.js")).resolve(preset);
5755
+ createRequire4(join27(cwd, "noop.js")).resolve(preset);
5461
5756
  return void 0;
5462
5757
  } catch {
5463
5758
  return preset;
@@ -5553,7 +5848,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5553
5848
  referencedProjects(cwd, config) {
5554
5849
  try {
5555
5850
  const parsed = parseJsonc(
5556
- readFileSync21(join26(cwd, config), "utf8"),
5851
+ readFileSync22(join27(cwd, config), "utf8"),
5557
5852
  config
5558
5853
  );
5559
5854
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5569,7 +5864,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5569
5864
  * check.
5570
5865
  */
5571
5866
  typecheckTarget(cwd) {
5572
- if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
5867
+ if (existsSync22(join27(cwd, "tsconfig.json"))) return "tsconfig.json";
5573
5868
  const target = resolveTsconfigTarget(cwd);
5574
5869
  return target.reason === "none" ? null : target.path;
5575
5870
  }
@@ -5717,7 +6012,7 @@ function detectFramework(packageJson) {
5717
6012
  }
5718
6013
 
5719
6014
  // src/core/dispatch.ts
5720
- import { basename as basename2 } from "path";
6015
+ import { basename as basename3 } from "path";
5721
6016
 
5722
6017
  // src/shared/text.ts
5723
6018
  function ensureLines(current, lines) {
@@ -5773,9 +6068,16 @@ function replaceLines(current, replacements) {
5773
6068
  }
5774
6069
 
5775
6070
  // src/core/apply-plan.ts
5776
- import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6071
+ import { existsSync as existsSync23, readFileSync as readFileSync23, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5777
6072
  import { resolve as resolve4, sep } from "path";
5778
6073
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6074
+
6075
+ // src/shared/deep-merge.ts
6076
+ function isPlainObject(value) {
6077
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6078
+ }
6079
+
6080
+ // src/core/apply-plan.ts
5779
6081
  function resolveWithinRoot(cwd, relativePath) {
5780
6082
  const root = resolve4(cwd);
5781
6083
  const absolutePath = resolve4(root, relativePath);
@@ -5785,7 +6087,7 @@ function resolveWithinRoot(cwd, relativePath) {
5785
6087
  return absolutePath;
5786
6088
  }
5787
6089
  function readIfExists(absolutePath) {
5788
- return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
6090
+ return existsSync23(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
5789
6091
  }
5790
6092
  function* leaves(value, prefix = []) {
5791
6093
  for (const [key, keyValue] of Object.entries(value)) {
@@ -5903,8 +6205,8 @@ function applyPlan(cwd, plan3) {
5903
6205
  }
5904
6206
 
5905
6207
  // src/core/config/preset-evidence.ts
5906
- import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "fs";
5907
- import { join as join27 } from "path";
6208
+ import { existsSync as existsSync24, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "fs";
6209
+ import { join as join28 } from "path";
5908
6210
  var PATH_SIGNALS = [
5909
6211
  {
5910
6212
  preset: "nest",
@@ -5918,11 +6220,11 @@ var DEPENDENCY_SIGNALS = [
5918
6220
  { preset: "nest", pattern: /^@nestjs\// }
5919
6221
  ];
5920
6222
  function dependencyNames(cwd) {
5921
- const path = join27(cwd, "package.json");
5922
- if (!existsSync23(path)) return [];
6223
+ const path = join28(cwd, "package.json");
6224
+ if (!existsSync24(path)) return [];
5923
6225
  try {
5924
6226
  const manifest = parseJsonc(
5925
- readFileSync23(path, "utf8"),
6227
+ readFileSync24(path, "utf8"),
5926
6228
  path
5927
6229
  );
5928
6230
  return [
@@ -5945,7 +6247,7 @@ function declaresJsx(cwd) {
5945
6247
  for (const name of entries) {
5946
6248
  try {
5947
6249
  const config = parseJsonc(
5948
- readFileSync23(join27(cwd, name), "utf8"),
6250
+ readFileSync24(join28(cwd, name), "utf8"),
5949
6251
  name
5950
6252
  );
5951
6253
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6086,7 +6388,7 @@ async function dispatch(opts) {
6086
6388
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
6087
6389
  `);
6088
6390
  await adapter.afterInit({
6089
- module: basename2(opts.cwd),
6391
+ module: basename3(opts.cwd),
6090
6392
  cwd: opts.cwd,
6091
6393
  preset,
6092
6394
  ci: false,
@@ -6108,9 +6410,11 @@ export {
6108
6410
  resolveBin,
6109
6411
  readOwnVersion,
6110
6412
  readProjectPackageJson,
6111
- readNxProjectName,
6413
+ moduleName,
6414
+ buildConfigFile,
6112
6415
  VERBS,
6113
6416
  TARGETS,
6417
+ LONG_RUNNING_TARGETS,
6114
6418
  SWEEPABLE_TARGETS,
6115
6419
  PRESET_NAMES,
6116
6420
  WORKSPACE_ROOT_MARKER,
@@ -6123,4 +6427,4 @@ export {
6123
6427
  detectFramework,
6124
6428
  dispatch
6125
6429
  };
6126
- //# sourceMappingURL=chunk-FLJ2QNFF.js.map
6430
+ //# sourceMappingURL=chunk-7WB7MQHD.js.map