@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.
- 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 +208 -158
- package/dist/{types-DnqiiIxe.d.mts → types-snfbujDH.d.mts} +23 -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 +86 -0
- package/src/program/commands/check.ts +52 -42
- 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 +31 -23
- package/src/types/tool.ts +22 -16
|
@@ -27,6 +27,17 @@ declare class ReleaseService {
|
|
|
27
27
|
}
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/types/tool.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* The outcome of a check-family tool (lint / format / static check / type
|
|
32
|
+
* check) captured rather than streamed. `ok` is the tool's exit code — never a
|
|
33
|
+
* guess parsed from output, since tool summaries are unstable and not uniform
|
|
34
|
+
* (tsc and oxfmt emit none) — and `output` is the combined stdout+stderr (color
|
|
35
|
+
* preserved), flushed verbatim under the package label. See decisions/013.
|
|
36
|
+
*/
|
|
37
|
+
type RunReport = {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
output: string;
|
|
40
|
+
};
|
|
30
41
|
type FormatOptions = {
|
|
31
42
|
fix?: boolean;
|
|
32
43
|
};
|
|
@@ -37,33 +48,29 @@ type StaticCheckerOptions = {
|
|
|
37
48
|
fix?: boolean;
|
|
38
49
|
fixStaged?: boolean;
|
|
39
50
|
};
|
|
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
51
|
type Doctor = {
|
|
50
52
|
ui: string;
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Verifies the tool is wired correctly. Returns a `RunReport` like every
|
|
55
|
+
* other verb so the board renders it identically — `output` leads with the
|
|
56
|
+
* `$ <bin> --help` liveness command, plus the error if the bin won't run.
|
|
57
|
+
*/
|
|
58
|
+
doctor: () => Promise<RunReport>;
|
|
52
59
|
};
|
|
53
60
|
type Formatter = {
|
|
54
61
|
bin: string;
|
|
55
62
|
ui: string;
|
|
56
|
-
format: (options: FormatOptions) => Promise<
|
|
63
|
+
format: (options: FormatOptions) => Promise<RunReport>;
|
|
57
64
|
};
|
|
58
65
|
type Linter = {
|
|
59
66
|
bin: string;
|
|
60
67
|
ui: string;
|
|
61
|
-
lint: (options: LintOptions) => Promise<
|
|
68
|
+
lint: (options: LintOptions) => Promise<RunReport>;
|
|
62
69
|
};
|
|
63
70
|
type StaticChecker = {
|
|
64
71
|
bin: string;
|
|
65
72
|
ui: string;
|
|
66
|
-
check: (options: StaticCheckerOptions) => Promise<
|
|
73
|
+
check: (options: StaticCheckerOptions) => Promise<RunReport>;
|
|
67
74
|
};
|
|
68
75
|
type TypeCheckOptions = {
|
|
69
76
|
/** Where to run the type checker. Defaults to the kernel's `cwd`. */cwd?: string;
|
|
@@ -71,14 +78,14 @@ type TypeCheckOptions = {
|
|
|
71
78
|
type TypeChecker = {
|
|
72
79
|
bin: string;
|
|
73
80
|
ui: string;
|
|
74
|
-
check: (options?: TypeCheckOptions) => Promise<
|
|
81
|
+
check: (options?: TypeCheckOptions) => Promise<RunReport>;
|
|
75
82
|
};
|
|
76
83
|
//#endregion
|
|
77
84
|
//#region src/plugin/types.d.ts
|
|
78
85
|
type Packer = {
|
|
79
86
|
bin: string;
|
|
80
87
|
ui: string;
|
|
81
|
-
pack: () => Promise<
|
|
88
|
+
pack: () => Promise<RunReport>;
|
|
82
89
|
};
|
|
83
90
|
declare const PLUGIN_KINDS: readonly ["lint", "format", "jsc", "tsc", "pack"];
|
|
84
91
|
type PluginKind = (typeof PLUGIN_KINDS)[number];
|
|
@@ -202,4 +209,4 @@ type UninstallResult = {
|
|
|
202
209
|
files?: FileOp[];
|
|
203
210
|
};
|
|
204
211
|
//#endregion
|
|
205
|
-
export {
|
|
212
|
+
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-908d2c0.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/
|
|
60
|
-
"@vlandoss/
|
|
59
|
+
"@vlandoss/loggy": "0.2.1",
|
|
60
|
+
"@vlandoss/clibuddy": "0.6.2-git-908d2c0.0"
|
|
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,86 @@
|
|
|
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
|
+
/** The canonical single-target row label, `<command> (<tool>) · <package>`, so every command reads alike. */
|
|
21
|
+
export function targetLabel(command: string, provider: Provider, appPkg: Pkg): string {
|
|
22
|
+
return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The canonical fan-out section title, `<command> (<tool>) · <n> <unit>`. The
|
|
27
|
+
* tool is omitted when the fan-out spans several tools (`rr doctor` → `doctor ·
|
|
28
|
+
* 3 tools`), since the rows then carry the per-tool name.
|
|
29
|
+
*/
|
|
30
|
+
export function fanoutTitle(command: string, provider: Provider | undefined, count: number, unit: string): string {
|
|
31
|
+
const head = provider ? commandTool(command, provider) : command;
|
|
32
|
+
return `${head} · ${count} ${unit}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Bridges a check-family verb (returns a `RunReport`) to a board row, its `output` becoming the flushed detail. */
|
|
36
|
+
export function reportTask(label: string, run: () => Promise<RunReport>): BoardTask {
|
|
37
|
+
return {
|
|
38
|
+
label,
|
|
39
|
+
async run() {
|
|
40
|
+
const report = await run();
|
|
41
|
+
return { ok: report.ok, detail: report.output };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// While `rr check` is dispatching, boards stay framed (to divide the sections)
|
|
47
|
+
// and their results land in this collector so `check` can print one verdict.
|
|
48
|
+
let collector: BoardResult[] | null = null;
|
|
49
|
+
|
|
50
|
+
export async function runCheckSections(run: () => Promise<void>): Promise<BoardResult[]> {
|
|
51
|
+
const previous = collector;
|
|
52
|
+
collector = [];
|
|
53
|
+
try {
|
|
54
|
+
await run();
|
|
55
|
+
return collector;
|
|
56
|
+
} finally {
|
|
57
|
+
collector = previous;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Runs the rows on the board and returns whether every row passed. */
|
|
62
|
+
export async function runBoard(tasks: BoardTask[], options: BoardOptions = {}): Promise<BoardResult> {
|
|
63
|
+
const sink = collector;
|
|
64
|
+
const result = await runTaskBoard(tasks, { ...options, frame: options.frame ?? (sink !== null || undefined) });
|
|
65
|
+
// Record into the active check collector synchronously (we already awaited the
|
|
66
|
+
// board), so it's populated before our caller's `await runBoard(...)` resolves
|
|
67
|
+
// — no microtask race with the section's own continuation.
|
|
68
|
+
if (sink) sink.push(result);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The shared action body for a single-provider tool command (lint, format, jsc,
|
|
74
|
+
* pack): require the provider, run its verb as one board row labelled
|
|
75
|
+
* `<name> (<tool>) · <pkg>`, and aggregate the exit code. Commands that fan out
|
|
76
|
+
* (tsc) or compose siblings (check) call `runBoard` directly instead.
|
|
77
|
+
*/
|
|
78
|
+
export async function runToolCommand<P extends Provider>(
|
|
79
|
+
ctx: Context,
|
|
80
|
+
spec: { name: string; kind: PluginKind; provider: P | undefined; run: (provider: P) => Promise<RunReport> },
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const { provider } = spec;
|
|
83
|
+
if (!provider) throw missingPluginError(spec.kind);
|
|
84
|
+
const result = await runBoard([reportTask(targetLabel(spec.name, provider, ctx.appPkg), () => spec.run(provider))]);
|
|
85
|
+
if (!result.ok) process.exitCode = 1;
|
|
86
|
+
}
|
|
@@ -1,26 +1,22 @@
|
|
|
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";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
* `rr check`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* applies its declared option defaults exactly as if the user had typed
|
|
13
|
-
* `rr jsc` directly.
|
|
14
|
-
*
|
|
15
|
-
* Commander binds the running command as `this` inside an action (see
|
|
16
|
-
* `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
|
|
17
|
-
* parent program without any late-binding ceremony.
|
|
9
|
+
* `rr check` runs `jsc` then `tsc`. Rather than keep a parallel action
|
|
10
|
+
* registry, it reuses commander's command tree: it finds each sibling on
|
|
11
|
+
* `this.parent` and runs it via `parseAsync([])`, which applies the sibling's
|
|
12
|
+
* own option defaults. (`this` is the running command inside a non-arrow
|
|
13
|
+
* action — see cli/CLAUDE.md.)
|
|
18
14
|
*/
|
|
19
15
|
export function createCheckCommand(ctx: Context) {
|
|
20
16
|
return createCommand("check")
|
|
21
17
|
.summary(`run static checks${checkAnnotation(ctx)}`)
|
|
22
18
|
.description(
|
|
23
|
-
"Runs `rr jsc`
|
|
19
|
+
"Runs `rr jsc` then `rr tsc` in-process, each as its own section. Aggregates their exit codes — non-zero when either subcommand fails.",
|
|
24
20
|
)
|
|
25
21
|
.action(async function checkAction(this: Command) {
|
|
26
22
|
const program = this.parent;
|
|
@@ -30,46 +26,60 @@ export function createCheckCommand(ctx: Context) {
|
|
|
30
26
|
throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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}`);
|
|
29
|
+
// Sequentially, not in parallel: two live boards can't animate the same
|
|
30
|
+
// terminal region at once (decision 012). Each section runs in its own
|
|
31
|
+
// `runCheckSections` scope, which frames it and returns the boards it
|
|
32
|
+
// rendered — so a failure is attributed by section name, not a fragile
|
|
33
|
+
// dispatch-vs-render index.
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const failed: string[] = [];
|
|
36
|
+
let rendered = false;
|
|
37
|
+
for (const name of ["jsc", "tsc"]) {
|
|
38
|
+
const cmd = findCommand(program, name);
|
|
39
|
+
if (!cmd) {
|
|
40
|
+
logger.error(`rr check: subcommand "${name}" is not registered.`);
|
|
41
|
+
failed.push(name);
|
|
42
|
+
continue;
|
|
56
43
|
}
|
|
57
|
-
process.
|
|
44
|
+
if (rendered) process.stderr.write("\n"); // one blank line between sections
|
|
45
|
+
let threw = false;
|
|
46
|
+
const results = await runCheckSections(async () => {
|
|
47
|
+
try {
|
|
48
|
+
await cmd.parseAsync([], { from: "user" });
|
|
49
|
+
} catch (reason) {
|
|
50
|
+
logger.error(`rr check (${name}): ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
51
|
+
threw = true;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (threw || results.some((r) => !r.ok)) failed.push(name);
|
|
55
|
+
rendered = true;
|
|
58
56
|
}
|
|
57
|
+
|
|
58
|
+
// One overall verdict so the bottom of the scroll always answers "did
|
|
59
|
+
// check pass?" — a green section summary can otherwise be the last line
|
|
60
|
+
// of a run that failed in the section above it.
|
|
61
|
+
process.stderr.write(`\n${checkVerdict(failed, Date.now() - start)}\n`);
|
|
62
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
59
63
|
});
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
function checkVerdict(failed: string[], ms: number): string {
|
|
67
|
+
const elapsed = palette.dim(ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`);
|
|
68
|
+
const sep = palette.dim(" · ");
|
|
69
|
+
if (failed.length > 0) {
|
|
70
|
+
return `${palette.error("✖")} check failed${sep}${[...new Set(failed)].join(", ")}${sep}${elapsed}`;
|
|
71
|
+
}
|
|
72
|
+
return `${palette.success("✔")} check passed${sep}${elapsed}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
function findCommand(program: Command, name: string): Command | undefined {
|
|
63
76
|
return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* neither sibling has a provider, falls back to the standard `(not
|
|
71
|
-
* configured)` annotation so the help reads consistently with other
|
|
72
|
-
* commands.
|
|
80
|
+
* Flattens the underlying tool labels of `jsc` + `tsc` for the help summary —
|
|
81
|
+
* e.g. `(biome, oxlint)`, deduped, not `(biome + biome, oxlint)`. Falls back to
|
|
82
|
+
* the standard `(not configured)` when neither sibling has a provider.
|
|
73
83
|
*/
|
|
74
84
|
function checkAnnotation(ctx: Context): string {
|
|
75
85
|
const directJsc = ctx.registry.get("jsc");
|
|
@@ -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;
|