@hublo/sentinel 1.2.0-alpha.27 → 1.2.0-alpha.29

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.
@@ -22,7 +22,7 @@ import {
22
22
  registerAdapters,
23
23
  resolve,
24
24
  resolveBin
25
- } from "../chunk-LNSAUFIM.js";
25
+ } from "../chunk-37Y7RALA.js";
26
26
 
27
27
  // bin/sentinel.ts
28
28
  import { program } from "commander";
@@ -298,6 +298,19 @@ function manifestOperation(cwd, scripts) {
298
298
  };
299
299
  }
300
300
 
301
+ // src/core/config/project-json.ts
302
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
303
+ import { join as join4 } from "path";
304
+ function projectTargets(cwd) {
305
+ const path = join4(cwd, "project.json");
306
+ if (!existsSync4(path)) return void 0;
307
+ try {
308
+ return JSON.parse(readFileSync4(path, "utf8")).targets;
309
+ } catch {
310
+ return void 0;
311
+ }
312
+ }
313
+
301
314
  // src/roles/build/presets/requirements.json
302
315
  var requirements_default = {
303
316
  svelte: {
@@ -413,19 +426,6 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
413
426
  (entry) => entry.package
414
427
  );
415
428
 
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;
422
- try {
423
- return JSON.parse(readFileSync4(path, "utf8")).targets;
424
- } catch {
425
- return void 0;
426
- }
427
- }
428
-
429
429
  // src/roles/build/adoption-drift.ts
430
430
  function buildAdoptionDrift(cwd) {
431
431
  const drift = [];
@@ -5582,12 +5582,356 @@ function registerLint() {
5582
5582
  setDefaultRunner("lint", "oxlint");
5583
5583
  }
5584
5584
 
5585
- // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5585
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5586
5586
  import { spawnSync as spawnSync5 } from "child_process";
5587
- import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
5588
- import { createRequire as createRequire4 } from "module";
5587
+ import { existsSync as existsSync26 } from "fs";
5589
5588
  import { join as join30 } from "path";
5590
5589
 
5590
+ // src/roles/test/presets/toolchain.json
5591
+ var toolchain_default2 = {
5592
+ $comment: "The test toolchain sentinel owns. Same shape and same rule as the build one: a single table drives what `--init` removes from a module's package.json, which import specifiers move, and the name each default import is re-exported under, so the three cannot disagree.",
5593
+ owned: [
5594
+ {
5595
+ package: "vitest",
5596
+ specifier: "vitest/config",
5597
+ named: ["defineConfig", "mergeConfig"],
5598
+ types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
5599
+ why: "the runner itself, declared at the workspace ROOT today, which is what stops a module being testable on its own. Note the package is `vitest` and the specifier is `vitest/config`: the same split as nitro on the build side, and the reason one table holds both."
5600
+ },
5601
+ {
5602
+ package: "vite",
5603
+ specifier: "vite",
5604
+ named: ["loadEnv"],
5605
+ why: "`loadEnv` has no equivalent in `vitest/config`, and two of the six configs import it from vite to build their test env. Re-exported here rather than claimed: the BUILD role owns the `vite` package, and two roles owning one package is how a version conflict gets written down twice."
5606
+ }
5607
+ ]
5608
+ };
5609
+
5610
+ // src/roles/test/preset-data.ts
5611
+ var OWNED_TEST_PACKAGES = toolchain_default2.owned;
5612
+
5613
+ // src/roles/test/config-policy.ts
5614
+ var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
5615
+ var TEST_CONFIG_FILES = [
5616
+ "vitest.config.ts",
5617
+ "vitest.config.mts",
5618
+ "vitest.config.js",
5619
+ "vitest.config.mjs"
5620
+ ];
5621
+ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
5622
+ var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
5623
+ (entry) => entry.package !== "vite"
5624
+ ).map((entry) => entry.package);
5625
+ var TEST_TOOLCHAIN = {
5626
+ presetSpecifier: TEST_PRESET_SPECIFIER,
5627
+ owned: TEST_OWNED_PACKAGES
5628
+ };
5629
+ var TEST_SCRIPT_NAME = "test";
5630
+ var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
5631
+ var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
5632
+ function rewriteVitestScript(command) {
5633
+ return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
5634
+ const args = tail.trim().split(/\s+/).filter(Boolean);
5635
+ const oneShot = args[0] === "run" || args.includes("--run");
5636
+ const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
5637
+ const passthrough = oneShot ? rest : ["--watch", ...rest];
5638
+ const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
5639
+ const trailing = /\s$/.test(tail) ? " " : "";
5640
+ return `${lead}sentinel --run --test${suffix}${trailing}`;
5641
+ });
5642
+ }
5643
+ function testTarget(options) {
5644
+ const { configFiles, outputs } = options;
5645
+ const cached = outputs !== void 0 && outputs.length > 0;
5646
+ return {
5647
+ [TEST_SCRIPT_NAME]: {
5648
+ cache: cached,
5649
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
5650
+ ...cached ? { outputs: [...outputs] } : {}
5651
+ }
5652
+ };
5653
+ }
5654
+
5655
+ // src/roles/test/read-adoption.ts
5656
+ import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
5657
+ import { join as join29 } from "path";
5658
+ var JEST_CONFIG_FILES = [
5659
+ "jest.config.ts",
5660
+ "jest.config.js",
5661
+ "jest.prisma.config.ts",
5662
+ "jest.functional.config.ts",
5663
+ "jest.integration-config.ts"
5664
+ ];
5665
+ function testConfigFile(cwd) {
5666
+ return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
5667
+ }
5668
+ function jestConfigFiles(cwd) {
5669
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
5670
+ }
5671
+ function rootJestConfigFiles(cwd) {
5672
+ const root = findWorkspaceRoot(cwd);
5673
+ if (root === void 0 || root === cwd) return [];
5674
+ return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
5675
+ }
5676
+ var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5677
+ var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
5678
+ function hasTestFiles(cwd) {
5679
+ const stack = [cwd];
5680
+ while (stack.length > 0) {
5681
+ const dir = stack.pop();
5682
+ let entries;
5683
+ try {
5684
+ entries = readdirSync3(dir, { withFileTypes: true });
5685
+ } catch {
5686
+ continue;
5687
+ }
5688
+ for (const entry of entries) {
5689
+ if (entry.isDirectory()) {
5690
+ if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
5691
+ } else if (TEST_FILE.test(entry.name)) {
5692
+ return true;
5693
+ }
5694
+ }
5695
+ }
5696
+ return false;
5697
+ }
5698
+ function readTestConfigSource(cwd, fileName) {
5699
+ try {
5700
+ return readFileSync23(join29(cwd, fileName), "utf8");
5701
+ } catch {
5702
+ return "";
5703
+ }
5704
+ }
5705
+ function importsPreset2(source) {
5706
+ const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5707
+ return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
5708
+ }
5709
+ function readTestAdoption(cwd) {
5710
+ const manifest = readProjectPackageJson(cwd);
5711
+ const jestConfigs = jestConfigFiles(cwd);
5712
+ const configFile = testConfigFile(cwd);
5713
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
5714
+ const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
5715
+ if (configFile === void 0) {
5716
+ const hasTests = hasTestFiles(cwd);
5717
+ const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
5718
+ return {
5719
+ configFile: null,
5720
+ preset: null,
5721
+ adopted: false,
5722
+ conformant: false,
5723
+ drift: [],
5724
+ unreadable: null,
5725
+ state: hasTests ? "jest" : "no-tests",
5726
+ runner: hasTests ? "jest" : "none",
5727
+ jestConfigs,
5728
+ inheritsRootConfig,
5729
+ ownDeclarations
5730
+ };
5731
+ }
5732
+ const source = readTestConfigSource(cwd, configFile);
5733
+ if (source === "") {
5734
+ return {
5735
+ configFile,
5736
+ preset: null,
5737
+ adopted: false,
5738
+ conformant: false,
5739
+ drift: [],
5740
+ unreadable: `${configFile} could not be read`,
5741
+ state: "vitest-own-toolchain",
5742
+ runner: "vitest",
5743
+ jestConfigs,
5744
+ inheritsRootConfig: false,
5745
+ // it has a config of its own, readable or not
5746
+ ownDeclarations
5747
+ };
5748
+ }
5749
+ const adopted = importsPreset2(source);
5750
+ const script = moduleScripts(cwd)["test"];
5751
+ return {
5752
+ configFile,
5753
+ preset: adopted ? "react" : null,
5754
+ adopted,
5755
+ conformant: adopted && script !== void 0 && jestConfigs.length === 0,
5756
+ drift: [],
5757
+ unreadable: null,
5758
+ state: adopted ? "adopted" : "vitest-own-toolchain",
5759
+ runner: "vitest",
5760
+ jestConfigs,
5761
+ inheritsRootConfig: false,
5762
+ // local always wins: this module is out of the root's reach
5763
+ ownDeclarations
5764
+ };
5765
+ }
5766
+
5767
+ // src/roles/test/plan.ts
5768
+ function ownedTestDependencies(cwd, declared) {
5769
+ return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
5770
+ ["dependencies", entry.name],
5771
+ ["devDependencies", entry.name]
5772
+ ]).filter(([section]) => section !== void 0);
5773
+ }
5774
+ function plan3(context) {
5775
+ const configFile = testConfigFile(context.cwd);
5776
+ const adoption = readTestAdoption(context.cwd);
5777
+ if (configFile === void 0) {
5778
+ return {
5779
+ operations: [],
5780
+ skipped: adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}). Migrating it moves its test FILES as well as its config, which is the next step of this role and not this one.` : `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
5781
+ };
5782
+ }
5783
+ const operations = [];
5784
+ const notes = [];
5785
+ const rewrite = rewriteToolchainImports(
5786
+ TEST_TOOLCHAIN,
5787
+ readTestConfigSource(context.cwd, configFile),
5788
+ configFile
5789
+ );
5790
+ if (rewrite.kind === "blocked") {
5791
+ return {
5792
+ operations: [],
5793
+ skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
5794
+ };
5795
+ }
5796
+ if (rewrite.kind === "rewritten") {
5797
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
5798
+ notes.push(
5799
+ `${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
5800
+ );
5801
+ }
5802
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
5803
+ const outputs = existing?.outputs ?? [];
5804
+ if (outputs.length === 0) {
5805
+ notes.push(
5806
+ `this target declares no \`outputs\`, so it is left UNCACHED. Caching a run that restores nothing is the defect where nx reports a hit and the coverage directory is empty; if this module writes coverage, declare where and the target can cache.`
5807
+ );
5808
+ }
5809
+ const siblingScripts = {};
5810
+ for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
5811
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
5812
+ const rewritten = rewriteVitestScript(command);
5813
+ if (rewritten !== command) siblingScripts[name] = rewritten;
5814
+ }
5815
+ if (Object.keys(siblingScripts).length > 0) {
5816
+ notes.push(
5817
+ `rewrote ${Object.keys(siblingScripts).join(", ")} to run through sentinel: they called vitest directly, and the dependency they relied on is removed below. A watch script keeps watching, through \`-- --watch\`.`
5818
+ );
5819
+ }
5820
+ operations.push(
5821
+ manifestOperation(context.cwd, {
5822
+ [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
5823
+ ...siblingScripts
5824
+ })
5825
+ );
5826
+ operations.push(
5827
+ ...nxTargetOperations({
5828
+ cwd: context.cwd,
5829
+ targets: testTarget({ configFiles: [configFile], outputs })
5830
+ })
5831
+ );
5832
+ const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
5833
+ if (owned.length > 0) {
5834
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
5835
+ notes.push(
5836
+ `removed ${[...new Set(owned.map(([, name]) => name))].join(", ")} from this module: sentinel owns the runner now, and two copies of Vitest in one run give the plugins a different Vite than the one running them.`
5837
+ );
5838
+ }
5839
+ if (adoption.jestConfigs.length > 0) {
5840
+ notes.push(
5841
+ `${adoption.jestConfigs.join(", ")} is still here beside the Vitest config. It is not this step's to delete, since something may still run it, but the workspace cannot drop jest while it exists.`
5842
+ );
5843
+ }
5844
+ return { operations, notes };
5845
+ }
5846
+
5847
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
5848
+ function resolveVitest(cwd) {
5849
+ const fromSentinel = join30(
5850
+ new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
5851
+ );
5852
+ if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
5853
+ const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
5854
+ return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
5855
+ }
5856
+ var VitestAdapter = class extends BaseAdapter {
5857
+ target = "test";
5858
+ runner = "vitest";
5859
+ appliesTo(_preset) {
5860
+ return true;
5861
+ }
5862
+ plan(context) {
5863
+ return plan3(context);
5864
+ }
5865
+ async status(ctx) {
5866
+ return Promise.resolve(readTestAdoption(ctx.cwd));
5867
+ }
5868
+ async inspect(ctx) {
5869
+ const adoption = readTestAdoption(ctx.cwd);
5870
+ return Promise.resolve({
5871
+ state: adoption.state,
5872
+ runner: adoption.runner,
5873
+ configFile: adoption.configFile,
5874
+ jestConfigs: adoption.jestConfigs,
5875
+ adopted: adoption.adopted,
5876
+ conformant: adoption.conformant,
5877
+ drift: adoption.drift,
5878
+ unreadable: adoption.unreadable
5879
+ });
5880
+ }
5881
+ /**
5882
+ * Run the module's tests.
5883
+ *
5884
+ * A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
5885
+ * with no named target sweeps the workspace, so failing here would exit non-zero on the 107
5886
+ * modules still on jest, which says nothing anyone can act on.
5887
+ */
5888
+ async run(ctx) {
5889
+ const adoption = readTestAdoption(ctx.cwd);
5890
+ if (!adoption.adopted) {
5891
+ this.say(
5892
+ adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
5893
+ );
5894
+ return Promise.resolve({ ok: true, code: 0 });
5895
+ }
5896
+ const { bin, origin } = resolveVitest(ctx.cwd);
5897
+ if (bin === void 0) {
5898
+ this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
5899
+ return Promise.resolve({ ok: false, code: 1 });
5900
+ }
5901
+ if (origin === "module") {
5902
+ this.say(
5903
+ `using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
5904
+ );
5905
+ }
5906
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
5907
+ const watching = options.includes("--watch") || options.includes("-w");
5908
+ const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
5909
+ const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
5910
+ if (result.error) {
5911
+ this.say(`could not run vitest (${result.error.message})`);
5912
+ return Promise.resolve({ ok: false, code: 1 });
5913
+ }
5914
+ const code = result.status ?? 1;
5915
+ return Promise.resolve({ ok: code === 0, code });
5916
+ }
5917
+ say(message) {
5918
+ process.stderr.write(`sentinel test(vitest): ${message}
5919
+ `);
5920
+ }
5921
+ };
5922
+
5923
+ // src/roles/test/register.ts
5924
+ function registerTest() {
5925
+ register(new VitestAdapter());
5926
+ setDefaultRunner("test", "vitest");
5927
+ }
5928
+
5929
+ // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5930
+ import { spawnSync as spawnSync6 } from "child_process";
5931
+ import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
5932
+ import { createRequire as createRequire4 } from "module";
5933
+ import { join as join32 } from "path";
5934
+
5591
5935
  // src/roles/typescript/presets/nest.json
5592
5936
  var nest_default2 = {
5593
5937
  compilerOptions: {
@@ -5767,8 +6111,8 @@ function readTsconfigAdoption(cwd) {
5767
6111
  }
5768
6112
 
5769
6113
  // src/roles/typescript/adapters/tsc/plan.ts
5770
- import { readFileSync as readFileSync23 } from "fs";
5771
- import { join as join29 } from "path";
6114
+ import { readFileSync as readFileSync24 } from "fs";
6115
+ import { join as join31 } from "path";
5772
6116
 
5773
6117
  // src/roles/typescript/typecheck-script.ts
5774
6118
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5861,7 +6205,7 @@ function planAdoption(context) {
5861
6205
  };
5862
6206
  }
5863
6207
  const existing = parseJsonc(
5864
- readFileSync23(join29(context.cwd, target.path), "utf8"),
6208
+ readFileSync24(join31(context.cwd, target.path), "utf8"),
5865
6209
  target.path
5866
6210
  );
5867
6211
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5999,7 +6343,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5999
6343
  let chain;
6000
6344
  try {
6001
6345
  const parsed = parseJsonc(
6002
- readFileSync24(join30(cwd, target.path), "utf8"),
6346
+ readFileSync25(join32(cwd, target.path), "utf8"),
6003
6347
  target.path
6004
6348
  );
6005
6349
  chain = parsed.extends;
@@ -6012,7 +6356,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6012
6356
  );
6013
6357
  if (preset === void 0) return void 0;
6014
6358
  try {
6015
- createRequire4(join30(cwd, "noop.js")).resolve(preset);
6359
+ createRequire4(join32(cwd, "noop.js")).resolve(preset);
6016
6360
  return void 0;
6017
6361
  } catch {
6018
6362
  return preset;
@@ -6053,7 +6397,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6053
6397
  return { ok: false, code: 1 };
6054
6398
  }
6055
6399
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
6056
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
6400
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
6057
6401
  if (result.error) {
6058
6402
  process.stderr.write(
6059
6403
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -6088,7 +6432,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6088
6432
  let worst = 0;
6089
6433
  for (const project of this.referencedProjects(ctx.cwd, config)) {
6090
6434
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
6091
- const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
6435
+ const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
6092
6436
  if (result.error) {
6093
6437
  process.stderr.write(
6094
6438
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -6108,7 +6452,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6108
6452
  referencedProjects(cwd, config) {
6109
6453
  try {
6110
6454
  const parsed = parseJsonc(
6111
- readFileSync24(join30(cwd, config), "utf8"),
6455
+ readFileSync25(join32(cwd, config), "utf8"),
6112
6456
  config
6113
6457
  );
6114
6458
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -6124,7 +6468,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6124
6468
  * check.
6125
6469
  */
6126
6470
  typecheckTarget(cwd) {
6127
- if (existsSync25(join30(cwd, "tsconfig.json"))) return "tsconfig.json";
6471
+ if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
6128
6472
  const target = resolveTsconfigTarget(cwd);
6129
6473
  return target.reason === "none" ? null : target.path;
6130
6474
  }
@@ -6177,7 +6521,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6177
6521
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
6178
6522
  }
6179
6523
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
6180
- const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
6524
+ const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
6181
6525
  if (result.error) {
6182
6526
  process.stderr.write(
6183
6527
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -6220,7 +6564,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
6220
6564
  * errors in the output mean the rule must be on.
6221
6565
  */
6222
6566
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
6223
- const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
6567
+ const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
6224
6568
  if (shown.status === 0 && shown.stdout) {
6225
6569
  try {
6226
6570
  const co = parseJsonc(
@@ -6247,6 +6591,7 @@ function registerAdapters() {
6247
6591
  registerLint();
6248
6592
  registerFormat();
6249
6593
  registerBuild();
6594
+ registerTest();
6250
6595
  }
6251
6596
 
6252
6597
  // src/core/detect-framework.ts
@@ -6328,7 +6673,7 @@ function replaceLines(current, replacements) {
6328
6673
  }
6329
6674
 
6330
6675
  // src/core/apply-plan.ts
6331
- import { existsSync as existsSync26, readFileSync as readFileSync25, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
6676
+ import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
6332
6677
  import { resolve as resolve4, sep } from "path";
6333
6678
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6334
6679
 
@@ -6347,7 +6692,7 @@ function resolveWithinRoot(cwd, relativePath) {
6347
6692
  return absolutePath;
6348
6693
  }
6349
6694
  function readIfExists(absolutePath) {
6350
- return existsSync26(absolutePath) ? readFileSync25(absolutePath, "utf8") : void 0;
6695
+ return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
6351
6696
  }
6352
6697
  function* leaves(value, prefix = []) {
6353
6698
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6428,9 +6773,9 @@ function applyOperationTo(current, operation) {
6428
6773
  }
6429
6774
  }
6430
6775
  }
6431
- function preparePlan(cwd, plan3) {
6776
+ function preparePlan(cwd, plan4) {
6432
6777
  const prepared = /* @__PURE__ */ new Map();
6433
- for (const operation of plan3.operations) {
6778
+ for (const operation of plan4.operations) {
6434
6779
  const absolutePath = resolveWithinRoot(cwd, operation.path);
6435
6780
  const existing = prepared.get(operation.path);
6436
6781
  const before = existing?.before ?? readIfExists(absolutePath) ?? "";
@@ -6452,8 +6797,8 @@ function writeFileAtomic(absolutePath, contents) {
6452
6797
  writeFileSync4(tempPath, contents);
6453
6798
  renameSync(tempPath, absolutePath);
6454
6799
  }
6455
- function applyPlan(cwd, plan3) {
6456
- const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
6800
+ function applyPlan(cwd, plan4) {
6801
+ const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
6457
6802
  for (const file of changed) {
6458
6803
  if (file.deleted) {
6459
6804
  rmSync3(file.absolutePath, { force: true });
@@ -6465,8 +6810,8 @@ function applyPlan(cwd, plan3) {
6465
6810
  }
6466
6811
 
6467
6812
  // src/core/config/preset-evidence.ts
6468
- import { existsSync as existsSync27, readdirSync as readdirSync3, readFileSync as readFileSync26 } from "fs";
6469
- import { join as join31 } from "path";
6813
+ import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
6814
+ import { join as join33 } from "path";
6470
6815
  var PATH_SIGNALS = [
6471
6816
  {
6472
6817
  preset: "nest",
@@ -6480,11 +6825,11 @@ var DEPENDENCY_SIGNALS = [
6480
6825
  { preset: "nest", pattern: /^@nestjs\// }
6481
6826
  ];
6482
6827
  function dependencyNames(cwd) {
6483
- const path = join31(cwd, "package.json");
6484
- if (!existsSync27(path)) return [];
6828
+ const path = join33(cwd, "package.json");
6829
+ if (!existsSync29(path)) return [];
6485
6830
  try {
6486
6831
  const manifest = parseJsonc(
6487
- readFileSync26(path, "utf8"),
6832
+ readFileSync27(path, "utf8"),
6488
6833
  path
6489
6834
  );
6490
6835
  return [
@@ -6498,7 +6843,7 @@ function dependencyNames(cwd) {
6498
6843
  function declaresJsx(cwd) {
6499
6844
  let entries;
6500
6845
  try {
6501
- entries = readdirSync3(cwd).filter(
6846
+ entries = readdirSync4(cwd).filter(
6502
6847
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
6503
6848
  );
6504
6849
  } catch {
@@ -6507,7 +6852,7 @@ function declaresJsx(cwd) {
6507
6852
  for (const name of entries) {
6508
6853
  try {
6509
6854
  const config = parseJsonc(
6510
- readFileSync26(join31(cwd, name), "utf8"),
6855
+ readFileSync27(join33(cwd, name), "utf8"),
6511
6856
  name
6512
6857
  );
6513
6858
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6558,14 +6903,14 @@ function resolveFlavour(opts) {
6558
6903
  }
6559
6904
  return detection.preset;
6560
6905
  }
6561
- function previewPlan(opts, plan3) {
6562
- const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
6906
+ function previewPlan(opts, plan4) {
6907
+ const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
6563
6908
  if (opts.json) {
6564
6909
  process.stdout.write(
6565
6910
  JSON.stringify(
6566
6911
  {
6567
6912
  dryRun: true,
6568
- notes: plan3.notes ?? [],
6913
+ notes: plan4.notes ?? [],
6569
6914
  files: changed.map(({ path, before, after, deleted }) => ({
6570
6915
  path,
6571
6916
  action: deleted ? "delete" : before.length === 0 ? "create" : "update",
@@ -6580,7 +6925,7 @@ function previewPlan(opts, plan3) {
6580
6925
  return 0;
6581
6926
  }
6582
6927
  process.stderr.write(" dry run: no files written\n");
6583
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6928
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6584
6929
  `);
6585
6930
  if (changed.length === 0) {
6586
6931
  process.stderr.write(" nothing to change\n");
@@ -6624,25 +6969,25 @@ async function dispatch(opts) {
6624
6969
  }
6625
6970
  }
6626
6971
  const context = { cwd: opts.cwd, preset: effective };
6627
- const plan3 = await adapter.plan(context);
6628
- if (plan3.blocked) {
6629
- process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
6972
+ const plan4 = await adapter.plan(context);
6973
+ if (plan4.blocked) {
6974
+ process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
6630
6975
  `);
6631
6976
  return 1;
6632
6977
  }
6633
- if (plan3.skipped) {
6634
- process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan3.skipped}`)}
6978
+ if (plan4.skipped) {
6979
+ process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
6635
6980
  `);
6636
6981
  return 0;
6637
6982
  }
6638
6983
  if (opts.dryRun) {
6639
- return previewPlan(opts, plan3);
6984
+ return previewPlan(opts, plan4);
6640
6985
  }
6641
- for (const change of applyPlan(opts.cwd, plan3)) {
6986
+ for (const change of applyPlan(opts.cwd, plan4)) {
6642
6987
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
6643
6988
  `);
6644
6989
  }
6645
- for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
6990
+ for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
6646
6991
  `);
6647
6992
  if (adapter.afterInit) {
6648
6993
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
@@ -6687,4 +7032,4 @@ export {
6687
7032
  detectFramework,
6688
7033
  dispatch
6689
7034
  };
6690
- //# sourceMappingURL=chunk-LNSAUFIM.js.map
7035
+ //# sourceMappingURL=chunk-37Y7RALA.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-LNSAUFIM.js";
10
+ } from "./chunk-37Y7RALA.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.2.0-alpha.27",
3
+ "version": "1.2.0-alpha.29",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",