@rrlab/cli 1.2.1-git-5a5faa9.0 → 1.2.2-git-9a2ee7f.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,7 +1,7 @@
1
1
  // @generated by @usage-spec/commander from Commander.js metadata
2
2
  name rr
3
3
  bin rr
4
- version "1.2.1-git-5a5faa9.0"
4
+ version "1.2.2-git-9a2ee7f.0"
5
5
  usage "[options] [command]"
6
6
  flag --about help="show credits & inspiration"
7
7
  flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
@@ -31,6 +31,14 @@ cmd format help="check & fix format issues" {
31
31
  flag --fix help="format all the code"
32
32
  cmd doctor help="check if the underlying tool is working correctly"
33
33
  }
34
+ cmd test help="run the test suite" {
35
+ long_help "Passthrough to the configured test runner. Forwards every flag and argument (e.g. --project, --watch) — including --help — straight to the tool. Loads .env.test or .env by default. ('rr test doctor' is reserved for the health check.)"
36
+ flag --env help="load this env file before running (default: .env.test, then .env)" {
37
+ arg <ENV>
38
+ }
39
+ arg "[args]…" help="arguments forwarded to the test runner" required=#false var=#true
40
+ cmd doctor help="check if the underlying tool is working correctly"
41
+ }
34
42
  cmd pack help="pack a ts library" {
35
43
  long_help "Compiles TypeScript code into JavaScript and generates type declaration files, packaging the library for distribution."
36
44
  cmd doctor help="check if the underlying tool is working correctly"
@@ -55,13 +63,13 @@ cmd plugins subcommand_required=#true help="manage @rrlab plugins" {
55
63
  flag --force help="re-run install even if the plugin is already configured"
56
64
  flag --yes help="skip prompts and use defaults (non-interactive)"
57
65
  flag --dry-run help="show what would happen, without applying changes"
58
- arg <name> help="plugin alias (ts|biome|oxc|tsdown), optionally with @<spec> e.g. biome@pr-226"
66
+ arg <name> help="plugin alias (ts|biome|oxc|tsdown|vitest), optionally with @<spec> e.g. biome@pr-226"
59
67
  }
60
68
  cmd remove help="uninstall an @rrlab plugin and undo its config files + deps" {
61
69
  flag --yes help="skip the confirmation prompt"
62
70
  flag --dry-run help="print the plan without applying changes"
63
71
  arg <name> help="plugin alias to remove" {
64
- choices ts biome oxc tsdown
72
+ choices ts biome oxc tsdown vitest
65
73
  }
66
74
  }
67
75
  }
package/dist/config.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as Plugin } from "./types-Iu4IyWof.mjs";
1
+ import { c as Plugin } from "./types-CNXhLCKf.mjs";
2
2
 
3
3
  //#region src/types/config.d.ts
4
4
  type UserConfig = {
@@ -1,4 +1,4 @@
1
- import { i as InstallContext } from "../types-Iu4IyWof.mjs";
1
+ import { i as InstallContext } from "../types-CNXhLCKf.mjs";
2
2
 
3
3
  //#region src/lib/plugin/decide-scaffold.d.ts
4
4
  type ScaffoldDecision = "create" | "patch" | "overwrite" | "skip";
@@ -1,4 +1,4 @@
1
- import { c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, o as InstallResult, u as PluginContext } from "../types-Iu4IyWof.mjs";
1
+ import { c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, o as InstallResult, u as PluginContext } from "../types-CNXhLCKf.mjs";
2
2
 
3
3
  //#region src/lib/plugin/define-plugin.d.ts
4
4
  type PluginDefinition<TServices extends PluginServices> = {
@@ -1,4 +1,4 @@
1
- import { l as PluginCapability } from "../types-Iu4IyWof.mjs";
1
+ import { l as PluginCapability } from "../types-CNXhLCKf.mjs";
2
2
 
3
3
  //#region src/lib/plugin/directory.d.ts
4
4
  declare const PLUGINS_DIRECTORY: {
@@ -22,6 +22,11 @@ declare const PLUGINS_DIRECTORY: {
22
22
  readonly name: "tsdown";
23
23
  readonly capabilities: ["pack"];
24
24
  };
25
+ readonly vitest: {
26
+ readonly pkg: "@rrlab/vitest-plugin";
27
+ readonly name: "vitest";
28
+ readonly capabilities: ["test"];
29
+ };
25
30
  };
26
31
  type PluginName = keyof typeof PLUGINS_DIRECTORY;
27
32
  declare function allPluginNames(): readonly PluginName[];
@@ -42,6 +47,10 @@ declare function providersOf(capability: PluginCapability): ({
42
47
  readonly pkg: "@rrlab/tsdown-plugin";
43
48
  readonly name: "tsdown";
44
49
  readonly capabilities: ["pack"];
50
+ } | {
51
+ readonly pkg: "@rrlab/vitest-plugin";
52
+ readonly name: "vitest";
53
+ readonly capabilities: ["test"];
45
54
  })[];
46
55
  //#endregion
47
56
  export { PLUGINS_DIRECTORY, PluginName, allPluginNames, isPluginName, providersOf };
@@ -28,6 +28,11 @@ const PLUGINS_DIRECTORY = {
28
28
  pkg: "@rrlab/tsdown-plugin",
29
29
  name: "tsdown",
30
30
  capabilities: ["pack"]
31
+ },
32
+ vitest: {
33
+ pkg: "@rrlab/vitest-plugin",
34
+ name: "vitest",
35
+ capabilities: ["test"]
31
36
  }
32
37
  };
33
38
  function allPluginNames() {
@@ -1,7 +1,7 @@
1
- import { a as InstallFlags, c as Plugin, d as PluginServices, f as UninstallContext, g as ReleaseServiceOptions, h as ReleaseService, i as InstallContext, l as PluginCapability, m as UninstallResult, n as ClackPromptsSelectOption, o as InstallResult, p as UninstallFlags, r as FileOp, s as JsonEdit, t as ClackPrompts, u as PluginContext } from "../types-Iu4IyWof.mjs";
2
- import { a as Linter, c as StaticChecker, d as TypeChecker, i as LintOptions, l as StaticCheckerOptions, n as FormatOptions, o as Packer, r as Formatter, s as RunReport, t as Doctor, u as TypeCheckOptions } from "../tool-DKL6TauZ.mjs";
1
+ import { a as InstallFlags, c as Plugin, d as PluginServices, f as UninstallContext, g as ReleaseServiceOptions, h as ReleaseService, i as InstallContext, l as PluginCapability, m as UninstallResult, n as ClackPromptsSelectOption, o as InstallResult, p as UninstallFlags, r as FileOp, s as JsonEdit, t as ClackPrompts, u as PluginContext } from "../types-CNXhLCKf.mjs";
2
+ import { a as Linter, c as StaticChecker, d as TestRunner, f as TypeCheckOptions, i as LintOptions, l as StaticCheckerOptions, n as FormatOptions, o as Packer, p as TypeChecker, r as Formatter, s as RunReport, t as Doctor, u as TestRunOptions } from "../tool-z8C0qtc5.mjs";
3
3
  import { DecideScaffoldOptions, ScaffoldDecision, decideScaffold } from "./decide-scaffold.mjs";
4
4
  import { PluginDefinition, definePlugin } from "./define-plugin.mjs";
5
5
  import { PickPresetOptions, pickPreset } from "./pick-preset.mjs";
6
6
  import { ToolService, ToolServiceOptions } from "./tool-service.mjs";
7
- export { type ClackPrompts, type ClackPromptsSelectOption, type DecideScaffoldOptions, type Doctor, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, type Packer, type PickPresetOptions, type Plugin, type PluginCapability, type PluginContext, type PluginDefinition, type PluginServices, ReleaseService, type ReleaseServiceOptions, type RunReport, type ScaffoldDecision, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, decideScaffold, definePlugin, pickPreset };
7
+ export { type ClackPrompts, type ClackPromptsSelectOption, type DecideScaffoldOptions, type Doctor, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, type Packer, type PickPresetOptions, type Plugin, type PluginCapability, type PluginContext, type PluginDefinition, type PluginServices, ReleaseService, type ReleaseServiceOptions, type RunReport, type ScaffoldDecision, type StaticChecker, type StaticCheckerOptions, type TestRunOptions, type TestRunner, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, decideScaffold, definePlugin, pickPreset };
@@ -1,4 +1,4 @@
1
- import { i as InstallContext } from "../types-Iu4IyWof.mjs";
1
+ import { i as InstallContext } from "../types-CNXhLCKf.mjs";
2
2
 
3
3
  //#region src/lib/plugin/pick-preset.d.ts
4
4
  type PickPresetOptions<K extends string> = {
@@ -1,5 +1,5 @@
1
- import { c as Plugin, d as PluginServices, l as PluginCapability } from "../types-Iu4IyWof.mjs";
2
- import { a as Linter, c as StaticChecker, d as TypeChecker, o as Packer, r as Formatter, t as Doctor } from "../tool-DKL6TauZ.mjs";
1
+ import { c as Plugin, d as PluginServices, l as PluginCapability } from "../types-CNXhLCKf.mjs";
2
+ import { a as Linter, c as StaticChecker, d as TestRunner, o as Packer, p as TypeChecker, r as Formatter, t as Doctor } from "../tool-z8C0qtc5.mjs";
3
3
 
4
4
  //#region src/lib/plugin/registry.d.ts
5
5
  type Provider<K extends PluginCapability> = {
@@ -11,7 +11,7 @@ declare class PluginRegistry {
11
11
  register(plugin: Plugin, services: PluginServices): void;
12
12
  getServiceOrThrow<K extends PluginCapability>(capability: K): NonNullable<PluginServices[K]>;
13
13
  getService<K extends PluginCapability>(capability: K): NonNullable<PluginServices[K]> | undefined;
14
- getAllServices(): NonNullable<(Linter & Doctor) | (Formatter & Doctor) | (StaticChecker & Doctor) | (TypeChecker & Doctor) | (Packer & Doctor) | undefined>[];
14
+ getAllServices(): NonNullable<(Linter & Doctor) | (Formatter & Doctor) | (StaticChecker & Doctor) | (TypeChecker & Doctor) | (Packer & Doctor) | (TestRunner & Doctor) | undefined>[];
15
15
  providersOf<K extends PluginCapability>(capability: K): Provider<K>[];
16
16
  providerOf<K extends PluginCapability>(capability: K): Provider<K> | undefined;
17
17
  }
@@ -1,4 +1,4 @@
1
- import { s as RunReport } from "../tool-DKL6TauZ.mjs";
1
+ import { s as RunReport } from "../tool-z8C0qtc5.mjs";
2
2
  import { ShellService } from "@vlandoss/clibuddy";
3
3
 
4
4
  //#region src/lib/plugin/tool-service.d.ts
@@ -39,6 +39,19 @@ declare class ToolService {
39
39
  * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
40
40
  */
41
41
  runReport(args?: string[], options?: RunReportOptions): Promise<RunReport>;
42
+ /**
43
+ * Streams the command straight to the terminal (`stdio: "inherit"`) and
44
+ * returns its exit code — the counterpart to `runReport` for passthrough
45
+ * verbs (e.g. a test runner) where capturing would break watch mode, colors,
46
+ * interactive UI, and `--help` forwarding. `verbose: false` suppresses the
47
+ * `$ <cmd>` line so the wrapped tool fully owns the terminal. Takes an
48
+ * explicit `command` because a passthrough may need to wrap its bin (e.g.
49
+ * `node --env-file=… <bin>`) rather than spawn it directly.
50
+ *
51
+ * A missing exit code (signal-killed, e.g. OOM) is reported as failure (`1`),
52
+ * mirroring `runReport`'s strict `=== 0`.
53
+ */
54
+ runStreamed(command: string, args?: string[], options?: RunReportOptions): Promise<number>;
42
55
  doctor(): Promise<RunReport>;
43
56
  }
44
57
  //#endregion
@@ -41,6 +41,24 @@ var ToolService = class {
41
41
  output: body ? `${header}\n${body}` : header
42
42
  };
43
43
  }
44
+ /**
45
+ * Streams the command straight to the terminal (`stdio: "inherit"`) and
46
+ * returns its exit code — the counterpart to `runReport` for passthrough
47
+ * verbs (e.g. a test runner) where capturing would break watch mode, colors,
48
+ * interactive UI, and `--help` forwarding. `verbose: false` suppresses the
49
+ * `$ <cmd>` line so the wrapped tool fully owns the terminal. Takes an
50
+ * explicit `command` because a passthrough may need to wrap its bin (e.g.
51
+ * `node --env-file=… <bin>`) rather than spawn it directly.
52
+ *
53
+ * A missing exit code (signal-killed, e.g. OOM) is reported as failure (`1`),
54
+ * mirroring `runReport`'s strict `=== 0`.
55
+ */
56
+ async runStreamed(command, args = [], options = {}) {
57
+ return (await (options.cwd ? this.#shellService.at(options.cwd) : this.#shellService).run(command, args, {
58
+ throwOnError: false,
59
+ verbose: false
60
+ })).exitCode ?? 1;
61
+ }
44
62
  async doctor() {
45
63
  const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
46
64
  const ok = output.exitCode === 0;
@@ -1,3 +1,3 @@
1
- import { a as InstallFlags, c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, n as ClackPromptsSelectOption, o as InstallResult, p as UninstallFlags, r as FileOp, s as JsonEdit, t as ClackPrompts, u as PluginContext } from "../types-Iu4IyWof.mjs";
2
- import { a as Linter, c as StaticChecker, d as TypeChecker, i as LintOptions, l as StaticCheckerOptions, n as FormatOptions, o as Packer, r as Formatter, s as RunReport, t as Doctor, u as TypeCheckOptions } from "../tool-DKL6TauZ.mjs";
3
- export { ClackPrompts, ClackPromptsSelectOption, type Doctor, FileOp, type FormatOptions, type Formatter, InstallContext, InstallFlags, InstallResult, JsonEdit, type LintOptions, type Linter, type Packer, Plugin, PluginCapability, PluginContext, PluginServices, type RunReport, type StaticChecker, type StaticCheckerOptions, type TypeCheckOptions, type TypeChecker, UninstallContext, UninstallFlags, UninstallResult };
1
+ import { a as InstallFlags, c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, n as ClackPromptsSelectOption, o as InstallResult, p as UninstallFlags, r as FileOp, s as JsonEdit, t as ClackPrompts, u as PluginContext } from "../types-CNXhLCKf.mjs";
2
+ import { a as Linter, c as StaticChecker, d as TestRunner, f as TypeCheckOptions, i as LintOptions, l as StaticCheckerOptions, n as FormatOptions, o as Packer, p as TypeChecker, r as Formatter, s as RunReport, t as Doctor, u as TestRunOptions } from "../tool-z8C0qtc5.mjs";
3
+ export { ClackPrompts, ClackPromptsSelectOption, type Doctor, FileOp, type FormatOptions, type Formatter, InstallContext, InstallFlags, InstallResult, JsonEdit, type LintOptions, type Linter, type Packer, Plugin, PluginCapability, PluginContext, PluginServices, type RunReport, type StaticChecker, type StaticCheckerOptions, type TestRunOptions, type TestRunner, type TypeCheckOptions, type TypeChecker, UninstallContext, UninstallFlags, UninstallResult };
package/dist/run.mjs CHANGED
@@ -53,6 +53,11 @@ const PLUGINS_DIRECTORY = {
53
53
  pkg: "@rrlab/tsdown-plugin",
54
54
  name: "tsdown",
55
55
  capabilities: ["pack"]
56
+ },
57
+ vitest: {
58
+ pkg: "@rrlab/vitest-plugin",
59
+ name: "vitest",
60
+ capabilities: ["test"]
56
61
  }
57
62
  };
58
63
  function allPluginNames() {
@@ -1578,6 +1583,38 @@ function createPluginsCommand(ctx) {
1578
1583
  return cmd;
1579
1584
  }
1580
1585
  //#endregion
1586
+ //#region src/actions/test.ts
1587
+ /**
1588
+ * The action for the `test` passthrough. Unlike the captured verbs (lint, pack,
1589
+ * …) it doesn't drive the task-board: the runner streams straight to the
1590
+ * terminal and resolves with the tool's exit code, which we mirror onto
1591
+ * `process.exitCode` so `rr test` fails when the suite fails.
1592
+ */
1593
+ async function testAction({ runner, options, args }) {
1594
+ const code = await runner.test({
1595
+ envFile: options.envFile,
1596
+ args
1597
+ });
1598
+ if (code !== 0) process.exitCode = code;
1599
+ }
1600
+ //#endregion
1601
+ //#region src/program/commands/test.ts
1602
+ function createTestCommand(ctx) {
1603
+ return createCommand("test").addCapabilities(["test"]).summary("run the test suite").description("Passthrough to the configured test runner. Forwards every flag and argument (e.g. --project, --watch) — including --help — straight to the tool. Loads .env.test or .env by default. ('rr test doctor' is reserved for the health check.)").allowUnknownOption(true).passThroughOptions(true).helpOption(false).option("--env <path>", "load this env file before running (default: .env.test, then .env)").argument("[args...]", "arguments forwarded to the test runner").action(async (args = [], options = {}) => {
1604
+ await testAction({
1605
+ ctx,
1606
+ runner: ctx.plugins.getServiceOrThrow("test"),
1607
+ options: { envFile: options.env },
1608
+ args
1609
+ });
1610
+ }).addDoctorCommand(async () => {
1611
+ await doctorOneAction({
1612
+ ctx,
1613
+ service: ctx.plugins.getServiceOrThrow("test")
1614
+ });
1615
+ });
1616
+ }
1617
+ //#endregion
1581
1618
  //#region src/program/commands/tscheck.ts
1582
1619
  function createTsCheckCommand(ctx) {
1583
1620
  return createCommand("tsc").alias("tscheck").addCapabilities(["typecheck"]).summary("check types errors").description("Checks type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.").action(async () => {
@@ -1653,7 +1690,7 @@ var RunRunCmd = class extends Command {
1653
1690
  async function createProgram(meta) {
1654
1691
  const ctx = await new ContextService(path.dirname(dirnameOf(meta))).getContext();
1655
1692
  const cmd = new RunRunCmd(ctx);
1656
- cmd.commandsGroup("Code quality:").addCommand(createCheckCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).commandsGroup("Build:").addCommand(createPackCommand(ctx)).commandsGroup("Maintenance:").addCommand(createCleanCommand()).addCommand(createDoctorCommand(ctx)).commandsGroup("Meta:").addCommand(createCompletionCommand()).addCommand(createPluginsCommand(ctx)).addCommand(createConfigCommand(ctx));
1693
+ cmd.commandsGroup("Code quality:").addCommand(createCheckCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).commandsGroup("Testing:").addCommand(createTestCommand(ctx)).commandsGroup("Build:").addCommand(createPackCommand(ctx)).commandsGroup("Maintenance:").addCommand(createCleanCommand()).addCommand(createDoctorCommand(ctx)).commandsGroup("Meta:").addCommand(createCompletionCommand()).addCommand(createPluginsCommand(ctx)).addCommand(createConfigCommand(ctx));
1657
1694
  return cmd;
1658
1695
  }
1659
1696
  //#endregion
@@ -39,5 +39,19 @@ type Packer = {
39
39
  readonly ui: string;
40
40
  pack: () => Promise<RunReport>;
41
41
  };
42
+ type TestRunOptions = {
43
+ /** Env file to load before running (resolved by the provider). */envFile?: string; /** Arguments forwarded verbatim to the test runner. */
44
+ args: string[];
45
+ };
46
+ type TestRunner = {
47
+ readonly ui: string;
48
+ /**
49
+ * Streams a passthrough test run and resolves with the tool's exit code.
50
+ * Unlike the other verbs it returns a number, not a `RunReport`: a test
51
+ * runner needs an inherited TTY (watch mode, colors, `--help`), so its output
52
+ * is never captured for the board.
53
+ */
54
+ test: (options: TestRunOptions) => Promise<number>;
55
+ };
42
56
  //#endregion
43
- export { Linter as a, StaticChecker as c, TypeChecker as d, LintOptions as i, StaticCheckerOptions as l, FormatOptions as n, Packer as o, Formatter as r, RunReport as s, Doctor as t, TypeCheckOptions as u };
57
+ export { Linter as a, StaticChecker as c, TestRunner as d, TypeCheckOptions as f, LintOptions as i, StaticCheckerOptions as l, FormatOptions as n, Packer as o, TypeChecker as p, Formatter as r, RunReport as s, Doctor as t, TestRunOptions as u };
@@ -1,4 +1,4 @@
1
- import { a as Linter, c as StaticChecker, d as TypeChecker, o as Packer, r as Formatter, t as Doctor } from "./tool-DKL6TauZ.mjs";
1
+ import { a as Linter, c as StaticChecker, d as TestRunner, o as Packer, p as TypeChecker, r as Formatter, t as Doctor } from "./tool-z8C0qtc5.mjs";
2
2
  import { Pkg, ShellService } from "@vlandoss/clibuddy";
3
3
  import { AnyLogger } from "@vlandoss/loggy";
4
4
 
@@ -35,6 +35,7 @@ type PluginServices = {
35
35
  jscheck?: StaticChecker & Doctor;
36
36
  typecheck?: TypeChecker & Doctor;
37
37
  pack?: Packer & Doctor;
38
+ test?: TestRunner & Doctor;
38
39
  };
39
40
  type PluginContext = {
40
41
  shell: ShellService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rrlab/cli",
3
- "version": "1.2.1-git-5a5faa9.0",
3
+ "version": "1.2.2-git-9a2ee7f.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": {
@@ -0,0 +1,20 @@
1
+ import type { ContextValue } from "#src/services/context.ts";
2
+ import type { Doctor, TestRunner } from "#src/types/tool.ts";
3
+
4
+ export type TestActionConfig = {
5
+ ctx: ContextValue;
6
+ runner: TestRunner & Doctor;
7
+ options: { envFile?: string };
8
+ args: string[];
9
+ };
10
+
11
+ /**
12
+ * The action for the `test` passthrough. Unlike the captured verbs (lint, pack,
13
+ * …) it doesn't drive the task-board: the runner streams straight to the
14
+ * terminal and resolves with the tool's exit code, which we mirror onto
15
+ * `process.exitCode` so `rr test` fails when the suite fails.
16
+ */
17
+ export async function testAction({ runner, options, args }: TestActionConfig): Promise<void> {
18
+ const code = await runner.test({ envFile: options.envFile, args });
19
+ if (code !== 0) process.exitCode = code;
20
+ }
@@ -11,6 +11,7 @@ export const PLUGINS_DIRECTORY = {
11
11
  biome: { pkg: "@rrlab/biome-plugin", name: "biome", capabilities: ["format", "jscheck", "lint"] },
12
12
  oxc: { pkg: "@rrlab/oxc-plugin", name: "oxc", capabilities: ["format", "lint", "jscheck", "typecheck"] },
13
13
  tsdown: { pkg: "@rrlab/tsdown-plugin", name: "tsdown", capabilities: ["pack"] },
14
+ vitest: { pkg: "@rrlab/vitest-plugin", name: "vitest", capabilities: ["test"] },
14
15
  } as const satisfies Record<string, PluginInfo>;
15
16
 
16
17
  export type PluginName = keyof typeof PLUGINS_DIRECTORY;
@@ -24,6 +24,8 @@ export type {
24
24
  RunReport,
25
25
  StaticChecker,
26
26
  StaticCheckerOptions,
27
+ TestRunner,
28
+ TestRunOptions,
27
29
  TypeChecker,
28
30
  TypeCheckOptions,
29
31
  UninstallContext,
@@ -70,6 +70,24 @@ export class ToolService {
70
70
  return { ok: output.exitCode === 0, output: body ? `${header}\n${body}` : header };
71
71
  }
72
72
 
73
+ /**
74
+ * Streams the command straight to the terminal (`stdio: "inherit"`) and
75
+ * returns its exit code — the counterpart to `runReport` for passthrough
76
+ * verbs (e.g. a test runner) where capturing would break watch mode, colors,
77
+ * interactive UI, and `--help` forwarding. `verbose: false` suppresses the
78
+ * `$ <cmd>` line so the wrapped tool fully owns the terminal. Takes an
79
+ * explicit `command` because a passthrough may need to wrap its bin (e.g.
80
+ * `node --env-file=… <bin>`) rather than spawn it directly.
81
+ *
82
+ * A missing exit code (signal-killed, e.g. OOM) is reported as failure (`1`),
83
+ * mirroring `runReport`'s strict `=== 0`.
84
+ */
85
+ async runStreamed(command: string, args: string[] = [], options: RunReportOptions = {}): Promise<number> {
86
+ const sh = options.cwd ? this.#shellService.at(options.cwd) : this.#shellService;
87
+ const result = await sh.run(command, args, { throwOnError: false, verbose: false });
88
+ return result.exitCode ?? 1;
89
+ }
90
+
73
91
  async doctor(): Promise<RunReport> {
74
92
  const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
75
93
  const ok = output.exitCode === 0;
@@ -1,7 +1,7 @@
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, Packer, StaticChecker, TypeChecker } from "#src/types/tool.ts";
4
+ import type { Doctor, Formatter, Linter, Packer, StaticChecker, TestRunner, TypeChecker } from "#src/types/tool.ts";
5
5
 
6
6
  export type * from "#src/types/tool.ts";
7
7
 
@@ -13,6 +13,7 @@ export type PluginServices = {
13
13
  jscheck?: StaticChecker & Doctor;
14
14
  typecheck?: TypeChecker & Doctor;
15
15
  pack?: Packer & Doctor;
16
+ test?: TestRunner & Doctor;
16
17
  };
17
18
 
18
19
  export type PluginContext = {
@@ -0,0 +1,36 @@
1
+ import { doctorOneAction } from "#src/actions/doctor.ts";
2
+ import { testAction } from "#src/actions/test.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import { createCommand } from "../base.ts";
5
+
6
+ type ActionOptions = {
7
+ env?: string;
8
+ };
9
+
10
+ export function createTestCommand(ctx: ContextValue) {
11
+ return (
12
+ createCommand("test")
13
+ .addCapabilities(["test"])
14
+ .summary("run the test suite")
15
+ .description(
16
+ "Passthrough to the configured test runner. Forwards every flag and argument (e.g. --project, --watch) — including --help — straight to the tool. Loads .env.test or .env by default. ('rr test doctor' is reserved for the health check.)",
17
+ )
18
+ // Pure passthrough: don't parse the tool's flags, don't capture --help.
19
+ // `--env` is consumed only when it precedes the forwarded args. It's not
20
+ // named `--env-file`: Node's early `--env-file` scanner would grab that
21
+ // token off the `rr` process's argv before Commander ever sees it.
22
+ .allowUnknownOption(true)
23
+ .passThroughOptions(true)
24
+ .helpOption(false)
25
+ .option("--env <path>", "load this env file before running (default: .env.test, then .env)")
26
+ .argument("[args...]", "arguments forwarded to the test runner")
27
+ .action(async (args: string[] = [], options: ActionOptions = {}) => {
28
+ const runner = ctx.plugins.getServiceOrThrow("test");
29
+ await testAction({ ctx, runner, options: { envFile: options.env }, args });
30
+ })
31
+ .addDoctorCommand(async () => {
32
+ const runner = ctx.plugins.getServiceOrThrow("test");
33
+ await doctorOneAction({ ctx, service: runner });
34
+ })
35
+ );
36
+ }
@@ -11,6 +11,7 @@ import { createJsCheckCommand } from "./commands/jscheck.ts";
11
11
  import { createLintCommand } from "./commands/lint.ts";
12
12
  import { createPackCommand } from "./commands/pack.ts";
13
13
  import { createPluginsCommand } from "./commands/plugins.ts";
14
+ import { createTestCommand } from "./commands/test.ts";
14
15
  import { createTsCheckCommand } from "./commands/tscheck.ts";
15
16
  import { RunRunCmd } from "./root.ts";
16
17
 
@@ -29,6 +30,8 @@ export async function createProgram(meta: ImportMeta) {
29
30
  .addCommand(createTsCheckCommand(ctx))
30
31
  .addCommand(createLintCommand(ctx))
31
32
  .addCommand(createFormatCommand(ctx))
33
+ .commandsGroup("Testing:")
34
+ .addCommand(createTestCommand(ctx))
32
35
  .commandsGroup("Build:")
33
36
  .addCommand(createPackCommand(ctx))
34
37
  .commandsGroup("Maintenance:")
package/src/types/tool.ts CHANGED
@@ -48,3 +48,21 @@ export type Packer = {
48
48
  readonly ui: string;
49
49
  pack: () => Promise<RunReport>;
50
50
  };
51
+
52
+ export type TestRunOptions = {
53
+ /** Env file to load before running (resolved by the provider). */
54
+ envFile?: string;
55
+ /** Arguments forwarded verbatim to the test runner. */
56
+ args: string[];
57
+ };
58
+
59
+ export type TestRunner = {
60
+ readonly ui: string;
61
+ /**
62
+ * Streams a passthrough test run and resolves with the tool's exit code.
63
+ * Unlike the other verbs it returns a number, not a `RunReport`: a test
64
+ * runner needs an inherited TTY (watch mode, colors, `--help`), so its output
65
+ * is never captured for the board.
66
+ */
67
+ test: (options: TestRunOptions) => Promise<number>;
68
+ };