runtime-resolver 0.3.22 → 1.0.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 +34 -35
- package/bin/runtime-resolver.js +12 -31
- package/command.js +120 -0
- package/layers.js +61 -0
- package/output.js +55 -0
- package/package.json +6 -18
- package/report.js +24 -0
- package/cli/commands/resolve.js +0 -242
- package/cli/schemas/json-schema.js +0 -12
- package/cli/schemas/response.js +0 -86
- package/data/bun-defaults.js +0 -850
- package/data/deno-defaults.js +0 -1538
- package/data/node-defaults.js +0 -4435
- package/errors/AuthenticationError.js +0 -35
- package/errors/CacheError.js +0 -32
- package/errors/FreshnessError.js +0 -38
- package/errors/InvalidInputError.js +0 -33
- package/errors/NetworkError.js +0 -34
- package/errors/ParseError.js +0 -33
- package/errors/RateLimitError.js +0 -37
- package/errors/VersionNotFoundError.js +0 -37
- package/index.d.ts +0 -2574
- package/index.js +0 -166
- package/layers/AutoBunCacheLive.js +0 -47
- package/layers/AutoDenoCacheLive.js +0 -47
- package/layers/AutoNodeCacheLive.js +0 -58
- package/layers/BunReleaseCacheLive.js +0 -25
- package/layers/BunResolverLive.js +0 -85
- package/layers/BunVersionFetcherLive.js +0 -55
- package/layers/DenoReleaseCacheLive.js +0 -25
- package/layers/DenoResolverLive.js +0 -84
- package/layers/DenoVersionFetcherLive.js +0 -51
- package/layers/FreshBunCacheLive.js +0 -60
- package/layers/FreshDenoCacheLive.js +0 -60
- package/layers/FreshNodeCacheLive.js +0 -55
- package/layers/GitHubAppAuth.js +0 -58
- package/layers/GitHubAutoAuth.js +0 -70
- package/layers/GitHubClientLive.js +0 -130
- package/layers/GitHubTokenAuth.js +0 -50
- package/layers/NodeReleaseCacheLive.js +0 -77
- package/layers/NodeResolverLive.js +0 -107
- package/layers/NodeScheduleFetcherLive.js +0 -37
- package/layers/NodeVersionFetcherLive.js +0 -55
- package/layers/OfflineBunCacheLive.js +0 -40
- package/layers/OfflineDenoCacheLive.js +0 -40
- package/layers/OfflineNodeCacheLive.js +0 -39
- package/layers/RuntimeCacheLive.js +0 -78
- package/lib/retry.js +0 -27
- package/lib/semver.js +0 -21
- package/schemas/bun-release.js +0 -63
- package/schemas/deno-release.js +0 -63
- package/schemas/github.js +0 -74
- package/schemas/node-release.js +0 -133
- package/schemas/node-schedule.js +0 -110
- package/schemas/node.js +0 -68
- package/services/BunReleaseCache.js +0 -33
- package/services/BunResolver.js +0 -36
- package/services/BunVersionFetcher.js +0 -41
- package/services/DenoReleaseCache.js +0 -33
- package/services/DenoResolver.js +0 -36
- package/services/DenoVersionFetcher.js +0 -41
- package/services/GitHubClient.js +0 -40
- package/services/NodeReleaseCache.js +0 -41
- package/services/NodeResolver.js +0 -44
- package/services/NodeScheduleFetcher.js +0 -38
- package/services/NodeVersionFetcher.js +0 -39
- package/services/OctokitInstance.js +0 -44
- package/tsdoc-metadata.json +0 -11
package/README.md
CHANGED
|
@@ -1,63 +1,62 @@
|
|
|
1
1
|
# runtime-resolver
|
|
2
2
|
|
|
3
|
-
[](https://effect.website/)
|
|
3
|
+
[](https://www.npmjs.com/package/runtime-resolver)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://effect.website/)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
A command-line tool that resolves semver-compatible versions of Node.js, Bun, and Deno runtimes and prints them as JSON. It fetches live version data with an automatic fallback to a bundled offline snapshot, so it keeps working in CI and air-gapped environments.
|
|
10
9
|
|
|
11
10
|
## Features
|
|
12
11
|
|
|
13
|
-
- Resolve matching versions for Node.js, Bun and Deno
|
|
14
|
-
- Filter Node.js results by release phase (current, active-lts, maintenance-lts)
|
|
15
|
-
- Control version granularity with increment levels (latest, minor, patch)
|
|
16
|
-
- Track data provenance with the `source` field (`"api"`
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Full Effect API for custom layer composition and typed error handling
|
|
12
|
+
- Resolve matching versions for Node.js, Bun, and Deno in a single call.
|
|
13
|
+
- Filter Node.js results by release phase (current, active-lts, maintenance-lts, end-of-life).
|
|
14
|
+
- Control version granularity with increment levels (latest, minor, patch) across every runtime.
|
|
15
|
+
- Track data provenance with the `source` field (`"api"` for a live fetch, `"cache"` for the offline snapshot).
|
|
16
|
+
- Fall back to a bundled offline snapshot when the network or GitHub API is unavailable.
|
|
17
|
+
- Emit structured JSON that pairs cleanly with `jq` for CI/CD version matrices.
|
|
20
18
|
|
|
21
|
-
##
|
|
19
|
+
## Install
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
npm install runtime-resolver
|
|
22
|
+
npm install -g runtime-resolver
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
Or run it on demand without installing:
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import { resolveNode, resolveBun, resolveDeno } from "runtime-resolver";
|
|
33
|
-
|
|
34
|
-
const node = await resolveNode({ semverRange: ">=20" });
|
|
35
|
-
console.log(node.latest); // e.g. "22.14.0"
|
|
36
|
-
console.log(node.source); // "api" or "cache"
|
|
37
|
-
console.log(node.default); // latest LTS version
|
|
38
|
-
|
|
39
|
-
const bun = await resolveBun({ semverRange: ">=1.1", increments: "minor" });
|
|
40
|
-
const deno = await resolveDeno({ semverRange: ">=2", increments: "minor" });
|
|
27
|
+
```bash
|
|
28
|
+
npx runtime-resolver --node ">=20"
|
|
41
29
|
```
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
The runtime dependencies (`effect`, `@effected/runtimes`, `@effected/semver`) install automatically — there are no peer dependencies to add by hand. Requires Node.js >=24.11.0.
|
|
44
32
|
|
|
45
|
-
|
|
33
|
+
## Quick start
|
|
46
34
|
|
|
47
35
|
```bash
|
|
48
|
-
npx runtime-resolver --node ">=
|
|
36
|
+
npx runtime-resolver --node ">=20"
|
|
37
|
+
# {"source":"api","versions":["22.14.0","20.19.0"],"latest":"22.14.0","lts":"20.19.0","default":"20.19.0"}
|
|
49
38
|
```
|
|
50
39
|
|
|
51
|
-
|
|
40
|
+
Request several runtimes at once and the output becomes an object keyed by runtime name:
|
|
52
41
|
|
|
53
42
|
```bash
|
|
54
|
-
npx runtime-resolver --
|
|
43
|
+
npx runtime-resolver --node ">=20" --bun ">=1" --deno ">=2" --pretty
|
|
44
|
+
# {
|
|
45
|
+
# "node": { "source": "api", "versions": ["22.14.0"], "latest": "22.14.0", "lts": "20.19.0", "default": "20.19.0" },
|
|
46
|
+
# "bun": { "source": "api", "versions": ["1.1.42"], "latest": "1.1.42" },
|
|
47
|
+
# "deno": { "source": "api", "versions": ["2.1.4"], "latest": "2.1.4" }
|
|
48
|
+
# }
|
|
55
49
|
```
|
|
56
50
|
|
|
51
|
+
The CLI exits `0` when every requested runtime resolves. A usage error or resolution failure writes a one-line `error: …` to stderr and exits non-zero, so scripts can branch on the exit status.
|
|
52
|
+
|
|
57
53
|
## Documentation
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
- [CLI reference](../docs/cli.md) — flags, output format, exit codes, jq recipes, and scripting.
|
|
56
|
+
- [GitHub authentication and rate limits](../docs/github.md) — why Bun and Deno need a token, rate limits, token detection order, offline fallback, and the GitHub Actions matrix example.
|
|
57
|
+
|
|
58
|
+
See [docs](../docs/) for the full guide index.
|
|
60
59
|
|
|
61
60
|
## License
|
|
62
61
|
|
|
63
|
-
[MIT](
|
|
62
|
+
[MIT](LICENSE)
|
package/bin/runtime-resolver.js
CHANGED
|
@@ -1,37 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { NodeCliEnvironment } from "../layers.js";
|
|
3
|
+
import { cli } from "../command.js";
|
|
4
|
+
import { failureMessage } from "../report.js";
|
|
5
|
+
import { Effect, Exit } from "effect";
|
|
6
|
+
import { Command } from "effect/unstable/cli";
|
|
6
7
|
|
|
7
8
|
//#region src/cli/index.ts
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
nodeDefault: nodeDefaultOption,
|
|
16
|
-
bunDefault: bunDefaultOption,
|
|
17
|
-
denoDefault: denoDefaultOption,
|
|
18
|
-
nodeDate: nodeDateOption,
|
|
19
|
-
pretty: prettyOption,
|
|
20
|
-
schema: schemaOption,
|
|
21
|
-
token: tokenOption,
|
|
22
|
-
appId: appIdOption,
|
|
23
|
-
appPrivateKey: appPrivateKeyOption,
|
|
24
|
-
appInstallationId: appInstallationIdOption
|
|
25
|
-
}, resolveHandler);
|
|
26
|
-
const cli = Command.run(rootCommand, {
|
|
27
|
-
name: "runtime-resolver",
|
|
28
|
-
version: "0.3.22"
|
|
29
|
-
});
|
|
30
|
-
const main = Effect.suspend(() => cli(process.argv)).pipe(Effect.provide(NodeContext.layer), Effect.catchAllCause((cause) => {
|
|
31
|
-
if (Cause.defects(cause).length > 0) return Console.error(Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
32
|
-
return Effect.failCause(cause);
|
|
33
|
-
}));
|
|
34
|
-
NodeRuntime.runMain(main);
|
|
9
|
+
const program = Command.runWith(cli, { version: "1.0.0" })(process.argv.slice(2)).pipe(Effect.provide(NodeCliEnvironment));
|
|
10
|
+
const exit = await Effect.runPromiseExit(program);
|
|
11
|
+
if (Exit.isFailure(exit)) {
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
const message = failureMessage(exit.cause);
|
|
14
|
+
if (message !== null) process.stderr.write(`${message}\n`);
|
|
15
|
+
}
|
|
35
16
|
|
|
36
17
|
//#endregion
|
|
37
18
|
export { };
|
package/command.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { selectResolverLayers } from "./layers.js";
|
|
2
|
+
import { NODE_PHASES, formatOutput, parsePhases } from "./output.js";
|
|
3
|
+
import { Console, DateTime, Effect, Option, Result } from "effect";
|
|
4
|
+
import { CliError, Command, Flag } from "effect/unstable/cli";
|
|
5
|
+
import { BunResolver, DenoResolver, NodeResolver } from "@effected/runtimes";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/command.ts
|
|
8
|
+
/**
|
|
9
|
+
* Turn a resolver's typed failure into the one-line, operator-facing message
|
|
10
|
+
* the process prints to stderr before exiting non-zero.
|
|
11
|
+
*/
|
|
12
|
+
const describeResolveError = (runtime, error) => {
|
|
13
|
+
switch (error._tag) {
|
|
14
|
+
case "InvalidRangeError": return `invalid ${runtime} range: ${error.message}`;
|
|
15
|
+
case "NoMatchingVersionError": {
|
|
16
|
+
const scope = error.phases !== void 0 ? ` within phase(s) ${error.phases.join(", ")}` : "";
|
|
17
|
+
return `no ${runtime} version matched "${error.constraint}"${scope}`;
|
|
18
|
+
}
|
|
19
|
+
case "UnresolvableDefaultError": return `no ${runtime} version matched the requested default "${error.defaultVersion}"`;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolve one runtime: run its resolver effect, map any typed resolution
|
|
24
|
+
* failure to a user-facing CLI error, and provide the runtime's chosen layer.
|
|
25
|
+
* Extracted so the node/bun/deno branches share one resolve/map/provide shape.
|
|
26
|
+
*/
|
|
27
|
+
const resolveRuntime = (runtime, resolve, layer) => resolve.pipe(Effect.mapError((error) => new CliError.UserError({ cause: describeResolveError(runtime, error) })), Effect.provide(layer));
|
|
28
|
+
/** Fail with the CLI framework's user-error, carrying an already-formatted message. */
|
|
29
|
+
const usageError = (message) => Effect.fail(new CliError.UserError({ cause: message }));
|
|
30
|
+
const nodeFlag = Flag.optional(Flag.string("node").pipe(Flag.withDescription("Resolve Node.js versions for this semver range")));
|
|
31
|
+
const bunFlag = Flag.optional(Flag.string("bun").pipe(Flag.withDescription("Resolve Bun versions for this semver range")));
|
|
32
|
+
const denoFlag = Flag.optional(Flag.string("deno").pipe(Flag.withDescription("Resolve Deno versions for this semver range")));
|
|
33
|
+
const nodePhasesFlag = Flag.optional(Flag.string("node-phases").pipe(Flag.withDescription(`Comma-separated Node lifecycle phases (${NODE_PHASES.join(", ")})`)));
|
|
34
|
+
const incrementsFlag = Flag.optional(Flag.choice("increments", [
|
|
35
|
+
"latest",
|
|
36
|
+
"minor",
|
|
37
|
+
"patch"
|
|
38
|
+
]).pipe(Flag.withDescription("Grouping granularity applied to every requested runtime")));
|
|
39
|
+
const nodeDefaultFlag = Flag.optional(Flag.string("node-default").pipe(Flag.withDescription("Range whose newest Node match becomes the `default` field")));
|
|
40
|
+
const bunDefaultFlag = Flag.optional(Flag.string("bun-default").pipe(Flag.withDescription("Range whose newest Bun match becomes the `default` field")));
|
|
41
|
+
const denoDefaultFlag = Flag.optional(Flag.string("deno-default").pipe(Flag.withDescription("Range whose newest Deno match becomes the `default` field")));
|
|
42
|
+
const nodeDateFlag = Flag.optional(Flag.date("node-date").pipe(Flag.withDescription("ISO date at which to evaluate Node lifecycle phases")));
|
|
43
|
+
const prettyFlag = Flag.boolean("pretty").pipe(Flag.withDescription("Pretty-print the JSON output with 2-space indentation"));
|
|
44
|
+
const offlineFlag = Flag.boolean("offline").pipe(Flag.withDescription("Resolve from the bundled snapshot only; make no network requests"));
|
|
45
|
+
const tokenFlag = Flag.optional(Flag.redacted("token").pipe(Flag.withDescription("GitHub personal access token used for Bun and Deno lookups")));
|
|
46
|
+
/**
|
|
47
|
+
* Build the `runtime-resolver` command around a resolver-layer selector.
|
|
48
|
+
*
|
|
49
|
+
* The selector is called once per run with the parsed `--offline` flag to pick
|
|
50
|
+
* the layer set (live feeds, or the snapshot-only `.layerOffline`). Each
|
|
51
|
+
* runtime's chosen layer is provided *inside* its flag-gated branch and nowhere
|
|
52
|
+
* else, so an unrequested runtime's layer is never built — and, because the
|
|
53
|
+
* kit's live `.layer` fetches its feed at acquisition, never touches the
|
|
54
|
+
* network. Parameterizing the selector is also what lets tests inject
|
|
55
|
+
* `.layerOffline` (or a tripwire that dies if built).
|
|
56
|
+
*/
|
|
57
|
+
const makeCli = (selectLayers) => Command.make("runtime-resolver", {
|
|
58
|
+
node: nodeFlag,
|
|
59
|
+
bun: bunFlag,
|
|
60
|
+
deno: denoFlag,
|
|
61
|
+
nodePhases: nodePhasesFlag,
|
|
62
|
+
increments: incrementsFlag,
|
|
63
|
+
nodeDefault: nodeDefaultFlag,
|
|
64
|
+
bunDefault: bunDefaultFlag,
|
|
65
|
+
denoDefault: denoDefaultFlag,
|
|
66
|
+
nodeDate: nodeDateFlag,
|
|
67
|
+
pretty: prettyFlag,
|
|
68
|
+
offline: offlineFlag,
|
|
69
|
+
token: tokenFlag
|
|
70
|
+
}, (config) => Effect.gen(function* () {
|
|
71
|
+
if (Option.isNone(config.node) && Option.isNone(config.bun) && Option.isNone(config.deno)) return yield* usageError("no runtime requested; pass at least one of --node, --bun, --deno");
|
|
72
|
+
const layers = selectLayers(config.offline);
|
|
73
|
+
let phases;
|
|
74
|
+
if (Option.isSome(config.nodePhases)) {
|
|
75
|
+
const parsed = parsePhases(config.nodePhases.value);
|
|
76
|
+
if (Result.isFailure(parsed)) return yield* usageError(parsed.failure);
|
|
77
|
+
phases = parsed.success;
|
|
78
|
+
}
|
|
79
|
+
const increments = Option.getOrUndefined(config.increments);
|
|
80
|
+
const nodeDate = Option.isSome(config.nodeDate) ? DateTime.fromDateUnsafe(config.nodeDate.value) : void 0;
|
|
81
|
+
const entries = [];
|
|
82
|
+
if (Option.isSome(config.node)) {
|
|
83
|
+
const range = config.node.value;
|
|
84
|
+
const defaultVersion = Option.getOrUndefined(config.nodeDefault);
|
|
85
|
+
const resolved = yield* resolveRuntime("node", Effect.flatMap(NodeResolver, (resolver) => resolver.resolve({
|
|
86
|
+
range,
|
|
87
|
+
...phases !== void 0 ? { phases } : {},
|
|
88
|
+
...increments !== void 0 ? { increments } : {},
|
|
89
|
+
...defaultVersion !== void 0 ? { defaultVersion } : {},
|
|
90
|
+
...nodeDate !== void 0 ? { date: nodeDate } : {}
|
|
91
|
+
})), layers.node);
|
|
92
|
+
entries.push(["node", resolved]);
|
|
93
|
+
}
|
|
94
|
+
if (Option.isSome(config.bun)) {
|
|
95
|
+
const range = config.bun.value;
|
|
96
|
+
const defaultVersion = Option.getOrUndefined(config.bunDefault);
|
|
97
|
+
const resolved = yield* resolveRuntime("bun", Effect.flatMap(BunResolver, (resolver) => resolver.resolve({
|
|
98
|
+
range,
|
|
99
|
+
...increments !== void 0 ? { increments } : {},
|
|
100
|
+
...defaultVersion !== void 0 ? { defaultVersion } : {}
|
|
101
|
+
})), layers.bun(config.token));
|
|
102
|
+
entries.push(["bun", resolved]);
|
|
103
|
+
}
|
|
104
|
+
if (Option.isSome(config.deno)) {
|
|
105
|
+
const range = config.deno.value;
|
|
106
|
+
const defaultVersion = Option.getOrUndefined(config.denoDefault);
|
|
107
|
+
const resolved = yield* resolveRuntime("deno", Effect.flatMap(DenoResolver, (resolver) => resolver.resolve({
|
|
108
|
+
range,
|
|
109
|
+
...increments !== void 0 ? { increments } : {},
|
|
110
|
+
...defaultVersion !== void 0 ? { defaultVersion } : {}
|
|
111
|
+
})), layers.deno(config.token));
|
|
112
|
+
entries.push(["deno", resolved]);
|
|
113
|
+
}
|
|
114
|
+
yield* Console.log(formatOutput(entries, config.pretty));
|
|
115
|
+
}));
|
|
116
|
+
/** The production command: `--offline` selects snapshot-only layers, else live. */
|
|
117
|
+
const cli = makeCli(selectResolverLayers);
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
export { cli, makeCli };
|
package/layers.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect, FileSystem, Layer, Option, Path, Stdio, Terminal } from "effect";
|
|
2
|
+
import { BunResolver, DenoResolver, GitHubAuth, GitHubClient, NodeResolver } from "@effected/runtimes";
|
|
3
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
4
|
+
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
5
|
+
|
|
6
|
+
//#region src/cli/layers.ts
|
|
7
|
+
/**
|
|
8
|
+
* The `fetch`-backed HTTP client, bound to one constant so every resolver layer
|
|
9
|
+
* shares the same reference and the runtime memoizes it to a single build.
|
|
10
|
+
*/
|
|
11
|
+
const httpClient = FetchHttpClient.layer;
|
|
12
|
+
/**
|
|
13
|
+
* Choose the GitHub credential wiring.
|
|
14
|
+
*
|
|
15
|
+
* With `--token`, authenticate with the supplied PAT over `fetch`. Without one,
|
|
16
|
+
* `GitHubClient.layerDefault` pre-wires environment-detected auth
|
|
17
|
+
* (`GITHUB_PERSONAL_ACCESS_TOKEN` → `GITHUB_TOKEN` → anonymous) over `fetch`.
|
|
18
|
+
*/
|
|
19
|
+
const githubClientFor = (token) => Option.match(token, {
|
|
20
|
+
onNone: () => GitHubClient.layerDefault,
|
|
21
|
+
onSome: (pat) => GitHubClient.layer.pipe(Layer.provide(Layer.mergeAll(GitHubAuth.token(pat), httpClient)))
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* The live resolver layers: real feeds over `fetch`, with the kit's snapshot
|
|
25
|
+
* fallback. Each is built only when its runtime's branch runs.
|
|
26
|
+
*/
|
|
27
|
+
const liveResolverLayers = {
|
|
28
|
+
node: NodeResolver.layer.pipe(Layer.provide(httpClient)),
|
|
29
|
+
bun: (token) => BunResolver.layer.pipe(Layer.provide(githubClientFor(token))),
|
|
30
|
+
deno: (token) => DenoResolver.layer.pipe(Layer.provide(githubClientFor(token)))
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The snapshot-only resolver layers used by `--offline`: the kit's
|
|
34
|
+
* `.layerOffline`, which performs no IO, requires nothing, and needs no token.
|
|
35
|
+
* Every answer is `source: "cache"`.
|
|
36
|
+
*/
|
|
37
|
+
const offlineResolverLayers = {
|
|
38
|
+
node: NodeResolver.layerOffline,
|
|
39
|
+
bun: () => BunResolver.layerOffline,
|
|
40
|
+
deno: () => DenoResolver.layerOffline
|
|
41
|
+
};
|
|
42
|
+
/** Pick the resolver layer set for a run: snapshot-only when `--offline`, else live. */
|
|
43
|
+
const selectResolverLayers = (offline) => offline ? offlineResolverLayers : liveResolverLayers;
|
|
44
|
+
/** A `Terminal` that dies if any member is touched — this CLI is non-interactive. */
|
|
45
|
+
const deadTerminal = Terminal.make({
|
|
46
|
+
columns: Effect.die("terminal is not available in runtime-resolver"),
|
|
47
|
+
rows: Effect.die("terminal is not available in runtime-resolver"),
|
|
48
|
+
readInput: Effect.die("terminal input is not available in runtime-resolver"),
|
|
49
|
+
readLine: Effect.die("terminal input is not available in runtime-resolver"),
|
|
50
|
+
display: () => Effect.die("terminal output is not available in runtime-resolver")
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* The five `Command.Environment` services, implemented for a non-interactive
|
|
54
|
+
* Node bin. `Path` is real; `FileSystem`, `Terminal` and `ChildProcessSpawner`
|
|
55
|
+
* are unused by this CLI and fail loudly if that ever changes; `Stdio` is inert
|
|
56
|
+
* because arguments come from `process.argv` and output goes through `Console`.
|
|
57
|
+
*/
|
|
58
|
+
const NodeCliEnvironment = Layer.mergeAll(Path.layer, FileSystem.layerNoop({}), Stdio.layerTest({}), Layer.succeed(Terminal.Terminal, deadTerminal), Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("subprocess spawning is not available in runtime-resolver"))));
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { NodeCliEnvironment, githubClientFor, liveResolverLayers, offlineResolverLayers, selectResolverLayers };
|
package/output.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/output.ts
|
|
4
|
+
/**
|
|
5
|
+
* The Node.js lifecycle phases a `--node-phases` value may name, mirroring the
|
|
6
|
+
* kit's `NodePhase` vocabulary. Kept as a runtime tuple so the parser can both
|
|
7
|
+
* validate against it and quote it back in an error message.
|
|
8
|
+
*/
|
|
9
|
+
const NODE_PHASES = [
|
|
10
|
+
"current",
|
|
11
|
+
"active-lts",
|
|
12
|
+
"maintenance-lts",
|
|
13
|
+
"end-of-life"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Parse the comma-separated `--node-phases` value into the kit's phase array.
|
|
17
|
+
*
|
|
18
|
+
* A malformed value is a usage error, not a data condition, so this returns a
|
|
19
|
+
* `Result` whose failure is the operator-facing message the caller prints to
|
|
20
|
+
* stderr before failing the process.
|
|
21
|
+
*/
|
|
22
|
+
const parsePhases = (csv) => {
|
|
23
|
+
const tokens = csv.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
24
|
+
if (tokens.length === 0) return Result.fail(`--node-phases was empty; expected any of: ${NODE_PHASES.join(", ")}`);
|
|
25
|
+
const known = new Set(NODE_PHASES);
|
|
26
|
+
const invalid = tokens.filter((token) => !known.has(token));
|
|
27
|
+
if (invalid.length > 0) return Result.fail(`invalid --node-phases value${invalid.length > 1 ? "s" : ""}: ${invalid.join(", ")}. Expected any of: ${NODE_PHASES.join(", ")}`);
|
|
28
|
+
return Result.succeed(tokens);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Project a `ResolvedVersions` onto a plain object so the output is exactly the
|
|
32
|
+
* kit's shape — no schema markers, and no explicit `undefined` for the optional
|
|
33
|
+
* fields the resolver omitted.
|
|
34
|
+
*/
|
|
35
|
+
const toPlain = (resolved) => ({
|
|
36
|
+
source: resolved.source,
|
|
37
|
+
versions: resolved.versions,
|
|
38
|
+
latest: resolved.latest,
|
|
39
|
+
...resolved.lts !== void 0 ? { lts: resolved.lts } : {},
|
|
40
|
+
...resolved.default !== void 0 ? { default: resolved.default } : {}
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Render the resolution output as JSON.
|
|
44
|
+
*
|
|
45
|
+
* A single requested runtime emits its `ResolvedVersions` directly; several
|
|
46
|
+
* emit an object keyed by runtime name, in the order the entries were supplied.
|
|
47
|
+
* `pretty` selects 2-space indentation over the compact default.
|
|
48
|
+
*/
|
|
49
|
+
const formatOutput = (entries, pretty) => {
|
|
50
|
+
const payload = entries.length === 1 ? toPlain(entries[0][1]) : Object.fromEntries(entries.map(([runtime, resolved]) => [runtime, toPlain(resolved)]));
|
|
51
|
+
return JSON.stringify(payload, null, pretty ? 2 : void 0);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { NODE_PHASES, formatOutput, parsePhases, toPlain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runtime-resolver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Resolve semver-compatible versions of Node.js, Bun, and Deno runtimes with Effect-based architecture and offline fallback.",
|
|
6
6
|
"keywords": [
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/spencerbeggs/runtime-resolver.git"
|
|
25
|
+
"url": "git+https://github.com/spencerbeggs/runtime-resolver.git",
|
|
26
|
+
"directory": "package"
|
|
26
27
|
},
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"author": {
|
|
@@ -32,26 +33,13 @@
|
|
|
32
33
|
},
|
|
33
34
|
"sideEffects": false,
|
|
34
35
|
"type": "module",
|
|
35
|
-
"exports": {
|
|
36
|
-
".": {
|
|
37
|
-
"types": "./index.d.ts",
|
|
38
|
-
"import": "./index.js"
|
|
39
|
-
},
|
|
40
|
-
"./package.json": "./package.json"
|
|
41
|
-
},
|
|
42
36
|
"bin": {
|
|
43
37
|
"runtime-resolver": "bin/runtime-resolver.js"
|
|
44
38
|
},
|
|
45
39
|
"dependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
},
|
|
50
|
-
"peerDependencies": {
|
|
51
|
-
"@effect/cli": "^0.75.2",
|
|
52
|
-
"@effect/platform": "^0.96.0",
|
|
53
|
-
"@effect/platform-node": "^0.107.0",
|
|
54
|
-
"effect": "^3.21.0"
|
|
40
|
+
"@effected/runtimes": "^0.1.4",
|
|
41
|
+
"@effected/semver": "~0.2.0",
|
|
42
|
+
"effect": "4.0.0-beta.99"
|
|
55
43
|
},
|
|
56
44
|
"engines": {
|
|
57
45
|
"node": ">=24.11.0"
|
package/report.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Cause, Option } from "effect";
|
|
2
|
+
import { CliError } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/report.ts
|
|
5
|
+
/**
|
|
6
|
+
* The stderr line to print for a failed run, or `null` when nothing should be
|
|
7
|
+
* printed because the CLI framework already rendered it.
|
|
8
|
+
*
|
|
9
|
+
* A `ShowHelp` failure has already written help + errors to the console, so it
|
|
10
|
+
* returns `null`. A `UserError` carries an already-formatted message. Anything
|
|
11
|
+
* else — a defect or interrupt with no typed failure — is surfaced through the
|
|
12
|
+
* full cause rather than swallowed.
|
|
13
|
+
*/
|
|
14
|
+
const failureMessage = (cause) => {
|
|
15
|
+
const error = Cause.findErrorOption(cause);
|
|
16
|
+
if (Option.isNone(error)) return Cause.pretty(cause);
|
|
17
|
+
const value = error.value;
|
|
18
|
+
if (CliError.isCliError(value) && value._tag === "ShowHelp") return null;
|
|
19
|
+
if (value instanceof CliError.UserError) return `error: ${String(value.cause)}`;
|
|
20
|
+
return `error: ${String(value)}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { failureMessage };
|