@rrlab/cli 0.0.4-git-9887d65.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.
- package/dist/cli.usage.kdl +2 -2
- package/dist/config.d.mts +1 -1
- package/dist/plugin.d.mts +12 -15
- package/dist/plugin.mjs +29 -13
- package/dist/run.mjs +207 -134
- package/dist/{types-DnqiiIxe.d.mts → types-6gZWuLJf.d.mts} +25 -16
- package/package.json +3 -3
- package/src/lib/plugin.ts +1 -2
- package/src/plugin/tool-service.ts +38 -18
- package/src/plugin/types.ts +3 -4
- package/src/program/board.ts +99 -0
- package/src/program/commands/check.ts +46 -25
- package/src/program/commands/doctor.ts +14 -38
- package/src/program/commands/format.ts +3 -4
- package/src/program/commands/jscheck.ts +3 -4
- package/src/program/commands/lint.ts +3 -4
- package/src/program/commands/pack.ts +3 -4
- package/src/program/commands/tscheck.ts +31 -62
- package/src/program/composed-jsc.ts +28 -16
- package/src/types/tool.ts +24 -16
|
@@ -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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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 {
|
|
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": "
|
|
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,8 +56,8 @@
|
|
|
56
56
|
"memoize": "10.2.0",
|
|
57
57
|
"nypm": "0.6.0",
|
|
58
58
|
"rimraf": "6.1.3",
|
|
59
|
-
"@vlandoss/clibuddy": "0.6.
|
|
60
|
-
"@vlandoss/loggy": "0.2.
|
|
59
|
+
"@vlandoss/clibuddy": "0.6.2-git-e008b4d.0",
|
|
60
|
+
"@vlandoss/loggy": "0.2.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"tsdown": "0.22.0",
|
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 {
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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<
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
package/src/plugin/types.ts
CHANGED
|
@@ -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<
|
|
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`
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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;
|