@hublo/sentinel 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +20 -4
- package/dist/bin/sentinel.js +45 -6
- package/dist/bin/sentinel.js.map +1 -1
- package/dist/{chunk-G7NVAZSZ.js → chunk-UG74KTIU.js} +64 -9
- package/dist/chunk-UG74KTIU.js.map +1 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-G7NVAZSZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -244,6 +244,7 @@ sentinel <verb> [type] [options]
|
|
|
244
244
|
VERBS --run execute the target's tool
|
|
245
245
|
--inspect show the resolved configuration (incl. deferred rules)
|
|
246
246
|
--report metrics and health
|
|
247
|
+
--status adoption + conformity (coverage + drift), read from configs
|
|
247
248
|
--update generate/apply the config stubs (writes; one module only)
|
|
248
249
|
|
|
249
250
|
TYPES --lint --format --typescript --build --test
|
|
@@ -263,13 +264,28 @@ OPTIONS --module <name> from the root: scope to one module
|
|
|
263
264
|
EXAMPLES sentinel --run --typescript # in a module → that module
|
|
264
265
|
sentinel --report --typescript --module bff-admin # from root → one module
|
|
265
266
|
sentinel --report # from root → all types, all modules
|
|
266
|
-
sentinel --
|
|
267
|
+
sentinel --status --typescript # from root → adoption coverage
|
|
268
|
+
sentinel --status --ci # from root → fail CI on drift
|
|
267
269
|
sentinel --update --typescript --flavour react # write stubs for the current module
|
|
268
270
|
```
|
|
269
271
|
|
|
270
|
-
`--run`/`--inspect`/`--report` share one context rule (developer from a
|
|
271
|
-
from the root for a name / affected / all); `--update` writes, so it targets
|
|
272
|
-
module only (adopting every module at once is refused, adopt gradually).
|
|
272
|
+
`--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
|
|
273
|
+
module, or from the root for a name / affected / all); `--update` writes, so it targets
|
|
274
|
+
one module only (adopting every module at once is refused, adopt gradually).
|
|
275
|
+
|
|
276
|
+
**`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
|
|
277
|
+
flavour guessing): for each module it reads the committed `tsconfig` `extends` chain and
|
|
278
|
+
reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
|
|
279
|
+
it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
|
|
280
|
+
footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
|
|
281
|
+
adopted module has drifted.
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
✓ host-admin (react) typescript — adopted (react) conformant
|
|
285
|
+
✗ some-bff (nest) typescript — adopted (nest) drift: strict, target
|
|
286
|
+
· legacy-app (node) typescript — not adopted
|
|
287
|
+
coverage: 12/40 adopted · 11/12 conformant · 1 drifted
|
|
288
|
+
```
|
|
273
289
|
|
|
274
290
|
## Repository layout
|
|
275
291
|
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
registerAdapters,
|
|
10
10
|
resolve,
|
|
11
11
|
resolveBin
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-UG74KTIU.js";
|
|
13
13
|
|
|
14
14
|
// bin/sentinel.ts
|
|
15
15
|
import { program } from "commander";
|
|
@@ -105,7 +105,7 @@ function resolveContext(cwd2, opts2) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// src/core/domain.ts
|
|
108
|
-
var VERBS = ["run", "inspect", "update", "report"];
|
|
108
|
+
var VERBS = ["run", "inspect", "update", "report", "status"];
|
|
109
109
|
var TARGETS = [
|
|
110
110
|
"lint",
|
|
111
111
|
"format",
|
|
@@ -162,9 +162,14 @@ async function analyse(params) {
|
|
|
162
162
|
data: result.metrics ?? {}
|
|
163
163
|
});
|
|
164
164
|
worstCode = Math.max(worstCode, result.code);
|
|
165
|
-
} else {
|
|
165
|
+
} else if (params.verb === "inspect") {
|
|
166
166
|
const config = await adapter.inspect(ctx);
|
|
167
167
|
results.push({ project: module.name, target, flavour: flavour2, ok: true, data: config });
|
|
168
|
+
} else {
|
|
169
|
+
const status = await adapter.status(ctx);
|
|
170
|
+
const ok = !status.adopted || status.conformant;
|
|
171
|
+
results.push({ project: module.name, target, flavour: flavour2, ok, data: status });
|
|
172
|
+
if (!ok) worstCode = Math.max(worstCode, 1);
|
|
168
173
|
}
|
|
169
174
|
} catch (error) {
|
|
170
175
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -212,19 +217,49 @@ function renderSummary(item, p) {
|
|
|
212
217
|
}
|
|
213
218
|
return lines.join("\n");
|
|
214
219
|
}
|
|
220
|
+
function statusCoverage(items) {
|
|
221
|
+
const adopted = items.filter((i) => i.adopted === true);
|
|
222
|
+
const conformant = adopted.filter((i) => i.conformant === true);
|
|
223
|
+
return {
|
|
224
|
+
total: items.length,
|
|
225
|
+
adopted: adopted.length,
|
|
226
|
+
conformant: conformant.length,
|
|
227
|
+
drifted: adopted.length - conformant.length
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function presetShort(preset) {
|
|
231
|
+
return typeof preset === "string" ? preset.replace("@hublo/sentinel/tsconfig/", "") : "?";
|
|
232
|
+
}
|
|
233
|
+
function renderStatusRow(item, p) {
|
|
234
|
+
const adopted = item.adopted === true;
|
|
235
|
+
const conformant = item.conformant === true;
|
|
236
|
+
const mark = !adopted ? p.dim("\xB7") : conformant ? p.ok("\u2713") : p.fail("\u2717");
|
|
237
|
+
const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
|
|
238
|
+
if (!adopted) return `${head} ${p.dim("\u2014 not adopted")}`;
|
|
239
|
+
if (conformant) {
|
|
240
|
+
return `${head} ${p.dim("\u2014")} ${p.ok("adopted")} ${p.dim(`(${presetShort(item.preset)}) conformant`)}`;
|
|
241
|
+
}
|
|
242
|
+
const drift = Array.isArray(item.drift) ? item.drift.join(", ") : "";
|
|
243
|
+
return `${head} ${p.dim("\u2014")} ${p.ok("adopted")} ${p.dim(`(${presetShort(item.preset)})`)} ${p.fail(`drift: ${drift}`)}`;
|
|
244
|
+
}
|
|
245
|
+
function renderStatusSummary(items, p) {
|
|
246
|
+
const c = statusCoverage(items);
|
|
247
|
+
const drift = c.drifted > 0 ? p.fail(` \xB7 ${c.drifted} drifted`) : "";
|
|
248
|
+
return ` ${p.strong("coverage:")} ${c.adopted}/${c.total} adopted ${p.dim("\xB7")} ${c.conformant}/${c.adopted} conformant${drift}`;
|
|
249
|
+
}
|
|
215
250
|
|
|
216
251
|
// bin/sentinel.ts
|
|
217
252
|
program.name("sentinel").description("One CLI that guards code health: presets, analysis, and arch checks.").version(readOwnVersion()).configureHelp({ sortOptions: false }).showSuggestionAfterError(true).showHelpAfterError('(run "sentinel --help" for usage)').addHelpText(
|
|
218
253
|
"before",
|
|
219
254
|
[
|
|
220
255
|
"A check composes: verb + type + location.",
|
|
221
|
-
" verb what to do: --run --inspect --report --update",
|
|
256
|
+
" verb what to do: --run --inspect --report --status --update",
|
|
222
257
|
" type which check: --lint --typescript ... (omit = all types; or --all)",
|
|
223
258
|
" where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
|
|
224
259
|
" --ci (affected), or all modules. --update targets one module only.",
|
|
225
260
|
""
|
|
226
261
|
].join("\n")
|
|
227
|
-
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--update", "generate/apply the config stubs").option("--report", "metrics and health report").option("--lint", "linting").option("--format", "formatting").option("--typescript", "type checking").option("--build", "build").option("--test", "tests").option("--static-analysis", "cycles, complexity, duplication, centrality").option("--runtime-analysis", "bundle, Lighthouse, web vitals").option("--arch", "architecture boundaries").option("--all", "every target").option(
|
|
262
|
+
).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--update", "generate/apply the config stubs").option("--report", "metrics and health report").option("--status", "adoption + conformity across modules (coverage + drift)").option("--lint", "linting").option("--format", "formatting").option("--typescript", "type checking").option("--build", "build").option("--test", "tests").option("--static-analysis", "cycles, complexity, duplication, centrality").option("--runtime-analysis", "bundle, Lighthouse, web vitals").option("--arch", "architecture boundaries").option("--all", "every target").option(
|
|
228
263
|
"--module <name>",
|
|
229
264
|
"from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
|
|
230
265
|
).option("--flavour <name>", `override the detected stack preset (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--update)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").addHelpText(
|
|
@@ -296,7 +331,11 @@ async function runVerb() {
|
|
|
296
331
|
});
|
|
297
332
|
const summary = generateSummaries(results);
|
|
298
333
|
if (opts.json && verb !== "run") {
|
|
299
|
-
|
|
334
|
+
const payload = verb === "status" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
|
|
335
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
336
|
+
} else if (verb === "status") {
|
|
337
|
+
for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + "\n");
|
|
338
|
+
process.stdout.write(renderStatusSummary(summary.results, out) + "\n");
|
|
300
339
|
} else {
|
|
301
340
|
for (const item of summary.results) {
|
|
302
341
|
process.stdout.write(renderSummary(item, out) + "\n");
|
package/dist/bin/sentinel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../bin/sentinel.ts","../../src/core/context.ts","../../src/core/discover-modules.ts","../../src/core/settings.ts","../../src/core/domain.ts","../../src/core/orchestrate.ts","../../src/core/render.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: a command composes verb × type × location.\n *\n * Exactly one verb (--run / --inspect / --report / --update); a type (--typescript …)\n * or ALL when none is named; and a location resolved by `resolveContext` (a module\n * directory, or the workspace root with --module / --ci / all). The CLI is generic: it\n * resolves each target's adapter (honouring --runner) and dispatches; tools arrive as\n * adapters in later tickets. The flavour is detected from a module's dependencies\n * (deterministic, `node` default) and can be overridden with --flavour (recommended\n * for --update).\n */\nimport { program } from 'commander'\n\nimport { registerAdapters } from '../src/adapters.js'\nimport { resolveContext } from '../src/core/context.js'\nimport { FLAVOURS, TARGETS, VERBS, type Flavour, type Target } from '../src/core/domain.js'\nimport { dispatch } from '../src/core/dispatch.js'\nimport { analyse, generateSummaries } from '../src/core/orchestrate.js'\nimport { renderSummary } from '../src/core/render.js'\nimport { palette } from '../src/shared/color.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\nprogram\n .name('sentinel')\n .description('One CLI that guards code health: presets, analysis, and arch checks.')\n .version(readOwnVersion())\n .configureHelp({ sortOptions: false })\n // On any parse error (unknown flag, missing value): suggest the closest flag and\n // point at --help, so a typo like `--lnt` gets \"did you mean --lint?\".\n .showSuggestionAfterError(true)\n .showHelpAfterError('(run \"sentinel --help\" for usage)')\n .addHelpText(\n 'before',\n [\n 'A check composes: verb + type + location.',\n ' verb what to do: --run --inspect --report --update',\n ' type which check: --lint --typescript ... (omit = all types; or --all)',\n ' where run from a MODULE dir → that module; from the ROOT → --module <name>,',\n ' --ci (affected), or all modules. --update targets one module only.',\n '',\n ].join('\\n'),\n )\n // verbs (pick one)\n .option('--run', 'execute the target tool')\n .option('--inspect', 'show the resolved configuration')\n .option('--update', 'generate/apply the config stubs')\n .option('--report', 'metrics and health report')\n // targets (pick one, or --all)\n .option('--lint', 'linting')\n .option('--format', 'formatting')\n .option('--typescript', 'type checking')\n .option('--build', 'build')\n .option('--test', 'tests')\n .option('--static-analysis', 'cycles, complexity, duplication, centrality')\n .option('--runtime-analysis', 'bundle, Lighthouse, web vitals')\n .option('--arch', 'architecture boundaries')\n .option('--all', 'every target')\n // modifiers\n .option(\n '--module <name>',\n 'from the workspace root: scope to one module (omit = all; inside a module dir, drop this)',\n )\n .option('--flavour <name>', `override the detected stack preset (${FLAVOURS.join(', ')})`)\n .option('--runner <tool>', 'override the default runner (e.g. eslint, biome)')\n .option('--ci', 'CI mode: from the root, only the affected modules; non-zero exit on failure')\n .option('--fix', 'auto-fix where applicable')\n .option('--dry-run', 'preview the changes without writing (--update)')\n .option('--json', 'machine-readable JSON output (report/inspect/--dry-run)')\n .addHelpText(\n 'after',\n [\n '',\n 'Examples:',\n ' sentinel --run --typescript # in a module → that module',\n ' sentinel --report --typescript --module bff-admin # from root → one module',\n ' sentinel --report # from root → all types, all modules',\n ' sentinel --report --ci # from root → affected only',\n ' sentinel --update --typescript --flavour react # write stubs for the current module',\n ].join('\\n'),\n )\n .parse()\n\nconst opts = program.opts()\n\n// commander camelCases hyphenated flags (--static-analysis -> staticAnalysis).\nfunction toCamel(flag: string): string {\n return flag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())\n}\n\n/** Exactly one of `keys` must be flagged; return it, else fail with guidance. */\nfunction pickOne<T extends string>(kind: string, keys: readonly T[]): T {\n const chosen = keys.filter((k) => opts[toCamel(k)])\n if (chosen.length !== 1) {\n const supported = keys.map((k) => `--${k}`).join(', ')\n program.error(\n chosen.length === 0\n ? `Missing a ${kind}. Supported: ${supported}.`\n : `Pick exactly one ${kind}: got ${chosen.map((k) => `--${k}`).join(', ')}.`,\n )\n }\n return chosen[0] as T\n}\n\n/**\n * The explicit `--flavour`, VALIDATED, or undefined when not given. When undefined,\n * the flavour is DETECTED from the module's dependencies (see `detect-framework.ts`);\n * this override is recommended for `--update`, where hoisted deps can make detection\n * fall back to `node`. An unknown value is a hard error (typo caught), not a silent guess.\n */\nfunction parseFlavour(value: unknown): Flavour | undefined {\n if (value === undefined) return undefined\n if (typeof value !== 'string' || !FLAVOURS.includes(value as Flavour)) {\n return program.error(\n `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(', ')}.`,\n )\n }\n return value as Flavour\n}\n\nfunction asMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nconst verb = pickOne('verb', VERBS)\nconst cwd = process.cwd()\nconst flavour = parseFlavour(opts.flavour)\n\n// Targets (the \"type\" axis): a named one, or ALL when none is given. Uniform for every\n// verb — run / inspect / report / update. `resolveContext` owns the \"location\" axis.\nconst namedTargets = TARGETS.filter((t) => opts[toCamel(t)])\nif (opts.all && namedTargets.length > 0) {\n program.error(\n `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(', ')}.`,\n )\n}\nconst targets: Target[] = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets\n\n// --dry-run only previews a write.\nif (opts.dryRun && verb !== 'update') {\n program.error('--dry-run only applies to --update (the read verbs never write).')\n}\n\n/**\n * run / inspect / report: resolve the module context (cwd module, `--module`, all, or\n * `--ci` affected), then execute verb × targets across it and print.\n */\nasync function runVerb(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) })\n const out = palette(process.stdout)\n const err = palette(process.stderr)\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'run' | 'report' | 'inspect',\n targets,\n modules,\n runner: opts.runner,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) =>\n process.stderr.write(err.dim(` [${done}/${total}] ${name}\\n`)),\n })\n\n // One rendering path for every verb: shape each outcome into a summary row, then\n // print it colored. `run` rows carry no metrics, so they render as a bare head line;\n // `--json` bypasses the human rendering entirely.\n const summary = generateSummaries(results)\n if (opts.json && verb !== 'run') {\n process.stdout.write(JSON.stringify(summary, null, 2) + '\\n')\n } else {\n for (const item of summary.results) {\n process.stdout.write(renderSummary(item, out) + '\\n')\n }\n }\n process.stderr.write(\n err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms\\n`),\n )\n // `run` always reports failures via its exit code; report/inspect only under --ci.\n return verb === 'run' || opts.ci ? worstCode : 0\n}\n\n/**\n * update: a WRITE. Same context rule, but it must resolve to exactly ONE module (the\n * current one, or `--module`). Adopting every module at once would be a big-bang, so\n * the implicit \"all\" is deliberately refused; adopt gradually.\n */\nasync function runUpdate(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false })\n const [module] = modules\n if (scope === 'all' || scope === 'affected' || !module) {\n program.error(\n '--update writes files: target one module (run from its directory, or pass --module). ' +\n 'Adopting every module at once is intentionally not allowed — adopt gradually.',\n )\n }\n let worst = 0\n for (const type of targets) {\n try {\n const code = await dispatch({\n verb,\n target: type,\n runner: opts.runner,\n module: module.name,\n cwd: module.root,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // With --all, report per target and keep going; a single target fails hard.\n process.stderr.write(`\\nsentinel (${type}): ${asMessage(err)}\\n`)\n if (targets.length === 1) return 1\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\n\nregisterAdapters() // wire in every tool adapter; the CLI itself never lists them\n\n;(verb === 'update' ? runUpdate() : runVerb())\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`\\nsentinel: ${asMessage(err)}\\n`)\n process.exit(1)\n })\n","/**\n * Context resolution: the composable \"location\" axis of a command.\n *\n * One rule for every read verb (run / inspect / report), so a developer works the way\n * they already work with nx, from their module or from the root:\n * - in a MODULE dir (has package.json/project.json, and is NOT the workspace root):\n * the context is THAT module; `--module` is redundant there and is rejected.\n * - at the workspace ROOT: `--module X` scopes to X, `--ci` scopes to the affected\n * set, and otherwise it is every module (the whole-repo status).\n * Update (a write) reuses this but forbids the implicit \"all\" (see the CLI).\n */\nimport { existsSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { readNxProjectName } from '../shared/package-json.js'\nimport { discoverModules, type ModuleRef } from './discover-modules.js'\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\n/** A directory is a module if it carries one of these (and is not the root). */\nconst MODULE_MARKERS = ['package.json', 'project.json'] as const\n\n/** How the context was resolved, for messages and the \"all\"-guard on update. */\nexport type ContextScope = 'cwd-module' | 'named-module' | 'affected' | 'all'\n\nexport interface ResolvedContext {\n modules: ModuleRef[]\n scope: ContextScope\n}\n\nfunction isModuleDir(cwd: string): boolean {\n return MODULE_MARKERS.some((marker) => existsSync(join(cwd, marker)))\n}\n\n/**\n * Resolve which modules a command targets from where it runs + its flags. Throws with\n * an actionable message when the combination is contradictory (e.g. `--module` from\n * inside a module) or the directory is neither a module nor the workspace root.\n */\nexport function resolveContext(\n cwd: string,\n opts: { module?: string; ci?: boolean },\n): ResolvedContext {\n const atRoot = existsSync(join(cwd, WORKSPACE_ROOT_MARKER))\n\n // In a module directory: the module IS the current one.\n if (!atRoot && isModuleDir(cwd)) {\n if (opts.module) {\n throw new Error(\n 'You are in a module directory: drop --module (the context is the current module).',\n )\n }\n if (opts.ci) {\n throw new Error('--ci selects the affected set from the workspace root; run it there.')\n }\n const name = readNxProjectName(cwd) ?? basename(cwd)\n return { modules: [{ name, root: cwd }], scope: 'cwd-module' }\n }\n\n // At the workspace root: --module (one), --ci (affected), or every module.\n if (atRoot) {\n if (opts.module) {\n const found = discoverModules(cwd).find((module) => module.name === opts.module)\n if (!found) throw new Error(`module \"${opts.module}\" not found in the workspace.`)\n return { modules: [found], scope: 'named-module' }\n }\n if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: 'affected' }\n return { modules: discoverModules(cwd), scope: 'all' }\n }\n\n throw new Error(\n `Run sentinel from a module directory or the workspace root ` +\n `(found neither ${MODULE_MARKERS.join('/')} nor ${WORKSPACE_ROOT_MARKER} here).`,\n )\n}\n","/**\n * Module discovery via nx. `--report`/`--inspect` without a `--module` analyse\n * every project; `--ci` narrows to the affected ones. We ask nx (its cached graph)\n * for the authoritative names + roots in a couple of calls, not one per project,\n * so it stays fast on the real monorepo.\n */\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { resolveBin } from '../shared/resolve-bin.js'\n\n/** A discovered module: its nx project name and absolute root. */\nexport interface ModuleRef {\n name: string\n root: string\n}\n\nfunction runNx(cwd: string, args: string[]): string {\n const nx = resolveBin(cwd, 'nx') ?? 'nx'\n try {\n // Disable the daemon for deterministic, CI-friendly one-shot invocations.\n return execFileSync(nx, args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, NX_DAEMON: 'false' },\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,\n { cause: error },\n )\n }\n}\n\n/** Every module (name -> absolute root) from the nx project graph, in one call. */\nfunction readGraph(cwd: string): ModuleRef[] {\n const dir = mkdtempSync(join(tmpdir(), 'sentinel-nx-'))\n const file = join(dir, 'graph.json')\n try {\n runNx(cwd, ['graph', '--file', file])\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as {\n graph?: { nodes?: Record<string, { data: { root: string } }> }\n }\n const nodes = parsed.graph?.nodes\n if (!nodes || typeof nodes !== 'object') {\n throw new Error(\n 'sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible.',\n )\n }\n return Object.entries(nodes).map(([name, node]) => ({\n name,\n root: join(cwd, node.data.root),\n }))\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n\nexport function discoverModules(cwd: string, options: { affected?: boolean } = {}): ModuleRef[] {\n const modules = readGraph(cwd)\n if (!options.affected) return modules\n const affected = new Set(\n JSON.parse(runNx(cwd, ['show', 'projects', '--affected', '--json'])) as string[],\n )\n return modules.filter((module) => affected.has(module.name))\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the flavour is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--flavour` overrides the detection (recommended for `--update`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n","/**\n * The domain vocabulary: the fixed sets of verbs, targets, and flavours, and the\n * types derived from them. This is the model, and the extension point: adding a\n * verb / target / flavour is a one-line edit to a list here, and because the types\n * are DERIVED (`(typeof LIST)[number]`), the compiler forces every switch/handler\n * to cover the new member.\n *\n * Tunable behaviour (defaults, detection signals, marker filenames) lives in\n * `settings.ts`, not here.\n */\n\n/** Verbs: what to do. Each maps to an adapter method in dispatch. */\nexport const VERBS = ['run', 'inspect', 'update', 'report'] as const\nexport type Verb = (typeof VERBS)[number]\n\n/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */\nexport const TARGETS = [\n 'lint',\n 'format',\n 'typescript',\n 'build',\n 'test',\n 'static-analysis',\n 'runtime-analysis',\n 'arch',\n] as const\nexport type Target = (typeof TARGETS)[number]\n\n/** Flavours: the stack preset a project resolves to (strict by default). */\nexport const FLAVOURS = ['react', 'nest', 'svelte', 'node'] as const\nexport type Flavour = (typeof FLAVOURS)[number]\n","/**\n * Orchestration for `--report`/`--inspect`: analyse a set of modules across a set\n * of targets, then shape the outcomes into one versioned, parseable summary.\n *\n * The engine resolves each module's flavour and adapter and calls the per-module\n * method; a target with no adapter yet is simply skipped. `generateSummaries`\n * builds the aggregate by reusing `generateSummary` for each result (one factory,\n * no duplicated shaping between the single- and multi-module paths).\n */\nimport { palette } from '../shared/color.js'\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, Target } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext } from './types.js'\n\n/** One project × target outcome. */\nexport interface AnalyseResult {\n project: string\n target: Target\n flavour: Flavour\n ok: boolean\n /** `--report`: metrics (errors, implicit-any); `--inspect`: the resolved config. */\n data: unknown\n}\n\nexport interface AnalyseParams {\n verb: 'run' | 'report' | 'inspect'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n /** Explicit `--flavour`, when declared; otherwise detected per module from deps. */\n flavour?: Flavour\n ci: boolean\n fix: boolean\n /** Called after each module, for progress display. */\n onProgress?: (done: number, total: number, moduleName: string) => void\n}\n\n/** Run the analyse across modules × targets. Returns the results + worst exit code. */\nexport async function analyse(\n params: AnalyseParams,\n): Promise<{ results: AnalyseResult[]; worstCode: number }> {\n const results: AnalyseResult[] = []\n let worstCode = 0\n let done = 0\n\n for (const module of params.modules) {\n // Explicit --flavour wins; otherwise detect it from the module's deps.\n const flavour = params.flavour ?? detectFramework(readProjectPackageJson(module.root))\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch {\n continue // no adapter for this target yet: skip it (not a failure)\n }\n // Isolate every check: one module/target that throws must not abort the whole\n // sweep. A failure becomes a `ok:false` row (with the error) so `--report --all`\n // still returns a complete picture across a large workspace.\n try {\n if (params.verb === 'run') {\n // Label the run so multi-module output (root/--all) is readable; the tool\n // streams its own output (stdio inherit) between headers.\n if (params.modules.length > 1) {\n const err = palette(process.stderr)\n process.stderr.write(\n `\\n ${err.accent('▶')} ${err.strong(module.name)} ${err.dim(`(${flavour})`)} ${target}\\n`,\n )\n }\n const result = await adapter.run(ctx)\n results.push({ project: module.name, target, flavour, ok: result.ok, data: {} })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'report') {\n const result = await adapter.report(ctx)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n data: result.metrics ?? {},\n })\n worstCode = Math.max(worstCode, result.code)\n } else {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n results.push({ project: module.name, target, flavour, ok: false, data: { error: message } })\n worstCode = Math.max(worstCode, 1)\n }\n }\n params.onProgress?.((done += 1), params.modules.length, module.name)\n }\n\n return { results, worstCode }\n}\n\n/** The shaped summary for ONE result (the factory unit). */\nexport function generateSummary(result: AnalyseResult): Record<string, unknown> {\n const { project, target, flavour, ok, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, ...details }\n}\n\n/** The aggregate, versioned envelope for MANY results, built from `generateSummary`. */\nexport function generateSummaries(results: readonly AnalyseResult[]): {\n schemaVersion: number\n results: Record<string, unknown>[]\n} {\n return { schemaVersion: 1, results: results.map(generateSummary) }\n}\n","/**\n * Human rendering of one summary row (`--run`/`--report`/`--inspect`), colored via a\n * `Palette`. Pure and stream-agnostic: it returns the text, the CLI writes it. `--json`\n * bypasses this entirely (machines get the raw envelope).\n *\n * The goal is legibility: the pass/fail mark and the module name read at a glance, the\n * error counts stand out when non-zero, and the phased `deferred` rules become their own\n * indented block instead of an inline JSON blob.\n */\nimport type { Palette } from '../shared/color.js'\n\n/** Keys carried by the row head (or redundant with it), never repeated in the details. */\nconst HEAD_KEYS = new Set(['project', 'module', 'target', 'flavour', 'ok'])\n\n/** A deferred (phased) rule, as `--inspect` reports it. */\ninterface DeferredRule {\n rule: string\n phase: number\n reason: string\n}\n\nfunction isDeferredRules(value: unknown): value is DeferredRule[] {\n return (\n Array.isArray(value) &&\n value.every((entry) => typeof entry === 'object' && entry !== null && 'rule' in entry)\n )\n}\n\n/** Color a detail value: error counts are the signal, so red when >0, green at 0. */\nfunction renderValue(key: string, value: unknown, p: Palette): string {\n if ((key === 'errors' || key === 'implicitAny') && typeof value === 'number') {\n return value > 0 ? p.fail(String(value)) : p.ok(String(value))\n }\n return typeof value === 'string' ? value : JSON.stringify(value)\n}\n\n/**\n * One summary row → colored lines: a head line (`✓ name (flavour) target — key=value …`)\n * plus, when present, an indented `deferred` block listing the phased rules.\n */\nexport function renderSummary(item: Record<string, unknown>, p: Palette): string {\n const ok = item.ok === true\n const mark = ok ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n // Split details: the phased `deferred` rules get their own block; everything else is\n // rendered inline as dim `key=value` pairs (the head keys are dropped as redundant).\n const entries = Object.entries(item).filter(([key]) => !HEAD_KEYS.has(key))\n const deferred = entries.find(([key]) => key === 'deferred')?.[1]\n const inline = entries\n .filter(([key]) => key !== 'deferred')\n .map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`)\n .join(' ')\n\n const lines = [inline ? `${head} ${p.dim('—')} ${inline}` : head]\n\n if (isDeferredRules(deferred) && deferred.length > 0) {\n lines.push(` ${p.warn('deferred (phase 1, non-breaking):')}`)\n for (const { rule, phase, reason } of deferred) {\n lines.push(` ${p.warn('•')} ${p.strong(rule)} ${p.dim(`— phase ${phase}: ${reason}`)}`)\n }\n }\n return lines.join('\\n')\n}\n"],"mappings":"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","names":["join","cwd","cwd","join","opts","flavour"]}
|
|
1
|
+
{"version":3,"sources":["../../bin/sentinel.ts","../../src/core/context.ts","../../src/core/discover-modules.ts","../../src/core/settings.ts","../../src/core/domain.ts","../../src/core/orchestrate.ts","../../src/core/render.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * sentinel CLI: a command composes verb × type × location.\n *\n * Exactly one verb (--run / --inspect / --report / --status / --update); a type (--typescript …)\n * or ALL when none is named; and a location resolved by `resolveContext` (a module\n * directory, or the workspace root with --module / --ci / all). The CLI is generic: it\n * resolves each target's adapter (honouring --runner) and dispatches; tools arrive as\n * adapters in later tickets. The flavour is detected from a module's dependencies\n * (deterministic, `node` default) and can be overridden with --flavour (recommended\n * for --update).\n */\nimport { program } from 'commander'\n\nimport { registerAdapters } from '../src/adapters.js'\nimport { resolveContext } from '../src/core/context.js'\nimport { FLAVOURS, TARGETS, VERBS, type Flavour, type Target } from '../src/core/domain.js'\nimport { dispatch } from '../src/core/dispatch.js'\nimport { analyse, generateSummaries } from '../src/core/orchestrate.js'\nimport {\n renderStatusRow,\n renderStatusSummary,\n renderSummary,\n statusCoverage,\n} from '../src/core/render.js'\nimport { palette } from '../src/shared/color.js'\nimport { readOwnVersion } from '../src/shared/package-json.js'\n\nprogram\n .name('sentinel')\n .description('One CLI that guards code health: presets, analysis, and arch checks.')\n .version(readOwnVersion())\n .configureHelp({ sortOptions: false })\n // On any parse error (unknown flag, missing value): suggest the closest flag and\n // point at --help, so a typo like `--lnt` gets \"did you mean --lint?\".\n .showSuggestionAfterError(true)\n .showHelpAfterError('(run \"sentinel --help\" for usage)')\n .addHelpText(\n 'before',\n [\n 'A check composes: verb + type + location.',\n ' verb what to do: --run --inspect --report --status --update',\n ' type which check: --lint --typescript ... (omit = all types; or --all)',\n ' where run from a MODULE dir → that module; from the ROOT → --module <name>,',\n ' --ci (affected), or all modules. --update targets one module only.',\n '',\n ].join('\\n'),\n )\n // verbs (pick one)\n .option('--run', 'execute the target tool')\n .option('--inspect', 'show the resolved configuration')\n .option('--update', 'generate/apply the config stubs')\n .option('--report', 'metrics and health report')\n .option('--status', 'adoption + conformity across modules (coverage + drift)')\n // targets (pick one, or --all)\n .option('--lint', 'linting')\n .option('--format', 'formatting')\n .option('--typescript', 'type checking')\n .option('--build', 'build')\n .option('--test', 'tests')\n .option('--static-analysis', 'cycles, complexity, duplication, centrality')\n .option('--runtime-analysis', 'bundle, Lighthouse, web vitals')\n .option('--arch', 'architecture boundaries')\n .option('--all', 'every target')\n // modifiers\n .option(\n '--module <name>',\n 'from the workspace root: scope to one module (omit = all; inside a module dir, drop this)',\n )\n .option('--flavour <name>', `override the detected stack preset (${FLAVOURS.join(', ')})`)\n .option('--runner <tool>', 'override the default runner (e.g. eslint, biome)')\n .option('--ci', 'CI mode: from the root, only the affected modules; non-zero exit on failure')\n .option('--fix', 'auto-fix where applicable')\n .option('--dry-run', 'preview the changes without writing (--update)')\n .option('--json', 'machine-readable JSON output (report/inspect/--dry-run)')\n .addHelpText(\n 'after',\n [\n '',\n 'Examples:',\n ' sentinel --run --typescript # in a module → that module',\n ' sentinel --report --typescript --module bff-admin # from root → one module',\n ' sentinel --report # from root → all types, all modules',\n ' sentinel --report --ci # from root → affected only',\n ' sentinel --update --typescript --flavour react # write stubs for the current module',\n ].join('\\n'),\n )\n .parse()\n\nconst opts = program.opts()\n\n// commander camelCases hyphenated flags (--static-analysis -> staticAnalysis).\nfunction toCamel(flag: string): string {\n return flag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())\n}\n\n/** Exactly one of `keys` must be flagged; return it, else fail with guidance. */\nfunction pickOne<T extends string>(kind: string, keys: readonly T[]): T {\n const chosen = keys.filter((k) => opts[toCamel(k)])\n if (chosen.length !== 1) {\n const supported = keys.map((k) => `--${k}`).join(', ')\n program.error(\n chosen.length === 0\n ? `Missing a ${kind}. Supported: ${supported}.`\n : `Pick exactly one ${kind}: got ${chosen.map((k) => `--${k}`).join(', ')}.`,\n )\n }\n return chosen[0] as T\n}\n\n/**\n * The explicit `--flavour`, VALIDATED, or undefined when not given. When undefined,\n * the flavour is DETECTED from the module's dependencies (see `detect-framework.ts`);\n * this override is recommended for `--update`, where hoisted deps can make detection\n * fall back to `node`. An unknown value is a hard error (typo caught), not a silent guess.\n */\nfunction parseFlavour(value: unknown): Flavour | undefined {\n if (value === undefined) return undefined\n if (typeof value !== 'string' || !FLAVOURS.includes(value as Flavour)) {\n return program.error(\n `sentinel: unknown --flavour ${JSON.stringify(value)}. Supported: ${FLAVOURS.join(', ')}.`,\n )\n }\n return value as Flavour\n}\n\nfunction asMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nconst verb = pickOne('verb', VERBS)\nconst cwd = process.cwd()\nconst flavour = parseFlavour(opts.flavour)\n\n// Targets (the \"type\" axis): a named one, or ALL when none is given. Uniform for every\n// verb — run / inspect / report / update. `resolveContext` owns the \"location\" axis.\nconst namedTargets = TARGETS.filter((t) => opts[toCamel(t)])\nif (opts.all && namedTargets.length > 0) {\n program.error(\n `--all runs every target; drop the specific one(s): ${namedTargets.map((t) => `--${t}`).join(', ')}.`,\n )\n}\nconst targets: Target[] = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets\n\n// --dry-run only previews a write.\nif (opts.dryRun && verb !== 'update') {\n program.error('--dry-run only applies to --update (the read verbs never write).')\n}\n\n/**\n * run / inspect / report: resolve the module context (cwd module, `--module`, all, or\n * `--ci` affected), then execute verb × targets across it and print.\n */\nasync function runVerb(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) })\n const out = palette(process.stdout)\n const err = palette(process.stderr)\n\n const started = Date.now()\n const { results, worstCode } = await analyse({\n verb: verb as 'run' | 'report' | 'inspect' | 'status',\n targets,\n modules,\n runner: opts.runner,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n onProgress: (done, total, name) =>\n process.stderr.write(err.dim(` [${done}/${total}] ${name}\\n`)),\n })\n\n // Shape each outcome into a summary row, then print it colored. `run` rows carry no\n // metrics (bare head line); `status` has its own 3-state rows + a coverage footer;\n // `--json` bypasses the human rendering entirely.\n const summary = generateSummaries(results)\n if (opts.json && verb !== 'run') {\n const payload =\n verb === 'status' ? { ...summary, coverage: statusCoverage(summary.results) } : summary\n process.stdout.write(JSON.stringify(payload, null, 2) + '\\n')\n } else if (verb === 'status') {\n for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + '\\n')\n process.stdout.write(renderStatusSummary(summary.results, out) + '\\n')\n } else {\n for (const item of summary.results) {\n process.stdout.write(renderSummary(item, out) + '\\n')\n }\n }\n process.stderr.write(\n err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms\\n`),\n )\n // `run` always reports failures via its exit code; report/inspect only under --ci.\n return verb === 'run' || opts.ci ? worstCode : 0\n}\n\n/**\n * update: a WRITE. Same context rule, but it must resolve to exactly ONE module (the\n * current one, or `--module`). Adopting every module at once would be a big-bang, so\n * the implicit \"all\" is deliberately refused; adopt gradually.\n */\nasync function runUpdate(): Promise<number> {\n const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false })\n const [module] = modules\n if (scope === 'all' || scope === 'affected' || !module) {\n program.error(\n '--update writes files: target one module (run from its directory, or pass --module). ' +\n 'Adopting every module at once is intentionally not allowed — adopt gradually.',\n )\n }\n let worst = 0\n for (const type of targets) {\n try {\n const code = await dispatch({\n verb,\n target: type,\n runner: opts.runner,\n module: module.name,\n cwd: module.root,\n flavour,\n ci: Boolean(opts.ci),\n fix: Boolean(opts.fix),\n dryRun: Boolean(opts.dryRun),\n json: Boolean(opts.json),\n })\n worst = Math.max(worst, code)\n } catch (err) {\n // With --all, report per target and keep going; a single target fails hard.\n process.stderr.write(`\\nsentinel (${type}): ${asMessage(err)}\\n`)\n if (targets.length === 1) return 1\n worst = Math.max(worst, 1)\n }\n }\n return worst\n}\n\nregisterAdapters() // wire in every tool adapter; the CLI itself never lists them\n\n;(verb === 'update' ? runUpdate() : runVerb())\n .then((code) => process.exit(code))\n .catch((err: unknown) => {\n process.stderr.write(`\\nsentinel: ${asMessage(err)}\\n`)\n process.exit(1)\n })\n","/**\n * Context resolution: the composable \"location\" axis of a command.\n *\n * One rule for every read verb (run / inspect / report), so a developer works the way\n * they already work with nx, from their module or from the root:\n * - in a MODULE dir (has package.json/project.json, and is NOT the workspace root):\n * the context is THAT module; `--module` is redundant there and is rejected.\n * - at the workspace ROOT: `--module X` scopes to X, `--ci` scopes to the affected\n * set, and otherwise it is every module (the whole-repo status).\n * Update (a write) reuses this but forbids the implicit \"all\" (see the CLI).\n */\nimport { existsSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { readNxProjectName } from '../shared/package-json.js'\nimport { discoverModules, type ModuleRef } from './discover-modules.js'\nimport { WORKSPACE_ROOT_MARKER } from './settings.js'\n\n/** A directory is a module if it carries one of these (and is not the root). */\nconst MODULE_MARKERS = ['package.json', 'project.json'] as const\n\n/** How the context was resolved, for messages and the \"all\"-guard on update. */\nexport type ContextScope = 'cwd-module' | 'named-module' | 'affected' | 'all'\n\nexport interface ResolvedContext {\n modules: ModuleRef[]\n scope: ContextScope\n}\n\nfunction isModuleDir(cwd: string): boolean {\n return MODULE_MARKERS.some((marker) => existsSync(join(cwd, marker)))\n}\n\n/**\n * Resolve which modules a command targets from where it runs + its flags. Throws with\n * an actionable message when the combination is contradictory (e.g. `--module` from\n * inside a module) or the directory is neither a module nor the workspace root.\n */\nexport function resolveContext(\n cwd: string,\n opts: { module?: string; ci?: boolean },\n): ResolvedContext {\n const atRoot = existsSync(join(cwd, WORKSPACE_ROOT_MARKER))\n\n // In a module directory: the module IS the current one.\n if (!atRoot && isModuleDir(cwd)) {\n if (opts.module) {\n throw new Error(\n 'You are in a module directory: drop --module (the context is the current module).',\n )\n }\n if (opts.ci) {\n throw new Error('--ci selects the affected set from the workspace root; run it there.')\n }\n const name = readNxProjectName(cwd) ?? basename(cwd)\n return { modules: [{ name, root: cwd }], scope: 'cwd-module' }\n }\n\n // At the workspace root: --module (one), --ci (affected), or every module.\n if (atRoot) {\n if (opts.module) {\n const found = discoverModules(cwd).find((module) => module.name === opts.module)\n if (!found) throw new Error(`module \"${opts.module}\" not found in the workspace.`)\n return { modules: [found], scope: 'named-module' }\n }\n if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: 'affected' }\n return { modules: discoverModules(cwd), scope: 'all' }\n }\n\n throw new Error(\n `Run sentinel from a module directory or the workspace root ` +\n `(found neither ${MODULE_MARKERS.join('/')} nor ${WORKSPACE_ROOT_MARKER} here).`,\n )\n}\n","/**\n * Module discovery via nx. `--report`/`--inspect` without a `--module` analyse\n * every project; `--ci` narrows to the affected ones. We ask nx (its cached graph)\n * for the authoritative names + roots in a couple of calls, not one per project,\n * so it stays fast on the real monorepo.\n */\nimport { execFileSync } from 'node:child_process'\nimport { mkdtempSync, readFileSync, rmSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { resolveBin } from '../shared/resolve-bin.js'\n\n/** A discovered module: its nx project name and absolute root. */\nexport interface ModuleRef {\n name: string\n root: string\n}\n\nfunction runNx(cwd: string, args: string[]): string {\n const nx = resolveBin(cwd, 'nx') ?? 'nx'\n try {\n // Disable the daemon for deterministic, CI-friendly one-shot invocations.\n return execFileSync(nx, args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, NX_DAEMON: 'false' },\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new Error(\n `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,\n { cause: error },\n )\n }\n}\n\n/** Every module (name -> absolute root) from the nx project graph, in one call. */\nfunction readGraph(cwd: string): ModuleRef[] {\n const dir = mkdtempSync(join(tmpdir(), 'sentinel-nx-'))\n const file = join(dir, 'graph.json')\n try {\n runNx(cwd, ['graph', '--file', file])\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as {\n graph?: { nodes?: Record<string, { data: { root: string } }> }\n }\n const nodes = parsed.graph?.nodes\n if (!nodes || typeof nodes !== 'object') {\n throw new Error(\n 'sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible.',\n )\n }\n return Object.entries(nodes).map(([name, node]) => ({\n name,\n root: join(cwd, node.data.root),\n }))\n } finally {\n rmSync(dir, { recursive: true, force: true })\n }\n}\n\nexport function discoverModules(cwd: string, options: { affected?: boolean } = {}): ModuleRef[] {\n const modules = readGraph(cwd)\n if (!options.affected) return modules\n const affected = new Set(\n JSON.parse(runNx(cwd, ['show', 'projects', '--affected', '--json'])) as string[],\n )\n return modules.filter((module) => affected.has(module.name))\n}\n","/**\n * Tunable settings: the knobs you would actually change. The fixed vocabulary and\n * its types live in `domain.ts`.\n *\n * Note: the flavour is DETECTED from a module's dependencies (see\n * `detect-framework.ts`), deterministically, `node` when no framework dep is present.\n * `--flavour` overrides the detection (recommended for `--update`, since hoisted deps\n * can make detection return `node`). The detection table lives in `detect-framework.ts`.\n */\n\n/** A file that marks a workspace root; sentinel refuses to operate there. */\nexport const WORKSPACE_ROOT_MARKER = 'nx.json'\n","/**\n * The domain vocabulary: the fixed sets of verbs, targets, and flavours, and the\n * types derived from them. This is the model, and the extension point: adding a\n * verb / target / flavour is a one-line edit to a list here, and because the types\n * are DERIVED (`(typeof LIST)[number]`), the compiler forces every switch/handler\n * to cover the new member.\n *\n * Tunable behaviour (defaults, detection signals, marker filenames) lives in\n * `settings.ts`, not here.\n */\n\n/** Verbs: what to do. Each maps to an adapter method in dispatch. */\nexport const VERBS = ['run', 'inspect', 'update', 'report', 'status'] as const\nexport type Verb = (typeof VERBS)[number]\n\n/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */\nexport const TARGETS = [\n 'lint',\n 'format',\n 'typescript',\n 'build',\n 'test',\n 'static-analysis',\n 'runtime-analysis',\n 'arch',\n] as const\nexport type Target = (typeof TARGETS)[number]\n\n/** Flavours: the stack preset a project resolves to (strict by default). */\nexport const FLAVOURS = ['react', 'nest', 'svelte', 'node'] as const\nexport type Flavour = (typeof FLAVOURS)[number]\n","/**\n * Orchestration for `--report`/`--inspect`: analyse a set of modules across a set\n * of targets, then shape the outcomes into one versioned, parseable summary.\n *\n * The engine resolves each module's flavour and adapter and calls the per-module\n * method; a target with no adapter yet is simply skipped. `generateSummaries`\n * builds the aggregate by reusing `generateSummary` for each result (one factory,\n * no duplicated shaping between the single- and multi-module paths).\n */\nimport { palette } from '../shared/color.js'\nimport { readProjectPackageJson } from '../shared/package-json.js'\nimport { detectFramework } from './detect-framework.js'\nimport type { ModuleRef } from './discover-modules.js'\nimport type { Flavour, Target } from './domain.js'\nimport { resolve } from './registry.js'\nimport type { RunContext } from './types.js'\n\n/** One project × target outcome. */\nexport interface AnalyseResult {\n project: string\n target: Target\n flavour: Flavour\n ok: boolean\n /** `--report`: metrics (errors, implicit-any); `--inspect`: the resolved config. */\n data: unknown\n}\n\nexport interface AnalyseParams {\n verb: 'run' | 'report' | 'inspect' | 'status'\n targets: readonly Target[]\n modules: readonly ModuleRef[]\n runner?: string\n /** Explicit `--flavour`, when declared; otherwise detected per module from deps. */\n flavour?: Flavour\n ci: boolean\n fix: boolean\n /** Called after each module, for progress display. */\n onProgress?: (done: number, total: number, moduleName: string) => void\n}\n\n/** Run the analyse across modules × targets. Returns the results + worst exit code. */\nexport async function analyse(\n params: AnalyseParams,\n): Promise<{ results: AnalyseResult[]; worstCode: number }> {\n const results: AnalyseResult[] = []\n let worstCode = 0\n let done = 0\n\n for (const module of params.modules) {\n // Explicit --flavour wins; otherwise detect it from the module's deps.\n const flavour = params.flavour ?? detectFramework(readProjectPackageJson(module.root))\n const ctx: RunContext = {\n module: module.name,\n cwd: module.root,\n flavour,\n ci: params.ci,\n fix: params.fix,\n }\n for (const target of params.targets) {\n let adapter\n try {\n adapter = resolve(target, flavour, params.runner)\n } catch {\n continue // no adapter for this target yet: skip it (not a failure)\n }\n // Isolate every check: one module/target that throws must not abort the whole\n // sweep. A failure becomes a `ok:false` row (with the error) so `--report --all`\n // still returns a complete picture across a large workspace.\n try {\n if (params.verb === 'run') {\n // Label the run so multi-module output (root/--all) is readable; the tool\n // streams its own output (stdio inherit) between headers.\n if (params.modules.length > 1) {\n const err = palette(process.stderr)\n process.stderr.write(\n `\\n ${err.accent('▶')} ${err.strong(module.name)} ${err.dim(`(${flavour})`)} ${target}\\n`,\n )\n }\n const result = await adapter.run(ctx)\n results.push({ project: module.name, target, flavour, ok: result.ok, data: {} })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'report') {\n const result = await adapter.report(ctx)\n results.push({\n project: module.name,\n target,\n flavour,\n ok: result.ok,\n data: result.metrics ?? {},\n })\n worstCode = Math.max(worstCode, result.code)\n } else if (params.verb === 'inspect') {\n const config = await adapter.inspect(ctx)\n results.push({ project: module.name, target, flavour, ok: true, data: config })\n } else {\n // status: adoption + conformity, read from the committed config (no tool run).\n // A drifted adopted module is a conformance failure; a non-adopted one is not.\n const status = await adapter.status(ctx)\n const ok = !status.adopted || status.conformant\n results.push({ project: module.name, target, flavour, ok, data: status })\n if (!ok) worstCode = Math.max(worstCode, 1)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n results.push({ project: module.name, target, flavour, ok: false, data: { error: message } })\n worstCode = Math.max(worstCode, 1)\n }\n }\n params.onProgress?.((done += 1), params.modules.length, module.name)\n }\n\n return { results, worstCode }\n}\n\n/** The shaped summary for ONE result (the factory unit). */\nexport function generateSummary(result: AnalyseResult): Record<string, unknown> {\n const { project, target, flavour, ok, data } = result\n const details =\n data && typeof data === 'object' ? (data as Record<string, unknown>) : { value: data }\n return { project, target, flavour, ok, ...details }\n}\n\n/** The aggregate, versioned envelope for MANY results, built from `generateSummary`. */\nexport function generateSummaries(results: readonly AnalyseResult[]): {\n schemaVersion: number\n results: Record<string, unknown>[]\n} {\n return { schemaVersion: 1, results: results.map(generateSummary) }\n}\n","/**\n * Human rendering of one summary row (`--run`/`--report`/`--inspect`), colored via a\n * `Palette`. Pure and stream-agnostic: it returns the text, the CLI writes it. `--json`\n * bypasses this entirely (machines get the raw envelope).\n *\n * The goal is legibility: the pass/fail mark and the module name read at a glance, the\n * error counts stand out when non-zero, and the phased `deferred` rules become their own\n * indented block instead of an inline JSON blob.\n */\nimport type { Palette } from '../shared/color.js'\n\n/** Keys carried by the row head (or redundant with it), never repeated in the details. */\nconst HEAD_KEYS = new Set(['project', 'module', 'target', 'flavour', 'ok'])\n\n/** A deferred (phased) rule, as `--inspect` reports it. */\ninterface DeferredRule {\n rule: string\n phase: number\n reason: string\n}\n\nfunction isDeferredRules(value: unknown): value is DeferredRule[] {\n return (\n Array.isArray(value) &&\n value.every((entry) => typeof entry === 'object' && entry !== null && 'rule' in entry)\n )\n}\n\n/** Color a detail value: error counts are the signal, so red when >0, green at 0. */\nfunction renderValue(key: string, value: unknown, p: Palette): string {\n if ((key === 'errors' || key === 'implicitAny') && typeof value === 'number') {\n return value > 0 ? p.fail(String(value)) : p.ok(String(value))\n }\n return typeof value === 'string' ? value : JSON.stringify(value)\n}\n\n/**\n * One summary row → colored lines: a head line (`✓ name (flavour) target — key=value …`)\n * plus, when present, an indented `deferred` block listing the phased rules.\n */\nexport function renderSummary(item: Record<string, unknown>, p: Palette): string {\n const ok = item.ok === true\n const mark = ok ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n // Split details: the phased `deferred` rules get their own block; everything else is\n // rendered inline as dim `key=value` pairs (the head keys are dropped as redundant).\n const entries = Object.entries(item).filter(([key]) => !HEAD_KEYS.has(key))\n const deferred = entries.find(([key]) => key === 'deferred')?.[1]\n const inline = entries\n .filter(([key]) => key !== 'deferred')\n .map(([key, value]) => `${p.dim(`${key}=`)}${renderValue(key, value, p)}`)\n .join(' ')\n\n const lines = [inline ? `${head} ${p.dim('—')} ${inline}` : head]\n\n if (isDeferredRules(deferred) && deferred.length > 0) {\n lines.push(` ${p.warn('deferred (phase 1, non-breaking):')}`)\n for (const { rule, phase, reason } of deferred) {\n lines.push(` ${p.warn('•')} ${p.strong(rule)} ${p.dim(`— phase ${phase}: ${reason}`)}`)\n }\n }\n return lines.join('\\n')\n}\n\n/** Workspace adoption + conformity totals, for the `--status` footer / JSON. */\nexport interface Coverage {\n total: number\n adopted: number\n conformant: number\n drifted: number\n}\n\nexport function statusCoverage(items: readonly Record<string, unknown>[]): Coverage {\n const adopted = items.filter((i) => i.adopted === true)\n const conformant = adopted.filter((i) => i.conformant === true)\n return {\n total: items.length,\n adopted: adopted.length,\n conformant: conformant.length,\n drifted: adopted.length - conformant.length,\n }\n}\n\n/** Short preset name for display (`@hublo/sentinel/tsconfig/nest` -> `nest`). */\nfunction presetShort(preset: unknown): string {\n return typeof preset === 'string' ? preset.replace('@hublo/sentinel/tsconfig/', '') : '?'\n}\n\n/**\n * One `--status` row, three states: not-adopted (dim `·`), adopted + conformant (green\n * `✓`), adopted + drifted (red `✗`, with the drifted keys named).\n */\nexport function renderStatusRow(item: Record<string, unknown>, p: Palette): string {\n const adopted = item.adopted === true\n const conformant = item.conformant === true\n const mark = !adopted ? p.dim('·') : conformant ? p.ok('✓') : p.fail('✗')\n const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`\n\n if (!adopted) return `${head} ${p.dim('— not adopted')}`\n if (conformant) {\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)}) conformant`)}`\n }\n const drift = Array.isArray(item.drift) ? item.drift.join(', ') : ''\n return `${head} ${p.dim('—')} ${p.ok('adopted')} ${p.dim(`(${presetShort(item.preset)})`)} ${p.fail(`drift: ${drift}`)}`\n}\n\n/** The `--status` footer: `coverage: X/N adopted · K/X conformant`. */\nexport function renderStatusSummary(items: readonly Record<string, unknown>[], p: Palette): string {\n const c = statusCoverage(items)\n const drift = c.drifted > 0 ? p.fail(` · ${c.drifted} drifted`) : ''\n return ` ${p.strong('coverage:')} ${c.adopted}/${c.total} adopted ${p.dim('·')} ${c.conformant}/${c.adopted} conformant${drift}`\n}\n"],"mappings":";;;;;;;;;;;;;;AAYA,SAAS,eAAe;;;ACDxB,SAAS,kBAAkB;AAC3B,SAAS,UAAU,QAAAA,aAAY;;;ACN/B,SAAS,oBAAoB;AAC7B,SAAS,aAAa,cAAc,cAAc;AAClD,SAAS,cAAc;AACvB,SAAS,YAAY;AAUrB,SAAS,MAAMC,MAAa,MAAwB;AAClD,QAAM,KAAK,WAAWA,MAAK,IAAI,KAAK;AACpC,MAAI;AAEF,WAAO,aAAa,IAAI,MAAM;AAAA,MAC5B,KAAAA;AAAA,MACA,UAAU;AAAA,MACV,KAAK,EAAE,GAAG,QAAQ,KAAK,WAAW,QAAQ;AAAA,IAC5C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAM,IAAI;AAAA,MACR,+BAA+B,OAAO;AAAA,MACtC,EAAE,OAAO,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAGA,SAAS,UAAUA,MAA0B;AAC3C,QAAM,MAAM,YAAY,KAAK,OAAO,GAAG,cAAc,CAAC;AACtD,QAAM,OAAO,KAAK,KAAK,YAAY;AACnC,MAAI;AACF,UAAMA,MAAK,CAAC,SAAS,UAAU,IAAI,CAAC;AACpC,UAAM,SAAS,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAGpD,UAAM,QAAQ,OAAO,OAAO;AAC5B,QAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO;AAAA,MAClD;AAAA,MACA,MAAM,KAAKA,MAAK,KAAK,KAAK,IAAI;AAAA,IAChC,EAAE;AAAA,EACJ,UAAE;AACA,WAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC9C;AACF;AAEO,SAAS,gBAAgBA,MAAa,UAAkC,CAAC,GAAgB;AAC9F,QAAM,UAAU,UAAUA,IAAG;AAC7B,MAAI,CAAC,QAAQ,SAAU,QAAO;AAC9B,QAAM,WAAW,IAAI;AAAA,IACnB,KAAK,MAAM,MAAMA,MAAK,CAAC,QAAQ,YAAY,cAAc,QAAQ,CAAC,CAAC;AAAA,EACrE;AACA,SAAO,QAAQ,OAAO,CAAC,WAAW,SAAS,IAAI,OAAO,IAAI,CAAC;AAC7D;;;ACzDO,IAAM,wBAAwB;;;AFQrC,IAAM,iBAAiB,CAAC,gBAAgB,cAAc;AAUtD,SAAS,YAAYC,MAAsB;AACzC,SAAO,eAAe,KAAK,CAAC,WAAW,WAAWC,MAAKD,MAAK,MAAM,CAAC,CAAC;AACtE;AAOO,SAAS,eACdA,MACAE,OACiB;AACjB,QAAM,SAAS,WAAWD,MAAKD,MAAK,qBAAqB,CAAC;AAG1D,MAAI,CAAC,UAAU,YAAYA,IAAG,GAAG;AAC/B,QAAIE,MAAK,QAAQ;AACf,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAIA,MAAK,IAAI;AACX,YAAM,IAAI,MAAM,sEAAsE;AAAA,IACxF;AACA,UAAM,OAAO,kBAAkBF,IAAG,KAAK,SAASA,IAAG;AACnD,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,MAAMA,KAAI,CAAC,GAAG,OAAO,aAAa;AAAA,EAC/D;AAGA,MAAI,QAAQ;AACV,QAAIE,MAAK,QAAQ;AACf,YAAM,QAAQ,gBAAgBF,IAAG,EAAE,KAAK,CAAC,WAAW,OAAO,SAASE,MAAK,MAAM;AAC/E,UAAI,CAAC,MAAO,OAAM,IAAI,MAAM,WAAWA,MAAK,MAAM,+BAA+B;AACjF,aAAO,EAAE,SAAS,CAAC,KAAK,GAAG,OAAO,eAAe;AAAA,IACnD;AACA,QAAIA,MAAK,GAAI,QAAO,EAAE,SAAS,gBAAgBF,MAAK,EAAE,UAAU,KAAK,CAAC,GAAG,OAAO,WAAW;AAC3F,WAAO,EAAE,SAAS,gBAAgBA,IAAG,GAAG,OAAO,MAAM;AAAA,EACvD;AAEA,QAAM,IAAI;AAAA,IACR,6EACoB,eAAe,KAAK,GAAG,CAAC,QAAQ,qBAAqB;AAAA,EAC3E;AACF;;;AG7DO,IAAM,QAAQ,CAAC,OAAO,WAAW,UAAU,UAAU,QAAQ;AAI7D,IAAM,UAAU;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,IAAM,WAAW,CAAC,SAAS,QAAQ,UAAU,MAAM;;;ACY1D,eAAsB,QACpB,QAC0D;AAC1D,QAAM,UAA2B,CAAC;AAClC,MAAI,YAAY;AAChB,MAAI,OAAO;AAEX,aAAW,UAAU,OAAO,SAAS;AAEnC,UAAMG,WAAU,OAAO,WAAW,gBAAgB,uBAAuB,OAAO,IAAI,CAAC;AACrF,UAAM,MAAkB;AAAA,MACtB,QAAQ,OAAO;AAAA,MACf,KAAK,OAAO;AAAA,MACZ,SAAAA;AAAA,MACA,IAAI,OAAO;AAAA,MACX,KAAK,OAAO;AAAA,IACd;AACA,eAAW,UAAU,OAAO,SAAS;AACnC,UAAI;AACJ,UAAI;AACF,kBAAU,QAAQ,QAAQA,UAAS,OAAO,MAAM;AAAA,MAClD,QAAQ;AACN;AAAA,MACF;AAIA,UAAI;AACF,YAAI,OAAO,SAAS,OAAO;AAGzB,cAAI,OAAO,QAAQ,SAAS,GAAG;AAC7B,kBAAM,MAAM,QAAQ,QAAQ,MAAM;AAClC,oBAAQ,OAAO;AAAA,cACb;AAAA,IAAO,IAAI,OAAO,QAAG,CAAC,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,IAAI,IAAI,IAAI,IAAIA,QAAO,GAAG,CAAC,IAAI,MAAM;AAAA;AAAA,YACxF;AAAA,UACF;AACA,gBAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AACpC,kBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC;AAC/E,sBAAY,KAAK,IAAI,WAAW,OAAO,IAAI;AAAA,QAC7C,WAAW,OAAO,SAAS,UAAU;AACnC,gBAAM,SAAS,MAAM,QAAQ,OAAO,GAAG;AACvC,kBAAQ,KAAK;AAAA,YACX,SAAS,OAAO;AAAA,YAChB;AAAA,YACA,SAAAA;AAAA,YACA,IAAI,OAAO;AAAA,YACX,MAAM,OAAO,WAAW,CAAC;AAAA,UAC3B,CAAC;AACD,sBAAY,KAAK,IAAI,WAAW,OAAO,IAAI;AAAA,QAC7C,WAAW,OAAO,SAAS,WAAW;AACpC,gBAAM,SAAS,MAAM,QAAQ,QAAQ,GAAG;AACxC,kBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,MAAM,MAAM,OAAO,CAAC;AAAA,QAChF,OAAO;AAGL,gBAAM,SAAS,MAAM,QAAQ,OAAO,GAAG;AACvC,gBAAM,KAAK,CAAC,OAAO,WAAW,OAAO;AACrC,kBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,MAAM,OAAO,CAAC;AACxE,cAAI,CAAC,GAAI,aAAY,KAAK,IAAI,WAAW,CAAC;AAAA,QAC5C;AAAA,MACF,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,gBAAQ,KAAK,EAAE,SAAS,OAAO,MAAM,QAAQ,SAAAA,UAAS,IAAI,OAAO,MAAM,EAAE,OAAO,QAAQ,EAAE,CAAC;AAC3F,oBAAY,KAAK,IAAI,WAAW,CAAC;AAAA,MACnC;AAAA,IACF;AACA,WAAO,aAAc,QAAQ,GAAI,OAAO,QAAQ,QAAQ,OAAO,IAAI;AAAA,EACrE;AAEA,SAAO,EAAE,SAAS,UAAU;AAC9B;AAGO,SAAS,gBAAgB,QAAgD;AAC9E,QAAM,EAAE,SAAS,QAAQ,SAAAA,UAAS,IAAI,KAAK,IAAI;AAC/C,QAAM,UACJ,QAAQ,OAAO,SAAS,WAAY,OAAmC,EAAE,OAAO,KAAK;AACvF,SAAO,EAAE,SAAS,QAAQ,SAAAA,UAAS,IAAI,GAAG,QAAQ;AACpD;AAGO,SAAS,kBAAkB,SAGhC;AACA,SAAO,EAAE,eAAe,GAAG,SAAS,QAAQ,IAAI,eAAe,EAAE;AACnE;;;ACpHA,IAAM,YAAY,oBAAI,IAAI,CAAC,WAAW,UAAU,UAAU,WAAW,IAAI,CAAC;AAS1E,SAAS,gBAAgB,OAAyC;AAChE,SACE,MAAM,QAAQ,KAAK,KACnB,MAAM,MAAM,CAAC,UAAU,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,KAAK;AAEzF;AAGA,SAAS,YAAY,KAAa,OAAgB,GAAoB;AACpE,OAAK,QAAQ,YAAY,QAAQ,kBAAkB,OAAO,UAAU,UAAU;AAC5E,WAAO,QAAQ,IAAI,EAAE,KAAK,OAAO,KAAK,CAAC,IAAI,EAAE,GAAG,OAAO,KAAK,CAAC;AAAA,EAC/D;AACA,SAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;AAMO,SAAS,cAAc,MAA+B,GAAoB;AAC/E,QAAM,KAAK,KAAK,OAAO;AACvB,QAAM,OAAO,KAAK,EAAE,GAAG,QAAG,IAAI,EAAE,KAAK,QAAG;AACxC,QAAM,OAAO,KAAK,IAAI,IAAI,EAAE,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,KAAK,OAAO,GAAG,CAAC,IAAI,KAAK,MAAM;AAIrG,QAAM,UAAU,OAAO,QAAQ,IAAI,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,IAAI,GAAG,CAAC;AAC1E,QAAM,WAAW,QAAQ,KAAK,CAAC,CAAC,GAAG,MAAM,QAAQ,UAAU,IAAI,CAAC;AAChE,QAAM,SAAS,QACZ,OAAO,CAAC,CAAC,GAAG,MAAM,QAAQ,UAAU,EACpC,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,GAAG,EAAE,IAAI,GAAG,GAAG,GAAG,CAAC,GAAG,YAAY,KAAK,OAAO,CAAC,CAAC,EAAE,EACxE,KAAK,GAAG;AAEX,QAAM,QAAQ,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,IAAI,QAAG,CAAC,IAAI,MAAM,KAAK,IAAI;AAEhE,MAAI,gBAAgB,QAAQ,KAAK,SAAS,SAAS,GAAG;AACpD,UAAM,KAAK,SAAS,EAAE,KAAK,mCAAmC,CAAC,EAAE;AACjE,eAAW,EAAE,MAAM,OAAO,OAAO,KAAK,UAAU;AAC9C,YAAM,KAAK,WAAW,EAAE,KAAK,QAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,gBAAW,KAAK,KAAK,MAAM,EAAE,CAAC,EAAE;AAAA,IAC/F;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAUO,SAAS,eAAe,OAAqD;AAClF,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,EAAE,YAAY,IAAI;AACtD,QAAM,aAAa,QAAQ,OAAO,CAAC,MAAM,EAAE,eAAe,IAAI;AAC9D,SAAO;AAAA,IACL,OAAO,MAAM;AAAA,IACb,SAAS,QAAQ;AAAA,IACjB,YAAY,WAAW;AAAA,IACvB,SAAS,QAAQ,SAAS,WAAW;AAAA,EACvC;AACF;AAGA,SAAS,YAAY,QAAyB;AAC5C,SAAO,OAAO,WAAW,WAAW,OAAO,QAAQ,6BAA6B,EAAE,IAAI;AACxF;AAMO,SAAS,gBAAgB,MAA+B,GAAoB;AACjF,QAAM,UAAU,KAAK,YAAY;AACjC,QAAM,aAAa,KAAK,eAAe;AACvC,QAAM,OAAO,CAAC,UAAU,EAAE,IAAI,MAAG,IAAI,aAAa,EAAE,GAAG,QAAG,IAAI,EAAE,KAAK,QAAG;AACxE,QAAM,OAAO,KAAK,IAAI,IAAI,EAAE,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,KAAK,OAAO,GAAG,CAAC,IAAI,KAAK,MAAM;AAErG,MAAI,CAAC,QAAS,QAAO,GAAG,IAAI,IAAI,EAAE,IAAI,oBAAe,CAAC;AACtD,MAAI,YAAY;AACd,WAAO,GAAG,IAAI,IAAI,EAAE,IAAI,QAAG,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,YAAY,KAAK,MAAM,CAAC,cAAc,CAAC;AAAA,EACtG;AACA,QAAM,QAAQ,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI,IAAI;AAClE,SAAO,GAAG,IAAI,IAAI,EAAE,IAAI,QAAG,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,YAAY,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,UAAU,KAAK,EAAE,CAAC;AACxH;AAGO,SAAS,oBAAoB,OAA2C,GAAoB;AACjG,QAAM,IAAI,eAAe,KAAK;AAC9B,QAAM,QAAQ,EAAE,UAAU,IAAI,EAAE,KAAK,SAAM,EAAE,OAAO,UAAU,IAAI;AAClE,SAAO,KAAK,EAAE,OAAO,WAAW,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,KAAK,YAAY,EAAE,IAAI,MAAG,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,OAAO,cAAc,KAAK;AACjI;;;ANpFA,QACG,KAAK,UAAU,EACf,YAAY,sEAAsE,EAClF,QAAQ,eAAe,CAAC,EACxB,cAAc,EAAE,aAAa,MAAM,CAAC,EAGpC,yBAAyB,IAAI,EAC7B,mBAAmB,mCAAmC,EACtD;AAAA,EACC;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb,EAEC,OAAO,SAAS,yBAAyB,EACzC,OAAO,aAAa,iCAAiC,EACrD,OAAO,YAAY,iCAAiC,EACpD,OAAO,YAAY,2BAA2B,EAC9C,OAAO,YAAY,yDAAyD,EAE5E,OAAO,UAAU,SAAS,EAC1B,OAAO,YAAY,YAAY,EAC/B,OAAO,gBAAgB,eAAe,EACtC,OAAO,WAAW,OAAO,EACzB,OAAO,UAAU,OAAO,EACxB,OAAO,qBAAqB,6CAA6C,EACzE,OAAO,sBAAsB,gCAAgC,EAC7D,OAAO,UAAU,yBAAyB,EAC1C,OAAO,SAAS,cAAc,EAE9B;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,oBAAoB,uCAAuC,SAAS,KAAK,IAAI,CAAC,GAAG,EACxF,OAAO,mBAAmB,kDAAkD,EAC5E,OAAO,QAAQ,6EAA6E,EAC5F,OAAO,SAAS,2BAA2B,EAC3C,OAAO,aAAa,gDAAgD,EACpE,OAAO,UAAU,yDAAyD,EAC1E;AAAA,EACC;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb,EACC,MAAM;AAET,IAAM,OAAO,QAAQ,KAAK;AAG1B,SAAS,QAAQ,MAAsB;AACrC,SAAO,KAAK,QAAQ,aAAa,CAAC,GAAG,MAAc,EAAE,YAAY,CAAC;AACpE;AAGA,SAAS,QAA0B,MAAc,MAAuB;AACtE,QAAM,SAAS,KAAK,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;AAClD,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,YAAY,KAAK,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI;AACrD,YAAQ;AAAA,MACN,OAAO,WAAW,IACd,aAAa,IAAI,gBAAgB,SAAS,MAC1C,oBAAoB,IAAI,SAAS,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IAC7E;AAAA,EACF;AACA,SAAO,OAAO,CAAC;AACjB;AAQA,SAAS,aAAa,OAAqC;AACzD,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI,OAAO,UAAU,YAAY,CAAC,SAAS,SAAS,KAAgB,GAAG;AACrE,WAAO,QAAQ;AAAA,MACb,+BAA+B,KAAK,UAAU,KAAK,CAAC,gBAAgB,SAAS,KAAK,IAAI,CAAC;AAAA,IACzF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,UAAU,KAAsB;AACvC,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,IAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,IAAM,MAAM,QAAQ,IAAI;AACxB,IAAM,UAAU,aAAa,KAAK,OAAO;AAIzC,IAAM,eAAe,QAAQ,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC;AAC3D,IAAI,KAAK,OAAO,aAAa,SAAS,GAAG;AACvC,UAAQ;AAAA,IACN,sDAAsD,aAAa,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,EACpG;AACF;AACA,IAAM,UAAoB,KAAK,OAAO,aAAa,WAAW,IAAI,CAAC,GAAG,OAAO,IAAI;AAGjF,IAAI,KAAK,UAAU,SAAS,UAAU;AACpC,UAAQ,MAAM,kEAAkE;AAClF;AAMA,eAAe,UAA2B;AACxC,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;AAC5F,QAAM,MAAM,QAAQ,QAAQ,MAAM;AAClC,QAAM,MAAM,QAAQ,QAAQ,MAAM;AAElC,QAAM,UAAU,KAAK,IAAI;AACzB,QAAM,EAAE,SAAS,UAAU,IAAI,MAAM,QAAQ;AAAA,IAC3C;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,KAAK;AAAA,IACb;AAAA,IACA,IAAI,QAAQ,KAAK,EAAE;AAAA,IACnB,KAAK,QAAQ,KAAK,GAAG;AAAA,IACrB,YAAY,CAAC,MAAM,OAAO,SACxB,QAAQ,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI;AAAA,CAAI,CAAC;AAAA,EAClE,CAAC;AAKD,QAAM,UAAU,kBAAkB,OAAO;AACzC,MAAI,KAAK,QAAQ,SAAS,OAAO;AAC/B,UAAM,UACJ,SAAS,WAAW,EAAE,GAAG,SAAS,UAAU,eAAe,QAAQ,OAAO,EAAE,IAAI;AAClF,YAAQ,OAAO,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC,IAAI,IAAI;AAAA,EAC9D,WAAW,SAAS,UAAU;AAC5B,eAAW,QAAQ,QAAQ,QAAS,SAAQ,OAAO,MAAM,gBAAgB,MAAM,GAAG,IAAI,IAAI;AAC1F,YAAQ,OAAO,MAAM,oBAAoB,QAAQ,SAAS,GAAG,IAAI,IAAI;AAAA,EACvE,OAAO;AACL,eAAW,QAAQ,QAAQ,SAAS;AAClC,cAAQ,OAAO,MAAM,cAAc,MAAM,GAAG,IAAI,IAAI;AAAA,IACtD;AAAA,EACF;AACA,UAAQ,OAAO;AAAA,IACb,IAAI,IAAI,KAAK,QAAQ,MAAM,eAAe,KAAK,QAAQ,KAAK,IAAI,IAAI,OAAO;AAAA,CAAM;AAAA,EACnF;AAEA,SAAO,SAAS,SAAS,KAAK,KAAK,YAAY;AACjD;AAOA,eAAe,YAA6B;AAC1C,QAAM,EAAE,SAAS,MAAM,IAAI,eAAe,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC;AACjF,QAAM,CAAC,MAAM,IAAI;AACjB,MAAI,UAAU,SAAS,UAAU,cAAc,CAAC,QAAQ;AACtD,YAAQ;AAAA,MACN;AAAA,IAEF;AAAA,EACF;AACA,MAAI,QAAQ;AACZ,aAAW,QAAQ,SAAS;AAC1B,QAAI;AACF,YAAM,OAAO,MAAM,SAAS;AAAA,QAC1B;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,KAAK;AAAA,QACb,QAAQ,OAAO;AAAA,QACf,KAAK,OAAO;AAAA,QACZ;AAAA,QACA,IAAI,QAAQ,KAAK,EAAE;AAAA,QACnB,KAAK,QAAQ,KAAK,GAAG;AAAA,QACrB,QAAQ,QAAQ,KAAK,MAAM;AAAA,QAC3B,MAAM,QAAQ,KAAK,IAAI;AAAA,MACzB,CAAC;AACD,cAAQ,KAAK,IAAI,OAAO,IAAI;AAAA,IAC9B,SAAS,KAAK;AAEZ,cAAQ,OAAO,MAAM;AAAA,YAAe,IAAI,MAAM,UAAU,GAAG,CAAC;AAAA,CAAI;AAChE,UAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,cAAQ,KAAK,IAAI,OAAO,CAAC;AAAA,IAC3B;AAAA,EACF;AACA,SAAO;AACT;AAEA,iBAAiB;AAAA,CAEf,SAAS,WAAW,UAAU,IAAI,QAAQ,GACzC,KAAK,CAAC,SAAS,QAAQ,KAAK,IAAI,CAAC,EACjC,MAAM,CAAC,QAAiB;AACvB,UAAQ,OAAO,MAAM;AAAA,YAAe,UAAU,GAAG,CAAC;AAAA,CAAI;AACtD,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["join","cwd","cwd","join","opts","flavour"]}
|
|
@@ -54,6 +54,9 @@ var BaseAdapter = class {
|
|
|
54
54
|
report(_ctx) {
|
|
55
55
|
throw new Error(`${this.runner}: --report not implemented yet`);
|
|
56
56
|
}
|
|
57
|
+
status(_ctx) {
|
|
58
|
+
throw new Error(`${this.runner}: --status not implemented yet`);
|
|
59
|
+
}
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
// src/shared/color.ts
|
|
@@ -114,8 +117,8 @@ function readNxProjectName(dir) {
|
|
|
114
117
|
|
|
115
118
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
116
119
|
import { spawnSync } from "child_process";
|
|
117
|
-
import { existsSync as
|
|
118
|
-
import { basename, join as
|
|
120
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
121
|
+
import { basename, join as join5 } from "path";
|
|
119
122
|
|
|
120
123
|
// src/shared/jsonc.ts
|
|
121
124
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -172,6 +175,10 @@ function hasShippedPreset(flavour) {
|
|
|
172
175
|
return SHIPPED_FLAVOURS.includes(flavour);
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
// src/roles/typescript/read-adoption.ts
|
|
179
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
180
|
+
import { join as join4 } from "path";
|
|
181
|
+
|
|
175
182
|
// src/roles/typescript/resolve-tsconfig-target.ts
|
|
176
183
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
177
184
|
import { join as join3 } from "path";
|
|
@@ -208,6 +215,38 @@ function resolveTsconfigTarget(moduleDir) {
|
|
|
208
215
|
return { path: "tsconfig.json", reason: "none" };
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// src/roles/typescript/read-adoption.ts
|
|
219
|
+
var SENTINEL_PRESET = /^@hublo\/sentinel\/tsconfig\/[a-z-]+$/;
|
|
220
|
+
function normaliseExtends(value) {
|
|
221
|
+
if (typeof value === "string") return [value];
|
|
222
|
+
if (Array.isArray(value))
|
|
223
|
+
return value.filter((entry) => typeof entry === "string");
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
var NOT_ADOPTED = (configFile) => ({
|
|
227
|
+
configFile,
|
|
228
|
+
preset: null,
|
|
229
|
+
adopted: false,
|
|
230
|
+
conformant: false,
|
|
231
|
+
drift: []
|
|
232
|
+
});
|
|
233
|
+
function readTsconfigAdoption(cwd) {
|
|
234
|
+
const target = resolveTsconfigTarget(cwd);
|
|
235
|
+
if (target.reason === "none" || !existsSync4(join4(cwd, target.path))) {
|
|
236
|
+
return NOT_ADOPTED(target.reason === "none" ? null : target.path);
|
|
237
|
+
}
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = parseJsonc(readFileSync3(join4(cwd, target.path), "utf8"), target.path);
|
|
241
|
+
} catch {
|
|
242
|
+
return NOT_ADOPTED(target.path);
|
|
243
|
+
}
|
|
244
|
+
const preset = normaliseExtends(parsed.extends).find((entry) => SENTINEL_PRESET.test(entry)) ?? null;
|
|
245
|
+
if (preset === null) return NOT_ADOPTED(target.path);
|
|
246
|
+
const drift = presetOwnedKeys(parsed.compilerOptions);
|
|
247
|
+
return { configFile: target.path, preset, adopted: true, conformant: drift.length === 0, drift };
|
|
248
|
+
}
|
|
249
|
+
|
|
211
250
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
212
251
|
var TYPECHECK_SCRIPT = { typecheck: "sentinel --run --typescript" };
|
|
213
252
|
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.`;
|
|
@@ -265,7 +304,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
265
304
|
};
|
|
266
305
|
}
|
|
267
306
|
const existing = parseJsonc(
|
|
268
|
-
|
|
307
|
+
readFileSync4(join5(context.cwd, target.path), "utf8"),
|
|
269
308
|
target.path
|
|
270
309
|
);
|
|
271
310
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -294,7 +333,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
294
333
|
* adoption, never written here, so the lockfile stays authoritative).
|
|
295
334
|
*/
|
|
296
335
|
typecheckScriptOperation(cwd) {
|
|
297
|
-
if (
|
|
336
|
+
if (existsSync5(join5(cwd, "package.json"))) {
|
|
298
337
|
return { kind: "merge-json", path: "package.json", value: { scripts: TYPECHECK_SCRIPT } };
|
|
299
338
|
}
|
|
300
339
|
const name = readNxProjectName(cwd) ?? basename(cwd);
|
|
@@ -338,7 +377,7 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
338
377
|
* check.
|
|
339
378
|
*/
|
|
340
379
|
typecheckTarget(cwd) {
|
|
341
|
-
if (
|
|
380
|
+
if (existsSync5(join5(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
342
381
|
const target = resolveTsconfigTarget(cwd);
|
|
343
382
|
return target.reason === "none" ? null : target.path;
|
|
344
383
|
}
|
|
@@ -349,17 +388,28 @@ var TscAdapter = class extends BaseAdapter {
|
|
|
349
388
|
*/
|
|
350
389
|
async inspect(ctx) {
|
|
351
390
|
const target = resolveTsconfigTarget(ctx.cwd);
|
|
391
|
+
const { preset, adopted } = readTsconfigAdoption(ctx.cwd);
|
|
352
392
|
return {
|
|
353
393
|
module: ctx.module,
|
|
354
394
|
target: "typescript",
|
|
355
395
|
flavour: ctx.flavour,
|
|
356
396
|
configFile: target.path,
|
|
357
397
|
configState: target.reason,
|
|
358
|
-
preset
|
|
398
|
+
preset,
|
|
399
|
+
adopted,
|
|
359
400
|
phase: 1,
|
|
360
401
|
deferred: DEFERRED_RULES
|
|
361
402
|
};
|
|
362
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Adoption + conformity from the committed tsconfig, for `--status`. No tsc run and
|
|
406
|
+
* no flavour guessing: reads the actual `extends` chain, so a workspace-wide scan is
|
|
407
|
+
* a cheap coverage + drift dashboard (adopted? which preset? drifted?).
|
|
408
|
+
*/
|
|
409
|
+
async status(ctx) {
|
|
410
|
+
const { adopted, preset, conformant, drift } = readTsconfigAdoption(ctx.cwd);
|
|
411
|
+
return { adopted, preset, conformant, drift };
|
|
412
|
+
}
|
|
363
413
|
/**
|
|
364
414
|
* Report conformance for the module: `tsc -b` (build mode, so app + spec are
|
|
365
415
|
* covered) and count total type errors plus the implicit-`any` family (TS70xx:
|
|
@@ -461,7 +511,7 @@ function diffLines(before, after) {
|
|
|
461
511
|
}
|
|
462
512
|
|
|
463
513
|
// src/core/apply-plan.ts
|
|
464
|
-
import { existsSync as
|
|
514
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, renameSync, writeFileSync } from "fs";
|
|
465
515
|
import { resolve as resolve2, sep } from "path";
|
|
466
516
|
import { applyEdits, modify } from "jsonc-parser";
|
|
467
517
|
|
|
@@ -480,7 +530,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
480
530
|
return absolutePath;
|
|
481
531
|
}
|
|
482
532
|
function readIfExists(absolutePath) {
|
|
483
|
-
return
|
|
533
|
+
return existsSync6(absolutePath) ? readFileSync5(absolutePath, "utf8") : void 0;
|
|
484
534
|
}
|
|
485
535
|
function* leaves(value, prefix = []) {
|
|
486
536
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -634,6 +684,11 @@ async function dispatch(opts) {
|
|
|
634
684
|
}
|
|
635
685
|
return res.code;
|
|
636
686
|
}
|
|
687
|
+
case "status": {
|
|
688
|
+
const report = await adapter.status(ctx);
|
|
689
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
690
|
+
return report.adopted && !report.conformant ? 1 : 0;
|
|
691
|
+
}
|
|
637
692
|
default: {
|
|
638
693
|
const unreachable = opts.verb;
|
|
639
694
|
throw new Error(`Unknown verb: ${String(unreachable)}`);
|
|
@@ -656,4 +711,4 @@ export {
|
|
|
656
711
|
detectFramework,
|
|
657
712
|
dispatch
|
|
658
713
|
};
|
|
659
|
-
//# sourceMappingURL=chunk-
|
|
714
|
+
//# sourceMappingURL=chunk-UG74KTIU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/registry.ts","../src/core/base-adapter.ts","../src/shared/color.ts","../src/shared/package-json.ts","../src/roles/typescript/adapters/tsc/tsc.adapter.ts","../src/shared/jsonc.ts","../src/shared/resolve-bin.ts","../src/roles/typescript/config-policy.ts","../src/roles/typescript/phased-rules.ts","../src/roles/typescript/presets.ts","../src/roles/typescript/read-adoption.ts","../src/roles/typescript/resolve-tsconfig-target.ts","../src/roles/typescript/register.ts","../src/adapters.ts","../src/core/detect-framework.ts","../src/shared/text.ts","../src/core/apply-plan.ts","../src/shared/deep-merge.ts","../src/core/dispatch.ts"],"sourcesContent":["/**\n * Adapter registry. Tool branches register their adapters here; the CLI resolves\n * an adapter by (target, flavour, runner). A default runner per target keeps the\n * common invocation tool-agnostic (`sentinel --lint`), while `--runner` overrides\n * it. Resolution is flavour-aware: candidates are filtered by `appliesTo(flavour)`,\n * so a React-only adapter is never picked for a Nest project, and two adapters can\n * share a (target, runner) if they specialise different flavours.\n */\nimport type { Flavour, Target } from './domain.js'\nimport type { Adapter } from './types.js'\n\nconst adapters: Adapter[] = []\n\n/** Default runner per target, so `--runner` stays optional. */\nconst defaultRunner: Partial<Record<Target, string>> = {\n // Filled in by tool branches, e.g. lint: 'eslint', typescript: 'tsc'.\n}\n\n/**\n * Register a tool adapter (called from each role's registration, wired into the\n * bootstrap in `src/adapters.ts`). No (target, runner) uniqueness guard here: two\n * adapters may share one for different flavours; a genuine clash (same target,\n * runner AND flavour) is caught at resolve time, where the flavour is known.\n */\nexport function register(adapter: Adapter): void {\n adapters.push(adapter)\n}\n\n/** Set the default runner for a target. */\nexport function setDefaultRunner(target: Target, runner: string): void {\n defaultRunner[target] = runner\n}\n\n/** All registered adapters (for `--all`, listing, and reports). */\nexport function all(): readonly Adapter[] {\n return adapters\n}\n\n/**\n * Resolve one adapter by target, honouring an explicit `--runner` or the target's\n * default. When a `flavour` is known it also filters by `appliesTo`, so a\n * React-only adapter is never picked for Nest; when it is absent (sentinel does\n * not detect it), every adapter for the target is a candidate. Throws with an\n * actionable message for each failure mode: no adapter for the target (yet), none\n * that handles the flavour, an ambiguous choice, an unknown runner, or two\n * adapters claiming the same (target, runner, flavour).\n */\nexport function resolve(target: Target, flavour?: Flavour, runner?: string): Adapter {\n const forTarget = adapters.filter((a) => a.target === target)\n if (forTarget.length === 0) {\n throw new Error(\n `No adapter registered for target \"${target}\" yet (it ships in a later ticket).`,\n )\n }\n\n const candidates = flavour ? forTarget.filter((a) => a.appliesTo(flavour)) : forTarget\n if (candidates.length === 0) {\n throw new Error(`No adapter for target \"${target}\" handles flavour \"${flavour}\".`)\n }\n\n const wanted = runner ?? defaultRunner[target]\n const available = candidates.map((a) => a.runner).join(', ')\n\n // No runner asked for and no default: only unambiguous when there is exactly one.\n if (!wanted) {\n const [first, ...rest] = candidates\n if (first && rest.length === 0) return first\n throw new Error(\n `Multiple runners for target \"${target}\" (${available}); pass --runner or set a default.`,\n )\n }\n\n const matching = candidates.filter((a) => a.runner === wanted)\n if (matching.length === 0) {\n throw new Error(\n `No runner \"${wanted}\" for target \"${target}\" (flavour \"${flavour}\"). Available: ${available}.`,\n )\n }\n if (matching.length > 1) {\n throw new Error(\n `Ambiguous: ${matching.length} adapters claim target \"${target}\", runner \"${wanted}\", flavour \"${flavour}\".`,\n )\n }\n return matching[0] as Adapter\n}\n","/**\n * `BaseAdapter`: an optional convenience base class for adapters. This is the\n * runtime part of the contract (the types live in `types.ts`); it provides\n * default `inspect`/`report` that throw \"not implemented yet\", so an adapter can\n * extend it and implement only what it needs.\n */\nimport type { Flavour, Target } from './domain.js'\nimport type {\n Adapter,\n AdapterResult,\n RunContext,\n StatusReport,\n UpdateContext,\n UpdatePlan,\n} from './types.js'\n\nexport abstract class BaseAdapter implements Adapter {\n abstract readonly target: Target\n abstract readonly runner: string\n abstract appliesTo(flavour: Flavour): boolean\n abstract plan(context: UpdateContext): UpdatePlan | Promise<UpdatePlan>\n abstract run(ctx: RunContext): Promise<AdapterResult>\n\n inspect(_ctx: RunContext): Promise<unknown> {\n throw new Error(`${this.runner}: --inspect not implemented yet`)\n }\n\n report(_ctx: RunContext): Promise<AdapterResult> {\n throw new Error(`${this.runner}: --report not implemented yet`)\n }\n\n status(_ctx: RunContext): Promise<StatusReport> {\n throw new Error(`${this.runner}: --status not implemented yet`)\n }\n}\n","/**\n * Terminal color, on top of Node's built-in `util.styleText` (no dependency).\n *\n * Passing `{ stream }` lets styleText itself decide whether to emit escape codes: it\n * returns the text UNCHANGED when the stream is not a TTY, when `NO_COLOR` is set, or\n * when the terminal has no color support. So the same code stays readable on a terminal\n * and clean in pipes, files, and CI logs, we never hand-roll a TTY check.\n */\nimport { styleText } from 'node:util'\n\ntype Format = Parameters<typeof styleText>[0]\n\n/** The semantic styles, each a `string -> string` painter. */\nexport interface Palette {\n /** A passing result. */ ok: (text: string) => string\n /** A failing result. */ fail: (text: string) => string\n /** A caution / deferred / deprecation notice. */ warn: (text: string) => string\n /** Emphasis (a name, a heading). */ strong: (text: string) => string\n /** Secondary, low-salience text (keys, timings, scope). */ dim: (text: string) => string\n /** A structural marker (a run header). */ accent: (text: string) => string\n}\n\n/**\n * Bind the semantic styles to ONE stream: `palette(process.stdout)` for the result\n * summary, `palette(process.stderr)` for notices/progress, so each is colored only when\n * that specific stream supports it.\n */\nexport function palette(stream: NodeJS.WriteStream): Palette {\n const paint = (format: Format, text: string): string => styleText(format, text, { stream })\n return {\n ok: (text) => paint(['green', 'bold'], text),\n fail: (text) => paint(['red', 'bold'], text),\n warn: (text) => paint('yellow', text),\n strong: (text) => paint('bold', text),\n dim: (text) => paint('dim', text),\n accent: (text) => paint('cyan', text),\n }\n}\n\n/**\n * An identity palette (no escapes), for pure rendering in tests and for any caller that\n * wants the plain text regardless of the stream.\n */\nexport const plainPalette: Palette = {\n ok: (text) => text,\n fail: (text) => text,\n warn: (text) => text,\n strong: (text) => text,\n dim: (text) => text,\n accent: (text) => text,\n}\n","/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\nexport function readOwnVersion(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return '0.0.0' // reached the filesystem root\n dir = parent\n }\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--update` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n","/**\n * TypeScript adapter (runner: `tsc`). Maps the four verbs onto tsc for one module:\n * - plan (--update) → thin conformant tsconfig stub extending the preset\n * - run (--run) → `tsc -b` (build mode), the module's own tsc\n * - inspect (--inspect) → the module's resolved config (preset, target, flavour)\n * - report (--report) → type-error + implicit-any counts (the conformance signal)\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { BaseAdapter } from '../../../../core/base-adapter.js'\nimport type { Flavour, Target } from '../../../../core/domain.js'\nimport { palette } from '../../../../shared/color.js'\nimport type {\n AdapterResult,\n FileOperation,\n RunContext,\n StatusReport,\n UpdateContext,\n UpdatePlan,\n} from '../../../../core/types.js'\nimport { parseJsonc } from '../../../../shared/jsonc.js'\nimport { readNxProjectName } from '../../../../shared/package-json.js'\nimport { resolveBin } from '../../../../shared/resolve-bin.js'\nimport { presetOwnedKeys } from '../../config-policy.js'\nimport { DEFERRED_RULES, deferredRuleNames } from '../../phased-rules.js'\nimport { hasShippedPreset } from '../../presets.js'\nimport { readTsconfigAdoption } from '../../read-adoption.js'\nimport { resolveTsconfigTarget } from '../../resolve-tsconfig-target.js'\n\nconst TYPECHECK_SCRIPT = { typecheck: 'sentinel --run --typescript' }\n\n/**\n * 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(`${palette(process.stderr).warn(PHASED_STRICTNESS_WARNING)}\\n`)\n const tsc = resolveBin(ctx.cwd, 'tsc') ?? 'tsc'\n const result = spawnSync(tsc, ['-b', config], { cwd: ctx.cwd, stdio: 'inherit' })\n if (result.error) {\n process.stderr.write(\n `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?\\n`,\n )\n return { ok: false, code: 1 }\n }\n const code = result.status ?? 1\n return { ok: code === 0, code }\n }\n\n /**\n * The config to type-check with `tsc -b`. Prefer the module's root `tsconfig.json`\n * (the solution the monorepo builds; `tsc -b` follows its `references` to cover\n * app + spec), else the base-extending file, else null when there is nothing to\n * check.\n */\n private typecheckTarget(cwd: string): string | null {\n if (existsSync(join(cwd, 'tsconfig.json'))) return 'tsconfig.json'\n const target = resolveTsconfigTarget(cwd)\n return target.reason === 'none' ? null : target.path\n }\n\n /**\n * The module's resolved TypeScript config: which preset, which file, how, and the\n * phased-strictness state (`deferred` rules that are off in phase 1). This is the\n * \"list what's deferred\" query, `sentinel --inspect --typescript`.\n */\n async inspect(ctx: RunContext): Promise<unknown> {\n const target = resolveTsconfigTarget(ctx.cwd)\n // The preset is READ from the module's committed `extends` chain, not derived from\n // the (possibly hoisted-fallback) flavour: a nest module reports `.../nest`, not\n // `.../node`, so `--inspect` reflects what the file actually uses.\n const { preset, adopted } = readTsconfigAdoption(ctx.cwd)\n return {\n module: ctx.module,\n target: 'typescript',\n flavour: ctx.flavour,\n configFile: target.path,\n configState: target.reason,\n preset,\n adopted,\n phase: 1,\n deferred: DEFERRED_RULES,\n }\n }\n\n /**\n * Adoption + conformity from the committed tsconfig, for `--status`. No tsc run and\n * no flavour guessing: reads the actual `extends` chain, so a workspace-wide scan is\n * a cheap coverage + drift dashboard (adopted? which preset? drifted?).\n */\n async status(ctx: RunContext): Promise<StatusReport> {\n const { adopted, preset, conformant, drift } = readTsconfigAdoption(ctx.cwd)\n return { adopted, preset, conformant, drift }\n }\n\n /**\n * Report conformance for the module: `tsc -b` (build mode, so app + spec are\n * covered) and count total type errors plus the implicit-`any` family (TS70xx:\n * 7006/7031/7053/… ), the signal that drives the noImplicitAny migration. No\n * tsconfig is a clean, empty report.\n */\n async report(ctx: RunContext): Promise<AdapterResult> {\n const config = this.typecheckTarget(ctx.cwd)\n if (!config) {\n return { ok: true, code: 0, metrics: { errors: 0, implicitAny: 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 * Read a module's ADOPTION + CONFORMITY of the TypeScript preset, straight from its\n * committed `tsconfig`. This is the truth `--status` reports, and it is deliberately\n * DETECTION-FREE: it reads the actual `extends` chain rather than guessing a flavour,\n * so a hoisted-deps monorepo (where detection falls back to `node`) can't corrupt it.\n *\n * - adopted = the resolved tsconfig extends a `@hublo/sentinel/tsconfig/*` preset\n * - preset = that exact preset string (or null), the real one, not a derived guess\n * - conformant = adopted AND no preset-owned `compilerOptions` drift (i.e. a re-`update`\n * would strip nothing), the same drift the update guard manages\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { parseJsonc } from '../../shared/jsonc.js'\nimport { presetOwnedKeys } from './config-policy.js'\nimport { resolveTsconfigTarget } from './resolve-tsconfig-target.js'\n\n/** Matches a sentinel tsconfig preset subpath, e.g. `@hublo/sentinel/tsconfig/nest`. */\nconst SENTINEL_PRESET = /^@hublo\\/sentinel\\/tsconfig\\/[a-z-]+$/\n\nexport interface TsconfigAdoption {\n /** The tsconfig inspected (relative to the module), or null when there is none. */\n configFile: string | null\n /** The sentinel preset the module extends, or null when it extends none. */\n preset: string | null\n /** True when the module extends a sentinel preset. */\n adopted: boolean\n /** True when adopted AND drift-free (a re-`update` would change nothing). */\n conformant: boolean\n /** Preset-owned `compilerOptions` kept locally (the drift); empty when conformant. */\n drift: string[]\n}\n\n/** `extends` as a list: a string, a TS 5.0 array, or absent, all normalised. */\nfunction normaliseExtends(value: unknown): string[] {\n if (typeof value === 'string') return [value]\n if (Array.isArray(value))\n return value.filter((entry): entry is string => typeof entry === 'string')\n return []\n}\n\nconst NOT_ADOPTED = (configFile: string | null): TsconfigAdoption => ({\n configFile,\n preset: null,\n adopted: false,\n conformant: false,\n drift: [],\n})\n\n/** Read a module's TypeScript adoption + conformity from its committed tsconfig. */\nexport function readTsconfigAdoption(cwd: string): TsconfigAdoption {\n const target = resolveTsconfigTarget(cwd)\n if (target.reason === 'none' || !existsSync(join(cwd, target.path))) {\n return NOT_ADOPTED(target.reason === 'none' ? null : target.path)\n }\n\n let parsed: { extends?: unknown; compilerOptions?: Record<string, unknown> }\n try {\n parsed = parseJsonc(readFileSync(join(cwd, target.path), 'utf8'), target.path)\n } catch {\n return NOT_ADOPTED(target.path) // malformed config: can't be conformant\n }\n\n const preset =\n normaliseExtends(parsed.extends).find((entry) => SENTINEL_PRESET.test(entry)) ?? null\n if (preset === null) return NOT_ADOPTED(target.path)\n\n const drift = presetOwnedKeys(parsed.compilerOptions)\n return { configFile: target.path, preset, adopted: true, conformant: drift.length === 0, drift }\n}\n","/**\n * Resolve which tsconfig file `--update --typescript` should write in a module.\n *\n * The rule (from the monorepo audit): target the file that currently `extends` the\n * shared base config, that is the entry point sentinel's preset replaces. Two\n * shapes exist:\n * - Pattern A (libs, nest services): `tsconfig.json` extends the base.\n * - Pattern B (Vite React apps): `tsconfig.json` is references-only and\n * `tsconfig.app.json` extends the base.\n * We check `tsconfig.app.json` before `tsconfig.json` so B is found first. The\n * `reason` distinguishes the three cases `--update` must handle differently:\n * - `extends-base`: found the file to migrate.\n * - `other-chain`: a tsconfig exists but extends something else (svelte's own\n * `.svelte-kit` chain), don't clobber it; handled separately.\n * - `none`: no tsconfig at all; `--update` creates one.\n *\n * The \"find the file that extends a shared root\" pattern is reusable; it will be\n * lifted to core when a second tool needs its own version.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { parseJsonc } from '../../shared/jsonc.js'\n\n/**\n * A file is the target if its `extends` references either the monorepo base (still\n * to migrate) or a sentinel preset (already migrated, so re-`update` re-strips any\n * new drift, keeping it idempotent and self-cleaning).\n */\nconst TARGET_EXTENDS_MARKERS = ['tsconfig.base.json', '@hublo/sentinel/tsconfig/'] as const\n\n/** Candidate entry points, most-specific first (Pattern B before Pattern A). */\nconst CANDIDATES = ['tsconfig.app.json', 'tsconfig.json'] as const\n\nexport interface TsconfigTarget {\n /** The tsconfig file to write, relative to the module root. */\n path: string\n /** How it was chosen, for observability and the per-case `--update` behaviour. */\n reason: 'extends-base' | 'other-chain' | 'none'\n}\n\n/**\n * The `extends` targets of a tsconfig as a list. `extends` may be a string or, since\n * TypeScript 5.0, an array of strings; both are normalised here (absent/unreadable\n * or non-string entries yield an empty list).\n */\nfunction readExtends(absolutePath: string): string[] {\n let parsed: { extends?: unknown }\n try {\n parsed = parseJsonc(readFileSync(absolutePath, 'utf8'), absolutePath)\n } catch {\n return [] // malformed candidate: skip it, try the next\n }\n if (typeof parsed.extends === 'string') return [parsed.extends]\n if (Array.isArray(parsed.extends)) {\n return parsed.extends.filter((entry): entry is string => typeof entry === 'string')\n }\n return []\n}\n\nexport function resolveTsconfigTarget(moduleDir: string): TsconfigTarget {\n let existing: string | undefined\n for (const candidate of CANDIDATES) {\n const absolutePath = join(moduleDir, candidate)\n if (!existsSync(absolutePath)) continue\n existing ??= candidate // remember the first tsconfig we saw\n const extendsValues = readExtends(absolutePath)\n const extendsBase = extendsValues.some((value) =>\n TARGET_EXTENDS_MARKERS.some((marker) => value.includes(marker)),\n )\n if (extendsBase) {\n return { path: candidate, reason: 'extends-base' }\n }\n }\n if (existing) return { path: existing, reason: 'other-chain' }\n return { path: 'tsconfig.json', reason: 'none' }\n}\n","/**\n * TypeScript role registration. The single entry the bootstrap\n * (`src/adapters.ts`) imports, so wiring stays greppable and the CLI never\n * changes. Adds the tsc adapter and makes it the default runner for `--typescript`.\n */\nimport { register, setDefaultRunner } from '../../core/registry.js'\nimport { TscAdapter } from './adapters/tsc/tsc.adapter.js'\n\nexport function registerTypescript(): void {\n register(new TscAdapter())\n setDefaultRunner('typescript', 'tsc')\n}\n","/**\n * Adapter bootstrap: the single place adapters are wired into the CLI.\n *\n * A `register(new MyAdapter())` call only runs if its module is imported, and the\n * CLI must not import every tool by hand, that would make the promise \"one tool =\n * one adapter, the CLI never changes\" false. So the CLI calls `registerAdapters()`\n * once at startup, and each tool ticket adds exactly ONE line here (its role's\n * registration), never touching the CLI entry point (`bin/sentinel.ts`) or the\n * registry.\n *\n * Empty until the first tool ticket lands. A tool ticket adds, e.g. (an explicit\n * module path, not a barrel, so the wiring stays greppable):\n *\n * import { registerTypescript } from './roles/typescript/register.js'\n * export function registerAdapters(): void {\n * registerTypescript()\n * }\n */\nimport { registerTypescript } from './roles/typescript/register.js'\n\nexport function registerAdapters(): void {\n registerTypescript()\n}\n","/**\n * Framework detection from a module's package.json dependencies. DETERMINISTIC:\n * a dependency maps to exactly one flavour, and a module with no framework\n * dependency is a plain TypeScript library (`node`). This is not the old silent\n * guessing (there is no \"assume react\" fallback); `node` is a real preset.\n *\n * Reusable across tools: every tool's `--update` needs the module's flavour to\n * pick its preset, so this lives in core, not in the TypeScript role.\n */\nimport type { Flavour } from './domain.js'\n\n/** The slice of a package.json we read for detection. */\nexport interface PackageDependencies {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n}\n\n/**\n * A framework signal in priority order: the first whose package is present wins.\n * Ordered most-specific-first so a backend (`@nestjs/core`) is never shadowed by a\n * transitive `react`. 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 case 'status': {\n // Adoption + conformity, read from the committed config (no tool run). A drifted\n // adopted module is a conformance failure; a non-adopted one is not.\n const report = await adapter.status(ctx)\n process.stdout.write(JSON.stringify(report, null, 2) + '\\n')\n return report.adopted && !report.conformant ? 1 : 0\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","existsSync","readFileSync","join","readFileSync","join","existsSync","existsSync","join","readFileSync","readFileSync","join","existsSync","i","j","existsSync","readFileSync","resolve","resolve","existsSync","readFileSync"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ declare function registerAdapters(): void;
|
|
|
11
11
|
* `settings.ts`, not here.
|
|
12
12
|
*/
|
|
13
13
|
/** Verbs: what to do. Each maps to an adapter method in dispatch. */
|
|
14
|
-
declare const VERBS: readonly ["run", "inspect", "update", "report"];
|
|
14
|
+
declare const VERBS: readonly ["run", "inspect", "update", "report", "status"];
|
|
15
15
|
type Verb = (typeof VERBS)[number];
|
|
16
16
|
/** Targets: the kind of check. The CLI `--<target>` flags map 1:1 to these. */
|
|
17
17
|
declare const TARGETS: readonly ["lint", "format", "typescript", "build", "test", "static-analysis", "runtime-analysis", "arch"];
|
|
@@ -119,6 +119,21 @@ interface AdapterResult {
|
|
|
119
119
|
/** Structured metrics for `--report` (adapter-defined). */
|
|
120
120
|
metrics?: Record<string, unknown>;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Adoption + conformity of one module for one target, for `--status`. Read from the
|
|
124
|
+
* module's COMMITTED config (no tool run, no flavour guessing), so a workspace-wide
|
|
125
|
+
* scan is a cheap coverage + drift dashboard.
|
|
126
|
+
*/
|
|
127
|
+
interface StatusReport {
|
|
128
|
+
/** The module extends this adapter's sentinel preset. */
|
|
129
|
+
adopted: boolean;
|
|
130
|
+
/** The exact preset the module extends, or null when not adopted. */
|
|
131
|
+
preset: string | null;
|
|
132
|
+
/** Adopted AND drift-free (a re-`update` would change nothing). */
|
|
133
|
+
conformant: boolean;
|
|
134
|
+
/** The drift when adopted-but-not-conformant (e.g. preset-owned keys kept locally). */
|
|
135
|
+
drift: string[];
|
|
136
|
+
}
|
|
122
137
|
/**
|
|
123
138
|
* One tool implementation for one target (and, optionally, a subset of flavours,
|
|
124
139
|
* see `appliesTo`). The 4 CLI verbs map onto these: `--run` -> run, `--inspect`
|
|
@@ -152,6 +167,12 @@ interface Adapter {
|
|
|
152
167
|
inspect(ctx: RunContext): Promise<unknown>;
|
|
153
168
|
/** Metrics/health for `--report` (bundle size, error counts, complexity, ...). */
|
|
154
169
|
report(ctx: RunContext): Promise<AdapterResult>;
|
|
170
|
+
/**
|
|
171
|
+
* Adoption + conformity of the module, for `--status`. Read from the committed
|
|
172
|
+
* config only (no tool run), so a whole-workspace scan stays cheap. Reports whether
|
|
173
|
+
* the module is on this adapter's preset and whether it has drifted.
|
|
174
|
+
*/
|
|
175
|
+
status(ctx: RunContext): Promise<StatusReport>;
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
/**
|
|
@@ -169,6 +190,7 @@ declare abstract class BaseAdapter implements Adapter {
|
|
|
169
190
|
abstract run(ctx: RunContext): Promise<AdapterResult>;
|
|
170
191
|
inspect(_ctx: RunContext): Promise<unknown>;
|
|
171
192
|
report(_ctx: RunContext): Promise<AdapterResult>;
|
|
193
|
+
status(_ctx: RunContext): Promise<StatusReport>;
|
|
172
194
|
}
|
|
173
195
|
|
|
174
196
|
/**
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.8",
|
|
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/color.ts","../src/shared/package-json.ts","../src/roles/typescript/adapters/tsc/tsc.adapter.ts","../src/shared/jsonc.ts","../src/shared/resolve-bin.ts","../src/roles/typescript/config-policy.ts","../src/roles/typescript/phased-rules.ts","../src/roles/typescript/presets.ts","../src/roles/typescript/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 * Terminal color, on top of Node's built-in `util.styleText` (no dependency).\n *\n * Passing `{ stream }` lets styleText itself decide whether to emit escape codes: it\n * returns the text UNCHANGED when the stream is not a TTY, when `NO_COLOR` is set, or\n * when the terminal has no color support. So the same code stays readable on a terminal\n * and clean in pipes, files, and CI logs, we never hand-roll a TTY check.\n */\nimport { styleText } from 'node:util'\n\ntype Format = Parameters<typeof styleText>[0]\n\n/** The semantic styles, each a `string -> string` painter. */\nexport interface Palette {\n /** A passing result. */ ok: (text: string) => string\n /** A failing result. */ fail: (text: string) => string\n /** A caution / deferred / deprecation notice. */ warn: (text: string) => string\n /** Emphasis (a name, a heading). */ strong: (text: string) => string\n /** Secondary, low-salience text (keys, timings, scope). */ dim: (text: string) => string\n /** A structural marker (a run header). */ accent: (text: string) => string\n}\n\n/**\n * Bind the semantic styles to ONE stream: `palette(process.stdout)` for the result\n * summary, `palette(process.stderr)` for notices/progress, so each is colored only when\n * that specific stream supports it.\n */\nexport function palette(stream: NodeJS.WriteStream): Palette {\n const paint = (format: Format, text: string): string => styleText(format, text, { stream })\n return {\n ok: (text) => paint(['green', 'bold'], text),\n fail: (text) => paint(['red', 'bold'], text),\n warn: (text) => paint('yellow', text),\n strong: (text) => paint('bold', text),\n dim: (text) => paint('dim', text),\n accent: (text) => paint('cyan', text),\n }\n}\n\n/**\n * An identity palette (no escapes), for pure rendering in tests and for any caller that\n * wants the plain text regardless of the stream.\n */\nexport const plainPalette: Palette = {\n ok: (text) => text,\n fail: (text) => text,\n warn: (text) => text,\n strong: (text) => text,\n dim: (text) => text,\n accent: (text) => text,\n}\n","/**\n * package.json helpers, shared by the CLI and the engine.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * sentinel's OWN version, for `--version`. We must NOT use\n * `process.env.npm_package_version`, that is the version of whatever script\n * invoked us (an app's package.json, or nothing). Instead walk up from this\n * module to the nearest package.json, which is always sentinel's own, whether\n * running from source or the bundled dist.\n */\nexport function readOwnVersion(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string }\n if (typeof pkg.version === 'string') return pkg.version\n } catch {\n // Malformed package.json; keep walking up.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) return '0.0.0' // reached the filesystem root\n dir = parent\n }\n}\n\n/**\n * Read a module's package.json for framework detection; tolerant of a missing or\n * malformed file (returns `{}`, so detection falls through to the `node` default).\n */\nexport function readProjectPackageJson(dir: string): {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n} {\n const path = join(dir, 'package.json')\n if (!existsSync(path)) return {}\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Record<string, never>\n } catch {\n process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.\\n`)\n return {}\n }\n}\n\n/**\n * The nx project name for a module, read from its `project.json`, or undefined if\n * there is none/unreadable. Used when `--update` has to scaffold a `package.json`\n * for a module that only has a `project.json` (common for nx apps/services): the\n * scaffolded file borrows this name so it is a valid, uniquely-named workspace\n * package. Reusable by any tool that scaffolds, not just TypeScript.\n */\nexport function readNxProjectName(dir: string): string | undefined {\n const path = join(dir, 'project.json')\n if (!existsSync(path)) return undefined\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown }\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n","/**\n * TypeScript adapter (runner: `tsc`). Maps the four verbs onto tsc for one module:\n * - plan (--update) → thin conformant tsconfig stub extending the preset\n * - run (--run) → `tsc -b` (build mode), the module's own tsc\n * - inspect (--inspect) → the module's resolved config (preset, target, flavour)\n * - report (--report) → type-error + implicit-any counts (the conformance signal)\n */\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { basename, join } from 'node:path'\n\nimport { BaseAdapter } from '../../../../core/base-adapter.js'\nimport type { Flavour, Target } from '../../../../core/domain.js'\nimport { palette } from '../../../../shared/color.js'\nimport type {\n AdapterResult,\n FileOperation,\n RunContext,\n 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(`${palette(process.stderr).warn(PHASED_STRICTNESS_WARNING)}\\n`)\n const tsc = resolveBin(ctx.cwd, 'tsc') ?? 'tsc'\n const result = spawnSync(tsc, ['-b', config], { cwd: ctx.cwd, stdio: 'inherit' })\n if (result.error) {\n process.stderr.write(\n `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?\\n`,\n )\n return { ok: false, code: 1 }\n }\n const code = result.status ?? 1\n return { ok: code === 0, code }\n }\n\n /**\n * The config to type-check with `tsc -b`. Prefer the module's root `tsconfig.json`\n * (the solution the monorepo builds; `tsc -b` follows its `references` to cover\n * app + spec), else the base-extending file, else null when there is nothing to\n * check.\n */\n private typecheckTarget(cwd: string): string | null {\n if (existsSync(join(cwd, 'tsconfig.json'))) return 'tsconfig.json'\n const target = resolveTsconfigTarget(cwd)\n return target.reason === 'none' ? null : target.path\n }\n\n /**\n * The module's resolved TypeScript config: which preset, which file, how, and the\n * phased-strictness state (`deferred` rules that are off in phase 1). This is the\n * \"list what's deferred\" query, `sentinel --inspect --typescript`.\n */\n async inspect(ctx: RunContext): Promise<unknown> {\n const target = resolveTsconfigTarget(ctx.cwd)\n 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"]}
|