@hublo/sentinel 1.1.6 → 1.2.0-alpha.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.
@@ -1,5 +1,20 @@
1
+ import {
2
+ REACT_APP_DEFAULTS,
3
+ isPlainObject
4
+ } from "./chunk-XSCDJZDY.js";
5
+
1
6
  // src/core/registry.ts
2
7
  var adapters = [];
8
+ var PresetUnsupportedError = class extends Error {
9
+ constructor(target, preset) {
10
+ super(`No adapter for target "${target}" handles preset "${preset}".`);
11
+ this.target = target;
12
+ this.preset = preset;
13
+ this.name = "PresetUnsupportedError";
14
+ }
15
+ target;
16
+ preset;
17
+ };
3
18
  var defaultRunner = {
4
19
  // Filled in by tool branches, e.g. lint: 'eslint', typescript: 'tsc'.
5
20
  };
@@ -12,6 +27,14 @@ function setDefaultRunner(target, runner) {
12
27
  function all() {
13
28
  return adapters;
14
29
  }
30
+ function declaredPresetFor(target, cwd) {
31
+ for (const adapter of adapters) {
32
+ if (adapter.target !== target) continue;
33
+ const declared = adapter.declaredPreset?.(cwd);
34
+ if (declared !== void 0) return declared;
35
+ }
36
+ return void 0;
37
+ }
15
38
  function availableTargets() {
16
39
  return [...new Set(adapters.map((a) => a.target))];
17
40
  }
@@ -24,7 +47,7 @@ function resolve(target, preset, runner) {
24
47
  }
25
48
  const candidates = preset ? forTarget.filter((a) => a.appliesTo(preset)) : forTarget;
26
49
  if (candidates.length === 0) {
27
- throw new Error(`No adapter for target "${target}" handles preset "${preset}".`);
50
+ throw new PresetUnsupportedError(target, preset);
28
51
  }
29
52
  const wanted = runner ?? defaultRunner[target];
30
53
  const available = candidates.map((a) => a.runner).join(", ");
@@ -131,21 +154,79 @@ var TARGETS = [
131
154
  "format",
132
155
  "typescript",
133
156
  "build",
157
+ "dev",
134
158
  "test",
135
159
  "static-analysis",
136
160
  "runtime-analysis",
137
161
  "arch"
138
162
  ];
163
+ var LONG_RUNNING_TARGETS = ["dev"];
164
+ var SWEEPABLE_TARGETS = TARGETS.filter(
165
+ (target) => !LONG_RUNNING_TARGETS.includes(target)
166
+ );
139
167
  var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
140
168
 
141
- // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
169
+ // src/roles/build/adapters/vite/vite-dev.adapter.ts
142
170
  import { spawnSync } from "child_process";
143
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
144
- import { join as join12 } from "path";
171
+
172
+ // src/core/config/tool-args.ts
173
+ import { existsSync as existsSync2 } from "fs";
174
+ import { isAbsolute, resolve as resolve2 } from "path";
175
+ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
176
+ const takesValue = new Set(valueFlags);
177
+ const options = [];
178
+ const paths = [];
179
+ let previousTakesValue = false;
180
+ for (const arg of toolArgs) {
181
+ const looksLikeOption = arg.startsWith("-");
182
+ const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
183
+ if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
184
+ else options.push(arg);
185
+ previousTakesValue = !arg.includes("=") && takesValue.has(arg);
186
+ }
187
+ return { options, paths };
188
+ }
189
+
190
+ // src/shared/resolve-bin.ts
191
+ import { existsSync as existsSync3 } from "fs";
192
+ import { createRequire } from "module";
193
+ import { delimiter, dirname as dirname2, join as join2 } from "path";
194
+ var require2 = createRequire(import.meta.url);
195
+ function resolveBin(fromDir, name) {
196
+ let dir = fromDir;
197
+ for (; ; ) {
198
+ const candidate = join2(dir, "node_modules", ".bin", name);
199
+ if (existsSync3(candidate)) return candidate;
200
+ const parent = dirname2(dir);
201
+ if (parent === dir) return void 0;
202
+ dir = parent;
203
+ }
204
+ }
205
+ function binFromOwnInstall(packageName, binName) {
206
+ try {
207
+ const manifest = require2.resolve(`${packageName}/package.json`);
208
+ const bin = require2(manifest).bin;
209
+ const relative3 = typeof bin === "string" ? bin : bin?.[binName];
210
+ if (!relative3) return void 0;
211
+ const executable = join2(dirname2(manifest), relative3);
212
+ return existsSync3(executable) ? executable : void 0;
213
+ } catch {
214
+ return void 0;
215
+ }
216
+ }
217
+ function binSearchPath(cwd) {
218
+ return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
219
+ " then "
220
+ );
221
+ }
222
+
223
+ // src/roles/build/plan.ts
224
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
225
+ import { join as join7 } from "path";
145
226
 
146
227
  // src/core/config/existing-command.ts
147
228
  import { readFileSync as readFileSync3 } from "fs";
148
- import { join as join3 } from "path";
229
+ import { join as join4 } from "path";
149
230
 
150
231
  // src/shared/jsonc.ts
151
232
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -160,11 +241,11 @@ function parseJsonc(text, source = "config") {
160
241
  }
161
242
 
162
243
  // src/core/config/manifest.ts
163
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
164
- import { basename, join as join2 } from "path";
244
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
245
+ import { basename, join as join3 } from "path";
165
246
  function moduleScripts(cwd) {
166
247
  try {
167
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
248
+ const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
168
249
  return pkg.scripts ?? {};
169
250
  } catch {
170
251
  return {};
@@ -172,7 +253,7 @@ function moduleScripts(cwd) {
172
253
  }
173
254
  function moduleName(cwd) {
174
255
  try {
175
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
256
+ const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
176
257
  return pkg.name;
177
258
  } catch {
178
259
  return void 0;
@@ -192,7 +273,7 @@ function manifestOperation(cwd, scripts) {
192
273
  scripts,
193
274
  devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
194
275
  };
195
- if (existsSync2(join2(cwd, "package.json"))) {
276
+ if (existsSync4(join3(cwd, "package.json"))) {
196
277
  return { kind: "merge-json", path: "package.json", value };
197
278
  }
198
279
  return {
@@ -207,7 +288,7 @@ function nxTargetCommand(cwd, target) {
207
288
  let project;
208
289
  try {
209
290
  project = parseJsonc(
210
- readFileSync3(join3(cwd, "project.json"), "utf8"),
291
+ readFileSync3(join4(cwd, "project.json"), "utf8"),
211
292
  "project.json"
212
293
  );
213
294
  } catch {
@@ -237,9 +318,630 @@ function existingCommand(cwd, target) {
237
318
  return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
238
319
  }
239
320
 
321
+ // src/core/config/nx-target.ts
322
+ import { existsSync as existsSync5 } from "fs";
323
+ import { join as join5 } from "path";
324
+ function nxTargetOperations(options) {
325
+ const { cwd, targets } = options;
326
+ const names = Object.keys(targets);
327
+ if (names.length === 0) return [];
328
+ const operations = [];
329
+ if (existsSync5(join5(cwd, "project.json"))) {
330
+ operations.push({
331
+ kind: "remove-json-keys",
332
+ path: "project.json",
333
+ keys: names.map((name) => ["targets", name])
334
+ });
335
+ }
336
+ operations.push({
337
+ kind: "merge-json",
338
+ path: "package.json",
339
+ value: { nx: { targets } }
340
+ });
341
+ return operations;
342
+ }
343
+
344
+ // src/core/config/tool-script.ts
345
+ var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
346
+ function isSeparatorAt(index) {
347
+ return index % 2 === 1;
348
+ }
349
+ function invokes(segment, binary) {
350
+ return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
351
+ }
352
+ function isSentinelSegment(segment, roleFlag) {
353
+ const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
354
+ return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
355
+ }
356
+ function composeToolScript(existing, options) {
357
+ const { command } = options;
358
+ if (!existing || existing.trim() === "") return command;
359
+ const { roleFlag } = options;
360
+ const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
361
+ const parts = existing.split(SEGMENT_SEPARATOR);
362
+ const commands = parts.filter((_, index) => !isSeparatorAt(index));
363
+ if (!commands.some(isReplaceable)) return existing;
364
+ let replacedOnce = false;
365
+ const rebuilt = parts.map((part, index) => {
366
+ if (isSeparatorAt(index) || !isReplaceable(part)) return part;
367
+ if (replacedOnce) return null;
368
+ replacedOnce = true;
369
+ return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
370
+ });
371
+ const kept = [];
372
+ for (let index = 0; index < rebuilt.length; index += 1) {
373
+ const part = rebuilt[index];
374
+ if (part === null) {
375
+ if (kept.length > 0) kept.pop();
376
+ continue;
377
+ }
378
+ kept.push(part);
379
+ }
380
+ return kept.join("").trim();
381
+ }
382
+ function keepsOtherCommands(script, sentinelCommand) {
383
+ return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
384
+ }
385
+
386
+ // src/roles/build/config-policy.ts
387
+ var BUILD_CONFIG_FILES = [
388
+ "vite.config.ts",
389
+ "vite.config.mts",
390
+ "vite.config.js",
391
+ "vite.config.mjs"
392
+ ];
393
+ var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
394
+ var BUILD_SCRIPT_NAME = "build";
395
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
396
+ var DEV_SCRIPT_NAME = "serve";
397
+ var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
398
+ function buildTarget() {
399
+ return {
400
+ [BUILD_SCRIPT_NAME]: {
401
+ cache: true,
402
+ inputs: ["default", "^default", `{projectRoot}/${BUILD_CONFIG_FILES[0]}`]
403
+ }
404
+ };
405
+ }
406
+ var SENTINEL_OWNED_BUILD_PACKAGES = [
407
+ "vite",
408
+ "@vitejs/plugin-react",
409
+ "@tailwindcss/vite",
410
+ "vite-plugin-svgr",
411
+ "nitro"
412
+ ];
413
+
414
+ // src/roles/build/dev-script.ts
415
+ var CHAIN = " -- ";
416
+ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
417
+ var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
418
+ function appOptions(rest) {
419
+ const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
420
+ const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
421
+ return kept.join(" ");
422
+ }
423
+ function composeDevScript(existing, command) {
424
+ if (existing === void 0 || existing.trim() === "") return command;
425
+ const chunks = existing.trim().split(CHAIN);
426
+ const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
427
+ if (at === -1) return existing;
428
+ const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
429
+ const options = appOptions(rest);
430
+ chunks[at] = options === "" ? command : `${command} -- ${options}`;
431
+ return chunks.join(CHAIN);
432
+ }
433
+
434
+ // src/roles/build/read-adoption.ts
435
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
436
+ import { join as join6 } from "path";
437
+ var NOT_ADOPTED = (configFile, unreadable = null) => ({
438
+ configFile,
439
+ preset: null,
440
+ adopted: false,
441
+ conformant: false,
442
+ drift: [],
443
+ ownDeclarations: [],
444
+ unreadable
445
+ });
446
+ function buildConfigFile(cwd) {
447
+ return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
448
+ }
449
+ function importsPreset(source) {
450
+ const specifier = BUILD_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
451
+ return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${specifier}['"\`]`).test(source);
452
+ }
453
+ var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
454
+ function declaredBuildPreset(cwd) {
455
+ const configFile = buildConfigFile(cwd);
456
+ if (!configFile) return void 0;
457
+ try {
458
+ const source = readFileSync4(join6(cwd, configFile), "utf8");
459
+ return REACT_PLUGIN_SPECIFIERS.some((specifier) => source.includes(specifier)) ? "react" : void 0;
460
+ } catch {
461
+ return void 0;
462
+ }
463
+ }
464
+ function readBuildAdoption(cwd) {
465
+ const configFile = buildConfigFile(cwd);
466
+ if (!configFile) return NOT_ADOPTED(null);
467
+ let source;
468
+ try {
469
+ source = readFileSync4(join6(cwd, configFile), "utf8");
470
+ } catch (error) {
471
+ return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
472
+ }
473
+ if (!importsPreset(source)) return NOT_ADOPTED(configFile);
474
+ const manifest = readProjectPackageJson(cwd);
475
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
476
+ const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
477
+ (name) => ({ name, version: declared[name] })
478
+ );
479
+ return {
480
+ configFile,
481
+ preset: "react",
482
+ adopted: true,
483
+ conformant: ownDeclarations.length === 0,
484
+ drift: ownDeclarations.map(
485
+ ({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
486
+ ),
487
+ ownDeclarations,
488
+ unreadable: null
489
+ };
490
+ }
491
+
492
+ // src/roles/build/plan.ts
493
+ function scaffold() {
494
+ return `import { defineConfig, reactApp } from '${BUILD_PRESET_SPECIFIER}'
495
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
496
+ import { tanstackRouter } from '@tanstack/router-plugin/vite'
497
+ import path from 'node:path'
498
+
499
+ /*
500
+ * Composition comes from sentinel: how the env is loaded, which plugins run under test and
501
+ * which under a real build, the order they run in. The values below are this app's own.
502
+ *
503
+ * TanStack is passed IN rather than imported by sentinel, because the app codes against it
504
+ * directly. \`alias\` is passed through verbatim and never read, so nothing in it can be lost.
505
+ * Anything sentinel does not set goes in \`overrides\`, which merges over the preset last.
506
+ */
507
+ export default defineConfig(({ mode }) =>
508
+ reactApp({
509
+ root: __dirname,
510
+ mode,
511
+ base: '/',
512
+ port: 3000,
513
+ // Explicit, NOT port + 1: the three apps in this repo disagree, one of them goes down.
514
+ hmrPort: 3001,
515
+ router: {
516
+ routesDirectory: path.resolve(__dirname, 'src/routes'),
517
+ generatedRouteTree: path.resolve(__dirname, 'src/routeTree.gen.ts'),
518
+ },
519
+ tanstack: { start: tanstackStart, router: tanstackRouter },
520
+ alias: [],
521
+ }),
522
+ )
523
+ `;
524
+ }
525
+ function ownedBuildDependencies(cwd) {
526
+ const manifest = readProjectPackageJson(cwd);
527
+ const keys = [];
528
+ for (const section of ["dependencies", "devDependencies"]) {
529
+ const declared = manifest[section];
530
+ if (!declared) continue;
531
+ for (const name of SENTINEL_OWNED_BUILD_PACKAGES) {
532
+ if (name in declared) keys.push([section, name]);
533
+ }
534
+ }
535
+ return keys;
536
+ }
537
+ function invokesVite(segment) {
538
+ return /(^|\s|\/)vite(\s|$)/.test(segment.trim());
539
+ }
540
+ function buildScripts(cwd) {
541
+ const own = selfCommand(cwd, "--run --build");
542
+ const scripts = {
543
+ [BUILD_SCRIPT_NAME]: composeToolScript(existingCommand(cwd, BUILD_SCRIPT_NAME), {
544
+ command: own ?? SENTINEL_BUILD_COMMAND,
545
+ replaces: invokesVite,
546
+ // Only meaningful for the self case: it lets a re-init correct a sentinel invocation
547
+ // whose FORM is wrong, without matching another role's script.
548
+ roleFlag: own === void 0 ? void 0 : "--build"
549
+ })
550
+ };
551
+ const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
552
+ if (existingDev !== void 0) {
553
+ scripts[DEV_SCRIPT_NAME] = composeDevScript(
554
+ existingDev,
555
+ selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND
556
+ );
557
+ }
558
+ return scripts;
559
+ }
560
+ function plan(context) {
561
+ if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
562
+ return {
563
+ operations: [],
564
+ skipped: `this module has a Vite config, but it does not build a React app (no @vitejs/plugin-react or @tanstack/react-start in it). The build role ships a React preset only; the two SvelteKit configs in this repo share nothing with it.`
565
+ };
566
+ }
567
+ const notes = [];
568
+ const operations = [];
569
+ const configFile = buildConfigFile(context.cwd);
570
+ if (configFile === void 0) {
571
+ operations.push({ kind: "write", path: BUILD_CONFIG_FILES[0], contents: scaffold() });
572
+ notes.push(
573
+ `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.`
574
+ );
575
+ } else if (!readsPreset(context.cwd, configFile)) {
576
+ notes.push(
577
+ `${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.`
578
+ );
579
+ }
580
+ operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
581
+ operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
582
+ const owned = ownedBuildDependencies(context.cwd);
583
+ if (owned.length > 0) {
584
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
585
+ notes.push(
586
+ `removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
587
+ );
588
+ }
589
+ return { operations, notes };
590
+ }
591
+ function readsPreset(cwd, configFile) {
592
+ if (!existsSync7(join7(cwd, configFile))) return false;
593
+ try {
594
+ return readFileSync5(join7(cwd, configFile), "utf8").includes(BUILD_PRESET_SPECIFIER);
595
+ } catch {
596
+ return false;
597
+ }
598
+ }
599
+
600
+ // src/roles/build/resolve-vite.ts
601
+ function resolveVite(cwd) {
602
+ return binFromOwnInstall("vite", "vite") ?? resolveBin(cwd, "vite");
603
+ }
604
+ function viteOrigin(cwd) {
605
+ if (binFromOwnInstall("vite", "vite")) return "sentinel";
606
+ return resolveBin(cwd, "vite") ? "module" : "none";
607
+ }
608
+
609
+ // src/roles/build/adapters/vite/vite-dev.adapter.ts
610
+ var ViteDevAdapter = class extends BaseAdapter {
611
+ target = "dev";
612
+ runner = "vite";
613
+ appliesTo(preset) {
614
+ return preset === "react";
615
+ }
616
+ declaredPreset(cwd) {
617
+ return declaredBuildPreset(cwd);
618
+ }
619
+ /**
620
+ * The same plan as `--build`, deliberately.
621
+ *
622
+ * `build` and `serve` are one app's two calls into one toolchain, so adopting one without
623
+ * the other leaves the module half-migrated in a way nothing reports. The operations are
624
+ * idempotent, so `--init --build` and `--init --dev` are interchangeable rather than
625
+ * additive.
626
+ */
627
+ plan(context) {
628
+ return plan(context);
629
+ }
630
+ /**
631
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
632
+ * beyond the exit code the developer's own Ctrl-C produces.
633
+ *
634
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
635
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
636
+ * running it again would race the watcher that is about to own the file.
637
+ */
638
+ async run(ctx) {
639
+ if (!readBuildAdoption(ctx.cwd).adopted) {
640
+ process.stderr.write(
641
+ `sentinel dev(vite): no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.
642
+ `
643
+ );
644
+ return { ok: true, code: 0 };
645
+ }
646
+ const vite = resolveVite(ctx.cwd);
647
+ if (!vite) {
648
+ process.stderr.write(
649
+ `sentinel dev(vite): could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
650
+ `
651
+ );
652
+ return { ok: false, code: 1 };
653
+ }
654
+ if (viteOrigin(ctx.cwd) === "module") {
655
+ process.stderr.write(
656
+ `sentinel dev(vite): serving with the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite in one process fail in ways that never mention a version. Remove vite from this module's package.json.
657
+ `
658
+ );
659
+ }
660
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
661
+ const result = spawnSync(vite, [...options, ...paths], { cwd: ctx.cwd, stdio: "inherit" });
662
+ if (result.error) {
663
+ process.stderr.write(`sentinel dev(vite): could not run vite (${result.error.message})
664
+ `);
665
+ return { ok: false, code: 1 };
666
+ }
667
+ const code = result.status ?? 0;
668
+ return { ok: code === 0, code };
669
+ }
670
+ async status(ctx) {
671
+ const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
672
+ return { adopted, preset, conformant, drift, unreadable };
673
+ }
674
+ };
675
+
676
+ // src/roles/build/adapters/vite/vite.adapter.ts
677
+ import { spawnSync as spawnSync3 } from "child_process";
678
+
679
+ // src/roles/build/owned-paths.ts
680
+ var PRESET_OWNED_KEYS = [
681
+ "base",
682
+ "root",
683
+ "define",
684
+ "server",
685
+ "build",
686
+ "resolve",
687
+ "plugins"
688
+ ];
689
+ function describeDepartures(config, defaults) {
690
+ const departures = [];
691
+ const build = asRecord(config.build);
692
+ const target = build?.target;
693
+ if (typeof target === "string" && target !== defaults.target) {
694
+ departures.push({
695
+ rule: "build.target",
696
+ reason: `${target}, where the convention is ${defaults.target}`
697
+ });
698
+ }
699
+ const server = asRecord(config.server);
700
+ const hosts = server?.allowedHosts;
701
+ if (Array.isArray(hosts) && !sameStrings(hosts, defaults.allowedHosts)) {
702
+ departures.push({
703
+ rule: "server.allowedHosts",
704
+ reason: `${JSON.stringify(hosts)}, where the convention is ${JSON.stringify(defaults.allowedHosts)}`
705
+ });
706
+ }
707
+ return departures;
708
+ }
709
+ function appAdditions(config) {
710
+ const owned = new Set(PRESET_OWNED_KEYS);
711
+ return Object.keys(config).filter((key) => !owned.has(key)).sort();
712
+ }
713
+ function asRecord(value) {
714
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
715
+ }
716
+ function sameStrings(a, b) {
717
+ return a.length === b.length && a.every((value, at) => value === b[at]);
718
+ }
719
+
720
+ // src/roles/build/prerequisite.ts
721
+ import { spawnSync as spawnSync2 } from "child_process";
722
+ var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
723
+ function insideBuildScript(env = process.env) {
724
+ return env.npm_lifecycle_event === BUILD_SCRIPT_NAME;
725
+ }
726
+ function runPrerequisite(cwd, env = process.env) {
727
+ const scripts = moduleScripts(cwd);
728
+ const script = scripts[PREBUILD_SCRIPT_NAME];
729
+ if (script === void 0 || script.trim() === "") return { kind: "none" };
730
+ if (insideBuildScript(env)) return { kind: "already-run" };
731
+ const result = spawnSync2("pnpm", ["run", PREBUILD_SCRIPT_NAME], { cwd, stdio: "inherit" });
732
+ if (result.error) {
733
+ return { kind: "failed", code: 1, reason: result.error.message };
734
+ }
735
+ const code = result.status ?? 1;
736
+ if (code !== 0) {
737
+ return {
738
+ kind: "failed",
739
+ code,
740
+ reason: `\`pnpm run ${PREBUILD_SCRIPT_NAME}\` exited ${code}`
741
+ };
742
+ }
743
+ return { kind: "ran" };
744
+ }
745
+
746
+ // src/roles/build/resolve-config.ts
747
+ async function resolveBuildConfig(cwd) {
748
+ try {
749
+ const { loadConfigFromFile } = await import("vite");
750
+ const loaded = await loadConfigFromFile(
751
+ { command: "build", mode: "production" },
752
+ void 0,
753
+ cwd,
754
+ "silent"
755
+ );
756
+ if (!loaded) return { kind: "failed", reason: "Vite found no config file in this module" };
757
+ return { kind: "loaded", config: loaded.config, from: loaded.path };
758
+ } catch (error) {
759
+ return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
760
+ }
761
+ }
762
+
763
+ // src/roles/build/adapters/vite/vite.adapter.ts
764
+ var VITE_VALUE_FLAGS = ["--config", "-c", "--mode", "-m", "--outDir", "--logLevel", "--base"];
765
+ var ViteAdapter = class extends BaseAdapter {
766
+ target = "build";
767
+ runner = "vite";
768
+ /**
769
+ * React only. The two SvelteKit configs in this repo are 34 and 21 lines and share nothing
770
+ * with the React three; claiming them would mean a preset that fits neither.
771
+ */
772
+ appliesTo(preset) {
773
+ return preset === "react";
774
+ }
775
+ /**
776
+ * `react`, read from the module's Vite config rather than from its dependencies.
777
+ *
778
+ * Without this the role is unreachable in practice: the toolchain is declared at the ROOT,
779
+ * so every front app detects as `node` and `appliesTo` filters the adapter out — including
780
+ * during `sentinel --inspect` with no target, which never passes `--preset`.
781
+ */
782
+ declaredPreset(cwd) {
783
+ return declaredBuildPreset(cwd);
784
+ }
785
+ plan(context) {
786
+ return plan(context);
787
+ }
788
+ /**
789
+ * Build the module.
790
+ *
791
+ * A module that has not adopted is reported and passes, the same stance every other role
792
+ * takes: `--run` with no named target sweeps every wired target across the workspace, so
793
+ * failing here would exit non-zero on every module that has not migrated, which is most of
794
+ * them.
795
+ */
796
+ async run(ctx) {
797
+ const adoption = readBuildAdoption(ctx.cwd);
798
+ if (!adoption.adopted) {
799
+ process.stderr.write(
800
+ `sentinel build(vite): no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.
801
+ `
802
+ );
803
+ return { ok: true, code: 0 };
804
+ }
805
+ const prerequisite = runPrerequisite(ctx.cwd);
806
+ if (prerequisite.kind === "failed") {
807
+ process.stderr.write(
808
+ `sentinel build(vite): the ${PREBUILD_SCRIPT_NAME} step failed, so the build was not started: ${prerequisite.reason}
809
+ `
810
+ );
811
+ return { ok: false, code: prerequisite.code };
812
+ }
813
+ const vite = resolveVite(ctx.cwd);
814
+ if (!vite) {
815
+ process.stderr.write(
816
+ `sentinel build(vite): could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
817
+ `
818
+ );
819
+ return { ok: false, code: 1 };
820
+ }
821
+ if (viteOrigin(ctx.cwd) === "module") {
822
+ process.stderr.write(
823
+ `sentinel build(vite): building with the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite in one build fail in ways that never mention a version. Remove vite from this module's package.json.
824
+ `
825
+ );
826
+ }
827
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
828
+ valueFlags: VITE_VALUE_FLAGS
829
+ });
830
+ const result = spawnSync3(vite, ["build", ...options, ...paths], {
831
+ cwd: ctx.cwd,
832
+ stdio: "inherit"
833
+ });
834
+ if (result.error) {
835
+ process.stderr.write(`sentinel build(vite): could not run vite (${result.error.message})
836
+ `);
837
+ return { ok: false, code: 1 };
838
+ }
839
+ const code = result.status ?? 1;
840
+ return { ok: code === 0, code };
841
+ }
842
+ /**
843
+ * What this module builds with, without building it.
844
+ *
845
+ * `--inspect` reads the committed config for every role, and the moment `--build` was wired
846
+ * it joined the sweep — so leaving this to throw would have broken `sentinel --inspect` on
847
+ * every React module, for a role that had only just arrived. Cheap and honest is the bar.
848
+ *
849
+ * `vite` is the interesting field and the one nothing else reports: an adopted config takes
850
+ * its plugins from sentinel, so a module answering `module` here is one build away from the
851
+ * two-copies failure, and this is where that is visible before it happens.
852
+ *
853
+ * The resolved config comes from Vite's own loader rather than from parsing the file. See
854
+ * `resolve-config` for why that is the only honest answer here, and what it costs.
855
+ */
856
+ async inspect(ctx) {
857
+ const adoption = readBuildAdoption(ctx.cwd);
858
+ const base = {
859
+ runner: "vite",
860
+ configFile: adoption.configFile,
861
+ ...adoption.unreadable ? { unreadable: adoption.unreadable } : {},
862
+ vite: viteOrigin(ctx.cwd),
863
+ // Rendered as a labelled block by the shared renderer, the same shape the lint role's
864
+ // parked rules and the format role's overrides already use.
865
+ overrides: adoption.ownDeclarations.map(({ name, version }) => ({
866
+ rule: name,
867
+ reason: `declared by this module at ${version}, where sentinel owns it`
868
+ })),
869
+ prerequisite: moduleScripts(ctx.cwd)[PREBUILD_SCRIPT_NAME] ?? null
870
+ };
871
+ if (adoption.configFile === null) return base;
872
+ const resolved = await resolveBuildConfig(ctx.cwd);
873
+ if (resolved.kind === "failed") {
874
+ return { ...base, configError: resolved.reason };
875
+ }
876
+ const config = resolved.config;
877
+ return {
878
+ ...base,
879
+ // What the app departs from, and what it adds, are different facts. A DEPARTURE is a
880
+ // disagreement with an opinion the preset holds; an ADDITION is the app needing
881
+ // something the preset never claimed, which is the preset working rather than being
882
+ // worked around.
883
+ departures: describeDepartures(config, REACT_APP_DEFAULTS),
884
+ additions: appAdditions(config),
885
+ resolved: {
886
+ base: config.base ?? null,
887
+ target: config.build?.target ?? null,
888
+ sourcemap: config.build?.sourcemap ?? null,
889
+ port: config.server?.port ?? null,
890
+ // The one number nothing else reports and that no formula predicts: career goes DOWN
891
+ // to 9998 where the others go up, which is why it is data rather than `port + 1`.
892
+ hmrPort: config.server?.hmr?.port ?? null,
893
+ // Counted, not listed. It is 56-67% of every config today, and printing 296 entries
894
+ // would bury everything above it. The count is what tells you whether it moved.
895
+ aliases: Array.isArray(config.resolve?.alias) ? config.resolve.alias.length : 0,
896
+ plugins: Array.isArray(config.plugins) ? config.plugins.flat(9).length : 0
897
+ }
898
+ };
899
+ }
900
+ /**
901
+ * Adoption as data, WITHOUT building.
902
+ *
903
+ * Deliberately not "run the build and attach metrics". `--report` is what a migration
904
+ * dashboard calls across the whole workspace, and a bundler is the one tool here where doing
905
+ * the real work costs minutes per module rather than seconds. A report that nobody can
906
+ * afford to run is a report nobody runs.
907
+ *
908
+ * Bundle size is the metric this will eventually want, and it needs a build to produce. It
909
+ * belongs behind an explicit opt-in rather than in the default sweep, and the ticket parks
910
+ * it for exactly that reason.
911
+ */
912
+ async report(ctx) {
913
+ const adoption = readBuildAdoption(ctx.cwd);
914
+ return {
915
+ ok: true,
916
+ code: 0,
917
+ metrics: {
918
+ vite: viteOrigin(ctx.cwd),
919
+ ownBuildDependencies: adoption.ownDeclarations.length
920
+ }
921
+ };
922
+ }
923
+ async status(ctx) {
924
+ const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
925
+ return { adopted, preset, conformant, drift, unreadable };
926
+ }
927
+ };
928
+
929
+ // src/roles/build/register.ts
930
+ function registerBuild() {
931
+ register(new ViteAdapter());
932
+ setDefaultRunner("build", "vite");
933
+ register(new ViteDevAdapter());
934
+ setDefaultRunner("dev", "vite");
935
+ }
936
+
937
+ // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
938
+ import { spawnSync as spawnSync4 } from "child_process";
939
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
940
+ import { join as join14 } from "path";
941
+
240
942
  // src/core/config/has-source.ts
241
943
  import { readdirSync } from "fs";
242
- import { extname, join as join4 } from "path";
944
+ import { extname, join as join8 } from "path";
243
945
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
244
946
  "node_modules",
245
947
  "dist",
@@ -290,7 +992,7 @@ function hasSourceFiles(cwd, extensions) {
290
992
  }
291
993
  for (const entry of entries) {
292
994
  if (entry.isDirectory()) {
293
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join4(dir, entry.name));
995
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
294
996
  continue;
295
997
  }
296
998
  if (wanted.has(extname(entry.name))) return true;
@@ -299,54 +1001,13 @@ function hasSourceFiles(cwd, extensions) {
299
1001
  return false;
300
1002
  }
301
1003
 
302
- // src/core/config/nx-target.ts
303
- import { existsSync as existsSync3 } from "fs";
304
- import { join as join5 } from "path";
305
- function nxTargetOperations(options) {
306
- const { cwd, targets } = options;
307
- const names = Object.keys(targets);
308
- if (names.length === 0) return [];
309
- const operations = [];
310
- if (existsSync3(join5(cwd, "project.json"))) {
311
- operations.push({
312
- kind: "remove-json-keys",
313
- path: "project.json",
314
- keys: names.map((name) => ["targets", name])
315
- });
316
- }
317
- operations.push({
318
- kind: "merge-json",
319
- path: "package.json",
320
- value: { nx: { targets } }
321
- });
322
- return operations;
323
- }
324
-
325
- // src/core/config/tool-args.ts
326
- import { existsSync as existsSync4 } from "fs";
327
- import { isAbsolute, resolve as resolve2 } from "path";
328
- function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
329
- const takesValue = new Set(valueFlags);
330
- const options = [];
331
- const paths = [];
332
- let previousTakesValue = false;
333
- for (const arg of toolArgs) {
334
- const looksLikeOption = arg.startsWith("-");
335
- const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
336
- if (!previousTakesValue && !looksLikeOption && existsSync4(target)) paths.push(arg);
337
- else options.push(arg);
338
- previousTakesValue = !arg.includes("=") && takesValue.has(arg);
339
- }
340
- return { options, paths };
341
- }
342
-
343
1004
  // src/core/settings.ts
344
1005
  var WORKSPACE_ROOT_MARKER = "nx.json";
345
1006
  var DEFAULT_MAX_DIAGNOSTICS = 100;
346
1007
 
347
1008
  // src/core/workspace-prep.ts
348
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync } from "fs";
349
- import { dirname as dirname2, join as join6, relative } from "path";
1009
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1010
+ import { dirname as dirname3, join as join9, relative } from "path";
350
1011
  var OVERRIDE_KEY = "i18next>typescript";
351
1012
  var NATIVE_TS_ALIAS = "@typescript/native";
352
1013
  var WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -355,8 +1016,8 @@ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
355
1016
  function findWorkspaceRoot(startDir) {
356
1017
  let dir = startDir;
357
1018
  for (; ; ) {
358
- if (existsSync5(join6(dir, WORKSPACE_ROOT_MARKER))) return dir;
359
- const parent = dirname2(dir);
1019
+ if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
1020
+ const parent = dirname3(dir);
360
1021
  if (parent === dir) return void 0;
361
1022
  dir = parent;
362
1023
  }
@@ -368,9 +1029,9 @@ function declaredNativeTs(pkg) {
368
1029
  return version || void 0;
369
1030
  }
370
1031
  function ensureI18nextSingleton(root, dryRun) {
371
- const pkgPath = join6(root, "package.json");
372
- if (!existsSync5(pkgPath)) return void 0;
373
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1032
+ const pkgPath = join9(root, "package.json");
1033
+ if (!existsSync8(pkgPath)) return void 0;
1034
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
374
1035
  const want = declaredNativeTs(pkg);
375
1036
  if (!want) return void 0;
376
1037
  const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
@@ -383,10 +1044,10 @@ function ensureI18nextSingleton(root, dryRun) {
383
1044
  return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
384
1045
  }
385
1046
  function ensureReleaseAgeAllowList(root, dryRun) {
386
- const yamlPath = join6(root, WORKSPACE_YAML);
387
- if (!existsSync5(yamlPath)) return void 0;
1047
+ const yamlPath = join9(root, WORKSPACE_YAML);
1048
+ if (!existsSync8(yamlPath)) return void 0;
388
1049
  const own = readOwnPackage().name;
389
- const lines = readFileSync4(yamlPath, "utf8").split("\n");
1050
+ const lines = readFileSync6(yamlPath, "utf8").split("\n");
390
1051
  const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
391
1052
  if (keyIdx === -1) return void 0;
392
1053
  let lastItemIdx = keyIdx;
@@ -421,11 +1082,11 @@ var ROOT_PRETTIER_CONFIGS = [
421
1082
  function ensureFormatterExclusion(root, moduleDir, dryRun) {
422
1083
  const rel = relative(root, moduleDir).replaceAll("\\", "/");
423
1084
  if (rel === "" || rel.startsWith("..")) return void 0;
424
- const ignorePath = join6(root, PRETTIER_IGNORE);
425
- const hasPrettier = existsSync5(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync5(join6(root, name)));
1085
+ const ignorePath = join9(root, PRETTIER_IGNORE);
1086
+ const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
426
1087
  if (!hasPrettier) return void 0;
427
1088
  const pattern = `/${rel}/`;
428
- const existing = existsSync5(ignorePath) ? readFileSync4(ignorePath, "utf8") : "";
1089
+ const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
429
1090
  const lines = existing.split("\n");
430
1091
  if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
431
1092
  return void 0;
@@ -461,10 +1122,10 @@ function ensureWorkspacePrep(opts) {
461
1122
  function inspectWorkspacePrep(root) {
462
1123
  const entries = [];
463
1124
  let pkg = {};
464
- const pkgPath = join6(root, "package.json");
465
- if (existsSync5(pkgPath)) {
1125
+ const pkgPath = join9(root, "package.json");
1126
+ if (existsSync8(pkgPath)) {
466
1127
  try {
467
- pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1128
+ pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
468
1129
  } catch {
469
1130
  pkg = {};
470
1131
  }
@@ -478,9 +1139,9 @@ function inspectWorkspacePrep(root) {
478
1139
  });
479
1140
  }
480
1141
  const own = readOwnPackage().name;
481
- const yamlPath = join6(root, WORKSPACE_YAML);
482
- if (existsSync5(yamlPath)) {
483
- const yaml = readFileSync4(yamlPath, "utf8");
1142
+ const yamlPath = join9(root, WORKSPACE_YAML);
1143
+ if (existsSync8(yamlPath)) {
1144
+ const yaml = readFileSync6(yamlPath, "utf8");
484
1145
  const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
485
1146
  if (listed) {
486
1147
  const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
@@ -493,39 +1154,6 @@ function inspectWorkspacePrep(root) {
493
1154
  return entries;
494
1155
  }
495
1156
 
496
- // src/shared/resolve-bin.ts
497
- import { existsSync as existsSync6 } from "fs";
498
- import { createRequire } from "module";
499
- import { delimiter, dirname as dirname3, join as join7 } from "path";
500
- var require2 = createRequire(import.meta.url);
501
- function resolveBin(fromDir, name) {
502
- let dir = fromDir;
503
- for (; ; ) {
504
- const candidate = join7(dir, "node_modules", ".bin", name);
505
- if (existsSync6(candidate)) return candidate;
506
- const parent = dirname3(dir);
507
- if (parent === dir) return void 0;
508
- dir = parent;
509
- }
510
- }
511
- function binFromOwnInstall(packageName, binName) {
512
- try {
513
- const manifest = require2.resolve(`${packageName}/package.json`);
514
- const bin = require2(manifest).bin;
515
- const relative3 = typeof bin === "string" ? bin : bin?.[binName];
516
- if (!relative3) return void 0;
517
- const executable = join7(dirname3(manifest), relative3);
518
- return existsSync6(executable) ? executable : void 0;
519
- } catch {
520
- return void 0;
521
- }
522
- }
523
- function binSearchPath(cwd) {
524
- return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
525
- " then "
526
- );
527
- }
528
-
529
1157
  // src/roles/format/config-policy.ts
530
1158
  var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
531
1159
  var FORMAT_SCRIPT_NAME = "format";
@@ -572,48 +1200,6 @@ function formatTargets() {
572
1200
  };
573
1201
  }
574
1202
 
575
- // src/core/config/tool-script.ts
576
- var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
577
- function isSeparatorAt(index) {
578
- return index % 2 === 1;
579
- }
580
- function invokes(segment, binary) {
581
- return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
582
- }
583
- function isSentinelSegment(segment, roleFlag) {
584
- const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
585
- return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
586
- }
587
- function composeToolScript(existing, options) {
588
- const { command } = options;
589
- if (!existing || existing.trim() === "") return command;
590
- const { roleFlag } = options;
591
- const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
592
- const parts = existing.split(SEGMENT_SEPARATOR);
593
- const commands = parts.filter((_, index) => !isSeparatorAt(index));
594
- if (!commands.some(isReplaceable)) return existing;
595
- let replacedOnce = false;
596
- const rebuilt = parts.map((part, index) => {
597
- if (isSeparatorAt(index) || !isReplaceable(part)) return part;
598
- if (replacedOnce) return null;
599
- replacedOnce = true;
600
- return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
601
- });
602
- const kept = [];
603
- for (let index = 0; index < rebuilt.length; index += 1) {
604
- const part = rebuilt[index];
605
- if (part === null) {
606
- if (kept.length > 0) kept.pop();
607
- continue;
608
- }
609
- kept.push(part);
610
- }
611
- return kept.join("").trim();
612
- }
613
- function keepsOtherCommands(script, sentinelCommand) {
614
- return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
615
- }
616
-
617
1203
  // src/roles/format/format-script.ts
618
1204
  var SENTINEL_FORMAT_COMMAND = "sentinel --run --format";
619
1205
  function isPrettierSegment(segment) {
@@ -642,8 +1228,8 @@ function writesWhenRewritten(name, command) {
642
1228
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
643
1229
 
644
1230
  // src/roles/format/inherited-ignores.ts
645
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
646
- import { join as join8 } from "path";
1231
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1232
+ import { join as join10 } from "path";
647
1233
  var ROOT_IGNORE_FILE = ".prettierignore";
648
1234
  function isPattern(line) {
649
1235
  const trimmed = line.trim();
@@ -658,11 +1244,11 @@ function toModulePattern(pattern) {
658
1244
  }
659
1245
  function inheritedIgnorePatterns(workspaceRoot) {
660
1246
  if (!workspaceRoot) return [];
661
- const path = join8(workspaceRoot, ROOT_IGNORE_FILE);
662
- if (!existsSync7(path)) return [];
1247
+ const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1248
+ if (!existsSync9(path)) return [];
663
1249
  let contents;
664
1250
  try {
665
- contents = readFileSync5(path, "utf8");
1251
+ contents = readFileSync7(path, "utf8");
666
1252
  } catch {
667
1253
  return [];
668
1254
  }
@@ -749,13 +1335,13 @@ function formatPresetFor(preset) {
749
1335
  }
750
1336
 
751
1337
  // src/roles/format/prettier-config.ts
752
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
753
- import { join as join10 } from "path";
1338
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1339
+ import { join as join12 } from "path";
754
1340
 
755
1341
  // src/roles/format/resolve-oxfmt.ts
756
- import { readFileSync as readFileSync6 } from "fs";
1342
+ import { readFileSync as readFileSync8 } from "fs";
757
1343
  import { createRequire as createRequire2 } from "module";
758
- import { dirname as dirname4, join as join9 } from "path";
1344
+ import { dirname as dirname4, join as join11 } from "path";
759
1345
  function resolveOxfmt(cwd) {
760
1346
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
761
1347
  }
@@ -784,8 +1370,8 @@ function configSchema() {
784
1370
  function readConfigSchema() {
785
1371
  try {
786
1372
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
787
- const schemaPath = join9(dirname4(manifest), "configuration_schema.json");
788
- return JSON.parse(readFileSync6(schemaPath, "utf8"));
1373
+ const schemaPath = join11(dirname4(manifest), "configuration_schema.json");
1374
+ return JSON.parse(readFileSync8(schemaPath, "utf8"));
789
1375
  } catch {
790
1376
  return void 0;
791
1377
  }
@@ -816,14 +1402,14 @@ function toOxfmtOverrides(value) {
816
1402
  return { overrides, unresolved };
817
1403
  }
818
1404
  function readPrettierSettings(cwd) {
819
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join10(cwd, name)));
1405
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
820
1406
  if (!file) return { options: {}, unresolved: [] };
821
1407
  if (/\.(js|cjs|mjs)$/.test(file)) {
822
1408
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
823
1409
  }
824
1410
  let parsed;
825
1411
  try {
826
- parsed = parseJsonc(readFileSync7(join10(cwd, file), "utf8"), file);
1412
+ parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
827
1413
  } catch {
828
1414
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
829
1415
  }
@@ -853,9 +1439,9 @@ function readPrettierSettings(cwd) {
853
1439
  }
854
1440
 
855
1441
  // src/roles/format/read-adoption.ts
856
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
857
- import { join as join11 } from "path";
858
- var NOT_ADOPTED = (configFile, unreadable = null) => ({
1442
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1443
+ import { join as join13 } from "path";
1444
+ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
859
1445
  configFile,
860
1446
  preset: null,
861
1447
  adopted: false,
@@ -870,19 +1456,19 @@ function sameValue(a, b) {
870
1456
  return JSON.stringify(a) === JSON.stringify(b);
871
1457
  }
872
1458
  function readFormatAdoption(cwd) {
873
- const path = join11(cwd, FORMAT_CONFIG_FILE);
874
- if (!existsSync9(path)) return NOT_ADOPTED(null);
1459
+ const path = join13(cwd, FORMAT_CONFIG_FILE);
1460
+ if (!existsSync11(path)) return NOT_ADOPTED2(null);
875
1461
  let parsed;
876
1462
  try {
877
- parsed = parseJsonc(readFileSync8(path, "utf8"), FORMAT_CONFIG_FILE);
1463
+ parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
878
1464
  } catch (error) {
879
1465
  const reason = error instanceof Error ? error.message : String(error);
880
- return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
1466
+ return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
881
1467
  }
882
1468
  const provenance = parsed[PROVENANCE_KEY];
883
- if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED(FORMAT_CONFIG_FILE);
1469
+ if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
884
1470
  if (!hasFormatPreset(provenance.preset)) {
885
- return NOT_ADOPTED(
1471
+ return NOT_ADOPTED2(
886
1472
  FORMAT_CONFIG_FILE,
887
1473
  `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
888
1474
  );
@@ -983,7 +1569,7 @@ var OxfmtAdapter = class extends BaseAdapter {
983
1569
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
984
1570
  ];
985
1571
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
986
- (name) => existsSync10(join12(context.cwd, name))
1572
+ (name) => existsSync12(join14(context.cwd, name))
987
1573
  );
988
1574
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
989
1575
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1042,7 +1628,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1042
1628
  async afterInit(ctx) {
1043
1629
  const oxfmt = resolveOxfmt(ctx.cwd);
1044
1630
  if (!oxfmt) return;
1045
- const pass = spawnSync(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1631
+ const pass = spawnSync4(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1046
1632
  process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
1047
1633
  `);
1048
1634
  if (pass.status === 0) return;
@@ -1105,7 +1691,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1105
1691
  const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1106
1692
  valueFlags: OXFMT_VALUE_FLAGS
1107
1693
  });
1108
- const result = spawnSync(oxfmt, [mode, ...options, ...paths.length > 0 ? paths : ["."]], {
1694
+ const result = spawnSync4(oxfmt, [mode, ...options, ...paths.length > 0 ? paths : ["."]], {
1109
1695
  cwd: ctx.cwd,
1110
1696
  stdio: "inherit"
1111
1697
  });
@@ -1183,7 +1769,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1183
1769
  );
1184
1770
  return { ok: false, code: 1, metrics: { ...base, unformatted: null } };
1185
1771
  }
1186
- const result = spawnSync(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1772
+ const result = spawnSync4(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1187
1773
  if (result.error) {
1188
1774
  process.stderr.write(
1189
1775
  `sentinel format(oxfmt): could not run oxfmt (${result.error.message})
@@ -1219,7 +1805,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1219
1805
  modulePrettierDependencies(cwd) {
1220
1806
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1221
1807
  try {
1222
- const manifest = JSON.parse(readFileSync9(join12(cwd, "package.json"), "utf8"));
1808
+ const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
1223
1809
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1224
1810
  } catch {
1225
1811
  return [];
@@ -1300,9 +1886,9 @@ function registerFormat() {
1300
1886
  }
1301
1887
 
1302
1888
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1303
- import { spawnSync as spawnSync2 } from "child_process";
1304
- import { existsSync as existsSync17, readFileSync as readFileSync16, rmSync, writeFileSync as writeFileSync2 } from "fs";
1305
- import { join as join20 } from "path";
1889
+ import { spawnSync as spawnSync5 } from "child_process";
1890
+ import { existsSync as existsSync19, readFileSync as readFileSync18, rmSync, writeFileSync as writeFileSync2 } from "fs";
1891
+ import { join as join22 } from "path";
1306
1892
 
1307
1893
  // src/core/config/deferred-rules.ts
1308
1894
  function deferredRuleNames(rules) {
@@ -1312,12 +1898,12 @@ function deferredRuleNames(rules) {
1312
1898
  // src/roles/lint/config-policy.ts
1313
1899
  var LINT_CONFIG_FILE = ".oxlintrc.json";
1314
1900
  var LINT_SCRIPT_NAME = "lint";
1315
- var PRESET_DIR = "./node_modules/@hublo/sentinel/oxlint";
1901
+ var PRESET_DIR = "./node_modules/@hublo/sentinel/lint";
1316
1902
  function presetPath(preset) {
1317
1903
  return `${PRESET_DIR}/${preset}.json`;
1318
1904
  }
1319
1905
  function isSentinelPreset(entry) {
1320
- return entry.includes("@hublo/sentinel/oxlint/");
1906
+ return /@hublo\/sentinel\/(lint|oxlint)\//.test(entry);
1321
1907
  }
1322
1908
  function extendsWithPreset(current, variant) {
1323
1909
  const own = presetPath(variant);
@@ -1331,7 +1917,7 @@ function presetVariant(preset, cwd) {
1331
1917
  function presetOfVariant(variant) {
1332
1918
  return variant === "react-lib" ? "react" : variant;
1333
1919
  }
1334
- var SENTINEL_LINT_PRESET = /@hublo\/sentinel\/oxlint\/([a-z-]+)\.json$/;
1920
+ var SENTINEL_LINT_PRESET = /@hublo\/sentinel\/(?:lint|oxlint)\/([a-z-]+)\.json$/;
1335
1921
  function presetNameFromPath(preset) {
1336
1922
  return preset ? SENTINEL_LINT_PRESET.exec(preset)?.[1] ?? void 0 : void 0;
1337
1923
  }
@@ -3116,8 +3702,8 @@ function downgradedRulesFor(preset) {
3116
3702
  }
3117
3703
 
3118
3704
  // src/roles/lint/extra-layers.ts
3119
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3120
- import { isAbsolute as isAbsolute2, join as join13, resolve as resolve3 } from "path";
3705
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3706
+ import { isAbsolute as isAbsolute2, join as join15, resolve as resolve3 } from "path";
3121
3707
 
3122
3708
  // src/roles/lint/module-baseline.ts
3123
3709
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
@@ -3164,15 +3750,12 @@ function describeHolds(holds) {
3164
3750
  }
3165
3751
 
3166
3752
  // src/roles/lint/extra-layers.ts
3167
- function isSentinelPreset2(entry) {
3168
- return entry.includes("@hublo/sentinel/oxlint/");
3169
- }
3170
3753
  function ruleCount(cwd, specifier) {
3171
3754
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3172
- if (!existsSync11(path)) return void 0;
3755
+ if (!existsSync13(path)) return void 0;
3173
3756
  try {
3174
3757
  const parsed = parseJsonc(
3175
- readFileSync10(path, "utf8"),
3758
+ readFileSync12(path, "utf8"),
3176
3759
  specifier
3177
3760
  );
3178
3761
  return Object.keys(parsed.rules ?? {}).length;
@@ -3181,7 +3764,7 @@ function ruleCount(cwd, specifier) {
3181
3764
  }
3182
3765
  }
3183
3766
  function extraLayers(cwd, extendsList) {
3184
- return extendsList.filter((entry) => !isSentinelPreset2(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3767
+ return extendsList.filter((entry) => !isSentinelPreset(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3185
3768
  const count = ruleCount(cwd, entry);
3186
3769
  return {
3187
3770
  rule: entry,
@@ -3192,7 +3775,7 @@ function extraLayers(cwd, extendsList) {
3192
3775
  function committedExtendsList(cwd, configFile) {
3193
3776
  try {
3194
3777
  const parsed = parseJsonc(
3195
- readFileSync10(join13(cwd, configFile), "utf8"),
3778
+ readFileSync12(join15(cwd, configFile), "utf8"),
3196
3779
  configFile
3197
3780
  );
3198
3781
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -3251,8 +3834,8 @@ function errorCountsByConfigRule(stdout) {
3251
3834
  }
3252
3835
 
3253
3836
  // src/core/config/read-adoption.ts
3254
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3255
- import { join as join15 } from "path";
3837
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
3838
+ import { join as join17 } from "path";
3256
3839
 
3257
3840
  // src/core/config/owned-keys.ts
3258
3841
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -3267,12 +3850,12 @@ function localOnlyKeys(config, permitted, presetSets) {
3267
3850
  }
3268
3851
 
3269
3852
  // src/core/config/resolve-config-target.ts
3270
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3271
- import { join as join14 } from "path";
3853
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3854
+ import { join as join16 } from "path";
3272
3855
  function readExtends(absolutePath) {
3273
3856
  let parsed;
3274
3857
  try {
3275
- parsed = parseJsonc(readFileSync11(absolutePath, "utf8"), absolutePath);
3858
+ parsed = parseJsonc(readFileSync13(absolutePath, "utf8"), absolutePath);
3276
3859
  } catch {
3277
3860
  return [];
3278
3861
  }
@@ -3286,8 +3869,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
3286
3869
  let existing;
3287
3870
  let existingExtendsSomething = false;
3288
3871
  for (const candidate of candidates) {
3289
- const absolutePath = join14(moduleDir, candidate);
3290
- if (!existsSync12(absolutePath)) continue;
3872
+ const absolutePath = join16(moduleDir, candidate);
3873
+ if (!existsSync14(absolutePath)) continue;
3291
3874
  const chain = readExtends(absolutePath);
3292
3875
  if (existing === void 0) {
3293
3876
  existing = candidate;
@@ -3310,7 +3893,7 @@ function normaliseExtends(value) {
3310
3893
  }
3311
3894
  return [];
3312
3895
  }
3313
- var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
3896
+ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
3314
3897
  configFile,
3315
3898
  preset: null,
3316
3899
  adopted: false,
@@ -3320,18 +3903,18 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
3320
3903
  });
3321
3904
  function readAdoption(cwd, options) {
3322
3905
  const target = resolveConfigTarget(cwd, options);
3323
- if (target.reason === "none" || !existsSync13(join15(cwd, target.path))) {
3324
- return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
3906
+ if (target.reason === "none" || !existsSync15(join17(cwd, target.path))) {
3907
+ return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
3325
3908
  }
3326
3909
  let parsed;
3327
3910
  try {
3328
- parsed = parseJsonc(readFileSync12(join15(cwd, target.path), "utf8"), target.path);
3911
+ parsed = parseJsonc(readFileSync14(join17(cwd, target.path), "utf8"), target.path);
3329
3912
  } catch (error) {
3330
3913
  const reason = error instanceof Error ? error.message : String(error);
3331
- return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
3914
+ return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
3332
3915
  }
3333
3916
  const preset = normaliseExtends(parsed.extends).find((entry) => options.presetPattern.test(entry)) ?? null;
3334
- if (preset === null) return NOT_ADOPTED2(target.path);
3917
+ if (preset === null) return NOT_ADOPTED3(target.path);
3335
3918
  const settings = options.settingsKey === null ? parsed : parsed[options.settingsKey];
3336
3919
  const drift = presetOwnedKeys(settings, options.permitted, options.presetOwns?.(preset));
3337
3920
  return {
@@ -3359,9 +3942,9 @@ function readLintAdoption(cwd) {
3359
3942
  }
3360
3943
 
3361
3944
  // src/roles/lint/resolve-oxlint.ts
3362
- import { existsSync as existsSync14 } from "fs";
3945
+ import { existsSync as existsSync16 } from "fs";
3363
3946
  import { createRequire as createRequire3 } from "module";
3364
- import { delimiter as delimiter2, dirname as dirname5, join as join16 } from "path";
3947
+ import { delimiter as delimiter2, dirname as dirname5, join as join18 } from "path";
3365
3948
  import { fileURLToPath as fileURLToPath2 } from "url";
3366
3949
  var PACKAGE_OF = {
3367
3950
  oxlint: "oxlint",
@@ -3386,30 +3969,30 @@ function tsgolintShim(cwd) {
3386
3969
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
3387
3970
  try {
3388
3971
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3389
- candidates.push(join16(packageDir, "node_modules", ".bin", "tsgolint"));
3390
- candidates.push(join16(packageDir, "..", ".bin", "tsgolint"));
3972
+ candidates.push(join18(packageDir, "node_modules", ".bin", "tsgolint"));
3973
+ candidates.push(join18(packageDir, "..", ".bin", "tsgolint"));
3391
3974
  } catch {
3392
3975
  }
3393
3976
  }
3394
3977
  try {
3395
3978
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3396
- candidates.push(join16(ownRoot, "node_modules", ".bin", "tsgolint"));
3979
+ candidates.push(join18(ownRoot, "node_modules", ".bin", "tsgolint"));
3397
3980
  } catch {
3398
3981
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3399
3982
  }
3400
- return candidates.find((candidate) => candidate !== "" && existsSync14(candidate));
3983
+ return candidates.find((candidate) => candidate !== "" && existsSync16(candidate));
3401
3984
  }
3402
3985
  function oxlintSearchPath(cwd) {
3403
3986
  return binSearchPath(cwd);
3404
3987
  }
3405
3988
 
3406
3989
  // src/roles/lint/adapters/oxlint/plan.ts
3407
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
3408
- import { join as join19 } from "path";
3990
+ import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
3991
+ import { join as join21 } from "path";
3409
3992
 
3410
3993
  // src/roles/lint/eslint-ignores.ts
3411
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3412
- import { join as join17 } from "path";
3994
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
3995
+ import { join as join19 } from "path";
3413
3996
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3414
3997
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3415
3998
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -3422,11 +4005,11 @@ function readRootEslintIgnores(root) {
3422
4005
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
3423
4006
  }
3424
4007
  function readEslintIgnores(cwd) {
3425
- const config = ESLINT_CONFIG_FILES.map((name) => join17(cwd, name)).find((path) => existsSync15(path));
4008
+ const config = ESLINT_CONFIG_FILES.map((name) => join19(cwd, name)).find((path) => existsSync17(path));
3426
4009
  if (!config) return { patterns: [], unresolved: [] };
3427
4010
  let source;
3428
4011
  try {
3429
- source = readFileSync13(config, "utf8");
4012
+ source = readFileSync15(config, "utf8");
3430
4013
  } catch {
3431
4014
  return { patterns: [], unresolved: [] };
3432
4015
  }
@@ -3480,8 +4063,8 @@ function lintPresetFor(preset) {
3480
4063
  }
3481
4064
 
3482
4065
  // src/roles/lint/rename-suppressions.ts
3483
- import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync } from "fs";
3484
- import { join as join18, relative as relative2 } from "path";
4066
+ import { readdirSync as readdirSync2, readFileSync as readFileSync16, statSync } from "fs";
4067
+ import { join as join20, relative as relative2 } from "path";
3485
4068
  var SOURCE_EXTENSIONS = [
3486
4069
  ".ts",
3487
4070
  ".tsx",
@@ -3528,7 +4111,7 @@ function* sourceFiles(dir) {
3528
4111
  return;
3529
4112
  }
3530
4113
  for (const entry of entries) {
3531
- const full = join18(dir, entry);
4114
+ const full = join20(dir, entry);
3532
4115
  let isDirectory;
3533
4116
  try {
3534
4117
  isDirectory = statSync(full).isDirectory();
@@ -3548,7 +4131,7 @@ function findSuppressionRenames(cwd, renames) {
3548
4131
  for (const file of sourceFiles(cwd)) {
3549
4132
  let content;
3550
4133
  try {
3551
- content = readFileSync14(file, "utf8");
4134
+ content = readFileSync16(file, "utf8");
3552
4135
  } catch {
3553
4136
  continue;
3554
4137
  }
@@ -3627,7 +4210,7 @@ function summariseByPlugin(rules) {
3627
4210
  }
3628
4211
  return [...counts.entries()].sort((left, right) => right[1] - left[1]).map(([plugin, count]) => `${count} ${plugin}`).join(", ");
3629
4212
  }
3630
- function plan(context) {
4213
+ function plan2(context) {
3631
4214
  if (!hasLintPreset(context.preset)) {
3632
4215
  return {
3633
4216
  operations: [],
@@ -3676,7 +4259,7 @@ function plan(context) {
3676
4259
  keys: removableDeps
3677
4260
  });
3678
4261
  }
3679
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync16(join19(context.cwd, name)));
4262
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync18(join21(context.cwd, name)));
3680
4263
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
3681
4264
  operations.push(
3682
4265
  ...nxTargetOperations({
@@ -3774,7 +4357,7 @@ function lintScripts(cwd) {
3774
4357
  function committedExtends(cwd) {
3775
4358
  try {
3776
4359
  const parsed = parseJsonc(
3777
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4360
+ readFileSync17(join21(cwd, LINT_CONFIG_FILE), "utf8"),
3778
4361
  LINT_CONFIG_FILE
3779
4362
  );
3780
4363
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -3785,7 +4368,7 @@ function committedExtends(cwd) {
3785
4368
  function committedIgnorePatterns(cwd) {
3786
4369
  try {
3787
4370
  const parsed = parseJsonc(
3788
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4371
+ readFileSync17(join21(cwd, LINT_CONFIG_FILE), "utf8"),
3789
4372
  LINT_CONFIG_FILE
3790
4373
  );
3791
4374
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -3797,7 +4380,7 @@ function moduleEslintDependencies(cwd) {
3797
4380
  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/");
3798
4381
  let manifest;
3799
4382
  try {
3800
- manifest = JSON.parse(readFileSync15(join19(cwd, "package.json"), "utf8"));
4383
+ manifest = JSON.parse(readFileSync17(join21(cwd, "package.json"), "utf8"));
3801
4384
  } catch {
3802
4385
  return [];
3803
4386
  }
@@ -3856,7 +4439,7 @@ var OxlintAdapter = class extends BaseAdapter {
3856
4439
  * lines. The adapter stays the contract with the engine.
3857
4440
  */
3858
4441
  plan(context) {
3859
- return plan(context);
4442
+ return plan2(context);
3860
4443
  }
3861
4444
  declaredPreset(cwd) {
3862
4445
  return presetOfVariant(presetNameFromPath(readLintAdoption(cwd).preset));
@@ -3866,7 +4449,7 @@ var OxlintAdapter = class extends BaseAdapter {
3866
4449
  * verification run that follows is the one a developer reads.
3867
4450
  */
3868
4451
  fixPass(ctx, oxlint, env) {
3869
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
4452
+ const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
3870
4453
  cwd: ctx.cwd,
3871
4454
  encoding: "utf8",
3872
4455
  env,
@@ -3916,20 +4499,20 @@ var OxlintAdapter = class extends BaseAdapter {
3916
4499
  * build the developer can see, rather than a silent half-adoption they cannot.
3917
4500
  */
3918
4501
  writeModuleBaseline(ctx, oxlint, env) {
3919
- const configPath = join20(ctx.cwd, LINT_CONFIG_FILE);
3920
- const baselinePath = join20(ctx.cwd, LINT_BASELINE_FILE);
3921
- if (!existsSync17(configPath)) return;
4502
+ const configPath = join22(ctx.cwd, LINT_CONFIG_FILE);
4503
+ const baselinePath = join22(ctx.cwd, LINT_BASELINE_FILE);
4504
+ if (!existsSync19(configPath)) return;
3922
4505
  let config;
3923
4506
  try {
3924
4507
  config = parseJsonc(
3925
- readFileSync16(configPath, "utf8"),
4508
+ readFileSync18(configPath, "utf8"),
3926
4509
  LINT_CONFIG_FILE
3927
4510
  );
3928
4511
  } catch {
3929
4512
  return;
3930
4513
  }
3931
4514
  const current = Array.isArray(config.extends) ? config.extends : [];
3932
- const measurePath = join20(ctx.cwd, LINT_MEASURE_FILE);
4515
+ const measurePath = join22(ctx.cwd, LINT_MEASURE_FILE);
3933
4516
  let measured;
3934
4517
  try {
3935
4518
  writeFileSync2(
@@ -3937,7 +4520,7 @@ var OxlintAdapter = class extends BaseAdapter {
3937
4520
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
3938
4521
  `
3939
4522
  );
3940
- measured = spawnSync2(
4523
+ measured = spawnSync5(
3941
4524
  oxlint,
3942
4525
  [
3943
4526
  "-c",
@@ -3977,7 +4560,7 @@ var OxlintAdapter = class extends BaseAdapter {
3977
4560
  }
3978
4561
  }
3979
4562
  async run(ctx) {
3980
- if (!existsSync17(join20(ctx.cwd, LINT_CONFIG_FILE))) {
4563
+ if (!existsSync19(join22(ctx.cwd, LINT_CONFIG_FILE))) {
3981
4564
  process.stderr.write(
3982
4565
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
3983
4566
  `
@@ -4024,7 +4607,7 @@ var OxlintAdapter = class extends BaseAdapter {
4024
4607
  const targets = paths.length > 0 ? paths : ["."];
4025
4608
  const lint = (extra) => {
4026
4609
  const passed = [...extra, ...passedOptions];
4027
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4610
+ const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4028
4611
  cwd: ctx.cwd,
4029
4612
  stdio: "inherit",
4030
4613
  env
@@ -4097,7 +4680,7 @@ var OxlintAdapter = class extends BaseAdapter {
4097
4680
  let stub;
4098
4681
  try {
4099
4682
  stub = parseJsonc(
4100
- readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4683
+ readFileSync18(join22(cwd, LINT_CONFIG_FILE), "utf8"),
4101
4684
  LINT_CONFIG_FILE
4102
4685
  );
4103
4686
  } catch {
@@ -4107,7 +4690,7 @@ var OxlintAdapter = class extends BaseAdapter {
4107
4690
  for (const entry of stub.extends ?? []) {
4108
4691
  try {
4109
4692
  const preset = parseJsonc(
4110
- readFileSync16(join20(cwd, entry), "utf8"),
4693
+ readFileSync18(join22(cwd, entry), "utf8"),
4111
4694
  entry
4112
4695
  );
4113
4696
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4124,7 +4707,7 @@ var OxlintAdapter = class extends BaseAdapter {
4124
4707
  const oxlint = resolveOxlint(ctx.cwd);
4125
4708
  if (!oxlint) return { ok: false, code: 1, metrics: { error: "oxlint not found" } };
4126
4709
  const typeAware = canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [];
4127
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4710
+ const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4128
4711
  cwd: ctx.cwd,
4129
4712
  encoding: "utf8",
4130
4713
  env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) },
@@ -4177,14 +4760,14 @@ var OxlintAdapter = class extends BaseAdapter {
4177
4760
  let parsed;
4178
4761
  try {
4179
4762
  parsed = parseJsonc(
4180
- readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4763
+ readFileSync18(join22(cwd, LINT_CONFIG_FILE), "utf8"),
4181
4764
  LINT_CONFIG_FILE
4182
4765
  );
4183
4766
  } catch {
4184
4767
  return void 0;
4185
4768
  }
4186
4769
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
4187
- return targets.find((target) => !existsSync17(join20(cwd, target)));
4770
+ return targets.find((target) => !existsSync19(join22(cwd, target)));
4188
4771
  }
4189
4772
  /** Announce what is not enforced, so reduced coverage is never silent. */
4190
4773
  announceDisabled(preset) {
@@ -4215,10 +4798,10 @@ function registerLint() {
4215
4798
  }
4216
4799
 
4217
4800
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4218
- import { spawnSync as spawnSync3 } from "child_process";
4219
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
4801
+ import { spawnSync as spawnSync6 } from "child_process";
4802
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
4220
4803
  import { createRequire as createRequire4 } from "module";
4221
- import { join as join22 } from "path";
4804
+ import { join as join24 } from "path";
4222
4805
 
4223
4806
  // src/roles/typescript/presets/base.json
4224
4807
  var base_default3 = {
@@ -4382,7 +4965,7 @@ function resolveTsconfigTarget(moduleDir) {
4382
4965
  }
4383
4966
 
4384
4967
  // src/roles/typescript/read-adoption.ts
4385
- var SENTINEL_PRESET = /^@hublo\/sentinel\/tsconfig\/[a-z-]+$/;
4968
+ var SENTINEL_PRESET = /^@hublo\/sentinel\/(?:typescript|tsconfig)\/[a-z-]+$/;
4386
4969
  function readTsconfigAdoption(cwd) {
4387
4970
  return readAdoption(cwd, {
4388
4971
  candidates: TSCONFIG_CANDIDATES,
@@ -4399,8 +4982,8 @@ function readTsconfigAdoption(cwd) {
4399
4982
  }
4400
4983
 
4401
4984
  // src/roles/typescript/adapters/tsc/plan.ts
4402
- import { readFileSync as readFileSync17 } from "fs";
4403
- import { join as join21 } from "path";
4985
+ import { readFileSync as readFileSync19 } from "fs";
4986
+ import { join as join23 } from "path";
4404
4987
 
4405
4988
  // src/roles/typescript/typecheck-script.ts
4406
4989
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -4434,9 +5017,11 @@ function typecheckScripts(cwd) {
4434
5017
  [TYPECHECK_SCRIPT_NAME]: composeTypecheckScript(existingCommand(cwd, TYPECHECK_SCRIPT_NAME))
4435
5018
  };
4436
5019
  }
5020
+ var SENTINEL_TS_PRESET = /@hublo\/sentinel\/(?:typescript|tsconfig)\//;
4437
5021
  function composeExtends(current, preset) {
4438
5022
  const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
4439
- return chain.includes(preset) ? chain : [...chain, preset];
5023
+ const withoutOwn = chain.filter((entry) => entry !== preset && !SENTINEL_TS_PRESET.test(entry));
5024
+ return [...withoutOwn, preset];
4440
5025
  }
4441
5026
  function declaredPreset(cwd) {
4442
5027
  const { preset } = readTsconfigAdoption(cwd);
@@ -4465,7 +5050,7 @@ function planAdoption(context) {
4465
5050
  };
4466
5051
  }
4467
5052
  const target = resolveTsconfigTarget(context.cwd);
4468
- const preset = `@hublo/sentinel/tsconfig/${context.preset}`;
5053
+ const preset = `@hublo/sentinel/typescript/${context.preset}`;
4469
5054
  const addScript = manifestOperation(context.cwd, typecheckScripts(context.cwd));
4470
5055
  const nxTargets = nxTargetOperations({
4471
5056
  cwd: context.cwd,
@@ -4491,7 +5076,7 @@ function planAdoption(context) {
4491
5076
  };
4492
5077
  }
4493
5078
  const existing = parseJsonc(
4494
- readFileSync17(join21(context.cwd, target.path), "utf8"),
5079
+ readFileSync19(join23(context.cwd, target.path), "utf8"),
4495
5080
  target.path
4496
5081
  );
4497
5082
  const extendsChain = composeExtends(existing.extends, preset);
@@ -4529,7 +5114,7 @@ function planAdoption(context) {
4529
5114
 
4530
5115
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4531
5116
  var SENTINEL_PACKAGE = "@hublo/sentinel";
4532
- var SENTINEL_PRESET_SCOPE = `${SENTINEL_PACKAGE}/tsconfig/`;
5117
+ var SENTINEL_PRESET_SCOPES = [`${SENTINEL_PACKAGE}/typescript/`, `${SENTINEL_PACKAGE}/tsconfig/`];
4533
5118
  var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
4534
5119
  function parseDiagnostics(output) {
4535
5120
  const diagnostics = [];
@@ -4629,7 +5214,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4629
5214
  let chain;
4630
5215
  try {
4631
5216
  const parsed = parseJsonc(
4632
- readFileSync18(join22(cwd, target.path), "utf8"),
5217
+ readFileSync20(join24(cwd, target.path), "utf8"),
4633
5218
  target.path
4634
5219
  );
4635
5220
  chain = parsed.extends;
@@ -4638,11 +5223,11 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4638
5223
  }
4639
5224
  const entries = typeof chain === "string" ? [chain] : Array.isArray(chain) ? chain : [];
4640
5225
  const preset = entries.find(
4641
- (entry) => typeof entry === "string" && entry.startsWith(SENTINEL_PRESET_SCOPE)
5226
+ (entry) => typeof entry === "string" && SENTINEL_PRESET_SCOPES.some((scope) => entry.startsWith(scope))
4642
5227
  );
4643
5228
  if (preset === void 0) return void 0;
4644
5229
  try {
4645
- createRequire4(join22(cwd, "noop.js")).resolve(preset);
5230
+ createRequire4(join24(cwd, "noop.js")).resolve(preset);
4646
5231
  return void 0;
4647
5232
  } catch {
4648
5233
  return preset;
@@ -4683,7 +5268,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4683
5268
  return { ok: false, code: 1 };
4684
5269
  }
4685
5270
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
4686
- const result = spawnSync3(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5271
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
4687
5272
  if (result.error) {
4688
5273
  process.stderr.write(
4689
5274
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4718,7 +5303,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4718
5303
  let worst = 0;
4719
5304
  for (const project of this.referencedProjects(ctx.cwd, config)) {
4720
5305
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
4721
- const result = spawnSync3(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5306
+ const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
4722
5307
  if (result.error) {
4723
5308
  process.stderr.write(
4724
5309
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4738,7 +5323,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4738
5323
  referencedProjects(cwd, config) {
4739
5324
  try {
4740
5325
  const parsed = parseJsonc(
4741
- readFileSync18(join22(cwd, config), "utf8"),
5326
+ readFileSync20(join24(cwd, config), "utf8"),
4742
5327
  config
4743
5328
  );
4744
5329
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -4754,7 +5339,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4754
5339
  * check.
4755
5340
  */
4756
5341
  typecheckTarget(cwd) {
4757
- if (existsSync18(join22(cwd, "tsconfig.json"))) return "tsconfig.json";
5342
+ if (existsSync20(join24(cwd, "tsconfig.json"))) return "tsconfig.json";
4758
5343
  const target = resolveTsconfigTarget(cwd);
4759
5344
  return target.reason === "none" ? null : target.path;
4760
5345
  }
@@ -4807,7 +5392,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4807
5392
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
4808
5393
  }
4809
5394
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
4810
- const result = spawnSync3(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5395
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
4811
5396
  if (result.error) {
4812
5397
  process.stderr.write(
4813
5398
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4850,7 +5435,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4850
5435
  * errors in the output mean the rule must be on.
4851
5436
  */
4852
5437
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
4853
- const shown = spawnSync3(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5438
+ const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
4854
5439
  if (shown.status === 0 && shown.stdout) {
4855
5440
  try {
4856
5441
  const co = parseJsonc(
@@ -4876,6 +5461,7 @@ function registerAdapters() {
4876
5461
  registerTypescript();
4877
5462
  registerLint();
4878
5463
  registerFormat();
5464
+ registerBuild();
4879
5465
  }
4880
5466
 
4881
5467
  // src/core/detect-framework.ts
@@ -4957,16 +5543,9 @@ function replaceLines(current, replacements) {
4957
5543
  }
4958
5544
 
4959
5545
  // src/core/apply-plan.ts
4960
- import { existsSync as existsSync19, readFileSync as readFileSync19, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5546
+ import { existsSync as existsSync21, readFileSync as readFileSync21, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4961
5547
  import { resolve as resolve4, sep } from "path";
4962
5548
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4963
-
4964
- // src/shared/deep-merge.ts
4965
- function isPlainObject(value) {
4966
- return typeof value === "object" && value !== null && !Array.isArray(value);
4967
- }
4968
-
4969
- // src/core/apply-plan.ts
4970
5549
  function resolveWithinRoot(cwd, relativePath) {
4971
5550
  const root = resolve4(cwd);
4972
5551
  const absolutePath = resolve4(root, relativePath);
@@ -4976,7 +5555,7 @@ function resolveWithinRoot(cwd, relativePath) {
4976
5555
  return absolutePath;
4977
5556
  }
4978
5557
  function readIfExists(absolutePath) {
4979
- return existsSync19(absolutePath) ? readFileSync19(absolutePath, "utf8") : void 0;
5558
+ return existsSync21(absolutePath) ? readFileSync21(absolutePath, "utf8") : void 0;
4980
5559
  }
4981
5560
  function* leaves(value, prefix = []) {
4982
5561
  for (const [key, keyValue] of Object.entries(value)) {
@@ -5057,9 +5636,9 @@ function applyOperationTo(current, operation) {
5057
5636
  }
5058
5637
  }
5059
5638
  }
5060
- function preparePlan(cwd, plan2) {
5639
+ function preparePlan(cwd, plan3) {
5061
5640
  const prepared = /* @__PURE__ */ new Map();
5062
- for (const operation of plan2.operations) {
5641
+ for (const operation of plan3.operations) {
5063
5642
  const absolutePath = resolveWithinRoot(cwd, operation.path);
5064
5643
  const existing = prepared.get(operation.path);
5065
5644
  const before = existing?.before ?? readIfExists(absolutePath) ?? "";
@@ -5081,8 +5660,8 @@ function writeFileAtomic(absolutePath, contents) {
5081
5660
  writeFileSync3(tempPath, contents);
5082
5661
  renameSync(tempPath, absolutePath);
5083
5662
  }
5084
- function applyPlan(cwd, plan2) {
5085
- const changed = preparePlan(cwd, plan2).filter((file) => file.before !== file.after);
5663
+ function applyPlan(cwd, plan3) {
5664
+ const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
5086
5665
  for (const file of changed) {
5087
5666
  if (file.deleted) {
5088
5667
  rmSync2(file.absolutePath, { force: true });
@@ -5094,8 +5673,8 @@ function applyPlan(cwd, plan2) {
5094
5673
  }
5095
5674
 
5096
5675
  // src/core/config/preset-evidence.ts
5097
- import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync20 } from "fs";
5098
- import { join as join23 } from "path";
5676
+ import { existsSync as existsSync22, readdirSync as readdirSync3, readFileSync as readFileSync22 } from "fs";
5677
+ import { join as join25 } from "path";
5099
5678
  var PATH_SIGNALS = [
5100
5679
  {
5101
5680
  preset: "nest",
@@ -5109,11 +5688,11 @@ var DEPENDENCY_SIGNALS = [
5109
5688
  { preset: "nest", pattern: /^@nestjs\// }
5110
5689
  ];
5111
5690
  function dependencyNames(cwd) {
5112
- const path = join23(cwd, "package.json");
5113
- if (!existsSync20(path)) return [];
5691
+ const path = join25(cwd, "package.json");
5692
+ if (!existsSync22(path)) return [];
5114
5693
  try {
5115
5694
  const manifest = parseJsonc(
5116
- readFileSync20(path, "utf8"),
5695
+ readFileSync22(path, "utf8"),
5117
5696
  path
5118
5697
  );
5119
5698
  return [
@@ -5136,7 +5715,7 @@ function declaresJsx(cwd) {
5136
5715
  for (const name of entries) {
5137
5716
  try {
5138
5717
  const config = parseJsonc(
5139
- readFileSync20(join23(cwd, name), "utf8"),
5718
+ readFileSync22(join25(cwd, name), "utf8"),
5140
5719
  name
5141
5720
  );
5142
5721
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -5187,14 +5766,14 @@ function resolveFlavour(opts) {
5187
5766
  }
5188
5767
  return detection.preset;
5189
5768
  }
5190
- function previewPlan(opts, plan2) {
5191
- const changed = preparePlan(opts.cwd, plan2).filter((file) => file.before !== file.after);
5769
+ function previewPlan(opts, plan3) {
5770
+ const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
5192
5771
  if (opts.json) {
5193
5772
  process.stdout.write(
5194
5773
  JSON.stringify(
5195
5774
  {
5196
5775
  dryRun: true,
5197
- notes: plan2.notes ?? [],
5776
+ notes: plan3.notes ?? [],
5198
5777
  files: changed.map(({ path, before, after, deleted }) => ({
5199
5778
  path,
5200
5779
  action: deleted ? "delete" : before.length === 0 ? "create" : "update",
@@ -5209,7 +5788,7 @@ function previewPlan(opts, plan2) {
5209
5788
  return 0;
5210
5789
  }
5211
5790
  process.stderr.write(" dry run: no files written\n");
5212
- for (const note of plan2.notes ?? []) process.stderr.write(` ${note}
5791
+ for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
5213
5792
  `);
5214
5793
  if (changed.length === 0) {
5215
5794
  process.stderr.write(" nothing to change\n");
@@ -5230,7 +5809,8 @@ async function dispatch(opts) {
5230
5809
  throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
5231
5810
  }
5232
5811
  const detected = resolveFlavour(opts);
5233
- const adapter = resolve(opts.target, detected, opts.runner);
5812
+ const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
5813
+ const adapter = resolve(opts.target, resolutionPreset, opts.runner);
5234
5814
  const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
5235
5815
  const contradiction = presetContradiction(preset, opts.cwd);
5236
5816
  if (contradiction !== void 0) {
@@ -5239,25 +5819,25 @@ async function dispatch(opts) {
5239
5819
  return 1;
5240
5820
  }
5241
5821
  const context = { cwd: opts.cwd, preset };
5242
- const plan2 = await adapter.plan(context);
5243
- if (plan2.blocked) {
5244
- process.stderr.write(`sentinel (${opts.target}): ${plan2.blocked}
5822
+ const plan3 = await adapter.plan(context);
5823
+ if (plan3.blocked) {
5824
+ process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
5245
5825
  `);
5246
5826
  return 1;
5247
5827
  }
5248
- if (plan2.skipped) {
5249
- process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan2.skipped}`)}
5828
+ if (plan3.skipped) {
5829
+ process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan3.skipped}`)}
5250
5830
  `);
5251
5831
  return 0;
5252
5832
  }
5253
5833
  if (opts.dryRun) {
5254
- return previewPlan(opts, plan2);
5834
+ return previewPlan(opts, plan3);
5255
5835
  }
5256
- for (const change of applyPlan(opts.cwd, plan2)) {
5836
+ for (const change of applyPlan(opts.cwd, plan3)) {
5257
5837
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
5258
5838
  `);
5259
5839
  }
5260
- for (const note of plan2.notes ?? []) process.stderr.write(` ${note}
5840
+ for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
5261
5841
  `);
5262
5842
  if (adapter.afterInit) {
5263
5843
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
@@ -5274,12 +5854,15 @@ async function dispatch(opts) {
5274
5854
  }
5275
5855
 
5276
5856
  export {
5857
+ PresetUnsupportedError,
5277
5858
  register,
5278
5859
  setDefaultRunner,
5279
5860
  all,
5861
+ declaredPresetFor,
5280
5862
  availableTargets,
5281
5863
  resolve,
5282
5864
  BaseAdapter,
5865
+ resolveBin,
5283
5866
  readOwnVersion,
5284
5867
  readProjectPackageJson,
5285
5868
  readNxProjectName,
@@ -5288,13 +5871,13 @@ export {
5288
5871
  ensureWorkspacePrep,
5289
5872
  inspectWorkspacePrep,
5290
5873
  palette,
5291
- resolveBin,
5292
5874
  VERBS,
5293
5875
  TARGETS,
5876
+ SWEEPABLE_TARGETS,
5294
5877
  PRESET_NAMES,
5295
5878
  registerAdapters,
5296
5879
  describeFramework,
5297
5880
  detectFramework,
5298
5881
  dispatch
5299
5882
  };
5300
- //# sourceMappingURL=chunk-RGIEWANO.js.map
5883
+ //# sourceMappingURL=chunk-TWL6T237.js.map