@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.
@@ -27,6 +27,19 @@ declare class ReleaseService {
27
27
  }
28
28
  //#endregion
29
29
  //#region src/types/tool.d.ts
30
+ /**
31
+ * The outcome of running a check-family tool (lint / format / static check /
32
+ * type check) captured rather than streamed. `ok` is the tool's own verdict —
33
+ * its exit code, never a guess parsed from output — and `output` is the
34
+ * combined captured stdout+stderr (color preserved), flushed verbatim grouped
35
+ * under the package label. We deliberately do NOT parse tool summaries for
36
+ * warning/error counts: the formats are unstable and not uniform across tools
37
+ * (tsc and oxfmt have no machine output at all). See decisions/013.
38
+ */
39
+ type RunReport = {
40
+ ok: boolean;
41
+ output: string;
42
+ };
30
43
  type FormatOptions = {
31
44
  fix?: boolean;
32
45
  };
@@ -37,33 +50,29 @@ type StaticCheckerOptions = {
37
50
  fix?: boolean;
38
51
  fixStaged?: boolean;
39
52
  };
40
- type DoctorOutput = {
41
- stdout: string;
42
- stderr: string;
43
- exitCode: number | undefined;
44
- };
45
- type DoctorResult = {
46
- ok: boolean;
47
- output: DoctorOutput;
48
- };
49
53
  type Doctor = {
50
54
  ui: string;
51
- doctor: () => Promise<DoctorResult>;
55
+ /**
56
+ * Verifies the tool is wired correctly. Returns a `RunReport` like every
57
+ * other verb so the board renders it identically — `output` leads with the
58
+ * `$ <bin> --help` liveness command, plus the error if the bin won't run.
59
+ */
60
+ doctor: () => Promise<RunReport>;
52
61
  };
53
62
  type Formatter = {
54
63
  bin: string;
55
64
  ui: string;
56
- format: (options: FormatOptions) => Promise<void>;
65
+ format: (options: FormatOptions) => Promise<RunReport>;
57
66
  };
58
67
  type Linter = {
59
68
  bin: string;
60
69
  ui: string;
61
- lint: (options: LintOptions) => Promise<void>;
70
+ lint: (options: LintOptions) => Promise<RunReport>;
62
71
  };
63
72
  type StaticChecker = {
64
73
  bin: string;
65
74
  ui: string;
66
- check: (options: StaticCheckerOptions) => Promise<void>;
75
+ check: (options: StaticCheckerOptions) => Promise<RunReport>;
67
76
  };
68
77
  type TypeCheckOptions = {
69
78
  /** Where to run the type checker. Defaults to the kernel's `cwd`. */cwd?: string;
@@ -71,14 +80,14 @@ type TypeCheckOptions = {
71
80
  type TypeChecker = {
72
81
  bin: string;
73
82
  ui: string;
74
- check: (options?: TypeCheckOptions) => Promise<void>;
83
+ check: (options?: TypeCheckOptions) => Promise<RunReport>;
75
84
  };
76
85
  //#endregion
77
86
  //#region src/plugin/types.d.ts
78
87
  type Packer = {
79
88
  bin: string;
80
89
  ui: string;
81
- pack: () => Promise<void>;
90
+ pack: () => Promise<RunReport>;
82
91
  };
83
92
  declare const PLUGIN_KINDS: readonly ["lint", "format", "jsc", "tsc", "pack"];
84
93
  type PluginKind = (typeof PLUGIN_KINDS)[number];
@@ -202,4 +211,4 @@ type UninstallResult = {
202
211
  files?: FileOp[];
203
212
  };
204
213
  //#endregion
205
- export { Linter as C, TypeChecker as D, TypeCheckOptions as E, ReleaseService as O, LintOptions as S, StaticCheckerOptions as T, Doctor as _, InstallFlags as a, FormatOptions as b, PLUGIN_KINDS as c, PluginCapabilities as d, PluginContext as f, UninstallResult as g, UninstallFlags as h, InstallContext as i, ReleaseServiceOptions as k, Packer as l, UninstallContext as m, ClackPromptsSelectOption as n, InstallResult as o, PluginKind as p, FileOp as r, JsonEdit as s, ClackPrompts as t, Plugin as u, DoctorOutput as v, StaticChecker as w, Formatter as x, DoctorResult as y };
214
+ export { StaticChecker as C, ReleaseService as D, TypeChecker as E, ReleaseServiceOptions as O, RunReport as S, TypeCheckOptions as T, Doctor as _, InstallFlags as a, LintOptions as b, PLUGIN_KINDS as c, PluginCapabilities as d, PluginContext as f, UninstallResult as g, UninstallFlags as h, InstallContext as i, Packer as l, UninstallContext as m, ClackPromptsSelectOption as n, InstallResult as o, PluginKind as p, FileOp as r, JsonEdit as s, ClackPrompts as t, Plugin as u, FormatOptions as v, StaticCheckerOptions as w, Linter as x, Formatter as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rrlab/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1-git-e008b4d.0",
4
4
  "description": "The CLI toolbox to fullstack common scripts in Variable Land",
5
5
  "homepage": "https://github.com/variableland/dx/tree/main/run-run/cli#readme",
6
6
  "bugs": {
@@ -56,7 +56,7 @@
56
56
  "memoize": "10.2.0",
57
57
  "nypm": "0.6.0",
58
58
  "rimraf": "6.1.3",
59
- "@vlandoss/clibuddy": "0.6.1",
59
+ "@vlandoss/clibuddy": "0.6.2-git-e008b4d.0",
60
60
  "@vlandoss/loggy": "0.2.1"
61
61
  },
62
62
  "devDependencies": {
package/src/lib/plugin.ts CHANGED
@@ -6,8 +6,6 @@ export type {
6
6
  ClackPrompts,
7
7
  ClackPromptsSelectOption,
8
8
  Doctor,
9
- DoctorOutput,
10
- DoctorResult,
11
9
  FileOp,
12
10
  FormatOptions,
13
11
  Formatter,
@@ -22,6 +20,7 @@ export type {
22
20
  PluginCapabilities,
23
21
  PluginContext,
24
22
  PluginKind,
23
+ RunReport,
25
24
  StaticChecker,
26
25
  StaticCheckerOptions,
27
26
  TypeChecker,
@@ -1,5 +1,5 @@
1
- import { resolvePackageBin, type ShellService } from "@vlandoss/clibuddy";
2
- import type { DoctorResult } from "#src/types/tool.ts";
1
+ import { palette, resolvePackageBin, type ShellService } from "@vlandoss/clibuddy";
2
+ import type { RunReport } from "#src/types/tool.ts";
3
3
 
4
4
  export type ToolServiceOptions = {
5
5
  pkg: string;
@@ -16,9 +16,8 @@ export type ToolServiceOptions = {
16
16
  from: string;
17
17
  };
18
18
 
19
- export type ExecOptions = {
19
+ export type RunReportOptions = {
20
20
  cwd?: string;
21
- verbose?: boolean;
22
21
  };
23
22
 
24
23
  export class ToolService {
@@ -55,23 +54,44 @@ export class ToolService {
55
54
  });
56
55
  }
57
56
 
58
- async exec(args: string[] = [], options: ExecOptions = {}) {
59
- const { cwd, verbose } = options;
60
- const sh = cwd ? this.#shellService.at(cwd) : this.#shellService;
61
- return sh.run(await this.getBinDir(), args, { display: this.#bin, verbose });
57
+ /**
58
+ * Runs the tool capturing its output instead of streaming it, and reports the
59
+ * verdict straight from the exit code — never a guess parsed from the output.
60
+ * The board needs the capture to attribute each parallel run's output to its
61
+ * package; the non-zero exit is returned (not thrown) so every task settles
62
+ * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
63
+ */
64
+ async runReport(args: string[] = [], options: RunReportOptions = {}): Promise<RunReport> {
65
+ const sh = options.cwd ? this.#shellService.at(options.cwd) : this.#shellService;
66
+ const output = await sh.runCaptured(await this.getBinDir(), args, { throwOnError: false });
67
+ // Lead the captured output with the command line that ran — the same
68
+ // `$ <cmd>` the streaming path prints via `printCmdLine`, so it stays
69
+ // visible even when captured. It's dim so it reads as context, not result.
70
+ const header = palette.dim(`$ ${[this.#bin, ...args].join(" ")}`);
71
+ const body = combine(output.stdout, output.stderr);
72
+ // Strict `=== 0`: a missing exit code (signal-killed, e.g. OOM) is a
73
+ // failure, not a pass — `?? 0` would silently report a crashed tool green.
74
+ return { ok: output.exitCode === 0, output: body ? `${header}\n${body}` : header };
62
75
  }
63
76
 
64
- async doctor(): Promise<DoctorResult> {
77
+ async doctor(): Promise<RunReport> {
65
78
  const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
66
79
  const ok = output.exitCode === 0;
67
-
68
- return {
69
- ok,
70
- output: {
71
- stdout: output.stdout,
72
- stderr: output.stderr,
73
- exitCode: output.exitCode,
74
- },
75
- };
80
+ // Same shape as the other verbs: lead with the `$ <bin> --help` liveness
81
+ // command (the tool's full help text is noise on success, so it's dropped);
82
+ // on failure surface whatever the bin printed — stdout AND stderr — so the
83
+ // reason it won't run is visible.
84
+ const command = palette.dim(`$ ${this.#bin} --help`);
85
+ if (ok) return { ok, output: command };
86
+ const detail = combine(output.stdout, output.stderr);
87
+ return { ok, output: detail ? `${command}\n${detail}` : command };
76
88
  }
77
89
  }
90
+
91
+ /** Joins the non-empty, trimmed streams of a captured run. */
92
+ function combine(stdout: string | undefined, stderr: string | undefined): string {
93
+ return [stdout, stderr]
94
+ .map((stream) => stream?.trim())
95
+ .filter(Boolean)
96
+ .join("\n");
97
+ }
@@ -1,16 +1,15 @@
1
1
  import type { Pkg, ShellService } from "@vlandoss/clibuddy";
2
2
  import type { AnyLogger as Logger } from "@vlandoss/loggy";
3
3
  import type { ReleaseService } from "#src/services/release.ts";
4
- import type { Doctor, Formatter, Linter, StaticChecker, TypeChecker } from "#src/types/tool.ts";
4
+ import type { Doctor, Formatter, Linter, RunReport, StaticChecker, TypeChecker } from "#src/types/tool.ts";
5
5
 
6
6
  export type {
7
7
  Doctor,
8
- DoctorOutput,
9
- DoctorResult,
10
8
  FormatOptions,
11
9
  Formatter,
12
10
  Linter,
13
11
  LintOptions,
12
+ RunReport,
14
13
  StaticChecker,
15
14
  StaticCheckerOptions,
16
15
  TypeChecker,
@@ -20,7 +19,7 @@ export type {
20
19
  export type Packer = {
21
20
  bin: string;
22
21
  ui: string;
23
- pack: () => Promise<void>;
22
+ pack: () => Promise<RunReport>;
24
23
  };
25
24
 
26
25
  export const PLUGIN_KINDS = ["lint", "format", "jsc", "tsc", "pack"] as const;
@@ -0,0 +1,99 @@
1
+ import { basename } from "node:path";
2
+ import { type BoardOptions, type BoardResult, type BoardTask, type Pkg, palette, runTaskBoard } from "@vlandoss/clibuddy";
3
+ import type { PluginKind, RunReport } from "#src/plugin/types.ts";
4
+ import type { Context } from "#src/services/ctx.ts";
5
+ import { missingPluginError } from "./missing-plugin.ts";
6
+
7
+ export type { BoardResult, BoardTask };
8
+
9
+ type Provider = { bin?: string; ui: string };
10
+
11
+ /** `<command> (<tool>)`, deduped to just `<command>` when the tool's binary is the command itself (e.g. `tsc`). */
12
+ function commandTool(command: string, provider: Provider): string {
13
+ return provider.bin === command ? command : `${command} (${provider.ui})`;
14
+ }
15
+
16
+ function pkgName(appPkg: Pkg): string {
17
+ return appPkg.packageJson.name ?? basename(appPkg.dirPath);
18
+ }
19
+
20
+ /**
21
+ * The one canonical row label for a single-target run: `<command> (<tool>) · <package>`.
22
+ * Every command/subcommand that acts on one target (lint, format, jsc, pack,
23
+ * single-app tsc, a `doctor` subcommand) builds its row through here so they
24
+ * all read identically.
25
+ */
26
+ export function targetLabel(command: string, provider: Provider, appPkg: Pkg): string {
27
+ return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
28
+ }
29
+
30
+ /**
31
+ * The one canonical section title for a fan-out run: `<command> (<tool>) · <n> <unit>`
32
+ * (e.g. `tsc (oxlint) · 8 packages`). The tool is omitted when the fan-out
33
+ * spans several tools (`rr doctor` → `doctor · 3 tools`); the rows then carry
34
+ * the per-unit name.
35
+ */
36
+ export function fanoutTitle(command: string, provider: Provider | undefined, count: number, unit: string): string {
37
+ const head = provider ? commandTool(command, provider) : command;
38
+ return `${head} · ${count} ${unit}`;
39
+ }
40
+
41
+ /**
42
+ * Bridges a check-family verb (which returns a `RunReport`) to a board row.
43
+ * The row's spinner reflects the in-flight run; the captured `output` becomes
44
+ * the detail the board flushes grouped under the label.
45
+ */
46
+ export function reportTask(label: string, run: () => Promise<RunReport>): BoardTask {
47
+ return {
48
+ label,
49
+ async run() {
50
+ const report = await run();
51
+ return { ok: report.ok, detail: report.output };
52
+ },
53
+ };
54
+ }
55
+
56
+ // `rr check` runs several single-task sections (jsc, tsc) back to back. On its
57
+ // own each would render compactly, but the frame is what visually divides the
58
+ // sections — so while check is dispatching, force every board to stay framed.
59
+ // While active, the collector also gathers each section's result in run order
60
+ // so `check` can print one overall verdict.
61
+ let collector: BoardResult[] | null = null;
62
+
63
+ export async function runCheckSections(run: () => Promise<void>): Promise<BoardResult[]> {
64
+ const previous = collector;
65
+ collector = [];
66
+ try {
67
+ await run();
68
+ return collector;
69
+ } finally {
70
+ collector = previous;
71
+ }
72
+ }
73
+
74
+ /** Runs the rows on the board and returns whether every row passed. */
75
+ export async function runBoard(tasks: BoardTask[], options: BoardOptions = {}): Promise<BoardResult> {
76
+ const sink = collector;
77
+ const result = await runTaskBoard(tasks, { ...options, frame: options.frame ?? (sink !== null || undefined) });
78
+ // Record into the active check collector synchronously (we already awaited the
79
+ // board), so it's populated before our caller's `await runBoard(...)` resolves
80
+ // — no microtask race with the section's own continuation.
81
+ if (sink) sink.push(result);
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * The shared action body for a single-provider tool command (lint, format, jsc,
87
+ * pack): require the provider, run its verb as one board row labelled
88
+ * `<name> (<tool>) · <pkg>`, and aggregate the exit code. Commands that fan out
89
+ * (tsc) or compose siblings (check) call `runBoard` directly instead.
90
+ */
91
+ export async function runToolCommand<P extends Provider>(
92
+ ctx: Context,
93
+ spec: { name: string; kind: PluginKind; provider: P | undefined; run: (provider: P) => Promise<RunReport> },
94
+ ): Promise<void> {
95
+ const { provider } = spec;
96
+ if (!provider) throw missingPluginError(spec.kind);
97
+ const result = await runBoard([reportTask(targetLabel(spec.name, provider, ctx.appPkg), () => spec.run(provider))]);
98
+ if (!result.ok) process.exitCode = 1;
99
+ }
@@ -1,4 +1,6 @@
1
+ import { palette } from "@vlandoss/clibuddy";
1
2
  import { type Command, createCommand } from "commander";
3
+ import { runCheckSections } from "#src/program/board.ts";
2
4
  import { pluginAnnotation } from "#src/program/ui.ts";
3
5
  import type { Context } from "#src/services/ctx.ts";
4
6
  import { logger } from "#src/services/logger.ts";
@@ -20,7 +22,7 @@ export function createCheckCommand(ctx: Context) {
20
22
  return createCommand("check")
21
23
  .summary(`run static checks${checkAnnotation(ctx)}`)
22
24
  .description(
23
- "Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.",
25
+ "Runs `rr jsc` then `rr tsc` in-process, each as its own section. Aggregates their exit codes — non-zero when either subcommand fails.",
24
26
  )
25
27
  .action(async function checkAction(this: Command) {
26
28
  const program = this.parent;
@@ -30,35 +32,54 @@ export function createCheckCommand(ctx: Context) {
30
32
  throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
31
33
  }
32
34
 
33
- const targets = ["jsc", "tsc"];
34
- const cmds = targets.map((name) => ({ name, cmd: findCommand(program, name) }));
35
-
36
- const missing = cmds.filter(({ cmd }) => !cmd).map(({ name }) => name);
37
- if (missing.length > 0) {
38
- for (const name of missing) logger.error(`rr check: subcommand "${name}" is not registered.`);
39
- process.exitCode = 1;
40
- return;
41
- }
42
-
43
- const results = await Promise.allSettled(
44
- // biome-ignore lint/style/noNonNullAssertion: missing is guarded above
45
- cmds.map(({ cmd }) => cmd!.parseAsync([], { from: "user" })),
46
- );
47
-
48
- const failed: Array<{ name: string; reason: unknown }> = [];
49
- for (const [i, r] of results.entries()) {
50
- if (r.status === "rejected") failed.push({ name: cmds[i]?.name ?? "?", reason: r.reason });
51
- }
52
- if (failed.length > 0) {
53
- for (const { name, reason } of failed) {
54
- const msg = reason instanceof Error ? reason.message : String(reason);
55
- logger.error(`rr check (${name}): ${msg}`);
35
+ // jsc then tsc, sequentially: each renders its own live board and two
36
+ // boards can't animate the same terminal region at once (decision 012).
37
+ // Each section runs inside its own `runCheckSections` scope — that both
38
+ // keeps it framed (so the frames divide the sections) and returns the
39
+ // boards THAT section rendered, so failure is attributed by section name,
40
+ // never by a fragile dispatch-vs-render index. A section that runs no
41
+ // board (tsc with no tsconfig) simply reports no results.
42
+ const start = Date.now();
43
+ const failed: string[] = [];
44
+ let rendered = false;
45
+ for (const name of ["jsc", "tsc"]) {
46
+ const cmd = findCommand(program, name);
47
+ if (!cmd) {
48
+ logger.error(`rr check: subcommand "${name}" is not registered.`);
49
+ failed.push(name);
50
+ continue;
56
51
  }
57
- process.exitCode = 1;
52
+ if (rendered) process.stderr.write("\n"); // one blank line between sections
53
+ let threw = false;
54
+ const results = await runCheckSections(async () => {
55
+ try {
56
+ await cmd.parseAsync([], { from: "user" });
57
+ } catch (reason) {
58
+ logger.error(`rr check (${name}): ${reason instanceof Error ? reason.message : String(reason)}`);
59
+ threw = true;
60
+ }
61
+ });
62
+ if (threw || results.some((r) => !r.ok)) failed.push(name);
63
+ rendered = true;
58
64
  }
65
+
66
+ // One overall verdict so the bottom of the scroll always answers "did
67
+ // check pass?" — a green section summary can otherwise be the last line
68
+ // of a run that failed in the section above it.
69
+ process.stderr.write(`\n${checkVerdict(failed, Date.now() - start)}\n`);
70
+ if (failed.length > 0) process.exitCode = 1;
59
71
  });
60
72
  }
61
73
 
74
+ function checkVerdict(failed: string[], ms: number): string {
75
+ const elapsed = palette.dim(ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
76
+ const sep = palette.dim(" · ");
77
+ if (failed.length > 0) {
78
+ return `${palette.error("✖")} check failed${sep}${[...new Set(failed)].join(", ")}${sep}${elapsed}`;
79
+ }
80
+ return `${palette.success("✔")} check passed${sep}${elapsed}`;
81
+ }
82
+
62
83
  function findCommand(program: Command, name: string): Command | undefined {
63
84
  return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
64
85
  }
@@ -1,29 +1,23 @@
1
+ import type { Pkg } from "@vlandoss/clibuddy";
1
2
  import { createCommand } from "commander";
2
- import type { Doctor, DoctorResult } from "#src/plugin/types.ts";
3
+ import type { Doctor } from "#src/plugin/types.ts";
3
4
  import { PLUGIN_KINDS } from "#src/plugin/types.ts";
4
5
  import type { Context } from "#src/services/ctx.ts";
5
6
  import { logger } from "#src/services/logger.ts";
7
+ import { fanoutTitle, reportTask, runBoard, targetLabel } from "../board.ts";
6
8
 
7
9
  /**
8
10
  * Subcommand factory used by every plugin-backed command (lint, format, jsc,
9
- * tsc, pack) to expose a `doctor` subcommand that verifies the underlying
10
- * tool is wired correctly. Each calls this with its own provider.
11
+ * tsc, pack) to expose a `doctor` subcommand that verifies the underlying tool
12
+ * is wired correctly. Renders the canonical `doctor (<tool>) · <pkg>` row like
13
+ * every other single-target command — `doctor()` returns a `RunReport`.
11
14
  */
12
- export function createDoctorSubcommand(service: Doctor) {
15
+ export function createDoctorSubcommand(service: Doctor, appPkg: Pkg) {
13
16
  return createCommand("doctor")
14
17
  .summary("check if the underlying tool is working correctly")
15
18
  .action(async function doctorAction() {
16
- const debug = logger.subdebug("doctor");
17
- const { ok, output } = await service.doctor();
18
-
19
- if (ok) {
20
- logger.success(`${service.ui} ok`);
21
- debug("%O", output);
22
- } else {
23
- logger.error(`${service.ui} not working`);
24
- debug("%O", output);
25
- process.exit(output.exitCode ?? 1);
26
- }
19
+ const result = await runBoard([reportTask(targetLabel("doctor", service, appPkg), () => service.doctor())]);
20
+ if (!result.ok) process.exitCode = 1;
27
21
  });
28
22
  }
29
23
 
@@ -46,27 +40,11 @@ export function createDoctorCommand(ctx: Context) {
46
40
  return;
47
41
  }
48
42
 
49
- const debug = logger.subdebug("doctor");
50
- const results = await Promise.all(
51
- services.map(async (svc) => {
52
- const result = await svc.doctor();
53
- return { svc, result };
54
- }),
55
- );
56
-
57
- let failures = 0;
58
- for (const { svc, result } of results) {
59
- if (result.ok) {
60
- logger.success(`${svc.ui} ok`);
61
- debug("%s: %O", svc.ui, result.output);
62
- } else {
63
- logger.error(`${svc.ui} not working`);
64
- debug("%s: %O", svc.ui, result.output);
65
- failures++;
66
- }
67
- }
68
-
69
- if (failures > 0) process.exitCode = 1;
43
+ // Each tool's health check is one parallel board row — a fan-out across
44
+ // tools, so the rows carry the tool name and the title omits a single tool.
45
+ const tasks = services.map((svc) => reportTask(svc.ui, () => svc.doctor()));
46
+ const result = await runBoard(tasks, { title: fanoutTitle("doctor", undefined, services.length, "tools") });
47
+ if (!result.ok) process.exitCode = 1;
70
48
  });
71
49
  }
72
50
 
@@ -81,5 +59,3 @@ function collectDistinctDoctors(ctx: Context): Doctor[] {
81
59
  }
82
60
  return [...seen];
83
61
  }
84
-
85
- export type { Doctor as _Doctor, DoctorResult as _DoctorResult };
@@ -1,6 +1,6 @@
1
1
  import { createCommand } from "commander";
2
2
  import type { Context } from "#src/services/ctx.ts";
3
- import { missingPluginError } from "../missing-plugin.ts";
3
+ import { runToolCommand } from "../board.ts";
4
4
  import { pluginAnnotation } from "../ui.ts";
5
5
  import { createDoctorSubcommand } from "./doctor.ts";
6
6
 
@@ -19,12 +19,11 @@ export function createFormatCommand(ctx: Context) {
19
19
  .option("--fix", "format all the code");
20
20
 
21
21
  if (formatter) {
22
- cmd.addCommand(createDoctorSubcommand(formatter));
22
+ cmd.addCommand(createDoctorSubcommand(formatter, ctx.appPkg));
23
23
  }
24
24
 
25
25
  cmd.action(async (options: ActionOptions = {}) => {
26
- if (!formatter) throw missingPluginError("format");
27
- await formatter.format(options);
26
+ await runToolCommand(ctx, { name: "format", kind: "format", provider: formatter, run: (p) => p.format(options) });
28
27
  });
29
28
 
30
29
  if (formatter) {
@@ -1,7 +1,7 @@
1
1
  import { createCommand } from "commander";
2
2
  import type { Context } from "#src/services/ctx.ts";
3
+ import { runToolCommand } from "../board.ts";
3
4
  import { composedJscProvider } from "../composed-jsc.ts";
4
- import { missingPluginError } from "../missing-plugin.ts";
5
5
  import { pluginAnnotation } from "../ui.ts";
6
6
  import { createDoctorSubcommand } from "./doctor.ts";
7
7
 
@@ -28,12 +28,11 @@ export function createJsCheckCommand(ctx: Context) {
28
28
  .option("--fix-staged", "try to fix staged files only");
29
29
 
30
30
  if (checker) {
31
- cmd.addCommand(createDoctorSubcommand(checker));
31
+ cmd.addCommand(createDoctorSubcommand(checker, ctx.appPkg));
32
32
  }
33
33
 
34
34
  cmd.action(async (options: ActionOptions = {}) => {
35
- if (!checker) throw missingPluginError("jsc");
36
- await checker.check(options);
35
+ await runToolCommand(ctx, { name: "jsc", kind: "jsc", provider: checker, run: (p) => p.check(options) });
37
36
  });
38
37
 
39
38
  if (checker) {
@@ -1,6 +1,6 @@
1
1
  import { createCommand } from "commander";
2
2
  import type { Context } from "#src/services/ctx.ts";
3
- import { missingPluginError } from "../missing-plugin.ts";
3
+ import { runToolCommand } from "../board.ts";
4
4
  import { pluginAnnotation } from "../ui.ts";
5
5
  import { createDoctorSubcommand } from "./doctor.ts";
6
6
 
@@ -21,12 +21,11 @@ export function createLintCommand(ctx: Context) {
21
21
  .option("--fix", "try to fix all the code");
22
22
 
23
23
  if (linter) {
24
- cmd.addCommand(createDoctorSubcommand(linter));
24
+ cmd.addCommand(createDoctorSubcommand(linter, ctx.appPkg));
25
25
  }
26
26
 
27
27
  cmd.action(async (options: ActionOptions = {}) => {
28
- if (!linter) throw missingPluginError("lint");
29
- await linter.lint(options);
28
+ await runToolCommand(ctx, { name: "lint", kind: "lint", provider: linter, run: (p) => p.lint(options) });
30
29
  });
31
30
 
32
31
  if (linter) {
@@ -1,6 +1,6 @@
1
1
  import { createCommand } from "commander";
2
2
  import type { Context } from "#src/services/ctx.ts";
3
- import { missingPluginError } from "../missing-plugin.ts";
3
+ import { runToolCommand } from "../board.ts";
4
4
  import { pluginAnnotation } from "../ui.ts";
5
5
  import { createDoctorSubcommand } from "./doctor.ts";
6
6
 
@@ -14,13 +14,12 @@ export function createPackCommand(ctx: Context) {
14
14
  );
15
15
 
16
16
  if (packer) {
17
- cmd.addCommand(createDoctorSubcommand(packer));
17
+ cmd.addCommand(createDoctorSubcommand(packer, ctx.appPkg));
18
18
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${packer.ui} CLI to pack the project.`);
19
19
  }
20
20
 
21
21
  cmd.action(async () => {
22
- if (!packer) throw missingPluginError("pack");
23
- await packer.pack();
22
+ await runToolCommand(ctx, { name: "pack", kind: "pack", provider: packer, run: (p) => p.pack() });
24
23
  });
25
24
 
26
25
  return cmd;