@rrlab/cli 0.0.2 → 0.0.3-git-aeeb52d.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 +20 -0
- package/dist/cli.usage.kdl +2 -4
- package/dist/config.d.mts +1 -1
- package/dist/plugin.d.mts +2 -2
- package/dist/plugin.mjs +46 -1
- package/dist/run.mjs +69 -7
- package/dist/{types-C3V27_kd.d.mts → types-BPHxibPr.d.mts} +33 -1
- package/package.json +1 -1
- package/src/lib/plugin.ts +1 -0
- package/src/plugin/types.ts +8 -0
- package/src/program/commands/plugins.ts +29 -8
- package/src/services/release.ts +53 -0
package/README.md
CHANGED
|
@@ -35,6 +35,26 @@ pnpm rr help
|
|
|
35
35
|
|
|
36
36
|
See [`CLI.md`](./CLI.md) for the full reference (auto-generated per release).
|
|
37
37
|
|
|
38
|
+
## Plugins
|
|
39
|
+
|
|
40
|
+
`rr` is a microkernel: every tool (Biome, TypeScript, tsdown, …) lives in its own `@rrlab/<tool>-plugin` package. Install one with:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
rr plugins add biome
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This installs `@rrlab/biome-plugin` plus its peer tool and shared config, scaffolds `biome.json` if missing, and wires the plugin into `run-run.config.{ts,mts}`.
|
|
47
|
+
|
|
48
|
+
To install from a specific dist-tag (e.g. a PR preview release published as `pr-<N>`, or a custom tag), append `@<spec>`:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
rr plugins add biome@pr-226 # preview tag
|
|
52
|
+
rr plugins add biome@next # any dist-tag
|
|
53
|
+
rr plugins add biome@^0.1.0 # explicit version range (sibling configs still use latest)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When the spec is a dist-tag, `rr` resolves any `@rrlab/*-config` sibling at the same tag, falling back to `latest` if the registry doesn't have the sibling at that tag.
|
|
57
|
+
|
|
38
58
|
## Shell completion
|
|
39
59
|
|
|
40
60
|
`rr` ships a `completion` subcommand that prints a shell-specific script.
|
package/dist/cli.usage.kdl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @generated by @usage-spec/commander from Commander.js metadata
|
|
2
2
|
name rr
|
|
3
3
|
bin rr
|
|
4
|
-
version "0.0.
|
|
4
|
+
version "0.0.3-git-aeeb52d.0"
|
|
5
5
|
usage "<command...> [options...]"
|
|
6
6
|
flag --usage help="print KDL spec for this CLI (https://kdl.dev)"
|
|
7
7
|
cmd completion help="print shell completion script (usage)" {
|
|
@@ -49,9 +49,7 @@ cmd plugins subcommand_required=#true help="manage @rrlab plugins" {
|
|
|
49
49
|
flag --force help="re-run install even if the plugin is already configured"
|
|
50
50
|
flag --yes help="skip prompts and use defaults (non-interactive)"
|
|
51
51
|
flag --dry-run help="show what would happen, without applying changes"
|
|
52
|
-
arg <name> help="plugin alias"
|
|
53
|
-
choices ts eslint biome oxc tsdown
|
|
54
|
-
}
|
|
52
|
+
arg <name> help="plugin alias (ts|eslint|biome|oxc|tsdown), optionally with @<spec> e.g. biome@pr-226"
|
|
55
53
|
}
|
|
56
54
|
cmd remove help="uninstall an @rrlab plugin and undo its config files + deps" {
|
|
57
55
|
flag --yes help="skip the confirmation prompt"
|
package/dist/config.d.mts
CHANGED
package/dist/plugin.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Linter, D as TypeChecker, E as TypeCheckOptions, S as LintOptions, T as StaticCheckerOptions, _ as Doctor, a as InstallFlags, b as FormatOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, l as Packer, m as UninstallContext, n as ClackPromptsSelectOption, o as InstallResult, p as PluginKind, r as FileOp, s as JsonEdit, t as ClackPrompts, u as Plugin, v as DoctorOutput, w as StaticChecker, x as Formatter, y as DoctorResult } from "./types-
|
|
1
|
+
import { C as Linter, D as TypeChecker, E as TypeCheckOptions, O as ReleaseService, S as LintOptions, T as StaticCheckerOptions, _ as Doctor, a as InstallFlags, b as FormatOptions, c as PLUGIN_KINDS, d as PluginCapabilities, f as PluginContext, g as UninstallResult, h as UninstallFlags, i as InstallContext, k as ReleaseServiceOptions, l as Packer, m as UninstallContext, n as ClackPromptsSelectOption, o as InstallResult, p as PluginKind, r as FileOp, s as JsonEdit, t as ClackPrompts, u as Plugin, v as DoctorOutput, w as StaticChecker, x as Formatter, y as DoctorResult } from "./types-BPHxibPr.mjs";
|
|
2
2
|
import { ShellService } from "@vlandoss/clibuddy";
|
|
3
3
|
|
|
4
4
|
//#region src/plugin/define-plugin.d.ts
|
|
@@ -49,4 +49,4 @@ declare class ToolService {
|
|
|
49
49
|
doctor(): Promise<DoctorResult>;
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
52
|
-
export { type ClackPrompts, type ClackPromptsSelectOption, type Doctor, type DoctorOutput, type DoctorResult, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, PLUGIN_KINDS, type Packer, type Plugin, type PluginCapabilities, type PluginContext, type PluginKind, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, definePlugin };
|
|
52
|
+
export { type ClackPrompts, type ClackPromptsSelectOption, type Doctor, type DoctorOutput, type DoctorResult, type FileOp, type FormatOptions, type Formatter, type InstallContext, type InstallFlags, type InstallResult, type JsonEdit, type LintOptions, type Linter, PLUGIN_KINDS, type Packer, type Plugin, type PluginCapabilities, type PluginContext, type PluginKind, ReleaseService, type ReleaseServiceOptions, type StaticChecker, type StaticCheckerOptions, ToolService, type ToolServiceOptions, type TypeCheckOptions, type TypeChecker, type UninstallContext, type UninstallFlags, type UninstallResult, definePlugin };
|
package/dist/plugin.mjs
CHANGED
|
@@ -62,4 +62,49 @@ const PLUGIN_KINDS = [
|
|
|
62
62
|
"pack"
|
|
63
63
|
];
|
|
64
64
|
//#endregion
|
|
65
|
-
|
|
65
|
+
//#region src/services/release.ts
|
|
66
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
67
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
68
|
+
/**
|
|
69
|
+
* Represents the "release" the current `rr plugins add` runs against — the
|
|
70
|
+
* dist-tag the user picked (default: latest), plus the logic to resolve install
|
|
71
|
+
* specs for related packages under that release.
|
|
72
|
+
*
|
|
73
|
+
* - With no `tag`, `resolve()` always returns `"latest"` and never hits the
|
|
74
|
+
* registry.
|
|
75
|
+
* - With a `tag` (e.g. `"pr-226"`), probes `<pkg>@<tag>`: returns the tag when
|
|
76
|
+
* it exists, falls back to `"latest"` otherwise so a partial preview release
|
|
77
|
+
* (where only a subset of packages got published) still installs cleanly.
|
|
78
|
+
* - Per-package result is cached within the service instance.
|
|
79
|
+
*/
|
|
80
|
+
var ReleaseService = class {
|
|
81
|
+
tag;
|
|
82
|
+
#fetcher;
|
|
83
|
+
#cache = /* @__PURE__ */ new Map();
|
|
84
|
+
constructor(tag, { fetcher = fetch } = {}) {
|
|
85
|
+
this.tag = tag;
|
|
86
|
+
this.#fetcher = fetcher;
|
|
87
|
+
}
|
|
88
|
+
resolve(pkg) {
|
|
89
|
+
if (!this.tag || this.tag === "latest") return Promise.resolve("latest");
|
|
90
|
+
const cached = this.#cache.get(pkg);
|
|
91
|
+
if (cached) return cached;
|
|
92
|
+
const promise = this.#probe(pkg);
|
|
93
|
+
this.#cache.set(pkg, promise);
|
|
94
|
+
return promise;
|
|
95
|
+
}
|
|
96
|
+
async #probe(pkg) {
|
|
97
|
+
const tag = this.tag;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
100
|
+
try {
|
|
101
|
+
return (await this.#fetcher(`${REGISTRY}/${pkg}/${encodeURIComponent(tag)}`, { signal: controller.signal })).ok ? tag : "latest";
|
|
102
|
+
} catch {
|
|
103
|
+
return "latest";
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
export { PLUGIN_KINDS, ReleaseService, ToolService, definePlugin };
|
package/dist/run.mjs
CHANGED
|
@@ -762,6 +762,51 @@ function createClackPrompts() {
|
|
|
762
762
|
};
|
|
763
763
|
}
|
|
764
764
|
//#endregion
|
|
765
|
+
//#region src/services/release.ts
|
|
766
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
767
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
768
|
+
/**
|
|
769
|
+
* Represents the "release" the current `rr plugins add` runs against — the
|
|
770
|
+
* dist-tag the user picked (default: latest), plus the logic to resolve install
|
|
771
|
+
* specs for related packages under that release.
|
|
772
|
+
*
|
|
773
|
+
* - With no `tag`, `resolve()` always returns `"latest"` and never hits the
|
|
774
|
+
* registry.
|
|
775
|
+
* - With a `tag` (e.g. `"pr-226"`), probes `<pkg>@<tag>`: returns the tag when
|
|
776
|
+
* it exists, falls back to `"latest"` otherwise so a partial preview release
|
|
777
|
+
* (where only a subset of packages got published) still installs cleanly.
|
|
778
|
+
* - Per-package result is cached within the service instance.
|
|
779
|
+
*/
|
|
780
|
+
var ReleaseService = class {
|
|
781
|
+
tag;
|
|
782
|
+
#fetcher;
|
|
783
|
+
#cache = /* @__PURE__ */ new Map();
|
|
784
|
+
constructor(tag, { fetcher = fetch } = {}) {
|
|
785
|
+
this.tag = tag;
|
|
786
|
+
this.#fetcher = fetcher;
|
|
787
|
+
}
|
|
788
|
+
resolve(pkg) {
|
|
789
|
+
if (!this.tag || this.tag === "latest") return Promise.resolve("latest");
|
|
790
|
+
const cached = this.#cache.get(pkg);
|
|
791
|
+
if (cached) return cached;
|
|
792
|
+
const promise = this.#probe(pkg);
|
|
793
|
+
this.#cache.set(pkg, promise);
|
|
794
|
+
return promise;
|
|
795
|
+
}
|
|
796
|
+
async #probe(pkg) {
|
|
797
|
+
const tag = this.tag;
|
|
798
|
+
const controller = new AbortController();
|
|
799
|
+
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
800
|
+
try {
|
|
801
|
+
return (await this.#fetcher(`${REGISTRY}/${pkg}/${encodeURIComponent(tag)}`, { signal: controller.signal })).ok ? tag : "latest";
|
|
802
|
+
} catch {
|
|
803
|
+
return "latest";
|
|
804
|
+
} finally {
|
|
805
|
+
clearTimeout(timeout);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
//#endregion
|
|
765
810
|
//#region src/services/workspace-target.ts
|
|
766
811
|
function resolveWorkspaceChoice(appPkg, pm) {
|
|
767
812
|
if (!appPkg.isMonorepo()) return { kind: "current" };
|
|
@@ -787,7 +832,7 @@ function pmNeedsRootFlag(pm) {
|
|
|
787
832
|
function createPluginsCommand(ctx) {
|
|
788
833
|
const cmd = createCommand("plugins").description("manage @rrlab plugins");
|
|
789
834
|
cmd.command("list").description("list plugins configured in run-run.config.{ts,mts}").action(() => runList(ctx));
|
|
790
|
-
cmd.command("add").description("install and configure an @rrlab plugin").addArgument(new Argument("<name>",
|
|
835
|
+
cmd.command("add").description("install and configure an @rrlab plugin").addArgument(new Argument("<name>", `plugin alias (${officialAliases().join("|")}), optionally with @<spec> e.g. biome@pr-226`)).option("--force", "re-run install even if the plugin is already configured").option("--yes", "skip prompts and use defaults (non-interactive)").option("--dry-run", "show what would happen, without applying changes").action((name, opts) => runAdd(ctx, name, opts));
|
|
791
836
|
cmd.command("remove").description("uninstall an @rrlab plugin and undo its config files + deps").addArgument(new Argument("<name>", "plugin alias to remove").choices(officialAliases())).option("--yes", "skip the confirmation prompt").option("--dry-run", "print the plan without applying changes").action((name, opts) => runRemove(ctx, name, opts));
|
|
792
837
|
return cmd;
|
|
793
838
|
}
|
|
@@ -807,9 +852,13 @@ async function runList(ctx) {
|
|
|
807
852
|
logger.info(`${rel}:`);
|
|
808
853
|
for (const name of plugins) logger.info(` - ${name}`);
|
|
809
854
|
}
|
|
810
|
-
async function runAdd(ctx,
|
|
855
|
+
async function runAdd(ctx, name, opts) {
|
|
856
|
+
const { alias, spec } = parseAliasSpec(name);
|
|
857
|
+
if (!(alias in OFFICIAL_PLUGINS)) throw new Error(`'${alias}' is invalid for argument 'name'. Allowed choices are ${officialAliases().join(", ")}.`);
|
|
811
858
|
const { pkg: pkgName, exportName } = OFFICIAL_PLUGINS[alias];
|
|
812
|
-
|
|
859
|
+
const tag = spec && isDistTag(spec) ? spec : void 0;
|
|
860
|
+
const installSpec = spec ? `${pkgName}@${spec}` : pkgName;
|
|
861
|
+
clack.intro(` rr plugins add ${name} `);
|
|
813
862
|
const inPkg = hasInPackageJson(ctx, pkgName);
|
|
814
863
|
const ast = new ConfigAstService();
|
|
815
864
|
const loaded = await ast.load(ctx.appPkg.dirPath);
|
|
@@ -824,7 +873,7 @@ async function runAdd(ctx, alias, opts) {
|
|
|
824
873
|
const workspace = toNypmWorkspace(wsChoice);
|
|
825
874
|
const targetLabel = describeWorkspaceChoice(wsChoice);
|
|
826
875
|
if (opts.dryRun) {
|
|
827
|
-
clack.log.info(`Would: install ${
|
|
876
|
+
clack.log.info(`Would: install ${installSpec} as a devDependency in ${targetLabel}${inPkg ? " (already present, skipped)" : ""}.`);
|
|
828
877
|
if (!inConfig) {
|
|
829
878
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
830
879
|
clack.log.info(`Would: add ${exportName}() to ${rel} (plugins[]).`);
|
|
@@ -835,8 +884,8 @@ async function runAdd(ctx, alias, opts) {
|
|
|
835
884
|
}
|
|
836
885
|
let installedNow = false;
|
|
837
886
|
if (!inPkg) {
|
|
838
|
-
await withSpinner(`Installing ${
|
|
839
|
-
await addDependency([
|
|
887
|
+
await withSpinner(`Installing ${installSpec}`, async () => {
|
|
888
|
+
await addDependency([installSpec], {
|
|
840
889
|
cwd: ctx.appPkg.dirPath,
|
|
841
890
|
dev: true,
|
|
842
891
|
silent: true,
|
|
@@ -860,7 +909,8 @@ async function runAdd(ctx, alias, opts) {
|
|
|
860
909
|
force: !!opts.force,
|
|
861
910
|
yes: !!opts.yes,
|
|
862
911
|
nonInteractive: !!opts.yes
|
|
863
|
-
}
|
|
912
|
+
},
|
|
913
|
+
release: new ReleaseService(tag)
|
|
864
914
|
};
|
|
865
915
|
installResult = await plugin.install(installCtx);
|
|
866
916
|
}
|
|
@@ -969,6 +1019,18 @@ async function runRemove(ctx, alias, opts) {
|
|
|
969
1019
|
});
|
|
970
1020
|
clack.outro(`Plugin '${alias}' removed.`);
|
|
971
1021
|
}
|
|
1022
|
+
function parseAliasSpec(input) {
|
|
1023
|
+
const at = input.indexOf("@");
|
|
1024
|
+
if (at <= 0) return { alias: input };
|
|
1025
|
+
return {
|
|
1026
|
+
alias: input.slice(0, at),
|
|
1027
|
+
spec: input.slice(at + 1)
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
/** A dist-tag starts with a letter and contains only safe identifier chars. Version ranges (`^0.1`, `>=1`, `0.0.2`, `*`) don't match. */
|
|
1031
|
+
function isDistTag(spec) {
|
|
1032
|
+
return /^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(spec) && spec !== "latest";
|
|
1033
|
+
}
|
|
972
1034
|
function hasInPackageJson(ctx, pkgName) {
|
|
973
1035
|
const pkg = ctx.appPkg.packageJson;
|
|
974
1036
|
return pkgName in {
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { Pkg, ShellService } from "@vlandoss/clibuddy";
|
|
2
2
|
import { AnyLogger } from "@vlandoss/loggy";
|
|
3
3
|
|
|
4
|
+
//#region src/services/release.d.ts
|
|
5
|
+
type ReleaseServiceOptions = {
|
|
6
|
+
/** Override the HTTP probe — used by tests. */fetcher?: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Represents the "release" the current `rr plugins add` runs against — the
|
|
10
|
+
* dist-tag the user picked (default: latest), plus the logic to resolve install
|
|
11
|
+
* specs for related packages under that release.
|
|
12
|
+
*
|
|
13
|
+
* - With no `tag`, `resolve()` always returns `"latest"` and never hits the
|
|
14
|
+
* registry.
|
|
15
|
+
* - With a `tag` (e.g. `"pr-226"`), probes `<pkg>@<tag>`: returns the tag when
|
|
16
|
+
* it exists, falls back to `"latest"` otherwise so a partial preview release
|
|
17
|
+
* (where only a subset of packages got published) still installs cleanly.
|
|
18
|
+
* - Per-package result is cached within the service instance.
|
|
19
|
+
*/
|
|
20
|
+
declare class ReleaseService {
|
|
21
|
+
#private;
|
|
22
|
+
readonly tag: string | undefined;
|
|
23
|
+
constructor(tag: string | undefined, {
|
|
24
|
+
fetcher
|
|
25
|
+
}?: ReleaseServiceOptions);
|
|
26
|
+
resolve(pkg: string): Promise<string>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
4
29
|
//#region src/types/tool.d.ts
|
|
5
30
|
type FormatOptions = {
|
|
6
31
|
fix?: boolean;
|
|
@@ -110,6 +135,13 @@ type InstallContext = {
|
|
|
110
135
|
appPkg: Pkg;
|
|
111
136
|
prompts: ClackPrompts;
|
|
112
137
|
flags: InstallFlags;
|
|
138
|
+
/**
|
|
139
|
+
* The release this install is running against — encapsulates the dist-tag the
|
|
140
|
+
* user picked (`rr plugins add biome@pr-226`) and resolves install specs for
|
|
141
|
+
* related packages under it. Use `ctx.release.resolve(pkg)` for every
|
|
142
|
+
* `@rrlab/*-config` sibling so previews and `latest` work uniformly.
|
|
143
|
+
*/
|
|
144
|
+
release: ReleaseService;
|
|
113
145
|
};
|
|
114
146
|
type UninstallContext = {
|
|
115
147
|
shell: ShellService;
|
|
@@ -170,4 +202,4 @@ type UninstallResult = {
|
|
|
170
202
|
files?: FileOp[];
|
|
171
203
|
};
|
|
172
204
|
//#endregion
|
|
173
|
-
export { Linter as C, TypeChecker as D, TypeCheckOptions as E, LintOptions as S, StaticCheckerOptions as T, Doctor as _, InstallFlags as a, FormatOptions as b, PLUGIN_KINDS as c, PluginCapabilities as d, PluginContext as f, UninstallResult as g, UninstallFlags as h, InstallContext as i, Packer as l, UninstallContext as m, ClackPromptsSelectOption as n, InstallResult as o, PluginKind as p, FileOp as r, JsonEdit as s, ClackPrompts as t, Plugin as u, DoctorOutput as v, StaticChecker as w, Formatter as x, DoctorResult as y };
|
|
205
|
+
export { Linter as C, TypeChecker as D, TypeCheckOptions as E, ReleaseService as O, LintOptions as S, StaticCheckerOptions as T, Doctor as _, InstallFlags as a, FormatOptions as b, PLUGIN_KINDS as c, PluginCapabilities as d, PluginContext as f, UninstallResult as g, UninstallFlags as h, InstallContext as i, ReleaseServiceOptions as k, Packer as l, UninstallContext as m, ClackPromptsSelectOption as n, InstallResult as o, PluginKind as p, FileOp as r, JsonEdit as s, ClackPrompts as t, Plugin as u, DoctorOutput as v, StaticChecker as w, Formatter as x, DoctorResult as y };
|
package/package.json
CHANGED
package/src/lib/plugin.ts
CHANGED
package/src/plugin/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Pkg, ShellService } from "@vlandoss/clibuddy";
|
|
2
2
|
import type { AnyLogger as Logger } from "@vlandoss/loggy";
|
|
3
|
+
import type { ReleaseService } from "#src/services/release.ts";
|
|
3
4
|
import type { Doctor, Formatter, Linter, StaticChecker, TypeChecker } from "#src/types/tool.ts";
|
|
4
5
|
|
|
5
6
|
export type {
|
|
@@ -83,6 +84,13 @@ export type InstallContext = {
|
|
|
83
84
|
appPkg: Pkg;
|
|
84
85
|
prompts: ClackPrompts;
|
|
85
86
|
flags: InstallFlags;
|
|
87
|
+
/**
|
|
88
|
+
* The release this install is running against — encapsulates the dist-tag the
|
|
89
|
+
* user picked (`rr plugins add biome@pr-226`) and resolves install specs for
|
|
90
|
+
* related packages under it. Use `ctx.release.resolve(pkg)` for every
|
|
91
|
+
* `@rrlab/*-config` sibling so previews and `latest` work uniformly.
|
|
92
|
+
*/
|
|
93
|
+
release: ReleaseService;
|
|
86
94
|
};
|
|
87
95
|
|
|
88
96
|
export type UninstallContext = {
|
|
@@ -10,6 +10,7 @@ import { applyJsonEdits } from "#src/services/json-edit.ts";
|
|
|
10
10
|
import { logger } from "#src/services/logger.ts";
|
|
11
11
|
import { OFFICIAL_PLUGINS, type OfficialAlias, officialAliases } from "#src/services/plugins-registry.ts";
|
|
12
12
|
import { createClackPrompts } from "#src/services/prompts.ts";
|
|
13
|
+
import { ReleaseService } from "#src/services/release.ts";
|
|
13
14
|
import { describeWorkspaceChoice, resolveWorkspaceChoice, toNypmWorkspace } from "#src/services/workspace-target.ts";
|
|
14
15
|
|
|
15
16
|
type AddOptions = {
|
|
@@ -38,11 +39,13 @@ export function createPluginsCommand(ctx: Context) {
|
|
|
38
39
|
cmd
|
|
39
40
|
.command("add")
|
|
40
41
|
.description("install and configure an @rrlab plugin")
|
|
41
|
-
.addArgument(
|
|
42
|
+
.addArgument(
|
|
43
|
+
new Argument("<name>", `plugin alias (${officialAliases().join("|")}), optionally with @<spec> e.g. biome@pr-226`),
|
|
44
|
+
)
|
|
42
45
|
.option("--force", "re-run install even if the plugin is already configured")
|
|
43
46
|
.option("--yes", "skip prompts and use defaults (non-interactive)")
|
|
44
47
|
.option("--dry-run", "show what would happen, without applying changes")
|
|
45
|
-
.action((name:
|
|
48
|
+
.action((name: string, opts: AddOptions) => runAdd(ctx, name, opts));
|
|
46
49
|
|
|
47
50
|
cmd
|
|
48
51
|
.command("remove")
|
|
@@ -74,10 +77,16 @@ async function runList(ctx: Context) {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
async function runAdd(ctx: Context,
|
|
78
|
-
const {
|
|
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;
|
|
79
88
|
|
|
80
|
-
clack.intro(` rr plugins add ${
|
|
89
|
+
clack.intro(` rr plugins add ${name} `);
|
|
81
90
|
|
|
82
91
|
const inPkg = hasInPackageJson(ctx, pkgName);
|
|
83
92
|
const ast = new ConfigAstService();
|
|
@@ -97,7 +106,7 @@ async function runAdd(ctx: Context, alias: OfficialAlias, opts: AddOptions) {
|
|
|
97
106
|
|
|
98
107
|
if (opts.dryRun) {
|
|
99
108
|
clack.log.info(
|
|
100
|
-
`Would: install ${
|
|
109
|
+
`Would: install ${installSpec} as a devDependency in ${targetLabel}${inPkg ? " (already present, skipped)" : ""}.`,
|
|
101
110
|
);
|
|
102
111
|
if (!inConfig) {
|
|
103
112
|
const rel = path.relative(ctx.appPkg.dirPath, loaded.filepath) || loaded.filepath;
|
|
@@ -110,8 +119,8 @@ async function runAdd(ctx: Context, alias: OfficialAlias, opts: AddOptions) {
|
|
|
110
119
|
|
|
111
120
|
let installedNow = false;
|
|
112
121
|
if (!inPkg) {
|
|
113
|
-
await withSpinner(`Installing ${
|
|
114
|
-
await addDependency([
|
|
122
|
+
await withSpinner(`Installing ${installSpec}`, async () => {
|
|
123
|
+
await addDependency([installSpec], { cwd: ctx.appPkg.dirPath, dev: true, silent: true, workspace });
|
|
115
124
|
});
|
|
116
125
|
installedNow = true;
|
|
117
126
|
}
|
|
@@ -135,6 +144,7 @@ async function runAdd(ctx: Context, alias: OfficialAlias, opts: AddOptions) {
|
|
|
135
144
|
yes: !!opts.yes,
|
|
136
145
|
nonInteractive: !!opts.yes,
|
|
137
146
|
},
|
|
147
|
+
release: new ReleaseService(tag),
|
|
138
148
|
};
|
|
139
149
|
installResult = await plugin.install(installCtx);
|
|
140
150
|
}
|
|
@@ -260,6 +270,17 @@ async function runRemove(ctx: Context, alias: OfficialAlias, opts: RemoveOptions
|
|
|
260
270
|
clack.outro(`Plugin '${alias}' removed.`);
|
|
261
271
|
}
|
|
262
272
|
|
|
273
|
+
function parseAliasSpec(input: string): { alias: string; spec?: string } {
|
|
274
|
+
const at = input.indexOf("@");
|
|
275
|
+
if (at <= 0) return { alias: input };
|
|
276
|
+
return { alias: input.slice(0, at), spec: input.slice(at + 1) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** A dist-tag starts with a letter and contains only safe identifier chars. Version ranges (`^0.1`, `>=1`, `0.0.2`, `*`) don't match. */
|
|
280
|
+
function isDistTag(spec: string): boolean {
|
|
281
|
+
return /^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(spec) && spec !== "latest";
|
|
282
|
+
}
|
|
283
|
+
|
|
263
284
|
function hasInPackageJson(ctx: Context, pkgName: string): boolean {
|
|
264
285
|
const pkg = ctx.appPkg.packageJson;
|
|
265
286
|
const deps = {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
3
|
+
|
|
4
|
+
export type ReleaseServiceOptions = {
|
|
5
|
+
/** Override the HTTP probe — used by tests. */
|
|
6
|
+
fetcher?: typeof fetch;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents the "release" the current `rr plugins add` runs against — the
|
|
11
|
+
* dist-tag the user picked (default: latest), plus the logic to resolve install
|
|
12
|
+
* specs for related packages under that release.
|
|
13
|
+
*
|
|
14
|
+
* - With no `tag`, `resolve()` always returns `"latest"` and never hits the
|
|
15
|
+
* registry.
|
|
16
|
+
* - With a `tag` (e.g. `"pr-226"`), probes `<pkg>@<tag>`: returns the tag when
|
|
17
|
+
* it exists, falls back to `"latest"` otherwise so a partial preview release
|
|
18
|
+
* (where only a subset of packages got published) still installs cleanly.
|
|
19
|
+
* - Per-package result is cached within the service instance.
|
|
20
|
+
*/
|
|
21
|
+
export class ReleaseService {
|
|
22
|
+
readonly tag: string | undefined;
|
|
23
|
+
readonly #fetcher: typeof fetch;
|
|
24
|
+
readonly #cache = new Map<string, Promise<string>>();
|
|
25
|
+
|
|
26
|
+
constructor(tag: string | undefined, { fetcher = fetch }: ReleaseServiceOptions = {}) {
|
|
27
|
+
this.tag = tag;
|
|
28
|
+
this.#fetcher = fetcher;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resolve(pkg: string): Promise<string> {
|
|
32
|
+
if (!this.tag || this.tag === "latest") return Promise.resolve("latest");
|
|
33
|
+
const cached = this.#cache.get(pkg);
|
|
34
|
+
if (cached) return cached;
|
|
35
|
+
const promise = this.#probe(pkg);
|
|
36
|
+
this.#cache.set(pkg, promise);
|
|
37
|
+
return promise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async #probe(pkg: string): Promise<string> {
|
|
41
|
+
const tag = this.tag as string;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
44
|
+
try {
|
|
45
|
+
const res = await this.#fetcher(`${REGISTRY}/${pkg}/${encodeURIComponent(tag)}`, { signal: controller.signal });
|
|
46
|
+
return res.ok ? tag : "latest";
|
|
47
|
+
} catch {
|
|
48
|
+
return "latest";
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|