@rrlab/cli 1.1.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 +6 -5
  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
@@ -1,386 +1,39 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import * as clack from "@clack/prompts";
4
- import { Argument, createCommand } from "commander";
5
- import { addDependency, detectPackageManager, removeDependency } from "nypm";
6
- import type { FileOp, InstallContext, InstallResult, UninstallContext, UninstallResult } from "#src/plugin/types.ts";
7
- import { ConfigAstService } from "#src/services/config-ast.ts";
8
- import type { Context } from "#src/services/ctx.ts";
9
- import { applyJsonEdits } from "#src/services/json-edit.ts";
10
- import { logger } from "#src/services/logger.ts";
11
- import { OFFICIAL_PLUGINS, type OfficialAlias, officialAliases } from "#src/services/plugins-registry.ts";
12
- import { createClackPrompts } from "#src/services/prompts.ts";
13
- import { ReleaseService } from "#src/services/release.ts";
14
- import { describeWorkspaceChoice, resolveWorkspaceChoice, toNypmWorkspace } from "#src/services/workspace-target.ts";
15
-
16
- type AddOptions = {
17
- force?: boolean;
18
- yes?: boolean;
19
- dryRun?: boolean;
20
- };
21
-
22
- type RemoveOptions = {
23
- yes?: boolean;
24
- dryRun?: boolean;
25
- };
26
-
27
- type InstallHook = (ctx: InstallContext) => Promise<InstallResult>;
28
- type UninstallHook = (ctx: UninstallContext) => Promise<UninstallResult>;
29
- type PluginModule = { default?: (opts?: unknown) => { install?: InstallHook; uninstall?: UninstallHook } };
30
-
31
- export function createPluginsCommand(ctx: Context) {
32
- const cmd = createCommand("plugins").description("manage @rrlab plugins");
1
+ import { Argument } from "commander";
2
+ import { addPluginAction } from "#src/actions/plugins/add.ts";
3
+ import { listPluginsAction } from "#src/actions/plugins/list.ts";
4
+ import { removePluginAction } from "#src/actions/plugins/remove.ts";
5
+ import type { AddOptions, RemoveOptions } from "#src/actions/plugins/shared.ts";
6
+ import { allPluginNames, type PluginName } from "#src/lib/plugin/directory.ts";
7
+ import type { ContextValue } from "#src/services/context.ts";
8
+ import { runRunColor } from "#src/ui/theme.ts";
9
+ import { createCommand } from "../base.ts";
10
+
11
+ export function createPluginsCommand(ctx: ContextValue) {
12
+ const cmd = createCommand("plugins").description(`manage ${runRunColor("@rrlab")} plugins`);
33
13
 
34
14
  cmd
35
15
  .command("list")
36
16
  .description("list plugins configured in run-run.config.{ts,mts}")
37
- .action(() => runList(ctx));
17
+ .action(() => listPluginsAction({ ctx }));
38
18
 
39
19
  cmd
40
20
  .command("add")
41
21
  .description("install and configure an @rrlab plugin")
42
22
  .addArgument(
43
- new Argument("<name>", `plugin alias (${officialAliases().join("|")}), optionally with @<spec> e.g. biome@pr-226`),
23
+ new Argument("<name>", `plugin alias (${allPluginNames().join("|")}), optionally with @<spec> e.g. biome@pr-226`),
44
24
  )
45
25
  .option("--force", "re-run install even if the plugin is already configured")
46
26
  .option("--yes", "skip prompts and use defaults (non-interactive)")
47
27
  .option("--dry-run", "show what would happen, without applying changes")
48
- .action((name: string, opts: AddOptions) => runAdd(ctx, name, opts));
28
+ .action((name: PluginName, options: AddOptions) => addPluginAction({ ctx, args: { name }, options }));
49
29
 
50
30
  cmd
51
31
  .command("remove")
52
32
  .description("uninstall an @rrlab plugin and undo its config files + deps")
53
- .addArgument(new Argument("<name>", "plugin alias to remove").choices(officialAliases()))
33
+ .addArgument(new Argument("<name>", "plugin alias to remove").choices(allPluginNames()))
54
34
  .option("--yes", "skip the confirmation prompt")
55
35
  .option("--dry-run", "print the plan without applying changes")
56
- .action((name: OfficialAlias, opts: RemoveOptions) => runRemove(ctx, name, opts));
36
+ .action((name: PluginName, options: RemoveOptions) => removePluginAction({ ctx, args: { name }, options }));
57
37
 
58
38
  return cmd;
59
39
  }
60
-
61
- async function runList(ctx: Context) {
62
- const ast = new ConfigAstService();
63
- const loaded = await ast.load(ctx.appPkg.dirPath);
64
- if (loaded.isNew) {
65
- logger.info("No run-run.config.{ts,mts} found. Use `rr plugins add <name>` to start.");
66
- return;
67
- }
68
- const plugins = ast.listPlugins(loaded.mod);
69
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
70
- if (plugins.length === 0) {
71
- logger.info(`${rel}: no plugins configured.`);
72
- return;
73
- }
74
- logger.info(`${rel}:`);
75
- for (const name of plugins) {
76
- logger.info(` - ${name}`);
77
- }
78
- }
79
-
80
- async function runAdd(ctx: Context, name: string, opts: AddOptions) {
81
- const { alias, spec } = parseAliasSpec(name);
82
- if (!(alias in OFFICIAL_PLUGINS)) {
83
- throw new Error(`'${alias}' is invalid for argument 'name'. Allowed choices are ${officialAliases().join(", ")}.`);
84
- }
85
- const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias as OfficialAlias];
86
- const tag = spec && isDistTag(spec) ? spec : undefined;
87
- const installSpec = spec ? `${pkgName}@${spec}` : pkgName;
88
-
89
- clack.intro(` rr plugins add ${name} `);
90
-
91
- const inPkg = hasInPackageJson(ctx, pkgName);
92
- const ast = new ConfigAstService();
93
- const loaded = await ast.load(ctx.appPkg.dirPath);
94
- const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, exportName);
95
-
96
- if (inPkg && inConfig && !opts.force && !spec) {
97
- clack.log.warn(`${pkgName} is already installed and configured. Use --force to re-run install.`);
98
- clack.outro("Nothing to do.");
99
- return;
100
- }
101
-
102
- const pm = await detectPackageManager(ctx.appPkg.dirPath);
103
- const wsChoice = resolveWorkspaceChoice(ctx.appPkg, pm);
104
- const workspace = toNypmWorkspace(wsChoice);
105
- const targetLabel = describeWorkspaceChoice(wsChoice);
106
- // A spec means "(re)install at this spec" — upgrade even when the package is already in package.json.
107
- const willInstall = !inPkg || !!spec;
108
-
109
- if (opts.dryRun) {
110
- const presence = willInstall
111
- ? inPkg
112
- ? " (already present, will be updated to this spec)"
113
- : ""
114
- : " (already present, skipped)";
115
- clack.log.info(`Would: install ${installSpec} as a devDependency in ${targetLabel}${presence}.`);
116
- if (!inConfig) {
117
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
118
- clack.log.info(`Would: add ${exportName}() to ${rel} (plugins[]).`);
119
- }
120
- clack.log.info("Would: run the plugin's install() hook (if any) to fetch peer deps and create files.");
121
- clack.outro("Dry run complete.");
122
- return;
123
- }
124
-
125
- let installedNow = false;
126
- if (willInstall) {
127
- await withSpinner(`Installing ${installSpec}`, async () => {
128
- await addDependency([installSpec], { cwd: ctx.appPkg.dirPath, dev: true, silent: true, workspace });
129
- });
130
- // Only mark for rollback when this was a fresh install — a failed upgrade can't be safely reverted to the previous version.
131
- if (!inPkg) installedNow = true;
132
- }
133
-
134
- let installResult: InstallResult | undefined;
135
- try {
136
- const mod = (await import(pkgName)) as PluginModule;
137
- const factory = mod.default;
138
- if (typeof factory !== "function") {
139
- throw new Error(`Plugin '${pkgName}' did not export a default factory function.`);
140
- }
141
- const plugin = factory();
142
- if (plugin.install) {
143
- const installCtx: InstallContext = {
144
- shell: ctx.shell,
145
- logger,
146
- appPkg: ctx.appPkg,
147
- prompts: createClackPrompts(),
148
- flags: {
149
- force: !!opts.force,
150
- yes: !!opts.yes,
151
- nonInteractive: !!opts.yes,
152
- },
153
- release: new ReleaseService(tag),
154
- };
155
- installResult = await plugin.install(installCtx);
156
- }
157
- } catch (err) {
158
- if (installedNow) {
159
- try {
160
- await removeDependency(pkgName, { cwd: ctx.appPkg.dirPath, silent: true, workspace });
161
- } catch {
162
- // best-effort rollback — don't mask the original error
163
- }
164
- }
165
- throw err;
166
- }
167
-
168
- if (installResult?.devDependencies && Object.keys(installResult.devDependencies).length > 0) {
169
- const names = Object.keys(installResult.devDependencies);
170
- const deps = Object.entries(installResult.devDependencies).map(([k, v]) => `${k}@${v}`);
171
- await withSpinner(`Installing ${names.join(", ")}`, async () => {
172
- await addDependency(deps, { cwd: ctx.appPkg.dirPath, dev: true, silent: true, workspace });
173
- });
174
- }
175
- for (const op of installResult?.files ?? []) {
176
- await applyFileOp(ctx.appPkg.dirPath, op, !!opts.force);
177
- }
178
-
179
- if (!inConfig) {
180
- ast.addPlugin(loaded.mod, { exportName, pkgName });
181
- await ast.save(loaded);
182
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
183
- clack.log.success(`Updated ${rel}`);
184
- }
185
-
186
- clack.outro(`Plugin '${alias}' ready 🎉`);
187
- }
188
-
189
- async function runRemove(ctx: Context, alias: OfficialAlias, opts: RemoveOptions) {
190
- const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
191
-
192
- clack.intro(` rr plugins remove ${alias} `);
193
-
194
- const ast = new ConfigAstService();
195
- const loaded = await ast.load(ctx.appPkg.dirPath);
196
- const inConfig = !loaded.isNew && ast.hasPlugin(loaded.mod, exportName);
197
-
198
- // Collect plugin's uninstall plan (only if pkg is installed + has uninstall hook).
199
- let uninstallResult: UninstallResult | undefined;
200
- if (hasInPackageJson(ctx, pkgName)) {
201
- try {
202
- const mod = (await import(pkgName)) as PluginModule;
203
- const factory = mod.default;
204
- const plugin = typeof factory === "function" ? factory() : undefined;
205
- if (plugin?.uninstall) {
206
- uninstallResult = await plugin.uninstall({
207
- shell: ctx.shell,
208
- logger,
209
- appPkg: ctx.appPkg,
210
- prompts: createClackPrompts(),
211
- flags: { yes: !!opts.yes, nonInteractive: !!opts.yes },
212
- });
213
- }
214
- } catch (err) {
215
- clack.log.warn(`Could not load ${pkgName} for uninstall hook: ${err instanceof Error ? err.message : String(err)}`);
216
- }
217
- }
218
-
219
- const planSteps: string[] = [];
220
- if (inConfig) {
221
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
222
- planSteps.push(`Remove ${exportName}() from ${rel}`);
223
- }
224
- for (const op of uninstallResult?.files ?? []) {
225
- planSteps.push(describeFileOp(op));
226
- }
227
- const depsToRemove = (uninstallResult?.removeDependencies ?? []).filter((dep) => hasInPackageJson(ctx, dep));
228
- if (hasInPackageJson(ctx, pkgName) && !depsToRemove.includes(pkgName)) {
229
- depsToRemove.unshift(pkgName);
230
- }
231
- const pm = await detectPackageManager(ctx.appPkg.dirPath);
232
- const wsChoice = resolveWorkspaceChoice(ctx.appPkg, pm);
233
- const workspace = toNypmWorkspace(wsChoice);
234
-
235
- if (depsToRemove.length > 0) {
236
- planSteps.push(`Uninstall: ${depsToRemove.join(", ")} (from ${describeWorkspaceChoice(wsChoice)})`);
237
- }
238
-
239
- if (planSteps.length === 0) {
240
- clack.log.warn(`Plugin '${alias}' is not installed nor configured.`);
241
- clack.outro("Nothing to do.");
242
- return;
243
- }
244
-
245
- clack.log.message(`Plan:\n${planSteps.map((s) => ` • ${s}`).join("\n")}`);
246
-
247
- if (opts.dryRun) {
248
- clack.outro("Dry run complete.");
249
- return;
250
- }
251
-
252
- if (!opts.yes) {
253
- const choice = await clack.confirm({ message: "Proceed?", initialValue: false });
254
- if (clack.isCancel(choice) || choice !== true) {
255
- clack.outro("Aborted.");
256
- return;
257
- }
258
- }
259
-
260
- // Apply file ops first (they may need the plugin's source to still be importable).
261
- for (const op of uninstallResult?.files ?? []) {
262
- await applyFileOp(ctx.appPkg.dirPath, op, /* force */ true);
263
- }
264
- if (inConfig) {
265
- ast.removePlugin(loaded.mod, exportName);
266
- await ast.save(loaded);
267
- const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
268
- clack.log.success(`Removed ${exportName}() from ${rel}`);
269
- }
270
- for (const dep of depsToRemove) {
271
- await withSpinner(`Uninstalling ${dep}`, async () => {
272
- await removeDependency(dep, { cwd: ctx.appPkg.dirPath, silent: true, workspace });
273
- });
274
- }
275
-
276
- clack.outro(`Plugin '${alias}' removed.`);
277
- }
278
-
279
- function parseAliasSpec(input: string): { alias: string; spec?: string } {
280
- const at = input.indexOf("@");
281
- if (at <= 0) return { alias: input };
282
- return { alias: input.slice(0, at), spec: input.slice(at + 1) };
283
- }
284
-
285
- /** A dist-tag starts with a letter and contains only safe identifier chars. Version ranges (`^0.1`, `>=1`, `0.0.2`, `*`) don't match. */
286
- function isDistTag(spec: string): boolean {
287
- return /^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(spec) && spec !== "latest";
288
- }
289
-
290
- function hasInPackageJson(ctx: Context, pkgName: string): boolean {
291
- const pkg = ctx.appPkg.packageJson;
292
- const deps = {
293
- ...pkg.dependencies,
294
- ...pkg.devDependencies,
295
- ...pkg.peerDependencies,
296
- };
297
- return pkgName in deps;
298
- }
299
-
300
- async function withSpinner<T>(message: string, fn: () => Promise<T>): Promise<T> {
301
- const sp = clack.spinner();
302
- sp.start(message);
303
- try {
304
- const result = await fn();
305
- sp.stop(message);
306
- return result;
307
- } catch (err) {
308
- sp.stop(`${message} — failed`, 1);
309
- throw err;
310
- }
311
- }
312
-
313
- function describeFileOp(op: FileOp): string {
314
- switch (op.kind) {
315
- case "create":
316
- return `${op.overwrite ? "Overwrite" : "Create"} ${op.path}`;
317
- case "edit-json":
318
- return `Edit ${op.path} (${op.edits.length} change${op.edits.length === 1 ? "" : "s"})`;
319
- case "edit-text":
320
- return `Edit ${op.path}`;
321
- case "delete":
322
- return `Delete ${op.path}`;
323
- }
324
- }
325
-
326
- async function applyFileOp(cwd: string, op: FileOp, force: boolean) {
327
- const abs = path.join(cwd, op.path);
328
- if (op.kind === "create") {
329
- const exists = await pathExists(abs);
330
- if (exists && !op.overwrite && !force) {
331
- clack.log.warn(`Skipping ${op.path} — already exists. Use --force to overwrite.`);
332
- return;
333
- }
334
- await fs.mkdir(path.dirname(abs), { recursive: true });
335
- await fs.writeFile(abs, op.content, "utf8");
336
- clack.log.success(`${exists ? "Overwrote" : "Created"} ${op.path}`);
337
- return;
338
- }
339
- if (op.kind === "edit-json") {
340
- if (!(await pathExists(abs))) {
341
- clack.log.warn(`Skipping ${op.path} — file does not exist.`);
342
- return;
343
- }
344
- const source = await fs.readFile(abs, "utf8");
345
- const next = applyJsonEdits(source, op.edits);
346
- if (next !== source) {
347
- await fs.writeFile(abs, next, "utf8");
348
- clack.log.success(`Edited ${op.path}`);
349
- } else {
350
- clack.log.info(`No changes for ${op.path}.`);
351
- }
352
- return;
353
- }
354
- if (op.kind === "edit-text") {
355
- if (!(await pathExists(abs))) {
356
- clack.log.warn(`Skipping ${op.path} — file does not exist.`);
357
- return;
358
- }
359
- const source = await fs.readFile(abs, "utf8");
360
- const next = op.edit(source);
361
- if (next !== source) {
362
- await fs.writeFile(abs, next, "utf8");
363
- clack.log.success(`Edited ${op.path}`);
364
- } else {
365
- clack.log.info(`No changes for ${op.path}.`);
366
- }
367
- return;
368
- }
369
- if (op.kind === "delete") {
370
- if (!(await pathExists(abs))) return;
371
- await fs.unlink(abs);
372
- clack.log.success(`Deleted ${op.path}`);
373
- return;
374
- }
375
- }
376
-
377
- async function pathExists(p: string): Promise<boolean> {
378
- try {
379
- await fs.access(p);
380
- return true;
381
- } catch {
382
- return false;
383
- }
384
- }
385
-
386
- export { OFFICIAL_PLUGINS };
@@ -1,81 +1,23 @@
1
- import { createCommand } from "commander";
2
- import type { Context } from "#src/services/ctx.ts";
3
- import { logger } from "#src/services/logger.ts";
4
- import { type BoardTask, fanoutTitle, reportTask, runBoard, targetLabel } from "../board.ts";
5
- import { missingPluginError } from "../missing-plugin.ts";
6
- import { pluginAnnotation } from "../ui.ts";
7
- import { createDoctorSubcommand } from "./doctor.ts";
1
+ import { doctorOneAction } from "#src/actions/doctor.ts";
2
+ import { tscAction } from "#src/actions/tsc.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import { createCommand } from "../base.ts";
8
5
 
9
- type Scripts = Record<string, string | undefined> | undefined;
10
-
11
- const getPreScript = (scripts: Scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
12
-
13
- export function createTsCheckCommand(ctx: Context) {
14
- const { appPkg, shell } = ctx;
15
- const tsc = ctx.registry.get("tsc");
16
-
17
- const cmd = createCommand("tsc")
6
+ export function createTsCheckCommand(ctx: ContextValue) {
7
+ return createCommand("tsc")
18
8
  .alias("tscheck")
19
- .summary(`check typescript errors${pluginAnnotation(tsc)}`)
9
+ .addCapabilities(["typecheck"])
10
+ .summary("check types errors")
20
11
  .description(
21
- "Checks the TypeScript code for type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.",
22
- );
23
-
24
- if (tsc) {
25
- cmd.addCommand(createDoctorSubcommand(tsc, ctx.appPkg));
26
- cmd.addHelpText("afterAll", `\nUnder the hood, this command uses the ${tsc.ui} CLI to check the code.`);
27
- }
28
-
29
- cmd.action(async () => {
30
- if (!tsc) throw missingPluginError("tsc");
31
-
32
- const isTsProject = (dir: string) => appPkg.hasFile("tsconfig.json", dir);
33
-
34
- // A package's `pretsc`/`pretypecheck` runs captured, inside the task, so its
35
- // output stays grouped with that package. It may use shell features, so it
36
- // goes through `/bin/sh -c`. A failing pre-script fails the task before tsc.
37
- const typecheckTask = (label: string, dir: string, scripts: Scripts): BoardTask =>
38
- reportTask(label, async () => {
39
- const preScript = getPreScript(scripts);
40
- if (preScript) {
41
- const pre = await shell.at(dir).runCaptured(preScript, [], { shell: true, throwOnError: false });
42
- if ((pre.exitCode ?? 0) !== 0) {
43
- const output = [pre.stdout, pre.stderr]
44
- .map((s) => s?.trim())
45
- .filter(Boolean)
46
- .join("\n");
47
- return { ok: false, output: `pre-script \`${preScript}\` failed\n${output}` };
48
- }
49
- }
50
- return tsc.check({ cwd: dir });
51
- });
52
-
53
- if (!appPkg.isMonorepo()) {
54
- if (!isTsProject(appPkg.dirPath)) {
55
- logger.info("No tsconfig.json found, skipping typecheck");
56
- return;
57
- }
58
-
59
- // Single package → compact board; the row carries the canonical
60
- // `tsc (<tool>) · <pkg>` label like every other single-target command.
61
- const label = targetLabel("tsc", tsc, appPkg);
62
- const result = await runBoard([typecheckTask(label, appPkg.dirPath, appPkg.packageJson.scripts)]);
63
- if (!result.ok) process.exitCode = 1;
64
- return;
65
- }
66
-
67
- const projects = await appPkg.getWorkspaceProjects();
68
- const tsProjects = projects.filter((project) => isTsProject(project.rootDir));
69
-
70
- if (!tsProjects.length) {
71
- logger.warn("No ts projects found in the monorepo, skipping typecheck");
72
- return;
73
- }
74
-
75
- const tasks = tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts));
76
- const result = await runBoard(tasks, { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") });
77
- if (!result.ok) process.exitCode = 1;
78
- });
79
-
80
- return cmd;
12
+ "Checks type errors, ensuring that the code adheres to the defined type constraints and helps catch potential issues before runtime.",
13
+ )
14
+ .action(async () => {
15
+ const tsc = ctx.plugins.getServiceOrThrow("typecheck");
16
+ await tscAction({ ctx, tsc });
17
+ })
18
+ .addHelpTextAfter(ctx)
19
+ .addDoctorCommand(async () => {
20
+ const tsc = ctx.plugins.getServiceOrThrow("typecheck");
21
+ await doctorOneAction({ ctx, service: tsc });
22
+ });
81
23
  }
@@ -1,7 +1,6 @@
1
- import { generateToStdout } from "@usage-spec/commander";
2
- import { palette } from "@vlandoss/clibuddy";
3
- import { type Command, createCommand, Option } from "commander";
4
- import { createContext } from "#src/services/ctx.ts";
1
+ import path from "node:path";
2
+ import { dirnameOf } from "@vlandoss/clibuddy";
3
+ import { ContextService } from "#src/services/context.ts";
5
4
  import { createCheckCommand } from "./commands/check.ts";
6
5
  import { createCleanCommand } from "./commands/clean.ts";
7
6
  import { createCompletionCommand } from "./commands/completion.ts";
@@ -13,38 +12,32 @@ import { createLintCommand } from "./commands/lint.ts";
13
12
  import { createPackCommand } from "./commands/pack.ts";
14
13
  import { createPluginsCommand } from "./commands/plugins.ts";
15
14
  import { createTsCheckCommand } from "./commands/tscheck.ts";
16
- import { CREDITS_TEXT, getBannerText } from "./ui.ts";
15
+ import { RunRunCmd } from "./root.ts";
17
16
 
18
- export type Options = {
19
- binDir: string;
20
- };
17
+ export async function createProgram(meta: ImportMeta) {
18
+ const binDir = path.dirname(dirnameOf(meta));
21
19
 
22
- export async function createProgram(options: Options) {
23
- const ctx = await createContext(options.binDir);
24
- const version = ctx.binPkg.version;
20
+ const ctxService = new ContextService(binDir);
21
+ const ctx = await ctxService.getContext();
25
22
 
26
- const program = createCommand("rr")
27
- .usage("<command...> [options...]")
28
- .enablePositionalOptions()
29
- .version(version, "-v, --version")
30
- .addOption(new Option("--usage", `print KDL spec for this CLI (${palette.muted(palette.link("https://kdl.dev"))})`))
31
- .on("option:usage", function onUsage(this: Command) {
32
- generateToStdout(this);
33
- process.exit(0);
34
- })
35
- .addHelpText("before", getBannerText(version))
36
- .addHelpText("after", CREDITS_TEXT)
37
- .addCommand(createCompletionCommand())
38
- .addCommand(createPackCommand(ctx))
23
+ const cmd = new RunRunCmd(ctx);
24
+
25
+ cmd
26
+ .commandsGroup("Code quality:")
27
+ .addCommand(createCheckCommand(ctx))
39
28
  .addCommand(createJsCheckCommand(ctx))
40
29
  .addCommand(createTsCheckCommand(ctx))
41
30
  .addCommand(createLintCommand(ctx))
42
31
  .addCommand(createFormatCommand(ctx))
43
- .addCommand(createCheckCommand(ctx))
32
+ .commandsGroup("Build:")
33
+ .addCommand(createPackCommand(ctx))
34
+ .commandsGroup("Maintenance:")
35
+ .addCommand(createCleanCommand())
44
36
  .addCommand(createDoctorCommand(ctx))
37
+ .commandsGroup("Meta:")
38
+ .addCommand(createCompletionCommand())
45
39
  .addCommand(createPluginsCommand(ctx))
46
- .addCommand(createCleanCommand())
47
40
  .addCommand(createConfigCommand(ctx));
48
41
 
49
- return { program, ctx };
42
+ return cmd;
50
43
  }
@@ -0,0 +1,62 @@
1
+ import { generateToStdout } from "@usage-spec/commander";
2
+ import { palette } from "@vlandoss/clibuddy";
3
+ import { Command } from "commander";
4
+ import { getBannerText } from "#src/render/banner.ts";
5
+ import { getFooterText } from "#src/render/footer.ts";
6
+ import { Lines } from "#src/render/lines.ts";
7
+ import type { ContextValue } from "#src/services/context.ts";
8
+
9
+ export class RunRunCmd extends Command {
10
+ ctx: ContextValue;
11
+
12
+ constructor(ctx: ContextValue) {
13
+ super("rr");
14
+ this.ctx = ctx;
15
+ this.#init();
16
+ }
17
+
18
+ async run() {
19
+ await this.parseAsync();
20
+ }
21
+
22
+ #init() {
23
+ this.enablePositionalOptions()
24
+ .showSuggestionAfterError(true)
25
+ .helpCommand(false)
26
+ .version(this.ctx.binPkg.version, "-v, --version", "output the version number")
27
+ .addHelpText("before", this.#banner())
28
+ .addHelpText("after", this.#footer())
29
+ .option("--about", "show credits & inspiration")
30
+ .option("--usage", `print KDL spec for this CLI (${palette.dim(palette.link("https://kdl.dev"))})`)
31
+ .on("option:about", () => this.#aboutStdout())
32
+ .on("option:usage", () => this.#usageStdout(this));
33
+ }
34
+
35
+ #aboutStdout() {
36
+ const lines = new Lines();
37
+
38
+ lines
39
+ .add(this.#banner())
40
+ .add(palette.bold("Inspired by:"), 2)
41
+ .add(`kcd-scripts — ${palette.link("https://github.com/kentcdodds/kcd-scripts")}`, 4)
42
+ .newline()
43
+ .add(palette.bold("Named in honor of:"), 2)
44
+ .add(`Run Run (Peruvian news segment) — ${palette.link("https://es.wikipedia.org/wiki/Run_Run")}`, 4)
45
+ .printStdout();
46
+
47
+ process.exit(0);
48
+ }
49
+
50
+ #usageStdout(cmd: Command) {
51
+ generateToStdout(cmd);
52
+ process.exit(0);
53
+ }
54
+
55
+ #banner() {
56
+ return getBannerText(this.ctx.binPkg.version);
57
+ }
58
+
59
+ #footer() {
60
+ return getFooterText(this.ctx);
61
+ }
62
+ }
@@ -0,0 +1,25 @@
1
+ import { palette, text } from "@vlandoss/clibuddy";
2
+ import { runRunColor } from "#src/ui/theme.ts";
3
+ import { Lines } from "./lines.ts";
4
+
5
+ // npx figlet -f "ANSI Shadow" "run-run"
6
+ export function getBannerText(version: string) {
7
+ const UI_LOGO = runRunColor(
8
+ `
9
+ ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
10
+ ██╔══██╗██║ ██║████╗ ██║ ██╔══██╗██║ ██║████╗ ██║
11
+ ██████╔╝██║ ██║██╔██╗ ██║█████╗██████╔╝██║ ██║██╔██╗ ██║
12
+ ██╔══██╗██║ ██║██║╚██╗██║╚════╝██╔══██╗██║ ██║██║╚██╗██║
13
+ ██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║╚██████╔╝██║ ╚████║
14
+ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ${text.version(version)}`.trimStart(),
15
+ );
16
+
17
+ const lines = new Lines();
18
+
19
+ return lines
20
+ .add(UI_LOGO)
21
+ .newline()
22
+ .add(`🦊 ${palette.italic(palette.dim("The CLI toolbox for"))} ${text.vland}`)
23
+ .newline()
24
+ .render();
25
+ }