@hublo/sentinel 0.1.0-alpha.9 → 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 +32 -24
- package/dist/bin/sentinel.js +103 -13
- package/dist/{chunk-NQ7GNHFT.js → chunk-HKGCWPRT.js} +45 -25
- package/dist/index.d.ts +22 -9
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/dist/bin/sentinel.js.map +0 -1
- package/dist/chunk-NQ7GNHFT.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,15 +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
|
-
> **Shipped today:** only the **TypeScript** tool, so `--
|
|
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
47
|
|
|
48
|
-
**Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed):
|
|
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:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
sentinel --init --typescript --flavour <react|nest|node>
|
|
52
|
+
pnpm install # fetch what --init declared, then commit
|
|
53
53
|
```
|
|
54
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
|
+
|
|
55
57
|
Those files are tiny, they just point at a sentinel preset. What gets committed:
|
|
56
58
|
|
|
57
59
|
```js
|
|
@@ -95,7 +97,7 @@ And the app's `package.json` scripts route every check through the one CLI (run
|
|
|
95
97
|
**Step 3 — evolve the toolchain, in one central place:**
|
|
96
98
|
|
|
97
99
|
- a **rule change** → bump the `@hublo/sentinel` version; the stubs already point at it, so there is **nothing to regenerate**;
|
|
98
|
-
- 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).
|
|
99
101
|
|
|
100
102
|
**Step 4 — stay converged:** a drift guard in CI flags any module whose config quietly diverged from the shared source.
|
|
101
103
|
|
|
@@ -103,6 +105,8 @@ The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …)
|
|
|
103
105
|
|
|
104
106
|
## Requirements & installing
|
|
105
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
|
+
|
|
106
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.
|
|
107
111
|
|
|
108
112
|
**Try it without installing.** A one-off run needs no auth and touches nothing:
|
|
@@ -113,6 +117,11 @@ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
|
|
|
113
117
|
|
|
114
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.
|
|
115
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
|
+
|
|
116
125
|
## Architecture: `target → runner → flavour`
|
|
117
126
|
|
|
118
127
|
Every check is described by three layers:
|
|
@@ -126,7 +135,7 @@ Every check is described by three layers:
|
|
|
126
135
|
A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
|
|
127
136
|
|
|
128
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).
|
|
129
|
-
- 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.
|
|
130
139
|
|
|
131
140
|
### Adapters & the engine (ports & adapters)
|
|
132
141
|
|
|
@@ -139,12 +148,12 @@ A run is `target × runner × flavour`, e.g. `sentinel --run --lint --runner=esl
|
|
|
139
148
|
`sentinel` does not reimplement tools. The contract:
|
|
140
149
|
|
|
141
150
|
- **`appliesTo(flavour)`** — which flavours this adapter handles (resolution filters on it, so a React-only adapter is never picked for Nest)
|
|
142
|
-
- **`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.
|
|
143
152
|
- **`run(ctx)`** — invoke the tool's bin against the project (used by `--run`)
|
|
144
153
|
- **`inspect(flavour)`** — the adapter's resolved base config (used by `--inspect`)
|
|
145
154
|
- **`report(ctx)`** — metrics (used by `--report`)
|
|
146
155
|
|
|
147
|
-
**`--
|
|
156
|
+
**`--init` is a declarative plan, not file-writing inside the adapter.** The adapter describes intent as operations; the engine executes them:
|
|
148
157
|
|
|
149
158
|
- `write { path, contents }` — a file the adapter fully owns (the thin stub)
|
|
150
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)
|
|
@@ -164,7 +173,7 @@ flowchart LR
|
|
|
164
173
|
D --> R["registry.resolve<br/>(target, flavour, runner)"]
|
|
165
174
|
R --> A["adapter<br/>eslint / tsc / vitest / ..."]
|
|
166
175
|
A -->|"--run"| Run["tool binary on the project"]
|
|
167
|
-
A -->|"--
|
|
176
|
+
A -->|"--init"| Upd["declarative plan → engine writes"]
|
|
168
177
|
A -->|"--inspect"| Ins["resolved config"]
|
|
169
178
|
A -->|"--report"| Rep["metrics"]
|
|
170
179
|
```
|
|
@@ -174,7 +183,7 @@ flowchart LR
|
|
|
174
183
|
```mermaid
|
|
175
184
|
flowchart TB
|
|
176
185
|
S["@hublo/sentinel<br/>rules = one source of truth"]
|
|
177
|
-
S -->|"--
|
|
186
|
+
S -->|"--init generates"| Stub["thin stub per module<br/>(extends sentinel)"]
|
|
178
187
|
Stub --> IDE["editor: live lint / type / format"]
|
|
179
188
|
Stub --> NX["nx: target inference"]
|
|
180
189
|
S -->|"--run injects config"| CI["CLI / CI: run tool on target"]
|
|
@@ -186,10 +195,10 @@ flowchart TB
|
|
|
186
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:
|
|
187
196
|
|
|
188
197
|
- **Rules in sentinel** — one source of truth, versioned.
|
|
189
|
-
- **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.
|
|
190
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.
|
|
191
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.
|
|
192
|
-
- **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.
|
|
193
202
|
|
|
194
203
|
## Composition & precedence
|
|
195
204
|
|
|
@@ -259,7 +268,7 @@ VERBS --run execute the target's tool
|
|
|
259
268
|
--inspect show the resolved configuration (incl. deferred rules)
|
|
260
269
|
--report metrics and health
|
|
261
270
|
--status adoption + conformity (coverage + drift), read from configs
|
|
262
|
-
--
|
|
271
|
+
--init generate/apply the config stubs (writes; one module only)
|
|
263
272
|
|
|
264
273
|
TYPES --lint --format --typescript --build --test
|
|
265
274
|
--static-analysis --runtime-analysis --arch
|
|
@@ -273,7 +282,7 @@ OPTIONS --module <name> from the root: scope to one module
|
|
|
273
282
|
--runner <tool> override the default runner
|
|
274
283
|
--ci from the root: affected only; non-zero exit on failure
|
|
275
284
|
--fix auto-fix where applicable
|
|
276
|
-
--dry-run preview a --
|
|
285
|
+
--dry-run preview a --init without writing
|
|
277
286
|
--json machine-readable output (report / inspect / status / --dry-run)
|
|
278
287
|
|
|
279
288
|
EXAMPLES sentinel --run --typescript # in a module → that module
|
|
@@ -281,11 +290,11 @@ EXAMPLES sentinel --run --typescript # in a module →
|
|
|
281
290
|
sentinel --report # from root → all types, all modules
|
|
282
291
|
sentinel --status --typescript # from root → adoption coverage
|
|
283
292
|
sentinel --status --ci # from root → fail CI on drift
|
|
284
|
-
sentinel --
|
|
293
|
+
sentinel --init --typescript --flavour react # write stubs for the current module
|
|
285
294
|
```
|
|
286
295
|
|
|
287
296
|
`--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
|
|
288
|
-
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
|
|
289
298
|
one module only (adopting every module at once is refused, adopt gradually).
|
|
290
299
|
|
|
291
300
|
**`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
|
|
@@ -315,7 +324,7 @@ src/
|
|
|
315
324
|
settings.ts # tunables (workspace-root marker, ...)
|
|
316
325
|
registry.ts # register + flavour-aware resolve
|
|
317
326
|
dispatch.ts # verb → adapter method
|
|
318
|
-
apply-plan.ts # the engine's filesystem port (applies --
|
|
327
|
+
apply-plan.ts # the engine's filesystem port (applies --init operations)
|
|
319
328
|
roles/<config-role>/ # lint, format, typescript, build, test
|
|
320
329
|
adapters/<runner>/ # one adapter per tool (implements the contract)
|
|
321
330
|
flavours/<stack>/ # config presets per stack (react, nest, svelte, ...)
|
|
@@ -355,8 +364,8 @@ nx is a **task runner**: it just runs the target's script. So `project.json` bar
|
|
|
355
364
|
|
|
356
365
|
This scaffold is the foundation; each tool is added one at a time on top of it:
|
|
357
366
|
|
|
358
|
-
1. **Foundation** — repo, exports, CLI skeleton, adapter contract,
|
|
359
|
-
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`**
|
|
360
369
|
3. **Lint** (`--lint`) — benchmark `eslint` vs `biome` vs `oxlint`.
|
|
361
370
|
4. **Build** (`--build`) — `vite`.
|
|
362
371
|
5. **Test** (`--test`) — `vitest`, plus a11y / w3c setups.
|
|
@@ -366,7 +375,6 @@ This scaffold is the foundation; each tool is added one at a time on top of it:
|
|
|
366
375
|
|
|
367
376
|
## Open decisions & risks being validated
|
|
368
377
|
|
|
369
|
-
- **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.
|
|
370
378
|
- **Performance:** prefer adopting Rust-native tools (`oxlint`/`biome`, `tsgo`) over hand-written Rust+WASM; reserve custom WASM for a _measured_ hot path only.
|
|
371
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.
|
|
372
380
|
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
dispatch,
|
|
6
6
|
palette,
|
|
7
7
|
readNxProjectName,
|
|
8
|
+
readOwnPackage,
|
|
8
9
|
readOwnVersion,
|
|
9
10
|
readProjectPackageJson,
|
|
10
11
|
registerAdapters,
|
|
11
12
|
resolve,
|
|
12
13
|
resolveBin
|
|
13
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-HKGCWPRT.js";
|
|
14
15
|
|
|
15
16
|
// bin/sentinel.ts
|
|
16
17
|
import { program } from "commander";
|
|
@@ -106,7 +107,7 @@ function resolveContext(cwd2, opts2) {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// src/core/domain.ts
|
|
109
|
-
var VERBS = ["run", "inspect", "
|
|
110
|
+
var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
|
|
110
111
|
var TARGETS = [
|
|
111
112
|
"lint",
|
|
112
113
|
"format",
|
|
@@ -331,6 +332,73 @@ function renderStatusSummary(items, p) {
|
|
|
331
332
|
return ` ${p.strong("coverage:")} ${c.adopted}/${c.total} adopted ${p.dim("\xB7")} ${c.conformant}/${c.adopted} conformant${drift}`;
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
// src/core/workspace-prep.ts
|
|
336
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
337
|
+
import { dirname, join as join3 } from "path";
|
|
338
|
+
var OVERRIDE_KEY = "i18next>typescript";
|
|
339
|
+
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
340
|
+
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
341
|
+
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
342
|
+
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
343
|
+
function findWorkspaceRoot(startDir) {
|
|
344
|
+
let dir = startDir;
|
|
345
|
+
for (; ; ) {
|
|
346
|
+
if (existsSync2(join3(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
347
|
+
const parent = dirname(dir);
|
|
348
|
+
if (parent === dir) return void 0;
|
|
349
|
+
dir = parent;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function declaredNativeTs(pkg) {
|
|
353
|
+
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
354
|
+
if (!spec) return void 0;
|
|
355
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
356
|
+
return version || void 0;
|
|
357
|
+
}
|
|
358
|
+
function ensureI18nextSingleton(root, dryRun) {
|
|
359
|
+
const pkgPath = join3(root, "package.json");
|
|
360
|
+
if (!existsSync2(pkgPath)) return void 0;
|
|
361
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
|
|
362
|
+
const want = declaredNativeTs(pkg);
|
|
363
|
+
if (!want) return void 0;
|
|
364
|
+
const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
365
|
+
if (have === want) return void 0;
|
|
366
|
+
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
|
|
367
|
+
pkg.pnpm ??= {};
|
|
368
|
+
pkg.pnpm.overrides ??= {};
|
|
369
|
+
pkg.pnpm.overrides[OVERRIDE_KEY] = want;
|
|
370
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
371
|
+
return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
|
|
372
|
+
}
|
|
373
|
+
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
374
|
+
const yamlPath = join3(root, WORKSPACE_YAML);
|
|
375
|
+
if (!existsSync2(yamlPath)) return void 0;
|
|
376
|
+
const own = readOwnPackage().name;
|
|
377
|
+
const lines = readFileSync2(yamlPath, "utf8").split("\n");
|
|
378
|
+
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
379
|
+
if (keyIdx === -1) return void 0;
|
|
380
|
+
let lastItemIdx = keyIdx;
|
|
381
|
+
let indent = " ";
|
|
382
|
+
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
383
|
+
const match = lines[i]?.match(LIST_ITEM);
|
|
384
|
+
if (!match) break;
|
|
385
|
+
indent = match[1] ?? indent;
|
|
386
|
+
lastItemIdx = i;
|
|
387
|
+
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
388
|
+
}
|
|
389
|
+
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
390
|
+
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
391
|
+
writeFileSync(yamlPath, lines.join("\n"));
|
|
392
|
+
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
393
|
+
}
|
|
394
|
+
function ensureWorkspacePrep(opts2) {
|
|
395
|
+
const dryRun = Boolean(opts2.dryRun);
|
|
396
|
+
return [
|
|
397
|
+
ensureI18nextSingleton(opts2.root, dryRun),
|
|
398
|
+
ensureReleaseAgeAllowList(opts2.root, dryRun)
|
|
399
|
+
].filter((message) => message !== void 0);
|
|
400
|
+
}
|
|
401
|
+
|
|
334
402
|
// src/shared/node-version.ts
|
|
335
403
|
var MIN_NODE = "20.12.0";
|
|
336
404
|
function parts(version) {
|
|
@@ -360,16 +428,16 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
360
428
|
"before",
|
|
361
429
|
[
|
|
362
430
|
"A check composes: verb + type + location.",
|
|
363
|
-
" verb what to do: --run --inspect --report --status --
|
|
431
|
+
" verb what to do: --run --inspect --report --status --init (--migrate: planned)",
|
|
364
432
|
" type which check: --lint --typescript ... (omit = all types; or --all)",
|
|
365
433
|
" where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
|
|
366
|
-
" --ci (affected), or all modules. --
|
|
434
|
+
" --ci (affected), or all modules. --init targets one module only.",
|
|
367
435
|
""
|
|
368
436
|
].join("\n")
|
|
369
|
-
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--
|
|
437
|
+
).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(
|
|
370
438
|
"--module <name>",
|
|
371
439
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
372
|
-
).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 (--
|
|
440
|
+
).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(
|
|
373
441
|
"--max-diagnostics <n>",
|
|
374
442
|
"cap the diagnostics embedded per module in --report (0 = no cap)",
|
|
375
443
|
"100"
|
|
@@ -382,7 +450,7 @@ program.name("sentinel").description("One CLI that guards code health: presets,
|
|
|
382
450
|
" sentinel --report --typescript --module bff-admin # from root \u2192 one module",
|
|
383
451
|
" sentinel --report # from root \u2192 all types, all modules",
|
|
384
452
|
" sentinel --report --ci # from root \u2192 affected only",
|
|
385
|
-
" sentinel --
|
|
453
|
+
" sentinel --init --typescript --flavour react # set up the current module + workspace"
|
|
386
454
|
].join("\n")
|
|
387
455
|
).addHelpText("after", () => {
|
|
388
456
|
const available = availableTargets();
|
|
@@ -436,8 +504,8 @@ if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
|
|
|
436
504
|
`--max-diagnostics must be a non-negative integer (0 = no cap); got ${JSON.stringify(opts.maxDiagnostics)}.`
|
|
437
505
|
);
|
|
438
506
|
}
|
|
439
|
-
if (opts.dryRun && verb !== "
|
|
440
|
-
program.error("--dry-run only applies to --
|
|
507
|
+
if (opts.dryRun && verb !== "init") {
|
|
508
|
+
program.error("--dry-run only applies to --init (the read verbs never write).");
|
|
441
509
|
}
|
|
442
510
|
async function runVerb() {
|
|
443
511
|
const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
|
|
@@ -491,12 +559,12 @@ async function runVerb() {
|
|
|
491
559
|
}
|
|
492
560
|
return verb === "run" || opts.ci ? worstCode : 0;
|
|
493
561
|
}
|
|
494
|
-
async function
|
|
562
|
+
async function runInit() {
|
|
495
563
|
const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false });
|
|
496
564
|
const [module] = modules;
|
|
497
565
|
if (scope === "all" || scope === "affected" || !module) {
|
|
498
566
|
program.error(
|
|
499
|
-
"--
|
|
567
|
+
"--init writes files: target one module (run from its directory, or pass --module). Adopting every module at once is intentionally not allowed \u2014 adopt gradually."
|
|
500
568
|
);
|
|
501
569
|
}
|
|
502
570
|
const available = availableTargets();
|
|
@@ -529,11 +597,33 @@ sentinel (${type}): ${asMessage(err)}
|
|
|
529
597
|
worst = Math.max(worst, 1);
|
|
530
598
|
}
|
|
531
599
|
}
|
|
600
|
+
if (worst === 0) {
|
|
601
|
+
const root = findWorkspaceRoot(module.root);
|
|
602
|
+
if (root) {
|
|
603
|
+
const changes = ensureWorkspacePrep({ root, dryRun: Boolean(opts.dryRun) });
|
|
604
|
+
const prefix = opts.dryRun ? " dry run: " : " ";
|
|
605
|
+
for (const change of changes) process.stderr.write(`${prefix}${change}
|
|
606
|
+
`);
|
|
607
|
+
if (changes.length > 0 && !opts.dryRun) {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
palette(process.stderr).dim(" run `pnpm install` to apply the workspace changes\n")
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
532
614
|
return worst;
|
|
533
615
|
}
|
|
534
|
-
|
|
616
|
+
async function main() {
|
|
617
|
+
if (verb === "migrate") {
|
|
618
|
+
program.error("--migrate is planned and not available yet; use --init to set a module up.");
|
|
619
|
+
}
|
|
620
|
+
const runSelectedVerb = verb === "init" ? runInit : runVerb;
|
|
621
|
+
const exitCode = await runSelectedVerb();
|
|
622
|
+
process.exit(exitCode);
|
|
623
|
+
}
|
|
624
|
+
main().catch((error) => {
|
|
535
625
|
process.stderr.write(`
|
|
536
|
-
sentinel: ${asMessage(
|
|
626
|
+
sentinel: ${asMessage(error)}
|
|
537
627
|
`);
|
|
538
628
|
process.exit(1);
|
|
539
629
|
});
|
|
@@ -80,22 +80,27 @@ function palette(stream) {
|
|
|
80
80
|
import { existsSync, readFileSync } from "fs";
|
|
81
81
|
import { dirname, join } from "path";
|
|
82
82
|
import { fileURLToPath } from "url";
|
|
83
|
-
function
|
|
83
|
+
function readOwnPackage() {
|
|
84
84
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
85
85
|
for (; ; ) {
|
|
86
86
|
const pkgPath = join(dir, "package.json");
|
|
87
87
|
if (existsSync(pkgPath)) {
|
|
88
88
|
try {
|
|
89
89
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
90
|
-
if (typeof pkg.version === "string")
|
|
90
|
+
if (typeof pkg.version === "string") {
|
|
91
|
+
return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version };
|
|
92
|
+
}
|
|
91
93
|
} catch {
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
const parent = dirname(dir);
|
|
95
|
-
if (parent === dir) return "0.0.0";
|
|
97
|
+
if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
|
|
96
98
|
dir = parent;
|
|
97
99
|
}
|
|
98
100
|
}
|
|
101
|
+
function readOwnVersion() {
|
|
102
|
+
return readOwnPackage().version;
|
|
103
|
+
}
|
|
99
104
|
function readProjectPackageJson(dir) {
|
|
100
105
|
const path = join(dir, "package.json");
|
|
101
106
|
if (!existsSync(path)) return {};
|
|
@@ -252,7 +257,7 @@ function readTsconfigAdoption(cwd) {
|
|
|
252
257
|
|
|
253
258
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
254
259
|
var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
|
|
255
|
-
var INSTALL_NOTE = "install
|
|
260
|
+
var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
|
|
256
261
|
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
257
262
|
var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
|
|
258
263
|
function parseDiagnostics(output) {
|
|
@@ -282,7 +287,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
282
287
|
/**
|
|
283
288
|
* The tsc adapter drives type-checking for any flavour: `--run`/`--report`/
|
|
284
289
|
* `--inspect` just execute tsc against the module's existing config, which is
|
|
285
|
-
* meaningful regardless of flavour. `--
|
|
290
|
+
* meaningful regardless of flavour. `--init` is the exception, it only WRITES a
|
|
286
291
|
* preset for flavours that ship one (gated inside `plan`), so svelte is not
|
|
287
292
|
* clobbered with a non-existent preset.
|
|
288
293
|
*/
|
|
@@ -290,10 +295,10 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
290
295
|
return true;
|
|
291
296
|
}
|
|
292
297
|
/**
|
|
293
|
-
* Plan `--
|
|
294
|
-
* conformant stub,
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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.
|
|
297
302
|
*
|
|
298
303
|
* Per resolved case:
|
|
299
304
|
* - extends-base: append the preset to the `extends` chain (keep the base for
|
|
@@ -306,16 +311,16 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
306
311
|
if (!hasShippedPreset(context.flavour)) {
|
|
307
312
|
return {
|
|
308
313
|
operations: [],
|
|
309
|
-
|
|
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.`
|
|
310
315
|
};
|
|
311
316
|
}
|
|
312
317
|
const target = resolveTsconfigTarget(context.cwd);
|
|
313
318
|
const preset = `@hublo/sentinel/tsconfig/${context.flavour}`;
|
|
314
|
-
const addScript = this.
|
|
319
|
+
const addScript = this.packageJsonOperation(context.cwd);
|
|
315
320
|
if (target.reason === "other-chain") {
|
|
316
321
|
return {
|
|
317
322
|
operations: [],
|
|
318
|
-
|
|
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.`
|
|
319
324
|
};
|
|
320
325
|
}
|
|
321
326
|
if (target.reason === "none") {
|
|
@@ -336,10 +341,12 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
336
341
|
];
|
|
337
342
|
const notes = [];
|
|
338
343
|
if (drift.length > 0) {
|
|
344
|
+
const localKeys = Object.keys(existing.compilerOptions ?? {});
|
|
345
|
+
const stripsAll = localKeys.length > 0 && localKeys.every((key) => drift.includes(key));
|
|
339
346
|
operations.push({
|
|
340
347
|
kind: "remove-json-keys",
|
|
341
348
|
path: target.path,
|
|
342
|
-
keys: drift.map((key) => ["compilerOptions", key])
|
|
349
|
+
keys: stripsAll ? [["compilerOptions"]] : drift.map((key) => ["compilerOptions", key])
|
|
343
350
|
});
|
|
344
351
|
notes.push(`stripped preset-owned compilerOptions: ${drift.join(", ")}`);
|
|
345
352
|
}
|
|
@@ -352,18 +359,25 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
352
359
|
* `package.json`, merge the script in and leave the rest untouched. If it does
|
|
353
360
|
* NOT (common for nx apps/services that carry only a `project.json`), scaffold a
|
|
354
361
|
* minimal, workspace-valid one, its nx name + `private: true`, so pnpm accepts it
|
|
355
|
-
* and it can
|
|
356
|
-
*
|
|
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.
|
|
357
365
|
*/
|
|
358
|
-
|
|
366
|
+
packageJsonOperation(cwd) {
|
|
367
|
+
const own = readOwnPackage();
|
|
368
|
+
const devDependencies = { [own.name]: own.version };
|
|
359
369
|
if (existsSync5(join5(cwd, "package.json"))) {
|
|
360
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
kind: "merge-json",
|
|
372
|
+
path: "package.json",
|
|
373
|
+
value: { scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
374
|
+
};
|
|
361
375
|
}
|
|
362
376
|
const name = readNxProjectName(cwd) ?? basename(cwd);
|
|
363
377
|
return {
|
|
364
378
|
kind: "merge-json",
|
|
365
379
|
path: "package.json",
|
|
366
|
-
value: { name, private: true, scripts: TYPECHECK_SCRIPT }
|
|
380
|
+
value: { name, private: true, scripts: TYPECHECK_SCRIPT, devDependencies }
|
|
367
381
|
};
|
|
368
382
|
}
|
|
369
383
|
/**
|
|
@@ -670,9 +684,9 @@ function writeFileAtomic(absolutePath, contents) {
|
|
|
670
684
|
renameSync(tempPath, absolutePath);
|
|
671
685
|
}
|
|
672
686
|
function applyPlan(cwd, plan) {
|
|
673
|
-
const
|
|
674
|
-
for (const file of
|
|
675
|
-
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);
|
|
676
690
|
}
|
|
677
691
|
|
|
678
692
|
// src/core/dispatch.ts
|
|
@@ -728,13 +742,18 @@ function previewPlan(opts, plan) {
|
|
|
728
742
|
return 0;
|
|
729
743
|
}
|
|
730
744
|
async function dispatch(opts) {
|
|
731
|
-
if (opts.verb !== "
|
|
732
|
-
throw new Error(`dispatch handles --
|
|
745
|
+
if (opts.verb !== "init") {
|
|
746
|
+
throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
|
|
733
747
|
}
|
|
734
|
-
const adapter = resolve(opts.target, opts.flavour, opts.runner);
|
|
735
748
|
const flavour = resolveFlavour(opts);
|
|
749
|
+
const adapter = resolve(opts.target, flavour, opts.runner);
|
|
736
750
|
const context = { cwd: opts.cwd, flavour };
|
|
737
751
|
const plan = await adapter.plan(context);
|
|
752
|
+
if (plan.blocked) {
|
|
753
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan.blocked}
|
|
754
|
+
`);
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
738
757
|
if (opts.dryRun) {
|
|
739
758
|
return previewPlan(opts, plan);
|
|
740
759
|
}
|
|
@@ -754,6 +773,7 @@ export {
|
|
|
754
773
|
resolve,
|
|
755
774
|
BaseAdapter,
|
|
756
775
|
palette,
|
|
776
|
+
readOwnPackage,
|
|
757
777
|
readOwnVersion,
|
|
758
778
|
readProjectPackageJson,
|
|
759
779
|
readNxProjectName,
|
|
@@ -763,4 +783,4 @@ export {
|
|
|
763
783
|
detectFramework,
|
|
764
784
|
dispatch
|
|
765
785
|
};
|
|
766
|
-
//# 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
|
|
@@ -63,7 +68,7 @@ interface RunContext {
|
|
|
63
68
|
maxDiagnostics?: number;
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
66
|
-
* A single, DECLARATIVE file operation an `--
|
|
71
|
+
* A single, DECLARATIVE file operation an `--init` wants to happen. Adapters
|
|
67
72
|
* describe intent in tool terms; they never touch the filesystem. The engine
|
|
68
73
|
* (which owns IO and the repo layout) executes these against the project root.
|
|
69
74
|
*
|
|
@@ -108,13 +113,21 @@ type FileOperation =
|
|
|
108
113
|
keys: string[][];
|
|
109
114
|
};
|
|
110
115
|
/**
|
|
111
|
-
* The result of planning an `--
|
|
116
|
+
* The result of planning an `--init`: an ordered list of declarative operations,
|
|
112
117
|
* plus optional human notes (what was created/stripped/skipped) surfaced by the
|
|
113
118
|
* CLI. Multi-operation on purpose; the drift guard and golden tests diff the plan.
|
|
114
119
|
*/
|
|
115
120
|
interface UpdatePlan {
|
|
116
121
|
operations: FileOperation[];
|
|
117
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;
|
|
118
131
|
}
|
|
119
132
|
/** Outcome of `run` / `report`. */
|
|
120
133
|
interface AdapterResult {
|
|
@@ -143,7 +156,7 @@ interface StatusReport {
|
|
|
143
156
|
/**
|
|
144
157
|
* One tool implementation for one target (and, optionally, a subset of flavours,
|
|
145
158
|
* see `appliesTo`). The CLI verbs map onto these: `--run` -> run, `--inspect` ->
|
|
146
|
-
* inspect, `--
|
|
159
|
+
* inspect, `--init` -> plan(+write), `--report` -> report, `--status` -> status.
|
|
147
160
|
*/
|
|
148
161
|
interface Adapter {
|
|
149
162
|
/** The target this adapter serves (e.g. 'lint'). */
|
|
@@ -157,7 +170,7 @@ interface Adapter {
|
|
|
157
170
|
*/
|
|
158
171
|
appliesTo(flavour: Flavour): boolean;
|
|
159
172
|
/**
|
|
160
|
-
* Plan what `--
|
|
173
|
+
* Plan what `--init` should write, as declarative operations (see
|
|
161
174
|
* `FileOperation`). Given a normalized `UpdateContext`, the adapter may READ the
|
|
162
175
|
* module (through bounded resolvers) to decide the ops, but never writes, the
|
|
163
176
|
* engine applies them and owns all IO. The drift guard diffs against this plan.
|
|
@@ -205,7 +218,7 @@ declare abstract class BaseAdapter implements Adapter {
|
|
|
205
218
|
* dependency is a plain TypeScript library (`node`). This is not the old silent
|
|
206
219
|
* guessing (there is no "assume react" fallback); `node` is a real preset.
|
|
207
220
|
*
|
|
208
|
-
* Reusable across tools: every tool's `--
|
|
221
|
+
* Reusable across tools: every tool's `--init` needs the module's flavour to
|
|
209
222
|
* pick its preset, so this lives in core, not in the TypeScript role.
|
|
210
223
|
*/
|
|
211
224
|
|
|
@@ -218,7 +231,7 @@ interface PackageDependencies {
|
|
|
218
231
|
declare function detectFramework(packageJson: PackageDependencies): Flavour;
|
|
219
232
|
|
|
220
233
|
interface DispatchOptions {
|
|
221
|
-
/** Always `
|
|
234
|
+
/** Always `init` (dispatch is the write path); other verbs route through `analyse`. */
|
|
222
235
|
verb: Verb;
|
|
223
236
|
target: Target;
|
|
224
237
|
runner?: string;
|
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",
|
|
@@ -31,7 +31,8 @@
|
|
|
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",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"test": "vitest run",
|
|
66
67
|
"test:watch": "vitest",
|
|
67
68
|
"test:e2e": "vitest run tests/e2e",
|
|
68
|
-
"cli": "node --import tsx bin/sentinel.ts"
|
|
69
|
+
"cli": "node --import tsx bin/sentinel.ts",
|
|
70
|
+
"docs:traces": "pnpm build && bash scripts/gen-traces.sh"
|
|
69
71
|
}
|
|
70
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","../../src/shared/node-version.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 { availableTargets } from '../src/core/registry.js'\nimport {\n renderStatusRow,\n renderStatusSummary,\n renderSummary,\n statusCoverage,\n} from '../src/core/render.js'\nimport { palette } from '../src/shared/color.js'\nimport { checkNodeVersion } from '../src/shared/node-version.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\n// Fail fast with a clear message on an unsupported Node (sentinel's coloured output needs\n// `styleText`'s stream option, Node 22.13+), instead of a cryptic crash deep in the CLI.\nconst nodeCheck = checkNodeVersion(process.versions.node)\nif (!nodeCheck.ok) {\n process.stderr.write(`${nodeCheck.message}\\n`)\n process.exit(1)\n}\n\n// Register every tool adapter up front, BEFORE parsing, so `--help` can reflect what is\n// actually wired (availableTargets) rather than a hardcoded list that pretends all targets\n// are ready. Registration has no dependency on the parsed options.\nregisterAdapters()\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 .option(\n '--max-diagnostics <n>',\n 'cap the diagnostics embedded per module in --report (0 = no cap)',\n '100',\n )\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 // Availability, derived from the registry so it never lies: only wired targets are\n // \"available now\"; the rest are honestly marked as planned.\n .addHelpText('after', () => {\n const available = availableTargets()\n const planned = TARGETS.filter((t) => !available.includes(t))\n return [\n '',\n `Available now: ${available.length ? available.map((t) => `--${t}`).join(', ') : '(none yet)'}`,\n planned.length\n ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(', ')}`\n : '',\n ]\n .filter(Boolean)\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// Whether the user named the target(s) themselves. When true, a target with no adapter is\n// a usage error (they asked for something sentinel cannot do). When false (`--all` / none),\n// an unsupported target is just an expected skip, shown but not failed.\nconst targetsExplicit = !(opts.all || namedTargets.length === 0)\n\n// --max-diagnostics: how many structured diagnostics --report embeds per module (0 = all).\n// `Number` (not parseInt) so a fractional value like `3.9` is rejected, not silently floored.\nconst maxDiagnostics = Number(opts.maxDiagnostics)\nif (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {\n program.error(\n `--max-diagnostics must be a non-negative integer (0 = no cap); got ${JSON.stringify(opts.maxDiagnostics)}.`,\n )\n}\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 // `--run` streams the tool's own output to stdout, so a JSON envelope would be mixed in;\n // `--json` is only honoured for report/inspect. Say so instead of silently ignoring it.\n if (opts.json && verb === 'run') {\n process.stderr.write(\n err.warn(\n 'note: --json is ignored for --run (it streams the tool output); use --report --json for a machine envelope.',\n ) + '\\n',\n )\n }\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 targetsExplicit,\n maxDiagnostics,\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, targets)\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\n // An explicitly requested target with no adapter is a usage error: say so clearly and\n // exit non-zero, so a human or agent never mistakes \"nothing ran\" for a success.\n if (targetsExplicit && summary.skipped.length > 0) {\n const avail = availableTargets()\n process.stderr.write(\n err.fail(\n `sentinel: target(s) not available yet: ${summary.skipped.map((t) => `--${t}`).join(', ')}. ` +\n `Available now: ${avail.length ? avail.map((t) => `--${t}`).join(', ') : '(none yet)'}.`,\n ) + '\\n',\n )\n return 1\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 // Same policy as the read verbs: a target the user NAMED but that has no adapter is a\n // usage error; targets swept in by --all / no target are just skipped, never flooded as\n // \"No adapter…\" errors that would fail an otherwise-successful update.\n const available = availableTargets()\n if (targetsExplicit) {\n const unwired = targets.filter((type) => !available.includes(type))\n if (unwired.length > 0) {\n program.error(\n `target(s) not available yet: ${unwired.map((t) => `--${t}`).join(', ')}. ` +\n `Available now: ${available.map((t) => `--${t}`).join(', ') || '(none yet)'}.`,\n )\n }\n }\n const toRun = targets.filter((type) => available.includes(type))\n\n let worst = 0\n for (const type of toRun) {\n try {\n const code = await dispatch({\n verb,\n target: type,\n runner: opts.runner,\n cwd: module.root,\n flavour,\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // A genuine adapter failure (not an unwired target, those are filtered above).\n process.stderr.write(`\\nsentinel (${type}): ${asMessage(err)}\\n`)\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\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/**\n * The outcome of one module × target check. `unsupported` is a distinct third state\n * (a requested target has no adapter yet): it must never read as a pass, and an\n * explicitly-requested unsupported target is a usage error (non-zero exit), while an\n * unsupported target swept in by `--all` is just skipped and shown.\n */\nexport type ResultStatus = 'ok' | 'failed' | 'unsupported'\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 { describeFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, ResultStatus, Target } from './domain.js'\nimport { availableTargets, 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 /**\n * The three-state outcome. `ok` stays for back-compat (`status === 'ok'`), but\n * `status` is the source of truth: `unsupported` (no adapter yet) must never read\n * as a pass.\n */\n status: ResultStatus\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 /**\n * Whether the targets were named by the user (vs swept in by `--all` / no target). When\n * true, a target with no adapter yields a visible per-module `unsupported` row; when\n * false, it is skipped silently per module (the envelope's `skipped` still lists it once,\n * so `--all` does not flood the output with N×M unsupported rows).\n */\n targetsExplicit: boolean\n ci: boolean\n fix: boolean\n /** Cap on structured diagnostics per module in `--report` (0 = no cap). */\n maxDiagnostics?: number\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 and, when the\n // module looks mixed (e.g. a Sails backend that also pulls React), warn and point at\n // --flavour rather than choosing silently.\n let flavour: Flavour\n if (params.flavour) {\n flavour = params.flavour\n } else {\n const detection = describeFramework(readProjectPackageJson(module.root))\n flavour = detection.flavour\n if (detection.ambiguous) {\n const warn = palette(process.stderr)\n process.stderr.write(\n warn.warn(\n `sentinel: ${module.name}: flavour is ambiguous, detected \"${flavour}\" (from ${detection.source}), also found ${detection.conflicts.join(', ')}. Pass --flavour to be explicit.`,\n ) + '\\n',\n )\n }\n }\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n maxDiagnostics: params.maxDiagnostics,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch (error) {\n // No adapter for this target yet. When the user named it, emit a visible per-module\n // `unsupported` row so it never reads as a pass. When it was swept in by `--all`,\n // skip silently per module: the envelope's `skipped` still lists it once, so `--all`\n // does not flood the output with one unsupported row per module × unwired target.\n if (params.targetsExplicit) {\n const reason = error instanceof Error ? error.message : String(error)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: false,\n status: 'unsupported',\n data: { reason },\n })\n }\n continue\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({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n status: result.ok ? 'ok' : 'failed',\n data: {},\n })\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 status: result.ok ? 'ok' : 'failed',\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({\n project: module.name,\n target,\n flavour,\n ok: true,\n status: 'ok',\n data: config,\n })\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({\n project: module.name,\n target,\n flavour,\n ok,\n status: ok ? 'ok' : 'failed',\n data: status,\n })\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({\n project: module.name,\n target,\n flavour,\n ok: false,\n status: 'failed',\n data: { error: message },\n })\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, status, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, status, ...details }\n}\n\n/**\n * The aggregate, versioned envelope for MANY results, built from `generateSummary`.\n * `executed` / `skipped` split the REQUESTED targets by whether an adapter is wired, so an\n * agent sees what actually ran vs what was unsupported at the top level without scanning\n * every row (and independently of `--all` suppressing per-module unsupported rows).\n * schemaVersion is 2 since these fields are new to the shape.\n */\nexport function generateSummaries(\n results: readonly AnalyseResult[],\n requestedTargets: readonly Target[],\n): {\n schemaVersion: number\n executed: Target[]\n skipped: Target[]\n results: Record<string, unknown>[]\n} {\n const available = new Set(availableTargets())\n return {\n schemaVersion: 2,\n executed: requestedTargets.filter((t) => available.has(t)),\n skipped: requestedTargets.filter((t) => !available.has(t)),\n results: results.map(generateSummary),\n }\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([\n 'project',\n 'module',\n 'target',\n 'flavour',\n 'ok',\n 'status',\n 'reason',\n // The structured diagnostics are for `--json` consumers; the human row stays the concise\n // `errors=N` line rather than dumping every diagnostic inline.\n 'diagnostics',\n 'diagnosticsTruncated',\n])\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 // implicitAny is `deferred` when the rule is off (phase 1): honest, not a fake 0.\n if (key === 'implicitAny' && value === 'deferred') return p.warn('deferred')\n return typeof value === 'string' ? value : JSON.stringify(value)\n}\n\n/**\n * `unsupported` is a third state (a requested target with no adapter yet): a distinct dim\n * marker, never a ✓ (would read as a pass) nor a ✗ (would read as a failure of the code).\n * Shared by every row renderer (summary AND status) so the state can never render as a\n * legitimate outcome on one path and not the other.\n */\nfunction renderUnsupportedRow(item: Record<string, unknown>, p: Palette): string {\n const reason = typeof item.reason === 'string' ? ` ${p.dim(`(${item.reason})`)}` : ''\n const head = ` ${p.dim('·')} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n return `${head} ${p.dim('—')} ${p.warn('unsupported')}${reason}`\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 if (item.status === 'unsupported') return renderUnsupportedRow(item, p)\n\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 // Only real status rows count toward coverage; an `unsupported` target (a requested\n // target with no adapter) must not inflate the denominator (`total`).\n const relevant = items.filter((i) => i.status !== 'unsupported')\n const adopted = relevant.filter((i) => i.adopted === true)\n const conformant = adopted.filter((i) => i.conformant === true)\n return {\n total: relevant.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 if (item.status === 'unsupported') return renderUnsupportedRow(item, p)\n\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","/**\n * Node version guard. sentinel colours output with `util.styleText`, added in Node 20.12, so\n * an older runtime (the tester ran serviceapp on Node 10) crashes cryptically. We check up\n * front and fail with a clear, actionable message instead. Kept as a pure function so it is\n * unit-testable without spawning Node. The `{ stream }` option we pass is newer (22.13) but\n * older 20.x ignore it gracefully, so 20.12 is the real floor (and matches CI on Node 20).\n */\n\n/** The minimum Node this CLI supports (the `util.styleText` floor). */\nexport const MIN_NODE = '20.12.0'\n\n/** Parse `24.15.0` (or `v24.15.0`) into `[major, minor, patch]`; missing parts are 0. */\nfunction parts(version: string): [number, number, number] {\n const [major = 0, minor = 0, patch = 0] = version\n .replace(/^v/, '')\n .split('.')\n .map((n) => Number.parseInt(n, 10) || 0)\n return [major, minor, patch]\n}\n\n/**\n * Whether `current` (e.g. `process.versions.node`) meets `min`. Compares major, then minor,\n * then patch. Returns `{ ok }` and, when not, a clear message telling the user their version,\n * the required one, and how to switch.\n */\nexport function checkNodeVersion(\n current: string,\n min: string = MIN_NODE,\n): { ok: boolean; message?: string } {\n const [cMajor, cMinor, cPatch] = parts(current)\n const [mMajor, mMinor, mPatch] = parts(min)\n const ok =\n cMajor > mMajor ||\n (cMajor === mMajor && cMinor > mMinor) ||\n (cMajor === mMajor && cMinor === mMinor && cPatch >= mPatch)\n if (ok) return { ok: true }\n return {\n ok: false,\n message:\n `sentinel requires Node >= ${min}, but you are on ${current}. ` +\n `Switch with fnm/nvm (e.g. \\`fnm use ${mMajor}\\`) and re-run.`,\n }\n}\n"],"mappings":"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","names":["join","cwd","cwd","join","opts","flavour"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/registry.ts","../src/core/base-adapter.ts","../src/shared/color.ts","../src/shared/package-json.ts","../src/roles/typescript/adapters/tsc/tsc.adapter.ts","../src/shared/jsonc.ts","../src/shared/resolve-bin.ts","../src/roles/typescript/config-policy.ts","../src/roles/typescript/phased-rules.ts","../src/roles/typescript/presets.ts","../src/roles/typescript/read-adoption.ts","../src/roles/typescript/resolve-tsconfig-target.ts","../src/roles/typescript/register.ts","../src/adapters.ts","../src/core/detect-framework.ts","../src/shared/text.ts","../src/core/apply-plan.ts","../src/shared/deep-merge.ts","../src/core/dispatch.ts"],"sourcesContent":["/**\n * Adapter registry. Tool branches register their adapters here; the CLI resolves\n * an adapter by (target, flavour, runner). A default runner per target keeps the\n * common invocation tool-agnostic (`sentinel --lint`), while `--runner` overrides\n * it. Resolution is flavour-aware: candidates are filtered by `appliesTo(flavour)`,\n * so a React-only adapter is never picked for a Nest project, and two adapters can\n * share a (target, runner) if they specialise different flavours.\n */\nimport type { Flavour, Target } from './domain.js'\nimport type { Adapter } from './types.js'\n\nconst adapters: Adapter[] = []\n\n/** Default runner per target, so `--runner` stays optional. */\nconst defaultRunner: Partial<Record<Target, string>> = {\n // Filled in by tool branches, e.g. lint: 'eslint', typescript: 'tsc'.\n}\n\n/**\n * Register a tool adapter (called from each role's registration, wired into the\n * bootstrap in `src/adapters.ts`). No (target, runner) uniqueness guard here: two\n * adapters may share one for different flavours; a genuine clash (same target,\n * runner AND flavour) is caught at resolve time, where the flavour is known.\n */\nexport function register(adapter: Adapter): void {\n adapters.push(adapter)\n}\n\n/** Set the default runner for a target. */\nexport function setDefaultRunner(target: Target, runner: string): void {\n defaultRunner[target] = runner\n}\n\n/** All registered adapters (for `--all`, listing, and reports). */\nexport function all(): readonly Adapter[] {\n return adapters\n}\n\n/**\n * The targets that actually have an adapter wired today (distinct, in registration\n * order). The source of truth for \"what can sentinel really do right now\" — used to\n * flag a requested-but-unwired target and to show availability in `--help`, instead\n * of a hardcoded list that pretends every target is ready.\n */\nexport function availableTargets(): Target[] {\n return [...new Set(adapters.map((a) => a.target))]\n}\n\n/**\n * Resolve one adapter by target, honouring an explicit `--runner` or the target's\n * default. When a `flavour` is known it also filters by `appliesTo`, so a\n * React-only adapter is never picked for Nest; when it is absent (sentinel does\n * not detect it), every adapter for the target is a candidate. Throws with an\n * actionable message for each failure mode: no adapter for the target (yet), none\n * that handles the flavour, an ambiguous choice, an unknown runner, or two\n * adapters claiming the same (target, runner, flavour).\n */\nexport function resolve(target: Target, flavour?: Flavour, runner?: string): Adapter {\n const forTarget = adapters.filter((a) => a.target === target)\n if (forTarget.length === 0) {\n throw new Error(\n `No adapter registered for target \"${target}\" yet (it ships in a later ticket).`,\n )\n }\n\n const candidates = flavour ? forTarget.filter((a) => a.appliesTo(flavour)) : forTarget\n if (candidates.length === 0) {\n throw new Error(`No adapter for target \"${target}\" handles flavour \"${flavour}\".`)\n }\n\n const wanted = runner ?? defaultRunner[target]\n const available = candidates.map((a) => a.runner).join(', ')\n\n // No runner asked for and no default: only unambiguous when there is exactly one.\n if (!wanted) {\n const [first, ...rest] = candidates\n if (first && rest.length === 0) return first\n throw new Error(\n `Multiple runners for target \"${target}\" (${available}); pass --runner or set a default.`,\n )\n }\n\n const matching = candidates.filter((a) => a.runner === wanted)\n if (matching.length === 0) {\n throw new Error(\n `No runner \"${wanted}\" for target \"${target}\" (flavour \"${flavour}\"). Available: ${available}.`,\n )\n }\n if (matching.length > 1) {\n throw new Error(\n `Ambiguous: ${matching.length} adapters claim target \"${target}\", runner \"${wanted}\", flavour \"${flavour}\".`,\n )\n }\n return matching[0] as Adapter\n}\n","/**\n * `BaseAdapter`: an optional convenience base class for adapters. This is the\n * runtime part of the contract (the types live in `types.ts`); it provides\n * default `inspect`/`report` that throw \"not implemented yet\", so an adapter can\n * extend it and implement only what it needs.\n */\nimport type { Flavour, Target } from './domain.js'\nimport type {\n Adapter,\n AdapterResult,\n RunContext,\n StatusReport,\n UpdateContext,\n UpdatePlan,\n} from './types.js'\n\nexport abstract class BaseAdapter implements Adapter {\n abstract readonly target: Target\n abstract readonly runner: string\n abstract appliesTo(flavour: Flavour): boolean\n abstract plan(context: UpdateContext): UpdatePlan | Promise<UpdatePlan>\n abstract run(ctx: RunContext): Promise<AdapterResult>\n\n inspect(_ctx: RunContext): Promise<unknown> {\n throw new Error(`${this.runner}: --inspect not implemented yet`)\n }\n\n report(_ctx: RunContext): Promise<AdapterResult> {\n throw new Error(`${this.runner}: --report not implemented yet`)\n }\n\n status(_ctx: RunContext): Promise<StatusReport> {\n throw new Error(`${this.runner}: --status not implemented yet`)\n }\n}\n","/**\n * Terminal color, on top of Node's built-in `util.styleText` (no dependency).\n *\n * Passing `{ stream }` lets styleText itself decide whether to emit escape codes: it\n * returns the text UNCHANGED when the stream is not a TTY, when `NO_COLOR` is set, or\n * when the terminal has no color support. So the same code stays readable on a terminal\n * and clean in pipes, files, and CI logs, we never hand-roll a TTY check.\n */\nimport { styleText } from 'node:util'\n\ntype Format = Parameters<typeof styleText>[0]\n\n/** The semantic styles, each a `string -> string` painter. */\nexport interface Palette {\n /** A passing result. */ ok: (text: string) => string\n /** A failing result. */ fail: (text: string) => string\n /** A caution / deferred / deprecation notice. */ warn: (text: string) => string\n /** Emphasis (a name, a heading). */ strong: (text: string) => string\n /** Secondary, low-salience text (keys, timings, scope). */ dim: (text: string) => string\n /** A structural marker (a run header). */ accent: (text: string) => string\n}\n\n/**\n * Bind the semantic styles to ONE stream: `palette(process.stdout)` for the result\n * summary, `palette(process.stderr)` for notices/progress, so each is colored only when\n * that specific stream supports it.\n */\nexport function palette(stream: NodeJS.WriteStream): Palette {\n const paint = (format: Format, text: string): string => styleText(format, text, { stream })\n return {\n ok: (text) => paint(['green', 'bold'], text),\n fail: (text) => paint(['red', 'bold'], text),\n warn: (text) => paint('yellow', text),\n strong: (text) => paint('bold', text),\n dim: (text) => paint('dim', text),\n accent: (text) => paint('cyan', text),\n }\n}\n\n/**\n * An identity palette (no escapes), for pure rendering in tests and for any caller that\n * wants the plain text regardless of the stream.\n */\nexport const plainPalette: Palette = {\n ok: (text) => text,\n fail: (text) => text,\n warn: (text) => text,\n strong: (text) => text,\n dim: (text) => text,\n accent: (text) => text,\n}\n","/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\nexport function readOwnVersion(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return '0.0.0' // reached the filesystem root\n dir = parent\n }\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--update` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n","/**\n * TypeScript adapter (runner: `tsc`). Maps the verbs onto tsc for one module:\n * - plan (--update) → thin conformant tsconfig stub extending the preset\n * - run (--run) → `tsc -b` (build mode), the module's own tsc\n * - inspect (--inspect) → the module's resolved config (preset, target, flavour)\n * - report (--report) → type-error + implicit-any counts (the conformance signal)\n * - status (--status) → adoption + conformity, read from the committed config\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { BaseAdapter } from '../../../../core/base-adapter.js'\nimport type { Flavour, Target } from '../../../../core/domain.js'\nimport { palette } from '../../../../shared/color.js'\nimport type {\n AdapterResult,\n FileOperation,\n RunContext,\n StatusReport,\n UpdateContext,\n UpdatePlan,\n} from '../../../../core/types.js'\nimport { parseJsonc } from '../../../../shared/jsonc.js'\nimport { readNxProjectName } from '../../../../shared/package-json.js'\nimport { resolveBin } from '../../../../shared/resolve-bin.js'\nimport { presetOwnedKeys } from '../../config-policy.js'\nimport { DEFERRED_RULES, deferredRuleNames } from '../../phased-rules.js'\nimport { hasShippedPreset } from '../../presets.js'\nimport { readTsconfigAdoption } from '../../read-adoption.js'\nimport { resolveTsconfigTarget } from '../../resolve-tsconfig-target.js'\n\nconst TYPECHECK_SCRIPT = { typecheck: 'sentinel --run --typescript' }\n\n/**\n * Adoption also needs the package installed: the written `extends` and the `typecheck`\n * script both resolve `@hublo/sentinel`. sentinel does not write dependencies (that is a\n * `pnpm add`, not a file edit), so the plan SAYS so, otherwise applying the plan alone\n * leaves an unusable `sentinel` script (the tester's #4).\n */\nconst INSTALL_NOTE =\n 'install: add @hublo/sentinel as a devDependency (`pnpm add -D @hublo/sentinel`) so `extends` and the typecheck script resolve'\n\n/** Default cap on embedded diagnostics per module (the full count stays in `errors`). */\nconst DEFAULT_MAX_DIAGNOSTICS = 100\n\n/** One structured tsc diagnostic, parsed from `tsc -b` output. */\nexport interface TscDiagnostic {\n file: string\n line: number\n col: number\n /** The TS error code, e.g. `TS7006`. */\n code: string\n message: string\n}\n\n/** `path/to/file.ts(12,5): error TS7006: Parameter 'x' implicitly has an 'any' type.` */\nconst DIAGNOSTIC_RE = /^(.+?)\\((\\d+),(\\d+)\\): error (TS\\d+): (.+)$/\n\n/**\n * Parse structured diagnostics out of `tsc` output so `--report` carries the detail (file,\n * line, code, message), not just a count. Location-less errors (e.g. a config-level\n * TS18003) do not match and are left to the `errors` count, which stays the authoritative\n * total, so a report never quietly loses one.\n */\nexport function parseDiagnostics(output: string): TscDiagnostic[] {\n const diagnostics: TscDiagnostic[] = []\n for (const raw of output.split('\\n')) {\n const m = DIAGNOSTIC_RE.exec(raw.trim())\n if (m) {\n diagnostics.push({\n file: m[1] as string,\n line: Number(m[2]),\n col: Number(m[3]),\n code: m[4] as string,\n message: m[5] as string,\n })\n }\n }\n return diagnostics\n}\n\n/**\n * Phase-1 notice, generated from the single source of truth (`DEFERRED_RULES`). First\n * adoption is a NON-BREAKING lateral move; the deferred rules are enabled centrally in\n * a later wave. Surfaced on every run so the reduced strictness is visible in logs,\n * never a silent gap; `--inspect` lists the full set.\n */\nconst PHASED_STRICTNESS_WARNING =\n `sentinel typescript: phase 1 (non-breaking) — deferred: ${deferredRuleNames().join(', ')}` +\n '. Enabled centrally in a later wave; run `sentinel --inspect --typescript` for the list.'\n\n/**\n * Append `preset` to a tsconfig's `extends`, as a TS 5.0 array, WITHOUT dropping\n * what is already there (the base config the module extends). Accepts the current\n * `extends` as a string, an array, or absent; returns the composed chain with the\n * preset last (so its options win) and never duplicated, so a re-run is a no-op.\n */\nfunction composeExtends(current: unknown, preset: string): string[] {\n const chain =\n typeof current === 'string'\n ? [current]\n : Array.isArray(current)\n ? current.filter((entry): entry is string => typeof entry === 'string')\n : []\n return chain.includes(preset) ? chain : [...chain, preset]\n}\n\nexport class TscAdapter extends BaseAdapter {\n readonly target: Target = 'typescript'\n readonly runner = 'tsc'\n\n /**\n * The tsc adapter drives type-checking for any flavour: `--run`/`--report`/\n * `--inspect` just execute tsc against the module's existing config, which is\n * meaningful regardless of flavour. `--update` is the exception, it only WRITES a\n * preset for flavours that ship one (gated inside `plan`), so svelte is not\n * clobbered with a non-existent preset.\n */\n appliesTo(_flavour: Flavour): boolean {\n return true\n }\n\n /**\n * Plan `--update`: make the module extend the sentinel preset with a THIN,\n * conformant stub, and route type-checking through the CLI. The engine applies\n * the ops; ensuring the `@hublo/sentinel` dependency is an adoption step\n * (`pnpm add`), not a file write.\n *\n * Per resolved case:\n * - extends-base: append the preset to the `extends` chain (keep the base for\n * the monorepo's paths/structure) + strip preset-owned `compilerOptions`\n * (drift), keeping the project's own paths/include (the allowlist).\n * - none: create a fresh thin `tsconfig.json`.\n * - other-chain (svelte): skip, its config extends a different base.\n */\n plan(context: UpdateContext): UpdatePlan {\n // Only write a preset for a flavour that actually ships one. A declared-but-\n // unshipped flavour (e.g. svelte) is skipped, never pointed at a preset that\n // does not exist, which would break the module's typecheck.\n if (!hasShippedPreset(context.flavour)) {\n return {\n operations: [],\n notes: [`skipped: no TypeScript preset for flavour \"${context.flavour}\" yet`],\n }\n }\n const target = resolveTsconfigTarget(context.cwd)\n const preset = `@hublo/sentinel/tsconfig/${context.flavour}`\n const addScript = this.typecheckScriptOperation(context.cwd)\n\n if (target.reason === 'other-chain') {\n return {\n operations: [],\n notes: [`skipped: ${target.path} extends a non-base config; handled separately`],\n }\n }\n\n if (target.reason === 'none') {\n const contents = JSON.stringify({ extends: preset, include: ['src'] }, null, 2) + '\\n'\n return {\n operations: [{ kind: 'write', path: target.path, contents }, addScript],\n notes: [`created ${target.path} (no tsconfig found)`, INSTALL_NOTE],\n }\n }\n\n // extends-base: KEEP the base (it carries the monorepo's paths/types/structure,\n // not just tooling) and APPEND the preset to the extends chain (TS 5.0 array\n // extends). Later wins, so the preset's tooling overrides the base while the\n // base's config survives. Then strip the module's own preset-owned options so the\n // preset wins over stale local copies.\n const existing = parseJsonc<{ extends?: unknown; compilerOptions?: Record<string, unknown> }>(\n readFileSync(join(context.cwd, target.path), 'utf8'),\n target.path,\n )\n const extendsChain = composeExtends(existing.extends, preset)\n const drift = presetOwnedKeys(existing.compilerOptions)\n const operations: FileOperation[] = [\n { kind: 'merge-json', path: target.path, value: { extends: extendsChain } },\n ]\n const notes: string[] = []\n if (drift.length > 0) {\n operations.push({\n kind: 'remove-json-keys',\n path: target.path,\n keys: drift.map((key) => ['compilerOptions', key]),\n })\n notes.push(`stripped preset-owned compilerOptions: ${drift.join(', ')}`)\n }\n operations.push(addScript)\n notes.push(INSTALL_NOTE)\n return { operations, notes }\n }\n\n /**\n * The op that routes type-checking through the CLI. If the module already has a\n * `package.json`, merge the script in and leave the rest untouched. If it does\n * NOT (common for nx apps/services that carry only a `project.json`), scaffold a\n * minimal, workspace-valid one, its nx name + `private: true`, so pnpm accepts it\n * and it can then receive the `@hublo/sentinel` devDep (added via `pnpm add` at\n * adoption, never written here, so the lockfile stays authoritative).\n */\n private typecheckScriptOperation(cwd: string): FileOperation {\n if (existsSync(join(cwd, 'package.json'))) {\n return { kind: 'merge-json', path: 'package.json', value: { scripts: TYPECHECK_SCRIPT } }\n }\n const name = readNxProjectName(cwd) ?? basename(cwd)\n return {\n kind: 'merge-json',\n path: 'package.json',\n value: { name, private: true, scripts: TYPECHECK_SCRIPT },\n }\n }\n\n /**\n * Type-check the module with `tsc -b` (build mode) on its solution config, the\n * way the monorepo itself does. Build mode walks the config's `references`, so a\n * references-only solution (Pattern A: app + spec) is actually checked instead of\n * passing vacuously; it also only caches SUCCESSFUL builds, so errors are always\n * re-reported. Uses the module's own tsc. Nothing to check is a pass.\n */\n async run(ctx: RunContext): Promise<AdapterResult> {\n const config = this.typecheckTarget(ctx.cwd)\n if (!config) {\n process.stderr.write('sentinel typescript(tsc): no tsconfig to check\\n')\n return { ok: true, code: 0 }\n }\n process.stderr.write(`${palette(process.stderr).warn(PHASED_STRICTNESS_WARNING)}\\n`)\n const tsc = resolveBin(ctx.cwd, 'tsc') ?? 'tsc'\n const result = spawnSync(tsc, ['-b', config], { cwd: ctx.cwd, stdio: 'inherit' })\n if (result.error) {\n process.stderr.write(\n `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?\\n`,\n )\n return { ok: false, code: 1 }\n }\n const code = result.status ?? 1\n return { ok: code === 0, code }\n }\n\n /**\n * The config to type-check with `tsc -b`. Prefer the module's root `tsconfig.json`\n * (the solution the monorepo builds; `tsc -b` follows its `references` to cover\n * app + spec), else the base-extending file, else null when there is nothing to\n * check.\n */\n private typecheckTarget(cwd: string): string | null {\n if (existsSync(join(cwd, 'tsconfig.json'))) return 'tsconfig.json'\n const target = resolveTsconfigTarget(cwd)\n return target.reason === 'none' ? null : target.path\n }\n\n /**\n * The module's resolved TypeScript config: which preset, which file, how, and the\n * phased-strictness state (`deferred` rules that are off in phase 1). This is the\n * \"list what's deferred\" query, `sentinel --inspect --typescript`.\n */\n async inspect(ctx: RunContext): Promise<unknown> {\n const target = resolveTsconfigTarget(ctx.cwd)\n // The preset is READ from the module's committed `extends` chain, not derived from\n // the (possibly hoisted-fallback) flavour: a nest module reports `.../nest`, not\n // `.../node`, so `--inspect` reflects what the file actually uses.\n const { preset, adopted } = readTsconfigAdoption(ctx.cwd)\n return {\n module: ctx.module,\n target: 'typescript',\n flavour: ctx.flavour,\n configFile: target.path,\n configState: target.reason,\n preset,\n adopted,\n phase: 1,\n deferred: DEFERRED_RULES,\n }\n }\n\n /**\n * Adoption + conformity from the committed tsconfig, for `--status`. No tsc run and\n * no flavour guessing: reads the actual `extends` chain, so a workspace-wide scan is\n * a cheap coverage + drift dashboard (adopted? which preset? drifted?).\n */\n async status(ctx: RunContext): Promise<StatusReport> {\n const { adopted, preset, conformant, drift } = readTsconfigAdoption(ctx.cwd)\n return { adopted, preset, conformant, drift }\n }\n\n /**\n * Report conformance for the module: `tsc -b` (build mode, so app + spec are\n * covered) and count total type errors plus the implicit-`any` family (TS70xx:\n * 7006/7031/7053/… ), the signal that drives the noImplicitAny migration. No\n * tsconfig is a clean, empty report.\n */\n async report(ctx: RunContext): Promise<AdapterResult> {\n const config = this.typecheckTarget(ctx.cwd)\n if (!config) {\n return { ok: true, code: 0, metrics: { errors: 0, implicitAny: 'deferred', diagnostics: [] } }\n }\n const tsc = resolveBin(ctx.cwd, 'tsc') ?? 'tsc'\n const result = spawnSync(tsc, ['-b', config], { cwd: ctx.cwd, encoding: 'utf8' })\n if (result.error) {\n process.stderr.write(\n `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?\\n`,\n )\n return { ok: false, code: 1, metrics: { error: 'tsc not available' } }\n }\n const output = `${result.stdout ?? ''}${result.stderr ?? ''}`\n const errors = (output.match(/error TS\\d+/g) ?? []).length\n // Embed the structured diagnostics so `--report --json` carries the detail (no need to\n // re-run `--run`). Cap to keep the envelope bounded; `errors` remains the full count and\n // `diagnosticsTruncated` flags when the cap dropped some, so nothing is hidden silently.\n const parsed = parseDiagnostics(output)\n const cap =\n ctx.maxDiagnostics === 0 ? Infinity : (ctx.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS)\n const diagnostics = Number.isFinite(cap) ? parsed.slice(0, cap) : parsed\n return {\n ok: errors === 0,\n code: result.status ?? 0,\n metrics: {\n errors,\n implicitAny: this.implicitAnyMetric(ctx.cwd, tsc, config, output),\n diagnostics,\n diagnosticsTruncated: diagnostics.length < parsed.length,\n },\n }\n }\n\n /**\n * The `implicitAny` report metric, honestly. Implicit-`any` violations (TS70xx) are\n * only *visible* to tsc when `noImplicitAny` is ON. In phase 1 the rule is DEFERRED\n * (off), so counting TS70xx from the committed-config run is structurally always 0 —\n * a misleading \"no implicit-any\" when the rule simply was not applied. So: report the\n * real count only when the rule is on, otherwise `'deferred'` (never a fake `0`). The\n * remaining debt while deferred is a separate, opt-in probe (a later `--migration`).\n */\n private implicitAnyMetric(\n cwd: string,\n tsc: string,\n config: string,\n mainOutput: string,\n ): number | 'deferred' {\n return this.noImplicitAnyEnabled(cwd, tsc, config, mainOutput)\n ? (mainOutput.match(/error TS70\\d\\d/g) ?? []).length\n : 'deferred'\n }\n\n /**\n * Whether `noImplicitAny` is effectively ON in the module's resolved config. Read from\n * `tsc --showConfig` (an explicit value wins; otherwise `strict` implies it). If\n * `--showConfig` is unavailable, fall back to the run's own evidence: implicit-`any`\n * errors in the output mean the rule must be on.\n */\n private noImplicitAnyEnabled(\n cwd: string,\n tsc: string,\n config: string,\n mainOutput: string,\n ): boolean {\n const shown = spawnSync(tsc, ['-p', config, '--showConfig'], { cwd, encoding: 'utf8' })\n if (shown.status === 0 && shown.stdout) {\n try {\n const co = parseJsonc<{ compilerOptions?: { noImplicitAny?: boolean; strict?: boolean } }>(\n shown.stdout,\n 'tsconfig(--showConfig)',\n ).compilerOptions\n if (co) return co.noImplicitAny ?? co.strict === true\n } catch {\n // malformed --showConfig output: fall through to the evidence-based check\n }\n }\n return /error TS70\\d\\d/.test(mainOutput)\n }\n}\n","/**\n * JSONC (JSON with comments + trailing commas) helpers. tsconfig files are JSONC,\n * so reading them with plain `JSON.parse` throws on real projects (e.g.\n * host-admin's tsconfig.app.json has comments). Backed by jsonc-parser (the VS\n * Code library), which also underpins content-preserving edits (added with the\n * `--update` merge).\n */\nimport { parse, printParseErrorCode, type ParseError } from 'jsonc-parser'\n\n/**\n * Parse JSONC text into a value. Throws with a clear message listing the parse\n * errors, so a malformed config fails loudly rather than silently mis-reading.\n */\nexport function parseJsonc<T = unknown>(text: string, source = 'config'): T {\n const errors: ParseError[] = []\n const value = parse(text, errors, { allowTrailingComma: true }) as T\n if (errors.length > 0) {\n const details = errors.map((error) => printParseErrorCode(error.error)).join(', ')\n throw new Error(`${source}: malformed JSONC (${details}).`)\n }\n return value\n}\n","/**\n * Find a tool binary the way node/npm would: walk up from a directory looking for\n * `node_modules/.bin/<name>`. Used so `--run` invokes the MODULE's own tool version\n * (its `tsc`), not sentinel's. Returns undefined if not found (caller falls back to\n * the name on PATH).\n */\nimport { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nexport function resolveBin(fromDir: string, name: string): string | undefined {\n let dir = fromDir\n for (;;) {\n const candidate = join(dir, 'node_modules', '.bin', name)\n if (existsSync(candidate)) return candidate\n const parent = dirname(dir)\n if (parent === dir) return undefined\n dir = parent\n }\n}\n","/**\n * The allowlist: which tsconfig `compilerOptions` a module may keep locally. Only\n * genuinely project-specific settings, everything else is owned by the sentinel\n * preset and stripped by `--update`, so every migrated module is conformant from\n * the start. A sanctioned exception would be added here (visible + reviewed).\n */\nexport const PERMITTED_COMPILER_OPTIONS: readonly string[] = [\n 'paths',\n 'baseUrl',\n 'rootDir',\n 'outDir',\n 'tsBuildInfoFile',\n]\n\n/**\n * The `compilerOptions` keys the preset owns: present in the project but not in the\n * allowlist. These are the drift `--update` strips.\n */\nexport function presetOwnedKeys(compilerOptions: Record<string, unknown> | undefined): string[] {\n if (!compilerOptions) return []\n return Object.keys(compilerOptions).filter((key) => !PERMITTED_COMPILER_OPTIONS.includes(key))\n}\n","/**\n * Phased TypeScript strictness, the single source of truth.\n *\n * These are the rules sentinel DEFERS on first adoption so it stays non-breaking, then\n * enables centrally in a later wave. The preset deliberately does NOT set them (so a\n * repo base that relaxes a rule keeps it off), they are announced by the run/report\n * warning, and reported by `--inspect` so a developer, or a CI conformance check, can\n * list exactly what is deferred. Each `rule` here MUST stay absent from every preset\n * (asserted in the preset tests), so this list and the presets can never drift.\n *\n * The paired other half lives in the consuming repo: the base config marks the matching\n * relaxations `@deprecated` (the levers to remove). deferred (here, coming) ⇄\n * @deprecated (there, going).\n */\nexport interface PhasedRule {\n /** The tsconfig compilerOption that is deferred. */\n rule: string\n /** The migration wave that will enable it. */\n phase: number\n /** Why it is deferred (what enabling it will surface). */\n reason: string\n}\n\nexport const DEFERRED_RULES: readonly PhasedRule[] = [\n { rule: 'noImplicitAny', phase: 2, reason: 'the implicit-any migration (TS70xx)' },\n { rule: 'noUnusedLocals', phase: 2, reason: 'unused-local cleanup (TS6133)' },\n { rule: 'noUnusedParameters', phase: 2, reason: 'unused-parameter cleanup (TS6133)' },\n] as const\n\n/** The deferred rule names, e.g. for a one-line warning. */\nexport function deferredRuleNames(): string[] {\n return DEFERRED_RULES.map((entry) => entry.rule)\n}\n","/**\n * The flavours whose TypeScript preset actually ships: one `flavours/<flavour>.ts`\n * source, flattened to `dist/tsconfig/<flavour>.json` by `scripts/build-presets.ts`.\n *\n * Single source of truth, shared by the build script and the adapter's `appliesTo`,\n * so a flavour is only ever offered when its preset exists. A declared flavour with\n * no preset yet (e.g. `svelte`) is deliberately absent: `--update` skips it rather\n * than writing an `extends` to a module that does not exist. Adding a preset is one\n * new file here plus its entry in this list.\n */\nimport type { Flavour } from '../../core/domain.js'\n\nexport const SHIPPED_FLAVOURS = ['react', 'nest', 'node'] as const satisfies readonly Flavour[]\n\n/** Whether a flavour's TypeScript preset is available. */\nexport function hasShippedPreset(flavour: Flavour): boolean {\n return (SHIPPED_FLAVOURS as readonly Flavour[]).includes(flavour)\n}\n","/**\n * Read a module's ADOPTION + CONFORMITY of the TypeScript preset, straight from its\n * committed `tsconfig`. This is the truth `--status` reports, and it is deliberately\n * DETECTION-FREE: it reads the actual `extends` chain rather than guessing a flavour,\n * so a hoisted-deps monorepo (where detection falls back to `node`) can't corrupt it.\n *\n * - adopted = the resolved tsconfig extends a `@hublo/sentinel/tsconfig/*` preset\n * - preset = that exact preset string (or null), the real one, not a derived guess\n * - conformant = adopted AND no preset-owned `compilerOptions` drift (i.e. a re-`update`\n * would strip nothing), the same drift the update guard manages\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { parseJsonc } from '../../shared/jsonc.js'\nimport { presetOwnedKeys } from './config-policy.js'\nimport { resolveTsconfigTarget } from './resolve-tsconfig-target.js'\n\n/** Matches a sentinel tsconfig preset subpath, e.g. `@hublo/sentinel/tsconfig/nest`. */\nconst SENTINEL_PRESET = /^@hublo\\/sentinel\\/tsconfig\\/[a-z-]+$/\n\nexport interface TsconfigAdoption {\n /** The tsconfig inspected (relative to the module), or null when there is none. */\n configFile: string | null\n /** The sentinel preset the module extends, or null when it extends none. */\n preset: string | null\n /** True when the module extends a sentinel preset. */\n adopted: boolean\n /** True when adopted AND drift-free (a re-`update` would change nothing). */\n conformant: boolean\n /** Preset-owned `compilerOptions` kept locally (the drift); empty when conformant. */\n drift: string[]\n}\n\n/** `extends` as a list: a string, a TS 5.0 array, or absent, all normalised. */\nfunction normaliseExtends(value: unknown): string[] {\n if (typeof value === 'string') return [value]\n if (Array.isArray(value))\n return value.filter((entry): entry is string => typeof entry === 'string')\n return []\n}\n\nconst NOT_ADOPTED = (configFile: string | null): TsconfigAdoption => ({\n configFile,\n preset: null,\n adopted: false,\n conformant: false,\n drift: [],\n})\n\n/** Read a module's TypeScript adoption + conformity from its committed tsconfig. */\nexport function readTsconfigAdoption(cwd: string): TsconfigAdoption {\n const target = resolveTsconfigTarget(cwd)\n if (target.reason === 'none' || !existsSync(join(cwd, target.path))) {\n return NOT_ADOPTED(target.reason === 'none' ? null : target.path)\n }\n\n let parsed: { extends?: unknown; compilerOptions?: Record<string, unknown> }\n try {\n parsed = parseJsonc(readFileSync(join(cwd, target.path), 'utf8'), target.path)\n } catch {\n return NOT_ADOPTED(target.path) // malformed config: can't be conformant\n }\n\n const preset =\n normaliseExtends(parsed.extends).find((entry) => SENTINEL_PRESET.test(entry)) ?? null\n if (preset === null) return NOT_ADOPTED(target.path)\n\n const drift = presetOwnedKeys(parsed.compilerOptions)\n return { configFile: target.path, preset, adopted: true, conformant: drift.length === 0, drift }\n}\n","/**\n * Resolve which tsconfig file `--update --typescript` should write in a module.\n *\n * The rule (from the monorepo audit): target the file that currently `extends` the\n * shared base config, that is the entry point sentinel's preset replaces. Two\n * shapes exist:\n * - Pattern A (libs, nest services): `tsconfig.json` extends the base.\n * - Pattern B (Vite React apps): `tsconfig.json` is references-only and\n * `tsconfig.app.json` extends the base.\n * We check `tsconfig.app.json` before `tsconfig.json` so B is found first. The\n * `reason` distinguishes the three cases `--update` must handle differently:\n * - `extends-base`: found the file to migrate.\n * - `other-chain`: a tsconfig exists but extends something else (svelte's own\n * `.svelte-kit` chain), don't clobber it; handled separately.\n * - `none`: no tsconfig at all; `--update` creates one.\n *\n * The \"find the file that extends a shared root\" pattern is reusable; it will be\n * lifted to core when a second tool needs its own version.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { parseJsonc } from '../../shared/jsonc.js'\n\n/**\n * A file is the target if its `extends` references either the monorepo base (still\n * to migrate) or a sentinel preset (already migrated, so re-`update` re-strips any\n * new drift, keeping it idempotent and self-cleaning).\n */\nconst TARGET_EXTENDS_MARKERS = ['tsconfig.base.json', '@hublo/sentinel/tsconfig/'] as const\n\n/** Candidate entry points, most-specific first (Pattern B before Pattern A). */\nconst CANDIDATES = ['tsconfig.app.json', 'tsconfig.json'] as const\n\nexport interface TsconfigTarget {\n /** The tsconfig file to write, relative to the module root. */\n path: string\n /** How it was chosen, for observability and the per-case `--update` behaviour. */\n reason: 'extends-base' | 'other-chain' | 'none'\n}\n\n/**\n * The `extends` targets of a tsconfig as a list. `extends` may be a string or, since\n * TypeScript 5.0, an array of strings; both are normalised here (absent/unreadable\n * or non-string entries yield an empty list).\n */\nfunction readExtends(absolutePath: string): string[] {\n let parsed: { extends?: unknown }\n try {\n parsed = parseJsonc(readFileSync(absolutePath, 'utf8'), absolutePath)\n } catch {\n return [] // malformed candidate: skip it, try the next\n }\n if (typeof parsed.extends === 'string') return [parsed.extends]\n if (Array.isArray(parsed.extends)) {\n return parsed.extends.filter((entry): entry is string => typeof entry === 'string')\n }\n return []\n}\n\nexport function resolveTsconfigTarget(moduleDir: string): TsconfigTarget {\n let existing: string | undefined\n for (const candidate of CANDIDATES) {\n const absolutePath = join(moduleDir, candidate)\n if (!existsSync(absolutePath)) continue\n existing ??= candidate // remember the first tsconfig we saw\n const extendsValues = readExtends(absolutePath)\n const extendsBase = extendsValues.some((value) =>\n TARGET_EXTENDS_MARKERS.some((marker) => value.includes(marker)),\n )\n if (extendsBase) {\n return { path: candidate, reason: 'extends-base' }\n }\n }\n if (existing) return { path: existing, reason: 'other-chain' }\n return { path: 'tsconfig.json', reason: 'none' }\n}\n","/**\n * TypeScript role registration. The single entry the bootstrap\n * (`src/adapters.ts`) imports, so wiring stays greppable and the CLI never\n * changes. Adds the tsc adapter and makes it the default runner for `--typescript`.\n */\nimport { register, setDefaultRunner } from '../../core/registry.js'\nimport { TscAdapter } from './adapters/tsc/tsc.adapter.js'\n\nexport function registerTypescript(): void {\n register(new TscAdapter())\n setDefaultRunner('typescript', 'tsc')\n}\n","/**\n * Adapter bootstrap: the single place adapters are wired into the CLI.\n *\n * A `register(new MyAdapter())` call only runs if its module is imported, and the\n * CLI must not import every tool by hand, that would make the promise \"one tool =\n * one adapter, the CLI never changes\" false. So the CLI calls `registerAdapters()`\n * once at startup, and each tool ticket adds exactly ONE line here (its role's\n * registration), never touching the CLI entry point (`bin/sentinel.ts`) or the\n * registry.\n *\n * Empty until the first tool ticket lands. A tool ticket adds, e.g. (an explicit\n * module path, not a barrel, so the wiring stays greppable):\n *\n * import { registerTypescript } from './roles/typescript/register.js'\n * export function registerAdapters(): void {\n * registerTypescript()\n * }\n */\nimport { registerTypescript } from './roles/typescript/register.js'\n\nexport function registerAdapters(): void {\n registerTypescript()\n}\n","/**\n * Framework detection from a module's package.json dependencies. DETERMINISTIC:\n * a dependency maps to exactly one flavour, and a module with no framework\n * dependency is a plain TypeScript library (`node`). This is not the old silent\n * guessing (there is no \"assume react\" fallback); `node` is a real preset.\n *\n * Reusable across tools: every tool's `--update` needs the module's flavour to\n * pick its preset, so this lives in core, not in the TypeScript role.\n */\nimport type { Flavour } from './domain.js'\n\n/** The slice of a package.json we read for detection. */\nexport interface PackageDependencies {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n}\n\n/**\n * A framework signal in priority order: the first whose package is present wins.\n * Ordered most-specific-first so a backend (`@nestjs/core`) is never shadowed by a\n * transitive `react`; `react` before `svelte` (svelte is being removed). Adding a\n * framework is one entry here.\n */\nconst FRAMEWORK_SIGNALS: ReadonlyArray<{ flavour: Flavour; dependency: string }> = [\n { flavour: 'nest', dependency: '@nestjs/core' },\n { flavour: 'react', dependency: 'react' },\n { flavour: 'svelte', dependency: 'svelte' },\n]\n\n/**\n * Backend frameworks that do NOT map to a flavour, but whose presence alongside a FRONTEND\n * flavour (react/svelte) means the module is mixed and the pick is probably wrong (the\n * tester's Sails + React root). They make detection ambiguous, not the choice. Deliberately\n * NOT including React meta-frameworks (e.g. `next`): a Next app legitimately has react +\n * next, and `react` is the correct pick, so it must not warn.\n */\nconst BACKEND_HINTS = ['sails', 'express', 'koa', 'fastify', '@hapi/hapi'] as const\n\n/** The detected flavour plus WHY, so a wrong silent guess is visible and can be flagged. */\nexport interface FrameworkDetection {\n flavour: Flavour\n /** The dependency that decided it, or a marker when none (node). */\n source: string\n /** True when the module carries signals for more than one stack: the pick is a guess. */\n ambiguous: boolean\n /** The other signals that make it ambiguous (extra flavour deps, or backend hints). */\n conflicts: string[]\n}\n\n/**\n * Detect the flavour AND explain it: the deciding dependency (`source`), and whether the\n * module is ambiguous (more than one flavour signal, or a frontend flavour next to a\n * backend framework). The CLI surfaces the source and warns on ambiguity, recommending\n * `--flavour`, instead of choosing silently.\n */\nexport function describeFramework(packageJson: PackageDependencies): FrameworkDetection {\n const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }\n const matched = FRAMEWORK_SIGNALS.filter((s) => s.dependency in dependencies)\n const chosen = matched[0]\n const flavour: Flavour = chosen?.flavour ?? 'node'\n const source = chosen?.dependency ?? '(no framework dependency)'\n\n const otherFlavourSignals = matched.slice(1).map((s) => s.dependency)\n const backendHints =\n flavour === 'react' || flavour === 'svelte'\n ? BACKEND_HINTS.filter((hint) => hint in dependencies)\n : []\n const conflicts = [...otherFlavourSignals, ...backendHints]\n return { flavour, source, ambiguous: conflicts.length > 0, conflicts }\n}\n\n/** The flavour for a module, from its dependencies. `node` when no framework. */\nexport function detectFramework(packageJson: PackageDependencies): Flavour {\n return describeFramework(packageJson).flavour\n}\n","/**\n * Small text helpers: `ensure-lines` for the engine, and a line diff for the\n * `--update --dry-run` preview.\n */\n\n/**\n * Append any of `lines` not already present in `current` (matched as a full,\n * trimmed line). Idempotent: running it twice adds nothing the second time.\n * Preserves a trailing newline and never duplicates existing lines.\n */\nexport function ensureLines(current: string, lines: string[]): string {\n const present = new Set(current.split('\\n').map((line) => line.trim()))\n const missing = lines.filter((line) => !present.has(line.trim()))\n if (missing.length === 0) return current\n const prefix = current.length === 0 || current.endsWith('\\n') ? current : current + '\\n'\n return prefix + missing.join('\\n') + '\\n'\n}\n\n/** Split into lines for diffing, dropping a single trailing newline (not content). */\nfunction toLines(text: string): string[] {\n if (text.length === 0) return []\n return text.replace(/\\n$/, '').split('\\n')\n}\n\n/**\n * A minimal line-level diff (LCS-based) between `before` and `after`. Returns one\n * string per line, prefixed `- ` (removed), `+ ` (added) or ` ` (unchanged), so a\n * dry-run can show exactly what a write would change instead of dumping the result.\n */\nexport function diffLines(before: string, after: string): string[] {\n const from = toLines(before)\n const to = toLines(after)\n // Longest common subsequence length table, filled bottom-up. `lcs[i][j]` is the\n // LCS length of from[i:] and to[j:]; reads past the edge are 0 (the base case).\n const lcs: number[][] = Array.from({ length: from.length + 1 }, () =>\n new Array<number>(to.length + 1).fill(0),\n )\n const cell = (i: number, j: number): number => lcs[i]?.[j] ?? 0\n for (let i = from.length - 1; i >= 0; i--) {\n const row = lcs[i]\n if (!row) continue\n for (let j = to.length - 1; j >= 0; j--) {\n row[j] = from[i] === to[j] ? cell(i + 1, j + 1) + 1 : Math.max(cell(i + 1, j), cell(i, j + 1))\n }\n }\n\n const out: string[] = []\n let i = 0\n let j = 0\n while (i < from.length && j < to.length) {\n if (from[i] === to[j]) {\n out.push(` ${from[i] ?? ''}`)\n i++\n j++\n } else if (cell(i + 1, j) >= cell(i, j + 1)) {\n out.push(`- ${from[i] ?? ''}`)\n i++\n } else {\n out.push(`+ ${to[j] ?? ''}`)\n j++\n }\n }\n while (i < from.length) out.push(`- ${from[i++] ?? ''}`)\n while (j < to.length) out.push(`+ ${to[j++] ?? ''}`)\n return out\n}\n","/**\n * Plan application: the engine's filesystem port.\n *\n * Adapters return a pure, declarative `UpdatePlan` (see `FileOperation`); this is\n * the ONE place that touches the disk. It resolves paths against the module root,\n * reads existing files, does the generic read/merge/write mechanics, and writes.\n * Keeping all IO here is what lets adapters stay pure and decoupled from the repo\n * layout: they say WHAT to change, the engine knows HOW and WHERE.\n *\n * Three guarantees, so `--update` is safe to run on real projects:\n * - CONTENT-PRESERVING merge: `merge-json` edits the file in place via\n * jsonc-parser, so a tsconfig's comments, key order and formatting survive.\n * - PREPARED-THEN-WRITTEN: the whole plan is computed before any write, so an\n * invalid operation fails before touching disk, and each file is written\n * atomically (temp file + rename) so an interrupted write never leaves a\n * truncated file. (Across multiple files the writes are sequential, not one\n * transaction: a crash mid-plan can leave earlier files written, recoverable\n * via git; a single file is always all-or-nothing.)\n * - CONFINED: every path is resolved and rejected if it escapes the module root.\n */\nimport { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { resolve, sep } from 'node:path'\n\nimport { applyEdits, modify } from 'jsonc-parser'\n\nimport { isPlainObject } from '../shared/deep-merge.js'\nimport { ensureLines } from '../shared/text.js'\nimport type { FileOperation, UpdatePlan } from './types.js'\n\n/** Resolve a plan-relative path and reject anything escaping the module root. */\nfunction resolveWithinRoot(cwd: string, relativePath: string): string {\n const root = resolve(cwd)\n const absolutePath = resolve(root, relativePath)\n if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {\n throw new Error(`Refusing to write outside the module root: \"${relativePath}\".`)\n }\n return absolutePath\n}\n\nfunction readIfExists(absolutePath: string): string | undefined {\n return existsSync(absolutePath) ? readFileSync(absolutePath, 'utf8') : undefined\n}\n\n/** Yield every leaf ([path, value]) of a nested object, for deep in-place edits. */\nfunction* leaves(\n value: Record<string, unknown>,\n prefix: string[] = [],\n): Generator<[path: string[], leaf: unknown]> {\n for (const [key, keyValue] of Object.entries(value)) {\n const path = [...prefix, key]\n if (isPlainObject(keyValue)) yield* leaves(keyValue, path)\n else yield [path, keyValue]\n }\n}\n\n/**\n * Deep-merge `value` into JSONC `current` IN PLACE (preserving comments/format).\n * Each leaf is set at its own path, so sibling keys, including a project's own\n * `paths`/`include`, survive untouched.\n */\nfunction mergeJsonc(current: string, value: Record<string, unknown>): string {\n let text = current.trim().length > 0 ? current : '{}\\n'\n for (const [path, leaf] of leaves(value)) {\n const edits = modify(text, path, leaf, {\n formattingOptions: { insertSpaces: true, tabSize: 2 },\n })\n text = applyEdits(text, edits)\n }\n return text.endsWith('\\n') ? text : text + '\\n'\n}\n\n/** Remove each key path from JSONC `current`, preserving comments/formatting. */\nfunction removeJsoncKeys(current: string, keys: string[][]): string {\n let text = current.trim().length > 0 ? current : '{}\\n'\n for (const path of keys) {\n const edits = modify(text, path, undefined, {\n formattingOptions: { insertSpaces: true, tabSize: 2 },\n })\n text = applyEdits(text, edits)\n }\n return text.endsWith('\\n') ? text : text + '\\n'\n}\n\n/** Apply one operation to the current file content (pure transform). */\nfunction applyOperationTo(current: string, operation: FileOperation): string {\n switch (operation.kind) {\n case 'write':\n return operation.contents\n case 'merge-json':\n return mergeJsonc(current, operation.value)\n case 'ensure-lines':\n return ensureLines(current, operation.lines)\n case 'remove-json-keys':\n return removeJsoncKeys(current, operation.keys)\n default: {\n // Exhaustiveness: a new FileOperation kind without a case here fails to compile.\n const unreachable: never = operation\n throw new Error(`Unknown file operation: ${JSON.stringify(unreachable)}`)\n }\n }\n}\n\n/** A file the plan would write: its original content and the computed result. */\nexport interface PreparedFile {\n path: string\n absolutePath: string\n before: string\n after: string\n}\n\n/**\n * Compute what a plan WOULD write, without touching the disk (for `--dry-run`).\n * All-or-nothing (throws before returning anything on a bad op), and multiple\n * operations on the SAME file chain in order, so `before` is the original content\n * and `after` is the final result.\n */\nexport function preparePlan(cwd: string, plan: UpdatePlan): PreparedFile[] {\n const prepared = new Map<string, PreparedFile>()\n for (const operation of plan.operations) {\n const absolutePath = resolveWithinRoot(cwd, operation.path)\n const existing = prepared.get(operation.path)\n const before = existing?.before ?? readIfExists(absolutePath) ?? ''\n const current = existing?.after ?? before\n prepared.set(operation.path, {\n path: operation.path,\n absolutePath,\n before,\n after: applyOperationTo(current, operation),\n })\n }\n return [...prepared.values()]\n}\n\n/**\n * Write `contents` to `absolutePath` atomically: write a sibling temp file, then\n * rename it over the target. rename is atomic on a single filesystem, so a reader\n * (or an interrupted run) never sees a half-written file, only the old or new one.\n */\nfunction writeFileAtomic(absolutePath: string, contents: string): void {\n const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`\n writeFileSync(tempPath, contents)\n renameSync(tempPath, absolutePath)\n}\n\n/**\n * Apply a plan against the module root; return the paths written. Prepares the\n * whole plan before the first write, then writes each file atomically.\n */\nexport function applyPlan(cwd: string, plan: UpdatePlan): string[] {\n const prepared = preparePlan(cwd, plan)\n for (const file of prepared) writeFileAtomic(file.absolutePath, file.after)\n return prepared.map((file) => file.path)\n}\n","/**\n * Generic deep-merge for plain JSON objects. Used by the engine to apply a\n * `merge-json` op (pin the keys sentinel owns while preserving a project's own).\n */\n\n/** True for a mergeable plain object (not null, not an array). */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Deep-merge `patch` into `base`, preserving keys `patch` does not mention.\n * Scalars and arrays from `patch` replace wholesale (no array concat surprises).\n */\nexport function deepMerge(\n base: Record<string, unknown>,\n patch: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n const existing = out[key]\n out[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value\n }\n return out\n}\n","/**\n * The single-module WRITE path (`--update`). The read verbs (`--run`/`--inspect`/\n * `--report`/`--status`) go through the multi-module orchestrator (`analyse`), which\n * resolves each module's adapter and calls the matching method; `dispatch` is what\n * `--update` uses because a write targets exactly one module.\n *\n * The engine owns IO: it resolves the flavour (explicit `--flavour`, else\n * `detectFramework` on the module's package.json), hands the adapter a normalized\n * `UpdateContext`; the adapter plans, the engine applies (`applyPlan`) or previews.\n */\nimport { palette } from '../shared/color.js'\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { diffLines } from '../shared/text.js'\nimport { applyPlan, preparePlan } from './apply-plan.js'\nimport { describeFramework } from './detect-framework.js'\nimport type { Flavour, Target, Verb } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { UpdateContext, UpdatePlan } from './types.js'\n\nexport type { Verb }\n\nexport interface DispatchOptions {\n /** Always `update` (dispatch is the write path); other verbs route through `analyse`. */\n verb: Verb\n target: Target\n runner?: string\n cwd: string\n /** Declared flavour (`--flavour`), else resolved by `detectFramework` from deps. */\n flavour?: Flavour\n /** `--dry-run`: preview the plan instead of writing anything. */\n dryRun?: boolean\n /** `--json`: emit the dry-run preview as machine-readable JSON. */\n json?: boolean\n}\n\n/**\n * The flavour for the update: the explicit one, else detected from deps. `--update` WRITES\n * a preset, so an ambiguous detection is risky; warn and point at `--flavour` rather than\n * silently writing the wrong preset.\n */\nfunction resolveFlavour(opts: DispatchOptions): Flavour {\n if (opts.flavour) return opts.flavour\n const detection = describeFramework(readProjectPackageJson(opts.cwd))\n if (detection.ambiguous) {\n const warn = palette(process.stderr)\n process.stderr.write(\n warn.warn(\n `sentinel: flavour is ambiguous, detected \"${detection.flavour}\" (from ${detection.source}), also found ${detection.conflicts.join(', ')}. Pass --flavour to write the intended preset.`,\n ) + '\\n',\n )\n }\n return detection.flavour\n}\n\n/**\n * `--update --dry-run`: show what the plan WOULD change and write nothing. Prints a\n * per-file line diff (or JSON with `--json`), plus the adapter's notes. An empty\n * plan is reported as \"nothing to change\" so the developer gets a clear signal\n * rather than silence. Always exits 0: previewing never fails a build.\n */\nfunction previewPlan(opts: DispatchOptions, plan: UpdatePlan): number {\n // Only files the plan would actually change: an idempotent re-run computes an\n // `after` equal to `before`, which is a no-op, not a change to preview.\n const changed = preparePlan(opts.cwd, plan).filter((file) => file.before !== file.after)\n if (opts.json) {\n process.stdout.write(\n JSON.stringify(\n {\n dryRun: true,\n notes: plan.notes ?? [],\n files: changed.map(({ path, before, after }) => ({\n path,\n action: before.length === 0 ? 'create' : 'update',\n before,\n after,\n })),\n },\n null,\n 2,\n ) + '\\n',\n )\n return 0\n }\n\n process.stderr.write(' dry run: no files written\\n')\n for (const note of plan.notes ?? []) process.stderr.write(` ${note}\\n`)\n if (changed.length === 0) {\n process.stderr.write(' nothing to change\\n')\n return 0\n }\n for (const { path, before, after } of changed) {\n const action = before.length === 0 ? 'create' : 'update'\n process.stdout.write(`\\n ${action} ${path}\\n`)\n for (const line of diffLines(before, after)) process.stdout.write(` ${line}\\n`)\n }\n return 0\n}\n\nexport async function dispatch(opts: DispatchOptions): Promise<number> {\n // dispatch is the write path only; read verbs route through `analyse`. Guard so a\n // future miswire fails loudly here instead of silently doing nothing.\n if (opts.verb !== 'update') {\n throw new Error(`dispatch handles --update only; --${opts.verb} routes through analyse()`)\n }\n const adapter = resolve(opts.target, opts.flavour, opts.runner)\n const flavour = resolveFlavour(opts)\n\n // Engine resolves + normalizes the context; the adapter plans, the engine applies the\n // operations against the module root (or previews them with --dry-run).\n const context: UpdateContext = { cwd: opts.cwd, flavour }\n const plan = await adapter.plan(context)\n if (opts.dryRun) {\n // Preview only: compute what WOULD be written, touch nothing on disk.\n return previewPlan(opts, plan)\n }\n const written = applyPlan(opts.cwd, plan)\n for (const path of written) process.stderr.write(` wrote ${path}\\n`)\n for (const note of plan.notes ?? []) process.stderr.write(` ${note}\\n`)\n return 0\n}\n"],"mappings":";AAWA,IAAM,WAAsB,CAAC;AAG7B,IAAM,gBAAiD;AAAA;AAEvD;AAQO,SAAS,SAAS,SAAwB;AAC/C,WAAS,KAAK,OAAO;AACvB;AAGO,SAAS,iBAAiB,QAAgB,QAAsB;AACrE,gBAAc,MAAM,IAAI;AAC1B;AAGO,SAAS,MAA0B;AACxC,SAAO;AACT;AAQO,SAAS,mBAA6B;AAC3C,SAAO,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACnD;AAWO,SAAS,QAAQ,QAAgB,SAAmB,QAA0B;AACnF,QAAM,YAAY,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM;AAC5D,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,IAAI;AAAA,MACR,qCAAqC,MAAM;AAAA,IAC7C;AAAA,EACF;AAEA,QAAM,aAAa,UAAU,UAAU,OAAO,CAAC,MAAM,EAAE,UAAU,OAAO,CAAC,IAAI;AAC7E,MAAI,WAAW,WAAW,GAAG;AAC3B,UAAM,IAAI,MAAM,0BAA0B,MAAM,sBAAsB,OAAO,IAAI;AAAA,EACnF;AAEA,QAAM,SAAS,UAAU,cAAc,MAAM;AAC7C,QAAM,YAAY,WAAW,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI;AAG3D,MAAI,CAAC,QAAQ;AACX,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AACzB,QAAI,SAAS,KAAK,WAAW,EAAG,QAAO;AACvC,UAAM,IAAI;AAAA,MACR,gCAAgC,MAAM,MAAM,SAAS;AAAA,IACvD;AAAA,EACF;AAEA,QAAM,WAAW,WAAW,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM;AAC7D,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,IAAI;AAAA,MACR,cAAc,MAAM,iBAAiB,MAAM,eAAe,OAAO,kBAAkB,SAAS;AAAA,IAC9F;AAAA,EACF;AACA,MAAI,SAAS,SAAS,GAAG;AACvB,UAAM,IAAI;AAAA,MACR,cAAc,SAAS,MAAM,2BAA2B,MAAM,cAAc,MAAM,eAAe,OAAO;AAAA,IAC1G;AAAA,EACF;AACA,SAAO,SAAS,CAAC;AACnB;;;AC9EO,IAAe,cAAf,MAA8C;AAAA,EAOnD,QAAQ,MAAoC;AAC1C,UAAM,IAAI,MAAM,GAAG,KAAK,MAAM,iCAAiC;AAAA,EACjE;AAAA,EAEA,OAAO,MAA0C;AAC/C,UAAM,IAAI,MAAM,GAAG,KAAK,MAAM,gCAAgC;AAAA,EAChE;AAAA,EAEA,OAAO,MAAyC;AAC9C,UAAM,IAAI,MAAM,GAAG,KAAK,MAAM,gCAAgC;AAAA,EAChE;AACF;;;AC1BA,SAAS,iBAAiB;AAmBnB,SAAS,QAAQ,QAAqC;AAC3D,QAAM,QAAQ,CAAC,QAAgB,SAAyB,UAAU,QAAQ,MAAM,EAAE,OAAO,CAAC;AAC1F,SAAO;AAAA,IACL,IAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM,GAAG,IAAI;AAAA,IAC3C,MAAM,CAAC,SAAS,MAAM,CAAC,OAAO,MAAM,GAAG,IAAI;AAAA,IAC3C,MAAM,CAAC,SAAS,MAAM,UAAU,IAAI;AAAA,IACpC,QAAQ,CAAC,SAAS,MAAM,QAAQ,IAAI;AAAA,IACpC,KAAK,CAAC,SAAS,MAAM,OAAO,IAAI;AAAA,IAChC,QAAQ,CAAC,SAAS,MAAM,QAAQ,IAAI;AAAA,EACtC;AACF;;;AClCA,SAAS,YAAY,oBAAoB;AACzC,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AASvB,SAAS,iBAAyB;AACvC,MAAI,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;AAChD,aAAS;AACP,UAAM,UAAU,KAAK,KAAK,cAAc;AACxC,QAAI,WAAW,OAAO,GAAG;AACvB,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,aAAa,SAAS,MAAM,CAAC;AACpD,YAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAAA,MAClD,QAAQ;AAAA,MAER;AAAA,IACF;AACA,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK,QAAO;AAC3B,UAAM;AAAA,EACR;AACF;AAMO,SAAS,uBAAuB,KAGrC;AACA,QAAM,OAAO,KAAK,KAAK,cAAc;AACrC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,YAAQ,OAAO,MAAM,6BAA6B,IAAI;AAAA,CAA6B;AACnF,WAAO,CAAC;AAAA,EACV;AACF;AASO,SAAS,kBAAkB,KAAiC;AACjE,QAAM,OAAO,KAAK,KAAK,cAAc;AACrC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AACpD,WAAO,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,EACzD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AC1DA,SAAS,iBAAiB;AAC1B,SAAS,cAAAA,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,UAAU,QAAAC,aAAY;;;ACH/B,SAAS,OAAO,2BAA4C;AAMrD,SAAS,WAAwB,MAAc,SAAS,UAAa;AAC1E,QAAM,SAAuB,CAAC;AAC9B,QAAM,QAAQ,MAAM,MAAM,QAAQ,EAAE,oBAAoB,KAAK,CAAC;AAC9D,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,UAAU,OAAO,IAAI,CAAC,UAAU,oBAAoB,MAAM,KAAK,CAAC,EAAE,KAAK,IAAI;AACjF,UAAM,IAAI,MAAM,GAAG,MAAM,sBAAsB,OAAO,IAAI;AAAA,EAC5D;AACA,SAAO;AACT;;;ACfA,SAAS,cAAAC,mBAAkB;AAC3B,SAAS,WAAAC,UAAS,QAAAC,aAAY;AAEvB,SAAS,WAAW,SAAiB,MAAkC;AAC5E,MAAI,MAAM;AACV,aAAS;AACP,UAAM,YAAYA,MAAK,KAAK,gBAAgB,QAAQ,IAAI;AACxD,QAAIF,YAAW,SAAS,EAAG,QAAO;AAClC,UAAM,SAASC,SAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK,QAAO;AAC3B,UAAM;AAAA,EACR;AACF;;;ACZO,IAAM,6BAAgD;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,gBAAgB,iBAAgE;AAC9F,MAAI,CAAC,gBAAiB,QAAO,CAAC;AAC9B,SAAO,OAAO,KAAK,eAAe,EAAE,OAAO,CAAC,QAAQ,CAAC,2BAA2B,SAAS,GAAG,CAAC;AAC/F;;;ACEO,IAAM,iBAAwC;AAAA,EACnD,EAAE,MAAM,iBAAiB,OAAO,GAAG,QAAQ,sCAAsC;AAAA,EACjF,EAAE,MAAM,kBAAkB,OAAO,GAAG,QAAQ,gCAAgC;AAAA,EAC5E,EAAE,MAAM,sBAAsB,OAAO,GAAG,QAAQ,oCAAoC;AACtF;AAGO,SAAS,oBAA8B;AAC5C,SAAO,eAAe,IAAI,CAAC,UAAU,MAAM,IAAI;AACjD;;;ACpBO,IAAM,mBAAmB,CAAC,SAAS,QAAQ,MAAM;AAGjD,SAAS,iBAAiB,SAA2B;AAC1D,SAAQ,iBAAwC,SAAS,OAAO;AAClE;;;ACNA,SAAS,cAAAE,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,QAAAC,aAAY;;;ACOrB,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,QAAAC,aAAY;AASrB,IAAM,yBAAyB,CAAC,sBAAsB,2BAA2B;AAGjF,IAAM,aAAa,CAAC,qBAAqB,eAAe;AAcxD,SAAS,YAAY,cAAgC;AACnD,MAAI;AACJ,MAAI;AACF,aAAS,WAAWC,cAAa,cAAc,MAAM,GAAG,YAAY;AAAA,EACtE,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,MAAI,OAAO,OAAO,YAAY,SAAU,QAAO,CAAC,OAAO,OAAO;AAC9D,MAAI,MAAM,QAAQ,OAAO,OAAO,GAAG;AACjC,WAAO,OAAO,QAAQ,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAAA,EACpF;AACA,SAAO,CAAC;AACV;AAEO,SAAS,sBAAsB,WAAmC;AACvE,MAAI;AACJ,aAAW,aAAa,YAAY;AAClC,UAAM,eAAeC,MAAK,WAAW,SAAS;AAC9C,QAAI,CAACC,YAAW,YAAY,EAAG;AAC/B,iBAAa;AACb,UAAM,gBAAgB,YAAY,YAAY;AAC9C,UAAM,cAAc,cAAc;AAAA,MAAK,CAAC,UACtC,uBAAuB,KAAK,CAAC,WAAW,MAAM,SAAS,MAAM,CAAC;AAAA,IAChE;AACA,QAAI,aAAa;AACf,aAAO,EAAE,MAAM,WAAW,QAAQ,eAAe;AAAA,IACnD;AAAA,EACF;AACA,MAAI,SAAU,QAAO,EAAE,MAAM,UAAU,QAAQ,cAAc;AAC7D,SAAO,EAAE,MAAM,iBAAiB,QAAQ,OAAO;AACjD;;;ADzDA,IAAM,kBAAkB;AAgBxB,SAAS,iBAAiB,OAA0B;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,CAAC,KAAK;AAC5C,MAAI,MAAM,QAAQ,KAAK;AACrB,WAAO,MAAM,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC3E,SAAO,CAAC;AACV;AAEA,IAAM,cAAc,CAAC,gBAAiD;AAAA,EACpE;AAAA,EACA,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,OAAO,CAAC;AACV;AAGO,SAAS,qBAAqB,KAA+B;AAClE,QAAM,SAAS,sBAAsB,GAAG;AACxC,MAAI,OAAO,WAAW,UAAU,CAACC,YAAWC,MAAK,KAAK,OAAO,IAAI,CAAC,GAAG;AACnE,WAAO,YAAY,OAAO,WAAW,SAAS,OAAO,OAAO,IAAI;AAAA,EAClE;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,WAAWC,cAAaD,MAAK,KAAK,OAAO,IAAI,GAAG,MAAM,GAAG,OAAO,IAAI;AAAA,EAC/E,QAAQ;AACN,WAAO,YAAY,OAAO,IAAI;AAAA,EAChC;AAEA,QAAM,SACJ,iBAAiB,OAAO,OAAO,EAAE,KAAK,CAAC,UAAU,gBAAgB,KAAK,KAAK,CAAC,KAAK;AACnF,MAAI,WAAW,KAAM,QAAO,YAAY,OAAO,IAAI;AAEnD,QAAM,QAAQ,gBAAgB,OAAO,eAAe;AACpD,SAAO,EAAE,YAAY,OAAO,MAAM,QAAQ,SAAS,MAAM,YAAY,MAAM,WAAW,GAAG,MAAM;AACjG;;;ANtCA,IAAM,mBAAmB,EAAE,WAAW,8BAA8B;AAQpE,IAAM,eACJ;AAGF,IAAM,0BAA0B;AAahC,IAAM,gBAAgB;AAQf,SAAS,iBAAiB,QAAiC;AAChE,QAAM,cAA+B,CAAC;AACtC,aAAW,OAAO,OAAO,MAAM,IAAI,GAAG;AACpC,UAAM,IAAI,cAAc,KAAK,IAAI,KAAK,CAAC;AACvC,QAAI,GAAG;AACL,kBAAY,KAAK;AAAA,QACf,MAAM,EAAE,CAAC;AAAA,QACT,MAAM,OAAO,EAAE,CAAC,CAAC;AAAA,QACjB,KAAK,OAAO,EAAE,CAAC,CAAC;AAAA,QAChB,MAAM,EAAE,CAAC;AAAA,QACT,SAAS,EAAE,CAAC;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAQA,IAAM,4BACJ,gEAA2D,kBAAkB,EAAE,KAAK,IAAI,CAAC;AAS3F,SAAS,eAAe,SAAkB,QAA0B;AAClE,QAAM,QACJ,OAAO,YAAY,WACf,CAAC,OAAO,IACR,MAAM,QAAQ,OAAO,IACnB,QAAQ,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,IACpE,CAAC;AACT,SAAO,MAAM,SAAS,MAAM,IAAI,QAAQ,CAAC,GAAG,OAAO,MAAM;AAC3D;AAEO,IAAM,aAAN,cAAyB,YAAY;AAAA,EACjC,SAAiB;AAAA,EACjB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASlB,UAAU,UAA4B;AACpC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,KAAK,SAAoC;AAIvC,QAAI,CAAC,iBAAiB,QAAQ,OAAO,GAAG;AACtC,aAAO;AAAA,QACL,YAAY,CAAC;AAAA,QACb,OAAO,CAAC,8CAA8C,QAAQ,OAAO,OAAO;AAAA,MAC9E;AAAA,IACF;AACA,UAAM,SAAS,sBAAsB,QAAQ,GAAG;AAChD,UAAM,SAAS,4BAA4B,QAAQ,OAAO;AAC1D,UAAM,YAAY,KAAK,yBAAyB,QAAQ,GAAG;AAE3D,QAAI,OAAO,WAAW,eAAe;AACnC,aAAO;AAAA,QACL,YAAY,CAAC;AAAA,QACb,OAAO,CAAC,YAAY,OAAO,IAAI,gDAAgD;AAAA,MACjF;AAAA,IACF;AAEA,QAAI,OAAO,WAAW,QAAQ;AAC5B,YAAM,WAAW,KAAK,UAAU,EAAE,SAAS,QAAQ,SAAS,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI;AAClF,aAAO;AAAA,QACL,YAAY,CAAC,EAAE,MAAM,SAAS,MAAM,OAAO,MAAM,SAAS,GAAG,SAAS;AAAA,QACtE,OAAO,CAAC,WAAW,OAAO,IAAI,wBAAwB,YAAY;AAAA,MACpE;AAAA,IACF;AAOA,UAAM,WAAW;AAAA,MACfE,cAAaC,MAAK,QAAQ,KAAK,OAAO,IAAI,GAAG,MAAM;AAAA,MACnD,OAAO;AAAA,IACT;AACA,UAAM,eAAe,eAAe,SAAS,SAAS,MAAM;AAC5D,UAAM,QAAQ,gBAAgB,SAAS,eAAe;AACtD,UAAM,aAA8B;AAAA,MAClC,EAAE,MAAM,cAAc,MAAM,OAAO,MAAM,OAAO,EAAE,SAAS,aAAa,EAAE;AAAA,IAC5E;AACA,UAAM,QAAkB,CAAC;AACzB,QAAI,MAAM,SAAS,GAAG;AACpB,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,MAAM,OAAO;AAAA,QACb,MAAM,MAAM,IAAI,CAAC,QAAQ,CAAC,mBAAmB,GAAG,CAAC;AAAA,MACnD,CAAC;AACD,YAAM,KAAK,0CAA0C,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,IACzE;AACA,eAAW,KAAK,SAAS;AACzB,UAAM,KAAK,YAAY;AACvB,WAAO,EAAE,YAAY,MAAM;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,yBAAyB,KAA4B;AAC3D,QAAIC,YAAWD,MAAK,KAAK,cAAc,CAAC,GAAG;AACzC,aAAO,EAAE,MAAM,cAAc,MAAM,gBAAgB,OAAO,EAAE,SAAS,iBAAiB,EAAE;AAAA,IAC1F;AACA,UAAM,OAAO,kBAAkB,GAAG,KAAK,SAAS,GAAG;AACnD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,SAAS,MAAM,SAAS,iBAAiB;AAAA,IAC1D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,IAAI,KAAyC;AACjD,UAAM,SAAS,KAAK,gBAAgB,IAAI,GAAG;AAC3C,QAAI,CAAC,QAAQ;AACX,cAAQ,OAAO,MAAM,kDAAkD;AACvE,aAAO,EAAE,IAAI,MAAM,MAAM,EAAE;AAAA,IAC7B;AACA,YAAQ,OAAO,MAAM,GAAG,QAAQ,QAAQ,MAAM,EAAE,KAAK,yBAAyB,CAAC;AAAA,CAAI;AACnF,UAAM,MAAM,WAAW,IAAI,KAAK,KAAK,KAAK;AAC1C,UAAM,SAAS,UAAU,KAAK,CAAC,MAAM,MAAM,GAAG,EAAE,KAAK,IAAI,KAAK,OAAO,UAAU,CAAC;AAChF,QAAI,OAAO,OAAO;AAChB,cAAQ,OAAO;AAAA,QACb,gDAAgD,OAAO,MAAM,OAAO;AAAA;AAAA,MACtE;AACA,aAAO,EAAE,IAAI,OAAO,MAAM,EAAE;AAAA,IAC9B;AACA,UAAM,OAAO,OAAO,UAAU;AAC9B,WAAO,EAAE,IAAI,SAAS,GAAG,KAAK;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,gBAAgB,KAA4B;AAClD,QAAIC,YAAWD,MAAK,KAAK,eAAe,CAAC,EAAG,QAAO;AACnD,UAAM,SAAS,sBAAsB,GAAG;AACxC,WAAO,OAAO,WAAW,SAAS,OAAO,OAAO;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAQ,KAAmC;AAC/C,UAAM,SAAS,sBAAsB,IAAI,GAAG;AAI5C,UAAM,EAAE,QAAQ,QAAQ,IAAI,qBAAqB,IAAI,GAAG;AACxD,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,QAAQ;AAAA,MACR,SAAS,IAAI;AAAA,MACb,YAAY,OAAO;AAAA,MACnB,aAAa,OAAO;AAAA,MACpB;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAO,KAAwC;AACnD,UAAM,EAAE,SAAS,QAAQ,YAAY,MAAM,IAAI,qBAAqB,IAAI,GAAG;AAC3E,WAAO,EAAE,SAAS,QAAQ,YAAY,MAAM;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAO,KAAyC;AACpD,UAAM,SAAS,KAAK,gBAAgB,IAAI,GAAG;AAC3C,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,IAAI,MAAM,MAAM,GAAG,SAAS,EAAE,QAAQ,GAAG,aAAa,YAAY,aAAa,CAAC,EAAE,EAAE;AAAA,IAC/F;AACA,UAAM,MAAM,WAAW,IAAI,KAAK,KAAK,KAAK;AAC1C,UAAM,SAAS,UAAU,KAAK,CAAC,MAAM,MAAM,GAAG,EAAE,KAAK,IAAI,KAAK,UAAU,OAAO,CAAC;AAChF,QAAI,OAAO,OAAO;AAChB,cAAQ,OAAO;AAAA,QACb,gDAAgD,OAAO,MAAM,OAAO;AAAA;AAAA,MACtE;AACA,aAAO,EAAE,IAAI,OAAO,MAAM,GAAG,SAAS,EAAE,OAAO,oBAAoB,EAAE;AAAA,IACvE;AACA,UAAM,SAAS,GAAG,OAAO,UAAU,EAAE,GAAG,OAAO,UAAU,EAAE;AAC3D,UAAM,UAAU,OAAO,MAAM,cAAc,KAAK,CAAC,GAAG;AAIpD,UAAM,SAAS,iBAAiB,MAAM;AACtC,UAAM,MACJ,IAAI,mBAAmB,IAAI,WAAY,IAAI,kBAAkB;AAC/D,UAAM,cAAc,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,GAAG,IAAI;AAClE,WAAO;AAAA,MACL,IAAI,WAAW;AAAA,MACf,MAAM,OAAO,UAAU;AAAA,MACvB,SAAS;AAAA,QACP;AAAA,QACA,aAAa,KAAK,kBAAkB,IAAI,KAAK,KAAK,QAAQ,MAAM;AAAA,QAChE;AAAA,QACA,sBAAsB,YAAY,SAAS,OAAO;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,kBACN,KACA,KACA,QACA,YACqB;AACrB,WAAO,KAAK,qBAAqB,KAAK,KAAK,QAAQ,UAAU,KACxD,WAAW,MAAM,iBAAiB,KAAK,CAAC,GAAG,SAC5C;AAAA,EACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,qBACN,KACA,KACA,QACA,YACS;AACT,UAAM,QAAQ,UAAU,KAAK,CAAC,MAAM,QAAQ,cAAc,GAAG,EAAE,KAAK,UAAU,OAAO,CAAC;AACtF,QAAI,MAAM,WAAW,KAAK,MAAM,QAAQ;AACtC,UAAI;AACF,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN;AAAA,QACF,EAAE;AACF,YAAI,GAAI,QAAO,GAAG,iBAAiB,GAAG,WAAW;AAAA,MACnD,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,iBAAiB,KAAK,UAAU;AAAA,EACzC;AACF;;;AQ1WO,SAAS,qBAA2B;AACzC,WAAS,IAAI,WAAW,CAAC;AACzB,mBAAiB,cAAc,KAAK;AACtC;;;ACSO,SAAS,mBAAyB;AACvC,qBAAmB;AACrB;;;ACCA,IAAM,oBAA6E;AAAA,EACjF,EAAE,SAAS,QAAQ,YAAY,eAAe;AAAA,EAC9C,EAAE,SAAS,SAAS,YAAY,QAAQ;AAAA,EACxC,EAAE,SAAS,UAAU,YAAY,SAAS;AAC5C;AASA,IAAM,gBAAgB,CAAC,SAAS,WAAW,OAAO,WAAW,YAAY;AAmBlE,SAAS,kBAAkB,aAAsD;AACtF,QAAM,eAAe,EAAE,GAAG,YAAY,cAAc,GAAG,YAAY,gBAAgB;AACnF,QAAM,UAAU,kBAAkB,OAAO,CAAC,MAAM,EAAE,cAAc,YAAY;AAC5E,QAAM,SAAS,QAAQ,CAAC;AACxB,QAAM,UAAmB,QAAQ,WAAW;AAC5C,QAAM,SAAS,QAAQ,cAAc;AAErC,QAAM,sBAAsB,QAAQ,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU;AACpE,QAAM,eACJ,YAAY,WAAW,YAAY,WAC/B,cAAc,OAAO,CAAC,SAAS,QAAQ,YAAY,IACnD,CAAC;AACP,QAAM,YAAY,CAAC,GAAG,qBAAqB,GAAG,YAAY;AAC1D,SAAO,EAAE,SAAS,QAAQ,WAAW,UAAU,SAAS,GAAG,UAAU;AACvE;AAGO,SAAS,gBAAgB,aAA2C;AACzE,SAAO,kBAAkB,WAAW,EAAE;AACxC;;;AChEO,SAAS,YAAY,SAAiB,OAAyB;AACpE,QAAM,UAAU,IAAI,IAAI,QAAQ,MAAM,IAAI,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC;AACtE,QAAM,UAAU,MAAM,OAAO,CAAC,SAAS,CAAC,QAAQ,IAAI,KAAK,KAAK,CAAC,CAAC;AAChE,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,SAAS,QAAQ,WAAW,KAAK,QAAQ,SAAS,IAAI,IAAI,UAAU,UAAU;AACpF,SAAO,SAAS,QAAQ,KAAK,IAAI,IAAI;AACvC;AAGA,SAAS,QAAQ,MAAwB;AACvC,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,SAAO,KAAK,QAAQ,OAAO,EAAE,EAAE,MAAM,IAAI;AAC3C;AAOO,SAAS,UAAU,QAAgB,OAAyB;AACjE,QAAM,OAAO,QAAQ,MAAM;AAC3B,QAAM,KAAK,QAAQ,KAAK;AAGxB,QAAM,MAAkB,MAAM;AAAA,IAAK,EAAE,QAAQ,KAAK,SAAS,EAAE;AAAA,IAAG,MAC9D,IAAI,MAAc,GAAG,SAAS,CAAC,EAAE,KAAK,CAAC;AAAA,EACzC;AACA,QAAM,OAAO,CAACE,IAAWC,OAAsB,IAAID,EAAC,IAAIC,EAAC,KAAK;AAC9D,WAASD,KAAI,KAAK,SAAS,GAAGA,MAAK,GAAGA,MAAK;AACzC,UAAM,MAAM,IAAIA,EAAC;AACjB,QAAI,CAAC,IAAK;AACV,aAASC,KAAI,GAAG,SAAS,GAAGA,MAAK,GAAGA,MAAK;AACvC,UAAIA,EAAC,IAAI,KAAKD,EAAC,MAAM,GAAGC,EAAC,IAAI,KAAKD,KAAI,GAAGC,KAAI,CAAC,IAAI,IAAI,KAAK,IAAI,KAAKD,KAAI,GAAGC,EAAC,GAAG,KAAKD,IAAGC,KAAI,CAAC,CAAC;AAAA,IAC/F;AAAA,EACF;AAEA,QAAM,MAAgB,CAAC;AACvB,MAAI,IAAI;AACR,MAAI,IAAI;AACR,SAAO,IAAI,KAAK,UAAU,IAAI,GAAG,QAAQ;AACvC,QAAI,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG;AACrB,UAAI,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,EAAE;AAC7B;AACA;AAAA,IACF,WAAW,KAAK,IAAI,GAAG,CAAC,KAAK,KAAK,GAAG,IAAI,CAAC,GAAG;AAC3C,UAAI,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,EAAE;AAC7B;AAAA,IACF,OAAO;AACL,UAAI,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,EAAE;AAC3B;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI,KAAK,OAAQ,KAAI,KAAK,KAAK,KAAK,GAAG,KAAK,EAAE,EAAE;AACvD,SAAO,IAAI,GAAG,OAAQ,KAAI,KAAK,KAAK,GAAG,GAAG,KAAK,EAAE,EAAE;AACnD,SAAO;AACT;;;AC7CA,SAAS,cAAAC,aAAY,gBAAAC,eAAc,YAAY,qBAAqB;AACpE,SAAS,WAAAC,UAAS,WAAW;AAE7B,SAAS,YAAY,cAAc;;;ACjB5B,SAAS,cAAc,OAAkD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;ADsBA,SAAS,kBAAkB,KAAa,cAA8B;AACpE,QAAM,OAAOC,SAAQ,GAAG;AACxB,QAAM,eAAeA,SAAQ,MAAM,YAAY;AAC/C,MAAI,iBAAiB,QAAQ,CAAC,aAAa,WAAW,OAAO,GAAG,GAAG;AACjE,UAAM,IAAI,MAAM,+CAA+C,YAAY,IAAI;AAAA,EACjF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,cAA0C;AAC9D,SAAOC,YAAW,YAAY,IAAIC,cAAa,cAAc,MAAM,IAAI;AACzE;AAGA,UAAU,OACR,OACA,SAAmB,CAAC,GACwB;AAC5C,aAAW,CAAC,KAAK,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACnD,UAAM,OAAO,CAAC,GAAG,QAAQ,GAAG;AAC5B,QAAI,cAAc,QAAQ,EAAG,QAAO,OAAO,UAAU,IAAI;AAAA,QACpD,OAAM,CAAC,MAAM,QAAQ;AAAA,EAC5B;AACF;AAOA,SAAS,WAAW,SAAiB,OAAwC;AAC3E,MAAI,OAAO,QAAQ,KAAK,EAAE,SAAS,IAAI,UAAU;AACjD,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,KAAK,GAAG;AACxC,UAAM,QAAQ,OAAO,MAAM,MAAM,MAAM;AAAA,MACrC,mBAAmB,EAAE,cAAc,MAAM,SAAS,EAAE;AAAA,IACtD,CAAC;AACD,WAAO,WAAW,MAAM,KAAK;AAAA,EAC/B;AACA,SAAO,KAAK,SAAS,IAAI,IAAI,OAAO,OAAO;AAC7C;AAGA,SAAS,gBAAgB,SAAiB,MAA0B;AAClE,MAAI,OAAO,QAAQ,KAAK,EAAE,SAAS,IAAI,UAAU;AACjD,aAAW,QAAQ,MAAM;AACvB,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAW;AAAA,MAC1C,mBAAmB,EAAE,cAAc,MAAM,SAAS,EAAE;AAAA,IACtD,CAAC;AACD,WAAO,WAAW,MAAM,KAAK;AAAA,EAC/B;AACA,SAAO,KAAK,SAAS,IAAI,IAAI,OAAO,OAAO;AAC7C;AAGA,SAAS,iBAAiB,SAAiB,WAAkC;AAC3E,UAAQ,UAAU,MAAM;AAAA,IACtB,KAAK;AACH,aAAO,UAAU;AAAA,IACnB,KAAK;AACH,aAAO,WAAW,SAAS,UAAU,KAAK;AAAA,IAC5C,KAAK;AACH,aAAO,YAAY,SAAS,UAAU,KAAK;AAAA,IAC7C,KAAK;AACH,aAAO,gBAAgB,SAAS,UAAU,IAAI;AAAA,IAChD,SAAS;AAEP,YAAM,cAAqB;AAC3B,YAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,WAAW,CAAC,EAAE;AAAA,IAC1E;AAAA,EACF;AACF;AAgBO,SAAS,YAAY,KAAa,MAAkC;AACzE,QAAM,WAAW,oBAAI,IAA0B;AAC/C,aAAW,aAAa,KAAK,YAAY;AACvC,UAAM,eAAe,kBAAkB,KAAK,UAAU,IAAI;AAC1D,UAAM,WAAW,SAAS,IAAI,UAAU,IAAI;AAC5C,UAAM,SAAS,UAAU,UAAU,aAAa,YAAY,KAAK;AACjE,UAAM,UAAU,UAAU,SAAS;AACnC,aAAS,IAAI,UAAU,MAAM;AAAA,MAC3B,MAAM,UAAU;AAAA,MAChB;AAAA,MACA;AAAA,MACA,OAAO,iBAAiB,SAAS,SAAS;AAAA,IAC5C,CAAC;AAAA,EACH;AACA,SAAO,CAAC,GAAG,SAAS,OAAO,CAAC;AAC9B;AAOA,SAAS,gBAAgB,cAAsB,UAAwB;AACrE,QAAM,WAAW,GAAG,YAAY,aAAa,QAAQ,GAAG;AACxD,gBAAc,UAAU,QAAQ;AAChC,aAAW,UAAU,YAAY;AACnC;AAMO,SAAS,UAAU,KAAa,MAA4B;AACjE,QAAM,WAAW,YAAY,KAAK,IAAI;AACtC,aAAW,QAAQ,SAAU,iBAAgB,KAAK,cAAc,KAAK,KAAK;AAC1E,SAAO,SAAS,IAAI,CAAC,SAAS,KAAK,IAAI;AACzC;;;AEhHA,SAAS,eAAe,MAAgC;AACtD,MAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,QAAM,YAAY,kBAAkB,uBAAuB,KAAK,GAAG,CAAC;AACpE,MAAI,UAAU,WAAW;AACvB,UAAM,OAAO,QAAQ,QAAQ,MAAM;AACnC,YAAQ,OAAO;AAAA,MACb,KAAK;AAAA,QACH,6CAA6C,UAAU,OAAO,WAAW,UAAU,MAAM,iBAAiB,UAAU,UAAU,KAAK,IAAI,CAAC;AAAA,MAC1I,IAAI;AAAA,IACN;AAAA,EACF;AACA,SAAO,UAAU;AACnB;AAQA,SAAS,YAAY,MAAuB,MAA0B;AAGpE,QAAM,UAAU,YAAY,KAAK,KAAK,IAAI,EAAE,OAAO,CAAC,SAAS,KAAK,WAAW,KAAK,KAAK;AACvF,MAAI,KAAK,MAAM;AACb,YAAQ,OAAO;AAAA,MACb,KAAK;AAAA,QACH;AAAA,UACE,QAAQ;AAAA,UACR,OAAO,KAAK,SAAS,CAAC;AAAA,UACtB,OAAO,QAAQ,IAAI,CAAC,EAAE,MAAM,QAAQ,MAAM,OAAO;AAAA,YAC/C;AAAA,YACA,QAAQ,OAAO,WAAW,IAAI,WAAW;AAAA,YACzC;AAAA,YACA;AAAA,UACF,EAAE;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,MACF,IAAI;AAAA,IACN;AACA,WAAO;AAAA,EACT;AAEA,UAAQ,OAAO,MAAM,+BAA+B;AACpD,aAAW,QAAQ,KAAK,SAAS,CAAC,EAAG,SAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,CAAI;AACvE,MAAI,QAAQ,WAAW,GAAG;AACxB,YAAQ,OAAO,MAAM,uBAAuB;AAC5C,WAAO;AAAA,EACT;AACA,aAAW,EAAE,MAAM,QAAQ,MAAM,KAAK,SAAS;AAC7C,UAAM,SAAS,OAAO,WAAW,IAAI,WAAW;AAChD,YAAQ,OAAO,MAAM;AAAA,IAAO,MAAM,IAAI,IAAI;AAAA,CAAI;AAC9C,eAAW,QAAQ,UAAU,QAAQ,KAAK,EAAG,SAAQ,OAAO,MAAM,OAAO,IAAI;AAAA,CAAI;AAAA,EACnF;AACA,SAAO;AACT;AAEA,eAAsB,SAAS,MAAwC;AAGrE,MAAI,KAAK,SAAS,UAAU;AAC1B,UAAM,IAAI,MAAM,qCAAqC,KAAK,IAAI,2BAA2B;AAAA,EAC3F;AACA,QAAM,UAAU,QAAQ,KAAK,QAAQ,KAAK,SAAS,KAAK,MAAM;AAC9D,QAAM,UAAU,eAAe,IAAI;AAInC,QAAM,UAAyB,EAAE,KAAK,KAAK,KAAK,QAAQ;AACxD,QAAM,OAAO,MAAM,QAAQ,KAAK,OAAO;AACvC,MAAI,KAAK,QAAQ;AAEf,WAAO,YAAY,MAAM,IAAI;AAAA,EAC/B;AACA,QAAM,UAAU,UAAU,KAAK,KAAK,IAAI;AACxC,aAAW,QAAQ,QAAS,SAAQ,OAAO,MAAM,WAAW,IAAI;AAAA,CAAI;AACpE,aAAW,QAAQ,KAAK,SAAS,CAAC,EAAG,SAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,CAAI;AACvE,SAAO;AACT;","names":["existsSync","readFileSync","join","existsSync","dirname","join","existsSync","readFileSync","join","existsSync","readFileSync","join","readFileSync","join","existsSync","existsSync","join","readFileSync","readFileSync","join","existsSync","i","j","existsSync","readFileSync","resolve","resolve","existsSync","readFileSync"]}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|