@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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- FLAVOURS,
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-EZCRU6XE.js";
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 flavour2;
119
- if (params.flavour) {
120
- flavour2 = params.flavour;
118
+ let preset2;
119
+ if (params.preset) {
120
+ preset2 = params.preset;
121
121
  } else {
122
122
  const detection = describeFramework(readProjectPackageJson(module.root));
123
- flavour2 = detection.flavour;
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}: flavour is ambiguous, detected "${flavour2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to be explicit.`
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, flavour2, params.runner);
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: flavour2,
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.flavour ?? adapter.declaredFlavour?.(module.root) ?? flavour2;
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
- flavour: effectiveFlavour,
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: true,
198
- status: "ok",
199
- data: config
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: flavour2, ok, status, data } = result;
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: flavour2, ok, status, ...details };
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 isDeferredRules(value) {
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 entries = Object.entries(item).filter(([key]) => !HEAD_KEYS.has(key));
281
- const deferred = entries.find(([key]) => key === "deferred")?.[1];
282
- const inline = entries.filter(([key]) => key !== "deferred").map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`).join(" ");
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
- if (isDeferredRules(deferred) && deferred.length > 0) {
285
- lines.push(` ${p.warn("deferred (phase 1, non-breaking):")}`);
286
- for (const { rule, phase, reason } of deferred) {
287
- lines.push(` ${p.warn("\u2022")} ${p.strong(rule)} ${p.dim(`\u2014 phase ${phase}: ${reason}`)}`);
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(preset) {
304
- return typeof preset === "string" ? preset.replace("@hublo/sentinel/tsconfig/", "") : "?";
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", "metrics and health report").option("--status", "adoption + conformity across modules (coverage + drift)").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(
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("--flavour <name>", `override the detected stack preset (${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 (--init)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").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 --report --typescript --module bff-admin # from root \u2192 one module",
441
- " sentinel --report # from root \u2192 all types, all modules",
442
- " sentinel --report --ci # from root \u2192 affected only",
443
- " sentinel --init --typescript --flavour react # set up the current module + workspace"
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" || !FLAVOURS.includes(value)) {
501
+ if (typeof value !== "string" || !PRESET_NAMES.includes(value)) {
471
502
  return program.error(
472
- `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(", ")}.`
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
- var flavour = parseFlavour(opts.flavour);
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
- if (opts.json && verb === "run") {
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
- "note: --json is ignored for --run (it streams the tool output); use --report --json for a machine envelope."
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
- flavour,
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 && verb !== "run") {
532
- const payload = verb === "status" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
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
- flavour,
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
- process.exit(exitCode);
688
+ await exitWithoutTruncating(exitCode);
618
689
  }
619
690
  main().catch((error) => {
620
691
  process.stderr.write(`
621
692
  sentinel: ${asMessage(error)}
622
693
  `);
623
- process.exit(1);
694
+ void exitWithoutTruncating(1);
624
695
  });
625
696
  //# sourceMappingURL=sentinel.js.map