@hublo/sentinel 1.2.0-alpha.3 → 1.2.0-alpha.30

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,19 +67,6 @@ 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
72
  import { basename, dirname, join } from "path";
@@ -132,6 +119,19 @@ function moduleName(dir) {
132
119
  return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
133
120
  }
134
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
+ };
134
+
135
135
  // src/core/domain.ts
136
136
  var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
137
137
  var TARGETS = [
@@ -165,86 +165,99 @@ function palette(stream) {
165
165
  };
166
166
  }
167
167
 
168
- // src/roles/build/adapters/vite/vite-role.adapter.ts
169
- import { spawnSync } from "child_process";
170
-
171
- // src/core/config/tool-args.ts
172
- import { existsSync as existsSync2 } from "fs";
173
- import { isAbsolute, resolve as resolve2 } from "path";
174
- function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
175
- const takesValue = new Set(valueFlags);
176
- const options = [];
177
- const paths = [];
178
- let previousTakesValue = false;
179
- for (const arg of toolArgs) {
180
- const looksLikeOption = arg.startsWith("-");
181
- const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
182
- if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
183
- else options.push(arg);
184
- previousTakesValue = !arg.includes("=") && takesValue.has(arg);
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;
185
180
  }
186
- return { options, paths };
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
+ };
187
199
  }
188
-
189
- // src/shared/resolve-bin.ts
190
- import { existsSync as existsSync3 } from "fs";
191
- import { createRequire } from "module";
192
- import { delimiter, dirname as dirname2, join as join2 } from "path";
193
- var require2 = createRequire(import.meta.url);
194
- function resolveBin(fromDir, name) {
195
- let dir = fromDir;
196
- for (; ; ) {
197
- const candidate = join2(dir, "node_modules", ".bin", name);
198
- if (existsSync3(candidate)) return candidate;
199
- const parent = dirname2(dir);
200
- if (parent === dir) return void 0;
201
- dir = parent;
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.`;
202
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;
203
212
  }
204
- function binFromOwnInstall(packageName, binName) {
205
- try {
206
- const manifest = require2.resolve(`${packageName}/package.json`);
207
- const bin = require2(manifest).bin;
208
- const relative4 = typeof bin === "string" ? bin : bin?.[binName];
209
- if (!relative4) return void 0;
210
- const executable = join2(dirname2(manifest), relative4);
211
- return existsSync3(executable) ? executable : void 0;
212
- } catch {
213
- return void 0;
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
+ }
214
223
  }
224
+ return live;
215
225
  }
216
- function binSearchPath(cwd) {
217
- return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
218
- " then "
219
- );
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;
220
238
  }
221
239
 
222
- // src/roles/build/plan.ts
223
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
224
- import { join as join7 } from "path";
225
-
226
- // src/core/config/existing-command.ts
227
- import { readFileSync as readFileSync3 } from "fs";
228
- import { join as join4 } from "path";
240
+ // src/roles/build/read-adoption.ts
241
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
242
+ import { join as join5 } from "path";
229
243
 
230
- // src/shared/jsonc.ts
231
- import { parse, printParseErrorCode } from "jsonc-parser";
232
- function parseJsonc(text, source = "config") {
233
- const errors = [];
234
- const value = parse(text, errors, { allowTrailingComma: true });
235
- if (errors.length > 0) {
236
- const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
237
- throw new Error(`${source}: malformed JSONC (${details}).`);
238
- }
239
- return value;
244
+ // src/core/config/module-type.ts
245
+ function esmReadiness(fileName, packageType) {
246
+ if (/\.(mts|mjs)$/.test(fileName)) return { kind: "ready" };
247
+ if (packageType === "module") return { kind: "ready" };
248
+ const rename = fileName.replace(/\.(ts|js)$/, ".mts");
249
+ return {
250
+ kind: "blocked",
251
+ why: `sentinel is ESM only, and this module ${packageType === void 0 ? 'declares no `"type"`' : `declares \`"type": "${packageType}"\``}, so Node loads ${fileName} as CommonJS and the import fails. Two remedies, and adoption takes neither by itself because both reach beyond this build: add \`"type": "module"\` to this module's package.json, which changes how EVERY \`.js\` here loads, or rename the config to \`${rename}\`, which settles it in the filename where nothing can undo it.`
252
+ };
240
253
  }
241
254
 
242
255
  // src/core/config/manifest.ts
243
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
256
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
244
257
  import { basename as basename2, join as join3 } from "path";
245
258
  function moduleScripts(cwd) {
246
259
  try {
247
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
260
+ const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
248
261
  return pkg.scripts ?? {};
249
262
  } catch {
250
263
  return {};
@@ -252,7 +265,7 @@ function moduleScripts(cwd) {
252
265
  }
253
266
  function moduleName2(cwd) {
254
267
  try {
255
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
268
+ const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
256
269
  return pkg.name;
257
270
  } catch {
258
271
  return void 0;
@@ -266,120 +279,36 @@ function selfCommand(cwd, flags) {
266
279
  if (!isSelfAdoption(cwd) || !own.bin) return void 0;
267
280
  return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
268
281
  }
282
+ function adoptedPackageName(cwd) {
283
+ return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ?? basename2(cwd);
284
+ }
269
285
  function manifestOperation(cwd, scripts) {
270
286
  const own = readOwnPackage();
271
287
  const value = {
272
288
  scripts,
273
289
  devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
274
290
  };
275
- if (existsSync4(join3(cwd, "package.json"))) {
291
+ if (existsSync3(join3(cwd, "package.json"))) {
276
292
  return { kind: "merge-json", path: "package.json", value };
277
293
  }
278
294
  return {
279
295
  kind: "merge-json",
280
296
  path: "package.json",
281
- value: { name: readNxProjectName(cwd) ?? basename2(cwd), private: true, ...value }
297
+ value: { name: adoptedPackageName(cwd), private: true, ...value }
282
298
  };
283
299
  }
284
300
 
285
- // src/core/config/existing-command.ts
286
- function nxTargetCommand(cwd, target) {
287
- let project;
301
+ // src/core/config/project-json.ts
302
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
303
+ import { join as join4 } from "path";
304
+ function projectTargets(cwd) {
305
+ const path = join4(cwd, "project.json");
306
+ if (!existsSync4(path)) return void 0;
288
307
  try {
289
- project = parseJsonc(
290
- readFileSync3(join4(cwd, "project.json"), "utf8"),
291
- "project.json"
292
- );
308
+ return JSON.parse(readFileSync4(path, "utf8")).targets;
293
309
  } catch {
294
310
  return void 0;
295
311
  }
296
- const entry = project.targets?.[target];
297
- if (!entry || entry.executor !== "nx:run-commands") return void 0;
298
- const { command, commands, cwd: targetCwd } = entry.options ?? {};
299
- const ranInModule = typeof targetCwd === "string" && targetCwd.trim() !== "";
300
- if (typeof command === "string" && command.trim() !== "") {
301
- return { command: command.trim(), ranInModule };
302
- }
303
- if (Array.isArray(commands)) {
304
- const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
305
- if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
306
- }
307
- return void 0;
308
- }
309
- function rootedForScript(command) {
310
- return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
311
- }
312
- function existingCommand(cwd, target) {
313
- const script = moduleScripts(cwd)[target];
314
- if (script !== void 0 && script.trim() !== "") return script;
315
- const fromTarget = nxTargetCommand(cwd, target);
316
- if (fromTarget === void 0) return void 0;
317
- return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
318
- }
319
-
320
- // src/core/config/nx-target.ts
321
- import { existsSync as existsSync5 } from "fs";
322
- import { join as join5 } from "path";
323
- function nxTargetOperations(options) {
324
- const { cwd, targets } = options;
325
- const names = Object.keys(targets);
326
- if (names.length === 0) return [];
327
- const operations = [];
328
- if (existsSync5(join5(cwd, "project.json"))) {
329
- operations.push({
330
- kind: "remove-json-keys",
331
- path: "project.json",
332
- keys: names.map((name) => ["targets", name])
333
- });
334
- }
335
- operations.push({
336
- kind: "merge-json",
337
- path: "package.json",
338
- value: { nx: { targets } }
339
- });
340
- return operations;
341
- }
342
-
343
- // src/core/config/tool-script.ts
344
- var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
345
- function isSeparatorAt(index) {
346
- return index % 2 === 1;
347
- }
348
- function invokes(segment, binary) {
349
- return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
350
- }
351
- function isSentinelSegment(segment, roleFlag) {
352
- const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
353
- return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
354
- }
355
- function composeToolScript(existing, options) {
356
- const { command } = options;
357
- if (!existing || existing.trim() === "") return command;
358
- const { roleFlag } = options;
359
- const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
360
- const parts = existing.split(SEGMENT_SEPARATOR);
361
- const commands = parts.filter((_, index) => !isSeparatorAt(index));
362
- if (!commands.some(isReplaceable)) return existing;
363
- let replacedOnce = false;
364
- const rebuilt = parts.map((part, index) => {
365
- if (isSeparatorAt(index) || !isReplaceable(part)) return part;
366
- if (replacedOnce) return null;
367
- replacedOnce = true;
368
- return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
369
- });
370
- const kept = [];
371
- for (let index = 0; index < rebuilt.length; index += 1) {
372
- const part = rebuilt[index];
373
- if (part === null) {
374
- if (kept.length > 0) kept.pop();
375
- continue;
376
- }
377
- kept.push(part);
378
- }
379
- return kept.join("").trim();
380
- }
381
- function keepsOtherCommands(script, sentinelCommand) {
382
- return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
383
312
  }
384
313
 
385
314
  // src/roles/build/presets/requirements.json
@@ -408,7 +337,8 @@ var toolchain_default = {
408
337
  "UserConfig",
409
338
  "UserConfigExport"
410
339
  ],
411
- why: "the bundler itself. `defineConfig`, `loadEnv` and `mergeConfig` are what every config in this repo imports from it, and the types travel with them: a config importing PluginOption from a package it no longer declares builds but does not typecheck."
340
+ 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.",
341
+ floor: "Never below 8.2.2. Vite 8.0.8 resolves rolldown 1.0.0-rc.15, a release candidate whose deferred-module-initialisation codegen is broken: it wrapped a plain re-export barrel in an init helper, then required the module without binding its exports before calling the very function that binding provides. Measured, that crashed 7 of the 8 Nest services with a transformer at their first require (`init_<module> is not defined`), and quietly moved the published contract of 4 others. 8.2.2 resolves rolldown 1.2.6 and all 35 services regenerate their committed OpenAPI document identically. The version is not a preference, it is the floor at which those measurements hold."
412
342
  },
413
343
  {
414
344
  package: "@vitejs/plugin-react",
@@ -467,16 +397,28 @@ var BUILD_CONFIG_FILES = [
467
397
  "vite.config.js",
468
398
  "vite.config.mjs"
469
399
  ];
470
- var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
400
+ var BUILD_PRESET_SPECIFIERS = {
401
+ react: "@hublo/sentinel/build/react",
402
+ nest: "@hublo/sentinel/build/nest"
403
+ };
404
+ var BUILD_PRESET_SPECIFIER = BUILD_PRESET_SPECIFIERS.react;
471
405
  var BUILD_SCRIPT_NAME = "build";
472
- var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
406
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
473
407
  var DEV_SCRIPT_NAME = "serve";
474
408
  var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
475
- function buildTarget() {
409
+ var BUILD_OUTPUT_CANDIDATES = ["dist", ".output", "build"];
410
+ function buildTarget(options) {
411
+ const { moduleName: moduleName3, configFiles = BUILD_CONFIG_FILES, outputs } = options;
476
412
  return {
477
413
  [BUILD_SCRIPT_NAME]: {
414
+ executor: "nx:run-commands",
415
+ options: {
416
+ command: `pnpm --filter ${moduleName3} run ${BUILD_SCRIPT_NAME}`,
417
+ forwardAllArgs: false
418
+ },
478
419
  cache: true,
479
- inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
420
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
421
+ outputs: [...outputs]
480
422
  }
481
423
  };
482
424
  }
@@ -484,29 +426,58 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
484
426
  (entry) => entry.package
485
427
  );
486
428
 
487
- // src/roles/build/dev-script.ts
488
- var CHAIN = " -- ";
489
- var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
490
- var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
491
- function appOptions(rest) {
492
- const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
493
- const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
494
- return kept.join(" ");
495
- }
496
- function composeDevScript(existing, command) {
497
- if (existing === void 0 || existing.trim() === "") return command;
498
- const chunks = existing.trim().split(CHAIN);
499
- const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
500
- if (at === -1) return existing;
501
- const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
502
- const options = appOptions(rest);
503
- chunks[at] = options === "" ? command : `${command} -- ${options}`;
504
- return chunks.join(CHAIN);
429
+ // src/roles/build/adoption-drift.ts
430
+ function buildAdoptionDrift(cwd) {
431
+ const drift = [];
432
+ const manifest = readProjectPackageJson(cwd);
433
+ const script = moduleScripts(cwd)[BUILD_SCRIPT_NAME];
434
+ if (script === void 0) {
435
+ drift.push(
436
+ `has no \`${BUILD_SCRIPT_NAME}\` npm script, so \`pnpm run ${BUILD_SCRIPT_NAME}\` does not build it and only nx can`
437
+ );
438
+ } else if (!script.includes(SENTINEL_BUILD_COMMAND.replace(/ --$/, ""))) {
439
+ drift.push(
440
+ `its \`${BUILD_SCRIPT_NAME}\` script does not invoke sentinel (${script}), so the config points at sentinel while the script runs something else`
441
+ );
442
+ }
443
+ const target = manifest.nx?.targets?.[BUILD_SCRIPT_NAME];
444
+ if (target === void 0) {
445
+ drift.push(
446
+ `declares no \`nx.targets.${BUILD_SCRIPT_NAME}\`, so its build runs on nx's default inputs while every other target it has declares its own`
447
+ );
448
+ } else {
449
+ if (target.cache === true && (target.outputs ?? []).length === 0) {
450
+ drift.push(
451
+ `caches its build and declares no \`outputs\`, so nx stores nothing and a cache hit reports success with no build on disk`
452
+ );
453
+ }
454
+ const inputs = target.inputs ?? [];
455
+ if (inputs.length > 0 && !inputs.includes("^default")) {
456
+ drift.push(
457
+ `declares \`inputs\` without \`^default\`, which REPLACES nx's default and stops hashing its dependencies: the build can replay a stale bundle after one changes`
458
+ );
459
+ }
460
+ if (target.options?.forwardAllArgs !== false) {
461
+ drift.push(
462
+ `does not set \`forwardAllArgs: false\`, so a caller's extra flag reaches the bundler, which refuses it (the image passes \`--generatePackageJson\`)`
463
+ );
464
+ }
465
+ }
466
+ if (projectTargets(cwd)?.[BUILD_SCRIPT_NAME] !== void 0) {
467
+ drift.push(
468
+ `still declares a \`${BUILD_SCRIPT_NAME}\` target in project.json beside the one in package.json, so the module has two and which runs depends on the caller`
469
+ );
470
+ }
471
+ const devScript = moduleScripts(cwd)[DEV_SCRIPT_NAME];
472
+ if (devScript?.includes(SENTINEL_DEV_COMMAND) === true && projectTargets(cwd)?.[DEV_SCRIPT_NAME] !== void 0) {
473
+ drift.push(
474
+ `runs its \`${DEV_SCRIPT_NAME}\` script through sentinel but still declares a \`${DEV_SCRIPT_NAME}\` target in project.json, so one dev server has two declarations and editing the target stops the script from being what runs`
475
+ );
476
+ }
477
+ return drift;
505
478
  }
506
479
 
507
480
  // src/roles/build/read-adoption.ts
508
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
509
- import { join as join6 } from "path";
510
481
  var NOT_ADOPTED = (configFile, unreadable = null) => ({
511
482
  configFile,
512
483
  preset: null,
@@ -517,19 +488,25 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
517
488
  unreadable
518
489
  });
519
490
  function buildConfigFile(cwd) {
520
- return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
491
+ return BUILD_CONFIG_FILES.find((name) => existsSync5(join5(cwd, name)));
521
492
  }
522
493
  function importsSpecifier(source, specifier) {
523
494
  const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
524
495
  return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
525
496
  }
526
497
  function importsPreset(source) {
527
- return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
498
+ return Object.values(BUILD_PRESET_SPECIFIERS).some(
499
+ (specifier) => importsSpecifier(source, specifier)
500
+ );
528
501
  }
529
502
  var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
530
503
  function declaredBuildPreset(cwd) {
531
504
  const source = readBuildConfigSource(cwd);
532
- if (source === void 0) return void 0;
505
+ if (source === void 0) {
506
+ if (buildConfigFile(cwd) !== void 0) return void 0;
507
+ return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
508
+ }
509
+ if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
533
510
  if (importsPreset(source)) return "react";
534
511
  return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
535
512
  }
@@ -537,7 +514,7 @@ function readBuildConfigSource(cwd) {
537
514
  const configFile = buildConfigFile(cwd);
538
515
  if (!configFile) return void 0;
539
516
  try {
540
- return readFileSync4(join6(cwd, configFile), "utf8");
517
+ return readFileSync5(join5(cwd, configFile), "utf8");
541
518
  } catch {
542
519
  return void 0;
543
520
  }
@@ -547,38 +524,494 @@ function readBuildAdoption(cwd) {
547
524
  if (!configFile) return NOT_ADOPTED(null);
548
525
  let source;
549
526
  try {
550
- source = readFileSync4(join6(cwd, configFile), "utf8");
527
+ source = readFileSync5(join5(cwd, configFile), "utf8");
551
528
  } catch (error) {
552
529
  return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
553
530
  }
554
- if (!importsPreset(source)) return NOT_ADOPTED(configFile);
531
+ if (!importsPreset(source)) {
532
+ const esm = esmReadiness(configFile, readProjectPackageJson(cwd).type);
533
+ return NOT_ADOPTED(configFile, esm.kind === "blocked" ? esm.why : null);
534
+ }
555
535
  const manifest = readProjectPackageJson(cwd);
556
536
  const declared = { ...manifest.dependencies, ...manifest.devDependencies };
557
537
  const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
558
538
  (name) => ({ name, version: declared[name] })
559
539
  );
540
+ const drift = [
541
+ ...ownDeclarations.map(
542
+ ({ 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`
543
+ ),
544
+ ...buildAdoptionDrift(cwd)
545
+ ];
560
546
  return {
561
547
  configFile,
562
- preset: "react",
548
+ // Read from the config rather than assumed: the two presets are adopted the same way and
549
+ // reporting the wrong one would make `--inspect` lie about what a module builds.
550
+ preset: declaredBuildPreset(cwd) ?? "react",
563
551
  adopted: true,
564
- conformant: ownDeclarations.length === 0,
565
- drift: ownDeclarations.map(
566
- ({ 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`
567
- ),
552
+ conformant: drift.length === 0,
553
+ drift,
568
554
  ownDeclarations,
569
555
  unreadable: null
570
556
  };
571
557
  }
572
558
 
573
- // src/roles/build/requirements.ts
574
- function atLeast(declared, needed) {
575
- const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
576
- const left = parse2(declared);
577
- const right = parse2(needed);
578
- for (let at = 0; at < Math.max(left.length, right.length); at++) {
579
- const a = left[at] ?? 0;
580
- const b = right[at] ?? 0;
581
- if (a !== b) return a > b;
559
+ // src/roles/build/adapters/vite/vite-role.adapter.ts
560
+ import { spawnSync } from "child_process";
561
+
562
+ // src/core/config/tool-args.ts
563
+ import { existsSync as existsSync6 } from "fs";
564
+ import { isAbsolute, resolve as resolve2 } from "path";
565
+ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
566
+ const takesValue = new Set(valueFlags);
567
+ const options = [];
568
+ const paths = [];
569
+ let previousTakesValue = false;
570
+ for (const arg of toolArgs) {
571
+ const looksLikeOption = arg.startsWith("-");
572
+ const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
573
+ if (!previousTakesValue && !looksLikeOption && existsSync6(target)) paths.push(arg);
574
+ else options.push(arg);
575
+ previousTakesValue = !arg.includes("=") && takesValue.has(arg);
576
+ }
577
+ return { options, paths };
578
+ }
579
+
580
+ // src/shared/resolve-bin.ts
581
+ import { existsSync as existsSync7 } from "fs";
582
+ import { createRequire } from "module";
583
+ import { delimiter, dirname as dirname2, join as join6 } from "path";
584
+ var require2 = createRequire(import.meta.url);
585
+ function resolveBin(fromDir, name) {
586
+ let dir = fromDir;
587
+ for (; ; ) {
588
+ const candidate = join6(dir, "node_modules", ".bin", name);
589
+ if (existsSync7(candidate)) return candidate;
590
+ const parent = dirname2(dir);
591
+ if (parent === dir) return void 0;
592
+ dir = parent;
593
+ }
594
+ }
595
+ function binFromOwnInstall(packageName, binName) {
596
+ try {
597
+ const manifest = require2.resolve(`${packageName}/package.json`);
598
+ const bin = require2(manifest).bin;
599
+ const relative5 = typeof bin === "string" ? bin : bin?.[binName];
600
+ if (!relative5) return void 0;
601
+ const executable = join6(dirname2(manifest), relative5);
602
+ return existsSync7(executable) ? executable : void 0;
603
+ } catch {
604
+ return void 0;
605
+ }
606
+ }
607
+ function binSearchPath(cwd) {
608
+ return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
609
+ " then "
610
+ );
611
+ }
612
+
613
+ // src/roles/build/plan.ts
614
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
615
+ import { join as join11 } from "path";
616
+
617
+ // src/core/config/existing-command.ts
618
+ import { readFileSync as readFileSync6 } from "fs";
619
+ import { join as join7 } from "path";
620
+
621
+ // src/shared/jsonc.ts
622
+ import { parse, printParseErrorCode } from "jsonc-parser";
623
+ function parseJsonc(text, source = "config") {
624
+ const errors = [];
625
+ const value = parse(text, errors, { allowTrailingComma: true });
626
+ if (errors.length > 0) {
627
+ const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
628
+ throw new Error(`${source}: malformed JSONC (${details}).`);
629
+ }
630
+ return value;
631
+ }
632
+
633
+ // src/core/config/existing-command.ts
634
+ function nxTargetCommand(cwd, target) {
635
+ let project;
636
+ try {
637
+ project = parseJsonc(
638
+ readFileSync6(join7(cwd, "project.json"), "utf8"),
639
+ "project.json"
640
+ );
641
+ } catch {
642
+ return void 0;
643
+ }
644
+ const entry = project.targets?.[target];
645
+ if (!entry || entry.executor !== "nx:run-commands") return void 0;
646
+ const { command, commands, cwd: targetCwd } = entry.options ?? {};
647
+ const ranInModule = typeof targetCwd === "string" && targetCwd.trim() !== "";
648
+ if (typeof command === "string" && command.trim() !== "") {
649
+ return { command: command.trim(), ranInModule };
650
+ }
651
+ if (Array.isArray(commands)) {
652
+ const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
653
+ if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
654
+ }
655
+ return void 0;
656
+ }
657
+ function rootedForScript(command) {
658
+ return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
659
+ }
660
+ function existingCommand(cwd, target) {
661
+ const script = moduleScripts(cwd)[target];
662
+ if (script !== void 0 && script.trim() !== "") return script;
663
+ const fromTarget = nxTargetCommand(cwd, target);
664
+ if (fromTarget === void 0) return void 0;
665
+ return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
666
+ }
667
+
668
+ // src/core/config/nx-target.ts
669
+ import { existsSync as existsSync8 } from "fs";
670
+ import { join as join8 } from "path";
671
+ function nxTargetMetadataOperations(options) {
672
+ const { targets } = options;
673
+ if (Object.keys(targets).length === 0) return [];
674
+ return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
675
+ }
676
+ function nxTargetOperations(options) {
677
+ const { cwd, targets } = options;
678
+ const names = Object.keys(targets);
679
+ if (names.length === 0) return [];
680
+ const operations = [];
681
+ if (existsSync8(join8(cwd, "project.json"))) {
682
+ operations.push({
683
+ kind: "remove-json-keys",
684
+ path: "project.json",
685
+ keys: names.map((name) => ["targets", name])
686
+ });
687
+ }
688
+ operations.push(...nxTargetMetadataOperations(options));
689
+ return operations;
690
+ }
691
+
692
+ // src/core/config/tool-script.ts
693
+ var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
694
+ function isSeparatorAt(index) {
695
+ return index % 2 === 1;
696
+ }
697
+ function invokes(segment, binary) {
698
+ return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
699
+ }
700
+ function isSentinelSegment(segment, roleFlag) {
701
+ const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
702
+ return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
703
+ }
704
+ function composeToolScript(existing, options) {
705
+ const { command } = options;
706
+ if (!existing || existing.trim() === "") return command;
707
+ const { roleFlag } = options;
708
+ const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
709
+ const parts = existing.split(SEGMENT_SEPARATOR);
710
+ const commands = parts.filter((_, index) => !isSeparatorAt(index));
711
+ if (!commands.some(isReplaceable)) return existing;
712
+ let replacedOnce = false;
713
+ const rebuilt = parts.map((part, index) => {
714
+ if (isSeparatorAt(index) || !isReplaceable(part)) return part;
715
+ if (replacedOnce) return null;
716
+ replacedOnce = true;
717
+ return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
718
+ });
719
+ const kept = [];
720
+ for (let index = 0; index < rebuilt.length; index += 1) {
721
+ const part = rebuilt[index];
722
+ if (part === null) {
723
+ if (kept.length > 0) kept.pop();
724
+ continue;
725
+ }
726
+ kept.push(part);
727
+ }
728
+ return kept.join("").trim();
729
+ }
730
+ function keepsOtherCommands(script, sentinelCommand) {
731
+ return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
732
+ }
733
+
734
+ // src/roles/format/read-adoption.ts
735
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
736
+ import { join as join9 } from "path";
737
+
738
+ // src/roles/format/config-policy.ts
739
+ var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
740
+ var FORMAT_SCRIPT_NAME = "format";
741
+ var PROVENANCE_KEY = "$sentinel";
742
+ var PERMITTED_LOCAL_KEYS = [
743
+ "useTabs",
744
+ "tabWidth",
745
+ "printWidth",
746
+ "semi",
747
+ "singleQuote",
748
+ "jsxSingleQuote",
749
+ "trailingComma",
750
+ "quoteProps",
751
+ "bracketSpacing",
752
+ "bracketSameLine",
753
+ "arrowParens",
754
+ "endOfLine",
755
+ "objectWrap",
756
+ "proseWrap",
757
+ "experimentalOperatorPosition"
758
+ ];
759
+ var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
760
+ var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
761
+ var PRETTIER_CONFIG_FILES = [
762
+ ".prettierrc",
763
+ ".prettierrc.json",
764
+ ".prettierrc.json5",
765
+ ".prettierrc.yml",
766
+ ".prettierrc.yaml",
767
+ ".prettierrc.js",
768
+ ".prettierrc.cjs",
769
+ ".prettierrc.mjs",
770
+ "prettier.config.js",
771
+ "prettier.config.cjs",
772
+ "prettier.config.mjs"
773
+ ];
774
+ function formatTargets() {
775
+ return {
776
+ [FORMAT_SCRIPT_NAME]: {
777
+ cache: true,
778
+ inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
779
+ },
780
+ [`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
781
+ };
782
+ }
783
+
784
+ // src/roles/format/presets/base.json
785
+ var base_default = {
786
+ printWidth: 80,
787
+ semi: false,
788
+ singleQuote: true,
789
+ trailingComma: "all",
790
+ sortPackageJson: false,
791
+ sortImports: false
792
+ };
793
+
794
+ // src/roles/format/presets/nest-imports.json
795
+ var nest_imports_default = {
796
+ customGroups: [
797
+ {
798
+ groupName: "workspace",
799
+ elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
800
+ }
801
+ ],
802
+ groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
803
+ newlinesBetween: true,
804
+ order: "asc",
805
+ ignoreCase: false
806
+ };
807
+
808
+ // src/roles/format/preset-data.ts
809
+ var FORMAT_PRESETS = ["base", "svelte", "nest"];
810
+ function hasFormatPreset(name) {
811
+ return FORMAT_PRESETS.includes(name);
812
+ }
813
+ function formatPresetName(declared) {
814
+ if (declared === "svelte") return "svelte";
815
+ if (declared === "nest") return "nest";
816
+ return "base";
817
+ }
818
+ function formatPresetFor(preset) {
819
+ const base = base_default;
820
+ const name = formatPresetName(preset);
821
+ if (name === "svelte") return { ...base, svelte: true };
822
+ if (name === "nest") return { ...base, sortImports: nest_imports_default };
823
+ return base;
824
+ }
825
+
826
+ // src/roles/format/read-adoption.ts
827
+ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
828
+ configFile,
829
+ preset: null,
830
+ adopted: false,
831
+ conformant: false,
832
+ drift: [],
833
+ local: {},
834
+ presetVersion: null,
835
+ effective: {},
836
+ unreadable
837
+ });
838
+ function sameValue(a, b) {
839
+ return JSON.stringify(a) === JSON.stringify(b);
840
+ }
841
+ function readFormatAdoption(cwd) {
842
+ const path = join9(cwd, FORMAT_CONFIG_FILE);
843
+ if (!existsSync9(path)) return NOT_ADOPTED2(null);
844
+ let parsed;
845
+ try {
846
+ parsed = parseJsonc(readFileSync7(path, "utf8"), FORMAT_CONFIG_FILE);
847
+ } catch (error) {
848
+ const reason = error instanceof Error ? error.message : String(error);
849
+ return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
850
+ }
851
+ const provenance = parsed[PROVENANCE_KEY];
852
+ if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
853
+ if (!hasFormatPreset(provenance.preset)) {
854
+ return NOT_ADOPTED2(
855
+ FORMAT_CONFIG_FILE,
856
+ `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
857
+ );
858
+ }
859
+ const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
860
+ const preset = formatPresetFor(provenance.preset);
861
+ const drift = [];
862
+ const local = {};
863
+ for (const [key, expected] of Object.entries(preset)) {
864
+ if (declaredLocal.includes(key)) {
865
+ local[key] = parsed[key];
866
+ continue;
867
+ }
868
+ if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
869
+ }
870
+ for (const key of declaredLocal) {
871
+ if (!(key in preset)) local[key] = parsed[key];
872
+ }
873
+ for (const key of ALWAYS_LOCAL_KEYS) {
874
+ if (key in parsed) local[key] = parsed[key];
875
+ }
876
+ return {
877
+ configFile: FORMAT_CONFIG_FILE,
878
+ preset: provenance.preset,
879
+ adopted: true,
880
+ conformant: drift.length === 0,
881
+ drift,
882
+ local,
883
+ presetVersion: typeof provenance.version === "string" ? provenance.version : null,
884
+ effective: parsed,
885
+ unreadable: null
886
+ };
887
+ }
888
+
889
+ // src/roles/build/dev-script.ts
890
+ var CHAIN = " -- ";
891
+ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
892
+ var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
893
+ function appOptions(rest) {
894
+ const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
895
+ const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
896
+ return kept.join(" ");
897
+ }
898
+ function composeDevScript(existing, command) {
899
+ if (existing === void 0 || existing.trim() === "") return command;
900
+ const chunks = existing.trim().split(CHAIN);
901
+ const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
902
+ if (at === -1) return existing;
903
+ const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
904
+ const options = appOptions(rest);
905
+ chunks[at] = options === "" ? `${command} --` : `${command} -- ${options}`;
906
+ return chunks.join(CHAIN);
907
+ }
908
+
909
+ // src/roles/build/dev-target.ts
910
+ var DELEGATION_KEYS = /* @__PURE__ */ new Set(["executor", "options"]);
911
+ var DELEGATION_OPTION_KEYS = /* @__PURE__ */ new Set(["command", "cwd"]);
912
+ function delegatesToScript(target, scriptName) {
913
+ if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
914
+ const entries = target;
915
+ if (!Object.keys(entries).every((key) => DELEGATION_KEYS.has(key))) return false;
916
+ if (entries["executor"] !== "nx:run-commands") return false;
917
+ const options = entries["options"];
918
+ if (typeof options !== "object" || options === null || Array.isArray(options)) return false;
919
+ const declared = options;
920
+ if (!Object.keys(declared).every((key) => DELEGATION_OPTION_KEYS.has(key))) return false;
921
+ const command = declared["command"];
922
+ if (typeof command !== "string") return false;
923
+ return new RegExp(String.raw`(^|\s)run\s+${scriptName}$`).test(command.trim());
924
+ }
925
+ function devScriptIsAdopted(devScript) {
926
+ return devScript !== void 0 && devScript.includes(SENTINEL_DEV_COMMAND);
927
+ }
928
+ function devTargetOperations(cwd, devScript) {
929
+ if (!devScriptIsAdopted(devScript)) return { operations: [] };
930
+ const target = projectTargets(cwd)?.[DEV_SCRIPT_NAME];
931
+ if (target === void 0) return { operations: [] };
932
+ if (!delegatesToScript(target, DEV_SCRIPT_NAME)) {
933
+ return {
934
+ operations: [],
935
+ note: `left the \`${DEV_SCRIPT_NAME}\` target in project.json alone: it does more than call \`pnpm run ${DEV_SCRIPT_NAME}\`, so removing it would drop behaviour this role never declared. Move what it does into the script, or keep it and accept the second home.`
936
+ };
937
+ }
938
+ return {
939
+ operations: [
940
+ { kind: "remove-json-keys", path: "project.json", keys: [["targets", DEV_SCRIPT_NAME]] }
941
+ ],
942
+ note: `removed the \`${DEV_SCRIPT_NAME}\` target from project.json: it only called \`pnpm run ${DEV_SCRIPT_NAME}\`, which nx infers from the script itself. host-admin has run without that target all along.`
943
+ };
944
+ }
945
+
946
+ // src/roles/build/nest/adopt.ts
947
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
948
+ import { join as join10 } from "path";
949
+ var NEST_CONFIG_FILE = "vite.config.mts";
950
+ function asSource(value, depth) {
951
+ const pad = " ".repeat(depth);
952
+ const inner = " ".repeat(depth + 1);
953
+ if (Array.isArray(value)) {
954
+ if (value.length === 0) return "[]";
955
+ return `[
956
+ ${value.map((item) => `${inner}${asSource(item, depth + 1)}`).join(",\n")},
957
+ ${pad}]`;
958
+ }
959
+ if (value !== null && typeof value === "object") {
960
+ const entries = Object.entries(value);
961
+ if (entries.length === 0) return "{}";
962
+ return `{
963
+ ${entries.map(([key, item]) => `${inner}${asKey(key)}: ${asSource(item, depth + 1)}`).join(",\n")},
964
+ ${pad}}`;
965
+ }
966
+ if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
967
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
968
+ throw new Error(
969
+ `sentinel build(nest): cannot write ${typeof value} into the generated config. The build target carries a shape this translation was not written for, and it should refuse rather than emit something nobody has read.`
970
+ );
971
+ }
972
+ function asKey(key) {
973
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${key}'`;
974
+ }
975
+ function nestConfigSource(cwd, target) {
976
+ const hops = workspaceRootHops(cwd, target);
977
+ const up = Array.from({ length: hops }, () => "..").join("/");
978
+ const carried = (name, value) => value === void 0 || value.length === 0 ? "" : ` ${name}: ${asSource(value, 1)},
979
+ `;
980
+ return `import path from 'node:path'
981
+
982
+ import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
983
+
984
+ export default nestService({
985
+ project: '${target.project}',
986
+ root: __dirname,
987
+ workspaceRoot: path.resolve(__dirname, '${up}'),
988
+ outDir: '${target.outputPath}',
989
+ ${carried("transformers", target.transformers)}${carried("assets", target.assets)}})
990
+ `;
991
+ }
992
+ function nestBuildOutputs(target) {
993
+ return [`{workspaceRoot}/${target.outputPath}`];
994
+ }
995
+ var OUT_DIR_IN_CONFIG = /^\s*outDir:\s*'([^']+)'/m;
996
+ function nestOutputsFromConfig(cwd) {
997
+ const configPath = join10(cwd, NEST_CONFIG_FILE);
998
+ if (!existsSync10(configPath)) return void 0;
999
+ const outDir = OUT_DIR_IN_CONFIG.exec(readFileSync8(configPath, "utf8"))?.[1];
1000
+ return outDir === void 0 ? void 0 : [`{workspaceRoot}/${outDir}`];
1001
+ }
1002
+ function nestAdoptionOperations(cwd, target) {
1003
+ return [{ kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) }];
1004
+ }
1005
+
1006
+ // src/roles/build/requirements.ts
1007
+ function atLeast(declared, needed) {
1008
+ const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
1009
+ const left = parse2(declared);
1010
+ const right = parse2(needed);
1011
+ for (let at = 0; at < Math.max(left.length, right.length); at++) {
1012
+ const a = left[at] ?? 0;
1013
+ const b = right[at] ?? 0;
1014
+ if (a !== b) return a > b;
582
1015
  }
583
1016
  return true;
584
1017
  }
@@ -598,32 +1031,26 @@ function describeUnmet(preset, unmet) {
598
1031
  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\`.`;
599
1032
  }
600
1033
 
601
- // src/roles/build/rewrite-imports.ts
1034
+ // src/core/config/rewrite-imports.ts
602
1035
  import { parseSync } from "oxc-parser";
603
- var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
604
- function esmBlocker(fileName, packageType) {
605
- if (/\.(mts|mjs)$/.test(fileName)) return void 0;
606
- if (packageType === "module") return void 0;
607
- 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.`;
608
- }
609
- var DEFAULT_EXPORT_NAMES = Object.fromEntries(
610
- OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
611
- entry.specifier,
612
- entry.default
613
- ])
1036
+ var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
1037
+ var defaultExportNames = (toolchain) => Object.fromEntries(
1038
+ toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
614
1039
  );
615
- function renderImport(bindings) {
1040
+ function renderImport(bindings, presetSpecifier) {
616
1041
  const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
617
1042
  const values = bindings.filter((b) => !b.typeOnly);
618
1043
  const types = bindings.filter((b) => b.typeOnly);
619
1044
  const lines = [];
620
- if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
1045
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
621
1046
  if (types.length > 0) {
622
- lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
1047
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
623
1048
  }
624
1049
  return lines.join("\n");
625
1050
  }
626
- function rewriteBuildImports(source, fileName = "vite.config.ts") {
1051
+ function rewriteToolchainImports(toolchain, source, fileName) {
1052
+ const moved = movedSpecifiers(toolchain);
1053
+ const defaults = defaultExportNames(toolchain);
627
1054
  const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
628
1055
  if (errors.length > 0) {
629
1056
  return {
@@ -634,10 +1061,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
634
1061
  const imports = program.body.filter(
635
1062
  (node) => node.type === "ImportDeclaration"
636
1063
  );
637
- if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
1064
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
638
1065
  return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
639
1066
  }
640
- const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
1067
+ const moving = imports.filter((node) => moved.has(node.source.value));
641
1068
  if (moving.length === 0) {
642
1069
  return {
643
1070
  kind: "unchanged",
@@ -654,7 +1081,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
654
1081
  for (const spec of specifiers) {
655
1082
  const typeOnly = node.importKind === "type" || spec.importKind === "type";
656
1083
  if (spec.type === "ImportDefaultSpecifier") {
657
- const exported = DEFAULT_EXPORT_NAMES[from];
1084
+ const exported = defaults[from];
658
1085
  if (!exported) {
659
1086
  return {
660
1087
  kind: "blocked",
@@ -686,7 +1113,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
686
1113
  const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
687
1114
  out = out.slice(0, span.start) + out.slice(span.end + after);
688
1115
  }
689
- out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
1116
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier) + out.slice(first.end);
690
1117
  return {
691
1118
  kind: "rewritten",
692
1119
  source: out,
@@ -694,7 +1121,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
694
1121
  };
695
1122
  }
696
1123
 
1124
+ // src/roles/build/rewrite-imports.ts
1125
+ var BUILD_TOOLCHAIN = {
1126
+ presetSpecifier: BUILD_PRESET_SPECIFIER,
1127
+ owned: OWNED_PACKAGES
1128
+ };
1129
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
1130
+ return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
1131
+ }
1132
+
697
1133
  // src/roles/build/plan.ts
1134
+ function candidateOutputs() {
1135
+ return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
1136
+ }
698
1137
  function ownedBuildDependencies(cwd) {
699
1138
  const manifest = readProjectPackageJson(cwd);
700
1139
  const keys = [];
@@ -726,6 +1165,8 @@ function buildScripts(cwd) {
726
1165
  const own2 = moduleName(cwd);
727
1166
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
728
1167
  existingDev,
1168
+ // No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
1169
+ // sides produced `-- --`, handing Vite an argument nobody wrote.
729
1170
  `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
730
1171
  );
731
1172
  }
@@ -740,54 +1181,73 @@ function plan(context) {
740
1181
  if (unmet.length > 0) {
741
1182
  return { operations: [], skipped: describeUnmet(context.preset, unmet) };
742
1183
  }
743
- if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
1184
+ const declaredPreset2 = declaredBuildPreset(context.cwd);
1185
+ const configFile = buildConfigFile(context.cwd);
1186
+ if (declaredPreset2 === void 0 && configFile !== void 0) {
1187
+ return {
1188
+ operations: [],
1189
+ 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.`
1190
+ };
1191
+ }
1192
+ const webpackTarget = webpackBuildTarget(context.cwd);
1193
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
1194
+ return planNestService(context, webpackTarget);
1195
+ }
1196
+ if (configFile === void 0) {
744
1197
  return {
745
1198
  operations: [],
746
- 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.`
1199
+ 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.`
747
1200
  };
748
1201
  }
1202
+ return planReactApp(context, configFile);
1203
+ }
1204
+ function planReactApp(context, configFile) {
749
1205
  const notes = [];
750
1206
  const operations = [];
751
- const configFile = buildConfigFile(context.cwd);
752
- if (configFile === void 0) {
1207
+ const esm = esmReadiness(configFile, readProjectPackageJson(context.cwd).type);
1208
+ if (esm.kind === "blocked") return { operations: [], skipped: esm.why };
1209
+ const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
1210
+ if (rewrite.kind === "blocked") {
753
1211
  return {
754
1212
  operations: [],
755
- 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.`
1213
+ 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.`
756
1214
  };
757
- } else {
758
- const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
759
- if (notEsm !== void 0) return { operations: [], skipped: notEsm };
760
- const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
761
- if (rewrite.kind === "blocked") {
762
- return {
763
- operations: [],
764
- 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.`
765
- };
766
- }
767
- if (rewrite.kind === "rewritten") {
768
- operations.push({ kind: "write", path: configFile, contents: rewrite.source });
769
- }
770
- for (const name of COMPANION_CONFIG_FILES) {
771
- const source = readConfigSource(context.cwd, name);
772
- if (source === "") continue;
773
- const companion = rewriteBuildImports(source, name);
774
- if (companion.kind === "rewritten") {
775
- operations.push({ kind: "write", path: name, contents: companion.source });
776
- notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
777
- } else if (companion.kind === "blocked") {
778
- notes.push(
779
- `${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.`
780
- );
781
- }
782
- }
783
- if (rewrite.kind === "rewritten") {
1215
+ }
1216
+ if (rewrite.kind === "rewritten") {
1217
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
1218
+ }
1219
+ for (const name of COMPANION_CONFIG_FILES) {
1220
+ const source = readConfigSource(context.cwd, name);
1221
+ if (source === "") continue;
1222
+ const companion = rewriteBuildImports(source, name);
1223
+ if (companion.kind === "rewritten") {
1224
+ operations.push({ kind: "write", path: name, contents: companion.source });
1225
+ notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
1226
+ } else if (companion.kind === "blocked") {
784
1227
  notes.push(
785
- `${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.`
1228
+ `${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.`
786
1229
  );
787
1230
  }
788
1231
  }
789
- operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
790
- operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
1232
+ if (rewrite.kind === "rewritten") {
1233
+ notes.push(
1234
+ `${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.`
1235
+ );
1236
+ }
1237
+ const scripts = buildScripts(context.cwd);
1238
+ operations.push(manifestOperation(context.cwd, scripts));
1239
+ operations.push(
1240
+ ...nxTargetOperations({
1241
+ cwd: context.cwd,
1242
+ targets: buildTarget({
1243
+ moduleName: adoptedPackageName(context.cwd),
1244
+ outputs: candidateOutputs()
1245
+ })
1246
+ })
1247
+ );
1248
+ const dev = devTargetOperations(context.cwd, scripts[DEV_SCRIPT_NAME]);
1249
+ operations.push(...dev.operations);
1250
+ if (dev.note !== void 0) notes.push(dev.note);
791
1251
  const owned = ownedBuildDependencies(context.cwd);
792
1252
  if (owned.length > 0) {
793
1253
  operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
@@ -795,12 +1255,53 @@ function plan(context) {
795
1255
  `removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
796
1256
  );
797
1257
  }
1258
+ const formatted = readFormatAdoption(context.cwd).adopted;
1259
+ notes.push(
1260
+ formatted ? `run \`sentinel --run --format --fix\` on this module: adoption rewrote ${configFile}, and a rewritten import can be longer than your formatter wraps at.` : `this module is formatted by the workspace, not by sentinel, so format ${configFile} with whatever owns it before committing: a rewritten import can be longer than that formatter wraps at, and the check would fail on a file nobody typed.`
1261
+ );
1262
+ notes.push(
1263
+ `this config is code, and it runs before the build does: any environment variable it reads has to be present wherever you build, including a fresh clone, another CI job or an image. Adoption does not change what it reads, but a failure there arrives early and names this role rather than the variable.`
1264
+ );
798
1265
  return { operations, notes };
799
1266
  }
1267
+ function planNestService(context, webpackTarget) {
1268
+ const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
1269
+ if (outputs === void 0) {
1270
+ return {
1271
+ operations: [],
1272
+ 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.`
1273
+ };
1274
+ }
1275
+ const metadata = [
1276
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND }),
1277
+ ...nxTargetOperations({
1278
+ cwd: context.cwd,
1279
+ targets: buildTarget({
1280
+ moduleName: adoptedPackageName(context.cwd),
1281
+ configFiles: [NEST_CONFIG_FILE],
1282
+ outputs
1283
+ })
1284
+ })
1285
+ ];
1286
+ if (webpackTarget === void 0) return { operations: metadata };
1287
+ const refusal = refusalFor(context.cwd, webpackTarget);
1288
+ if (refusal !== void 0) return { operations: [], blocked: refusal };
1289
+ const dropped = Object.keys(webpackTarget.configurations ?? {});
1290
+ return {
1291
+ operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
1292
+ notes: [
1293
+ `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.`,
1294
+ ...dropped.length > 0 ? [
1295
+ `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.`
1296
+ ] : [],
1297
+ `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.`
1298
+ ]
1299
+ };
1300
+ }
800
1301
  function readConfigSource(cwd, configFile) {
801
- if (!existsSync7(join7(cwd, configFile))) return "";
1302
+ if (!existsSync11(join11(cwd, configFile))) return "";
802
1303
  try {
803
- return readFileSync5(join7(cwd, configFile), "utf8");
1304
+ return readFileSync9(join11(cwd, configFile), "utf8");
804
1305
  } catch {
805
1306
  return "";
806
1307
  }
@@ -840,11 +1341,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
840
1341
  * resolution let it run at all — filtering here would give "no adapter for preset svelte",
841
1342
  * which tells nobody what to do about it.
842
1343
  *
843
- * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
844
- * would change that.
1344
+ * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
1345
+ * grounds that "there is nothing to bundle", which was never true: those 38 services bundle
1346
+ * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
1347
+ * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
1348
+ * migration generator refuses every one of them because they use `@nx/js:node`
1349
+ * (nrwl/nx#36389). So the way out runs through here.
1350
+ *
1351
+ * `node` and `tools` remain genuinely declined: nothing under them bundles at all.
845
1352
  */
846
1353
  appliesTo(preset) {
847
- return preset === "react" || preset === "svelte";
1354
+ return preset === "react" || preset === "svelte" || preset === "nest";
848
1355
  }
849
1356
  /**
850
1357
  * `react`, read from the module's Vite config rather than from its dependencies.
@@ -935,40 +1442,252 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
935
1442
  return "dev";
936
1443
  }
937
1444
  /**
938
- * There is no such thing as adopting the dev server on its own.
1445
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
1446
+ *
1447
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1448
+ * because adopting one without the other leaves the module half-migrated. What was missing
1449
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
1450
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
1451
+ * wrong correction, it added friction to a command that already produced the right result.
1452
+ */
1453
+ plan(context) {
1454
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1455
+ return {
1456
+ operations: [],
1457
+ 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.`
1458
+ };
1459
+ }
1460
+ const planned = super.plan(context);
1461
+ if (planned.operations.length === 0) return planned;
1462
+ return {
1463
+ ...planned,
1464
+ notes: [
1465
+ ...planned.notes ?? [],
1466
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1467
+ ]
1468
+ };
1469
+ }
1470
+ /**
1471
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
1472
+ * beyond the exit code the developer's own Ctrl-C produces.
939
1473
  *
940
- * `ViteRoleAdapter.plan()` is shared with `--build` because everything else about the two
941
- * targets is shared, and inheriting it here made `--init --dev` perform the FULL build
942
- * adoption, silently, under a message that only ever mentioned the build. Exiting 0 on that
943
- * is the worst of the options: it teaches that dev can be adopted by itself, and that belief
944
- * is what produces the half-migrated module this role exists to prevent (see the class
945
- * comment above, and `registerBuild`).
1474
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
1475
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
1476
+ * running it again would race the watcher that is about to own the file.
946
1477
  *
947
- * A single plan writes both scripts, so the honest answer is to name the command that does
948
- * it rather than to do it under a different name.
1478
+ * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
1479
+ * bare form is the one every version accepts.
949
1480
  */
950
- plan() {
951
- throw new Error(
952
- "the dev server is adopted together with the build, never on its own. Run --init --build: one plan writes both the `build` and the `serve` scripts."
1481
+ async run(ctx) {
1482
+ const ready = this.prepare(ctx);
1483
+ if (ready.kind === "stop") return ready.result;
1484
+ return this.spawnVite(ctx, ready.vite, [], 0);
1485
+ }
1486
+ };
1487
+
1488
+ // src/roles/build/declared-outputs.ts
1489
+ import { existsSync as existsSync13, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
1490
+ import { join as join13 } from "path";
1491
+
1492
+ // src/core/workspace-prep.ts
1493
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
1494
+ import { dirname as dirname3, join as join12, relative as relative2 } from "path";
1495
+
1496
+ // src/core/settings.ts
1497
+ var WORKSPACE_ROOT_MARKER = "nx.json";
1498
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
1499
+
1500
+ // src/core/workspace-prep.ts
1501
+ var OVERRIDE_KEY = "i18next>typescript";
1502
+ var NATIVE_TS_ALIAS = "@typescript/native";
1503
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1504
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1505
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1506
+ function findWorkspaceRoot(startDir) {
1507
+ let dir = startDir;
1508
+ for (; ; ) {
1509
+ if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
1510
+ const parent = dirname3(dir);
1511
+ if (parent === dir) return void 0;
1512
+ dir = parent;
1513
+ }
1514
+ }
1515
+ function declaredNativeTs(pkg) {
1516
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1517
+ if (!spec) return void 0;
1518
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1519
+ return version || void 0;
1520
+ }
1521
+ function ensureI18nextSingleton(root, dryRun) {
1522
+ const pkgPath = join12(root, "package.json");
1523
+ if (!existsSync12(pkgPath)) return void 0;
1524
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1525
+ const want = declaredNativeTs(pkg);
1526
+ if (!want) return void 0;
1527
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1528
+ if (have === want) return void 0;
1529
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1530
+ pkg.pnpm ??= {};
1531
+ pkg.pnpm.overrides ??= {};
1532
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1533
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1534
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1535
+ }
1536
+ function ensureReleaseAgeAllowList(root, dryRun) {
1537
+ const yamlPath = join12(root, WORKSPACE_YAML);
1538
+ if (!existsSync12(yamlPath)) return void 0;
1539
+ const own = readOwnPackage().name;
1540
+ const lines = readFileSync10(yamlPath, "utf8").split("\n");
1541
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1542
+ if (keyIdx === -1) return void 0;
1543
+ let lastItemIdx = keyIdx;
1544
+ let indent = " ";
1545
+ for (let i = keyIdx + 1; i < lines.length; i++) {
1546
+ const match = lines[i]?.match(LIST_ITEM);
1547
+ if (!match) break;
1548
+ indent = match[1] ?? indent;
1549
+ lastItemIdx = i;
1550
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1551
+ }
1552
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1553
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1554
+ writeFileSync(yamlPath, lines.join("\n"));
1555
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1556
+ }
1557
+ var PRETTIER_IGNORE = ".prettierignore";
1558
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1559
+ var ROOT_PRETTIER_CONFIGS = [
1560
+ ".prettierrc",
1561
+ ".prettierrc.json",
1562
+ ".prettierrc.json5",
1563
+ ".prettierrc.yml",
1564
+ ".prettierrc.yaml",
1565
+ ".prettierrc.js",
1566
+ ".prettierrc.cjs",
1567
+ ".prettierrc.mjs",
1568
+ "prettier.config.js",
1569
+ "prettier.config.cjs",
1570
+ "prettier.config.mjs"
1571
+ ];
1572
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
1573
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1574
+ if (rel === "" || rel.startsWith("..")) return void 0;
1575
+ const ignorePath = join12(root, PRETTIER_IGNORE);
1576
+ const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
1577
+ if (!hasPrettier) return void 0;
1578
+ const pattern = `/${rel}/`;
1579
+ const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
1580
+ const lines = existing.split("\n");
1581
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1582
+ return void 0;
1583
+ }
1584
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1585
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1586
+ if (headerIdx === -1) {
1587
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1588
+
1589
+ `;
1590
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1591
+ ${pattern}
1592
+ `);
1593
+ } else {
1594
+ let end = headerIdx + 1;
1595
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1596
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1597
+ writeFileSync(
1598
+ ignorePath,
1599
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
953
1600
  );
954
1601
  }
955
- /**
956
- * Start the dev server. It does not return until stopped, so there is no verdict to report
957
- * beyond the exit code the developer's own Ctrl-C produces.
958
- *
959
- * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
960
- * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
961
- * running it again would race the watcher that is about to own the file.
962
- *
963
- * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
964
- * bare form is the one every version accepts.
965
- */
966
- async run(ctx) {
967
- const ready = this.prepare(ctx);
968
- if (ready.kind === "stop") return ready.result;
969
- return this.spawnVite(ctx, ready.vite, [], 0);
1602
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1603
+ }
1604
+ function ensureWorkspacePrep(opts) {
1605
+ const dryRun = Boolean(opts.dryRun);
1606
+ return [
1607
+ ensureI18nextSingleton(opts.root, dryRun),
1608
+ ensureReleaseAgeAllowList(opts.root, dryRun),
1609
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1610
+ ].filter((message) => message !== void 0);
1611
+ }
1612
+ function inspectWorkspacePrep(root) {
1613
+ const entries = [];
1614
+ let pkg = {};
1615
+ const pkgPath = join12(root, "package.json");
1616
+ if (existsSync12(pkgPath)) {
1617
+ try {
1618
+ pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1619
+ } catch {
1620
+ pkg = {};
1621
+ }
1622
+ }
1623
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1624
+ if (pinned !== void 0) {
1625
+ const stillForks = declaredNativeTs(pkg);
1626
+ entries.push({
1627
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1628
+ 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.`
1629
+ });
1630
+ }
1631
+ const own = readOwnPackage().name;
1632
+ const yamlPath = join12(root, WORKSPACE_YAML);
1633
+ if (existsSync12(yamlPath)) {
1634
+ const yaml = readFileSync10(yamlPath, "utf8");
1635
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1636
+ if (listed) {
1637
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1638
+ entries.push({
1639
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1640
+ 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.`
1641
+ });
1642
+ }
1643
+ }
1644
+ return entries;
1645
+ }
1646
+
1647
+ // src/roles/build/declared-outputs.ts
1648
+ function declaredBuildTarget(cwd) {
1649
+ return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1650
+ }
1651
+ function resolveOutputToken(token, cwd) {
1652
+ if (token.startsWith("{projectRoot}/")) return join13(cwd, token.slice("{projectRoot}/".length));
1653
+ if (token.startsWith("{workspaceRoot}/")) {
1654
+ const root = findWorkspaceRoot(cwd);
1655
+ return root === void 0 ? void 0 : join13(root, token.slice("{workspaceRoot}/".length));
1656
+ }
1657
+ return join13(cwd, token);
1658
+ }
1659
+ function buildStamp(cwd) {
1660
+ const marker = join13(cwd, `.sentinel-build-stamp-${process.pid}`);
1661
+ try {
1662
+ writeFileSync2(marker, "");
1663
+ const stamp = statSync(marker).mtimeMs;
1664
+ rmSync(marker, { force: true });
1665
+ return stamp;
1666
+ } catch {
1667
+ return Date.now();
1668
+ }
1669
+ }
1670
+ function writtenSince(path, startedAt) {
1671
+ if (!existsSync13(path)) return false;
1672
+ try {
1673
+ return statSync(path).mtimeMs >= startedAt;
1674
+ } catch {
1675
+ return true;
1676
+ }
1677
+ }
1678
+ function outputsProblem(cwd, startedAt) {
1679
+ const target = declaredBuildTarget(cwd);
1680
+ if (target === void 0) return void 0;
1681
+ const declared = target.outputs ?? [];
1682
+ if (declared.length === 0) {
1683
+ if (target.cache !== true) return void 0;
1684
+ 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.`;
970
1685
  }
971
- };
1686
+ const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
1687
+ if (resolved.length === 0) return void 0;
1688
+ if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
1689
+ 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.`;
1690
+ }
972
1691
 
973
1692
  // src/roles/build/prerequisite.ts
974
1693
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1018,7 +1737,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
1018
1737
  );
1019
1738
  return { ok: false, code: prerequisite.code };
1020
1739
  }
1021
- return this.spawnVite(ctx, ready.vite, ["build"], 1);
1740
+ const startedAt = buildStamp(ctx.cwd);
1741
+ const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
1742
+ if (!result.ok) return result;
1743
+ const problem = outputsProblem(ctx.cwd, startedAt);
1744
+ if (problem !== void 0) {
1745
+ this.say(problem);
1746
+ return { ok: false, code: 1 };
1747
+ }
1748
+ return result;
1022
1749
  }
1023
1750
  /**
1024
1751
  * What this module builds with, WITHOUT loading its config.
@@ -1090,12 +1817,12 @@ function registerBuild() {
1090
1817
 
1091
1818
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1092
1819
  import { spawnSync as spawnSync3 } from "child_process";
1093
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1094
- import { join as join14 } from "path";
1820
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1821
+ import { join as join18 } from "path";
1095
1822
 
1096
1823
  // src/core/config/has-source.ts
1097
1824
  import { readdirSync } from "fs";
1098
- import { extname, join as join8, relative } from "path";
1825
+ import { extname, join as join14, relative as relative3 } from "path";
1099
1826
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1100
1827
  "node_modules",
1101
1828
  "dist",
@@ -1146,7 +1873,7 @@ function hasSourceFiles(cwd, extensions) {
1146
1873
  }
1147
1874
  for (const entry of entries) {
1148
1875
  if (entry.isDirectory()) {
1149
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
1876
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join14(dir, entry.name));
1150
1877
  continue;
1151
1878
  }
1152
1879
  if (wanted.has(extname(entry.name))) return true;
@@ -1167,12 +1894,12 @@ function listSourceFiles(cwd, extensions) {
1167
1894
  continue;
1168
1895
  }
1169
1896
  for (const entry of entries) {
1170
- const full = join8(dir, entry.name);
1897
+ const full = join14(dir, entry.name);
1171
1898
  if (entry.isDirectory()) {
1172
1899
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1173
1900
  continue;
1174
1901
  }
1175
- if (wanted.has(extname(entry.name))) found.push(relative(cwd, full));
1902
+ if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1176
1903
  }
1177
1904
  }
1178
1905
  return found;
@@ -1180,207 +1907,9 @@ function listSourceFiles(cwd, extensions) {
1180
1907
 
1181
1908
  // src/core/named-scope.ts
1182
1909
  var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1183
- function scopeFlags(paths) {
1184
- return paths.length > 0 ? [TOLERATE_EMPTY_FLAG] : [];
1185
- }
1186
-
1187
- // src/core/settings.ts
1188
- var WORKSPACE_ROOT_MARKER = "nx.json";
1189
- var DEFAULT_MAX_DIAGNOSTICS = 100;
1190
-
1191
- // src/core/workspace-prep.ts
1192
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1193
- import { dirname as dirname3, join as join9, relative as relative2 } from "path";
1194
- var OVERRIDE_KEY = "i18next>typescript";
1195
- var NATIVE_TS_ALIAS = "@typescript/native";
1196
- var WORKSPACE_YAML = "pnpm-workspace.yaml";
1197
- var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1198
- var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1199
- function findWorkspaceRoot(startDir) {
1200
- let dir = startDir;
1201
- for (; ; ) {
1202
- if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
1203
- const parent = dirname3(dir);
1204
- if (parent === dir) return void 0;
1205
- dir = parent;
1206
- }
1207
- }
1208
- function declaredNativeTs(pkg) {
1209
- const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1210
- if (!spec) return void 0;
1211
- const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1212
- return version || void 0;
1213
- }
1214
- function ensureI18nextSingleton(root, dryRun) {
1215
- const pkgPath = join9(root, "package.json");
1216
- if (!existsSync8(pkgPath)) return void 0;
1217
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1218
- const want = declaredNativeTs(pkg);
1219
- if (!want) return void 0;
1220
- const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1221
- if (have === want) return void 0;
1222
- if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1223
- pkg.pnpm ??= {};
1224
- pkg.pnpm.overrides ??= {};
1225
- pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1226
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1227
- return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1228
- }
1229
- function ensureReleaseAgeAllowList(root, dryRun) {
1230
- const yamlPath = join9(root, WORKSPACE_YAML);
1231
- if (!existsSync8(yamlPath)) return void 0;
1232
- const own = readOwnPackage().name;
1233
- const lines = readFileSync6(yamlPath, "utf8").split("\n");
1234
- const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1235
- if (keyIdx === -1) return void 0;
1236
- let lastItemIdx = keyIdx;
1237
- let indent = " ";
1238
- for (let i = keyIdx + 1; i < lines.length; i++) {
1239
- const match = lines[i]?.match(LIST_ITEM);
1240
- if (!match) break;
1241
- indent = match[1] ?? indent;
1242
- lastItemIdx = i;
1243
- if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1244
- }
1245
- if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1246
- lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1247
- writeFileSync(yamlPath, lines.join("\n"));
1248
- return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1249
- }
1250
- var PRETTIER_IGNORE = ".prettierignore";
1251
- var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1252
- var ROOT_PRETTIER_CONFIGS = [
1253
- ".prettierrc",
1254
- ".prettierrc.json",
1255
- ".prettierrc.json5",
1256
- ".prettierrc.yml",
1257
- ".prettierrc.yaml",
1258
- ".prettierrc.js",
1259
- ".prettierrc.cjs",
1260
- ".prettierrc.mjs",
1261
- "prettier.config.js",
1262
- "prettier.config.cjs",
1263
- "prettier.config.mjs"
1264
- ];
1265
- function ensureFormatterExclusion(root, moduleDir, dryRun) {
1266
- const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1267
- if (rel === "" || rel.startsWith("..")) return void 0;
1268
- const ignorePath = join9(root, PRETTIER_IGNORE);
1269
- const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
1270
- if (!hasPrettier) return void 0;
1271
- const pattern = `/${rel}/`;
1272
- const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
1273
- const lines = existing.split("\n");
1274
- if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1275
- return void 0;
1276
- }
1277
- if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1278
- const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1279
- if (headerIdx === -1) {
1280
- const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1281
-
1282
- `;
1283
- writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1284
- ${pattern}
1285
- `);
1286
- } else {
1287
- let end = headerIdx + 1;
1288
- while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1289
- const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1290
- writeFileSync(
1291
- ignorePath,
1292
- [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1293
- );
1294
- }
1295
- return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1296
- }
1297
- function ensureWorkspacePrep(opts) {
1298
- const dryRun = Boolean(opts.dryRun);
1299
- return [
1300
- ensureI18nextSingleton(opts.root, dryRun),
1301
- ensureReleaseAgeAllowList(opts.root, dryRun),
1302
- opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1303
- ].filter((message) => message !== void 0);
1304
- }
1305
- function inspectWorkspacePrep(root) {
1306
- const entries = [];
1307
- let pkg = {};
1308
- const pkgPath = join9(root, "package.json");
1309
- if (existsSync8(pkgPath)) {
1310
- try {
1311
- pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1312
- } catch {
1313
- pkg = {};
1314
- }
1315
- }
1316
- const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1317
- if (pinned !== void 0) {
1318
- const stillForks = declaredNativeTs(pkg);
1319
- entries.push({
1320
- rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1321
- 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.`
1322
- });
1323
- }
1324
- const own = readOwnPackage().name;
1325
- const yamlPath = join9(root, WORKSPACE_YAML);
1326
- if (existsSync8(yamlPath)) {
1327
- const yaml = readFileSync6(yamlPath, "utf8");
1328
- const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1329
- if (listed) {
1330
- const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1331
- entries.push({
1332
- rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1333
- 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.`
1334
- });
1335
- }
1336
- }
1337
- return entries;
1338
- }
1339
-
1340
- // src/roles/format/config-policy.ts
1341
- var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
1342
- var FORMAT_SCRIPT_NAME = "format";
1343
- var PROVENANCE_KEY = "$sentinel";
1344
- var PERMITTED_LOCAL_KEYS = [
1345
- "useTabs",
1346
- "tabWidth",
1347
- "printWidth",
1348
- "semi",
1349
- "singleQuote",
1350
- "jsxSingleQuote",
1351
- "trailingComma",
1352
- "quoteProps",
1353
- "bracketSpacing",
1354
- "bracketSameLine",
1355
- "arrowParens",
1356
- "endOfLine",
1357
- "objectWrap",
1358
- "proseWrap",
1359
- "experimentalOperatorPosition"
1360
- ];
1361
- var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
1362
- var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
1363
- var PRETTIER_CONFIG_FILES = [
1364
- ".prettierrc",
1365
- ".prettierrc.json",
1366
- ".prettierrc.json5",
1367
- ".prettierrc.yml",
1368
- ".prettierrc.yaml",
1369
- ".prettierrc.js",
1370
- ".prettierrc.cjs",
1371
- ".prettierrc.mjs",
1372
- "prettier.config.js",
1373
- "prettier.config.cjs",
1374
- "prettier.config.mjs"
1375
- ];
1376
- function formatTargets() {
1377
- return {
1378
- [FORMAT_SCRIPT_NAME]: {
1379
- cache: true,
1380
- inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
1381
- },
1382
- [`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
1383
- };
1910
+ function scopeFlags(paths, options = []) {
1911
+ if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1912
+ return [TOLERATE_EMPTY_FLAG];
1384
1913
  }
1385
1914
 
1386
1915
  // src/roles/format/format-script.ts
@@ -1411,8 +1940,8 @@ function writesWhenRewritten(name, command) {
1411
1940
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1412
1941
 
1413
1942
  // src/roles/format/inherited-ignores.ts
1414
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1415
- import { join as join10 } from "path";
1943
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
1944
+ import { join as join15 } from "path";
1416
1945
  var ROOT_IGNORE_FILE = ".prettierignore";
1417
1946
  function isPattern(line) {
1418
1947
  const trimmed = line.trim();
@@ -1427,11 +1956,11 @@ function toModulePattern(pattern) {
1427
1956
  }
1428
1957
  function inheritedIgnorePatterns(workspaceRoot) {
1429
1958
  if (!workspaceRoot) return [];
1430
- const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1431
- if (!existsSync9(path)) return [];
1959
+ const path = join15(workspaceRoot, ROOT_IGNORE_FILE);
1960
+ if (!existsSync14(path)) return [];
1432
1961
  let contents;
1433
1962
  try {
1434
- contents = readFileSync7(path, "utf8");
1963
+ contents = readFileSync11(path, "utf8");
1435
1964
  } catch {
1436
1965
  return [];
1437
1966
  }
@@ -1453,78 +1982,36 @@ function unparseableFiles(output) {
1453
1982
  const location = LOCATION.exec(raw);
1454
1983
  if (location && pending) {
1455
1984
  diagnostics.push({
1456
- file: location[1] ?? "",
1457
- line: Number(location[2] ?? 0),
1458
- message: pending
1459
- });
1460
- pending = void 0;
1461
- continue;
1462
- }
1463
- const message = MESSAGE.exec(raw);
1464
- if (message) pending = message[1];
1465
- }
1466
- return diagnostics;
1467
- }
1468
- function oxfmtSaid(output) {
1469
- const lines = plain(output).split("\n");
1470
- const diagnostic = lines.find((line) => MESSAGE.test(line));
1471
- if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
1472
- return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
1473
- }
1474
- function needsSvelteCompiler(output) {
1475
- return /svelte\/compiler/.test(output);
1476
- }
1477
-
1478
- // src/roles/format/presets/base.json
1479
- var base_default = {
1480
- printWidth: 80,
1481
- semi: false,
1482
- singleQuote: true,
1483
- trailingComma: "all",
1484
- sortPackageJson: false,
1485
- sortImports: false
1486
- };
1487
-
1488
- // src/roles/format/presets/nest-imports.json
1489
- var nest_imports_default = {
1490
- customGroups: [
1491
- {
1492
- groupName: "workspace",
1493
- elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
1494
- }
1495
- ],
1496
- groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
1497
- newlinesBetween: true,
1498
- order: "asc",
1499
- ignoreCase: false
1500
- };
1501
-
1502
- // src/roles/format/preset-data.ts
1503
- var FORMAT_PRESETS = ["base", "svelte", "nest"];
1504
- function hasFormatPreset(name) {
1505
- return FORMAT_PRESETS.includes(name);
1985
+ file: location[1] ?? "",
1986
+ line: Number(location[2] ?? 0),
1987
+ message: pending
1988
+ });
1989
+ pending = void 0;
1990
+ continue;
1991
+ }
1992
+ const message = MESSAGE.exec(raw);
1993
+ if (message) pending = message[1];
1994
+ }
1995
+ return diagnostics;
1506
1996
  }
1507
- function formatPresetName(declared) {
1508
- if (declared === "svelte") return "svelte";
1509
- if (declared === "nest") return "nest";
1510
- return "base";
1997
+ function oxfmtSaid(output) {
1998
+ const lines = plain(output).split("\n");
1999
+ const diagnostic = lines.find((line) => MESSAGE.test(line));
2000
+ if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
2001
+ return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
1511
2002
  }
1512
- function formatPresetFor(preset) {
1513
- const base = base_default;
1514
- const name = formatPresetName(preset);
1515
- if (name === "svelte") return { ...base, svelte: true };
1516
- if (name === "nest") return { ...base, sortImports: nest_imports_default };
1517
- return base;
2003
+ function needsSvelteCompiler(output) {
2004
+ return /svelte\/compiler/.test(output);
1518
2005
  }
1519
2006
 
1520
2007
  // src/roles/format/prettier-config.ts
1521
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1522
- import { join as join12 } from "path";
2008
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2009
+ import { join as join17 } from "path";
1523
2010
 
1524
2011
  // src/roles/format/resolve-oxfmt.ts
1525
- import { readFileSync as readFileSync8 } from "fs";
2012
+ import { readFileSync as readFileSync12 } from "fs";
1526
2013
  import { createRequire as createRequire2 } from "module";
1527
- import { dirname as dirname4, join as join11 } from "path";
2014
+ import { dirname as dirname4, join as join16 } from "path";
1528
2015
  function resolveOxfmt(cwd) {
1529
2016
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1530
2017
  }
@@ -1553,8 +2040,8 @@ function configSchema() {
1553
2040
  function readConfigSchema() {
1554
2041
  try {
1555
2042
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
1556
- const schemaPath = join11(dirname4(manifest), "configuration_schema.json");
1557
- return JSON.parse(readFileSync8(schemaPath, "utf8"));
2043
+ const schemaPath = join16(dirname4(manifest), "configuration_schema.json");
2044
+ return JSON.parse(readFileSync12(schemaPath, "utf8"));
1558
2045
  } catch {
1559
2046
  return void 0;
1560
2047
  }
@@ -1585,14 +2072,14 @@ function toOxfmtOverrides(value) {
1585
2072
  return { overrides, unresolved };
1586
2073
  }
1587
2074
  function readPrettierSettings(cwd) {
1588
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
2075
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync15(join17(cwd, name)));
1589
2076
  if (!file) return { options: {}, unresolved: [] };
1590
2077
  if (/\.(js|cjs|mjs)$/.test(file)) {
1591
2078
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1592
2079
  }
1593
2080
  let parsed;
1594
2081
  try {
1595
- parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
2082
+ parsed = parseJsonc(readFileSync13(join17(cwd, file), "utf8"), file);
1596
2083
  } catch {
1597
2084
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1598
2085
  }
@@ -1621,71 +2108,6 @@ function readPrettierSettings(cwd) {
1621
2108
  return { options, file, unresolved };
1622
2109
  }
1623
2110
 
1624
- // src/roles/format/read-adoption.ts
1625
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1626
- import { join as join13 } from "path";
1627
- var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
1628
- configFile,
1629
- preset: null,
1630
- adopted: false,
1631
- conformant: false,
1632
- drift: [],
1633
- local: {},
1634
- presetVersion: null,
1635
- effective: {},
1636
- unreadable
1637
- });
1638
- function sameValue(a, b) {
1639
- return JSON.stringify(a) === JSON.stringify(b);
1640
- }
1641
- function readFormatAdoption(cwd) {
1642
- const path = join13(cwd, FORMAT_CONFIG_FILE);
1643
- if (!existsSync11(path)) return NOT_ADOPTED2(null);
1644
- let parsed;
1645
- try {
1646
- parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
1647
- } catch (error) {
1648
- const reason = error instanceof Error ? error.message : String(error);
1649
- return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
1650
- }
1651
- const provenance = parsed[PROVENANCE_KEY];
1652
- if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
1653
- if (!hasFormatPreset(provenance.preset)) {
1654
- return NOT_ADOPTED2(
1655
- FORMAT_CONFIG_FILE,
1656
- `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
1657
- );
1658
- }
1659
- const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
1660
- const preset = formatPresetFor(provenance.preset);
1661
- const drift = [];
1662
- const local = {};
1663
- for (const [key, expected] of Object.entries(preset)) {
1664
- if (declaredLocal.includes(key)) {
1665
- local[key] = parsed[key];
1666
- continue;
1667
- }
1668
- if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
1669
- }
1670
- for (const key of declaredLocal) {
1671
- if (!(key in preset)) local[key] = parsed[key];
1672
- }
1673
- for (const key of ALWAYS_LOCAL_KEYS) {
1674
- if (key in parsed) local[key] = parsed[key];
1675
- }
1676
- return {
1677
- configFile: FORMAT_CONFIG_FILE,
1678
- preset: provenance.preset,
1679
- adopted: true,
1680
- conformant: drift.length === 0,
1681
- drift,
1682
- local,
1683
- presetVersion: typeof provenance.version === "string" ? provenance.version : null,
1684
- effective: parsed,
1685
- unreadable: null
1686
- };
1687
- }
1688
-
1689
2111
  // src/roles/format/unenforced.ts
1690
2112
  function unenforcedFormatOptions(options) {
1691
2113
  const gaps = [];
@@ -1768,7 +2190,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1768
2190
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1769
2191
  ];
1770
2192
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1771
- (name) => existsSync12(join14(context.cwd, name))
2193
+ (name) => existsSync16(join18(context.cwd, name))
1772
2194
  );
1773
2195
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1774
2196
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1892,7 +2314,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1892
2314
  });
1893
2315
  const result = spawnSync3(
1894
2316
  oxfmt,
1895
- [mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
2317
+ [mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
1896
2318
  {
1897
2319
  cwd: ctx.cwd,
1898
2320
  stdio: "inherit"
@@ -2008,7 +2430,7 @@ var OxfmtAdapter = class extends BaseAdapter {
2008
2430
  modulePrettierDependencies(cwd) {
2009
2431
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
2010
2432
  try {
2011
- const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
2433
+ const manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
2012
2434
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
2013
2435
  } catch {
2014
2436
  return [];
@@ -2090,8 +2512,8 @@ function registerFormat() {
2090
2512
 
2091
2513
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2092
2514
  import { spawnSync as spawnSync4 } from "child_process";
2093
- import { existsSync as existsSync20, readFileSync as readFileSync19, rmSync, writeFileSync as writeFileSync2 } from "fs";
2094
- import { join as join24 } from "path";
2515
+ import { existsSync as existsSync24, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2516
+ import { join as join28 } from "path";
2095
2517
 
2096
2518
  // src/core/config/deferred-rules.ts
2097
2519
  function deferredRuleNames(rules) {
@@ -2099,8 +2521,8 @@ function deferredRuleNames(rules) {
2099
2521
  }
2100
2522
 
2101
2523
  // src/core/fix-report.ts
2102
- import { statSync } from "fs";
2103
- import { join as join15 } from "path";
2524
+ import { statSync as statSync2 } from "fs";
2525
+ import { join as join19 } from "path";
2104
2526
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2105
2527
  function readFailure(output) {
2106
2528
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2111,7 +2533,7 @@ function readFailure(output) {
2111
2533
  }
2112
2534
  function fingerprint(path) {
2113
2535
  try {
2114
- const stat = statSync(path);
2536
+ const stat = statSync2(path);
2115
2537
  return `${stat.size}:${stat.mtimeMs}`;
2116
2538
  } catch {
2117
2539
  return void 0;
@@ -2120,7 +2542,7 @@ function fingerprint(path) {
2120
2542
  function snapshotFiles(cwd, extensions) {
2121
2543
  const snapshot = /* @__PURE__ */ new Map();
2122
2544
  for (const file of listSourceFiles(cwd, extensions)) {
2123
- const mark = fingerprint(join15(cwd, file));
2545
+ const mark = fingerprint(join19(cwd, file));
2124
2546
  if (mark !== void 0) snapshot.set(file, mark);
2125
2547
  }
2126
2548
  return snapshot;
@@ -2128,7 +2550,7 @@ function snapshotFiles(cwd, extensions) {
2128
2550
  function changedSince(cwd, before, extensions) {
2129
2551
  const changed = [];
2130
2552
  for (const file of listSourceFiles(cwd, extensions)) {
2131
- const now = fingerprint(join15(cwd, file));
2553
+ const now = fingerprint(join19(cwd, file));
2132
2554
  if (now === void 0) continue;
2133
2555
  if (before.get(file) !== now) changed.push(file);
2134
2556
  }
@@ -2147,8 +2569,8 @@ function describeFixOutcome(outcome, toolLabel) {
2147
2569
  }
2148
2570
 
2149
2571
  // src/roles/lint/module-baseline.ts
2150
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2151
- import { join as join16 } from "path";
2572
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
2573
+ import { join as join20 } from "path";
2152
2574
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2153
2575
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2154
2576
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2192,11 +2614,11 @@ function describeHolds(holds) {
2192
2614
  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`;
2193
2615
  }
2194
2616
  function heldRules(cwd) {
2195
- const path = join16(cwd, LINT_BASELINE_FILE);
2196
- if (!existsSync13(path)) return [];
2617
+ const path = join20(cwd, LINT_BASELINE_FILE);
2618
+ if (!existsSync17(path)) return [];
2197
2619
  try {
2198
2620
  const parsed = parseJsonc(
2199
- readFileSync12(path, "utf8"),
2621
+ readFileSync15(path, "utf8"),
2200
2622
  LINT_BASELINE_FILE
2201
2623
  );
2202
2624
  return Object.keys(parsed.rules ?? {});
@@ -2627,7 +3049,8 @@ var react_lib_default = {
2627
3049
  {
2628
3050
  name: "storybook",
2629
3051
  specifier: "eslint-plugin-storybook"
2630
- }
3052
+ },
3053
+ "../plugins/hublo.js"
2631
3054
  ],
2632
3055
  settings: null,
2633
3056
  renames: {
@@ -2636,6 +3059,7 @@ var react_lib_default = {
2636
3059
  rules: {
2637
3060
  curly: ["error", "all"],
2638
3061
  "for-direction": "error",
3062
+ "hublo/no-regex-lookbehind": "error",
2639
3063
  "jest-dom/prefer-checked": "error",
2640
3064
  "jest-dom/prefer-empty": "error",
2641
3065
  "jest-dom/prefer-enabled-disabled": "error",
@@ -2987,7 +3411,7 @@ var react_lib_default = {
2987
3411
  ],
2988
3412
  uncovered: {
2989
3413
  "no-octal": "no oxlint equivalent",
2990
- "no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, which is why the app tier hosts `../plugins/hublo.js`. The library tier enforces none of those rules today, so it hosts nothing.",
3414
+ "no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, so this tier hosts `../plugins/hublo.js` too. It enforces exactly one of them: `hublo/no-regex-lookbehind`, because a library is where that bug is WRITTEN and it reaches an app tier only by being imported (HUB-43431 shipped from libs/front/components into the worker app). The palette selectors stay app-tier only.",
2991
3415
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
2992
3416
  }
2993
3417
  };
@@ -3051,6 +3475,7 @@ var react_default = {
3051
3475
  severity: "off",
3052
3476
  reason: "the house rule finds 3 real violations; they are fixed as their own change rather than inside a linter swap, so the rule ships visible-but-off and can be switched on once they are"
3053
3477
  },
3478
+ "hublo/no-regex-lookbehind": "error",
3054
3479
  "jest/expect-expect": {
3055
3480
  severity: "off",
3056
3481
  reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
@@ -3537,7 +3962,7 @@ var react_default = {
3537
3962
  uncovered: {
3538
3963
  "import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
3539
3964
  "no-octal": "no oxlint equivalent",
3540
- "no-restricted-syntax": "no oxlint equivalent as configuration, so the eight AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
3965
+ "no-restricted-syntax": "no oxlint equivalent as configuration, so the AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Two are hosted today: the eight palette selectors as `hublo/no-palette-common`, and the lookbehind selector as `hublo/no-regex-lookbehind`. Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
3541
3966
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
3542
3967
  }
3543
3968
  };
@@ -4019,14 +4444,14 @@ function downgradedRulesFor(preset) {
4019
4444
  }
4020
4445
 
4021
4446
  // src/roles/lint/extra-layers.ts
4022
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
4023
- import { isAbsolute as isAbsolute2, join as join17, resolve as resolve3 } from "path";
4447
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4448
+ import { isAbsolute as isAbsolute2, join as join21, resolve as resolve3 } from "path";
4024
4449
  function ruleCount(cwd, specifier) {
4025
4450
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4026
- if (!existsSync14(path)) return void 0;
4451
+ if (!existsSync18(path)) return void 0;
4027
4452
  try {
4028
4453
  const parsed = parseJsonc(
4029
- readFileSync13(path, "utf8"),
4454
+ readFileSync16(path, "utf8"),
4030
4455
  specifier
4031
4456
  );
4032
4457
  return Object.keys(parsed.rules ?? {}).length;
@@ -4046,7 +4471,7 @@ function extraLayers(cwd, extendsList) {
4046
4471
  function committedExtendsList(cwd, configFile) {
4047
4472
  try {
4048
4473
  const parsed = parseJsonc(
4049
- readFileSync13(join17(cwd, configFile), "utf8"),
4474
+ readFileSync16(join21(cwd, configFile), "utf8"),
4050
4475
  configFile
4051
4476
  );
4052
4477
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4105,8 +4530,8 @@ function errorCountsByConfigRule(stdout) {
4105
4530
  }
4106
4531
 
4107
4532
  // src/core/config/read-adoption.ts
4108
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4109
- import { join as join19 } from "path";
4533
+ import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
4534
+ import { join as join23 } from "path";
4110
4535
 
4111
4536
  // src/core/config/owned-keys.ts
4112
4537
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4121,12 +4546,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4121
4546
  }
4122
4547
 
4123
4548
  // src/core/config/resolve-config-target.ts
4124
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4125
- import { join as join18 } from "path";
4549
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4550
+ import { join as join22 } from "path";
4126
4551
  function readExtends(absolutePath) {
4127
4552
  let parsed;
4128
4553
  try {
4129
- parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
4554
+ parsed = parseJsonc(readFileSync17(absolutePath, "utf8"), absolutePath);
4130
4555
  } catch {
4131
4556
  return [];
4132
4557
  }
@@ -4140,8 +4565,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4140
4565
  let existing;
4141
4566
  let existingExtendsSomething = false;
4142
4567
  for (const candidate of candidates) {
4143
- const absolutePath = join18(moduleDir, candidate);
4144
- if (!existsSync15(absolutePath)) continue;
4568
+ const absolutePath = join22(moduleDir, candidate);
4569
+ if (!existsSync19(absolutePath)) continue;
4145
4570
  const chain = readExtends(absolutePath);
4146
4571
  if (existing === void 0) {
4147
4572
  existing = candidate;
@@ -4174,12 +4599,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4174
4599
  });
4175
4600
  function readAdoption(cwd, options) {
4176
4601
  const target = resolveConfigTarget(cwd, options);
4177
- if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
4602
+ if (target.reason === "none" || !existsSync20(join23(cwd, target.path))) {
4178
4603
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4179
4604
  }
4180
4605
  let parsed;
4181
4606
  try {
4182
- parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
4607
+ parsed = parseJsonc(readFileSync18(join23(cwd, target.path), "utf8"), target.path);
4183
4608
  } catch (error) {
4184
4609
  const reason = error instanceof Error ? error.message : String(error);
4185
4610
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4213,9 +4638,9 @@ function readLintAdoption(cwd) {
4213
4638
  }
4214
4639
 
4215
4640
  // src/roles/lint/resolve-oxlint.ts
4216
- import { existsSync as existsSync17 } from "fs";
4641
+ import { existsSync as existsSync21 } from "fs";
4217
4642
  import { createRequire as createRequire3 } from "module";
4218
- import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
4643
+ import { delimiter as delimiter2, dirname as dirname5, join as join24 } from "path";
4219
4644
  import { fileURLToPath as fileURLToPath2 } from "url";
4220
4645
  var PACKAGE_OF = {
4221
4646
  oxlint: "oxlint",
@@ -4240,30 +4665,30 @@ function tsgolintShim(cwd) {
4240
4665
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4241
4666
  try {
4242
4667
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4243
- candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4244
- candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
4668
+ candidates.push(join24(packageDir, "node_modules", ".bin", "tsgolint"));
4669
+ candidates.push(join24(packageDir, "..", ".bin", "tsgolint"));
4245
4670
  } catch {
4246
4671
  }
4247
4672
  }
4248
4673
  try {
4249
4674
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4250
- candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
4675
+ candidates.push(join24(ownRoot, "node_modules", ".bin", "tsgolint"));
4251
4676
  } catch {
4252
4677
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4253
4678
  }
4254
- return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
4679
+ return candidates.find((candidate) => candidate !== "" && existsSync21(candidate));
4255
4680
  }
4256
4681
  function oxlintSearchPath(cwd) {
4257
4682
  return binSearchPath(cwd);
4258
4683
  }
4259
4684
 
4260
4685
  // src/roles/lint/adapters/oxlint/plan.ts
4261
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4262
- import { join as join23 } from "path";
4686
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
4687
+ import { join as join27 } from "path";
4263
4688
 
4264
4689
  // src/roles/lint/eslint-ignores.ts
4265
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4266
- import { join as join21 } from "path";
4690
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
4691
+ import { join as join25 } from "path";
4267
4692
  import { parseSync as parseSync2 } from "oxc-parser";
4268
4693
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4269
4694
  function toOxlintPattern(pattern) {
@@ -4275,11 +4700,11 @@ function readRootEslintIgnores(root) {
4275
4700
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4276
4701
  }
4277
4702
  function readEslintIgnores(cwd) {
4278
- const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
4703
+ const config = ESLINT_CONFIG_FILES.map((name) => join25(cwd, name)).find((path) => existsSync22(path));
4279
4704
  if (!config) return { patterns: [], unresolved: [] };
4280
4705
  let source;
4281
4706
  try {
4282
- source = readFileSync16(config, "utf8");
4707
+ source = readFileSync19(config, "utf8");
4283
4708
  } catch {
4284
4709
  return { patterns: [], unresolved: [] };
4285
4710
  }
@@ -4380,8 +4805,8 @@ function lintPresetFor(preset) {
4380
4805
  }
4381
4806
 
4382
4807
  // src/roles/lint/rename-suppressions.ts
4383
- import { readdirSync as readdirSync2, readFileSync as readFileSync17, statSync as statSync2 } from "fs";
4384
- import { join as join22, relative as relative3 } from "path";
4808
+ import { readdirSync as readdirSync2, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
4809
+ import { join as join26, relative as relative4 } from "path";
4385
4810
  var SOURCE_EXTENSIONS = [
4386
4811
  ".ts",
4387
4812
  ".tsx",
@@ -4428,10 +4853,10 @@ function* sourceFiles(dir) {
4428
4853
  return;
4429
4854
  }
4430
4855
  for (const entry of entries) {
4431
- const full = join22(dir, entry);
4856
+ const full = join26(dir, entry);
4432
4857
  let isDirectory;
4433
4858
  try {
4434
- isDirectory = statSync2(full).isDirectory();
4859
+ isDirectory = statSync3(full).isDirectory();
4435
4860
  } catch {
4436
4861
  continue;
4437
4862
  }
@@ -4448,7 +4873,7 @@ function findSuppressionRenames(cwd, renames) {
4448
4873
  for (const file of sourceFiles(cwd)) {
4449
4874
  let content;
4450
4875
  try {
4451
- content = readFileSync17(file, "utf8");
4876
+ content = readFileSync20(file, "utf8");
4452
4877
  } catch {
4453
4878
  continue;
4454
4879
  }
@@ -4472,7 +4897,7 @@ function findSuppressionRenames(cwd, renames) {
4472
4897
  }
4473
4898
  if (hits.length === 0) return;
4474
4899
  found.push({
4475
- file: relative3(cwd, file),
4900
+ file: relative4(cwd, file),
4476
4901
  line: index + 1,
4477
4902
  from: line,
4478
4903
  to: line.replace(rules, renamed.join(", ")),
@@ -4576,7 +5001,7 @@ function plan2(context) {
4576
5001
  keys: removableDeps
4577
5002
  });
4578
5003
  }
4579
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
5004
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync23(join27(context.cwd, name)));
4580
5005
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4581
5006
  operations.push(
4582
5007
  ...nxTargetOperations({
@@ -4674,7 +5099,7 @@ function lintScripts(cwd) {
4674
5099
  function committedExtends(cwd) {
4675
5100
  try {
4676
5101
  const parsed = parseJsonc(
4677
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
5102
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4678
5103
  LINT_CONFIG_FILE
4679
5104
  );
4680
5105
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4685,7 +5110,7 @@ function committedExtends(cwd) {
4685
5110
  function committedIgnorePatterns(cwd) {
4686
5111
  try {
4687
5112
  const parsed = parseJsonc(
4688
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
5113
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4689
5114
  LINT_CONFIG_FILE
4690
5115
  );
4691
5116
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4697,7 +5122,7 @@ function moduleEslintDependencies(cwd) {
4697
5122
  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/");
4698
5123
  let manifest;
4699
5124
  try {
4700
- manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
5125
+ manifest = JSON.parse(readFileSync21(join27(cwd, "package.json"), "utf8"));
4701
5126
  } catch {
4702
5127
  return [];
4703
5128
  }
@@ -4831,23 +5256,23 @@ ${result.stderr ?? ""}`);
4831
5256
  * build the developer can see, rather than a silent half-adoption they cannot.
4832
5257
  */
4833
5258
  writeModuleBaseline(ctx, oxlint, env) {
4834
- const configPath = join24(ctx.cwd, LINT_CONFIG_FILE);
4835
- const baselinePath = join24(ctx.cwd, LINT_BASELINE_FILE);
4836
- if (!existsSync20(configPath)) return;
5259
+ const configPath = join28(ctx.cwd, LINT_CONFIG_FILE);
5260
+ const baselinePath = join28(ctx.cwd, LINT_BASELINE_FILE);
5261
+ if (!existsSync24(configPath)) return;
4837
5262
  let config;
4838
5263
  try {
4839
5264
  config = parseJsonc(
4840
- readFileSync19(configPath, "utf8"),
5265
+ readFileSync22(configPath, "utf8"),
4841
5266
  LINT_CONFIG_FILE
4842
5267
  );
4843
5268
  } catch {
4844
5269
  return;
4845
5270
  }
4846
5271
  const current = Array.isArray(config.extends) ? config.extends : [];
4847
- const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
5272
+ const measurePath = join28(ctx.cwd, LINT_MEASURE_FILE);
4848
5273
  let measured;
4849
5274
  try {
4850
- writeFileSync2(
5275
+ writeFileSync3(
4851
5276
  measurePath,
4852
5277
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
4853
5278
  `
@@ -4865,7 +5290,7 @@ ${result.stderr ?? ""}`);
4865
5290
  { cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
4866
5291
  );
4867
5292
  } finally {
4868
- rmSync(measurePath, { force: true });
5293
+ rmSync2(measurePath, { force: true });
4869
5294
  }
4870
5295
  if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
4871
5296
  process.stderr.write(
@@ -4879,20 +5304,20 @@ ${result.stderr ?? ""}`);
4879
5304
  const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
4880
5305
  const next = extendsWithBaseline(current, holds.length > 0);
4881
5306
  if (holds.length > 0) {
4882
- writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
5307
+ writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
4883
5308
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
4884
5309
  `);
4885
5310
  } else {
4886
- rmSync(baselinePath, { force: true });
5311
+ rmSync2(baselinePath, { force: true });
4887
5312
  }
4888
5313
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
4889
5314
  config.extends = next;
4890
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
5315
+ writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
4891
5316
  `);
4892
5317
  }
4893
5318
  }
4894
5319
  async run(ctx) {
4895
- if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
5320
+ if (!existsSync24(join28(ctx.cwd, LINT_CONFIG_FILE))) {
4896
5321
  process.stderr.write(
4897
5322
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
4898
5323
  `
@@ -4937,7 +5362,7 @@ ${result.stderr ?? ""}`);
4937
5362
  valueFlags: OXLINT_VALUE_FLAGS
4938
5363
  });
4939
5364
  const targets = paths.length > 0 ? paths : ["."];
4940
- const emptyScope = scopeFlags(paths);
5365
+ const emptyScope = scopeFlags(paths, passedOptions);
4941
5366
  const lint = (extra) => {
4942
5367
  const passed = [...extra, ...emptyScope, ...passedOptions];
4943
5368
  const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
@@ -5040,7 +5465,7 @@ ${result.stderr ?? ""}`);
5040
5465
  let stub;
5041
5466
  try {
5042
5467
  stub = parseJsonc(
5043
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5468
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5044
5469
  LINT_CONFIG_FILE
5045
5470
  );
5046
5471
  } catch {
@@ -5050,7 +5475,7 @@ ${result.stderr ?? ""}`);
5050
5475
  for (const entry of stub.extends ?? []) {
5051
5476
  try {
5052
5477
  const preset = parseJsonc(
5053
- readFileSync19(join24(cwd, entry), "utf8"),
5478
+ readFileSync22(join28(cwd, entry), "utf8"),
5054
5479
  entry
5055
5480
  );
5056
5481
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5120,14 +5545,14 @@ ${result.stderr ?? ""}`);
5120
5545
  let parsed;
5121
5546
  try {
5122
5547
  parsed = parseJsonc(
5123
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5548
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5124
5549
  LINT_CONFIG_FILE
5125
5550
  );
5126
5551
  } catch {
5127
5552
  return void 0;
5128
5553
  }
5129
5554
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5130
- return targets.find((target) => !existsSync20(join24(cwd, target)));
5555
+ return targets.find((target) => !existsSync24(join28(cwd, target)));
5131
5556
  }
5132
5557
  /** Announce what is not enforced, so reduced coverage is never silent. */
5133
5558
  announceDisabled(preset) {
@@ -5157,11 +5582,355 @@ function registerLint() {
5157
5582
  setDefaultRunner("lint", "oxlint");
5158
5583
  }
5159
5584
 
5160
- // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5585
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5161
5586
  import { spawnSync as spawnSync5 } from "child_process";
5162
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
5587
+ import { existsSync as existsSync26 } from "fs";
5588
+ import { join as join30 } from "path";
5589
+
5590
+ // src/roles/test/presets/toolchain.json
5591
+ var toolchain_default2 = {
5592
+ $comment: "The test toolchain sentinel owns. Same shape and same rule as the build one: a single table drives what `--init` removes from a module's package.json, which import specifiers move, and the name each default import is re-exported under, so the three cannot disagree.",
5593
+ owned: [
5594
+ {
5595
+ package: "vitest",
5596
+ specifier: "vitest/config",
5597
+ named: ["defineConfig", "mergeConfig"],
5598
+ types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
5599
+ why: "the runner itself, declared at the workspace ROOT today, which is what stops a module being testable on its own. Note the package is `vitest` and the specifier is `vitest/config`: the same split as nitro on the build side, and the reason one table holds both."
5600
+ },
5601
+ {
5602
+ package: "vite",
5603
+ specifier: "vite",
5604
+ named: ["loadEnv"],
5605
+ why: "`loadEnv` has no equivalent in `vitest/config`, and two of the six configs import it from vite to build their test env. Re-exported here rather than claimed: the BUILD role owns the `vite` package, and two roles owning one package is how a version conflict gets written down twice."
5606
+ }
5607
+ ]
5608
+ };
5609
+
5610
+ // src/roles/test/preset-data.ts
5611
+ var OWNED_TEST_PACKAGES = toolchain_default2.owned;
5612
+
5613
+ // src/roles/test/config-policy.ts
5614
+ var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
5615
+ var TEST_CONFIG_FILES = [
5616
+ "vitest.config.ts",
5617
+ "vitest.config.mts",
5618
+ "vitest.config.js",
5619
+ "vitest.config.mjs"
5620
+ ];
5621
+ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
5622
+ var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
5623
+ (entry) => entry.package !== "vite"
5624
+ ).map((entry) => entry.package);
5625
+ var TEST_TOOLCHAIN = {
5626
+ presetSpecifier: TEST_PRESET_SPECIFIER,
5627
+ owned: TEST_OWNED_PACKAGES
5628
+ };
5629
+ var TEST_SCRIPT_NAME = "test";
5630
+ var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
5631
+ var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
5632
+ function rewriteVitestScript(command) {
5633
+ return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
5634
+ const args = tail.trim().split(/\s+/).filter(Boolean);
5635
+ const oneShot = args[0] === "run" || args.includes("--run");
5636
+ const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
5637
+ const passthrough = oneShot ? rest : ["--watch", ...rest];
5638
+ const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
5639
+ const trailing = /\s$/.test(tail) ? " " : "";
5640
+ return `${lead}sentinel --run --test${suffix}${trailing}`;
5641
+ });
5642
+ }
5643
+ function testTarget(options) {
5644
+ const { configFiles, outputs } = options;
5645
+ const cached = outputs !== void 0 && outputs.length > 0;
5646
+ return {
5647
+ [TEST_SCRIPT_NAME]: {
5648
+ cache: cached,
5649
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
5650
+ ...cached ? { outputs: [...outputs] } : {}
5651
+ }
5652
+ };
5653
+ }
5654
+
5655
+ // src/roles/test/read-adoption.ts
5656
+ import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
5657
+ import { join as join29 } from "path";
5658
+ var JEST_CONFIG_FILES = [
5659
+ "jest.config.ts",
5660
+ "jest.config.js",
5661
+ "jest.prisma.config.ts",
5662
+ "jest.functional.config.ts",
5663
+ "jest.integration-config.ts"
5664
+ ];
5665
+ function testConfigFile(cwd) {
5666
+ return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
5667
+ }
5668
+ function jestConfigFiles(cwd) {
5669
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
5670
+ }
5671
+ function rootJestConfigFiles(cwd) {
5672
+ const root = findWorkspaceRoot(cwd);
5673
+ if (root === void 0 || root === cwd) return [];
5674
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
5675
+ }
5676
+ var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5677
+ var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
5678
+ function hasTestFiles(cwd) {
5679
+ const stack = [cwd];
5680
+ while (stack.length > 0) {
5681
+ const dir = stack.pop();
5682
+ let entries;
5683
+ try {
5684
+ entries = readdirSync3(dir, { withFileTypes: true });
5685
+ } catch {
5686
+ continue;
5687
+ }
5688
+ for (const entry of entries) {
5689
+ if (entry.isDirectory()) {
5690
+ if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
5691
+ } else if (TEST_FILE.test(entry.name)) {
5692
+ return true;
5693
+ }
5694
+ }
5695
+ }
5696
+ return false;
5697
+ }
5698
+ function readTestConfigSource(cwd, fileName) {
5699
+ try {
5700
+ return readFileSync23(join29(cwd, fileName), "utf8");
5701
+ } catch {
5702
+ return "";
5703
+ }
5704
+ }
5705
+ function importsPreset2(source) {
5706
+ const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5707
+ return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
5708
+ }
5709
+ function readTestAdoption(cwd) {
5710
+ const manifest = readProjectPackageJson(cwd);
5711
+ const jestConfigs = jestConfigFiles(cwd);
5712
+ const configFile = testConfigFile(cwd);
5713
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
5714
+ const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
5715
+ if (configFile === void 0) {
5716
+ const hasTests = hasTestFiles(cwd);
5717
+ const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
5718
+ return {
5719
+ configFile: null,
5720
+ preset: null,
5721
+ adopted: false,
5722
+ conformant: false,
5723
+ drift: [],
5724
+ unreadable: null,
5725
+ state: hasTests ? "jest" : "no-tests",
5726
+ runner: hasTests ? "jest" : "none",
5727
+ jestConfigs,
5728
+ inheritsRootConfig,
5729
+ ownDeclarations
5730
+ };
5731
+ }
5732
+ const source = readTestConfigSource(cwd, configFile);
5733
+ if (source === "") {
5734
+ return {
5735
+ configFile,
5736
+ preset: null,
5737
+ adopted: false,
5738
+ conformant: false,
5739
+ drift: [],
5740
+ unreadable: `${configFile} could not be read`,
5741
+ state: "vitest-own-toolchain",
5742
+ runner: "vitest",
5743
+ jestConfigs,
5744
+ inheritsRootConfig: false,
5745
+ // it has a config of its own, readable or not
5746
+ ownDeclarations
5747
+ };
5748
+ }
5749
+ const adopted = importsPreset2(source);
5750
+ const script = moduleScripts(cwd)["test"];
5751
+ return {
5752
+ configFile,
5753
+ preset: adopted ? "react" : null,
5754
+ adopted,
5755
+ conformant: adopted && script !== void 0 && jestConfigs.length === 0,
5756
+ drift: [],
5757
+ unreadable: null,
5758
+ state: adopted ? "adopted" : "vitest-own-toolchain",
5759
+ runner: "vitest",
5760
+ jestConfigs,
5761
+ inheritsRootConfig: false,
5762
+ // local always wins: this module is out of the root's reach
5763
+ ownDeclarations
5764
+ };
5765
+ }
5766
+
5767
+ // src/roles/test/plan.ts
5768
+ function ownedTestDependencies(cwd, declared) {
5769
+ return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
5770
+ ["dependencies", entry.name],
5771
+ ["devDependencies", entry.name]
5772
+ ]).filter(([section]) => section !== void 0);
5773
+ }
5774
+ function plan3(context) {
5775
+ const configFile = testConfigFile(context.cwd);
5776
+ const adoption = readTestAdoption(context.cwd);
5777
+ if (configFile === void 0) {
5778
+ return {
5779
+ operations: [],
5780
+ skipped: adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}). Migrating it moves its test FILES as well as its config, which is the next step of this role and not this one.` : `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
5781
+ };
5782
+ }
5783
+ const operations = [];
5784
+ const notes = [];
5785
+ const rewrite = rewriteToolchainImports(
5786
+ TEST_TOOLCHAIN,
5787
+ readTestConfigSource(context.cwd, configFile),
5788
+ configFile
5789
+ );
5790
+ if (rewrite.kind === "blocked") {
5791
+ return {
5792
+ operations: [],
5793
+ 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.`
5794
+ };
5795
+ }
5796
+ if (rewrite.kind === "rewritten") {
5797
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
5798
+ notes.push(
5799
+ `${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
5800
+ );
5801
+ }
5802
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
5803
+ const outputs = existing?.outputs ?? [];
5804
+ if (outputs.length === 0) {
5805
+ notes.push(
5806
+ `this target declares no \`outputs\`, so it is left UNCACHED. Caching a run that restores nothing is the defect where nx reports a hit and the coverage directory is empty; if this module writes coverage, declare where and the target can cache.`
5807
+ );
5808
+ }
5809
+ const siblingScripts = {};
5810
+ for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
5811
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
5812
+ const rewritten = rewriteVitestScript(command);
5813
+ if (rewritten !== command) siblingScripts[name] = rewritten;
5814
+ }
5815
+ if (Object.keys(siblingScripts).length > 0) {
5816
+ notes.push(
5817
+ `rewrote ${Object.keys(siblingScripts).join(", ")} to run through sentinel: they called vitest directly, and the dependency they relied on is removed below. A watch script keeps watching, through \`-- --watch\`.`
5818
+ );
5819
+ }
5820
+ operations.push(
5821
+ manifestOperation(context.cwd, {
5822
+ [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
5823
+ ...siblingScripts
5824
+ })
5825
+ );
5826
+ operations.push(
5827
+ ...nxTargetOperations({
5828
+ cwd: context.cwd,
5829
+ targets: testTarget({ configFiles: [configFile], outputs })
5830
+ })
5831
+ );
5832
+ const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
5833
+ if (owned.length > 0) {
5834
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
5835
+ notes.push(
5836
+ `removed ${[...new Set(owned.map(([, name]) => name))].join(", ")} from this module: sentinel owns the runner now, and two copies of Vitest in one run give the plugins a different Vite than the one running them.`
5837
+ );
5838
+ }
5839
+ if (adoption.jestConfigs.length > 0) {
5840
+ notes.push(
5841
+ `${adoption.jestConfigs.join(", ")} is still here beside the Vitest config. It is not this step's to delete, since something may still run it, but the workspace cannot drop jest while it exists.`
5842
+ );
5843
+ }
5844
+ return { operations, notes };
5845
+ }
5846
+
5847
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5848
+ function resolveVitest(cwd) {
5849
+ const fromSentinel = join30(
5850
+ new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
5851
+ );
5852
+ if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
5853
+ const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
5854
+ return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
5855
+ }
5856
+ var VitestAdapter = class extends BaseAdapter {
5857
+ target = "test";
5858
+ runner = "vitest";
5859
+ appliesTo(_preset) {
5860
+ return true;
5861
+ }
5862
+ plan(context) {
5863
+ return plan3(context);
5864
+ }
5865
+ async status(ctx) {
5866
+ return Promise.resolve(readTestAdoption(ctx.cwd));
5867
+ }
5868
+ async inspect(ctx) {
5869
+ const adoption = readTestAdoption(ctx.cwd);
5870
+ return Promise.resolve({
5871
+ state: adoption.state,
5872
+ runner: adoption.runner,
5873
+ configFile: adoption.configFile,
5874
+ jestConfigs: adoption.jestConfigs,
5875
+ adopted: adoption.adopted,
5876
+ conformant: adoption.conformant,
5877
+ drift: adoption.drift,
5878
+ unreadable: adoption.unreadable
5879
+ });
5880
+ }
5881
+ /**
5882
+ * Run the module's tests.
5883
+ *
5884
+ * A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
5885
+ * with no named target sweeps the workspace, so failing here would exit non-zero on the 107
5886
+ * modules still on jest, which says nothing anyone can act on.
5887
+ */
5888
+ async run(ctx) {
5889
+ const adoption = readTestAdoption(ctx.cwd);
5890
+ if (!adoption.adopted) {
5891
+ this.say(
5892
+ adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
5893
+ );
5894
+ return Promise.resolve({ ok: true, code: 0 });
5895
+ }
5896
+ const { bin, origin } = resolveVitest(ctx.cwd);
5897
+ if (bin === void 0) {
5898
+ this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
5899
+ return Promise.resolve({ ok: false, code: 1 });
5900
+ }
5901
+ if (origin === "module") {
5902
+ this.say(
5903
+ `using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
5904
+ );
5905
+ }
5906
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
5907
+ const watching = options.includes("--watch") || options.includes("-w");
5908
+ const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
5909
+ const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
5910
+ if (result.error) {
5911
+ this.say(`could not run vitest (${result.error.message})`);
5912
+ return Promise.resolve({ ok: false, code: 1 });
5913
+ }
5914
+ const code = result.status ?? 1;
5915
+ return Promise.resolve({ ok: code === 0, code });
5916
+ }
5917
+ say(message) {
5918
+ process.stderr.write(`sentinel test(vitest): ${message}
5919
+ `);
5920
+ }
5921
+ };
5922
+
5923
+ // src/roles/test/register.ts
5924
+ function registerTest() {
5925
+ register(new VitestAdapter());
5926
+ setDefaultRunner("test", "vitest");
5927
+ }
5928
+
5929
+ // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5930
+ import { spawnSync as spawnSync6 } from "child_process";
5931
+ import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
5163
5932
  import { createRequire as createRequire4 } from "module";
5164
- import { join as join26 } from "path";
5933
+ import { join as join32 } from "path";
5165
5934
 
5166
5935
  // src/roles/typescript/presets/nest.json
5167
5936
  var nest_default2 = {
@@ -5342,8 +6111,8 @@ function readTsconfigAdoption(cwd) {
5342
6111
  }
5343
6112
 
5344
6113
  // src/roles/typescript/adapters/tsc/plan.ts
5345
- import { readFileSync as readFileSync20 } from "fs";
5346
- import { join as join25 } from "path";
6114
+ import { readFileSync as readFileSync24 } from "fs";
6115
+ import { join as join31 } from "path";
5347
6116
 
5348
6117
  // src/roles/typescript/typecheck-script.ts
5349
6118
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5436,7 +6205,7 @@ function planAdoption(context) {
5436
6205
  };
5437
6206
  }
5438
6207
  const existing = parseJsonc(
5439
- readFileSync20(join25(context.cwd, target.path), "utf8"),
6208
+ readFileSync24(join31(context.cwd, target.path), "utf8"),
5440
6209
  target.path
5441
6210
  );
5442
6211
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5574,7 +6343,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5574
6343
  let chain;
5575
6344
  try {
5576
6345
  const parsed = parseJsonc(
5577
- readFileSync21(join26(cwd, target.path), "utf8"),
6346
+ readFileSync25(join32(cwd, target.path), "utf8"),
5578
6347
  target.path
5579
6348
  );
5580
6349
  chain = parsed.extends;
@@ -5587,7 +6356,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5587
6356
  );
5588
6357
  if (preset === void 0) return void 0;
5589
6358
  try {
5590
- createRequire4(join26(cwd, "noop.js")).resolve(preset);
6359
+ createRequire4(join32(cwd, "noop.js")).resolve(preset);
5591
6360
  return void 0;
5592
6361
  } catch {
5593
6362
  return preset;
@@ -5628,7 +6397,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5628
6397
  return { ok: false, code: 1 };
5629
6398
  }
5630
6399
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
5631
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
6400
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5632
6401
  if (result.error) {
5633
6402
  process.stderr.write(
5634
6403
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5663,7 +6432,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5663
6432
  let worst = 0;
5664
6433
  for (const project of this.referencedProjects(ctx.cwd, config)) {
5665
6434
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
5666
- const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
6435
+ const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5667
6436
  if (result.error) {
5668
6437
  process.stderr.write(
5669
6438
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5683,7 +6452,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5683
6452
  referencedProjects(cwd, config) {
5684
6453
  try {
5685
6454
  const parsed = parseJsonc(
5686
- readFileSync21(join26(cwd, config), "utf8"),
6455
+ readFileSync25(join32(cwd, config), "utf8"),
5687
6456
  config
5688
6457
  );
5689
6458
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5699,7 +6468,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5699
6468
  * check.
5700
6469
  */
5701
6470
  typecheckTarget(cwd) {
5702
- if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
6471
+ if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
5703
6472
  const target = resolveTsconfigTarget(cwd);
5704
6473
  return target.reason === "none" ? null : target.path;
5705
6474
  }
@@ -5752,7 +6521,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5752
6521
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
5753
6522
  }
5754
6523
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
5755
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
6524
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5756
6525
  if (result.error) {
5757
6526
  process.stderr.write(
5758
6527
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5795,7 +6564,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5795
6564
  * errors in the output mean the rule must be on.
5796
6565
  */
5797
6566
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
5798
- const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
6567
+ const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5799
6568
  if (shown.status === 0 && shown.stdout) {
5800
6569
  try {
5801
6570
  const co = parseJsonc(
@@ -5822,6 +6591,7 @@ function registerAdapters() {
5822
6591
  registerLint();
5823
6592
  registerFormat();
5824
6593
  registerBuild();
6594
+ registerTest();
5825
6595
  }
5826
6596
 
5827
6597
  // src/core/detect-framework.ts
@@ -5903,7 +6673,7 @@ function replaceLines(current, replacements) {
5903
6673
  }
5904
6674
 
5905
6675
  // src/core/apply-plan.ts
5906
- import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6676
+ import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5907
6677
  import { resolve as resolve4, sep } from "path";
5908
6678
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
5909
6679
 
@@ -5922,7 +6692,7 @@ function resolveWithinRoot(cwd, relativePath) {
5922
6692
  return absolutePath;
5923
6693
  }
5924
6694
  function readIfExists(absolutePath) {
5925
- return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
6695
+ return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
5926
6696
  }
5927
6697
  function* leaves(value, prefix = []) {
5928
6698
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6003,9 +6773,9 @@ function applyOperationTo(current, operation) {
6003
6773
  }
6004
6774
  }
6005
6775
  }
6006
- function preparePlan(cwd, plan3) {
6776
+ function preparePlan(cwd, plan4) {
6007
6777
  const prepared = /* @__PURE__ */ new Map();
6008
- for (const operation of plan3.operations) {
6778
+ for (const operation of plan4.operations) {
6009
6779
  const absolutePath = resolveWithinRoot(cwd, operation.path);
6010
6780
  const existing = prepared.get(operation.path);
6011
6781
  const before = existing?.before ?? readIfExists(absolutePath) ?? "";
@@ -6024,14 +6794,14 @@ function preparePlan(cwd, plan3) {
6024
6794
  }
6025
6795
  function writeFileAtomic(absolutePath, contents) {
6026
6796
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
6027
- writeFileSync3(tempPath, contents);
6797
+ writeFileSync4(tempPath, contents);
6028
6798
  renameSync(tempPath, absolutePath);
6029
6799
  }
6030
- function applyPlan(cwd, plan3) {
6031
- const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
6800
+ function applyPlan(cwd, plan4) {
6801
+ const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
6032
6802
  for (const file of changed) {
6033
6803
  if (file.deleted) {
6034
- rmSync2(file.absolutePath, { force: true });
6804
+ rmSync3(file.absolutePath, { force: true });
6035
6805
  continue;
6036
6806
  }
6037
6807
  writeFileAtomic(file.absolutePath, file.after);
@@ -6040,8 +6810,8 @@ function applyPlan(cwd, plan3) {
6040
6810
  }
6041
6811
 
6042
6812
  // src/core/config/preset-evidence.ts
6043
- import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "fs";
6044
- import { join as join27 } from "path";
6813
+ import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
6814
+ import { join as join33 } from "path";
6045
6815
  var PATH_SIGNALS = [
6046
6816
  {
6047
6817
  preset: "nest",
@@ -6055,11 +6825,11 @@ var DEPENDENCY_SIGNALS = [
6055
6825
  { preset: "nest", pattern: /^@nestjs\// }
6056
6826
  ];
6057
6827
  function dependencyNames(cwd) {
6058
- const path = join27(cwd, "package.json");
6059
- if (!existsSync23(path)) return [];
6828
+ const path = join33(cwd, "package.json");
6829
+ if (!existsSync29(path)) return [];
6060
6830
  try {
6061
6831
  const manifest = parseJsonc(
6062
- readFileSync23(path, "utf8"),
6832
+ readFileSync27(path, "utf8"),
6063
6833
  path
6064
6834
  );
6065
6835
  return [
@@ -6073,7 +6843,7 @@ function dependencyNames(cwd) {
6073
6843
  function declaresJsx(cwd) {
6074
6844
  let entries;
6075
6845
  try {
6076
- entries = readdirSync3(cwd).filter(
6846
+ entries = readdirSync4(cwd).filter(
6077
6847
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
6078
6848
  );
6079
6849
  } catch {
@@ -6082,7 +6852,7 @@ function declaresJsx(cwd) {
6082
6852
  for (const name of entries) {
6083
6853
  try {
6084
6854
  const config = parseJsonc(
6085
- readFileSync23(join27(cwd, name), "utf8"),
6855
+ readFileSync27(join33(cwd, name), "utf8"),
6086
6856
  name
6087
6857
  );
6088
6858
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6133,14 +6903,14 @@ function resolveFlavour(opts) {
6133
6903
  }
6134
6904
  return detection.preset;
6135
6905
  }
6136
- function previewPlan(opts, plan3) {
6137
- const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
6906
+ function previewPlan(opts, plan4) {
6907
+ const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
6138
6908
  if (opts.json) {
6139
6909
  process.stdout.write(
6140
6910
  JSON.stringify(
6141
6911
  {
6142
6912
  dryRun: true,
6143
- notes: plan3.notes ?? [],
6913
+ notes: plan4.notes ?? [],
6144
6914
  files: changed.map(({ path, before, after, deleted }) => ({
6145
6915
  path,
6146
6916
  action: deleted ? "delete" : before.length === 0 ? "create" : "update",
@@ -6155,7 +6925,7 @@ function previewPlan(opts, plan3) {
6155
6925
  return 0;
6156
6926
  }
6157
6927
  process.stderr.write(" dry run: no files written\n");
6158
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6928
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6159
6929
  `);
6160
6930
  if (changed.length === 0) {
6161
6931
  process.stderr.write(" nothing to change\n");
@@ -6199,25 +6969,25 @@ async function dispatch(opts) {
6199
6969
  }
6200
6970
  }
6201
6971
  const context = { cwd: opts.cwd, preset: effective };
6202
- const plan3 = await adapter.plan(context);
6203
- if (plan3.blocked) {
6204
- process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
6972
+ const plan4 = await adapter.plan(context);
6973
+ if (plan4.blocked) {
6974
+ process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
6205
6975
  `);
6206
6976
  return 1;
6207
6977
  }
6208
- if (plan3.skipped) {
6209
- process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan3.skipped}`)}
6978
+ if (plan4.skipped) {
6979
+ process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
6210
6980
  `);
6211
6981
  return 0;
6212
6982
  }
6213
6983
  if (opts.dryRun) {
6214
- return previewPlan(opts, plan3);
6984
+ return previewPlan(opts, plan4);
6215
6985
  }
6216
- for (const change of applyPlan(opts.cwd, plan3)) {
6986
+ for (const change of applyPlan(opts.cwd, plan4)) {
6217
6987
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
6218
6988
  `);
6219
6989
  }
6220
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6990
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6221
6991
  `);
6222
6992
  if (adapter.afterInit) {
6223
6993
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
@@ -6241,24 +7011,25 @@ export {
6241
7011
  declaredPresetFor,
6242
7012
  availableTargets,
6243
7013
  resolve,
6244
- BaseAdapter,
6245
- resolveBin,
6246
7014
  readOwnVersion,
6247
7015
  readProjectPackageJson,
6248
7016
  moduleName,
7017
+ buildConfigFile,
7018
+ BaseAdapter,
7019
+ resolveBin,
7020
+ WORKSPACE_ROOT_MARKER,
7021
+ findWorkspaceRoot,
7022
+ ensureWorkspacePrep,
7023
+ inspectWorkspacePrep,
6249
7024
  VERBS,
6250
7025
  TARGETS,
6251
7026
  LONG_RUNNING_TARGETS,
6252
7027
  SWEEPABLE_TARGETS,
6253
7028
  PRESET_NAMES,
6254
- WORKSPACE_ROOT_MARKER,
6255
- findWorkspaceRoot,
6256
- ensureWorkspacePrep,
6257
- inspectWorkspacePrep,
6258
7029
  palette,
6259
7030
  registerAdapters,
6260
7031
  describeFramework,
6261
7032
  detectFramework,
6262
7033
  dispatch
6263
7034
  };
6264
- //# sourceMappingURL=chunk-YZ4EKBK7.js.map
7035
+ //# sourceMappingURL=chunk-37Y7RALA.js.map