@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,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,35 +1,43 @@
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
- * Synthesises a `StaticChecker & Doctor` (the `jsc` capability) by composing
5
- * a separately-registered linter and formatter. Used when the user's plugin
6
- * set provides `lint` and `format` independently (e.g. oxc, or eslint +
7
- * prettier) but no single plugin claims `jsc`.
8
- *
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
11
- * the underlying tools don't have a uniform staged-aware mode.
4
+ * Synthesises the `jsc` capability (`StaticChecker & Doctor`) by composing a
5
+ * separately-registered linter and formatter used when the plugin set
6
+ * provides `lint` and `format` independently (e.g. oxc) but no plugin claims
7
+ * `jsc`. Runs lint then format sequentially (parallel stdout interleaves badly)
8
+ * and merges their reports into one board row.
12
9
  */
13
10
  export function composedJscProvider(linter: Linter & Doctor, formatter: Formatter & Doctor): StaticChecker & Doctor {
14
11
  return {
15
12
  bin: `${linter.bin}+${formatter.bin}`,
16
13
  ui: `${linter.ui} + ${formatter.ui}`,
17
- async check({ fix }: StaticCheckerOptions) {
18
- await linter.lint({ fix });
19
- await formatter.format({ fix });
14
+ async check({ fix }: StaticCheckerOptions): Promise<RunReport> {
15
+ const lintReport = await linter.lint({ fix });
16
+ const formatReport = await formatter.format({ fix });
17
+ return mergeReports([
18
+ { ui: linter.ui, report: lintReport },
19
+ { ui: formatter.ui, report: formatReport },
20
+ ]);
20
21
  },
21
- async doctor(): Promise<DoctorResult> {
22
+ async doctor(): Promise<RunReport> {
22
23
  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
- };
24
+ return mergeReports([
25
+ { ui: linter.ui, report: lintRes },
26
+ { ui: formatter.ui, report: fmtRes },
27
+ ]);
33
28
  },
34
29
  };
35
30
  }
31
+
32
+ /**
33
+ * Folds the lint + format reports into one so the composed `jsc` renders as a
34
+ * single board row: ok only when both passed, with each tool's output kept
35
+ * under its own header so the flushed detail stays attributable.
36
+ */
37
+ function mergeReports(parts: Array<{ ui: string; report: RunReport }>): RunReport {
38
+ const sections = parts
39
+ .filter((part) => part.report.output.trim())
40
+ .map((part) => `${part.ui}:\n${part.report.output}`)
41
+ .join("\n\n");
42
+ return { ok: parts.every((part) => part.report.ok), output: sections };
43
+ }
package/src/types/tool.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /**
2
+ * The outcome of a check-family tool (lint / format / static check / type
3
+ * check) captured rather than streamed. `ok` is the tool's exit code — never a
4
+ * guess parsed from output, since tool summaries are unstable and not uniform
5
+ * (tsc and oxfmt emit none) — and `output` is the combined stdout+stderr (color
6
+ * preserved), flushed verbatim under the package label. See decisions/013.
7
+ */
8
+ export type RunReport = {
9
+ ok: boolean;
10
+ output: string;
11
+ };
12
+
1
13
  export type FormatOptions = {
2
14
  fix?: boolean;
3
15
  };
@@ -11,38 +23,32 @@ export type StaticCheckerOptions = {
11
23
  fixStaged?: boolean;
12
24
  };
13
25
 
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
26
  export type Doctor = {
26
27
  ui: string;
27
- doctor: () => Promise<DoctorResult>;
28
+ /**
29
+ * Verifies the tool is wired correctly. Returns a `RunReport` like every
30
+ * other verb so the board renders it identically — `output` leads with the
31
+ * `$ <bin> --help` liveness command, plus the error if the bin won't run.
32
+ */
33
+ doctor: () => Promise<RunReport>;
28
34
  };
29
35
 
30
36
  export type Formatter = {
31
37
  bin: string;
32
38
  ui: string;
33
- format: (options: FormatOptions) => Promise<void>;
39
+ format: (options: FormatOptions) => Promise<RunReport>;
34
40
  };
35
41
 
36
42
  export type Linter = {
37
43
  bin: string;
38
44
  ui: string;
39
- lint: (options: LintOptions) => Promise<void>;
45
+ lint: (options: LintOptions) => Promise<RunReport>;
40
46
  };
41
47
 
42
48
  export type StaticChecker = {
43
49
  bin: string;
44
50
  ui: string;
45
- check: (options: StaticCheckerOptions) => Promise<void>;
51
+ check: (options: StaticCheckerOptions) => Promise<RunReport>;
46
52
  };
47
53
 
48
54
  export type TypeCheckOptions = {
@@ -53,5 +59,5 @@ export type TypeCheckOptions = {
53
59
  export type TypeChecker = {
54
60
  bin: string;
55
61
  ui: string;
56
- check: (options?: TypeCheckOptions) => Promise<void>;
62
+ check: (options?: TypeCheckOptions) => Promise<RunReport>;
57
63
  };