@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
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,64 @@
1
+ import { ToolService } from "../tool-service.mjs";
2
+ import { probeBins } from "../bin-probe.mjs";
3
+ import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
4
+ //#region src/lib/plugin/__tests__/bin-probe.test.ts
5
+ const FROM = import.meta.url;
6
+ var ResolvableService = class extends ToolService {
7
+ constructor() {
8
+ super({
9
+ pkg: "typescript",
10
+ bin: "tsc",
11
+ color: (s) => s,
12
+ shellService: {},
13
+ from: FROM
14
+ });
15
+ }
16
+ };
17
+ var MissingService = class extends ToolService {
18
+ constructor() {
19
+ super({
20
+ bin: "ghostly-bin-that-does-not-exist",
21
+ color: (s) => s,
22
+ shellService: {},
23
+ from: FROM
24
+ });
25
+ }
26
+ };
27
+ var AlsoMissing = class extends ToolService {
28
+ constructor() {
29
+ super({
30
+ bin: "another-ghost-bin",
31
+ color: (s) => s,
32
+ shellService: {},
33
+ from: FROM
34
+ });
35
+ }
36
+ };
37
+ describe("probeBins", () => {
38
+ it("is a no-op when given an empty list", async () => {
39
+ await globalExpect(probeBins([], "noop")).resolves.toBeUndefined();
40
+ });
41
+ it("ignores non-ToolService values silently", async () => {
42
+ await globalExpect(probeBins([
43
+ { random: "object" },
44
+ 42,
45
+ null
46
+ ], "noop")).resolves.toBeUndefined();
47
+ });
48
+ it("succeeds when every distinct pkg resolves", async () => {
49
+ await globalExpect(probeBins([new ResolvableService(), new ResolvableService()], "ts")).resolves.toBeUndefined();
50
+ });
51
+ it("throws with the canonical message listing the missing pkg", async () => {
52
+ await globalExpect(probeBins([new MissingService()], "ghostly")).rejects.toThrow(/@rrlab\/ghostly-plugin requires ghostly-bin-that-does-not-exist to be installed in the host project\. Run: rr plugins add ghostly {2}\(or: pnpm add -D ghostly-bin-that-does-not-exist\)/);
53
+ });
54
+ it("lists multiple distinct missing pkgs in the same error", async () => {
55
+ await globalExpect(probeBins([new MissingService(), new AlsoMissing()], "ghostly")).rejects.toThrow(/requires .*(ghostly-bin-that-does-not-exist|another-ghost-bin).*(another-ghost-bin|ghostly-bin-that-does-not-exist)/);
56
+ });
57
+ it("deduplicates services sharing a pkg into a single probe", async () => {
58
+ const err = await probeBins([new MissingService(), new MissingService()], "ghostly").catch((e) => e);
59
+ globalExpect(err).toBeInstanceOf(Error);
60
+ globalExpect(err.message.match(/ghostly-bin-that-does-not-exist/g)?.length).toBe(2);
61
+ });
62
+ });
63
+ //#endregion
64
+ export {};
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,103 @@
1
+ import { decideScaffold } from "../decide-scaffold.mjs";
2
+ import { i as it, n as vi, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
3
+ //#region src/lib/plugin/__tests__/decide-scaffold.test.ts
4
+ function makeCtx(overrides = {}) {
5
+ return {
6
+ shell: {},
7
+ logger: {},
8
+ appPkg: { dirPath: process.cwd() },
9
+ prompts: overrides.prompts ?? {},
10
+ flags: {
11
+ force: false,
12
+ yes: false,
13
+ nonInteractive: false,
14
+ ...overrides.flags
15
+ },
16
+ release: {}
17
+ };
18
+ }
19
+ describe("decideScaffold", () => {
20
+ describe("unattended (--yes or non-interactive)", () => {
21
+ it("returns 'create' when file does not exist", async () => {
22
+ globalExpect(await decideScaffold(makeCtx({ flags: { yes: true } }), {
23
+ label: "biome.json",
24
+ fileExists: false,
25
+ patchHint: "x"
26
+ })).toBe("create");
27
+ });
28
+ it("returns 'patch' when file exists and default unattendedExistingAction", async () => {
29
+ globalExpect(await decideScaffold(makeCtx({ flags: { yes: true } }), {
30
+ label: "biome.json",
31
+ fileExists: true,
32
+ patchHint: "x"
33
+ })).toBe("patch");
34
+ });
35
+ it("returns 'skip' when file exists and unattendedExistingAction: 'skip'", async () => {
36
+ globalExpect(await decideScaffold(makeCtx({ flags: { nonInteractive: true } }), {
37
+ label: "tsdown.config.ts",
38
+ fileExists: true,
39
+ patchHint: "x",
40
+ unattendedExistingAction: "skip"
41
+ })).toBe("skip");
42
+ });
43
+ });
44
+ describe("interactive", () => {
45
+ it("confirms creation when file does not exist", async () => {
46
+ const confirm = vi.fn().mockResolvedValue(true);
47
+ globalExpect(await decideScaffold(makeCtx({ prompts: {
48
+ confirm,
49
+ select: vi.fn(),
50
+ isCancel: () => false
51
+ } }), {
52
+ label: "biome.json",
53
+ fileExists: false,
54
+ patchHint: "x"
55
+ })).toBe("create");
56
+ globalExpect(confirm).toHaveBeenCalledWith(globalExpect.objectContaining({ message: globalExpect.stringContaining("biome.json") }));
57
+ });
58
+ it("returns 'skip' when user declines the create confirm", async () => {
59
+ globalExpect(await decideScaffold(makeCtx({ prompts: {
60
+ confirm: vi.fn().mockResolvedValue(false),
61
+ select: vi.fn(),
62
+ isCancel: () => false
63
+ } }), {
64
+ label: "biome.json",
65
+ fileExists: false,
66
+ patchHint: "x"
67
+ })).toBe("skip");
68
+ });
69
+ it("shows the patch/skip/overwrite select when file exists", async () => {
70
+ const select = vi.fn().mockResolvedValue("overwrite");
71
+ globalExpect(await decideScaffold(makeCtx({ prompts: {
72
+ confirm: vi.fn(),
73
+ select,
74
+ isCancel: () => false
75
+ } }), {
76
+ label: "biome.json",
77
+ fileExists: true,
78
+ patchHint: "add @rrlab/biome-config to extends"
79
+ })).toBe("overwrite");
80
+ const call = select.mock.calls[0]?.[0];
81
+ globalExpect(call.options.map((o) => o.value).sort()).toEqual([
82
+ "overwrite",
83
+ "patch",
84
+ "skip"
85
+ ]);
86
+ globalExpect(call.options.find((o) => o.value === "patch")?.label).toMatch(/add @rrlab\/biome-config to extends/);
87
+ });
88
+ it("throws 'Cancelled by user.' when the user cancels", async () => {
89
+ const cancelSymbol = Symbol("cancel");
90
+ await globalExpect(decideScaffold(makeCtx({ prompts: {
91
+ confirm: vi.fn().mockResolvedValue(cancelSymbol),
92
+ select: vi.fn(),
93
+ isCancel: (v) => v === cancelSymbol
94
+ } }), {
95
+ label: "x",
96
+ fileExists: false,
97
+ patchHint: "y"
98
+ })).rejects.toThrow("Cancelled by user.");
99
+ });
100
+ });
101
+ });
102
+ //#endregion
103
+ export {};
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,130 @@
1
+ import { ToolService } from "../tool-service.mjs";
2
+ import { definePlugin } from "../define-plugin.mjs";
3
+ import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
4
+ //#region src/lib/plugin/__tests__/define-plugin.test.ts
5
+ function ctx() {
6
+ return {
7
+ shell: {},
8
+ logger: {},
9
+ appPkg: { dirPath: process.cwd() },
10
+ binPkg: { dirPath: process.cwd() },
11
+ cwd: process.cwd()
12
+ };
13
+ }
14
+ const FROM = import.meta.url;
15
+ const emptyReport = {
16
+ ok: true,
17
+ output: ""
18
+ };
19
+ var FakeBiome = class extends ToolService {
20
+ constructor() {
21
+ super({
22
+ pkg: "@biomejs/biome",
23
+ bin: "biome",
24
+ color: (s) => s,
25
+ shellService: {},
26
+ from: FROM
27
+ });
28
+ }
29
+ async lint() {
30
+ return emptyReport;
31
+ }
32
+ async format() {
33
+ return emptyReport;
34
+ }
35
+ async check() {
36
+ return emptyReport;
37
+ }
38
+ };
39
+ var FakeTsc = class extends ToolService {
40
+ constructor() {
41
+ super({
42
+ pkg: "typescript",
43
+ bin: "tsc",
44
+ color: (s) => s,
45
+ shellService: {},
46
+ from: FROM
47
+ });
48
+ }
49
+ async check() {
50
+ return emptyReport;
51
+ }
52
+ };
53
+ var MissingService = class extends ToolService {
54
+ constructor() {
55
+ super({
56
+ bin: "ghostly-bin-that-does-not-exist",
57
+ color: (s) => s,
58
+ shellService: {},
59
+ from: FROM
60
+ });
61
+ }
62
+ async lint() {
63
+ return emptyReport;
64
+ }
65
+ };
66
+ describe("definePlugin", () => {
67
+ it("returns a Plugin shape the registry expects", async () => {
68
+ const plugin = definePlugin({
69
+ name: "fake-linter",
70
+ apiVersion: 1,
71
+ color: (s) => s,
72
+ services: () => ({ lint: new FakeBiome() })
73
+ })();
74
+ globalExpect(plugin.name).toBe("fake-linter");
75
+ globalExpect(plugin.apiVersion).toBe(1);
76
+ globalExpect(typeof plugin.services).toBe("function");
77
+ const caps = await plugin.services(ctx());
78
+ globalExpect(Object.keys(caps)).toEqual(["lint"]);
79
+ });
80
+ it("filters capabilities by 'only'", async () => {
81
+ const caps = await definePlugin({
82
+ name: "biome",
83
+ apiVersion: 1,
84
+ color: (s) => s,
85
+ services: () => {
86
+ const svc = new FakeBiome();
87
+ return {
88
+ lint: svc,
89
+ format: svc,
90
+ jscheck: svc
91
+ };
92
+ }
93
+ })({ only: ["lint", "format"] }).services(ctx());
94
+ globalExpect(Object.keys(caps).sort()).toEqual(["format", "lint"]);
95
+ });
96
+ it("throws on unknown kind in 'only' with the canonical message", async () => {
97
+ await globalExpect(definePlugin({
98
+ name: "biome",
99
+ apiVersion: 1,
100
+ color: (s) => s,
101
+ services: () => {
102
+ const svc = new FakeBiome();
103
+ return {
104
+ lint: svc,
105
+ format: svc,
106
+ jscheck: svc
107
+ };
108
+ }
109
+ })({ only: ["tsc"] }).services(ctx())).rejects.toThrow(/@rrlab\/biome-plugin: unknown capability 'tsc' in 'only'\. Available: /);
110
+ });
111
+ it("throws when a required pkg is missing in the host", async () => {
112
+ await globalExpect(definePlugin({
113
+ name: "ghostly",
114
+ apiVersion: 1,
115
+ color: (s) => s,
116
+ services: () => ({ lint: new MissingService() })
117
+ })().services(ctx())).rejects.toThrow(/@rrlab\/ghostly-plugin requires ghostly-bin-that-does-not-exist to be installed/);
118
+ });
119
+ it("succeeds when distinct services share a pkg (deduplicated probe)", async () => {
120
+ const caps = await definePlugin({
121
+ name: "ts",
122
+ apiVersion: 1,
123
+ color: (s) => s,
124
+ services: () => ({ typecheck: new FakeTsc() })
125
+ })().services(ctx());
126
+ globalExpect(Object.keys(caps)).toEqual(["typecheck"]);
127
+ });
128
+ });
129
+ //#endregion
130
+ export {};
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,72 @@
1
+ import { pickPreset } from "../pick-preset.mjs";
2
+ import { i as it, n as vi, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
3
+ //#region src/lib/plugin/__tests__/pick-preset.test.ts
4
+ function makeCtx(overrides = {}) {
5
+ return {
6
+ shell: {},
7
+ logger: {},
8
+ appPkg: { dirPath: process.cwd() },
9
+ prompts: overrides.prompts ?? {},
10
+ flags: {
11
+ force: false,
12
+ yes: false,
13
+ nonInteractive: false,
14
+ ...overrides.flags
15
+ },
16
+ release: {}
17
+ };
18
+ }
19
+ const PRESETS = {
20
+ lib: { label: "Library" },
21
+ bin: { label: "CLI / Node binary" }
22
+ };
23
+ describe("pickPreset", () => {
24
+ it("returns the defaultPreset under --yes", async () => {
25
+ globalExpect(await pickPreset(makeCtx({ flags: { yes: true } }), {
26
+ message: "Which kind of build?",
27
+ presets: PRESETS,
28
+ defaultPreset: "lib"
29
+ })).toBe("lib");
30
+ });
31
+ it("returns the defaultPreset under non-interactive", async () => {
32
+ globalExpect(await pickPreset(makeCtx({ flags: { nonInteractive: true } }), {
33
+ message: "x",
34
+ presets: PRESETS,
35
+ defaultPreset: "bin"
36
+ })).toBe("bin");
37
+ });
38
+ it("returns the user's interactive choice", async () => {
39
+ const select = vi.fn().mockResolvedValue("bin");
40
+ globalExpect(await pickPreset(makeCtx({ prompts: {
41
+ confirm: vi.fn(),
42
+ select,
43
+ isCancel: () => false
44
+ } }), {
45
+ message: "Which kind of build?",
46
+ presets: PRESETS,
47
+ defaultPreset: "lib"
48
+ })).toBe("bin");
49
+ const call = select.mock.calls[0]?.[0];
50
+ globalExpect(call.options).toEqual([{
51
+ value: "lib",
52
+ label: "Library"
53
+ }, {
54
+ value: "bin",
55
+ label: "CLI / Node binary"
56
+ }]);
57
+ });
58
+ it("throws 'Cancelled by user.' when the user cancels", async () => {
59
+ const cancelSymbol = Symbol("cancel");
60
+ await globalExpect(pickPreset(makeCtx({ prompts: {
61
+ confirm: vi.fn(),
62
+ select: vi.fn().mockResolvedValue(cancelSymbol),
63
+ isCancel: (v) => v === cancelSymbol
64
+ } }), {
65
+ message: "x",
66
+ presets: PRESETS,
67
+ defaultPreset: "lib"
68
+ })).rejects.toThrow("Cancelled by user.");
69
+ });
70
+ });
71
+ //#endregion
72
+ export {};
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,104 @@
1
+ import { t as PluginRegistry } from "../../registry-BgqfKK5L.mjs";
2
+ import { i as it, r as describe, t as globalExpect } from "../../test.DNmyFkvJ-09ScyH13.mjs";
3
+ //#region src/lib/plugin/__tests__/registry.test.ts
4
+ function plugin(name) {
5
+ return {
6
+ name,
7
+ ui: name,
8
+ color: (str) => str,
9
+ apiVersion: 1,
10
+ services: async () => ({})
11
+ };
12
+ }
13
+ async function okReport() {
14
+ return {
15
+ ok: true,
16
+ output: ""
17
+ };
18
+ }
19
+ function fakeLinter() {
20
+ return {
21
+ ui: "fake",
22
+ lint: okReport,
23
+ doctor: okReport
24
+ };
25
+ }
26
+ function fakeFormatter() {
27
+ return {
28
+ ui: "fake",
29
+ format: okReport,
30
+ doctor: okReport
31
+ };
32
+ }
33
+ function fakeTypeChecker() {
34
+ return {
35
+ ui: "fake",
36
+ check: okReport,
37
+ doctor: okReport
38
+ };
39
+ }
40
+ function fakePacker() {
41
+ return {
42
+ ui: "fake",
43
+ pack: okReport,
44
+ doctor: okReport
45
+ };
46
+ }
47
+ describe("PluginRegistry", () => {
48
+ describe("getService(capability)", () => {
49
+ it("returns undefined when no plugin provides the capability", () => {
50
+ globalExpect(new PluginRegistry().getService("lint")).toBeUndefined();
51
+ });
52
+ it("returns the impl when exactly one plugin provides the capability", () => {
53
+ const registry = new PluginRegistry();
54
+ const linter = fakeLinter();
55
+ registry.register(plugin("biome"), { lint: linter });
56
+ globalExpect(registry.getService("lint")).toBe(linter);
57
+ });
58
+ it("does not collide across kinds", () => {
59
+ const registry = new PluginRegistry();
60
+ const linter = fakeLinter();
61
+ const formatter = fakeFormatter();
62
+ registry.register(plugin("oxc"), {
63
+ lint: linter,
64
+ format: formatter
65
+ });
66
+ globalExpect(registry.getService("lint")).toBe(linter);
67
+ globalExpect(registry.getService("format")).toBe(formatter);
68
+ globalExpect(registry.getService("jscheck")).toBeUndefined();
69
+ });
70
+ it("throws when two plugins provide the same capability, listing both", () => {
71
+ const registry = new PluginRegistry();
72
+ registry.register(plugin("biome"), { lint: fakeLinter() });
73
+ registry.register(plugin("oxc"), { lint: fakeLinter() });
74
+ globalExpect(() => registry.getService("lint")).toThrowError(/biome.*oxc|oxc.*biome/);
75
+ });
76
+ it("suggests the 'only' option when reporting multi-provider conflicts", () => {
77
+ const registry = new PluginRegistry();
78
+ registry.register(plugin("biome"), { lint: fakeLinter() });
79
+ registry.register(plugin("oxc"), { lint: fakeLinter() });
80
+ globalExpect(() => registry.getService("lint")).toThrowError(/only:\s*\['lint'\]/);
81
+ });
82
+ it("supports typecheck and pack kinds", () => {
83
+ const registry = new PluginRegistry();
84
+ const tc = fakeTypeChecker();
85
+ const packer = fakePacker();
86
+ registry.register(plugin("ts"), { typecheck: tc });
87
+ registry.register(plugin("tsdown"), { pack: packer });
88
+ globalExpect(registry.getService("typecheck")).toBe(tc);
89
+ globalExpect(registry.getService("pack")).toBe(packer);
90
+ });
91
+ });
92
+ describe("providersOf(capability)", () => {
93
+ it("returns every plugin that provides the capability", () => {
94
+ const registry = new PluginRegistry();
95
+ registry.register(plugin("biome"), { lint: fakeLinter() });
96
+ registry.register(plugin("oxc"), { lint: fakeLinter() });
97
+ registry.register(plugin("tsdown"), { pack: fakePacker() });
98
+ globalExpect(registry.providersOf("lint").map((p) => p.plugin.name).sort()).toEqual(["biome", "oxc"]);
99
+ globalExpect(registry.providersOf("format")).toEqual([]);
100
+ });
101
+ });
102
+ });
103
+ //#endregion
104
+ export {};
@@ -0,0 +1,4 @@
1
+ //#region src/lib/plugin/bin-probe.d.ts
2
+ declare function probeBins(services: readonly unknown[], pluginName: string): Promise<void>;
3
+ //#endregion
4
+ export { probeBins };
@@ -0,0 +1,22 @@
1
+ import { ToolService } from "./tool-service.mjs";
2
+ //#region src/lib/plugin/bin-probe.ts
3
+ async function probeBins(services, pluginName) {
4
+ const toolServices = services.filter((s) => s instanceof ToolService);
5
+ const distinct = /* @__PURE__ */ new Map();
6
+ for (const svc of toolServices) if (!distinct.has(svc.pkg)) distinct.set(svc.pkg, svc);
7
+ if (distinct.size === 0) return;
8
+ const probes = [...distinct.values()].map(async (svc) => {
9
+ try {
10
+ await svc.getBinDir();
11
+ } catch {
12
+ return svc.pkg;
13
+ }
14
+ return null;
15
+ });
16
+ const missing = (await Promise.all(probes)).filter((p) => p !== null);
17
+ if (missing.length === 0) return;
18
+ const pkgName = `@rrlab/${pluginName}-plugin`;
19
+ throw new Error(`${pkgName} requires ${missing.join(", ")} to be installed in the host project. Run: rr plugins add ${pluginName} (or: pnpm add -D ${missing.join(" ")})`);
20
+ }
21
+ //#endregion
22
+ export { probeBins };
@@ -0,0 +1,18 @@
1
+ import { i as InstallContext } from "../types-Iu4IyWof.mjs";
2
+
3
+ //#region src/lib/plugin/decide-scaffold.d.ts
4
+ type ScaffoldDecision = "create" | "patch" | "overwrite" | "skip";
5
+ type DecideScaffoldOptions = {
6
+ /** The config file label shown to the user (e.g. `"biome.json"`, `"tsdown.config.ts"`). */label: string; /** Whether the file currently exists in the app project. */
7
+ fileExists: boolean; /** Short description of what "patch" does, shown in the select option. */
8
+ patchHint: string;
9
+ /**
10
+ * What to return when the file exists and the run is unattended (`--yes` / non-interactive).
11
+ * - `"patch"` (default): assume the user wants to merge our config into theirs (safe for JSON we can edit).
12
+ * - `"skip"`: assume the user owns the file (right for TS modules we'd otherwise rewrite blindly).
13
+ */
14
+ unattendedExistingAction?: "patch" | "skip";
15
+ };
16
+ declare function decideScaffold(ctx: InstallContext, opts: DecideScaffoldOptions): Promise<ScaffoldDecision>;
17
+ //#endregion
18
+ export { DecideScaffoldOptions, ScaffoldDecision, decideScaffold };
@@ -0,0 +1,36 @@
1
+ //#region src/lib/plugin/decide-scaffold.ts
2
+ async function decideScaffold(ctx, opts) {
3
+ const { label, fileExists, patchHint, unattendedExistingAction = "patch" } = opts;
4
+ if (!fileExists) {
5
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return "create";
6
+ const choice = await ctx.prompts.confirm({
7
+ message: `Scaffold ${label}?`,
8
+ initialValue: true
9
+ });
10
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
11
+ return choice ? "create" : "skip";
12
+ }
13
+ if (ctx.flags.yes || ctx.flags.nonInteractive) return unattendedExistingAction;
14
+ const choice = await ctx.prompts.select({
15
+ message: `${label} already exists. What do you want to do?`,
16
+ options: [
17
+ {
18
+ value: "patch",
19
+ label: `Patch — ${patchHint}`
20
+ },
21
+ {
22
+ value: "skip",
23
+ label: "Skip — leave it alone"
24
+ },
25
+ {
26
+ value: "overwrite",
27
+ label: "Overwrite — replace with a fresh scaffold"
28
+ }
29
+ ],
30
+ initialValue: "patch"
31
+ });
32
+ if (ctx.prompts.isCancel(choice)) throw new Error("Cancelled by user.");
33
+ return choice;
34
+ }
35
+ //#endregion
36
+ export { decideScaffold };
@@ -0,0 +1,17 @@
1
+ import { c as Plugin, d as PluginServices, f as UninstallContext, i as InstallContext, l as PluginCapability, m as UninstallResult, o as InstallResult, u as PluginContext } from "../types-Iu4IyWof.mjs";
2
+
3
+ //#region src/lib/plugin/define-plugin.d.ts
4
+ type PluginDefinition<TServices extends PluginServices> = {
5
+ apiVersion: 1;
6
+ name: string;
7
+ color: (value: string) => string;
8
+ services(ctx: PluginContext): TServices | Promise<TServices>;
9
+ install?(this: void, ctx: InstallContext): Promise<InstallResult>;
10
+ uninstall?(this: void, ctx: UninstallContext): Promise<UninstallResult>;
11
+ };
12
+ type Options<TKind extends PluginCapability> = {
13
+ only?: readonly TKind[];
14
+ };
15
+ declare function definePlugin<TServices extends PluginServices>(definition: PluginDefinition<TServices>): (options?: Options<keyof TServices & PluginCapability>) => Plugin;
16
+ //#endregion
17
+ export { PluginDefinition, definePlugin };
@@ -0,0 +1,25 @@
1
+ import { probeBins } from "./bin-probe.mjs";
2
+ //#region src/lib/plugin/define-plugin.ts
3
+ function definePlugin(definition) {
4
+ return (options) => {
5
+ const only = options?.only;
6
+ const pkgName = `@rrlab/${definition.name}-plugin`;
7
+ return {
8
+ name: definition.name,
9
+ color: definition.color,
10
+ ui: definition.color(definition.name),
11
+ apiVersion: definition.apiVersion,
12
+ install: definition.install,
13
+ uninstall: definition.uninstall,
14
+ async services(ctx) {
15
+ const services = await definition.services(ctx);
16
+ await probeBins(Object.values(services), definition.name);
17
+ if (!only) return services;
18
+ for (const k of only) if (!(k in services)) throw new Error(`${pkgName}: unknown capability '${k}' in 'only'. Available: ${Object.keys(services).join(", ")}.`);
19
+ return Object.fromEntries(only.map((capability) => [capability, services[capability]]));
20
+ }
21
+ };
22
+ };
23
+ }
24
+ //#endregion
25
+ export { definePlugin };
@@ -0,0 +1,47 @@
1
+ import { l as PluginCapability } from "../types-Iu4IyWof.mjs";
2
+
3
+ //#region src/lib/plugin/directory.d.ts
4
+ declare const PLUGINS_DIRECTORY: {
5
+ readonly ts: {
6
+ readonly pkg: "@rrlab/ts-plugin";
7
+ readonly name: "ts";
8
+ readonly capabilities: ["typecheck"];
9
+ };
10
+ readonly biome: {
11
+ readonly pkg: "@rrlab/biome-plugin";
12
+ readonly name: "biome";
13
+ readonly capabilities: ["format", "jscheck", "lint"];
14
+ };
15
+ readonly oxc: {
16
+ readonly pkg: "@rrlab/oxc-plugin";
17
+ readonly name: "oxc";
18
+ readonly capabilities: ["format", "lint", "jscheck", "typecheck"];
19
+ };
20
+ readonly tsdown: {
21
+ readonly pkg: "@rrlab/tsdown-plugin";
22
+ readonly name: "tsdown";
23
+ readonly capabilities: ["pack"];
24
+ };
25
+ };
26
+ type PluginName = keyof typeof PLUGINS_DIRECTORY;
27
+ declare function allPluginNames(): readonly PluginName[];
28
+ declare function isPluginName(name: string): name is PluginName;
29
+ declare function providersOf(capability: PluginCapability): ({
30
+ readonly pkg: "@rrlab/ts-plugin";
31
+ readonly name: "ts";
32
+ readonly capabilities: ["typecheck"];
33
+ } | {
34
+ readonly pkg: "@rrlab/biome-plugin";
35
+ readonly name: "biome";
36
+ readonly capabilities: ["format", "jscheck", "lint"];
37
+ } | {
38
+ readonly pkg: "@rrlab/oxc-plugin";
39
+ readonly name: "oxc";
40
+ readonly capabilities: ["format", "lint", "jscheck", "typecheck"];
41
+ } | {
42
+ readonly pkg: "@rrlab/tsdown-plugin";
43
+ readonly name: "tsdown";
44
+ readonly capabilities: ["pack"];
45
+ })[];
46
+ //#endregion
47
+ export { PLUGINS_DIRECTORY, PluginName, allPluginNames, isPluginName, providersOf };