@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.
- package/README.md +110 -0
- package/bin +54 -0
- package/dist/cli.usage.kdl +71 -0
- package/dist/config.d.mts +11 -0
- package/dist/config.mjs +6 -0
- package/dist/plugin.d.mts +52 -0
- package/dist/plugin.mjs +65 -0
- package/dist/run.mjs +1137 -0
- package/dist/types-C3V27_kd.d.mts +173 -0
- package/package.json +74 -0
- package/src/lib/config.ts +5 -0
- package/src/lib/plugin.ts +31 -0
- package/src/plugin/define-plugin.ts +5 -0
- package/src/plugin/registry.ts +47 -0
- package/src/plugin/tool-service.ts +77 -0
- package/src/plugin/types.ts +139 -0
- package/src/program/commands/check.ts +63 -0
- package/src/program/commands/clean.ts +53 -0
- package/src/program/commands/completion.ts +26 -0
- package/src/program/commands/config.ts +15 -0
- package/src/program/commands/doctor.ts +85 -0
- package/src/program/commands/format.ts +35 -0
- package/src/program/commands/jscheck.ts +44 -0
- package/src/program/commands/lint.ts +37 -0
- package/src/program/commands/pack.ts +27 -0
- package/src/program/commands/plugins.ts +359 -0
- package/src/program/commands/tscheck.ts +112 -0
- package/src/program/composed-jsc.ts +35 -0
- package/src/program/index.ts +50 -0
- package/src/program/missing-plugin.ts +18 -0
- package/src/program/ui.ts +59 -0
- package/src/run.ts +11 -0
- package/src/services/config-ast.ts +202 -0
- package/src/services/config.ts +54 -0
- package/src/services/ctx.ts +72 -0
- package/src/services/json-edit.ts +147 -0
- package/src/services/logger.ts +5 -0
- package/src/services/plugins-registry.ts +21 -0
- package/src/services/prompts.ts +26 -0
- package/src/services/workspace-target.ts +27 -0
- package/src/types/config.ts +13 -0
- package/src/types/tool.ts +57 -0
- 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,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,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