@hublo/sentinel 1.2.0-alpha.2 → 1.2.0-alpha.21

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.
@@ -67,22 +67,9 @@ function resolve(target, preset, runner) {
67
67
  return matching[0];
68
68
  }
69
69
 
70
- // src/core/base-adapter.ts
71
- var BaseAdapter = class {
72
- inspect(_ctx) {
73
- throw new Error(`${this.runner}: --inspect not implemented yet`);
74
- }
75
- report(_ctx) {
76
- throw new Error(`${this.runner}: --report not implemented yet`);
77
- }
78
- status(_ctx) {
79
- throw new Error(`${this.runner}: --status not implemented yet`);
80
- }
81
- };
82
-
83
70
  // src/shared/package-json.ts
84
71
  import { existsSync, readFileSync } from "fs";
85
- import { dirname, join } from "path";
72
+ import { basename, dirname, join } from "path";
86
73
  import { fileURLToPath } from "url";
87
74
  function readOwnPackage() {
88
75
  let dir = dirname(fileURLToPath(import.meta.url));
@@ -127,6 +114,23 @@ function readNxProjectName(dir) {
127
114
  return void 0;
128
115
  }
129
116
  }
117
+ function moduleName(dir) {
118
+ const declared = readProjectPackageJson(dir).name;
119
+ return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
120
+ }
121
+
122
+ // src/core/base-adapter.ts
123
+ var BaseAdapter = class {
124
+ inspect(_ctx) {
125
+ throw new Error(`${this.runner}: --inspect not implemented yet`);
126
+ }
127
+ report(_ctx) {
128
+ throw new Error(`${this.runner}: --report not implemented yet`);
129
+ }
130
+ status(_ctx) {
131
+ throw new Error(`${this.runner}: --status not implemented yet`);
132
+ }
133
+ };
130
134
 
131
135
  // src/core/domain.ts
132
136
  var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
@@ -161,11 +165,273 @@ function palette(stream) {
161
165
  };
162
166
  }
163
167
 
168
+ // src/roles/build/nest/nx-target.ts
169
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
170
+ import { join as join2, relative } from "path";
171
+ var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
172
+ var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
173
+ var readProjectJson = (cwd) => {
174
+ const path = join2(cwd, "project.json");
175
+ if (!existsSync2(path)) return void 0;
176
+ try {
177
+ return JSON.parse(readFileSync2(path, "utf8"));
178
+ } catch {
179
+ return void 0;
180
+ }
181
+ };
182
+ function webpackBuildTarget(cwd) {
183
+ const project = readProjectJson(cwd);
184
+ const target = project?.targets?.build;
185
+ if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
186
+ const { outputPath, main, webpackConfig, transformers, assets } = target.options ?? {};
187
+ if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
188
+ return void 0;
189
+ }
190
+ return {
191
+ project: project.name,
192
+ outputPath,
193
+ main,
194
+ webpackConfig,
195
+ configurations: target.configurations,
196
+ transformers,
197
+ assets
198
+ };
199
+ }
200
+ function refusalFor(cwd, target) {
201
+ if (workspaceRootFor(cwd, target) === void 0) {
202
+ 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.`;
203
+ }
204
+ if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
205
+ 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.`;
206
+ }
207
+ const live = liveFileReplacements(cwd, target);
208
+ if (live.length > 0) {
209
+ 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.`;
210
+ }
211
+ return void 0;
212
+ }
213
+ function liveFileReplacements(cwd, target) {
214
+ const workspaceRoot = workspaceRootFor(cwd, target);
215
+ if (workspaceRoot === void 0) return [];
216
+ const configurations = target.configurations ?? {};
217
+ const live = [];
218
+ for (const configuration of Object.values(configurations)) {
219
+ for (const replacement of configuration.fileReplacements ?? []) {
220
+ const exists = existsSync2(join2(workspaceRoot, replacement.replace)) || existsSync2(join2(workspaceRoot, replacement.with));
221
+ if (exists) live.push(replacement.replace);
222
+ }
223
+ }
224
+ return live;
225
+ }
226
+ function workspaceRootFor(cwd, target) {
227
+ const moduleRoot = target.main.replace(/\/src\/.*$/, "");
228
+ if (moduleRoot === target.main || !cwd.endsWith(moduleRoot)) return void 0;
229
+ return cwd.slice(0, cwd.length - moduleRoot.length - 1);
230
+ }
231
+ function workspaceRootHops(cwd, target) {
232
+ const root = workspaceRootFor(cwd, target);
233
+ if (root === void 0) {
234
+ throw new Error(`sentinel build(nest): cannot place ${cwd} in a workspace from ${target.main}`);
235
+ }
236
+ const rel = relative(root, cwd);
237
+ return rel === "" ? 0 : rel.split(/[\\/]/).length;
238
+ }
239
+
240
+ // src/roles/build/read-adoption.ts
241
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
242
+ import { join as join3 } from "path";
243
+
244
+ // src/roles/build/presets/requirements.json
245
+ var requirements_default = {
246
+ svelte: {
247
+ why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
248
+ requires: {
249
+ "@sveltejs/vite-plugin-svelte": "7.0.0",
250
+ svelte: "5.46.4"
251
+ }
252
+ }
253
+ };
254
+
255
+ // src/roles/build/presets/toolchain.json
256
+ var toolchain_default = {
257
+ $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.",
258
+ owned: [
259
+ {
260
+ package: "vite",
261
+ specifier: "vite",
262
+ named: ["defineConfig", "loadEnv", "mergeConfig"],
263
+ types: [
264
+ "Plugin",
265
+ "PluginOption",
266
+ "SassPreprocessorOptions",
267
+ "UserConfig",
268
+ "UserConfigExport"
269
+ ],
270
+ 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."
271
+ },
272
+ {
273
+ package: "@vitejs/plugin-react",
274
+ specifier: "@vitejs/plugin-react",
275
+ default: "react",
276
+ why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
277
+ },
278
+ {
279
+ package: "@tailwindcss/vite",
280
+ specifier: "@tailwindcss/vite",
281
+ default: "tailwindcss",
282
+ why: "the Tailwind pipeline, identical in all three front apps."
283
+ },
284
+ {
285
+ package: "vite-plugin-svgr",
286
+ specifier: "vite-plugin-svgr",
287
+ default: "svgr",
288
+ why: "SVG as components, identical in all three front apps."
289
+ },
290
+ {
291
+ package: "nitro",
292
+ specifier: "nitro/vite",
293
+ named: ["nitro"],
294
+ 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."
295
+ }
296
+ ],
297
+ add: [
298
+ {
299
+ package: "@hublo/sentinel",
300
+ 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."
301
+ }
302
+ ],
303
+ $notOwned: {
304
+ "@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.",
305
+ "@locator/babel-jsx": "same, and same owners.",
306
+ "@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
307
+ "@tanstack/router-plugin": "generates the app's own route tree.",
308
+ "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."
309
+ }
310
+ };
311
+
312
+ // src/roles/build/preset-data.ts
313
+ var REQUIREMENTS = requirements_default;
314
+ var OWNED_PACKAGES = toolchain_default.owned;
315
+
316
+ // src/roles/build/config-policy.ts
317
+ var COMPANION_CONFIG_FILES = [
318
+ "vitest.config.ts",
319
+ "vitest.config.mts",
320
+ "vitest.config.js",
321
+ "vitest.config.mjs"
322
+ ];
323
+ var BUILD_CONFIG_FILES = [
324
+ "vite.config.ts",
325
+ "vite.config.mts",
326
+ "vite.config.js",
327
+ "vite.config.mjs"
328
+ ];
329
+ var BUILD_PRESET_SPECIFIERS = {
330
+ react: "@hublo/sentinel/build/react",
331
+ nest: "@hublo/sentinel/build/nest"
332
+ };
333
+ var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
334
+ var BUILD_SCRIPT_NAME = "build";
335
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
336
+ var DEV_SCRIPT_NAME = "serve";
337
+ var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
338
+ var BUILD_OUTPUT_CANDIDATES = ["dist", ".output", "build"];
339
+ function buildTarget(options) {
340
+ const { moduleName: moduleName3, configFiles = BUILD_CONFIG_FILES, outputs } = options;
341
+ return {
342
+ [BUILD_SCRIPT_NAME]: {
343
+ executor: "nx:run-commands",
344
+ options: {
345
+ command: `pnpm --filter ${moduleName3} run ${BUILD_SCRIPT_NAME}`,
346
+ forwardAllArgs: false
347
+ },
348
+ cache: true,
349
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
350
+ outputs: [...outputs]
351
+ }
352
+ };
353
+ }
354
+ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
355
+ (entry) => entry.package
356
+ );
357
+
358
+ // src/roles/build/read-adoption.ts
359
+ var NOT_ADOPTED = (configFile, unreadable = null) => ({
360
+ configFile,
361
+ preset: null,
362
+ adopted: false,
363
+ conformant: false,
364
+ drift: [],
365
+ ownDeclarations: [],
366
+ unreadable
367
+ });
368
+ function buildConfigFile(cwd) {
369
+ return BUILD_CONFIG_FILES.find((name) => existsSync3(join3(cwd, name)));
370
+ }
371
+ function importsSpecifier(source, specifier) {
372
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
373
+ return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
374
+ }
375
+ function importsPreset(source) {
376
+ return Object.values(BUILD_PRESET_SPECIFIERS).some(
377
+ (specifier) => importsSpecifier(source, specifier)
378
+ );
379
+ }
380
+ var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
381
+ function declaredBuildPreset(cwd) {
382
+ const source = readBuildConfigSource(cwd);
383
+ if (source === void 0) {
384
+ if (buildConfigFile(cwd) !== void 0) return void 0;
385
+ return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
386
+ }
387
+ if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
388
+ if (importsPreset(source)) return "react";
389
+ return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
390
+ }
391
+ function readBuildConfigSource(cwd) {
392
+ const configFile = buildConfigFile(cwd);
393
+ if (!configFile) return void 0;
394
+ try {
395
+ return readFileSync3(join3(cwd, configFile), "utf8");
396
+ } catch {
397
+ return void 0;
398
+ }
399
+ }
400
+ function readBuildAdoption(cwd) {
401
+ const configFile = buildConfigFile(cwd);
402
+ if (!configFile) return NOT_ADOPTED(null);
403
+ let source;
404
+ try {
405
+ source = readFileSync3(join3(cwd, configFile), "utf8");
406
+ } catch (error) {
407
+ return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
408
+ }
409
+ if (!importsPreset(source)) return NOT_ADOPTED(configFile);
410
+ const manifest = readProjectPackageJson(cwd);
411
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
412
+ const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
413
+ (name) => ({ name, version: declared[name] })
414
+ );
415
+ return {
416
+ configFile,
417
+ // Read from the config rather than assumed: the two presets are adopted the same way and
418
+ // reporting the wrong one would make `--inspect` lie about what a module builds.
419
+ preset: declaredBuildPreset(cwd) ?? "react",
420
+ adopted: true,
421
+ conformant: ownDeclarations.length === 0,
422
+ drift: ownDeclarations.map(
423
+ ({ 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`
424
+ ),
425
+ ownDeclarations,
426
+ unreadable: null
427
+ };
428
+ }
429
+
164
430
  // src/roles/build/adapters/vite/vite-role.adapter.ts
165
431
  import { spawnSync } from "child_process";
166
432
 
167
433
  // src/core/config/tool-args.ts
168
- import { existsSync as existsSync2 } from "fs";
434
+ import { existsSync as existsSync4 } from "fs";
169
435
  import { isAbsolute, resolve as resolve2 } from "path";
170
436
  function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
171
437
  const takesValue = new Set(valueFlags);
@@ -175,7 +441,7 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
175
441
  for (const arg of toolArgs) {
176
442
  const looksLikeOption = arg.startsWith("-");
177
443
  const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
178
- if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
444
+ if (!previousTakesValue && !looksLikeOption && existsSync4(target)) paths.push(arg);
179
445
  else options.push(arg);
180
446
  previousTakesValue = !arg.includes("=") && takesValue.has(arg);
181
447
  }
@@ -183,15 +449,15 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
183
449
  }
184
450
 
185
451
  // src/shared/resolve-bin.ts
186
- import { existsSync as existsSync3 } from "fs";
452
+ import { existsSync as existsSync5 } from "fs";
187
453
  import { createRequire } from "module";
188
- import { delimiter, dirname as dirname2, join as join2 } from "path";
454
+ import { delimiter, dirname as dirname2, join as join4 } from "path";
189
455
  var require2 = createRequire(import.meta.url);
190
456
  function resolveBin(fromDir, name) {
191
457
  let dir = fromDir;
192
458
  for (; ; ) {
193
- const candidate = join2(dir, "node_modules", ".bin", name);
194
- if (existsSync3(candidate)) return candidate;
459
+ const candidate = join4(dir, "node_modules", ".bin", name);
460
+ if (existsSync5(candidate)) return candidate;
195
461
  const parent = dirname2(dir);
196
462
  if (parent === dir) return void 0;
197
463
  dir = parent;
@@ -201,10 +467,10 @@ function binFromOwnInstall(packageName, binName) {
201
467
  try {
202
468
  const manifest = require2.resolve(`${packageName}/package.json`);
203
469
  const bin = require2(manifest).bin;
204
- const relative4 = typeof bin === "string" ? bin : bin?.[binName];
205
- if (!relative4) return void 0;
206
- const executable = join2(dirname2(manifest), relative4);
207
- return existsSync3(executable) ? executable : void 0;
470
+ const relative5 = typeof bin === "string" ? bin : bin?.[binName];
471
+ if (!relative5) return void 0;
472
+ const executable = join4(dirname2(manifest), relative5);
473
+ return existsSync5(executable) ? executable : void 0;
208
474
  } catch {
209
475
  return void 0;
210
476
  }
@@ -216,12 +482,12 @@ function binSearchPath(cwd) {
216
482
  }
217
483
 
218
484
  // src/roles/build/plan.ts
219
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
220
- import { basename as basename2, join as join7 } from "path";
485
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
486
+ import { join as join9 } from "path";
221
487
 
222
488
  // src/core/config/existing-command.ts
223
- import { readFileSync as readFileSync3 } from "fs";
224
- import { join as join4 } from "path";
489
+ import { readFileSync as readFileSync5 } from "fs";
490
+ import { join as join6 } from "path";
225
491
 
226
492
  // src/shared/jsonc.ts
227
493
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -236,45 +502,48 @@ function parseJsonc(text, source = "config") {
236
502
  }
237
503
 
238
504
  // src/core/config/manifest.ts
239
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
240
- import { basename, join as join3 } from "path";
505
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
506
+ import { basename as basename2, join as join5 } from "path";
241
507
  function moduleScripts(cwd) {
242
508
  try {
243
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
509
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
244
510
  return pkg.scripts ?? {};
245
511
  } catch {
246
512
  return {};
247
513
  }
248
514
  }
249
- function moduleName(cwd) {
515
+ function moduleName2(cwd) {
250
516
  try {
251
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
517
+ const pkg = JSON.parse(readFileSync4(join5(cwd, "package.json"), "utf8"));
252
518
  return pkg.name;
253
519
  } catch {
254
520
  return void 0;
255
521
  }
256
522
  }
257
523
  function isSelfAdoption(cwd) {
258
- return moduleName(cwd) === readOwnPackage().name;
524
+ return moduleName2(cwd) === readOwnPackage().name;
259
525
  }
260
526
  function selfCommand(cwd, flags) {
261
527
  const own = readOwnPackage();
262
528
  if (!isSelfAdoption(cwd) || !own.bin) return void 0;
263
529
  return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
264
530
  }
531
+ function adoptedPackageName(cwd) {
532
+ return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ?? basename2(cwd);
533
+ }
265
534
  function manifestOperation(cwd, scripts) {
266
535
  const own = readOwnPackage();
267
536
  const value = {
268
537
  scripts,
269
538
  devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
270
539
  };
271
- if (existsSync4(join3(cwd, "package.json"))) {
540
+ if (existsSync6(join5(cwd, "package.json"))) {
272
541
  return { kind: "merge-json", path: "package.json", value };
273
542
  }
274
543
  return {
275
544
  kind: "merge-json",
276
545
  path: "package.json",
277
- value: { name: readNxProjectName(cwd) ?? basename(cwd), private: true, ...value }
546
+ value: { name: adoptedPackageName(cwd), private: true, ...value }
278
547
  };
279
548
  }
280
549
 
@@ -283,7 +552,7 @@ function nxTargetCommand(cwd, target) {
283
552
  let project;
284
553
  try {
285
554
  project = parseJsonc(
286
- readFileSync3(join4(cwd, "project.json"), "utf8"),
555
+ readFileSync5(join6(cwd, "project.json"), "utf8"),
287
556
  "project.json"
288
557
  );
289
558
  } catch {
@@ -314,25 +583,26 @@ function existingCommand(cwd, target) {
314
583
  }
315
584
 
316
585
  // src/core/config/nx-target.ts
317
- import { existsSync as existsSync5 } from "fs";
318
- import { join as join5 } from "path";
586
+ import { existsSync as existsSync7 } from "fs";
587
+ import { join as join7 } from "path";
588
+ function nxTargetMetadataOperations(options) {
589
+ const { targets } = options;
590
+ if (Object.keys(targets).length === 0) return [];
591
+ return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
592
+ }
319
593
  function nxTargetOperations(options) {
320
594
  const { cwd, targets } = options;
321
595
  const names = Object.keys(targets);
322
596
  if (names.length === 0) return [];
323
597
  const operations = [];
324
- if (existsSync5(join5(cwd, "project.json"))) {
598
+ if (existsSync7(join7(cwd, "project.json"))) {
325
599
  operations.push({
326
600
  kind: "remove-json-keys",
327
601
  path: "project.json",
328
602
  keys: names.map((name) => ["targets", name])
329
603
  });
330
604
  }
331
- operations.push({
332
- kind: "merge-json",
333
- path: "package.json",
334
- value: { nx: { targets } }
335
- });
605
+ operations.push(...nxTargetMetadataOperations(options));
336
606
  return operations;
337
607
  }
338
608
 
@@ -378,108 +648,6 @@ function keepsOtherCommands(script, sentinelCommand) {
378
648
  return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
379
649
  }
380
650
 
381
- // src/roles/build/presets/requirements.json
382
- var requirements_default = {
383
- svelte: {
384
- why: "sentinel ships Vite 8, and @sveltejs/vite-plugin-svelte caps at Vite 6 until its major 7",
385
- requires: {
386
- "@sveltejs/vite-plugin-svelte": "7.0.0",
387
- svelte: "5.46.4"
388
- }
389
- }
390
- };
391
-
392
- // src/roles/build/presets/toolchain.json
393
- var toolchain_default = {
394
- $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.",
395
- owned: [
396
- {
397
- package: "vite",
398
- specifier: "vite",
399
- named: ["defineConfig", "loadEnv", "mergeConfig"],
400
- types: [
401
- "Plugin",
402
- "PluginOption",
403
- "SassPreprocessorOptions",
404
- "UserConfig",
405
- "UserConfigExport"
406
- ],
407
- 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."
408
- },
409
- {
410
- package: "@vitejs/plugin-react",
411
- specifier: "@vitejs/plugin-react",
412
- default: "react",
413
- why: "the React transform. Bound to the Vite that runs it, so it has to come from the same install."
414
- },
415
- {
416
- package: "@tailwindcss/vite",
417
- specifier: "@tailwindcss/vite",
418
- default: "tailwindcss",
419
- why: "the Tailwind pipeline, identical in all three front apps."
420
- },
421
- {
422
- package: "vite-plugin-svgr",
423
- specifier: "vite-plugin-svgr",
424
- default: "svgr",
425
- why: "SVG as components, identical in all three front apps."
426
- },
427
- {
428
- package: "nitro",
429
- specifier: "nitro/vite",
430
- named: ["nitro"],
431
- 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."
432
- }
433
- ],
434
- add: [
435
- {
436
- package: "@hublo/sentinel",
437
- 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."
438
- }
439
- ],
440
- $notOwned: {
441
- "@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.",
442
- "@locator/babel-jsx": "same, and same owners.",
443
- "@tanstack/react-start": "a framework the apps code against: 48 source files import it directly.",
444
- "@tanstack/router-plugin": "generates the app's own route tree.",
445
- "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."
446
- }
447
- };
448
-
449
- // src/roles/build/preset-data.ts
450
- var REQUIREMENTS = requirements_default;
451
- var OWNED_PACKAGES = toolchain_default.owned;
452
-
453
- // src/roles/build/config-policy.ts
454
- var COMPANION_CONFIG_FILES = [
455
- "vitest.config.ts",
456
- "vitest.config.mts",
457
- "vitest.config.js",
458
- "vitest.config.mjs"
459
- ];
460
- var BUILD_CONFIG_FILES = [
461
- "vite.config.ts",
462
- "vite.config.mts",
463
- "vite.config.js",
464
- "vite.config.mjs"
465
- ];
466
- var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
467
- var BUILD_SCRIPT_NAME = "build";
468
- var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
469
- var DEV_SCRIPT_NAME = "serve";
470
- var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
471
- function buildTarget() {
472
- return {
473
- [BUILD_SCRIPT_NAME]: {
474
- cache: true,
475
- inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
476
- }
477
- };
478
- }
479
- var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
480
- (entry) => entry.package
481
- );
482
-
483
651
  // src/roles/build/dev-script.ts
484
652
  var CHAIN = " -- ";
485
653
  var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
@@ -496,73 +664,43 @@ function composeDevScript(existing, command) {
496
664
  if (at === -1) return existing;
497
665
  const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
498
666
  const options = appOptions(rest);
499
- chunks[at] = options === "" ? command : `${command} -- ${options}`;
667
+ chunks[at] = options === "" ? `${command} --` : `${command} -- ${options}`;
500
668
  return chunks.join(CHAIN);
501
669
  }
502
670
 
503
- // src/roles/build/read-adoption.ts
504
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
505
- import { join as join6 } from "path";
506
- var NOT_ADOPTED = (configFile, unreadable = null) => ({
507
- configFile,
508
- preset: null,
509
- adopted: false,
510
- conformant: false,
511
- drift: [],
512
- ownDeclarations: [],
513
- unreadable
514
- });
515
- function buildConfigFile(cwd) {
516
- return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
517
- }
518
- function importsSpecifier(source, specifier) {
519
- const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
520
- return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
521
- }
522
- function importsPreset(source) {
523
- return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
671
+ // src/roles/build/nest/adopt.ts
672
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
673
+ import { join as join8 } from "path";
674
+ var NEST_CONFIG_FILE = "vite.config.mts";
675
+ function nestConfigSource(cwd, target) {
676
+ const hops = workspaceRootHops(cwd, target);
677
+ const up = Array.from({ length: hops }, () => "..").join("/");
678
+ const carried = (name, value) => value === void 0 || value.length === 0 ? "" : ` ${name}: ${JSON.stringify(value)},
679
+ `;
680
+ return `import path from 'node:path'
681
+
682
+ import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
683
+
684
+ export default nestService({
685
+ project: '${target.project}',
686
+ root: __dirname,
687
+ workspaceRoot: path.resolve(__dirname, '${up}'),
688
+ outDir: '${target.outputPath}',
689
+ ${carried("transformers", target.transformers)}${carried("assets", target.assets)}})
690
+ `;
524
691
  }
525
- var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
526
- function declaredBuildPreset(cwd) {
527
- const source = readBuildConfigSource(cwd);
528
- if (source === void 0) return void 0;
529
- return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
692
+ function nestBuildOutputs(target) {
693
+ return [`{workspaceRoot}/${target.outputPath}`];
530
694
  }
531
- function readBuildConfigSource(cwd) {
532
- const configFile = buildConfigFile(cwd);
533
- if (!configFile) return void 0;
534
- try {
535
- return readFileSync4(join6(cwd, configFile), "utf8");
536
- } catch {
537
- return void 0;
538
- }
695
+ var OUT_DIR_IN_CONFIG = /^\s*outDir:\s*'([^']+)'/m;
696
+ function nestOutputsFromConfig(cwd) {
697
+ const configPath = join8(cwd, NEST_CONFIG_FILE);
698
+ if (!existsSync8(configPath)) return void 0;
699
+ const outDir = OUT_DIR_IN_CONFIG.exec(readFileSync6(configPath, "utf8"))?.[1];
700
+ return outDir === void 0 ? void 0 : [`{workspaceRoot}/${outDir}`];
539
701
  }
540
- function readBuildAdoption(cwd) {
541
- const configFile = buildConfigFile(cwd);
542
- if (!configFile) return NOT_ADOPTED(null);
543
- let source;
544
- try {
545
- source = readFileSync4(join6(cwd, configFile), "utf8");
546
- } catch (error) {
547
- return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
548
- }
549
- if (!importsPreset(source)) return NOT_ADOPTED(configFile);
550
- const manifest = readProjectPackageJson(cwd);
551
- const declared = { ...manifest.dependencies, ...manifest.devDependencies };
552
- const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
553
- (name) => ({ name, version: declared[name] })
554
- );
555
- return {
556
- configFile,
557
- preset: "react",
558
- adopted: true,
559
- conformant: ownDeclarations.length === 0,
560
- drift: ownDeclarations.map(
561
- ({ 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`
562
- ),
563
- ownDeclarations,
564
- unreadable: null
565
- };
702
+ function nestAdoptionOperations(cwd, target) {
703
+ return [{ kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) }];
566
704
  }
567
705
 
568
706
  // src/roles/build/requirements.ts
@@ -593,32 +731,31 @@ function describeUnmet(preset, unmet) {
593
731
  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\`.`;
594
732
  }
595
733
 
596
- // src/roles/build/rewrite-imports.ts
734
+ // src/core/config/rewrite-imports.ts
597
735
  import { parseSync } from "oxc-parser";
598
- var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
736
+ var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
599
737
  function esmBlocker(fileName, packageType) {
600
738
  if (/\.(mts|mjs)$/.test(fileName)) return void 0;
601
739
  if (packageType === "module") return void 0;
602
740
  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.`;
603
741
  }
604
- var DEFAULT_EXPORT_NAMES = Object.fromEntries(
605
- OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
606
- entry.specifier,
607
- entry.default
608
- ])
742
+ var defaultExportNames = (toolchain) => Object.fromEntries(
743
+ toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
609
744
  );
610
- function renderImport(bindings) {
745
+ function renderImport(bindings, presetSpecifier) {
611
746
  const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
612
747
  const values = bindings.filter((b) => !b.typeOnly);
613
748
  const types = bindings.filter((b) => b.typeOnly);
614
749
  const lines = [];
615
- if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
750
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
616
751
  if (types.length > 0) {
617
- lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
752
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
618
753
  }
619
754
  return lines.join("\n");
620
755
  }
621
- function rewriteBuildImports(source, fileName = "vite.config.ts") {
756
+ function rewriteToolchainImports(toolchain, source, fileName) {
757
+ const moved = movedSpecifiers(toolchain);
758
+ const defaults = defaultExportNames(toolchain);
622
759
  const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
623
760
  if (errors.length > 0) {
624
761
  return {
@@ -629,10 +766,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
629
766
  const imports = program.body.filter(
630
767
  (node) => node.type === "ImportDeclaration"
631
768
  );
632
- if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
769
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
633
770
  return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
634
771
  }
635
- const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
772
+ const moving = imports.filter((node) => moved.has(node.source.value));
636
773
  if (moving.length === 0) {
637
774
  return {
638
775
  kind: "unchanged",
@@ -649,7 +786,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
649
786
  for (const spec of specifiers) {
650
787
  const typeOnly = node.importKind === "type" || spec.importKind === "type";
651
788
  if (spec.type === "ImportDefaultSpecifier") {
652
- const exported = DEFAULT_EXPORT_NAMES[from];
789
+ const exported = defaults[from];
653
790
  if (!exported) {
654
791
  return {
655
792
  kind: "blocked",
@@ -681,7 +818,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
681
818
  const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
682
819
  out = out.slice(0, span.start) + out.slice(span.end + after);
683
820
  }
684
- out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
821
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier) + out.slice(first.end);
685
822
  return {
686
823
  kind: "rewritten",
687
824
  source: out,
@@ -689,7 +826,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
689
826
  };
690
827
  }
691
828
 
829
+ // src/roles/build/rewrite-imports.ts
830
+ var BUILD_TOOLCHAIN = {
831
+ presetSpecifier: BUILD_PRESET_SPECIFIER,
832
+ owned: OWNED_PACKAGES
833
+ };
834
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
835
+ return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
836
+ }
837
+
692
838
  // src/roles/build/plan.ts
839
+ function candidateOutputs() {
840
+ return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
841
+ }
693
842
  function ownedBuildDependencies(cwd) {
694
843
  const manifest = readProjectPackageJson(cwd);
695
844
  const keys = [];
@@ -718,9 +867,11 @@ function buildScripts(cwd) {
718
867
  };
719
868
  const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
720
869
  if (existingDev !== void 0) {
721
- const own2 = basename2(cwd);
870
+ const own2 = moduleName(cwd);
722
871
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
723
872
  existingDev,
873
+ // No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
874
+ // sides produced `-- --`, handing Vite an argument nobody wrote.
724
875
  `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
725
876
  );
726
877
  }
@@ -735,54 +886,69 @@ function plan(context) {
735
886
  if (unmet.length > 0) {
736
887
  return { operations: [], skipped: describeUnmet(context.preset, unmet) };
737
888
  }
738
- if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
889
+ const declaredPreset2 = declaredBuildPreset(context.cwd);
890
+ const configFile = buildConfigFile(context.cwd);
891
+ if (declaredPreset2 === void 0 && configFile !== void 0) {
892
+ return {
893
+ operations: [],
894
+ skipped: `this module has a Vite config, but it builds neither a React app (no @vitejs/plugin-react or @tanstack/react-start) nor a Nest service (no ${BUILD_PRESET_SPECIFIERS.nest}). Those are the two families this role serves.`
895
+ };
896
+ }
897
+ const webpackTarget = webpackBuildTarget(context.cwd);
898
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
899
+ return planNestService(context, webpackTarget);
900
+ }
901
+ if (configFile === void 0) {
739
902
  return {
740
903
  operations: [],
741
- 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.`
904
+ 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 (38 services here build through \`@nx/webpack\`), the build role does not apply to it.`
742
905
  };
743
906
  }
907
+ return planReactApp(context, configFile);
908
+ }
909
+ function planReactApp(context, configFile) {
744
910
  const notes = [];
745
911
  const operations = [];
746
- const configFile = buildConfigFile(context.cwd);
747
- if (configFile === void 0) {
912
+ const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
913
+ if (notEsm !== void 0) return { operations: [], skipped: notEsm };
914
+ const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
915
+ if (rewrite.kind === "blocked") {
748
916
  return {
749
917
  operations: [],
750
- 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 (38 projects here build through \`@nx/webpack\`), the build role does not apply to it.`
918
+ 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.`
751
919
  };
752
- } else {
753
- const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
754
- if (notEsm !== void 0) return { operations: [], skipped: notEsm };
755
- const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
756
- if (rewrite.kind === "blocked") {
757
- return {
758
- operations: [],
759
- 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.`
760
- };
761
- }
762
- if (rewrite.kind === "rewritten") {
763
- operations.push({ kind: "write", path: configFile, contents: rewrite.source });
764
- }
765
- for (const name of COMPANION_CONFIG_FILES) {
766
- const source = readConfigSource(context.cwd, name);
767
- if (source === "") continue;
768
- const companion = rewriteBuildImports(source, name);
769
- if (companion.kind === "rewritten") {
770
- operations.push({ kind: "write", path: name, contents: companion.source });
771
- notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
772
- } else if (companion.kind === "blocked") {
773
- notes.push(
774
- `${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.`
775
- );
776
- }
777
- }
778
- if (rewrite.kind === "rewritten") {
920
+ }
921
+ if (rewrite.kind === "rewritten") {
922
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
923
+ }
924
+ for (const name of COMPANION_CONFIG_FILES) {
925
+ const source = readConfigSource(context.cwd, name);
926
+ if (source === "") continue;
927
+ const companion = rewriteBuildImports(source, name);
928
+ if (companion.kind === "rewritten") {
929
+ operations.push({ kind: "write", path: name, contents: companion.source });
930
+ notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
931
+ } else if (companion.kind === "blocked") {
779
932
  notes.push(
780
- `${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.`
933
+ `${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.`
781
934
  );
782
935
  }
783
936
  }
937
+ if (rewrite.kind === "rewritten") {
938
+ notes.push(
939
+ `${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.`
940
+ );
941
+ }
784
942
  operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
785
- operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
943
+ operations.push(
944
+ ...nxTargetOperations({
945
+ cwd: context.cwd,
946
+ targets: buildTarget({
947
+ moduleName: adoptedPackageName(context.cwd),
948
+ outputs: candidateOutputs()
949
+ })
950
+ })
951
+ );
786
952
  const owned = ownedBuildDependencies(context.cwd);
787
953
  if (owned.length > 0) {
788
954
  operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
@@ -792,10 +958,44 @@ function plan(context) {
792
958
  }
793
959
  return { operations, notes };
794
960
  }
961
+ function planNestService(context, webpackTarget) {
962
+ const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
963
+ if (outputs === void 0) {
964
+ return {
965
+ operations: [],
966
+ blocked: `${NEST_CONFIG_FILE} does not declare an \`outDir\`, so sentinel cannot say where this service's build lands. Declaring an nx target without it would cache the build and restore nothing from that cache. Nothing was written.`
967
+ };
968
+ }
969
+ const metadata = [
970
+ ...nxTargetOperations({
971
+ cwd: context.cwd,
972
+ targets: buildTarget({
973
+ moduleName: adoptedPackageName(context.cwd),
974
+ configFiles: [NEST_CONFIG_FILE],
975
+ outputs
976
+ })
977
+ }),
978
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND })
979
+ ];
980
+ if (webpackTarget === void 0) return { operations: metadata };
981
+ const refusal = refusalFor(context.cwd, webpackTarget);
982
+ if (refusal !== void 0) return { operations: [], blocked: refusal };
983
+ const dropped = Object.keys(webpackTarget.configurations ?? {});
984
+ return {
985
+ operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
986
+ notes: [
987
+ `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.`,
988
+ ...dropped.length > 0 ? [
989
+ `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.`
990
+ ] : [],
991
+ `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.`
992
+ ]
993
+ };
994
+ }
795
995
  function readConfigSource(cwd, configFile) {
796
- if (!existsSync7(join7(cwd, configFile))) return "";
996
+ if (!existsSync9(join9(cwd, configFile))) return "";
797
997
  try {
798
- return readFileSync5(join7(cwd, configFile), "utf8");
998
+ return readFileSync7(join9(cwd, configFile), "utf8");
799
999
  } catch {
800
1000
  return "";
801
1001
  }
@@ -835,11 +1035,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
835
1035
  * resolution let it run at all — filtering here would give "no adapter for preset svelte",
836
1036
  * which tells nobody what to do about it.
837
1037
  *
838
- * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
839
- * would change that.
1038
+ * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
1039
+ * grounds that "there is nothing to bundle", which was never true: those 38 services bundle
1040
+ * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
1041
+ * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
1042
+ * migration generator refuses every one of them because they use `@nx/js:node`
1043
+ * (nrwl/nx#36389). So the way out runs through here.
1044
+ *
1045
+ * `node` and `tools` remain genuinely declined: nothing under them bundles at all.
840
1046
  */
841
1047
  appliesTo(preset) {
842
- return preset === "react" || preset === "svelte";
1048
+ return preset === "react" || preset === "svelte" || preset === "nest";
843
1049
  }
844
1050
  /**
845
1051
  * `react`, read from the module's Vite config rather than from its dependencies.
@@ -892,60 +1098,279 @@ var ViteRoleAdapter = class extends BaseAdapter {
892
1098
  `using the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite fail in ways that never mention a version. Remove vite from this module's package.json.`
893
1099
  );
894
1100
  }
895
- return { kind: "ready", vite: bin };
1101
+ return { kind: "ready", vite: bin };
1102
+ }
1103
+ /**
1104
+ * Run Vite with the caller's own arguments after `--`, and report what happened.
1105
+ *
1106
+ * `whenSignalled` is the only difference between the two targets here, and it is a real one:
1107
+ * a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
1108
+ * did exactly what the developer asked.
1109
+ */
1110
+ spawnVite(ctx, vite, argv, whenSignalled) {
1111
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1112
+ valueFlags: VITE_VALUE_FLAGS
1113
+ });
1114
+ const result = spawnSync(vite, [...argv, ...options, ...paths], {
1115
+ cwd: ctx.cwd,
1116
+ stdio: "inherit"
1117
+ });
1118
+ if (result.error) {
1119
+ this.say(`could not run vite (${result.error.message})`);
1120
+ return { ok: false, code: 1 };
1121
+ }
1122
+ const code = result.status ?? whenSignalled;
1123
+ return { ok: code === 0, code };
1124
+ }
1125
+ /** One prefix for every message this role emits, so they cannot drift apart. */
1126
+ say(message) {
1127
+ process.stderr.write(`sentinel ${this.label}(vite): ${message}
1128
+ `);
1129
+ }
1130
+ };
1131
+
1132
+ // src/roles/build/adapters/vite/vite-dev.adapter.ts
1133
+ var ViteDevAdapter = class extends ViteRoleAdapter {
1134
+ target = "dev";
1135
+ get label() {
1136
+ return "dev";
1137
+ }
1138
+ /**
1139
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
1140
+ *
1141
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1142
+ * because adopting one without the other leaves the module half-migrated. What was missing
1143
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
1144
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
1145
+ * wrong correction, it added friction to a command that already produced the right result.
1146
+ */
1147
+ plan(context) {
1148
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1149
+ return {
1150
+ operations: [],
1151
+ skipped: `a Nest service has no dev server, so --dev has nothing to adopt here. Its build is \`sentinel --init --build\`, and it runs as a Node process afterwards.`
1152
+ };
1153
+ }
1154
+ const planned = super.plan(context);
1155
+ if (planned.operations.length === 0) return planned;
1156
+ return {
1157
+ ...planned,
1158
+ notes: [
1159
+ ...planned.notes ?? [],
1160
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1161
+ ]
1162
+ };
1163
+ }
1164
+ /**
1165
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
1166
+ * beyond the exit code the developer's own Ctrl-C produces.
1167
+ *
1168
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
1169
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
1170
+ * running it again would race the watcher that is about to own the file.
1171
+ *
1172
+ * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
1173
+ * bare form is the one every version accepts.
1174
+ */
1175
+ async run(ctx) {
1176
+ const ready = this.prepare(ctx);
1177
+ if (ready.kind === "stop") return ready.result;
1178
+ return this.spawnVite(ctx, ready.vite, [], 0);
1179
+ }
1180
+ };
1181
+
1182
+ // src/roles/build/declared-outputs.ts
1183
+ import { existsSync as existsSync11, statSync } from "fs";
1184
+ import { join as join11 } from "path";
1185
+
1186
+ // src/core/workspace-prep.ts
1187
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync } from "fs";
1188
+ import { dirname as dirname3, join as join10, relative as relative2 } from "path";
1189
+
1190
+ // src/core/settings.ts
1191
+ var WORKSPACE_ROOT_MARKER = "nx.json";
1192
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
1193
+
1194
+ // src/core/workspace-prep.ts
1195
+ var OVERRIDE_KEY = "i18next>typescript";
1196
+ var NATIVE_TS_ALIAS = "@typescript/native";
1197
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1198
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1199
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1200
+ function findWorkspaceRoot(startDir) {
1201
+ let dir = startDir;
1202
+ for (; ; ) {
1203
+ if (existsSync10(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
1204
+ const parent = dirname3(dir);
1205
+ if (parent === dir) return void 0;
1206
+ dir = parent;
1207
+ }
1208
+ }
1209
+ function declaredNativeTs(pkg) {
1210
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1211
+ if (!spec) return void 0;
1212
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1213
+ return version || void 0;
1214
+ }
1215
+ function ensureI18nextSingleton(root, dryRun) {
1216
+ const pkgPath = join10(root, "package.json");
1217
+ if (!existsSync10(pkgPath)) return void 0;
1218
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1219
+ const want = declaredNativeTs(pkg);
1220
+ if (!want) return void 0;
1221
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1222
+ if (have === want) return void 0;
1223
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1224
+ pkg.pnpm ??= {};
1225
+ pkg.pnpm.overrides ??= {};
1226
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1227
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1228
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1229
+ }
1230
+ function ensureReleaseAgeAllowList(root, dryRun) {
1231
+ const yamlPath = join10(root, WORKSPACE_YAML);
1232
+ if (!existsSync10(yamlPath)) return void 0;
1233
+ const own = readOwnPackage().name;
1234
+ const lines = readFileSync8(yamlPath, "utf8").split("\n");
1235
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1236
+ if (keyIdx === -1) return void 0;
1237
+ let lastItemIdx = keyIdx;
1238
+ let indent = " ";
1239
+ for (let i = keyIdx + 1; i < lines.length; i++) {
1240
+ const match = lines[i]?.match(LIST_ITEM);
1241
+ if (!match) break;
1242
+ indent = match[1] ?? indent;
1243
+ lastItemIdx = i;
1244
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1245
+ }
1246
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1247
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1248
+ writeFileSync(yamlPath, lines.join("\n"));
1249
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1250
+ }
1251
+ var PRETTIER_IGNORE = ".prettierignore";
1252
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1253
+ var ROOT_PRETTIER_CONFIGS = [
1254
+ ".prettierrc",
1255
+ ".prettierrc.json",
1256
+ ".prettierrc.json5",
1257
+ ".prettierrc.yml",
1258
+ ".prettierrc.yaml",
1259
+ ".prettierrc.js",
1260
+ ".prettierrc.cjs",
1261
+ ".prettierrc.mjs",
1262
+ "prettier.config.js",
1263
+ "prettier.config.cjs",
1264
+ "prettier.config.mjs"
1265
+ ];
1266
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
1267
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1268
+ if (rel === "" || rel.startsWith("..")) return void 0;
1269
+ const ignorePath = join10(root, PRETTIER_IGNORE);
1270
+ const hasPrettier = existsSync10(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync10(join10(root, name)));
1271
+ if (!hasPrettier) return void 0;
1272
+ const pattern = `/${rel}/`;
1273
+ const existing = existsSync10(ignorePath) ? readFileSync8(ignorePath, "utf8") : "";
1274
+ const lines = existing.split("\n");
1275
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1276
+ return void 0;
1277
+ }
1278
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1279
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1280
+ if (headerIdx === -1) {
1281
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1282
+
1283
+ `;
1284
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1285
+ ${pattern}
1286
+ `);
1287
+ } else {
1288
+ let end = headerIdx + 1;
1289
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1290
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1291
+ writeFileSync(
1292
+ ignorePath,
1293
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1294
+ );
1295
+ }
1296
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1297
+ }
1298
+ function ensureWorkspacePrep(opts) {
1299
+ const dryRun = Boolean(opts.dryRun);
1300
+ return [
1301
+ ensureI18nextSingleton(opts.root, dryRun),
1302
+ ensureReleaseAgeAllowList(opts.root, dryRun),
1303
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1304
+ ].filter((message) => message !== void 0);
1305
+ }
1306
+ function inspectWorkspacePrep(root) {
1307
+ const entries = [];
1308
+ let pkg = {};
1309
+ const pkgPath = join10(root, "package.json");
1310
+ if (existsSync10(pkgPath)) {
1311
+ try {
1312
+ pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1313
+ } catch {
1314
+ pkg = {};
1315
+ }
896
1316
  }
897
- /**
898
- * Run Vite with the caller's own arguments after `--`, and report what happened.
899
- *
900
- * `whenSignalled` is the only difference between the two targets here, and it is a real one:
901
- * a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
902
- * did exactly what the developer asked.
903
- */
904
- spawnVite(ctx, vite, argv, whenSignalled) {
905
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
906
- valueFlags: VITE_VALUE_FLAGS
907
- });
908
- const result = spawnSync(vite, [...argv, ...options, ...paths], {
909
- cwd: ctx.cwd,
910
- stdio: "inherit"
1317
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1318
+ if (pinned !== void 0) {
1319
+ const stillForks = declaredNativeTs(pkg);
1320
+ entries.push({
1321
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1322
+ reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
911
1323
  });
912
- if (result.error) {
913
- this.say(`could not run vite (${result.error.message})`);
914
- return { ok: false, code: 1 };
915
- }
916
- const code = result.status ?? whenSignalled;
917
- return { ok: code === 0, code };
918
1324
  }
919
- /** One prefix for every message this role emits, so they cannot drift apart. */
920
- say(message) {
921
- process.stderr.write(`sentinel ${this.label}(vite): ${message}
922
- `);
1325
+ const own = readOwnPackage().name;
1326
+ const yamlPath = join10(root, WORKSPACE_YAML);
1327
+ if (existsSync10(yamlPath)) {
1328
+ const yaml = readFileSync8(yamlPath, "utf8");
1329
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1330
+ if (listed) {
1331
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1332
+ entries.push({
1333
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1334
+ reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
1335
+ });
1336
+ }
923
1337
  }
924
- };
1338
+ return entries;
1339
+ }
925
1340
 
926
- // src/roles/build/adapters/vite/vite-dev.adapter.ts
927
- var ViteDevAdapter = class extends ViteRoleAdapter {
928
- target = "dev";
929
- get label() {
930
- return "dev";
1341
+ // src/roles/build/declared-outputs.ts
1342
+ function declaredBuildTarget(cwd) {
1343
+ return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1344
+ }
1345
+ function resolveOutputToken(token, cwd) {
1346
+ if (token.startsWith("{projectRoot}/")) return join11(cwd, token.slice("{projectRoot}/".length));
1347
+ if (token.startsWith("{workspaceRoot}/")) {
1348
+ const root = findWorkspaceRoot(cwd);
1349
+ return root === void 0 ? void 0 : join11(root, token.slice("{workspaceRoot}/".length));
931
1350
  }
932
- /**
933
- * Start the dev server. It does not return until stopped, so there is no verdict to report
934
- * beyond the exit code the developer's own Ctrl-C produces.
935
- *
936
- * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
937
- * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
938
- * running it again would race the watcher that is about to own the file.
939
- *
940
- * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
941
- * bare form is the one every version accepts.
942
- */
943
- async run(ctx) {
944
- const ready = this.prepare(ctx);
945
- if (ready.kind === "stop") return ready.result;
946
- return this.spawnVite(ctx, ready.vite, [], 0);
1351
+ return join11(cwd, token);
1352
+ }
1353
+ function writtenSince(path, startedAt) {
1354
+ if (!existsSync11(path)) return false;
1355
+ try {
1356
+ return statSync(path).mtimeMs >= startedAt;
1357
+ } catch {
1358
+ return true;
947
1359
  }
948
- };
1360
+ }
1361
+ function outputsProblem(cwd, startedAt) {
1362
+ const target = declaredBuildTarget(cwd);
1363
+ if (target === void 0) return void 0;
1364
+ const declared = target.outputs ?? [];
1365
+ if (declared.length === 0) {
1366
+ if (target.cache !== true) return void 0;
1367
+ return `this module's nx build target sets \`cache: true\` and declares no \`outputs\`, so nx will cache this build and restore nothing from it: the next cache hit reports success and leaves no build on disk. Add the directory this build writes to under \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json, or re-run \`sentinel --init --build\`, which declares it.`;
1368
+ }
1369
+ const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
1370
+ if (resolved.length === 0) return void 0;
1371
+ if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
1372
+ return `the build wrote nothing to any of the directories this module declares as its nx \`outputs\` (${declared.join(", ")}), so nx would cache this build and restore an empty result from it. Whatever directory the build DID write to belongs in \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json. The build itself succeeded; only its cache declaration is wrong.`;
1373
+ }
949
1374
 
950
1375
  // src/roles/build/prerequisite.ts
951
1376
  import { spawnSync as spawnSync2 } from "child_process";
@@ -995,7 +1420,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
995
1420
  );
996
1421
  return { ok: false, code: prerequisite.code };
997
1422
  }
998
- return this.spawnVite(ctx, ready.vite, ["build"], 1);
1423
+ const startedAt = Date.now();
1424
+ const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
1425
+ if (!result.ok) return result;
1426
+ const problem = outputsProblem(ctx.cwd, startedAt);
1427
+ if (problem !== void 0) {
1428
+ this.say(problem);
1429
+ return { ok: false, code: 1 };
1430
+ }
1431
+ return result;
999
1432
  }
1000
1433
  /**
1001
1434
  * What this module builds with, WITHOUT loading its config.
@@ -1067,12 +1500,12 @@ function registerBuild() {
1067
1500
 
1068
1501
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1069
1502
  import { spawnSync as spawnSync3 } from "child_process";
1070
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1071
- import { join as join14 } from "path";
1503
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
1504
+ import { join as join17 } from "path";
1072
1505
 
1073
1506
  // src/core/config/has-source.ts
1074
1507
  import { readdirSync } from "fs";
1075
- import { extname, join as join8, relative } from "path";
1508
+ import { extname, join as join12, relative as relative3 } from "path";
1076
1509
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1077
1510
  "node_modules",
1078
1511
  "dist",
@@ -1123,7 +1556,7 @@ function hasSourceFiles(cwd, extensions) {
1123
1556
  }
1124
1557
  for (const entry of entries) {
1125
1558
  if (entry.isDirectory()) {
1126
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
1559
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join12(dir, entry.name));
1127
1560
  continue;
1128
1561
  }
1129
1562
  if (wanted.has(extname(entry.name))) return true;
@@ -1144,12 +1577,12 @@ function listSourceFiles(cwd, extensions) {
1144
1577
  continue;
1145
1578
  }
1146
1579
  for (const entry of entries) {
1147
- const full = join8(dir, entry.name);
1580
+ const full = join12(dir, entry.name);
1148
1581
  if (entry.isDirectory()) {
1149
1582
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1150
1583
  continue;
1151
1584
  }
1152
- if (wanted.has(extname(entry.name))) found.push(relative(cwd, full));
1585
+ if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1153
1586
  }
1154
1587
  }
1155
1588
  return found;
@@ -1157,161 +1590,9 @@ function listSourceFiles(cwd, extensions) {
1157
1590
 
1158
1591
  // src/core/named-scope.ts
1159
1592
  var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1160
- function scopeFlags(paths) {
1161
- return paths.length > 0 ? [TOLERATE_EMPTY_FLAG] : [];
1162
- }
1163
-
1164
- // src/core/settings.ts
1165
- var WORKSPACE_ROOT_MARKER = "nx.json";
1166
- var DEFAULT_MAX_DIAGNOSTICS = 100;
1167
-
1168
- // src/core/workspace-prep.ts
1169
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1170
- import { dirname as dirname3, join as join9, relative as relative2 } from "path";
1171
- var OVERRIDE_KEY = "i18next>typescript";
1172
- var NATIVE_TS_ALIAS = "@typescript/native";
1173
- var WORKSPACE_YAML = "pnpm-workspace.yaml";
1174
- var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1175
- var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1176
- function findWorkspaceRoot(startDir) {
1177
- let dir = startDir;
1178
- for (; ; ) {
1179
- if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
1180
- const parent = dirname3(dir);
1181
- if (parent === dir) return void 0;
1182
- dir = parent;
1183
- }
1184
- }
1185
- function declaredNativeTs(pkg) {
1186
- const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1187
- if (!spec) return void 0;
1188
- const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1189
- return version || void 0;
1190
- }
1191
- function ensureI18nextSingleton(root, dryRun) {
1192
- const pkgPath = join9(root, "package.json");
1193
- if (!existsSync8(pkgPath)) return void 0;
1194
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1195
- const want = declaredNativeTs(pkg);
1196
- if (!want) return void 0;
1197
- const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1198
- if (have === want) return void 0;
1199
- if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1200
- pkg.pnpm ??= {};
1201
- pkg.pnpm.overrides ??= {};
1202
- pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1203
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1204
- return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1205
- }
1206
- function ensureReleaseAgeAllowList(root, dryRun) {
1207
- const yamlPath = join9(root, WORKSPACE_YAML);
1208
- if (!existsSync8(yamlPath)) return void 0;
1209
- const own = readOwnPackage().name;
1210
- const lines = readFileSync6(yamlPath, "utf8").split("\n");
1211
- const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1212
- if (keyIdx === -1) return void 0;
1213
- let lastItemIdx = keyIdx;
1214
- let indent = " ";
1215
- for (let i = keyIdx + 1; i < lines.length; i++) {
1216
- const match = lines[i]?.match(LIST_ITEM);
1217
- if (!match) break;
1218
- indent = match[1] ?? indent;
1219
- lastItemIdx = i;
1220
- if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1221
- }
1222
- if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1223
- lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1224
- writeFileSync(yamlPath, lines.join("\n"));
1225
- return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1226
- }
1227
- var PRETTIER_IGNORE = ".prettierignore";
1228
- var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1229
- var ROOT_PRETTIER_CONFIGS = [
1230
- ".prettierrc",
1231
- ".prettierrc.json",
1232
- ".prettierrc.json5",
1233
- ".prettierrc.yml",
1234
- ".prettierrc.yaml",
1235
- ".prettierrc.js",
1236
- ".prettierrc.cjs",
1237
- ".prettierrc.mjs",
1238
- "prettier.config.js",
1239
- "prettier.config.cjs",
1240
- "prettier.config.mjs"
1241
- ];
1242
- function ensureFormatterExclusion(root, moduleDir, dryRun) {
1243
- const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1244
- if (rel === "" || rel.startsWith("..")) return void 0;
1245
- const ignorePath = join9(root, PRETTIER_IGNORE);
1246
- const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
1247
- if (!hasPrettier) return void 0;
1248
- const pattern = `/${rel}/`;
1249
- const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
1250
- const lines = existing.split("\n");
1251
- if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1252
- return void 0;
1253
- }
1254
- if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1255
- const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1256
- if (headerIdx === -1) {
1257
- const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1258
-
1259
- `;
1260
- writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1261
- ${pattern}
1262
- `);
1263
- } else {
1264
- let end = headerIdx + 1;
1265
- while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1266
- const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1267
- writeFileSync(
1268
- ignorePath,
1269
- [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1270
- );
1271
- }
1272
- return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1273
- }
1274
- function ensureWorkspacePrep(opts) {
1275
- const dryRun = Boolean(opts.dryRun);
1276
- return [
1277
- ensureI18nextSingleton(opts.root, dryRun),
1278
- ensureReleaseAgeAllowList(opts.root, dryRun),
1279
- opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1280
- ].filter((message) => message !== void 0);
1281
- }
1282
- function inspectWorkspacePrep(root) {
1283
- const entries = [];
1284
- let pkg = {};
1285
- const pkgPath = join9(root, "package.json");
1286
- if (existsSync8(pkgPath)) {
1287
- try {
1288
- pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1289
- } catch {
1290
- pkg = {};
1291
- }
1292
- }
1293
- const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1294
- if (pinned !== void 0) {
1295
- const stillForks = declaredNativeTs(pkg);
1296
- entries.push({
1297
- rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1298
- reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
1299
- });
1300
- }
1301
- const own = readOwnPackage().name;
1302
- const yamlPath = join9(root, WORKSPACE_YAML);
1303
- if (existsSync8(yamlPath)) {
1304
- const yaml = readFileSync6(yamlPath, "utf8");
1305
- const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1306
- if (listed) {
1307
- const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1308
- entries.push({
1309
- rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1310
- reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
1311
- });
1312
- }
1313
- }
1314
- return entries;
1593
+ function scopeFlags(paths, options = []) {
1594
+ if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1595
+ return [TOLERATE_EMPTY_FLAG];
1315
1596
  }
1316
1597
 
1317
1598
  // src/roles/format/config-policy.ts
@@ -1388,8 +1669,8 @@ function writesWhenRewritten(name, command) {
1388
1669
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1389
1670
 
1390
1671
  // src/roles/format/inherited-ignores.ts
1391
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1392
- import { join as join10 } from "path";
1672
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1673
+ import { join as join13 } from "path";
1393
1674
  var ROOT_IGNORE_FILE = ".prettierignore";
1394
1675
  function isPattern(line) {
1395
1676
  const trimmed = line.trim();
@@ -1404,11 +1685,11 @@ function toModulePattern(pattern) {
1404
1685
  }
1405
1686
  function inheritedIgnorePatterns(workspaceRoot) {
1406
1687
  if (!workspaceRoot) return [];
1407
- const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1408
- if (!existsSync9(path)) return [];
1688
+ const path = join13(workspaceRoot, ROOT_IGNORE_FILE);
1689
+ if (!existsSync12(path)) return [];
1409
1690
  let contents;
1410
1691
  try {
1411
- contents = readFileSync7(path, "utf8");
1692
+ contents = readFileSync9(path, "utf8");
1412
1693
  } catch {
1413
1694
  return [];
1414
1695
  }
@@ -1495,13 +1776,13 @@ function formatPresetFor(preset) {
1495
1776
  }
1496
1777
 
1497
1778
  // src/roles/format/prettier-config.ts
1498
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1499
- import { join as join12 } from "path";
1779
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
1780
+ import { join as join15 } from "path";
1500
1781
 
1501
1782
  // src/roles/format/resolve-oxfmt.ts
1502
- import { readFileSync as readFileSync8 } from "fs";
1783
+ import { readFileSync as readFileSync10 } from "fs";
1503
1784
  import { createRequire as createRequire2 } from "module";
1504
- import { dirname as dirname4, join as join11 } from "path";
1785
+ import { dirname as dirname4, join as join14 } from "path";
1505
1786
  function resolveOxfmt(cwd) {
1506
1787
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1507
1788
  }
@@ -1530,8 +1811,8 @@ function configSchema() {
1530
1811
  function readConfigSchema() {
1531
1812
  try {
1532
1813
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
1533
- const schemaPath = join11(dirname4(manifest), "configuration_schema.json");
1534
- return JSON.parse(readFileSync8(schemaPath, "utf8"));
1814
+ const schemaPath = join14(dirname4(manifest), "configuration_schema.json");
1815
+ return JSON.parse(readFileSync10(schemaPath, "utf8"));
1535
1816
  } catch {
1536
1817
  return void 0;
1537
1818
  }
@@ -1562,14 +1843,14 @@ function toOxfmtOverrides(value) {
1562
1843
  return { overrides, unresolved };
1563
1844
  }
1564
1845
  function readPrettierSettings(cwd) {
1565
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
1846
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync13(join15(cwd, name)));
1566
1847
  if (!file) return { options: {}, unresolved: [] };
1567
1848
  if (/\.(js|cjs|mjs)$/.test(file)) {
1568
1849
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1569
1850
  }
1570
1851
  let parsed;
1571
1852
  try {
1572
- parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
1853
+ parsed = parseJsonc(readFileSync11(join15(cwd, file), "utf8"), file);
1573
1854
  } catch {
1574
1855
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1575
1856
  }
@@ -1599,8 +1880,8 @@ function readPrettierSettings(cwd) {
1599
1880
  }
1600
1881
 
1601
1882
  // src/roles/format/read-adoption.ts
1602
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1603
- import { join as join13 } from "path";
1883
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
1884
+ import { join as join16 } from "path";
1604
1885
  var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
1605
1886
  configFile,
1606
1887
  preset: null,
@@ -1616,11 +1897,11 @@ function sameValue(a, b) {
1616
1897
  return JSON.stringify(a) === JSON.stringify(b);
1617
1898
  }
1618
1899
  function readFormatAdoption(cwd) {
1619
- const path = join13(cwd, FORMAT_CONFIG_FILE);
1620
- if (!existsSync11(path)) return NOT_ADOPTED2(null);
1900
+ const path = join16(cwd, FORMAT_CONFIG_FILE);
1901
+ if (!existsSync14(path)) return NOT_ADOPTED2(null);
1621
1902
  let parsed;
1622
1903
  try {
1623
- parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
1904
+ parsed = parseJsonc(readFileSync12(path, "utf8"), FORMAT_CONFIG_FILE);
1624
1905
  } catch (error) {
1625
1906
  const reason = error instanceof Error ? error.message : String(error);
1626
1907
  return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -1745,7 +2026,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1745
2026
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1746
2027
  ];
1747
2028
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1748
- (name) => existsSync12(join14(context.cwd, name))
2029
+ (name) => existsSync15(join17(context.cwd, name))
1749
2030
  );
1750
2031
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1751
2032
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1869,7 +2150,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1869
2150
  });
1870
2151
  const result = spawnSync3(
1871
2152
  oxfmt,
1872
- [mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
2153
+ [mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
1873
2154
  {
1874
2155
  cwd: ctx.cwd,
1875
2156
  stdio: "inherit"
@@ -1985,7 +2266,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1985
2266
  modulePrettierDependencies(cwd) {
1986
2267
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1987
2268
  try {
1988
- const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
2269
+ const manifest = JSON.parse(readFileSync13(join17(cwd, "package.json"), "utf8"));
1989
2270
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1990
2271
  } catch {
1991
2272
  return [];
@@ -2067,8 +2348,8 @@ function registerFormat() {
2067
2348
 
2068
2349
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2069
2350
  import { spawnSync as spawnSync4 } from "child_process";
2070
- import { existsSync as existsSync20, readFileSync as readFileSync19, rmSync, writeFileSync as writeFileSync2 } from "fs";
2071
- import { join as join24 } from "path";
2351
+ import { existsSync as existsSync23, readFileSync as readFileSync21, rmSync, writeFileSync as writeFileSync2 } from "fs";
2352
+ import { join as join27 } from "path";
2072
2353
 
2073
2354
  // src/core/config/deferred-rules.ts
2074
2355
  function deferredRuleNames(rules) {
@@ -2076,8 +2357,8 @@ function deferredRuleNames(rules) {
2076
2357
  }
2077
2358
 
2078
2359
  // src/core/fix-report.ts
2079
- import { statSync } from "fs";
2080
- import { join as join15 } from "path";
2360
+ import { statSync as statSync2 } from "fs";
2361
+ import { join as join18 } from "path";
2081
2362
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2082
2363
  function readFailure(output) {
2083
2364
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2088,7 +2369,7 @@ function readFailure(output) {
2088
2369
  }
2089
2370
  function fingerprint(path) {
2090
2371
  try {
2091
- const stat = statSync(path);
2372
+ const stat = statSync2(path);
2092
2373
  return `${stat.size}:${stat.mtimeMs}`;
2093
2374
  } catch {
2094
2375
  return void 0;
@@ -2097,7 +2378,7 @@ function fingerprint(path) {
2097
2378
  function snapshotFiles(cwd, extensions) {
2098
2379
  const snapshot = /* @__PURE__ */ new Map();
2099
2380
  for (const file of listSourceFiles(cwd, extensions)) {
2100
- const mark = fingerprint(join15(cwd, file));
2381
+ const mark = fingerprint(join18(cwd, file));
2101
2382
  if (mark !== void 0) snapshot.set(file, mark);
2102
2383
  }
2103
2384
  return snapshot;
@@ -2105,7 +2386,7 @@ function snapshotFiles(cwd, extensions) {
2105
2386
  function changedSince(cwd, before, extensions) {
2106
2387
  const changed = [];
2107
2388
  for (const file of listSourceFiles(cwd, extensions)) {
2108
- const now = fingerprint(join15(cwd, file));
2389
+ const now = fingerprint(join18(cwd, file));
2109
2390
  if (now === void 0) continue;
2110
2391
  if (before.get(file) !== now) changed.push(file);
2111
2392
  }
@@ -2124,8 +2405,8 @@ function describeFixOutcome(outcome, toolLabel) {
2124
2405
  }
2125
2406
 
2126
2407
  // src/roles/lint/module-baseline.ts
2127
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2128
- import { join as join16 } from "path";
2408
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
2409
+ import { join as join19 } from "path";
2129
2410
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2130
2411
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2131
2412
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2169,11 +2450,11 @@ function describeHolds(holds) {
2169
2450
  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`;
2170
2451
  }
2171
2452
  function heldRules(cwd) {
2172
- const path = join16(cwd, LINT_BASELINE_FILE);
2173
- if (!existsSync13(path)) return [];
2453
+ const path = join19(cwd, LINT_BASELINE_FILE);
2454
+ if (!existsSync16(path)) return [];
2174
2455
  try {
2175
2456
  const parsed = parseJsonc(
2176
- readFileSync12(path, "utf8"),
2457
+ readFileSync14(path, "utf8"),
2177
2458
  LINT_BASELINE_FILE
2178
2459
  );
2179
2460
  return Object.keys(parsed.rules ?? {});
@@ -3996,14 +4277,14 @@ function downgradedRulesFor(preset) {
3996
4277
  }
3997
4278
 
3998
4279
  // src/roles/lint/extra-layers.ts
3999
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4000
- import { isAbsolute as isAbsolute2, join as join17, resolve as resolve3 } from "path";
4280
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4281
+ import { isAbsolute as isAbsolute2, join as join20, resolve as resolve3 } from "path";
4001
4282
  function ruleCount(cwd, specifier) {
4002
4283
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4003
- if (!existsSync14(path)) return void 0;
4284
+ if (!existsSync17(path)) return void 0;
4004
4285
  try {
4005
4286
  const parsed = parseJsonc(
4006
- readFileSync13(path, "utf8"),
4287
+ readFileSync15(path, "utf8"),
4007
4288
  specifier
4008
4289
  );
4009
4290
  return Object.keys(parsed.rules ?? {}).length;
@@ -4023,7 +4304,7 @@ function extraLayers(cwd, extendsList) {
4023
4304
  function committedExtendsList(cwd, configFile) {
4024
4305
  try {
4025
4306
  const parsed = parseJsonc(
4026
- readFileSync13(join17(cwd, configFile), "utf8"),
4307
+ readFileSync15(join20(cwd, configFile), "utf8"),
4027
4308
  configFile
4028
4309
  );
4029
4310
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4082,8 +4363,8 @@ function errorCountsByConfigRule(stdout) {
4082
4363
  }
4083
4364
 
4084
4365
  // src/core/config/read-adoption.ts
4085
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4086
- import { join as join19 } from "path";
4366
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4367
+ import { join as join22 } from "path";
4087
4368
 
4088
4369
  // src/core/config/owned-keys.ts
4089
4370
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4098,12 +4379,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4098
4379
  }
4099
4380
 
4100
4381
  // src/core/config/resolve-config-target.ts
4101
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4102
- import { join as join18 } from "path";
4382
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4383
+ import { join as join21 } from "path";
4103
4384
  function readExtends(absolutePath) {
4104
4385
  let parsed;
4105
4386
  try {
4106
- parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
4387
+ parsed = parseJsonc(readFileSync16(absolutePath, "utf8"), absolutePath);
4107
4388
  } catch {
4108
4389
  return [];
4109
4390
  }
@@ -4117,8 +4398,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4117
4398
  let existing;
4118
4399
  let existingExtendsSomething = false;
4119
4400
  for (const candidate of candidates) {
4120
- const absolutePath = join18(moduleDir, candidate);
4121
- if (!existsSync15(absolutePath)) continue;
4401
+ const absolutePath = join21(moduleDir, candidate);
4402
+ if (!existsSync18(absolutePath)) continue;
4122
4403
  const chain = readExtends(absolutePath);
4123
4404
  if (existing === void 0) {
4124
4405
  existing = candidate;
@@ -4151,12 +4432,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4151
4432
  });
4152
4433
  function readAdoption(cwd, options) {
4153
4434
  const target = resolveConfigTarget(cwd, options);
4154
- if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
4435
+ if (target.reason === "none" || !existsSync19(join22(cwd, target.path))) {
4155
4436
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4156
4437
  }
4157
4438
  let parsed;
4158
4439
  try {
4159
- parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
4440
+ parsed = parseJsonc(readFileSync17(join22(cwd, target.path), "utf8"), target.path);
4160
4441
  } catch (error) {
4161
4442
  const reason = error instanceof Error ? error.message : String(error);
4162
4443
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4190,9 +4471,9 @@ function readLintAdoption(cwd) {
4190
4471
  }
4191
4472
 
4192
4473
  // src/roles/lint/resolve-oxlint.ts
4193
- import { existsSync as existsSync17 } from "fs";
4474
+ import { existsSync as existsSync20 } from "fs";
4194
4475
  import { createRequire as createRequire3 } from "module";
4195
- import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
4476
+ import { delimiter as delimiter2, dirname as dirname5, join as join23 } from "path";
4196
4477
  import { fileURLToPath as fileURLToPath2 } from "url";
4197
4478
  var PACKAGE_OF = {
4198
4479
  oxlint: "oxlint",
@@ -4217,30 +4498,30 @@ function tsgolintShim(cwd) {
4217
4498
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4218
4499
  try {
4219
4500
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4220
- candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4221
- candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
4501
+ candidates.push(join23(packageDir, "node_modules", ".bin", "tsgolint"));
4502
+ candidates.push(join23(packageDir, "..", ".bin", "tsgolint"));
4222
4503
  } catch {
4223
4504
  }
4224
4505
  }
4225
4506
  try {
4226
4507
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4227
- candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
4508
+ candidates.push(join23(ownRoot, "node_modules", ".bin", "tsgolint"));
4228
4509
  } catch {
4229
4510
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4230
4511
  }
4231
- return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
4512
+ return candidates.find((candidate) => candidate !== "" && existsSync20(candidate));
4232
4513
  }
4233
4514
  function oxlintSearchPath(cwd) {
4234
4515
  return binSearchPath(cwd);
4235
4516
  }
4236
4517
 
4237
4518
  // src/roles/lint/adapters/oxlint/plan.ts
4238
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4239
- import { join as join23 } from "path";
4519
+ import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
4520
+ import { join as join26 } from "path";
4240
4521
 
4241
4522
  // src/roles/lint/eslint-ignores.ts
4242
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4243
- import { join as join21 } from "path";
4523
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
4524
+ import { join as join24 } from "path";
4244
4525
  import { parseSync as parseSync2 } from "oxc-parser";
4245
4526
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4246
4527
  function toOxlintPattern(pattern) {
@@ -4252,11 +4533,11 @@ function readRootEslintIgnores(root) {
4252
4533
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4253
4534
  }
4254
4535
  function readEslintIgnores(cwd) {
4255
- const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
4536
+ const config = ESLINT_CONFIG_FILES.map((name) => join24(cwd, name)).find((path) => existsSync21(path));
4256
4537
  if (!config) return { patterns: [], unresolved: [] };
4257
4538
  let source;
4258
4539
  try {
4259
- source = readFileSync16(config, "utf8");
4540
+ source = readFileSync18(config, "utf8");
4260
4541
  } catch {
4261
4542
  return { patterns: [], unresolved: [] };
4262
4543
  }
@@ -4357,8 +4638,8 @@ function lintPresetFor(preset) {
4357
4638
  }
4358
4639
 
4359
4640
  // src/roles/lint/rename-suppressions.ts
4360
- import { readdirSync as readdirSync2, readFileSync as readFileSync17, statSync as statSync2 } from "fs";
4361
- import { join as join22, relative as relative3 } from "path";
4641
+ import { readdirSync as readdirSync2, readFileSync as readFileSync19, statSync as statSync3 } from "fs";
4642
+ import { join as join25, relative as relative4 } from "path";
4362
4643
  var SOURCE_EXTENSIONS = [
4363
4644
  ".ts",
4364
4645
  ".tsx",
@@ -4405,10 +4686,10 @@ function* sourceFiles(dir) {
4405
4686
  return;
4406
4687
  }
4407
4688
  for (const entry of entries) {
4408
- const full = join22(dir, entry);
4689
+ const full = join25(dir, entry);
4409
4690
  let isDirectory;
4410
4691
  try {
4411
- isDirectory = statSync2(full).isDirectory();
4692
+ isDirectory = statSync3(full).isDirectory();
4412
4693
  } catch {
4413
4694
  continue;
4414
4695
  }
@@ -4425,7 +4706,7 @@ function findSuppressionRenames(cwd, renames) {
4425
4706
  for (const file of sourceFiles(cwd)) {
4426
4707
  let content;
4427
4708
  try {
4428
- content = readFileSync17(file, "utf8");
4709
+ content = readFileSync19(file, "utf8");
4429
4710
  } catch {
4430
4711
  continue;
4431
4712
  }
@@ -4449,7 +4730,7 @@ function findSuppressionRenames(cwd, renames) {
4449
4730
  }
4450
4731
  if (hits.length === 0) return;
4451
4732
  found.push({
4452
- file: relative3(cwd, file),
4733
+ file: relative4(cwd, file),
4453
4734
  line: index + 1,
4454
4735
  from: line,
4455
4736
  to: line.replace(rules, renamed.join(", ")),
@@ -4553,7 +4834,7 @@ function plan2(context) {
4553
4834
  keys: removableDeps
4554
4835
  });
4555
4836
  }
4556
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
4837
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync22(join26(context.cwd, name)));
4557
4838
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4558
4839
  operations.push(
4559
4840
  ...nxTargetOperations({
@@ -4651,7 +4932,7 @@ function lintScripts(cwd) {
4651
4932
  function committedExtends(cwd) {
4652
4933
  try {
4653
4934
  const parsed = parseJsonc(
4654
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4935
+ readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
4655
4936
  LINT_CONFIG_FILE
4656
4937
  );
4657
4938
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4662,7 +4943,7 @@ function committedExtends(cwd) {
4662
4943
  function committedIgnorePatterns(cwd) {
4663
4944
  try {
4664
4945
  const parsed = parseJsonc(
4665
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4946
+ readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
4666
4947
  LINT_CONFIG_FILE
4667
4948
  );
4668
4949
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4674,7 +4955,7 @@ function moduleEslintDependencies(cwd) {
4674
4955
  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/");
4675
4956
  let manifest;
4676
4957
  try {
4677
- manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
4958
+ manifest = JSON.parse(readFileSync20(join26(cwd, "package.json"), "utf8"));
4678
4959
  } catch {
4679
4960
  return [];
4680
4961
  }
@@ -4808,20 +5089,20 @@ ${result.stderr ?? ""}`);
4808
5089
  * build the developer can see, rather than a silent half-adoption they cannot.
4809
5090
  */
4810
5091
  writeModuleBaseline(ctx, oxlint, env) {
4811
- const configPath = join24(ctx.cwd, LINT_CONFIG_FILE);
4812
- const baselinePath = join24(ctx.cwd, LINT_BASELINE_FILE);
4813
- if (!existsSync20(configPath)) return;
5092
+ const configPath = join27(ctx.cwd, LINT_CONFIG_FILE);
5093
+ const baselinePath = join27(ctx.cwd, LINT_BASELINE_FILE);
5094
+ if (!existsSync23(configPath)) return;
4814
5095
  let config;
4815
5096
  try {
4816
5097
  config = parseJsonc(
4817
- readFileSync19(configPath, "utf8"),
5098
+ readFileSync21(configPath, "utf8"),
4818
5099
  LINT_CONFIG_FILE
4819
5100
  );
4820
5101
  } catch {
4821
5102
  return;
4822
5103
  }
4823
5104
  const current = Array.isArray(config.extends) ? config.extends : [];
4824
- const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
5105
+ const measurePath = join27(ctx.cwd, LINT_MEASURE_FILE);
4825
5106
  let measured;
4826
5107
  try {
4827
5108
  writeFileSync2(
@@ -4869,7 +5150,7 @@ ${result.stderr ?? ""}`);
4869
5150
  }
4870
5151
  }
4871
5152
  async run(ctx) {
4872
- if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
5153
+ if (!existsSync23(join27(ctx.cwd, LINT_CONFIG_FILE))) {
4873
5154
  process.stderr.write(
4874
5155
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
4875
5156
  `
@@ -4914,7 +5195,7 @@ ${result.stderr ?? ""}`);
4914
5195
  valueFlags: OXLINT_VALUE_FLAGS
4915
5196
  });
4916
5197
  const targets = paths.length > 0 ? paths : ["."];
4917
- const emptyScope = scopeFlags(paths);
5198
+ const emptyScope = scopeFlags(paths, passedOptions);
4918
5199
  const lint = (extra) => {
4919
5200
  const passed = [...extra, ...emptyScope, ...passedOptions];
4920
5201
  const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
@@ -5017,7 +5298,7 @@ ${result.stderr ?? ""}`);
5017
5298
  let stub;
5018
5299
  try {
5019
5300
  stub = parseJsonc(
5020
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5301
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
5021
5302
  LINT_CONFIG_FILE
5022
5303
  );
5023
5304
  } catch {
@@ -5027,7 +5308,7 @@ ${result.stderr ?? ""}`);
5027
5308
  for (const entry of stub.extends ?? []) {
5028
5309
  try {
5029
5310
  const preset = parseJsonc(
5030
- readFileSync19(join24(cwd, entry), "utf8"),
5311
+ readFileSync21(join27(cwd, entry), "utf8"),
5031
5312
  entry
5032
5313
  );
5033
5314
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5097,14 +5378,14 @@ ${result.stderr ?? ""}`);
5097
5378
  let parsed;
5098
5379
  try {
5099
5380
  parsed = parseJsonc(
5100
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5381
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
5101
5382
  LINT_CONFIG_FILE
5102
5383
  );
5103
5384
  } catch {
5104
5385
  return void 0;
5105
5386
  }
5106
5387
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5107
- return targets.find((target) => !existsSync20(join24(cwd, target)));
5388
+ return targets.find((target) => !existsSync23(join27(cwd, target)));
5108
5389
  }
5109
5390
  /** Announce what is not enforced, so reduced coverage is never silent. */
5110
5391
  announceDisabled(preset) {
@@ -5136,9 +5417,9 @@ function registerLint() {
5136
5417
 
5137
5418
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5138
5419
  import { spawnSync as spawnSync5 } from "child_process";
5139
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
5420
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
5140
5421
  import { createRequire as createRequire4 } from "module";
5141
- import { join as join26 } from "path";
5422
+ import { join as join29 } from "path";
5142
5423
 
5143
5424
  // src/roles/typescript/presets/nest.json
5144
5425
  var nest_default2 = {
@@ -5319,8 +5600,8 @@ function readTsconfigAdoption(cwd) {
5319
5600
  }
5320
5601
 
5321
5602
  // src/roles/typescript/adapters/tsc/plan.ts
5322
- import { readFileSync as readFileSync20 } from "fs";
5323
- import { join as join25 } from "path";
5603
+ import { readFileSync as readFileSync22 } from "fs";
5604
+ import { join as join28 } from "path";
5324
5605
 
5325
5606
  // src/roles/typescript/typecheck-script.ts
5326
5607
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5413,7 +5694,7 @@ function planAdoption(context) {
5413
5694
  };
5414
5695
  }
5415
5696
  const existing = parseJsonc(
5416
- readFileSync20(join25(context.cwd, target.path), "utf8"),
5697
+ readFileSync22(join28(context.cwd, target.path), "utf8"),
5417
5698
  target.path
5418
5699
  );
5419
5700
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5551,7 +5832,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5551
5832
  let chain;
5552
5833
  try {
5553
5834
  const parsed = parseJsonc(
5554
- readFileSync21(join26(cwd, target.path), "utf8"),
5835
+ readFileSync23(join29(cwd, target.path), "utf8"),
5555
5836
  target.path
5556
5837
  );
5557
5838
  chain = parsed.extends;
@@ -5564,7 +5845,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5564
5845
  );
5565
5846
  if (preset === void 0) return void 0;
5566
5847
  try {
5567
- createRequire4(join26(cwd, "noop.js")).resolve(preset);
5848
+ createRequire4(join29(cwd, "noop.js")).resolve(preset);
5568
5849
  return void 0;
5569
5850
  } catch {
5570
5851
  return preset;
@@ -5660,7 +5941,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5660
5941
  referencedProjects(cwd, config) {
5661
5942
  try {
5662
5943
  const parsed = parseJsonc(
5663
- readFileSync21(join26(cwd, config), "utf8"),
5944
+ readFileSync23(join29(cwd, config), "utf8"),
5664
5945
  config
5665
5946
  );
5666
5947
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5676,7 +5957,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5676
5957
  * check.
5677
5958
  */
5678
5959
  typecheckTarget(cwd) {
5679
- if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
5960
+ if (existsSync24(join29(cwd, "tsconfig.json"))) return "tsconfig.json";
5680
5961
  const target = resolveTsconfigTarget(cwd);
5681
5962
  return target.reason === "none" ? null : target.path;
5682
5963
  }
@@ -5880,7 +6161,7 @@ function replaceLines(current, replacements) {
5880
6161
  }
5881
6162
 
5882
6163
  // src/core/apply-plan.ts
5883
- import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6164
+ import { existsSync as existsSync25, readFileSync as readFileSync24, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5884
6165
  import { resolve as resolve4, sep } from "path";
5885
6166
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
5886
6167
 
@@ -5899,7 +6180,7 @@ function resolveWithinRoot(cwd, relativePath) {
5899
6180
  return absolutePath;
5900
6181
  }
5901
6182
  function readIfExists(absolutePath) {
5902
- return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
6183
+ return existsSync25(absolutePath) ? readFileSync24(absolutePath, "utf8") : void 0;
5903
6184
  }
5904
6185
  function* leaves(value, prefix = []) {
5905
6186
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6017,8 +6298,8 @@ function applyPlan(cwd, plan3) {
6017
6298
  }
6018
6299
 
6019
6300
  // src/core/config/preset-evidence.ts
6020
- import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "fs";
6021
- import { join as join27 } from "path";
6301
+ import { existsSync as existsSync26, readdirSync as readdirSync3, readFileSync as readFileSync25 } from "fs";
6302
+ import { join as join30 } from "path";
6022
6303
  var PATH_SIGNALS = [
6023
6304
  {
6024
6305
  preset: "nest",
@@ -6032,11 +6313,11 @@ var DEPENDENCY_SIGNALS = [
6032
6313
  { preset: "nest", pattern: /^@nestjs\// }
6033
6314
  ];
6034
6315
  function dependencyNames(cwd) {
6035
- const path = join27(cwd, "package.json");
6036
- if (!existsSync23(path)) return [];
6316
+ const path = join30(cwd, "package.json");
6317
+ if (!existsSync26(path)) return [];
6037
6318
  try {
6038
6319
  const manifest = parseJsonc(
6039
- readFileSync23(path, "utf8"),
6320
+ readFileSync25(path, "utf8"),
6040
6321
  path
6041
6322
  );
6042
6323
  return [
@@ -6059,7 +6340,7 @@ function declaresJsx(cwd) {
6059
6340
  for (const name of entries) {
6060
6341
  try {
6061
6342
  const config = parseJsonc(
6062
- readFileSync23(join27(cwd, name), "utf8"),
6343
+ readFileSync25(join30(cwd, name), "utf8"),
6063
6344
  name
6064
6345
  );
6065
6346
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6218,24 +6499,25 @@ export {
6218
6499
  declaredPresetFor,
6219
6500
  availableTargets,
6220
6501
  resolve,
6221
- BaseAdapter,
6222
- resolveBin,
6223
6502
  readOwnVersion,
6224
6503
  readProjectPackageJson,
6225
- readNxProjectName,
6504
+ moduleName,
6505
+ buildConfigFile,
6506
+ BaseAdapter,
6507
+ resolveBin,
6508
+ WORKSPACE_ROOT_MARKER,
6509
+ findWorkspaceRoot,
6510
+ ensureWorkspacePrep,
6511
+ inspectWorkspacePrep,
6226
6512
  VERBS,
6227
6513
  TARGETS,
6228
6514
  LONG_RUNNING_TARGETS,
6229
6515
  SWEEPABLE_TARGETS,
6230
6516
  PRESET_NAMES,
6231
- WORKSPACE_ROOT_MARKER,
6232
- findWorkspaceRoot,
6233
- ensureWorkspacePrep,
6234
- inspectWorkspacePrep,
6235
6517
  palette,
6236
6518
  registerAdapters,
6237
6519
  describeFramework,
6238
6520
  detectFramework,
6239
6521
  dispatch
6240
6522
  };
6241
- //# sourceMappingURL=chunk-SA2RMTVI.js.map
6523
+ //# sourceMappingURL=chunk-RKVGZNJ7.js.map