@rrlab/cli 0.0.3 → 0.0.4-git-c5c6e0f.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.usage.kdl +2 -2
- package/dist/config.d.mts +1 -1
- package/dist/plugin.d.mts +41 -3
- package/dist/plugin.mjs +92 -4
- package/dist/run.mjs +35 -10
- package/dist/{types-BPHxibPr.d.mts → types-DnqiiIxe.d.mts} +7 -7
- package/package.json +2 -2
- package/src/lib/plugin.ts +3 -1
- package/src/plugin/bin-probe.ts +30 -0
- package/src/plugin/decide-scaffold.ts +46 -0
- package/src/plugin/define-plugin.ts +52 -3
- package/src/plugin/pick-preset.ts +23 -0
- package/src/plugin/registry.ts +2 -1
- package/src/plugin/types.ts +2 -2
- package/src/program/commands/check.ts +32 -3
- package/src/program/commands/plugins.ts +4 -4
- package/src/program/index.ts +1 -1
- package/src/services/ctx.ts +4 -5
- package/src/types/tool.ts +5 -5
package/dist/cli.usage.kdl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @generated by @usage-spec/commander from Commander.js metadata
|
|
2
2
|
name rr
|
|
3
3
|
bin rr
|
|
4
|
-
version "0.0.
|
|
4
|
+
version "0.0.4-git-c5c6e0f.0"
|
|
5
5
|
usage "<command...> [options...]"
|
|
6
6
|
flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
|
|
7
7
|
cmd completion help="print shell completion script (usage)" {
|
|
@@ -37,7 +37,7 @@ cmd format help="check & fix format errors" {
|
|
|
37
37
|
flag --fix help="format all the code"
|
|
38
38
|
cmd doctor help="check if the underlying tool is working correctly"
|
|
39
39
|
}
|
|
40
|
-
cmd check help="run static checks
|
|
40
|
+
cmd check help="run static checks" {
|
|
41
41
|
long_help "Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails."
|
|
42
42
|
}
|
|
43
43
|
cmd doctor help="run all plugin doctors" {
|
package/dist/config.d.mts
CHANGED
package/dist/plugin.d.mts
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
|
-
import { C as Linter, D as TypeChecker, E as TypeCheckOptions, O as ReleaseService, S as LintOptions, T as StaticCheckerOptions, _ as Doctor, a as InstallFlags, b as FormatOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, k as ReleaseServiceOptions, 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 DoctorOutput, w as StaticChecker, x as Formatter, y as DoctorResult } from "./types-
|
|
1
|
+
import { C as Linter, D as TypeChecker, E as TypeCheckOptions, O as ReleaseService, S as LintOptions, T as StaticCheckerOptions, _ as Doctor, a as InstallFlags, b as FormatOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, k as ReleaseServiceOptions, 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 DoctorOutput, w as StaticChecker, x as Formatter, y as DoctorResult } from "./types-DnqiiIxe.mjs";
|
|
2
2
|
import { ShellService } from "@vlandoss/clibuddy";
|
|
3
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
|
|
4
19
|
//#region src/plugin/define-plugin.d.ts
|
|
5
|
-
|
|
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>;
|
|
6
44
|
//#endregion
|
|
7
45
|
//#region ../../node_modules/.pnpm/tinyexec@1.1.2/node_modules/tinyexec/dist/main.d.mts
|
|
8
46
|
//#endregion
|
|
@@ -49,4 +87,4 @@ declare class ToolService {
|
|
|
49
87
|
doctor(): Promise<DoctorResult>;
|
|
50
88
|
}
|
|
51
89
|
//#endregion
|
|
52
|
-
export { type ClackPrompts, type ClackPromptsSelectOption, type Doctor, type DoctorOutput, type DoctorResult, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, PLUGIN_KINDS, type Packer, type Plugin, type PluginCapabilities, type PluginContext, type PluginKind, ReleaseService, type ReleaseServiceOptions, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, definePlugin };
|
|
90
|
+
export { type ClackPrompts, type ClackPromptsSelectOption, type DecideScaffoldOptions, type Doctor, type DoctorOutput, type DoctorResult, 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 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
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
import { resolvePackageBin } from "@vlandoss/clibuddy";
|
|
2
|
-
//#region src/plugin/
|
|
3
|
-
function
|
|
4
|
-
|
|
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;
|
|
5
35
|
}
|
|
6
36
|
//#endregion
|
|
7
37
|
//#region src/plugin/tool-service.ts
|
|
@@ -53,6 +83,64 @@ var ToolService = class {
|
|
|
53
83
|
}
|
|
54
84
|
};
|
|
55
85
|
//#endregion
|
|
86
|
+
//#region src/plugin/bin-probe.ts
|
|
87
|
+
async function probeBins(services, pluginName) {
|
|
88
|
+
const toolServices = services.filter((s) => s instanceof ToolService);
|
|
89
|
+
const distinct = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const svc of toolServices) if (!distinct.has(svc.pkg)) distinct.set(svc.pkg, svc);
|
|
91
|
+
if (distinct.size === 0) return;
|
|
92
|
+
const probes = [...distinct.values()].map(async (svc) => {
|
|
93
|
+
try {
|
|
94
|
+
await svc.getBinDir();
|
|
95
|
+
} catch {
|
|
96
|
+
return svc.pkg;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
const missing = (await Promise.all(probes)).filter((p) => p !== null);
|
|
101
|
+
if (missing.length === 0) return;
|
|
102
|
+
const pkgName = `@rrlab/${pluginName}-plugin`;
|
|
103
|
+
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(" ")})`);
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/plugin/define-plugin.ts
|
|
107
|
+
function definePlugin(factory) {
|
|
108
|
+
return (options) => {
|
|
109
|
+
const def = factory(options);
|
|
110
|
+
const only = options?.only;
|
|
111
|
+
const pkgName = `@rrlab/${def.name}-plugin`;
|
|
112
|
+
return {
|
|
113
|
+
name: def.name,
|
|
114
|
+
apiVersion: def.apiVersion,
|
|
115
|
+
install: def.install,
|
|
116
|
+
uninstall: def.uninstall,
|
|
117
|
+
async capabilities(ctx) {
|
|
118
|
+
const map = await def.capabilities(ctx);
|
|
119
|
+
await probeBins(Object.values(map), def.name);
|
|
120
|
+
if (!only) return map;
|
|
121
|
+
for (const k of only) if (!(k in map)) throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(map).join(", ")}.`);
|
|
122
|
+
return Object.fromEntries(only.map((k) => [k, map[k]]));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/plugin/pick-preset.ts
|
|
129
|
+
async function pickPreset(ctx, opts) {
|
|
130
|
+
const { message, presets, defaultPreset } = opts;
|
|
131
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return defaultPreset;
|
|
132
|
+
const choice = await ctx.prompts.select({
|
|
133
|
+
message,
|
|
134
|
+
options: Object.entries(presets).map(([value, meta]) => ({
|
|
135
|
+
value,
|
|
136
|
+
label: meta.label
|
|
137
|
+
})),
|
|
138
|
+
initialValue: defaultPreset
|
|
139
|
+
});
|
|
140
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
141
|
+
return choice;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
56
144
|
//#region src/plugin/types.ts
|
|
57
145
|
const PLUGIN_KINDS = [
|
|
58
146
|
"lint",
|
|
@@ -107,4 +195,4 @@ var ReleaseService = class {
|
|
|
107
195
|
}
|
|
108
196
|
};
|
|
109
197
|
//#endregion
|
|
110
|
-
export { PLUGIN_KINDS, ReleaseService, ToolService, definePlugin };
|
|
198
|
+
export { PLUGIN_KINDS, ReleaseService, ToolService, decideScaffold, definePlugin, pickPreset };
|
package/dist/run.mjs
CHANGED
|
@@ -28,7 +28,8 @@ var PluginRegistry = class {
|
|
|
28
28
|
if (!first) return void 0;
|
|
29
29
|
if (rest.length > 0) {
|
|
30
30
|
const names = providers.map(({ plugin }) => plugin.name).join(", ");
|
|
31
|
-
|
|
31
|
+
const example = providers.map(({ plugin }) => `${plugin.name}({ only: ['${kind}'] })`).join(" or ");
|
|
32
|
+
throw new Error(`Multiple plugins provide capability '${kind}': ${names}. Narrow each plugin's capabilities in run-run.config.ts using the 'only' option — e.g. ${example}.`);
|
|
32
33
|
}
|
|
33
34
|
return first.impl;
|
|
34
35
|
}
|
|
@@ -121,9 +122,10 @@ async function createContext(binDir) {
|
|
|
121
122
|
cwd
|
|
122
123
|
};
|
|
123
124
|
for (const plugin of config.config.plugins ?? []) {
|
|
124
|
-
|
|
125
|
+
const got = plugin.apiVersion;
|
|
126
|
+
if (got !== 1) throw new Error(`Plugin '${plugin.name}' targets apiVersion ${got}, but this kernel supports only apiVersion 1.`);
|
|
125
127
|
debug("registering plugin: %s", plugin.name);
|
|
126
|
-
const capabilities = await plugin.
|
|
128
|
+
const capabilities = await plugin.capabilities(pluginContext);
|
|
127
129
|
registry.register(plugin, capabilities);
|
|
128
130
|
}
|
|
129
131
|
return {
|
|
@@ -198,8 +200,8 @@ ${runRunColor(`
|
|
|
198
200
|
* `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
|
|
199
201
|
* parent program without any late-binding ceremony.
|
|
200
202
|
*/
|
|
201
|
-
function createCheckCommand() {
|
|
202
|
-
return createCommand("check").summary(`run static checks
|
|
203
|
+
function createCheckCommand(ctx) {
|
|
204
|
+
return createCommand("check").summary(`run static checks${checkAnnotation(ctx)}`).description("Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.").action(async function checkAction() {
|
|
203
205
|
const program = this.parent;
|
|
204
206
|
if (!program) throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
|
|
205
207
|
const cmds = ["jsc", "tsc"].map((name) => ({
|
|
@@ -230,6 +232,29 @@ function createCheckCommand() {
|
|
|
230
232
|
function findCommand(program, name) {
|
|
231
233
|
return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
|
|
232
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Mirrors the provider resolution of `jsc` + `tsc` and flattens the
|
|
237
|
+
* underlying tool labels — e.g. biome (composed lint+format) + oxc (tsc)
|
|
238
|
+
* renders as `(biome, oxlint)` rather than `(biome + biome, oxlint)`. When
|
|
239
|
+
* neither sibling has a provider, falls back to the standard `(not
|
|
240
|
+
* configured)` annotation so the help reads consistently with other
|
|
241
|
+
* commands.
|
|
242
|
+
*/
|
|
243
|
+
function checkAnnotation(ctx) {
|
|
244
|
+
const directJsc = ctx.registry.get("jsc");
|
|
245
|
+
const linter = ctx.registry.get("lint");
|
|
246
|
+
const formatter = ctx.registry.get("format");
|
|
247
|
+
const tsc = ctx.registry.get("tsc");
|
|
248
|
+
const labels = [];
|
|
249
|
+
if (directJsc) labels.push(directJsc.ui);
|
|
250
|
+
else {
|
|
251
|
+
if (linter) labels.push(linter.ui);
|
|
252
|
+
if (formatter) labels.push(formatter.ui);
|
|
253
|
+
}
|
|
254
|
+
if (tsc) labels.push(tsc.ui);
|
|
255
|
+
if (labels.length === 0) return pluginAnnotation(void 0);
|
|
256
|
+
return pluginAnnotation({ ui: [...new Set(labels)].join(", ") });
|
|
257
|
+
}
|
|
233
258
|
//#endregion
|
|
234
259
|
//#region src/program/commands/clean.ts
|
|
235
260
|
function createCleanCommand() {
|
|
@@ -971,7 +996,7 @@ async function runRemove(ctx, alias, opts) {
|
|
|
971
996
|
}
|
|
972
997
|
});
|
|
973
998
|
} catch (err) {
|
|
974
|
-
clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : err}`);
|
|
999
|
+
clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
975
1000
|
}
|
|
976
1001
|
const planSteps = [];
|
|
977
1002
|
if (inConfig) {
|
|
@@ -1036,9 +1061,9 @@ function isDistTag(spec) {
|
|
|
1036
1061
|
function hasInPackageJson(ctx, pkgName) {
|
|
1037
1062
|
const pkg = ctx.appPkg.packageJson;
|
|
1038
1063
|
return pkgName in {
|
|
1039
|
-
...pkg.dependencies
|
|
1040
|
-
...pkg.devDependencies
|
|
1041
|
-
...pkg.peerDependencies
|
|
1064
|
+
...pkg.dependencies,
|
|
1065
|
+
...pkg.devDependencies,
|
|
1066
|
+
...pkg.peerDependencies
|
|
1042
1067
|
};
|
|
1043
1068
|
}
|
|
1044
1069
|
async function withSpinner(message, fn) {
|
|
@@ -1189,7 +1214,7 @@ async function createProgram(options) {
|
|
|
1189
1214
|
program: createCommand("rr").usage("<command...> [options...]").enablePositionalOptions().version(version, "-v, --version").addOption(new Option("--usage", `print KDL spec for this CLI (${palette.muted(palette.link("https://kdl.dev"))})`)).on("option:usage", function onUsage() {
|
|
1190
1215
|
generateToStdout(this);
|
|
1191
1216
|
process.exit(0);
|
|
1192
|
-
}).addHelpText("before", getBannerText(version)).addHelpText("after", CREDITS_TEXT).addCommand(createCompletionCommand()).addCommand(createPackCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).addCommand(createCheckCommand()).addCommand(createDoctorCommand(ctx)).addCommand(createPluginsCommand(ctx)).addCommand(createCleanCommand()).addCommand(createConfigCommand(ctx)),
|
|
1217
|
+
}).addHelpText("before", getBannerText(version)).addHelpText("after", CREDITS_TEXT).addCommand(createCompletionCommand()).addCommand(createPackCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).addCommand(createCheckCommand(ctx)).addCommand(createDoctorCommand(ctx)).addCommand(createPluginsCommand(ctx)).addCommand(createCleanCommand()).addCommand(createConfigCommand(ctx)),
|
|
1193
1218
|
ctx
|
|
1194
1219
|
};
|
|
1195
1220
|
}
|
|
@@ -48,22 +48,22 @@ type DoctorResult = {
|
|
|
48
48
|
};
|
|
49
49
|
type Doctor = {
|
|
50
50
|
ui: string;
|
|
51
|
-
doctor()
|
|
51
|
+
doctor: () => Promise<DoctorResult>;
|
|
52
52
|
};
|
|
53
53
|
type Formatter = {
|
|
54
54
|
bin: string;
|
|
55
55
|
ui: string;
|
|
56
|
-
format(options: FormatOptions)
|
|
56
|
+
format: (options: FormatOptions) => Promise<void>;
|
|
57
57
|
};
|
|
58
58
|
type Linter = {
|
|
59
59
|
bin: string;
|
|
60
60
|
ui: string;
|
|
61
|
-
lint(options: LintOptions)
|
|
61
|
+
lint: (options: LintOptions) => Promise<void>;
|
|
62
62
|
};
|
|
63
63
|
type StaticChecker = {
|
|
64
64
|
bin: string;
|
|
65
65
|
ui: string;
|
|
66
|
-
check(options: StaticCheckerOptions)
|
|
66
|
+
check: (options: StaticCheckerOptions) => Promise<void>;
|
|
67
67
|
};
|
|
68
68
|
type TypeCheckOptions = {
|
|
69
69
|
/** Where to run the type checker. Defaults to the kernel's `cwd`. */cwd?: string;
|
|
@@ -71,14 +71,14 @@ type TypeCheckOptions = {
|
|
|
71
71
|
type TypeChecker = {
|
|
72
72
|
bin: string;
|
|
73
73
|
ui: string;
|
|
74
|
-
check(options?: TypeCheckOptions)
|
|
74
|
+
check: (options?: TypeCheckOptions) => Promise<void>;
|
|
75
75
|
};
|
|
76
76
|
//#endregion
|
|
77
77
|
//#region src/plugin/types.d.ts
|
|
78
78
|
type Packer = {
|
|
79
79
|
bin: string;
|
|
80
80
|
ui: string;
|
|
81
|
-
pack()
|
|
81
|
+
pack: () => Promise<void>;
|
|
82
82
|
};
|
|
83
83
|
declare const PLUGIN_KINDS: readonly ["lint", "format", "jsc", "tsc", "pack"];
|
|
84
84
|
type PluginKind = (typeof PLUGIN_KINDS)[number];
|
|
@@ -99,7 +99,7 @@ type PluginContext = {
|
|
|
99
99
|
type Plugin = {
|
|
100
100
|
name: string;
|
|
101
101
|
apiVersion: 1;
|
|
102
|
-
|
|
102
|
+
capabilities(ctx: PluginContext): Promise<PluginCapabilities>;
|
|
103
103
|
install?(ctx: InstallContext): Promise<InstallResult>;
|
|
104
104
|
uninstall?(ctx: UninstallContext): Promise<UninstallResult>;
|
|
105
105
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rrlab/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-git-c5c6e0f.0",
|
|
4
4
|
"description": "The CLI toolbox to fullstack common scripts in Variable Land",
|
|
5
5
|
"homepage": "https://github.com/variableland/dx/tree/main/run-run/cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"memoize": "10.2.0",
|
|
57
57
|
"nypm": "0.6.0",
|
|
58
58
|
"rimraf": "6.1.3",
|
|
59
|
-
"@vlandoss/loggy": "0.2.
|
|
59
|
+
"@vlandoss/loggy": "0.2.2-git-c5c6e0f.0",
|
|
60
60
|
"@vlandoss/clibuddy": "0.6.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
package/src/lib/plugin.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { type DecideScaffoldOptions, decideScaffold, type ScaffoldDecision } from "#src/plugin/decide-scaffold.ts";
|
|
2
|
+
export { definePlugin, type PluginDefinition } from "#src/plugin/define-plugin.ts";
|
|
3
|
+
export { type PickPresetOptions, pickPreset } from "#src/plugin/pick-preset.ts";
|
|
2
4
|
export { ToolService, type ToolServiceOptions } from "#src/plugin/tool-service.ts";
|
|
3
5
|
export type {
|
|
4
6
|
ClackPrompts,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ToolService } from "./tool-service.ts";
|
|
2
|
+
|
|
3
|
+
export async function probeBins(services: readonly unknown[], pluginName: string): Promise<void> {
|
|
4
|
+
const toolServices = services.filter((s): s is ToolService => s instanceof ToolService);
|
|
5
|
+
const distinct = new Map<string, ToolService>();
|
|
6
|
+
for (const svc of toolServices) {
|
|
7
|
+
if (!distinct.has(svc.pkg)) distinct.set(svc.pkg, svc);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (distinct.size === 0) return;
|
|
11
|
+
|
|
12
|
+
const probes = [...distinct.values()].map(async (svc) => {
|
|
13
|
+
try {
|
|
14
|
+
await svc.getBinDir();
|
|
15
|
+
} catch {
|
|
16
|
+
return svc.pkg;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const results = await Promise.all(probes);
|
|
22
|
+
const missing = results.filter((p): p is string => p !== null);
|
|
23
|
+
if (missing.length === 0) return;
|
|
24
|
+
|
|
25
|
+
const pkgName = `@rrlab/${pluginName}-plugin`;
|
|
26
|
+
throw new Error(
|
|
27
|
+
`${pkgName} requires ${missing.join(", ")} to be installed in the host project. ` +
|
|
28
|
+
`Run: rr plugins add ${pluginName} (or: pnpm add -D ${missing.join(" ")})`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InstallContext } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export type ScaffoldDecision = "create" | "patch" | "overwrite" | "skip";
|
|
4
|
+
|
|
5
|
+
export type DecideScaffoldOptions = {
|
|
6
|
+
/** The config file label shown to the user (e.g. `"biome.json"`, `"tsdown.config.ts"`). */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Whether the file currently exists in the app project. */
|
|
9
|
+
fileExists: boolean;
|
|
10
|
+
/** Short description of what "patch" does, shown in the select option. */
|
|
11
|
+
patchHint: string;
|
|
12
|
+
/**
|
|
13
|
+
* What to return when the file exists and the run is unattended (`--yes` / non-interactive).
|
|
14
|
+
* - `"patch"` (default): assume the user wants to merge our config into theirs (safe for JSON we can edit).
|
|
15
|
+
* - `"skip"`: assume the user owns the file (right for TS modules we'd otherwise rewrite blindly).
|
|
16
|
+
*/
|
|
17
|
+
unattendedExistingAction?: "patch" | "skip";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function decideScaffold(ctx: InstallContext, opts: DecideScaffoldOptions): Promise<ScaffoldDecision> {
|
|
21
|
+
const { label, fileExists, patchHint, unattendedExistingAction = "patch" } = opts;
|
|
22
|
+
|
|
23
|
+
if (!fileExists) {
|
|
24
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
|
|
25
|
+
const choice = await ctx.prompts.confirm({
|
|
26
|
+
message: `Scaffold ${label}?`,
|
|
27
|
+
initialValue: true,
|
|
28
|
+
});
|
|
29
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
30
|
+
return choice ? "create" : "skip";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return unattendedExistingAction;
|
|
34
|
+
|
|
35
|
+
const choice = await ctx.prompts.select<ScaffoldDecision>({
|
|
36
|
+
message: `${label} already exists. What do you want to do?`,
|
|
37
|
+
options: [
|
|
38
|
+
{ value: "patch", label: `Patch — ${patchHint}` },
|
|
39
|
+
{ value: "skip", label: "Skip — leave it alone" },
|
|
40
|
+
{ value: "overwrite", label: "Overwrite — replace with a fresh scaffold" },
|
|
41
|
+
],
|
|
42
|
+
initialValue: "patch",
|
|
43
|
+
});
|
|
44
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
45
|
+
return choice;
|
|
46
|
+
}
|
|
@@ -1,5 +1,54 @@
|
|
|
1
|
-
import
|
|
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";
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
};
|
|
5
54
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { InstallContext } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export type PickPresetOptions<K extends string> = {
|
|
4
|
+
message: string;
|
|
5
|
+
presets: Record<K, { label: string }>;
|
|
6
|
+
defaultPreset: K;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function pickPreset<K extends string>(ctx: InstallContext, opts: PickPresetOptions<K>): Promise<K> {
|
|
10
|
+
const { message, presets, defaultPreset } = opts;
|
|
11
|
+
if (ctx.flags.yes || ctx.flags.nonInteractive) return defaultPreset;
|
|
12
|
+
|
|
13
|
+
const choice = await ctx.prompts.select<K>({
|
|
14
|
+
message,
|
|
15
|
+
options: (Object.entries(presets) as Array<[K, { label: string }]>).map(([value, meta]) => ({
|
|
16
|
+
value,
|
|
17
|
+
label: meta.label,
|
|
18
|
+
})),
|
|
19
|
+
initialValue: defaultPreset,
|
|
20
|
+
});
|
|
21
|
+
if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
|
|
22
|
+
return choice;
|
|
23
|
+
}
|
package/src/plugin/registry.ts
CHANGED
|
@@ -18,9 +18,10 @@ export class PluginRegistry {
|
|
|
18
18
|
if (!first) return undefined;
|
|
19
19
|
if (rest.length > 0) {
|
|
20
20
|
const names = providers.map(({ plugin }) => plugin.name).join(", ");
|
|
21
|
+
const example = providers.map(({ plugin }) => `${plugin.name}({ only: ['${kind}'] })`).join(" or ");
|
|
21
22
|
throw new Error(
|
|
22
23
|
`Multiple plugins provide capability '${kind}': ${names}. ` +
|
|
23
|
-
|
|
24
|
+
`Narrow each plugin's capabilities in run-run.config.ts using the 'only' option — e.g. ${example}.`,
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
return first.impl;
|
package/src/plugin/types.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type {
|
|
|
20
20
|
export type Packer = {
|
|
21
21
|
bin: string;
|
|
22
22
|
ui: string;
|
|
23
|
-
pack()
|
|
23
|
+
pack: () => Promise<void>;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export const PLUGIN_KINDS = ["lint", "format", "jsc", "tsc", "pack"] as const;
|
|
@@ -46,7 +46,7 @@ export type PluginContext = {
|
|
|
46
46
|
export type Plugin = {
|
|
47
47
|
name: string;
|
|
48
48
|
apiVersion: 1;
|
|
49
|
-
|
|
49
|
+
capabilities(ctx: PluginContext): Promise<PluginCapabilities>;
|
|
50
50
|
install?(ctx: InstallContext): Promise<InstallResult>;
|
|
51
51
|
uninstall?(ctx: UninstallContext): Promise<UninstallResult>;
|
|
52
52
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Command, createCommand } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { pluginAnnotation } from "#src/program/ui.ts";
|
|
3
|
+
import type { Context } from "#src/services/ctx.ts";
|
|
3
4
|
import { logger } from "#src/services/logger.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -15,9 +16,9 @@ import { logger } from "#src/services/logger.ts";
|
|
|
15
16
|
* `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
|
|
16
17
|
* parent program without any late-binding ceremony.
|
|
17
18
|
*/
|
|
18
|
-
export function createCheckCommand() {
|
|
19
|
+
export function createCheckCommand(ctx: Context) {
|
|
19
20
|
return createCommand("check")
|
|
20
|
-
.summary(`run static checks
|
|
21
|
+
.summary(`run static checks${checkAnnotation(ctx)}`)
|
|
21
22
|
.description(
|
|
22
23
|
"Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.",
|
|
23
24
|
)
|
|
@@ -61,3 +62,31 @@ export function createCheckCommand() {
|
|
|
61
62
|
function findCommand(program: Command, name: string): Command | undefined {
|
|
62
63
|
return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Mirrors the provider resolution of `jsc` + `tsc` and flattens the
|
|
68
|
+
* underlying tool labels — e.g. biome (composed lint+format) + oxc (tsc)
|
|
69
|
+
* renders as `(biome, oxlint)` rather than `(biome + biome, oxlint)`. When
|
|
70
|
+
* neither sibling has a provider, falls back to the standard `(not
|
|
71
|
+
* configured)` annotation so the help reads consistently with other
|
|
72
|
+
* commands.
|
|
73
|
+
*/
|
|
74
|
+
function checkAnnotation(ctx: Context): string {
|
|
75
|
+
const directJsc = ctx.registry.get("jsc");
|
|
76
|
+
const linter = ctx.registry.get("lint");
|
|
77
|
+
const formatter = ctx.registry.get("format");
|
|
78
|
+
const tsc = ctx.registry.get("tsc");
|
|
79
|
+
|
|
80
|
+
const labels: string[] = [];
|
|
81
|
+
if (directJsc) {
|
|
82
|
+
labels.push(directJsc.ui);
|
|
83
|
+
} else {
|
|
84
|
+
if (linter) labels.push(linter.ui);
|
|
85
|
+
if (formatter) labels.push(formatter.ui);
|
|
86
|
+
}
|
|
87
|
+
if (tsc) labels.push(tsc.ui);
|
|
88
|
+
|
|
89
|
+
if (labels.length === 0) return pluginAnnotation(undefined);
|
|
90
|
+
const distinct = [...new Set(labels)];
|
|
91
|
+
return pluginAnnotation({ ui: distinct.join(", ") });
|
|
92
|
+
}
|
|
@@ -212,7 +212,7 @@ async function runRemove(ctx: Context, alias: OfficialAlias, opts: RemoveOptions
|
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
} catch (err) {
|
|
215
|
-
clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : err}`);
|
|
215
|
+
clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -290,9 +290,9 @@ function isDistTag(spec: string): boolean {
|
|
|
290
290
|
function hasInPackageJson(ctx: Context, pkgName: string): boolean {
|
|
291
291
|
const pkg = ctx.appPkg.packageJson;
|
|
292
292
|
const deps = {
|
|
293
|
-
...
|
|
294
|
-
...
|
|
295
|
-
...
|
|
293
|
+
...pkg.dependencies,
|
|
294
|
+
...pkg.devDependencies,
|
|
295
|
+
...pkg.peerDependencies,
|
|
296
296
|
};
|
|
297
297
|
return pkgName in deps;
|
|
298
298
|
}
|
package/src/program/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export async function createProgram(options: Options) {
|
|
|
40
40
|
.addCommand(createTsCheckCommand(ctx))
|
|
41
41
|
.addCommand(createLintCommand(ctx))
|
|
42
42
|
.addCommand(createFormatCommand(ctx))
|
|
43
|
-
.addCommand(createCheckCommand())
|
|
43
|
+
.addCommand(createCheckCommand(ctx))
|
|
44
44
|
.addCommand(createDoctorCommand(ctx))
|
|
45
45
|
.addCommand(createPluginsCommand(ctx))
|
|
46
46
|
.addCommand(createCleanCommand())
|
package/src/services/ctx.ts
CHANGED
|
@@ -52,13 +52,12 @@ export async function createContext(binDir: string): Promise<Context> {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
for (const plugin of config.config.plugins ?? []) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
55
|
+
const got = plugin.apiVersion as number;
|
|
56
|
+
if (got !== 1) {
|
|
57
|
+
throw new Error(`Plugin '${plugin.name}' targets apiVersion ${got}, but this kernel supports only apiVersion 1.`);
|
|
59
58
|
}
|
|
60
59
|
debug("registering plugin: %s", plugin.name);
|
|
61
|
-
const capabilities = await plugin.
|
|
60
|
+
const capabilities = await plugin.capabilities(pluginContext);
|
|
62
61
|
registry.register(plugin, capabilities);
|
|
63
62
|
}
|
|
64
63
|
|
package/src/types/tool.ts
CHANGED
|
@@ -24,25 +24,25 @@ export type DoctorResult = {
|
|
|
24
24
|
|
|
25
25
|
export type Doctor = {
|
|
26
26
|
ui: string;
|
|
27
|
-
doctor()
|
|
27
|
+
doctor: () => Promise<DoctorResult>;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export type Formatter = {
|
|
31
31
|
bin: string;
|
|
32
32
|
ui: string;
|
|
33
|
-
format(options: FormatOptions)
|
|
33
|
+
format: (options: FormatOptions) => Promise<void>;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
export type Linter = {
|
|
37
37
|
bin: string;
|
|
38
38
|
ui: string;
|
|
39
|
-
lint(options: LintOptions)
|
|
39
|
+
lint: (options: LintOptions) => Promise<void>;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export type StaticChecker = {
|
|
43
43
|
bin: string;
|
|
44
44
|
ui: string;
|
|
45
|
-
check(options: StaticCheckerOptions)
|
|
45
|
+
check: (options: StaticCheckerOptions) => Promise<void>;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
export type TypeCheckOptions = {
|
|
@@ -53,5 +53,5 @@ export type TypeCheckOptions = {
|
|
|
53
53
|
export type TypeChecker = {
|
|
54
54
|
bin: string;
|
|
55
55
|
ui: string;
|
|
56
|
-
check(options?: TypeCheckOptions)
|
|
56
|
+
check: (options?: TypeCheckOptions) => Promise<void>;
|
|
57
57
|
};
|