@rrlab/cli 0.0.1-git-87d22db.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 (43) hide show
  1. package/README.md +110 -0
  2. package/bin +54 -0
  3. package/dist/cli.usage.kdl +71 -0
  4. package/dist/config.d.mts +11 -0
  5. package/dist/config.mjs +6 -0
  6. package/dist/plugin.d.mts +52 -0
  7. package/dist/plugin.mjs +65 -0
  8. package/dist/run.mjs +1137 -0
  9. package/dist/types-C3V27_kd.d.mts +173 -0
  10. package/package.json +74 -0
  11. package/src/lib/config.ts +5 -0
  12. package/src/lib/plugin.ts +31 -0
  13. package/src/plugin/define-plugin.ts +5 -0
  14. package/src/plugin/registry.ts +47 -0
  15. package/src/plugin/tool-service.ts +77 -0
  16. package/src/plugin/types.ts +139 -0
  17. package/src/program/commands/check.ts +63 -0
  18. package/src/program/commands/clean.ts +53 -0
  19. package/src/program/commands/completion.ts +26 -0
  20. package/src/program/commands/config.ts +15 -0
  21. package/src/program/commands/doctor.ts +85 -0
  22. package/src/program/commands/format.ts +35 -0
  23. package/src/program/commands/jscheck.ts +44 -0
  24. package/src/program/commands/lint.ts +37 -0
  25. package/src/program/commands/pack.ts +27 -0
  26. package/src/program/commands/plugins.ts +359 -0
  27. package/src/program/commands/tscheck.ts +112 -0
  28. package/src/program/composed-jsc.ts +35 -0
  29. package/src/program/index.ts +50 -0
  30. package/src/program/missing-plugin.ts +18 -0
  31. package/src/program/ui.ts +59 -0
  32. package/src/run.ts +11 -0
  33. package/src/services/config-ast.ts +202 -0
  34. package/src/services/config.ts +54 -0
  35. package/src/services/ctx.ts +72 -0
  36. package/src/services/json-edit.ts +147 -0
  37. package/src/services/logger.ts +5 -0
  38. package/src/services/plugins-registry.ts +21 -0
  39. package/src/services/prompts.ts +26 -0
  40. package/src/services/workspace-target.ts +27 -0
  41. package/src/types/config.ts +13 -0
  42. package/src/types/tool.ts +57 -0
  43. package/tsconfig.json +3 -0
package/dist/run.mjs ADDED
@@ -0,0 +1,1137 @@
1
+ import path from "node:path";
2
+ import { colorize, createPkg, createShellService, cwd, dirnameOf, palette, run, text } from "@vlandoss/clibuddy";
3
+ import { generateToStdout } from "@usage-spec/commander";
4
+ import { Argument, Option, createCommand } from "commander";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import { lilconfig } from "lilconfig";
8
+ import { createLoggy } from "@vlandoss/loggy";
9
+ import { glob } from "glob";
10
+ import { rimraf } from "rimraf";
11
+ import fs$1 from "node:fs/promises";
12
+ import * as clack from "@clack/prompts";
13
+ import { addDependency, detectPackageManager, removeDependency } from "nypm";
14
+ import { builders, generateCode, loadFile, parseModule, writeFile } from "magicast";
15
+ import * as cjson from "comment-json";
16
+ //#region src/plugin/registry.ts
17
+ var PluginRegistry = class {
18
+ #entries = [];
19
+ register(plugin, capabilities) {
20
+ this.#entries.push({
21
+ plugin,
22
+ capabilities
23
+ });
24
+ }
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
+ throw new Error(`Multiple plugins provide capability '${kind}': ${names}. Disambiguate by narrowing each plugin's capabilities in run-run.config.ts.`);
32
+ }
33
+ return first.impl;
34
+ }
35
+ providersOf(kind) {
36
+ return this.#providersOf(kind).map(({ plugin, impl }) => ({
37
+ name: plugin.name,
38
+ impl
39
+ }));
40
+ }
41
+ plugins() {
42
+ return this.#entries.map(({ plugin }) => plugin);
43
+ }
44
+ #providersOf(kind) {
45
+ const out = [];
46
+ for (const { plugin, capabilities } of this.#entries) {
47
+ const impl = capabilities[kind];
48
+ if (impl != null) out.push({
49
+ plugin,
50
+ impl
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+ };
56
+ //#endregion
57
+ //#region src/services/logger.ts
58
+ const logger = createLoggy({ namespace: "run-run" });
59
+ //#endregion
60
+ //#region src/services/config.ts
61
+ const DEFAULT_CONFIG = { plugins: [] };
62
+ var ConfigService = class {
63
+ #searcher;
64
+ constructor() {
65
+ this.#searcher = lilconfig("run-run", {
66
+ searchPlaces: ["run-run.config.ts", "run-run.config.mts"],
67
+ cache: true,
68
+ stopDir: os.homedir(),
69
+ loaders: {
70
+ ".ts": (filepath) => import(filepath).then((mod) => mod.default),
71
+ ".mts": (filepath) => import(filepath).then((mod) => mod.default)
72
+ }
73
+ });
74
+ }
75
+ async load() {
76
+ const debug = logger.subdebug("load-config");
77
+ const searchResult = await this.#searcher.search();
78
+ if (!searchResult || searchResult?.isEmpty) {
79
+ debug("loaded default config: %O", DEFAULT_CONFIG);
80
+ return {
81
+ config: DEFAULT_CONFIG,
82
+ meta: {
83
+ isDefault: true,
84
+ filepath: void 0
85
+ }
86
+ };
87
+ }
88
+ const config = searchResult.config;
89
+ debug("loaded config: %O", config);
90
+ debug("config file: %s", searchResult.filepath);
91
+ return {
92
+ config,
93
+ meta: {
94
+ isDefault: false,
95
+ filepath: searchResult.filepath
96
+ }
97
+ };
98
+ }
99
+ };
100
+ //#endregion
101
+ //#region src/services/ctx.ts
102
+ async function createContext(binDir) {
103
+ const debug = logger.subdebug("create-context");
104
+ const binPath = fs.realpathSync(binDir);
105
+ debug("bin path:", binPath);
106
+ debug("process cwd:", process.cwd());
107
+ const [appPkg, binPkg] = await Promise.all([createPkg(cwd), createPkg(binPath)]);
108
+ if (!binPkg) throw new Error("Could not find bin package.json");
109
+ if (!appPkg) throw new Error("Could not find app package.json");
110
+ debug("app pkg info: %O", appPkg.info());
111
+ debug("bin pkg info: %O", binPkg.info());
112
+ const shell = createShellService();
113
+ debug("shell service options: %O", shell.options);
114
+ const config = await new ConfigService().load();
115
+ const registry = new PluginRegistry();
116
+ const pluginContext = {
117
+ shell,
118
+ logger,
119
+ appPkg,
120
+ binPkg,
121
+ cwd
122
+ };
123
+ for (const plugin of config.config.plugins ?? []) {
124
+ if (plugin.apiVersion !== 1) throw new Error(`Plugin '${plugin.name}' targets apiVersion ${plugin.apiVersion}, but this kernel supports only apiVersion 1.`);
125
+ debug("registering plugin: %s", plugin.name);
126
+ const capabilities = await plugin.setup(pluginContext);
127
+ registry.register(plugin, capabilities);
128
+ }
129
+ return {
130
+ appPkg,
131
+ binPkg,
132
+ shell,
133
+ config,
134
+ registry
135
+ };
136
+ }
137
+ //#endregion
138
+ //#region src/program/ui.ts
139
+ const CREDITS_TEXT = `\nAcknowledgment:
140
+ - kcd-scripts: for main inspiration
141
+ ${palette.link("https://github.com/kentcdodds/kcd-scripts")}
142
+
143
+ - peruvian news: in honor to Run Run
144
+ ${palette.link("https://es.wikipedia.org/wiki/Run_Run")}`;
145
+ const rimrafColor = colorize("#7C7270");
146
+ const runRunColor = colorize("#E8722A");
147
+ const usageColor = colorize("#24C55E");
148
+ /**
149
+ * Labels used by kernel-internal commands. Plugin-owned tools (biome, oxc,
150
+ * tsdown, tsc) define their own colored labels inside each plugin's
151
+ * `src/index.ts`.
152
+ */
153
+ const TOOL_LABELS = {
154
+ RIMRAF: rimrafColor("rimraf"),
155
+ RUN_RUN: runRunColor("run-run"),
156
+ USAGE: usageColor("usage")
157
+ };
158
+ const IS_USAGE_MODE = process.env.RR_USAGE_MODE === "1";
159
+ /**
160
+ * Renders the parenthesised backend hint that follows a command's summary,
161
+ * e.g. `pack a ts library 📦 (tsdown)` or `… (not configured)` when no plugin
162
+ * provides the capability.
163
+ *
164
+ * Returns an empty string when `RR_USAGE_MODE=1` is set (the kernel's `bin`
165
+ * script exports it during `rr --usage`) so the KDL spec stays free of
166
+ * per-environment state — the active plugin set is a property of the host
167
+ * project, not of the CLI surface.
168
+ */
169
+ function pluginAnnotation(provider) {
170
+ if (IS_USAGE_MODE) return "";
171
+ return provider ? ` (${provider.ui})` : " (not configured)";
172
+ }
173
+ function getBannerText(version) {
174
+ return `
175
+ ${runRunColor(`
176
+ ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
177
+ ██╔══██╗██║ ██║████╗ ██║ ██╔══██╗██║ ██║████╗ ██║
178
+ ██████╔╝██║ ██║██╔██╗ ██║█████╗██████╔╝██║ ██║██╔██╗ ██║
179
+ ██╔══██╗██║ ██║██║╚██╗██║╚════╝██╔══██╗██║ ██║██║╚██╗██║
180
+ ██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
181
+ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ${text.version(version)}
182
+ `.trim())}
183
+
184
+ 🦊 ${palette.italic(palette.muted("The CLI toolbox for"))} ${text.vland}\n`.trimStart();
185
+ }
186
+ //#endregion
187
+ //#region src/program/commands/check.ts
188
+ /**
189
+ * `rr check` is the umbrella that runs the JS check (lint+format) and the
190
+ * TS type check together. Both subcommands are already wired into
191
+ * commander as siblings (`rr jsc`, `rr tsc`), so we reuse the program's
192
+ * command tree as the action registry instead of duplicating it: look the
193
+ * sibling up by name and invoke its action via `parseAsync([])`, which
194
+ * applies its declared option defaults exactly as if the user had typed
195
+ * `rr jsc` directly.
196
+ *
197
+ * Commander binds the running command as `this` inside an action (see
198
+ * `command.js` — `fn.apply(this, actionArgs)`). `this.parent` gives us the
199
+ * parent program without any late-binding ceremony.
200
+ */
201
+ function createCheckCommand() {
202
+ return createCommand("check").summary(`run static checks (${TOOL_LABELS.RUN_RUN})`).description("Runs `rr jsc` and `rr tsc` concurrently in-process. Aggregates their exit codes — non-zero when either subcommand fails.").action(async function checkAction() {
203
+ const program = this.parent;
204
+ if (!program) throw new Error("`rr check` requires the parent program to dispatch sibling subcommands.");
205
+ const cmds = ["jsc", "tsc"].map((name) => ({
206
+ name,
207
+ cmd: findCommand(program, name)
208
+ }));
209
+ const missing = cmds.filter(({ cmd }) => !cmd).map(({ name }) => name);
210
+ if (missing.length > 0) {
211
+ for (const name of missing) logger.error(`rr check: subcommand "${name}" is not registered.`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ const results = await Promise.allSettled(cmds.map(({ cmd }) => cmd.parseAsync([], { from: "user" })));
216
+ const failed = [];
217
+ for (const [i, r] of results.entries()) if (r.status === "rejected") failed.push({
218
+ name: cmds[i]?.name ?? "?",
219
+ reason: r.reason
220
+ });
221
+ if (failed.length > 0) {
222
+ for (const { name, reason } of failed) {
223
+ const msg = reason instanceof Error ? reason.message : String(reason);
224
+ logger.error(`rr check (${name}): ${msg}`);
225
+ }
226
+ process.exitCode = 1;
227
+ }
228
+ });
229
+ }
230
+ function findCommand(program, name) {
231
+ return program.commands.find((c) => c.name() === name || c.aliases().includes(name));
232
+ }
233
+ //#endregion
234
+ //#region src/program/commands/clean.ts
235
+ function createCleanCommand() {
236
+ 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) {
237
+ async function run(paths, globOptions) {
238
+ if (options.dryRun) {
239
+ const toDelete = await glob(paths, globOptions);
240
+ logger.info("Paths that would be deleted: %O", toDelete);
241
+ return;
242
+ }
243
+ logger.start("Clean started");
244
+ await rimraf(paths, { glob: globOptions });
245
+ logger.success("Clean completed");
246
+ }
247
+ const BUILD_PATHS = ["**/dist"];
248
+ const ALL_DIRTY_PATHS = [
249
+ "**/.turbo",
250
+ "**/node_modules",
251
+ "pnpm-lock.yaml",
252
+ "bun.lock",
253
+ ...BUILD_PATHS
254
+ ];
255
+ if (options.onlyDist) await run(BUILD_PATHS, {
256
+ cwd,
257
+ ignore: ["**/node_modules/**"]
258
+ });
259
+ else await run(ALL_DIRTY_PATHS, { cwd });
260
+ }).addHelpText("afterAll", `\nUnder the hood, this command uses ${TOOL_LABELS.RIMRAF} to delete dirty folders or files.`);
261
+ }
262
+ //#endregion
263
+ //#region src/program/commands/completion.ts
264
+ const SHELLS = [
265
+ "bash",
266
+ "zsh",
267
+ "fish"
268
+ ];
269
+ function createCompletionCommand() {
270
+ 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:
271
+
272
+ bash: eval "$(rr completion bash)"
273
+ zsh: eval "$(rr completion zsh)"
274
+ 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).
275
+ Make sure to have it installed and available in your PATH.`);
276
+ }
277
+ //#endregion
278
+ //#region src/program/commands/config.ts
279
+ function createConfigCommand(ctx) {
280
+ 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() {
281
+ const { config, meta } = ctx.config;
282
+ console.log(palette.muted("Config:"));
283
+ console.log(config);
284
+ console.log(palette.muted(`Loaded from ${meta.filepath ? palette.link(meta.filepath) : "n/a"}`));
285
+ });
286
+ }
287
+ //#endregion
288
+ //#region src/plugin/types.ts
289
+ const PLUGIN_KINDS = [
290
+ "lint",
291
+ "format",
292
+ "jsc",
293
+ "tsc",
294
+ "pack"
295
+ ];
296
+ //#endregion
297
+ //#region src/program/commands/doctor.ts
298
+ /**
299
+ * Subcommand factory used by every plugin-backed command (lint, format, jsc,
300
+ * tsc, pack) to expose a `doctor` subcommand that verifies the underlying
301
+ * tool is wired correctly. Each calls this with its own provider.
302
+ */
303
+ function createDoctorSubcommand(service) {
304
+ return createCommand("doctor").summary("check if the underlying tool is working correctly").action(async function doctorAction() {
305
+ const debug = logger.subdebug("doctor");
306
+ const { ok, output } = await service.doctor();
307
+ if (ok) {
308
+ logger.success(`${service.ui} ok`);
309
+ debug("%O", output);
310
+ } else {
311
+ logger.error(`${service.ui} not working`);
312
+ debug("%O", output);
313
+ process.exit(output.exitCode ?? 1);
314
+ }
315
+ });
316
+ }
317
+ /**
318
+ * Top-level `rr doctor` — runs the `doctor()` of every distinct capability
319
+ * impl registered with the kernel. Distinct because a single plugin (e.g.
320
+ * biome) often serves multiple kinds (`lint`, `format`, `jsc`) from the same
321
+ * `BiomeService` instance; running its doctor three times is wasteful.
322
+ */
323
+ function createDoctorCommand(ctx) {
324
+ 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 () => {
325
+ const services = collectDistinctDoctors(ctx);
326
+ if (services.length === 0) {
327
+ logger.info("No plugins configured. Use `rr plugins add <name>` to install one.");
328
+ return;
329
+ }
330
+ const debug = logger.subdebug("doctor");
331
+ const results = await Promise.all(services.map(async (svc) => {
332
+ return {
333
+ svc,
334
+ result: await svc.doctor()
335
+ };
336
+ }));
337
+ let failures = 0;
338
+ for (const { svc, result } of results) if (result.ok) {
339
+ logger.success(`${svc.ui} ok`);
340
+ debug("%s: %O", svc.ui, result.output);
341
+ } else {
342
+ logger.error(`${svc.ui} not working`);
343
+ debug("%s: %O", svc.ui, result.output);
344
+ failures++;
345
+ }
346
+ if (failures > 0) process.exitCode = 1;
347
+ });
348
+ }
349
+ function collectDistinctDoctors(ctx) {
350
+ const seen = /* @__PURE__ */ new Set();
351
+ for (const kind of PLUGIN_KINDS) for (const { impl } of ctx.registry.providersOf(kind)) seen.add(impl);
352
+ return [...seen];
353
+ }
354
+ //#endregion
355
+ //#region src/program/missing-plugin.ts
356
+ const SUGGESTIONS = {
357
+ lint: [
358
+ "biome",
359
+ "oxc",
360
+ "eslint"
361
+ ],
362
+ format: ["biome", "oxc"],
363
+ jsc: ["biome"],
364
+ tsc: ["ts"],
365
+ pack: ["tsdown"]
366
+ };
367
+ function missingPluginError(kind) {
368
+ const aliases = SUGGESTIONS[kind] ?? [];
369
+ const officialList = aliases.map((a) => `@rrlab/plugin-${a}`).join(", ");
370
+ const addList = aliases.map((a) => `rr plugins add ${a}`).join(" | ");
371
+ return /* @__PURE__ */ new Error(`No plugin provides the '${kind}' capability.` + (officialList ? `\n Install one of: ${officialList}.` : "") + (addList ? `\n Try: ${addList}.` : ""));
372
+ }
373
+ //#endregion
374
+ //#region src/program/commands/format.ts
375
+ function createFormatCommand(ctx) {
376
+ const formatter = ctx.registry.get("format");
377
+ 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");
378
+ if (formatter) cmd.addCommand(createDoctorSubcommand(formatter));
379
+ cmd.action(async (options = {}) => {
380
+ if (!formatter) throw missingPluginError("format");
381
+ await formatter.format(options);
382
+ });
383
+ if (formatter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${formatter.ui} CLI to format the code.`);
384
+ return cmd;
385
+ }
386
+ //#endregion
387
+ //#region src/program/composed-jsc.ts
388
+ /**
389
+ * Synthesises a `StaticChecker & Doctor` (the `jsc` capability) by composing
390
+ * a separately-registered linter and formatter. Used when the user's plugin
391
+ * set provides `lint` and `format` independently (e.g. oxc, or eslint +
392
+ * prettier) but no single plugin claims `jsc`.
393
+ *
394
+ * The check runs lint then format sequentially — interleaved stdout from a
395
+ * parallel run is hard to read for the user. `fixStaged` is dropped because
396
+ * the underlying tools don't have a uniform staged-aware mode.
397
+ */
398
+ function composedJscProvider(linter, formatter) {
399
+ return {
400
+ bin: `${linter.bin}+${formatter.bin}`,
401
+ ui: `${linter.ui} + ${formatter.ui}`,
402
+ async check({ fix }) {
403
+ await linter.lint({ fix });
404
+ await formatter.format({ fix });
405
+ },
406
+ async doctor() {
407
+ const [lintRes, fmtRes] = await Promise.all([linter.doctor(), formatter.doctor()]);
408
+ const ok = lintRes.ok && fmtRes.ok;
409
+ const firstFailure = !lintRes.ok ? lintRes : !fmtRes.ok ? fmtRes : void 0;
410
+ return {
411
+ ok,
412
+ output: {
413
+ stdout: `${linter.ui}:\n${lintRes.output.stdout}\n\n${formatter.ui}:\n${fmtRes.output.stdout}`,
414
+ stderr: `${linter.ui}:\n${lintRes.output.stderr}\n\n${formatter.ui}:\n${fmtRes.output.stderr}`,
415
+ exitCode: firstFailure?.output.exitCode ?? 0
416
+ }
417
+ };
418
+ }
419
+ };
420
+ }
421
+ //#endregion
422
+ //#region src/program/commands/jscheck.ts
423
+ function createJsCheckCommand(ctx) {
424
+ const direct = ctx.registry.get("jsc");
425
+ const linter = ctx.registry.get("lint");
426
+ const formatter = ctx.registry.get("format");
427
+ const checker = direct ?? (linter && formatter ? composedJscProvider(linter, formatter) : void 0);
428
+ 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");
429
+ if (checker) cmd.addCommand(createDoctorSubcommand(checker));
430
+ cmd.action(async (options = {}) => {
431
+ if (!checker) throw missingPluginError("jsc");
432
+ await checker.check(options);
433
+ });
434
+ if (checker) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${checker.ui} CLI to check the code.`);
435
+ return cmd;
436
+ }
437
+ //#endregion
438
+ //#region src/program/commands/lint.ts
439
+ function createLintCommand(ctx) {
440
+ const linter = ctx.registry.get("lint");
441
+ 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");
442
+ if (linter) cmd.addCommand(createDoctorSubcommand(linter));
443
+ cmd.action(async (options = {}) => {
444
+ if (!linter) throw missingPluginError("lint");
445
+ await linter.lint(options);
446
+ });
447
+ if (linter) cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${linter.ui} CLI to lint the code.`);
448
+ return cmd;
449
+ }
450
+ //#endregion
451
+ //#region src/program/commands/pack.ts
452
+ function createPackCommand(ctx) {
453
+ const packer = ctx.registry.get("pack");
454
+ 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.");
455
+ if (packer) {
456
+ cmd.addCommand(createDoctorSubcommand(packer));
457
+ cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${packer.ui} CLI to pack the project.`);
458
+ }
459
+ cmd.action(async () => {
460
+ if (!packer) throw missingPluginError("pack");
461
+ await packer.pack();
462
+ });
463
+ return cmd;
464
+ }
465
+ //#endregion
466
+ //#region src/services/config-ast.ts
467
+ const CONFIG_FILENAMES = ["run-run.config.ts", "run-run.config.mts"];
468
+ /**
469
+ * AST-level read/write for `run-run.config.{ts,mts}`. Wraps `magicast` so that
470
+ * adding/removing a plugin survives formatting, comments, and unrelated config
471
+ * options. The kernel and the `rr plugins add | remove` command use this
472
+ * service exclusively — no regex-based edits.
473
+ */
474
+ var ConfigAstService = class {
475
+ /**
476
+ * Looks for an existing `run-run.config.{ts,mts}` in `cwd`. If neither
477
+ * exists, returns a fresh magicast module built from a minimal template
478
+ * marked as `isNew: true`; the caller decides where to save it.
479
+ */
480
+ async load(cwd) {
481
+ for (const name of CONFIG_FILENAMES) {
482
+ const candidate = path.join(cwd, name);
483
+ try {
484
+ await fs$1.access(candidate);
485
+ return {
486
+ mod: await loadFile(candidate),
487
+ filepath: candidate,
488
+ isNew: false
489
+ };
490
+ } catch {}
491
+ }
492
+ const filepath = path.join(cwd, "run-run.config.mts");
493
+ return {
494
+ mod: parseModule(MINIMAL_TEMPLATE),
495
+ filepath,
496
+ isNew: true
497
+ };
498
+ }
499
+ async save(loaded) {
500
+ if (loaded.isNew) {
501
+ const { code } = generateCode(loaded.mod);
502
+ await fs$1.writeFile(loaded.filepath, code, "utf8");
503
+ return;
504
+ }
505
+ await writeFile(loaded.mod.$ast, loaded.filepath);
506
+ }
507
+ /**
508
+ * Returns true when the config's `plugins` array contains a call to the
509
+ * given `exportName` (e.g. `biome()`).
510
+ */
511
+ hasPlugin(mod, exportName) {
512
+ const plugins = this.#pluginsArray(mod);
513
+ if (!plugins) return false;
514
+ for (let i = 0; i < plugins.length; i++) {
515
+ const item = plugins[i];
516
+ if (this.#isCallTo(item, exportName)) return true;
517
+ }
518
+ return false;
519
+ }
520
+ /**
521
+ * Adds the import binding and pushes a `exportName()` call onto `plugins`.
522
+ * No-op when the call is already present (idempotent).
523
+ */
524
+ addPlugin(mod, entry) {
525
+ if (this.hasPlugin(mod, entry.exportName)) return { changed: false };
526
+ if (!mod.imports[entry.exportName]) mod.imports.$add({
527
+ from: entry.pkgName,
528
+ imported: "default",
529
+ local: entry.exportName
530
+ });
531
+ this.#ensureDefineConfig(mod);
532
+ this.#ensurePluginsArray(mod).push(builders.functionCall(entry.exportName));
533
+ return { changed: true };
534
+ }
535
+ /**
536
+ * Removes the `exportName()` call from `plugins[]` and, if it was the last
537
+ * use of that local binding, removes the import too.
538
+ */
539
+ removePlugin(mod, exportName) {
540
+ const plugins = this.#pluginsArray(mod);
541
+ if (!plugins) return { changed: false };
542
+ let changed = false;
543
+ for (let i = plugins.length - 1; i >= 0; i--) if (this.#isCallTo(plugins[i], exportName)) {
544
+ plugins.splice(i, 1);
545
+ changed = true;
546
+ }
547
+ if (changed && mod.imports[exportName]) delete mod.imports[exportName];
548
+ return { changed };
549
+ }
550
+ /** Returns the list of plugin exportNames currently in `plugins[]`. */
551
+ listPlugins(mod) {
552
+ const plugins = this.#pluginsArray(mod);
553
+ if (!plugins) return [];
554
+ const out = [];
555
+ for (let i = 0; i < plugins.length; i++) {
556
+ const item = plugins[i];
557
+ const name = this.#calleeName(item);
558
+ if (name) out.push(name);
559
+ }
560
+ return out;
561
+ }
562
+ /**
563
+ * Locates `defineConfig({ plugins: [...] })` and returns the plugins array
564
+ * proxy. Returns `undefined` if the config doesn't have that shape.
565
+ */
566
+ #pluginsArray(mod) {
567
+ const def = mod.exports.default;
568
+ if (!def || def.$type !== "function-call") return void 0;
569
+ const args = def.$args;
570
+ if (!args || args.length === 0) return void 0;
571
+ const opts = args[0];
572
+ if (!opts || typeof opts !== "object") return void 0;
573
+ return opts.plugins;
574
+ }
575
+ #ensureDefineConfig(mod) {
576
+ if (!mod.imports.defineConfig) mod.imports.$add({
577
+ from: "@rrlab/cli/config",
578
+ imported: "defineConfig",
579
+ local: "defineConfig"
580
+ });
581
+ if (!mod.exports.default) mod.exports.default = builders.functionCall("defineConfig", { plugins: [] });
582
+ }
583
+ #ensurePluginsArray(mod) {
584
+ const args = mod.exports.default.$args;
585
+ if (args.length === 0) args.push({ plugins: [] });
586
+ const opts = args[0];
587
+ if (!opts.plugins) opts.plugins = [];
588
+ return opts.plugins;
589
+ }
590
+ #isCallTo(item, exportName) {
591
+ return this.#calleeName(item) === exportName;
592
+ }
593
+ #calleeName(item) {
594
+ if (!item || typeof item !== "object") return void 0;
595
+ const proxy = item;
596
+ if (proxy.$type !== "function-call") return void 0;
597
+ const callee = proxy.$callee;
598
+ if (typeof callee === "string") return callee;
599
+ }
600
+ };
601
+ const MINIMAL_TEMPLATE = `import { defineConfig } from "@rrlab/cli/config";
602
+
603
+ export default defineConfig({
604
+ plugins: [],
605
+ });
606
+ `;
607
+ //#endregion
608
+ //#region src/services/json-edit.ts
609
+ /**
610
+ * Applies a sequence of `JsonEdit` ops to a JSON / JSONC source string.
611
+ * Uses `comment-json` so user comments and `extends`-like top-level shape
612
+ * survive a parse → mutate → stringify round-trip.
613
+ *
614
+ * Paths follow JSON Pointer (RFC 6901): `"/extends"`,
615
+ * `"/compilerOptions/strict"`. Indices are valid path segments for arrays
616
+ * (`"/extends/0"`).
617
+ */
618
+ function applyJsonEdits(source, edits) {
619
+ let root = source.trim() === "" ? {} : cjson.parse(source);
620
+ if (root == null || typeof root !== "object") throw new Error(`applyJsonEdits: expected a JSON object/array at the top level, got ${typeof root}.`);
621
+ for (const edit of edits) root = applyOne(root, edit);
622
+ return `${cjson.stringify(root, null, 2)}\n`;
623
+ }
624
+ function applyOne(root, edit) {
625
+ const segments = parsePointer(edit.path);
626
+ if (edit.op === "set") {
627
+ const existing = resolve(root, segments);
628
+ if (edit.mode === "if-missing" && existing !== void 0) return root;
629
+ return setAt(root, segments, edit.value);
630
+ }
631
+ if (edit.op === "unset") return unsetAt(root, segments);
632
+ if (edit.op === "include") return includeInArray(root, segments, edit.value, edit.position ?? "end");
633
+ if (edit.op === "exclude") return excludeFromArray(root, segments, edit.value);
634
+ return root;
635
+ }
636
+ /** Parses a JSON Pointer (RFC 6901) into its segments, decoding `~1` and `~0`. */
637
+ function parsePointer(pointer) {
638
+ if (pointer === "" || pointer === "/") return [];
639
+ if (!pointer.startsWith("/")) throw new Error(`Invalid JSON Pointer "${pointer}": must start with "/" or be empty.`);
640
+ return pointer.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
641
+ }
642
+ function resolve(root, segments) {
643
+ let cur = root;
644
+ for (const seg of segments) {
645
+ if (cur == null) return void 0;
646
+ cur = cur[arrayIndexIfNumeric(cur, seg)];
647
+ }
648
+ return cur;
649
+ }
650
+ function setAt(root, segments, value) {
651
+ if (segments.length === 0) return value;
652
+ let cur = root;
653
+ for (let i = 0; i < segments.length - 1; i++) {
654
+ const seg = segments[i] ?? "";
655
+ const key = arrayIndexIfNumeric(cur, seg);
656
+ if (cur[key] == null || typeof cur[key] !== "object") cur[key] = isNumericIndex(segments[i + 1] ?? "") ? [] : {};
657
+ cur = cur[key];
658
+ }
659
+ const last = segments[segments.length - 1] ?? "";
660
+ cur[arrayIndexIfNumeric(cur, last)] = value;
661
+ return root;
662
+ }
663
+ function unsetAt(root, segments) {
664
+ if (segments.length === 0) return root;
665
+ let cur = root;
666
+ for (let i = 0; i < segments.length - 1; i++) {
667
+ const seg = segments[i] ?? "";
668
+ if (cur == null) return root;
669
+ cur = cur[arrayIndexIfNumeric(cur, seg)];
670
+ }
671
+ if (cur == null) return root;
672
+ const last = segments[segments.length - 1] ?? "";
673
+ if (Array.isArray(cur) && isNumericIndex(last)) cur.splice(Number(last), 1);
674
+ else delete cur[last];
675
+ return root;
676
+ }
677
+ function includeInArray(root, segments, value, position) {
678
+ const existing = resolve(root, segments);
679
+ if (existing === void 0) return setAt(root, segments, [value]);
680
+ if (!Array.isArray(existing)) throw new Error(`include: expected an array at "${segments.join("/")}", got ${typeof existing}.`);
681
+ if (existing.some((item) => deepEqual(item, value))) return root;
682
+ if (position === "start") existing.unshift(value);
683
+ else existing.push(value);
684
+ return root;
685
+ }
686
+ function excludeFromArray(root, segments, value) {
687
+ const existing = resolve(root, segments);
688
+ if (!Array.isArray(existing)) return root;
689
+ const idx = existing.findIndex((item) => deepEqual(item, value));
690
+ if (idx >= 0) existing.splice(idx, 1);
691
+ return root;
692
+ }
693
+ function arrayIndexIfNumeric(target, seg) {
694
+ return Array.isArray(target) && isNumericIndex(seg) ? Number(seg) : seg;
695
+ }
696
+ function isNumericIndex(seg) {
697
+ return seg !== "" && /^\d+$/.test(seg);
698
+ }
699
+ function deepEqual(a, b) {
700
+ if (a === b) return true;
701
+ if (typeof a !== "object" || typeof b !== "object" || a == null || b == null) return false;
702
+ return JSON.stringify(a) === JSON.stringify(b);
703
+ }
704
+ //#endregion
705
+ //#region src/services/plugins-registry.ts
706
+ /**
707
+ * Map from short aliases the user types (`rr plugins add ts`) to the
708
+ * full npm package name and the canonical local binding the plugin file
709
+ * is imported as inside `run-run.config.{ts,mts}`.
710
+ *
711
+ * Third-party plugins use their full package name; the binding is derived
712
+ * by stripping a `@scope/plugin-` (or `@scope/run-run-plugin-`) prefix.
713
+ */
714
+ const OFFICIAL_PLUGINS = {
715
+ ts: {
716
+ pkg: "@rrlab/plugin-ts",
717
+ exportName: "ts"
718
+ },
719
+ eslint: {
720
+ pkg: "@rrlab/plugin-eslint",
721
+ exportName: "eslint"
722
+ },
723
+ biome: {
724
+ pkg: "@rrlab/plugin-biome",
725
+ exportName: "biome"
726
+ },
727
+ oxc: {
728
+ pkg: "@rrlab/plugin-oxc",
729
+ exportName: "oxc"
730
+ },
731
+ tsdown: {
732
+ pkg: "@rrlab/plugin-tsdown",
733
+ exportName: "tsdown"
734
+ }
735
+ };
736
+ function officialAliases() {
737
+ return Object.keys(OFFICIAL_PLUGINS);
738
+ }
739
+ //#endregion
740
+ //#region src/services/prompts.ts
741
+ /**
742
+ * Adapter that exposes the subset of `@clack/prompts` matching the
743
+ * `ClackPrompts` contract from `@rrlab/cli/plugin`. The contract intentionally
744
+ * stays narrow (`select`, `confirm`, `isCancel`) so plugin install hooks can
745
+ * be tested by injecting a stub without pulling in the real terminal IO.
746
+ *
747
+ * The casts compensate for two small type-shape mismatches:
748
+ * - Our contract requires `label: string`; clack types `label?: string`.
749
+ * - `clack.select` returns `Promise<unknown>` (a non-cancelled value or
750
+ * `symbol`); our contract narrows that to `Promise<T | symbol>`.
751
+ *
752
+ * Both widenings are safe: every required field is present, and clack's
753
+ * runtime returns either the picked option's `value` (typed `T`) or
754
+ * `clack.isCancel(...)`-detectable `symbol`.
755
+ */
756
+ function createClackPrompts() {
757
+ return {
758
+ select: (opts) => clack.select(opts),
759
+ confirm: (opts) => clack.confirm(opts),
760
+ isCancel: (value) => clack.isCancel(value)
761
+ };
762
+ }
763
+ //#endregion
764
+ //#region src/services/workspace-target.ts
765
+ function resolveWorkspaceChoice(appPkg, pm) {
766
+ if (!appPkg.isMonorepo()) return { kind: "current" };
767
+ return {
768
+ kind: "root",
769
+ flag: pmNeedsRootFlag(pm) ? true : void 0
770
+ };
771
+ }
772
+ function toNypmWorkspace(choice) {
773
+ return choice.kind === "current" ? void 0 : choice.flag;
774
+ }
775
+ function describeWorkspaceChoice(choice) {
776
+ return choice.kind === "current" ? "the current package" : "the workspace root";
777
+ }
778
+ function pmNeedsRootFlag(pm) {
779
+ if (!pm) return false;
780
+ if (pm.name === "pnpm") return true;
781
+ if (pm.name === "yarn" && (!pm.majorVersion || pm.majorVersion === "1")) return true;
782
+ return false;
783
+ }
784
+ //#endregion
785
+ //#region src/program/commands/plugins.ts
786
+ function createPluginsCommand(ctx) {
787
+ const cmd = createCommand("plugins").description("manage @rrlab plugins");
788
+ cmd.command("list").description("list plugins configured in run-run.config.{ts,mts}").action(() => runList(ctx));
789
+ cmd.command("add").description("install and configure an @rrlab plugin").addArgument(new Argument("<name>", "plugin alias").choices(officialAliases())).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));
790
+ 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));
791
+ return cmd;
792
+ }
793
+ async function runList(ctx) {
794
+ const ast = new ConfigAstService();
795
+ const loaded = await ast.load(ctx.appPkg.dirPath);
796
+ if (loaded.isNew) {
797
+ logger.info("No run-run.config.{ts,mts} found. Use `rr plugins add <name>` to start.");
798
+ return;
799
+ }
800
+ const plugins = ast.listPlugins(loaded.mod);
801
+ const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
802
+ if (plugins.length === 0) {
803
+ logger.info(`${rel}: no plugins configured.`);
804
+ return;
805
+ }
806
+ logger.info(`${rel}:`);
807
+ for (const name of plugins) logger.info(` - ${name}`);
808
+ }
809
+ async function runAdd(ctx, alias, opts) {
810
+ const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
811
+ clack.intro(` rr plugins add ${alias} `);
812
+ const inPkg = hasInPackageJson(ctx, pkgName);
813
+ const ast = new ConfigAstService();
814
+ const loaded = await ast.load(ctx.appPkg.dirPath);
815
+ const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, exportName);
816
+ if (inPkg && inConfig && !opts.force) {
817
+ clack.log.warn(`${pkgName} is already installed and configured. Use --force to re-run install.`);
818
+ clack.outro("Nothing to do.");
819
+ return;
820
+ }
821
+ const pm = await detectPackageManager(ctx.appPkg.dirPath);
822
+ const wsChoice = resolveWorkspaceChoice(ctx.appPkg, pm);
823
+ const workspace = toNypmWorkspace(wsChoice);
824
+ const targetLabel = describeWorkspaceChoice(wsChoice);
825
+ if (opts.dryRun) {
826
+ clack.log.info(`Would: install ${pkgName} as a devDependency in ${targetLabel}${inPkg ? " (already present, skipped)" : ""}.`);
827
+ if (!inConfig) {
828
+ const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
829
+ clack.log.info(`Would: add ${exportName}() to ${rel} (plugins[]).`);
830
+ }
831
+ clack.log.info("Would: run the plugin's install() hook (if any) to fetch peer deps and create files.");
832
+ clack.outro("Dry run complete.");
833
+ return;
834
+ }
835
+ let installedNow = false;
836
+ if (!inPkg) {
837
+ await withSpinner(`Installing ${pkgName}`, async () => {
838
+ await addDependency([pkgName], {
839
+ cwd: ctx.appPkg.dirPath,
840
+ dev: true,
841
+ silent: true,
842
+ workspace
843
+ });
844
+ });
845
+ installedNow = true;
846
+ }
847
+ let installResult;
848
+ try {
849
+ const factory = (await import(pkgName)).default;
850
+ if (typeof factory !== "function") throw new Error(`Plugin '${pkgName}' did not export a default factory function.`);
851
+ const plugin = factory();
852
+ if (plugin.install) {
853
+ const installCtx = {
854
+ shell: ctx.shell,
855
+ logger,
856
+ appPkg: ctx.appPkg,
857
+ prompts: createClackPrompts(),
858
+ flags: {
859
+ force: !!opts.force,
860
+ yes: !!opts.yes,
861
+ nonInteractive: !!opts.yes
862
+ }
863
+ };
864
+ installResult = await plugin.install(installCtx);
865
+ }
866
+ } catch (err) {
867
+ if (installedNow) try {
868
+ await removeDependency(pkgName, {
869
+ cwd: ctx.appPkg.dirPath,
870
+ workspace
871
+ });
872
+ } catch {}
873
+ throw err;
874
+ }
875
+ if (installResult?.devDependencies && Object.keys(installResult.devDependencies).length > 0) {
876
+ const names = Object.keys(installResult.devDependencies);
877
+ const deps = Object.entries(installResult.devDependencies).map(([k, v]) => `${k}@${v}`);
878
+ await withSpinner(`Installing ${names.join(", ")}`, async () => {
879
+ await addDependency(deps, {
880
+ cwd: ctx.appPkg.dirPath,
881
+ dev: true,
882
+ silent: true,
883
+ workspace
884
+ });
885
+ });
886
+ }
887
+ for (const op of installResult?.files ?? []) await applyFileOp(ctx.appPkg.dirPath, op, !!opts.force);
888
+ if (!inConfig) {
889
+ ast.addPlugin(loaded.mod, {
890
+ exportName,
891
+ pkgName
892
+ });
893
+ await ast.save(loaded);
894
+ const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
895
+ clack.log.success(`Updated ${rel}`);
896
+ }
897
+ clack.outro(`Plugin '${alias}' ready 🎉`);
898
+ }
899
+ async function runRemove(ctx, alias, opts) {
900
+ const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
901
+ clack.intro(` rr plugins remove ${alias} `);
902
+ const ast = new ConfigAstService();
903
+ const loaded = await ast.load(ctx.appPkg.dirPath);
904
+ const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, exportName);
905
+ let uninstallResult;
906
+ if (hasInPackageJson(ctx, pkgName)) try {
907
+ const factory = (await import(pkgName)).default;
908
+ const plugin = typeof factory === "function" ? factory() : void 0;
909
+ if (plugin?.uninstall) uninstallResult = await plugin.uninstall({
910
+ shell: ctx.shell,
911
+ logger,
912
+ appPkg: ctx.appPkg,
913
+ prompts: createClackPrompts(),
914
+ flags: {
915
+ yes: !!opts.yes,
916
+ nonInteractive: !!opts.yes
917
+ }
918
+ });
919
+ } catch (err) {
920
+ clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : err}`);
921
+ }
922
+ const planSteps = [];
923
+ if (inConfig) {
924
+ const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
925
+ planSteps.push(`Remove ${exportName}() from ${rel}`);
926
+ }
927
+ for (const op of uninstallResult?.files ?? []) planSteps.push(describeFileOp(op));
928
+ const depsToRemove = (uninstallResult?.removeDependencies ?? []).filter((dep) => hasInPackageJson(ctx, dep));
929
+ if (hasInPackageJson(ctx, pkgName) && !depsToRemove.includes(pkgName)) depsToRemove.unshift(pkgName);
930
+ const pm = await detectPackageManager(ctx.appPkg.dirPath);
931
+ const wsChoice = resolveWorkspaceChoice(ctx.appPkg, pm);
932
+ const workspace = toNypmWorkspace(wsChoice);
933
+ if (depsToRemove.length > 0) planSteps.push(`Uninstall: ${depsToRemove.join(", ")} (from ${describeWorkspaceChoice(wsChoice)})`);
934
+ if (planSteps.length === 0) {
935
+ clack.log.warn(`Plugin '${alias}' is not installed nor configured.`);
936
+ clack.outro("Nothing to do.");
937
+ return;
938
+ }
939
+ clack.log.message(`Plan:\n${planSteps.map((s) => ` • ${s}`).join("\n")}`);
940
+ if (opts.dryRun) {
941
+ clack.outro("Dry run complete.");
942
+ return;
943
+ }
944
+ if (!opts.yes) {
945
+ const choice = await clack.confirm({
946
+ message: "Proceed?",
947
+ initialValue: false
948
+ });
949
+ if (clack.isCancel(choice) || choice !== true) {
950
+ clack.outro("Aborted.");
951
+ return;
952
+ }
953
+ }
954
+ for (const op of uninstallResult?.files ?? []) await applyFileOp(ctx.appPkg.dirPath, op, true);
955
+ if (inConfig) {
956
+ ast.removePlugin(loaded.mod, exportName);
957
+ await ast.save(loaded);
958
+ const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
959
+ clack.log.success(`Removed ${exportName}() from ${rel}`);
960
+ }
961
+ for (const dep of depsToRemove) await withSpinner(`Uninstalling ${dep}`, async () => {
962
+ await removeDependency(dep, {
963
+ cwd: ctx.appPkg.dirPath,
964
+ workspace
965
+ });
966
+ });
967
+ clack.outro(`Plugin '${alias}' removed.`);
968
+ }
969
+ function hasInPackageJson(ctx, pkgName) {
970
+ const pkg = ctx.appPkg.packageJson;
971
+ return pkgName in {
972
+ ...pkg.dependencies ?? {},
973
+ ...pkg.devDependencies ?? {},
974
+ ...pkg.peerDependencies ?? {}
975
+ };
976
+ }
977
+ async function withSpinner(message, fn) {
978
+ const sp = clack.spinner();
979
+ sp.start(message);
980
+ try {
981
+ const result = await fn();
982
+ sp.stop(message);
983
+ return result;
984
+ } catch (err) {
985
+ sp.stop(`${message} — failed`, 1);
986
+ throw err;
987
+ }
988
+ }
989
+ function describeFileOp(op) {
990
+ switch (op.kind) {
991
+ case "create": return `${op.overwrite ? "Overwrite" : "Create"} ${op.path}`;
992
+ case "edit-json": return `Edit ${op.path} (${op.edits.length} change${op.edits.length === 1 ? "" : "s"})`;
993
+ case "edit-text": return `Edit ${op.path}`;
994
+ case "delete": return `Delete ${op.path}`;
995
+ }
996
+ }
997
+ async function applyFileOp(cwd, op, force) {
998
+ const abs = path.join(cwd, op.path);
999
+ if (op.kind === "create") {
1000
+ const exists = await pathExists(abs);
1001
+ if (exists && !op.overwrite && !force) {
1002
+ clack.log.warn(`Skipping ${op.path} — already exists. Use --force to overwrite.`);
1003
+ return;
1004
+ }
1005
+ await fs$1.mkdir(path.dirname(abs), { recursive: true });
1006
+ await fs$1.writeFile(abs, op.content, "utf8");
1007
+ clack.log.success(`${exists ? "Overwrote" : "Created"} ${op.path}`);
1008
+ return;
1009
+ }
1010
+ if (op.kind === "edit-json") {
1011
+ if (!await pathExists(abs)) {
1012
+ clack.log.warn(`Skipping ${op.path} — file does not exist.`);
1013
+ return;
1014
+ }
1015
+ const source = await fs$1.readFile(abs, "utf8");
1016
+ const next = applyJsonEdits(source, op.edits);
1017
+ if (next !== source) {
1018
+ await fs$1.writeFile(abs, next, "utf8");
1019
+ clack.log.success(`Edited ${op.path}`);
1020
+ } else clack.log.info(`No changes for ${op.path}.`);
1021
+ return;
1022
+ }
1023
+ if (op.kind === "edit-text") {
1024
+ if (!await pathExists(abs)) {
1025
+ clack.log.warn(`Skipping ${op.path} — file does not exist.`);
1026
+ return;
1027
+ }
1028
+ const source = await fs$1.readFile(abs, "utf8");
1029
+ const next = op.edit(source);
1030
+ if (next !== source) {
1031
+ await fs$1.writeFile(abs, next, "utf8");
1032
+ clack.log.success(`Edited ${op.path}`);
1033
+ } else clack.log.info(`No changes for ${op.path}.`);
1034
+ return;
1035
+ }
1036
+ if (op.kind === "delete") {
1037
+ if (!await pathExists(abs)) return;
1038
+ await fs$1.unlink(abs);
1039
+ clack.log.success(`Deleted ${op.path}`);
1040
+ return;
1041
+ }
1042
+ }
1043
+ async function pathExists(p) {
1044
+ try {
1045
+ await fs$1.access(p);
1046
+ return true;
1047
+ } catch {
1048
+ return false;
1049
+ }
1050
+ }
1051
+ //#endregion
1052
+ //#region src/program/commands/tscheck.ts
1053
+ const getPreScript = (scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
1054
+ async function typecheckAt({ dir, scripts, log, shell, tsc }) {
1055
+ log.debug(`checking types at ${dir}`);
1056
+ const shellAt = cwd === dir ? shell : shell.at(dir);
1057
+ try {
1058
+ const preScript = getPreScript(scripts);
1059
+ if (preScript) {
1060
+ log.start(`Running pre-script: ${preScript}`);
1061
+ await shellAt.run(preScript, [], { shell: true });
1062
+ log.success("Pre-script completed");
1063
+ }
1064
+ log.start("Type checking started");
1065
+ if (cwd === dir) await tsc.check();
1066
+ else await tsc.check({ cwd: dir });
1067
+ log.success("Typecheck completed");
1068
+ } catch (error) {
1069
+ log.error("Typecheck failed");
1070
+ throw error;
1071
+ }
1072
+ }
1073
+ function createTsCheckCommand(ctx) {
1074
+ const { appPkg, shell } = ctx;
1075
+ const tsc = ctx.registry.get("tsc");
1076
+ 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.");
1077
+ if (tsc) {
1078
+ cmd.addCommand(createDoctorSubcommand(tsc));
1079
+ cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
1080
+ }
1081
+ cmd.action(async () => {
1082
+ if (!tsc) throw missingPluginError("tsc");
1083
+ const isTsProject = (dir) => appPkg.hasFile("tsconfig.json", dir);
1084
+ if (!appPkg.isMonorepo()) {
1085
+ if (!isTsProject(appPkg.dirPath)) {
1086
+ logger.info("No tsconfig.json found, skipping typecheck");
1087
+ return;
1088
+ }
1089
+ await typecheckAt({
1090
+ shell,
1091
+ tsc,
1092
+ dir: appPkg.dirPath,
1093
+ scripts: appPkg.packageJson.scripts,
1094
+ log: logger
1095
+ });
1096
+ return;
1097
+ }
1098
+ const tsProjects = (await appPkg.getWorkspaceProjects()).filter((project) => isTsProject(project.rootDir));
1099
+ if (!tsProjects.length) {
1100
+ logger.warn("No ts projects found in the monorepo, skipping typecheck");
1101
+ return;
1102
+ }
1103
+ await Promise.all(tsProjects.map((p) => typecheckAt({
1104
+ shell,
1105
+ tsc,
1106
+ dir: p.rootDir,
1107
+ scripts: p.manifest.scripts,
1108
+ log: logger.child({
1109
+ tag: p.manifest.name,
1110
+ namespace: "typecheck"
1111
+ })
1112
+ })));
1113
+ });
1114
+ return cmd;
1115
+ }
1116
+ //#endregion
1117
+ //#region src/program/index.ts
1118
+ async function createProgram(options) {
1119
+ const ctx = await createContext(options.binDir);
1120
+ const version = ctx.binPkg.version;
1121
+ return {
1122
+ 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() {
1123
+ generateToStdout(this);
1124
+ process.exit(0);
1125
+ }).addHelpText("before", getBannerText(version)).addHelpText("after", CREDITS_TEXT).addCommand(createCompletionCommand()).addCommand(createPackCommand(ctx)).addCommand(createJsCheckCommand(ctx)).addCommand(createTsCheckCommand(ctx)).addCommand(createLintCommand(ctx)).addCommand(createFormatCommand(ctx)).addCommand(createCheckCommand()).addCommand(createDoctorCommand(ctx)).addCommand(createPluginsCommand(ctx)).addCommand(createCleanCommand()).addCommand(createConfigCommand(ctx)),
1126
+ ctx
1127
+ };
1128
+ }
1129
+ //#endregion
1130
+ //#region src/run.ts
1131
+ const BIN_DIR = path.dirname(dirnameOf(import.meta));
1132
+ await run(async () => {
1133
+ const { program } = await createProgram({ binDir: BIN_DIR });
1134
+ await program.parseAsync(process.argv, { from: "node" });
1135
+ }, logger);
1136
+ //#endregion
1137
+ export {};