@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>
|
|
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
|
-
|
|
248
|
-
--
|
|
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>
|
|
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
|
|
257
|
-
sentinel --
|
|
258
|
-
sentinel --
|
|
259
|
-
sentinel --
|
|
260
|
-
sentinel --
|
|
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
|
```
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -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-
|
|
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 === "
|
|
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
|
|
144
|
-
" verb what to do:
|
|
145
|
-
"
|
|
146
|
-
"
|
|
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
|
|
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
|
|
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
|
|
157
|
-
" sentinel --run --typescript",
|
|
158
|
-
" sentinel --
|
|
159
|
-
" sentinel --
|
|
160
|
-
" sentinel --report --
|
|
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 ||
|
|
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 (
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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:
|
|
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 (${
|
|
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
|
-
(
|
|
322
|
+
(verb === "update" ? runUpdate() : runVerb()).then((code) => process.exit(code)).catch((err) => {
|
|
284
323
|
process.stderr.write(`
|
|
285
324
|
sentinel: ${asMessage(err)}
|
|
286
325
|
`);
|
package/dist/bin/sentinel.js.map
CHANGED
|
@@ -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":"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","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-
|
|
644
|
+
//# sourceMappingURL=chunk-PK3MT5ZK.js.map
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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",
|
|
File without changes
|