@hublo/sentinel 1.1.0-alpha.2 → 1.1.0-alpha.4
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 +148 -35
- package/dist/bin/sentinel.js +119 -48
- package/dist/chunk-FLLX5MN3.js +3155 -0
- package/dist/index.d.ts +59 -37
- package/dist/index.js +1 -1
- package/oxlint/nest.json +184 -187
- package/oxlint/node.json +27 -30
- package/oxlint/react.json +189 -204
- package/oxlint/svelte.json +215 -0
- package/package.json +6 -4
- package/dist/chunk-EZCRU6XE.js +0 -2062
package/dist/bin/sentinel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
PRESET_NAMES,
|
|
4
4
|
TARGETS,
|
|
5
5
|
VERBS,
|
|
6
6
|
availableTargets,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
registerAdapters,
|
|
15
15
|
resolve,
|
|
16
16
|
resolveBin
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-FLLX5MN3.js";
|
|
18
18
|
|
|
19
19
|
// bin/sentinel.ts
|
|
20
20
|
import { program } from "commander";
|
|
@@ -115,17 +115,17 @@ async function analyse(params) {
|
|
|
115
115
|
let worstCode = 0;
|
|
116
116
|
let done = 0;
|
|
117
117
|
for (const module of params.modules) {
|
|
118
|
-
let
|
|
119
|
-
if (params.
|
|
120
|
-
|
|
118
|
+
let preset2;
|
|
119
|
+
if (params.preset) {
|
|
120
|
+
preset2 = params.preset;
|
|
121
121
|
} else {
|
|
122
122
|
const detection = describeFramework(readProjectPackageJson(module.root));
|
|
123
|
-
|
|
123
|
+
preset2 = detection.preset;
|
|
124
124
|
if (detection.ambiguous) {
|
|
125
125
|
const warn = palette(process.stderr);
|
|
126
126
|
process.stderr.write(
|
|
127
127
|
warn.warn(
|
|
128
|
-
`sentinel: ${module.name}:
|
|
128
|
+
`sentinel: ${module.name}: preset is ambiguous, detected "${preset2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --preset to be explicit.`
|
|
129
129
|
) + "\n"
|
|
130
130
|
);
|
|
131
131
|
}
|
|
@@ -133,14 +133,14 @@ async function analyse(params) {
|
|
|
133
133
|
for (const target of params.targets) {
|
|
134
134
|
let adapter;
|
|
135
135
|
try {
|
|
136
|
-
adapter = resolve(target,
|
|
136
|
+
adapter = resolve(target, preset2, params.runner);
|
|
137
137
|
} catch (error) {
|
|
138
138
|
if (params.targetsExplicit) {
|
|
139
139
|
const reason = error instanceof Error ? error.message : String(error);
|
|
140
140
|
results.push({
|
|
141
141
|
project: module.name,
|
|
142
142
|
target,
|
|
143
|
-
flavour:
|
|
143
|
+
flavour: preset2,
|
|
144
144
|
ok: false,
|
|
145
145
|
status: "unsupported",
|
|
146
146
|
data: { reason }
|
|
@@ -148,14 +148,15 @@ async function analyse(params) {
|
|
|
148
148
|
}
|
|
149
149
|
continue;
|
|
150
150
|
}
|
|
151
|
-
const effectiveFlavour = params.
|
|
151
|
+
const effectiveFlavour = params.preset ?? adapter.declaredPreset?.(module.root) ?? preset2;
|
|
152
152
|
const ctx = {
|
|
153
153
|
module: module.name,
|
|
154
154
|
cwd: module.root,
|
|
155
|
-
|
|
155
|
+
preset: effectiveFlavour,
|
|
156
156
|
ci: params.ci,
|
|
157
157
|
fix: params.fix,
|
|
158
|
-
maxDiagnostics: params.maxDiagnostics
|
|
158
|
+
maxDiagnostics: params.maxDiagnostics,
|
|
159
|
+
toolArgs: params.toolArgs
|
|
159
160
|
};
|
|
160
161
|
try {
|
|
161
162
|
if (params.verb === "run") {
|
|
@@ -189,15 +190,20 @@ async function analyse(params) {
|
|
|
189
190
|
});
|
|
190
191
|
worstCode = Math.max(worstCode, result.code);
|
|
191
192
|
} else if (params.verb === "inspect") {
|
|
192
|
-
const config = await adapter.inspect(ctx);
|
|
193
|
+
const [config, adoption] = await Promise.all([adapter.inspect(ctx), adapter.status(ctx)]);
|
|
194
|
+
const ok = !adoption.adopted || adoption.conformant;
|
|
193
195
|
results.push({
|
|
194
196
|
project: module.name,
|
|
195
197
|
target,
|
|
196
198
|
flavour: effectiveFlavour,
|
|
197
|
-
ok
|
|
198
|
-
status: "ok",
|
|
199
|
-
|
|
199
|
+
ok,
|
|
200
|
+
status: ok ? "ok" : "failed",
|
|
201
|
+
// `inspect` is typed as `unknown` (each role shapes its own detail), so the
|
|
202
|
+
// merge is guarded rather than assumed: a role returning a non-object still
|
|
203
|
+
// gets its adoption fields instead of throwing here.
|
|
204
|
+
data: config && typeof config === "object" ? { ...config, ...adoption } : { ...adoption }
|
|
200
205
|
});
|
|
206
|
+
if (!ok) worstCode = Math.max(worstCode, 1);
|
|
201
207
|
} else {
|
|
202
208
|
const status = await adapter.status(ctx);
|
|
203
209
|
const ok = !status.adopted || status.conformant;
|
|
@@ -229,9 +235,9 @@ async function analyse(params) {
|
|
|
229
235
|
return { results, worstCode };
|
|
230
236
|
}
|
|
231
237
|
function generateSummary(result) {
|
|
232
|
-
const { project, target, flavour
|
|
238
|
+
const { project, target, flavour, ok, status, data } = result;
|
|
233
239
|
const details = data && typeof data === "object" ? data : { value: data };
|
|
234
|
-
return { project, target, flavour
|
|
240
|
+
return { project, target, flavour, ok, status, ...details };
|
|
235
241
|
}
|
|
236
242
|
function generateSummaries(results, requestedTargets) {
|
|
237
243
|
const available = new Set(availableTargets());
|
|
@@ -257,9 +263,14 @@ var HEAD_KEYS = /* @__PURE__ */ new Set([
|
|
|
257
263
|
"diagnostics",
|
|
258
264
|
"diagnosticsTruncated"
|
|
259
265
|
]);
|
|
260
|
-
function
|
|
266
|
+
function isRuleList(value) {
|
|
261
267
|
return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && "rule" in entry);
|
|
262
268
|
}
|
|
269
|
+
var RULE_BLOCKS = [
|
|
270
|
+
{ key: "deferred", label: "deferred (phase 1, non-breaking):" },
|
|
271
|
+
{ key: "disabled", label: "not enforced, by decision:" },
|
|
272
|
+
{ key: "downgraded", label: "reported, but not blocking:" }
|
|
273
|
+
];
|
|
263
274
|
function renderValue(key, value, p) {
|
|
264
275
|
if ((key === "errors" || key === "implicitAny") && typeof value === "number") {
|
|
265
276
|
return value > 0 ? p.fail(String(value)) : p.ok(String(value));
|
|
@@ -277,14 +288,26 @@ function renderSummary(item, p) {
|
|
|
277
288
|
const ok = item.ok === true;
|
|
278
289
|
const mark = ok ? p.ok("\u2713") : p.fail("\u2717");
|
|
279
290
|
const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
291
|
+
const unadopted = item.adopted === false;
|
|
292
|
+
const entries = Object.entries(item).filter(
|
|
293
|
+
([key]) => !HEAD_KEYS.has(key) && !(unadopted && (key === "conformant" || key === "drift"))
|
|
294
|
+
);
|
|
295
|
+
const blocks = RULE_BLOCKS.map((block) => ({
|
|
296
|
+
...block,
|
|
297
|
+
rules: entries.find(([key]) => key === block.key)?.[1]
|
|
298
|
+
})).filter((block) => isRuleList(block.rules));
|
|
299
|
+
const blockKeys = new Set(blocks.map((block) => block.key));
|
|
300
|
+
const inline = entries.map(([key, value]) => [
|
|
301
|
+
key,
|
|
302
|
+
blockKeys.has(key) && isRuleList(value) ? value.length : value
|
|
303
|
+
]).map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`).join(" ");
|
|
283
304
|
const lines = [inline ? `${head} ${p.dim("\u2014")} ${inline}` : head];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
305
|
+
for (const { label, rules } of blocks) {
|
|
306
|
+
if (rules.length === 0) continue;
|
|
307
|
+
lines.push(` ${p.warn(label)}`);
|
|
308
|
+
for (const { rule, phase, reason } of rules) {
|
|
309
|
+
const detail = phase === void 0 ? reason : `phase ${phase}: ${reason}`;
|
|
310
|
+
lines.push(` ${p.warn("\u2022")} ${p.strong(rule)} ${p.dim(`\u2014 ${detail}`)}`);
|
|
288
311
|
}
|
|
289
312
|
}
|
|
290
313
|
return lines.join("\n");
|
|
@@ -300,8 +323,8 @@ function statusCoverage(items) {
|
|
|
300
323
|
drifted: adopted.length - conformant.length
|
|
301
324
|
};
|
|
302
325
|
}
|
|
303
|
-
function presetShort(
|
|
304
|
-
return typeof
|
|
326
|
+
function presetShort(preset2) {
|
|
327
|
+
return typeof preset2 === "string" ? preset2.replace("@hublo/sentinel/tsconfig/", "") : "?";
|
|
305
328
|
}
|
|
306
329
|
function renderStatusRow(item, p) {
|
|
307
330
|
if (item.status === "unsupported") return renderUnsupportedRow(item, p);
|
|
@@ -414,6 +437,9 @@ if (!nodeCheck.ok) {
|
|
|
414
437
|
process.exit(1);
|
|
415
438
|
}
|
|
416
439
|
registerAdapters();
|
|
440
|
+
var separator = process.argv.indexOf("--");
|
|
441
|
+
var toolArgs = separator === -1 ? [] : process.argv.slice(separator + 1);
|
|
442
|
+
var cliArgv = separator === -1 ? process.argv : process.argv.slice(0, separator);
|
|
417
443
|
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(
|
|
418
444
|
"before",
|
|
419
445
|
[
|
|
@@ -424,23 +450,28 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
424
450
|
" --ci (affected), or all modules. --init targets one module only.",
|
|
425
451
|
""
|
|
426
452
|
].join("\n")
|
|
427
|
-
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "
|
|
453
|
+
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "deprecated: use --run --json").option("--status", "deprecated: use --inspect").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(
|
|
428
454
|
"--module <name>",
|
|
429
455
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
430
|
-
).option("--
|
|
456
|
+
).option("--preset <name>", `the stack preset to apply (${PRESET_NAMES.join(", ")})`).option("--flavour <name>", "deprecated: use --preset").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 (--init)").option("--json", "machine-readable output, for every verb").option(
|
|
431
457
|
"--max-diagnostics <n>",
|
|
432
458
|
"cap the diagnostics embedded per module in --report (0 = no cap)",
|
|
433
459
|
"100"
|
|
460
|
+
).addHelpText(
|
|
461
|
+
"after",
|
|
462
|
+
"\n -- <tool options> --run only, one type: pass the rest to the tool itself\n e.g. -- --noImplicitAny (tsc), -- --deny-warnings (oxlint)"
|
|
434
463
|
).addHelpText(
|
|
435
464
|
"after",
|
|
436
465
|
[
|
|
437
466
|
"",
|
|
438
467
|
"Examples:",
|
|
439
468
|
" sentinel --run --typescript # in a module \u2192 that module",
|
|
440
|
-
" sentinel --
|
|
441
|
-
" sentinel --
|
|
442
|
-
" sentinel --
|
|
443
|
-
" sentinel --
|
|
469
|
+
" sentinel --run --typescript --module bff-admin # from root \u2192 one module",
|
|
470
|
+
" sentinel --run --json # from root \u2192 all types, machine output",
|
|
471
|
+
" sentinel --run --ci # from root \u2192 affected only",
|
|
472
|
+
" sentinel --inspect --typescript # from root \u2192 adoption + what is deferred",
|
|
473
|
+
" sentinel --init --typescript --preset react # set up the current module + workspace",
|
|
474
|
+
" sentinel --run --typescript -- --noImplicitAny # ask the tool a question of your own"
|
|
444
475
|
].join("\n")
|
|
445
476
|
).addHelpText("after", () => {
|
|
446
477
|
const available = availableTargets();
|
|
@@ -450,7 +481,7 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
450
481
|
`Available now: ${available.length ? available.map((t) => `--${t}`).join(", ") : "(none yet)"}`,
|
|
451
482
|
planned.length ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(", ")}` : ""
|
|
452
483
|
].filter(Boolean).join("\n");
|
|
453
|
-
}).parse();
|
|
484
|
+
}).parse(cliArgv);
|
|
454
485
|
var opts = program.opts();
|
|
455
486
|
function toCamel(flag) {
|
|
456
487
|
return flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -467,9 +498,9 @@ function pickOne(kind, keys) {
|
|
|
467
498
|
}
|
|
468
499
|
function parseFlavour(value) {
|
|
469
500
|
if (value === void 0) return void 0;
|
|
470
|
-
if (typeof value !== "string" || !
|
|
501
|
+
if (typeof value !== "string" || !PRESET_NAMES.includes(value)) {
|
|
471
502
|
return program.error(
|
|
472
|
-
`sentinel: unknown --
|
|
503
|
+
`sentinel: unknown --preset ${JSON.stringify(value)}. Supported: ${PRESET_NAMES.join(", ")}.`
|
|
473
504
|
);
|
|
474
505
|
}
|
|
475
506
|
return value;
|
|
@@ -479,7 +510,16 @@ function asMessage(err) {
|
|
|
479
510
|
}
|
|
480
511
|
var verb = pickOne("verb", VERBS);
|
|
481
512
|
var cwd = process.cwd();
|
|
482
|
-
|
|
513
|
+
if (opts.preset !== void 0 && opts.flavour !== void 0) {
|
|
514
|
+
program.error("--preset and --flavour are the same option; pass only --preset.");
|
|
515
|
+
}
|
|
516
|
+
if (opts.flavour !== void 0) {
|
|
517
|
+
process.stderr.write(
|
|
518
|
+
`${palette(process.stderr).warn("sentinel: --flavour is deprecated, use --preset")} (it still works)
|
|
519
|
+
`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
var preset = parseFlavour(opts.preset ?? opts.flavour);
|
|
483
523
|
var namedTargets = TARGETS.filter((t) => opts[toCamel(t)]);
|
|
484
524
|
if (opts.all && namedTargets.length > 0) {
|
|
485
525
|
program.error(
|
|
@@ -497,28 +537,52 @@ if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
|
|
|
497
537
|
if (opts.dryRun && verb !== "init") {
|
|
498
538
|
program.error("--dry-run only applies to --init (the read verbs never write).");
|
|
499
539
|
}
|
|
540
|
+
if (toolArgs.length > 0) {
|
|
541
|
+
if (verb !== "run") {
|
|
542
|
+
program.error(
|
|
543
|
+
`options after \`--\` are passed to the tool, so they only apply to --run; --${verb} does not run one (--inspect reads the committed config, --init writes it). Try: sentinel --run --<target> -- ${toolArgs.join(" ")}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (targets.length !== 1) {
|
|
547
|
+
program.error(
|
|
548
|
+
`options after \`--\` go to ONE tool, so name one target. You asked for ${targets.length === TARGETS.length ? "all targets" : targets.map((t) => `--${t}`).join(", ")}, and \`${toolArgs.join(" ")}\` cannot be meaningful to every one of them. Try: sentinel --run --typescript -- ${toolArgs.join(" ")}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
500
552
|
async function runVerb() {
|
|
501
553
|
const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
|
|
502
554
|
const out = palette(process.stdout);
|
|
503
555
|
const err = palette(process.stderr);
|
|
504
|
-
|
|
556
|
+
const shaped = verb === "run" && opts.json ? "report" : verb;
|
|
557
|
+
const replacement = {
|
|
558
|
+
report: "--run --json",
|
|
559
|
+
status: "--inspect"
|
|
560
|
+
};
|
|
561
|
+
const instead = replacement[verb];
|
|
562
|
+
if (instead) {
|
|
563
|
+
process.stderr.write(
|
|
564
|
+
err.warn(`sentinel: --${verb} is deprecated, use ${instead}`) + " (it still works)\n"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (toolArgs.length > 0) {
|
|
505
568
|
process.stderr.write(
|
|
506
569
|
err.warn(
|
|
507
|
-
|
|
570
|
+
`sentinel: NOT the standard check \u2014 passing ${toolArgs.join(" ")} to the ${targets[0]} tool.`
|
|
508
571
|
) + "\n"
|
|
509
572
|
);
|
|
510
573
|
}
|
|
511
574
|
const started = Date.now();
|
|
512
575
|
const { results, worstCode } = await analyse({
|
|
513
|
-
verb,
|
|
576
|
+
verb: shaped,
|
|
514
577
|
targets,
|
|
515
578
|
modules,
|
|
516
579
|
runner: opts.runner,
|
|
517
|
-
|
|
580
|
+
preset,
|
|
518
581
|
targetsExplicit,
|
|
519
582
|
maxDiagnostics,
|
|
520
583
|
ci: Boolean(opts.ci),
|
|
521
584
|
fix: Boolean(opts.fix),
|
|
585
|
+
toolArgs,
|
|
522
586
|
// Per-module progress is a HUMAN affordance: it tells someone staring at a slow sweep
|
|
523
587
|
// that it is alive. On this monorepo's 441 projects it is 441 lines that bury the four
|
|
524
588
|
// that matter, and it is pure noise in a captured log. So emit it only for an
|
|
@@ -528,10 +592,11 @@ async function runVerb() {
|
|
|
528
592
|
`)) : void 0
|
|
529
593
|
});
|
|
530
594
|
const summary = generateSummaries(results, targets);
|
|
531
|
-
if (opts.json
|
|
532
|
-
const
|
|
595
|
+
if (opts.json) {
|
|
596
|
+
const base = verb === "status" || verb === "inspect" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
|
|
597
|
+
const payload = toolArgs.length > 0 ? { ...base, toolArgs } : base;
|
|
533
598
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
534
|
-
} else if (verb === "status") {
|
|
599
|
+
} else if (verb === "status" || verb === "inspect" && modules.length > 1) {
|
|
535
600
|
for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + "\n");
|
|
536
601
|
process.stdout.write(renderStatusSummary(summary.results, out) + "\n");
|
|
537
602
|
} else {
|
|
@@ -580,7 +645,7 @@ async function runInit() {
|
|
|
580
645
|
target: type,
|
|
581
646
|
runner: opts.runner,
|
|
582
647
|
cwd: module.root,
|
|
583
|
-
|
|
648
|
+
preset,
|
|
584
649
|
dryRun: Boolean(opts.dryRun),
|
|
585
650
|
json: Boolean(opts.json)
|
|
586
651
|
});
|
|
@@ -608,18 +673,24 @@ sentinel (${type}): ${asMessage(err)}
|
|
|
608
673
|
}
|
|
609
674
|
return worst;
|
|
610
675
|
}
|
|
676
|
+
async function exitWithoutTruncating(code) {
|
|
677
|
+
process.exitCode = code;
|
|
678
|
+
const stream = process.stdout;
|
|
679
|
+
if (stream.write("")) return;
|
|
680
|
+
await new Promise((resolve2) => stream.once("drain", () => resolve2()));
|
|
681
|
+
}
|
|
611
682
|
async function main() {
|
|
612
683
|
if (verb === "migrate") {
|
|
613
684
|
program.error("--migrate is planned and not available yet; use --init to set a module up.");
|
|
614
685
|
}
|
|
615
686
|
const runSelectedVerb = verb === "init" ? runInit : runVerb;
|
|
616
687
|
const exitCode = await runSelectedVerb();
|
|
617
|
-
|
|
688
|
+
await exitWithoutTruncating(exitCode);
|
|
618
689
|
}
|
|
619
690
|
main().catch((error) => {
|
|
620
691
|
process.stderr.write(`
|
|
621
692
|
sentinel: ${asMessage(error)}
|
|
622
693
|
`);
|
|
623
|
-
|
|
694
|
+
void exitWithoutTruncating(1);
|
|
624
695
|
});
|
|
625
696
|
//# sourceMappingURL=sentinel.js.map
|