@hublo/sentinel 1.2.0-alpha.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.`;
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.`;
202
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 relative6 = typeof bin === "string" ? bin : bin?.[binName];
209
- if (!relative6) return void 0;
210
- const executable = join2(dirname2(manifest), relative6);
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 existsSync8, readFileSync as readFileSync6 } from "fs";
224
- import { join as join8 } 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,122 +279,25 @@ 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;
288
- try {
289
- project = parseJsonc(
290
- readFileSync3(join4(cwd, "project.json"), "utf8"),
291
- "project.json"
292
- );
293
- } catch {
294
- return void 0;
295
- }
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
- }
384
-
385
301
  // src/roles/build/presets/requirements.json
386
302
  var requirements_default = {
387
303
  svelte: {
@@ -408,7 +324,8 @@ var toolchain_default = {
408
324
  "UserConfig",
409
325
  "UserConfigExport"
410
326
  ],
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."
327
+ 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.",
328
+ 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
329
  },
413
330
  {
414
331
  package: "@vitejs/plugin-react",
@@ -476,11 +393,19 @@ var BUILD_SCRIPT_NAME = "build";
476
393
  var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
477
394
  var DEV_SCRIPT_NAME = "serve";
478
395
  var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
479
- function buildTarget() {
396
+ var BUILD_OUTPUT_CANDIDATES = ["dist", ".output", "build"];
397
+ function buildTarget(options) {
398
+ const { moduleName: moduleName3, configFiles = BUILD_CONFIG_FILES, outputs } = options;
480
399
  return {
481
400
  [BUILD_SCRIPT_NAME]: {
401
+ executor: "nx:run-commands",
402
+ options: {
403
+ command: `pnpm --filter ${moduleName3} run ${BUILD_SCRIPT_NAME}`,
404
+ forwardAllArgs: false
405
+ },
482
406
  cache: true,
483
- inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
407
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
408
+ outputs: [...outputs]
484
409
  }
485
410
  };
486
411
  }
@@ -488,136 +413,71 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
488
413
  (entry) => entry.package
489
414
  );
490
415
 
491
- // src/roles/build/dev-script.ts
492
- var CHAIN = " -- ";
493
- var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
494
- var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
495
- function appOptions(rest) {
496
- const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
497
- const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
498
- return kept.join(" ");
499
- }
500
- function composeDevScript(existing, command) {
501
- if (existing === void 0 || existing.trim() === "") return command;
502
- const chunks = existing.trim().split(CHAIN);
503
- const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
504
- if (at === -1) return existing;
505
- const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
506
- const options = appOptions(rest);
507
- chunks[at] = options === "" ? command : `${command} -- ${options}`;
508
- return chunks.join(CHAIN);
509
- }
510
-
511
- // src/roles/build/nest/adopt.ts
512
- import { relative as relative2 } from "path";
513
-
514
- // src/roles/build/nest/nx-target.ts
515
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
516
- import { join as join6, relative } from "path";
517
- var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
518
- var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
519
- var readProjectJson = (cwd) => {
520
- const path = join6(cwd, "project.json");
521
- if (!existsSync6(path)) return void 0;
416
+ // src/roles/build/project-json.ts
417
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
418
+ import { join as join4 } from "path";
419
+ function projectTargets(cwd) {
420
+ const path = join4(cwd, "project.json");
421
+ if (!existsSync4(path)) return void 0;
522
422
  try {
523
- return JSON.parse(readFileSync4(path, "utf8"));
423
+ return JSON.parse(readFileSync4(path, "utf8")).targets;
524
424
  } catch {
525
425
  return void 0;
526
426
  }
527
- };
528
- function webpackBuildTarget(cwd) {
529
- const project = readProjectJson(cwd);
530
- const target = project?.targets?.build;
531
- if (target?.executor !== DEPRECATED_NX_BUILDER) return void 0;
532
- const { outputPath, main, webpackConfig } = target.options ?? {};
533
- if (typeof project?.name !== "string" || typeof outputPath !== "string" || typeof main !== "string" || typeof webpackConfig !== "string") {
534
- return void 0;
535
- }
536
- return {
537
- project: project.name,
538
- outputPath,
539
- main,
540
- webpackConfig,
541
- configurations: target.configurations
542
- };
543
427
  }
544
- function refusalFor(cwd, target) {
545
- if (target.webpackConfig !== TRANSLATABLE_WEBPACK_CONFIG) {
546
- 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.`;
547
- }
548
- const live = liveFileReplacements(cwd, target);
549
- if (live.length > 0) {
550
- 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.`;
428
+
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
+ );
551
442
  }
552
- return void 0;
553
- }
554
- function liveFileReplacements(cwd, target) {
555
- const workspaceRoot = workspaceRootFor(cwd, target);
556
- const configurations = target.configurations ?? {};
557
- const live = [];
558
- for (const configuration of Object.values(configurations)) {
559
- for (const replacement of configuration.fileReplacements ?? []) {
560
- const exists = existsSync6(join6(workspaceRoot, replacement.replace)) || existsSync6(join6(workspaceRoot, replacement.with));
561
- if (exists) live.push(replacement.replace);
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
+ );
562
453
  }
563
- }
564
- return live;
565
- }
566
- function workspaceRootFor(cwd, target) {
567
- const moduleRoot = target.main.replace(/\/src\/.*$/, "");
568
- const suffix = cwd.endsWith(moduleRoot) ? moduleRoot : "";
569
- return suffix === "" ? cwd : cwd.slice(0, cwd.length - suffix.length - 1);
570
- }
571
- function workspaceRootHops(cwd, target) {
572
- const root = workspaceRootFor(cwd, target);
573
- const rel = relative(root, cwd);
574
- return rel === "" ? 0 : rel.split(/[\\/]/).length;
575
- }
576
-
577
- // src/roles/build/nest/adopt.ts
578
- var NEST_CONFIG_FILE = "vite.config.mts";
579
- function nestConfigSource(cwd, target) {
580
- const hops = workspaceRootHops(cwd, target);
581
- const up = Array.from({ length: hops }, () => "..").join("/");
582
- return `import path from 'node:path'
583
-
584
- import { nestService } from '${BUILD_PRESET_SPECIFIERS.nest}'
585
-
586
- export default nestService({
587
- project: '${target.project}',
588
- root: __dirname,
589
- workspaceRoot: path.resolve(__dirname, '${up}'),
590
- outDir: '${target.outputPath}',
591
- })
592
- `;
593
- }
594
- function nestBuildTarget(cwd, target) {
595
- const moduleRoot = relative2(workspaceRootFor(cwd, target), cwd);
596
- return {
597
- executor: "nx:run-commands",
598
- outputs: [`{workspaceRoot}/${target.outputPath}`],
599
- options: {
600
- cwd: moduleRoot,
601
- command: "pnpm exec sentinel --run --build",
602
- forwardAllArgs: false
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
+ );
603
459
  }
604
- };
605
- }
606
- function nestAdoptionOperations(cwd, target) {
607
- return [
608
- { kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) },
609
- { kind: "remove-json-keys", path: "project.json", keys: [["targets", "build"]] },
610
- {
611
- kind: "merge-json",
612
- path: "project.json",
613
- value: { targets: { build: nestBuildTarget(cwd, target) } }
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
+ );
614
464
  }
615
- ];
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;
616
478
  }
617
479
 
618
480
  // src/roles/build/read-adoption.ts
619
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
620
- import { join as join7 } from "path";
621
481
  var NOT_ADOPTED = (configFile, unreadable = null) => ({
622
482
  configFile,
623
483
  preset: null,
@@ -628,7 +488,7 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
628
488
  unreadable
629
489
  });
630
490
  function buildConfigFile(cwd) {
631
- return BUILD_CONFIG_FILES.find((name) => existsSync7(join7(cwd, name)));
491
+ return BUILD_CONFIG_FILES.find((name) => existsSync5(join5(cwd, name)));
632
492
  }
633
493
  function importsSpecifier(source, specifier) {
634
494
  const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -643,6 +503,7 @@ var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
643
503
  function declaredBuildPreset(cwd) {
644
504
  const source = readBuildConfigSource(cwd);
645
505
  if (source === void 0) {
506
+ if (buildConfigFile(cwd) !== void 0) return void 0;
646
507
  return webpackBuildTarget(cwd) === void 0 ? void 0 : "nest";
647
508
  }
648
509
  if (importsSpecifier(source, BUILD_PRESET_SPECIFIERS.nest)) return "nest";
@@ -653,7 +514,7 @@ function readBuildConfigSource(cwd) {
653
514
  const configFile = buildConfigFile(cwd);
654
515
  if (!configFile) return void 0;
655
516
  try {
656
- return readFileSync5(join7(cwd, configFile), "utf8");
517
+ return readFileSync5(join5(cwd, configFile), "utf8");
657
518
  } catch {
658
519
  return void 0;
659
520
  }
@@ -663,31 +524,485 @@ function readBuildAdoption(cwd) {
663
524
  if (!configFile) return NOT_ADOPTED(null);
664
525
  let source;
665
526
  try {
666
- source = readFileSync5(join7(cwd, configFile), "utf8");
527
+ source = readFileSync5(join5(cwd, configFile), "utf8");
667
528
  } catch (error) {
668
529
  return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
669
530
  }
670
- 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
+ }
671
535
  const manifest = readProjectPackageJson(cwd);
672
536
  const declared = { ...manifest.dependencies, ...manifest.devDependencies };
673
537
  const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
674
538
  (name) => ({ name, version: declared[name] })
675
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
+ ];
676
546
  return {
677
547
  configFile,
678
548
  // Read from the config rather than assumed: the two presets are adopted the same way and
679
549
  // reporting the wrong one would make `--inspect` lie about what a module builds.
680
550
  preset: declaredBuildPreset(cwd) ?? "react",
681
551
  adopted: true,
682
- conformant: ownDeclarations.length === 0,
683
- drift: ownDeclarations.map(
684
- ({ 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`
685
- ),
552
+ conformant: drift.length === 0,
553
+ drift,
686
554
  ownDeclarations,
687
555
  unreadable: null
688
556
  };
689
557
  }
690
558
 
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
+
691
1006
  // src/roles/build/requirements.ts
692
1007
  function atLeast(declared, needed) {
693
1008
  const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -716,32 +1031,26 @@ function describeUnmet(preset, unmet) {
716
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\`.`;
717
1032
  }
718
1033
 
719
- // src/roles/build/rewrite-imports.ts
1034
+ // src/core/config/rewrite-imports.ts
720
1035
  import { parseSync } from "oxc-parser";
721
- var MOVED_SPECIFIERS = new Set(OWNED_PACKAGES.map((entry) => entry.specifier));
722
- function esmBlocker(fileName, packageType) {
723
- if (/\.(mts|mjs)$/.test(fileName)) return void 0;
724
- if (packageType === "module") return void 0;
725
- 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.`;
726
- }
727
- var DEFAULT_EXPORT_NAMES = Object.fromEntries(
728
- OWNED_PACKAGES.filter((entry) => entry.default !== void 0).map((entry) => [
729
- entry.specifier,
730
- entry.default
731
- ])
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])
732
1039
  );
733
- function renderImport(bindings) {
1040
+ function renderImport(bindings, presetSpecifier) {
734
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(", ");
735
1042
  const values = bindings.filter((b) => !b.typeOnly);
736
1043
  const types = bindings.filter((b) => b.typeOnly);
737
1044
  const lines = [];
738
- if (values.length > 0) lines.push(`import { ${render(values)} } from '${BUILD_PRESET_SPECIFIER}'`);
1045
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
739
1046
  if (types.length > 0) {
740
- lines.push(`import type { ${render(types)} } from '${BUILD_PRESET_SPECIFIER}'`);
1047
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
741
1048
  }
742
1049
  return lines.join("\n");
743
1050
  }
744
- function rewriteBuildImports(source, fileName = "vite.config.ts") {
1051
+ function rewriteToolchainImports(toolchain, source, fileName) {
1052
+ const moved = movedSpecifiers(toolchain);
1053
+ const defaults = defaultExportNames(toolchain);
745
1054
  const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
746
1055
  if (errors.length > 0) {
747
1056
  return {
@@ -752,10 +1061,10 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
752
1061
  const imports = program.body.filter(
753
1062
  (node) => node.type === "ImportDeclaration"
754
1063
  );
755
- if (imports.some((node) => node.source.value === BUILD_PRESET_SPECIFIER)) {
1064
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
756
1065
  return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
757
1066
  }
758
- const moving = imports.filter((node) => MOVED_SPECIFIERS.has(node.source.value));
1067
+ const moving = imports.filter((node) => moved.has(node.source.value));
759
1068
  if (moving.length === 0) {
760
1069
  return {
761
1070
  kind: "unchanged",
@@ -772,7 +1081,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
772
1081
  for (const spec of specifiers) {
773
1082
  const typeOnly = node.importKind === "type" || spec.importKind === "type";
774
1083
  if (spec.type === "ImportDefaultSpecifier") {
775
- const exported = DEFAULT_EXPORT_NAMES[from];
1084
+ const exported = defaults[from];
776
1085
  if (!exported) {
777
1086
  return {
778
1087
  kind: "blocked",
@@ -804,7 +1113,7 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
804
1113
  const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
805
1114
  out = out.slice(0, span.start) + out.slice(span.end + after);
806
1115
  }
807
- out = out.slice(0, first.start) + renderImport(bindings) + out.slice(first.end);
1116
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier) + out.slice(first.end);
808
1117
  return {
809
1118
  kind: "rewritten",
810
1119
  source: out,
@@ -812,7 +1121,19 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
812
1121
  };
813
1122
  }
814
1123
 
1124
+ // src/roles/build/rewrite-imports.ts
1125
+ var BUILD_TOOLCHAIN = {
1126
+ presetSpecifier: BUILD_PRESET_SPECIFIER,
1127
+ owned: OWNED_PACKAGES
1128
+ };
1129
+ function rewriteBuildImports(source, fileName = "vite.config.ts") {
1130
+ return rewriteToolchainImports(BUILD_TOOLCHAIN, source, fileName);
1131
+ }
1132
+
815
1133
  // src/roles/build/plan.ts
1134
+ function candidateOutputs() {
1135
+ return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
1136
+ }
816
1137
  function ownedBuildDependencies(cwd) {
817
1138
  const manifest = readProjectPackageJson(cwd);
818
1139
  const keys = [];
@@ -844,9 +1165,9 @@ function buildScripts(cwd) {
844
1165
  const own2 = moduleName(cwd);
845
1166
  scripts[DEV_SCRIPT_NAME] = composeDevScript(
846
1167
  existingDev,
847
- // Trailing `--` for the same reason as the build script: whatever the caller appends
848
- // reaches Vite, not sentinel's parser. It goes after `--module`, which is sentinel's own.
849
- `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2} --`
1168
+ // No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
1169
+ // sides produced `-- --`, handing Vite an argument nobody wrote.
1170
+ `${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
850
1171
  );
851
1172
  }
852
1173
  return scripts;
@@ -860,70 +1181,73 @@ function plan(context) {
860
1181
  if (unmet.length > 0) {
861
1182
  return { operations: [], skipped: describeUnmet(context.preset, unmet) };
862
1183
  }
863
- if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
1184
+ const declaredPreset2 = declaredBuildPreset(context.cwd);
1185
+ const configFile = buildConfigFile(context.cwd);
1186
+ if (declaredPreset2 === void 0 && configFile !== void 0) {
864
1187
  return {
865
1188
  operations: [],
866
- 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.`
1189
+ skipped: `this module has a Vite config, but it builds neither a React app (no @vitejs/plugin-react or @tanstack/react-start) nor a Nest service (no ${BUILD_PRESET_SPECIFIERS.nest}). Those are the two families this role serves.`
867
1190
  };
868
1191
  }
869
- const notes = [];
870
- const operations = [];
871
- const configFile = buildConfigFile(context.cwd);
872
1192
  const webpackTarget = webpackBuildTarget(context.cwd);
873
- if (configFile === void 0 && webpackTarget !== void 0) {
874
- const refusal = refusalFor(context.cwd, webpackTarget);
875
- if (refusal !== void 0) return { operations: [], blocked: refusal };
876
- const dropped = Object.keys(webpackTarget.configurations ?? {});
1193
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
1194
+ return planNestService(context, webpackTarget);
1195
+ }
1196
+ if (configFile === void 0) {
877
1197
  return {
878
- operations: nestAdoptionOperations(context.cwd, webpackTarget),
879
- notes: [
880
- `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.`,
881
- ...dropped.length > 0 ? [
882
- `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.`
883
- ] : [],
884
- `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.`
885
- ]
1198
+ operations: [],
1199
+ skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 services here build through \`@nx/webpack\`), the build role does not apply to it.`
886
1200
  };
887
1201
  }
888
- if (configFile === void 0) {
1202
+ return planReactApp(context, configFile);
1203
+ }
1204
+ function planReactApp(context, configFile) {
1205
+ const notes = [];
1206
+ const operations = [];
1207
+ const esm = esmReadiness(configFile, readProjectPackageJson(context.cwd).type);
1208
+ if (esm.kind === "blocked") return { operations: [], skipped: esm.why };
1209
+ const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
1210
+ if (rewrite.kind === "blocked") {
889
1211
  return {
890
1212
  operations: [],
891
- skipped: `no Vite config in this module, so there is nothing to point at sentinel. This role moves an existing config's imports; it does not introduce a bundler. If this module builds another way (38 projects here build through \`@nx/webpack\`), the build role does not apply to it.`
1213
+ skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
892
1214
  };
893
- } else {
894
- const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
895
- if (notEsm !== void 0) return { operations: [], skipped: notEsm };
896
- const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
897
- if (rewrite.kind === "blocked") {
898
- return {
899
- operations: [],
900
- 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.`
901
- };
902
- }
903
- if (rewrite.kind === "rewritten") {
904
- operations.push({ kind: "write", path: configFile, contents: rewrite.source });
905
- }
906
- for (const name of COMPANION_CONFIG_FILES) {
907
- const source = readConfigSource(context.cwd, name);
908
- if (source === "") continue;
909
- const companion = rewriteBuildImports(source, name);
910
- if (companion.kind === "rewritten") {
911
- operations.push({ kind: "write", path: name, contents: companion.source });
912
- notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
913
- } else if (companion.kind === "blocked") {
914
- notes.push(
915
- `${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.`
916
- );
917
- }
918
- }
919
- if (rewrite.kind === "rewritten") {
1215
+ }
1216
+ if (rewrite.kind === "rewritten") {
1217
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
1218
+ }
1219
+ for (const name of COMPANION_CONFIG_FILES) {
1220
+ const source = readConfigSource(context.cwd, name);
1221
+ if (source === "") continue;
1222
+ const companion = rewriteBuildImports(source, name);
1223
+ if (companion.kind === "rewritten") {
1224
+ operations.push({ kind: "write", path: name, contents: companion.source });
1225
+ notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
1226
+ } else if (companion.kind === "blocked") {
920
1227
  notes.push(
921
- `${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
1228
+ `${name} imports the toolchain and could NOT be pointed at sentinel: ${companion.why}. It will stop resolving when the workspace root drops these packages, and it will fail your TESTS rather than your build.`
922
1229
  );
923
1230
  }
924
1231
  }
925
- operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
926
- operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
1232
+ if (rewrite.kind === "rewritten") {
1233
+ notes.push(
1234
+ `${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
1235
+ );
1236
+ }
1237
+ const scripts = buildScripts(context.cwd);
1238
+ operations.push(manifestOperation(context.cwd, scripts));
1239
+ operations.push(
1240
+ ...nxTargetOperations({
1241
+ cwd: context.cwd,
1242
+ targets: buildTarget({
1243
+ moduleName: adoptedPackageName(context.cwd),
1244
+ outputs: candidateOutputs()
1245
+ })
1246
+ })
1247
+ );
1248
+ const dev = devTargetOperations(context.cwd, scripts[DEV_SCRIPT_NAME]);
1249
+ operations.push(...dev.operations);
1250
+ if (dev.note !== void 0) notes.push(dev.note);
927
1251
  const owned = ownedBuildDependencies(context.cwd);
928
1252
  if (owned.length > 0) {
929
1253
  operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
@@ -931,12 +1255,53 @@ function plan(context) {
931
1255
  `removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
932
1256
  );
933
1257
  }
1258
+ const formatted = readFormatAdoption(context.cwd).adopted;
1259
+ notes.push(
1260
+ formatted ? `run \`sentinel --run --format --fix\` on this module: adoption rewrote ${configFile}, and a rewritten import can be longer than your formatter wraps at.` : `this module is formatted by the workspace, not by sentinel, so format ${configFile} with whatever owns it before committing: a rewritten import can be longer than that formatter wraps at, and the check would fail on a file nobody typed.`
1261
+ );
1262
+ notes.push(
1263
+ `this config is code, and it runs before the build does: any environment variable it reads has to be present wherever you build, including a fresh clone, another CI job or an image. Adoption does not change what it reads, but a failure there arrives early and names this role rather than the variable.`
1264
+ );
934
1265
  return { operations, notes };
935
1266
  }
1267
+ function planNestService(context, webpackTarget) {
1268
+ const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
1269
+ if (outputs === void 0) {
1270
+ return {
1271
+ operations: [],
1272
+ blocked: `${NEST_CONFIG_FILE} does not declare an \`outDir\`, so sentinel cannot say where this service's build lands. Declaring an nx target without it would cache the build and restore nothing from that cache. Nothing was written.`
1273
+ };
1274
+ }
1275
+ const metadata = [
1276
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND }),
1277
+ ...nxTargetOperations({
1278
+ cwd: context.cwd,
1279
+ targets: buildTarget({
1280
+ moduleName: adoptedPackageName(context.cwd),
1281
+ configFiles: [NEST_CONFIG_FILE],
1282
+ outputs
1283
+ })
1284
+ })
1285
+ ];
1286
+ if (webpackTarget === void 0) return { operations: metadata };
1287
+ const refusal = refusalFor(context.cwd, webpackTarget);
1288
+ if (refusal !== void 0) return { operations: [], blocked: refusal };
1289
+ const dropped = Object.keys(webpackTarget.configurations ?? {});
1290
+ return {
1291
+ operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
1292
+ notes: [
1293
+ `wrote ${NEST_CONFIG_FILE} and pointed the nx build target at sentinel. The target stays \`nx:run-commands\` with \`forwardAllArgs: false\`, so the image's \`--generatePackageJson\` still works and the Dockerfile needs no change.`,
1294
+ ...dropped.length > 0 ? [
1295
+ `dropped the \`${dropped.join("`, `")}\` configuration${dropped.length > 1 ? "s" : ""} from the build target: its fileReplacements point at files that do not exist, so it replaced nothing. A live one would have blocked this instead.`
1296
+ ] : [],
1297
+ `verify with \`nx run ${webpackTarget.project}:generate-swagger-file\` and \`git diff --exit-code\`: it runs the built bundle, so it proves the service boots AND that its published contract did not move.`
1298
+ ]
1299
+ };
1300
+ }
936
1301
  function readConfigSource(cwd, configFile) {
937
- if (!existsSync8(join8(cwd, configFile))) return "";
1302
+ if (!existsSync11(join11(cwd, configFile))) return "";
938
1303
  try {
939
- return readFileSync6(join8(cwd, configFile), "utf8");
1304
+ return readFileSync9(join11(cwd, configFile), "utf8");
940
1305
  } catch {
941
1306
  return "";
942
1307
  }
@@ -1039,80 +1404,290 @@ var ViteRoleAdapter = class extends BaseAdapter {
1039
1404
  `using the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite fail in ways that never mention a version. Remove vite from this module's package.json.`
1040
1405
  );
1041
1406
  }
1042
- return { kind: "ready", vite: bin };
1407
+ return { kind: "ready", vite: bin };
1408
+ }
1409
+ /**
1410
+ * Run Vite with the caller's own arguments after `--`, and report what happened.
1411
+ *
1412
+ * `whenSignalled` is the only difference between the two targets here, and it is a real one:
1413
+ * a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
1414
+ * did exactly what the developer asked.
1415
+ */
1416
+ spawnVite(ctx, vite, argv, whenSignalled) {
1417
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1418
+ valueFlags: VITE_VALUE_FLAGS
1419
+ });
1420
+ const result = spawnSync(vite, [...argv, ...options, ...paths], {
1421
+ cwd: ctx.cwd,
1422
+ stdio: "inherit"
1423
+ });
1424
+ if (result.error) {
1425
+ this.say(`could not run vite (${result.error.message})`);
1426
+ return { ok: false, code: 1 };
1427
+ }
1428
+ const code = result.status ?? whenSignalled;
1429
+ return { ok: code === 0, code };
1430
+ }
1431
+ /** One prefix for every message this role emits, so they cannot drift apart. */
1432
+ say(message) {
1433
+ process.stderr.write(`sentinel ${this.label}(vite): ${message}
1434
+ `);
1435
+ }
1436
+ };
1437
+
1438
+ // src/roles/build/adapters/vite/vite-dev.adapter.ts
1439
+ var ViteDevAdapter = class extends ViteRoleAdapter {
1440
+ target = "dev";
1441
+ get label() {
1442
+ return "dev";
1443
+ }
1444
+ /**
1445
+ * `--init --dev` does the same thing as `--init --build`, and now says so.
1446
+ *
1447
+ * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1448
+ * because adopting one without the other leaves the module half-migrated. What was missing
1449
+ * was only the telling: the run printed a message about the build and nothing about `serve`,
1450
+ * so a developer could believe the dev server had been adopted on its own. Refusing was the
1451
+ * wrong correction, it added friction to a command that already produced the right result.
1452
+ */
1453
+ plan(context) {
1454
+ if (declaredBuildPreset(context.cwd) === "nest" || webpackBuildTarget(context.cwd)) {
1455
+ return {
1456
+ operations: [],
1457
+ skipped: `a Nest service has no dev server, so --dev has nothing to adopt here. Its build is \`sentinel --init --build\`, and it runs as a Node process afterwards.`
1458
+ };
1459
+ }
1460
+ const planned = super.plan(context);
1461
+ if (planned.operations.length === 0) return planned;
1462
+ return {
1463
+ ...planned,
1464
+ notes: [
1465
+ ...planned.notes ?? [],
1466
+ "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1467
+ ]
1468
+ };
1469
+ }
1470
+ /**
1471
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
1472
+ * beyond the exit code the developer's own Ctrl-C produces.
1473
+ *
1474
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
1475
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
1476
+ * running it again would race the watcher that is about to own the file.
1477
+ *
1478
+ * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
1479
+ * bare form is the one every version accepts.
1480
+ */
1481
+ async run(ctx) {
1482
+ const ready = this.prepare(ctx);
1483
+ if (ready.kind === "stop") return ready.result;
1484
+ return this.spawnVite(ctx, ready.vite, [], 0);
1485
+ }
1486
+ };
1487
+
1488
+ // src/roles/build/declared-outputs.ts
1489
+ import { existsSync as existsSync13, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
1490
+ import { join as join13 } from "path";
1491
+
1492
+ // src/core/workspace-prep.ts
1493
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
1494
+ import { dirname as dirname3, join as join12, relative as relative2 } from "path";
1495
+
1496
+ // src/core/settings.ts
1497
+ var WORKSPACE_ROOT_MARKER = "nx.json";
1498
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
1499
+
1500
+ // src/core/workspace-prep.ts
1501
+ var OVERRIDE_KEY = "i18next>typescript";
1502
+ var NATIVE_TS_ALIAS = "@typescript/native";
1503
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1504
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1505
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1506
+ function findWorkspaceRoot(startDir) {
1507
+ let dir = startDir;
1508
+ for (; ; ) {
1509
+ if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
1510
+ const parent = dirname3(dir);
1511
+ if (parent === dir) return void 0;
1512
+ dir = parent;
1513
+ }
1514
+ }
1515
+ function declaredNativeTs(pkg) {
1516
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1517
+ if (!spec) return void 0;
1518
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1519
+ return version || void 0;
1520
+ }
1521
+ function ensureI18nextSingleton(root, dryRun) {
1522
+ const pkgPath = join12(root, "package.json");
1523
+ if (!existsSync12(pkgPath)) return void 0;
1524
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1525
+ const want = declaredNativeTs(pkg);
1526
+ if (!want) return void 0;
1527
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1528
+ if (have === want) return void 0;
1529
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1530
+ pkg.pnpm ??= {};
1531
+ pkg.pnpm.overrides ??= {};
1532
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1533
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1534
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1535
+ }
1536
+ function ensureReleaseAgeAllowList(root, dryRun) {
1537
+ const yamlPath = join12(root, WORKSPACE_YAML);
1538
+ if (!existsSync12(yamlPath)) return void 0;
1539
+ const own = readOwnPackage().name;
1540
+ const lines = readFileSync10(yamlPath, "utf8").split("\n");
1541
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1542
+ if (keyIdx === -1) return void 0;
1543
+ let lastItemIdx = keyIdx;
1544
+ let indent = " ";
1545
+ for (let i = keyIdx + 1; i < lines.length; i++) {
1546
+ const match = lines[i]?.match(LIST_ITEM);
1547
+ if (!match) break;
1548
+ indent = match[1] ?? indent;
1549
+ lastItemIdx = i;
1550
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1551
+ }
1552
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1553
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1554
+ writeFileSync(yamlPath, lines.join("\n"));
1555
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1556
+ }
1557
+ var PRETTIER_IGNORE = ".prettierignore";
1558
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1559
+ var ROOT_PRETTIER_CONFIGS = [
1560
+ ".prettierrc",
1561
+ ".prettierrc.json",
1562
+ ".prettierrc.json5",
1563
+ ".prettierrc.yml",
1564
+ ".prettierrc.yaml",
1565
+ ".prettierrc.js",
1566
+ ".prettierrc.cjs",
1567
+ ".prettierrc.mjs",
1568
+ "prettier.config.js",
1569
+ "prettier.config.cjs",
1570
+ "prettier.config.mjs"
1571
+ ];
1572
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
1573
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1574
+ if (rel === "" || rel.startsWith("..")) return void 0;
1575
+ const ignorePath = join12(root, PRETTIER_IGNORE);
1576
+ const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
1577
+ if (!hasPrettier) return void 0;
1578
+ const pattern = `/${rel}/`;
1579
+ const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
1580
+ const lines = existing.split("\n");
1581
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1582
+ return void 0;
1583
+ }
1584
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1585
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1586
+ if (headerIdx === -1) {
1587
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1588
+
1589
+ `;
1590
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1591
+ ${pattern}
1592
+ `);
1593
+ } else {
1594
+ let end = headerIdx + 1;
1595
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1596
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1597
+ writeFileSync(
1598
+ ignorePath,
1599
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1600
+ );
1601
+ }
1602
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1603
+ }
1604
+ function ensureWorkspacePrep(opts) {
1605
+ const dryRun = Boolean(opts.dryRun);
1606
+ return [
1607
+ ensureI18nextSingleton(opts.root, dryRun),
1608
+ ensureReleaseAgeAllowList(opts.root, dryRun),
1609
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1610
+ ].filter((message) => message !== void 0);
1611
+ }
1612
+ function inspectWorkspacePrep(root) {
1613
+ const entries = [];
1614
+ let pkg = {};
1615
+ const pkgPath = join12(root, "package.json");
1616
+ if (existsSync12(pkgPath)) {
1617
+ try {
1618
+ pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1619
+ } catch {
1620
+ pkg = {};
1621
+ }
1043
1622
  }
1044
- /**
1045
- * Run Vite with the caller's own arguments after `--`, and report what happened.
1046
- *
1047
- * `whenSignalled` is the only difference between the two targets here, and it is a real one:
1048
- * a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
1049
- * did exactly what the developer asked.
1050
- */
1051
- spawnVite(ctx, vite, argv, whenSignalled) {
1052
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1053
- valueFlags: VITE_VALUE_FLAGS
1054
- });
1055
- const result = spawnSync(vite, [...argv, ...options, ...paths], {
1056
- cwd: ctx.cwd,
1057
- stdio: "inherit"
1623
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1624
+ if (pinned !== void 0) {
1625
+ const stillForks = declaredNativeTs(pkg);
1626
+ entries.push({
1627
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1628
+ reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
1058
1629
  });
1059
- if (result.error) {
1060
- this.say(`could not run vite (${result.error.message})`);
1061
- return { ok: false, code: 1 };
1062
- }
1063
- const code = result.status ?? whenSignalled;
1064
- return { ok: code === 0, code };
1065
1630
  }
1066
- /** One prefix for every message this role emits, so they cannot drift apart. */
1067
- say(message) {
1068
- process.stderr.write(`sentinel ${this.label}(vite): ${message}
1069
- `);
1631
+ const own = readOwnPackage().name;
1632
+ const yamlPath = join12(root, WORKSPACE_YAML);
1633
+ if (existsSync12(yamlPath)) {
1634
+ const yaml = readFileSync10(yamlPath, "utf8");
1635
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1636
+ if (listed) {
1637
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1638
+ entries.push({
1639
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1640
+ reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
1641
+ });
1642
+ }
1070
1643
  }
1071
- };
1644
+ return entries;
1645
+ }
1072
1646
 
1073
- // src/roles/build/adapters/vite/vite-dev.adapter.ts
1074
- var ViteDevAdapter = class extends ViteRoleAdapter {
1075
- target = "dev";
1076
- get label() {
1077
- return "dev";
1647
+ // src/roles/build/declared-outputs.ts
1648
+ function declaredBuildTarget(cwd) {
1649
+ return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1650
+ }
1651
+ function resolveOutputToken(token, cwd) {
1652
+ if (token.startsWith("{projectRoot}/")) return join13(cwd, token.slice("{projectRoot}/".length));
1653
+ if (token.startsWith("{workspaceRoot}/")) {
1654
+ const root = findWorkspaceRoot(cwd);
1655
+ return root === void 0 ? void 0 : join13(root, token.slice("{workspaceRoot}/".length));
1078
1656
  }
1079
- /**
1080
- * `--init --dev` does the same thing as `--init --build`, and now says so.
1081
- *
1082
- * The shared plan is deliberate (see `ViteRoleAdapter.plan`): one plan writes both scripts,
1083
- * because adopting one without the other leaves the module half-migrated. What was missing
1084
- * was only the telling: the run printed a message about the build and nothing about `serve`,
1085
- * so a developer could believe the dev server had been adopted on its own. Refusing was the
1086
- * wrong correction, it added friction to a command that already produced the right result.
1087
- */
1088
- plan(context) {
1089
- const planned = super.plan(context);
1090
- if (planned.operations.length === 0) return planned;
1091
- return {
1092
- ...planned,
1093
- notes: [
1094
- ...planned.notes ?? [],
1095
- "--init --dev adopts the build too: one plan writes both `build` and `serve`."
1096
- ]
1097
- };
1657
+ return join13(cwd, token);
1658
+ }
1659
+ function buildStamp(cwd) {
1660
+ const marker = join13(cwd, `.sentinel-build-stamp-${process.pid}`);
1661
+ try {
1662
+ writeFileSync2(marker, "");
1663
+ const stamp = statSync(marker).mtimeMs;
1664
+ rmSync(marker, { force: true });
1665
+ return stamp;
1666
+ } catch {
1667
+ return Date.now();
1098
1668
  }
1099
- /**
1100
- * Start the dev server. It does not return until stopped, so there is no verdict to report
1101
- * beyond the exit code the developer's own Ctrl-C produces.
1102
- *
1103
- * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
1104
- * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
1105
- * running it again would race the watcher that is about to own the file.
1106
- *
1107
- * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
1108
- * bare form is the one every version accepts.
1109
- */
1110
- async run(ctx) {
1111
- const ready = this.prepare(ctx);
1112
- if (ready.kind === "stop") return ready.result;
1113
- return this.spawnVite(ctx, ready.vite, [], 0);
1669
+ }
1670
+ function writtenSince(path, startedAt) {
1671
+ if (!existsSync13(path)) return false;
1672
+ try {
1673
+ return statSync(path).mtimeMs >= startedAt;
1674
+ } catch {
1675
+ return true;
1114
1676
  }
1115
- };
1677
+ }
1678
+ function outputsProblem(cwd, startedAt) {
1679
+ const target = declaredBuildTarget(cwd);
1680
+ if (target === void 0) return void 0;
1681
+ const declared = target.outputs ?? [];
1682
+ if (declared.length === 0) {
1683
+ if (target.cache !== true) return void 0;
1684
+ return `this module's nx build target sets \`cache: true\` and declares no \`outputs\`, so nx will cache this build and restore nothing from it: the next cache hit reports success and leaves no build on disk. Add the directory this build writes to under \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json, or re-run \`sentinel --init --build\`, which declares it.`;
1685
+ }
1686
+ const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
1687
+ if (resolved.length === 0) return void 0;
1688
+ if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
1689
+ return `the build wrote nothing to any of the directories this module declares as its nx \`outputs\` (${declared.join(", ")}), so nx would cache this build and restore an empty result from it. Whatever directory the build DID write to belongs in \`nx.targets.${BUILD_SCRIPT_NAME}.outputs\` in package.json. The build itself succeeded; only its cache declaration is wrong.`;
1690
+ }
1116
1691
 
1117
1692
  // src/roles/build/prerequisite.ts
1118
1693
  import { spawnSync as spawnSync2 } from "child_process";
@@ -1162,7 +1737,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
1162
1737
  );
1163
1738
  return { ok: false, code: prerequisite.code };
1164
1739
  }
1165
- return this.spawnVite(ctx, ready.vite, ["build"], 1);
1740
+ const startedAt = buildStamp(ctx.cwd);
1741
+ const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
1742
+ if (!result.ok) return result;
1743
+ const problem = outputsProblem(ctx.cwd, startedAt);
1744
+ if (problem !== void 0) {
1745
+ this.say(problem);
1746
+ return { ok: false, code: 1 };
1747
+ }
1748
+ return result;
1166
1749
  }
1167
1750
  /**
1168
1751
  * What this module builds with, WITHOUT loading its config.
@@ -1234,12 +1817,12 @@ function registerBuild() {
1234
1817
 
1235
1818
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1236
1819
  import { spawnSync as spawnSync3 } from "child_process";
1237
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
1238
- import { join as join15 } from "path";
1820
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1821
+ import { join as join18 } from "path";
1239
1822
 
1240
1823
  // src/core/config/has-source.ts
1241
1824
  import { readdirSync } from "fs";
1242
- import { extname, join as join9, relative as relative3 } from "path";
1825
+ import { extname, join as join14, relative as relative3 } from "path";
1243
1826
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1244
1827
  "node_modules",
1245
1828
  "dist",
@@ -1274,258 +1857,59 @@ var FORMATTABLE_EXTENSIONS = [
1274
1857
  ".yml",
1275
1858
  ".html",
1276
1859
  ".css",
1277
- ".scss",
1278
- ".hbs"
1279
- ];
1280
- function hasSourceFiles(cwd, extensions) {
1281
- const wanted = new Set(extensions);
1282
- const queue = [cwd];
1283
- while (queue.length > 0) {
1284
- const dir = queue.pop();
1285
- let entries;
1286
- try {
1287
- entries = readdirSync(dir, { withFileTypes: true });
1288
- } catch {
1289
- continue;
1290
- }
1291
- for (const entry of entries) {
1292
- if (entry.isDirectory()) {
1293
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join9(dir, entry.name));
1294
- continue;
1295
- }
1296
- if (wanted.has(extname(entry.name))) return true;
1297
- }
1298
- }
1299
- return false;
1300
- }
1301
- function listSourceFiles(cwd, extensions) {
1302
- const wanted = new Set(extensions);
1303
- const queue = [cwd];
1304
- const found = [];
1305
- while (queue.length > 0) {
1306
- const dir = queue.pop();
1307
- let entries;
1308
- try {
1309
- entries = readdirSync(dir, { withFileTypes: true });
1310
- } catch {
1311
- continue;
1312
- }
1313
- for (const entry of entries) {
1314
- const full = join9(dir, entry.name);
1315
- if (entry.isDirectory()) {
1316
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1317
- continue;
1318
- }
1319
- if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1320
- }
1321
- }
1322
- return found;
1323
- }
1324
-
1325
- // src/core/named-scope.ts
1326
- var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1327
- function scopeFlags(paths, options = []) {
1328
- if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1329
- return [TOLERATE_EMPTY_FLAG];
1330
- }
1331
-
1332
- // src/core/settings.ts
1333
- var WORKSPACE_ROOT_MARKER = "nx.json";
1334
- var DEFAULT_MAX_DIAGNOSTICS = 100;
1335
-
1336
- // src/core/workspace-prep.ts
1337
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync } from "fs";
1338
- import { dirname as dirname3, join as join10, relative as relative4 } from "path";
1339
- var OVERRIDE_KEY = "i18next>typescript";
1340
- var NATIVE_TS_ALIAS = "@typescript/native";
1341
- var WORKSPACE_YAML = "pnpm-workspace.yaml";
1342
- var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1343
- var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1344
- function findWorkspaceRoot(startDir) {
1345
- let dir = startDir;
1346
- for (; ; ) {
1347
- if (existsSync9(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
1348
- const parent = dirname3(dir);
1349
- if (parent === dir) return void 0;
1350
- dir = parent;
1351
- }
1352
- }
1353
- function declaredNativeTs(pkg) {
1354
- const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1355
- if (!spec) return void 0;
1356
- const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1357
- return version || void 0;
1358
- }
1359
- function ensureI18nextSingleton(root, dryRun) {
1360
- const pkgPath = join10(root, "package.json");
1361
- if (!existsSync9(pkgPath)) return void 0;
1362
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1363
- const want = declaredNativeTs(pkg);
1364
- if (!want) return void 0;
1365
- const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1366
- if (have === want) return void 0;
1367
- if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1368
- pkg.pnpm ??= {};
1369
- pkg.pnpm.overrides ??= {};
1370
- pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1371
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1372
- return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1373
- }
1374
- function ensureReleaseAgeAllowList(root, dryRun) {
1375
- const yamlPath = join10(root, WORKSPACE_YAML);
1376
- if (!existsSync9(yamlPath)) return void 0;
1377
- const own = readOwnPackage().name;
1378
- const lines = readFileSync7(yamlPath, "utf8").split("\n");
1379
- const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1380
- if (keyIdx === -1) return void 0;
1381
- let lastItemIdx = keyIdx;
1382
- let indent = " ";
1383
- for (let i = keyIdx + 1; i < lines.length; i++) {
1384
- const match = lines[i]?.match(LIST_ITEM);
1385
- if (!match) break;
1386
- indent = match[1] ?? indent;
1387
- lastItemIdx = i;
1388
- if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1389
- }
1390
- if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1391
- lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1392
- writeFileSync(yamlPath, lines.join("\n"));
1393
- return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1394
- }
1395
- var PRETTIER_IGNORE = ".prettierignore";
1396
- var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1397
- var ROOT_PRETTIER_CONFIGS = [
1398
- ".prettierrc",
1399
- ".prettierrc.json",
1400
- ".prettierrc.json5",
1401
- ".prettierrc.yml",
1402
- ".prettierrc.yaml",
1403
- ".prettierrc.js",
1404
- ".prettierrc.cjs",
1405
- ".prettierrc.mjs",
1406
- "prettier.config.js",
1407
- "prettier.config.cjs",
1408
- "prettier.config.mjs"
1409
- ];
1410
- function ensureFormatterExclusion(root, moduleDir, dryRun) {
1411
- const rel = relative4(root, moduleDir).replaceAll("\\", "/");
1412
- if (rel === "" || rel.startsWith("..")) return void 0;
1413
- const ignorePath = join10(root, PRETTIER_IGNORE);
1414
- const hasPrettier = existsSync9(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync9(join10(root, name)));
1415
- if (!hasPrettier) return void 0;
1416
- const pattern = `/${rel}/`;
1417
- const existing = existsSync9(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1418
- const lines = existing.split("\n");
1419
- if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1420
- return void 0;
1421
- }
1422
- if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1423
- const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1424
- if (headerIdx === -1) {
1425
- const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1426
-
1427
- `;
1428
- writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1429
- ${pattern}
1430
- `);
1431
- } else {
1432
- let end = headerIdx + 1;
1433
- while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1434
- const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1435
- writeFileSync(
1436
- ignorePath,
1437
- [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1438
- );
1860
+ ".scss",
1861
+ ".hbs"
1862
+ ];
1863
+ function hasSourceFiles(cwd, extensions) {
1864
+ const wanted = new Set(extensions);
1865
+ const queue = [cwd];
1866
+ while (queue.length > 0) {
1867
+ const dir = queue.pop();
1868
+ let entries;
1869
+ try {
1870
+ entries = readdirSync(dir, { withFileTypes: true });
1871
+ } catch {
1872
+ continue;
1873
+ }
1874
+ for (const entry of entries) {
1875
+ if (entry.isDirectory()) {
1876
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join14(dir, entry.name));
1877
+ continue;
1878
+ }
1879
+ if (wanted.has(extname(entry.name))) return true;
1880
+ }
1439
1881
  }
1440
- return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1441
- }
1442
- function ensureWorkspacePrep(opts) {
1443
- const dryRun = Boolean(opts.dryRun);
1444
- return [
1445
- ensureI18nextSingleton(opts.root, dryRun),
1446
- ensureReleaseAgeAllowList(opts.root, dryRun),
1447
- opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1448
- ].filter((message) => message !== void 0);
1882
+ return false;
1449
1883
  }
1450
- function inspectWorkspacePrep(root) {
1451
- const entries = [];
1452
- let pkg = {};
1453
- const pkgPath = join10(root, "package.json");
1454
- if (existsSync9(pkgPath)) {
1884
+ function listSourceFiles(cwd, extensions) {
1885
+ const wanted = new Set(extensions);
1886
+ const queue = [cwd];
1887
+ const found = [];
1888
+ while (queue.length > 0) {
1889
+ const dir = queue.pop();
1890
+ let entries;
1455
1891
  try {
1456
- pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1892
+ entries = readdirSync(dir, { withFileTypes: true });
1457
1893
  } catch {
1458
- pkg = {};
1894
+ continue;
1459
1895
  }
1460
- }
1461
- const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1462
- if (pinned !== void 0) {
1463
- const stillForks = declaredNativeTs(pkg);
1464
- entries.push({
1465
- rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1466
- 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.`
1467
- });
1468
- }
1469
- const own = readOwnPackage().name;
1470
- const yamlPath = join10(root, WORKSPACE_YAML);
1471
- if (existsSync9(yamlPath)) {
1472
- const yaml = readFileSync7(yamlPath, "utf8");
1473
- const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1474
- if (listed) {
1475
- const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1476
- entries.push({
1477
- rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1478
- 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.`
1479
- });
1896
+ for (const entry of entries) {
1897
+ const full = join14(dir, entry.name);
1898
+ if (entry.isDirectory()) {
1899
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1900
+ continue;
1901
+ }
1902
+ if (wanted.has(extname(entry.name))) found.push(relative3(cwd, full));
1480
1903
  }
1481
1904
  }
1482
- return entries;
1905
+ return found;
1483
1906
  }
1484
1907
 
1485
- // src/roles/format/config-policy.ts
1486
- var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
1487
- var FORMAT_SCRIPT_NAME = "format";
1488
- var PROVENANCE_KEY = "$sentinel";
1489
- var PERMITTED_LOCAL_KEYS = [
1490
- "useTabs",
1491
- "tabWidth",
1492
- "printWidth",
1493
- "semi",
1494
- "singleQuote",
1495
- "jsxSingleQuote",
1496
- "trailingComma",
1497
- "quoteProps",
1498
- "bracketSpacing",
1499
- "bracketSameLine",
1500
- "arrowParens",
1501
- "endOfLine",
1502
- "objectWrap",
1503
- "proseWrap",
1504
- "experimentalOperatorPosition"
1505
- ];
1506
- var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
1507
- var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
1508
- var PRETTIER_CONFIG_FILES = [
1509
- ".prettierrc",
1510
- ".prettierrc.json",
1511
- ".prettierrc.json5",
1512
- ".prettierrc.yml",
1513
- ".prettierrc.yaml",
1514
- ".prettierrc.js",
1515
- ".prettierrc.cjs",
1516
- ".prettierrc.mjs",
1517
- "prettier.config.js",
1518
- "prettier.config.cjs",
1519
- "prettier.config.mjs"
1520
- ];
1521
- function formatTargets() {
1522
- return {
1523
- [FORMAT_SCRIPT_NAME]: {
1524
- cache: true,
1525
- inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
1526
- },
1527
- [`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
1528
- };
1908
+ // src/core/named-scope.ts
1909
+ var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1910
+ function scopeFlags(paths, options = []) {
1911
+ if (paths.length === 0 || options.includes(TOLERATE_EMPTY_FLAG)) return [];
1912
+ return [TOLERATE_EMPTY_FLAG];
1529
1913
  }
1530
1914
 
1531
1915
  // src/roles/format/format-script.ts
@@ -1556,8 +1940,8 @@ function writesWhenRewritten(name, command) {
1556
1940
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1557
1941
 
1558
1942
  // src/roles/format/inherited-ignores.ts
1559
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
1560
- import { join as join11 } from "path";
1943
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
1944
+ import { join as join15 } from "path";
1561
1945
  var ROOT_IGNORE_FILE = ".prettierignore";
1562
1946
  function isPattern(line) {
1563
1947
  const trimmed = line.trim();
@@ -1572,11 +1956,11 @@ function toModulePattern(pattern) {
1572
1956
  }
1573
1957
  function inheritedIgnorePatterns(workspaceRoot) {
1574
1958
  if (!workspaceRoot) return [];
1575
- const path = join11(workspaceRoot, ROOT_IGNORE_FILE);
1576
- if (!existsSync10(path)) return [];
1959
+ const path = join15(workspaceRoot, ROOT_IGNORE_FILE);
1960
+ if (!existsSync14(path)) return [];
1577
1961
  let contents;
1578
1962
  try {
1579
- contents = readFileSync8(path, "utf8");
1963
+ contents = readFileSync11(path, "utf8");
1580
1964
  } catch {
1581
1965
  return [];
1582
1966
  }
@@ -1620,56 +2004,14 @@ function needsSvelteCompiler(output) {
1620
2004
  return /svelte\/compiler/.test(output);
1621
2005
  }
1622
2006
 
1623
- // src/roles/format/presets/base.json
1624
- var base_default = {
1625
- printWidth: 80,
1626
- semi: false,
1627
- singleQuote: true,
1628
- trailingComma: "all",
1629
- sortPackageJson: false,
1630
- sortImports: false
1631
- };
1632
-
1633
- // src/roles/format/presets/nest-imports.json
1634
- var nest_imports_default = {
1635
- customGroups: [
1636
- {
1637
- groupName: "workspace",
1638
- elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
1639
- }
1640
- ],
1641
- groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
1642
- newlinesBetween: true,
1643
- order: "asc",
1644
- ignoreCase: false
1645
- };
1646
-
1647
- // src/roles/format/preset-data.ts
1648
- var FORMAT_PRESETS = ["base", "svelte", "nest"];
1649
- function hasFormatPreset(name) {
1650
- return FORMAT_PRESETS.includes(name);
1651
- }
1652
- function formatPresetName(declared) {
1653
- if (declared === "svelte") return "svelte";
1654
- if (declared === "nest") return "nest";
1655
- return "base";
1656
- }
1657
- function formatPresetFor(preset) {
1658
- const base = base_default;
1659
- const name = formatPresetName(preset);
1660
- if (name === "svelte") return { ...base, svelte: true };
1661
- if (name === "nest") return { ...base, sortImports: nest_imports_default };
1662
- return base;
1663
- }
1664
-
1665
2007
  // src/roles/format/prettier-config.ts
1666
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1667
- import { join as join13 } from "path";
2008
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2009
+ import { join as join17 } from "path";
1668
2010
 
1669
2011
  // src/roles/format/resolve-oxfmt.ts
1670
- import { readFileSync as readFileSync9 } from "fs";
2012
+ import { readFileSync as readFileSync12 } from "fs";
1671
2013
  import { createRequire as createRequire2 } from "module";
1672
- import { dirname as dirname4, join as join12 } from "path";
2014
+ import { dirname as dirname4, join as join16 } from "path";
1673
2015
  function resolveOxfmt(cwd) {
1674
2016
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1675
2017
  }
@@ -1698,8 +2040,8 @@ function configSchema() {
1698
2040
  function readConfigSchema() {
1699
2041
  try {
1700
2042
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
1701
- const schemaPath = join12(dirname4(manifest), "configuration_schema.json");
1702
- return JSON.parse(readFileSync9(schemaPath, "utf8"));
2043
+ const schemaPath = join16(dirname4(manifest), "configuration_schema.json");
2044
+ return JSON.parse(readFileSync12(schemaPath, "utf8"));
1703
2045
  } catch {
1704
2046
  return void 0;
1705
2047
  }
@@ -1730,14 +2072,14 @@ function toOxfmtOverrides(value) {
1730
2072
  return { overrides, unresolved };
1731
2073
  }
1732
2074
  function readPrettierSettings(cwd) {
1733
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync11(join13(cwd, name)));
2075
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync15(join17(cwd, name)));
1734
2076
  if (!file) return { options: {}, unresolved: [] };
1735
2077
  if (/\.(js|cjs|mjs)$/.test(file)) {
1736
2078
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1737
2079
  }
1738
2080
  let parsed;
1739
2081
  try {
1740
- parsed = parseJsonc(readFileSync10(join13(cwd, file), "utf8"), file);
2082
+ parsed = parseJsonc(readFileSync13(join17(cwd, file), "utf8"), file);
1741
2083
  } catch {
1742
2084
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1743
2085
  }
@@ -1766,71 +2108,6 @@ function readPrettierSettings(cwd) {
1766
2108
  return { options, file, unresolved };
1767
2109
  }
1768
2110
 
1769
- // src/roles/format/read-adoption.ts
1770
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1771
- import { join as join14 } from "path";
1772
- var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
1773
- configFile,
1774
- preset: null,
1775
- adopted: false,
1776
- conformant: false,
1777
- drift: [],
1778
- local: {},
1779
- presetVersion: null,
1780
- effective: {},
1781
- unreadable
1782
- });
1783
- function sameValue(a, b) {
1784
- return JSON.stringify(a) === JSON.stringify(b);
1785
- }
1786
- function readFormatAdoption(cwd) {
1787
- const path = join14(cwd, FORMAT_CONFIG_FILE);
1788
- if (!existsSync12(path)) return NOT_ADOPTED2(null);
1789
- let parsed;
1790
- try {
1791
- parsed = parseJsonc(readFileSync11(path, "utf8"), FORMAT_CONFIG_FILE);
1792
- } catch (error) {
1793
- const reason = error instanceof Error ? error.message : String(error);
1794
- return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
1795
- }
1796
- const provenance = parsed[PROVENANCE_KEY];
1797
- if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
1798
- if (!hasFormatPreset(provenance.preset)) {
1799
- return NOT_ADOPTED2(
1800
- FORMAT_CONFIG_FILE,
1801
- `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
1802
- );
1803
- }
1804
- const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
1805
- const preset = formatPresetFor(provenance.preset);
1806
- const drift = [];
1807
- const local = {};
1808
- for (const [key, expected] of Object.entries(preset)) {
1809
- if (declaredLocal.includes(key)) {
1810
- local[key] = parsed[key];
1811
- continue;
1812
- }
1813
- if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
1814
- }
1815
- for (const key of declaredLocal) {
1816
- if (!(key in preset)) local[key] = parsed[key];
1817
- }
1818
- for (const key of ALWAYS_LOCAL_KEYS) {
1819
- if (key in parsed) local[key] = parsed[key];
1820
- }
1821
- return {
1822
- configFile: FORMAT_CONFIG_FILE,
1823
- preset: provenance.preset,
1824
- adopted: true,
1825
- conformant: drift.length === 0,
1826
- drift,
1827
- local,
1828
- presetVersion: typeof provenance.version === "string" ? provenance.version : null,
1829
- effective: parsed,
1830
- unreadable: null
1831
- };
1832
- }
1833
-
1834
2111
  // src/roles/format/unenforced.ts
1835
2112
  function unenforcedFormatOptions(options) {
1836
2113
  const gaps = [];
@@ -1913,7 +2190,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1913
2190
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1914
2191
  ];
1915
2192
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1916
- (name) => existsSync13(join15(context.cwd, name))
2193
+ (name) => existsSync16(join18(context.cwd, name))
1917
2194
  );
1918
2195
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1919
2196
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -2153,7 +2430,7 @@ var OxfmtAdapter = class extends BaseAdapter {
2153
2430
  modulePrettierDependencies(cwd) {
2154
2431
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
2155
2432
  try {
2156
- const manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
2433
+ const manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
2157
2434
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
2158
2435
  } catch {
2159
2436
  return [];
@@ -2235,8 +2512,8 @@ function registerFormat() {
2235
2512
 
2236
2513
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2237
2514
  import { spawnSync as spawnSync4 } from "child_process";
2238
- import { existsSync as existsSync21, readFileSync as readFileSync20, rmSync, writeFileSync as writeFileSync2 } from "fs";
2239
- import { join as join25 } from "path";
2515
+ import { existsSync as existsSync24, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2516
+ import { join as join28 } from "path";
2240
2517
 
2241
2518
  // src/core/config/deferred-rules.ts
2242
2519
  function deferredRuleNames(rules) {
@@ -2244,8 +2521,8 @@ function deferredRuleNames(rules) {
2244
2521
  }
2245
2522
 
2246
2523
  // src/core/fix-report.ts
2247
- import { statSync } from "fs";
2248
- import { join as join16 } from "path";
2524
+ import { statSync as statSync2 } from "fs";
2525
+ import { join as join19 } from "path";
2249
2526
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2250
2527
  function readFailure(output) {
2251
2528
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2256,7 +2533,7 @@ function readFailure(output) {
2256
2533
  }
2257
2534
  function fingerprint(path) {
2258
2535
  try {
2259
- const stat = statSync(path);
2536
+ const stat = statSync2(path);
2260
2537
  return `${stat.size}:${stat.mtimeMs}`;
2261
2538
  } catch {
2262
2539
  return void 0;
@@ -2265,7 +2542,7 @@ function fingerprint(path) {
2265
2542
  function snapshotFiles(cwd, extensions) {
2266
2543
  const snapshot = /* @__PURE__ */ new Map();
2267
2544
  for (const file of listSourceFiles(cwd, extensions)) {
2268
- const mark = fingerprint(join16(cwd, file));
2545
+ const mark = fingerprint(join19(cwd, file));
2269
2546
  if (mark !== void 0) snapshot.set(file, mark);
2270
2547
  }
2271
2548
  return snapshot;
@@ -2273,7 +2550,7 @@ function snapshotFiles(cwd, extensions) {
2273
2550
  function changedSince(cwd, before, extensions) {
2274
2551
  const changed = [];
2275
2552
  for (const file of listSourceFiles(cwd, extensions)) {
2276
- const now = fingerprint(join16(cwd, file));
2553
+ const now = fingerprint(join19(cwd, file));
2277
2554
  if (now === void 0) continue;
2278
2555
  if (before.get(file) !== now) changed.push(file);
2279
2556
  }
@@ -2292,8 +2569,8 @@ function describeFixOutcome(outcome, toolLabel) {
2292
2569
  }
2293
2570
 
2294
2571
  // src/roles/lint/module-baseline.ts
2295
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
2296
- import { join as join17 } from "path";
2572
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
2573
+ import { join as join20 } from "path";
2297
2574
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2298
2575
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2299
2576
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2337,11 +2614,11 @@ function describeHolds(holds) {
2337
2614
  return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
2338
2615
  }
2339
2616
  function heldRules(cwd) {
2340
- const path = join17(cwd, LINT_BASELINE_FILE);
2341
- if (!existsSync14(path)) return [];
2617
+ const path = join20(cwd, LINT_BASELINE_FILE);
2618
+ if (!existsSync17(path)) return [];
2342
2619
  try {
2343
2620
  const parsed = parseJsonc(
2344
- readFileSync13(path, "utf8"),
2621
+ readFileSync15(path, "utf8"),
2345
2622
  LINT_BASELINE_FILE
2346
2623
  );
2347
2624
  return Object.keys(parsed.rules ?? {});
@@ -2772,7 +3049,8 @@ var react_lib_default = {
2772
3049
  {
2773
3050
  name: "storybook",
2774
3051
  specifier: "eslint-plugin-storybook"
2775
- }
3052
+ },
3053
+ "../plugins/hublo.js"
2776
3054
  ],
2777
3055
  settings: null,
2778
3056
  renames: {
@@ -2781,6 +3059,7 @@ var react_lib_default = {
2781
3059
  rules: {
2782
3060
  curly: ["error", "all"],
2783
3061
  "for-direction": "error",
3062
+ "hublo/no-regex-lookbehind": "error",
2784
3063
  "jest-dom/prefer-checked": "error",
2785
3064
  "jest-dom/prefer-empty": "error",
2786
3065
  "jest-dom/prefer-enabled-disabled": "error",
@@ -3132,7 +3411,7 @@ var react_lib_default = {
3132
3411
  ],
3133
3412
  uncovered: {
3134
3413
  "no-octal": "no oxlint equivalent",
3135
- "no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, which is why the app tier hosts `../plugins/hublo.js`. The library tier enforces none of those rules today, so it hosts nothing.",
3414
+ "no-restricted-syntax": "no oxlint equivalent; the house rules it encoded ship as the local `hublo` plugin instead, so this tier hosts `../plugins/hublo.js` too. It enforces exactly one of them: `hublo/no-regex-lookbehind`, because a library is where that bug is WRITTEN and it reaches an app tier only by being imported (HUB-43431 shipped from libs/front/components into the worker app). The palette selectors stay app-tier only.",
3136
3415
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
3137
3416
  }
3138
3417
  };
@@ -3196,6 +3475,7 @@ var react_default = {
3196
3475
  severity: "off",
3197
3476
  reason: "the house rule finds 3 real violations; they are fixed as their own change rather than inside a linter swap, so the rule ships visible-but-off and can be switched on once they are"
3198
3477
  },
3478
+ "hublo/no-regex-lookbehind": "error",
3199
3479
  "jest/expect-expect": {
3200
3480
  severity: "off",
3201
3481
  reason: "pre-existing test-suite debt; enabling it on adoption would surface a wall of findings and make a lateral move look like a regression"
@@ -3682,7 +3962,7 @@ var react_default = {
3682
3962
  uncovered: {
3683
3963
  "import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
3684
3964
  "no-octal": "no oxlint equivalent",
3685
- "no-restricted-syntax": "no oxlint equivalent as configuration, so the eight AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
3965
+ "no-restricted-syntax": "no oxlint equivalent as configuration, so the AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Two are hosted today: the eight palette selectors as `hublo/no-palette-common`, and the lookbehind selector as `hublo/no-regex-lookbehind`. Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
3686
3966
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
3687
3967
  }
3688
3968
  };
@@ -4164,14 +4444,14 @@ function downgradedRulesFor(preset) {
4164
4444
  }
4165
4445
 
4166
4446
  // src/roles/lint/extra-layers.ts
4167
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4168
- import { isAbsolute as isAbsolute2, join as join18, resolve as resolve3 } from "path";
4447
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4448
+ import { isAbsolute as isAbsolute2, join as join21, resolve as resolve3 } from "path";
4169
4449
  function ruleCount(cwd, specifier) {
4170
4450
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4171
- if (!existsSync15(path)) return void 0;
4451
+ if (!existsSync18(path)) return void 0;
4172
4452
  try {
4173
4453
  const parsed = parseJsonc(
4174
- readFileSync14(path, "utf8"),
4454
+ readFileSync16(path, "utf8"),
4175
4455
  specifier
4176
4456
  );
4177
4457
  return Object.keys(parsed.rules ?? {}).length;
@@ -4191,7 +4471,7 @@ function extraLayers(cwd, extendsList) {
4191
4471
  function committedExtendsList(cwd, configFile) {
4192
4472
  try {
4193
4473
  const parsed = parseJsonc(
4194
- readFileSync14(join18(cwd, configFile), "utf8"),
4474
+ readFileSync16(join21(cwd, configFile), "utf8"),
4195
4475
  configFile
4196
4476
  );
4197
4477
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4250,8 +4530,8 @@ function errorCountsByConfigRule(stdout) {
4250
4530
  }
4251
4531
 
4252
4532
  // src/core/config/read-adoption.ts
4253
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4254
- import { join as join20 } from "path";
4533
+ import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
4534
+ import { join as join23 } from "path";
4255
4535
 
4256
4536
  // src/core/config/owned-keys.ts
4257
4537
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4266,12 +4546,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4266
4546
  }
4267
4547
 
4268
4548
  // src/core/config/resolve-config-target.ts
4269
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4270
- import { join as join19 } from "path";
4549
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4550
+ import { join as join22 } from "path";
4271
4551
  function readExtends(absolutePath) {
4272
4552
  let parsed;
4273
4553
  try {
4274
- parsed = parseJsonc(readFileSync15(absolutePath, "utf8"), absolutePath);
4554
+ parsed = parseJsonc(readFileSync17(absolutePath, "utf8"), absolutePath);
4275
4555
  } catch {
4276
4556
  return [];
4277
4557
  }
@@ -4285,8 +4565,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4285
4565
  let existing;
4286
4566
  let existingExtendsSomething = false;
4287
4567
  for (const candidate of candidates) {
4288
- const absolutePath = join19(moduleDir, candidate);
4289
- if (!existsSync16(absolutePath)) continue;
4568
+ const absolutePath = join22(moduleDir, candidate);
4569
+ if (!existsSync19(absolutePath)) continue;
4290
4570
  const chain = readExtends(absolutePath);
4291
4571
  if (existing === void 0) {
4292
4572
  existing = candidate;
@@ -4319,12 +4599,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4319
4599
  });
4320
4600
  function readAdoption(cwd, options) {
4321
4601
  const target = resolveConfigTarget(cwd, options);
4322
- if (target.reason === "none" || !existsSync17(join20(cwd, target.path))) {
4602
+ if (target.reason === "none" || !existsSync20(join23(cwd, target.path))) {
4323
4603
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4324
4604
  }
4325
4605
  let parsed;
4326
4606
  try {
4327
- parsed = parseJsonc(readFileSync16(join20(cwd, target.path), "utf8"), target.path);
4607
+ parsed = parseJsonc(readFileSync18(join23(cwd, target.path), "utf8"), target.path);
4328
4608
  } catch (error) {
4329
4609
  const reason = error instanceof Error ? error.message : String(error);
4330
4610
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4358,9 +4638,9 @@ function readLintAdoption(cwd) {
4358
4638
  }
4359
4639
 
4360
4640
  // src/roles/lint/resolve-oxlint.ts
4361
- import { existsSync as existsSync18 } from "fs";
4641
+ import { existsSync as existsSync21 } from "fs";
4362
4642
  import { createRequire as createRequire3 } from "module";
4363
- import { delimiter as delimiter2, dirname as dirname5, join as join21 } from "path";
4643
+ import { delimiter as delimiter2, dirname as dirname5, join as join24 } from "path";
4364
4644
  import { fileURLToPath as fileURLToPath2 } from "url";
4365
4645
  var PACKAGE_OF = {
4366
4646
  oxlint: "oxlint",
@@ -4385,30 +4665,30 @@ function tsgolintShim(cwd) {
4385
4665
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4386
4666
  try {
4387
4667
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4388
- candidates.push(join21(packageDir, "node_modules", ".bin", "tsgolint"));
4389
- candidates.push(join21(packageDir, "..", ".bin", "tsgolint"));
4668
+ candidates.push(join24(packageDir, "node_modules", ".bin", "tsgolint"));
4669
+ candidates.push(join24(packageDir, "..", ".bin", "tsgolint"));
4390
4670
  } catch {
4391
4671
  }
4392
4672
  }
4393
4673
  try {
4394
4674
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4395
- candidates.push(join21(ownRoot, "node_modules", ".bin", "tsgolint"));
4675
+ candidates.push(join24(ownRoot, "node_modules", ".bin", "tsgolint"));
4396
4676
  } catch {
4397
4677
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4398
4678
  }
4399
- return candidates.find((candidate) => candidate !== "" && existsSync18(candidate));
4679
+ return candidates.find((candidate) => candidate !== "" && existsSync21(candidate));
4400
4680
  }
4401
4681
  function oxlintSearchPath(cwd) {
4402
4682
  return binSearchPath(cwd);
4403
4683
  }
4404
4684
 
4405
4685
  // src/roles/lint/adapters/oxlint/plan.ts
4406
- import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
4407
- import { join as join24 } from "path";
4686
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
4687
+ import { join as join27 } from "path";
4408
4688
 
4409
4689
  // src/roles/lint/eslint-ignores.ts
4410
- import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4411
- import { join as join22 } from "path";
4690
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
4691
+ import { join as join25 } from "path";
4412
4692
  import { parseSync as parseSync2 } from "oxc-parser";
4413
4693
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4414
4694
  function toOxlintPattern(pattern) {
@@ -4420,11 +4700,11 @@ function readRootEslintIgnores(root) {
4420
4700
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4421
4701
  }
4422
4702
  function readEslintIgnores(cwd) {
4423
- const config = ESLINT_CONFIG_FILES.map((name) => join22(cwd, name)).find((path) => existsSync19(path));
4703
+ const config = ESLINT_CONFIG_FILES.map((name) => join25(cwd, name)).find((path) => existsSync22(path));
4424
4704
  if (!config) return { patterns: [], unresolved: [] };
4425
4705
  let source;
4426
4706
  try {
4427
- source = readFileSync17(config, "utf8");
4707
+ source = readFileSync19(config, "utf8");
4428
4708
  } catch {
4429
4709
  return { patterns: [], unresolved: [] };
4430
4710
  }
@@ -4525,8 +4805,8 @@ function lintPresetFor(preset) {
4525
4805
  }
4526
4806
 
4527
4807
  // src/roles/lint/rename-suppressions.ts
4528
- import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
4529
- import { join as join23, relative as relative5 } from "path";
4808
+ import { readdirSync as readdirSync2, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
4809
+ import { join as join26, relative as relative4 } from "path";
4530
4810
  var SOURCE_EXTENSIONS = [
4531
4811
  ".ts",
4532
4812
  ".tsx",
@@ -4573,10 +4853,10 @@ function* sourceFiles(dir) {
4573
4853
  return;
4574
4854
  }
4575
4855
  for (const entry of entries) {
4576
- const full = join23(dir, entry);
4856
+ const full = join26(dir, entry);
4577
4857
  let isDirectory;
4578
4858
  try {
4579
- isDirectory = statSync2(full).isDirectory();
4859
+ isDirectory = statSync3(full).isDirectory();
4580
4860
  } catch {
4581
4861
  continue;
4582
4862
  }
@@ -4593,7 +4873,7 @@ function findSuppressionRenames(cwd, renames) {
4593
4873
  for (const file of sourceFiles(cwd)) {
4594
4874
  let content;
4595
4875
  try {
4596
- content = readFileSync18(file, "utf8");
4876
+ content = readFileSync20(file, "utf8");
4597
4877
  } catch {
4598
4878
  continue;
4599
4879
  }
@@ -4617,7 +4897,7 @@ function findSuppressionRenames(cwd, renames) {
4617
4897
  }
4618
4898
  if (hits.length === 0) return;
4619
4899
  found.push({
4620
- file: relative5(cwd, file),
4900
+ file: relative4(cwd, file),
4621
4901
  line: index + 1,
4622
4902
  from: line,
4623
4903
  to: line.replace(rules, renamed.join(", ")),
@@ -4721,7 +5001,7 @@ function plan2(context) {
4721
5001
  keys: removableDeps
4722
5002
  });
4723
5003
  }
4724
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync20(join24(context.cwd, name)));
5004
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync23(join27(context.cwd, name)));
4725
5005
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4726
5006
  operations.push(
4727
5007
  ...nxTargetOperations({
@@ -4819,7 +5099,7 @@ function lintScripts(cwd) {
4819
5099
  function committedExtends(cwd) {
4820
5100
  try {
4821
5101
  const parsed = parseJsonc(
4822
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5102
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4823
5103
  LINT_CONFIG_FILE
4824
5104
  );
4825
5105
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4830,7 +5110,7 @@ function committedExtends(cwd) {
4830
5110
  function committedIgnorePatterns(cwd) {
4831
5111
  try {
4832
5112
  const parsed = parseJsonc(
4833
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
5113
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
4834
5114
  LINT_CONFIG_FILE
4835
5115
  );
4836
5116
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4842,7 +5122,7 @@ function moduleEslintDependencies(cwd) {
4842
5122
  const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
4843
5123
  let manifest;
4844
5124
  try {
4845
- manifest = JSON.parse(readFileSync19(join24(cwd, "package.json"), "utf8"));
5125
+ manifest = JSON.parse(readFileSync21(join27(cwd, "package.json"), "utf8"));
4846
5126
  } catch {
4847
5127
  return [];
4848
5128
  }
@@ -4976,23 +5256,23 @@ ${result.stderr ?? ""}`);
4976
5256
  * build the developer can see, rather than a silent half-adoption they cannot.
4977
5257
  */
4978
5258
  writeModuleBaseline(ctx, oxlint, env) {
4979
- const configPath = join25(ctx.cwd, LINT_CONFIG_FILE);
4980
- const baselinePath = join25(ctx.cwd, LINT_BASELINE_FILE);
4981
- if (!existsSync21(configPath)) return;
5259
+ const configPath = join28(ctx.cwd, LINT_CONFIG_FILE);
5260
+ const baselinePath = join28(ctx.cwd, LINT_BASELINE_FILE);
5261
+ if (!existsSync24(configPath)) return;
4982
5262
  let config;
4983
5263
  try {
4984
5264
  config = parseJsonc(
4985
- readFileSync20(configPath, "utf8"),
5265
+ readFileSync22(configPath, "utf8"),
4986
5266
  LINT_CONFIG_FILE
4987
5267
  );
4988
5268
  } catch {
4989
5269
  return;
4990
5270
  }
4991
5271
  const current = Array.isArray(config.extends) ? config.extends : [];
4992
- const measurePath = join25(ctx.cwd, LINT_MEASURE_FILE);
5272
+ const measurePath = join28(ctx.cwd, LINT_MEASURE_FILE);
4993
5273
  let measured;
4994
5274
  try {
4995
- writeFileSync2(
5275
+ writeFileSync3(
4996
5276
  measurePath,
4997
5277
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
4998
5278
  `
@@ -5010,7 +5290,7 @@ ${result.stderr ?? ""}`);
5010
5290
  { cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
5011
5291
  );
5012
5292
  } finally {
5013
- rmSync(measurePath, { force: true });
5293
+ rmSync2(measurePath, { force: true });
5014
5294
  }
5015
5295
  if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
5016
5296
  process.stderr.write(
@@ -5024,20 +5304,20 @@ ${result.stderr ?? ""}`);
5024
5304
  const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
5025
5305
  const next = extendsWithBaseline(current, holds.length > 0);
5026
5306
  if (holds.length > 0) {
5027
- writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
5307
+ writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
5028
5308
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
5029
5309
  `);
5030
5310
  } else {
5031
- rmSync(baselinePath, { force: true });
5311
+ rmSync2(baselinePath, { force: true });
5032
5312
  }
5033
5313
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
5034
5314
  config.extends = next;
5035
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
5315
+ writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
5036
5316
  `);
5037
5317
  }
5038
5318
  }
5039
5319
  async run(ctx) {
5040
- if (!existsSync21(join25(ctx.cwd, LINT_CONFIG_FILE))) {
5320
+ if (!existsSync24(join28(ctx.cwd, LINT_CONFIG_FILE))) {
5041
5321
  process.stderr.write(
5042
5322
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
5043
5323
  `
@@ -5185,7 +5465,7 @@ ${result.stderr ?? ""}`);
5185
5465
  let stub;
5186
5466
  try {
5187
5467
  stub = parseJsonc(
5188
- readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
5468
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5189
5469
  LINT_CONFIG_FILE
5190
5470
  );
5191
5471
  } catch {
@@ -5195,7 +5475,7 @@ ${result.stderr ?? ""}`);
5195
5475
  for (const entry of stub.extends ?? []) {
5196
5476
  try {
5197
5477
  const preset = parseJsonc(
5198
- readFileSync20(join25(cwd, entry), "utf8"),
5478
+ readFileSync22(join28(cwd, entry), "utf8"),
5199
5479
  entry
5200
5480
  );
5201
5481
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5265,14 +5545,14 @@ ${result.stderr ?? ""}`);
5265
5545
  let parsed;
5266
5546
  try {
5267
5547
  parsed = parseJsonc(
5268
- readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
5548
+ readFileSync22(join28(cwd, LINT_CONFIG_FILE), "utf8"),
5269
5549
  LINT_CONFIG_FILE
5270
5550
  );
5271
5551
  } catch {
5272
5552
  return void 0;
5273
5553
  }
5274
5554
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5275
- return targets.find((target) => !existsSync21(join25(cwd, target)));
5555
+ return targets.find((target) => !existsSync24(join28(cwd, target)));
5276
5556
  }
5277
5557
  /** Announce what is not enforced, so reduced coverage is never silent. */
5278
5558
  announceDisabled(preset) {
@@ -5304,9 +5584,9 @@ function registerLint() {
5304
5584
 
5305
5585
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5306
5586
  import { spawnSync as spawnSync5 } from "child_process";
5307
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
5587
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
5308
5588
  import { createRequire as createRequire4 } from "module";
5309
- import { join as join27 } from "path";
5589
+ import { join as join30 } from "path";
5310
5590
 
5311
5591
  // src/roles/typescript/presets/nest.json
5312
5592
  var nest_default2 = {
@@ -5487,8 +5767,8 @@ function readTsconfigAdoption(cwd) {
5487
5767
  }
5488
5768
 
5489
5769
  // src/roles/typescript/adapters/tsc/plan.ts
5490
- import { readFileSync as readFileSync21 } from "fs";
5491
- import { join as join26 } from "path";
5770
+ import { readFileSync as readFileSync23 } from "fs";
5771
+ import { join as join29 } from "path";
5492
5772
 
5493
5773
  // src/roles/typescript/typecheck-script.ts
5494
5774
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5581,7 +5861,7 @@ function planAdoption(context) {
5581
5861
  };
5582
5862
  }
5583
5863
  const existing = parseJsonc(
5584
- readFileSync21(join26(context.cwd, target.path), "utf8"),
5864
+ readFileSync23(join29(context.cwd, target.path), "utf8"),
5585
5865
  target.path
5586
5866
  );
5587
5867
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5719,7 +5999,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5719
5999
  let chain;
5720
6000
  try {
5721
6001
  const parsed = parseJsonc(
5722
- readFileSync22(join27(cwd, target.path), "utf8"),
6002
+ readFileSync24(join30(cwd, target.path), "utf8"),
5723
6003
  target.path
5724
6004
  );
5725
6005
  chain = parsed.extends;
@@ -5732,7 +6012,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5732
6012
  );
5733
6013
  if (preset === void 0) return void 0;
5734
6014
  try {
5735
- createRequire4(join27(cwd, "noop.js")).resolve(preset);
6015
+ createRequire4(join30(cwd, "noop.js")).resolve(preset);
5736
6016
  return void 0;
5737
6017
  } catch {
5738
6018
  return preset;
@@ -5828,7 +6108,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5828
6108
  referencedProjects(cwd, config) {
5829
6109
  try {
5830
6110
  const parsed = parseJsonc(
5831
- readFileSync22(join27(cwd, config), "utf8"),
6111
+ readFileSync24(join30(cwd, config), "utf8"),
5832
6112
  config
5833
6113
  );
5834
6114
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5844,7 +6124,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5844
6124
  * check.
5845
6125
  */
5846
6126
  typecheckTarget(cwd) {
5847
- if (existsSync22(join27(cwd, "tsconfig.json"))) return "tsconfig.json";
6127
+ if (existsSync25(join30(cwd, "tsconfig.json"))) return "tsconfig.json";
5848
6128
  const target = resolveTsconfigTarget(cwd);
5849
6129
  return target.reason === "none" ? null : target.path;
5850
6130
  }
@@ -6048,7 +6328,7 @@ function replaceLines(current, replacements) {
6048
6328
  }
6049
6329
 
6050
6330
  // src/core/apply-plan.ts
6051
- import { existsSync as existsSync23, readFileSync as readFileSync23, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6331
+ import { existsSync as existsSync26, readFileSync as readFileSync25, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
6052
6332
  import { resolve as resolve4, sep } from "path";
6053
6333
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6054
6334
 
@@ -6067,7 +6347,7 @@ function resolveWithinRoot(cwd, relativePath) {
6067
6347
  return absolutePath;
6068
6348
  }
6069
6349
  function readIfExists(absolutePath) {
6070
- return existsSync23(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
6350
+ return existsSync26(absolutePath) ? readFileSync25(absolutePath, "utf8") : void 0;
6071
6351
  }
6072
6352
  function* leaves(value, prefix = []) {
6073
6353
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6169,14 +6449,14 @@ function preparePlan(cwd, plan3) {
6169
6449
  }
6170
6450
  function writeFileAtomic(absolutePath, contents) {
6171
6451
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
6172
- writeFileSync3(tempPath, contents);
6452
+ writeFileSync4(tempPath, contents);
6173
6453
  renameSync(tempPath, absolutePath);
6174
6454
  }
6175
6455
  function applyPlan(cwd, plan3) {
6176
6456
  const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
6177
6457
  for (const file of changed) {
6178
6458
  if (file.deleted) {
6179
- rmSync2(file.absolutePath, { force: true });
6459
+ rmSync3(file.absolutePath, { force: true });
6180
6460
  continue;
6181
6461
  }
6182
6462
  writeFileAtomic(file.absolutePath, file.after);
@@ -6185,8 +6465,8 @@ function applyPlan(cwd, plan3) {
6185
6465
  }
6186
6466
 
6187
6467
  // src/core/config/preset-evidence.ts
6188
- import { existsSync as existsSync24, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "fs";
6189
- import { join as join28 } from "path";
6468
+ import { existsSync as existsSync27, readdirSync as readdirSync3, readFileSync as readFileSync26 } from "fs";
6469
+ import { join as join31 } from "path";
6190
6470
  var PATH_SIGNALS = [
6191
6471
  {
6192
6472
  preset: "nest",
@@ -6200,11 +6480,11 @@ var DEPENDENCY_SIGNALS = [
6200
6480
  { preset: "nest", pattern: /^@nestjs\// }
6201
6481
  ];
6202
6482
  function dependencyNames(cwd) {
6203
- const path = join28(cwd, "package.json");
6204
- if (!existsSync24(path)) return [];
6483
+ const path = join31(cwd, "package.json");
6484
+ if (!existsSync27(path)) return [];
6205
6485
  try {
6206
6486
  const manifest = parseJsonc(
6207
- readFileSync24(path, "utf8"),
6487
+ readFileSync26(path, "utf8"),
6208
6488
  path
6209
6489
  );
6210
6490
  return [
@@ -6227,7 +6507,7 @@ function declaresJsx(cwd) {
6227
6507
  for (const name of entries) {
6228
6508
  try {
6229
6509
  const config = parseJsonc(
6230
- readFileSync24(join28(cwd, name), "utf8"),
6510
+ readFileSync26(join31(cwd, name), "utf8"),
6231
6511
  name
6232
6512
  );
6233
6513
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6386,25 +6666,25 @@ export {
6386
6666
  declaredPresetFor,
6387
6667
  availableTargets,
6388
6668
  resolve,
6389
- BaseAdapter,
6390
- resolveBin,
6391
6669
  readOwnVersion,
6392
6670
  readProjectPackageJson,
6393
6671
  moduleName,
6394
6672
  buildConfigFile,
6673
+ BaseAdapter,
6674
+ resolveBin,
6675
+ WORKSPACE_ROOT_MARKER,
6676
+ findWorkspaceRoot,
6677
+ ensureWorkspacePrep,
6678
+ inspectWorkspacePrep,
6395
6679
  VERBS,
6396
6680
  TARGETS,
6397
6681
  LONG_RUNNING_TARGETS,
6398
6682
  SWEEPABLE_TARGETS,
6399
6683
  PRESET_NAMES,
6400
- WORKSPACE_ROOT_MARKER,
6401
- findWorkspaceRoot,
6402
- ensureWorkspacePrep,
6403
- inspectWorkspacePrep,
6404
6684
  palette,
6405
6685
  registerAdapters,
6406
6686
  describeFramework,
6407
6687
  detectFramework,
6408
6688
  dispatch
6409
6689
  };
6410
- //# sourceMappingURL=chunk-6RH3LZQZ.js.map
6690
+ //# sourceMappingURL=chunk-Z5I6SEKF.js.map