@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,50 +1,14 @@
1
- import { cwd, type ShellService } from "@vlandoss/clibuddy";
2
- import type { AnyLogger } from "@vlandoss/loggy";
3
1
  import { createCommand } from "commander";
4
- import type { Doctor, TypeChecker } from "#src/plugin/types.ts";
5
2
  import type { Context } from "#src/services/ctx.ts";
6
3
  import { logger } from "#src/services/logger.ts";
4
+ import { type BoardTask, fanoutTitle, reportTask, runBoard, targetLabel } from "../board.ts";
7
5
  import { missingPluginError } from "../missing-plugin.ts";
8
6
  import { pluginAnnotation } from "../ui.ts";
9
7
  import { createDoctorSubcommand } from "./doctor.ts";
10
8
 
11
- type TypecheckAtOptions = {
12
- dir: string;
13
- scripts: Record<string, string | undefined> | undefined;
14
- log: AnyLogger;
15
- shell: ShellService;
16
- tsc: TypeChecker & Doctor;
17
- };
9
+ type Scripts = Record<string, string | undefined> | undefined;
18
10
 
19
- const getPreScript = (scripts: Record<string, string | undefined> | undefined) => scripts?.pretsc ?? scripts?.pretypecheck;
20
-
21
- async function typecheckAt({ dir, scripts, log, shell, tsc }: TypecheckAtOptions) {
22
- log.debug(`checking types at ${dir}`);
23
-
24
- const shellAt = cwd === dir ? shell : shell.at(dir);
25
-
26
- try {
27
- const preScript = getPreScript(scripts);
28
- if (preScript) {
29
- log.start(`Running pre-script: ${preScript}`);
30
- // Pre-scripts come from package.json and may contain shell features
31
- // (`&&`, pipes, env-var substitution) — run them through `/bin/sh -c`.
32
- await shellAt.run(preScript, [], { shell: true });
33
- log.success("Pre-script completed");
34
- }
35
-
36
- log.start("Type checking started");
37
- if (cwd === dir) {
38
- await tsc.check();
39
- } else {
40
- await tsc.check({ cwd: dir });
41
- }
42
- log.success("Typecheck completed");
43
- } catch (error) {
44
- log.error("Typecheck failed");
45
- throw error;
46
- }
47
- }
11
+ const getPreScript = (scripts: Scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
48
12
 
49
13
  export function createTsCheckCommand(ctx: Context) {
50
14
  const { appPkg, shell } = ctx;
@@ -58,7 +22,7 @@ export function createTsCheckCommand(ctx: Context) {
58
22
  );
59
23
 
60
24
  if (tsc) {
61
- cmd.addCommand(createDoctorSubcommand(tsc));
25
+ cmd.addCommand(createDoctorSubcommand(tsc, ctx.appPkg));
62
26
  cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
63
27
  }
64
28
 
@@ -67,20 +31,36 @@ export function createTsCheckCommand(ctx: Context) {
67
31
 
68
32
  const isTsProject = (dir: string) => appPkg.hasFile("tsconfig.json", dir);
69
33
 
34
+ // A package's `pretsc`/`pretypecheck` runs captured, inside the task, so its
35
+ // output stays grouped with that package. It may use shell features, so it
36
+ // goes through `/bin/sh -c`. A failing pre-script fails the task before tsc.
37
+ const typecheckTask = (label: string, dir: string, scripts: Scripts): BoardTask =>
38
+ reportTask(label, async () => {
39
+ const preScript = getPreScript(scripts);
40
+ if (preScript) {
41
+ const pre = await shell.at(dir).runCaptured(preScript, [], { shell: true, throwOnError: false });
42
+ if ((pre.exitCode ?? 0) !== 0) {
43
+ const output = [pre.stdout, pre.stderr]
44
+ .map((s) => s?.trim())
45
+ .filter(Boolean)
46
+ .join("\n");
47
+ return { ok: false, output: `pre-script \`${preScript}\` failed\n${output}` };
48
+ }
49
+ }
50
+ return tsc.check({ cwd: dir });
51
+ });
52
+
70
53
  if (!appPkg.isMonorepo()) {
71
54
  if (!isTsProject(appPkg.dirPath)) {
72
55
  logger.info("No tsconfig.json found, skipping typecheck");
73
56
  return;
74
57
  }
75
58
 
76
- await typecheckAt({
77
- shell,
78
- tsc,
79
- dir: appPkg.dirPath,
80
- scripts: appPkg.packageJson.scripts,
81
- log: logger,
82
- });
83
-
59
+ // Single package → compact board; the row carries the canonical
60
+ // `tsc (<tool>) · <pkg>` label like every other single-target command.
61
+ const label = targetLabel("tsc", tsc, appPkg);
62
+ const result = await runBoard([typecheckTask(label, appPkg.dirPath, appPkg.packageJson.scripts)]);
63
+ if (!result.ok) process.exitCode = 1;
84
64
  return;
85
65
  }
86
66
 
@@ -92,20 +72,9 @@ export function createTsCheckCommand(ctx: Context) {
92
72
  return;
93
73
  }
94
74
 
95
- await Promise.all(
96
- tsProjects.map((p) =>
97
- typecheckAt({
98
- shell,
99
- tsc,
100
- dir: p.rootDir,
101
- scripts: p.manifest.scripts,
102
- log: logger.child({
103
- tag: p.manifest.name,
104
- namespace: "typecheck",
105
- }),
106
- }),
107
- ),
108
- );
75
+ const tasks = tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts));
76
+ const result = await runBoard(tasks, { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") });
77
+ if (!result.ok) process.exitCode = 1;
109
78
  });
110
79
 
111
80
  return cmd;
@@ -1,4 +1,4 @@
1
- import type { Doctor, DoctorResult, Formatter, Linter, StaticChecker, StaticCheckerOptions } from "#src/plugin/types.ts";
1
+ import type { Doctor, Formatter, Linter, RunReport, StaticChecker, StaticCheckerOptions } from "#src/plugin/types.ts";
2
2
 
3
3
  /**
4
4
  * Synthesises a `StaticChecker & Doctor` (the `jsc` capability) by composing
@@ -7,29 +7,41 @@ import type { Doctor, DoctorResult, Formatter, Linter, StaticChecker, StaticChec
7
7
  * prettier) but no single plugin claims `jsc`.
8
8
  *
9
9
  * The check runs lint then format sequentially — interleaved stdout from a
10
- * parallel run is hard to read for the user. `fixStaged` is dropped because
10
+ * parallel run is hard to read for the user and merges their reports into one
11
+ * so the board renders the pair as a single row. `fixStaged` is dropped because
11
12
  * the underlying tools don't have a uniform staged-aware mode.
12
13
  */
13
14
  export function composedJscProvider(linter: Linter & Doctor, formatter: Formatter & Doctor): StaticChecker & Doctor {
14
15
  return {
15
16
  bin: `${linter.bin}+${formatter.bin}`,
16
17
  ui: `${linter.ui} + ${formatter.ui}`,
17
- async check({ fix }: StaticCheckerOptions) {
18
- await linter.lint({ fix });
19
- await formatter.format({ fix });
18
+ async check({ fix }: StaticCheckerOptions): Promise<RunReport> {
19
+ const lintReport = await linter.lint({ fix });
20
+ const formatReport = await formatter.format({ fix });
21
+ return mergeReports([
22
+ { ui: linter.ui, report: lintReport },
23
+ { ui: formatter.ui, report: formatReport },
24
+ ]);
20
25
  },
21
- async doctor(): Promise<DoctorResult> {
26
+ async doctor(): Promise<RunReport> {
22
27
  const [lintRes, fmtRes] = await Promise.all([linter.doctor(), formatter.doctor()]);
23
- const ok = lintRes.ok && fmtRes.ok;
24
- const firstFailure = !lintRes.ok ? lintRes : !fmtRes.ok ? fmtRes : undefined;
25
- return {
26
- ok,
27
- output: {
28
- stdout: `${linter.ui}:\n${lintRes.output.stdout}\n\n${formatter.ui}:\n${fmtRes.output.stdout}`,
29
- stderr: `${linter.ui}:\n${lintRes.output.stderr}\n\n${formatter.ui}:\n${fmtRes.output.stderr}`,
30
- exitCode: firstFailure?.output.exitCode ?? 0,
31
- },
32
- };
28
+ return mergeReports([
29
+ { ui: linter.ui, report: lintRes },
30
+ { ui: formatter.ui, report: fmtRes },
31
+ ]);
33
32
  },
34
33
  };
35
34
  }
35
+
36
+ /**
37
+ * Folds the lint + format reports into one so the composed `jsc` renders as a
38
+ * single board row: ok only when both passed, with each tool's output kept
39
+ * under its own header so the flushed detail stays attributable.
40
+ */
41
+ function mergeReports(parts: Array<{ ui: string; report: RunReport }>): RunReport {
42
+ const sections = parts
43
+ .filter((part) => part.report.output.trim())
44
+ .map((part) => `${part.ui}:\n${part.report.output}`)
45
+ .join("\n\n");
46
+ return { ok: parts.every((part) => part.report.ok), output: sections };
47
+ }
package/src/types/tool.ts CHANGED
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The outcome of running a check-family tool (lint / format / static check /
3
+ * type check) captured rather than streamed. `ok` is the tool's own verdict —
4
+ * its exit code, never a guess parsed from output — and `output` is the
5
+ * combined captured stdout+stderr (color preserved), flushed verbatim grouped
6
+ * under the package label. We deliberately do NOT parse tool summaries for
7
+ * warning/error counts: the formats are unstable and not uniform across tools
8
+ * (tsc and oxfmt have no machine output at all). See decisions/013.
9
+ */
10
+ export type RunReport = {
11
+ ok: boolean;
12
+ output: string;
13
+ };
14
+
1
15
  export type FormatOptions = {
2
16
  fix?: boolean;
3
17
  };
@@ -11,38 +25,32 @@ export type StaticCheckerOptions = {
11
25
  fixStaged?: boolean;
12
26
  };
13
27
 
14
- export type DoctorOutput = {
15
- stdout: string;
16
- stderr: string;
17
- exitCode: number | undefined;
18
- };
19
-
20
- export type DoctorResult = {
21
- ok: boolean;
22
- output: DoctorOutput;
23
- };
24
-
25
28
  export type Doctor = {
26
29
  ui: string;
27
- doctor: () => Promise<DoctorResult>;
30
+ /**
31
+ * Verifies the tool is wired correctly. Returns a `RunReport` like every
32
+ * other verb so the board renders it identically — `output` leads with the
33
+ * `$ <bin> --help` liveness command, plus the error if the bin won't run.
34
+ */
35
+ doctor: () => Promise<RunReport>;
28
36
  };
29
37
 
30
38
  export type Formatter = {
31
39
  bin: string;
32
40
  ui: string;
33
- format: (options: FormatOptions) => Promise<void>;
41
+ format: (options: FormatOptions) => Promise<RunReport>;
34
42
  };
35
43
 
36
44
  export type Linter = {
37
45
  bin: string;
38
46
  ui: string;
39
- lint: (options: LintOptions) => Promise<void>;
47
+ lint: (options: LintOptions) => Promise<RunReport>;
40
48
  };
41
49
 
42
50
  export type StaticChecker = {
43
51
  bin: string;
44
52
  ui: string;
45
- check: (options: StaticCheckerOptions) => Promise<void>;
53
+ check: (options: StaticCheckerOptions) => Promise<RunReport>;
46
54
  };
47
55
 
48
56
  export type TypeCheckOptions = {
@@ -53,5 +61,5 @@ export type TypeCheckOptions = {
53
61
  export type TypeChecker = {
54
62
  bin: string;
55
63
  ui: string;
56
- check: (options?: TypeCheckOptions) => Promise<void>;
64
+ check: (options?: TypeCheckOptions) => Promise<RunReport>;
57
65
  };