@rrlab/cli 1.0.1-git-908d2c0.0 → 1.1.1-git-4903a88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/bin +3 -5
  2. package/dist/cli.usage.kdl +26 -25
  3. package/dist/config.d.mts +1 -1
  4. package/dist/magic-string.es-BgIV5Mu3.mjs +1011 -0
  5. package/dist/plugin/__tests__/bin-probe.test.d.mts +1 -0
  6. package/dist/plugin/__tests__/bin-probe.test.mjs +64 -0
  7. package/dist/plugin/__tests__/decide-scaffold.test.d.mts +1 -0
  8. package/dist/plugin/__tests__/decide-scaffold.test.mjs +103 -0
  9. package/dist/plugin/__tests__/define-plugin.test.d.mts +1 -0
  10. package/dist/plugin/__tests__/define-plugin.test.mjs +130 -0
  11. package/dist/plugin/__tests__/pick-preset.test.d.mts +1 -0
  12. package/dist/plugin/__tests__/pick-preset.test.mjs +72 -0
  13. package/dist/plugin/__tests__/registry.test.d.mts +1 -0
  14. package/dist/plugin/__tests__/registry.test.mjs +104 -0
  15. package/dist/plugin/bin-probe.d.mts +4 -0
  16. package/dist/plugin/bin-probe.mjs +22 -0
  17. package/dist/plugin/decide-scaffold.d.mts +18 -0
  18. package/dist/plugin/decide-scaffold.mjs +36 -0
  19. package/dist/plugin/define-plugin.d.mts +17 -0
  20. package/dist/plugin/define-plugin.mjs +25 -0
  21. package/dist/plugin/directory.d.mts +47 -0
  22. package/dist/plugin/directory.mjs +45 -0
  23. package/dist/plugin/errors.d.mts +11 -0
  24. package/dist/plugin/errors.mjs +15 -0
  25. package/dist/plugin/index.d.mts +7 -0
  26. package/dist/plugin/index.mjs +50 -0
  27. package/dist/plugin/pick-preset.d.mts +13 -0
  28. package/dist/plugin/pick-preset.mjs +17 -0
  29. package/dist/plugin/registry.d.mts +19 -0
  30. package/dist/plugin/registry.mjs +2 -0
  31. package/dist/plugin/tool-service.d.mts +45 -0
  32. package/dist/plugin/tool-service.mjs +64 -0
  33. package/dist/plugin/types.d.mts +3 -0
  34. package/dist/plugin/types.mjs +1 -0
  35. package/dist/registry-BgqfKK5L.mjs +55 -0
  36. package/dist/run.mjs +969 -585
  37. package/dist/test.DNmyFkvJ-09ScyH13.mjs +13617 -0
  38. package/dist/tool-DKL6TauZ.d.mts +43 -0
  39. package/dist/{types-snfbujDH.d.mts → types-Iu4IyWof.d.mts} +11 -75
  40. package/package.json +7 -6
  41. package/src/actions/clean.ts +36 -0
  42. package/src/actions/config.ts +46 -0
  43. package/src/actions/doctor.ts +47 -0
  44. package/src/actions/format.ts +13 -0
  45. package/src/actions/jsc.ts +13 -0
  46. package/src/actions/lint.ts +13 -0
  47. package/src/actions/pack.ts +12 -0
  48. package/src/actions/plugins/add.ts +143 -0
  49. package/src/actions/plugins/list.ts +27 -0
  50. package/src/actions/plugins/remove.ts +110 -0
  51. package/src/actions/plugins/shared.ts +58 -0
  52. package/src/actions/run-tool.ts +23 -0
  53. package/src/actions/tsc.ts +65 -0
  54. package/src/errors/invalid-plugin-module.ts +6 -0
  55. package/src/errors/missing-plugin.ts +17 -0
  56. package/src/errors/plugin-api-version.ts +6 -0
  57. package/src/errors/unknown-plugin.ts +7 -0
  58. package/src/lib/plugin/define-plugin.ts +56 -0
  59. package/src/lib/plugin/directory.ts +30 -0
  60. package/src/lib/plugin/errors.ts +15 -0
  61. package/src/lib/{plugin.ts → plugin/index.ts} +8 -9
  62. package/src/lib/plugin/registry.ts +82 -0
  63. package/src/{plugin → lib/plugin}/tool-service.ts +10 -14
  64. package/src/{plugin → lib/plugin}/types.ts +10 -33
  65. package/src/program/base.ts +75 -0
  66. package/src/program/commands/check.ts +31 -62
  67. package/src/program/commands/clean.ts +12 -43
  68. package/src/program/commands/completion.ts +6 -4
  69. package/src/program/commands/config.ts +6 -11
  70. package/src/program/commands/doctor.ts +5 -54
  71. package/src/program/commands/format.ts +18 -25
  72. package/src/program/commands/jscheck.ts +18 -31
  73. package/src/program/commands/lint.ts +18 -26
  74. package/src/program/commands/pack.ts +18 -22
  75. package/src/program/commands/plugins.ts +17 -364
  76. package/src/program/commands/tscheck.ts +19 -77
  77. package/src/program/index.ts +20 -27
  78. package/src/program/root.ts +62 -0
  79. package/src/render/banner.ts +25 -0
  80. package/src/render/board.ts +41 -0
  81. package/src/render/footer.ts +31 -0
  82. package/src/render/labels.ts +28 -0
  83. package/src/render/lines.ts +100 -0
  84. package/src/render/plugin-view.ts +68 -0
  85. package/src/render/steps.ts +20 -0
  86. package/src/run.ts +2 -8
  87. package/src/services/config.ts +4 -0
  88. package/src/services/context.ts +84 -0
  89. package/src/services/file-ops.ts +79 -0
  90. package/src/services/json-edit.ts +1 -1
  91. package/src/services/plugin-meta.ts +63 -0
  92. package/src/services/plugin-services.ts +41 -0
  93. package/src/services/prompts.ts +1 -1
  94. package/src/services/static-checker.ts +46 -0
  95. package/src/types/config.ts +2 -1
  96. package/src/types/tool.ts +13 -26
  97. package/src/ui/theme.ts +5 -0
  98. package/dist/plugin.d.mts +0 -87
  99. package/dist/plugin.mjs +0 -214
  100. package/src/plugin/define-plugin.ts +0 -54
  101. package/src/plugin/registry.ts +0 -48
  102. package/src/program/board.ts +0 -86
  103. package/src/program/composed-jsc.ts +0 -43
  104. package/src/program/missing-plugin.ts +0 -18
  105. package/src/program/ui.ts +0 -59
  106. package/src/services/ctx.ts +0 -71
  107. package/src/services/plugins-registry.ts +0 -22
  108. /package/src/{plugin → lib/plugin}/bin-probe.ts +0 -0
  109. /package/src/{plugin → lib/plugin}/decide-scaffold.ts +0 -0
  110. /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, run, runTaskBoard, text } from "@vlandoss/clibuddy";
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
- //#region src/plugin/registry.ts
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, capabilities) {
97
+ register(plugin, services) {
20
98
  this.#entries.push({
21
99
  plugin,
22
- capabilities
100
+ services
23
101
  });
24
102
  }
25
- get(kind) {
26
- const providers = this.#providersOf(kind);
27
- const [first, ...rest] = providers;
28
- if (!first) return void 0;
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
- providersOf(kind) {
37
- return this.#providersOf(kind).map(({ plugin, impl }) => ({
38
- name: plugin.name,
39
- impl
40
- }));
108
+ getService(capability) {
109
+ return this.providerOf(capability)?.service;
41
110
  }
42
- plugins() {
43
- return this.#entries.map(({ plugin }) => plugin);
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
- #providersOf(kind) {
46
- const out = [];
47
- for (const { plugin, capabilities } of this.#entries) {
48
- const impl = capabilities[kind];
49
- if (impl != null) out.push({
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
- impl
122
+ service
52
123
  });
53
124
  }
54
- return out;
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/ctx.ts
103
- async function createContext(binDir) {
104
- const debug = logger.subdebug("create-context");
105
- const binPath = fs.realpathSync(binDir);
106
- debug("bin path:", binPath);
107
- debug("process cwd:", process.cwd());
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
- return {
132
- appPkg,
133
- binPkg,
134
- shell,
135
- config,
136
- registry
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/program/missing-plugin.ts
141
- const SUGGESTIONS = {
142
- lint: [
143
- "biome",
144
- "oxc",
145
- "eslint"
146
- ],
147
- format: ["biome", "oxc"],
148
- jsc: ["biome"],
149
- tsc: ["ts"],
150
- pack: ["tsdown"]
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/program/board.ts
160
- /** `<command> (<tool>)`, deduped to just `<command>` when the tool's binary is the command itself (e.g. `tsc`). */
161
- function commandTool(command, provider) {
162
- return provider.bin === command ? command : `${command} (${provider.ui})`;
163
- }
164
- function pkgName(appPkg) {
165
- return appPkg.packageJson.name ?? basename(appPkg.dirPath);
166
- }
167
- /** The canonical single-target row label, `<command> (<tool>) · <package>`, so every command reads alike. */
168
- function targetLabel(command, provider, appPkg) {
169
- return `${commandTool(command, provider)} ${palette.dim(`· ${pkgName(appPkg)}`)}`;
170
- }
171
- /**
172
- * The canonical fan-out section title, `<command> (<tool>) · <n> <unit>`. The
173
- * tool is omitted when the fan-out spans several tools (`rr doctor` → `doctor ·
174
- * 3 tools`), since the rows then carry the per-tool name.
175
- */
176
- function fanoutTitle(command, provider, count, unit) {
177
- return `${provider ? commandTool(command, provider) : command} · ${count} ${unit}`;
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 shared action body for a single-provider tool command (lint, format, jsc,
215
- * pack): require the provider, run its verb as one board row labelled
216
- * `<name> (<tool>) · <pkg>`, and aggregate the exit code. Commands that fan out
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
- async function runToolCommand(ctx, spec) {
220
- const { provider } = spec;
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/program/ui.ts
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
- * Labels used by kernel-internal commands. Plugin-owned tools (biome, oxc,
237
- * tsdown, tsc) define their own colored labels inside each plugin's
238
- * `src/index.ts`.
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
- const TOOL_LABELS = {
241
- RIMRAF: rimrafColor("rimraf"),
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
- function getBannerText(version) {
261
- return `
262
- ${runRunColor(`
263
- ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
264
- ██╔══██╗██║ ██║████╗ ██║ ██╔══██╗██║ ██║████╗ ██║
265
- ██████╔╝██║ ██║██╔██╗ ██║█████╗██████╔╝██║ ██║██╔██╗ ██║
266
- ██╔══██╗██║ ██║██║╚██╗██║╚════╝██╔══██╗██║ ██║██║╚██╗██║
267
- ██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
268
- ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ${text.version(version)}
269
- `.trim())}
270
-
271
- 🦊 ${palette.italic(palette.muted("The CLI toolbox for"))} ${text.vland}\n`.trimStart();
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 `jsc` then `tsc`. Rather than keep a parallel action
277
- * registry, it reuses commander's command tree: it finds each sibling on
278
- * `this.parent` and runs it via `parseAsync([])`, which applies the sibling's
279
- * own option defaults. (`this` is the running command inside a non-arrow
280
- * action see cli/CLAUDE.md.)
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").summary(`run static checks${checkAnnotation(ctx)}`).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 function checkAction() {
284
- const program = this.parent;
285
- if (!program) throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
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 name of ["jsc", "tsc"]) {
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 cmd.parseAsync([], { from: "user" });
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
- function findCommand(program, name) {
320
- return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
321
- }
322
- /**
323
- * Flattens the underlying tool labels of `jsc` + `tsc` for the help summary —
324
- * e.g. `(biome, oxlint)`, deduped, not `(biome + biome, oxlint)`. Falls back to
325
- * the standard `(not configured)` when neither sibling has a provider.
326
- */
327
- function checkAnnotation(ctx) {
328
- const directJsc = ctx.registry.get("jsc");
329
- const linter = ctx.registry.get("lint");
330
- const formatter = ctx.registry.get("format");
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
- if (tsc) labels.push(tsc.ui);
339
- if (labels.length === 0) return pluginAnnotation(void 0);
340
- return pluginAnnotation({ ui: [...new Set(labels)].join(", ") });
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(`delete dirty files (${TOOL_LABELS.RIMRAF})`).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(async function cleanCommandAction(options) {
346
- async function run(paths, globOptions) {
347
- if (options.dryRun) {
348
- const toDelete = await glob(paths, globOptions);
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 (${TOOL_LABELS.USAGE})`).description(`Prints a shell completion script for rr. Add to your shell rc file:
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 ${TOOL_LABELS.USAGE} (https://usage.jdx.dev).
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 if available.").action(async function configAction() {
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/plugin/types.ts
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
- * Subcommand factory used by every plugin-backed command (lint, format, jsc,
409
- * tsc, pack) to expose a `doctor` subcommand that verifies the underlying tool
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 createDoctorSubcommand(service, appPkg) {
414
- return createCommand("doctor").summary("check if the underlying tool is working correctly").action(async function doctorAction() {
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`, `jsc`) from the same
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(async () => {
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
- function collectDistinctDoctors(ctx) {
435
- const seen = /* @__PURE__ */ new Set();
436
- for (const kind of PLUGIN_KINDS) for (const { impl } of ctx.registry.providersOf(kind)) seen.add(impl);
437
- return [...seen];
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
- const formatter = ctx.registry.get("format");
443
- const cmd = createCommand("format").summary(`check & fix format errors${pluginAnnotation(formatter)}`).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");
444
- if (formatter) cmd.addCommand(createDoctorSubcommand(formatter, ctx.appPkg));
445
- cmd.action(async (options = {}) => {
446
- await runToolCommand(ctx, {
447
- name: "format",
448
- kind: "format",
449
- provider: formatter,
450
- run: (p) => p.format(options)
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
- const direct = ctx.registry.get("jsc");
508
- const linter = ctx.registry.get("lint");
509
- const formatter = ctx.registry.get("format");
510
- const checker = direct ?? (linter && formatter ? composedJscProvider(linter, formatter) : void 0);
511
- const cmd = createCommand("jsc").alias("jscheck").summary(`check format and lint${pluginAnnotation(checker)}`).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");
512
- if (checker) cmd.addCommand(createDoctorSubcommand(checker, ctx.appPkg));
513
- cmd.action(async (options = {}) => {
514
- await runToolCommand(ctx, {
515
- name: "jsc",
516
- kind: "jsc",
517
- provider: checker,
518
- run: (p) => p.check(options)
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
- if (checker) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${checker.ui} CLI to check the code.`);
522
- return cmd;
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
- const linter = ctx.registry.get("lint");
528
- const cmd = createCommand("lint").summary(`check & fix lint errors${pluginAnnotation(linter)}`).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");
529
- if (linter) cmd.addCommand(createDoctorSubcommand(linter, ctx.appPkg));
530
- cmd.action(async (options = {}) => {
531
- await runToolCommand(ctx, {
532
- name: "lint",
533
- kind: "lint",
534
- provider: linter,
535
- run: (p) => p.lint(options)
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
- if (linter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${linter.ui} CLI to lint the code.`);
539
- return cmd;
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
- const packer = ctx.registry.get("pack");
545
- const cmd = createCommand("pack").summary(`pack a ts library${pluginAnnotation(packer)}`).description("Compiles TypeScript code into JavaScript and generates type declaration files, packaging the library for distribution.");
546
- if (packer) {
547
- cmd.addCommand(createDoctorSubcommand(packer, ctx.appPkg));
548
- cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${packer.ui} CLI to pack the project.`);
549
- }
550
- cmd.action(async () => {
551
- await runToolCommand(ctx, {
552
- name: "pack",
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
- return cmd;
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/plugins-registry.ts
1139
+ //#region src/services/file-ops.ts
801
1140
  /**
802
- * Map from short aliases the user types (`rr plugins add ts`) to the
803
- * full npm package name and the canonical local binding the plugin file
804
- * is imported as inside `run-run.config.{ts,mts}`.
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
- const OFFICIAL_PLUGINS = {
811
- ts: {
812
- pkg: "@rrlab/ts-plugin",
813
- exportName: "ts"
814
- },
815
- eslint: {
816
- pkg: "@rrlab/eslint-plugin",
817
- exportName: "eslint"
818
- },
819
- biome: {
820
- pkg: "@rrlab/biome-plugin",
821
- exportName: "biome"
822
- },
823
- oxc: {
824
- pkg: "@rrlab/oxc-plugin",
825
- exportName: "oxc"
826
- },
827
- tsdown: {
828
- pkg: "@rrlab/tsdown-plugin",
829
- exportName: "tsdown"
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/program/commands/plugins.ts
927
- function createPluginsCommand(ctx) {
928
- const cmd = createCommand("plugins").description("manage @rrlab plugins");
929
- cmd.command("list").description("list plugins configured in run-run.config.{ts,mts}").action(() => runList(ctx));
930
- cmd.command("add").description("install and configure an @rrlab plugin").addArgument(new Argument("<name>", `plugin alias (${officialAliases().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, opts) => runAdd(ctx, name, opts));
931
- cmd.command("remove").description("uninstall an @rrlab plugin and undo its config files + deps").addArgument(new Argument("<name>", "plugin alias to remove").choices(officialAliases())).option("--yes", "skip the confirmation prompt").option("--dry-run", "print the plan without applying changes").action((name, opts) => runRemove(ctx, name, opts));
932
- return cmd;
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
- async function runList(ctx) {
935
- const ast = new ConfigAstService();
936
- const loaded = await ast.load(ctx.appPkg.dirPath);
937
- if (loaded.isNew) {
938
- logger.info("No run-run.config.{ts,mts} found. Use `rr plugins add <name>` to start.");
939
- return;
940
- }
941
- const plugins = ast.listPlugins(loaded.mod);
942
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
943
- if (plugins.length === 0) {
944
- logger.info(`${rel}: no plugins configured.`);
945
- return;
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
- async function runAdd(ctx, name, opts) {
951
- const { alias, spec } = parseAliasSpec(name);
952
- if (!(alias in OFFICIAL_PLUGINS)) throw new Error(`'${alias}' is invalid for argument 'name'. Allowed choices are ${officialAliases().join(", ")}.`);
953
- const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
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, exportName);
961
- if (inPkg && inConfig && !opts.force && !spec) {
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 (opts.dryRun) {
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 ${exportName}() to ${rel} (plugins[]).`);
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 Error(`Plugin '${pkgName}' did not export a default factory function.`);
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: !!opts.force,
1007
- yes: !!opts.yes,
1008
- nonInteractive: !!opts.yes
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, !!opts.force);
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 '${alias}' ready 🎉`);
1469
+ clack.outro(`Plugin '${pluginName}' ready 🎉`);
1047
1470
  }
1048
- async function runRemove(ctx, alias, opts) {
1049
- const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
1050
- clack.intro(` rr plugins remove ${alias} `);
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
- const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, exportName);
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: !!opts.yes,
1065
- nonInteractive: !!opts.yes
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 ${exportName}() from ${rel}`);
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 '${alias}' is not installed nor configured.`);
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 (opts.dryRun) {
1533
+ if (options.dryRun) {
1090
1534
  clack.outro("Dry run complete.");
1091
1535
  return;
1092
1536
  }
1093
- if (!opts.yes) {
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, exportName);
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 ${exportName}() from ${rel}`);
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 '${alias}' removed.`);
1561
+ clack.outro(`Plugin '${name}' removed.`);
1118
1562
  }
1119
- function parseAliasSpec(input) {
1120
- const at = input.indexOf("@");
1121
- if (at <= 0) return { alias: input };
1122
- return {
1123
- alias: input.slice(0, at),
1124
- spec: input.slice(at + 1)
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
- /** A dist-tag starts with a letter and contains only safe identifier chars. Version ranges (`^0.1`, `>=1`, `0.0.2`, `*`) don't match. */
1128
- function isDistTag(spec) {
1129
- return /^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(spec) && spec !== "latest";
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
- function hasInPackageJson(ctx, pkgName) {
1132
- const pkg = ctx.appPkg.packageJson;
1133
- return pkgName in {
1134
- ...pkg.dependencies,
1135
- ...pkg.devDependencies,
1136
- ...pkg.peerDependencies
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
- async function withSpinner(message, fn) {
1140
- const sp = clack.spinner();
1141
- sp.start(message);
1142
- try {
1143
- const result = await fn();
1144
- sp.stop(message);
1145
- return result;
1146
- } catch (err) {
1147
- sp.stop(`${message} — failed`, 1);
1148
- throw err;
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
- function describeFileOp(op) {
1152
- switch (op.kind) {
1153
- case "create": return `${op.overwrite ? "Overwrite" : "Create"} ${op.path}`;
1154
- case "edit-json": return `Edit ${op.path} (${op.edits.length} change${op.edits.length === 1 ? "" : "s"})`;
1155
- case "edit-text": return `Edit ${op.path}`;
1156
- case "delete": return `Delete ${op.path}`;
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
- async function applyFileOp(cwd, op, force) {
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
- if (op.kind === "edit-json") {
1173
- if (!await pathExists(abs)) {
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
- if (op.kind === "edit-text") {
1186
- if (!await pathExists(abs)) {
1187
- clack.log.warn(`Skipping ${op.path} — file does not exist.`);
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
- if (op.kind === "delete") {
1199
- if (!await pathExists(abs)) return;
1200
- await fs$1.unlink(abs);
1201
- clack.log.success(`Deleted ${op.path}`);
1202
- return;
1640
+ #usageStdout(cmd) {
1641
+ generateToStdout(cmd);
1642
+ process.exit(0);
1203
1643
  }
1204
- }
1205
- async function pathExists(p) {
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
- //#endregion
1214
- //#region src/program/commands/tscheck.ts
1215
- const getPreScript = (scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
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(options) {
1261
- const ctx = await createContext(options.binDir);
1262
- const version = ctx.binPkg.version;
1263
- return {
1264
- 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() {
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
- const BIN_DIR = path.dirname(dirnameOf(import.meta));
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 {};