@hublo/sentinel 1.5.1-alpha.0 → 1.5.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,10 +55,13 @@ sentinel --init --preset <react|nest|node|svelte> # every role sentinel ships
55
55
  pnpm install # fetch what --init declared, then commit
56
56
  ```
57
57
 
58
- With no target named, `--init` adopts every role, in the order that makes the result correct:
59
- the linter's autofix runs, then the formatter runs **last** and formats everything every role
60
- wrote. Naming a target (`--init --build`) adopts just that role and leaves the rest alone, with
58
+ With no target named, `--init` on a module that has adopted nothing adopts every role, in the
59
+ order that makes the result correct: the linter's autofix runs, then the formatter runs **last**
60
+ and formats everything every role wrote. On a module that has already adopted a role, the same
61
+ bare `--init` re-inits only the roles it has, read the way `--inspect` reports them, and says
62
+ which. Naming a target (`--init --build`) adopts just that role and leaves the rest alone, with
61
63
  one pairing: lint and format are always adopted together, so `--init --lint` adopts oxfmt too.
64
+ `--init --all` adopts every role on any module.
62
65
 
63
66
  `--init` writes the config stubs, the `typecheck`/`lint`/... scripts, and pins the `@hublo/sentinel` devDependency into the module (no manual `pnpm add`); it scaffolds a `package.json` for a `project.json`-only module. It also applies, once, the workspace prep that module needs at the root, only when the root's own config shows it is needed (e.g. an i18next singleton override when the repo runs a second TypeScript, a release-age allow-list when the repo uses that pnpm gate). See the [adoption cheat sheet](docs/typescript-adoption.md) for the full list.
64
67
 
@@ -128,7 +131,7 @@ And the app's `package.json` scripts route every check through the one CLI (run
128
131
  **Step 3 — evolve the toolchain, in one central place:**
129
132
 
130
133
  - a **rule change** → bump the `@hublo/sentinel` version; the stubs already point at it, so there is **nothing to regenerate**;
131
- - a **structural change** (new tool, new preset, runner swap) → run `sentinel --init` once to refresh the stubs (sentinel tells you when this is needed).
134
+ - a **structural change** (new tool, new preset, runner swap) → run `sentinel --init` once to refresh the stubs (sentinel tells you when this is needed). On an adopted module it re-inits the roles the module has, and no others.
132
135
 
133
136
  **Step 4 — stay converged:** a drift guard in CI flags any module whose config quietly diverged from the shared source.
134
137
 
@@ -11,10 +11,10 @@ import {
11
11
  presetNotInstalledYet,
12
12
  registerAdapters,
13
13
  resolve
14
- } from "../chunk-CL7HOHTU.js";
14
+ } from "../chunk-64H5HBPH.js";
15
15
  import {
16
16
  resolveContext
17
- } from "../chunk-I2WDZNPR.js";
17
+ } from "../chunk-J2USE3KV.js";
18
18
  import {
19
19
  ensureWorkspacePrep,
20
20
  findWorkspaceRoot,
@@ -36,6 +36,175 @@ import "../chunk-QXFCZON7.js";
36
36
  // bin/sentinel.ts
37
37
  import { program } from "commander";
38
38
 
39
+ // src/core/orchestrate.ts
40
+ async function analyse(params) {
41
+ const results = [];
42
+ let worstCode = 0;
43
+ let done = 0;
44
+ for (const module of params.modules) {
45
+ let preset2;
46
+ if (params.preset) {
47
+ preset2 = params.preset;
48
+ } else {
49
+ const detection = describeFramework(readProjectPackageJson(module.root));
50
+ preset2 = detection.preset;
51
+ if (detection.ambiguous) {
52
+ const warn = palette(process.stderr);
53
+ process.stderr.write(
54
+ warn.warn(
55
+ `sentinel: ${module.name}: preset is ambiguous, detected "${preset2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --preset to be explicit.`
56
+ ) + "\n"
57
+ );
58
+ }
59
+ preset2 = correctingSignal(preset2, module.root)?.preset ?? preset2;
60
+ }
61
+ for (const target of params.targets) {
62
+ let adapter;
63
+ const declared = declaredPresetFor(target, module.root);
64
+ const corrected = params.preset ?? declared ?? correctingSignal(preset2, module.root)?.preset ?? preset2;
65
+ const resolutionPreset = corrected;
66
+ try {
67
+ adapter = resolve(target, resolutionPreset, params.runner);
68
+ } catch (error) {
69
+ if (params.targetsExplicit) {
70
+ const reason = error instanceof Error ? error.message : String(error);
71
+ results.push({
72
+ project: module.name,
73
+ target,
74
+ flavour: preset2,
75
+ ok: false,
76
+ status: "unsupported",
77
+ data: { reason }
78
+ });
79
+ }
80
+ continue;
81
+ }
82
+ const effectiveFlavour = params.preset ?? adapter.declaredPreset?.(module.root) ?? preset2;
83
+ const ctx = {
84
+ module: module.name,
85
+ cwd: module.root,
86
+ preset: effectiveFlavour,
87
+ ci: params.ci,
88
+ fix: params.fix,
89
+ maxDiagnostics: params.maxDiagnostics,
90
+ toolArgs: params.toolArgs,
91
+ output: params.output
92
+ };
93
+ try {
94
+ let adoption;
95
+ if (params.verb === "run" || params.verb === "report") {
96
+ try {
97
+ adoption = await adapter.status(ctx);
98
+ } catch {
99
+ }
100
+ }
101
+ const adoptionFields = adoption ? { adopted: adoption.adopted, preset: adoption.preset, conformant: adoption.conformant } : {};
102
+ if (params.verb === "run") {
103
+ if (params.modules.length > 1) {
104
+ const err = palette(process.stderr);
105
+ process.stderr.write(
106
+ `
107
+ ${err.accent("\u25B6")} ${err.strong(module.name)} ${err.dim(`(${effectiveFlavour})`)} ${target}
108
+ `
109
+ );
110
+ }
111
+ const result = await adapter.run(ctx);
112
+ results.push({
113
+ project: module.name,
114
+ target,
115
+ flavour: effectiveFlavour,
116
+ ok: result.ok,
117
+ status: result.ok ? "ok" : "failed",
118
+ data: { ...adoptionFields }
119
+ });
120
+ worstCode = Math.max(worstCode, result.code);
121
+ } else if (params.verb === "report") {
122
+ const result = await adapter.report(ctx);
123
+ results.push({
124
+ project: module.name,
125
+ target,
126
+ flavour: effectiveFlavour,
127
+ ok: result.ok,
128
+ status: result.ok ? "ok" : "failed",
129
+ // Engine fields LAST, so they WIN. Spreading them first let a role's own metrics
130
+ // override them, which is the disagreement this was written to remove: the point
131
+ // is that adoption has exactly one source, not that it has a default.
132
+ data: { ...result.metrics ?? {}, ...adoptionFields }
133
+ });
134
+ worstCode = Math.max(worstCode, result.code);
135
+ } else if (params.verb === "inspect") {
136
+ const [config, adoption2] = await Promise.all([adapter.inspect(ctx), adapter.status(ctx)]);
137
+ const ok = !adoption2.unreadable && (!adoption2.adopted || adoption2.conformant);
138
+ results.push({
139
+ project: module.name,
140
+ target,
141
+ flavour: effectiveFlavour,
142
+ ok,
143
+ status: ok ? "ok" : "failed",
144
+ // `inspect` is typed as `unknown` (each role shapes its own detail), so the
145
+ // merge is guarded rather than assumed: a role returning a non-object still
146
+ // gets its adoption fields instead of throwing here.
147
+ data: config && typeof config === "object" ? { ...config, ...adoption2 } : { ...adoption2 }
148
+ });
149
+ if (!ok) worstCode = Math.max(worstCode, 1);
150
+ } else {
151
+ const status = await adapter.status(ctx);
152
+ const ok = !status.unreadable && (!status.adopted || status.conformant);
153
+ results.push({
154
+ project: module.name,
155
+ target,
156
+ flavour: effectiveFlavour,
157
+ ok,
158
+ status: ok ? "ok" : "failed",
159
+ data: status
160
+ });
161
+ if (!ok) worstCode = Math.max(worstCode, 1);
162
+ }
163
+ } catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ results.push({
166
+ project: module.name,
167
+ target,
168
+ flavour: effectiveFlavour,
169
+ ok: false,
170
+ status: "failed",
171
+ data: { error: message }
172
+ });
173
+ worstCode = Math.max(worstCode, 1);
174
+ }
175
+ }
176
+ params.onProgress?.(done += 1, params.modules.length, module.name);
177
+ }
178
+ return { results, worstCode };
179
+ }
180
+ function generateSummary(result) {
181
+ const { project, target, flavour, ok, status, data } = result;
182
+ const details = data && typeof data === "object" ? data : { value: data };
183
+ return { project, target, flavour, ok, status, ...details };
184
+ }
185
+ function generateSummaries(results, requestedTargets) {
186
+ const available = new Set(availableTargets());
187
+ return {
188
+ schemaVersion: 2,
189
+ executed: requestedTargets.filter((t) => available.has(t)),
190
+ skipped: requestedTargets.filter((t) => !available.has(t)),
191
+ results: results.map(generateSummary)
192
+ };
193
+ }
194
+ async function adoptedTargets(params) {
195
+ const { results } = await analyse({
196
+ verb: "status",
197
+ targets: params.targets,
198
+ modules: [params.module],
199
+ preset: params.preset,
200
+ runner: params.runner,
201
+ targetsExplicit: false,
202
+ ci: false,
203
+ fix: false
204
+ });
205
+ return results.filter((row) => row.data?.adopted === true).map((row) => row.target);
206
+ }
207
+
39
208
  // src/core/rehearse.ts
40
209
  import { createHash } from "node:crypto";
41
210
  import {
@@ -202,6 +371,24 @@ function describeRehearsal(target, rewrites, failure) {
202
371
  const rest = rewrites.length - NAMED_REWRITES;
203
372
  return `${pass} would rewrite ${rewrites.length} file${rewrites.length === 1 ? "" : "s"} beyond the ones above: ${rest > 0 ? `${named} and ${rest} more` : named}${said}`;
204
373
  }
374
+ async function rolesToInit(ctx, module, available) {
375
+ const requested = ctx.targets.filter((type) => available.includes(type));
376
+ if (ctx.targetsExplicit || ctx.allTargets) return requested;
377
+ const adopted = await adoptedTargets({
378
+ module,
379
+ targets: requested,
380
+ preset: ctx.preset,
381
+ runner: ctx.runner
382
+ });
383
+ if (adopted.length === 0) return requested;
384
+ process.stderr.write(
385
+ palette(process.stderr).dim(
386
+ ` re-initializing the roles this module has adopted (${adopted.map((t) => `--${t}`).join(", ")}); to adopt another, name it (sentinel --init --<role>), or pass --all for every role
387
+ `
388
+ )
389
+ );
390
+ return adopted;
391
+ }
205
392
  async function runInit(ctx) {
206
393
  const { modules, scope } = resolveContext(ctx.cwd, { module: ctx.module, ci: false });
207
394
  const [module] = modules;
@@ -219,7 +406,7 @@ async function runInit(ctx) {
219
406
  );
220
407
  }
221
408
  }
222
- const named = ctx.targets.filter((type) => available.includes(type));
409
+ const named = await rolesToInit(ctx, module, available);
223
410
  const paired = pairedTargets(named, available);
224
411
  const toRun = [...named, ...paired].sort(
225
412
  (a, b) => Number(a === "format") - Number(b === "format")
@@ -340,7 +527,7 @@ async function runValidate(ctx) {
340
527
  );
341
528
  }
342
529
  if (!ctx.targets.includes("test")) return 1;
343
- const { validateModuleTests } = await import("../validate-S6PIOVAH.js");
530
+ const { validateModuleTests } = await import("../validate-TIPIECG6.js");
344
531
  const outcomes = modules.flatMap(
345
532
  (module) => validateModuleTests(module.name, module.root)
346
533
  );
@@ -354,162 +541,6 @@ async function runValidate(ctx) {
354
541
  return outcomes.every((outcome) => outcome.ok) ? 0 : 1;
355
542
  }
356
543
 
357
- // src/core/orchestrate.ts
358
- async function analyse(params) {
359
- const results = [];
360
- let worstCode = 0;
361
- let done = 0;
362
- for (const module of params.modules) {
363
- let preset2;
364
- if (params.preset) {
365
- preset2 = params.preset;
366
- } else {
367
- const detection = describeFramework(readProjectPackageJson(module.root));
368
- preset2 = detection.preset;
369
- if (detection.ambiguous) {
370
- const warn = palette(process.stderr);
371
- process.stderr.write(
372
- warn.warn(
373
- `sentinel: ${module.name}: preset is ambiguous, detected "${preset2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --preset to be explicit.`
374
- ) + "\n"
375
- );
376
- }
377
- preset2 = correctingSignal(preset2, module.root)?.preset ?? preset2;
378
- }
379
- for (const target of params.targets) {
380
- let adapter;
381
- const declared = declaredPresetFor(target, module.root);
382
- const corrected = params.preset ?? declared ?? correctingSignal(preset2, module.root)?.preset ?? preset2;
383
- const resolutionPreset = corrected;
384
- try {
385
- adapter = resolve(target, resolutionPreset, params.runner);
386
- } catch (error) {
387
- if (params.targetsExplicit) {
388
- const reason = error instanceof Error ? error.message : String(error);
389
- results.push({
390
- project: module.name,
391
- target,
392
- flavour: preset2,
393
- ok: false,
394
- status: "unsupported",
395
- data: { reason }
396
- });
397
- }
398
- continue;
399
- }
400
- const effectiveFlavour = params.preset ?? adapter.declaredPreset?.(module.root) ?? preset2;
401
- const ctx = {
402
- module: module.name,
403
- cwd: module.root,
404
- preset: effectiveFlavour,
405
- ci: params.ci,
406
- fix: params.fix,
407
- maxDiagnostics: params.maxDiagnostics,
408
- toolArgs: params.toolArgs,
409
- output: params.output
410
- };
411
- try {
412
- let adoption;
413
- if (params.verb === "run" || params.verb === "report") {
414
- try {
415
- adoption = await adapter.status(ctx);
416
- } catch {
417
- }
418
- }
419
- const adoptionFields = adoption ? { adopted: adoption.adopted, preset: adoption.preset, conformant: adoption.conformant } : {};
420
- if (params.verb === "run") {
421
- if (params.modules.length > 1) {
422
- const err = palette(process.stderr);
423
- process.stderr.write(
424
- `
425
- ${err.accent("\u25B6")} ${err.strong(module.name)} ${err.dim(`(${effectiveFlavour})`)} ${target}
426
- `
427
- );
428
- }
429
- const result = await adapter.run(ctx);
430
- results.push({
431
- project: module.name,
432
- target,
433
- flavour: effectiveFlavour,
434
- ok: result.ok,
435
- status: result.ok ? "ok" : "failed",
436
- data: { ...adoptionFields }
437
- });
438
- worstCode = Math.max(worstCode, result.code);
439
- } else if (params.verb === "report") {
440
- const result = await adapter.report(ctx);
441
- results.push({
442
- project: module.name,
443
- target,
444
- flavour: effectiveFlavour,
445
- ok: result.ok,
446
- status: result.ok ? "ok" : "failed",
447
- // Engine fields LAST, so they WIN. Spreading them first let a role's own metrics
448
- // override them, which is the disagreement this was written to remove: the point
449
- // is that adoption has exactly one source, not that it has a default.
450
- data: { ...result.metrics ?? {}, ...adoptionFields }
451
- });
452
- worstCode = Math.max(worstCode, result.code);
453
- } else if (params.verb === "inspect") {
454
- const [config, adoption2] = await Promise.all([adapter.inspect(ctx), adapter.status(ctx)]);
455
- const ok = !adoption2.unreadable && (!adoption2.adopted || adoption2.conformant);
456
- results.push({
457
- project: module.name,
458
- target,
459
- flavour: effectiveFlavour,
460
- ok,
461
- status: ok ? "ok" : "failed",
462
- // `inspect` is typed as `unknown` (each role shapes its own detail), so the
463
- // merge is guarded rather than assumed: a role returning a non-object still
464
- // gets its adoption fields instead of throwing here.
465
- data: config && typeof config === "object" ? { ...config, ...adoption2 } : { ...adoption2 }
466
- });
467
- if (!ok) worstCode = Math.max(worstCode, 1);
468
- } else {
469
- const status = await adapter.status(ctx);
470
- const ok = !status.unreadable && (!status.adopted || status.conformant);
471
- results.push({
472
- project: module.name,
473
- target,
474
- flavour: effectiveFlavour,
475
- ok,
476
- status: ok ? "ok" : "failed",
477
- data: status
478
- });
479
- if (!ok) worstCode = Math.max(worstCode, 1);
480
- }
481
- } catch (error) {
482
- const message = error instanceof Error ? error.message : String(error);
483
- results.push({
484
- project: module.name,
485
- target,
486
- flavour: effectiveFlavour,
487
- ok: false,
488
- status: "failed",
489
- data: { error: message }
490
- });
491
- worstCode = Math.max(worstCode, 1);
492
- }
493
- }
494
- params.onProgress?.(done += 1, params.modules.length, module.name);
495
- }
496
- return { results, worstCode };
497
- }
498
- function generateSummary(result) {
499
- const { project, target, flavour, ok, status, data } = result;
500
- const details = data && typeof data === "object" ? data : { value: data };
501
- return { project, target, flavour, ok, status, ...details };
502
- }
503
- function generateSummaries(results, requestedTargets) {
504
- const available = new Set(availableTargets());
505
- return {
506
- schemaVersion: 2,
507
- executed: requestedTargets.filter((t) => available.has(t)),
508
- skipped: requestedTargets.filter((t) => !available.has(t)),
509
- results: results.map(generateSummary)
510
- };
511
- }
512
-
513
544
  // src/core/render.ts
514
545
  var HEAD_KEYS = /* @__PURE__ */ new Set([
515
546
  "project",
@@ -821,7 +852,8 @@ program.name("sentinel").description("One CLI that guards code health: presets,
821
852
  // Deliberately not enumerated here. A hand-written list drifts, and this one did: it
822
853
  // still said "lint, format, typescript" after the build role shipped, so the help told
823
854
  // adopters the build was not covered when it was. "Available now", below, is derived.
824
- " sentinel --init --preset react # adopt every available role (see below)",
855
+ " sentinel --init --preset react # a new module: adopt every available role",
856
+ " sentinel --init # an adopted module: re-init the roles it has",
825
857
  " sentinel --init --typescript --preset react # set up the current module + workspace",
826
858
  " sentinel --run --typescript -- --noImplicitAny # ask the tool a question of your own"
827
859
  ].join("\n")
@@ -945,6 +977,7 @@ async function main() {
945
977
  cwd,
946
978
  targets,
947
979
  targetsExplicit,
980
+ allTargets: Boolean(opts.all),
948
981
  preset,
949
982
  toolArgs,
950
983
  maxDiagnostics,