@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/dist/run.mjs
CHANGED
|
@@ -1,57 +1,135 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path, { basename } from "node:path";
|
|
2
|
-
import { colorize, createPkg, createShellService, cwd, dirnameOf, palette,
|
|
3
|
-
import { generateToStdout } from "@usage-spec/commander";
|
|
4
|
-
import { Argument, Option, createCommand } from "commander";
|
|
3
|
+
import { colorize, createPkg, createShellService, cwd, dirnameOf, palette, runTaskBoard, text } from "@vlandoss/clibuddy";
|
|
5
4
|
import fs from "node:fs";
|
|
6
5
|
import os from "node:os";
|
|
7
6
|
import { lilconfig } from "lilconfig";
|
|
8
7
|
import { createLoggy } from "@vlandoss/loggy";
|
|
8
|
+
import { Argument, Command } from "commander";
|
|
9
|
+
import stringWidth from "fast-string-width";
|
|
9
10
|
import { glob } from "glob";
|
|
10
11
|
import { rimraf } from "rimraf";
|
|
11
|
-
import fs$1 from "node:fs/promises";
|
|
12
12
|
import * as clack from "@clack/prompts";
|
|
13
13
|
import { addDependency, detectPackageManager, removeDependency } from "nypm";
|
|
14
|
+
import fs$1 from "node:fs/promises";
|
|
14
15
|
import { builders, generateCode, loadFile, parseModule, writeFile } from "magicast";
|
|
15
16
|
import * as cjson from "comment-json";
|
|
16
|
-
|
|
17
|
+
import { generateToStdout } from "@usage-spec/commander";
|
|
18
|
+
//#region src/errors/plugin-api-version.ts
|
|
19
|
+
/** Thrown when a configured plugin targets an apiVersion the kernel doesn't support. */
|
|
20
|
+
var PluginApiVersionError = class extends Error {
|
|
21
|
+
constructor(pluginName, got) {
|
|
22
|
+
super(`Plugin '${pluginName}' targets apiVersion ${got}, but this kernel supports only apiVersion 1.`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/lib/plugin/directory.ts
|
|
27
|
+
const PLUGINS_DIRECTORY = {
|
|
28
|
+
ts: {
|
|
29
|
+
pkg: "@rrlab/ts-plugin",
|
|
30
|
+
name: "ts",
|
|
31
|
+
capabilities: ["typecheck"]
|
|
32
|
+
},
|
|
33
|
+
biome: {
|
|
34
|
+
pkg: "@rrlab/biome-plugin",
|
|
35
|
+
name: "biome",
|
|
36
|
+
capabilities: [
|
|
37
|
+
"format",
|
|
38
|
+
"jscheck",
|
|
39
|
+
"lint"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
oxc: {
|
|
43
|
+
pkg: "@rrlab/oxc-plugin",
|
|
44
|
+
name: "oxc",
|
|
45
|
+
capabilities: [
|
|
46
|
+
"format",
|
|
47
|
+
"lint",
|
|
48
|
+
"jscheck",
|
|
49
|
+
"typecheck"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
tsdown: {
|
|
53
|
+
pkg: "@rrlab/tsdown-plugin",
|
|
54
|
+
name: "tsdown",
|
|
55
|
+
capabilities: ["pack"]
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function allPluginNames() {
|
|
59
|
+
return Object.keys(PLUGINS_DIRECTORY);
|
|
60
|
+
}
|
|
61
|
+
function isPluginName(name) {
|
|
62
|
+
return Object.hasOwn(PLUGINS_DIRECTORY, name);
|
|
63
|
+
}
|
|
64
|
+
function providersOf(capability) {
|
|
65
|
+
return Object.values(PLUGINS_DIRECTORY).filter((info) => {
|
|
66
|
+
return info.capabilities.includes(capability);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/errors/missing-plugin.ts
|
|
71
|
+
var MissingPluginError = class extends Error {
|
|
72
|
+
constructor(capability) {
|
|
73
|
+
const plugins = providersOf(capability);
|
|
74
|
+
const pkgList = plugins.map((it) => it.pkg).join(", ");
|
|
75
|
+
const addList = plugins.map((it) => `rr plugins add ${it.name}`).join(" | ");
|
|
76
|
+
super(`No plugin provides the '${capability}' capability.` + (pkgList ? `\n Install one of: ${pkgList}.` : "") + (addList ? `\n Try: ${addList}.` : ""));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/lib/plugin/errors.ts
|
|
81
|
+
/**
|
|
82
|
+
* Thrown by `PluginRegistry.get` when more than one plugin provides the same
|
|
83
|
+
* capability. The message is load-bearing for the "ambiguity → user narrows
|
|
84
|
+
* config" UX (decision 003) — `registry.test.ts` asserts the plugin names appear.
|
|
85
|
+
*/
|
|
86
|
+
var MultipleProvidersError = class extends Error {
|
|
87
|
+
constructor(kind, pluginNames) {
|
|
88
|
+
const names = pluginNames.join(", ");
|
|
89
|
+
const example = pluginNames.map((name) => `${name}({ only: ['${kind}'] })`).join(" or ");
|
|
90
|
+
super(`Multiple plugins provide capability '${kind}': ${names}. Narrow each plugin's capabilities in run-run.config.ts using the 'only' option — e.g. ${example}.`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/lib/plugin/registry.ts
|
|
17
95
|
var PluginRegistry = class {
|
|
18
96
|
#entries = [];
|
|
19
|
-
register(plugin,
|
|
97
|
+
register(plugin, services) {
|
|
20
98
|
this.#entries.push({
|
|
21
99
|
plugin,
|
|
22
|
-
|
|
100
|
+
services
|
|
23
101
|
});
|
|
24
102
|
}
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (rest.length > 0) {
|
|
30
|
-
const names = providers.map(({ plugin }) => plugin.name).join(", ");
|
|
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}.`);
|
|
33
|
-
}
|
|
34
|
-
return first.impl;
|
|
103
|
+
getServiceOrThrow(capability) {
|
|
104
|
+
const provider = this.providerOf(capability);
|
|
105
|
+
if (!provider) throw new MissingPluginError(capability);
|
|
106
|
+
return provider.service;
|
|
35
107
|
}
|
|
36
|
-
|
|
37
|
-
return this
|
|
38
|
-
name: plugin.name,
|
|
39
|
-
impl
|
|
40
|
-
}));
|
|
108
|
+
getService(capability) {
|
|
109
|
+
return this.providerOf(capability)?.service;
|
|
41
110
|
}
|
|
42
|
-
|
|
43
|
-
|
|
111
|
+
getAllServices() {
|
|
112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const { services } of this.#entries) for (const service of Object.values(services)) if (service) seen.add(service);
|
|
114
|
+
return [...seen];
|
|
44
115
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
for (const { plugin,
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
116
|
+
providersOf(capability) {
|
|
117
|
+
const providers = [];
|
|
118
|
+
for (const { plugin, services } of this.#entries) {
|
|
119
|
+
const service = services[capability];
|
|
120
|
+
if (service) providers.push({
|
|
50
121
|
plugin,
|
|
51
|
-
|
|
122
|
+
service
|
|
52
123
|
});
|
|
53
124
|
}
|
|
54
|
-
return
|
|
125
|
+
return providers;
|
|
126
|
+
}
|
|
127
|
+
providerOf(capability) {
|
|
128
|
+
const providers = this.providersOf(capability);
|
|
129
|
+
const [first, ...rest] = providers;
|
|
130
|
+
if (!first) return;
|
|
131
|
+
if (rest.length > 0) throw new MultipleProvidersError(capability, providers.map(({ plugin }) => plugin.name));
|
|
132
|
+
return first;
|
|
55
133
|
}
|
|
56
134
|
};
|
|
57
135
|
//#endregion
|
|
@@ -75,14 +153,17 @@ var ConfigService = class {
|
|
|
75
153
|
}
|
|
76
154
|
async load() {
|
|
77
155
|
const debug = logger.subdebug("load-config");
|
|
156
|
+
const start = performance.now();
|
|
78
157
|
const searchResult = await this.#searcher.search();
|
|
158
|
+
const loadMs = performance.now() - start;
|
|
79
159
|
if (!searchResult || searchResult?.isEmpty) {
|
|
80
160
|
debug("loaded default config: %O", DEFAULT_CONFIG);
|
|
81
161
|
return {
|
|
82
162
|
config: DEFAULT_CONFIG,
|
|
83
163
|
meta: {
|
|
84
164
|
isDefault: true,
|
|
85
|
-
filepath: void 0
|
|
165
|
+
filepath: void 0,
|
|
166
|
+
loadMs
|
|
86
167
|
}
|
|
87
168
|
};
|
|
88
169
|
}
|
|
@@ -93,89 +174,124 @@ var ConfigService = class {
|
|
|
93
174
|
config,
|
|
94
175
|
meta: {
|
|
95
176
|
isDefault: false,
|
|
96
|
-
filepath: searchResult.filepath
|
|
177
|
+
filepath: searchResult.filepath,
|
|
178
|
+
loadMs
|
|
97
179
|
}
|
|
98
180
|
};
|
|
99
181
|
}
|
|
100
182
|
};
|
|
101
183
|
//#endregion
|
|
102
|
-
//#region src/services/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const [appPkg, binPkg] = await Promise.all([createPkg(cwd), createPkg(binPath)]);
|
|
109
|
-
if (!binPkg) throw new Error("Could not find bin package.json");
|
|
110
|
-
if (!appPkg) throw new Error("Could not find app package.json");
|
|
111
|
-
debug("app pkg info: %O", appPkg.info());
|
|
112
|
-
debug("bin pkg info: %O", binPkg.info());
|
|
113
|
-
const shell = createShellService();
|
|
114
|
-
debug("shell service options: %O", shell.options);
|
|
115
|
-
const config = await new ConfigService().load();
|
|
116
|
-
const registry = new PluginRegistry();
|
|
117
|
-
const pluginContext = {
|
|
118
|
-
shell,
|
|
119
|
-
logger,
|
|
120
|
-
appPkg,
|
|
121
|
-
binPkg,
|
|
122
|
-
cwd
|
|
123
|
-
};
|
|
124
|
-
for (const plugin of config.config.plugins ?? []) {
|
|
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.`);
|
|
127
|
-
debug("registering plugin: %s", plugin.name);
|
|
128
|
-
const capabilities = await plugin.capabilities(pluginContext);
|
|
129
|
-
registry.register(plugin, capabilities);
|
|
184
|
+
//#region src/services/static-checker.ts
|
|
185
|
+
var StaticCheckService = class {
|
|
186
|
+
#linter;
|
|
187
|
+
#formatter;
|
|
188
|
+
get ui() {
|
|
189
|
+
return `${this.#linter.ui} + ${this.#formatter.ui}`;
|
|
130
190
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
191
|
+
constructor(linter, formatter) {
|
|
192
|
+
this.#linter = linter;
|
|
193
|
+
this.#formatter = formatter;
|
|
194
|
+
}
|
|
195
|
+
async check(options) {
|
|
196
|
+
const lintReport = await this.#linter.lint(options);
|
|
197
|
+
const formatReport = await this.#formatter.format(options);
|
|
198
|
+
return this.#mergeReports([{
|
|
199
|
+
ui: this.#linter.ui,
|
|
200
|
+
report: lintReport
|
|
201
|
+
}, {
|
|
202
|
+
ui: this.#formatter.ui,
|
|
203
|
+
report: formatReport
|
|
204
|
+
}]);
|
|
205
|
+
}
|
|
206
|
+
async doctor() {
|
|
207
|
+
const [lintRes, fmtRes] = await Promise.all([this.#linter.doctor(), this.#formatter.doctor()]);
|
|
208
|
+
return this.#mergeReports([{
|
|
209
|
+
ui: this.#linter.ui,
|
|
210
|
+
report: lintRes
|
|
211
|
+
}, {
|
|
212
|
+
ui: this.#formatter.ui,
|
|
213
|
+
report: fmtRes
|
|
214
|
+
}]);
|
|
215
|
+
}
|
|
216
|
+
#mergeReports(parts) {
|
|
217
|
+
const sections = parts.filter((part) => part.report.output.trim()).map((part) => `${part.ui}:\n${part.report.output}`).join("\n\n");
|
|
218
|
+
return {
|
|
219
|
+
ok: parts.every((part) => part.report.ok),
|
|
220
|
+
output: sections
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
};
|
|
139
224
|
//#endregion
|
|
140
|
-
//#region src/
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
225
|
+
//#region src/services/plugin-services.ts
|
|
226
|
+
var PluginServices = class {
|
|
227
|
+
#registry;
|
|
228
|
+
constructor(registry) {
|
|
229
|
+
this.#registry = registry;
|
|
230
|
+
}
|
|
231
|
+
getAllServices() {
|
|
232
|
+
return this.#registry.getAllServices();
|
|
233
|
+
}
|
|
234
|
+
providerOf(capability) {
|
|
235
|
+
return this.#registry.providerOf(capability);
|
|
236
|
+
}
|
|
237
|
+
getServiceOrThrow(capability) {
|
|
238
|
+
return this.#registry.getServiceOrThrow(capability);
|
|
239
|
+
}
|
|
240
|
+
getJsChecker() {
|
|
241
|
+
const checker = this.#registry.getService("jscheck");
|
|
242
|
+
if (checker) return checker;
|
|
243
|
+
const linter = this.#registry.getService("lint");
|
|
244
|
+
const formatter = this.#registry.getService("format");
|
|
245
|
+
if (linter && formatter) return new StaticCheckService(linter, formatter);
|
|
246
|
+
throw new MissingPluginError("jscheck");
|
|
247
|
+
}
|
|
151
248
|
};
|
|
152
|
-
function missingPluginError(kind) {
|
|
153
|
-
const aliases = SUGGESTIONS[kind] ?? [];
|
|
154
|
-
const officialList = aliases.map((a) => `@rrlab/${a}-plugin`).join(", ");
|
|
155
|
-
const addList = aliases.map((a) => `rr plugins add ${a}`).join(" | ");
|
|
156
|
-
return /* @__PURE__ */ new Error(`No plugin provides the '${kind}' capability.` + (officialList ? `\n Install one of: ${officialList}.` : "") + (addList ? `\n Try: ${addList}.` : ""));
|
|
157
|
-
}
|
|
158
249
|
//#endregion
|
|
159
|
-
//#region src/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
250
|
+
//#region src/services/context.ts
|
|
251
|
+
var ContextService = class {
|
|
252
|
+
#binDir;
|
|
253
|
+
constructor(binDir) {
|
|
254
|
+
this.#binDir = binDir;
|
|
255
|
+
}
|
|
256
|
+
async getContext() {
|
|
257
|
+
const debug = logger.subdebug("create-context");
|
|
258
|
+
const binPath = fs.realpathSync(this.#binDir);
|
|
259
|
+
debug("bin path:", binPath);
|
|
260
|
+
debug("process cwd:", process.cwd());
|
|
261
|
+
const [appPkg, binPkg] = await Promise.all([createPkg(cwd), createPkg(binPath)]);
|
|
262
|
+
if (!binPkg) throw new Error("Could not find bin package.json");
|
|
263
|
+
if (!appPkg) throw new Error("Could not find app package.json");
|
|
264
|
+
debug("app pkg info: %O", appPkg.info());
|
|
265
|
+
debug("bin pkg info: %O", binPkg.info());
|
|
266
|
+
const shell = createShellService();
|
|
267
|
+
debug("shell service options: %O", shell.options);
|
|
268
|
+
const config = await new ConfigService().load();
|
|
269
|
+
const registry = new PluginRegistry();
|
|
270
|
+
const pluginContext = {
|
|
271
|
+
shell,
|
|
272
|
+
logger,
|
|
273
|
+
appPkg,
|
|
274
|
+
binPkg,
|
|
275
|
+
cwd
|
|
276
|
+
};
|
|
277
|
+
for (const plugin of config.config.plugins ?? []) {
|
|
278
|
+
const got = plugin.apiVersion;
|
|
279
|
+
if (got !== 1) throw new PluginApiVersionError(plugin.name, got);
|
|
280
|
+
debug("registering plugin: %s", plugin.name);
|
|
281
|
+
const services = await plugin.services(pluginContext);
|
|
282
|
+
registry.register(plugin, services);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
appPkg,
|
|
286
|
+
binPkg,
|
|
287
|
+
shell,
|
|
288
|
+
config,
|
|
289
|
+
plugins: new PluginServices(registry)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/render/board.ts
|
|
179
295
|
/** Bridges a check-family verb (returns a `RunReport`) to a board row, its `output` becoming the flushed detail. */
|
|
180
296
|
function reportTask(label, run) {
|
|
181
297
|
return {
|
|
@@ -210,105 +326,233 @@ async function runBoard(tasks, options = {}) {
|
|
|
210
326
|
if (sink) sink.push(result);
|
|
211
327
|
return result;
|
|
212
328
|
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/render/labels.ts
|
|
331
|
+
/** `<command> (<tool>)` — the verb plus the `ui` of the tool that backs it (e.g. `lint (biome)`, `tsc (tsc)`). */
|
|
332
|
+
function commandTool(command, provider) {
|
|
333
|
+
return `${command} (${provider.ui})`;
|
|
334
|
+
}
|
|
335
|
+
function pkgName(appPkg) {
|
|
336
|
+
return appPkg.packageJson.name ?? basename(appPkg.dirPath);
|
|
337
|
+
}
|
|
338
|
+
/** The canonical single-target row label, `<command> (<tool>) · <package>`, so every command reads alike. */
|
|
339
|
+
function targetLabel(command, provider, appPkg) {
|
|
340
|
+
return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
|
|
341
|
+
}
|
|
213
342
|
/**
|
|
214
|
-
* The
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* (tsc) or compose siblings (check) call `runBoard` directly instead.
|
|
343
|
+
* The canonical fan-out section title, `<command> (<tool>) · <n> <unit>`. The
|
|
344
|
+
* tool is omitted when the fan-out spans several tools (`rr doctor` → `doctor ·
|
|
345
|
+
* 3 tools`), since the rows then carry the per-tool name.
|
|
218
346
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!provider) throw missingPluginError(spec.kind);
|
|
222
|
-
if (!(await runBoard([reportTask(targetLabel(spec.name, provider, ctx.appPkg), () => spec.run(provider))])).ok) process.exitCode = 1;
|
|
347
|
+
function fanoutTitle(command, provider, count, unit) {
|
|
348
|
+
return `${provider ? commandTool(command, provider) : command} · ${count} ${unit}`;
|
|
223
349
|
}
|
|
224
350
|
//#endregion
|
|
225
|
-
//#region src/
|
|
226
|
-
const CREDITS_TEXT = `\nAcknowledgment:
|
|
227
|
-
- kcd-scripts: for main inspiration
|
|
228
|
-
${palette.link("https://github.com/kentcdodds/kcd-scripts")}
|
|
229
|
-
|
|
230
|
-
- peruvian news: in honor to Run Run
|
|
231
|
-
${palette.link("https://es.wikipedia.org/wiki/Run_Run")}`;
|
|
232
|
-
const rimrafColor = colorize("#7C7270");
|
|
233
|
-
const runRunColor = colorize("#E8722A");
|
|
234
|
-
const usageColor = colorize("#24C55E");
|
|
351
|
+
//#region src/actions/run-tool.ts
|
|
235
352
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
353
|
+
* The shared action for a single-provider tool command (lint, format, jsc,
|
|
354
|
+
* pack): run the provider's verb as one board row labelled `<name> (<tool>) ·
|
|
355
|
+
* <pkg>`, and aggregate the exit code. The command resolves the provider and
|
|
356
|
+
* throws MissingPluginError when it's absent, so the provider is required here.
|
|
357
|
+
* Commands that fan out (tsc) or compose siblings (check) drive the board themselves.
|
|
239
358
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
RUN_RUN: runRunColor("run-run"),
|
|
243
|
-
USAGE: usageColor("usage")
|
|
244
|
-
};
|
|
245
|
-
const IS_USAGE_MODE = process.env.RR_USAGE_MODE === "1";
|
|
246
|
-
/**
|
|
247
|
-
* Renders the parenthesised backend hint that follows a command's summary,
|
|
248
|
-
* e.g. `pack a ts library 📦 (tsdown)` or `… (not configured)` when no plugin
|
|
249
|
-
* provides the capability.
|
|
250
|
-
*
|
|
251
|
-
* Returns an empty string when `RR_USAGE_MODE=1` is set (the kernel's `bin`
|
|
252
|
-
* script exports it during `rr --usage`) so the KDL spec stays free of
|
|
253
|
-
* per-environment state — the active plugin set is a property of the host
|
|
254
|
-
* project, not of the CLI surface.
|
|
255
|
-
*/
|
|
256
|
-
function pluginAnnotation(provider) {
|
|
257
|
-
if (IS_USAGE_MODE) return "";
|
|
258
|
-
return provider ? ` (${provider.ui})` : " (not configured)";
|
|
359
|
+
async function runToolAction({ ctx, name, provider, run }) {
|
|
360
|
+
if (!(await runBoard([reportTask(targetLabel(name, provider, ctx.appPkg), () => run(provider))])).ok) process.exitCode = 1;
|
|
259
361
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/actions/jsc.ts
|
|
364
|
+
function jscAction({ ctx, checker, options }) {
|
|
365
|
+
return runToolAction({
|
|
366
|
+
ctx,
|
|
367
|
+
name: "jsc",
|
|
368
|
+
provider: checker,
|
|
369
|
+
run: (p) => p.check(options)
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/actions/tsc.ts
|
|
374
|
+
const getPreScript = (scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
|
|
375
|
+
async function tscAction({ ctx, tsc }) {
|
|
376
|
+
const { appPkg, shell } = ctx;
|
|
377
|
+
const isTsProject = (dir) => appPkg.hasFile("tsconfig.json", dir);
|
|
378
|
+
const typecheckTask = (label, dir, scripts) => reportTask(label, async () => {
|
|
379
|
+
const preScript = getPreScript(scripts);
|
|
380
|
+
if (preScript) {
|
|
381
|
+
const pre = await shell.at(dir).runCaptured(preScript, [], {
|
|
382
|
+
shell: true,
|
|
383
|
+
throwOnError: false
|
|
384
|
+
});
|
|
385
|
+
if ((pre.exitCode ?? 0) !== 0) return {
|
|
386
|
+
ok: false,
|
|
387
|
+
output: `pre-script \`${preScript}\` failed\n${[pre.stdout, pre.stderr].map((s) => s?.trim()).filter(Boolean).join("\n")}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return tsc.check({ cwd: dir });
|
|
391
|
+
});
|
|
392
|
+
if (!appPkg.isMonorepo()) {
|
|
393
|
+
if (!isTsProject(appPkg.dirPath)) {
|
|
394
|
+
logger.info("No tsconfig.json found, skipping typecheck");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!(await runBoard([typecheckTask(targetLabel("tsc", tsc, appPkg), appPkg.dirPath, appPkg.packageJson.scripts)])).ok) process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const tsProjects = (await appPkg.getWorkspaceProjects()).filter((project) => isTsProject(project.rootDir));
|
|
401
|
+
if (!tsProjects.length) {
|
|
402
|
+
logger.warn("No ts projects found in the monorepo, skipping typecheck");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!(await runBoard(tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts)), { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") })).ok) process.exitCode = 1;
|
|
406
|
+
}
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/render/lines.ts
|
|
409
|
+
var Lines = class {
|
|
410
|
+
#lines;
|
|
411
|
+
constructor() {
|
|
412
|
+
this.#lines = [];
|
|
413
|
+
}
|
|
414
|
+
isEmpty() {
|
|
415
|
+
return !this.#lines.length;
|
|
416
|
+
}
|
|
417
|
+
add(data, padStart = 0) {
|
|
418
|
+
if (Array.isArray(data)) data.forEach((it) => {
|
|
419
|
+
this.#append(it, padStart);
|
|
420
|
+
});
|
|
421
|
+
else this.#append(data, padStart);
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
addTable(rows, columns, opts = {}) {
|
|
425
|
+
const { gap = 3, padStart = 0 } = opts;
|
|
426
|
+
const sized = columns.map((col) => ({
|
|
427
|
+
...col,
|
|
428
|
+
width: Math.max(...rows.map((row) => stringWidth(String(row[col.key]))))
|
|
429
|
+
}));
|
|
430
|
+
const sep = this.#sep(gap);
|
|
431
|
+
rows.forEach((row) => {
|
|
432
|
+
const cells = sized.map((col) => {
|
|
433
|
+
const raw = String(row[col.key]);
|
|
434
|
+
return this.#padCell(raw, col.width, col.align ?? "left");
|
|
435
|
+
});
|
|
436
|
+
this.#append(cells.join(sep), padStart);
|
|
437
|
+
});
|
|
438
|
+
return this;
|
|
439
|
+
}
|
|
440
|
+
newline(prepend = false) {
|
|
441
|
+
if (prepend) this.#prepend("");
|
|
442
|
+
else this.#append("");
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
printStdout() {
|
|
446
|
+
process.stdout.write(`${this.render()}\n`);
|
|
447
|
+
}
|
|
448
|
+
render() {
|
|
449
|
+
return this.#lines.join("\n");
|
|
450
|
+
}
|
|
451
|
+
#padCell(str, width, align) {
|
|
452
|
+
const diff = Math.max(0, width - stringWidth(str));
|
|
453
|
+
const fill = this.#sep(diff);
|
|
454
|
+
return align === "right" ? `${fill}${str}` : `${str}${fill}`;
|
|
455
|
+
}
|
|
456
|
+
#append(str, padStart = 0) {
|
|
457
|
+
if (padStart > 0) this.#lines.push(`${this.#sep(padStart)}${str}`);
|
|
458
|
+
else this.#lines.push(str);
|
|
459
|
+
}
|
|
460
|
+
#prepend(str, padStart = 0) {
|
|
461
|
+
if (padStart > 0) this.#lines.unshift(`${this.#sep(padStart)}${str}`);
|
|
462
|
+
else this.#lines.unshift(str);
|
|
463
|
+
}
|
|
464
|
+
#sep(count) {
|
|
465
|
+
return " ".repeat(count);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/program/base.ts
|
|
470
|
+
var Cmd = class Cmd extends Command {
|
|
471
|
+
capabilities = [];
|
|
472
|
+
addCapabilities(capabilities) {
|
|
473
|
+
this.capabilities = capabilities;
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
addHelpTextAfter(ctx) {
|
|
477
|
+
super.addHelpText("after", () => {
|
|
478
|
+
const seeAlso = /* @__PURE__ */ new Set();
|
|
479
|
+
this.parent?.commands?.forEach((cmd) => {
|
|
480
|
+
if (cmd instanceof Cmd) {
|
|
481
|
+
if (cmd.capabilities.some((it) => this.capabilities.includes(it)) && cmd.name() !== this.name()) seeAlso.add(cmd.name());
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const poweredBy = /* @__PURE__ */ new Set();
|
|
485
|
+
this.capabilities.forEach((it) => {
|
|
486
|
+
const provider = ctx.plugins.providerOf(it);
|
|
487
|
+
if (provider) poweredBy.add(provider.plugin.ui);
|
|
488
|
+
});
|
|
489
|
+
const lines = new Lines();
|
|
490
|
+
if (seeAlso.size > 0) lines.add("See also:").add([...seeAlso].map((it) => `- ${it}`), 2);
|
|
491
|
+
if (seeAlso.size > 0 && poweredBy.size > 0) lines.newline();
|
|
492
|
+
if (poweredBy.size > 0) lines.add("Powered by:").add([...poweredBy].map((it) => `- ${it}`), 2);
|
|
493
|
+
if (lines.isEmpty()) return "";
|
|
494
|
+
return lines.newline(true).render();
|
|
495
|
+
});
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
addDoctorCommand(actionFn) {
|
|
499
|
+
const cmd = new Command("doctor").summary("check if the underlying tool is working correctly").action(actionFn);
|
|
500
|
+
return this.addCommand(cmd);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
function createCommand(name) {
|
|
504
|
+
return new Cmd(name);
|
|
272
505
|
}
|
|
273
506
|
//#endregion
|
|
274
507
|
//#region src/program/commands/check.ts
|
|
275
508
|
/**
|
|
276
|
-
* `rr check` runs
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
509
|
+
* `rr check` — runs jsc then tsc. Rather than dispatch through commander's
|
|
510
|
+
* command tree, it calls the same `jscAction`/`tscAction` directly — each
|
|
511
|
+
* wrapped in its own `runCheckSections` scope so failures are attributed by
|
|
512
|
+
* section name. A blank line separates the sections; `checkVerdict` is the
|
|
513
|
+
* final overall line.
|
|
281
514
|
*/
|
|
282
515
|
function createCheckCommand(ctx) {
|
|
283
|
-
return createCommand("check").
|
|
284
|
-
|
|
285
|
-
|
|
516
|
+
return createCommand("check").addCapabilities([
|
|
517
|
+
"lint",
|
|
518
|
+
"format",
|
|
519
|
+
"jscheck",
|
|
520
|
+
"typecheck"
|
|
521
|
+
]).summary("run static checks").description("Runs `rr jsc` then `rr tsc` in-process, each as its own section. Aggregates their exit codes — non-zero when either subcommand fails.").action(async () => {
|
|
522
|
+
const sections = [{
|
|
523
|
+
name: "jsc",
|
|
524
|
+
run: () => jscAction({
|
|
525
|
+
ctx,
|
|
526
|
+
checker: ctx.plugins.getJsChecker(),
|
|
527
|
+
options: {}
|
|
528
|
+
})
|
|
529
|
+
}, {
|
|
530
|
+
name: "tsc",
|
|
531
|
+
run: () => tscAction({
|
|
532
|
+
ctx,
|
|
533
|
+
tsc: ctx.plugins.getServiceOrThrow("typecheck")
|
|
534
|
+
})
|
|
535
|
+
}];
|
|
286
536
|
const start = Date.now();
|
|
287
537
|
const failed = [];
|
|
288
538
|
let rendered = false;
|
|
289
|
-
for (const
|
|
290
|
-
const cmd = findCommand(program, name);
|
|
291
|
-
if (!cmd) {
|
|
292
|
-
logger.error(`rr check: subcommand "${name}" is not registered.`);
|
|
293
|
-
failed.push(name);
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
539
|
+
for (const section of sections) {
|
|
296
540
|
if (rendered) process.stderr.write("\n");
|
|
297
541
|
let threw = false;
|
|
298
542
|
const results = await runCheckSections(async () => {
|
|
299
543
|
try {
|
|
300
|
-
await
|
|
544
|
+
await section.run();
|
|
301
545
|
} catch (reason) {
|
|
302
|
-
logger.error(`rr check (${name}): ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
546
|
+
logger.error(`rr check (${section.name}): ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
303
547
|
threw = true;
|
|
304
548
|
}
|
|
305
549
|
});
|
|
306
|
-
if (threw || results.some((r) => !r.ok)) failed.push(name);
|
|
550
|
+
if (threw || results.some((r) => !r.ok)) failed.push(section.name);
|
|
307
551
|
rendered = true;
|
|
308
552
|
}
|
|
309
553
|
process.stderr.write(`\n${checkVerdict(failed, Date.now() - start)}\n`);
|
|
310
554
|
if (failed.length > 0) process.exitCode = 1;
|
|
311
|
-
});
|
|
555
|
+
}).addHelpTextAfter(ctx);
|
|
312
556
|
}
|
|
313
557
|
function checkVerdict(failed, ms) {
|
|
314
558
|
const elapsed = palette.dim(ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`);
|
|
@@ -316,57 +560,41 @@ function checkVerdict(failed, ms) {
|
|
|
316
560
|
if (failed.length > 0) return `${palette.error("✖")} check failed${sep}${[...new Set(failed)].join(", ")}${sep}${elapsed}`;
|
|
317
561
|
return `${palette.success("✔")} check passed${sep}${elapsed}`;
|
|
318
562
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const tsc = ctx.registry.get("tsc");
|
|
332
|
-
const labels = [];
|
|
333
|
-
if (directJsc) labels.push(directJsc.ui);
|
|
334
|
-
else {
|
|
335
|
-
if (linter) labels.push(linter.ui);
|
|
336
|
-
if (formatter) labels.push(formatter.ui);
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/actions/clean.ts
|
|
565
|
+
async function cleanAction({ options }) {
|
|
566
|
+
async function run(paths, globOptions) {
|
|
567
|
+
if (options.dryRun) {
|
|
568
|
+
const toDelete = await glob(paths, globOptions);
|
|
569
|
+
logger.info("Paths that would be deleted: %O", toDelete);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
logger.start("Clean started");
|
|
573
|
+
await rimraf(paths, { glob: globOptions });
|
|
574
|
+
logger.success("Clean completed");
|
|
337
575
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
576
|
+
const BUILD_PATHS = ["**/dist"];
|
|
577
|
+
const ALL_DIRTY_PATHS = [
|
|
578
|
+
"**/.turbo",
|
|
579
|
+
"**/node_modules",
|
|
580
|
+
"pnpm-lock.yaml",
|
|
581
|
+
"bun.lock",
|
|
582
|
+
...BUILD_PATHS
|
|
583
|
+
];
|
|
584
|
+
if (options.onlyDist) await run(BUILD_PATHS, {
|
|
585
|
+
cwd,
|
|
586
|
+
ignore: ["**/node_modules/**"]
|
|
587
|
+
});
|
|
588
|
+
else await run(ALL_DIRTY_PATHS, { cwd });
|
|
341
589
|
}
|
|
342
590
|
//#endregion
|
|
343
591
|
//#region src/program/commands/clean.ts
|
|
592
|
+
const rimrafColor = colorize("#7C7270");
|
|
344
593
|
function createCleanCommand() {
|
|
345
|
-
return createCommand("clean").summary(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
logger.info("Paths that would be deleted: %O", toDelete);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
logger.start("Clean started");
|
|
353
|
-
await rimraf(paths, { glob: globOptions });
|
|
354
|
-
logger.success("Clean completed");
|
|
355
|
-
}
|
|
356
|
-
const BUILD_PATHS = ["**/dist"];
|
|
357
|
-
const ALL_DIRTY_PATHS = [
|
|
358
|
-
"**/.turbo",
|
|
359
|
-
"**/node_modules",
|
|
360
|
-
"pnpm-lock.yaml",
|
|
361
|
-
"bun.lock",
|
|
362
|
-
...BUILD_PATHS
|
|
363
|
-
];
|
|
364
|
-
if (options.onlyDist) await run(BUILD_PATHS, {
|
|
365
|
-
cwd,
|
|
366
|
-
ignore: ["**/node_modules/**"]
|
|
367
|
-
});
|
|
368
|
-
else await run(ALL_DIRTY_PATHS, { cwd });
|
|
369
|
-
}).addHelpText("afterAll", `\nUnder the hood, this command uses ${TOOL_LABELS.RIMRAF} to delete dirty folders or files.`);
|
|
594
|
+
return createCommand("clean").summary("delete dirty files").description("Deletes generated files and folders such as 'dist', 'node_modules', and lock files to ensure a clean state.").option("--only-dist", "delete 'dist' folders only").option("--dry-run", "outputs the paths that would be deleted").action((options) => cleanAction({ options: {
|
|
595
|
+
onlyDist: Boolean(options.onlyDist),
|
|
596
|
+
dryRun: Boolean(options.dryRun)
|
|
597
|
+
} })).addHelpText("after", `\nUnder the hood, this command uses ${rimrafColor("rimraf")} to delete dirty folders or files.`);
|
|
370
598
|
}
|
|
371
599
|
//#endregion
|
|
372
600
|
//#region src/program/commands/completion.ts
|
|
@@ -375,187 +603,298 @@ const SHELLS = [
|
|
|
375
603
|
"zsh",
|
|
376
604
|
"fish"
|
|
377
605
|
];
|
|
606
|
+
const usageColor = colorize("#24C55E");
|
|
378
607
|
function createCompletionCommand() {
|
|
379
|
-
return createCommand("completion").summary(`print shell completion script
|
|
608
|
+
return createCommand("completion").summary(`print shell completion script`).description(`Prints a shell completion script for rr. Add to your shell rc file:
|
|
380
609
|
|
|
381
610
|
bash: eval "$(rr completion bash)"
|
|
382
611
|
zsh: eval "$(rr completion zsh)"
|
|
383
|
-
fish: rr completion fish | source`).addArgument(new Argument("<shell>", `target shell`).choices(SHELLS)).addHelpText("afterAll", `\nUnder the hood, this command uses ${
|
|
612
|
+
fish: rr completion fish | source`).addArgument(new Argument("<shell>", `target shell`).choices(SHELLS)).addHelpText("afterAll", `\nUnder the hood, this command uses ${usageColor("usage")} (https://usage.jdx.dev).
|
|
384
613
|
Make sure to have it installed and available in your PATH.`);
|
|
385
614
|
}
|
|
386
615
|
//#endregion
|
|
616
|
+
//#region src/services/plugin-meta.ts
|
|
617
|
+
const require = createRequire(import.meta.url);
|
|
618
|
+
/** Resolves the installed version of a plugin's npm package from the host's `node_modules`. */
|
|
619
|
+
function readPluginMeta(pkgName, fromDir) {
|
|
620
|
+
const result = { pkgName };
|
|
621
|
+
const manifest = readPackageJson(pkgName, fromDir);
|
|
622
|
+
if (!manifest) return result;
|
|
623
|
+
result.pluginVersion = typeof manifest.version === "string" ? manifest.version : void 0;
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
function readPackageJson(pkgName, fromDir) {
|
|
627
|
+
try {
|
|
628
|
+
const resolved = require.resolve(`${pkgName}/package.json`, { paths: [fromDir] });
|
|
629
|
+
return JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
630
|
+
} catch {
|
|
631
|
+
try {
|
|
632
|
+
const found = findPackageJsonUpwards(require.resolve(pkgName, { paths: [fromDir] }), pkgName);
|
|
633
|
+
return found ? JSON.parse(fs.readFileSync(found, "utf8")) : void 0;
|
|
634
|
+
} catch {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function findPackageJsonUpwards(file, pkgName) {
|
|
640
|
+
let dir = path.dirname(file);
|
|
641
|
+
for (let i = 0; i < 12; i += 1) {
|
|
642
|
+
const candidate = path.join(dir, "package.json");
|
|
643
|
+
if (fs.existsSync(candidate)) try {
|
|
644
|
+
if (JSON.parse(fs.readFileSync(candidate, "utf8")).name === pkgName) return candidate;
|
|
645
|
+
} catch {}
|
|
646
|
+
const parent = path.dirname(dir);
|
|
647
|
+
if (parent === dir) break;
|
|
648
|
+
dir = parent;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/render/plugin-view.ts
|
|
653
|
+
/**
|
|
654
|
+
* The single source of truth for how a plugin renders across the UI. The two
|
|
655
|
+
* plugin screens (the root help footer and `rr config`) stay separate
|
|
656
|
+
* composing functions, but every painted plugin cell comes from here — so a
|
|
657
|
+
* color/dot/version-format change happens in one place.
|
|
658
|
+
*/
|
|
659
|
+
/** Path of `abs` relative to the host project root, or `abs` itself when already there. */
|
|
660
|
+
function relPath(ctx, abs) {
|
|
661
|
+
const rel = path.relative(ctx.appPkg.dirPath, abs);
|
|
662
|
+
return rel === "" ? abs : rel;
|
|
663
|
+
}
|
|
664
|
+
/** The plugin names present in the host's `run-run.config`, in config-file order. */
|
|
665
|
+
function configuredPlugins(ctx) {
|
|
666
|
+
return ctx.config.config.plugins ?? [];
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Partition every plugin name into installed (configured) vs available (not yet
|
|
670
|
+
* configured), preserving `PLUGINS_DIRECTORY` declaration order — the order the
|
|
671
|
+
* root footer renders both rows in.
|
|
672
|
+
*/
|
|
673
|
+
function partitionPlugins(ctx) {
|
|
674
|
+
const configured = configuredPlugins(ctx);
|
|
675
|
+
const present = Object.fromEntries(configured.map((it) => [it.name, true]));
|
|
676
|
+
return {
|
|
677
|
+
available: allPluginNames().filter((name) => !present[name]),
|
|
678
|
+
installed: configured
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/** `● <name>` — a configured plugin in the root footer. */
|
|
682
|
+
function installedCell(plugin) {
|
|
683
|
+
return `${plugin.color("●")} ${plugin.name}`;
|
|
684
|
+
}
|
|
685
|
+
/** `○ <name>` (dim) — an available-but-unconfigured plugin in the root footer. */
|
|
686
|
+
function availableCell(label) {
|
|
687
|
+
return `${palette.dim("○")} ${palette.dim(label)}`;
|
|
688
|
+
}
|
|
689
|
+
/** The npm package for a (kernel-trusted) plugin name — official lookup, else the `@rrlab/<name>-plugin` convention. */
|
|
690
|
+
function pkgOf(name) {
|
|
691
|
+
return isPluginName(name) ? PLUGINS_DIRECTORY[name].pkg : `@rrlab/${name}-plugin`;
|
|
692
|
+
}
|
|
693
|
+
/** The plugin's npm package name (dim) — `rr config` table column. */
|
|
694
|
+
function pkgCell(name) {
|
|
695
|
+
return palette.dim(pkgOf(name));
|
|
696
|
+
}
|
|
697
|
+
/** `v<plugin-version>` or `v?` — `rr config` table column. */
|
|
698
|
+
function pluginVersionCell(name, appDir) {
|
|
699
|
+
const { pluginVersion } = readPluginMeta(pkgOf(name), appDir);
|
|
700
|
+
return pluginVersion ? palette.success(`v${pluginVersion}`) : palette.dim("v?");
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/ui/theme.ts
|
|
704
|
+
const SEP = ` ${palette.dim("·")} `;
|
|
705
|
+
const runRunColor = colorize("#E8722A");
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/actions/config.ts
|
|
708
|
+
function configAction({ ctx }) {
|
|
709
|
+
const { meta } = ctx.config;
|
|
710
|
+
const lines = new Lines();
|
|
711
|
+
lines.add(palette.bold("Source:")).add(meta.filepath ? `${relPath(ctx, meta.filepath)}${SEP}${palette.dim(`loaded in ${Math.round(meta.loadMs)}ms`)}` : `${palette.dim("(no run-run.config — using defaults)")}`, 2).add(palette.bold("\nPlugins:"));
|
|
712
|
+
const plugins = configuredPlugins(ctx);
|
|
713
|
+
if (!plugins.length) lines.add(palette.dim("No plugins configured. Try `rr plugins add <name>`."), 2);
|
|
714
|
+
else {
|
|
715
|
+
const rows = plugins.map((p) => ({
|
|
716
|
+
name: `${p.color("●")} ${p.name}`,
|
|
717
|
+
pkg: pkgCell(p.name),
|
|
718
|
+
version: pluginVersionCell(p.name, ctx.appPkg.dirPath)
|
|
719
|
+
}));
|
|
720
|
+
lines.addTable(rows, [
|
|
721
|
+
{ key: "name" },
|
|
722
|
+
{
|
|
723
|
+
key: "pkg",
|
|
724
|
+
align: "right"
|
|
725
|
+
},
|
|
726
|
+
{ key: "version" }
|
|
727
|
+
], { padStart: 2 });
|
|
728
|
+
}
|
|
729
|
+
lines.printStdout();
|
|
730
|
+
}
|
|
731
|
+
//#endregion
|
|
387
732
|
//#region src/program/commands/config.ts
|
|
388
733
|
function createConfigCommand(ctx) {
|
|
389
|
-
return createCommand("config").summary("display the current config").description("Displays the current configuration settings, including their source file path
|
|
390
|
-
const { config, meta } = ctx.config;
|
|
391
|
-
console.log(palette.muted("Config:"));
|
|
392
|
-
console.log(config);
|
|
393
|
-
console.log(palette.muted(`Loaded from ${meta.filepath ? palette.link(meta.filepath) : "n/a"}`));
|
|
394
|
-
});
|
|
734
|
+
return createCommand("config").summary("display the current config").description("Displays the current configuration settings, including their source file path and the plugins it registers.").action(() => configAction({ ctx }));
|
|
395
735
|
}
|
|
396
736
|
//#endregion
|
|
397
|
-
//#region src/
|
|
398
|
-
const PLUGIN_KINDS = [
|
|
399
|
-
"lint",
|
|
400
|
-
"format",
|
|
401
|
-
"jsc",
|
|
402
|
-
"tsc",
|
|
403
|
-
"pack"
|
|
404
|
-
];
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/program/commands/doctor.ts
|
|
737
|
+
//#region src/actions/doctor.ts
|
|
407
738
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
* is wired correctly. Renders the canonical `doctor (<tool>) · <pkg>` row like
|
|
411
|
-
* every other single-target command — `doctor()` returns a `RunReport`.
|
|
739
|
+
* A single tool's `doctor` as one board row (`doctor (<tool>) · <pkg>`) — used
|
|
740
|
+
* by the `doctor` subcommand each plugin-backed command exposes.
|
|
412
741
|
*/
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
if (!(await runBoard([reportTask(targetLabel("doctor", service, appPkg), () => service.doctor())])).ok) process.exitCode = 1;
|
|
416
|
-
});
|
|
742
|
+
async function doctorOneAction({ ctx, service }) {
|
|
743
|
+
if (!(await runBoard([reportTask(targetLabel("doctor", service, ctx.appPkg), () => service.doctor())])).ok) process.exitCode = 1;
|
|
417
744
|
}
|
|
418
745
|
/**
|
|
419
746
|
* Top-level `rr doctor` — runs the `doctor()` of every distinct capability
|
|
420
747
|
* impl registered with the kernel. Distinct because a single plugin (e.g.
|
|
421
|
-
* biome) often serves multiple kinds (`lint`, `format`, `
|
|
748
|
+
* biome) often serves multiple kinds (`lint`, `format`, `jscheck`) from the same
|
|
422
749
|
* `BiomeService` instance; running its doctor three times is wasteful.
|
|
423
750
|
*/
|
|
751
|
+
async function doctorAction({ ctx }) {
|
|
752
|
+
const services = ctx.plugins.getAllServices();
|
|
753
|
+
if (services.length === 0) {
|
|
754
|
+
logger.info("No plugins configured. Use `rr plugins add <name>` to install one.");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!(await runBoard(services.map((svc) => reportTask(svc.ui, () => svc.doctor())), { title: fanoutTitle("doctor", void 0, services.length, "tools") })).ok) process.exitCode = 1;
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/program/commands/doctor.ts
|
|
424
761
|
function createDoctorCommand(ctx) {
|
|
425
|
-
return createCommand("doctor").summary("run all plugin doctors").description("Runs the `doctor()` of every configured plugin capability. Each plugin reports ok / not working. The exit code is non-zero if any reports not working.").action(
|
|
426
|
-
const services = collectDistinctDoctors(ctx);
|
|
427
|
-
if (services.length === 0) {
|
|
428
|
-
logger.info("No plugins configured. Use `rr plugins add <name>` to install one.");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (!(await runBoard(services.map((svc) => reportTask(svc.ui, () => svc.doctor())), { title: fanoutTitle("doctor", void 0, services.length, "tools") })).ok) process.exitCode = 1;
|
|
432
|
-
});
|
|
762
|
+
return createCommand("doctor").summary("run all plugin doctors").description("Runs the `doctor()` of every configured plugin capability. Each plugin reports ok / not working. The exit code is non-zero if any reports not working.").action(() => doctorAction({ ctx }));
|
|
433
763
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return
|
|
764
|
+
//#endregion
|
|
765
|
+
//#region src/actions/format.ts
|
|
766
|
+
function formatAction({ ctx, formatter, options }) {
|
|
767
|
+
return runToolAction({
|
|
768
|
+
ctx,
|
|
769
|
+
name: "format",
|
|
770
|
+
provider: formatter,
|
|
771
|
+
run: (p) => p.format(options)
|
|
772
|
+
});
|
|
438
773
|
}
|
|
439
774
|
//#endregion
|
|
440
775
|
//#region src/program/commands/format.ts
|
|
441
776
|
function createFormatCommand(ctx) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
777
|
+
return createCommand("format").addCapabilities(["format"]).summary("check & fix format issues").description("Checks the code for formatting issues and optionally fixes them, ensuring it adheres to the defined style standards.").option("--fix", "format all the code").action(async (options = {}) => {
|
|
778
|
+
await formatAction({
|
|
779
|
+
ctx,
|
|
780
|
+
formatter: ctx.plugins.getServiceOrThrow("format"),
|
|
781
|
+
options: { fix: options.fix }
|
|
782
|
+
});
|
|
783
|
+
}).addHelpTextAfter(ctx).addDoctorCommand(async () => {
|
|
784
|
+
await doctorOneAction({
|
|
785
|
+
ctx,
|
|
786
|
+
service: ctx.plugins.getServiceOrThrow("format")
|
|
451
787
|
});
|
|
452
788
|
});
|
|
453
|
-
if (formatter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${formatter.ui} CLI to format the code.`);
|
|
454
|
-
return cmd;
|
|
455
|
-
}
|
|
456
|
-
//#endregion
|
|
457
|
-
//#region src/program/composed-jsc.ts
|
|
458
|
-
/**
|
|
459
|
-
* Synthesises the `jsc` capability (`StaticChecker & Doctor`) by composing a
|
|
460
|
-
* separately-registered linter and formatter — used when the plugin set
|
|
461
|
-
* provides `lint` and `format` independently (e.g. oxc) but no plugin claims
|
|
462
|
-
* `jsc`. Runs lint then format sequentially (parallel stdout interleaves badly)
|
|
463
|
-
* and merges their reports into one board row.
|
|
464
|
-
*/
|
|
465
|
-
function composedJscProvider(linter, formatter) {
|
|
466
|
-
return {
|
|
467
|
-
bin: `${linter.bin}+${formatter.bin}`,
|
|
468
|
-
ui: `${linter.ui} + ${formatter.ui}`,
|
|
469
|
-
async check({ fix }) {
|
|
470
|
-
const lintReport = await linter.lint({ fix });
|
|
471
|
-
const formatReport = await formatter.format({ fix });
|
|
472
|
-
return mergeReports([{
|
|
473
|
-
ui: linter.ui,
|
|
474
|
-
report: lintReport
|
|
475
|
-
}, {
|
|
476
|
-
ui: formatter.ui,
|
|
477
|
-
report: formatReport
|
|
478
|
-
}]);
|
|
479
|
-
},
|
|
480
|
-
async doctor() {
|
|
481
|
-
const [lintRes, fmtRes] = await Promise.all([linter.doctor(), formatter.doctor()]);
|
|
482
|
-
return mergeReports([{
|
|
483
|
-
ui: linter.ui,
|
|
484
|
-
report: lintRes
|
|
485
|
-
}, {
|
|
486
|
-
ui: formatter.ui,
|
|
487
|
-
report: fmtRes
|
|
488
|
-
}]);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Folds the lint + format reports into one so the composed `jsc` renders as a
|
|
494
|
-
* single board row: ok only when both passed, with each tool's output kept
|
|
495
|
-
* under its own header so the flushed detail stays attributable.
|
|
496
|
-
*/
|
|
497
|
-
function mergeReports(parts) {
|
|
498
|
-
const sections = parts.filter((part) => part.report.output.trim()).map((part) => `${part.ui}:\n${part.report.output}`).join("\n\n");
|
|
499
|
-
return {
|
|
500
|
-
ok: parts.every((part) => part.report.ok),
|
|
501
|
-
output: sections
|
|
502
|
-
};
|
|
503
789
|
}
|
|
504
790
|
//#endregion
|
|
505
791
|
//#region src/program/commands/jscheck.ts
|
|
506
792
|
function createJsCheckCommand(ctx) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
793
|
+
return createCommand("jsc").alias("jscheck").addCapabilities([
|
|
794
|
+
"lint",
|
|
795
|
+
"format",
|
|
796
|
+
"jscheck"
|
|
797
|
+
]).summary("check format and lint").description("Checks the code for formatting and linting issues, ensuring it adheres to the defined style and quality standards.").option("--fix", "try to fix issues automatically").option("--fix-staged", "try to fix staged files only").action(async (options = {}) => {
|
|
798
|
+
await jscAction({
|
|
799
|
+
ctx,
|
|
800
|
+
checker: ctx.plugins.getJsChecker(),
|
|
801
|
+
options: {
|
|
802
|
+
fix: options.fix,
|
|
803
|
+
fixStaged: options.fixStaged
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}).addHelpTextAfter(ctx).addDoctorCommand(async () => {
|
|
807
|
+
await doctorOneAction({
|
|
808
|
+
ctx,
|
|
809
|
+
service: ctx.plugins.getJsChecker()
|
|
519
810
|
});
|
|
520
811
|
});
|
|
521
|
-
|
|
522
|
-
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/actions/lint.ts
|
|
815
|
+
function lintAction({ ctx, linter, options }) {
|
|
816
|
+
return runToolAction({
|
|
817
|
+
ctx,
|
|
818
|
+
name: "lint",
|
|
819
|
+
provider: linter,
|
|
820
|
+
run: (p) => p.lint(options)
|
|
821
|
+
});
|
|
523
822
|
}
|
|
524
823
|
//#endregion
|
|
525
824
|
//#region src/program/commands/lint.ts
|
|
526
825
|
function createLintCommand(ctx) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
826
|
+
return createCommand("lint").addCapabilities(["lint"]).summary("check & fix lint issues").description("Checks the code for linting issues and optionally fixes them, ensuring it adheres to the defined quality standards.").option("-c, --check", "check if the code is valid", true).option("--fix", "try to fix all the code").action(async (options = {}) => {
|
|
827
|
+
await lintAction({
|
|
828
|
+
ctx,
|
|
829
|
+
linter: ctx.plugins.getServiceOrThrow("lint"),
|
|
830
|
+
options: { fix: options.fix }
|
|
831
|
+
});
|
|
832
|
+
}).addHelpTextAfter(ctx).addDoctorCommand(async () => {
|
|
833
|
+
await doctorOneAction({
|
|
834
|
+
ctx,
|
|
835
|
+
service: ctx.plugins.getServiceOrThrow("lint")
|
|
536
836
|
});
|
|
537
837
|
});
|
|
538
|
-
|
|
539
|
-
|
|
838
|
+
}
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/actions/pack.ts
|
|
841
|
+
function packAction({ ctx, packer }) {
|
|
842
|
+
return runToolAction({
|
|
843
|
+
ctx,
|
|
844
|
+
name: "pack",
|
|
845
|
+
provider: packer,
|
|
846
|
+
run: (p) => p.pack()
|
|
847
|
+
});
|
|
540
848
|
}
|
|
541
849
|
//#endregion
|
|
542
850
|
//#region src/program/commands/pack.ts
|
|
543
851
|
function createPackCommand(ctx) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
kind: "pack",
|
|
554
|
-
provider: packer,
|
|
555
|
-
run: (p) => p.pack()
|
|
852
|
+
return createCommand("pack").addCapabilities(["pack"]).summary("pack a ts library").description("Compiles TypeScript code into JavaScript and generates type declaration files, packaging the library for distribution.").action(async () => {
|
|
853
|
+
await packAction({
|
|
854
|
+
ctx,
|
|
855
|
+
packer: ctx.plugins.getServiceOrThrow("pack")
|
|
856
|
+
});
|
|
857
|
+
}).addHelpTextAfter(ctx).addDoctorCommand(async () => {
|
|
858
|
+
await doctorOneAction({
|
|
859
|
+
ctx,
|
|
860
|
+
service: ctx.plugins.getServiceOrThrow("pack")
|
|
556
861
|
});
|
|
557
862
|
});
|
|
558
|
-
|
|
863
|
+
}
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/errors/invalid-plugin-module.ts
|
|
866
|
+
/** Thrown when an installed plugin package doesn't export a default factory function. */
|
|
867
|
+
var InvalidPluginModuleError = class extends Error {
|
|
868
|
+
constructor(pkgName) {
|
|
869
|
+
super(`Plugin '${pkgName}' did not export a default factory function.`);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/errors/unknown-plugin.ts
|
|
874
|
+
var UnknownPluginError = class extends Error {
|
|
875
|
+
constructor(name) {
|
|
876
|
+
super(`'${name}' is invalid for argument 'name'. Allowed choices are ${allPluginNames().join(", ")}.`);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/render/steps.ts
|
|
881
|
+
/**
|
|
882
|
+
* Runs `fn` under a clack spinner: the message stays on success, and is
|
|
883
|
+
* suffixed with `— failed` (error level) before the error re-throws. Used by
|
|
884
|
+
* the interactive `plugins add/remove` flows to frame each install/uninstall
|
|
885
|
+
* step.
|
|
886
|
+
*/
|
|
887
|
+
async function withSpinner(message, fn) {
|
|
888
|
+
const sp = clack.spinner();
|
|
889
|
+
sp.start(message);
|
|
890
|
+
try {
|
|
891
|
+
const result = await fn();
|
|
892
|
+
sp.stop(message);
|
|
893
|
+
return result;
|
|
894
|
+
} catch (err) {
|
|
895
|
+
sp.stop(`${message} — failed`, 1);
|
|
896
|
+
throw err;
|
|
897
|
+
}
|
|
559
898
|
}
|
|
560
899
|
//#endregion
|
|
561
900
|
//#region src/services/config-ast.ts
|
|
@@ -797,40 +1136,97 @@ function deepEqual(a, b) {
|
|
|
797
1136
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
798
1137
|
}
|
|
799
1138
|
//#endregion
|
|
800
|
-
//#region src/services/
|
|
1139
|
+
//#region src/services/file-ops.ts
|
|
801
1140
|
/**
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
* Third-party plugins use their full package name; the binding is derived
|
|
807
|
-
* by stripping a `@scope/<tool>-plugin` (or `@scope/<tool>-run-run-plugin`)
|
|
808
|
-
* suffix and using the tool segment.
|
|
1141
|
+
* Applies a single declarative `FileOp` under `cwd`. `force` overrides the
|
|
1142
|
+
* `create` overwrite guard. Idempotent for `delete` (absent file is a no-op)
|
|
1143
|
+
* and skips edits when the target file is missing.
|
|
809
1144
|
*/
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1145
|
+
async function applyFileOp(cwd, op, force) {
|
|
1146
|
+
const abs = path.join(cwd, op.path);
|
|
1147
|
+
if (op.kind === "create") {
|
|
1148
|
+
const exists = await pathExists(abs);
|
|
1149
|
+
if (exists && !op.overwrite && !force) return {
|
|
1150
|
+
op: "create",
|
|
1151
|
+
path: op.path,
|
|
1152
|
+
status: "skipped-exists"
|
|
1153
|
+
};
|
|
1154
|
+
await fs$1.mkdir(path.dirname(abs), { recursive: true });
|
|
1155
|
+
await fs$1.writeFile(abs, op.content, "utf8");
|
|
1156
|
+
return {
|
|
1157
|
+
op: "create",
|
|
1158
|
+
path: op.path,
|
|
1159
|
+
status: exists ? "overwritten" : "created"
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (op.kind === "edit-json") {
|
|
1163
|
+
if (!await pathExists(abs)) return {
|
|
1164
|
+
op: "edit",
|
|
1165
|
+
path: op.path,
|
|
1166
|
+
status: "missing"
|
|
1167
|
+
};
|
|
1168
|
+
const source = await fs$1.readFile(abs, "utf8");
|
|
1169
|
+
const next = applyJsonEdits(source, op.edits);
|
|
1170
|
+
if (next === source) return {
|
|
1171
|
+
op: "edit",
|
|
1172
|
+
path: op.path,
|
|
1173
|
+
status: "unchanged"
|
|
1174
|
+
};
|
|
1175
|
+
await fs$1.writeFile(abs, next, "utf8");
|
|
1176
|
+
return {
|
|
1177
|
+
op: "edit",
|
|
1178
|
+
path: op.path,
|
|
1179
|
+
status: "edited"
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
if (op.kind === "edit-text") {
|
|
1183
|
+
if (!await pathExists(abs)) return {
|
|
1184
|
+
op: "edit",
|
|
1185
|
+
path: op.path,
|
|
1186
|
+
status: "missing"
|
|
1187
|
+
};
|
|
1188
|
+
const source = await fs$1.readFile(abs, "utf8");
|
|
1189
|
+
const next = op.edit(source);
|
|
1190
|
+
if (next === source) return {
|
|
1191
|
+
op: "edit",
|
|
1192
|
+
path: op.path,
|
|
1193
|
+
status: "unchanged"
|
|
1194
|
+
};
|
|
1195
|
+
await fs$1.writeFile(abs, next, "utf8");
|
|
1196
|
+
return {
|
|
1197
|
+
op: "edit",
|
|
1198
|
+
path: op.path,
|
|
1199
|
+
status: "edited"
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
if (!await pathExists(abs)) return {
|
|
1203
|
+
op: "delete",
|
|
1204
|
+
path: op.path,
|
|
1205
|
+
status: "absent"
|
|
1206
|
+
};
|
|
1207
|
+
await fs$1.unlink(abs);
|
|
1208
|
+
return {
|
|
1209
|
+
op: "delete",
|
|
1210
|
+
path: op.path,
|
|
1211
|
+
status: "deleted"
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
/** A one-line, side-effect-free description of a `FileOp` for the remove plan. */
|
|
1215
|
+
function describeFileOp(op) {
|
|
1216
|
+
switch (op.kind) {
|
|
1217
|
+
case "create": return `${op.overwrite ? "Overwrite" : "Create"} ${op.path}`;
|
|
1218
|
+
case "edit-json": return `Edit ${op.path} (${op.edits.length} change${op.edits.length === 1 ? "" : "s"})`;
|
|
1219
|
+
case "edit-text": return `Edit ${op.path}`;
|
|
1220
|
+
case "delete": return `Delete ${op.path}`;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
async function pathExists(p) {
|
|
1224
|
+
try {
|
|
1225
|
+
await fs$1.access(p);
|
|
1226
|
+
return true;
|
|
1227
|
+
} catch {
|
|
1228
|
+
return false;
|
|
830
1229
|
}
|
|
831
|
-
};
|
|
832
|
-
function officialAliases() {
|
|
833
|
-
return Object.keys(OFFICIAL_PLUGINS);
|
|
834
1230
|
}
|
|
835
1231
|
//#endregion
|
|
836
1232
|
//#region src/services/prompts.ts
|
|
@@ -923,42 +1319,69 @@ function pmNeedsRootFlag(pm) {
|
|
|
923
1319
|
return false;
|
|
924
1320
|
}
|
|
925
1321
|
//#endregion
|
|
926
|
-
//#region src/
|
|
927
|
-
function
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1322
|
+
//#region src/actions/plugins/shared.ts
|
|
1323
|
+
function hasInPackageJson(ctx, pkgName) {
|
|
1324
|
+
const pkg = ctx.appPkg.packageJson;
|
|
1325
|
+
return pkgName in {
|
|
1326
|
+
...pkg.dependencies,
|
|
1327
|
+
...pkg.devDependencies,
|
|
1328
|
+
...pkg.peerDependencies
|
|
1329
|
+
};
|
|
933
1330
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1331
|
+
/** Renders a `FileOpOutcome` (from the `file-ops` engine) as the matching clack log line. */
|
|
1332
|
+
function reportFileOp(outcome) {
|
|
1333
|
+
switch (outcome.status) {
|
|
1334
|
+
case "skipped-exists":
|
|
1335
|
+
clack.log.warn(`Skipping ${outcome.path} — already exists. Use --force to overwrite.`);
|
|
1336
|
+
return;
|
|
1337
|
+
case "created":
|
|
1338
|
+
clack.log.success(`Created ${outcome.path}`);
|
|
1339
|
+
return;
|
|
1340
|
+
case "overwritten":
|
|
1341
|
+
clack.log.success(`Overwrote ${outcome.path}`);
|
|
1342
|
+
return;
|
|
1343
|
+
case "missing":
|
|
1344
|
+
clack.log.warn(`Skipping ${outcome.path} — file does not exist.`);
|
|
1345
|
+
return;
|
|
1346
|
+
case "edited":
|
|
1347
|
+
clack.log.success(`Edited ${outcome.path}`);
|
|
1348
|
+
return;
|
|
1349
|
+
case "unchanged":
|
|
1350
|
+
clack.log.info(`No changes for ${outcome.path}.`);
|
|
1351
|
+
return;
|
|
1352
|
+
case "deleted":
|
|
1353
|
+
clack.log.success(`Deleted ${outcome.path}`);
|
|
1354
|
+
return;
|
|
1355
|
+
case "absent": return;
|
|
946
1356
|
}
|
|
947
|
-
logger.info(`${rel}:`);
|
|
948
|
-
for (const name of plugins) logger.info(` - ${name}`);
|
|
949
1357
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/actions/plugins/add.ts
|
|
1360
|
+
/** Split a `<name>[@<spec>]` input (e.g. `biome@pr-226`) into the plugin name and optional spec. */
|
|
1361
|
+
function parsePluginSpec(input) {
|
|
1362
|
+
const at = input.indexOf("@");
|
|
1363
|
+
if (at <= 0) return { name: input };
|
|
1364
|
+
return {
|
|
1365
|
+
name: input.slice(0, at),
|
|
1366
|
+
spec: input.slice(at + 1)
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/** A dist-tag starts with a letter and contains only safe identifier chars. Version ranges (`^0.1`, `>=1`, `0.0.2`, `*`) don't match. */
|
|
1370
|
+
function isDistTag(spec) {
|
|
1371
|
+
return /^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(spec) && spec !== "latest";
|
|
1372
|
+
}
|
|
1373
|
+
async function addPluginAction({ ctx, args, options }) {
|
|
1374
|
+
const { name: pluginName, spec } = parsePluginSpec(args.name);
|
|
1375
|
+
if (!(pluginName in PLUGINS_DIRECTORY)) throw new UnknownPluginError(pluginName);
|
|
1376
|
+
const { pkg: pkgName, name: binding } = PLUGINS_DIRECTORY[pluginName];
|
|
954
1377
|
const tag = spec && isDistTag(spec) ? spec : void 0;
|
|
955
1378
|
const installSpec = spec ? `${pkgName}@${spec}` : pkgName;
|
|
956
|
-
clack.intro(` rr plugins add ${name} `);
|
|
1379
|
+
clack.intro(` rr plugins add ${args.name} `);
|
|
957
1380
|
const inPkg = hasInPackageJson(ctx, pkgName);
|
|
958
1381
|
const ast = new ConfigAstService();
|
|
959
1382
|
const loaded = await ast.load(ctx.appPkg.dirPath);
|
|
960
|
-
const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod,
|
|
961
|
-
if (inPkg && inConfig && !
|
|
1383
|
+
const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, binding);
|
|
1384
|
+
if (inPkg && inConfig && !options.force && !spec) {
|
|
962
1385
|
clack.log.warn(`${pkgName} is already installed and configured. Use --force to re-run install.`);
|
|
963
1386
|
clack.outro("Nothing to do.");
|
|
964
1387
|
return;
|
|
@@ -968,12 +1391,12 @@ async function runAdd(ctx, name, opts) {
|
|
|
968
1391
|
const workspace = toNypmWorkspace(wsChoice);
|
|
969
1392
|
const targetLabel = describeWorkspaceChoice(wsChoice);
|
|
970
1393
|
const willInstall = !inPkg || !!spec;
|
|
971
|
-
if (
|
|
1394
|
+
if (options.dryRun) {
|
|
972
1395
|
const presence = willInstall ? inPkg ? " (already present, will be updated to this spec)" : "" : " (already present, skipped)";
|
|
973
1396
|
clack.log.info(`Would: install ${installSpec} as a devDependency in ${targetLabel}${presence}.`);
|
|
974
1397
|
if (!inConfig) {
|
|
975
1398
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
976
|
-
clack.log.info(`Would: add ${
|
|
1399
|
+
clack.log.info(`Would: add ${binding}() to ${rel} (plugins[]).`);
|
|
977
1400
|
}
|
|
978
1401
|
clack.log.info("Would: run the plugin's install() hook (if any) to fetch peer deps and create files.");
|
|
979
1402
|
clack.outro("Dry run complete.");
|
|
@@ -994,7 +1417,7 @@ async function runAdd(ctx, name, opts) {
|
|
|
994
1417
|
let installResult;
|
|
995
1418
|
try {
|
|
996
1419
|
const factory = (await import(pkgName)).default;
|
|
997
|
-
if (typeof factory !== "function") throw new
|
|
1420
|
+
if (typeof factory !== "function") throw new InvalidPluginModuleError(pkgName);
|
|
998
1421
|
const plugin = factory();
|
|
999
1422
|
if (plugin.install) {
|
|
1000
1423
|
const installCtx = {
|
|
@@ -1003,9 +1426,9 @@ async function runAdd(ctx, name, opts) {
|
|
|
1003
1426
|
appPkg: ctx.appPkg,
|
|
1004
1427
|
prompts: createClackPrompts(),
|
|
1005
1428
|
flags: {
|
|
1006
|
-
force: !!
|
|
1007
|
-
yes: !!
|
|
1008
|
-
nonInteractive: !!
|
|
1429
|
+
force: !!options.force,
|
|
1430
|
+
yes: !!options.yes,
|
|
1431
|
+
nonInteractive: !!options.yes
|
|
1009
1432
|
},
|
|
1010
1433
|
release: new ReleaseService(tag)
|
|
1011
1434
|
};
|
|
@@ -1033,24 +1456,45 @@ async function runAdd(ctx, name, opts) {
|
|
|
1033
1456
|
});
|
|
1034
1457
|
});
|
|
1035
1458
|
}
|
|
1036
|
-
for (const op of installResult?.files ?? []) await applyFileOp(ctx.appPkg.dirPath, op, !!
|
|
1459
|
+
for (const op of installResult?.files ?? []) reportFileOp(await applyFileOp(ctx.appPkg.dirPath, op, !!options.force));
|
|
1037
1460
|
if (!inConfig) {
|
|
1038
1461
|
ast.addPlugin(loaded.mod, {
|
|
1039
|
-
exportName,
|
|
1462
|
+
exportName: binding,
|
|
1040
1463
|
pkgName
|
|
1041
1464
|
});
|
|
1042
1465
|
await ast.save(loaded);
|
|
1043
1466
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
1044
1467
|
clack.log.success(`Updated ${rel}`);
|
|
1045
1468
|
}
|
|
1046
|
-
clack.outro(`Plugin '${
|
|
1469
|
+
clack.outro(`Plugin '${pluginName}' ready 🎉`);
|
|
1047
1470
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1471
|
+
//#endregion
|
|
1472
|
+
//#region src/actions/plugins/list.ts
|
|
1473
|
+
async function listPluginsAction({ ctx }) {
|
|
1051
1474
|
const ast = new ConfigAstService();
|
|
1052
1475
|
const loaded = await ast.load(ctx.appPkg.dirPath);
|
|
1053
|
-
|
|
1476
|
+
if (loaded.isNew) {
|
|
1477
|
+
logger.info("No run-run.config.{ts,mts} found. Use `rr plugins add <name>` to start.");
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const plugins = ast.listPlugins(loaded.mod);
|
|
1481
|
+
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
1482
|
+
if (plugins.length === 0) {
|
|
1483
|
+
logger.info(`${rel}: no plugins configured.`);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
logger.info(`${rel}:`);
|
|
1487
|
+
for (const name of plugins) logger.info(` - ${name}`);
|
|
1488
|
+
}
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/actions/plugins/remove.ts
|
|
1491
|
+
async function removePluginAction({ ctx, args, options }) {
|
|
1492
|
+
const { name } = args;
|
|
1493
|
+
const { pkg: pkgName, name: binding } = PLUGINS_DIRECTORY[name];
|
|
1494
|
+
clack.intro(` rr plugins remove ${name} `);
|
|
1495
|
+
const ast = new ConfigAstService();
|
|
1496
|
+
const loaded = await ast.load(ctx.appPkg.dirPath);
|
|
1497
|
+
const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, binding);
|
|
1054
1498
|
let uninstallResult;
|
|
1055
1499
|
if (hasInPackageJson(ctx, pkgName)) try {
|
|
1056
1500
|
const factory = (await import(pkgName)).default;
|
|
@@ -1061,8 +1505,8 @@ async function runRemove(ctx, alias, opts) {
|
|
|
1061
1505
|
appPkg: ctx.appPkg,
|
|
1062
1506
|
prompts: createClackPrompts(),
|
|
1063
1507
|
flags: {
|
|
1064
|
-
yes: !!
|
|
1065
|
-
nonInteractive: !!
|
|
1508
|
+
yes: !!options.yes,
|
|
1509
|
+
nonInteractive: !!options.yes
|
|
1066
1510
|
}
|
|
1067
1511
|
});
|
|
1068
1512
|
} catch (err) {
|
|
@@ -1071,7 +1515,7 @@ async function runRemove(ctx, alias, opts) {
|
|
|
1071
1515
|
const planSteps = [];
|
|
1072
1516
|
if (inConfig) {
|
|
1073
1517
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
1074
|
-
planSteps.push(`Remove ${
|
|
1518
|
+
planSteps.push(`Remove ${binding}() from ${rel}`);
|
|
1075
1519
|
}
|
|
1076
1520
|
for (const op of uninstallResult?.files ?? []) planSteps.push(describeFileOp(op));
|
|
1077
1521
|
const depsToRemove = (uninstallResult?.removeDependencies ?? []).filter((dep) => hasInPackageJson(ctx, dep));
|
|
@@ -1081,16 +1525,16 @@ async function runRemove(ctx, alias, opts) {
|
|
|
1081
1525
|
const workspace = toNypmWorkspace(wsChoice);
|
|
1082
1526
|
if (depsToRemove.length > 0) planSteps.push(`Uninstall: ${depsToRemove.join(", ")} (from ${describeWorkspaceChoice(wsChoice)})`);
|
|
1083
1527
|
if (planSteps.length === 0) {
|
|
1084
|
-
clack.log.warn(`Plugin '${
|
|
1528
|
+
clack.log.warn(`Plugin '${name}' is not installed nor configured.`);
|
|
1085
1529
|
clack.outro("Nothing to do.");
|
|
1086
1530
|
return;
|
|
1087
1531
|
}
|
|
1088
1532
|
clack.log.message(`Plan:\n${planSteps.map((s) => ` • ${s}`).join("\n")}`);
|
|
1089
|
-
if (
|
|
1533
|
+
if (options.dryRun) {
|
|
1090
1534
|
clack.outro("Dry run complete.");
|
|
1091
1535
|
return;
|
|
1092
1536
|
}
|
|
1093
|
-
if (!
|
|
1537
|
+
if (!options.yes) {
|
|
1094
1538
|
const choice = await clack.confirm({
|
|
1095
1539
|
message: "Proceed?",
|
|
1096
1540
|
initialValue: false
|
|
@@ -1100,12 +1544,12 @@ async function runRemove(ctx, alias, opts) {
|
|
|
1100
1544
|
return;
|
|
1101
1545
|
}
|
|
1102
1546
|
}
|
|
1103
|
-
for (const op of uninstallResult?.files ?? []) await applyFileOp(ctx.appPkg.dirPath, op, true);
|
|
1547
|
+
for (const op of uninstallResult?.files ?? []) reportFileOp(await applyFileOp(ctx.appPkg.dirPath, op, true));
|
|
1104
1548
|
if (inConfig) {
|
|
1105
|
-
ast.removePlugin(loaded.mod,
|
|
1549
|
+
ast.removePlugin(loaded.mod, binding);
|
|
1106
1550
|
await ast.save(loaded);
|
|
1107
1551
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
1108
|
-
clack.log.success(`Removed ${
|
|
1552
|
+
clack.log.success(`Removed ${binding}() from ${rel}`);
|
|
1109
1553
|
}
|
|
1110
1554
|
for (const dep of depsToRemove) await withSpinner(`Uninstalling ${dep}`, async () => {
|
|
1111
1555
|
await removeDependency(dep, {
|
|
@@ -1114,166 +1558,106 @@ async function runRemove(ctx, alias, opts) {
|
|
|
1114
1558
|
workspace
|
|
1115
1559
|
});
|
|
1116
1560
|
});
|
|
1117
|
-
clack.outro(`Plugin '${
|
|
1561
|
+
clack.outro(`Plugin '${name}' removed.`);
|
|
1118
1562
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/program/commands/plugins.ts
|
|
1565
|
+
function createPluginsCommand(ctx) {
|
|
1566
|
+
const cmd = createCommand("plugins").description(`manage ${runRunColor("@rrlab")} plugins`);
|
|
1567
|
+
cmd.command("list").description("list plugins configured in run-run.config.{ts,mts}").action(() => listPluginsAction({ ctx }));
|
|
1568
|
+
cmd.command("add").description("install and configure an @rrlab plugin").addArgument(new Argument("<name>", `plugin alias (${allPluginNames().join("|")}), optionally with @<spec> e.g. biome@pr-226`)).option("--force", "re-run install even if the plugin is already configured").option("--yes", "skip prompts and use defaults (non-interactive)").option("--dry-run", "show what would happen, without applying changes").action((name, options) => addPluginAction({
|
|
1569
|
+
ctx,
|
|
1570
|
+
args: { name },
|
|
1571
|
+
options
|
|
1572
|
+
}));
|
|
1573
|
+
cmd.command("remove").description("uninstall an @rrlab plugin and undo its config files + deps").addArgument(new Argument("<name>", "plugin alias to remove").choices(allPluginNames())).option("--yes", "skip the confirmation prompt").option("--dry-run", "print the plan without applying changes").action((name, options) => removePluginAction({
|
|
1574
|
+
ctx,
|
|
1575
|
+
args: { name },
|
|
1576
|
+
options
|
|
1577
|
+
}));
|
|
1578
|
+
return cmd;
|
|
1126
1579
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/program/commands/tscheck.ts
|
|
1582
|
+
function createTsCheckCommand(ctx) {
|
|
1583
|
+
return createCommand("tsc").alias("tscheck").addCapabilities(["typecheck"]).summary("check types errors").description("Checks type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.").action(async () => {
|
|
1584
|
+
await tscAction({
|
|
1585
|
+
ctx,
|
|
1586
|
+
tsc: ctx.plugins.getServiceOrThrow("typecheck")
|
|
1587
|
+
});
|
|
1588
|
+
}).addHelpTextAfter(ctx).addDoctorCommand(async () => {
|
|
1589
|
+
await doctorOneAction({
|
|
1590
|
+
ctx,
|
|
1591
|
+
service: ctx.plugins.getServiceOrThrow("typecheck")
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1130
1594
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1595
|
+
//#endregion
|
|
1596
|
+
//#region src/render/banner.ts
|
|
1597
|
+
function getBannerText(version) {
|
|
1598
|
+
const UI_LOGO = runRunColor(`
|
|
1599
|
+
██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
1600
|
+
██╔══██╗██║ ██║████╗ ██║ ██╔══██╗██║ ██║████╗ ██║
|
|
1601
|
+
██████╔╝██║ ██║██╔██╗ ██║█████╗██████╔╝██║ ██║██╔██╗ ██║
|
|
1602
|
+
██╔══██╗██║ ██║██║╚██╗██║╚════╝██╔══██╗██║ ██║██║╚██╗██║
|
|
1603
|
+
██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
|
|
1604
|
+
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ${text.version(version)}`.trimStart());
|
|
1605
|
+
return new Lines().add(UI_LOGO).newline().add(`🦊 ${palette.italic(palette.dim("The CLI toolbox for"))} ${text.vland}`).newline().render();
|
|
1138
1606
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
}
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/render/footer.ts
|
|
1609
|
+
function getFooterText(ctx) {
|
|
1610
|
+
const { installed, available } = partitionPlugins(ctx);
|
|
1611
|
+
const installedLine = installed.map(installedCell).join(" ");
|
|
1612
|
+
const availableLine = available.map(availableCell).join(" ");
|
|
1613
|
+
const fromLine = ctx.config.meta.filepath ? palette.dim(`from ${relPath(ctx, ctx.config.meta.filepath)}`) : palette.dim("(no run-run.config — using defaults)");
|
|
1614
|
+
const lines = new Lines();
|
|
1615
|
+
lines.newline().add(palette.bold("Plugins:"));
|
|
1616
|
+
if (installed.length > 0) lines.add(`${palette.bold("installed:")} ${installedLine}${SEP}${fromLine}`, 2);
|
|
1617
|
+
else lines.add(`${palette.bold("installed:")} ${palette.dim("(none)")}${SEP}${fromLine}`, 2);
|
|
1618
|
+
if (available.length > 0) lines.add(`${palette.bold("available:")} ${availableLine}${SEP}${palette.dim("install with `rr plugins add <name>`")}`, 2);
|
|
1619
|
+
return lines.render();
|
|
1150
1620
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/program/root.ts
|
|
1623
|
+
var RunRunCmd = class extends Command {
|
|
1624
|
+
ctx;
|
|
1625
|
+
constructor(ctx) {
|
|
1626
|
+
super("rr");
|
|
1627
|
+
this.ctx = ctx;
|
|
1628
|
+
this.#init();
|
|
1157
1629
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const abs = path.join(cwd, op.path);
|
|
1161
|
-
if (op.kind === "create") {
|
|
1162
|
-
const exists = await pathExists(abs);
|
|
1163
|
-
if (exists && !op.overwrite && !force) {
|
|
1164
|
-
clack.log.warn(`Skipping ${op.path} — already exists. Use --force to overwrite.`);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
await fs$1.mkdir(path.dirname(abs), { recursive: true });
|
|
1168
|
-
await fs$1.writeFile(abs, op.content, "utf8");
|
|
1169
|
-
clack.log.success(`${exists ? "Overwrote" : "Created"} ${op.path}`);
|
|
1170
|
-
return;
|
|
1630
|
+
async run() {
|
|
1631
|
+
await this.parseAsync();
|
|
1171
1632
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
clack.log.warn(`Skipping ${op.path} — file does not exist.`);
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
const source = await fs$1.readFile(abs, "utf8");
|
|
1178
|
-
const next = applyJsonEdits(source, op.edits);
|
|
1179
|
-
if (next !== source) {
|
|
1180
|
-
await fs$1.writeFile(abs, next, "utf8");
|
|
1181
|
-
clack.log.success(`Edited ${op.path}`);
|
|
1182
|
-
} else clack.log.info(`No changes for ${op.path}.`);
|
|
1183
|
-
return;
|
|
1633
|
+
#init() {
|
|
1634
|
+
this.enablePositionalOptions().showSuggestionAfterError(true).helpCommand(false).version(this.ctx.binPkg.version, "-v, --version", "output the version number").addHelpText("before", this.#banner()).addHelpText("after", this.#footer()).option("--about", "show credits & inspiration").option("--usage", `print KDL spec for this CLI (${palette.dim(palette.link("https://kdl.dev"))})`).on("option:about", () => this.#aboutStdout()).on("option:usage", () => this.#usageStdout(this));
|
|
1184
1635
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
const source = await fs$1.readFile(abs, "utf8");
|
|
1191
|
-
const next = op.edit(source);
|
|
1192
|
-
if (next !== source) {
|
|
1193
|
-
await fs$1.writeFile(abs, next, "utf8");
|
|
1194
|
-
clack.log.success(`Edited ${op.path}`);
|
|
1195
|
-
} else clack.log.info(`No changes for ${op.path}.`);
|
|
1196
|
-
return;
|
|
1636
|
+
#aboutStdout() {
|
|
1637
|
+
new Lines().add(this.#banner()).add(palette.bold("Inspired by:"), 2).add(`kcd-scripts — ${palette.link("https://github.com/kentcdodds/kcd-scripts")}`, 4).newline().add(palette.bold("Named in honor of:"), 2).add(`Run Run (Peruvian news segment) — ${palette.link("https://es.wikipedia.org/wiki/Run_Run")}`, 4).printStdout();
|
|
1638
|
+
process.exit(0);
|
|
1197
1639
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
clack.log.success(`Deleted ${op.path}`);
|
|
1202
|
-
return;
|
|
1640
|
+
#usageStdout(cmd) {
|
|
1641
|
+
generateToStdout(cmd);
|
|
1642
|
+
process.exit(0);
|
|
1203
1643
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
try {
|
|
1207
|
-
await fs$1.access(p);
|
|
1208
|
-
return true;
|
|
1209
|
-
} catch {
|
|
1210
|
-
return false;
|
|
1644
|
+
#banner() {
|
|
1645
|
+
return getBannerText(this.ctx.binPkg.version);
|
|
1211
1646
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
function createTsCheckCommand(ctx) {
|
|
1217
|
-
const { appPkg, shell } = ctx;
|
|
1218
|
-
const tsc = ctx.registry.get("tsc");
|
|
1219
|
-
const cmd = createCommand("tsc").alias("tscheck").summary(`check typescript errors${pluginAnnotation(tsc)}`).description("Checks the TypeScript code for type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.");
|
|
1220
|
-
if (tsc) {
|
|
1221
|
-
cmd.addCommand(createDoctorSubcommand(tsc, ctx.appPkg));
|
|
1222
|
-
cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
|
|
1223
|
-
}
|
|
1224
|
-
cmd.action(async () => {
|
|
1225
|
-
if (!tsc) throw missingPluginError("tsc");
|
|
1226
|
-
const isTsProject = (dir) => appPkg.hasFile("tsconfig.json", dir);
|
|
1227
|
-
const typecheckTask = (label, dir, scripts) => reportTask(label, async () => {
|
|
1228
|
-
const preScript = getPreScript(scripts);
|
|
1229
|
-
if (preScript) {
|
|
1230
|
-
const pre = await shell.at(dir).runCaptured(preScript, [], {
|
|
1231
|
-
shell: true,
|
|
1232
|
-
throwOnError: false
|
|
1233
|
-
});
|
|
1234
|
-
if ((pre.exitCode ?? 0) !== 0) return {
|
|
1235
|
-
ok: false,
|
|
1236
|
-
output: `pre-script \`${preScript}\` failed\n${[pre.stdout, pre.stderr].map((s) => s?.trim()).filter(Boolean).join("\n")}`
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
return tsc.check({ cwd: dir });
|
|
1240
|
-
});
|
|
1241
|
-
if (!appPkg.isMonorepo()) {
|
|
1242
|
-
if (!isTsProject(appPkg.dirPath)) {
|
|
1243
|
-
logger.info("No tsconfig.json found, skipping typecheck");
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
if (!(await runBoard([typecheckTask(targetLabel("tsc", tsc, appPkg), appPkg.dirPath, appPkg.packageJson.scripts)])).ok) process.exitCode = 1;
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
const tsProjects = (await appPkg.getWorkspaceProjects()).filter((project) => isTsProject(project.rootDir));
|
|
1250
|
-
if (!tsProjects.length) {
|
|
1251
|
-
logger.warn("No ts projects found in the monorepo, skipping typecheck");
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
if (!(await runBoard(tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts)), { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") })).ok) process.exitCode = 1;
|
|
1255
|
-
});
|
|
1256
|
-
return cmd;
|
|
1257
|
-
}
|
|
1647
|
+
#footer() {
|
|
1648
|
+
return getFooterText(this.ctx);
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1258
1651
|
//#endregion
|
|
1259
1652
|
//#region src/program/index.ts
|
|
1260
|
-
async function createProgram(
|
|
1261
|
-
const ctx = await
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
generateToStdout(this);
|
|
1266
|
-
process.exit(0);
|
|
1267
|
-
}).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)),
|
|
1268
|
-
ctx
|
|
1269
|
-
};
|
|
1653
|
+
async function createProgram(meta) {
|
|
1654
|
+
const ctx = await new ContextService(path.dirname(dirnameOf(meta))).getContext();
|
|
1655
|
+
const cmd = new RunRunCmd(ctx);
|
|
1656
|
+
cmd.commandsGroup("Code quality:").addCommand(createCheckCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).commandsGroup("Build:").addCommand(createPackCommand(ctx)).commandsGroup("Maintenance:").addCommand(createCleanCommand()).addCommand(createDoctorCommand(ctx)).commandsGroup("Meta:").addCommand(createCompletionCommand()).addCommand(createPluginsCommand(ctx)).addCommand(createConfigCommand(ctx));
|
|
1657
|
+
return cmd;
|
|
1270
1658
|
}
|
|
1271
1659
|
//#endregion
|
|
1272
1660
|
//#region src/run.ts
|
|
1273
|
-
|
|
1274
|
-
await run(async () => {
|
|
1275
|
-
const { program } = await createProgram({ binDir: BIN_DIR });
|
|
1276
|
-
await program.parseAsync(process.argv, { from: "node" });
|
|
1277
|
-
}, logger);
|
|
1661
|
+
await (await createProgram(import.meta)).run();
|
|
1278
1662
|
//#endregion
|
|
1279
1663
|
export {};
|