@hublo/sentinel 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,14 +59,17 @@ export default react
59
59
  ```
60
60
 
61
61
  ```jsonc
62
- // tsconfig.json — extends the preset; only project-specific paths/include stay local
62
+ // tsconfig.json — COMPOSES the repo base with the sentinel preset (see "Composition
63
+ // & precedence"); only project-specific paths/include stay local
63
64
  {
64
- "extends": "@hublo/sentinel/typescript/react",
65
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"],
65
66
  "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } },
66
67
  "include": ["src"]
67
68
  }
68
69
  ```
69
70
 
71
+ > A module can extend the preset alone (`"extends": "@hublo/sentinel/tsconfig/react"`) when there is no repo base. Compose with the base when the repo owns structure (a monorepo `paths` map) the preset should not, see "Composition & precedence".
72
+
70
73
  And the app's `package.json` scripts route every check through the one CLI (run from the app dir, sentinel scopes to it):
71
74
 
72
75
  ```jsonc
@@ -174,6 +177,63 @@ The rules live in `sentinel`. Each module keeps a **thin, generated stub** per t
174
177
  - **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.
175
178
  - **Runner binaries** (`eslint`, `typescript`, `vite`, `vitest`, …) are **peer dependencies** so they install once and stay resolvable by the editor + nx, while sentinel still dictates their versions.
176
179
 
180
+ ## Composition & precedence
181
+
182
+ A repo often has a base config that is **structural**, not just tooling, e.g. a
183
+ monorepo `tsconfig.base.json` carrying the workspace `paths` map. sentinel does **not**
184
+ replace it; it **composes** with it, so the structure survives and sentinel owns the
185
+ standards:
186
+
187
+ ```jsonc
188
+ "extends": ["../../tsconfig.base.json", "@hublo/sentinel/tsconfig/react"]
189
+ ```
190
+
191
+ **Precedence — sentinel wins.** With array `extends` (TS 5.0), entries merge in order
192
+ and the **last one wins**; the module's own `compilerOptions` win over both. So it is
193
+ always **base → sentinel → module-local**. sentinel is listed **last on purpose**:
194
+ it is a **reference** (a source of truth), not a suggestion the base can silently
195
+ veto. For any option sentinel declares, sentinel's value is the effective one.
196
+
197
+ For a module composing the base, the resolved config (`tsc --showConfig`) looks like:
198
+
199
+ | Option | Effective value | Owner |
200
+ | ----------------------------- | -------------------- | ------------------------------------------ |
201
+ | `strict`, `jsx`, decorators | preset's | **sentinel** (quality + framework) |
202
+ | `module` / `moduleResolution` | `esnext` / `bundler` | **sentinel** |
203
+ | `target` / `lib` / `paths` | the repo's | **base** (environment; sentinel won't set) |
204
+ | `noImplicitAny` | **off** (phase 1) | **base**, the preset defers it (see below) |
205
+
206
+ - **sentinel owns quality + framework semantics** (`strict`, `jsx`, decorators, module
207
+ system). The base can never quietly undo those, which is what makes sentinel a
208
+ reliable reference.
209
+ - **the base owns the environment** (`paths`, `target`, `lib`, project references).
210
+ sentinel does **not** set these, `extends` REPLACES arrays rather than merging, so
211
+ overriding `lib`/`target` would drop the repo's DOM libs or flip class-field emit.
212
+
213
+ **Least astonishment / phased strictness.** So a reference doesn't turn every rule on
214
+ the instant a module adopts it, sentinel **phases** the rules that would surface new
215
+ errors (`noImplicitAny`, `noUnusedLocals`, `noUnusedParameters`). They are kept
216
+ **commented** in the preset (visible, never silently dropped) and announced by a
217
+ runtime **warning**. `strict` stays on (so a standalone consumer still gets
218
+ `noImplicitAny`), but a repo base that relaxes `noImplicitAny` keeps it off until that
219
+ relaxation is removed. First adoption is therefore a **non-breaking lateral move**;
220
+ the rules are enabled **centrally** in a later wave.
221
+
222
+ **Removing the base is two decoupled moves, and neither loses quality:**
223
+
224
+ 1. **Compose** (`extends: [base, preset]`). Quality flips to sentinel **immediately**,
225
+ because it wins. The base's own tooling options are still present but now **dead
226
+ config** (overridden).
227
+ 2. **Slim the base later** to structure only (`paths` + references). This moves nothing
228
+ quality-wise, sentinel already won in step 1, so it is safe. The base is never fully
229
+ removed while it still owns `paths`.
230
+
231
+ **The discipline this demands:** sentinel must **explicitly declare every quality
232
+ option it means to own.** An option sentinel forgets to set falls through to the base,
233
+ a silent gap in the reference. (Be deliberate with array/replace options like `types`:
234
+ setting them **overrides** the base's rather than merging, so only own them when you
235
+ mean to.)
236
+
177
237
  ## CLI
178
238
 
179
239
  ```
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  resolveBin
10
- } from "../chunk-4VULEIWZ.js";
10
+ } from "../chunk-6AT72CEW.js";
11
11
 
12
12
  // bin/sentinel.ts
13
13
  import { existsSync } from "fs";
@@ -142,6 +142,16 @@ function presetOwnedKeys(compilerOptions) {
142
142
  return Object.keys(compilerOptions).filter((key) => !PERMITTED_COMPILER_OPTIONS.includes(key));
143
143
  }
144
144
 
145
+ // src/roles/typescript/phased-rules.ts
146
+ var DEFERRED_RULES = [
147
+ { rule: "noImplicitAny", phase: 2, reason: "the implicit-any migration (TS70xx)" },
148
+ { rule: "noUnusedLocals", phase: 2, reason: "unused-local cleanup (TS6133)" },
149
+ { rule: "noUnusedParameters", phase: 2, reason: "unused-parameter cleanup (TS6133)" }
150
+ ];
151
+ function deferredRuleNames() {
152
+ return DEFERRED_RULES.map((entry) => entry.rule);
153
+ }
154
+
145
155
  // src/roles/typescript/presets.ts
146
156
  var SHIPPED_FLAVOURS = ["react", "nest", "node"];
147
157
  function hasShippedPreset(flavour) {
@@ -186,7 +196,7 @@ function resolveTsconfigTarget(moduleDir) {
186
196
 
187
197
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
188
198
  var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
189
- var PHASED_STRICTNESS_WARNING = "sentinel typescript: phase 1 strictness \u2014 enforcing noImplicitAny; noUnusedLocals/noUnusedParameters are deferred to a later migration.";
199
+ var PHASED_STRICTNESS_WARNING = `sentinel typescript: phase 1 (non-breaking) \u2014 deferred: ${deferredRuleNames().join(", ")}. Enabled centrally in a later wave; run \`sentinel --inspect --typescript\` for the list.`;
190
200
  function composeExtends(current, preset) {
191
201
  const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
192
202
  return chain.includes(preset) ? chain : [...chain, preset];
@@ -318,7 +328,11 @@ var TscAdapter = class extends BaseAdapter {
318
328
  const target = resolveTsconfigTarget(cwd);
319
329
  return target.reason === "none" ? null : target.path;
320
330
  }
321
- /** The module's resolved TypeScript config: which preset, which file, and how. */
331
+ /**
332
+ * The module's resolved TypeScript config: which preset, which file, how, and the
333
+ * phased-strictness state (`deferred` rules that are off in phase 1). This is the
334
+ * "list what's deferred" query, `sentinel --inspect --typescript`.
335
+ */
322
336
  async inspect(ctx) {
323
337
  const target = resolveTsconfigTarget(ctx.cwd);
324
338
  return {
@@ -327,7 +341,9 @@ var TscAdapter = class extends BaseAdapter {
327
341
  flavour: ctx.flavour,
328
342
  configFile: target.path,
329
343
  configState: target.reason,
330
- preset: target.reason === "none" ? null : `@hublo/sentinel/tsconfig/${ctx.flavour}`
344
+ preset: target.reason === "none" ? null : `@hublo/sentinel/tsconfig/${ctx.flavour}`,
345
+ phase: 1,
346
+ deferred: DEFERRED_RULES
331
347
  };
332
348
  }
333
349
  /**
@@ -624,4 +640,4 @@ export {
624
640
  detectFramework,
625
641
  dispatch
626
642
  };
627
- //# sourceMappingURL=chunk-4VULEIWZ.js.map
643
+ //# sourceMappingURL=chunk-6AT72CEW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/registry.ts","../src/core/base-adapter.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/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 * 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 { Adapter, AdapterResult, RunContext, UpdateContext, UpdatePlan } 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","/**\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 four verbs onto tsc for one module:\n * - plan (--update) → thin conformant tsconfig stub extending the preset\n * - run (--run) → `tsc --noEmit`, 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 */\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 type {\n AdapterResult,\n FileOperation,\n RunContext,\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 { resolveTsconfigTarget } from '../../resolve-tsconfig-target.js'\n\nconst TYPECHECK_SCRIPT = { typecheck: 'sentinel --run --typescript' }\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)`],\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 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(`${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 return {\n module: ctx.module,\n target: 'typescript',\n flavour: ctx.flavour,\n configFile: target.path,\n configState: target.reason,\n preset: target.reason === 'none' ? null : `@hublo/sentinel/tsconfig/${ctx.flavour}`,\n phase: 1,\n deferred: DEFERRED_RULES,\n }\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: 0 } }\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 // TS70xx is the whole implicit-`any` family (param, binding, index, variable),\n // not just TS7006, so the count reflects the real migration size.\n const implicitAny = (output.match(/error TS70\\d\\d/g) ?? []).length\n return { ok: errors === 0, code: result.status ?? 0, metrics: { errors, implicitAny } }\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 * 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`. Adding a framework is one entry here.\n */\nconst FRAMEWORK_SIGNALS: ReadonlyArray<{ flavour: Flavour; dependency: string }> = [\n { flavour: 'nest', dependency: '@nestjs/core' },\n { flavour: 'svelte', dependency: 'svelte' },\n { flavour: 'react', dependency: 'react' },\n]\n\n/** The flavour for a module, from its dependencies. `node` when no framework. */\nexport function detectFramework(packageJson: PackageDependencies): Flavour {\n const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }\n for (const { flavour, dependency } of FRAMEWORK_SIGNALS) {\n if (dependency in dependencies) return flavour\n }\n return 'node'\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 * Verb dispatch. Maps the 4 CLI verbs onto adapter methods, uniformly for every\n * target/runner. This is the whole \"engine\": resolve an adapter, build a\n * normalized context, call the matching method. Tool branches only add adapters.\n *\n * The engine owns IO: for `--update`/`--inspect` it resolves the flavour (explicit\n * `--flavour`, else `detectFramework` on the module's package.json) and hands the\n * adapter a normalized context; the adapter plans, the engine applies (`applyPlan`).\n * `--run`/`--report` hand the adapter a `RunContext` to point its tool at the module.\n */\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { diffLines } from '../shared/text.js'\nimport { applyPlan, preparePlan } from './apply-plan.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { Flavour, Target, Verb } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext, UpdateContext, UpdatePlan } from './types.js'\n\nexport type { Verb }\n\nexport interface DispatchOptions {\n verb: Verb\n target: Target\n runner?: string\n module: string\n cwd: string\n /**\n * Declared flavour, when known (`--flavour`). `--run`/`--report` don't need it;\n * `--update`/`--inspect` resolve it, falling back to `detectFramework`.\n */\n flavour?: Flavour\n ci: boolean\n fix: boolean\n /** `--dry-run`: for `--update`, 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/** The flavour for update/inspect: the explicit one, else detected from deps. */\nfunction resolveFlavour(opts: DispatchOptions): Flavour {\n return opts.flavour ?? detectFramework(readProjectPackageJson(opts.cwd))\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 const adapter = resolve(opts.target, opts.flavour, opts.runner)\n const flavour = resolveFlavour(opts)\n const ctx: RunContext = {\n module: opts.module,\n cwd: opts.cwd,\n flavour,\n ci: opts.ci,\n fix: opts.fix,\n }\n\n switch (opts.verb) {\n case 'run': {\n const res = await adapter.run(ctx)\n return res.code\n }\n case 'inspect': {\n const config = await adapter.inspect(ctx)\n process.stdout.write(JSON.stringify(config, null, 2) + '\\n')\n return 0\n }\n case 'update': {\n // Engine resolves + normalizes the context; the adapter plans, the engine\n // applies the operations against the module root.\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 case 'report': {\n const res = await adapter.report(ctx)\n if (res.metrics) {\n process.stdout.write(JSON.stringify(res.metrics, null, 2) + '\\n')\n }\n return res.code\n }\n default: {\n // Exhaustiveness: every Verb is handled above. If this line ever fails to\n // compile, a new verb was added without a case here.\n const unreachable: never = opts.verb\n throw new Error(`Unknown verb: ${String(unreachable)}`)\n }\n }\n}\n"],"mappings":"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","names":["existsSync","readFileSync","join","existsSync","dirname","join","existsSync","readFileSync","join","readFileSync","join","existsSync","readFileSync","join","existsSync","i","j","existsSync","readFileSync","resolve","resolve","existsSync","readFileSync"]}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-4VULEIWZ.js";
10
+ } from "./chunk-6AT72CEW.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
@@ -2,17 +2,12 @@
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
3
  "compilerOptions": {
4
4
  "strict": true,
5
- "noImplicitAny": true,
6
5
  "noFallthroughCasesInSwitch": true,
7
6
  "forceConsistentCasingInFileNames": true,
8
7
  "esModuleInterop": true,
9
8
  "skipLibCheck": true,
10
- "target": "ES2022",
11
9
  "module": "esnext",
12
10
  "moduleResolution": "bundler",
13
- "lib": [
14
- "ES2022"
15
- ],
16
11
  "types": [
17
12
  "node"
18
13
  ],
@@ -2,17 +2,12 @@
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
3
  "compilerOptions": {
4
4
  "strict": true,
5
- "noImplicitAny": true,
6
5
  "noFallthroughCasesInSwitch": true,
7
6
  "forceConsistentCasingInFileNames": true,
8
7
  "esModuleInterop": true,
9
8
  "skipLibCheck": true,
10
- "target": "ES2022",
11
9
  "module": "esnext",
12
10
  "moduleResolution": "bundler",
13
- "lib": [
14
- "ES2022"
15
- ],
16
11
  "types": [
17
12
  "node"
18
13
  ],
@@ -2,7 +2,6 @@
2
2
  "$schema": "https://json.schemastore.org/tsconfig",
3
3
  "compilerOptions": {
4
4
  "strict": true,
5
- "noImplicitAny": true,
6
5
  "noFallthroughCasesInSwitch": true,
7
6
  "forceConsistentCasingInFileNames": true,
8
7
  "esModuleInterop": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
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",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/registry.ts","../src/core/base-adapter.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/presets.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 * 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 { Adapter, AdapterResult, RunContext, UpdateContext, UpdatePlan } 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","/**\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 four verbs onto tsc for one module:\n * - plan (--update) → thin conformant tsconfig stub extending the preset\n * - run (--run) → `tsc --noEmit`, 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 */\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 type {\n AdapterResult,\n FileOperation,\n RunContext,\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 { hasShippedPreset } from '../../presets.js'\nimport { resolveTsconfigTarget } from '../../resolve-tsconfig-target.js'\n\nconst TYPECHECK_SCRIPT = { typecheck: 'sentinel --run --typescript' }\n\n/**\n * Phase-1 strictness notice. The preset enforces `noImplicitAny` now and defers\n * `noUnusedLocals`/`noUnusedParameters` to a later migration (see the base preset),\n * so a run stays focused on implicit-`any`. Surfaced so the reduced strictness is\n * visible in logs, never a silent gap.\n */\nconst PHASED_STRICTNESS_WARNING =\n 'sentinel typescript: phase 1 strictness — enforcing noImplicitAny; noUnusedLocals/noUnusedParameters are deferred to a later migration.'\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)`],\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 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(`${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 /** The module's resolved TypeScript config: which preset, which file, and how. */\n async inspect(ctx: RunContext): Promise<unknown> {\n const target = resolveTsconfigTarget(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: target.reason === 'none' ? null : `@hublo/sentinel/tsconfig/${ctx.flavour}`,\n }\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: 0 } }\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 // TS70xx is the whole implicit-`any` family (param, binding, index, variable),\n // not just TS7006, so the count reflects the real migration size.\n const implicitAny = (output.match(/error TS70\\d\\d/g) ?? []).length\n return { ok: errors === 0, code: result.status ?? 0, metrics: { errors, implicitAny } }\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 * 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 * 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`. Adding a framework is one entry here.\n */\nconst FRAMEWORK_SIGNALS: ReadonlyArray<{ flavour: Flavour; dependency: string }> = [\n { flavour: 'nest', dependency: '@nestjs/core' },\n { flavour: 'svelte', dependency: 'svelte' },\n { flavour: 'react', dependency: 'react' },\n]\n\n/** The flavour for a module, from its dependencies. `node` when no framework. */\nexport function detectFramework(packageJson: PackageDependencies): Flavour {\n const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }\n for (const { flavour, dependency } of FRAMEWORK_SIGNALS) {\n if (dependency in dependencies) return flavour\n }\n return 'node'\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 * Verb dispatch. Maps the 4 CLI verbs onto adapter methods, uniformly for every\n * target/runner. This is the whole \"engine\": resolve an adapter, build a\n * normalized context, call the matching method. Tool branches only add adapters.\n *\n * The engine owns IO: for `--update`/`--inspect` it resolves the flavour (explicit\n * `--flavour`, else `detectFramework` on the module's package.json) and hands the\n * adapter a normalized context; the adapter plans, the engine applies (`applyPlan`).\n * `--run`/`--report` hand the adapter a `RunContext` to point its tool at the module.\n */\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { diffLines } from '../shared/text.js'\nimport { applyPlan, preparePlan } from './apply-plan.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { Flavour, Target, Verb } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext, UpdateContext, UpdatePlan } from './types.js'\n\nexport type { Verb }\n\nexport interface DispatchOptions {\n verb: Verb\n target: Target\n runner?: string\n module: string\n cwd: string\n /**\n * Declared flavour, when known (`--flavour`). `--run`/`--report` don't need it;\n * `--update`/`--inspect` resolve it, falling back to `detectFramework`.\n */\n flavour?: Flavour\n ci: boolean\n fix: boolean\n /** `--dry-run`: for `--update`, 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/** The flavour for update/inspect: the explicit one, else detected from deps. */\nfunction resolveFlavour(opts: DispatchOptions): Flavour {\n return opts.flavour ?? detectFramework(readProjectPackageJson(opts.cwd))\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 const adapter = resolve(opts.target, opts.flavour, opts.runner)\n const flavour = resolveFlavour(opts)\n const ctx: RunContext = {\n module: opts.module,\n cwd: opts.cwd,\n flavour,\n ci: opts.ci,\n fix: opts.fix,\n }\n\n switch (opts.verb) {\n case 'run': {\n const res = await adapter.run(ctx)\n return res.code\n }\n case 'inspect': {\n const config = await adapter.inspect(ctx)\n process.stdout.write(JSON.stringify(config, null, 2) + '\\n')\n return 0\n }\n case 'update': {\n // Engine resolves + normalizes the context; the adapter plans, the engine\n // applies the operations against the module root.\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 case 'report': {\n const res = await adapter.report(ctx)\n if (res.metrics) {\n process.stdout.write(JSON.stringify(res.metrics, null, 2) + '\\n')\n }\n return res.code\n }\n default: {\n // Exhaustiveness: every Verb is handled above. If this line ever fails to\n // compile, a new verb was added without a case here.\n const unreachable: never = opts.verb\n throw new Error(`Unknown verb: ${String(unreachable)}`)\n }\n }\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;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;;;AC3EO,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;AACF;;;ACpBA,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;;;AC3DA,SAAS,iBAAiB;AAC1B,SAAS,cAAAA,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,UAAU,QAAAC,aAAY;;;ACF/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;;;ACTO,IAAM,mBAAmB,CAAC,SAAS,QAAQ,MAAM;AAGjD,SAAS,iBAAiB,SAA2B;AAC1D,SAAQ,iBAAwC,SAAS,OAAO;AAClE;;;ACEA,SAAS,cAAAE,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;;;ALjDA,IAAM,mBAAmB,EAAE,WAAW,8BAA8B;AAQpE,IAAM,4BACJ;AAQF,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,sBAAsB;AAAA,MACtD;AAAA,IACF;AAOA,UAAM,WAAW;AAAA,MACfC,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,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,yBAAyB;AAAA,CAAI;AACrD,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,EAGA,MAAM,QAAQ,KAAmC;AAC/C,UAAM,SAAS,sBAAsB,IAAI,GAAG;AAC5C,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,QAAQ;AAAA,MACR,SAAS,IAAI;AAAA,MACb,YAAY,OAAO;AAAA,MACnB,aAAa,OAAO;AAAA,MACpB,QAAQ,OAAO,WAAW,SAAS,OAAO,4BAA4B,IAAI,OAAO;AAAA,IACnF;AAAA,EACF;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,EAAE,EAAE;AAAA,IACrE;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;AAGpD,UAAM,eAAe,OAAO,MAAM,iBAAiB,KAAK,CAAC,GAAG;AAC5D,WAAO,EAAE,IAAI,WAAW,GAAG,MAAM,OAAO,UAAU,GAAG,SAAS,EAAE,QAAQ,YAAY,EAAE;AAAA,EACxF;AACF;;;AMnOO,SAAS,qBAA2B;AACzC,WAAS,IAAI,WAAW,CAAC;AACzB,mBAAiB,cAAc,KAAK;AACtC;;;ACSO,SAAS,mBAAyB;AACvC,qBAAmB;AACrB;;;ACAA,IAAM,oBAA6E;AAAA,EACjF,EAAE,SAAS,QAAQ,YAAY,eAAe;AAAA,EAC9C,EAAE,SAAS,UAAU,YAAY,SAAS;AAAA,EAC1C,EAAE,SAAS,SAAS,YAAY,QAAQ;AAC1C;AAGO,SAAS,gBAAgB,aAA2C;AACzE,QAAM,eAAe,EAAE,GAAG,YAAY,cAAc,GAAG,YAAY,gBAAgB;AACnF,aAAW,EAAE,SAAS,WAAW,KAAK,mBAAmB;AACvD,QAAI,cAAc,aAAc,QAAO;AAAA,EACzC;AACA,SAAO;AACT;;;ACzBO,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,SAAO,KAAK,WAAW,gBAAgB,uBAAuB,KAAK,GAAG,CAAC;AACzE;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;AACrE,QAAM,UAAU,QAAQ,KAAK,QAAQ,KAAK,SAAS,KAAK,MAAM;AAC9D,QAAM,UAAU,eAAe,IAAI;AACnC,QAAM,MAAkB;AAAA,IACtB,QAAQ,KAAK;AAAA,IACb,KAAK,KAAK;AAAA,IACV;AAAA,IACA,IAAI,KAAK;AAAA,IACT,KAAK,KAAK;AAAA,EACZ;AAEA,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,OAAO;AACV,YAAM,MAAM,MAAM,QAAQ,IAAI,GAAG;AACjC,aAAO,IAAI;AAAA,IACb;AAAA,IACA,KAAK,WAAW;AACd,YAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG;AACxC,cAAQ,OAAO,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,IAAI;AAC3D,aAAO;AAAA,IACT;AAAA,IACA,KAAK,UAAU;AAGb,YAAM,UAAyB,EAAE,KAAK,KAAK,KAAK,QAAQ;AACxD,YAAM,OAAO,MAAM,QAAQ,KAAK,OAAO;AACvC,UAAI,KAAK,QAAQ;AAEf,eAAO,YAAY,MAAM,IAAI;AAAA,MAC/B;AACA,YAAM,UAAU,UAAU,KAAK,KAAK,IAAI;AACxC,iBAAW,QAAQ,QAAS,SAAQ,OAAO,MAAM,WAAW,IAAI;AAAA,CAAI;AACpE,iBAAW,QAAQ,KAAK,SAAS,CAAC,EAAG,SAAQ,OAAO,MAAM,KAAK,IAAI;AAAA,CAAI;AACvE,aAAO;AAAA,IACT;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,QAAQ,OAAO,GAAG;AACpC,UAAI,IAAI,SAAS;AACf,gBAAQ,OAAO,MAAM,KAAK,UAAU,IAAI,SAAS,MAAM,CAAC,IAAI,IAAI;AAAA,MAClE;AACA,aAAO,IAAI;AAAA,IACb;AAAA,IACA,SAAS;AAGP,YAAM,cAAqB,KAAK;AAChC,YAAM,IAAI,MAAM,iBAAiB,OAAO,WAAW,CAAC,EAAE;AAAA,IACxD;AAAA,EACF;AACF;","names":["existsSync","readFileSync","join","existsSync","dirname","join","existsSync","readFileSync","join","readFileSync","join","existsSync","readFileSync","join","existsSync","i","j","existsSync","readFileSync","resolve","resolve","existsSync","readFileSync"]}