@rrlab/cli 1.0.0 → 1.0.1-git-e008b4d.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,7 +1,7 @@
1
1
  // @generated by @usage-spec/commander from Commander.js metadata
2
2
  name rr
3
3
  bin rr
4
- version "1.0.0"
4
+ version "1.0.1-git-e008b4d.0"
5
5
  usage "<command...> [options...]"
6
6
  flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
7
7
  cmd completion help="print shell completion script (usage)" {
@@ -38,7 +38,7 @@ cmd format help="check & fix format errors" {
38
38
  cmd doctor help="check if the underlying tool is working correctly"
39
39
  }
40
40
  cmd check help="run static checks" {
41
- long_help "Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails."
41
+ long_help "Runs `rr jsc` then `rr tsc` in-process, each as its own section. Aggregates their exit codes — non-zero when either subcommand fails."
42
42
  }
43
43
  cmd doctor help="run all plugin doctors" {
44
44
  long_help "Runs the `doctor()` of every configured plugin capability. Each plugin reports ok / not working. The exit code is non-zero if any reports not working."
package/dist/config.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { u as Plugin } from "./types-DnqiiIxe.mjs";
1
+ import { u as Plugin } from "./types-6gZWuLJf.mjs";
2
2
 
3
3
  //#region src/types/config.d.ts
4
4
  type UserConfig = {
package/dist/plugin.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Linter, D as TypeChecker, E as TypeCheckOptions, O as ReleaseService, S as LintOptions, T as StaticCheckerOptions, _ as Doctor, a as InstallFlags, b as FormatOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, k as ReleaseServiceOptions, l as Packer, m as UninstallContext, n as ClackPromptsSelectOption, o as InstallResult, p as PluginKind, r as FileOp, s as JsonEdit, t as ClackPrompts, u as Plugin, v as DoctorOutput, w as StaticChecker, x as Formatter, y as DoctorResult } from "./types-DnqiiIxe.mjs";
1
+ import { C as StaticChecker, D as ReleaseService, E as TypeChecker, O as ReleaseServiceOptions, S as RunReport, T as TypeCheckOptions, _ as Doctor, a as InstallFlags, b as LintOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, l as Packer, m as UninstallContext, n as ClackPromptsSelectOption, o as InstallResult, p as PluginKind, r as FileOp, s as JsonEdit, t as ClackPrompts, u as Plugin, v as FormatOptions, w as StaticCheckerOptions, x as Linter, y as Formatter } from "./types-6gZWuLJf.mjs";
2
2
  import { ShellService } from "@vlandoss/clibuddy";
3
3
 
4
4
  //#region src/plugin/decide-scaffold.d.ts
@@ -42,15 +42,6 @@ type PickPresetOptions<K extends string> = {
42
42
  };
43
43
  declare function pickPreset<K extends string>(ctx: InstallContext, opts: PickPresetOptions<K>): Promise<K>;
44
44
  //#endregion
45
- //#region ../../node_modules/.pnpm/tinyexec@1.1.2/node_modules/tinyexec/dist/main.d.mts
46
- //#endregion
47
- //#region src/main.d.ts
48
- interface Output {
49
- stderr: string;
50
- stdout: string;
51
- exitCode: number | undefined;
52
- }
53
- //#endregion
54
45
  //#region src/plugin/tool-service.d.ts
55
46
  type ToolServiceOptions = {
56
47
  pkg: string;
@@ -66,9 +57,8 @@ type ToolServiceOptions = {
66
57
  */
67
58
  from: string;
68
59
  };
69
- type ExecOptions = {
60
+ type RunReportOptions = {
70
61
  cwd?: string;
71
- verbose?: boolean;
72
62
  };
73
63
  declare class ToolService {
74
64
  #private;
@@ -83,8 +73,15 @@ declare class ToolService {
83
73
  from
84
74
  }: ToolServiceOptions);
85
75
  getBinDir(): Promise<string>;
86
- exec(args?: string[], options?: ExecOptions): Promise<Output>;
87
- doctor(): Promise<DoctorResult>;
76
+ /**
77
+ * Runs the tool capturing its output instead of streaming it, and reports the
78
+ * verdict straight from the exit code — never a guess parsed from the output.
79
+ * The board needs the capture to attribute each parallel run's output to its
80
+ * package; the non-zero exit is returned (not thrown) so every task settles
81
+ * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
82
+ */
83
+ runReport(args?: string[], options?: RunReportOptions): Promise<RunReport>;
84
+ doctor(): Promise<RunReport>;
88
85
  }
89
86
  //#endregion
90
- export { type ClackPrompts, type ClackPromptsSelectOption, type DecideScaffoldOptions, type Doctor, type DoctorOutput, type DoctorResult, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, PLUGIN_KINDS, type Packer, type PickPresetOptions, type Plugin, type PluginCapabilities, type PluginContext, type PluginDefinition, type PluginKind, ReleaseService, type ReleaseServiceOptions, type ScaffoldDecision, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, decideScaffold, definePlugin, pickPreset };
87
+ export { type ClackPrompts, type ClackPromptsSelectOption, type DecideScaffoldOptions, type Doctor, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, PLUGIN_KINDS, type Packer, type PickPresetOptions, type Plugin, type PluginCapabilities, type PluginContext, type PluginDefinition, type PluginKind, ReleaseService, type ReleaseServiceOptions, type RunReport, type ScaffoldDecision, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, decideScaffold, definePlugin, pickPreset };
package/dist/plugin.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { resolvePackageBin } from "@vlandoss/clibuddy";
1
+ import { palette, resolvePackageBin } from "@vlandoss/clibuddy";
2
2
  //#region src/plugin/decide-scaffold.ts
3
3
  async function decideScaffold(ctx, opts) {
4
4
  const { label, fileExists, patchHint, unattendedExistingAction = "patch" } = opts;
@@ -63,25 +63,41 @@ var ToolService = class {
63
63
  binName: this.#bin
64
64
  });
65
65
  }
66
- async exec(args = [], options = {}) {
67
- const { cwd, verbose } = options;
68
- return (cwd ? this.#shellService.at(cwd) : this.#shellService).run(await this.getBinDir(), args, {
69
- display: this.#bin,
70
- verbose
71
- });
66
+ /**
67
+ * Runs the tool capturing its output instead of streaming it, and reports the
68
+ * verdict straight from the exit code never a guess parsed from the output.
69
+ * The board needs the capture to attribute each parallel run's output to its
70
+ * package; the non-zero exit is returned (not thrown) so every task settles
71
+ * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
72
+ */
73
+ async runReport(args = [], options = {}) {
74
+ const output = await (options.cwd ? this.#shellService.at(options.cwd) : this.#shellService).runCaptured(await this.getBinDir(), args, { throwOnError: false });
75
+ const header = palette.dim(`$ ${[this.#bin, ...args].join(" ")}`);
76
+ const body = combine(output.stdout, output.stderr);
77
+ return {
78
+ ok: output.exitCode === 0,
79
+ output: body ? `${header}\n${body}` : header
80
+ };
72
81
  }
73
82
  async doctor() {
74
83
  const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
84
+ const ok = output.exitCode === 0;
85
+ const command = palette.dim(`$ ${this.#bin} --help`);
86
+ if (ok) return {
87
+ ok,
88
+ output: command
89
+ };
90
+ const detail = combine(output.stdout, output.stderr);
75
91
  return {
76
- ok: output.exitCode === 0,
77
- output: {
78
- stdout: output.stdout,
79
- stderr: output.stderr,
80
- exitCode: output.exitCode
81
- }
92
+ ok,
93
+ output: detail ? `${command}\n${detail}` : command
82
94
  };
83
95
  }
84
96
  };
97
+ /** Joins the non-empty, trimmed streams of a captured run. */
98
+ function combine(stdout, stderr) {
99
+ return [stdout, stderr].map((stream) => stream?.trim()).filter(Boolean).join("\n");
100
+ }
85
101
  //#endregion
86
102
  //#region src/plugin/bin-probe.ts
87
103
  async function probeBins(services, pluginName) {
package/dist/run.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
- import { colorize, createPkg, createShellService, cwd, dirnameOf, palette, run, text } from "@vlandoss/clibuddy";
1
+ import path, { basename } from "node:path";
2
+ import { colorize, createPkg, createShellService, cwd, dirnameOf, palette, run, runTaskBoard, text } from "@vlandoss/clibuddy";
3
3
  import { generateToStdout } from "@usage-spec/commander";
4
4
  import { Argument, Option, createCommand } from "commander";
5
5
  import fs from "node:fs";
@@ -137,6 +137,101 @@ async function createContext(binDir) {
137
137
  };
138
138
  }
139
139
  //#endregion
140
+ //#region src/program/missing-plugin.ts
141
+ const SUGGESTIONS = {
142
+ lint: [
143
+ "biome",
144
+ "oxc",
145
+ "eslint"
146
+ ],
147
+ format: ["biome", "oxc"],
148
+ jsc: ["biome"],
149
+ tsc: ["ts"],
150
+ pack: ["tsdown"]
151
+ };
152
+ function missingPluginError(kind) {
153
+ const aliases = SUGGESTIONS[kind] ?? [];
154
+ const officialList = aliases.map((a) => `@rrlab/${a}-plugin`).join(", ");
155
+ const addList = aliases.map((a) => `rr plugins add ${a}`).join(" | ");
156
+ return /* @__PURE__ */ new Error(`No plugin provides the '${kind}' capability.` + (officialList ? `\n Install one of: ${officialList}.` : "") + (addList ? `\n Try: ${addList}.` : ""));
157
+ }
158
+ //#endregion
159
+ //#region src/program/board.ts
160
+ /** `<command> (<tool>)`, deduped to just `<command>` when the tool's binary is the command itself (e.g. `tsc`). */
161
+ function commandTool(command, provider) {
162
+ return provider.bin === command ? command : `${command} (${provider.ui})`;
163
+ }
164
+ function pkgName(appPkg) {
165
+ return appPkg.packageJson.name ?? basename(appPkg.dirPath);
166
+ }
167
+ /**
168
+ * The one canonical row label for a single-target run: `<command> (<tool>) · <package>`.
169
+ * Every command/subcommand that acts on one target (lint, format, jsc, pack,
170
+ * single-app tsc, a `doctor` subcommand) builds its row through here so they
171
+ * all read identically.
172
+ */
173
+ function targetLabel(command, provider, appPkg) {
174
+ return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
175
+ }
176
+ /**
177
+ * The one canonical section title for a fan-out run: `<command> (<tool>) · <n> <unit>`
178
+ * (e.g. `tsc (oxlint) · 8 packages`). The tool is omitted when the fan-out
179
+ * spans several tools (`rr doctor` → `doctor · 3 tools`); the rows then carry
180
+ * the per-unit name.
181
+ */
182
+ function fanoutTitle(command, provider, count, unit) {
183
+ return `${provider ? commandTool(command, provider) : command} · ${count} ${unit}`;
184
+ }
185
+ /**
186
+ * Bridges a check-family verb (which returns a `RunReport`) to a board row.
187
+ * The row's spinner reflects the in-flight run; the captured `output` becomes
188
+ * the detail the board flushes grouped under the label.
189
+ */
190
+ function reportTask(label, run) {
191
+ return {
192
+ label,
193
+ async run() {
194
+ const report = await run();
195
+ return {
196
+ ok: report.ok,
197
+ detail: report.output
198
+ };
199
+ }
200
+ };
201
+ }
202
+ let collector = null;
203
+ async function runCheckSections(run) {
204
+ const previous = collector;
205
+ collector = [];
206
+ try {
207
+ await run();
208
+ return collector;
209
+ } finally {
210
+ collector = previous;
211
+ }
212
+ }
213
+ /** Runs the rows on the board and returns whether every row passed. */
214
+ async function runBoard(tasks, options = {}) {
215
+ const sink = collector;
216
+ const result = await runTaskBoard(tasks, {
217
+ ...options,
218
+ frame: options.frame ?? (sink !== null || void 0)
219
+ });
220
+ if (sink) sink.push(result);
221
+ return result;
222
+ }
223
+ /**
224
+ * The shared action body for a single-provider tool command (lint, format, jsc,
225
+ * pack): require the provider, run its verb as one board row labelled
226
+ * `<name> (<tool>) · <pkg>`, and aggregate the exit code. Commands that fan out
227
+ * (tsc) or compose siblings (check) call `runBoard` directly instead.
228
+ */
229
+ async function runToolCommand(ctx, spec) {
230
+ const { provider } = spec;
231
+ if (!provider) throw missingPluginError(spec.kind);
232
+ if (!(await runBoard([reportTask(targetLabel(spec.name, provider, ctx.appPkg), () => spec.run(provider))])).ok) process.exitCode = 1;
233
+ }
234
+ //#endregion
140
235
  //#region src/program/ui.ts
141
236
  const CREDITS_TEXT = `\nAcknowledgment:
142
237
  - kcd-scripts: for main inspiration
@@ -201,34 +296,42 @@ ${runRunColor(`
201
296
  * parent program without any late-binding ceremony.
202
297
  */
203
298
  function createCheckCommand(ctx) {
204
- return createCommand("check").summary(`run static checks${checkAnnotation(ctx)}`).description("Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.").action(async function checkAction() {
299
+ return createCommand("check").summary(`run static checks${checkAnnotation(ctx)}`).description("Runs `rr jsc` then `rr tsc` in-process, each as its own section. Aggregates their exit codes — non-zero when either subcommand fails.").action(async function checkAction() {
205
300
  const program = this.parent;
206
301
  if (!program) throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
207
- const cmds = ["jsc", "tsc"].map((name) => ({
208
- name,
209
- cmd: findCommand(program, name)
210
- }));
211
- const missing = cmds.filter(({ cmd }) => !cmd).map(({ name }) => name);
212
- if (missing.length > 0) {
213
- for (const name of missing) logger.error(`rr check: subcommand "${name}" is not registered.`);
214
- process.exitCode = 1;
215
- return;
216
- }
217
- const results = await Promise.allSettled(cmds.map(({ cmd }) => cmd.parseAsync([], { from: "user" })));
302
+ const start = Date.now();
218
303
  const failed = [];
219
- for (const [i, r] of results.entries()) if (r.status === "rejected") failed.push({
220
- name: cmds[i]?.name ?? "?",
221
- reason: r.reason
222
- });
223
- if (failed.length > 0) {
224
- for (const { name, reason } of failed) {
225
- const msg = reason instanceof Error ? reason.message : String(reason);
226
- logger.error(`rr check (${name}): ${msg}`);
304
+ let rendered = false;
305
+ for (const name of ["jsc", "tsc"]) {
306
+ const cmd = findCommand(program, name);
307
+ if (!cmd) {
308
+ logger.error(`rr check: subcommand "${name}" is not registered.`);
309
+ failed.push(name);
310
+ continue;
227
311
  }
228
- process.exitCode = 1;
312
+ if (rendered) process.stderr.write("\n");
313
+ let threw = false;
314
+ const results = await runCheckSections(async () => {
315
+ try {
316
+ await cmd.parseAsync([], { from: "user" });
317
+ } catch (reason) {
318
+ logger.error(`rr check (${name}): ${reason instanceof Error ? reason.message : String(reason)}`);
319
+ threw = true;
320
+ }
321
+ });
322
+ if (threw || results.some((r) => !r.ok)) failed.push(name);
323
+ rendered = true;
229
324
  }
325
+ process.stderr.write(`\n${checkVerdict(failed, Date.now() - start)}\n`);
326
+ if (failed.length > 0) process.exitCode = 1;
230
327
  });
231
328
  }
329
+ function checkVerdict(failed, ms) {
330
+ const elapsed = palette.dim(ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`);
331
+ const sep = palette.dim(" · ");
332
+ if (failed.length > 0) return `${palette.error("✖")} check failed${sep}${[...new Set(failed)].join(", ")}${sep}${elapsed}`;
333
+ return `${palette.success("✔")} check passed${sep}${elapsed}`;
334
+ }
232
335
  function findCommand(program, name) {
233
336
  return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
234
337
  }
@@ -322,21 +425,13 @@ const PLUGIN_KINDS = [
322
425
  //#region src/program/commands/doctor.ts
323
426
  /**
324
427
  * Subcommand factory used by every plugin-backed command (lint, format, jsc,
325
- * tsc, pack) to expose a `doctor` subcommand that verifies the underlying
326
- * tool is wired correctly. Each calls this with its own provider.
428
+ * tsc, pack) to expose a `doctor` subcommand that verifies the underlying tool
429
+ * is wired correctly. Renders the canonical `doctor (<tool>) · <pkg>` row like
430
+ * every other single-target command — `doctor()` returns a `RunReport`.
327
431
  */
328
- function createDoctorSubcommand(service) {
432
+ function createDoctorSubcommand(service, appPkg) {
329
433
  return createCommand("doctor").summary("check if the underlying tool is working correctly").action(async function doctorAction() {
330
- const debug = logger.subdebug("doctor");
331
- const { ok, output } = await service.doctor();
332
- if (ok) {
333
- logger.success(`${service.ui} ok`);
334
- debug("%O", output);
335
- } else {
336
- logger.error(`${service.ui} not working`);
337
- debug("%O", output);
338
- process.exit(output.exitCode ?? 1);
339
- }
434
+ if (!(await runBoard([reportTask(targetLabel("doctor", service, appPkg), () => service.doctor())])).ok) process.exitCode = 1;
340
435
  });
341
436
  }
342
437
  /**
@@ -352,23 +447,7 @@ function createDoctorCommand(ctx) {
352
447
  logger.info("No plugins configured. Use `rr plugins add <name>` to install one.");
353
448
  return;
354
449
  }
355
- const debug = logger.subdebug("doctor");
356
- const results = await Promise.all(services.map(async (svc) => {
357
- return {
358
- svc,
359
- result: await svc.doctor()
360
- };
361
- }));
362
- let failures = 0;
363
- for (const { svc, result } of results) if (result.ok) {
364
- logger.success(`${svc.ui} ok`);
365
- debug("%s: %O", svc.ui, result.output);
366
- } else {
367
- logger.error(`${svc.ui} not working`);
368
- debug("%s: %O", svc.ui, result.output);
369
- failures++;
370
- }
371
- if (failures > 0) process.exitCode = 1;
450
+ if (!(await runBoard(services.map((svc) => reportTask(svc.ui, () => svc.doctor())), { title: fanoutTitle("doctor", void 0, services.length, "tools") })).ok) process.exitCode = 1;
372
451
  });
373
452
  }
374
453
  function collectDistinctDoctors(ctx) {
@@ -377,33 +456,18 @@ function collectDistinctDoctors(ctx) {
377
456
  return [...seen];
378
457
  }
379
458
  //#endregion
380
- //#region src/program/missing-plugin.ts
381
- const SUGGESTIONS = {
382
- lint: [
383
- "biome",
384
- "oxc",
385
- "eslint"
386
- ],
387
- format: ["biome", "oxc"],
388
- jsc: ["biome"],
389
- tsc: ["ts"],
390
- pack: ["tsdown"]
391
- };
392
- function missingPluginError(kind) {
393
- const aliases = SUGGESTIONS[kind] ?? [];
394
- const officialList = aliases.map((a) => `@rrlab/${a}-plugin`).join(", ");
395
- const addList = aliases.map((a) => `rr plugins add ${a}`).join(" | ");
396
- return /* @__PURE__ */ new Error(`No plugin provides the '${kind}' capability.` + (officialList ? `\n Install one of: ${officialList}.` : "") + (addList ? `\n Try: ${addList}.` : ""));
397
- }
398
- //#endregion
399
459
  //#region src/program/commands/format.ts
400
460
  function createFormatCommand(ctx) {
401
461
  const formatter = ctx.registry.get("format");
402
462
  const cmd = createCommand("format").summary(`check & fix format errors${pluginAnnotation(formatter)}`).description("Checks the code for formatting issues and optionally fixes them, ensuring it adheres to the defined style standards.").option("--fix", "format all the code");
403
- if (formatter) cmd.addCommand(createDoctorSubcommand(formatter));
463
+ if (formatter) cmd.addCommand(createDoctorSubcommand(formatter, ctx.appPkg));
404
464
  cmd.action(async (options = {}) => {
405
- if (!formatter) throw missingPluginError("format");
406
- await formatter.format(options);
465
+ await runToolCommand(ctx, {
466
+ name: "format",
467
+ kind: "format",
468
+ provider: formatter,
469
+ run: (p) => p.format(options)
470
+ });
407
471
  });
408
472
  if (formatter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${formatter.ui} CLI to format the code.`);
409
473
  return cmd;
@@ -417,7 +481,8 @@ function createFormatCommand(ctx) {
417
481
  * prettier) but no single plugin claims `jsc`.
418
482
  *
419
483
  * The check runs lint then format sequentially — interleaved stdout from a
420
- * parallel run is hard to read for the user. `fixStaged` is dropped because
484
+ * parallel run is hard to read for the user and merges their reports into one
485
+ * so the board renders the pair as a single row. `fixStaged` is dropped because
421
486
  * the underlying tools don't have a uniform staged-aware mode.
422
487
  */
423
488
  function composedJscProvider(linter, formatter) {
@@ -425,24 +490,40 @@ function composedJscProvider(linter, formatter) {
425
490
  bin: `${linter.bin}+${formatter.bin}`,
426
491
  ui: `${linter.ui} + ${formatter.ui}`,
427
492
  async check({ fix }) {
428
- await linter.lint({ fix });
429
- await formatter.format({ fix });
493
+ const lintReport = await linter.lint({ fix });
494
+ const formatReport = await formatter.format({ fix });
495
+ return mergeReports([{
496
+ ui: linter.ui,
497
+ report: lintReport
498
+ }, {
499
+ ui: formatter.ui,
500
+ report: formatReport
501
+ }]);
430
502
  },
431
503
  async doctor() {
432
504
  const [lintRes, fmtRes] = await Promise.all([linter.doctor(), formatter.doctor()]);
433
- const ok = lintRes.ok && fmtRes.ok;
434
- const firstFailure = !lintRes.ok ? lintRes : !fmtRes.ok ? fmtRes : void 0;
435
- return {
436
- ok,
437
- output: {
438
- stdout: `${linter.ui}:\n${lintRes.output.stdout}\n\n${formatter.ui}:\n${fmtRes.output.stdout}`,
439
- stderr: `${linter.ui}:\n${lintRes.output.stderr}\n\n${formatter.ui}:\n${fmtRes.output.stderr}`,
440
- exitCode: firstFailure?.output.exitCode ?? 0
441
- }
442
- };
505
+ return mergeReports([{
506
+ ui: linter.ui,
507
+ report: lintRes
508
+ }, {
509
+ ui: formatter.ui,
510
+ report: fmtRes
511
+ }]);
443
512
  }
444
513
  };
445
514
  }
515
+ /**
516
+ * Folds the lint + format reports into one so the composed `jsc` renders as a
517
+ * single board row: ok only when both passed, with each tool's output kept
518
+ * under its own header so the flushed detail stays attributable.
519
+ */
520
+ function mergeReports(parts) {
521
+ const sections = parts.filter((part) => part.report.output.trim()).map((part) => `${part.ui}:\n${part.report.output}`).join("\n\n");
522
+ return {
523
+ ok: parts.every((part) => part.report.ok),
524
+ output: sections
525
+ };
526
+ }
446
527
  //#endregion
447
528
  //#region src/program/commands/jscheck.ts
448
529
  function createJsCheckCommand(ctx) {
@@ -451,10 +532,14 @@ function createJsCheckCommand(ctx) {
451
532
  const formatter = ctx.registry.get("format");
452
533
  const checker = direct ?? (linter && formatter ? composedJscProvider(linter, formatter) : void 0);
453
534
  const cmd = createCommand("jsc").alias("jscheck").summary(`check format and lint${pluginAnnotation(checker)}`).description("Checks the code for formatting and linting issues, ensuring it adheres to the defined style and quality standards.").option("--fix", "try to fix issues automatically").option("--fix-staged", "try to fix staged files only");
454
- if (checker) cmd.addCommand(createDoctorSubcommand(checker));
535
+ if (checker) cmd.addCommand(createDoctorSubcommand(checker, ctx.appPkg));
455
536
  cmd.action(async (options = {}) => {
456
- if (!checker) throw missingPluginError("jsc");
457
- await checker.check(options);
537
+ await runToolCommand(ctx, {
538
+ name: "jsc",
539
+ kind: "jsc",
540
+ provider: checker,
541
+ run: (p) => p.check(options)
542
+ });
458
543
  });
459
544
  if (checker) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${checker.ui} CLI to check the code.`);
460
545
  return cmd;
@@ -464,10 +549,14 @@ function createJsCheckCommand(ctx) {
464
549
  function createLintCommand(ctx) {
465
550
  const linter = ctx.registry.get("lint");
466
551
  const cmd = createCommand("lint").summary(`check & fix lint errors${pluginAnnotation(linter)}`).description("Checks the code for linting issues and optionally fixes them, ensuring it adheres to the defined quality standards.").option("-c, --check", "check if the code is valid", true).option("--fix", "try to fix all the code");
467
- if (linter) cmd.addCommand(createDoctorSubcommand(linter));
552
+ if (linter) cmd.addCommand(createDoctorSubcommand(linter, ctx.appPkg));
468
553
  cmd.action(async (options = {}) => {
469
- if (!linter) throw missingPluginError("lint");
470
- await linter.lint(options);
554
+ await runToolCommand(ctx, {
555
+ name: "lint",
556
+ kind: "lint",
557
+ provider: linter,
558
+ run: (p) => p.lint(options)
559
+ });
471
560
  });
472
561
  if (linter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${linter.ui} CLI to lint the code.`);
473
562
  return cmd;
@@ -478,12 +567,16 @@ function createPackCommand(ctx) {
478
567
  const packer = ctx.registry.get("pack");
479
568
  const cmd = createCommand("pack").summary(`pack a ts library${pluginAnnotation(packer)}`).description("Compiles TypeScript code into JavaScript and generates type declaration files, packaging the library for distribution.");
480
569
  if (packer) {
481
- cmd.addCommand(createDoctorSubcommand(packer));
570
+ cmd.addCommand(createDoctorSubcommand(packer, ctx.appPkg));
482
571
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${packer.ui} CLI to pack the project.`);
483
572
  }
484
573
  cmd.action(async () => {
485
- if (!packer) throw missingPluginError("pack");
486
- await packer.pack();
574
+ await runToolCommand(ctx, {
575
+ name: "pack",
576
+ kind: "pack",
577
+ provider: packer,
578
+ run: (p) => p.pack()
579
+ });
487
580
  });
488
581
  return cmd;
489
582
  }
@@ -1143,48 +1236,37 @@ async function pathExists(p) {
1143
1236
  //#endregion
1144
1237
  //#region src/program/commands/tscheck.ts
1145
1238
  const getPreScript = (scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
1146
- async function typecheckAt({ dir, scripts, log, shell, tsc }) {
1147
- log.debug(`checking types at ${dir}`);
1148
- const shellAt = cwd === dir ? shell : shell.at(dir);
1149
- try {
1150
- const preScript = getPreScript(scripts);
1151
- if (preScript) {
1152
- log.start(`Running pre-script: ${preScript}`);
1153
- await shellAt.run(preScript, [], { shell: true });
1154
- log.success("Pre-script completed");
1155
- }
1156
- log.start("Type checking started");
1157
- if (cwd === dir) await tsc.check();
1158
- else await tsc.check({ cwd: dir });
1159
- log.success("Typecheck completed");
1160
- } catch (error) {
1161
- log.error("Typecheck failed");
1162
- throw error;
1163
- }
1164
- }
1165
1239
  function createTsCheckCommand(ctx) {
1166
1240
  const { appPkg, shell } = ctx;
1167
1241
  const tsc = ctx.registry.get("tsc");
1168
1242
  const cmd = createCommand("tsc").alias("tscheck").summary(`check typescript errors${pluginAnnotation(tsc)}`).description("Checks the TypeScript code for type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.");
1169
1243
  if (tsc) {
1170
- cmd.addCommand(createDoctorSubcommand(tsc));
1244
+ cmd.addCommand(createDoctorSubcommand(tsc, ctx.appPkg));
1171
1245
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
1172
1246
  }
1173
1247
  cmd.action(async () => {
1174
1248
  if (!tsc) throw missingPluginError("tsc");
1175
1249
  const isTsProject = (dir) => appPkg.hasFile("tsconfig.json", dir);
1250
+ const typecheckTask = (label, dir, scripts) => reportTask(label, async () => {
1251
+ const preScript = getPreScript(scripts);
1252
+ if (preScript) {
1253
+ const pre = await shell.at(dir).runCaptured(preScript, [], {
1254
+ shell: true,
1255
+ throwOnError: false
1256
+ });
1257
+ if ((pre.exitCode ?? 0) !== 0) return {
1258
+ ok: false,
1259
+ output: `pre-script \`${preScript}\` failed\n${[pre.stdout, pre.stderr].map((s) => s?.trim()).filter(Boolean).join("\n")}`
1260
+ };
1261
+ }
1262
+ return tsc.check({ cwd: dir });
1263
+ });
1176
1264
  if (!appPkg.isMonorepo()) {
1177
1265
  if (!isTsProject(appPkg.dirPath)) {
1178
1266
  logger.info("No tsconfig.json found, skipping typecheck");
1179
1267
  return;
1180
1268
  }
1181
- await typecheckAt({
1182
- shell,
1183
- tsc,
1184
- dir: appPkg.dirPath,
1185
- scripts: appPkg.packageJson.scripts,
1186
- log: logger
1187
- });
1269
+ if (!(await runBoard([typecheckTask(targetLabel("tsc", tsc, appPkg), appPkg.dirPath, appPkg.packageJson.scripts)])).ok) process.exitCode = 1;
1188
1270
  return;
1189
1271
  }
1190
1272
  const tsProjects = (await appPkg.getWorkspaceProjects()).filter((project) => isTsProject(project.rootDir));
@@ -1192,16 +1274,7 @@ function createTsCheckCommand(ctx) {
1192
1274
  logger.warn("No ts projects found in the monorepo, skipping typecheck");
1193
1275
  return;
1194
1276
  }
1195
- await Promise.all(tsProjects.map((p) => typecheckAt({
1196
- shell,
1197
- tsc,
1198
- dir: p.rootDir,
1199
- scripts: p.manifest.scripts,
1200
- log: logger.child({
1201
- tag: p.manifest.name,
1202
- namespace: "typecheck"
1203
- })
1204
- })));
1277
+ if (!(await runBoard(tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts)), { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") })).ok) process.exitCode = 1;
1205
1278
  });
1206
1279
  return cmd;
1207
1280
  }