@rrlab/cli 1.0.0 → 1.0.1-git-908d2c0.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-908d2c0.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-snfbujDH.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-snfbujDH.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,91 @@ 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
+ /** The canonical single-target row label, `<command> (<tool>) · <package>`, so every command reads alike. */
168
+ function targetLabel(command, provider, appPkg) {
169
+ return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
170
+ }
171
+ /**
172
+ * The canonical fan-out section title, `<command> (<tool>) · <n> <unit>`. The
173
+ * tool is omitted when the fan-out spans several tools (`rr doctor` → `doctor ·
174
+ * 3 tools`), since the rows then carry the per-tool name.
175
+ */
176
+ function fanoutTitle(command, provider, count, unit) {
177
+ return `${provider ? commandTool(command, provider) : command} · ${count} ${unit}`;
178
+ }
179
+ /** Bridges a check-family verb (returns a `RunReport`) to a board row, its `output` becoming the flushed detail. */
180
+ function reportTask(label, run) {
181
+ return {
182
+ label,
183
+ async run() {
184
+ const report = await run();
185
+ return {
186
+ ok: report.ok,
187
+ detail: report.output
188
+ };
189
+ }
190
+ };
191
+ }
192
+ let collector = null;
193
+ async function runCheckSections(run) {
194
+ const previous = collector;
195
+ collector = [];
196
+ try {
197
+ await run();
198
+ return collector;
199
+ } finally {
200
+ collector = previous;
201
+ }
202
+ }
203
+ /** Runs the rows on the board and returns whether every row passed. */
204
+ async function runBoard(tasks, options = {}) {
205
+ const sink = collector;
206
+ const result = await runTaskBoard(tasks, {
207
+ ...options,
208
+ frame: options.frame ?? (sink !== null || void 0)
209
+ });
210
+ if (sink) sink.push(result);
211
+ return result;
212
+ }
213
+ /**
214
+ * The shared action body for a single-provider tool command (lint, format, jsc,
215
+ * pack): require the provider, run its verb as one board row labelled
216
+ * `<name> (<tool>) · <pkg>`, and aggregate the exit code. Commands that fan out
217
+ * (tsc) or compose siblings (check) call `runBoard` directly instead.
218
+ */
219
+ async function runToolCommand(ctx, spec) {
220
+ const { provider } = spec;
221
+ if (!provider) throw missingPluginError(spec.kind);
222
+ if (!(await runBoard([reportTask(targetLabel(spec.name, provider, ctx.appPkg), () => spec.run(provider))])).ok) process.exitCode = 1;
223
+ }
224
+ //#endregion
140
225
  //#region src/program/ui.ts
141
226
  const CREDITS_TEXT = `\nAcknowledgment:
142
227
  - kcd-scripts: for main inspiration
@@ -188,57 +273,56 @@ ${runRunColor(`
188
273
  //#endregion
189
274
  //#region src/program/commands/check.ts
190
275
  /**
191
- * `rr check` is the umbrella that runs the JS check (lint+format) and the
192
- * TS type check together. Both subcommands are already wired into
193
- * commander as siblings (`rr jsc`, `rr tsc`), so we reuse the program's
194
- * command tree as the action registry instead of duplicating it: look the
195
- * sibling up by name and invoke its action via `parseAsync([])`, which
196
- * applies its declared option defaults exactly as if the user had typed
197
- * `rr jsc` directly.
198
- *
199
- * Commander binds the running command as `this` inside an action (see
200
- * `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
201
- * parent program without any late-binding ceremony.
276
+ * `rr check` runs `jsc` then `tsc`. Rather than keep a parallel action
277
+ * registry, it reuses commander's command tree: it finds each sibling on
278
+ * `this.parent` and runs it via `parseAsync([])`, which applies the sibling's
279
+ * own option defaults. (`this` is the running command inside a non-arrow
280
+ * action see cli/CLAUDE.md.)
202
281
  */
203
282
  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() {
283
+ 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
284
  const program = this.parent;
206
285
  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" })));
286
+ const start = Date.now();
218
287
  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}`);
288
+ let rendered = false;
289
+ for (const name of ["jsc", "tsc"]) {
290
+ const cmd = findCommand(program, name);
291
+ if (!cmd) {
292
+ logger.error(`rr check: subcommand "${name}" is not registered.`);
293
+ failed.push(name);
294
+ continue;
227
295
  }
228
- process.exitCode = 1;
296
+ if (rendered) process.stderr.write("\n");
297
+ let threw = false;
298
+ const results = await runCheckSections(async () => {
299
+ try {
300
+ await cmd.parseAsync([], { from: "user" });
301
+ } catch (reason) {
302
+ logger.error(`rr check (${name}): ${reason instanceof Error ? reason.message : String(reason)}`);
303
+ threw = true;
304
+ }
305
+ });
306
+ if (threw || results.some((r) => !r.ok)) failed.push(name);
307
+ rendered = true;
229
308
  }
309
+ process.stderr.write(`\n${checkVerdict(failed, Date.now() - start)}\n`);
310
+ if (failed.length > 0) process.exitCode = 1;
230
311
  });
231
312
  }
313
+ function checkVerdict(failed, ms) {
314
+ const elapsed = palette.dim(ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`);
315
+ const sep = palette.dim(" · ");
316
+ if (failed.length > 0) return `${palette.error("✖")} check failed${sep}${[...new Set(failed)].join(", ")}${sep}${elapsed}`;
317
+ return `${palette.success("✔")} check passed${sep}${elapsed}`;
318
+ }
232
319
  function findCommand(program, name) {
233
320
  return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
234
321
  }
235
322
  /**
236
- * Mirrors the provider resolution of `jsc` + `tsc` and flattens the
237
- * underlying tool labels — e.g. biome (composed lint+format) + oxc (tsc)
238
- * renders as `(biome, oxlint)` rather than `(biome + biome, oxlint)`. When
239
- * neither sibling has a provider, falls back to the standard `(not
240
- * configured)` annotation so the help reads consistently with other
241
- * commands.
323
+ * Flattens the underlying tool labels of `jsc` + `tsc` for the help summary —
324
+ * e.g. `(biome, oxlint)`, deduped, not `(biome + biome, oxlint)`. Falls back to
325
+ * the standard `(not configured)` when neither sibling has a provider.
242
326
  */
243
327
  function checkAnnotation(ctx) {
244
328
  const directJsc = ctx.registry.get("jsc");
@@ -322,21 +406,13 @@ const PLUGIN_KINDS = [
322
406
  //#region src/program/commands/doctor.ts
323
407
  /**
324
408
  * 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.
409
+ * tsc, pack) to expose a `doctor` subcommand that verifies the underlying tool
410
+ * is wired correctly. Renders the canonical `doctor (<tool>) · <pkg>` row like
411
+ * every other single-target command — `doctor()` returns a `RunReport`.
327
412
  */
328
- function createDoctorSubcommand(service) {
413
+ function createDoctorSubcommand(service, appPkg) {
329
414
  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
- }
415
+ if (!(await runBoard([reportTask(targetLabel("doctor", service, appPkg), () => service.doctor())])).ok) process.exitCode = 1;
340
416
  });
341
417
  }
342
418
  /**
@@ -352,23 +428,7 @@ function createDoctorCommand(ctx) {
352
428
  logger.info("No plugins configured. Use `rr plugins add <name>` to install one.");
353
429
  return;
354
430
  }
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;
431
+ if (!(await runBoard(services.map((svc) => reportTask(svc.ui, () => svc.doctor())), { title: fanoutTitle("doctor", void 0, services.length, "tools") })).ok) process.exitCode = 1;
372
432
  });
373
433
  }
374
434
  function collectDistinctDoctors(ctx) {
@@ -377,33 +437,18 @@ function collectDistinctDoctors(ctx) {
377
437
  return [...seen];
378
438
  }
379
439
  //#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
440
  //#region src/program/commands/format.ts
400
441
  function createFormatCommand(ctx) {
401
442
  const formatter = ctx.registry.get("format");
402
443
  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));
444
+ if (formatter) cmd.addCommand(createDoctorSubcommand(formatter, ctx.appPkg));
404
445
  cmd.action(async (options = {}) => {
405
- if (!formatter) throw missingPluginError("format");
406
- await formatter.format(options);
446
+ await runToolCommand(ctx, {
447
+ name: "format",
448
+ kind: "format",
449
+ provider: formatter,
450
+ run: (p) => p.format(options)
451
+ });
407
452
  });
408
453
  if (formatter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${formatter.ui} CLI to format the code.`);
409
454
  return cmd;
@@ -411,38 +456,51 @@ function createFormatCommand(ctx) {
411
456
  //#endregion
412
457
  //#region src/program/composed-jsc.ts
413
458
  /**
414
- * Synthesises a `StaticChecker & Doctor` (the `jsc` capability) by composing
415
- * a separately-registered linter and formatter. Used when the user's plugin
416
- * set provides `lint` and `format` independently (e.g. oxc, or eslint +
417
- * prettier) but no single plugin claims `jsc`.
418
- *
419
- * 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
421
- * the underlying tools don't have a uniform staged-aware mode.
459
+ * Synthesises the `jsc` capability (`StaticChecker & Doctor`) by composing a
460
+ * separately-registered linter and formatter used when the plugin set
461
+ * provides `lint` and `format` independently (e.g. oxc) but no plugin claims
462
+ * `jsc`. Runs lint then format sequentially (parallel stdout interleaves badly)
463
+ * and merges their reports into one board row.
422
464
  */
423
465
  function composedJscProvider(linter, formatter) {
424
466
  return {
425
467
  bin: `${linter.bin}+${formatter.bin}`,
426
468
  ui: `${linter.ui} + ${formatter.ui}`,
427
469
  async check({ fix }) {
428
- await linter.lint({ fix });
429
- await formatter.format({ fix });
470
+ const lintReport = await linter.lint({ fix });
471
+ const formatReport = await formatter.format({ fix });
472
+ return mergeReports([{
473
+ ui: linter.ui,
474
+ report: lintReport
475
+ }, {
476
+ ui: formatter.ui,
477
+ report: formatReport
478
+ }]);
430
479
  },
431
480
  async doctor() {
432
481
  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
- };
482
+ return mergeReports([{
483
+ ui: linter.ui,
484
+ report: lintRes
485
+ }, {
486
+ ui: formatter.ui,
487
+ report: fmtRes
488
+ }]);
443
489
  }
444
490
  };
445
491
  }
492
+ /**
493
+ * Folds the lint + format reports into one so the composed `jsc` renders as a
494
+ * single board row: ok only when both passed, with each tool's output kept
495
+ * under its own header so the flushed detail stays attributable.
496
+ */
497
+ function mergeReports(parts) {
498
+ const sections = parts.filter((part) => part.report.output.trim()).map((part) => `${part.ui}:\n${part.report.output}`).join("\n\n");
499
+ return {
500
+ ok: parts.every((part) => part.report.ok),
501
+ output: sections
502
+ };
503
+ }
446
504
  //#endregion
447
505
  //#region src/program/commands/jscheck.ts
448
506
  function createJsCheckCommand(ctx) {
@@ -451,10 +509,14 @@ function createJsCheckCommand(ctx) {
451
509
  const formatter = ctx.registry.get("format");
452
510
  const checker = direct ?? (linter && formatter ? composedJscProvider(linter, formatter) : void 0);
453
511
  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));
512
+ if (checker) cmd.addCommand(createDoctorSubcommand(checker, ctx.appPkg));
455
513
  cmd.action(async (options = {}) => {
456
- if (!checker) throw missingPluginError("jsc");
457
- await checker.check(options);
514
+ await runToolCommand(ctx, {
515
+ name: "jsc",
516
+ kind: "jsc",
517
+ provider: checker,
518
+ run: (p) => p.check(options)
519
+ });
458
520
  });
459
521
  if (checker) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${checker.ui} CLI to check the code.`);
460
522
  return cmd;
@@ -464,10 +526,14 @@ function createJsCheckCommand(ctx) {
464
526
  function createLintCommand(ctx) {
465
527
  const linter = ctx.registry.get("lint");
466
528
  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));
529
+ if (linter) cmd.addCommand(createDoctorSubcommand(linter, ctx.appPkg));
468
530
  cmd.action(async (options = {}) => {
469
- if (!linter) throw missingPluginError("lint");
470
- await linter.lint(options);
531
+ await runToolCommand(ctx, {
532
+ name: "lint",
533
+ kind: "lint",
534
+ provider: linter,
535
+ run: (p) => p.lint(options)
536
+ });
471
537
  });
472
538
  if (linter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${linter.ui} CLI to lint the code.`);
473
539
  return cmd;
@@ -478,12 +544,16 @@ function createPackCommand(ctx) {
478
544
  const packer = ctx.registry.get("pack");
479
545
  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
546
  if (packer) {
481
- cmd.addCommand(createDoctorSubcommand(packer));
547
+ cmd.addCommand(createDoctorSubcommand(packer, ctx.appPkg));
482
548
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${packer.ui} CLI to pack the project.`);
483
549
  }
484
550
  cmd.action(async () => {
485
- if (!packer) throw missingPluginError("pack");
486
- await packer.pack();
551
+ await runToolCommand(ctx, {
552
+ name: "pack",
553
+ kind: "pack",
554
+ provider: packer,
555
+ run: (p) => p.pack()
556
+ });
487
557
  });
488
558
  return cmd;
489
559
  }
@@ -1143,48 +1213,37 @@ async function pathExists(p) {
1143
1213
  //#endregion
1144
1214
  //#region src/program/commands/tscheck.ts
1145
1215
  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
1216
  function createTsCheckCommand(ctx) {
1166
1217
  const { appPkg, shell } = ctx;
1167
1218
  const tsc = ctx.registry.get("tsc");
1168
1219
  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
1220
  if (tsc) {
1170
- cmd.addCommand(createDoctorSubcommand(tsc));
1221
+ cmd.addCommand(createDoctorSubcommand(tsc, ctx.appPkg));
1171
1222
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
1172
1223
  }
1173
1224
  cmd.action(async () => {
1174
1225
  if (!tsc) throw missingPluginError("tsc");
1175
1226
  const isTsProject = (dir) => appPkg.hasFile("tsconfig.json", dir);
1227
+ const typecheckTask = (label, dir, scripts) => reportTask(label, async () => {
1228
+ const preScript = getPreScript(scripts);
1229
+ if (preScript) {
1230
+ const pre = await shell.at(dir).runCaptured(preScript, [], {
1231
+ shell: true,
1232
+ throwOnError: false
1233
+ });
1234
+ if ((pre.exitCode ?? 0) !== 0) return {
1235
+ ok: false,
1236
+ output: `pre-script \`${preScript}\` failed\n${[pre.stdout, pre.stderr].map((s) => s?.trim()).filter(Boolean).join("\n")}`
1237
+ };
1238
+ }
1239
+ return tsc.check({ cwd: dir });
1240
+ });
1176
1241
  if (!appPkg.isMonorepo()) {
1177
1242
  if (!isTsProject(appPkg.dirPath)) {
1178
1243
  logger.info("No tsconfig.json found, skipping typecheck");
1179
1244
  return;
1180
1245
  }
1181
- await typecheckAt({
1182
- shell,
1183
- tsc,
1184
- dir: appPkg.dirPath,
1185
- scripts: appPkg.packageJson.scripts,
1186
- log: logger
1187
- });
1246
+ if (!(await runBoard([typecheckTask(targetLabel("tsc", tsc, appPkg), appPkg.dirPath, appPkg.packageJson.scripts)])).ok) process.exitCode = 1;
1188
1247
  return;
1189
1248
  }
1190
1249
  const tsProjects = (await appPkg.getWorkspaceProjects()).filter((project) => isTsProject(project.rootDir));
@@ -1192,16 +1251,7 @@ function createTsCheckCommand(ctx) {
1192
1251
  logger.warn("No ts projects found in the monorepo, skipping typecheck");
1193
1252
  return;
1194
1253
  }
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
- })));
1254
+ 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
1255
  });
1206
1256
  return cmd;
1207
1257
  }