@hublo/sentinel 0.1.0-alpha.7 → 0.1.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,8 @@ A large monorepo accumulates:
43
43
 
44
44
  sentinel writes **standard config files** into a project (each just `extends` a sentinel preset) and runs the checks. Your editor and the tools read those **normal files natively**, they never call sentinel at runtime, so nothing is coupled to it or brittle.
45
45
 
46
+ > **Shipped today:** only the **TypeScript** tool, so `--update` writes the `tsconfig` stub, and `--run`/`--report`/`--status` work for `--typescript`. The `eslint.config.js` / `--lint` / `--test` snippets below illustrate the end state; those subpaths (`@hublo/sentinel/lint/*`, …) land with their tool ticket.
47
+
46
48
  **Step 1 — put a module on sentinel** (once per module, by a dev; the files are committed):
47
49
 
48
50
  ```bash
@@ -99,6 +101,18 @@ And the app's `package.json` scripts route every check through the one CLI (run
99
101
 
100
102
  The per-tool knowledge (eslint → `eslint.config.js`, tsc → `tsconfig`, …) lives **inside sentinel as an adapter**, swappable centrally, but never a runtime dependency of the project.
101
103
 
104
+ ## Requirements & installing
105
+
106
+ **Node.** sentinel needs **Node >= 20.12** (its coloured output uses `util.styleText`, added in 20.12). It fails fast with a clear message on an older runtime rather than crashing. If a project runs on an older Node (e.g. a legacy app on Node 10), run sentinel with a modern Node via `fnm`/`nvm`; you do not need to change the project's own Node.
107
+
108
+ **Try it without installing.** A one-off run needs no auth and touches nothing:
109
+
110
+ ```bash
111
+ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
112
+ ```
113
+
114
+ **Installing a pre-release (`minimumReleaseAge`).** The monorepo enforces a 3-day `minimumReleaseAge` supply-chain gate (a freshly published version cannot be installed until it has aged 3 days). A brand-new `alpha` therefore cannot be added yet, so while testing pre-releases you either exclude the package (`pnpm-workspace.yaml` → `minimumReleaseAgeExclude`) or install with `--config.minimumReleaseAge=0`. This is a deliberate protection, not a bug: **always pin the exact version** (`@hublo/sentinel@0.1.0-alpha.9`) rather than `@latest`, so a run is reproducible and the gate stays meaningful.
115
+
102
116
  ## Architecture: `target → runner → flavour`
103
117
 
104
118
  Every check is described by three layers:
@@ -244,6 +258,7 @@ sentinel <verb> [type] [options]
244
258
  VERBS --run execute the target's tool
245
259
  --inspect show the resolved configuration (incl. deferred rules)
246
260
  --report metrics and health
261
+ --status adoption + conformity (coverage + drift), read from configs
247
262
  --update generate/apply the config stubs (writes; one module only)
248
263
 
249
264
  TYPES --lint --format --typescript --build --test
@@ -259,17 +274,33 @@ OPTIONS --module <name> from the root: scope to one module
259
274
  --ci from the root: affected only; non-zero exit on failure
260
275
  --fix auto-fix where applicable
261
276
  --dry-run preview a --update without writing
277
+ --json machine-readable output (report / inspect / status / --dry-run)
262
278
 
263
279
  EXAMPLES sentinel --run --typescript # in a module → that module
264
280
  sentinel --report --typescript --module bff-admin # from root → one module
265
281
  sentinel --report # from root → all types, all modules
266
- sentinel --report --ci # from root → affected (CI)
282
+ sentinel --status --typescript # from root → adoption coverage
283
+ sentinel --status --ci # from root → fail CI on drift
267
284
  sentinel --update --typescript --flavour react # write stubs for the current module
268
285
  ```
269
286
 
270
- `--run`/`--inspect`/`--report` share one context rule (developer from a module, or
271
- from the root for a name / affected / all); `--update` writes, so it targets one
272
- module only (adopting every module at once is refused, adopt gradually).
287
+ `--run`/`--inspect`/`--report`/`--status` share one context rule (developer from a
288
+ module, or from the root for a name / affected / all); `--update` writes, so it targets
289
+ one module only (adopting every module at once is refused, adopt gradually).
290
+
291
+ **`--status` — adoption + conformity.** A cheap, workspace-wide read (no tool run, no
292
+ flavour guessing): for each module it reads the committed `tsconfig` `extends` chain and
293
+ reports whether it is **adopted** (extends a sentinel preset), which preset, and whether
294
+ it is **conformant** (drift-free, a re-`update` would change nothing), plus a coverage
295
+ footer. This is the **drift guard** as a command, `--status --ci` exits non-zero when an
296
+ adopted module has drifted.
297
+
298
+ ```
299
+ ✓ host-admin (react) typescript — adopted (react) conformant
300
+ ✗ some-bff (nest) typescript — adopted (nest) drift: strict, target
301
+ · legacy-app (node) typescript — not adopted
302
+ coverage: 12/40 adopted · 11/12 conformant · 1 drifted
303
+ ```
273
304
 
274
305
  ## Repository layout
275
306
 
@@ -293,8 +324,7 @@ src/
293
324
  runners/ # one runner per sub-tool (duplication, complexity, ...)
294
325
  shared/ # reusable utils (package-json, deep-merge, text)
295
326
  tests/ # unit tests + tests/e2e (runs the built dist binary)
296
- .changeset/ # release notes
297
- .github/workflows/ # ci.yml (PR checks) + release.yml (changesets publish)
327
+ .github/workflows/ # ci.yml (PR checks) + publish.yml (manual, version-input publish)
298
328
  ```
299
329
 
300
330
  Subpath exports (in `package.json`) expose presets to consumers. Shipped today:
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- detectFramework,
3
+ availableTargets,
4
+ describeFramework,
4
5
  dispatch,
5
6
  palette,
6
7
  readNxProjectName,
@@ -9,7 +10,7 @@ import {
9
10
  registerAdapters,
10
11
  resolve,
11
12
  resolveBin
12
- } from "../chunk-G7NVAZSZ.js";
13
+ } from "../chunk-NQ7GNHFT.js";
13
14
 
14
15
  // bin/sentinel.ts
15
16
  import { program } from "commander";
@@ -105,7 +106,7 @@ function resolveContext(cwd2, opts2) {
105
106
  }
106
107
 
107
108
  // src/core/domain.ts
108
- var VERBS = ["run", "inspect", "update", "report"];
109
+ var VERBS = ["run", "inspect", "update", "report", "status"];
109
110
  var TARGETS = [
110
111
  "lint",
111
112
  "format",
@@ -124,19 +125,45 @@ async function analyse(params) {
124
125
  let worstCode = 0;
125
126
  let done = 0;
126
127
  for (const module of params.modules) {
127
- const flavour2 = params.flavour ?? detectFramework(readProjectPackageJson(module.root));
128
+ let flavour2;
129
+ if (params.flavour) {
130
+ flavour2 = params.flavour;
131
+ } else {
132
+ const detection = describeFramework(readProjectPackageJson(module.root));
133
+ flavour2 = detection.flavour;
134
+ if (detection.ambiguous) {
135
+ const warn = palette(process.stderr);
136
+ process.stderr.write(
137
+ warn.warn(
138
+ `sentinel: ${module.name}: flavour is ambiguous, detected "${flavour2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to be explicit.`
139
+ ) + "\n"
140
+ );
141
+ }
142
+ }
128
143
  const ctx = {
129
144
  module: module.name,
130
145
  cwd: module.root,
131
146
  flavour: flavour2,
132
147
  ci: params.ci,
133
- fix: params.fix
148
+ fix: params.fix,
149
+ maxDiagnostics: params.maxDiagnostics
134
150
  };
135
151
  for (const target of params.targets) {
136
152
  let adapter;
137
153
  try {
138
154
  adapter = resolve(target, flavour2, params.runner);
139
- } catch {
155
+ } catch (error) {
156
+ if (params.targetsExplicit) {
157
+ const reason = error instanceof Error ? error.message : String(error);
158
+ results.push({
159
+ project: module.name,
160
+ target,
161
+ flavour: flavour2,
162
+ ok: false,
163
+ status: "unsupported",
164
+ data: { reason }
165
+ });
166
+ }
140
167
  continue;
141
168
  }
142
169
  try {
@@ -150,7 +177,14 @@ async function analyse(params) {
150
177
  );
151
178
  }
152
179
  const result = await adapter.run(ctx);
153
- results.push({ project: module.name, target, flavour: flavour2, ok: result.ok, data: {} });
180
+ results.push({
181
+ project: module.name,
182
+ target,
183
+ flavour: flavour2,
184
+ ok: result.ok,
185
+ status: result.ok ? "ok" : "failed",
186
+ data: {}
187
+ });
154
188
  worstCode = Math.max(worstCode, result.code);
155
189
  } else if (params.verb === "report") {
156
190
  const result = await adapter.report(ctx);
@@ -159,16 +193,43 @@ async function analyse(params) {
159
193
  target,
160
194
  flavour: flavour2,
161
195
  ok: result.ok,
196
+ status: result.ok ? "ok" : "failed",
162
197
  data: result.metrics ?? {}
163
198
  });
164
199
  worstCode = Math.max(worstCode, result.code);
165
- } else {
200
+ } else if (params.verb === "inspect") {
166
201
  const config = await adapter.inspect(ctx);
167
- results.push({ project: module.name, target, flavour: flavour2, ok: true, data: config });
202
+ results.push({
203
+ project: module.name,
204
+ target,
205
+ flavour: flavour2,
206
+ ok: true,
207
+ status: "ok",
208
+ data: config
209
+ });
210
+ } else {
211
+ const status = await adapter.status(ctx);
212
+ const ok = !status.adopted || status.conformant;
213
+ results.push({
214
+ project: module.name,
215
+ target,
216
+ flavour: flavour2,
217
+ ok,
218
+ status: ok ? "ok" : "failed",
219
+ data: status
220
+ });
221
+ if (!ok) worstCode = Math.max(worstCode, 1);
168
222
  }
169
223
  } catch (error) {
170
224
  const message = error instanceof Error ? error.message : String(error);
171
- results.push({ project: module.name, target, flavour: flavour2, ok: false, data: { error: message } });
225
+ results.push({
226
+ project: module.name,
227
+ target,
228
+ flavour: flavour2,
229
+ ok: false,
230
+ status: "failed",
231
+ data: { error: message }
232
+ });
172
233
  worstCode = Math.max(worstCode, 1);
173
234
  }
174
235
  }
@@ -177,16 +238,34 @@ async function analyse(params) {
177
238
  return { results, worstCode };
178
239
  }
179
240
  function generateSummary(result) {
180
- const { project, target, flavour: flavour2, ok, data } = result;
241
+ const { project, target, flavour: flavour2, ok, status, data } = result;
181
242
  const details = data && typeof data === "object" ? data : { value: data };
182
- return { project, target, flavour: flavour2, ok, ...details };
243
+ return { project, target, flavour: flavour2, ok, status, ...details };
183
244
  }
184
- function generateSummaries(results) {
185
- return { schemaVersion: 1, results: results.map(generateSummary) };
245
+ function generateSummaries(results, requestedTargets) {
246
+ const available = new Set(availableTargets());
247
+ return {
248
+ schemaVersion: 2,
249
+ executed: requestedTargets.filter((t) => available.has(t)),
250
+ skipped: requestedTargets.filter((t) => !available.has(t)),
251
+ results: results.map(generateSummary)
252
+ };
186
253
  }
187
254
 
188
255
  // src/core/render.ts
189
- var HEAD_KEYS = /* @__PURE__ */ new Set(["project", "module", "target", "flavour", "ok"]);
256
+ var HEAD_KEYS = /* @__PURE__ */ new Set([
257
+ "project",
258
+ "module",
259
+ "target",
260
+ "flavour",
261
+ "ok",
262
+ "status",
263
+ "reason",
264
+ // The structured diagnostics are for `--json` consumers; the human row stays the concise
265
+ // `errors=N` line rather than dumping every diagnostic inline.
266
+ "diagnostics",
267
+ "diagnosticsTruncated"
268
+ ]);
190
269
  function isDeferredRules(value) {
191
270
  return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && "rule" in entry);
192
271
  }
@@ -194,9 +273,16 @@ function renderValue(key, value, p) {
194
273
  if ((key === "errors" || key === "implicitAny") && typeof value === "number") {
195
274
  return value > 0 ? p.fail(String(value)) : p.ok(String(value));
196
275
  }
276
+ if (key === "implicitAny" && value === "deferred") return p.warn("deferred");
197
277
  return typeof value === "string" ? value : JSON.stringify(value);
198
278
  }
279
+ function renderUnsupportedRow(item, p) {
280
+ const reason = typeof item.reason === "string" ? ` ${p.dim(`(${item.reason})`)}` : "";
281
+ const head = ` ${p.dim("\xB7")} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
282
+ return `${head} ${p.dim("\u2014")} ${p.warn("unsupported")}${reason}`;
283
+ }
199
284
  function renderSummary(item, p) {
285
+ if (item.status === "unsupported") return renderUnsupportedRow(item, p);
200
286
  const ok = item.ok === true;
201
287
  const mark = ok ? p.ok("\u2713") : p.fail("\u2717");
202
288
  const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
@@ -212,22 +298,82 @@ function renderSummary(item, p) {
212
298
  }
213
299
  return lines.join("\n");
214
300
  }
301
+ function statusCoverage(items) {
302
+ const relevant = items.filter((i) => i.status !== "unsupported");
303
+ const adopted = relevant.filter((i) => i.adopted === true);
304
+ const conformant = adopted.filter((i) => i.conformant === true);
305
+ return {
306
+ total: relevant.length,
307
+ adopted: adopted.length,
308
+ conformant: conformant.length,
309
+ drifted: adopted.length - conformant.length
310
+ };
311
+ }
312
+ function presetShort(preset) {
313
+ return typeof preset === "string" ? preset.replace("@hublo/sentinel/tsconfig/", "") : "?";
314
+ }
315
+ function renderStatusRow(item, p) {
316
+ if (item.status === "unsupported") return renderUnsupportedRow(item, p);
317
+ const adopted = item.adopted === true;
318
+ const conformant = item.conformant === true;
319
+ const mark = !adopted ? p.dim("\xB7") : conformant ? p.ok("\u2713") : p.fail("\u2717");
320
+ const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
321
+ if (!adopted) return `${head} ${p.dim("\u2014 not adopted")}`;
322
+ if (conformant) {
323
+ return `${head} ${p.dim("\u2014")} ${p.ok("adopted")} ${p.dim(`(${presetShort(item.preset)}) conformant`)}`;
324
+ }
325
+ const drift = Array.isArray(item.drift) ? item.drift.join(", ") : "";
326
+ return `${head} ${p.dim("\u2014")} ${p.ok("adopted")} ${p.dim(`(${presetShort(item.preset)})`)} ${p.fail(`drift: ${drift}`)}`;
327
+ }
328
+ function renderStatusSummary(items, p) {
329
+ const c = statusCoverage(items);
330
+ const drift = c.drifted > 0 ? p.fail(` \xB7 ${c.drifted} drifted`) : "";
331
+ return ` ${p.strong("coverage:")} ${c.adopted}/${c.total} adopted ${p.dim("\xB7")} ${c.conformant}/${c.adopted} conformant${drift}`;
332
+ }
333
+
334
+ // src/shared/node-version.ts
335
+ var MIN_NODE = "20.12.0";
336
+ function parts(version) {
337
+ const [major = 0, minor = 0, patch = 0] = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
338
+ return [major, minor, patch];
339
+ }
340
+ function checkNodeVersion(current, min = MIN_NODE) {
341
+ const [cMajor, cMinor, cPatch] = parts(current);
342
+ const [mMajor, mMinor, mPatch] = parts(min);
343
+ const ok = cMajor > mMajor || cMajor === mMajor && cMinor > mMinor || cMajor === mMajor && cMinor === mMinor && cPatch >= mPatch;
344
+ if (ok) return { ok: true };
345
+ return {
346
+ ok: false,
347
+ message: `sentinel requires Node >= ${min}, but you are on ${current}. Switch with fnm/nvm (e.g. \`fnm use ${mMajor}\`) and re-run.`
348
+ };
349
+ }
215
350
 
216
351
  // bin/sentinel.ts
352
+ var nodeCheck = checkNodeVersion(process.versions.node);
353
+ if (!nodeCheck.ok) {
354
+ process.stderr.write(`${nodeCheck.message}
355
+ `);
356
+ process.exit(1);
357
+ }
358
+ registerAdapters();
217
359
  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
360
  "before",
219
361
  [
220
362
  "A check composes: verb + type + location.",
221
- " verb what to do: --run --inspect --report --update",
363
+ " verb what to do: --run --inspect --report --status --update",
222
364
  " type which check: --lint --typescript ... (omit = all types; or --all)",
223
365
  " where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
224
366
  " --ci (affected), or all modules. --update targets one module only.",
225
367
  ""
226
368
  ].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(
369
+ ).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
370
  "--module <name>",
229
371
  "from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
230
- ).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(
372
+ ).option("--flavour <name>", `override the detected stack preset (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--update)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").option(
373
+ "--max-diagnostics <n>",
374
+ "cap the diagnostics embedded per module in --report (0 = no cap)",
375
+ "100"
376
+ ).addHelpText(
231
377
  "after",
232
378
  [
233
379
  "",
@@ -238,7 +384,15 @@ program.name("sentinel").description("One CLI that guards code health: presets,
238
384
  " sentinel --report --ci # from root \u2192 affected only",
239
385
  " sentinel --update --typescript --flavour react # write stubs for the current module"
240
386
  ].join("\n")
241
- ).parse();
387
+ ).addHelpText("after", () => {
388
+ const available = availableTargets();
389
+ const planned = TARGETS.filter((t) => !available.includes(t));
390
+ return [
391
+ "",
392
+ `Available now: ${available.length ? available.map((t) => `--${t}`).join(", ") : "(none yet)"}`,
393
+ planned.length ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(", ")}` : ""
394
+ ].filter(Boolean).join("\n");
395
+ }).parse();
242
396
  var opts = program.opts();
243
397
  function toCamel(flag) {
244
398
  return flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -275,6 +429,13 @@ if (opts.all && namedTargets.length > 0) {
275
429
  );
276
430
  }
277
431
  var targets = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets;
432
+ var targetsExplicit = !(opts.all || namedTargets.length === 0);
433
+ var maxDiagnostics = Number(opts.maxDiagnostics);
434
+ if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
435
+ program.error(
436
+ `--max-diagnostics must be a non-negative integer (0 = no cap); got ${JSON.stringify(opts.maxDiagnostics)}.`
437
+ );
438
+ }
278
439
  if (opts.dryRun && verb !== "update") {
279
440
  program.error("--dry-run only applies to --update (the read verbs never write).");
280
441
  }
@@ -282,6 +443,13 @@ async function runVerb() {
282
443
  const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
283
444
  const out = palette(process.stdout);
284
445
  const err = palette(process.stderr);
446
+ if (opts.json && verb === "run") {
447
+ process.stderr.write(
448
+ err.warn(
449
+ "note: --json is ignored for --run (it streams the tool output); use --report --json for a machine envelope."
450
+ ) + "\n"
451
+ );
452
+ }
285
453
  const started = Date.now();
286
454
  const { results, worstCode } = await analyse({
287
455
  verb,
@@ -289,14 +457,20 @@ async function runVerb() {
289
457
  modules,
290
458
  runner: opts.runner,
291
459
  flavour,
460
+ targetsExplicit,
461
+ maxDiagnostics,
292
462
  ci: Boolean(opts.ci),
293
463
  fix: Boolean(opts.fix),
294
464
  onProgress: (done, total, name) => process.stderr.write(err.dim(` [${done}/${total}] ${name}
295
465
  `))
296
466
  });
297
- const summary = generateSummaries(results);
467
+ const summary = generateSummaries(results, targets);
298
468
  if (opts.json && verb !== "run") {
299
- process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
469
+ const payload = verb === "status" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
470
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
471
+ } else if (verb === "status") {
472
+ for (const item of summary.results) process.stdout.write(renderStatusRow(item, out) + "\n");
473
+ process.stdout.write(renderStatusSummary(summary.results, out) + "\n");
300
474
  } else {
301
475
  for (const item of summary.results) {
302
476
  process.stdout.write(renderSummary(item, out) + "\n");
@@ -306,6 +480,15 @@ async function runVerb() {
306
480
  err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
307
481
  `)
308
482
  );
483
+ if (targetsExplicit && summary.skipped.length > 0) {
484
+ const avail = availableTargets();
485
+ process.stderr.write(
486
+ err.fail(
487
+ `sentinel: target(s) not available yet: ${summary.skipped.map((t) => `--${t}`).join(", ")}. Available now: ${avail.length ? avail.map((t) => `--${t}`).join(", ") : "(none yet)"}.`
488
+ ) + "\n"
489
+ );
490
+ return 1;
491
+ }
309
492
  return verb === "run" || opts.ci ? worstCode : 0;
310
493
  }
311
494
  async function runUpdate() {
@@ -316,18 +499,25 @@ async function runUpdate() {
316
499
  "--update writes files: target one module (run from its directory, or pass --module). Adopting every module at once is intentionally not allowed \u2014 adopt gradually."
317
500
  );
318
501
  }
502
+ const available = availableTargets();
503
+ if (targetsExplicit) {
504
+ const unwired = targets.filter((type) => !available.includes(type));
505
+ if (unwired.length > 0) {
506
+ program.error(
507
+ `target(s) not available yet: ${unwired.map((t) => `--${t}`).join(", ")}. Available now: ${available.map((t) => `--${t}`).join(", ") || "(none yet)"}.`
508
+ );
509
+ }
510
+ }
511
+ const toRun = targets.filter((type) => available.includes(type));
319
512
  let worst = 0;
320
- for (const type of targets) {
513
+ for (const type of toRun) {
321
514
  try {
322
515
  const code = await dispatch({
323
516
  verb,
324
517
  target: type,
325
518
  runner: opts.runner,
326
- module: module.name,
327
519
  cwd: module.root,
328
520
  flavour,
329
- ci: Boolean(opts.ci),
330
- fix: Boolean(opts.fix),
331
521
  dryRun: Boolean(opts.dryRun),
332
522
  json: Boolean(opts.json)
333
523
  });
@@ -336,13 +526,11 @@ async function runUpdate() {
336
526
  process.stderr.write(`
337
527
  sentinel (${type}): ${asMessage(err)}
338
528
  `);
339
- if (targets.length === 1) return 1;
340
529
  worst = Math.max(worst, 1);
341
530
  }
342
531
  }
343
532
  return worst;
344
533
  }
345
- registerAdapters();
346
534
  (verb === "update" ? runUpdate() : runVerb()).then((code) => process.exit(code)).catch((err) => {
347
535
  process.stderr.write(`
348
536
  sentinel: ${asMessage(err)}