@rrlab/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +10 -10
  2. package/bin +3 -5
  3. package/dist/cli.usage.kdl +26 -25
  4. package/dist/config.d.mts +1 -1
  5. package/dist/magic-string.es-BgIV5Mu3.mjs +1011 -0
  6. package/dist/plugin/__tests__/bin-probe.test.d.mts +1 -0
  7. package/dist/plugin/__tests__/bin-probe.test.mjs +64 -0
  8. package/dist/plugin/__tests__/decide-scaffold.test.d.mts +1 -0
  9. package/dist/plugin/__tests__/decide-scaffold.test.mjs +103 -0
  10. package/dist/plugin/__tests__/define-plugin.test.d.mts +1 -0
  11. package/dist/plugin/__tests__/define-plugin.test.mjs +130 -0
  12. package/dist/plugin/__tests__/pick-preset.test.d.mts +1 -0
  13. package/dist/plugin/__tests__/pick-preset.test.mjs +72 -0
  14. package/dist/plugin/__tests__/registry.test.d.mts +1 -0
  15. package/dist/plugin/__tests__/registry.test.mjs +104 -0
  16. package/dist/plugin/bin-probe.d.mts +4 -0
  17. package/dist/plugin/bin-probe.mjs +22 -0
  18. package/dist/plugin/decide-scaffold.d.mts +18 -0
  19. package/dist/plugin/decide-scaffold.mjs +36 -0
  20. package/dist/plugin/define-plugin.d.mts +17 -0
  21. package/dist/plugin/define-plugin.mjs +25 -0
  22. package/dist/plugin/directory.d.mts +47 -0
  23. package/dist/plugin/directory.mjs +45 -0
  24. package/dist/plugin/errors.d.mts +11 -0
  25. package/dist/plugin/errors.mjs +15 -0
  26. package/dist/plugin/index.d.mts +7 -0
  27. package/dist/plugin/index.mjs +50 -0
  28. package/dist/plugin/pick-preset.d.mts +13 -0
  29. package/dist/plugin/pick-preset.mjs +17 -0
  30. package/dist/plugin/registry.d.mts +19 -0
  31. package/dist/plugin/registry.mjs +2 -0
  32. package/dist/plugin/tool-service.d.mts +45 -0
  33. package/dist/plugin/tool-service.mjs +64 -0
  34. package/dist/plugin/types.d.mts +3 -0
  35. package/dist/plugin/types.mjs +1 -0
  36. package/dist/registry-BgqfKK5L.mjs +55 -0
  37. package/dist/run.mjs +969 -585
  38. package/dist/test.DNmyFkvJ-09ScyH13.mjs +13617 -0
  39. package/dist/tool-DKL6TauZ.d.mts +43 -0
  40. package/dist/{types-snfbujDH.d.mts → types-Iu4IyWof.d.mts} +11 -75
  41. package/package.json +6 -5
  42. package/src/actions/clean.ts +36 -0
  43. package/src/actions/config.ts +46 -0
  44. package/src/actions/doctor.ts +47 -0
  45. package/src/actions/format.ts +13 -0
  46. package/src/actions/jsc.ts +13 -0
  47. package/src/actions/lint.ts +13 -0
  48. package/src/actions/pack.ts +12 -0
  49. package/src/actions/plugins/add.ts +143 -0
  50. package/src/actions/plugins/list.ts +27 -0
  51. package/src/actions/plugins/remove.ts +110 -0
  52. package/src/actions/plugins/shared.ts +58 -0
  53. package/src/actions/run-tool.ts +23 -0
  54. package/src/actions/tsc.ts +65 -0
  55. package/src/errors/invalid-plugin-module.ts +6 -0
  56. package/src/errors/missing-plugin.ts +17 -0
  57. package/src/errors/plugin-api-version.ts +6 -0
  58. package/src/errors/unknown-plugin.ts +7 -0
  59. package/src/lib/plugin/define-plugin.ts +56 -0
  60. package/src/lib/plugin/directory.ts +30 -0
  61. package/src/lib/plugin/errors.ts +15 -0
  62. package/src/lib/{plugin.ts → plugin/index.ts} +8 -9
  63. package/src/lib/plugin/registry.ts +82 -0
  64. package/src/{plugin → lib/plugin}/tool-service.ts +10 -14
  65. package/src/{plugin → lib/plugin}/types.ts +10 -33
  66. package/src/program/base.ts +75 -0
  67. package/src/program/commands/check.ts +31 -62
  68. package/src/program/commands/clean.ts +12 -43
  69. package/src/program/commands/completion.ts +6 -4
  70. package/src/program/commands/config.ts +6 -11
  71. package/src/program/commands/doctor.ts +5 -54
  72. package/src/program/commands/format.ts +18 -25
  73. package/src/program/commands/jscheck.ts +18 -31
  74. package/src/program/commands/lint.ts +18 -26
  75. package/src/program/commands/pack.ts +18 -22
  76. package/src/program/commands/plugins.ts +17 -364
  77. package/src/program/commands/tscheck.ts +19 -77
  78. package/src/program/index.ts +20 -27
  79. package/src/program/root.ts +62 -0
  80. package/src/render/banner.ts +25 -0
  81. package/src/render/board.ts +41 -0
  82. package/src/render/footer.ts +31 -0
  83. package/src/render/labels.ts +28 -0
  84. package/src/render/lines.ts +100 -0
  85. package/src/render/plugin-view.ts +68 -0
  86. package/src/render/steps.ts +20 -0
  87. package/src/run.ts +2 -8
  88. package/src/services/config.ts +4 -0
  89. package/src/services/context.ts +84 -0
  90. package/src/services/file-ops.ts +79 -0
  91. package/src/services/json-edit.ts +1 -1
  92. package/src/services/plugin-meta.ts +63 -0
  93. package/src/services/plugin-services.ts +41 -0
  94. package/src/services/prompts.ts +1 -1
  95. package/src/services/static-checker.ts +46 -0
  96. package/src/types/config.ts +2 -1
  97. package/src/types/tool.ts +13 -26
  98. package/src/ui/theme.ts +5 -0
  99. package/dist/plugin.d.mts +0 -87
  100. package/dist/plugin.mjs +0 -214
  101. package/src/plugin/define-plugin.ts +0 -54
  102. package/src/plugin/registry.ts +0 -48
  103. package/src/program/board.ts +0 -86
  104. package/src/program/composed-jsc.ts +0 -43
  105. package/src/program/missing-plugin.ts +0 -18
  106. package/src/program/ui.ts +0 -59
  107. package/src/services/ctx.ts +0 -71
  108. package/src/services/plugins-registry.ts +0 -22
  109. /package/src/{plugin → lib/plugin}/bin-probe.ts +0 -0
  110. /package/src/{plugin → lib/plugin}/decide-scaffold.ts +0 -0
  111. /package/src/{plugin → lib/plugin}/pick-preset.ts +0 -0
@@ -0,0 +1,58 @@
1
+ import * as clack from "@clack/prompts";
2
+ import type { InstallContext, InstallResult, UninstallContext, UninstallResult } from "#src/lib/plugin/types.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import type { FileOpOutcome } from "#src/services/file-ops.ts";
5
+
6
+ export type AddOptions = {
7
+ force?: boolean;
8
+ yes?: boolean;
9
+ dryRun?: boolean;
10
+ };
11
+
12
+ export type RemoveOptions = {
13
+ yes?: boolean;
14
+ dryRun?: boolean;
15
+ };
16
+
17
+ type InstallHook = (ctx: InstallContext) => Promise<InstallResult>;
18
+ type UninstallHook = (ctx: UninstallContext) => Promise<UninstallResult>;
19
+ export type PluginModule = { default?: (opts?: unknown) => { install?: InstallHook; uninstall?: UninstallHook } };
20
+
21
+ export function hasInPackageJson(ctx: ContextValue, pkgName: string): boolean {
22
+ const pkg = ctx.appPkg.packageJson;
23
+ const deps = {
24
+ ...pkg.dependencies,
25
+ ...pkg.devDependencies,
26
+ ...pkg.peerDependencies,
27
+ };
28
+ return pkgName in deps;
29
+ }
30
+
31
+ /** Renders a `FileOpOutcome` (from the `file-ops` engine) as the matching clack log line. */
32
+ export function reportFileOp(outcome: FileOpOutcome): void {
33
+ switch (outcome.status) {
34
+ case "skipped-exists":
35
+ clack.log.warn(`Skipping ${outcome.path} — already exists. Use --force to overwrite.`);
36
+ return;
37
+ case "created":
38
+ clack.log.success(`Created ${outcome.path}`);
39
+ return;
40
+ case "overwritten":
41
+ clack.log.success(`Overwrote ${outcome.path}`);
42
+ return;
43
+ case "missing":
44
+ clack.log.warn(`Skipping ${outcome.path} — file does not exist.`);
45
+ return;
46
+ case "edited":
47
+ clack.log.success(`Edited ${outcome.path}`);
48
+ return;
49
+ case "unchanged":
50
+ clack.log.info(`No changes for ${outcome.path}.`);
51
+ return;
52
+ case "deleted":
53
+ clack.log.success(`Deleted ${outcome.path}`);
54
+ return;
55
+ case "absent":
56
+ return;
57
+ }
58
+ }
@@ -0,0 +1,23 @@
1
+ import { reportTask, runBoard } from "#src/render/board.ts";
2
+ import { type Provider, targetLabel } from "#src/render/labels.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import type { RunReport } from "#src/types/tool.ts";
5
+
6
+ type RunToolActionOptions<P extends Provider> = {
7
+ ctx: ContextValue;
8
+ name: string;
9
+ provider: P;
10
+ run: (provider: P) => Promise<RunReport>;
11
+ };
12
+
13
+ /**
14
+ * The shared action for a single-provider tool command (lint, format, jsc,
15
+ * pack): run the provider's verb as one board row labelled `<name> (<tool>) ·
16
+ * <pkg>`, and aggregate the exit code. The command resolves the provider and
17
+ * throws MissingPluginError when it's absent, so the provider is required here.
18
+ * Commands that fan out (tsc) or compose siblings (check) drive the board themselves.
19
+ */
20
+ export async function runToolAction<P extends Provider>({ ctx, name, provider, run }: RunToolActionOptions<P>): Promise<void> {
21
+ const result = await runBoard([reportTask(targetLabel(name, provider, ctx.appPkg), () => run(provider))]);
22
+ if (!result.ok) process.exitCode = 1;
23
+ }
@@ -0,0 +1,65 @@
1
+ import { type BoardTask, reportTask, runBoard } from "#src/render/board.ts";
2
+ import { fanoutTitle, targetLabel } from "#src/render/labels.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import { logger } from "#src/services/logger.ts";
5
+ import type { Doctor, TypeChecker } from "#src/types/tool.ts";
6
+
7
+ export type TscActionConfig = {
8
+ ctx: ContextValue;
9
+ tsc: TypeChecker & Doctor;
10
+ };
11
+
12
+ type Scripts = Record<string, string | undefined> | undefined;
13
+
14
+ const getPreScript = (scripts: Scripts) => scripts?.pretsc ?? scripts?.pretypecheck;
15
+
16
+ export async function tscAction({ ctx, tsc }: TscActionConfig): Promise<void> {
17
+ const { appPkg, shell } = ctx;
18
+
19
+ const isTsProject = (dir: string) => appPkg.hasFile("tsconfig.json", dir);
20
+
21
+ // A package's `pretsc`/`pretypecheck` runs captured, inside the task, so its
22
+ // output stays grouped with that package. It may use shell features, so it
23
+ // goes through `/bin/sh -c`. A failing pre-script fails the task before tsc.
24
+ const typecheckTask = (label: string, dir: string, scripts: Scripts): BoardTask =>
25
+ reportTask(label, async () => {
26
+ const preScript = getPreScript(scripts);
27
+ if (preScript) {
28
+ const pre = await shell.at(dir).runCaptured(preScript, [], { shell: true, throwOnError: false });
29
+ if ((pre.exitCode ?? 0) !== 0) {
30
+ const output = [pre.stdout, pre.stderr]
31
+ .map((s) => s?.trim())
32
+ .filter(Boolean)
33
+ .join("\n");
34
+ return { ok: false, output: `pre-script \`${preScript}\` failed\n${output}` };
35
+ }
36
+ }
37
+ return tsc.check({ cwd: dir });
38
+ });
39
+
40
+ if (!appPkg.isMonorepo()) {
41
+ if (!isTsProject(appPkg.dirPath)) {
42
+ logger.info("No tsconfig.json found, skipping typecheck");
43
+ return;
44
+ }
45
+
46
+ // Single package → compact board; the row carries the canonical
47
+ // `tsc (<tool>) · <pkg>` label like every other single-target command.
48
+ const label = targetLabel("tsc", tsc, appPkg);
49
+ const result = await runBoard([typecheckTask(label, appPkg.dirPath, appPkg.packageJson.scripts)]);
50
+ if (!result.ok) process.exitCode = 1;
51
+ return;
52
+ }
53
+
54
+ const projects = await appPkg.getWorkspaceProjects();
55
+ const tsProjects = projects.filter((project) => isTsProject(project.rootDir));
56
+
57
+ if (!tsProjects.length) {
58
+ logger.warn("No ts projects found in the monorepo, skipping typecheck");
59
+ return;
60
+ }
61
+
62
+ const tasks = tsProjects.map((p) => typecheckTask(p.manifest.name ?? p.rootDir, p.rootDir, p.manifest.scripts));
63
+ const result = await runBoard(tasks, { title: fanoutTitle("tsc", tsc, tsProjects.length, "packages") });
64
+ if (!result.ok) process.exitCode = 1;
65
+ }
@@ -0,0 +1,6 @@
1
+ /** Thrown when an installed plugin package doesn't export a default factory function. */
2
+ export class InvalidPluginModuleError extends Error {
3
+ constructor(pkgName: string) {
4
+ super(`Plugin '${pkgName}' did not export a default factory function.`);
5
+ }
6
+ }
@@ -0,0 +1,17 @@
1
+ import { providersOf } from "#src/lib/plugin/directory.ts";
2
+ import type { PluginCapability } from "../lib/plugin/index.ts";
3
+
4
+ export class MissingPluginError extends Error {
5
+ constructor(capability: PluginCapability) {
6
+ const plugins = providersOf(capability);
7
+
8
+ const pkgList = plugins.map((it) => it.pkg).join(", ");
9
+ const addList = plugins.map((it) => `rr plugins add ${it.name}`).join(" | ");
10
+
11
+ super(
12
+ `No plugin provides the '${capability}' capability.` +
13
+ (pkgList ? `\n Install one of: ${pkgList}.` : "") +
14
+ (addList ? `\n Try: ${addList}.` : ""),
15
+ );
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /** Thrown when a configured plugin targets an apiVersion the kernel doesn't support. */
2
+ export class PluginApiVersionError extends Error {
3
+ constructor(pluginName: string, got: number) {
4
+ super(`Plugin '${pluginName}' targets apiVersion ${got}, but this kernel supports only apiVersion 1.`);
5
+ }
6
+ }
@@ -0,0 +1,7 @@
1
+ import { allPluginNames } from "#src/lib/plugin/directory.ts";
2
+
3
+ export class UnknownPluginError extends Error {
4
+ constructor(name: string) {
5
+ super(`'${name}' is invalid for argument 'name'. Allowed choices are ${allPluginNames().join(", ")}.`);
6
+ }
7
+ }
@@ -0,0 +1,56 @@
1
+ import { probeBins } from "./bin-probe.ts";
2
+ import type {
3
+ InstallContext,
4
+ InstallResult,
5
+ Plugin,
6
+ PluginCapability,
7
+ PluginContext,
8
+ PluginServices,
9
+ UninstallContext,
10
+ UninstallResult,
11
+ } from "./types.ts";
12
+
13
+ export type PluginDefinition<TServices extends PluginServices> = {
14
+ apiVersion: 1;
15
+ name: string;
16
+ color: (value: string) => string;
17
+ services(ctx: PluginContext): TServices | Promise<TServices>;
18
+ install?(this: void, ctx: InstallContext): Promise<InstallResult>;
19
+ uninstall?(this: void, ctx: UninstallContext): Promise<UninstallResult>;
20
+ };
21
+
22
+ type Options<TKind extends PluginCapability> = { only?: readonly TKind[] };
23
+
24
+ export function definePlugin<TServices extends PluginServices>(
25
+ definition: PluginDefinition<TServices>,
26
+ ): (options?: Options<keyof TServices & PluginCapability>) => Plugin {
27
+ return (options) => {
28
+ const only = options?.only;
29
+ const pkgName = `@rrlab/${definition.name}-plugin`;
30
+
31
+ return {
32
+ name: definition.name,
33
+ color: definition.color,
34
+ ui: definition.color(definition.name),
35
+ apiVersion: definition.apiVersion,
36
+ install: definition.install,
37
+ uninstall: definition.uninstall,
38
+ async services(ctx: PluginContext): Promise<PluginServices> {
39
+ const services = await definition.services(ctx);
40
+ await probeBins(Object.values(services), definition.name);
41
+
42
+ if (!only) {
43
+ return services;
44
+ }
45
+
46
+ for (const k of only) {
47
+ if (!(k in services)) {
48
+ throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(services).join(", ")}.`);
49
+ }
50
+ }
51
+
52
+ return Object.fromEntries(only.map((capability) => [capability, services[capability]]));
53
+ },
54
+ };
55
+ };
56
+ }
@@ -0,0 +1,30 @@
1
+ import type { PluginCapability } from "./types.ts";
2
+
3
+ type PluginInfo = {
4
+ readonly pkg: string;
5
+ readonly name: string;
6
+ readonly capabilities: PluginCapability[];
7
+ };
8
+
9
+ export const PLUGINS_DIRECTORY = {
10
+ ts: { pkg: "@rrlab/ts-plugin", name: "ts", capabilities: ["typecheck"] },
11
+ biome: { pkg: "@rrlab/biome-plugin", name: "biome", capabilities: ["format", "jscheck", "lint"] },
12
+ oxc: { pkg: "@rrlab/oxc-plugin", name: "oxc", capabilities: ["format", "lint", "jscheck", "typecheck"] },
13
+ tsdown: { pkg: "@rrlab/tsdown-plugin", name: "tsdown", capabilities: ["pack"] },
14
+ } as const satisfies Record<string, PluginInfo>;
15
+
16
+ export type PluginName = keyof typeof PLUGINS_DIRECTORY;
17
+
18
+ export function allPluginNames(): readonly PluginName[] {
19
+ return Object.keys(PLUGINS_DIRECTORY) as PluginName[];
20
+ }
21
+
22
+ export function isPluginName(name: string): name is PluginName {
23
+ return Object.hasOwn(PLUGINS_DIRECTORY, name);
24
+ }
25
+
26
+ export function providersOf(capability: PluginCapability) {
27
+ return Object.values(PLUGINS_DIRECTORY).filter((info: PluginInfo) => {
28
+ return info.capabilities.includes(capability);
29
+ });
30
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Thrown by `PluginRegistry.get` when more than one plugin provides the same
3
+ * capability. The message is load-bearing for the "ambiguity → user narrows
4
+ * config" UX (decision 003) — `registry.test.ts` asserts the plugin names appear.
5
+ */
6
+ export class MultipleProvidersError extends Error {
7
+ constructor(kind: string, pluginNames: readonly string[]) {
8
+ const names = pluginNames.join(", ");
9
+ const example = pluginNames.map((name) => `${name}({ only: ['${kind}'] })`).join(" or ");
10
+ super(
11
+ `Multiple plugins provide capability '${kind}': ${names}. ` +
12
+ `Narrow each plugin's capabilities in run-run.config.ts using the 'only' option — e.g. ${example}.`,
13
+ );
14
+ }
15
+ }
@@ -1,7 +1,8 @@
1
- export { type DecideScaffoldOptions, decideScaffold, type ScaffoldDecision } from "#src/plugin/decide-scaffold.ts";
2
- export { definePlugin, type PluginDefinition } from "#src/plugin/define-plugin.ts";
3
- export { type PickPresetOptions, pickPreset } from "#src/plugin/pick-preset.ts";
4
- export { ToolService, type ToolServiceOptions } from "#src/plugin/tool-service.ts";
1
+ export { ReleaseService, type ReleaseServiceOptions } from "#src/services/release.ts";
2
+ export { type DecideScaffoldOptions, decideScaffold, type ScaffoldDecision } from "./decide-scaffold.ts";
3
+ export { definePlugin, type PluginDefinition } from "./define-plugin.ts";
4
+ export { type PickPresetOptions, pickPreset } from "./pick-preset.ts";
5
+ export { ToolService, type ToolServiceOptions } from "./tool-service.ts";
5
6
  export type {
6
7
  ClackPrompts,
7
8
  ClackPromptsSelectOption,
@@ -17,9 +18,9 @@ export type {
17
18
  LintOptions,
18
19
  Packer,
19
20
  Plugin,
20
- PluginCapabilities,
21
+ PluginCapability,
21
22
  PluginContext,
22
- PluginKind,
23
+ PluginServices,
23
24
  RunReport,
24
25
  StaticChecker,
25
26
  StaticCheckerOptions,
@@ -28,6 +29,4 @@ export type {
28
29
  UninstallContext,
29
30
  UninstallFlags,
30
31
  UninstallResult,
31
- } from "#src/plugin/types.ts";
32
- export { PLUGIN_KINDS } from "#src/plugin/types.ts";
33
- export { ReleaseService, type ReleaseServiceOptions } from "#src/services/release.ts";
32
+ } from "./types.ts";
@@ -0,0 +1,82 @@
1
+ import { MissingPluginError } from "#src/errors/missing-plugin.ts";
2
+ import { MultipleProvidersError } from "./errors.ts";
3
+ import type { Plugin, PluginCapability, PluginServices } from "./types.ts";
4
+
5
+ type RegistryEntry = {
6
+ plugin: Plugin;
7
+ services: PluginServices;
8
+ };
9
+
10
+ type Provider<K extends PluginCapability> = {
11
+ plugin: Plugin;
12
+ service: NonNullable<PluginServices[K]>;
13
+ };
14
+
15
+ export class PluginRegistry {
16
+ #entries: RegistryEntry[] = [];
17
+
18
+ register(plugin: Plugin, services: PluginServices) {
19
+ this.#entries.push({ plugin, services });
20
+ }
21
+
22
+ getServiceOrThrow<K extends PluginCapability>(capability: K) {
23
+ const provider = this.providerOf(capability);
24
+ if (!provider) {
25
+ throw new MissingPluginError(capability);
26
+ }
27
+ return provider.service;
28
+ }
29
+
30
+ getService<K extends PluginCapability>(capability: K) {
31
+ const provider = this.providerOf(capability);
32
+ return provider?.service;
33
+ }
34
+
35
+ getAllServices() {
36
+ const seen = new Set<NonNullable<PluginServices[PluginCapability]>>();
37
+
38
+ for (const { services } of this.#entries) {
39
+ for (const service of Object.values(services)) {
40
+ if (service) seen.add(service);
41
+ }
42
+ }
43
+
44
+ return [...seen];
45
+ }
46
+
47
+ providersOf<K extends PluginCapability>(capability: K) {
48
+ const providers: Array<Provider<K>> = [];
49
+
50
+ for (const { plugin, services } of this.#entries) {
51
+ const service = services[capability];
52
+
53
+ if (service) {
54
+ providers.push({
55
+ plugin,
56
+ service: service as NonNullable<PluginServices[K]>,
57
+ });
58
+ }
59
+ }
60
+
61
+ return providers;
62
+ }
63
+
64
+ providerOf<K extends PluginCapability>(capability: K) {
65
+ const providers = this.providersOf(capability);
66
+
67
+ const [first, ...rest] = providers;
68
+
69
+ if (!first) {
70
+ return undefined;
71
+ }
72
+
73
+ if (rest.length > 0) {
74
+ throw new MultipleProvidersError(
75
+ capability,
76
+ providers.map(({ plugin }) => plugin.name),
77
+ );
78
+ }
79
+
80
+ return first;
81
+ }
82
+ }
@@ -2,9 +2,9 @@ import { palette, resolvePackageBin, type ShellService } from "@vlandoss/clibudd
2
2
  import type { RunReport } from "#src/types/tool.ts";
3
3
 
4
4
  export type ToolServiceOptions = {
5
- pkg: string;
6
- bin?: string;
7
- ui: string;
5
+ pkg?: string;
6
+ bin: string;
7
+ color: (str: string) => string;
8
8
  shellService: ShellService;
9
9
  /**
10
10
  * Module URL the resolver walks up from when looking for `pkg` in
@@ -24,25 +24,21 @@ export class ToolService {
24
24
  #shellService: ShellService;
25
25
  #pkg: string;
26
26
  #bin: string;
27
- #ui: string;
28
27
  #from: string;
28
+ #ui: string;
29
29
 
30
- get bin() {
31
- return this.#bin;
30
+ get pkg() {
31
+ return this.#pkg;
32
32
  }
33
33
 
34
34
  get ui() {
35
35
  return this.#ui;
36
36
  }
37
37
 
38
- get pkg() {
39
- return this.#pkg;
40
- }
41
-
42
- constructor({ pkg, bin, ui, shellService, from }: ToolServiceOptions) {
43
- this.#pkg = pkg;
44
- this.#bin = bin ?? pkg;
45
- this.#ui = ui;
38
+ constructor({ pkg, bin, color, shellService, from }: ToolServiceOptions) {
39
+ this.#bin = bin;
40
+ this.#pkg = pkg ?? bin;
41
+ this.#ui = color(bin);
46
42
  this.#shellService = shellService;
47
43
  this.#from = from;
48
44
  }
@@ -1,36 +1,17 @@
1
1
  import type { Pkg, ShellService } from "@vlandoss/clibuddy";
2
2
  import type { AnyLogger as Logger } from "@vlandoss/loggy";
3
3
  import type { ReleaseService } from "#src/services/release.ts";
4
- import type { Doctor, Formatter, Linter, RunReport, StaticChecker, TypeChecker } from "#src/types/tool.ts";
4
+ import type { Doctor, Formatter, Linter, Packer, StaticChecker, TypeChecker } from "#src/types/tool.ts";
5
5
 
6
- export type {
7
- Doctor,
8
- FormatOptions,
9
- Formatter,
10
- Linter,
11
- LintOptions,
12
- RunReport,
13
- StaticChecker,
14
- StaticCheckerOptions,
15
- TypeChecker,
16
- TypeCheckOptions,
17
- } from "#src/types/tool.ts";
6
+ export type * from "#src/types/tool.ts";
18
7
 
19
- export type Packer = {
20
- bin: string;
21
- ui: string;
22
- pack: () => Promise<RunReport>;
23
- };
24
-
25
- export const PLUGIN_KINDS = ["lint", "format", "jsc", "tsc", "pack"] as const;
26
-
27
- export type PluginKind = (typeof PLUGIN_KINDS)[number];
8
+ export type PluginCapability = keyof PluginServices;
28
9
 
29
- export type PluginCapabilities = {
10
+ export type PluginServices = {
30
11
  lint?: Linter & Doctor;
31
12
  format?: Formatter & Doctor;
32
- jsc?: StaticChecker & Doctor;
33
- tsc?: TypeChecker & Doctor;
13
+ jscheck?: StaticChecker & Doctor;
14
+ typecheck?: TypeChecker & Doctor;
34
15
  pack?: Packer & Doctor;
35
16
  };
36
17
 
@@ -43,9 +24,11 @@ export type PluginContext = {
43
24
  };
44
25
 
45
26
  export type Plugin = {
46
- name: string;
47
27
  apiVersion: 1;
48
- capabilities(ctx: PluginContext): Promise<PluginCapabilities>;
28
+ name: string;
29
+ readonly ui: string;
30
+ color(label: string): string;
31
+ services(ctx: PluginContext): Promise<PluginServices>;
49
32
  install?(ctx: InstallContext): Promise<InstallResult>;
50
33
  uninstall?(ctx: UninstallContext): Promise<UninstallResult>;
51
34
  };
@@ -83,12 +66,6 @@ export type InstallContext = {
83
66
  appPkg: Pkg;
84
67
  prompts: ClackPrompts;
85
68
  flags: InstallFlags;
86
- /**
87
- * The release this install is running against — encapsulates the dist-tag the
88
- * user picked (`rr plugins add biome@pr-226`) and resolves install specs for
89
- * related packages under it. Use `ctx.release.resolve(pkg)` for every
90
- * `@rrlab/*-config` sibling so previews and `latest` work uniformly.
91
- */
92
69
  release: ReleaseService;
93
70
  };
94
71
 
@@ -0,0 +1,75 @@
1
+ import { Command } from "commander";
2
+ import { Lines } from "#src/render/lines.ts";
3
+ import type { ContextValue } from "#src/services/context.ts";
4
+ import type { PluginCapability } from "../lib/plugin/index.ts";
5
+
6
+ class Cmd extends Command {
7
+ capabilities: PluginCapability[] = [];
8
+
9
+ addCapabilities(capabilities: PluginCapability[]) {
10
+ this.capabilities = capabilities;
11
+ return this;
12
+ }
13
+
14
+ addHelpTextAfter(ctx: ContextValue) {
15
+ super.addHelpText("after", () => {
16
+ const seeAlso = new Set<string>();
17
+
18
+ this.parent?.commands?.forEach((cmd) => {
19
+ if (cmd instanceof Cmd) {
20
+ const sameWork = cmd.capabilities.some((it) => this.capabilities.includes(it));
21
+ if (sameWork && cmd.name() !== this.name()) {
22
+ seeAlso.add(cmd.name());
23
+ }
24
+ }
25
+ });
26
+
27
+ const poweredBy = new Set<string>();
28
+
29
+ this.capabilities.forEach((it) => {
30
+ const provider = ctx.plugins.providerOf(it);
31
+
32
+ if (provider) {
33
+ poweredBy.add(provider.plugin.ui);
34
+ }
35
+ });
36
+
37
+ const lines = new Lines();
38
+
39
+ if (seeAlso.size > 0) {
40
+ lines.add("See also:").add(
41
+ [...seeAlso].map((it) => `- ${it}`),
42
+ 2,
43
+ );
44
+ }
45
+
46
+ if (seeAlso.size > 0 && poweredBy.size > 0) {
47
+ lines.newline();
48
+ }
49
+
50
+ if (poweredBy.size > 0) {
51
+ lines.add("Powered by:").add(
52
+ [...poweredBy].map((it) => `- ${it}`),
53
+ 2,
54
+ );
55
+ }
56
+
57
+ if (lines.isEmpty()) {
58
+ return "";
59
+ }
60
+
61
+ return lines.newline(true).render();
62
+ });
63
+
64
+ return this;
65
+ }
66
+
67
+ addDoctorCommand(actionFn: () => Promise<void>) {
68
+ const cmd = new Command("doctor").summary("check if the underlying tool is working correctly").action(actionFn);
69
+ return this.addCommand(cmd);
70
+ }
71
+ }
72
+
73
+ export function createCommand(name: string) {
74
+ return new Cmd(name);
75
+ }