@hublo/sentinel 0.1.0-alpha.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -25
- package/dist/bin/sentinel.js +275 -36
- package/dist/{chunk-UG74KTIU.js → chunk-HKGCWPRT.js} +151 -79
- package/dist/index.d.ts +31 -17
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/dist/bin/sentinel.js.map +0 -1
- package/dist/chunk-UG74KTIU.js.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -12,6 +12,9 @@ function setDefaultRunner(target, runner) {
|
|
|
12
12
|
function all() {
|
|
13
13
|
return adapters;
|
|
14
14
|
}
|
|
15
|
+
function availableTargets() {
|
|
16
|
+
return [...new Set(adapters.map((a) => a.target))];
|
|
17
|
+
}
|
|
15
18
|
function resolve(target, flavour, runner) {
|
|
16
19
|
const forTarget = adapters.filter((a) => a.target === target);
|
|
17
20
|
if (forTarget.length === 0) {
|
|
@@ -77,22 +80,27 @@ function palette(stream) {
|
|
|
77
80
|
import { existsSync, readFileSync } from "fs";
|
|
78
81
|
import { dirname, join } from "path";
|
|
79
82
|
import { fileURLToPath } from "url";
|
|
80
|
-
function
|
|
83
|
+
function readOwnPackage() {
|
|
81
84
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
82
85
|
for (; ; ) {
|
|
83
86
|
const pkgPath = join(dir, "package.json");
|
|
84
87
|
if (existsSync(pkgPath)) {
|
|
85
88
|
try {
|
|
86
89
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
87
|
-
if (typeof pkg.version === "string")
|
|
90
|
+
if (typeof pkg.version === "string") {
|
|
91
|
+
return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version };
|
|
92
|
+
}
|
|
88
93
|
} catch {
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
const parent = dirname(dir);
|
|
92
|
-
if (parent === dir) return "0.0.0";
|
|
97
|
+
if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
|
|
93
98
|
dir = parent;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
101
|
+
function readOwnVersion() {
|
|
102
|
+
return readOwnPackage().version;
|
|
103
|
+
}
|
|
96
104
|
function readProjectPackageJson(dir) {
|
|
97
105
|
const path = join(dir, "package.json");
|
|
98
106
|
if (!existsSync(path)) return {};
|
|
@@ -249,6 +257,25 @@ function readTsconfigAdoption(cwd) {
|
|
|
249
257
|
|
|
250
258
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
251
259
|
var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
|
|
260
|
+
var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
|
|
261
|
+
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
262
|
+
var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
|
|
263
|
+
function parseDiagnostics(output) {
|
|
264
|
+
const diagnostics = [];
|
|
265
|
+
for (const raw of output.split("\n")) {
|
|
266
|
+
const m = DIAGNOSTIC_RE.exec(raw.trim());
|
|
267
|
+
if (m) {
|
|
268
|
+
diagnostics.push({
|
|
269
|
+
file: m[1],
|
|
270
|
+
line: Number(m[2]),
|
|
271
|
+
col: Number(m[3]),
|
|
272
|
+
code: m[4],
|
|
273
|
+
message: m[5]
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return diagnostics;
|
|
278
|
+
}
|
|
252
279
|
var PHASED_STRICTNESS_WARNING = `sentinel typescript: phase 1 (non-breaking) \u2014 deferred: ${deferredRuleNames().join(", ")}. Enabled centrally in a later wave; run \`sentinel --inspect --typescript\` for the list.`;
|
|
253
280
|
function composeExtends(current, preset) {
|
|
254
281
|
const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
|
|
@@ -260,7 +287,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
260
287
|
/**
|
|
261
288
|
* The tsc adapter drives type-checking for any flavour: `--run`/`--report`/
|
|
262
289
|
* `--inspect` just execute tsc against the module's existing config, which is
|
|
263
|
-
* meaningful regardless of flavour. `--
|
|
290
|
+
* meaningful regardless of flavour. `--init` is the exception, it only WRITES a
|
|
264
291
|
* preset for flavours that ship one (gated inside `plan`), so svelte is not
|
|
265
292
|
* clobbered with a non-existent preset.
|
|
266
293
|
*/
|
|
@@ -268,10 +295,10 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
268
295
|
return true;
|
|
269
296
|
}
|
|
270
297
|
/**
|
|
271
|
-
* Plan `--
|
|
272
|
-
* conformant stub,
|
|
273
|
-
*
|
|
274
|
-
*
|
|
298
|
+
* Plan `--init`: make the module extend the sentinel preset with a THIN,
|
|
299
|
+
* conformant stub, route type-checking through the CLI, and pin the
|
|
300
|
+
* `@hublo/sentinel` devDependency into the module. The engine applies the ops, so
|
|
301
|
+
* adoption is `--init` then `pnpm install`, with nothing to add by hand.
|
|
275
302
|
*
|
|
276
303
|
* Per resolved case:
|
|
277
304
|
* - extends-base: append the preset to the `extends` chain (keep the base for
|
|
@@ -284,23 +311,23 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
284
311
|
if (!hasShippedPreset(context.flavour)) {
|
|
285
312
|
return {
|
|
286
313
|
operations: [],
|
|
287
|
-
|
|
314
|
+
blocked: `no TypeScript preset for flavour "${context.flavour}" yet (shipped: ${SHIPPED_FLAVOURS.join(", ")}). Nothing was written; this module cannot adopt the TypeScript preset until that flavour ships.`
|
|
288
315
|
};
|
|
289
316
|
}
|
|
290
317
|
const target = resolveTsconfigTarget(context.cwd);
|
|
291
318
|
const preset = `@hublo/sentinel/tsconfig/${context.flavour}`;
|
|
292
|
-
const addScript = this.
|
|
319
|
+
const addScript = this.packageJsonOperation(context.cwd);
|
|
293
320
|
if (target.reason === "other-chain") {
|
|
294
321
|
return {
|
|
295
322
|
operations: [],
|
|
296
|
-
|
|
323
|
+
blocked: `${target.path} extends a config chain sentinel does not handle, so nothing was written. Point it at the workspace base (or a plain config) and re-run.`
|
|
297
324
|
};
|
|
298
325
|
}
|
|
299
326
|
if (target.reason === "none") {
|
|
300
327
|
const contents = JSON.stringify({ extends: preset, include: ["src"] }, null, 2) + "\n";
|
|
301
328
|
return {
|
|
302
329
|
operations: [{ kind: "write", path: target.path, contents }, addScript],
|
|
303
|
-
notes: [`created ${target.path} (no tsconfig found)
|
|
330
|
+
notes: [`created ${target.path} (no tsconfig found)`, INSTALL_NOTE]
|
|
304
331
|
};
|
|
305
332
|
}
|
|
306
333
|
const existing = parseJsonc(
|
|
@@ -314,14 +341,17 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
314
341
|
];
|
|
315
342
|
const notes = [];
|
|
316
343
|
if (drift.length > 0) {
|
|
344
|
+
const localKeys = Object.keys(existing.compilerOptions ?? {});
|
|
345
|
+
const stripsAll = localKeys.length > 0 && localKeys.every((key) => drift.includes(key));
|
|
317
346
|
operations.push({
|
|
318
347
|
kind: "remove-json-keys",
|
|
319
348
|
path: target.path,
|
|
320
|
-
keys: drift.map((key) => ["compilerOptions", key])
|
|
349
|
+
keys: stripsAll ? [["compilerOptions"]] : drift.map((key) => ["compilerOptions", key])
|
|
321
350
|
});
|
|
322
351
|
notes.push(`stripped preset-owned compilerOptions: ${drift.join(", ")}`);
|
|
323
352
|
}
|
|
324
353
|
operations.push(addScript);
|
|
354
|
+
notes.push(INSTALL_NOTE);
|
|
325
355
|
return { operations, notes };
|
|
326
356
|
}
|
|
327
357
|
/**
|
|
@@ -329,18 +359,25 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
329
359
|
* `package.json`, merge the script in and leave the rest untouched. If it does
|
|
330
360
|
* NOT (common for nx apps/services that carry only a `project.json`), scaffold a
|
|
331
361
|
* minimal, workspace-valid one, its nx name + `private: true`, so pnpm accepts it
|
|
332
|
-
* and it can
|
|
333
|
-
*
|
|
362
|
+
* and it can carry the pinned `@hublo/sentinel` devDep this op also writes. The
|
|
363
|
+
* exact version is written (never a range), so `pnpm install` resolves the same
|
|
364
|
+
* build the stub's `extends` points at.
|
|
334
365
|
*/
|
|
335
|
-
|
|
366
|
+
packageJsonOperation(cwd) {
|
|
367
|
+
const own = readOwnPackage();
|
|
368
|
+
const devDependencies = { [own.name]: own.version };
|
|
336
369
|
if (existsSync5(join5(cwd, "package.json"))) {
|
|
337
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
kind: "merge-json",
|
|
372
|
+
path: "package.json",
|
|
373
|
+
value: { scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
374
|
+
};
|
|
338
375
|
}
|
|
339
376
|
const name = readNxProjectName(cwd) ?? basename(cwd);
|
|
340
377
|
return {
|
|
341
378
|
kind: "merge-json",
|
|
342
379
|
path: "package.json",
|
|
343
|
-
value: { name, private: true, scripts: TYPECHECK_SCRIPT }
|
|
380
|
+
value: { name, private: true, scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
344
381
|
};
|
|
345
382
|
}
|
|
346
383
|
/**
|
|
@@ -419,7 +456,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
419
456
|
async report(ctx) {
|
|
420
457
|
const config = this.typecheckTarget(ctx.cwd);
|
|
421
458
|
if (!config) {
|
|
422
|
-
return { ok: true, code: 0, metrics: { errors: 0, implicitAny:
|
|
459
|
+
return { ok: true, code: 0, metrics: { errors: 0, implicitAny: "deferred", diagnostics: [] } };
|
|
423
460
|
}
|
|
424
461
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
425
462
|
const result = spawnSync(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
@@ -432,8 +469,50 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
432
469
|
}
|
|
433
470
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
434
471
|
const errors = (output.match(/error TS\d+/g) ?? []).length;
|
|
435
|
-
const
|
|
436
|
-
|
|
472
|
+
const parsed = parseDiagnostics(output);
|
|
473
|
+
const cap = ctx.maxDiagnostics === 0 ? Infinity : ctx.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS;
|
|
474
|
+
const diagnostics = Number.isFinite(cap) ? parsed.slice(0, cap) : parsed;
|
|
475
|
+
return {
|
|
476
|
+
ok: errors === 0,
|
|
477
|
+
code: result.status ?? 0,
|
|
478
|
+
metrics: {
|
|
479
|
+
errors,
|
|
480
|
+
implicitAny: this.implicitAnyMetric(ctx.cwd, tsc, config, output),
|
|
481
|
+
diagnostics,
|
|
482
|
+
diagnosticsTruncated: diagnostics.length < parsed.length
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The `implicitAny` report metric, honestly. Implicit-`any` violations (TS70xx) are
|
|
488
|
+
* only *visible* to tsc when `noImplicitAny` is ON. In phase 1 the rule is DEFERRED
|
|
489
|
+
* (off), so counting TS70xx from the committed-config run is structurally always 0 —
|
|
490
|
+
* a misleading "no implicit-any" when the rule simply was not applied. So: report the
|
|
491
|
+
* real count only when the rule is on, otherwise `'deferred'` (never a fake `0`). The
|
|
492
|
+
* remaining debt while deferred is a separate, opt-in probe (a later `--migration`).
|
|
493
|
+
*/
|
|
494
|
+
implicitAnyMetric(cwd, tsc, config, mainOutput) {
|
|
495
|
+
return this.noImplicitAnyEnabled(cwd, tsc, config, mainOutput) ? (mainOutput.match(/error TS70\d\d/g) ?? []).length : "deferred";
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Whether `noImplicitAny` is effectively ON in the module's resolved config. Read from
|
|
499
|
+
* `tsc --showConfig` (an explicit value wins; otherwise `strict` implies it). If
|
|
500
|
+
* `--showConfig` is unavailable, fall back to the run's own evidence: implicit-`any`
|
|
501
|
+
* errors in the output mean the rule must be on.
|
|
502
|
+
*/
|
|
503
|
+
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
504
|
+
const shown = spawnSync(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
505
|
+
if (shown.status === 0 && shown.stdout) {
|
|
506
|
+
try {
|
|
507
|
+
const co = parseJsonc(
|
|
508
|
+
shown.stdout,
|
|
509
|
+
"tsconfig(--showConfig)"
|
|
510
|
+
).compilerOptions;
|
|
511
|
+
if (co) return co.noImplicitAny ?? co.strict === true;
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return /error TS70\d\d/.test(mainOutput);
|
|
437
516
|
}
|
|
438
517
|
};
|
|
439
518
|
|
|
@@ -451,15 +530,23 @@ function registerAdapters() {
|
|
|
451
530
|
// src/core/detect-framework.ts
|
|
452
531
|
var FRAMEWORK_SIGNALS = [
|
|
453
532
|
{ flavour: "nest", dependency: "@nestjs/core" },
|
|
454
|
-
{ flavour: "
|
|
455
|
-
{ flavour: "
|
|
533
|
+
{ flavour: "react", dependency: "react" },
|
|
534
|
+
{ flavour: "svelte", dependency: "svelte" }
|
|
456
535
|
];
|
|
457
|
-
|
|
536
|
+
var BACKEND_HINTS = ["sails", "express", "koa", "fastify", "@hapi/hapi"];
|
|
537
|
+
function describeFramework(packageJson) {
|
|
458
538
|
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
539
|
+
const matched = FRAMEWORK_SIGNALS.filter((s) => s.dependency in dependencies);
|
|
540
|
+
const chosen = matched[0];
|
|
541
|
+
const flavour = chosen?.flavour ?? "node";
|
|
542
|
+
const source = chosen?.dependency ?? "(no framework dependency)";
|
|
543
|
+
const otherFlavourSignals = matched.slice(1).map((s) => s.dependency);
|
|
544
|
+
const backendHints = flavour === "react" || flavour === "svelte" ? BACKEND_HINTS.filter((hint) => hint in dependencies) : [];
|
|
545
|
+
const conflicts = [...otherFlavourSignals, ...backendHints];
|
|
546
|
+
return { flavour, source, ambiguous: conflicts.length > 0, conflicts };
|
|
547
|
+
}
|
|
548
|
+
function detectFramework(packageJson) {
|
|
549
|
+
return describeFramework(packageJson).flavour;
|
|
463
550
|
}
|
|
464
551
|
|
|
465
552
|
// src/shared/text.ts
|
|
@@ -597,14 +684,24 @@ function writeFileAtomic(absolutePath, contents) {
|
|
|
597
684
|
renameSync(tempPath, absolutePath);
|
|
598
685
|
}
|
|
599
686
|
function applyPlan(cwd, plan) {
|
|
600
|
-
const
|
|
601
|
-
for (const file of
|
|
602
|
-
return
|
|
687
|
+
const changed = preparePlan(cwd, plan).filter((file) => file.before !== file.after);
|
|
688
|
+
for (const file of changed) writeFileAtomic(file.absolutePath, file.after);
|
|
689
|
+
return changed.map((file) => file.path);
|
|
603
690
|
}
|
|
604
691
|
|
|
605
692
|
// src/core/dispatch.ts
|
|
606
693
|
function resolveFlavour(opts) {
|
|
607
|
-
|
|
694
|
+
if (opts.flavour) return opts.flavour;
|
|
695
|
+
const detection = describeFramework(readProjectPackageJson(opts.cwd));
|
|
696
|
+
if (detection.ambiguous) {
|
|
697
|
+
const warn = palette(process.stderr);
|
|
698
|
+
process.stderr.write(
|
|
699
|
+
warn.warn(
|
|
700
|
+
`sentinel: flavour is ambiguous, detected "${detection.flavour}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to write the intended preset.`
|
|
701
|
+
) + "\n"
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return detection.flavour;
|
|
608
705
|
}
|
|
609
706
|
function previewPlan(opts, plan) {
|
|
610
707
|
const changed = preparePlan(opts.cwd, plan).filter((file) => file.before !== file.after);
|
|
@@ -645,70 +742,45 @@ function previewPlan(opts, plan) {
|
|
|
645
742
|
return 0;
|
|
646
743
|
}
|
|
647
744
|
async function dispatch(opts) {
|
|
648
|
-
|
|
745
|
+
if (opts.verb !== "init") {
|
|
746
|
+
throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
|
|
747
|
+
}
|
|
649
748
|
const flavour = resolveFlavour(opts);
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
fix: opts.fix
|
|
656
|
-
};
|
|
657
|
-
switch (opts.verb) {
|
|
658
|
-
case "run": {
|
|
659
|
-
const res = await adapter.run(ctx);
|
|
660
|
-
return res.code;
|
|
661
|
-
}
|
|
662
|
-
case "inspect": {
|
|
663
|
-
const config = await adapter.inspect(ctx);
|
|
664
|
-
process.stdout.write(JSON.stringify(config, null, 2) + "\n");
|
|
665
|
-
return 0;
|
|
666
|
-
}
|
|
667
|
-
case "update": {
|
|
668
|
-
const context = { cwd: opts.cwd, flavour };
|
|
669
|
-
const plan = await adapter.plan(context);
|
|
670
|
-
if (opts.dryRun) {
|
|
671
|
-
return previewPlan(opts, plan);
|
|
672
|
-
}
|
|
673
|
-
const written = applyPlan(opts.cwd, plan);
|
|
674
|
-
for (const path of written) process.stderr.write(` wrote ${path}
|
|
675
|
-
`);
|
|
676
|
-
for (const note of plan.notes ?? []) process.stderr.write(` ${note}
|
|
749
|
+
const adapter = resolve(opts.target, flavour, opts.runner);
|
|
750
|
+
const context = { cwd: opts.cwd, flavour };
|
|
751
|
+
const plan = await adapter.plan(context);
|
|
752
|
+
if (plan.blocked) {
|
|
753
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan.blocked}
|
|
677
754
|
`);
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
case "report": {
|
|
681
|
-
const res = await adapter.report(ctx);
|
|
682
|
-
if (res.metrics) {
|
|
683
|
-
process.stdout.write(JSON.stringify(res.metrics, null, 2) + "\n");
|
|
684
|
-
}
|
|
685
|
-
return res.code;
|
|
686
|
-
}
|
|
687
|
-
case "status": {
|
|
688
|
-
const report = await adapter.status(ctx);
|
|
689
|
-
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
690
|
-
return report.adopted && !report.conformant ? 1 : 0;
|
|
691
|
-
}
|
|
692
|
-
default: {
|
|
693
|
-
const unreachable = opts.verb;
|
|
694
|
-
throw new Error(`Unknown verb: ${String(unreachable)}`);
|
|
695
|
-
}
|
|
755
|
+
return 1;
|
|
696
756
|
}
|
|
757
|
+
if (opts.dryRun) {
|
|
758
|
+
return previewPlan(opts, plan);
|
|
759
|
+
}
|
|
760
|
+
const written = applyPlan(opts.cwd, plan);
|
|
761
|
+
for (const path of written) process.stderr.write(` wrote ${path}
|
|
762
|
+
`);
|
|
763
|
+
for (const note of plan.notes ?? []) process.stderr.write(` ${note}
|
|
764
|
+
`);
|
|
765
|
+
return 0;
|
|
697
766
|
}
|
|
698
767
|
|
|
699
768
|
export {
|
|
700
769
|
register,
|
|
701
770
|
setDefaultRunner,
|
|
702
771
|
all,
|
|
772
|
+
availableTargets,
|
|
703
773
|
resolve,
|
|
704
774
|
BaseAdapter,
|
|
705
775
|
palette,
|
|
776
|
+
readOwnPackage,
|
|
706
777
|
readOwnVersion,
|
|
707
778
|
readProjectPackageJson,
|
|
708
779
|
readNxProjectName,
|
|
709
780
|
resolveBin,
|
|
710
781
|
registerAdapters,
|
|
782
|
+
describeFramework,
|
|
711
783
|
detectFramework,
|
|
712
784
|
dispatch
|
|
713
785
|
};
|
|
714
|
-
//# sourceMappingURL=chunk-
|
|
786
|
+
//# sourceMappingURL=chunk-HKGCWPRT.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,13 @@ declare function registerAdapters(): void;
|
|
|
10
10
|
* Tunable behaviour (defaults, detection signals, marker filenames) lives in
|
|
11
11
|
* `settings.ts`, not here.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Verbs: what to do. `init` writes a module's config + the workspace prep it needs (the
|
|
15
|
+
* one-time setup); the read verbs (run/inspect/report/status) execute or introspect.
|
|
16
|
+
* `migrate` is reserved for changing an already-initialized setup and is not wired yet
|
|
17
|
+
* (the CLI lists it as planned and refuses it).
|
|
18
|
+
*/
|
|
19
|
+
declare const VERBS: readonly ["run", "inspect", "init", "migrate", "report", "status"];
|
|
15
20
|
type Verb = (typeof VERBS)[number];
|
|
16
21
|
/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */
|
|
17
22
|
declare const TARGETS: readonly ["lint", "format", "typescript", "build", "test", "static-analysis", "runtime-analysis", "arch"];
|
|
@@ -31,7 +36,7 @@ type Flavour = (typeof FLAVOURS)[number];
|
|
|
31
36
|
*/
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
|
-
* Normalized context for `--
|
|
39
|
+
* Normalized context for `--init`, built and guaranteed by the engine. The
|
|
35
40
|
* adapter TRUSTS this shape (no re-validation): `cwd` is the absolute module root,
|
|
36
41
|
* `flavour` is resolved (explicit `--flavour`, else detected). The adapter may READ
|
|
37
42
|
* the module through bounded resolvers to plan, but never writes, the engine
|
|
@@ -55,9 +60,15 @@ interface RunContext {
|
|
|
55
60
|
ci: boolean;
|
|
56
61
|
/** Apply auto-fixes where the tool supports it. */
|
|
57
62
|
fix: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Cap on the number of structured diagnostics a `--report` embeds per module (the
|
|
65
|
+
* full count still lives in `errors`, so nothing is hidden silently). `0` means no
|
|
66
|
+
* cap. Undefined lets the adapter use its default.
|
|
67
|
+
*/
|
|
68
|
+
maxDiagnostics?: number;
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
60
|
-
* A single, DECLARATIVE file operation an `--
|
|
71
|
+
* A single, DECLARATIVE file operation an `--init` wants to happen. Adapters
|
|
61
72
|
* describe intent in tool terms; they never touch the filesystem. The engine
|
|
62
73
|
* (which owns IO and the repo layout) executes these against the project root.
|
|
63
74
|
*
|
|
@@ -102,13 +113,21 @@ type FileOperation =
|
|
|
102
113
|
keys: string[][];
|
|
103
114
|
};
|
|
104
115
|
/**
|
|
105
|
-
* The result of planning an `--
|
|
116
|
+
* The result of planning an `--init`: an ordered list of declarative operations,
|
|
106
117
|
* plus optional human notes (what was created/stripped/skipped) surfaced by the
|
|
107
118
|
* CLI. Multi-operation on purpose; the drift guard and golden tests diff the plan.
|
|
108
119
|
*/
|
|
109
120
|
interface UpdatePlan {
|
|
110
121
|
operations: FileOperation[];
|
|
111
122
|
notes?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Set when the adapter CANNOT set this module up at all (no preset for the flavour,
|
|
125
|
+
* a config chain it does not handle, …). The engine reports it as a FAILURE with a
|
|
126
|
+
* non-zero exit, never as a silent success: `--init` that writes nothing must not
|
|
127
|
+
* look like adoption to a person reading a log or to a script reading `$?`. Carries
|
|
128
|
+
* the reason, phrased for the developer who has to act on it.
|
|
129
|
+
*/
|
|
130
|
+
blocked?: string;
|
|
112
131
|
}
|
|
113
132
|
/** Outcome of `run` / `report`. */
|
|
114
133
|
interface AdapterResult {
|
|
@@ -136,8 +155,8 @@ interface StatusReport {
|
|
|
136
155
|
}
|
|
137
156
|
/**
|
|
138
157
|
* One tool implementation for one target (and, optionally, a subset of flavours,
|
|
139
|
-
* see `appliesTo`). The
|
|
140
|
-
*
|
|
158
|
+
* see `appliesTo`). The CLI verbs map onto these: `--run` -> run, `--inspect` ->
|
|
159
|
+
* inspect, `--init` -> plan(+write), `--report` -> report, `--status` -> status.
|
|
141
160
|
*/
|
|
142
161
|
interface Adapter {
|
|
143
162
|
/** The target this adapter serves (e.g. 'lint'). */
|
|
@@ -151,7 +170,7 @@ interface Adapter {
|
|
|
151
170
|
*/
|
|
152
171
|
appliesTo(flavour: Flavour): boolean;
|
|
153
172
|
/**
|
|
154
|
-
* Plan what `--
|
|
173
|
+
* Plan what `--init` should write, as declarative operations (see
|
|
155
174
|
* `FileOperation`). Given a normalized `UpdateContext`, the adapter may READ the
|
|
156
175
|
* module (through bounded resolvers) to decide the ops, but never writes, the
|
|
157
176
|
* engine applies them and owns all IO. The drift guard diffs against this plan.
|
|
@@ -199,7 +218,7 @@ declare abstract class BaseAdapter implements Adapter {
|
|
|
199
218
|
* dependency is a plain TypeScript library (`node`). This is not the old silent
|
|
200
219
|
* guessing (there is no "assume react" fallback); `node` is a real preset.
|
|
201
220
|
*
|
|
202
|
-
* Reusable across tools: every tool's `--
|
|
221
|
+
* Reusable across tools: every tool's `--init` needs the module's flavour to
|
|
203
222
|
* pick its preset, so this lives in core, not in the TypeScript role.
|
|
204
223
|
*/
|
|
205
224
|
|
|
@@ -212,19 +231,14 @@ interface PackageDependencies {
|
|
|
212
231
|
declare function detectFramework(packageJson: PackageDependencies): Flavour;
|
|
213
232
|
|
|
214
233
|
interface DispatchOptions {
|
|
234
|
+
/** Always `init` (dispatch is the write path); other verbs route through `analyse`. */
|
|
215
235
|
verb: Verb;
|
|
216
236
|
target: Target;
|
|
217
237
|
runner?: string;
|
|
218
|
-
module: string;
|
|
219
238
|
cwd: string;
|
|
220
|
-
/**
|
|
221
|
-
* Declared flavour, when known (`--flavour`). `--run`/`--report` don't need it;
|
|
222
|
-
* `--update`/`--inspect` resolve it, falling back to `detectFramework`.
|
|
223
|
-
*/
|
|
239
|
+
/** Declared flavour (`--flavour`), else resolved by `detectFramework` from deps. */
|
|
224
240
|
flavour?: Flavour;
|
|
225
|
-
|
|
226
|
-
fix: boolean;
|
|
227
|
-
/** `--dry-run`: for `--update`, preview the plan instead of writing anything. */
|
|
241
|
+
/** `--dry-run`: preview the plan instead of writing anything. */
|
|
228
242
|
dryRun?: boolean;
|
|
229
243
|
/** `--json`: emit the dry-run preview as machine-readable JSON. */
|
|
230
244
|
json?: boolean;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "git+https://github.com/hublo/sentinel.git"
|
|
11
11
|
},
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=20"
|
|
13
|
+
"node": ">=20.12.0"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
16
|
"sentinel": "./dist/bin/sentinel.js"
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"dist"
|
|
34
|
+
"dist",
|
|
35
|
+
"!dist/**/*.map"
|
|
35
36
|
],
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"commander": "^13.0.0",
|
|
38
39
|
"jsonc-parser": "^3.3.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@changesets/cli": "^2.27.0",
|
|
42
42
|
"@eslint/js": "^10.0.1",
|
|
43
43
|
"@types/node": "^22.0.0",
|
|
44
44
|
"eslint": "^10.8.0",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"vitest": "^2.1.0"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
|
+
"registry": "https://registry.npmjs.org",
|
|
55
56
|
"access": "public"
|
|
56
57
|
},
|
|
57
58
|
"scripts": {
|
|
@@ -66,7 +67,6 @@
|
|
|
66
67
|
"test:watch": "vitest",
|
|
67
68
|
"test:e2e": "vitest run tests/e2e",
|
|
68
69
|
"cli": "node --import tsx bin/sentinel.ts",
|
|
69
|
-
"
|
|
70
|
-
"release": "pnpm build && changeset publish"
|
|
70
|
+
"docs:traces": "pnpm build && bash scripts/gen-traces.sh"
|
|
71
71
|
}
|
|
72
72
|
}
|
package/dist/bin/sentinel.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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","../../src/core/render.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: a command composes verb × type × location.\n *\n * Exactly one verb (--run / --inspect / --report / --status / --update); a type (--typescript …)\n * or ALL when none is named; and a location resolved by `resolveContext` (a module\n * directory, or the workspace root with --module / --ci / all). The CLI is generic: it\n * resolves each target's adapter (honouring --runner) and dispatches; tools arrive as\n * adapters in later tickets. The flavour is detected from a module's dependencies\n * (deterministic, `node` default) and can be overridden with --flavour (recommended\n * for --update).\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 {\n renderStatusRow,\n renderStatusSummary,\n renderSummary,\n statusCoverage,\n} from '../src/core/render.js'\nimport { palette } from '../src/shared/color.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 --status --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 .option('--status', 'adoption + conformity across modules (coverage + drift)')\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>', `override the detected stack preset (${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. When undefined,\n * the flavour is DETECTED from the module's dependencies (see `detect-framework.ts`);\n * this override is recommended for `--update`, where hoisted deps can make detection\n * fall back to `node`. An unknown value is a hard error (typo caught), not 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 const out = palette(process.stdout)\n const err = palette(process.stderr)\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'run' | 'report' | 'inspect' | 'status',\n targets,\n modules,\n runner: opts.runner,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) =>\n process.stderr.write(err.dim(` [${done}/${total}] ${name}\\n`)),\n })\n\n // Shape each outcome into a summary row, then print it colored. `run` rows carry no\n // metrics (bare head line); `status` has its own 3-state rows + a coverage footer;\n // `--json` bypasses the human rendering entirely.\n const summary = generateSummaries(results)\n if (opts.json && verb !== 'run') {\n const payload =\n verb === 'status' ? { ...summary, coverage: statusCoverage(summary.results) } : summary\n process.stdout.write(JSON.stringify(payload, null, 2) + '\\n')\n } else if (verb === 'status') {\n for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + '\\n')\n process.stdout.write(renderStatusSummary(summary.results, out) + '\\n')\n } else {\n for (const item of summary.results) {\n process.stdout.write(renderSummary(item, out) + '\\n')\n }\n }\n process.stderr.write(\n err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms\\n`),\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: the flavour is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--flavour` overrides the detection (recommended for `--update`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\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', 'status'] 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 { palette } from '../shared/color.js'\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' | 'status'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n /** Explicit `--flavour`, when declared; otherwise detected per module from deps. */\n flavour?: Flavour\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 // Explicit --flavour wins; otherwise detect it from the module's deps.\n const flavour = params.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 const err = palette(process.stderr)\n process.stderr.write(\n `\\n ${err.accent('▶')} ${err.strong(module.name)} ${err.dim(`(${flavour})`)} ${target}\\n`,\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 if (params.verb === 'inspect') {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n } else {\n // status: adoption + conformity, read from the committed config (no tool run).\n // A drifted adopted module is a conformance failure; a non-adopted one is not.\n const status = await adapter.status(ctx)\n const ok = !status.adopted || status.conformant\n results.push({ project: module.name, target, flavour, ok, data: status })\n if (!ok) worstCode = Math.max(worstCode, 1)\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 * Human rendering of one summary row (`--run`/`--report`/`--inspect`), colored via a\n * `Palette`. Pure and stream-agnostic: it returns the text, the CLI writes it. `--json`\n * bypasses this entirely (machines get the raw envelope).\n *\n * The goal is legibility: the pass/fail mark and the module name read at a glance, the\n * error counts stand out when non-zero, and the phased `deferred` rules become their own\n * indented block instead of an inline JSON blob.\n */\nimport type { Palette } from '../shared/color.js'\n\n/** Keys carried by the row head (or redundant with it), never repeated in the details. */\nconst HEAD_KEYS = new Set(['project', 'module', 'target', 'flavour', 'ok'])\n\n/** A deferred (phased) rule, as `--inspect` reports it. */\ninterface DeferredRule {\n rule: string\n phase: number\n reason: string\n}\n\nfunction isDeferredRules(value: unknown): value is DeferredRule[] {\n return (\n Array.isArray(value) &&\n value.every((entry) => typeof entry === 'object' && entry !== null && 'rule' in entry)\n )\n}\n\n/** Color a detail value: error counts are the signal, so red when >0, green at 0. */\nfunction renderValue(key: string, value: unknown, p: Palette): string {\n if ((key === 'errors' || key === 'implicitAny') && typeof value === 'number') {\n return value > 0 ? p.fail(String(value)) : p.ok(String(value))\n }\n return typeof value === 'string' ? value : JSON.stringify(value)\n}\n\n/**\n * One summary row → colored lines: a head line (`✓ name (flavour) target — key=value …`)\n * plus, when present, an indented `deferred` block listing the phased rules.\n */\nexport function renderSummary(item: Record<string, unknown>, p: Palette): string {\n const ok = item.ok === true\n const mark = ok ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n // Split details: the phased `deferred` rules get their own block; everything else is\n // rendered inline as dim `key=value` pairs (the head keys are dropped as redundant).\n const entries = Object.entries(item).filter(([key]) => !HEAD_KEYS.has(key))\n const deferred = entries.find(([key]) => key === 'deferred')?.[1]\n const inline = entries\n .filter(([key]) => key !== 'deferred')\n .map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`)\n .join(' ')\n\n const lines = [inline ? `${head} ${p.dim('—')} ${inline}` : head]\n\n if (isDeferredRules(deferred) && deferred.length > 0) {\n lines.push(` ${p.warn('deferred (phase 1, non-breaking):')}`)\n for (const { rule, phase, reason } of deferred) {\n lines.push(` ${p.warn('•')} ${p.strong(rule)} ${p.dim(`— phase ${phase}: ${reason}`)}`)\n }\n }\n return lines.join('\\n')\n}\n\n/** Workspace adoption + conformity totals, for the `--status` footer / JSON. */\nexport interface Coverage {\n total: number\n adopted: number\n conformant: number\n drifted: number\n}\n\nexport function statusCoverage(items: readonly Record<string, unknown>[]): Coverage {\n const adopted = items.filter((i) => i.adopted === true)\n const conformant = adopted.filter((i) => i.conformant === true)\n return {\n total: items.length,\n adopted: adopted.length,\n conformant: conformant.length,\n drifted: adopted.length - conformant.length,\n }\n}\n\n/** Short preset name for display (`@hublo/sentinel/tsconfig/nest` -> `nest`). */\nfunction presetShort(preset: unknown): string {\n return typeof preset === 'string' ? preset.replace('@hublo/sentinel/tsconfig/', '') : '?'\n}\n\n/**\n * One `--status` row, three states: not-adopted (dim `·`), adopted + conformant (green\n * `✓`), adopted + drifted (red `✗`, with the drifted keys named).\n */\nexport function renderStatusRow(item: Record<string, unknown>, p: Palette): string {\n const adopted = item.adopted === true\n const conformant = item.conformant === true\n const mark = !adopted ? p.dim('·') : conformant ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n if (!adopted) return `${head} ${p.dim('— not adopted')}`\n if (conformant) {\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)}) conformant`)}`\n }\n const drift = Array.isArray(item.drift) ? item.drift.join(', ') : ''\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)})`)} ${p.fail(`drift: ${drift}`)}`\n}\n\n/** The `--status` footer: `coverage: X/N adopted · K/X conformant`. */\nexport function renderStatusSummary(items: readonly Record<string, unknown>[], p: Palette): string {\n const c = statusCoverage(items)\n const drift = c.drifted > 0 ? p.fail(` · ${c.drifted} drifted`) : ''\n return ` ${p.strong('coverage:')} ${c.adopted}/${c.total} adopted ${p.dim('·')} ${c.conformant}/${c.adopted} conformant${drift}`\n}\n"],"mappings":"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","names":["join","cwd","cwd","join","opts","flavour"]}
|