@hublo/sentinel 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
package/README.md CHANGED
@@ -236,30 +236,41 @@ mean to.)
236
236
 
237
237
  ## CLI
238
238
 
239
+ A command **composes** three axes: **verb + type + location**.
240
+
239
241
  ```
240
- sentinel <verb> <target> [options]
242
+ sentinel <verb> [type] [options]
241
243
 
242
244
  VERBS --run execute the target's tool
243
- --inspect show the resolved configuration
244
- --update generate/apply the config stubs
245
+ --inspect show the resolved configuration (incl. deferred rules)
245
246
  --report metrics and health
247
+ --update generate/apply the config stubs (writes; one module only)
248
+
249
+ TYPES --lint --format --typescript --build --test
250
+ --static-analysis --runtime-analysis --arch
251
+ (omit a type → ALL types; or --all)
246
252
 
247
- TARGETS --lint --format --typescript --build --test
248
- --static-analysis --runtime-analysis --arch --all
253
+ LOCATION in a MODULE dir → that module (do NOT pass --module)
254
+ at the workspace ROOT → --module <name> (one) · --ci (affected) · else all
249
255
 
250
- OPTIONS --module <name> scope to a module (planned; run sentinel from the module dir for now)
256
+ OPTIONS --module <name> from the root: scope to one module
251
257
  --flavour <name> stack preset, declared not detected (react, nest, ...)
252
258
  --runner <tool> override the default runner
253
- --ci non-zero exit on failure
259
+ --ci from the root: affected only; non-zero exit on failure
254
260
  --fix auto-fix where applicable
261
+ --dry-run preview a --update without writing
255
262
 
256
- EXAMPLES (run from the app directory)
257
- sentinel --run --typescript
258
- sentinel --update --lint --flavour react
259
- sentinel --run --lint --runner=oxlint
260
- sentinel --report --all --ci
263
+ EXAMPLES sentinel --run --typescript # in a module → that module
264
+ sentinel --report --typescript --module bff-admin # from root → one module
265
+ sentinel --report # from root → all types, all modules
266
+ sentinel --report --ci # from root → affected (CI)
267
+ sentinel --update --typescript --flavour react # write stubs for the current module
261
268
  ```
262
269
 
270
+ `--run`/`--inspect`/`--report` share one context rule (developer from a module, or
271
+ from the root for a name / affected / all); `--update` writes, so it targets one
272
+ module only (adopting every module at once is refused, adopt gradually).
273
+
263
274
  ## Repository layout
264
275
 
265
276
  ```
@@ -2,17 +2,20 @@
2
2
  import {
3
3
  detectFramework,
4
4
  dispatch,
5
+ readNxProjectName,
5
6
  readOwnVersion,
6
7
  readProjectPackageJson,
7
8
  registerAdapters,
8
9
  resolve,
9
10
  resolveBin
10
- } from "../chunk-6AT72CEW.js";
11
+ } from "../chunk-PK3MT5ZK.js";
11
12
 
12
13
  // bin/sentinel.ts
14
+ import { program } from "commander";
15
+
16
+ // src/core/context.ts
13
17
  import { existsSync } from "fs";
14
18
  import { basename, join as join2 } from "path";
15
- import { program } from "commander";
16
19
 
17
20
  // src/core/discover-modules.ts
18
21
  import { execFileSync } from "child_process";
@@ -64,6 +67,42 @@ function discoverModules(cwd2, options = {}) {
64
67
  return modules.filter((module) => affected.has(module.name));
65
68
  }
66
69
 
70
+ // src/core/settings.ts
71
+ var WORKSPACE_ROOT_MARKER = "nx.json";
72
+
73
+ // src/core/context.ts
74
+ var MODULE_MARKERS = ["package.json", "project.json"];
75
+ function isModuleDir(cwd2) {
76
+ return MODULE_MARKERS.some((marker) => existsSync(join2(cwd2, marker)));
77
+ }
78
+ function resolveContext(cwd2, opts2) {
79
+ const atRoot = existsSync(join2(cwd2, WORKSPACE_ROOT_MARKER));
80
+ if (!atRoot && isModuleDir(cwd2)) {
81
+ if (opts2.module) {
82
+ throw new Error(
83
+ "You are in a module directory: drop --module (the context is the current module)."
84
+ );
85
+ }
86
+ if (opts2.ci) {
87
+ throw new Error("--ci selects the affected set from the workspace root; run it there.");
88
+ }
89
+ const name = readNxProjectName(cwd2) ?? basename(cwd2);
90
+ return { modules: [{ name, root: cwd2 }], scope: "cwd-module" };
91
+ }
92
+ if (atRoot) {
93
+ if (opts2.module) {
94
+ const found = discoverModules(cwd2).find((module) => module.name === opts2.module);
95
+ if (!found) throw new Error(`module "${opts2.module}" not found in the workspace.`);
96
+ return { modules: [found], scope: "named-module" };
97
+ }
98
+ if (opts2.ci) return { modules: discoverModules(cwd2, { affected: true }), scope: "affected" };
99
+ return { modules: discoverModules(cwd2), scope: "all" };
100
+ }
101
+ throw new Error(
102
+ `Run sentinel from a module directory or the workspace root (found neither ${MODULE_MARKERS.join("/")} nor ${WORKSPACE_ROOT_MARKER} here).`
103
+ );
104
+ }
105
+
67
106
  // src/core/domain.ts
68
107
  var VERBS = ["run", "inspect", "update", "report"];
69
108
  var TARGETS = [
@@ -100,7 +139,16 @@ async function analyse(params) {
100
139
  continue;
101
140
  }
102
141
  try {
103
- if (params.verb === "report") {
142
+ if (params.verb === "run") {
143
+ if (params.modules.length > 1) {
144
+ process.stderr.write(`
145
+ \u25B6 ${module.name} (${flavour2}) ${target}
146
+ `);
147
+ }
148
+ const result = await adapter.run(ctx);
149
+ results.push({ project: module.name, target, flavour: flavour2, ok: result.ok, data: {} });
150
+ worstCode = Math.max(worstCode, result.code);
151
+ } else if (params.verb === "report") {
104
152
  const result = await adapter.report(ctx);
105
153
  results.push({
106
154
  project: module.name,
@@ -133,31 +181,30 @@ function generateSummaries(results) {
133
181
  return { schemaVersion: 1, results: results.map(generateSummary) };
134
182
  }
135
183
 
136
- // src/core/settings.ts
137
- var WORKSPACE_ROOT_MARKER = "nx.json";
138
-
139
184
  // bin/sentinel.ts
140
185
  program.name("sentinel").description("One CLI that guards code health: presets, analysis, and arch checks.").version(readOwnVersion()).configureHelp({ sortOptions: false }).showSuggestionAfterError(true).showHelpAfterError('(run "sentinel --help" for usage)').addHelpText(
141
186
  "before",
142
187
  [
143
- "A check reads as: verb + target [+ --runner]. Run it from the app directory.",
144
- " verb what to do: --run --inspect --update --report",
145
- " target the check: --lint --typescript ... (or --all)",
146
- " runner the tool behind a target (a default is set per target; override here)",
188
+ "A check composes: verb + type + location.",
189
+ " verb what to do: --run --inspect --report --update",
190
+ " type which check: --lint --typescript ... (omit = all types; or --all)",
191
+ " where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
192
+ " --ci (affected), or all modules. --update targets one module only.",
147
193
  ""
148
194
  ].join("\n")
149
195
  ).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--update", "generate/apply the config stubs").option("--report", "metrics and health report").option("--lint", "linting").option("--format", "formatting").option("--typescript", "type checking").option("--build", "build").option("--test", "tests").option("--static-analysis", "cycles, complexity, duplication, centrality").option("--runtime-analysis", "bundle, Lighthouse, web vitals").option("--arch", "architecture boundaries").option("--all", "every target").option(
150
196
  "--module <name>",
151
- "scope to a module (planned; for now run sentinel from the module directory)"
152
- ).option("--flavour <name>", `stack preset, declared not detected (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: non-zero exit on failure (report/inspect: affected only)").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--update)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").addHelpText(
197
+ "from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
198
+ ).option("--flavour <name>", `stack preset, declared not detected (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--update)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").addHelpText(
153
199
  "after",
154
200
  [
155
201
  "",
156
- "Examples (run from the app directory):",
157
- " sentinel --run --typescript",
158
- " sentinel --update --lint --flavour react",
159
- " sentinel --run --lint --runner=oxlint",
160
- " sentinel --report --all --ci"
202
+ "Examples:",
203
+ " sentinel --run --typescript # in a module \u2192 that module",
204
+ " sentinel --report --typescript --module bff-admin # from root \u2192 one module",
205
+ " sentinel --report # from root \u2192 all types, all modules",
206
+ " sentinel --report --ci # from root \u2192 affected only",
207
+ " sentinel --update --typescript --flavour react # write stubs for the current module"
161
208
  ].join("\n")
162
209
  ).parse();
163
210
  var opts = program.opts();
@@ -187,44 +234,20 @@ function asMessage(err) {
187
234
  return err instanceof Error ? err.message : String(err);
188
235
  }
189
236
  var verb = pickOne("verb", VERBS);
190
- var isAnalyse = verb === "report" || verb === "inspect";
191
237
  var cwd = process.cwd();
238
+ var flavour = parseFlavour(opts.flavour);
192
239
  var namedTargets = TARGETS.filter((t) => opts[toCamel(t)]);
193
240
  if (opts.all && namedTargets.length > 0) {
194
241
  program.error(
195
242
  `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(", ")}.`
196
243
  );
197
244
  }
198
- var targets = opts.all || isAnalyse && namedTargets.length === 0 ? [...TARGETS] : isAnalyse ? namedTargets : [pickOne("target", TARGETS)];
199
- var flavour = parseFlavour(opts.flavour);
245
+ var targets = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets;
200
246
  if (opts.dryRun && verb !== "update") {
201
- program.error("--dry-run only applies to --update (report/inspect never write).");
202
- }
203
- if (opts.module && !isAnalyse) {
204
- program.error(
205
- "--module is not supported yet for --run/--update: cd into the module directory and run sentinel there."
206
- );
247
+ program.error("--dry-run only applies to --update (the read verbs never write).");
207
248
  }
208
- if (!isAnalyse && existsSync(join2(cwd, WORKSPACE_ROOT_MARKER))) {
209
- program.error(
210
- `Run sentinel from a module directory, not the workspace root (found ${WORKSPACE_ROOT_MARKER} here). Per-module is the model until --module resolution lands.`
211
- );
212
- }
213
- var MODULE_MARKERS = ["package.json", "project.json"];
214
- if (!isAnalyse && !MODULE_MARKERS.some((marker) => existsSync(join2(cwd, marker)))) {
215
- program.error(
216
- `This directory is not a module (no ${MODULE_MARKERS.join(" or ")}). cd into the module you want to ${verb === "update" ? "update" : "check"} and run sentinel there.`
217
- );
218
- }
219
- async function runAnalyse() {
220
- let modules;
221
- if (opts.module) {
222
- const found = discoverModules(cwd).find((m) => m.name === opts.module);
223
- if (!found) program.error(`module "${opts.module}" not found in the workspace.`);
224
- modules = [found];
225
- } else {
226
- modules = discoverModules(cwd, { affected: Boolean(opts.ci) });
227
- }
249
+ async function runVerb() {
250
+ const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
228
251
  const started = Date.now();
229
252
  const { results, worstCode } = await analyse({
230
253
  verb,
@@ -236,32 +259,48 @@ async function runAnalyse() {
236
259
  onProgress: (done, total, name) => process.stderr.write(` [${done}/${total}] ${name}
237
260
  `)
238
261
  });
239
- const summary = generateSummaries(results);
240
- if (opts.json) {
241
- process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
262
+ if (verb === "report" || verb === "inspect") {
263
+ const summary = generateSummaries(results);
264
+ if (opts.json) {
265
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
266
+ } else {
267
+ for (const item of summary.results) {
268
+ const details = Object.entries(item).filter(([key]) => !["project", "target", "flavour", "ok"].includes(key)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ");
269
+ process.stdout.write(
270
+ ` ${item.ok ? "\u2713" : "\u2717"} ${item.project} (${item.flavour}) ${item.target}${details ? ` \u2014 ${details}` : ""}
271
+ `
272
+ );
273
+ }
274
+ }
242
275
  } else {
243
- for (const item of summary.results) {
244
- const details = Object.entries(item).filter(([key]) => !["project", "target", "flavour", "ok"].includes(key)).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ");
276
+ for (const item of results) {
245
277
  process.stdout.write(
246
- ` ${item.ok ? "\u2713" : "\u2717"} ${item.project} (${item.flavour}) ${item.target}${details ? ` \u2014 ${details}` : ""}
278
+ ` ${item.ok ? "\u2713" : "\u2717"} ${item.project} (${item.flavour}) ${item.target}
247
279
  `
248
280
  );
249
281
  }
250
282
  }
251
- process.stderr.write(` ${modules.length} module(s) in ${Date.now() - started}ms
283
+ process.stderr.write(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
252
284
  `);
253
- return opts.ci ? worstCode : 0;
285
+ return verb === "run" || opts.ci ? worstCode : 0;
254
286
  }
255
- async function runPerModule() {
287
+ async function runUpdate() {
288
+ const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false });
289
+ const [module] = modules;
290
+ if (scope === "all" || scope === "affected" || !module) {
291
+ program.error(
292
+ "--update writes files: target one module (run from its directory, or pass --module). Adopting every module at once is intentionally not allowed \u2014 adopt gradually."
293
+ );
294
+ }
256
295
  let worst = 0;
257
- for (const target of targets) {
296
+ for (const type of targets) {
258
297
  try {
259
298
  const code = await dispatch({
260
299
  verb,
261
- target,
300
+ target: type,
262
301
  runner: opts.runner,
263
- module: basename(cwd),
264
- cwd,
302
+ module: module.name,
303
+ cwd: module.root,
265
304
  flavour,
266
305
  ci: Boolean(opts.ci),
267
306
  fix: Boolean(opts.fix),
@@ -271,7 +310,7 @@ async function runPerModule() {
271
310
  worst = Math.max(worst, code);
272
311
  } catch (err) {
273
312
  process.stderr.write(`
274
- sentinel (${target}): ${asMessage(err)}
313
+ sentinel (${type}): ${asMessage(err)}
275
314
  `);
276
315
  if (targets.length === 1) return 1;
277
316
  worst = Math.max(worst, 1);
@@ -280,7 +319,7 @@ sentinel (${target}): ${asMessage(err)}
280
319
  return worst;
281
320
  }
282
321
  registerAdapters();
283
- (isAnalyse ? runAnalyse() : runPerModule()).then((code) => process.exit(code)).catch((err) => {
322
+ (verb === "update" ? runUpdate() : runVerb()).then((code) => process.exit(code)).catch((err) => {
284
323
  process.stderr.write(`
285
324
  sentinel: ${asMessage(err)}
286
325
  `);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../bin/sentinel.ts","../../src/core/discover-modules.ts","../../src/core/domain.ts","../../src/core/orchestrate.ts","../../src/core/settings.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: `sentinel <verb> <target> [options]`.\n *\n * Verbs and targets are boolean flags (exactly one verb; one target, or `--all`).\n * The CLI is generic: it resolves each target's adapter (honouring `--runner`) and\n * dispatches. It knows nothing about any specific tool; those arrive as adapters in\n * later tickets. It never detects the stack: the flavour is declared in a project's\n * committed config, or passed explicitly with `--flavour`.\n */\nimport { existsSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { program } from 'commander'\n\nimport { registerAdapters } from '../src/adapters.js'\nimport { discoverModules, type ModuleRef } from '../src/core/discover-modules.js'\nimport { FLAVOURS, TARGETS, VERBS, type Flavour, type Target } from '../src/core/domain.js'\nimport { dispatch } from '../src/core/dispatch.js'\nimport { analyse, generateSummaries } from '../src/core/orchestrate.js'\nimport { WORKSPACE_ROOT_MARKER } from '../src/core/settings.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\nprogram\n .name('sentinel')\n .description('One CLI that guards code health: presets, analysis, and arch checks.')\n .version(readOwnVersion())\n .configureHelp({ sortOptions: false })\n // On any parse error (unknown flag, missing value): suggest the closest flag and\n // point at --help, so a typo like `--lnt` gets \"did you mean --lint?\".\n .showSuggestionAfterError(true)\n .showHelpAfterError('(run \"sentinel --help\" for usage)')\n .addHelpText(\n 'before',\n [\n 'A check reads as: verb + target [+ --runner]. Run it from the app directory.',\n ' verb what to do: --run --inspect --update --report',\n ' target the check: --lint --typescript ... (or --all)',\n ' runner the tool behind a target (a default is set per target; override here)',\n '',\n ].join('\\n'),\n )\n // verbs (pick one)\n .option('--run', 'execute the target tool')\n .option('--inspect', 'show the resolved configuration')\n .option('--update', 'generate/apply the config stubs')\n .option('--report', 'metrics and health report')\n // targets (pick one, or --all)\n .option('--lint', 'linting')\n .option('--format', 'formatting')\n .option('--typescript', 'type checking')\n .option('--build', 'build')\n .option('--test', 'tests')\n .option('--static-analysis', 'cycles, complexity, duplication, centrality')\n .option('--runtime-analysis', 'bundle, Lighthouse, web vitals')\n .option('--arch', 'architecture boundaries')\n .option('--all', 'every target')\n // modifiers\n .option(\n '--module <name>',\n 'scope to a module (planned; for now run sentinel from the module directory)',\n )\n .option('--flavour <name>', `stack preset, declared not detected (${FLAVOURS.join(', ')})`)\n .option('--runner <tool>', 'override the default runner (e.g. eslint, biome)')\n .option('--ci', 'CI mode: non-zero exit on failure (report/inspect: affected only)')\n .option('--fix', 'auto-fix where applicable')\n .option('--dry-run', 'preview the changes without writing (--update)')\n .option('--json', 'machine-readable JSON output (report/inspect/--dry-run)')\n .addHelpText(\n 'after',\n [\n '',\n 'Examples (run from the app directory):',\n ' sentinel --run --typescript',\n ' sentinel --update --lint --flavour react',\n ' sentinel --run --lint --runner=oxlint',\n ' sentinel --report --all --ci',\n ].join('\\n'),\n )\n .parse()\n\nconst opts = program.opts()\n\n// commander camelCases hyphenated flags (--static-analysis -> staticAnalysis).\nfunction toCamel(flag: string): string {\n return flag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())\n}\n\n/** Exactly one of `keys` must be flagged; return it, else fail with guidance. */\nfunction pickOne<T extends string>(kind: string, keys: readonly T[]): T {\n const chosen = keys.filter((k) => opts[toCamel(k)])\n if (chosen.length !== 1) {\n const supported = keys.map((k) => `--${k}`).join(', ')\n program.error(\n chosen.length === 0\n ? `Missing a ${kind}. Supported: ${supported}.`\n : `Pick exactly one ${kind}: got ${chosen.map((k) => `--${k}`).join(', ')}.`,\n )\n }\n return chosen[0] as T\n}\n\n/**\n * The explicit `--flavour`, VALIDATED, or undefined when not given. sentinel never\n * detects the stack: a project's flavour is declared in its committed config, or\n * passed here for `--update`. An unknown value is a hard error (typo caught), not\n * a silent guess.\n */\nfunction parseFlavour(value: unknown): Flavour | undefined {\n if (value === undefined) return undefined\n if (typeof value !== 'string' || !FLAVOURS.includes(value as Flavour)) {\n return program.error(\n `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(', ')}.`,\n )\n }\n return value as Flavour\n}\n\nfunction asMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nconst verb = pickOne('verb', VERBS)\n// report/inspect are read-only workspace queries; run/update mutate one module.\nconst isAnalyse = verb === 'report' || verb === 'inspect'\nconst cwd = process.cwd()\n\n// Reject a contradictory `--all --<target>` rather than silently ignoring one.\nconst namedTargets = TARGETS.filter((t) => opts[toCamel(t)])\nif (opts.all && namedTargets.length > 0) {\n program.error(\n `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(', ')}.`,\n )\n}\n\n// Targets: report/inspect default to ALL when none is named; run/update need one.\nconst targets: Target[] =\n opts.all || (isAnalyse && namedTargets.length === 0)\n ? [...TARGETS]\n : isAnalyse\n ? namedTargets\n : [pickOne('target', TARGETS)]\n\nconst flavour = parseFlavour(opts.flavour)\n\n// --dry-run only previews a mutation; report/inspect never write, so it is moot there.\nif (opts.dryRun && verb !== 'update') {\n program.error('--dry-run only applies to --update (report/inspect never write).')\n}\n\n// --module is resolved via nx for report/inspect; deferred for the cwd-based verbs.\nif (opts.module && !isAnalyse) {\n program.error(\n '--module is not supported yet for --run/--update: cd into the module directory and run sentinel there.',\n )\n}\n\n// Workspace-root guard: only the mutating/per-cwd verbs. report/inspect are meant\n// to run at the root (that is where they discover every module).\nif (!isAnalyse && existsSync(join(cwd, WORKSPACE_ROOT_MARKER))) {\n program.error(\n `Run sentinel from a module directory, not the workspace root (found ${WORKSPACE_ROOT_MARKER} here). Per-module is the model until --module resolution lands.`,\n )\n}\n\n// Positive module guard: run/update act on the current directory, so refuse to run\n// (and never scaffold files) unless it actually looks like a module. This stops a\n// mistyped path from creating a stray package.json/tsconfig anywhere on disk.\nconst MODULE_MARKERS = ['package.json', 'project.json']\nif (!isAnalyse && !MODULE_MARKERS.some((marker) => existsSync(join(cwd, marker)))) {\n program.error(\n `This directory is not a module (no ${MODULE_MARKERS.join(' or ')}). cd into the module you want to ${verb === 'update' ? 'update' : 'check'} and run sentinel there.`,\n )\n}\n\n/** report/inspect: discover the modules, analyse them, aggregate + print. */\nasync function runAnalyse(): Promise<number> {\n let modules: ModuleRef[]\n if (opts.module) {\n const found = discoverModules(cwd).find((m) => m.name === opts.module)\n if (!found) program.error(`module \"${opts.module}\" not found in the workspace.`)\n modules = [found]\n } else {\n modules = discoverModules(cwd, { affected: Boolean(opts.ci) })\n }\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'report' | 'inspect',\n targets,\n modules,\n runner: opts.runner,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) => process.stderr.write(` [${done}/${total}] ${name}\\n`),\n })\n\n const summary = generateSummaries(results)\n if (opts.json) {\n process.stdout.write(JSON.stringify(summary, null, 2) + '\\n')\n } else {\n for (const item of summary.results) {\n const details = Object.entries(item)\n .filter(([key]) => !['project', 'target', 'flavour', 'ok'].includes(key))\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ')\n process.stdout.write(\n ` ${item.ok ? '✓' : '✗'} ${item.project} (${item.flavour}) ${item.target}${details ? ` — ${details}` : ''}\\n`,\n )\n }\n }\n process.stderr.write(` ${modules.length} module(s) in ${Date.now() - started}ms\\n`)\n return opts.ci ? worstCode : 0\n}\n\n/** run/update: operate on the current module (one target, or `--all`). */\nasync function runPerModule(): Promise<number> {\n let worst = 0\n for (const target of targets) {\n try {\n const code = await dispatch({\n verb,\n target,\n runner: opts.runner,\n module: basename(cwd),\n cwd,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // With --all, report per target and keep going; a single target fails hard.\n process.stderr.write(`\\nsentinel (${target}): ${asMessage(err)}\\n`)\n if (targets.length === 1) return 1\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\n\nregisterAdapters() // wire in every tool adapter; the CLI itself never lists them\n\n;(isAnalyse ? runAnalyse() : runPerModule())\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`\\nsentinel: ${asMessage(err)}\\n`)\n process.exit(1)\n })\n","/**\n * Module discovery via nx. `--report`/`--inspect` without a `--module` analyse\n * every project; `--ci` narrows to the affected ones. We ask nx (its cached graph)\n * for the authoritative names + roots in a couple of calls, not one per project,\n * so it stays fast on the real monorepo.\n */\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { resolveBin } from '../shared/resolve-bin.js'\n\n/** A discovered module: its nx project name and absolute root. */\nexport interface ModuleRef {\n name: string\n root: string\n}\n\nfunction runNx(cwd: string, args: string[]): string {\n const nx = resolveBin(cwd, 'nx') ?? 'nx'\n try {\n // Disable the daemon for deterministic, CI-friendly one-shot invocations.\n return execFileSync(nx, args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, NX_DAEMON: 'false' },\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,\n { cause: error },\n )\n }\n}\n\n/** Every module (name -> absolute root) from the nx project graph, in one call. */\nfunction readGraph(cwd: string): ModuleRef[] {\n const dir = mkdtempSync(join(tmpdir(), 'sentinel-nx-'))\n const file = join(dir, 'graph.json')\n try {\n runNx(cwd, ['graph', '--file', file])\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as {\n graph?: { nodes?: Record<string, { data: { root: string } }> }\n }\n const nodes = parsed.graph?.nodes\n if (!nodes || typeof nodes !== 'object') {\n throw new Error(\n 'sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible.',\n )\n }\n return Object.entries(nodes).map(([name, node]) => ({\n name,\n root: join(cwd, node.data.root),\n }))\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n\nexport function discoverModules(cwd: string, options: { affected?: boolean } = {}): ModuleRef[] {\n const modules = readGraph(cwd)\n if (!options.affected) return modules\n const affected = new Set(\n JSON.parse(runNx(cwd, ['show', 'projects', '--affected', '--json'])) as string[],\n )\n return modules.filter((module) => affected.has(module.name))\n}\n","/**\n * The domain vocabulary: the fixed sets of verbs, targets, and flavours, and the\n * types derived from them. This is the model, and the extension point: adding a\n * verb / target / flavour is a one-line edit to a list here, and because the types\n * are DERIVED (`(typeof LIST)[number]`), the compiler forces every switch/handler\n * to cover the new member.\n *\n * Tunable behaviour (defaults, detection signals, marker filenames) lives in\n * `settings.ts`, not here.\n */\n\n/** Verbs: what to do. Each maps to an adapter method in dispatch. */\nexport const VERBS = ['run', 'inspect', 'update', 'report'] as const\nexport type Verb = (typeof VERBS)[number]\n\n/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */\nexport const TARGETS = [\n 'lint',\n 'format',\n 'typescript',\n 'build',\n 'test',\n 'static-analysis',\n 'runtime-analysis',\n 'arch',\n] as const\nexport type Target = (typeof TARGETS)[number]\n\n/** Flavours: the stack preset a project resolves to (strict by default). */\nexport const FLAVOURS = ['react', 'nest', 'svelte', 'node'] as const\nexport type Flavour = (typeof FLAVOURS)[number]\n","/**\n * Orchestration for `--report`/`--inspect`: analyse a set of modules across a set\n * of targets, then shape the outcomes into one versioned, parseable summary.\n *\n * The engine resolves each module's flavour and adapter and calls the per-module\n * method; a target with no adapter yet is simply skipped. `generateSummaries`\n * builds the aggregate by reusing `generateSummary` for each result (one factory,\n * no duplicated shaping between the single- and multi-module paths).\n */\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, Target } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext } from './types.js'\n\n/** One project × target outcome. */\nexport interface AnalyseResult {\n project: string\n target: Target\n flavour: Flavour\n ok: boolean\n /** `--report`: metrics (errors, implicit-any); `--inspect`: the resolved config. */\n data: unknown\n}\n\nexport interface AnalyseParams {\n verb: 'report' | 'inspect'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n ci: boolean\n fix: boolean\n /** Called after each module, for progress display. */\n onProgress?: (done: number, total: number, moduleName: string) => void\n}\n\n/** Run the analyse across modules × targets. Returns the results + worst exit code. */\nexport async function analyse(\n params: AnalyseParams,\n): Promise<{ results: AnalyseResult[]; worstCode: number }> {\n const results: AnalyseResult[] = []\n let worstCode = 0\n let done = 0\n\n for (const module of params.modules) {\n const flavour = detectFramework(readProjectPackageJson(module.root))\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch {\n continue // no adapter for this target yet: skip it (not a failure)\n }\n // Isolate every check: one module/target that throws must not abort the whole\n // sweep. A failure becomes a `ok:false` row (with the error) so `--report --all`\n // still returns a complete picture across a large workspace.\n try {\n if (params.verb === 'report') {\n const result = await adapter.report(ctx)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n data: result.metrics ?? {},\n })\n worstCode = Math.max(worstCode, result.code)\n } else {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n results.push({ project: module.name, target, flavour, ok: false, data: { error: message } })\n worstCode = Math.max(worstCode, 1)\n }\n }\n params.onProgress?.((done += 1), params.modules.length, module.name)\n }\n\n return { results, worstCode }\n}\n\n/** The shaped summary for ONE result (the factory unit). */\nexport function generateSummary(result: AnalyseResult): Record<string, unknown> {\n const { project, target, flavour, ok, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, ...details }\n}\n\n/** The aggregate, versioned envelope for MANY results, built from `generateSummary`. */\nexport function generateSummaries(results: readonly AnalyseResult[]): {\n schemaVersion: number\n results: Record<string, unknown>[]\n} {\n return { schemaVersion: 1, results: results.map(generateSummary) }\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: sentinel does NOT detect the flavour. A project's flavour is declared in\n * its committed config (the stub's `extends`), or passed explicitly to `--update`.\n * We never guess, so there is no default flavour or detection table here.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n"],"mappings":"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","names":["join","cwd","flavour","join"]}
1
+ {"version":3,"sources":["../../bin/sentinel.ts","../../src/core/context.ts","../../src/core/discover-modules.ts","../../src/core/settings.ts","../../src/core/domain.ts","../../src/core/orchestrate.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: `sentinel <verb> <target> [options]`.\n *\n * Verbs and targets are boolean flags (exactly one verb; one target, or `--all`).\n * The CLI is generic: it resolves each target's adapter (honouring `--runner`) and\n * dispatches. It knows nothing about any specific tool; those arrive as adapters in\n * later tickets. It never detects the stack: the flavour is declared in a project's\n * committed config, or passed explicitly with `--flavour`.\n */\nimport { program } from 'commander'\n\nimport { registerAdapters } from '../src/adapters.js'\nimport { resolveContext } from '../src/core/context.js'\nimport { FLAVOURS, TARGETS, VERBS, type Flavour, type Target } from '../src/core/domain.js'\nimport { dispatch } from '../src/core/dispatch.js'\nimport { analyse, generateSummaries } from '../src/core/orchestrate.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\nprogram\n .name('sentinel')\n .description('One CLI that guards code health: presets, analysis, and arch checks.')\n .version(readOwnVersion())\n .configureHelp({ sortOptions: false })\n // On any parse error (unknown flag, missing value): suggest the closest flag and\n // point at --help, so a typo like `--lnt` gets \"did you mean --lint?\".\n .showSuggestionAfterError(true)\n .showHelpAfterError('(run \"sentinel --help\" for usage)')\n .addHelpText(\n 'before',\n [\n 'A check composes: verb + type + location.',\n ' verb what to do: --run --inspect --report --update',\n ' type which check: --lint --typescript ... (omit = all types; or --all)',\n ' where run from a MODULE dir → that module; from the ROOT → --module <name>,',\n ' --ci (affected), or all modules. --update targets one module only.',\n '',\n ].join('\\n'),\n )\n // verbs (pick one)\n .option('--run', 'execute the target tool')\n .option('--inspect', 'show the resolved configuration')\n .option('--update', 'generate/apply the config stubs')\n .option('--report', 'metrics and health report')\n // targets (pick one, or --all)\n .option('--lint', 'linting')\n .option('--format', 'formatting')\n .option('--typescript', 'type checking')\n .option('--build', 'build')\n .option('--test', 'tests')\n .option('--static-analysis', 'cycles, complexity, duplication, centrality')\n .option('--runtime-analysis', 'bundle, Lighthouse, web vitals')\n .option('--arch', 'architecture boundaries')\n .option('--all', 'every target')\n // modifiers\n .option(\n '--module <name>',\n 'from the workspace root: scope to one module (omit = all; inside a module dir, drop this)',\n )\n .option('--flavour <name>', `stack preset, declared not detected (${FLAVOURS.join(', ')})`)\n .option('--runner <tool>', 'override the default runner (e.g. eslint, biome)')\n .option('--ci', 'CI mode: from the root, only the affected modules; non-zero exit on failure')\n .option('--fix', 'auto-fix where applicable')\n .option('--dry-run', 'preview the changes without writing (--update)')\n .option('--json', 'machine-readable JSON output (report/inspect/--dry-run)')\n .addHelpText(\n 'after',\n [\n '',\n 'Examples:',\n ' sentinel --run --typescript # in a module → that module',\n ' sentinel --report --typescript --module bff-admin # from root → one module',\n ' sentinel --report # from root → all types, all modules',\n ' sentinel --report --ci # from root → affected only',\n ' sentinel --update --typescript --flavour react # write stubs for the current module',\n ].join('\\n'),\n )\n .parse()\n\nconst opts = program.opts()\n\n// commander camelCases hyphenated flags (--static-analysis -> staticAnalysis).\nfunction toCamel(flag: string): string {\n return flag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())\n}\n\n/** Exactly one of `keys` must be flagged; return it, else fail with guidance. */\nfunction pickOne<T extends string>(kind: string, keys: readonly T[]): T {\n const chosen = keys.filter((k) => opts[toCamel(k)])\n if (chosen.length !== 1) {\n const supported = keys.map((k) => `--${k}`).join(', ')\n program.error(\n chosen.length === 0\n ? `Missing a ${kind}. Supported: ${supported}.`\n : `Pick exactly one ${kind}: got ${chosen.map((k) => `--${k}`).join(', ')}.`,\n )\n }\n return chosen[0] as T\n}\n\n/**\n * The explicit `--flavour`, VALIDATED, or undefined when not given. sentinel never\n * detects the stack: a project's flavour is declared in its committed config, or\n * passed here for `--update`. An unknown value is a hard error (typo caught), not\n * a silent guess.\n */\nfunction parseFlavour(value: unknown): Flavour | undefined {\n if (value === undefined) return undefined\n if (typeof value !== 'string' || !FLAVOURS.includes(value as Flavour)) {\n return program.error(\n `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(', ')}.`,\n )\n }\n return value as Flavour\n}\n\nfunction asMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nconst verb = pickOne('verb', VERBS)\nconst cwd = process.cwd()\nconst flavour = parseFlavour(opts.flavour)\n\n// Targets (the \"type\" axis): a named one, or ALL when none is given. Uniform for every\n// verb — run / inspect / report / update. `resolveContext` owns the \"location\" axis.\nconst namedTargets = TARGETS.filter((t) => opts[toCamel(t)])\nif (opts.all && namedTargets.length > 0) {\n program.error(\n `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(', ')}.`,\n )\n}\nconst targets: Target[] = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets\n\n// --dry-run only previews a write.\nif (opts.dryRun && verb !== 'update') {\n program.error('--dry-run only applies to --update (the read verbs never write).')\n}\n\n/**\n * run / inspect / report: resolve the module context (cwd module, `--module`, all, or\n * `--ci` affected), then execute verb × targets across it and print.\n */\nasync function runVerb(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) })\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'run' | 'report' | 'inspect',\n targets,\n modules,\n runner: opts.runner,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) => process.stderr.write(` [${done}/${total}] ${name}\\n`),\n })\n\n if (verb === 'report' || verb === 'inspect') {\n const summary = generateSummaries(results)\n if (opts.json) {\n process.stdout.write(JSON.stringify(summary, null, 2) + '\\n')\n } else {\n for (const item of summary.results) {\n const details = Object.entries(item)\n .filter(([key]) => !['project', 'target', 'flavour', 'ok'].includes(key))\n .map(([key, value]) => `${key}=${JSON.stringify(value)}`)\n .join(' ')\n process.stdout.write(\n ` ${item.ok ? '✓' : '✗'} ${item.project} (${item.flavour}) ${item.target}${details ? ` — ${details}` : ''}\\n`,\n )\n }\n }\n } else {\n // run: the tools streamed their own output; print a pass/fail summary line each.\n for (const item of results) {\n process.stdout.write(\n ` ${item.ok ? '✓' : '✗'} ${item.project} (${item.flavour}) ${item.target}\\n`,\n )\n }\n }\n process.stderr.write(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms\\n`)\n // `run` always reports failures via its exit code; report/inspect only under --ci.\n return verb === 'run' || opts.ci ? worstCode : 0\n}\n\n/**\n * update: a WRITE. Same context rule, but it must resolve to exactly ONE module (the\n * current one, or `--module`). Adopting every module at once would be a big-bang, so\n * the implicit \"all\" is deliberately refused; adopt gradually.\n */\nasync function runUpdate(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false })\n const [module] = modules\n if (scope === 'all' || scope === 'affected' || !module) {\n program.error(\n '--update writes files: target one module (run from its directory, or pass --module). ' +\n 'Adopting every module at once is intentionally not allowed — adopt gradually.',\n )\n }\n let worst = 0\n for (const type of targets) {\n try {\n const code = await dispatch({\n verb,\n target: type,\n runner: opts.runner,\n module: module.name,\n cwd: module.root,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // With --all, report per target and keep going; a single target fails hard.\n process.stderr.write(`\\nsentinel (${type}): ${asMessage(err)}\\n`)\n if (targets.length === 1) return 1\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\n\nregisterAdapters() // wire in every tool adapter; the CLI itself never lists them\n\n;(verb === 'update' ? runUpdate() : runVerb())\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`\\nsentinel: ${asMessage(err)}\\n`)\n process.exit(1)\n })\n","/**\n * Context resolution: the composable \"location\" axis of a command.\n *\n * One rule for every read verb (run / inspect / report), so a developer works the way\n * they already work with nx, from their module or from the root:\n * - in a MODULE dir (has package.json/project.json, and is NOT the workspace root):\n * the context is THAT module; `--module` is redundant there and is rejected.\n * - at the workspace ROOT: `--module X` scopes to X, `--ci` scopes to the affected\n * set, and otherwise it is every module (the whole-repo status).\n * Update (a write) reuses this but forbids the implicit \"all\" (see the CLI).\n */\nimport { existsSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { readNxProjectName } from '../shared/package-json.js'\nimport { discoverModules, type ModuleRef } from './discover-modules.js'\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\n/** A directory is a module if it carries one of these (and is not the root). */\nconst MODULE_MARKERS = ['package.json', 'project.json'] as const\n\n/** How the context was resolved, for messages and the \"all\"-guard on update. */\nexport type ContextScope = 'cwd-module' | 'named-module' | 'affected' | 'all'\n\nexport interface ResolvedContext {\n modules: ModuleRef[]\n scope: ContextScope\n}\n\nfunction isModuleDir(cwd: string): boolean {\n return MODULE_MARKERS.some((marker) => existsSync(join(cwd, marker)))\n}\n\n/**\n * Resolve which modules a command targets from where it runs + its flags. Throws with\n * an actionable message when the combination is contradictory (e.g. `--module` from\n * inside a module) or the directory is neither a module nor the workspace root.\n */\nexport function resolveContext(\n cwd: string,\n opts: { module?: string; ci?: boolean },\n): ResolvedContext {\n const atRoot = existsSync(join(cwd, WORKSPACE_ROOT_MARKER))\n\n // In a module directory: the module IS the current one.\n if (!atRoot && isModuleDir(cwd)) {\n if (opts.module) {\n throw new Error(\n 'You are in a module directory: drop --module (the context is the current module).',\n )\n }\n if (opts.ci) {\n throw new Error('--ci selects the affected set from the workspace root; run it there.')\n }\n const name = readNxProjectName(cwd) ?? basename(cwd)\n return { modules: [{ name, root: cwd }], scope: 'cwd-module' }\n }\n\n // At the workspace root: --module (one), --ci (affected), or every module.\n if (atRoot) {\n if (opts.module) {\n const found = discoverModules(cwd).find((module) => module.name === opts.module)\n if (!found) throw new Error(`module \"${opts.module}\" not found in the workspace.`)\n return { modules: [found], scope: 'named-module' }\n }\n if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: 'affected' }\n return { modules: discoverModules(cwd), scope: 'all' }\n }\n\n throw new Error(\n `Run sentinel from a module directory or the workspace root ` +\n `(found neither ${MODULE_MARKERS.join('/')} nor ${WORKSPACE_ROOT_MARKER} here).`,\n )\n}\n","/**\n * Module discovery via nx. `--report`/`--inspect` without a `--module` analyse\n * every project; `--ci` narrows to the affected ones. We ask nx (its cached graph)\n * for the authoritative names + roots in a couple of calls, not one per project,\n * so it stays fast on the real monorepo.\n */\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { resolveBin } from '../shared/resolve-bin.js'\n\n/** A discovered module: its nx project name and absolute root. */\nexport interface ModuleRef {\n name: string\n root: string\n}\n\nfunction runNx(cwd: string, args: string[]): string {\n const nx = resolveBin(cwd, 'nx') ?? 'nx'\n try {\n // Disable the daemon for deterministic, CI-friendly one-shot invocations.\n return execFileSync(nx, args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, NX_DAEMON: 'false' },\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,\n { cause: error },\n )\n }\n}\n\n/** Every module (name -> absolute root) from the nx project graph, in one call. */\nfunction readGraph(cwd: string): ModuleRef[] {\n const dir = mkdtempSync(join(tmpdir(), 'sentinel-nx-'))\n const file = join(dir, 'graph.json')\n try {\n runNx(cwd, ['graph', '--file', file])\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as {\n graph?: { nodes?: Record<string, { data: { root: string } }> }\n }\n const nodes = parsed.graph?.nodes\n if (!nodes || typeof nodes !== 'object') {\n throw new Error(\n 'sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible.',\n )\n }\n return Object.entries(nodes).map(([name, node]) => ({\n name,\n root: join(cwd, node.data.root),\n }))\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n\nexport function discoverModules(cwd: string, options: { affected?: boolean } = {}): ModuleRef[] {\n const modules = readGraph(cwd)\n if (!options.affected) return modules\n const affected = new Set(\n JSON.parse(runNx(cwd, ['show', 'projects', '--affected', '--json'])) as string[],\n )\n return modules.filter((module) => affected.has(module.name))\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: sentinel does NOT detect the flavour. A project's flavour is declared in\n * its committed config (the stub's `extends`), or passed explicitly to `--update`.\n * We never guess, so there is no default flavour or detection table here.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n","/**\n * The domain vocabulary: the fixed sets of verbs, targets, and flavours, and the\n * types derived from them. This is the model, and the extension point: adding a\n * verb / target / flavour is a one-line edit to a list here, and because the types\n * are DERIVED (`(typeof LIST)[number]`), the compiler forces every switch/handler\n * to cover the new member.\n *\n * Tunable behaviour (defaults, detection signals, marker filenames) lives in\n * `settings.ts`, not here.\n */\n\n/** Verbs: what to do. Each maps to an adapter method in dispatch. */\nexport const VERBS = ['run', 'inspect', 'update', 'report'] as const\nexport type Verb = (typeof VERBS)[number]\n\n/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */\nexport const TARGETS = [\n 'lint',\n 'format',\n 'typescript',\n 'build',\n 'test',\n 'static-analysis',\n 'runtime-analysis',\n 'arch',\n] as const\nexport type Target = (typeof TARGETS)[number]\n\n/** Flavours: the stack preset a project resolves to (strict by default). */\nexport const FLAVOURS = ['react', 'nest', 'svelte', 'node'] as const\nexport type Flavour = (typeof FLAVOURS)[number]\n","/**\n * Orchestration for `--report`/`--inspect`: analyse a set of modules across a set\n * of targets, then shape the outcomes into one versioned, parseable summary.\n *\n * The engine resolves each module's flavour and adapter and calls the per-module\n * method; a target with no adapter yet is simply skipped. `generateSummaries`\n * builds the aggregate by reusing `generateSummary` for each result (one factory,\n * no duplicated shaping between the single- and multi-module paths).\n */\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, Target } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext } from './types.js'\n\n/** One project × target outcome. */\nexport interface AnalyseResult {\n project: string\n target: Target\n flavour: Flavour\n ok: boolean\n /** `--report`: metrics (errors, implicit-any); `--inspect`: the resolved config. */\n data: unknown\n}\n\nexport interface AnalyseParams {\n verb: 'run' | 'report' | 'inspect'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n ci: boolean\n fix: boolean\n /** Called after each module, for progress display. */\n onProgress?: (done: number, total: number, moduleName: string) => void\n}\n\n/** Run the analyse across modules × targets. Returns the results + worst exit code. */\nexport async function analyse(\n params: AnalyseParams,\n): Promise<{ results: AnalyseResult[]; worstCode: number }> {\n const results: AnalyseResult[] = []\n let worstCode = 0\n let done = 0\n\n for (const module of params.modules) {\n const flavour = detectFramework(readProjectPackageJson(module.root))\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch {\n continue // no adapter for this target yet: skip it (not a failure)\n }\n // Isolate every check: one module/target that throws must not abort the whole\n // sweep. A failure becomes a `ok:false` row (with the error) so `--report --all`\n // still returns a complete picture across a large workspace.\n try {\n if (params.verb === 'run') {\n // Label the run so multi-module output (root/--all) is readable; the tool\n // streams its own output (stdio inherit) between headers.\n if (params.modules.length > 1) {\n process.stderr.write(`\\n ▶ ${module.name} (${flavour}) ${target}\\n`)\n }\n const result = await adapter.run(ctx)\n results.push({ project: module.name, target, flavour, ok: result.ok, data: {} })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'report') {\n const result = await adapter.report(ctx)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n data: result.metrics ?? {},\n })\n worstCode = Math.max(worstCode, result.code)\n } else {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n results.push({ project: module.name, target, flavour, ok: false, data: { error: message } })\n worstCode = Math.max(worstCode, 1)\n }\n }\n params.onProgress?.((done += 1), params.modules.length, module.name)\n }\n\n return { results, worstCode }\n}\n\n/** The shaped summary for ONE result (the factory unit). */\nexport function generateSummary(result: AnalyseResult): Record<string, unknown> {\n const { project, target, flavour, ok, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, ...details }\n}\n\n/** The aggregate, versioned envelope for MANY results, built from `generateSummary`. */\nexport function generateSummaries(results: readonly AnalyseResult[]): {\n schemaVersion: number\n results: Record<string, unknown>[]\n} {\n return { schemaVersion: 1, results: results.map(generateSummary) }\n}\n"],"mappings":";;;;;;;;;;;;;AAUA,SAAS,eAAe;;;ACCxB,SAAS,kBAAkB;AAC3B,SAAS,UAAU,QAAAA,aAAY;;;ACN/B,SAAS,oBAAoB;AAC7B,SAAS,aAAa,cAAc,cAAc;AAClD,SAAS,cAAc;AACvB,SAAS,YAAY;AAUrB,SAAS,MAAMC,MAAa,MAAwB;AAClD,QAAM,KAAK,WAAWA,MAAK,IAAI,KAAK;AACpC,MAAI;AAEF,WAAO,aAAa,IAAI,MAAM;AAAA,MAC5B,KAAAA;AAAA,MACA,UAAU;AAAA,MACV,KAAK,EAAE,GAAG,QAAQ,KAAK,WAAW,QAAQ;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAM,IAAI;AAAA,MACR,+BAA+B,OAAO;AAAA,MACtC,EAAE,OAAO,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAGA,SAAS,UAAUA,MAA0B;AAC3C,QAAM,MAAM,YAAY,KAAK,OAAO,GAAG,cAAc,CAAC;AACtD,QAAM,OAAO,KAAK,KAAK,YAAY;AACnC,MAAI;AACF,UAAMA,MAAK,CAAC,SAAS,UAAU,IAAI,CAAC;AACpC,UAAM,SAAS,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAGpD,UAAM,QAAQ,OAAO,OAAO;AAC5B,QAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO;AAAA,MAClD;AAAA,MACA,MAAM,KAAKA,MAAK,KAAK,KAAK,IAAI;AAAA,IAChC,EAAE;AAAA,EACJ,UAAE;AACA,WAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC9C;AACF;AAEO,SAAS,gBAAgBA,MAAa,UAAkC,CAAC,GAAgB;AAC9F,QAAM,UAAU,UAAUA,IAAG;AAC7B,MAAI,CAAC,QAAQ,SAAU,QAAO;AAC9B,QAAM,WAAW,IAAI;AAAA,IACnB,KAAK,MAAM,MAAMA,MAAK,CAAC,QAAQ,YAAY,cAAc,QAAQ,CAAC,CAAC;AAAA,EACrE;AACA,SAAO,QAAQ,OAAO,CAAC,WAAW,SAAS,IAAI,OAAO,IAAI,CAAC;AAC7D;;;AC1DO,IAAM,wBAAwB;;;AFSrC,IAAM,iBAAiB,CAAC,gBAAgB,cAAc;AAUtD,SAAS,YAAYC,MAAsB;AACzC,SAAO,eAAe,KAAK,CAAC,WAAW,WAAWC,MAAKD,MAAK,MAAM,CAAC,CAAC;AACtE;AAOO,SAAS,eACdA,MACAE,OACiB;AACjB,QAAM,SAAS,WAAWD,MAAKD,MAAK,qBAAqB,CAAC;AAG1D,MAAI,CAAC,UAAU,YAAYA,IAAG,GAAG;AAC/B,QAAIE,MAAK,QAAQ;AACf,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAIA,MAAK,IAAI;AACX,YAAM,IAAI,MAAM,sEAAsE;AAAA,IACxF;AACA,UAAM,OAAO,kBAAkBF,IAAG,KAAK,SAASA,IAAG;AACnD,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,MAAMA,KAAI,CAAC,GAAG,OAAO,aAAa;AAAA,EAC/D;AAGA,MAAI,QAAQ;AACV,QAAIE,MAAK,QAAQ;AACf,YAAM,QAAQ,gBAAgBF,IAAG,EAAE,KAAK,CAAC,WAAW,OAAO,SAASE,MAAK,MAAM;AAC/E,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,WAAWA,MAAK,MAAM,+BAA+B;AACjF,aAAO,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,eAAe;AAAA,IACnD;AACA,QAAIA,MAAK,GAAI,QAAO,EAAE,SAAS,gBAAgBF,MAAK,EAAE,UAAU,KAAK,CAAC,GAAG,OAAO,WAAW;AAC3F,WAAO,EAAE,SAAS,gBAAgBA,IAAG,GAAG,OAAO,MAAM;AAAA,EACvD;AAEA,QAAM,IAAI;AAAA,IACR,6EACoB,eAAe,KAAK,GAAG,CAAC,QAAQ,qBAAqB;AAAA,EAC3E;AACF;;;AG7DO,IAAM,QAAQ,CAAC,OAAO,WAAW,UAAU,QAAQ;AAInD,IAAM,UAAU;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,IAAM,WAAW,CAAC,SAAS,QAAQ,UAAU,MAAM;;;ACS1D,eAAsB,QACpB,QAC0D;AAC1D,QAAM,UAA2B,CAAC;AAClC,MAAI,YAAY;AAChB,MAAI,OAAO;AAEX,aAAW,UAAU,OAAO,SAAS;AACnC,UAAMG,WAAU,gBAAgB,uBAAuB,OAAO,IAAI,CAAC;AACnE,UAAM,MAAkB;AAAA,MACtB,QAAQ,OAAO;AAAA,MACf,KAAK,OAAO;AAAA,MACZ,SAAAA;AAAA,MACA,IAAI,OAAO;AAAA,MACX,KAAK,OAAO;AAAA,IACd;AACA,eAAW,UAAU,OAAO,SAAS;AACnC,UAAI;AACJ,UAAI;AACF,kBAAU,QAAQ,QAAQA,UAAS,OAAO,MAAM;AAAA,MAClD,QAAQ;AACN;AAAA,MACF;AAIA,UAAI;AACF,YAAI,OAAO,SAAS,OAAO;AAGzB,cAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,oBAAQ,OAAO,MAAM;AAAA,WAAS,OAAO,IAAI,KAAKA,QAAO,KAAK,MAAM;AAAA,CAAI;AAAA,UACtE;AACA,gBAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AACpC,kBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC;AAC/E,sBAAY,KAAK,IAAI,WAAW,OAAO,IAAI;AAAA,QAC7C,WAAW,OAAO,SAAS,UAAU;AACnC,gBAAM,SAAS,MAAM,QAAQ,OAAO,GAAG;AACvC,kBAAQ,KAAK;AAAA,YACX,SAAS,OAAO;AAAA,YAChB;AAAA,YACA,SAAAA;AAAA,YACA,IAAI,OAAO;AAAA,YACX,MAAM,OAAO,WAAW,CAAC;AAAA,UAC3B,CAAC;AACD,sBAAY,KAAK,IAAI,WAAW,OAAO,IAAI;AAAA,QAC7C,OAAO;AACL,gBAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG;AACxC,kBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,MAAM,MAAM,OAAO,CAAC;AAAA,QAChF;AAAA,MACF,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,gBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,OAAO,MAAM,EAAE,OAAO,QAAQ,EAAE,CAAC;AAC3F,oBAAY,KAAK,IAAI,WAAW,CAAC;AAAA,MACnC;AAAA,IACF;AACA,WAAO,aAAc,QAAQ,GAAI,OAAO,QAAQ,QAAQ,OAAO,IAAI;AAAA,EACrE;AAEA,SAAO,EAAE,SAAS,UAAU;AAC9B;AAGO,SAAS,gBAAgB,QAAgD;AAC9E,QAAM,EAAE,SAAS,QAAQ,SAAAA,UAAS,IAAI,KAAK,IAAI;AAC/C,QAAM,UACJ,QAAQ,OAAO,SAAS,WAAY,OAAmC,EAAE,OAAO,KAAK;AACvF,SAAO,EAAE,SAAS,QAAQ,SAAAA,UAAS,IAAI,GAAG,QAAQ;AACpD;AAGO,SAAS,kBAAkB,SAGhC;AACA,SAAO,EAAE,eAAe,GAAG,SAAS,QAAQ,IAAI,eAAe,EAAE;AACnE;;;AL/FA,QACG,KAAK,UAAU,EACf,YAAY,sEAAsE,EAClF,QAAQ,eAAe,CAAC,EACxB,cAAc,EAAE,aAAa,MAAM,CAAC,EAGpC,yBAAyB,IAAI,EAC7B,mBAAmB,mCAAmC,EACtD;AAAA,EACC;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb,EAEC,OAAO,SAAS,yBAAyB,EACzC,OAAO,aAAa,iCAAiC,EACrD,OAAO,YAAY,iCAAiC,EACpD,OAAO,YAAY,2BAA2B,EAE9C,OAAO,UAAU,SAAS,EAC1B,OAAO,YAAY,YAAY,EAC/B,OAAO,gBAAgB,eAAe,EACtC,OAAO,WAAW,OAAO,EACzB,OAAO,UAAU,OAAO,EACxB,OAAO,qBAAqB,6CAA6C,EACzE,OAAO,sBAAsB,gCAAgC,EAC7D,OAAO,UAAU,yBAAyB,EAC1C,OAAO,SAAS,cAAc,EAE9B;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,oBAAoB,wCAAwC,SAAS,KAAK,IAAI,CAAC,GAAG,EACzF,OAAO,mBAAmB,kDAAkD,EAC5E,OAAO,QAAQ,6EAA6E,EAC5F,OAAO,SAAS,2BAA2B,EAC3C,OAAO,aAAa,gDAAgD,EACpE,OAAO,UAAU,yDAAyD,EAC1E;AAAA,EACC;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb,EACC,MAAM;AAET,IAAM,OAAO,QAAQ,KAAK;AAG1B,SAAS,QAAQ,MAAsB;AACrC,SAAO,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc,EAAE,YAAY,CAAC;AACpE;AAGA,SAAS,QAA0B,MAAc,MAAuB;AACtE,QAAM,SAAS,KAAK,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;AAClD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,YAAY,KAAK,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI;AACrD,YAAQ;AAAA,MACN,OAAO,WAAW,IACd,aAAa,IAAI,gBAAgB,SAAS,MAC1C,oBAAoB,IAAI,SAAS,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IAC7E;AAAA,EACF;AACA,SAAO,OAAO,CAAC;AACjB;AAQA,SAAS,aAAa,OAAqC;AACzD,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,YAAY,CAAC,SAAS,SAAS,KAAgB,GAAG;AACrE,WAAO,QAAQ;AAAA,MACb,+BAA+B,KAAK,UAAU,KAAK,CAAC,gBAAgB,SAAS,KAAK,IAAI,CAAC;AAAA,IACzF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,UAAU,KAAsB;AACvC,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,IAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,IAAM,MAAM,QAAQ,IAAI;AACxB,IAAM,UAAU,aAAa,KAAK,OAAO;AAIzC,IAAM,eAAe,QAAQ,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;AAC3D,IAAI,KAAK,OAAO,aAAa,SAAS,GAAG;AACvC,UAAQ;AAAA,IACN,sDAAsD,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,EACpG;AACF;AACA,IAAM,UAAoB,KAAK,OAAO,aAAa,WAAW,IAAI,CAAC,GAAG,OAAO,IAAI;AAGjF,IAAI,KAAK,UAAU,SAAS,UAAU;AACpC,UAAQ,MAAM,kEAAkE;AAClF;AAMA,eAAe,UAA2B;AACxC,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;AAE5F,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,EAAE,SAAS,UAAU,IAAI,MAAM,QAAQ;AAAA,IAC3C;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,IAAI,QAAQ,KAAK,EAAE;AAAA,IACnB,KAAK,QAAQ,KAAK,GAAG;AAAA,IACrB,YAAY,CAAC,MAAM,OAAO,SAAS,QAAQ,OAAO,MAAM,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI;AAAA,CAAI;AAAA,EAC1F,CAAC;AAED,MAAI,SAAS,YAAY,SAAS,WAAW;AAC3C,UAAM,UAAU,kBAAkB,OAAO;AACzC,QAAI,KAAK,MAAM;AACb,cAAQ,OAAO,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC,IAAI,IAAI;AAAA,IAC9D,OAAO;AACL,iBAAW,QAAQ,QAAQ,SAAS;AAClC,cAAM,UAAU,OAAO,QAAQ,IAAI,EAChC,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,WAAW,UAAU,WAAW,IAAI,EAAE,SAAS,GAAG,CAAC,EACvE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI,KAAK,UAAU,KAAK,CAAC,EAAE,EACvD,KAAK,GAAG;AACX,gBAAQ,OAAO;AAAA,UACb,KAAK,KAAK,KAAK,WAAM,QAAG,IAAI,KAAK,OAAO,KAAK,KAAK,OAAO,KAAK,KAAK,MAAM,GAAG,UAAU,WAAM,OAAO,KAAK,EAAE;AAAA;AAAA,QAC5G;AAAA,MACF;AAAA,IACF;AAAA,EACF,OAAO;AAEL,eAAW,QAAQ,SAAS;AAC1B,cAAQ,OAAO;AAAA,QACb,KAAK,KAAK,KAAK,WAAM,QAAG,IAAI,KAAK,OAAO,KAAK,KAAK,OAAO,KAAK,KAAK,MAAM;AAAA;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,KAAK,QAAQ,MAAM,eAAe,KAAK,QAAQ,KAAK,IAAI,IAAI,OAAO;AAAA,CAAM;AAE9F,SAAO,SAAS,SAAS,KAAK,KAAK,YAAY;AACjD;AAOA,eAAe,YAA6B;AAC1C,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC;AACjF,QAAM,CAAC,MAAM,IAAI;AACjB,MAAI,UAAU,SAAS,UAAU,cAAc,CAAC,QAAQ;AACtD,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AACA,MAAI,QAAQ;AACZ,aAAW,QAAQ,SAAS;AAC1B,QAAI;AACF,YAAM,OAAO,MAAM,SAAS;AAAA,QAC1B;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,KAAK;AAAA,QACb,QAAQ,OAAO;AAAA,QACf,KAAK,OAAO;AAAA,QACZ;AAAA,QACA,IAAI,QAAQ,KAAK,EAAE;AAAA,QACnB,KAAK,QAAQ,KAAK,GAAG;AAAA,QACrB,QAAQ,QAAQ,KAAK,MAAM;AAAA,QAC3B,MAAM,QAAQ,KAAK,IAAI;AAAA,MACzB,CAAC;AACD,cAAQ,KAAK,IAAI,OAAO,IAAI;AAAA,IAC9B,SAAS,KAAK;AAEZ,cAAQ,OAAO,MAAM;AAAA,YAAe,IAAI,MAAM,UAAU,GAAG,CAAC;AAAA,CAAI;AAChE,UAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,cAAQ,KAAK,IAAI,OAAO,CAAC;AAAA,IAC3B;AAAA,EACF;AACA,SAAO;AACT;AAEA,iBAAiB;AAAA,CAEf,SAAS,WAAW,UAAU,IAAI,QAAQ,GACzC,KAAK,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC,EACjC,MAAM,CAAC,QAAiB;AACvB,UAAQ,OAAO,MAAM;AAAA,YAAe,UAAU,GAAG,CAAC;AAAA,CAAI;AACtD,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["join","cwd","cwd","join","opts","flavour"]}
@@ -635,9 +635,10 @@ export {
635
635
  BaseAdapter,
636
636
  readOwnVersion,
637
637
  readProjectPackageJson,
638
+ readNxProjectName,
638
639
  resolveBin,
639
640
  registerAdapters,
640
641
  detectFramework,
641
642
  dispatch
642
643
  };
643
- //# sourceMappingURL=chunk-6AT72CEW.js.map
644
+ //# sourceMappingURL=chunk-PK3MT5ZK.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-6AT72CEW.js";
10
+ } from "./chunk-PK3MT5ZK.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": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
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",