@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
@@ -0,0 +1,147 @@
1
+ import * as cjson from "comment-json";
2
+ import type { JsonEdit } from "#src/plugin/types.ts";
3
+
4
+ /**
5
+ * Applies a sequence of `JsonEdit` ops to a JSON / JSONC source string.
6
+ * Uses `comment-json` so user comments and `extends`-like top-level shape
7
+ * survive a parse → mutate → stringify round-trip.
8
+ *
9
+ * Paths follow JSON Pointer (RFC 6901): `"/extends"`,
10
+ * `"/compilerOptions/strict"`. Indices are valid path segments for arrays
11
+ * (`"/extends/0"`).
12
+ */
13
+ export function applyJsonEdits(source: string, edits: JsonEdit[]): string {
14
+ // biome-ignore lint/suspicious/noExplicitAny: cjson exposes the parsed tree as a structurally-typed JS object/array
15
+ let root: any = source.trim() === "" ? {} : cjson.parse(source);
16
+ if (root == null || typeof root !== "object") {
17
+ throw new Error(`applyJsonEdits: expected a JSON object/array at the top level, got ${typeof root}.`);
18
+ }
19
+ for (const edit of edits) {
20
+ root = applyOne(root, edit);
21
+ }
22
+ return `${cjson.stringify(root, null, 2)}\n`;
23
+ }
24
+
25
+ // biome-ignore lint/suspicious/noExplicitAny: see above
26
+ function applyOne(root: any, edit: JsonEdit): any {
27
+ const segments = parsePointer(edit.path);
28
+ if (edit.op === "set") {
29
+ const existing = resolve(root, segments);
30
+ if (edit.mode === "if-missing" && existing !== undefined) return root;
31
+ return setAt(root, segments, edit.value);
32
+ }
33
+ if (edit.op === "unset") {
34
+ return unsetAt(root, segments);
35
+ }
36
+ if (edit.op === "include") {
37
+ return includeInArray(root, segments, edit.value, edit.position ?? "end");
38
+ }
39
+ if (edit.op === "exclude") {
40
+ return excludeFromArray(root, segments, edit.value);
41
+ }
42
+ return root;
43
+ }
44
+
45
+ /** Parses a JSON Pointer (RFC 6901) into its segments, decoding `~1` and `~0`. */
46
+ function parsePointer(pointer: string): string[] {
47
+ if (pointer === "" || pointer === "/") return [];
48
+ if (!pointer.startsWith("/")) {
49
+ throw new Error(`Invalid JSON Pointer "${pointer}": must start with "/" or be empty.`);
50
+ }
51
+ return pointer
52
+ .slice(1)
53
+ .split("/")
54
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
55
+ }
56
+
57
+ // biome-ignore lint/suspicious/noExplicitAny: see above
58
+ function resolve(root: any, segments: string[]): unknown {
59
+ // biome-ignore lint/suspicious/noExplicitAny: see above
60
+ let cur: any = root;
61
+ for (const seg of segments) {
62
+ if (cur == null) return undefined;
63
+ cur = cur[arrayIndexIfNumeric(cur, seg)];
64
+ }
65
+ return cur;
66
+ }
67
+
68
+ // biome-ignore lint/suspicious/noExplicitAny: see above
69
+ function setAt(root: any, segments: string[], value: unknown): any {
70
+ if (segments.length === 0) {
71
+ // Replacing the whole root — uncommon for our use cases but harmless.
72
+ return value;
73
+ }
74
+ // biome-ignore lint/suspicious/noExplicitAny: see above
75
+ let cur: any = root;
76
+ for (let i = 0; i < segments.length - 1; i++) {
77
+ const seg = segments[i] ?? "";
78
+ const key = arrayIndexIfNumeric(cur, seg);
79
+ if (cur[key] == null || typeof cur[key] !== "object") {
80
+ cur[key] = isNumericIndex(segments[i + 1] ?? "") ? [] : {};
81
+ }
82
+ cur = cur[key];
83
+ }
84
+ const last = segments[segments.length - 1] ?? "";
85
+ cur[arrayIndexIfNumeric(cur, last)] = value;
86
+ return root;
87
+ }
88
+
89
+ // biome-ignore lint/suspicious/noExplicitAny: see above
90
+ function unsetAt(root: any, segments: string[]): any {
91
+ if (segments.length === 0) return root;
92
+ // biome-ignore lint/suspicious/noExplicitAny: see above
93
+ let cur: any = root;
94
+ for (let i = 0; i < segments.length - 1; i++) {
95
+ const seg = segments[i] ?? "";
96
+ if (cur == null) return root;
97
+ cur = cur[arrayIndexIfNumeric(cur, seg)];
98
+ }
99
+ if (cur == null) return root;
100
+ const last = segments[segments.length - 1] ?? "";
101
+ if (Array.isArray(cur) && isNumericIndex(last)) {
102
+ cur.splice(Number(last), 1);
103
+ } else {
104
+ delete cur[last];
105
+ }
106
+ return root;
107
+ }
108
+
109
+ // biome-ignore lint/suspicious/noExplicitAny: see above
110
+ function includeInArray(root: any, segments: string[], value: unknown, position: "start" | "end"): any {
111
+ const existing = resolve(root, segments);
112
+ if (existing === undefined) {
113
+ // Create the array with the single value.
114
+ return setAt(root, segments, [value]);
115
+ }
116
+ if (!Array.isArray(existing)) {
117
+ throw new Error(`include: expected an array at "${segments.join("/")}", got ${typeof existing}.`);
118
+ }
119
+ if (existing.some((item) => deepEqual(item, value))) return root;
120
+ if (position === "start") existing.unshift(value);
121
+ else existing.push(value);
122
+ return root;
123
+ }
124
+
125
+ // biome-ignore lint/suspicious/noExplicitAny: see above
126
+ function excludeFromArray(root: any, segments: string[], value: unknown): any {
127
+ const existing = resolve(root, segments);
128
+ if (!Array.isArray(existing)) return root;
129
+ const idx = existing.findIndex((item) => deepEqual(item, value));
130
+ if (idx >= 0) existing.splice(idx, 1);
131
+ return root;
132
+ }
133
+
134
+ // biome-ignore lint/suspicious/noExplicitAny: see above
135
+ function arrayIndexIfNumeric(target: any, seg: string): string | number {
136
+ return Array.isArray(target) && isNumericIndex(seg) ? Number(seg) : seg;
137
+ }
138
+
139
+ function isNumericIndex(seg: string): boolean {
140
+ return seg !== "" && /^\d+$/.test(seg);
141
+ }
142
+
143
+ function deepEqual(a: unknown, b: unknown): boolean {
144
+ if (a === b) return true;
145
+ if (typeof a !== "object" || typeof b !== "object" || a == null || b == null) return false;
146
+ return JSON.stringify(a) === JSON.stringify(b);
147
+ }
@@ -0,0 +1,5 @@
1
+ import { createLoggy } from "@vlandoss/loggy";
2
+
3
+ export const logger = createLoggy({
4
+ namespace: "run-run",
5
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Map from short aliases the user types (`rr plugins add ts`) to the
3
+ * full npm package name and the canonical local binding the plugin file
4
+ * is imported as inside `run-run.config.{ts,mts}`.
5
+ *
6
+ * Third-party plugins use their full package name; the binding is derived
7
+ * by stripping a `@scope/plugin-` (or `@scope/run-run-plugin-`) prefix.
8
+ */
9
+ export const OFFICIAL_PLUGINS = {
10
+ ts: { pkg: "@rrlab/plugin-ts", exportName: "ts" },
11
+ eslint: { pkg: "@rrlab/plugin-eslint", exportName: "eslint" },
12
+ biome: { pkg: "@rrlab/plugin-biome", exportName: "biome" },
13
+ oxc: { pkg: "@rrlab/plugin-oxc", exportName: "oxc" },
14
+ tsdown: { pkg: "@rrlab/plugin-tsdown", exportName: "tsdown" },
15
+ } as const;
16
+
17
+ export type OfficialAlias = keyof typeof OFFICIAL_PLUGINS;
18
+
19
+ export function officialAliases(): readonly OfficialAlias[] {
20
+ return Object.keys(OFFICIAL_PLUGINS) as OfficialAlias[];
21
+ }
@@ -0,0 +1,26 @@
1
+ import * as clack from "@clack/prompts";
2
+ import type { ClackPrompts } from "#src/plugin/types.ts";
3
+
4
+ /**
5
+ * Adapter that exposes the subset of `@clack/prompts` matching the
6
+ * `ClackPrompts` contract from `@rrlab/cli/plugin`. The contract intentionally
7
+ * stays narrow (`select`, `confirm`, `isCancel`) so plugin install hooks can
8
+ * be tested by injecting a stub without pulling in the real terminal IO.
9
+ *
10
+ * The casts compensate for two small type-shape mismatches:
11
+ * - Our contract requires `label: string`; clack types `label?: string`.
12
+ * - `clack.select` returns `Promise<unknown>` (a non-cancelled value or
13
+ * `symbol`); our contract narrows that to `Promise<T | symbol>`.
14
+ *
15
+ * Both widenings are safe: every required field is present, and clack's
16
+ * runtime returns either the picked option's `value` (typed `T`) or
17
+ * `clack.isCancel(...)`-detectable `symbol`.
18
+ */
19
+ export function createClackPrompts(): ClackPrompts {
20
+ return {
21
+ select: <T extends string>(opts: Parameters<ClackPrompts["select"]>[0]) =>
22
+ clack.select(opts as Parameters<typeof clack.select>[0]) as Promise<T | symbol>,
23
+ confirm: (opts) => clack.confirm(opts),
24
+ isCancel: (value): value is symbol => clack.isCancel(value),
25
+ };
26
+ }
@@ -0,0 +1,27 @@
1
+ import type { Pkg } from "@vlandoss/clibuddy";
2
+ import type { PackageManager } from "nypm";
3
+
4
+ export type WorkspaceChoice = { kind: "current" } | { kind: "root"; flag: true | undefined };
5
+ export type WorkspaceTarget = true | undefined;
6
+
7
+ export function resolveWorkspaceChoice(appPkg: Pkg, pm: PackageManager | undefined): WorkspaceChoice {
8
+ if (!appPkg.isMonorepo()) return { kind: "current" };
9
+ return { kind: "root", flag: pmNeedsRootFlag(pm) ? true : undefined };
10
+ }
11
+
12
+ export function toNypmWorkspace(choice: WorkspaceChoice): WorkspaceTarget {
13
+ return choice.kind === "current" ? undefined : choice.flag;
14
+ }
15
+
16
+ export function describeWorkspaceChoice(choice: WorkspaceChoice): string {
17
+ return choice.kind === "current" ? "the current package" : "the workspace root";
18
+ }
19
+
20
+ // pnpm and yarn-classic refuse to install at the workspace root without an
21
+ // explicit flag; npm and yarn-berry don't need it.
22
+ function pmNeedsRootFlag(pm: PackageManager | undefined): boolean {
23
+ if (!pm) return false;
24
+ if (pm.name === "pnpm") return true;
25
+ if (pm.name === "yarn" && (!pm.majorVersion || pm.majorVersion === "1")) return true;
26
+ return false;
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from "#src/plugin/types.ts";
2
+
3
+ export type UserConfig = {
4
+ plugins?: Plugin[];
5
+ };
6
+
7
+ export type ExportedConfig = {
8
+ config: UserConfig;
9
+ meta: {
10
+ isDefault: boolean;
11
+ filepath?: string;
12
+ };
13
+ };
@@ -0,0 +1,57 @@
1
+ export type FormatOptions = {
2
+ fix?: boolean;
3
+ };
4
+
5
+ export type LintOptions = {
6
+ fix?: boolean;
7
+ };
8
+
9
+ export type StaticCheckerOptions = {
10
+ fix?: boolean;
11
+ fixStaged?: boolean;
12
+ };
13
+
14
+ export type DoctorOutput = {
15
+ stdout: string;
16
+ stderr: string;
17
+ exitCode: number | undefined;
18
+ };
19
+
20
+ export type DoctorResult = {
21
+ ok: boolean;
22
+ output: DoctorOutput;
23
+ };
24
+
25
+ export type Doctor = {
26
+ ui: string;
27
+ doctor(): Promise<DoctorResult>;
28
+ };
29
+
30
+ export type Formatter = {
31
+ bin: string;
32
+ ui: string;
33
+ format(options: FormatOptions): Promise<void>;
34
+ };
35
+
36
+ export type Linter = {
37
+ bin: string;
38
+ ui: string;
39
+ lint(options: LintOptions): Promise<void>;
40
+ };
41
+
42
+ export type StaticChecker = {
43
+ bin: string;
44
+ ui: string;
45
+ check(options: StaticCheckerOptions): Promise<void>;
46
+ };
47
+
48
+ export type TypeCheckOptions = {
49
+ /** Where to run the type checker. Defaults to the kernel's `cwd`. */
50
+ cwd?: string;
51
+ };
52
+
53
+ export type TypeChecker = {
54
+ bin: string;
55
+ ui: string;
56
+ check(options?: TypeCheckOptions): Promise<void>;
57
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["@rrlab/ts-config/no-dom/lib"]
3
+ }