@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
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ A large monorepo accumulates:
|
|
|
23
23
|
|
|
24
24
|
- **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
|
|
25
25
|
- **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
|
|
26
|
-
- **`--
|
|
27
|
-
- **Move one module at a time** — installed per module, so you
|
|
28
|
-
- **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--
|
|
26
|
+
- **`--init` sets a module up** — the one command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and applies the workspace prep the module needs. Run it module by module to roll out gradually. (`--migrate`, for changing an already-initialized setup, is a reserved future verb.)
|
|
27
|
+
- **Move one module at a time** — installed per module, so you adopt at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
|
|
28
|
+
- **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--init` regenerates the stubs.
|
|
29
29
|
- **No silent drift** — the guard keeps every project's config converged on the source of truth.
|
|
30
30
|
|
|
31
31
|
### Before → after
|
|
@@ -35,7 +35,7 @@ A large monorepo accumulates:
|
|
|
35
35
|
| **Config** | ~116 eslint + ~528 tsconfig files with real, drifting content | thin stubs that `extends` a versioned preset; rules in one place |
|
|
36
36
|
| **Tooling deps** | ~165 devDeps at the root, shared by all | one `@hublo/sentinel` per module; the toolchain rides its version |
|
|
37
37
|
| **Upgrade a tool** | big-bang: every project at once, untested in isolation | bump one version, tested in sentinel first, atomic |
|
|
38
|
-
| **Swap a tool** | edit config in every project | swap an adapter + `--
|
|
38
|
+
| **Swap a tool** | edit config in every project | swap an adapter + `--init`; zero project churn |
|
|
39
39
|
| **A rule change** | edit many configs, hope they stay consistent | change once; the drift guard enforces it |
|
|
40
40
|
| **Migration** | all-or-nothing | module by module, at your pace |
|
|
41
41
|
|
|
@@ -43,13 +43,17 @@ A large monorepo accumulates:
|
|
|
43
43
|
|
|
44
44
|
sentinel writes **standard config files** into a project (each just `extends` a sentinel preset) and runs the checks. Your editor and the tools read those **normal files natively**, they never call sentinel at runtime, so nothing is coupled to it or brittle.
|
|
45
45
|
|
|
46
|
-
**
|
|
46
|
+
> **Shipped today:** only the **TypeScript** tool, so `--init` writes the `tsconfig` stub, and `--run`/`--report`/`--status` work for `--typescript`. The `eslint.config.js` / `--lint` / `--test` snippets below illustrate the end state; those subpaths (`@hublo/sentinel/lint/*`, …) land with their tool ticket.
|
|
47
|
+
|
|
48
|
+
**Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed). Run from the app dir; `--init` does it all, nothing is hand-edited:
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
sentinel --init --typescript --flavour <react|nest|node>
|
|
52
|
+
pnpm install # fetch what --init declared, then commit
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
`--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.
|
|
56
|
+
|
|
53
57
|
Those files are tiny, they just point at a sentinel preset. What gets committed:
|
|
54
58
|
|
|
55
59
|
```js
|
|
@@ -93,12 +97,31 @@ And the app's `package.json` scripts route every check through the one CLI (run
|
|
|
93
97
|
**Step 3 — evolve the toolchain, in one central place:**
|
|
94
98
|
|
|
95
99
|
- a **rule change** → bump the `@hublo/sentinel` version; the stubs already point at it, so there is **nothing to regenerate**;
|
|
96
|
-
- a **structural change** (new tool, new preset, runner swap) → run `sentinel --
|
|
100
|
+
- a **structural change** (new tool, new preset, runner swap) → run `sentinel --init` once to refresh the stubs (sentinel tells you when this is needed).
|
|
97
101
|
|
|
98
102
|
**Step 4 — stay converged:** a drift guard in CI flags any module whose config quietly diverged from the shared source.
|
|
99
103
|
|
|
100
104
|
The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …) lives **inside sentinel as an adapter**, swappable centrally, but never a runtime dependency of the project.
|
|
101
105
|
|
|
106
|
+
## Requirements & installing
|
|
107
|
+
|
|
108
|
+
**Registry: public npm**, under `@hublo`. We started on **GitHub Packages (private)** and moved off it: it authenticates every consumer, including a one-off `pnpm dlx`, which is incompatible with the zero-setup adoption above. Public npm needs no consumer auth, so a module adopts sentinel without any `.npmrc` or token. Releases go out through the repo's `publish` workflow, never from a laptop; a prerelease is published under its prerelease dist-tag (`alpha`) and a stable one under `latest`.
|
|
109
|
+
|
|
110
|
+
**Node.** sentinel needs **Node >= 20.12** (its coloured output uses `util.styleText`, added in 20.12). It fails fast with a clear message on an older runtime rather than crashing. If a project runs on an older Node (e.g. a legacy app on Node 10), run sentinel with a modern Node via `fnm`/`nvm`; you do not need to change the project's own Node.
|
|
111
|
+
|
|
112
|
+
**Try it without installing.** A one-off run needs no auth and touches nothing:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Installing a pre-release (`minimumReleaseAge`).** The monorepo enforces a 3-day `minimumReleaseAge` supply-chain gate (a freshly published version cannot be installed until it has aged 3 days). A brand-new `alpha` therefore cannot be added yet, so while testing pre-releases you either exclude the package (`pnpm-workspace.yaml` → `minimumReleaseAgeExclude`) or install with `--config.minimumReleaseAge=0`. This is a deliberate protection, not a bug: **always pin the exact version** (`@hublo/sentinel@0.1.0-alpha.9`) rather than `@latest`, so a run is reproducible and the gate stays meaningful.
|
|
119
|
+
|
|
120
|
+
## Docs & cheat sheets
|
|
121
|
+
|
|
122
|
+
- [`docs/typescript-adoption.md`](docs/typescript-adoption.md) — the adoption cheat sheet: the two adoption steps, the command model (verb x type x location), options, reading a report, and troubleshooting.
|
|
123
|
+
- [`docs/typescript-traces.md`](docs/typescript-traces.md) — a **generated, versioned** reference of live command + output traces (every verb, option, config result and edge case) against the mock monorepo. Regenerate after CLI changes with `pnpm docs:traces`.
|
|
124
|
+
|
|
102
125
|
## Architecture: `target → runner → flavour`
|
|
103
126
|
|
|
104
127
|
Every check is described by three layers:
|
|
@@ -112,7 +135,7 @@ Every check is described by three layers:
|
|
|
112
135
|
A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
|
|
113
136
|
|
|
114
137
|
- `--runner` is an **optional override on a central default**. `sentinel --lint` uses the configured default runner, so swapping a tool globally is a one-place change; `--runner=biome` overrides for a single run (great for benchmarking eslint vs biome vs oxlint, and for gradual migration).
|
|
115
|
-
- The **flavour is detected** from the module's dependencies (deterministic: a framework dep → its flavour, else `node`), and **`--flavour` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--flavour` for `--
|
|
138
|
+
- The **flavour is detected** from the module's dependencies (deterministic: a framework dep → its flavour, else `node`), and **`--flavour` overrides it**. Detection can be wrong where deps are hoisted at the repo root (it returns `node`), so pass `--flavour` for `--init` (that is where the preset is chosen). `--run`/`--report` don't depend on it, they run the tool on the committed config.
|
|
116
139
|
|
|
117
140
|
### Adapters & the engine (ports & adapters)
|
|
118
141
|
|
|
@@ -125,12 +148,12 @@ A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=esl
|
|
|
125
148
|
`sentinel` does not reimplement tools. The contract:
|
|
126
149
|
|
|
127
150
|
- **`appliesTo(flavour)`** — which flavours this adapter handles (resolution filters on it, so a React-only adapter is never picked for Nest)
|
|
128
|
-
- **`plan(flavour)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--
|
|
151
|
+
- **`plan(flavour)`** — PURE: returns a declarative `UpdatePlan` of file operations (used by `--init`). The adapter never touches the disk; the engine applies the plan.
|
|
129
152
|
- **`run(ctx)`** — invoke the tool's bin against the project (used by `--run`)
|
|
130
153
|
- **`inspect(flavour)`** — the adapter's resolved base config (used by `--inspect`)
|
|
131
154
|
- **`report(ctx)`** — metrics (used by `--report`)
|
|
132
155
|
|
|
133
|
-
**`--
|
|
156
|
+
**`--init` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
|
|
134
157
|
|
|
135
158
|
- `write { path, contents }` — a file the adapter fully owns (the thin stub)
|
|
136
159
|
- `merge-json { path, value }` — pin the keys sentinel owns while **preserving** a project's own (this is how a tsconfig's `paths`/`include` survive)
|
|
@@ -150,7 +173,7 @@ flowchart LR
|
|
|
150
173
|
D --> R["registry.resolve<br/>(target, flavour, runner)"]
|
|
151
174
|
R --> A["adapter<br/>eslint / tsc / vitest / ..."]
|
|
152
175
|
A -->|"--run"| Run["tool binary on the project"]
|
|
153
|
-
A -->|"--
|
|
176
|
+
A -->|"--init"| Upd["declarative plan → engine writes"]
|
|
154
177
|
A -->|"--inspect"| Ins["resolved config"]
|
|
155
178
|
A -->|"--report"| Rep["metrics"]
|
|
156
179
|
```
|
|
@@ -160,7 +183,7 @@ flowchart LR
|
|
|
160
183
|
```mermaid
|
|
161
184
|
flowchart TB
|
|
162
185
|
S["@hublo/sentinel<br/>rules = one source of truth"]
|
|
163
|
-
S -->|"--
|
|
186
|
+
S -->|"--init generates"| Stub["thin stub per module<br/>(extends sentinel)"]
|
|
164
187
|
Stub --> IDE["editor: live lint / type / format"]
|
|
165
188
|
Stub --> NX["nx: target inference"]
|
|
166
189
|
S -->|"--run injects config"| CI["CLI / CI: run tool on target"]
|
|
@@ -172,10 +195,10 @@ flowchart TB
|
|
|
172
195
|
The rules live in `sentinel`. Each module keeps a **thin, generated stub** per tool, a few lines that `extends`/re-export the sentinel preset:
|
|
173
196
|
|
|
174
197
|
- **Rules in sentinel** — one source of truth, versioned.
|
|
175
|
-
- **Thin stubs per module** — the stub is what keeps the **editor working** (VS Code discovers config by file, real-time lint/type/format stay live) and what **nx** uses to infer targets. Stubs are **generated by `sentinel --
|
|
198
|
+
- **Thin stubs per module** — the stub is what keeps the **editor working** (VS Code discovers config by file, real-time lint/type/format stay live) and what **nx** uses to infer targets. Stubs are **generated by `sentinel --init`**, never hand-written; swapping a runner regenerates them.
|
|
176
199
|
- **Drift guard** — sentinel validates that each stub is _only_ the sanctioned `extends`, with nothing added or overridden. Unsanctioned drift is flagged in CI; a genuine exception must be **declared in an allowlist** (visible, reviewed), never silent.
|
|
177
200
|
- **Per-module install** — `@hublo/sentinel` is added per module, so adoption is **gradual** (migrate lot by lot; a module can adopt sentinel while its neighbour still uses the old config). Root configs are removed only once the **last** module has migrated.
|
|
178
|
-
- **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **
|
|
201
|
+
- **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **resolved from the adopting module** at run time (sentinel looks for the tool in the module, then falls back to `PATH`), so the editor and nx keep using the exact binary the project already installs. sentinel does **not** declare them as dependencies today, so it does not pin their versions: the module still owns its own `typescript`. Having sentinel dictate those versions (as peer dependencies, so the whole toolchain rides the sentinel version) is the intended end state, and it lands with the tool tickets that actually bundle a runner.
|
|
179
202
|
|
|
180
203
|
## Composition & precedence
|
|
181
204
|
|
|
@@ -245,7 +268,7 @@ VERBS --run execute the target's tool
|
|
|
245
268
|
--inspect show the resolved configuration (incl. deferred rules)
|
|
246
269
|
--report metrics and health
|
|
247
270
|
--status adoption + conformity (coverage + drift), read from configs
|
|
248
|
-
--
|
|
271
|
+
--init generate/apply the config stubs (writes; one module only)
|
|
249
272
|
|
|
250
273
|
TYPES --lint --format --typescript --build --test
|
|
251
274
|
--static-analysis --runtime-analysis --arch
|
|
@@ -259,18 +282,19 @@ OPTIONS --module <name> from the root: scope to one module
|
|
|
259
282
|
--runner <tool> override the default runner
|
|
260
283
|
--ci from the root: affected only; non-zero exit on failure
|
|
261
284
|
--fix auto-fix where applicable
|
|
262
|
-
--dry-run preview a --
|
|
285
|
+
--dry-run preview a --init without writing
|
|
286
|
+
--json machine-readable output (report / inspect / status / --dry-run)
|
|
263
287
|
|
|
264
288
|
EXAMPLES sentinel --run --typescript # in a module → that module
|
|
265
289
|
sentinel --report --typescript --module bff-admin # from root → one module
|
|
266
290
|
sentinel --report # from root → all types, all modules
|
|
267
291
|
sentinel --status --typescript # from root → adoption coverage
|
|
268
292
|
sentinel --status --ci # from root → fail CI on drift
|
|
269
|
-
sentinel --
|
|
293
|
+
sentinel --init --typescript --flavour react # write stubs for the current module
|
|
270
294
|
```
|
|
271
295
|
|
|
272
296
|
`--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
|
|
273
|
-
module, or from the root for a name / affected / all); `--
|
|
297
|
+
module, or from the root for a name / affected / all); `--init` writes, so it targets
|
|
274
298
|
one module only (adopting every module at once is refused, adopt gradually).
|
|
275
299
|
|
|
276
300
|
**`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
|
|
@@ -300,7 +324,7 @@ src/
|
|
|
300
324
|
settings.ts # tunables (workspace-root marker, ...)
|
|
301
325
|
registry.ts # register + flavour-aware resolve
|
|
302
326
|
dispatch.ts # verb → adapter method
|
|
303
|
-
apply-plan.ts # the engine's filesystem port (applies --
|
|
327
|
+
apply-plan.ts # the engine's filesystem port (applies --init operations)
|
|
304
328
|
roles/<config-role>/ # lint, format, typescript, build, test
|
|
305
329
|
adapters/<runner>/ # one adapter per tool (implements the contract)
|
|
306
330
|
flavours/<stack>/ # config presets per stack (react, nest, svelte, ...)
|
|
@@ -309,8 +333,7 @@ src/
|
|
|
309
333
|
runners/ # one runner per sub-tool (duplication, complexity, ...)
|
|
310
334
|
shared/ # reusable utils (package-json, deep-merge, text)
|
|
311
335
|
tests/ # unit tests + tests/e2e (runs the built dist binary)
|
|
312
|
-
.
|
|
313
|
-
.github/workflows/ # ci.yml (PR checks) + release.yml (changesets publish)
|
|
336
|
+
.github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
|
|
314
337
|
```
|
|
315
338
|
|
|
316
339
|
Subpath exports (in `package.json`) expose presets to consumers. Shipped today:
|
|
@@ -341,8 +364,8 @@ nx is a **task runner**: it just runs the target's script. So `project.json` bar
|
|
|
341
364
|
|
|
342
365
|
This scaffold is the foundation; each tool is added one at a time on top of it:
|
|
343
366
|
|
|
344
|
-
1. **Foundation** — repo, exports, CLI skeleton, adapter contract,
|
|
345
|
-
2. **TypeScript** (`--typescript`) — runner `tsc` (later `tsgo`): presets react/nest/node, `--run`/`--inspect`/`--report`/`--
|
|
367
|
+
1. **Foundation** — repo, exports, CLI skeleton, adapter contract, CI + the single dispatch `publish` workflow. **✅ shipped**
|
|
368
|
+
2. **TypeScript** (`--typescript`) — runner `tsc` (later `tsgo`): presets react/nest/node, `--run`/`--inspect`/`--report`/`--init`, the composable grid, phased (non-breaking) strictness. **✅ shipped in `0.1.0-alpha.x`**
|
|
346
369
|
3. **Lint** (`--lint`) — benchmark `eslint` vs `biome` vs `oxlint`.
|
|
347
370
|
4. **Build** (`--build`) — `vite`.
|
|
348
371
|
5. **Test** (`--test`) — `vitest`, plus a11y / w3c setups.
|
|
@@ -352,7 +375,6 @@ This scaffold is the foundation; each tool is added one at a time on top of it:
|
|
|
352
375
|
|
|
353
376
|
## Open decisions & risks being validated
|
|
354
377
|
|
|
355
|
-
- **Registry:** **GitHub Packages (private)** for the foundation, publishing needs no extra secret and there is no consumer to authenticate yet. Public npm under `@hublo` (zero consumer auth, OSS-ready) stays an option once the package is stable; the consuming repo's `.npmrc`/auth is set up with the first tool ticket, when there is actually something to install.
|
|
356
378
|
- **Performance:** prefer adopting Rust-native tools (`oxlint`/`biome`, `tsgo`) over hand-written Rust+WASM; reserve custom WASM for a _measured_ hot path only.
|
|
357
379
|
- **Validation spike (before wide rollout):** wire one real module to sentinel and confirm on real pnpm layout that (1) the editor keeps live lint/type/format, (2) runner binaries + plugins resolve per module, (3) nx target inference survives a runner swap, (4) old-config and sentinel modules coexist during migration.
|
|
358
380
|
|