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

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,27 @@ 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, semi) {
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(", ");
1042
+ const end = semi ? ";" : "";
617
1043
  const values = bindings.filter((b) => !b.typeOnly);
618
1044
  const types = bindings.filter((b) => b.typeOnly);
619
1045
  const lines = [];
620
- if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
1046
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'${end}`);
621
1047
  if (types.length > 0) {
622
- lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
1048
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'${end}`);
623
1049
  }
624
1050
  return lines.join("\n");
625
1051
  }
626
- function rewriteBuildImports(source, fileName = "vite.config.ts") {
1052
+ function rewriteToolchainImports(toolchain, source, fileName) {
1053
+ const moved = movedSpecifiers(toolchain);
1054
+ const defaults = defaultExportNames(toolchain);
627
1055
  const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
628
1056
  if (errors.length > 0) {
629
1057
  return {
@@ -634,10 +1062,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
634
1062
  const imports = program.body.filter(
635
1063
  (node) => node.type === "ImportDeclaration"
636
1064
  );
637
- if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
1065
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
638
1066
  return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
639
1067
  }
640
- const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
1068
+ const moving = imports.filter((node) => moved.has(node.source.value));
641
1069
  if (moving.length === 0) {
642
1070
  return {
643
1071
  kind: "unchanged",
@@ -654,7 +1082,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
654
1082
  for (const spec of specifiers) {
655
1083
  const typeOnly = node.importKind === "type" || spec.importKind === "type";
656
1084
  if (spec.type === "ImportDefaultSpecifier") {
657
- const exported = DEFAULT_EXPORT_NAMES[from];
1085
+ const exported = defaults[from];
658
1086
  if (!exported) {
659
1087
  return {
660
1088
  kind: "blocked",
@@ -686,7 +1114,8 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
686
1114
  const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
687
1115
  out = out.slice(0, span.start) + out.slice(span.end + after);
688
1116
  }
689
- out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
1117
+ const semi = source.slice(first.start, first.end).trimEnd().endsWith(";");
1118
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier, semi) + out.slice(first.end);
690
1119
  return {
691
1120
  kind: "rewritten",
692
1121
  source: out,
@@ -694,7 +1123,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
694
1123
  };
695
1124
  }
696
1125
 
1126
+ // src/roles/build/rewrite-imports.ts
1127
+ var BUILD_TOOLCHAIN = {
1128
+ presetSpecifier: BUILD_PRESET_SPECIFIER,
1129
+ owned: OWNED_PACKAGES
1130
+ };
1131
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
1132
+ return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
1133
+ }
1134
+
697
1135
  // src/roles/build/plan.ts
1136
+ function candidateOutputs() {
1137
+ return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
1138
+ }
698
1139
  function ownedBuildDependencies(cwd) {
699
1140
  const manifest = readProjectPackageJson(cwd);
700
1141
  const keys = [];
@@ -726,6 +1167,8 @@ function buildScripts(cwd) {
726
1167
  const own2 = moduleName(cwd);
727
1168
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
728
1169
  existingDev,
1170
+ // No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
1171
+ // sides produced `-- --`, handing Vite an argument nobody wrote.
729
1172
  `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
730
1173
  );
731
1174
  }
@@ -740,54 +1183,73 @@ function plan(context) {
740
1183
  if (unmet.length > 0) {
741
1184
  return { operations: [], skipped: describeUnmet(context.preset, unmet) };
742
1185
  }
743
- if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
1186
+ const declaredPreset2 = declaredBuildPreset(context.cwd);
1187
+ const configFile = buildConfigFile(context.cwd);
1188
+ if (declaredPreset2 === void 0 && configFile !== void 0) {
1189
+ return {
1190
+ operations: [],
1191
+ 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.`
1192
+ };
1193
+ }
1194
+ const webpackTarget = webpackBuildTarget(context.cwd);
1195
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
1196
+ return planNestService(context, webpackTarget);
1197
+ }
1198
+ if (configFile === void 0) {
744
1199
  return {
745
1200
  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.`
1201
+ 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
1202
  };
748
1203
  }
1204
+ return planReactApp(context, configFile);
1205
+ }
1206
+ function planReactApp(context, configFile) {
749
1207
  const notes = [];
750
1208
  const operations = [];
751
- const configFile = buildConfigFile(context.cwd);
752
- if (configFile === void 0) {
1209
+ const esm = esmReadiness(configFile, readProjectPackageJson(context.cwd).type);
1210
+ if (esm.kind === "blocked") return { operations: [], skipped: esm.why };
1211
+ const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
1212
+ if (rewrite.kind === "blocked") {
753
1213
  return {
754
1214
  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.`
1215
+ 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
1216
  };
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") {
1217
+ }
1218
+ if (rewrite.kind === "rewritten") {
1219
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
1220
+ }
1221
+ for (const name of COMPANION_CONFIG_FILES) {
1222
+ const source = readConfigSource(context.cwd, name);
1223
+ if (source === "") continue;
1224
+ const companion = rewriteBuildImports(source, name);
1225
+ if (companion.kind === "rewritten") {
1226
+ operations.push({ kind: "write", path: name, contents: companion.source });
1227
+ notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
1228
+ } else if (companion.kind === "blocked") {
784
1229
  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.`
1230
+ `${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
1231
  );
787
1232
  }
788
1233
  }
789
- operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
790
- operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
1234
+ if (rewrite.kind === "rewritten") {
1235
+ notes.push(
1236
+ `${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.`
1237
+ );
1238
+ }
1239
+ const scripts = buildScripts(context.cwd);
1240
+ operations.push(manifestOperation(context.cwd, scripts));
1241
+ operations.push(
1242
+ ...nxTargetOperations({
1243
+ cwd: context.cwd,
1244
+ targets: buildTarget({
1245
+ moduleName: adoptedPackageName(context.cwd),
1246
+ outputs: candidateOutputs()
1247
+ })
1248
+ })
1249
+ );
1250
+ const dev = devTargetOperations(context.cwd, scripts[DEV_SCRIPT_NAME]);
1251
+ operations.push(...dev.operations);
1252
+ if (dev.note !== void 0) notes.push(dev.note);
791
1253
  const owned = ownedBuildDependencies(context.cwd);
792
1254
  if (owned.length > 0) {
793
1255
  operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
@@ -795,12 +1257,53 @@ function plan(context) {
795
1257
  `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
1258
  );
797
1259
  }
1260
+ const formatted = readFormatAdoption(context.cwd).adopted;
1261
+ notes.push(
1262
+ 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.`
1263
+ );
1264
+ notes.push(
1265
+ `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.`
1266
+ );
798
1267
  return { operations, notes };
799
1268
  }
1269
+ function planNestService(context, webpackTarget) {
1270
+ const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
1271
+ if (outputs === void 0) {
1272
+ return {
1273
+ operations: [],
1274
+ 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.`
1275
+ };
1276
+ }
1277
+ const metadata = [
1278
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND }),
1279
+ ...nxTargetOperations({
1280
+ cwd: context.cwd,
1281
+ targets: buildTarget({
1282
+ moduleName: adoptedPackageName(context.cwd),
1283
+ configFiles: [NEST_CONFIG_FILE],
1284
+ outputs
1285
+ })
1286
+ })
1287
+ ];
1288
+ if (webpackTarget === void 0) return { operations: metadata };
1289
+ const refusal = refusalFor(context.cwd, webpackTarget);
1290
+ if (refusal !== void 0) return { operations: [], blocked: refusal };
1291
+ const dropped = Object.keys(webpackTarget.configurations ?? {});
1292
+ return {
1293
+ operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
1294
+ notes: [
1295
+ `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.`,
1296
+ ...dropped.length > 0 ? [
1297
+ `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.`
1298
+ ] : [],
1299
+ `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.`
1300
+ ]
1301
+ };
1302
+ }
800
1303
  function readConfigSource(cwd, configFile) {
801
- if (!existsSync7(join7(cwd, configFile))) return "";
1304
+ if (!existsSync11(join11(cwd, configFile))) return "";
802
1305
  try {
803
- return readFileSync5(join7(cwd, configFile), "utf8");
1306
+ return readFileSync9(join11(cwd, configFile), "utf8");
804
1307
  } catch {
805
1308
  return "";
806
1309
  }
@@ -840,11 +1343,17 @@ var ViteRoleAdapter = class extends BaseAdapter {
840
1343
  * resolution let it run at all — filtering here would give "no adapter for preset svelte",
841
1344
  * which tells nobody what to do about it.
842
1345
  *
843
- * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
844
- * would change that.
1346
+ * Nest it now serves too, through `@hublo/sentinel/build/nest`. It used to be declined on the
1347
+ * grounds that "there is nothing to bundle", which was never true: those 38 services bundle
1348
+ * with webpack. What changed is that Nx v24 removes the `@nx/webpack:webpack` executor and
1349
+ * the `composePlugins` / `withNx` helpers their shared config is built on, and nx's own
1350
+ * migration generator refuses every one of them because they use `@nx/js:node`
1351
+ * (nrwl/nx#36389). So the way out runs through here.
1352
+ *
1353
+ * `node` and `tools` remain genuinely declined: nothing under them bundles at all.
845
1354
  */
846
1355
  appliesTo(preset) {
847
- return preset === "react" || preset === "svelte";
1356
+ return preset === "react" || preset === "svelte" || preset === "nest";
848
1357
  }
849
1358
  /**
850
1359
  * `react`, read from the module's Vite config rather than from its dependencies.
@@ -935,40 +1444,252 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
935
1444
  return "dev";
936
1445
  }
937
1446
  /**
938
- * There is no such thing as adopting the dev server on its own.
1447
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
1448
+ *
1449
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1450
+ * because adopting one without the other leaves the module half-migrated. What was missing
1451
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
1452
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
1453
+ * wrong correction, it added friction to a command that already produced the right result.
1454
+ */
1455
+ plan(context) {
1456
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1457
+ return {
1458
+ operations: [],
1459
+ 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.`
1460
+ };
1461
+ }
1462
+ const planned = super.plan(context);
1463
+ if (planned.operations.length === 0) return planned;
1464
+ return {
1465
+ ...planned,
1466
+ notes: [
1467
+ ...planned.notes ?? [],
1468
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1469
+ ]
1470
+ };
1471
+ }
1472
+ /**
1473
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
1474
+ * beyond the exit code the developer's own Ctrl-C produces.
939
1475
  *
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`).
1476
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
1477
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
1478
+ * running it again would race the watcher that is about to own the file.
946
1479
  *
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.
1480
+ * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
1481
+ * bare form is the one every version accepts.
949
1482
  */
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."
1483
+ async run(ctx) {
1484
+ const ready = this.prepare(ctx);
1485
+ if (ready.kind === "stop") return ready.result;
1486
+ return this.spawnVite(ctx, ready.vite, [], 0);
1487
+ }
1488
+ };
1489
+
1490
+ // src/roles/build/declared-outputs.ts
1491
+ import { existsSync as existsSync13, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
1492
+ import { join as join13 } from "path";
1493
+
1494
+ // src/core/workspace-prep.ts
1495
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
1496
+ import { dirname as dirname3, join as join12, relative as relative2 } from "path";
1497
+
1498
+ // src/core/settings.ts
1499
+ var WORKSPACE_ROOT_MARKER = "nx.json";
1500
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
1501
+
1502
+ // src/core/workspace-prep.ts
1503
+ var OVERRIDE_KEY = "i18next>typescript";
1504
+ var NATIVE_TS_ALIAS = "@typescript/native";
1505
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1506
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1507
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1508
+ function findWorkspaceRoot(startDir) {
1509
+ let dir = startDir;
1510
+ for (; ; ) {
1511
+ if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
1512
+ const parent = dirname3(dir);
1513
+ if (parent === dir) return void 0;
1514
+ dir = parent;
1515
+ }
1516
+ }
1517
+ function declaredNativeTs(pkg) {
1518
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1519
+ if (!spec) return void 0;
1520
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1521
+ return version || void 0;
1522
+ }
1523
+ function ensureI18nextSingleton(root, dryRun) {
1524
+ const pkgPath = join12(root, "package.json");
1525
+ if (!existsSync12(pkgPath)) return void 0;
1526
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1527
+ const want = declaredNativeTs(pkg);
1528
+ if (!want) return void 0;
1529
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1530
+ if (have === want) return void 0;
1531
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1532
+ pkg.pnpm ??= {};
1533
+ pkg.pnpm.overrides ??= {};
1534
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1535
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1536
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1537
+ }
1538
+ function ensureReleaseAgeAllowList(root, dryRun) {
1539
+ const yamlPath = join12(root, WORKSPACE_YAML);
1540
+ if (!existsSync12(yamlPath)) return void 0;
1541
+ const own = readOwnPackage().name;
1542
+ const lines = readFileSync10(yamlPath, "utf8").split("\n");
1543
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1544
+ if (keyIdx === -1) return void 0;
1545
+ let lastItemIdx = keyIdx;
1546
+ let indent = " ";
1547
+ for (let i = keyIdx + 1; i < lines.length; i++) {
1548
+ const match = lines[i]?.match(LIST_ITEM);
1549
+ if (!match) break;
1550
+ indent = match[1] ?? indent;
1551
+ lastItemIdx = i;
1552
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1553
+ }
1554
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1555
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1556
+ writeFileSync(yamlPath, lines.join("\n"));
1557
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1558
+ }
1559
+ var PRETTIER_IGNORE = ".prettierignore";
1560
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1561
+ var ROOT_PRETTIER_CONFIGS = [
1562
+ ".prettierrc",
1563
+ ".prettierrc.json",
1564
+ ".prettierrc.json5",
1565
+ ".prettierrc.yml",
1566
+ ".prettierrc.yaml",
1567
+ ".prettierrc.js",
1568
+ ".prettierrc.cjs",
1569
+ ".prettierrc.mjs",
1570
+ "prettier.config.js",
1571
+ "prettier.config.cjs",
1572
+ "prettier.config.mjs"
1573
+ ];
1574
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
1575
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1576
+ if (rel === "" || rel.startsWith("..")) return void 0;
1577
+ const ignorePath = join12(root, PRETTIER_IGNORE);
1578
+ const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
1579
+ if (!hasPrettier) return void 0;
1580
+ const pattern = `/${rel}/`;
1581
+ const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
1582
+ const lines = existing.split("\n");
1583
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1584
+ return void 0;
1585
+ }
1586
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1587
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1588
+ if (headerIdx === -1) {
1589
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1590
+
1591
+ `;
1592
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1593
+ ${pattern}
1594
+ `);
1595
+ } else {
1596
+ let end = headerIdx + 1;
1597
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1598
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1599
+ writeFileSync(
1600
+ ignorePath,
1601
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
953
1602
  );
954
1603
  }
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);
1604
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1605
+ }
1606
+ function ensureWorkspacePrep(opts) {
1607
+ const dryRun = Boolean(opts.dryRun);
1608
+ return [
1609
+ ensureI18nextSingleton(opts.root, dryRun),
1610
+ ensureReleaseAgeAllowList(opts.root, dryRun),
1611
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1612
+ ].filter((message) => message !== void 0);
1613
+ }
1614
+ function inspectWorkspacePrep(root) {
1615
+ const entries = [];
1616
+ let pkg = {};
1617
+ const pkgPath = join12(root, "package.json");
1618
+ if (existsSync12(pkgPath)) {
1619
+ try {
1620
+ pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1621
+ } catch {
1622
+ pkg = {};
1623
+ }
1624
+ }
1625
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1626
+ if (pinned !== void 0) {
1627
+ const stillForks = declaredNativeTs(pkg);
1628
+ entries.push({
1629
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1630
+ 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.`
1631
+ });
1632
+ }
1633
+ const own = readOwnPackage().name;
1634
+ const yamlPath = join12(root, WORKSPACE_YAML);
1635
+ if (existsSync12(yamlPath)) {
1636
+ const yaml = readFileSync10(yamlPath, "utf8");
1637
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1638
+ if (listed) {
1639
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1640
+ entries.push({
1641
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1642
+ 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.`
1643
+ });
1644
+ }
1645
+ }
1646
+ return entries;
1647
+ }
1648
+
1649
+ // src/roles/build/declared-outputs.ts
1650
+ function declaredBuildTarget(cwd) {
1651
+ return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1652
+ }
1653
+ function resolveOutputToken(token, cwd) {
1654
+ if (token.startsWith("{projectRoot}/")) return join13(cwd, token.slice("{projectRoot}/".length));
1655
+ if (token.startsWith("{workspaceRoot}/")) {
1656
+ const root = findWorkspaceRoot(cwd);
1657
+ return root === void 0 ? void 0 : join13(root, token.slice("{workspaceRoot}/".length));
1658
+ }
1659
+ return join13(cwd, token);
1660
+ }
1661
+ function buildStamp(cwd) {
1662
+ const marker = join13(cwd, `.sentinel-build-stamp-${process.pid}`);
1663
+ try {
1664
+ writeFileSync2(marker, "");
1665
+ const stamp = statSync(marker).mtimeMs;
1666
+ rmSync(marker, { force: true });
1667
+ return stamp;
1668
+ } catch {
1669
+ return Date.now();
1670
+ }
1671
+ }
1672
+ function writtenSince(path, startedAt) {
1673
+ if (!existsSync13(path)) return false;
1674
+ try {
1675
+ return statSync(path).mtimeMs >= startedAt;
1676
+ } catch {
1677
+ return true;
1678
+ }
1679
+ }
1680
+ function outputsProblem(cwd, startedAt) {
1681
+ const target = declaredBuildTarget(cwd);
1682
+ if (target === void 0) return void 0;
1683
+ const declared = target.outputs ?? [];
1684
+ if (declared.length === 0) {
1685
+ if (target.cache !== true) return void 0;
1686
+ 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
1687
  }
971
- };
1688
+ const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
1689
+ if (resolved.length === 0) return void 0;
1690
+ if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
1691
+ 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.`;
1692
+ }
972
1693
 
973
1694
  // src/roles/build/prerequisite.ts
974
1695
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1018,7 +1739,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
1018
1739
  );
1019
1740
  return { ok: false, code: prerequisite.code };
1020
1741
  }
1021
- return this.spawnVite(ctx, ready.vite, ["build"], 1);
1742
+ const startedAt = buildStamp(ctx.cwd);
1743
+ const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
1744
+ if (!result.ok) return result;
1745
+ const problem = outputsProblem(ctx.cwd, startedAt);
1746
+ if (problem !== void 0) {
1747
+ this.say(problem);
1748
+ return { ok: false, code: 1 };
1749
+ }
1750
+ return result;
1022
1751
  }
1023
1752
  /**
1024
1753
  * What this module builds with, WITHOUT loading its config.
@@ -1090,12 +1819,12 @@ function registerBuild() {
1090
1819
 
1091
1820
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1092
1821
  import { spawnSync as spawnSync3 } from "child_process";
1093
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1094
- import { join as join14 } from "path";
1822
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1823
+ import { join as join18 } from "path";
1095
1824
 
1096
1825
  // src/core/config/has-source.ts
1097
1826
  import { readdirSync } from "fs";
1098
- import { extname, join as join8, relative } from "path";
1827
+ import { extname, join as join14, relative as relative3 } from "path";
1099
1828
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1100
1829
  "node_modules",
1101
1830
  "dist",
@@ -1146,7 +1875,7 @@ function hasSourceFiles(cwd, extensions) {
1146
1875
  }
1147
1876
  for (const entry of entries) {
1148
1877
  if (entry.isDirectory()) {
1149
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
1878
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join14(dir, entry.name));
1150
1879
  continue;
1151
1880
  }
1152
1881
  if (wanted.has(extname(entry.name))) return true;
@@ -1167,12 +1896,12 @@ function listSourceFiles(cwd, extensions) {
1167
1896
  continue;
1168
1897
  }
1169
1898
  for (const entry of entries) {
1170
- const full = join8(dir, entry.name);
1899
+ const full = join14(dir, entry.name);
1171
1900
  if (entry.isDirectory()) {
1172
1901
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1173
1902
  continue;
1174
1903
  }
1175
- if (wanted.has(extname(entry.name))) found.push(relative(cwd, full));
1904
+ if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1176
1905
  }
1177
1906
  }
1178
1907
  return found;
@@ -1180,207 +1909,9 @@ function listSourceFiles(cwd, extensions) {
1180
1909
 
1181
1910
  // src/core/named-scope.ts
1182
1911
  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
- };
1912
+ function scopeFlags(paths, options = []) {
1913
+ if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1914
+ return [TOLERATE_EMPTY_FLAG];
1384
1915
  }
1385
1916
 
1386
1917
  // src/roles/format/format-script.ts
@@ -1411,8 +1942,8 @@ function writesWhenRewritten(name, command) {
1411
1942
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1412
1943
 
1413
1944
  // src/roles/format/inherited-ignores.ts
1414
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1415
- import { join as join10 } from "path";
1945
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
1946
+ import { join as join15 } from "path";
1416
1947
  var ROOT_IGNORE_FILE = ".prettierignore";
1417
1948
  function isPattern(line) {
1418
1949
  const trimmed = line.trim();
@@ -1427,11 +1958,11 @@ function toModulePattern(pattern) {
1427
1958
  }
1428
1959
  function inheritedIgnorePatterns(workspaceRoot) {
1429
1960
  if (!workspaceRoot) return [];
1430
- const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1431
- if (!existsSync9(path)) return [];
1961
+ const path = join15(workspaceRoot, ROOT_IGNORE_FILE);
1962
+ if (!existsSync14(path)) return [];
1432
1963
  let contents;
1433
1964
  try {
1434
- contents = readFileSync7(path, "utf8");
1965
+ contents = readFileSync11(path, "utf8");
1435
1966
  } catch {
1436
1967
  return [];
1437
1968
  }
@@ -1453,78 +1984,36 @@ function unparseableFiles(output) {
1453
1984
  const location = LOCATION.exec(raw);
1454
1985
  if (location && pending) {
1455
1986
  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);
1987
+ file: location[1] ?? "",
1988
+ line: Number(location[2] ?? 0),
1989
+ message: pending
1990
+ });
1991
+ pending = void 0;
1992
+ continue;
1993
+ }
1994
+ const message = MESSAGE.exec(raw);
1995
+ if (message) pending = message[1];
1996
+ }
1997
+ return diagnostics;
1506
1998
  }
1507
- function formatPresetName(declared) {
1508
- if (declared === "svelte") return "svelte";
1509
- if (declared === "nest") return "nest";
1510
- return "base";
1999
+ function oxfmtSaid(output) {
2000
+ const lines = plain(output).split("\n");
2001
+ const diagnostic = lines.find((line) => MESSAGE.test(line));
2002
+ if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
2003
+ return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
1511
2004
  }
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;
2005
+ function needsSvelteCompiler(output) {
2006
+ return /svelte\/compiler/.test(output);
1518
2007
  }
1519
2008
 
1520
2009
  // src/roles/format/prettier-config.ts
1521
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1522
- import { join as join12 } from "path";
2010
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2011
+ import { join as join17 } from "path";
1523
2012
 
1524
2013
  // src/roles/format/resolve-oxfmt.ts
1525
- import { readFileSync as readFileSync8 } from "fs";
2014
+ import { readFileSync as readFileSync12 } from "fs";
1526
2015
  import { createRequire as createRequire2 } from "module";
1527
- import { dirname as dirname4, join as join11 } from "path";
2016
+ import { dirname as dirname4, join as join16 } from "path";
1528
2017
  function resolveOxfmt(cwd) {
1529
2018
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1530
2019
  }
@@ -1553,8 +2042,8 @@ function configSchema() {
1553
2042
  function readConfigSchema() {
1554
2043
  try {
1555
2044
  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"));
2045
+ const schemaPath = join16(dirname4(manifest), "configuration_schema.json");
2046
+ return JSON.parse(readFileSync12(schemaPath, "utf8"));
1558
2047
  } catch {
1559
2048
  return void 0;
1560
2049
  }
@@ -1585,14 +2074,14 @@ function toOxfmtOverrides(value) {
1585
2074
  return { overrides, unresolved };
1586
2075
  }
1587
2076
  function readPrettierSettings(cwd) {
1588
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
2077
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync15(join17(cwd, name)));
1589
2078
  if (!file) return { options: {}, unresolved: [] };
1590
2079
  if (/\.(js|cjs|mjs)$/.test(file)) {
1591
2080
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1592
2081
  }
1593
2082
  let parsed;
1594
2083
  try {
1595
- parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
2084
+ parsed = parseJsonc(readFileSync13(join17(cwd, file), "utf8"), file);
1596
2085
  } catch {
1597
2086
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1598
2087
  }
@@ -1621,71 +2110,6 @@ function readPrettierSettings(cwd) {
1621
2110
  return { options, file, unresolved };
1622
2111
  }
1623
2112
 
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
2113
  // src/roles/format/unenforced.ts
1690
2114
  function unenforcedFormatOptions(options) {
1691
2115
  const gaps = [];
@@ -1768,7 +2192,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1768
2192
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1769
2193
  ];
1770
2194
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1771
- (name) => existsSync12(join14(context.cwd, name))
2195
+ (name) => existsSync16(join18(context.cwd, name))
1772
2196
  );
1773
2197
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1774
2198
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1892,7 +2316,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1892
2316
  });
1893
2317
  const result = spawnSync3(
1894
2318
  oxfmt,
1895
- [mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
2319
+ [mode, ...scopeFlags(paths, options), ...options, ...paths.length > 0 ? paths : ["."]],
1896
2320
  {
1897
2321
  cwd: ctx.cwd,
1898
2322
  stdio: "inherit"
@@ -2008,7 +2432,7 @@ var OxfmtAdapter = class extends BaseAdapter {
2008
2432
  modulePrettierDependencies(cwd) {
2009
2433
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
2010
2434
  try {
2011
- const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
2435
+ const manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
2012
2436
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
2013
2437
  } catch {
2014
2438
  return [];
@@ -2090,8 +2514,8 @@ function registerFormat() {
2090
2514
 
2091
2515
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2092
2516
  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";
2517
+ import { existsSync as existsSync24, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2518
+ import { join as join28 } from "path";
2095
2519
 
2096
2520
  // src/core/config/deferred-rules.ts
2097
2521
  function deferredRuleNames(rules) {
@@ -2099,8 +2523,8 @@ function deferredRuleNames(rules) {
2099
2523
  }
2100
2524
 
2101
2525
  // src/core/fix-report.ts
2102
- import { statSync } from "fs";
2103
- import { join as join15 } from "path";
2526
+ import { statSync as statSync2 } from "fs";
2527
+ import { join as join19 } from "path";
2104
2528
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2105
2529
  function readFailure(output) {
2106
2530
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2111,7 +2535,7 @@ function readFailure(output) {
2111
2535
  }
2112
2536
  function fingerprint(path) {
2113
2537
  try {
2114
- const stat = statSync(path);
2538
+ const stat = statSync2(path);
2115
2539
  return `${stat.size}:${stat.mtimeMs}`;
2116
2540
  } catch {
2117
2541
  return void 0;
@@ -2120,7 +2544,7 @@ function fingerprint(path) {
2120
2544
  function snapshotFiles(cwd, extensions) {
2121
2545
  const snapshot = /* @__PURE__ */ new Map();
2122
2546
  for (const file of listSourceFiles(cwd, extensions)) {
2123
- const mark = fingerprint(join15(cwd, file));
2547
+ const mark = fingerprint(join19(cwd, file));
2124
2548
  if (mark !== void 0) snapshot.set(file, mark);
2125
2549
  }
2126
2550
  return snapshot;
@@ -2128,7 +2552,7 @@ function snapshotFiles(cwd, extensions) {
2128
2552
  function changedSince(cwd, before, extensions) {
2129
2553
  const changed = [];
2130
2554
  for (const file of listSourceFiles(cwd, extensions)) {
2131
- const now = fingerprint(join15(cwd, file));
2555
+ const now = fingerprint(join19(cwd, file));
2132
2556
  if (now === void 0) continue;
2133
2557
  if (before.get(file) !== now) changed.push(file);
2134
2558
  }
@@ -2147,8 +2571,8 @@ function describeFixOutcome(outcome, toolLabel) {
2147
2571
  }
2148
2572
 
2149
2573
  // src/roles/lint/module-baseline.ts
2150
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2151
- import { join as join16 } from "path";
2574
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
2575
+ import { join as join20 } from "path";
2152
2576
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2153
2577
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2154
2578
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2192,11 +2616,11 @@ function describeHolds(holds) {
2192
2616
  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
2617
  }
2194
2618
  function heldRules(cwd) {
2195
- const path = join16(cwd, LINT_BASELINE_FILE);
2196
- if (!existsSync13(path)) return [];
2619
+ const path = join20(cwd, LINT_BASELINE_FILE);
2620
+ if (!existsSync17(path)) return [];
2197
2621
  try {
2198
2622
  const parsed = parseJsonc(
2199
- readFileSync12(path, "utf8"),
2623
+ readFileSync15(path, "utf8"),
2200
2624
  LINT_BASELINE_FILE
2201
2625
  );
2202
2626
  return Object.keys(parsed.rules ?? {});
@@ -2627,7 +3051,8 @@ var react_lib_default = {
2627
3051
  {
2628
3052
  name: "storybook",
2629
3053
  specifier: "eslint-plugin-storybook"
2630
- }
3054
+ },
3055
+ "../plugins/hublo.js"
2631
3056
  ],
2632
3057
  settings: null,
2633
3058
  renames: {
@@ -2636,6 +3061,7 @@ var react_lib_default = {
2636
3061
  rules: {
2637
3062
  curly: ["error", "all"],
2638
3063
  "for-direction": "error",
3064
+ "hublo/no-regex-lookbehind": "error",
2639
3065
  "jest-dom/prefer-checked": "error",
2640
3066
  "jest-dom/prefer-empty": "error",
2641
3067
  "jest-dom/prefer-enabled-disabled": "error",
@@ -2987,7 +3413,7 @@ var react_lib_default = {
2987
3413
  ],
2988
3414
  uncovered: {
2989
3415
  "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.",
3416
+ "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
3417
  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
3418
  }
2993
3419
  };
@@ -3051,6 +3477,7 @@ var react_default = {
3051
3477
  severity: "off",
3052
3478
  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
3479
  },
3480
+ "hublo/no-regex-lookbehind": "error",
3054
3481
  "jest/expect-expect": {
3055
3482
  severity: "off",
3056
3483
  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 +3964,7 @@ var react_default = {
3537
3964
  uncovered: {
3538
3965
  "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
3966
  "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.",
3967
+ "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
3968
  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
3969
  }
3543
3970
  };
@@ -4019,14 +4446,14 @@ function downgradedRulesFor(preset) {
4019
4446
  }
4020
4447
 
4021
4448
  // 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";
4449
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4450
+ import { isAbsolute as isAbsolute2, join as join21, resolve as resolve3 } from "path";
4024
4451
  function ruleCount(cwd, specifier) {
4025
4452
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4026
- if (!existsSync14(path)) return void 0;
4453
+ if (!existsSync18(path)) return void 0;
4027
4454
  try {
4028
4455
  const parsed = parseJsonc(
4029
- readFileSync13(path, "utf8"),
4456
+ readFileSync16(path, "utf8"),
4030
4457
  specifier
4031
4458
  );
4032
4459
  return Object.keys(parsed.rules ?? {}).length;
@@ -4046,7 +4473,7 @@ function extraLayers(cwd, extendsList) {
4046
4473
  function committedExtendsList(cwd, configFile) {
4047
4474
  try {
4048
4475
  const parsed = parseJsonc(
4049
- readFileSync13(join17(cwd, configFile), "utf8"),
4476
+ readFileSync16(join21(cwd, configFile), "utf8"),
4050
4477
  configFile
4051
4478
  );
4052
4479
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4105,8 +4532,8 @@ function errorCountsByConfigRule(stdout) {
4105
4532
  }
4106
4533
 
4107
4534
  // src/core/config/read-adoption.ts
4108
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4109
- import { join as join19 } from "path";
4535
+ import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
4536
+ import { join as join23 } from "path";
4110
4537
 
4111
4538
  // src/core/config/owned-keys.ts
4112
4539
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4121,12 +4548,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4121
4548
  }
4122
4549
 
4123
4550
  // src/core/config/resolve-config-target.ts
4124
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4125
- import { join as join18 } from "path";
4551
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4552
+ import { join as join22 } from "path";
4126
4553
  function readExtends(absolutePath) {
4127
4554
  let parsed;
4128
4555
  try {
4129
- parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
4556
+ parsed = parseJsonc(readFileSync17(absolutePath, "utf8"), absolutePath);
4130
4557
  } catch {
4131
4558
  return [];
4132
4559
  }
@@ -4140,8 +4567,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4140
4567
  let existing;
4141
4568
  let existingExtendsSomething = false;
4142
4569
  for (const candidate of candidates) {
4143
- const absolutePath = join18(moduleDir, candidate);
4144
- if (!existsSync15(absolutePath)) continue;
4570
+ const absolutePath = join22(moduleDir, candidate);
4571
+ if (!existsSync19(absolutePath)) continue;
4145
4572
  const chain = readExtends(absolutePath);
4146
4573
  if (existing === void 0) {
4147
4574
  existing = candidate;
@@ -4174,12 +4601,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4174
4601
  });
4175
4602
  function readAdoption(cwd, options) {
4176
4603
  const target = resolveConfigTarget(cwd, options);
4177
- if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
4604
+ if (target.reason === "none" || !existsSync20(join23(cwd, target.path))) {
4178
4605
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4179
4606
  }
4180
4607
  let parsed;
4181
4608
  try {
4182
- parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
4609
+ parsed = parseJsonc(readFileSync18(join23(cwd, target.path), "utf8"), target.path);
4183
4610
  } catch (error) {
4184
4611
  const reason = error instanceof Error ? error.message : String(error);
4185
4612
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4213,9 +4640,9 @@ function readLintAdoption(cwd) {
4213
4640
  }
4214
4641
 
4215
4642
  // src/roles/lint/resolve-oxlint.ts
4216
- import { existsSync as existsSync17 } from "fs";
4643
+ import { existsSync as existsSync21 } from "fs";
4217
4644
  import { createRequire as createRequire3 } from "module";
4218
- import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
4645
+ import { delimiter as delimiter2, dirname as dirname5, join as join24 } from "path";
4219
4646
  import { fileURLToPath as fileURLToPath2 } from "url";
4220
4647
  var PACKAGE_OF = {
4221
4648
  oxlint: "oxlint",
@@ -4240,30 +4667,30 @@ function tsgolintShim(cwd) {
4240
4667
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4241
4668
  try {
4242
4669
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4243
- candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4244
- candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
4670
+ candidates.push(join24(packageDir, "node_modules", ".bin", "tsgolint"));
4671
+ candidates.push(join24(packageDir, "..", ".bin", "tsgolint"));
4245
4672
  } catch {
4246
4673
  }
4247
4674
  }
4248
4675
  try {
4249
4676
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4250
- candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
4677
+ candidates.push(join24(ownRoot, "node_modules", ".bin", "tsgolint"));
4251
4678
  } catch {
4252
4679
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4253
4680
  }
4254
- return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
4681
+ return candidates.find((candidate) => candidate !== "" && existsSync21(candidate));
4255
4682
  }
4256
4683
  function oxlintSearchPath(cwd) {
4257
4684
  return binSearchPath(cwd);
4258
4685
  }
4259
4686
 
4260
4687
  // src/roles/lint/adapters/oxlint/plan.ts
4261
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4262
- import { join as join23 } from "path";
4688
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
4689
+ import { join as join27 } from "path";
4263
4690
 
4264
4691
  // src/roles/lint/eslint-ignores.ts
4265
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4266
- import { join as join21 } from "path";
4692
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
4693
+ import { join as join25 } from "path";
4267
4694
  import { parseSync as parseSync2 } from "oxc-parser";
4268
4695
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4269
4696
  function toOxlintPattern(pattern) {
@@ -4275,11 +4702,11 @@ function readRootEslintIgnores(root) {
4275
4702
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4276
4703
  }
4277
4704
  function readEslintIgnores(cwd) {
4278
- const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
4705
+ const config = ESLINT_CONFIG_FILES.map((name) => join25(cwd, name)).find((path) => existsSync22(path));
4279
4706
  if (!config) return { patterns: [], unresolved: [] };
4280
4707
  let source;
4281
4708
  try {
4282
- source = readFileSync16(config, "utf8");
4709
+ source = readFileSync19(config, "utf8");
4283
4710
  } catch {
4284
4711
  return { patterns: [], unresolved: [] };
4285
4712
  }
@@ -4380,8 +4807,8 @@ function lintPresetFor(preset) {
4380
4807
  }
4381
4808
 
4382
4809
  // 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";
4810
+ import { readdirSync as readdirSync2, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
4811
+ import { join as join26, relative as relative4 } from "path";
4385
4812
  var SOURCE_EXTENSIONS = [
4386
4813
  ".ts",
4387
4814
  ".tsx",
@@ -4428,10 +4855,10 @@ function* sourceFiles(dir) {
4428
4855
  return;
4429
4856
  }
4430
4857
  for (const entry of entries) {
4431
- const full = join22(dir, entry);
4858
+ const full = join26(dir, entry);
4432
4859
  let isDirectory;
4433
4860
  try {
4434
- isDirectory = statSync2(full).isDirectory();
4861
+ isDirectory = statSync3(full).isDirectory();
4435
4862
  } catch {
4436
4863
  continue;
4437
4864
  }
@@ -4448,7 +4875,7 @@ function findSuppressionRenames(cwd, renames) {
4448
4875
  for (const file of sourceFiles(cwd)) {
4449
4876
  let content;
4450
4877
  try {
4451
- content = readFileSync17(file, "utf8");
4878
+ content = readFileSync20(file, "utf8");
4452
4879
  } catch {
4453
4880
  continue;
4454
4881
  }
@@ -4472,7 +4899,7 @@ function findSuppressionRenames(cwd, renames) {
4472
4899
  }
4473
4900
  if (hits.length === 0) return;
4474
4901
  found.push({
4475
- file: relative3(cwd, file),
4902
+ file: relative4(cwd, file),
4476
4903
  line: index + 1,
4477
4904
  from: line,
4478
4905
  to: line.replace(rules, renamed.join(", ")),
@@ -4576,7 +5003,7 @@ function plan2(context) {
4576
5003
  keys: removableDeps
4577
5004
  });
4578
5005
  }
4579
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
5006
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync23(join27(context.cwd, name)));
4580
5007
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4581
5008
  operations.push(
4582
5009
  ...nxTargetOperations({
@@ -4674,7 +5101,7 @@ function lintScripts(cwd) {
4674
5101
  function committedExtends(cwd) {
4675
5102
  try {
4676
5103
  const parsed = parseJsonc(
4677
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
5104
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4678
5105
  LINT_CONFIG_FILE
4679
5106
  );
4680
5107
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4685,7 +5112,7 @@ function committedExtends(cwd) {
4685
5112
  function committedIgnorePatterns(cwd) {
4686
5113
  try {
4687
5114
  const parsed = parseJsonc(
4688
- readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
5115
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4689
5116
  LINT_CONFIG_FILE
4690
5117
  );
4691
5118
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4697,7 +5124,7 @@ function moduleEslintDependencies(cwd) {
4697
5124
  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
5125
  let manifest;
4699
5126
  try {
4700
- manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
5127
+ manifest = JSON.parse(readFileSync21(join27(cwd, "package.json"), "utf8"));
4701
5128
  } catch {
4702
5129
  return [];
4703
5130
  }
@@ -4831,23 +5258,23 @@ ${result.stderr ?? ""}`);
4831
5258
  * build the developer can see, rather than a silent half-adoption they cannot.
4832
5259
  */
4833
5260
  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;
5261
+ const configPath = join28(ctx.cwd, LINT_CONFIG_FILE);
5262
+ const baselinePath = join28(ctx.cwd, LINT_BASELINE_FILE);
5263
+ if (!existsSync24(configPath)) return;
4837
5264
  let config;
4838
5265
  try {
4839
5266
  config = parseJsonc(
4840
- readFileSync19(configPath, "utf8"),
5267
+ readFileSync22(configPath, "utf8"),
4841
5268
  LINT_CONFIG_FILE
4842
5269
  );
4843
5270
  } catch {
4844
5271
  return;
4845
5272
  }
4846
5273
  const current = Array.isArray(config.extends) ? config.extends : [];
4847
- const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
5274
+ const measurePath = join28(ctx.cwd, LINT_MEASURE_FILE);
4848
5275
  let measured;
4849
5276
  try {
4850
- writeFileSync2(
5277
+ writeFileSync3(
4851
5278
  measurePath,
4852
5279
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
4853
5280
  `
@@ -4865,7 +5292,7 @@ ${result.stderr ?? ""}`);
4865
5292
  { cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
4866
5293
  );
4867
5294
  } finally {
4868
- rmSync(measurePath, { force: true });
5295
+ rmSync2(measurePath, { force: true });
4869
5296
  }
4870
5297
  if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
4871
5298
  process.stderr.write(
@@ -4879,20 +5306,20 @@ ${result.stderr ?? ""}`);
4879
5306
  const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
4880
5307
  const next = extendsWithBaseline(current, holds.length > 0);
4881
5308
  if (holds.length > 0) {
4882
- writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
5309
+ writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
4883
5310
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
4884
5311
  `);
4885
5312
  } else {
4886
- rmSync(baselinePath, { force: true });
5313
+ rmSync2(baselinePath, { force: true });
4887
5314
  }
4888
5315
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
4889
5316
  config.extends = next;
4890
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
5317
+ writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
4891
5318
  `);
4892
5319
  }
4893
5320
  }
4894
5321
  async run(ctx) {
4895
- if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
5322
+ if (!existsSync24(join28(ctx.cwd, LINT_CONFIG_FILE))) {
4896
5323
  process.stderr.write(
4897
5324
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
4898
5325
  `
@@ -4937,7 +5364,7 @@ ${result.stderr ?? ""}`);
4937
5364
  valueFlags: OXLINT_VALUE_FLAGS
4938
5365
  });
4939
5366
  const targets = paths.length > 0 ? paths : ["."];
4940
- const emptyScope = scopeFlags(paths);
5367
+ const emptyScope = scopeFlags(paths, passedOptions);
4941
5368
  const lint = (extra) => {
4942
5369
  const passed = [...extra, ...emptyScope, ...passedOptions];
4943
5370
  const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
@@ -5040,7 +5467,7 @@ ${result.stderr ?? ""}`);
5040
5467
  let stub;
5041
5468
  try {
5042
5469
  stub = parseJsonc(
5043
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5470
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5044
5471
  LINT_CONFIG_FILE
5045
5472
  );
5046
5473
  } catch {
@@ -5050,7 +5477,7 @@ ${result.stderr ?? ""}`);
5050
5477
  for (const entry of stub.extends ?? []) {
5051
5478
  try {
5052
5479
  const preset = parseJsonc(
5053
- readFileSync19(join24(cwd, entry), "utf8"),
5480
+ readFileSync22(join28(cwd, entry), "utf8"),
5054
5481
  entry
5055
5482
  );
5056
5483
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5120,14 +5547,14 @@ ${result.stderr ?? ""}`);
5120
5547
  let parsed;
5121
5548
  try {
5122
5549
  parsed = parseJsonc(
5123
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5550
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5124
5551
  LINT_CONFIG_FILE
5125
5552
  );
5126
5553
  } catch {
5127
5554
  return void 0;
5128
5555
  }
5129
5556
  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)));
5557
+ return targets.find((target) => !existsSync24(join28(cwd, target)));
5131
5558
  }
5132
5559
  /** Announce what is not enforced, so reduced coverage is never silent. */
5133
5560
  announceDisabled(preset) {
@@ -5157,11 +5584,355 @@ function registerLint() {
5157
5584
  setDefaultRunner("lint", "oxlint");
5158
5585
  }
5159
5586
 
5160
- // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5587
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5161
5588
  import { spawnSync as spawnSync5 } from "child_process";
5162
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
5589
+ import { existsSync as existsSync26 } from "fs";
5590
+ import { join as join30 } from "path";
5591
+
5592
+ // src/roles/test/presets/toolchain.json
5593
+ var toolchain_default2 = {
5594
+ $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.",
5595
+ owned: [
5596
+ {
5597
+ package: "vitest",
5598
+ specifier: "vitest/config",
5599
+ named: ["defineConfig", "mergeConfig"],
5600
+ types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
5601
+ 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."
5602
+ },
5603
+ {
5604
+ package: "vite",
5605
+ specifier: "vite",
5606
+ named: ["loadEnv"],
5607
+ 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."
5608
+ }
5609
+ ]
5610
+ };
5611
+
5612
+ // src/roles/test/preset-data.ts
5613
+ var OWNED_TEST_PACKAGES = toolchain_default2.owned;
5614
+
5615
+ // src/roles/test/config-policy.ts
5616
+ var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
5617
+ var TEST_CONFIG_FILES = [
5618
+ "vitest.config.ts",
5619
+ "vitest.config.mts",
5620
+ "vitest.config.js",
5621
+ "vitest.config.mjs"
5622
+ ];
5623
+ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
5624
+ var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
5625
+ (entry) => entry.package !== "vite"
5626
+ ).map((entry) => entry.package);
5627
+ var TEST_TOOLCHAIN = {
5628
+ presetSpecifier: TEST_PRESET_SPECIFIER,
5629
+ owned: TEST_OWNED_PACKAGES
5630
+ };
5631
+ var TEST_SCRIPT_NAME = "test";
5632
+ var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
5633
+ var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
5634
+ function rewriteVitestScript(command) {
5635
+ return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
5636
+ const args = tail.trim().split(/\s+/).filter(Boolean);
5637
+ const oneShot = args[0] === "run" || args.includes("--run");
5638
+ const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
5639
+ const passthrough = oneShot ? rest : ["--watch", ...rest];
5640
+ const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
5641
+ const trailing = /\s$/.test(tail) ? " " : "";
5642
+ return `${lead}sentinel --run --test${suffix}${trailing}`;
5643
+ });
5644
+ }
5645
+ function testTarget(options) {
5646
+ const { configFiles, outputs } = options;
5647
+ const cached = outputs !== void 0 && outputs.length > 0;
5648
+ return {
5649
+ [TEST_SCRIPT_NAME]: {
5650
+ cache: cached,
5651
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
5652
+ ...cached ? { outputs: [...outputs] } : {}
5653
+ }
5654
+ };
5655
+ }
5656
+
5657
+ // src/roles/test/read-adoption.ts
5658
+ import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
5659
+ import { join as join29 } from "path";
5660
+ var JEST_CONFIG_FILES = [
5661
+ "jest.config.ts",
5662
+ "jest.config.js",
5663
+ "jest.prisma.config.ts",
5664
+ "jest.functional.config.ts",
5665
+ "jest.integration-config.ts"
5666
+ ];
5667
+ function testConfigFile(cwd) {
5668
+ return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
5669
+ }
5670
+ function jestConfigFiles(cwd) {
5671
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
5672
+ }
5673
+ function rootJestConfigFiles(cwd) {
5674
+ const root = findWorkspaceRoot(cwd);
5675
+ if (root === void 0 || root === cwd) return [];
5676
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
5677
+ }
5678
+ var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5679
+ var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
5680
+ function hasTestFiles(cwd) {
5681
+ const stack = [cwd];
5682
+ while (stack.length > 0) {
5683
+ const dir = stack.pop();
5684
+ let entries;
5685
+ try {
5686
+ entries = readdirSync3(dir, { withFileTypes: true });
5687
+ } catch {
5688
+ continue;
5689
+ }
5690
+ for (const entry of entries) {
5691
+ if (entry.isDirectory()) {
5692
+ if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
5693
+ } else if (TEST_FILE.test(entry.name)) {
5694
+ return true;
5695
+ }
5696
+ }
5697
+ }
5698
+ return false;
5699
+ }
5700
+ function readTestConfigSource(cwd, fileName) {
5701
+ try {
5702
+ return readFileSync23(join29(cwd, fileName), "utf8");
5703
+ } catch {
5704
+ return "";
5705
+ }
5706
+ }
5707
+ function importsPreset2(source) {
5708
+ const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5709
+ return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
5710
+ }
5711
+ function readTestAdoption(cwd) {
5712
+ const manifest = readProjectPackageJson(cwd);
5713
+ const jestConfigs = jestConfigFiles(cwd);
5714
+ const configFile = testConfigFile(cwd);
5715
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
5716
+ const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
5717
+ if (configFile === void 0) {
5718
+ const hasTests = hasTestFiles(cwd);
5719
+ const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
5720
+ return {
5721
+ configFile: null,
5722
+ preset: null,
5723
+ adopted: false,
5724
+ conformant: false,
5725
+ drift: [],
5726
+ unreadable: null,
5727
+ state: hasTests ? "jest" : "no-tests",
5728
+ runner: hasTests ? "jest" : "none",
5729
+ jestConfigs,
5730
+ inheritsRootConfig,
5731
+ ownDeclarations
5732
+ };
5733
+ }
5734
+ const source = readTestConfigSource(cwd, configFile);
5735
+ if (source === "") {
5736
+ return {
5737
+ configFile,
5738
+ preset: null,
5739
+ adopted: false,
5740
+ conformant: false,
5741
+ drift: [],
5742
+ unreadable: `${configFile} could not be read`,
5743
+ state: "vitest-own-toolchain",
5744
+ runner: "vitest",
5745
+ jestConfigs,
5746
+ inheritsRootConfig: false,
5747
+ // it has a config of its own, readable or not
5748
+ ownDeclarations
5749
+ };
5750
+ }
5751
+ const adopted = importsPreset2(source);
5752
+ const script = moduleScripts(cwd)["test"];
5753
+ return {
5754
+ configFile,
5755
+ preset: adopted ? "react" : null,
5756
+ adopted,
5757
+ conformant: adopted && script !== void 0 && jestConfigs.length === 0,
5758
+ drift: [],
5759
+ unreadable: null,
5760
+ state: adopted ? "adopted" : "vitest-own-toolchain",
5761
+ runner: "vitest",
5762
+ jestConfigs,
5763
+ inheritsRootConfig: false,
5764
+ // local always wins: this module is out of the root's reach
5765
+ ownDeclarations
5766
+ };
5767
+ }
5768
+
5769
+ // src/roles/test/plan.ts
5770
+ function ownedTestDependencies(cwd, declared) {
5771
+ return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
5772
+ ["dependencies", entry.name],
5773
+ ["devDependencies", entry.name]
5774
+ ]).filter(([section]) => section !== void 0);
5775
+ }
5776
+ function plan3(context) {
5777
+ const configFile = testConfigFile(context.cwd);
5778
+ const adoption = readTestAdoption(context.cwd);
5779
+ if (configFile === void 0) {
5780
+ return {
5781
+ operations: [],
5782
+ 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.`
5783
+ };
5784
+ }
5785
+ const operations = [];
5786
+ const notes = [];
5787
+ const rewrite = rewriteToolchainImports(
5788
+ TEST_TOOLCHAIN,
5789
+ readTestConfigSource(context.cwd, configFile),
5790
+ configFile
5791
+ );
5792
+ if (rewrite.kind === "blocked") {
5793
+ return {
5794
+ operations: [],
5795
+ 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.`
5796
+ };
5797
+ }
5798
+ if (rewrite.kind === "rewritten") {
5799
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
5800
+ notes.push(
5801
+ `${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.`
5802
+ );
5803
+ }
5804
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
5805
+ const outputs = existing?.outputs ?? [];
5806
+ if (outputs.length === 0) {
5807
+ notes.push(
5808
+ `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.`
5809
+ );
5810
+ }
5811
+ const siblingScripts = {};
5812
+ for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
5813
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
5814
+ const rewritten = rewriteVitestScript(command);
5815
+ if (rewritten !== command) siblingScripts[name] = rewritten;
5816
+ }
5817
+ if (Object.keys(siblingScripts).length > 0) {
5818
+ notes.push(
5819
+ `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\`.`
5820
+ );
5821
+ }
5822
+ operations.push(
5823
+ manifestOperation(context.cwd, {
5824
+ [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
5825
+ ...siblingScripts
5826
+ })
5827
+ );
5828
+ operations.push(
5829
+ ...nxTargetOperations({
5830
+ cwd: context.cwd,
5831
+ targets: testTarget({ configFiles: [configFile], outputs })
5832
+ })
5833
+ );
5834
+ const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
5835
+ if (owned.length > 0) {
5836
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
5837
+ notes.push(
5838
+ `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.`
5839
+ );
5840
+ }
5841
+ if (adoption.jestConfigs.length > 0) {
5842
+ notes.push(
5843
+ `${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.`
5844
+ );
5845
+ }
5846
+ return { operations, notes };
5847
+ }
5848
+
5849
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5850
+ function resolveVitest(cwd) {
5851
+ const fromSentinel = join30(
5852
+ new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
5853
+ );
5854
+ if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
5855
+ const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
5856
+ return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
5857
+ }
5858
+ var VitestAdapter = class extends BaseAdapter {
5859
+ target = "test";
5860
+ runner = "vitest";
5861
+ appliesTo(_preset) {
5862
+ return true;
5863
+ }
5864
+ plan(context) {
5865
+ return plan3(context);
5866
+ }
5867
+ async status(ctx) {
5868
+ return Promise.resolve(readTestAdoption(ctx.cwd));
5869
+ }
5870
+ async inspect(ctx) {
5871
+ const adoption = readTestAdoption(ctx.cwd);
5872
+ return Promise.resolve({
5873
+ state: adoption.state,
5874
+ runner: adoption.runner,
5875
+ configFile: adoption.configFile,
5876
+ jestConfigs: adoption.jestConfigs,
5877
+ adopted: adoption.adopted,
5878
+ conformant: adoption.conformant,
5879
+ drift: adoption.drift,
5880
+ unreadable: adoption.unreadable
5881
+ });
5882
+ }
5883
+ /**
5884
+ * Run the module's tests.
5885
+ *
5886
+ * A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
5887
+ * with no named target sweeps the workspace, so failing here would exit non-zero on the 107
5888
+ * modules still on jest, which says nothing anyone can act on.
5889
+ */
5890
+ async run(ctx) {
5891
+ const adoption = readTestAdoption(ctx.cwd);
5892
+ if (!adoption.adopted) {
5893
+ this.say(
5894
+ 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.`
5895
+ );
5896
+ return Promise.resolve({ ok: true, code: 0 });
5897
+ }
5898
+ const { bin, origin } = resolveVitest(ctx.cwd);
5899
+ if (bin === void 0) {
5900
+ this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
5901
+ return Promise.resolve({ ok: false, code: 1 });
5902
+ }
5903
+ if (origin === "module") {
5904
+ this.say(
5905
+ `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.`
5906
+ );
5907
+ }
5908
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
5909
+ const watching = options.includes("--watch") || options.includes("-w");
5910
+ const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
5911
+ const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
5912
+ if (result.error) {
5913
+ this.say(`could not run vitest (${result.error.message})`);
5914
+ return Promise.resolve({ ok: false, code: 1 });
5915
+ }
5916
+ const code = result.status ?? 1;
5917
+ return Promise.resolve({ ok: code === 0, code });
5918
+ }
5919
+ say(message) {
5920
+ process.stderr.write(`sentinel test(vitest): ${message}
5921
+ `);
5922
+ }
5923
+ };
5924
+
5925
+ // src/roles/test/register.ts
5926
+ function registerTest() {
5927
+ register(new VitestAdapter());
5928
+ setDefaultRunner("test", "vitest");
5929
+ }
5930
+
5931
+ // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5932
+ import { spawnSync as spawnSync6 } from "child_process";
5933
+ import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
5163
5934
  import { createRequire as createRequire4 } from "module";
5164
- import { join as join26 } from "path";
5935
+ import { join as join32 } from "path";
5165
5936
 
5166
5937
  // src/roles/typescript/presets/nest.json
5167
5938
  var nest_default2 = {
@@ -5342,8 +6113,8 @@ function readTsconfigAdoption(cwd) {
5342
6113
  }
5343
6114
 
5344
6115
  // src/roles/typescript/adapters/tsc/plan.ts
5345
- import { readFileSync as readFileSync20 } from "fs";
5346
- import { join as join25 } from "path";
6116
+ import { readFileSync as readFileSync24 } from "fs";
6117
+ import { join as join31 } from "path";
5347
6118
 
5348
6119
  // src/roles/typescript/typecheck-script.ts
5349
6120
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5436,7 +6207,7 @@ function planAdoption(context) {
5436
6207
  };
5437
6208
  }
5438
6209
  const existing = parseJsonc(
5439
- readFileSync20(join25(context.cwd, target.path), "utf8"),
6210
+ readFileSync24(join31(context.cwd, target.path), "utf8"),
5440
6211
  target.path
5441
6212
  );
5442
6213
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5574,7 +6345,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5574
6345
  let chain;
5575
6346
  try {
5576
6347
  const parsed = parseJsonc(
5577
- readFileSync21(join26(cwd, target.path), "utf8"),
6348
+ readFileSync25(join32(cwd, target.path), "utf8"),
5578
6349
  target.path
5579
6350
  );
5580
6351
  chain = parsed.extends;
@@ -5587,7 +6358,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5587
6358
  );
5588
6359
  if (preset === void 0) return void 0;
5589
6360
  try {
5590
- createRequire4(join26(cwd, "noop.js")).resolve(preset);
6361
+ createRequire4(join32(cwd, "noop.js")).resolve(preset);
5591
6362
  return void 0;
5592
6363
  } catch {
5593
6364
  return preset;
@@ -5628,7 +6399,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5628
6399
  return { ok: false, code: 1 };
5629
6400
  }
5630
6401
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
5631
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
6402
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5632
6403
  if (result.error) {
5633
6404
  process.stderr.write(
5634
6405
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5663,7 +6434,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5663
6434
  let worst = 0;
5664
6435
  for (const project of this.referencedProjects(ctx.cwd, config)) {
5665
6436
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
5666
- const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
6437
+ const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5667
6438
  if (result.error) {
5668
6439
  process.stderr.write(
5669
6440
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5683,7 +6454,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5683
6454
  referencedProjects(cwd, config) {
5684
6455
  try {
5685
6456
  const parsed = parseJsonc(
5686
- readFileSync21(join26(cwd, config), "utf8"),
6457
+ readFileSync25(join32(cwd, config), "utf8"),
5687
6458
  config
5688
6459
  );
5689
6460
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5699,7 +6470,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5699
6470
  * check.
5700
6471
  */
5701
6472
  typecheckTarget(cwd) {
5702
- if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
6473
+ if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
5703
6474
  const target = resolveTsconfigTarget(cwd);
5704
6475
  return target.reason === "none" ? null : target.path;
5705
6476
  }
@@ -5752,7 +6523,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5752
6523
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
5753
6524
  }
5754
6525
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
5755
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
6526
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5756
6527
  if (result.error) {
5757
6528
  process.stderr.write(
5758
6529
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5795,7 +6566,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5795
6566
  * errors in the output mean the rule must be on.
5796
6567
  */
5797
6568
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
5798
- const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
6569
+ const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5799
6570
  if (shown.status === 0 && shown.stdout) {
5800
6571
  try {
5801
6572
  const co = parseJsonc(
@@ -5822,6 +6593,7 @@ function registerAdapters() {
5822
6593
  registerLint();
5823
6594
  registerFormat();
5824
6595
  registerBuild();
6596
+ registerTest();
5825
6597
  }
5826
6598
 
5827
6599
  // src/core/detect-framework.ts
@@ -5903,7 +6675,7 @@ function replaceLines(current, replacements) {
5903
6675
  }
5904
6676
 
5905
6677
  // src/core/apply-plan.ts
5906
- import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6678
+ import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5907
6679
  import { resolve as resolve4, sep } from "path";
5908
6680
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
5909
6681
 
@@ -5922,7 +6694,7 @@ function resolveWithinRoot(cwd, relativePath) {
5922
6694
  return absolutePath;
5923
6695
  }
5924
6696
  function readIfExists(absolutePath) {
5925
- return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
6697
+ return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
5926
6698
  }
5927
6699
  function* leaves(value, prefix = []) {
5928
6700
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6003,9 +6775,9 @@ function applyOperationTo(current, operation) {
6003
6775
  }
6004
6776
  }
6005
6777
  }
6006
- function preparePlan(cwd, plan3) {
6778
+ function preparePlan(cwd, plan4) {
6007
6779
  const prepared = /* @__PURE__ */ new Map();
6008
- for (const operation of plan3.operations) {
6780
+ for (const operation of plan4.operations) {
6009
6781
  const absolutePath = resolveWithinRoot(cwd, operation.path);
6010
6782
  const existing = prepared.get(operation.path);
6011
6783
  const before = existing?.before ?? readIfExists(absolutePath) ?? "";
@@ -6024,14 +6796,14 @@ function preparePlan(cwd, plan3) {
6024
6796
  }
6025
6797
  function writeFileAtomic(absolutePath, contents) {
6026
6798
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
6027
- writeFileSync3(tempPath, contents);
6799
+ writeFileSync4(tempPath, contents);
6028
6800
  renameSync(tempPath, absolutePath);
6029
6801
  }
6030
- function applyPlan(cwd, plan3) {
6031
- const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
6802
+ function applyPlan(cwd, plan4) {
6803
+ const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
6032
6804
  for (const file of changed) {
6033
6805
  if (file.deleted) {
6034
- rmSync2(file.absolutePath, { force: true });
6806
+ rmSync3(file.absolutePath, { force: true });
6035
6807
  continue;
6036
6808
  }
6037
6809
  writeFileAtomic(file.absolutePath, file.after);
@@ -6040,8 +6812,8 @@ function applyPlan(cwd, plan3) {
6040
6812
  }
6041
6813
 
6042
6814
  // 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";
6815
+ import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
6816
+ import { join as join33 } from "path";
6045
6817
  var PATH_SIGNALS = [
6046
6818
  {
6047
6819
  preset: "nest",
@@ -6055,11 +6827,11 @@ var DEPENDENCY_SIGNALS = [
6055
6827
  { preset: "nest", pattern: /^@nestjs\// }
6056
6828
  ];
6057
6829
  function dependencyNames(cwd) {
6058
- const path = join27(cwd, "package.json");
6059
- if (!existsSync23(path)) return [];
6830
+ const path = join33(cwd, "package.json");
6831
+ if (!existsSync29(path)) return [];
6060
6832
  try {
6061
6833
  const manifest = parseJsonc(
6062
- readFileSync23(path, "utf8"),
6834
+ readFileSync27(path, "utf8"),
6063
6835
  path
6064
6836
  );
6065
6837
  return [
@@ -6073,7 +6845,7 @@ function dependencyNames(cwd) {
6073
6845
  function declaresJsx(cwd) {
6074
6846
  let entries;
6075
6847
  try {
6076
- entries = readdirSync3(cwd).filter(
6848
+ entries = readdirSync4(cwd).filter(
6077
6849
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
6078
6850
  );
6079
6851
  } catch {
@@ -6082,7 +6854,7 @@ function declaresJsx(cwd) {
6082
6854
  for (const name of entries) {
6083
6855
  try {
6084
6856
  const config = parseJsonc(
6085
- readFileSync23(join27(cwd, name), "utf8"),
6857
+ readFileSync27(join33(cwd, name), "utf8"),
6086
6858
  name
6087
6859
  );
6088
6860
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6133,14 +6905,14 @@ function resolveFlavour(opts) {
6133
6905
  }
6134
6906
  return detection.preset;
6135
6907
  }
6136
- function previewPlan(opts, plan3) {
6137
- const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
6908
+ function previewPlan(opts, plan4) {
6909
+ const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
6138
6910
  if (opts.json) {
6139
6911
  process.stdout.write(
6140
6912
  JSON.stringify(
6141
6913
  {
6142
6914
  dryRun: true,
6143
- notes: plan3.notes ?? [],
6915
+ notes: plan4.notes ?? [],
6144
6916
  files: changed.map(({ path, before, after, deleted }) => ({
6145
6917
  path,
6146
6918
  action: deleted ? "delete" : before.length === 0 ? "create" : "update",
@@ -6155,7 +6927,7 @@ function previewPlan(opts, plan3) {
6155
6927
  return 0;
6156
6928
  }
6157
6929
  process.stderr.write(" dry run: no files written\n");
6158
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6930
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6159
6931
  `);
6160
6932
  if (changed.length === 0) {
6161
6933
  process.stderr.write(" nothing to change\n");
@@ -6199,25 +6971,25 @@ async function dispatch(opts) {
6199
6971
  }
6200
6972
  }
6201
6973
  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}
6974
+ const plan4 = await adapter.plan(context);
6975
+ if (plan4.blocked) {
6976
+ process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
6205
6977
  `);
6206
6978
  return 1;
6207
6979
  }
6208
- if (plan3.skipped) {
6209
- process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan3.skipped}`)}
6980
+ if (plan4.skipped) {
6981
+ process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
6210
6982
  `);
6211
6983
  return 0;
6212
6984
  }
6213
6985
  if (opts.dryRun) {
6214
- return previewPlan(opts, plan3);
6986
+ return previewPlan(opts, plan4);
6215
6987
  }
6216
- for (const change of applyPlan(opts.cwd, plan3)) {
6988
+ for (const change of applyPlan(opts.cwd, plan4)) {
6217
6989
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
6218
6990
  `);
6219
6991
  }
6220
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6992
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6221
6993
  `);
6222
6994
  if (adapter.afterInit) {
6223
6995
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
@@ -6241,24 +7013,25 @@ export {
6241
7013
  declaredPresetFor,
6242
7014
  availableTargets,
6243
7015
  resolve,
6244
- BaseAdapter,
6245
- resolveBin,
6246
7016
  readOwnVersion,
6247
7017
  readProjectPackageJson,
6248
7018
  moduleName,
7019
+ buildConfigFile,
7020
+ BaseAdapter,
7021
+ resolveBin,
7022
+ WORKSPACE_ROOT_MARKER,
7023
+ findWorkspaceRoot,
7024
+ ensureWorkspacePrep,
7025
+ inspectWorkspacePrep,
6249
7026
  VERBS,
6250
7027
  TARGETS,
6251
7028
  LONG_RUNNING_TARGETS,
6252
7029
  SWEEPABLE_TARGETS,
6253
7030
  PRESET_NAMES,
6254
- WORKSPACE_ROOT_MARKER,
6255
- findWorkspaceRoot,
6256
- ensureWorkspacePrep,
6257
- inspectWorkspacePrep,
6258
7031
  palette,
6259
7032
  registerAdapters,
6260
7033
  describeFramework,
6261
7034
  detectFramework,
6262
7035
  dispatch
6263
7036
  };
6264
- //# sourceMappingURL=chunk-YZ4EKBK7.js.map
7037
+ //# sourceMappingURL=chunk-PLZ45MWU.js.map