@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.
- package/README.md +10 -10
- package/bin +3 -5
- package/dist/cli.usage.kdl +26 -25
- package/dist/config.d.mts +1 -1
- package/dist/magic-string.es-BgIV5Mu3.mjs +1011 -0
- package/dist/plugin/__tests__/bin-probe.test.d.mts +1 -0
- package/dist/plugin/__tests__/bin-probe.test.mjs +64 -0
- package/dist/plugin/__tests__/decide-scaffold.test.d.mts +1 -0
- package/dist/plugin/__tests__/decide-scaffold.test.mjs +103 -0
- package/dist/plugin/__tests__/define-plugin.test.d.mts +1 -0
- package/dist/plugin/__tests__/define-plugin.test.mjs +130 -0
- package/dist/plugin/__tests__/pick-preset.test.d.mts +1 -0
- package/dist/plugin/__tests__/pick-preset.test.mjs +72 -0
- package/dist/plugin/__tests__/registry.test.d.mts +1 -0
- package/dist/plugin/__tests__/registry.test.mjs +104 -0
- package/dist/plugin/bin-probe.d.mts +4 -0
- package/dist/plugin/bin-probe.mjs +22 -0
- package/dist/plugin/decide-scaffold.d.mts +18 -0
- package/dist/plugin/decide-scaffold.mjs +36 -0
- package/dist/plugin/define-plugin.d.mts +17 -0
- package/dist/plugin/define-plugin.mjs +25 -0
- package/dist/plugin/directory.d.mts +47 -0
- package/dist/plugin/directory.mjs +45 -0
- package/dist/plugin/errors.d.mts +11 -0
- package/dist/plugin/errors.mjs +15 -0
- package/dist/plugin/index.d.mts +7 -0
- package/dist/plugin/index.mjs +50 -0
- package/dist/plugin/pick-preset.d.mts +13 -0
- package/dist/plugin/pick-preset.mjs +17 -0
- package/dist/plugin/registry.d.mts +19 -0
- package/dist/plugin/registry.mjs +2 -0
- package/dist/plugin/tool-service.d.mts +45 -0
- package/dist/plugin/tool-service.mjs +64 -0
- package/dist/plugin/types.d.mts +3 -0
- package/dist/plugin/types.mjs +1 -0
- package/dist/registry-BgqfKK5L.mjs +55 -0
- package/dist/run.mjs +969 -585
- package/dist/test.DNmyFkvJ-09ScyH13.mjs +13617 -0
- package/dist/tool-DKL6TauZ.d.mts +43 -0
- package/dist/{types-snfbujDH.d.mts → types-Iu4IyWof.d.mts} +11 -75
- package/package.json +6 -5
- package/src/actions/clean.ts +36 -0
- package/src/actions/config.ts +46 -0
- package/src/actions/doctor.ts +47 -0
- package/src/actions/format.ts +13 -0
- package/src/actions/jsc.ts +13 -0
- package/src/actions/lint.ts +13 -0
- package/src/actions/pack.ts +12 -0
- package/src/actions/plugins/add.ts +143 -0
- package/src/actions/plugins/list.ts +27 -0
- package/src/actions/plugins/remove.ts +110 -0
- package/src/actions/plugins/shared.ts +58 -0
- package/src/actions/run-tool.ts +23 -0
- package/src/actions/tsc.ts +65 -0
- package/src/errors/invalid-plugin-module.ts +6 -0
- package/src/errors/missing-plugin.ts +17 -0
- package/src/errors/plugin-api-version.ts +6 -0
- package/src/errors/unknown-plugin.ts +7 -0
- package/src/lib/plugin/define-plugin.ts +56 -0
- package/src/lib/plugin/directory.ts +30 -0
- package/src/lib/plugin/errors.ts +15 -0
- package/src/lib/{plugin.ts → plugin/index.ts} +8 -9
- package/src/lib/plugin/registry.ts +82 -0
- package/src/{plugin → lib/plugin}/tool-service.ts +10 -14
- package/src/{plugin → lib/plugin}/types.ts +10 -33
- package/src/program/base.ts +75 -0
- package/src/program/commands/check.ts +31 -62
- package/src/program/commands/clean.ts +12 -43
- package/src/program/commands/completion.ts +6 -4
- package/src/program/commands/config.ts +6 -11
- package/src/program/commands/doctor.ts +5 -54
- package/src/program/commands/format.ts +18 -25
- package/src/program/commands/jscheck.ts +18 -31
- package/src/program/commands/lint.ts +18 -26
- package/src/program/commands/pack.ts +18 -22
- package/src/program/commands/plugins.ts +17 -364
- package/src/program/commands/tscheck.ts +19 -77
- package/src/program/index.ts +20 -27
- package/src/program/root.ts +62 -0
- package/src/render/banner.ts +25 -0
- package/src/render/board.ts +41 -0
- package/src/render/footer.ts +31 -0
- package/src/render/labels.ts +28 -0
- package/src/render/lines.ts +100 -0
- package/src/render/plugin-view.ts +68 -0
- package/src/render/steps.ts +20 -0
- package/src/run.ts +2 -8
- package/src/services/config.ts +4 -0
- package/src/services/context.ts +84 -0
- package/src/services/file-ops.ts +79 -0
- package/src/services/json-edit.ts +1 -1
- package/src/services/plugin-meta.ts +63 -0
- package/src/services/plugin-services.ts +41 -0
- package/src/services/prompts.ts +1 -1
- package/src/services/static-checker.ts +46 -0
- package/src/types/config.ts +2 -1
- package/src/types/tool.ts +13 -26
- package/src/ui/theme.ts +5 -0
- package/dist/plugin.d.mts +0 -87
- package/dist/plugin.mjs +0 -214
- package/src/plugin/define-plugin.ts +0 -54
- package/src/plugin/registry.ts +0 -48
- package/src/program/board.ts +0 -86
- package/src/program/composed-jsc.ts +0 -43
- package/src/program/missing-plugin.ts +0 -18
- package/src/program/ui.ts +0 -59
- package/src/services/ctx.ts +0 -71
- package/src/services/plugins-registry.ts +0 -22
- /package/src/{plugin → lib/plugin}/bin-probe.ts +0 -0
- /package/src/{plugin → lib/plugin}/decide-scaffold.ts +0 -0
- /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
|
-
|
|
38
|
-
ui: string;
|
|
28
|
+
readonly ui: string;
|
|
39
29
|
format: (options: FormatOptions) => Promise<RunReport>;
|
|
40
30
|
};
|
|
41
31
|
|
|
42
32
|
export type Linter = {
|
|
43
|
-
|
|
44
|
-
ui: string;
|
|
33
|
+
readonly ui: string;
|
|
45
34
|
lint: (options: LintOptions) => Promise<RunReport>;
|
|
46
35
|
};
|
|
47
36
|
|
|
48
37
|
export type StaticChecker = {
|
|
49
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/src/ui/theme.ts
ADDED
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
|
-
}
|
package/src/plugin/registry.ts
DELETED
|
@@ -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
|
-
}
|
package/src/program/board.ts
DELETED
|
@@ -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
|
-
}
|