@rrlab/cli 0.0.1-git-87d22db.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.
Files changed (43) hide show
  1. package/README.md +110 -0
  2. package/bin +54 -0
  3. package/dist/cli.usage.kdl +71 -0
  4. package/dist/config.d.mts +11 -0
  5. package/dist/config.mjs +6 -0
  6. package/dist/plugin.d.mts +52 -0
  7. package/dist/plugin.mjs +65 -0
  8. package/dist/run.mjs +1137 -0
  9. package/dist/types-C3V27_kd.d.mts +173 -0
  10. package/package.json +74 -0
  11. package/src/lib/config.ts +5 -0
  12. package/src/lib/plugin.ts +31 -0
  13. package/src/plugin/define-plugin.ts +5 -0
  14. package/src/plugin/registry.ts +47 -0
  15. package/src/plugin/tool-service.ts +77 -0
  16. package/src/plugin/types.ts +139 -0
  17. package/src/program/commands/check.ts +63 -0
  18. package/src/program/commands/clean.ts +53 -0
  19. package/src/program/commands/completion.ts +26 -0
  20. package/src/program/commands/config.ts +15 -0
  21. package/src/program/commands/doctor.ts +85 -0
  22. package/src/program/commands/format.ts +35 -0
  23. package/src/program/commands/jscheck.ts +44 -0
  24. package/src/program/commands/lint.ts +37 -0
  25. package/src/program/commands/pack.ts +27 -0
  26. package/src/program/commands/plugins.ts +359 -0
  27. package/src/program/commands/tscheck.ts +112 -0
  28. package/src/program/composed-jsc.ts +35 -0
  29. package/src/program/index.ts +50 -0
  30. package/src/program/missing-plugin.ts +18 -0
  31. package/src/program/ui.ts +59 -0
  32. package/src/run.ts +11 -0
  33. package/src/services/config-ast.ts +202 -0
  34. package/src/services/config.ts +54 -0
  35. package/src/services/ctx.ts +72 -0
  36. package/src/services/json-edit.ts +147 -0
  37. package/src/services/logger.ts +5 -0
  38. package/src/services/plugins-registry.ts +21 -0
  39. package/src/services/prompts.ts +26 -0
  40. package/src/services/workspace-target.ts +27 -0
  41. package/src/types/config.ts +13 -0
  42. package/src/types/tool.ts +57 -0
  43. package/tsconfig.json +3 -0
@@ -0,0 +1,173 @@
1
+ import { Pkg, ShellService } from "@vlandoss/clibuddy";
2
+ import { AnyLogger } from "@vlandoss/loggy";
3
+
4
+ //#region src/types/tool.d.ts
5
+ type FormatOptions = {
6
+ fix?: boolean;
7
+ };
8
+ type LintOptions = {
9
+ fix?: boolean;
10
+ };
11
+ type StaticCheckerOptions = {
12
+ fix?: boolean;
13
+ fixStaged?: boolean;
14
+ };
15
+ type DoctorOutput = {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number | undefined;
19
+ };
20
+ type DoctorResult = {
21
+ ok: boolean;
22
+ output: DoctorOutput;
23
+ };
24
+ type Doctor = {
25
+ ui: string;
26
+ doctor(): Promise<DoctorResult>;
27
+ };
28
+ type Formatter = {
29
+ bin: string;
30
+ ui: string;
31
+ format(options: FormatOptions): Promise<void>;
32
+ };
33
+ type Linter = {
34
+ bin: string;
35
+ ui: string;
36
+ lint(options: LintOptions): Promise<void>;
37
+ };
38
+ type StaticChecker = {
39
+ bin: string;
40
+ ui: string;
41
+ check(options: StaticCheckerOptions): Promise<void>;
42
+ };
43
+ type TypeCheckOptions = {
44
+ /** Where to run the type checker. Defaults to the kernel's `cwd`. */cwd?: string;
45
+ };
46
+ type TypeChecker = {
47
+ bin: string;
48
+ ui: string;
49
+ check(options?: TypeCheckOptions): Promise<void>;
50
+ };
51
+ //#endregion
52
+ //#region src/plugin/types.d.ts
53
+ type Packer = {
54
+ bin: string;
55
+ ui: string;
56
+ pack(): Promise<void>;
57
+ };
58
+ declare const PLUGIN_KINDS: readonly ["lint", "format", "jsc", "tsc", "pack"];
59
+ type PluginKind = (typeof PLUGIN_KINDS)[number];
60
+ type PluginCapabilities = {
61
+ lint?: Linter & Doctor;
62
+ format?: Formatter & Doctor;
63
+ jsc?: StaticChecker & Doctor;
64
+ tsc?: TypeChecker & Doctor;
65
+ pack?: Packer & Doctor;
66
+ };
67
+ type PluginContext = {
68
+ shell: ShellService;
69
+ logger: AnyLogger;
70
+ appPkg: Pkg;
71
+ binPkg: Pkg;
72
+ cwd: string;
73
+ };
74
+ type Plugin = {
75
+ name: string;
76
+ apiVersion: 1;
77
+ setup(ctx: PluginContext): Promise<PluginCapabilities> | PluginCapabilities;
78
+ install?(ctx: InstallContext): Promise<InstallResult>;
79
+ uninstall?(ctx: UninstallContext): Promise<UninstallResult>;
80
+ };
81
+ type ClackPromptsSelectOption<T extends string> = {
82
+ value: T;
83
+ label: string;
84
+ hint?: string;
85
+ };
86
+ type ClackPrompts = {
87
+ select<T extends string>(opts: {
88
+ message: string;
89
+ options: Array<ClackPromptsSelectOption<T>>;
90
+ initialValue?: T;
91
+ }): Promise<T | symbol>;
92
+ confirm(opts: {
93
+ message: string;
94
+ initialValue?: boolean;
95
+ }): Promise<boolean | symbol>;
96
+ isCancel(value: unknown): value is symbol;
97
+ };
98
+ type InstallFlags = {
99
+ force: boolean;
100
+ yes: boolean;
101
+ nonInteractive: boolean;
102
+ };
103
+ type UninstallFlags = {
104
+ yes: boolean;
105
+ nonInteractive: boolean;
106
+ };
107
+ type InstallContext = {
108
+ shell: ShellService;
109
+ logger: AnyLogger;
110
+ appPkg: Pkg;
111
+ prompts: ClackPrompts;
112
+ flags: InstallFlags;
113
+ };
114
+ type UninstallContext = {
115
+ shell: ShellService;
116
+ logger: AnyLogger;
117
+ appPkg: Pkg;
118
+ prompts: ClackPrompts;
119
+ flags: UninstallFlags;
120
+ };
121
+ /**
122
+ * Declarative edits on a JSON file. Paths follow JSON Pointer (RFC 6901):
123
+ * `"/extends"`, `"/compilerOptions/strict"`, `"/extends/0"`.
124
+ *
125
+ * NOT strict RFC 6902 JSON Patch — those ops fail on path-condition
126
+ * mismatches (`add` fails if key exists; `replace` fails if missing), which
127
+ * doesn't work for our idempotent merge semantics. See D-005.
128
+ */
129
+ type JsonEdit = {
130
+ op: "set";
131
+ path: string;
132
+ value: unknown; /** "replace" = always set; "if-missing" = only insert when the path doesn't resolve. */
133
+ mode?: "replace" | "if-missing";
134
+ } | {
135
+ op: "unset";
136
+ path: string;
137
+ } | {
138
+ op: "include";
139
+ path: string;
140
+ value: unknown; /** Where to insert into the target array if the value isn't already present. */
141
+ position?: "start" | "end";
142
+ } | {
143
+ op: "exclude";
144
+ path: string;
145
+ value: unknown;
146
+ };
147
+ type FileOp = {
148
+ kind: "create";
149
+ path: string;
150
+ content: string;
151
+ overwrite?: boolean;
152
+ } | {
153
+ kind: "edit-json";
154
+ path: string;
155
+ edits: JsonEdit[];
156
+ } /** Escape hatch for non-JSON / TS-module edits. The plugin owns the parse. */ | {
157
+ kind: "edit-text";
158
+ path: string;
159
+ edit: (source: string) => string;
160
+ } | {
161
+ kind: "delete";
162
+ path: string;
163
+ };
164
+ type InstallResult = {
165
+ /** Packages to install in the host's package.json as devDependencies. */devDependencies?: Record<string, string>;
166
+ files?: FileOp[];
167
+ };
168
+ type UninstallResult = {
169
+ /** Packages to remove from the host's package.json. */removeDependencies?: string[];
170
+ files?: FileOp[];
171
+ };
172
+ //#endregion
173
+ export { Linter as C, TypeChecker as D, TypeCheckOptions as E, LintOptions as S, StaticCheckerOptions as T, Doctor as _, InstallFlags as a, FormatOptions 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, DoctorOutput as v, StaticChecker as w, Formatter as x, DoctorResult as y };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@rrlab/cli",
3
+ "version": "0.0.1-git-87d22db.0",
4
+ "description": "The CLI toolbox to fullstack common scripts in Variable Land",
5
+ "homepage": "https://github.com/variableland/dx/tree/main/run-run/cli#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/variableland/dx/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/variableland/dx.git",
12
+ "directory": "run-run/cli"
13
+ },
14
+ "license": "MIT",
15
+ "author": "rcrd <rcrd@variable.land>",
16
+ "type": "module",
17
+ "imports": {
18
+ "#src/*": "./src/*",
19
+ "#test/*": "./test/*"
20
+ },
21
+ "exports": {
22
+ "./config": {
23
+ "types": "./dist/config.d.mts",
24
+ "default": "./dist/config.mjs"
25
+ },
26
+ "./plugin": {
27
+ "types": "./dist/plugin.d.mts",
28
+ "default": "./dist/plugin.mjs"
29
+ }
30
+ },
31
+ "bin": {
32
+ "rr": "./bin"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "dist",
37
+ "src",
38
+ "!src/**/__tests__",
39
+ "!src/**/*.test.*",
40
+ "tsconfig.json"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
48
+ "dependencies": {
49
+ "@clack/prompts": "0.11.0",
50
+ "@usage-spec/commander": "1.1.0",
51
+ "comment-json": "4.2.5",
52
+ "commander": "14.0.3",
53
+ "glob": "13.0.6",
54
+ "lilconfig": "3.1.3",
55
+ "magicast": "0.3.5",
56
+ "memoize": "10.2.0",
57
+ "nypm": "0.6.0",
58
+ "rimraf": "6.1.3",
59
+ "@vlandoss/clibuddy": "0.6.1-git-87d22db.0",
60
+ "@vlandoss/loggy": "0.2.1-git-87d22db.0"
61
+ },
62
+ "devDependencies": {
63
+ "tsdown": "0.22.0",
64
+ "@rrlab/tsdown-config": "^0.0.1-git-87d22db.0"
65
+ },
66
+ "scripts": {
67
+ "build": "tsdown && pnpm build:kdl",
68
+ "build:kdl": "./bin --usage > dist/cli.usage.kdl",
69
+ "test": "vitest run",
70
+ "test:unit": "vitest run --project unit",
71
+ "test:integration": "vitest run --project integration",
72
+ "test:types": "rr tsc"
73
+ }
74
+ }
@@ -0,0 +1,5 @@
1
+ import type { UserConfig } from "../types/config.ts";
2
+
3
+ export function defineConfig(config: UserConfig) {
4
+ return config;
5
+ }
@@ -0,0 +1,31 @@
1
+ export { definePlugin } from "#src/plugin/define-plugin.ts";
2
+ export { ToolService, type ToolServiceOptions } from "#src/plugin/tool-service.ts";
3
+ export type {
4
+ ClackPrompts,
5
+ ClackPromptsSelectOption,
6
+ Doctor,
7
+ DoctorOutput,
8
+ DoctorResult,
9
+ FileOp,
10
+ FormatOptions,
11
+ Formatter,
12
+ InstallContext,
13
+ InstallFlags,
14
+ InstallResult,
15
+ JsonEdit,
16
+ Linter,
17
+ LintOptions,
18
+ Packer,
19
+ Plugin,
20
+ PluginCapabilities,
21
+ PluginContext,
22
+ PluginKind,
23
+ StaticChecker,
24
+ StaticCheckerOptions,
25
+ TypeChecker,
26
+ TypeCheckOptions,
27
+ UninstallContext,
28
+ UninstallFlags,
29
+ UninstallResult,
30
+ } from "#src/plugin/types.ts";
31
+ export { PLUGIN_KINDS } from "#src/plugin/types.ts";
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "./types.ts";
2
+
3
+ export function definePlugin<T = void>(factory: (options: T) => Plugin): (options: T) => Plugin {
4
+ return factory;
5
+ }
@@ -0,0 +1,47 @@
1
+ import type { Plugin, PluginCapabilities, PluginKind } from "./types.ts";
2
+
3
+ type Entry = {
4
+ plugin: Plugin;
5
+ capabilities: PluginCapabilities;
6
+ };
7
+
8
+ export class PluginRegistry {
9
+ #entries: Entry[] = [];
10
+
11
+ register(plugin: Plugin, capabilities: PluginCapabilities): void {
12
+ this.#entries.push({ plugin, capabilities });
13
+ }
14
+
15
+ get<K extends PluginKind>(kind: K): NonNullable<PluginCapabilities[K]> | undefined {
16
+ const providers = this.#providersOf(kind);
17
+ const [first, ...rest] = providers;
18
+ if (!first) return undefined;
19
+ if (rest.length > 0) {
20
+ const names = providers.map(({ plugin }) => plugin.name).join(", ");
21
+ throw new Error(
22
+ `Multiple plugins provide capability '${kind}': ${names}. ` +
23
+ "Disambiguate by narrowing each plugin's capabilities in run-run.config.ts.",
24
+ );
25
+ }
26
+ return first.impl;
27
+ }
28
+
29
+ providersOf<K extends PluginKind>(kind: K): Array<{ name: string; impl: NonNullable<PluginCapabilities[K]> }> {
30
+ return this.#providersOf(kind).map(({ plugin, impl }) => ({ name: plugin.name, impl }));
31
+ }
32
+
33
+ plugins(): readonly Plugin[] {
34
+ return this.#entries.map(({ plugin }) => plugin);
35
+ }
36
+
37
+ #providersOf<K extends PluginKind>(kind: K): Array<{ plugin: Plugin; impl: NonNullable<PluginCapabilities[K]> }> {
38
+ const out: Array<{ plugin: Plugin; impl: NonNullable<PluginCapabilities[K]> }> = [];
39
+ for (const { plugin, capabilities } of this.#entries) {
40
+ const impl = capabilities[kind];
41
+ if (impl != null) {
42
+ out.push({ plugin, impl: impl as NonNullable<PluginCapabilities[K]> });
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ }
@@ -0,0 +1,77 @@
1
+ import { resolvePackageBin, type ShellService } from "@vlandoss/clibuddy";
2
+ import type { DoctorResult } from "#src/types/tool.ts";
3
+
4
+ export type ToolServiceOptions = {
5
+ pkg: string;
6
+ bin?: string;
7
+ ui: string;
8
+ shellService: ShellService;
9
+ /**
10
+ * Module URL the resolver walks up from when looking for `pkg` in
11
+ * `node_modules`. Plugins MUST pass their own `import.meta.url` so the
12
+ * binary is resolved from the plugin's own dependency graph (peer-installed
13
+ * by the host project), not the kernel's. Kernel-internal services pass
14
+ * `import.meta.url` of their own module file.
15
+ */
16
+ from: string;
17
+ };
18
+
19
+ export type ExecOptions = {
20
+ cwd?: string;
21
+ verbose?: boolean;
22
+ };
23
+
24
+ export class ToolService {
25
+ #shellService: ShellService;
26
+ #pkg: string;
27
+ #bin: string;
28
+ #ui: string;
29
+ #from: string;
30
+
31
+ get bin() {
32
+ return this.#bin;
33
+ }
34
+
35
+ get ui() {
36
+ return this.#ui;
37
+ }
38
+
39
+ get pkg() {
40
+ return this.#pkg;
41
+ }
42
+
43
+ constructor({ pkg, bin, ui, shellService, from }: ToolServiceOptions) {
44
+ this.#pkg = pkg;
45
+ this.#bin = bin ?? pkg;
46
+ this.#ui = ui;
47
+ this.#shellService = shellService;
48
+ this.#from = from;
49
+ }
50
+
51
+ async getBinDir() {
52
+ return resolvePackageBin(this.#pkg, {
53
+ from: this.#from,
54
+ binName: this.#bin,
55
+ });
56
+ }
57
+
58
+ async exec(args: string[] = [], options: ExecOptions = {}) {
59
+ const { cwd, verbose } = options;
60
+ const sh = cwd ? this.#shellService.at(cwd) : this.#shellService;
61
+ return sh.run(await this.getBinDir(), args, { display: this.#bin, verbose });
62
+ }
63
+
64
+ async doctor(): Promise<DoctorResult> {
65
+ const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
66
+ const ok = output.exitCode === 0;
67
+
68
+ return {
69
+ ok,
70
+ output: {
71
+ stdout: output.stdout,
72
+ stderr: output.stderr,
73
+ exitCode: output.exitCode,
74
+ },
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,139 @@
1
+ import type { Pkg, ShellService } from "@vlandoss/clibuddy";
2
+ import type { AnyLogger as Logger } from "@vlandoss/loggy";
3
+ import type { Doctor, Formatter, Linter, StaticChecker, TypeChecker } from "#src/types/tool.ts";
4
+
5
+ export type {
6
+ Doctor,
7
+ DoctorOutput,
8
+ DoctorResult,
9
+ FormatOptions,
10
+ Formatter,
11
+ Linter,
12
+ LintOptions,
13
+ StaticChecker,
14
+ StaticCheckerOptions,
15
+ TypeChecker,
16
+ TypeCheckOptions,
17
+ } from "#src/types/tool.ts";
18
+
19
+ export type Packer = {
20
+ bin: string;
21
+ ui: string;
22
+ pack(): Promise<void>;
23
+ };
24
+
25
+ export const PLUGIN_KINDS = ["lint", "format", "jsc", "tsc", "pack"] as const;
26
+
27
+ export type PluginKind = (typeof PLUGIN_KINDS)[number];
28
+
29
+ export type PluginCapabilities = {
30
+ lint?: Linter & Doctor;
31
+ format?: Formatter & Doctor;
32
+ jsc?: StaticChecker & Doctor;
33
+ tsc?: TypeChecker & Doctor;
34
+ pack?: Packer & Doctor;
35
+ };
36
+
37
+ export type PluginContext = {
38
+ shell: ShellService;
39
+ logger: Logger;
40
+ appPkg: Pkg;
41
+ binPkg: Pkg;
42
+ cwd: string;
43
+ };
44
+
45
+ export type Plugin = {
46
+ name: string;
47
+ apiVersion: 1;
48
+ setup(ctx: PluginContext): Promise<PluginCapabilities> | PluginCapabilities;
49
+ install?(ctx: InstallContext): Promise<InstallResult>;
50
+ uninstall?(ctx: UninstallContext): Promise<UninstallResult>;
51
+ };
52
+
53
+ export type ClackPromptsSelectOption<T extends string> = {
54
+ value: T;
55
+ label: string;
56
+ hint?: string;
57
+ };
58
+
59
+ export type ClackPrompts = {
60
+ select<T extends string>(opts: {
61
+ message: string;
62
+ options: Array<ClackPromptsSelectOption<T>>;
63
+ initialValue?: T;
64
+ }): Promise<T | symbol>;
65
+ confirm(opts: { message: string; initialValue?: boolean }): Promise<boolean | symbol>;
66
+ isCancel(value: unknown): value is symbol;
67
+ };
68
+
69
+ export type InstallFlags = {
70
+ force: boolean;
71
+ yes: boolean;
72
+ nonInteractive: boolean;
73
+ };
74
+
75
+ export type UninstallFlags = {
76
+ yes: boolean;
77
+ nonInteractive: boolean;
78
+ };
79
+
80
+ export type InstallContext = {
81
+ shell: ShellService;
82
+ logger: Logger;
83
+ appPkg: Pkg;
84
+ prompts: ClackPrompts;
85
+ flags: InstallFlags;
86
+ };
87
+
88
+ export type UninstallContext = {
89
+ shell: ShellService;
90
+ logger: Logger;
91
+ appPkg: Pkg;
92
+ prompts: ClackPrompts;
93
+ flags: UninstallFlags;
94
+ };
95
+
96
+ /**
97
+ * Declarative edits on a JSON file. Paths follow JSON Pointer (RFC 6901):
98
+ * `"/extends"`, `"/compilerOptions/strict"`, `"/extends/0"`.
99
+ *
100
+ * NOT strict RFC 6902 JSON Patch — those ops fail on path-condition
101
+ * mismatches (`add` fails if key exists; `replace` fails if missing), which
102
+ * doesn't work for our idempotent merge semantics. See D-005.
103
+ */
104
+ export type JsonEdit =
105
+ | {
106
+ op: "set";
107
+ path: string;
108
+ value: unknown;
109
+ /** "replace" = always set; "if-missing" = only insert when the path doesn't resolve. */
110
+ mode?: "replace" | "if-missing";
111
+ }
112
+ | { op: "unset"; path: string }
113
+ | {
114
+ op: "include";
115
+ path: string;
116
+ value: unknown;
117
+ /** Where to insert into the target array if the value isn't already present. */
118
+ position?: "start" | "end";
119
+ }
120
+ | { op: "exclude"; path: string; value: unknown };
121
+
122
+ export type FileOp =
123
+ | { kind: "create"; path: string; content: string; overwrite?: boolean }
124
+ | { kind: "edit-json"; path: string; edits: JsonEdit[] }
125
+ /** Escape hatch for non-JSON / TS-module edits. The plugin owns the parse. */
126
+ | { kind: "edit-text"; path: string; edit: (source: string) => string }
127
+ | { kind: "delete"; path: string };
128
+
129
+ export type InstallResult = {
130
+ /** Packages to install in the host's package.json as devDependencies. */
131
+ devDependencies?: Record<string, string>;
132
+ files?: FileOp[];
133
+ };
134
+
135
+ export type UninstallResult = {
136
+ /** Packages to remove from the host's package.json. */
137
+ removeDependencies?: string[];
138
+ files?: FileOp[];
139
+ };
@@ -0,0 +1,63 @@
1
+ import { type Command, createCommand } from "commander";
2
+ import { TOOL_LABELS } from "#src/program/ui.ts";
3
+ import { logger } from "#src/services/logger.ts";
4
+
5
+ /**
6
+ * `rr check` is the umbrella that runs the JS check (lint+format) and the
7
+ * TS type check together. Both subcommands are already wired into
8
+ * commander as siblings (`rr jsc`, `rr tsc`), so we reuse the program's
9
+ * command tree as the action registry instead of duplicating it: look the
10
+ * sibling up by name and invoke its action via `parseAsync([])`, which
11
+ * applies its declared option defaults exactly as if the user had typed
12
+ * `rr jsc` directly.
13
+ *
14
+ * Commander binds the running command as `this` inside an action (see
15
+ * `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
16
+ * parent program without any late-binding ceremony.
17
+ */
18
+ export function createCheckCommand() {
19
+ return createCommand("check")
20
+ .summary(`run static checks (${TOOL_LABELS.RUN_RUN})`)
21
+ .description(
22
+ "Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.",
23
+ )
24
+ .action(async function checkAction(this: Command) {
25
+ const program = this.parent;
26
+ if (!program) {
27
+ // Can only happen if this command is invoked detached from the root
28
+ // program — current bin only constructs it as a subcommand.
29
+ throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
30
+ }
31
+
32
+ const targets = ["jsc", "tsc"];
33
+ const cmds = targets.map((name) => ({ name, cmd: findCommand(program, name) }));
34
+
35
+ const missing = cmds.filter(({ cmd }) => !cmd).map(({ name }) => name);
36
+ if (missing.length > 0) {
37
+ for (const name of missing) logger.error(`rr check: subcommand "${name}" is not registered.`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ const results = await Promise.allSettled(
43
+ // biome-ignore lint/style/noNonNullAssertion: missing is guarded above
44
+ cmds.map(({ cmd }) => cmd!.parseAsync([], { from: "user" })),
45
+ );
46
+
47
+ const failed: Array<{ name: string; reason: unknown }> = [];
48
+ for (const [i, r] of results.entries()) {
49
+ if (r.status === "rejected") failed.push({ name: cmds[i]?.name ?? "?", reason: r.reason });
50
+ }
51
+ if (failed.length > 0) {
52
+ for (const { name, reason } of failed) {
53
+ const msg = reason instanceof Error ? reason.message : String(reason);
54
+ logger.error(`rr check (${name}): ${msg}`);
55
+ }
56
+ process.exitCode = 1;
57
+ }
58
+ });
59
+ }
60
+
61
+ function findCommand(program: Command, name: string): Command | undefined {
62
+ return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
63
+ }
@@ -0,0 +1,53 @@
1
+ import { cwd } from "@vlandoss/clibuddy";
2
+ import { createCommand } from "commander";
3
+ import { type GlobOptions, glob } from "glob";
4
+ import { rimraf } from "rimraf";
5
+ import { logger } from "#src/services/logger.ts";
6
+ import { TOOL_LABELS } from "../ui.ts";
7
+
8
+ type Options = {
9
+ onlyDist: boolean;
10
+ dryRun: boolean;
11
+ };
12
+
13
+ export function createCleanCommand() {
14
+ return createCommand("clean")
15
+ .summary(`delete dirty files (${TOOL_LABELS.RIMRAF})`)
16
+ .description("Deletes generated files and folders such as 'dist', 'node_modules', and lock files to ensure a clean state.")
17
+ .option("--only-dist", "delete 'dist' folders only")
18
+ .option("--dry-run", "outputs the paths that would be deleted")
19
+ .action(async function cleanCommandAction(options: Options) {
20
+ async function run(paths: string[], globOptions: GlobOptions) {
21
+ if (options.dryRun) {
22
+ const toDelete = await glob(paths, globOptions);
23
+
24
+ logger.info("Paths that would be deleted: %O", toDelete);
25
+
26
+ return;
27
+ }
28
+
29
+ logger.start("Clean started");
30
+
31
+ await rimraf(paths, {
32
+ glob: globOptions,
33
+ });
34
+
35
+ logger.success("Clean completed");
36
+ }
37
+
38
+ const BUILD_PATHS = ["**/dist"];
39
+ const ALL_DIRTY_PATHS = ["**/.turbo", "**/node_modules", "pnpm-lock.yaml", "bun.lock", ...BUILD_PATHS];
40
+
41
+ if (options.onlyDist) {
42
+ await run(BUILD_PATHS, {
43
+ cwd,
44
+ ignore: ["**/node_modules/**"],
45
+ });
46
+ } else {
47
+ await run(ALL_DIRTY_PATHS, {
48
+ cwd,
49
+ });
50
+ }
51
+ })
52
+ .addHelpText("afterAll", `\nUnder the hood, this command uses ${TOOL_LABELS.RIMRAF} to delete dirty folders or files.`);
53
+ }