@rrlab/cli 1.1.0 → 1.2.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 (111) hide show
  1. package/README.md +10 -10
  2. package/bin +3 -5
  3. package/dist/cli.usage.kdl +26 -25
  4. package/dist/config.d.mts +1 -1
  5. package/dist/magic-string.es-BgIV5Mu3.mjs +1011 -0
  6. package/dist/plugin/__tests__/bin-probe.test.d.mts +1 -0
  7. package/dist/plugin/__tests__/bin-probe.test.mjs +64 -0
  8. package/dist/plugin/__tests__/decide-scaffold.test.d.mts +1 -0
  9. package/dist/plugin/__tests__/decide-scaffold.test.mjs +103 -0
  10. package/dist/plugin/__tests__/define-plugin.test.d.mts +1 -0
  11. package/dist/plugin/__tests__/define-plugin.test.mjs +130 -0
  12. package/dist/plugin/__tests__/pick-preset.test.d.mts +1 -0
  13. package/dist/plugin/__tests__/pick-preset.test.mjs +72 -0
  14. package/dist/plugin/__tests__/registry.test.d.mts +1 -0
  15. package/dist/plugin/__tests__/registry.test.mjs +104 -0
  16. package/dist/plugin/bin-probe.d.mts +4 -0
  17. package/dist/plugin/bin-probe.mjs +22 -0
  18. package/dist/plugin/decide-scaffold.d.mts +18 -0
  19. package/dist/plugin/decide-scaffold.mjs +36 -0
  20. package/dist/plugin/define-plugin.d.mts +17 -0
  21. package/dist/plugin/define-plugin.mjs +25 -0
  22. package/dist/plugin/directory.d.mts +47 -0
  23. package/dist/plugin/directory.mjs +45 -0
  24. package/dist/plugin/errors.d.mts +11 -0
  25. package/dist/plugin/errors.mjs +15 -0
  26. package/dist/plugin/index.d.mts +7 -0
  27. package/dist/plugin/index.mjs +50 -0
  28. package/dist/plugin/pick-preset.d.mts +13 -0
  29. package/dist/plugin/pick-preset.mjs +17 -0
  30. package/dist/plugin/registry.d.mts +19 -0
  31. package/dist/plugin/registry.mjs +2 -0
  32. package/dist/plugin/tool-service.d.mts +45 -0
  33. package/dist/plugin/tool-service.mjs +64 -0
  34. package/dist/plugin/types.d.mts +3 -0
  35. package/dist/plugin/types.mjs +1 -0
  36. package/dist/registry-BgqfKK5L.mjs +55 -0
  37. package/dist/run.mjs +969 -585
  38. package/dist/test.DNmyFkvJ-09ScyH13.mjs +13617 -0
  39. package/dist/tool-DKL6TauZ.d.mts +43 -0
  40. package/dist/{types-snfbujDH.d.mts → types-Iu4IyWof.d.mts} +11 -75
  41. package/package.json +6 -5
  42. package/src/actions/clean.ts +36 -0
  43. package/src/actions/config.ts +46 -0
  44. package/src/actions/doctor.ts +47 -0
  45. package/src/actions/format.ts +13 -0
  46. package/src/actions/jsc.ts +13 -0
  47. package/src/actions/lint.ts +13 -0
  48. package/src/actions/pack.ts +12 -0
  49. package/src/actions/plugins/add.ts +143 -0
  50. package/src/actions/plugins/list.ts +27 -0
  51. package/src/actions/plugins/remove.ts +110 -0
  52. package/src/actions/plugins/shared.ts +58 -0
  53. package/src/actions/run-tool.ts +23 -0
  54. package/src/actions/tsc.ts +65 -0
  55. package/src/errors/invalid-plugin-module.ts +6 -0
  56. package/src/errors/missing-plugin.ts +17 -0
  57. package/src/errors/plugin-api-version.ts +6 -0
  58. package/src/errors/unknown-plugin.ts +7 -0
  59. package/src/lib/plugin/define-plugin.ts +56 -0
  60. package/src/lib/plugin/directory.ts +30 -0
  61. package/src/lib/plugin/errors.ts +15 -0
  62. package/src/lib/{plugin.ts → plugin/index.ts} +8 -9
  63. package/src/lib/plugin/registry.ts +82 -0
  64. package/src/{plugin → lib/plugin}/tool-service.ts +10 -14
  65. package/src/{plugin → lib/plugin}/types.ts +10 -33
  66. package/src/program/base.ts +75 -0
  67. package/src/program/commands/check.ts +31 -62
  68. package/src/program/commands/clean.ts +12 -43
  69. package/src/program/commands/completion.ts +6 -4
  70. package/src/program/commands/config.ts +6 -11
  71. package/src/program/commands/doctor.ts +5 -54
  72. package/src/program/commands/format.ts +18 -25
  73. package/src/program/commands/jscheck.ts +18 -31
  74. package/src/program/commands/lint.ts +18 -26
  75. package/src/program/commands/pack.ts +18 -22
  76. package/src/program/commands/plugins.ts +17 -364
  77. package/src/program/commands/tscheck.ts +19 -77
  78. package/src/program/index.ts +20 -27
  79. package/src/program/root.ts +62 -0
  80. package/src/render/banner.ts +25 -0
  81. package/src/render/board.ts +41 -0
  82. package/src/render/footer.ts +31 -0
  83. package/src/render/labels.ts +28 -0
  84. package/src/render/lines.ts +100 -0
  85. package/src/render/plugin-view.ts +68 -0
  86. package/src/render/steps.ts +20 -0
  87. package/src/run.ts +2 -8
  88. package/src/services/config.ts +4 -0
  89. package/src/services/context.ts +84 -0
  90. package/src/services/file-ops.ts +79 -0
  91. package/src/services/json-edit.ts +1 -1
  92. package/src/services/plugin-meta.ts +63 -0
  93. package/src/services/plugin-services.ts +41 -0
  94. package/src/services/prompts.ts +1 -1
  95. package/src/services/static-checker.ts +46 -0
  96. package/src/types/config.ts +2 -1
  97. package/src/types/tool.ts +13 -26
  98. package/src/ui/theme.ts +5 -0
  99. package/dist/plugin.d.mts +0 -87
  100. package/dist/plugin.mjs +0 -214
  101. package/src/plugin/define-plugin.ts +0 -54
  102. package/src/plugin/registry.ts +0 -48
  103. package/src/program/board.ts +0 -86
  104. package/src/program/composed-jsc.ts +0 -43
  105. package/src/program/missing-plugin.ts +0 -18
  106. package/src/program/ui.ts +0 -59
  107. package/src/services/ctx.ts +0 -71
  108. package/src/services/plugins-registry.ts +0 -22
  109. /package/src/{plugin → lib/plugin}/bin-probe.ts +0 -0
  110. /package/src/{plugin → lib/plugin}/decide-scaffold.ts +0 -0
  111. /package/src/{plugin → lib/plugin}/pick-preset.ts +0 -0
package/src/types/tool.ts CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * The outcome of a check-family tool (lint / format / static check / type
3
- * check) captured rather than streamed. `ok` is the tool's exit code — never a
4
- * guess parsed from output, since tool summaries are unstable and not uniform
5
- * (tsc and oxfmt emit none) — and `output` is the combined stdout+stderr (color
6
- * preserved), flushed verbatim under the package label. See decisions/013.
7
- */
8
1
  export type RunReport = {
9
2
  ok: boolean;
10
3
  output: string;
@@ -23,41 +16,35 @@ export type StaticCheckerOptions = {
23
16
  fixStaged?: boolean;
24
17
  };
25
18
 
19
+ export type TypeCheckOptions = {
20
+ cwd?: string;
21
+ };
22
+
26
23
  export type Doctor = {
27
- ui: string;
28
- /**
29
- * Verifies the tool is wired correctly. Returns a `RunReport` like every
30
- * other verb so the board renders it identically — `output` leads with the
31
- * `$ <bin> --help` liveness command, plus the error if the bin won't run.
32
- */
33
24
  doctor: () => Promise<RunReport>;
34
25
  };
35
26
 
36
27
  export type Formatter = {
37
- bin: string;
38
- ui: string;
28
+ readonly ui: string;
39
29
  format: (options: FormatOptions) => Promise<RunReport>;
40
30
  };
41
31
 
42
32
  export type Linter = {
43
- bin: string;
44
- ui: string;
33
+ readonly ui: string;
45
34
  lint: (options: LintOptions) => Promise<RunReport>;
46
35
  };
47
36
 
48
37
  export type StaticChecker = {
49
- bin: string;
50
- ui: string;
38
+ readonly ui: string;
51
39
  check: (options: StaticCheckerOptions) => Promise<RunReport>;
52
40
  };
53
41
 
54
- export type TypeCheckOptions = {
55
- /** Where to run the type checker. Defaults to the kernel's `cwd`. */
56
- cwd?: string;
57
- };
58
-
59
42
  export type TypeChecker = {
60
- bin: string;
61
- ui: string;
43
+ readonly ui: string;
62
44
  check: (options?: TypeCheckOptions) => Promise<RunReport>;
63
45
  };
46
+
47
+ export type Packer = {
48
+ readonly ui: string;
49
+ pack: () => Promise<RunReport>;
50
+ };
@@ -0,0 +1,5 @@
1
+ import { colorize, palette } from "@vlandoss/clibuddy";
2
+
3
+ export const SEP = ` ${palette.dim("·")} `;
4
+
5
+ export const runRunColor = colorize("#E8722A");
package/dist/plugin.d.mts DELETED
@@ -1,87 +0,0 @@
1
- import { C as StaticChecker, D as ReleaseService, E as TypeChecker, O as ReleaseServiceOptions, S as RunReport, T as TypeCheckOptions, _ as Doctor, a as InstallFlags, b as LintOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, l as Packer, m as UninstallContext, n as ClackPromptsSelectOption, o as InstallResult, p as PluginKind, r as FileOp, s as JsonEdit, t as ClackPrompts, u as Plugin, v as FormatOptions, w as StaticCheckerOptions, x as Linter, y as Formatter } from "./types-snfbujDH.mjs";
2
- import { ShellService } from "@vlandoss/clibuddy";
3
-
4
- //#region src/plugin/decide-scaffold.d.ts
5
- type ScaffoldDecision = "create" | "patch" | "overwrite" | "skip";
6
- type DecideScaffoldOptions = {
7
- /** The config file label shown to the user (e.g. `"biome.json"`, `"tsdown.config.ts"`). */label: string; /** Whether the file currently exists in the app project. */
8
- fileExists: boolean; /** Short description of what "patch" does, shown in the select option. */
9
- patchHint: string;
10
- /**
11
- * What to return when the file exists and the run is unattended (`--yes` / non-interactive).
12
- * - `"patch"` (default): assume the user wants to merge our config into theirs (safe for JSON we can edit).
13
- * - `"skip"`: assume the user owns the file (right for TS modules we'd otherwise rewrite blindly).
14
- */
15
- unattendedExistingAction?: "patch" | "skip";
16
- };
17
- declare function decideScaffold(ctx: InstallContext, opts: DecideScaffoldOptions): Promise<ScaffoldDecision>;
18
- //#endregion
19
- //#region src/plugin/define-plugin.d.ts
20
- type Caps = Partial<PluginCapabilities>;
21
- type PluginDefinition<TCaps extends Caps> = {
22
- name: string;
23
- apiVersion: 1;
24
- capabilities(ctx: PluginContext): TCaps | Promise<TCaps>;
25
- install?(this: void, ctx: InstallContext): Promise<InstallResult>;
26
- uninstall?(this: void, ctx: UninstallContext): Promise<UninstallResult>;
27
- };
28
- type WithOnly<TOptions, TKind extends PluginKind> = TOptions extends void ? {
29
- only?: readonly TKind[];
30
- } : TOptions & {
31
- only?: readonly TKind[];
32
- };
33
- declare function definePlugin<TCaps extends Caps, TOptions = void>(factory: (options: TOptions) => PluginDefinition<TCaps>): (options?: WithOnly<TOptions, keyof TCaps & PluginKind>) => Plugin;
34
- //#endregion
35
- //#region src/plugin/pick-preset.d.ts
36
- type PickPresetOptions<K extends string> = {
37
- message: string;
38
- presets: Record<K, {
39
- label: string;
40
- }>;
41
- defaultPreset: K;
42
- };
43
- declare function pickPreset<K extends string>(ctx: InstallContext, opts: PickPresetOptions<K>): Promise<K>;
44
- //#endregion
45
- //#region src/plugin/tool-service.d.ts
46
- type ToolServiceOptions = {
47
- pkg: string;
48
- bin?: string;
49
- ui: string;
50
- shellService: ShellService;
51
- /**
52
- * Module URL the resolver walks up from when looking for `pkg` in
53
- * `node_modules`. Plugins MUST pass their own `import.meta.url` so the
54
- * binary is resolved from the plugin's own dependency graph (peer-installed
55
- * by the host project), not the kernel's. Kernel-internal services pass
56
- * `import.meta.url` of their own module file.
57
- */
58
- from: string;
59
- };
60
- type RunReportOptions = {
61
- cwd?: string;
62
- };
63
- declare class ToolService {
64
- #private;
65
- get bin(): string;
66
- get ui(): string;
67
- get pkg(): string;
68
- constructor({
69
- pkg,
70
- bin,
71
- ui,
72
- shellService,
73
- from
74
- }: ToolServiceOptions);
75
- getBinDir(): Promise<string>;
76
- /**
77
- * Runs the tool capturing its output instead of streaming it, and reports the
78
- * verdict straight from the exit code — never a guess parsed from the output.
79
- * The board needs the capture to attribute each parallel run's output to its
80
- * package; the non-zero exit is returned (not thrown) so every task settles
81
- * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
82
- */
83
- runReport(args?: string[], options?: RunReportOptions): Promise<RunReport>;
84
- doctor(): Promise<RunReport>;
85
- }
86
- //#endregion
87
- 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, PLUGIN_KINDS, type Packer, type PickPresetOptions, type Plugin, type PluginCapabilities, type PluginContext, type PluginDefinition, type PluginKind, 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 };
package/dist/plugin.mjs DELETED
@@ -1,214 +0,0 @@
1
- import { palette, resolvePackageBin } from "@vlandoss/clibuddy";
2
- //#region src/plugin/decide-scaffold.ts
3
- async function decideScaffold(ctx, opts) {
4
- const { label, fileExists, patchHint, unattendedExistingAction = "patch" } = opts;
5
- if (!fileExists) {
6
- if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
7
- const choice = await ctx.prompts.confirm({
8
- message: `Scaffold ${label}?`,
9
- initialValue: true
10
- });
11
- if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
12
- return choice ? "create" : "skip";
13
- }
14
- if (ctx.flags.yes || ctx.flags.nonInteractive) return unattendedExistingAction;
15
- const choice = await ctx.prompts.select({
16
- message: `${label} already exists. What do you want to do?`,
17
- options: [
18
- {
19
- value: "patch",
20
- label: `Patch — ${patchHint}`
21
- },
22
- {
23
- value: "skip",
24
- label: "Skip — leave it alone"
25
- },
26
- {
27
- value: "overwrite",
28
- label: "Overwrite — replace with a fresh scaffold"
29
- }
30
- ],
31
- initialValue: "patch"
32
- });
33
- if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
34
- return choice;
35
- }
36
- //#endregion
37
- //#region src/plugin/tool-service.ts
38
- var ToolService = class {
39
- #shellService;
40
- #pkg;
41
- #bin;
42
- #ui;
43
- #from;
44
- get bin() {
45
- return this.#bin;
46
- }
47
- get ui() {
48
- return this.#ui;
49
- }
50
- get pkg() {
51
- return this.#pkg;
52
- }
53
- constructor({ pkg, bin, ui, shellService, from }) {
54
- this.#pkg = pkg;
55
- this.#bin = bin ?? pkg;
56
- this.#ui = ui;
57
- this.#shellService = shellService;
58
- this.#from = from;
59
- }
60
- async getBinDir() {
61
- return resolvePackageBin(this.#pkg, {
62
- from: this.#from,
63
- binName: this.#bin
64
- });
65
- }
66
- /**
67
- * Runs the tool capturing its output instead of streaming it, and reports the
68
- * verdict straight from the exit code — never a guess parsed from the output.
69
- * The board needs the capture to attribute each parallel run's output to its
70
- * package; the non-zero exit is returned (not thrown) so every task settles
71
- * and the caller can aggregate. See `decisions/013-check-stream-to-capture-contract.md`.
72
- */
73
- async runReport(args = [], options = {}) {
74
- const output = await (options.cwd ? this.#shellService.at(options.cwd) : this.#shellService).runCaptured(await this.getBinDir(), args, { throwOnError: false });
75
- const header = palette.dim(`$ ${[this.#bin, ...args].join(" ")}`);
76
- const body = combine(output.stdout, output.stderr);
77
- return {
78
- ok: output.exitCode === 0,
79
- output: body ? `${header}\n${body}` : header
80
- };
81
- }
82
- async doctor() {
83
- const output = await this.#shellService.runCaptured(await this.getBinDir(), ["--help"], { throwOnError: false });
84
- const ok = output.exitCode === 0;
85
- const command = palette.dim(`$ ${this.#bin} --help`);
86
- if (ok) return {
87
- ok,
88
- output: command
89
- };
90
- const detail = combine(output.stdout, output.stderr);
91
- return {
92
- ok,
93
- output: detail ? `${command}\n${detail}` : command
94
- };
95
- }
96
- };
97
- /** Joins the non-empty, trimmed streams of a captured run. */
98
- function combine(stdout, stderr) {
99
- return [stdout, stderr].map((stream) => stream?.trim()).filter(Boolean).join("\n");
100
- }
101
- //#endregion
102
- //#region src/plugin/bin-probe.ts
103
- async function probeBins(services, pluginName) {
104
- const toolServices = services.filter((s) => s instanceof ToolService);
105
- const distinct = /* @__PURE__ */ new Map();
106
- for (const svc of toolServices) if (!distinct.has(svc.pkg)) distinct.set(svc.pkg, svc);
107
- if (distinct.size === 0) return;
108
- const probes = [...distinct.values()].map(async (svc) => {
109
- try {
110
- await svc.getBinDir();
111
- } catch {
112
- return svc.pkg;
113
- }
114
- return null;
115
- });
116
- const missing = (await Promise.all(probes)).filter((p) => p !== null);
117
- if (missing.length === 0) return;
118
- const pkgName = `@rrlab/${pluginName}-plugin`;
119
- throw new Error(`${pkgName} requires ${missing.join(", ")} to be installed in the host project. Run: rr plugins add ${pluginName} (or: pnpm add -D ${missing.join(" ")})`);
120
- }
121
- //#endregion
122
- //#region src/plugin/define-plugin.ts
123
- function definePlugin(factory) {
124
- return (options) => {
125
- const def = factory(options);
126
- const only = options?.only;
127
- const pkgName = `@rrlab/${def.name}-plugin`;
128
- return {
129
- name: def.name,
130
- apiVersion: def.apiVersion,
131
- install: def.install,
132
- uninstall: def.uninstall,
133
- async capabilities(ctx) {
134
- const map = await def.capabilities(ctx);
135
- await probeBins(Object.values(map), def.name);
136
- if (!only) return map;
137
- for (const k of only) if (!(k in map)) throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(map).join(", ")}.`);
138
- return Object.fromEntries(only.map((k) => [k, map[k]]));
139
- }
140
- };
141
- };
142
- }
143
- //#endregion
144
- //#region src/plugin/pick-preset.ts
145
- async function pickPreset(ctx, opts) {
146
- const { message, presets, defaultPreset } = opts;
147
- if (ctx.flags.yes || ctx.flags.nonInteractive) return defaultPreset;
148
- const choice = await ctx.prompts.select({
149
- message,
150
- options: Object.entries(presets).map(([value, meta]) => ({
151
- value,
152
- label: meta.label
153
- })),
154
- initialValue: defaultPreset
155
- });
156
- if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
157
- return choice;
158
- }
159
- //#endregion
160
- //#region src/plugin/types.ts
161
- const PLUGIN_KINDS = [
162
- "lint",
163
- "format",
164
- "jsc",
165
- "tsc",
166
- "pack"
167
- ];
168
- //#endregion
169
- //#region src/services/release.ts
170
- const REGISTRY = "https://registry.npmjs.org";
171
- const PROBE_TIMEOUT_MS = 5e3;
172
- /**
173
- * Represents the "release" the current `rr plugins add` runs against — the
174
- * dist-tag the user picked (default: latest), plus the logic to resolve install
175
- * specs for related packages under that release.
176
- *
177
- * - With no `tag`, `resolve()` always returns `"latest"` and never hits the
178
- * registry.
179
- * - With a `tag` (e.g. `"pr-226"`), probes `<pkg>@<tag>`: returns the tag when
180
- * it exists, falls back to `"latest"` otherwise so a partial preview release
181
- * (where only a subset of packages got published) still installs cleanly.
182
- * - Per-package result is cached within the service instance.
183
- */
184
- var ReleaseService = class {
185
- tag;
186
- #fetcher;
187
- #cache = /* @__PURE__ */ new Map();
188
- constructor(tag, { fetcher = fetch } = {}) {
189
- this.tag = tag;
190
- this.#fetcher = fetcher;
191
- }
192
- resolve(pkg) {
193
- if (!this.tag || this.tag === "latest") return Promise.resolve("latest");
194
- const cached = this.#cache.get(pkg);
195
- if (cached) return cached;
196
- const promise = this.#probe(pkg);
197
- this.#cache.set(pkg, promise);
198
- return promise;
199
- }
200
- async #probe(pkg) {
201
- const tag = this.tag;
202
- const controller = new AbortController();
203
- const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
204
- try {
205
- return (await this.#fetcher(`${REGISTRY}/${pkg}/${encodeURIComponent(tag)}`, { signal: controller.signal })).ok ? tag : "latest";
206
- } catch {
207
- return "latest";
208
- } finally {
209
- clearTimeout(timeout);
210
- }
211
- }
212
- };
213
- //#endregion
214
- export { PLUGIN_KINDS, ReleaseService, ToolService, decideScaffold, definePlugin, pickPreset };
@@ -1,54 +0,0 @@
1
- import { probeBins } from "./bin-probe.ts";
2
- import type {
3
- InstallContext,
4
- InstallResult,
5
- Plugin,
6
- PluginCapabilities,
7
- PluginContext,
8
- PluginKind,
9
- UninstallContext,
10
- UninstallResult,
11
- } from "./types.ts";
12
-
13
- type Caps = Partial<PluginCapabilities>;
14
-
15
- export type PluginDefinition<TCaps extends Caps> = {
16
- name: string;
17
- apiVersion: 1;
18
- capabilities(ctx: PluginContext): TCaps | Promise<TCaps>;
19
- install?(this: void, ctx: InstallContext): Promise<InstallResult>;
20
- uninstall?(this: void, ctx: UninstallContext): Promise<UninstallResult>;
21
- };
22
-
23
- type WithOnly<TOptions, TKind extends PluginKind> = TOptions extends void
24
- ? { only?: readonly TKind[] }
25
- : TOptions & { only?: readonly TKind[] };
26
-
27
- export function definePlugin<TCaps extends Caps, TOptions = void>(
28
- factory: (options: TOptions) => PluginDefinition<TCaps>,
29
- ): (options?: WithOnly<TOptions, keyof TCaps & PluginKind>) => Plugin {
30
- return (options) => {
31
- // biome-ignore lint/suspicious/noExplicitAny: factory accepts TOptions; callers without options pass undefined
32
- const def = factory(options as any);
33
- const only = (options as { only?: readonly string[] } | undefined)?.only;
34
- const pkgName = `@rrlab/${def.name}-plugin`;
35
-
36
- return {
37
- name: def.name,
38
- apiVersion: def.apiVersion,
39
- install: def.install,
40
- uninstall: def.uninstall,
41
- async capabilities(ctx: PluginContext): Promise<PluginCapabilities> {
42
- const map = (await def.capabilities(ctx)) as PluginCapabilities;
43
- await probeBins(Object.values(map), def.name);
44
- if (!only) return map;
45
- for (const k of only) {
46
- if (!(k in map)) {
47
- throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(map).join(", ")}.`);
48
- }
49
- }
50
- return Object.fromEntries(only.map((k) => [k, map[k as PluginKind]])) as PluginCapabilities;
51
- },
52
- };
53
- };
54
- }
@@ -1,48 +0,0 @@
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
- const example = providers.map(({ plugin }) => `${plugin.name}({ only: ['${kind}'] })`).join(" or ");
22
- throw new Error(
23
- `Multiple plugins provide capability '${kind}': ${names}. ` +
24
- `Narrow each plugin's capabilities in run-run.config.ts using the 'only' option — e.g. ${example}.`,
25
- );
26
- }
27
- return first.impl;
28
- }
29
-
30
- providersOf<K extends PluginKind>(kind: K): Array<{ name: string; impl: NonNullable<PluginCapabilities[K]> }> {
31
- return this.#providersOf(kind).map(({ plugin, impl }) => ({ name: plugin.name, impl }));
32
- }
33
-
34
- plugins(): readonly Plugin[] {
35
- return this.#entries.map(({ plugin }) => plugin);
36
- }
37
-
38
- #providersOf<K extends PluginKind>(kind: K): Array<{ plugin: Plugin; impl: NonNullable<PluginCapabilities[K]> }> {
39
- const out: Array<{ plugin: Plugin; impl: NonNullable<PluginCapabilities[K]> }> = [];
40
- for (const { plugin, capabilities } of this.#entries) {
41
- const impl = capabilities[kind];
42
- if (impl != null) {
43
- out.push({ plugin, impl: impl as NonNullable<PluginCapabilities[K]> });
44
- }
45
- }
46
- return out;
47
- }
48
- }
@@ -1,86 +0,0 @@
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,43 +0,0 @@
1
- import type { Doctor, Formatter, Linter, RunReport, StaticChecker, StaticCheckerOptions } from "#src/plugin/types.ts";
2
-
3
- /**
4
- * Synthesises the `jsc` capability (`StaticChecker & Doctor`) by composing a
5
- * separately-registered linter and formatter — used when the plugin set
6
- * provides `lint` and `format` independently (e.g. oxc) but no plugin claims
7
- * `jsc`. Runs lint then format sequentially (parallel stdout interleaves badly)
8
- * and merges their reports into one board row.
9
- */
10
- export function composedJscProvider(linter: Linter & Doctor, formatter: Formatter & Doctor): StaticChecker & Doctor {
11
- return {
12
- bin: `${linter.bin}+${formatter.bin}`,
13
- ui: `${linter.ui} + ${formatter.ui}`,
14
- async check({ fix }: StaticCheckerOptions): Promise<RunReport> {
15
- const lintReport = await linter.lint({ fix });
16
- const formatReport = await formatter.format({ fix });
17
- return mergeReports([
18
- { ui: linter.ui, report: lintReport },
19
- { ui: formatter.ui, report: formatReport },
20
- ]);
21
- },
22
- async doctor(): Promise<RunReport> {
23
- const [lintRes, fmtRes] = await Promise.all([linter.doctor(), formatter.doctor()]);
24
- return mergeReports([
25
- { ui: linter.ui, report: lintRes },
26
- { ui: formatter.ui, report: fmtRes },
27
- ]);
28
- },
29
- };
30
- }
31
-
32
- /**
33
- * Folds the lint + format reports into one so the composed `jsc` renders as a
34
- * single board row: ok only when both passed, with each tool's output kept
35
- * under its own header so the flushed detail stays attributable.
36
- */
37
- function mergeReports(parts: Array<{ ui: string; report: RunReport }>): RunReport {
38
- const sections = parts
39
- .filter((part) => part.report.output.trim())
40
- .map((part) => `${part.ui}:\n${part.report.output}`)
41
- .join("\n\n");
42
- return { ok: parts.every((part) => part.report.ok), output: sections };
43
- }
@@ -1,18 +0,0 @@
1
- const SUGGESTIONS: Record<string, string[]> = {
2
- lint: ["biome", "oxc", "eslint"],
3
- format: ["biome", "oxc"],
4
- jsc: ["biome"],
5
- tsc: ["ts"],
6
- pack: ["tsdown"],
7
- };
8
-
9
- export function missingPluginError(kind: string): Error {
10
- const aliases = SUGGESTIONS[kind] ?? [];
11
- const officialList = aliases.map((a) => `@rrlab/${a}-plugin`).join(", ");
12
- const addList = aliases.map((a) => `rr plugins add ${a}`).join(" | ");
13
- return new Error(
14
- `No plugin provides the '${kind}' capability.` +
15
- (officialList ? `\n Install one of: ${officialList}.` : "") +
16
- (addList ? `\n Try: ${addList}.` : ""),
17
- );
18
- }
package/src/program/ui.ts DELETED
@@ -1,59 +0,0 @@
1
- import { colorize, palette, text } from "@vlandoss/clibuddy";
2
-
3
- export const CREDITS_TEXT = `\nAcknowledgment:
4
- - kcd-scripts: for main inspiration
5
- ${palette.link("https://github.com/kentcdodds/kcd-scripts")}
6
-
7
- - peruvian news: in honor to Run Run
8
- ${palette.link("https://es.wikipedia.org/wiki/Run_Run")}`;
9
-
10
- const rimrafColor = colorize("#7C7270");
11
- const runRunColor = colorize("#E8722A");
12
- const usageColor = colorize("#24C55E");
13
-
14
- /**
15
- * Labels used by kernel-internal commands. Plugin-owned tools (biome, oxc,
16
- * tsdown, tsc) define their own colored labels inside each plugin's
17
- * `src/index.ts`.
18
- */
19
- export const TOOL_LABELS = {
20
- RIMRAF: rimrafColor("rimraf"),
21
- RUN_RUN: runRunColor("run-run"),
22
- USAGE: usageColor("usage"),
23
- };
24
-
25
- const IS_USAGE_MODE = process.env.RR_USAGE_MODE === "1";
26
-
27
- /**
28
- * Renders the parenthesised backend hint that follows a command's summary,
29
- * e.g. `pack a ts library 📦 (tsdown)` or `… (not configured)` when no plugin
30
- * provides the capability.
31
- *
32
- * Returns an empty string when `RR_USAGE_MODE=1` is set (the kernel's `bin`
33
- * script exports it during `rr --usage`) so the KDL spec stays free of
34
- * per-environment state — the active plugin set is a property of the host
35
- * project, not of the CLI surface.
36
- */
37
- export function pluginAnnotation(provider: { ui: string } | undefined): string {
38
- if (IS_USAGE_MODE) return "";
39
- return provider ? ` (${provider.ui})` : " (not configured)";
40
- }
41
-
42
- // npx figlet -f "ANSI Shadow" "run-run"
43
- export function getBannerText(version: string) {
44
- const uiLogo = runRunColor(
45
- `
46
- ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
47
- ██╔══██╗██║ ██║████╗ ██║ ██╔══██╗██║ ██║████╗ ██║
48
- ██████╔╝██║ ██║██╔██╗ ██║█████╗██████╔╝██║ ██║██╔██╗ ██║
49
- ██╔══██╗██║ ██║██║╚██╗██║╚════╝██╔══██╗██║ ██║██║╚██╗██║
50
- ██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
51
- ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ${text.version(version)}
52
- `.trim(),
53
- );
54
-
55
- return `
56
- ${uiLogo}
57
-
58
- 🦊 ${palette.italic(palette.muted("The CLI toolbox for"))} ${text.vland}\n`.trimStart();
59
- }