@hublo/sentinel 0.1.0-alpha.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- detectFramework,
3
+ availableTargets,
4
+ describeFramework,
4
5
  dispatch,
5
6
  palette,
6
7
  readNxProjectName,
8
+ readOwnPackage,
7
9
  readOwnVersion,
8
10
  readProjectPackageJson,
9
11
  registerAdapters,
10
12
  resolve,
11
13
  resolveBin
12
- } from "../chunk-UG74KTIU.js";
14
+ } from "../chunk-HKGCWPRT.js";
13
15
 
14
16
  // bin/sentinel.ts
15
17
  import { program } from "commander";
@@ -105,7 +107,7 @@ function resolveContext(cwd2, opts2) {
105
107
  }
106
108
 
107
109
  // src/core/domain.ts
108
- var VERBS = ["run", "inspect", "update", "report", "status"];
110
+ var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
109
111
  var TARGETS = [
110
112
  "lint",
111
113
  "format",
@@ -124,19 +126,45 @@ async function analyse(params) {
124
126
  let worstCode = 0;
125
127
  let done = 0;
126
128
  for (const module of params.modules) {
127
- const flavour2 = params.flavour ?? detectFramework(readProjectPackageJson(module.root));
129
+ let flavour2;
130
+ if (params.flavour) {
131
+ flavour2 = params.flavour;
132
+ } else {
133
+ const detection = describeFramework(readProjectPackageJson(module.root));
134
+ flavour2 = detection.flavour;
135
+ if (detection.ambiguous) {
136
+ const warn = palette(process.stderr);
137
+ process.stderr.write(
138
+ warn.warn(
139
+ `sentinel: ${module.name}: flavour is ambiguous, detected "${flavour2}" (from ${detection.source}), also found ${detection.conflicts.join(", ")}. Pass --flavour to be explicit.`
140
+ ) + "\n"
141
+ );
142
+ }
143
+ }
128
144
  const ctx = {
129
145
  module: module.name,
130
146
  cwd: module.root,
131
147
  flavour: flavour2,
132
148
  ci: params.ci,
133
- fix: params.fix
149
+ fix: params.fix,
150
+ maxDiagnostics: params.maxDiagnostics
134
151
  };
135
152
  for (const target of params.targets) {
136
153
  let adapter;
137
154
  try {
138
155
  adapter = resolve(target, flavour2, params.runner);
139
- } catch {
156
+ } catch (error) {
157
+ if (params.targetsExplicit) {
158
+ const reason = error instanceof Error ? error.message : String(error);
159
+ results.push({
160
+ project: module.name,
161
+ target,
162
+ flavour: flavour2,
163
+ ok: false,
164
+ status: "unsupported",
165
+ data: { reason }
166
+ });
167
+ }
140
168
  continue;
141
169
  }
142
170
  try {
@@ -150,7 +178,14 @@ async function analyse(params) {
150
178
  );
151
179
  }
152
180
  const result = await adapter.run(ctx);
153
- results.push({ project: module.name, target, flavour: flavour2, ok: result.ok, data: {} });
181
+ results.push({
182
+ project: module.name,
183
+ target,
184
+ flavour: flavour2,
185
+ ok: result.ok,
186
+ status: result.ok ? "ok" : "failed",
187
+ data: {}
188
+ });
154
189
  worstCode = Math.max(worstCode, result.code);
155
190
  } else if (params.verb === "report") {
156
191
  const result = await adapter.report(ctx);
@@ -159,21 +194,43 @@ async function analyse(params) {
159
194
  target,
160
195
  flavour: flavour2,
161
196
  ok: result.ok,
197
+ status: result.ok ? "ok" : "failed",
162
198
  data: result.metrics ?? {}
163
199
  });
164
200
  worstCode = Math.max(worstCode, result.code);
165
201
  } else if (params.verb === "inspect") {
166
202
  const config = await adapter.inspect(ctx);
167
- results.push({ project: module.name, target, flavour: flavour2, ok: true, data: config });
203
+ results.push({
204
+ project: module.name,
205
+ target,
206
+ flavour: flavour2,
207
+ ok: true,
208
+ status: "ok",
209
+ data: config
210
+ });
168
211
  } else {
169
212
  const status = await adapter.status(ctx);
170
213
  const ok = !status.adopted || status.conformant;
171
- results.push({ project: module.name, target, flavour: flavour2, ok, data: status });
214
+ results.push({
215
+ project: module.name,
216
+ target,
217
+ flavour: flavour2,
218
+ ok,
219
+ status: ok ? "ok" : "failed",
220
+ data: status
221
+ });
172
222
  if (!ok) worstCode = Math.max(worstCode, 1);
173
223
  }
174
224
  } catch (error) {
175
225
  const message = error instanceof Error ? error.message : String(error);
176
- results.push({ project: module.name, target, flavour: flavour2, ok: false, data: { error: message } });
226
+ results.push({
227
+ project: module.name,
228
+ target,
229
+ flavour: flavour2,
230
+ ok: false,
231
+ status: "failed",
232
+ data: { error: message }
233
+ });
177
234
  worstCode = Math.max(worstCode, 1);
178
235
  }
179
236
  }
@@ -182,16 +239,34 @@ async function analyse(params) {
182
239
  return { results, worstCode };
183
240
  }
184
241
  function generateSummary(result) {
185
- const { project, target, flavour: flavour2, ok, data } = result;
242
+ const { project, target, flavour: flavour2, ok, status, data } = result;
186
243
  const details = data && typeof data === "object" ? data : { value: data };
187
- return { project, target, flavour: flavour2, ok, ...details };
244
+ return { project, target, flavour: flavour2, ok, status, ...details };
188
245
  }
189
- function generateSummaries(results) {
190
- return { schemaVersion: 1, results: results.map(generateSummary) };
246
+ function generateSummaries(results, requestedTargets) {
247
+ const available = new Set(availableTargets());
248
+ return {
249
+ schemaVersion: 2,
250
+ executed: requestedTargets.filter((t) => available.has(t)),
251
+ skipped: requestedTargets.filter((t) => !available.has(t)),
252
+ results: results.map(generateSummary)
253
+ };
191
254
  }
192
255
 
193
256
  // src/core/render.ts
194
- var HEAD_KEYS = /* @__PURE__ */ new Set(["project", "module", "target", "flavour", "ok"]);
257
+ var HEAD_KEYS = /* @__PURE__ */ new Set([
258
+ "project",
259
+ "module",
260
+ "target",
261
+ "flavour",
262
+ "ok",
263
+ "status",
264
+ "reason",
265
+ // The structured diagnostics are for `--json` consumers; the human row stays the concise
266
+ // `errors=N` line rather than dumping every diagnostic inline.
267
+ "diagnostics",
268
+ "diagnosticsTruncated"
269
+ ]);
195
270
  function isDeferredRules(value) {
196
271
  return Array.isArray(value) && value.every((entry) => typeof entry === "object" && entry !== null && "rule" in entry);
197
272
  }
@@ -199,9 +274,16 @@ function renderValue(key, value, p) {
199
274
  if ((key === "errors" || key === "implicitAny") && typeof value === "number") {
200
275
  return value > 0 ? p.fail(String(value)) : p.ok(String(value));
201
276
  }
277
+ if (key === "implicitAny" && value === "deferred") return p.warn("deferred");
202
278
  return typeof value === "string" ? value : JSON.stringify(value);
203
279
  }
280
+ function renderUnsupportedRow(item, p) {
281
+ const reason = typeof item.reason === "string" ? ` ${p.dim(`(${item.reason})`)}` : "";
282
+ const head = ` ${p.dim("\xB7")} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
283
+ return `${head} ${p.dim("\u2014")} ${p.warn("unsupported")}${reason}`;
284
+ }
204
285
  function renderSummary(item, p) {
286
+ if (item.status === "unsupported") return renderUnsupportedRow(item, p);
205
287
  const ok = item.ok === true;
206
288
  const mark = ok ? p.ok("\u2713") : p.fail("\u2717");
207
289
  const head = ` ${mark} ${p.strong(String(item.project))} ${p.dim(`(${item.flavour})`)} ${item.target}`;
@@ -218,10 +300,11 @@ function renderSummary(item, p) {
218
300
  return lines.join("\n");
219
301
  }
220
302
  function statusCoverage(items) {
221
- const adopted = items.filter((i) => i.adopted === true);
303
+ const relevant = items.filter((i) => i.status !== "unsupported");
304
+ const adopted = relevant.filter((i) => i.adopted === true);
222
305
  const conformant = adopted.filter((i) => i.conformant === true);
223
306
  return {
224
- total: items.length,
307
+ total: relevant.length,
225
308
  adopted: adopted.length,
226
309
  conformant: conformant.length,
227
310
  drifted: adopted.length - conformant.length
@@ -231,6 +314,7 @@ function presetShort(preset) {
231
314
  return typeof preset === "string" ? preset.replace("@hublo/sentinel/tsconfig/", "") : "?";
232
315
  }
233
316
  function renderStatusRow(item, p) {
317
+ if (item.status === "unsupported") return renderUnsupportedRow(item, p);
234
318
  const adopted = item.adopted === true;
235
319
  const conformant = item.conformant === true;
236
320
  const mark = !adopted ? p.dim("\xB7") : conformant ? p.ok("\u2713") : p.fail("\u2717");
@@ -248,21 +332,116 @@ function renderStatusSummary(items, p) {
248
332
  return ` ${p.strong("coverage:")} ${c.adopted}/${c.total} adopted ${p.dim("\xB7")} ${c.conformant}/${c.adopted} conformant${drift}`;
249
333
  }
250
334
 
335
+ // src/core/workspace-prep.ts
336
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
337
+ import { dirname, join as join3 } from "path";
338
+ var OVERRIDE_KEY = "i18next>typescript";
339
+ var NATIVE_TS_ALIAS = "@typescript/native";
340
+ var WORKSPACE_YAML = "pnpm-workspace.yaml";
341
+ var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
342
+ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
343
+ function findWorkspaceRoot(startDir) {
344
+ let dir = startDir;
345
+ for (; ; ) {
346
+ if (existsSync2(join3(dir, WORKSPACE_ROOT_MARKER))) return dir;
347
+ const parent = dirname(dir);
348
+ if (parent === dir) return void 0;
349
+ dir = parent;
350
+ }
351
+ }
352
+ function declaredNativeTs(pkg) {
353
+ const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
354
+ if (!spec) return void 0;
355
+ const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
356
+ return version || void 0;
357
+ }
358
+ function ensureI18nextSingleton(root, dryRun) {
359
+ const pkgPath = join3(root, "package.json");
360
+ if (!existsSync2(pkgPath)) return void 0;
361
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
362
+ const want = declaredNativeTs(pkg);
363
+ if (!want) return void 0;
364
+ const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
365
+ if (have === want) return void 0;
366
+ if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} (i18next singleton)`;
367
+ pkg.pnpm ??= {};
368
+ pkg.pnpm.overrides ??= {};
369
+ pkg.pnpm.overrides[OVERRIDE_KEY] = want;
370
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
371
+ return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
372
+ }
373
+ function ensureReleaseAgeAllowList(root, dryRun) {
374
+ const yamlPath = join3(root, WORKSPACE_YAML);
375
+ if (!existsSync2(yamlPath)) return void 0;
376
+ const own = readOwnPackage().name;
377
+ const lines = readFileSync2(yamlPath, "utf8").split("\n");
378
+ const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
379
+ if (keyIdx === -1) return void 0;
380
+ let lastItemIdx = keyIdx;
381
+ let indent = " ";
382
+ for (let i = keyIdx + 1; i < lines.length; i++) {
383
+ const match = lines[i]?.match(LIST_ITEM);
384
+ if (!match) break;
385
+ indent = match[1] ?? indent;
386
+ lastItemIdx = i;
387
+ if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
388
+ }
389
+ if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
390
+ lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
391
+ writeFileSync(yamlPath, lines.join("\n"));
392
+ return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
393
+ }
394
+ function ensureWorkspacePrep(opts2) {
395
+ const dryRun = Boolean(opts2.dryRun);
396
+ return [
397
+ ensureI18nextSingleton(opts2.root, dryRun),
398
+ ensureReleaseAgeAllowList(opts2.root, dryRun)
399
+ ].filter((message) => message !== void 0);
400
+ }
401
+
402
+ // src/shared/node-version.ts
403
+ var MIN_NODE = "20.12.0";
404
+ function parts(version) {
405
+ const [major = 0, minor = 0, patch = 0] = version.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
406
+ return [major, minor, patch];
407
+ }
408
+ function checkNodeVersion(current, min = MIN_NODE) {
409
+ const [cMajor, cMinor, cPatch] = parts(current);
410
+ const [mMajor, mMinor, mPatch] = parts(min);
411
+ const ok = cMajor > mMajor || cMajor === mMajor && cMinor > mMinor || cMajor === mMajor && cMinor === mMinor && cPatch >= mPatch;
412
+ if (ok) return { ok: true };
413
+ return {
414
+ ok: false,
415
+ message: `sentinel requires Node >= ${min}, but you are on ${current}. Switch with fnm/nvm (e.g. \`fnm use ${mMajor}\`) and re-run.`
416
+ };
417
+ }
418
+
251
419
  // bin/sentinel.ts
420
+ var nodeCheck = checkNodeVersion(process.versions.node);
421
+ if (!nodeCheck.ok) {
422
+ process.stderr.write(`${nodeCheck.message}
423
+ `);
424
+ process.exit(1);
425
+ }
426
+ registerAdapters();
252
427
  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(
253
428
  "before",
254
429
  [
255
430
  "A check composes: verb + type + location.",
256
- " verb what to do: --run --inspect --report --status --update",
431
+ " verb what to do: --run --inspect --report --status --init (--migrate: planned)",
257
432
  " type which check: --lint --typescript ... (omit = all types; or --all)",
258
433
  " where run from a MODULE dir \u2192 that module; from the ROOT \u2192 --module <name>,",
259
- " --ci (affected), or all modules. --update targets one module only.",
434
+ " --ci (affected), or all modules. --init targets one module only.",
260
435
  ""
261
436
  ].join("\n")
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(
437
+ ).option("--run", "execute the target tool").option("--inspect", "show the resolved configuration").option("--init", "set up a module: write its config stubs + the workspace prep it needs").option("--migrate", "planned: change an already-initialized setup (not available yet)").option("--report", "metrics and health report").option("--status", "adoption + conformity across modules (coverage + drift)").option("--lint", "linting").option("--format", "formatting").option("--typescript", "type checking").option("--build", "build").option("--test", "tests").option("--static-analysis", "cycles, complexity, duplication, centrality").option("--runtime-analysis", "bundle, Lighthouse, web vitals").option("--arch", "architecture boundaries").option("--all", "every target").option(
263
438
  "--module <name>",
264
439
  "from the workspace root: scope to one module (omit = all; inside a module dir, drop this)"
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(
440
+ ).option("--flavour <name>", `override the detected stack preset (${FLAVOURS.join(", ")})`).option("--runner <tool>", "override the default runner (e.g. eslint, biome)").option("--ci", "CI mode: from the root, only the affected modules; non-zero exit on failure").option("--fix", "auto-fix where applicable").option("--dry-run", "preview the changes without writing (--init)").option("--json", "machine-readable JSON output (report/inspect/--dry-run)").option(
441
+ "--max-diagnostics <n>",
442
+ "cap the diagnostics embedded per module in --report (0 = no cap)",
443
+ "100"
444
+ ).addHelpText(
266
445
  "after",
267
446
  [
268
447
  "",
@@ -271,9 +450,17 @@ program.name("sentinel").description("One CLI that guards code health: presets,
271
450
  " sentinel --report --typescript --module bff-admin # from root \u2192 one module",
272
451
  " sentinel --report # from root \u2192 all types, all modules",
273
452
  " sentinel --report --ci # from root \u2192 affected only",
274
- " sentinel --update --typescript --flavour react # write stubs for the current module"
453
+ " sentinel --init --typescript --flavour react # set up the current module + workspace"
275
454
  ].join("\n")
276
- ).parse();
455
+ ).addHelpText("after", () => {
456
+ const available = availableTargets();
457
+ const planned = TARGETS.filter((t) => !available.includes(t));
458
+ return [
459
+ "",
460
+ `Available now: ${available.length ? available.map((t) => `--${t}`).join(", ") : "(none yet)"}`,
461
+ planned.length ? `Planned (ship in later tickets): ${planned.map((t) => `--${t}`).join(", ")}` : ""
462
+ ].filter(Boolean).join("\n");
463
+ }).parse();
277
464
  var opts = program.opts();
278
465
  function toCamel(flag) {
279
466
  return flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -310,13 +497,27 @@ if (opts.all && namedTargets.length > 0) {
310
497
  );
311
498
  }
312
499
  var targets = opts.all || namedTargets.length === 0 ? [...TARGETS] : namedTargets;
313
- if (opts.dryRun && verb !== "update") {
314
- program.error("--dry-run only applies to --update (the read verbs never write).");
500
+ var targetsExplicit = !(opts.all || namedTargets.length === 0);
501
+ var maxDiagnostics = Number(opts.maxDiagnostics);
502
+ if (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 0) {
503
+ program.error(
504
+ `--max-diagnostics must be a non-negative integer (0 = no cap); got ${JSON.stringify(opts.maxDiagnostics)}.`
505
+ );
506
+ }
507
+ if (opts.dryRun && verb !== "init") {
508
+ program.error("--dry-run only applies to --init (the read verbs never write).");
315
509
  }
316
510
  async function runVerb() {
317
511
  const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: Boolean(opts.ci) });
318
512
  const out = palette(process.stdout);
319
513
  const err = palette(process.stderr);
514
+ if (opts.json && verb === "run") {
515
+ process.stderr.write(
516
+ err.warn(
517
+ "note: --json is ignored for --run (it streams the tool output); use --report --json for a machine envelope."
518
+ ) + "\n"
519
+ );
520
+ }
320
521
  const started = Date.now();
321
522
  const { results, worstCode } = await analyse({
322
523
  verb,
@@ -324,12 +525,14 @@ async function runVerb() {
324
525
  modules,
325
526
  runner: opts.runner,
326
527
  flavour,
528
+ targetsExplicit,
529
+ maxDiagnostics,
327
530
  ci: Boolean(opts.ci),
328
531
  fix: Boolean(opts.fix),
329
532
  onProgress: (done, total, name) => process.stderr.write(err.dim(` [${done}/${total}] ${name}
330
533
  `))
331
534
  });
332
- const summary = generateSummaries(results);
535
+ const summary = generateSummaries(results, targets);
333
536
  if (opts.json && verb !== "run") {
334
537
  const payload = verb === "status" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
335
538
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
@@ -345,28 +548,44 @@ async function runVerb() {
345
548
  err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
346
549
  `)
347
550
  );
551
+ if (targetsExplicit && summary.skipped.length > 0) {
552
+ const avail = availableTargets();
553
+ process.stderr.write(
554
+ err.fail(
555
+ `sentinel: target(s) not available yet: ${summary.skipped.map((t) => `--${t}`).join(", ")}. Available now: ${avail.length ? avail.map((t) => `--${t}`).join(", ") : "(none yet)"}.`
556
+ ) + "\n"
557
+ );
558
+ return 1;
559
+ }
348
560
  return verb === "run" || opts.ci ? worstCode : 0;
349
561
  }
350
- async function runUpdate() {
562
+ async function runInit() {
351
563
  const { modules, scope } = resolveContext(cwd, { module: opts.module, ci: false });
352
564
  const [module] = modules;
353
565
  if (scope === "all" || scope === "affected" || !module) {
354
566
  program.error(
355
- "--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."
567
+ "--init writes files: target one module (run from its directory, or pass --module). Adopting every module at once is intentionally not allowed \u2014 adopt gradually."
356
568
  );
357
569
  }
570
+ const available = availableTargets();
571
+ if (targetsExplicit) {
572
+ const unwired = targets.filter((type) => !available.includes(type));
573
+ if (unwired.length > 0) {
574
+ program.error(
575
+ `target(s) not available yet: ${unwired.map((t) => `--${t}`).join(", ")}. Available now: ${available.map((t) => `--${t}`).join(", ") || "(none yet)"}.`
576
+ );
577
+ }
578
+ }
579
+ const toRun = targets.filter((type) => available.includes(type));
358
580
  let worst = 0;
359
- for (const type of targets) {
581
+ for (const type of toRun) {
360
582
  try {
361
583
  const code = await dispatch({
362
584
  verb,
363
585
  target: type,
364
586
  runner: opts.runner,
365
- module: module.name,
366
587
  cwd: module.root,
367
588
  flavour,
368
- ci: Boolean(opts.ci),
369
- fix: Boolean(opts.fix),
370
589
  dryRun: Boolean(opts.dryRun),
371
590
  json: Boolean(opts.json)
372
591
  });
@@ -375,16 +594,36 @@ async function runUpdate() {
375
594
  process.stderr.write(`
376
595
  sentinel (${type}): ${asMessage(err)}
377
596
  `);
378
- if (targets.length === 1) return 1;
379
597
  worst = Math.max(worst, 1);
380
598
  }
381
599
  }
600
+ if (worst === 0) {
601
+ const root = findWorkspaceRoot(module.root);
602
+ if (root) {
603
+ const changes = ensureWorkspacePrep({ root, dryRun: Boolean(opts.dryRun) });
604
+ const prefix = opts.dryRun ? " dry run: " : " ";
605
+ for (const change of changes) process.stderr.write(`${prefix}${change}
606
+ `);
607
+ if (changes.length > 0 && !opts.dryRun) {
608
+ process.stderr.write(
609
+ palette(process.stderr).dim(" run `pnpm install` to apply the workspace changes\n")
610
+ );
611
+ }
612
+ }
613
+ }
382
614
  return worst;
383
615
  }
384
- registerAdapters();
385
- (verb === "update" ? runUpdate() : runVerb()).then((code) => process.exit(code)).catch((err) => {
616
+ async function main() {
617
+ if (verb === "migrate") {
618
+ program.error("--migrate is planned and not available yet; use --init to set a module up.");
619
+ }
620
+ const runSelectedVerb = verb === "init" ? runInit : runVerb;
621
+ const exitCode = await runSelectedVerb();
622
+ process.exit(exitCode);
623
+ }
624
+ main().catch((error) => {
386
625
  process.stderr.write(`
387
- sentinel: ${asMessage(err)}
626
+ sentinel: ${asMessage(error)}
388
627
  `);
389
628
  process.exit(1);
390
629
  });