@hublo/sentinel 1.2.0-alpha.15 → 1.2.0-alpha.17

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-RV34GQVH.js";
25
+ } from "../chunk-CWJX5DVF.js";
26
26
 
27
27
  // bin/sentinel.ts
28
28
  import { program } from "commander";
@@ -333,11 +333,19 @@ var BUILD_SCRIPT_NAME = "build";
333
333
  var SENTINEL_BUILD_COMMAND = "sentinel --run --build --";
334
334
  var DEV_SCRIPT_NAME = "serve";
335
335
  var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
336
- function buildTarget(configFiles = BUILD_CONFIG_FILES) {
336
+ var BUILD_OUTPUT_CANDIDATES = ["dist", ".output", "build"];
337
+ function buildTarget(options) {
338
+ const { moduleName: moduleName3, configFiles = BUILD_CONFIG_FILES, outputs } = options;
337
339
  return {
338
340
  [BUILD_SCRIPT_NAME]: {
341
+ executor: "nx:run-commands",
342
+ options: {
343
+ command: `pnpm --filter ${moduleName3} run ${BUILD_SCRIPT_NAME}`,
344
+ forwardAllArgs: false
345
+ },
339
346
  cache: true,
340
- inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)]
347
+ inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
348
+ outputs: [...outputs]
341
349
  }
342
350
  };
343
351
  }
@@ -457,9 +465,9 @@ function binFromOwnInstall(packageName, binName) {
457
465
  try {
458
466
  const manifest = require2.resolve(`${packageName}/package.json`);
459
467
  const bin = require2(manifest).bin;
460
- const relative6 = typeof bin === "string" ? bin : bin?.[binName];
461
- if (!relative6) return void 0;
462
- const executable = join4(dirname2(manifest), relative6);
468
+ const relative5 = typeof bin === "string" ? bin : bin?.[binName];
469
+ if (!relative5) return void 0;
470
+ const executable = join4(dirname2(manifest), relative5);
463
471
  return existsSync5(executable) ? executable : void 0;
464
472
  } catch {
465
473
  return void 0;
@@ -472,8 +480,8 @@ function binSearchPath(cwd) {
472
480
  }
473
481
 
474
482
  // src/roles/build/plan.ts
475
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
476
- import { join as join8 } from "path";
483
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
484
+ import { join as join9 } from "path";
477
485
 
478
486
  // src/core/config/existing-command.ts
479
487
  import { readFileSync as readFileSync5 } from "fs";
@@ -656,7 +664,8 @@ function composeDevScript(existing, command) {
656
664
  }
657
665
 
658
666
  // src/roles/build/nest/adopt.ts
659
- import { relative as relative2 } from "path";
667
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
668
+ import { join as join8 } from "path";
660
669
  var NEST_CONFIG_FILE = "vite.config.mts";
661
670
  function nestConfigSource(cwd, target) {
662
671
  const hops = workspaceRootHops(cwd, target);
@@ -673,34 +682,18 @@ export default nestService({
673
682
  })
674
683
  `;
675
684
  }
676
- function nestBuildTarget(cwd, target) {
677
- const workspaceRoot = workspaceRootFor(cwd, target);
678
- if (workspaceRoot === void 0) {
679
- throw new Error(
680
- `sentinel build(nest): cannot place ${cwd} inside a workspace from ${target.main}`
681
- );
682
- }
683
- const moduleRoot = relative2(workspaceRoot, cwd);
684
- return {
685
- executor: "nx:run-commands",
686
- outputs: [`{workspaceRoot}/${target.outputPath}`],
687
- options: {
688
- cwd: moduleRoot,
689
- command: "pnpm exec sentinel --run --build",
690
- forwardAllArgs: false
691
- }
692
- };
685
+ function nestBuildOutputs(target) {
686
+ return [`{workspaceRoot}/${target.outputPath}`];
687
+ }
688
+ var OUT_DIR_IN_CONFIG = /^\s*outDir:\s*'([^']+)'/m;
689
+ function nestOutputsFromConfig(cwd) {
690
+ const configPath = join8(cwd, NEST_CONFIG_FILE);
691
+ if (!existsSync8(configPath)) return void 0;
692
+ const outDir = OUT_DIR_IN_CONFIG.exec(readFileSync6(configPath, "utf8"))?.[1];
693
+ return outDir === void 0 ? void 0 : [`{workspaceRoot}/${outDir}`];
693
694
  }
694
695
  function nestAdoptionOperations(cwd, target) {
695
- return [
696
- { kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) },
697
- { kind: "remove-json-keys", path: "project.json", keys: [["targets", "build"]] },
698
- {
699
- kind: "merge-json",
700
- path: "project.json",
701
- value: { targets: { build: nestBuildTarget(cwd, target) } }
702
- }
703
- ];
696
+ return [{ kind: "write", path: NEST_CONFIG_FILE, contents: nestConfigSource(cwd, target) }];
704
697
  }
705
698
 
706
699
  // src/roles/build/requirements.ts
@@ -836,6 +829,13 @@ function rewriteBuildImports(source, fileName = "vite.config.ts") {
836
829
  }
837
830
 
838
831
  // src/roles/build/plan.ts
832
+ function packageNameFor(cwd) {
833
+ return readProjectPackageJson(cwd).name;
834
+ }
835
+ function candidateOutputs() {
836
+ return BUILD_OUTPUT_CANDIDATES.map((name) => `{projectRoot}/${name}`);
837
+ }
838
+ var NO_PACKAGE_NAME = `this module's package.json declares no \`name\`, so the nx build target would have no way to reach it: the command is \`pnpm --filter <name> run build\`, and an unnamed package matches nothing. Name the package and re-run; nothing was written.`;
839
839
  function ownedBuildDependencies(cwd) {
840
840
  const manifest = readProjectPackageJson(cwd);
841
841
  const keys = [];
@@ -893,7 +893,7 @@ function plan(context) {
893
893
  }
894
894
  const webpackTarget = webpackBuildTarget(context.cwd);
895
895
  if (declaredBuildPreset(context.cwd) === "nest" || webpackTarget !== void 0) {
896
- return planNestService(context, configFile, webpackTarget);
896
+ return planNestService(context, webpackTarget);
897
897
  }
898
898
  if (configFile === void 0) {
899
899
  return {
@@ -908,6 +908,8 @@ function planReactApp(context, configFile) {
908
908
  const operations = [];
909
909
  const notEsm = esmBlocker(configFile, readProjectPackageJson(context.cwd).type);
910
910
  if (notEsm !== void 0) return { operations: [], skipped: notEsm };
911
+ const packageName = packageNameFor(context.cwd);
912
+ if (packageName === void 0) return { operations: [], skipped: NO_PACKAGE_NAME };
911
913
  const rewrite = rewriteBuildImports(readConfigSource(context.cwd, configFile), configFile);
912
914
  if (rewrite.kind === "blocked") {
913
915
  return {
@@ -937,7 +939,12 @@ function planReactApp(context, configFile) {
937
939
  );
938
940
  }
939
941
  operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
940
- operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
942
+ operations.push(
943
+ ...nxTargetOperations({
944
+ cwd: context.cwd,
945
+ targets: buildTarget({ moduleName: packageName, outputs: candidateOutputs() })
946
+ })
947
+ );
941
948
  const owned = ownedBuildDependencies(context.cwd);
942
949
  if (owned.length > 0) {
943
950
  operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
@@ -947,27 +954,33 @@ function planReactApp(context, configFile) {
947
954
  }
948
955
  return { operations, notes };
949
956
  }
950
- function planNestService(context, configFile, webpackTarget) {
951
- if (webpackTarget === void 0) {
957
+ function planNestService(context, webpackTarget) {
958
+ const packageName = packageNameFor(context.cwd);
959
+ if (packageName === void 0) return { operations: [], skipped: NO_PACKAGE_NAME };
960
+ const outputs = webpackTarget === void 0 ? nestOutputsFromConfig(context.cwd) : nestBuildOutputs(webpackTarget);
961
+ if (outputs === void 0) {
952
962
  return {
953
963
  operations: [],
954
- skipped: `already building through sentinel: ${configFile ?? NEST_CONFIG_FILE} imports the nest preset and the nx target runs it. Nothing to change.`
964
+ 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.`
955
965
  };
956
966
  }
967
+ const metadata = [
968
+ ...nxTargetOperations({
969
+ cwd: context.cwd,
970
+ targets: buildTarget({
971
+ moduleName: packageName,
972
+ configFiles: [NEST_CONFIG_FILE],
973
+ outputs
974
+ })
975
+ }),
976
+ manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND })
977
+ ];
978
+ if (webpackTarget === void 0) return { operations: metadata };
957
979
  const refusal = refusalFor(context.cwd, webpackTarget);
958
980
  if (refusal !== void 0) return { operations: [], blocked: refusal };
959
981
  const dropped = Object.keys(webpackTarget.configurations ?? {});
960
982
  return {
961
- operations: [
962
- ...nestAdoptionOperations(context.cwd, webpackTarget),
963
- // The same nx metadata the React path writes, and for the same reason: declaring `inputs`
964
- // at all REPLACES nx's default `['default','^default']`, so a target that declares none
965
- // runs on defaults while every other target this module has declares them. The Nest path
966
- // wrote none, which left its build the one target that could replay a stale bundle after a
967
- // dependency changed. One name, because a Nest service's config can only be `.mts`.
968
- ...nxTargetMetadataOperations({ cwd: context.cwd, targets: buildTarget([NEST_CONFIG_FILE]) }),
969
- manifestOperation(context.cwd, { [BUILD_SCRIPT_NAME]: SENTINEL_BUILD_COMMAND })
970
- ],
983
+ operations: [...nestAdoptionOperations(context.cwd, webpackTarget), ...metadata],
971
984
  notes: [
972
985
  `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.`,
973
986
  ...dropped.length > 0 ? [
@@ -978,9 +991,9 @@ function planNestService(context, configFile, webpackTarget) {
978
991
  };
979
992
  }
980
993
  function readConfigSource(cwd, configFile) {
981
- if (!existsSync8(join8(cwd, configFile))) return "";
994
+ if (!existsSync9(join9(cwd, configFile))) return "";
982
995
  try {
983
- return readFileSync6(join8(cwd, configFile), "utf8");
996
+ return readFileSync7(join9(cwd, configFile), "utf8");
984
997
  } catch {
985
998
  return "";
986
999
  }
@@ -1164,6 +1177,199 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
1164
1177
  }
1165
1178
  };
1166
1179
 
1180
+ // src/roles/build/declared-outputs.ts
1181
+ import { existsSync as existsSync11, statSync } from "fs";
1182
+ import { join as join11 } from "path";
1183
+
1184
+ // src/core/workspace-prep.ts
1185
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync } from "fs";
1186
+ import { dirname as dirname3, join as join10, relative as relative2 } from "path";
1187
+
1188
+ // src/core/settings.ts
1189
+ var WORKSPACE_ROOT_MARKER = "nx.json";
1190
+ var DEFAULT_MAX_DIAGNOSTICS = 100;
1191
+
1192
+ // src/core/workspace-prep.ts
1193
+ var OVERRIDE_KEY = "i18next>typescript";
1194
+ var NATIVE_TS_ALIAS = "@typescript/native";
1195
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
1196
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1197
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1198
+ function findWorkspaceRoot(startDir) {
1199
+ let dir = startDir;
1200
+ for (; ; ) {
1201
+ if (existsSync10(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
1202
+ const parent = dirname3(dir);
1203
+ if (parent === dir) return void 0;
1204
+ dir = parent;
1205
+ }
1206
+ }
1207
+ function declaredNativeTs(pkg) {
1208
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1209
+ if (!spec) return void 0;
1210
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1211
+ return version || void 0;
1212
+ }
1213
+ function ensureI18nextSingleton(root, dryRun) {
1214
+ const pkgPath = join10(root, "package.json");
1215
+ if (!existsSync10(pkgPath)) return void 0;
1216
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1217
+ const want = declaredNativeTs(pkg);
1218
+ if (!want) return void 0;
1219
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1220
+ if (have === want) return void 0;
1221
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1222
+ pkg.pnpm ??= {};
1223
+ pkg.pnpm.overrides ??= {};
1224
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1225
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1226
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1227
+ }
1228
+ function ensureReleaseAgeAllowList(root, dryRun) {
1229
+ const yamlPath = join10(root, WORKSPACE_YAML);
1230
+ if (!existsSync10(yamlPath)) return void 0;
1231
+ const own = readOwnPackage().name;
1232
+ const lines = readFileSync8(yamlPath, "utf8").split("\n");
1233
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1234
+ if (keyIdx === -1) return void 0;
1235
+ let lastItemIdx = keyIdx;
1236
+ let indent = " ";
1237
+ for (let i = keyIdx + 1; i < lines.length; i++) {
1238
+ const match = lines[i]?.match(LIST_ITEM);
1239
+ if (!match) break;
1240
+ indent = match[1] ?? indent;
1241
+ lastItemIdx = i;
1242
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1243
+ }
1244
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1245
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1246
+ writeFileSync(yamlPath, lines.join("\n"));
1247
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1248
+ }
1249
+ var PRETTIER_IGNORE = ".prettierignore";
1250
+ var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1251
+ var ROOT_PRETTIER_CONFIGS = [
1252
+ ".prettierrc",
1253
+ ".prettierrc.json",
1254
+ ".prettierrc.json5",
1255
+ ".prettierrc.yml",
1256
+ ".prettierrc.yaml",
1257
+ ".prettierrc.js",
1258
+ ".prettierrc.cjs",
1259
+ ".prettierrc.mjs",
1260
+ "prettier.config.js",
1261
+ "prettier.config.cjs",
1262
+ "prettier.config.mjs"
1263
+ ];
1264
+ function ensureFormatterExclusion(root, moduleDir, dryRun) {
1265
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1266
+ if (rel === "" || rel.startsWith("..")) return void 0;
1267
+ const ignorePath = join10(root, PRETTIER_IGNORE);
1268
+ const hasPrettier = existsSync10(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync10(join10(root, name)));
1269
+ if (!hasPrettier) return void 0;
1270
+ const pattern = `/${rel}/`;
1271
+ const existing = existsSync10(ignorePath) ? readFileSync8(ignorePath, "utf8") : "";
1272
+ const lines = existing.split("\n");
1273
+ if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1274
+ return void 0;
1275
+ }
1276
+ if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1277
+ const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1278
+ if (headerIdx === -1) {
1279
+ const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1280
+
1281
+ `;
1282
+ writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1283
+ ${pattern}
1284
+ `);
1285
+ } else {
1286
+ let end = headerIdx + 1;
1287
+ while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1288
+ const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1289
+ writeFileSync(
1290
+ ignorePath,
1291
+ [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1292
+ );
1293
+ }
1294
+ return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1295
+ }
1296
+ function ensureWorkspacePrep(opts) {
1297
+ const dryRun = Boolean(opts.dryRun);
1298
+ return [
1299
+ ensureI18nextSingleton(opts.root, dryRun),
1300
+ ensureReleaseAgeAllowList(opts.root, dryRun),
1301
+ opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1302
+ ].filter((message) => message !== void 0);
1303
+ }
1304
+ function inspectWorkspacePrep(root) {
1305
+ const entries = [];
1306
+ let pkg = {};
1307
+ const pkgPath = join10(root, "package.json");
1308
+ if (existsSync10(pkgPath)) {
1309
+ try {
1310
+ pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1311
+ } catch {
1312
+ pkg = {};
1313
+ }
1314
+ }
1315
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1316
+ if (pinned !== void 0) {
1317
+ const stillForks = declaredNativeTs(pkg);
1318
+ entries.push({
1319
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1320
+ 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.`
1321
+ });
1322
+ }
1323
+ const own = readOwnPackage().name;
1324
+ const yamlPath = join10(root, WORKSPACE_YAML);
1325
+ if (existsSync10(yamlPath)) {
1326
+ const yaml = readFileSync8(yamlPath, "utf8");
1327
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1328
+ if (listed) {
1329
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1330
+ entries.push({
1331
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1332
+ 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.`
1333
+ });
1334
+ }
1335
+ }
1336
+ return entries;
1337
+ }
1338
+
1339
+ // src/roles/build/declared-outputs.ts
1340
+ function declaredBuildTarget(cwd) {
1341
+ return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1342
+ }
1343
+ function resolveOutputToken(token, cwd) {
1344
+ if (token.startsWith("{projectRoot}/")) return join11(cwd, token.slice("{projectRoot}/".length));
1345
+ if (token.startsWith("{workspaceRoot}/")) {
1346
+ const root = findWorkspaceRoot(cwd);
1347
+ return root === void 0 ? void 0 : join11(root, token.slice("{workspaceRoot}/".length));
1348
+ }
1349
+ return join11(cwd, token);
1350
+ }
1351
+ function writtenSince(path, startedAt) {
1352
+ if (!existsSync11(path)) return false;
1353
+ try {
1354
+ return statSync(path).mtimeMs >= startedAt;
1355
+ } catch {
1356
+ return true;
1357
+ }
1358
+ }
1359
+ function outputsProblem(cwd, startedAt) {
1360
+ const target = declaredBuildTarget(cwd);
1361
+ if (target === void 0) return void 0;
1362
+ const declared = target.outputs ?? [];
1363
+ if (declared.length === 0) {
1364
+ if (target.cache !== true) return void 0;
1365
+ 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.`;
1366
+ }
1367
+ const resolved = declared.map((token) => ({ token, path: resolveOutputToken(token, cwd) })).filter((entry) => entry.path !== void 0);
1368
+ if (resolved.length === 0) return void 0;
1369
+ if (resolved.some((entry) => writtenSince(entry.path, startedAt))) return void 0;
1370
+ 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.`;
1371
+ }
1372
+
1167
1373
  // src/roles/build/prerequisite.ts
1168
1374
  import { spawnSync as spawnSync2 } from "child_process";
1169
1375
  var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
@@ -1212,7 +1418,15 @@ var ViteAdapter = class extends ViteRoleAdapter {
1212
1418
  );
1213
1419
  return { ok: false, code: prerequisite.code };
1214
1420
  }
1215
- return this.spawnVite(ctx, ready.vite, ["build"], 1);
1421
+ const startedAt = Date.now();
1422
+ const result = await this.spawnVite(ctx, ready.vite, ["build"], 1);
1423
+ if (!result.ok) return result;
1424
+ const problem = outputsProblem(ctx.cwd, startedAt);
1425
+ if (problem !== void 0) {
1426
+ this.say(problem);
1427
+ return { ok: false, code: 1 };
1428
+ }
1429
+ return result;
1216
1430
  }
1217
1431
  /**
1218
1432
  * What this module builds with, WITHOUT loading its config.
@@ -1284,12 +1498,12 @@ function registerBuild() {
1284
1498
 
1285
1499
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1286
1500
  import { spawnSync as spawnSync3 } from "child_process";
1287
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
1288
- import { join as join15 } from "path";
1501
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
1502
+ import { join as join17 } from "path";
1289
1503
 
1290
1504
  // src/core/config/has-source.ts
1291
1505
  import { readdirSync } from "fs";
1292
- import { extname, join as join9, relative as relative3 } from "path";
1506
+ import { extname, join as join12, relative as relative3 } from "path";
1293
1507
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1294
1508
  "node_modules",
1295
1509
  "dist",
@@ -1340,7 +1554,7 @@ function hasSourceFiles(cwd, extensions) {
1340
1554
  }
1341
1555
  for (const entry of entries) {
1342
1556
  if (entry.isDirectory()) {
1343
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join9(dir, entry.name));
1557
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join12(dir, entry.name));
1344
1558
  continue;
1345
1559
  }
1346
1560
  if (wanted.has(extname(entry.name))) return true;
@@ -1361,7 +1575,7 @@ function listSourceFiles(cwd, extensions) {
1361
1575
  continue;
1362
1576
  }
1363
1577
  for (const entry of entries) {
1364
- const full = join9(dir, entry.name);
1578
+ const full = join12(dir, entry.name);
1365
1579
  if (entry.isDirectory()) {
1366
1580
  if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1367
1581
  continue;
@@ -1379,159 +1593,6 @@ function scopeFlags(paths, options = []) {
1379
1593
  return [TOLERATE_EMPTY_FLAG];
1380
1594
  }
1381
1595
 
1382
- // src/core/settings.ts
1383
- var WORKSPACE_ROOT_MARKER = "nx.json";
1384
- var DEFAULT_MAX_DIAGNOSTICS = 100;
1385
-
1386
- // src/core/workspace-prep.ts
1387
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync } from "fs";
1388
- import { dirname as dirname3, join as join10, relative as relative4 } from "path";
1389
- var OVERRIDE_KEY = "i18next>typescript";
1390
- var NATIVE_TS_ALIAS = "@typescript/native";
1391
- var WORKSPACE_YAML = "pnpm-workspace.yaml";
1392
- var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
1393
- var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
1394
- function findWorkspaceRoot(startDir) {
1395
- let dir = startDir;
1396
- for (; ; ) {
1397
- if (existsSync9(join10(dir, WORKSPACE_ROOT_MARKER))) return dir;
1398
- const parent = dirname3(dir);
1399
- if (parent === dir) return void 0;
1400
- dir = parent;
1401
- }
1402
- }
1403
- function declaredNativeTs(pkg) {
1404
- const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
1405
- if (!spec) return void 0;
1406
- const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
1407
- return version || void 0;
1408
- }
1409
- function ensureI18nextSingleton(root, dryRun) {
1410
- const pkgPath = join10(root, "package.json");
1411
- if (!existsSync9(pkgPath)) return void 0;
1412
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1413
- const want = declaredNativeTs(pkg);
1414
- if (!want) return void 0;
1415
- const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1416
- if (have === want) return void 0;
1417
- if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
1418
- pkg.pnpm ??= {};
1419
- pkg.pnpm.overrides ??= {};
1420
- pkg.pnpm.overrides[OVERRIDE_KEY] = want;
1421
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1422
- return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
1423
- }
1424
- function ensureReleaseAgeAllowList(root, dryRun) {
1425
- const yamlPath = join10(root, WORKSPACE_YAML);
1426
- if (!existsSync9(yamlPath)) return void 0;
1427
- const own = readOwnPackage().name;
1428
- const lines = readFileSync7(yamlPath, "utf8").split("\n");
1429
- const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
1430
- if (keyIdx === -1) return void 0;
1431
- let lastItemIdx = keyIdx;
1432
- let indent = " ";
1433
- for (let i = keyIdx + 1; i < lines.length; i++) {
1434
- const match = lines[i]?.match(LIST_ITEM);
1435
- if (!match) break;
1436
- indent = match[1] ?? indent;
1437
- lastItemIdx = i;
1438
- if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
1439
- }
1440
- if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
1441
- lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
1442
- writeFileSync(yamlPath, lines.join("\n"));
1443
- return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
1444
- }
1445
- var PRETTIER_IGNORE = ".prettierignore";
1446
- var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
1447
- var ROOT_PRETTIER_CONFIGS = [
1448
- ".prettierrc",
1449
- ".prettierrc.json",
1450
- ".prettierrc.json5",
1451
- ".prettierrc.yml",
1452
- ".prettierrc.yaml",
1453
- ".prettierrc.js",
1454
- ".prettierrc.cjs",
1455
- ".prettierrc.mjs",
1456
- "prettier.config.js",
1457
- "prettier.config.cjs",
1458
- "prettier.config.mjs"
1459
- ];
1460
- function ensureFormatterExclusion(root, moduleDir, dryRun) {
1461
- const rel = relative4(root, moduleDir).replaceAll("\\", "/");
1462
- if (rel === "" || rel.startsWith("..")) return void 0;
1463
- const ignorePath = join10(root, PRETTIER_IGNORE);
1464
- const hasPrettier = existsSync9(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync9(join10(root, name)));
1465
- if (!hasPrettier) return void 0;
1466
- const pattern = `/${rel}/`;
1467
- const existing = existsSync9(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
1468
- const lines = existing.split("\n");
1469
- if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
1470
- return void 0;
1471
- }
1472
- if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
1473
- const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
1474
- if (headerIdx === -1) {
1475
- const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
1476
-
1477
- `;
1478
- writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
1479
- ${pattern}
1480
- `);
1481
- } else {
1482
- let end = headerIdx + 1;
1483
- while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
1484
- const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
1485
- writeFileSync(
1486
- ignorePath,
1487
- [...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
1488
- );
1489
- }
1490
- return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
1491
- }
1492
- function ensureWorkspacePrep(opts) {
1493
- const dryRun = Boolean(opts.dryRun);
1494
- return [
1495
- ensureI18nextSingleton(opts.root, dryRun),
1496
- ensureReleaseAgeAllowList(opts.root, dryRun),
1497
- opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
1498
- ].filter((message) => message !== void 0);
1499
- }
1500
- function inspectWorkspacePrep(root) {
1501
- const entries = [];
1502
- let pkg = {};
1503
- const pkgPath = join10(root, "package.json");
1504
- if (existsSync9(pkgPath)) {
1505
- try {
1506
- pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1507
- } catch {
1508
- pkg = {};
1509
- }
1510
- }
1511
- const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
1512
- if (pinned !== void 0) {
1513
- const stillForks = declaredNativeTs(pkg);
1514
- entries.push({
1515
- rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
1516
- 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.`
1517
- });
1518
- }
1519
- const own = readOwnPackage().name;
1520
- const yamlPath = join10(root, WORKSPACE_YAML);
1521
- if (existsSync9(yamlPath)) {
1522
- const yaml = readFileSync7(yamlPath, "utf8");
1523
- const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
1524
- if (listed) {
1525
- const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
1526
- entries.push({
1527
- rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
1528
- 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.`
1529
- });
1530
- }
1531
- }
1532
- return entries;
1533
- }
1534
-
1535
1596
  // src/roles/format/config-policy.ts
1536
1597
  var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
1537
1598
  var FORMAT_SCRIPT_NAME = "format";
@@ -1606,8 +1667,8 @@ function writesWhenRewritten(name, command) {
1606
1667
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1607
1668
 
1608
1669
  // src/roles/format/inherited-ignores.ts
1609
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
1610
- import { join as join11 } from "path";
1670
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1671
+ import { join as join13 } from "path";
1611
1672
  var ROOT_IGNORE_FILE = ".prettierignore";
1612
1673
  function isPattern(line) {
1613
1674
  const trimmed = line.trim();
@@ -1622,11 +1683,11 @@ function toModulePattern(pattern) {
1622
1683
  }
1623
1684
  function inheritedIgnorePatterns(workspaceRoot) {
1624
1685
  if (!workspaceRoot) return [];
1625
- const path = join11(workspaceRoot, ROOT_IGNORE_FILE);
1626
- if (!existsSync10(path)) return [];
1686
+ const path = join13(workspaceRoot, ROOT_IGNORE_FILE);
1687
+ if (!existsSync12(path)) return [];
1627
1688
  let contents;
1628
1689
  try {
1629
- contents = readFileSync8(path, "utf8");
1690
+ contents = readFileSync9(path, "utf8");
1630
1691
  } catch {
1631
1692
  return [];
1632
1693
  }
@@ -1713,13 +1774,13 @@ function formatPresetFor(preset) {
1713
1774
  }
1714
1775
 
1715
1776
  // src/roles/format/prettier-config.ts
1716
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1717
- import { join as join13 } from "path";
1777
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
1778
+ import { join as join15 } from "path";
1718
1779
 
1719
1780
  // src/roles/format/resolve-oxfmt.ts
1720
- import { readFileSync as readFileSync9 } from "fs";
1781
+ import { readFileSync as readFileSync10 } from "fs";
1721
1782
  import { createRequire as createRequire2 } from "module";
1722
- import { dirname as dirname4, join as join12 } from "path";
1783
+ import { dirname as dirname4, join as join14 } from "path";
1723
1784
  function resolveOxfmt(cwd) {
1724
1785
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1725
1786
  }
@@ -1748,8 +1809,8 @@ function configSchema() {
1748
1809
  function readConfigSchema() {
1749
1810
  try {
1750
1811
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
1751
- const schemaPath = join12(dirname4(manifest), "configuration_schema.json");
1752
- return JSON.parse(readFileSync9(schemaPath, "utf8"));
1812
+ const schemaPath = join14(dirname4(manifest), "configuration_schema.json");
1813
+ return JSON.parse(readFileSync10(schemaPath, "utf8"));
1753
1814
  } catch {
1754
1815
  return void 0;
1755
1816
  }
@@ -1780,14 +1841,14 @@ function toOxfmtOverrides(value) {
1780
1841
  return { overrides, unresolved };
1781
1842
  }
1782
1843
  function readPrettierSettings(cwd) {
1783
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync11(join13(cwd, name)));
1844
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync13(join15(cwd, name)));
1784
1845
  if (!file) return { options: {}, unresolved: [] };
1785
1846
  if (/\.(js|cjs|mjs)$/.test(file)) {
1786
1847
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1787
1848
  }
1788
1849
  let parsed;
1789
1850
  try {
1790
- parsed = parseJsonc(readFileSync10(join13(cwd, file), "utf8"), file);
1851
+ parsed = parseJsonc(readFileSync11(join15(cwd, file), "utf8"), file);
1791
1852
  } catch {
1792
1853
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1793
1854
  }
@@ -1817,8 +1878,8 @@ function readPrettierSettings(cwd) {
1817
1878
  }
1818
1879
 
1819
1880
  // src/roles/format/read-adoption.ts
1820
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1821
- import { join as join14 } from "path";
1881
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
1882
+ import { join as join16 } from "path";
1822
1883
  var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
1823
1884
  configFile,
1824
1885
  preset: null,
@@ -1834,11 +1895,11 @@ function sameValue(a, b) {
1834
1895
  return JSON.stringify(a) === JSON.stringify(b);
1835
1896
  }
1836
1897
  function readFormatAdoption(cwd) {
1837
- const path = join14(cwd, FORMAT_CONFIG_FILE);
1838
- if (!existsSync12(path)) return NOT_ADOPTED2(null);
1898
+ const path = join16(cwd, FORMAT_CONFIG_FILE);
1899
+ if (!existsSync14(path)) return NOT_ADOPTED2(null);
1839
1900
  let parsed;
1840
1901
  try {
1841
- parsed = parseJsonc(readFileSync11(path, "utf8"), FORMAT_CONFIG_FILE);
1902
+ parsed = parseJsonc(readFileSync12(path, "utf8"), FORMAT_CONFIG_FILE);
1842
1903
  } catch (error) {
1843
1904
  const reason = error instanceof Error ? error.message : String(error);
1844
1905
  return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -1963,7 +2024,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1963
2024
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1964
2025
  ];
1965
2026
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1966
- (name) => existsSync13(join15(context.cwd, name))
2027
+ (name) => existsSync15(join17(context.cwd, name))
1967
2028
  );
1968
2029
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1969
2030
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -2203,7 +2264,7 @@ var OxfmtAdapter = class extends BaseAdapter {
2203
2264
  modulePrettierDependencies(cwd) {
2204
2265
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
2205
2266
  try {
2206
- const manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
2267
+ const manifest = JSON.parse(readFileSync13(join17(cwd, "package.json"), "utf8"));
2207
2268
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
2208
2269
  } catch {
2209
2270
  return [];
@@ -2285,8 +2346,8 @@ function registerFormat() {
2285
2346
 
2286
2347
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2287
2348
  import { spawnSync as spawnSync4 } from "child_process";
2288
- import { existsSync as existsSync21, readFileSync as readFileSync20, rmSync, writeFileSync as writeFileSync2 } from "fs";
2289
- import { join as join25 } from "path";
2349
+ import { existsSync as existsSync23, readFileSync as readFileSync21, rmSync, writeFileSync as writeFileSync2 } from "fs";
2350
+ import { join as join27 } from "path";
2290
2351
 
2291
2352
  // src/core/config/deferred-rules.ts
2292
2353
  function deferredRuleNames(rules) {
@@ -2294,8 +2355,8 @@ function deferredRuleNames(rules) {
2294
2355
  }
2295
2356
 
2296
2357
  // src/core/fix-report.ts
2297
- import { statSync } from "fs";
2298
- import { join as join16 } from "path";
2358
+ import { statSync as statSync2 } from "fs";
2359
+ import { join as join18 } from "path";
2299
2360
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2300
2361
  function readFailure(output) {
2301
2362
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2306,7 +2367,7 @@ function readFailure(output) {
2306
2367
  }
2307
2368
  function fingerprint(path) {
2308
2369
  try {
2309
- const stat = statSync(path);
2370
+ const stat = statSync2(path);
2310
2371
  return `${stat.size}:${stat.mtimeMs}`;
2311
2372
  } catch {
2312
2373
  return void 0;
@@ -2315,7 +2376,7 @@ function fingerprint(path) {
2315
2376
  function snapshotFiles(cwd, extensions) {
2316
2377
  const snapshot = /* @__PURE__ */ new Map();
2317
2378
  for (const file of listSourceFiles(cwd, extensions)) {
2318
- const mark = fingerprint(join16(cwd, file));
2379
+ const mark = fingerprint(join18(cwd, file));
2319
2380
  if (mark !== void 0) snapshot.set(file, mark);
2320
2381
  }
2321
2382
  return snapshot;
@@ -2323,7 +2384,7 @@ function snapshotFiles(cwd, extensions) {
2323
2384
  function changedSince(cwd, before, extensions) {
2324
2385
  const changed = [];
2325
2386
  for (const file of listSourceFiles(cwd, extensions)) {
2326
- const now = fingerprint(join16(cwd, file));
2387
+ const now = fingerprint(join18(cwd, file));
2327
2388
  if (now === void 0) continue;
2328
2389
  if (before.get(file) !== now) changed.push(file);
2329
2390
  }
@@ -2342,8 +2403,8 @@ function describeFixOutcome(outcome, toolLabel) {
2342
2403
  }
2343
2404
 
2344
2405
  // src/roles/lint/module-baseline.ts
2345
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
2346
- import { join as join17 } from "path";
2406
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
2407
+ import { join as join19 } from "path";
2347
2408
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2348
2409
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2349
2410
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -2387,11 +2448,11 @@ function describeHolds(holds) {
2387
2448
  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`;
2388
2449
  }
2389
2450
  function heldRules(cwd) {
2390
- const path = join17(cwd, LINT_BASELINE_FILE);
2391
- if (!existsSync14(path)) return [];
2451
+ const path = join19(cwd, LINT_BASELINE_FILE);
2452
+ if (!existsSync16(path)) return [];
2392
2453
  try {
2393
2454
  const parsed = parseJsonc(
2394
- readFileSync13(path, "utf8"),
2455
+ readFileSync14(path, "utf8"),
2395
2456
  LINT_BASELINE_FILE
2396
2457
  );
2397
2458
  return Object.keys(parsed.rules ?? {});
@@ -4214,14 +4275,14 @@ function downgradedRulesFor(preset) {
4214
4275
  }
4215
4276
 
4216
4277
  // src/roles/lint/extra-layers.ts
4217
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4218
- import { isAbsolute as isAbsolute2, join as join18, resolve as resolve3 } from "path";
4278
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4279
+ import { isAbsolute as isAbsolute2, join as join20, resolve as resolve3 } from "path";
4219
4280
  function ruleCount(cwd, specifier) {
4220
4281
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4221
- if (!existsSync15(path)) return void 0;
4282
+ if (!existsSync17(path)) return void 0;
4222
4283
  try {
4223
4284
  const parsed = parseJsonc(
4224
- readFileSync14(path, "utf8"),
4285
+ readFileSync15(path, "utf8"),
4225
4286
  specifier
4226
4287
  );
4227
4288
  return Object.keys(parsed.rules ?? {}).length;
@@ -4241,7 +4302,7 @@ function extraLayers(cwd, extendsList) {
4241
4302
  function committedExtendsList(cwd, configFile) {
4242
4303
  try {
4243
4304
  const parsed = parseJsonc(
4244
- readFileSync14(join18(cwd, configFile), "utf8"),
4305
+ readFileSync15(join20(cwd, configFile), "utf8"),
4245
4306
  configFile
4246
4307
  );
4247
4308
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4300,8 +4361,8 @@ function errorCountsByConfigRule(stdout) {
4300
4361
  }
4301
4362
 
4302
4363
  // src/core/config/read-adoption.ts
4303
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4304
- import { join as join20 } from "path";
4364
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4365
+ import { join as join22 } from "path";
4305
4366
 
4306
4367
  // src/core/config/owned-keys.ts
4307
4368
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4316,12 +4377,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4316
4377
  }
4317
4378
 
4318
4379
  // src/core/config/resolve-config-target.ts
4319
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4320
- import { join as join19 } from "path";
4380
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4381
+ import { join as join21 } from "path";
4321
4382
  function readExtends(absolutePath) {
4322
4383
  let parsed;
4323
4384
  try {
4324
- parsed = parseJsonc(readFileSync15(absolutePath, "utf8"), absolutePath);
4385
+ parsed = parseJsonc(readFileSync16(absolutePath, "utf8"), absolutePath);
4325
4386
  } catch {
4326
4387
  return [];
4327
4388
  }
@@ -4335,8 +4396,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
4335
4396
  let existing;
4336
4397
  let existingExtendsSomething = false;
4337
4398
  for (const candidate of candidates) {
4338
- const absolutePath = join19(moduleDir, candidate);
4339
- if (!existsSync16(absolutePath)) continue;
4399
+ const absolutePath = join21(moduleDir, candidate);
4400
+ if (!existsSync18(absolutePath)) continue;
4340
4401
  const chain = readExtends(absolutePath);
4341
4402
  if (existing === void 0) {
4342
4403
  existing = candidate;
@@ -4369,12 +4430,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
4369
4430
  });
4370
4431
  function readAdoption(cwd, options) {
4371
4432
  const target = resolveConfigTarget(cwd, options);
4372
- if (target.reason === "none" || !existsSync17(join20(cwd, target.path))) {
4433
+ if (target.reason === "none" || !existsSync19(join22(cwd, target.path))) {
4373
4434
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
4374
4435
  }
4375
4436
  let parsed;
4376
4437
  try {
4377
- parsed = parseJsonc(readFileSync16(join20(cwd, target.path), "utf8"), target.path);
4438
+ parsed = parseJsonc(readFileSync17(join22(cwd, target.path), "utf8"), target.path);
4378
4439
  } catch (error) {
4379
4440
  const reason = error instanceof Error ? error.message : String(error);
4380
4441
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -4408,9 +4469,9 @@ function readLintAdoption(cwd) {
4408
4469
  }
4409
4470
 
4410
4471
  // src/roles/lint/resolve-oxlint.ts
4411
- import { existsSync as existsSync18 } from "fs";
4472
+ import { existsSync as existsSync20 } from "fs";
4412
4473
  import { createRequire as createRequire3 } from "module";
4413
- import { delimiter as delimiter2, dirname as dirname5, join as join21 } from "path";
4474
+ import { delimiter as delimiter2, dirname as dirname5, join as join23 } from "path";
4414
4475
  import { fileURLToPath as fileURLToPath2 } from "url";
4415
4476
  var PACKAGE_OF = {
4416
4477
  oxlint: "oxlint",
@@ -4435,30 +4496,30 @@ function tsgolintShim(cwd) {
4435
4496
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
4436
4497
  try {
4437
4498
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
4438
- candidates.push(join21(packageDir, "node_modules", ".bin", "tsgolint"));
4439
- candidates.push(join21(packageDir, "..", ".bin", "tsgolint"));
4499
+ candidates.push(join23(packageDir, "node_modules", ".bin", "tsgolint"));
4500
+ candidates.push(join23(packageDir, "..", ".bin", "tsgolint"));
4440
4501
  } catch {
4441
4502
  }
4442
4503
  }
4443
4504
  try {
4444
4505
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
4445
- candidates.push(join21(ownRoot, "node_modules", ".bin", "tsgolint"));
4506
+ candidates.push(join23(ownRoot, "node_modules", ".bin", "tsgolint"));
4446
4507
  } catch {
4447
4508
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
4448
4509
  }
4449
- return candidates.find((candidate) => candidate !== "" && existsSync18(candidate));
4510
+ return candidates.find((candidate) => candidate !== "" && existsSync20(candidate));
4450
4511
  }
4451
4512
  function oxlintSearchPath(cwd) {
4452
4513
  return binSearchPath(cwd);
4453
4514
  }
4454
4515
 
4455
4516
  // src/roles/lint/adapters/oxlint/plan.ts
4456
- import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
4457
- import { join as join24 } from "path";
4517
+ import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
4518
+ import { join as join26 } from "path";
4458
4519
 
4459
4520
  // src/roles/lint/eslint-ignores.ts
4460
- import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
4461
- import { join as join22 } from "path";
4521
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
4522
+ import { join as join24 } from "path";
4462
4523
  import { parseSync as parseSync2 } from "oxc-parser";
4463
4524
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4464
4525
  function toOxlintPattern(pattern) {
@@ -4470,11 +4531,11 @@ function readRootEslintIgnores(root) {
4470
4531
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4471
4532
  }
4472
4533
  function readEslintIgnores(cwd) {
4473
- const config = ESLINT_CONFIG_FILES.map((name) => join22(cwd, name)).find((path) => existsSync19(path));
4534
+ const config = ESLINT_CONFIG_FILES.map((name) => join24(cwd, name)).find((path) => existsSync21(path));
4474
4535
  if (!config) return { patterns: [], unresolved: [] };
4475
4536
  let source;
4476
4537
  try {
4477
- source = readFileSync17(config, "utf8");
4538
+ source = readFileSync18(config, "utf8");
4478
4539
  } catch {
4479
4540
  return { patterns: [], unresolved: [] };
4480
4541
  }
@@ -4575,8 +4636,8 @@ function lintPresetFor(preset) {
4575
4636
  }
4576
4637
 
4577
4638
  // src/roles/lint/rename-suppressions.ts
4578
- import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync2 } from "fs";
4579
- import { join as join23, relative as relative5 } from "path";
4639
+ import { readdirSync as readdirSync2, readFileSync as readFileSync19, statSync as statSync3 } from "fs";
4640
+ import { join as join25, relative as relative4 } from "path";
4580
4641
  var SOURCE_EXTENSIONS = [
4581
4642
  ".ts",
4582
4643
  ".tsx",
@@ -4623,10 +4684,10 @@ function* sourceFiles(dir) {
4623
4684
  return;
4624
4685
  }
4625
4686
  for (const entry of entries) {
4626
- const full = join23(dir, entry);
4687
+ const full = join25(dir, entry);
4627
4688
  let isDirectory;
4628
4689
  try {
4629
- isDirectory = statSync2(full).isDirectory();
4690
+ isDirectory = statSync3(full).isDirectory();
4630
4691
  } catch {
4631
4692
  continue;
4632
4693
  }
@@ -4643,7 +4704,7 @@ function findSuppressionRenames(cwd, renames) {
4643
4704
  for (const file of sourceFiles(cwd)) {
4644
4705
  let content;
4645
4706
  try {
4646
- content = readFileSync18(file, "utf8");
4707
+ content = readFileSync19(file, "utf8");
4647
4708
  } catch {
4648
4709
  continue;
4649
4710
  }
@@ -4667,7 +4728,7 @@ function findSuppressionRenames(cwd, renames) {
4667
4728
  }
4668
4729
  if (hits.length === 0) return;
4669
4730
  found.push({
4670
- file: relative5(cwd, file),
4731
+ file: relative4(cwd, file),
4671
4732
  line: index + 1,
4672
4733
  from: line,
4673
4734
  to: line.replace(rules, renamed.join(", ")),
@@ -4771,7 +4832,7 @@ function plan2(context) {
4771
4832
  keys: removableDeps
4772
4833
  });
4773
4834
  }
4774
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync20(join24(context.cwd, name)));
4835
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync22(join26(context.cwd, name)));
4775
4836
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4776
4837
  operations.push(
4777
4838
  ...nxTargetOperations({
@@ -4869,7 +4930,7 @@ function lintScripts(cwd) {
4869
4930
  function committedExtends(cwd) {
4870
4931
  try {
4871
4932
  const parsed = parseJsonc(
4872
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4933
+ readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
4873
4934
  LINT_CONFIG_FILE
4874
4935
  );
4875
4936
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4880,7 +4941,7 @@ function committedExtends(cwd) {
4880
4941
  function committedIgnorePatterns(cwd) {
4881
4942
  try {
4882
4943
  const parsed = parseJsonc(
4883
- readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4944
+ readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
4884
4945
  LINT_CONFIG_FILE
4885
4946
  );
4886
4947
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4892,7 +4953,7 @@ function moduleEslintDependencies(cwd) {
4892
4953
  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/");
4893
4954
  let manifest;
4894
4955
  try {
4895
- manifest = JSON.parse(readFileSync19(join24(cwd, "package.json"), "utf8"));
4956
+ manifest = JSON.parse(readFileSync20(join26(cwd, "package.json"), "utf8"));
4896
4957
  } catch {
4897
4958
  return [];
4898
4959
  }
@@ -5026,20 +5087,20 @@ ${result.stderr ?? ""}`);
5026
5087
  * build the developer can see, rather than a silent half-adoption they cannot.
5027
5088
  */
5028
5089
  writeModuleBaseline(ctx, oxlint, env) {
5029
- const configPath = join25(ctx.cwd, LINT_CONFIG_FILE);
5030
- const baselinePath = join25(ctx.cwd, LINT_BASELINE_FILE);
5031
- if (!existsSync21(configPath)) return;
5090
+ const configPath = join27(ctx.cwd, LINT_CONFIG_FILE);
5091
+ const baselinePath = join27(ctx.cwd, LINT_BASELINE_FILE);
5092
+ if (!existsSync23(configPath)) return;
5032
5093
  let config;
5033
5094
  try {
5034
5095
  config = parseJsonc(
5035
- readFileSync20(configPath, "utf8"),
5096
+ readFileSync21(configPath, "utf8"),
5036
5097
  LINT_CONFIG_FILE
5037
5098
  );
5038
5099
  } catch {
5039
5100
  return;
5040
5101
  }
5041
5102
  const current = Array.isArray(config.extends) ? config.extends : [];
5042
- const measurePath = join25(ctx.cwd, LINT_MEASURE_FILE);
5103
+ const measurePath = join27(ctx.cwd, LINT_MEASURE_FILE);
5043
5104
  let measured;
5044
5105
  try {
5045
5106
  writeFileSync2(
@@ -5087,7 +5148,7 @@ ${result.stderr ?? ""}`);
5087
5148
  }
5088
5149
  }
5089
5150
  async run(ctx) {
5090
- if (!existsSync21(join25(ctx.cwd, LINT_CONFIG_FILE))) {
5151
+ if (!existsSync23(join27(ctx.cwd, LINT_CONFIG_FILE))) {
5091
5152
  process.stderr.write(
5092
5153
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
5093
5154
  `
@@ -5235,7 +5296,7 @@ ${result.stderr ?? ""}`);
5235
5296
  let stub;
5236
5297
  try {
5237
5298
  stub = parseJsonc(
5238
- readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
5299
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
5239
5300
  LINT_CONFIG_FILE
5240
5301
  );
5241
5302
  } catch {
@@ -5245,7 +5306,7 @@ ${result.stderr ?? ""}`);
5245
5306
  for (const entry of stub.extends ?? []) {
5246
5307
  try {
5247
5308
  const preset = parseJsonc(
5248
- readFileSync20(join25(cwd, entry), "utf8"),
5309
+ readFileSync21(join27(cwd, entry), "utf8"),
5249
5310
  entry
5250
5311
  );
5251
5312
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5315,14 +5376,14 @@ ${result.stderr ?? ""}`);
5315
5376
  let parsed;
5316
5377
  try {
5317
5378
  parsed = parseJsonc(
5318
- readFileSync20(join25(cwd, LINT_CONFIG_FILE), "utf8"),
5379
+ readFileSync21(join27(cwd, LINT_CONFIG_FILE), "utf8"),
5319
5380
  LINT_CONFIG_FILE
5320
5381
  );
5321
5382
  } catch {
5322
5383
  return void 0;
5323
5384
  }
5324
5385
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5325
- return targets.find((target) => !existsSync21(join25(cwd, target)));
5386
+ return targets.find((target) => !existsSync23(join27(cwd, target)));
5326
5387
  }
5327
5388
  /** Announce what is not enforced, so reduced coverage is never silent. */
5328
5389
  announceDisabled(preset) {
@@ -5354,9 +5415,9 @@ function registerLint() {
5354
5415
 
5355
5416
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
5356
5417
  import { spawnSync as spawnSync5 } from "child_process";
5357
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
5418
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
5358
5419
  import { createRequire as createRequire4 } from "module";
5359
- import { join as join27 } from "path";
5420
+ import { join as join29 } from "path";
5360
5421
 
5361
5422
  // src/roles/typescript/presets/nest.json
5362
5423
  var nest_default2 = {
@@ -5537,8 +5598,8 @@ function readTsconfigAdoption(cwd) {
5537
5598
  }
5538
5599
 
5539
5600
  // src/roles/typescript/adapters/tsc/plan.ts
5540
- import { readFileSync as readFileSync21 } from "fs";
5541
- import { join as join26 } from "path";
5601
+ import { readFileSync as readFileSync22 } from "fs";
5602
+ import { join as join28 } from "path";
5542
5603
 
5543
5604
  // src/roles/typescript/typecheck-script.ts
5544
5605
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5631,7 +5692,7 @@ function planAdoption(context) {
5631
5692
  };
5632
5693
  }
5633
5694
  const existing = parseJsonc(
5634
- readFileSync21(join26(context.cwd, target.path), "utf8"),
5695
+ readFileSync22(join28(context.cwd, target.path), "utf8"),
5635
5696
  target.path
5636
5697
  );
5637
5698
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5769,7 +5830,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5769
5830
  let chain;
5770
5831
  try {
5771
5832
  const parsed = parseJsonc(
5772
- readFileSync22(join27(cwd, target.path), "utf8"),
5833
+ readFileSync23(join29(cwd, target.path), "utf8"),
5773
5834
  target.path
5774
5835
  );
5775
5836
  chain = parsed.extends;
@@ -5782,7 +5843,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5782
5843
  );
5783
5844
  if (preset === void 0) return void 0;
5784
5845
  try {
5785
- createRequire4(join27(cwd, "noop.js")).resolve(preset);
5846
+ createRequire4(join29(cwd, "noop.js")).resolve(preset);
5786
5847
  return void 0;
5787
5848
  } catch {
5788
5849
  return preset;
@@ -5878,7 +5939,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5878
5939
  referencedProjects(cwd, config) {
5879
5940
  try {
5880
5941
  const parsed = parseJsonc(
5881
- readFileSync22(join27(cwd, config), "utf8"),
5942
+ readFileSync23(join29(cwd, config), "utf8"),
5882
5943
  config
5883
5944
  );
5884
5945
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5894,7 +5955,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5894
5955
  * check.
5895
5956
  */
5896
5957
  typecheckTarget(cwd) {
5897
- if (existsSync22(join27(cwd, "tsconfig.json"))) return "tsconfig.json";
5958
+ if (existsSync24(join29(cwd, "tsconfig.json"))) return "tsconfig.json";
5898
5959
  const target = resolveTsconfigTarget(cwd);
5899
5960
  return target.reason === "none" ? null : target.path;
5900
5961
  }
@@ -6098,7 +6159,7 @@ function replaceLines(current, replacements) {
6098
6159
  }
6099
6160
 
6100
6161
  // src/core/apply-plan.ts
6101
- import { existsSync as existsSync23, readFileSync as readFileSync23, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6162
+ import { existsSync as existsSync25, readFileSync as readFileSync24, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6102
6163
  import { resolve as resolve4, sep } from "path";
6103
6164
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
6104
6165
 
@@ -6117,7 +6178,7 @@ function resolveWithinRoot(cwd, relativePath) {
6117
6178
  return absolutePath;
6118
6179
  }
6119
6180
  function readIfExists(absolutePath) {
6120
- return existsSync23(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
6181
+ return existsSync25(absolutePath) ? readFileSync24(absolutePath, "utf8") : void 0;
6121
6182
  }
6122
6183
  function* leaves(value, prefix = []) {
6123
6184
  for (const [key, keyValue] of Object.entries(value)) {
@@ -6235,8 +6296,8 @@ function applyPlan(cwd, plan3) {
6235
6296
  }
6236
6297
 
6237
6298
  // src/core/config/preset-evidence.ts
6238
- import { existsSync as existsSync24, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "fs";
6239
- import { join as join28 } from "path";
6299
+ import { existsSync as existsSync26, readdirSync as readdirSync3, readFileSync as readFileSync25 } from "fs";
6300
+ import { join as join30 } from "path";
6240
6301
  var PATH_SIGNALS = [
6241
6302
  {
6242
6303
  preset: "nest",
@@ -6250,11 +6311,11 @@ var DEPENDENCY_SIGNALS = [
6250
6311
  { preset: "nest", pattern: /^@nestjs\// }
6251
6312
  ];
6252
6313
  function dependencyNames(cwd) {
6253
- const path = join28(cwd, "package.json");
6254
- if (!existsSync24(path)) return [];
6314
+ const path = join30(cwd, "package.json");
6315
+ if (!existsSync26(path)) return [];
6255
6316
  try {
6256
6317
  const manifest = parseJsonc(
6257
- readFileSync24(path, "utf8"),
6318
+ readFileSync25(path, "utf8"),
6258
6319
  path
6259
6320
  );
6260
6321
  return [
@@ -6277,7 +6338,7 @@ function declaresJsx(cwd) {
6277
6338
  for (const name of entries) {
6278
6339
  try {
6279
6340
  const config = parseJsonc(
6280
- readFileSync24(join28(cwd, name), "utf8"),
6341
+ readFileSync25(join30(cwd, name), "utf8"),
6281
6342
  name
6282
6343
  );
6283
6344
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -6442,19 +6503,19 @@ export {
6442
6503
  buildConfigFile,
6443
6504
  BaseAdapter,
6444
6505
  resolveBin,
6506
+ WORKSPACE_ROOT_MARKER,
6507
+ findWorkspaceRoot,
6508
+ ensureWorkspacePrep,
6509
+ inspectWorkspacePrep,
6445
6510
  VERBS,
6446
6511
  TARGETS,
6447
6512
  LONG_RUNNING_TARGETS,
6448
6513
  SWEEPABLE_TARGETS,
6449
6514
  PRESET_NAMES,
6450
- WORKSPACE_ROOT_MARKER,
6451
- findWorkspaceRoot,
6452
- ensureWorkspacePrep,
6453
- inspectWorkspacePrep,
6454
6515
  palette,
6455
6516
  registerAdapters,
6456
6517
  describeFramework,
6457
6518
  detectFramework,
6458
6519
  dispatch
6459
6520
  };
6460
- //# sourceMappingURL=chunk-RV34GQVH.js.map
6521
+ //# sourceMappingURL=chunk-CWJX5DVF.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-RV34GQVH.js";
10
+ } from "./chunk-CWJX5DVF.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.15",
3
+ "version": "1.2.0-alpha.17",
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",