runtime-resolver 0.3.22 → 1.0.1

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 (68) hide show
  1. package/README.md +34 -35
  2. package/bin/runtime-resolver.js +12 -31
  3. package/command.js +120 -0
  4. package/layers.js +61 -0
  5. package/output.js +55 -0
  6. package/package.json +6 -18
  7. package/report.js +24 -0
  8. package/cli/commands/resolve.js +0 -242
  9. package/cli/schemas/json-schema.js +0 -12
  10. package/cli/schemas/response.js +0 -86
  11. package/data/bun-defaults.js +0 -850
  12. package/data/deno-defaults.js +0 -1538
  13. package/data/node-defaults.js +0 -4435
  14. package/errors/AuthenticationError.js +0 -35
  15. package/errors/CacheError.js +0 -32
  16. package/errors/FreshnessError.js +0 -38
  17. package/errors/InvalidInputError.js +0 -33
  18. package/errors/NetworkError.js +0 -34
  19. package/errors/ParseError.js +0 -33
  20. package/errors/RateLimitError.js +0 -37
  21. package/errors/VersionNotFoundError.js +0 -37
  22. package/index.d.ts +0 -2574
  23. package/index.js +0 -166
  24. package/layers/AutoBunCacheLive.js +0 -47
  25. package/layers/AutoDenoCacheLive.js +0 -47
  26. package/layers/AutoNodeCacheLive.js +0 -58
  27. package/layers/BunReleaseCacheLive.js +0 -25
  28. package/layers/BunResolverLive.js +0 -85
  29. package/layers/BunVersionFetcherLive.js +0 -55
  30. package/layers/DenoReleaseCacheLive.js +0 -25
  31. package/layers/DenoResolverLive.js +0 -84
  32. package/layers/DenoVersionFetcherLive.js +0 -51
  33. package/layers/FreshBunCacheLive.js +0 -60
  34. package/layers/FreshDenoCacheLive.js +0 -60
  35. package/layers/FreshNodeCacheLive.js +0 -55
  36. package/layers/GitHubAppAuth.js +0 -58
  37. package/layers/GitHubAutoAuth.js +0 -70
  38. package/layers/GitHubClientLive.js +0 -130
  39. package/layers/GitHubTokenAuth.js +0 -50
  40. package/layers/NodeReleaseCacheLive.js +0 -77
  41. package/layers/NodeResolverLive.js +0 -107
  42. package/layers/NodeScheduleFetcherLive.js +0 -37
  43. package/layers/NodeVersionFetcherLive.js +0 -55
  44. package/layers/OfflineBunCacheLive.js +0 -40
  45. package/layers/OfflineDenoCacheLive.js +0 -40
  46. package/layers/OfflineNodeCacheLive.js +0 -39
  47. package/layers/RuntimeCacheLive.js +0 -78
  48. package/lib/retry.js +0 -27
  49. package/lib/semver.js +0 -21
  50. package/schemas/bun-release.js +0 -63
  51. package/schemas/deno-release.js +0 -63
  52. package/schemas/github.js +0 -74
  53. package/schemas/node-release.js +0 -133
  54. package/schemas/node-schedule.js +0 -110
  55. package/schemas/node.js +0 -68
  56. package/services/BunReleaseCache.js +0 -33
  57. package/services/BunResolver.js +0 -36
  58. package/services/BunVersionFetcher.js +0 -41
  59. package/services/DenoReleaseCache.js +0 -33
  60. package/services/DenoResolver.js +0 -36
  61. package/services/DenoVersionFetcher.js +0 -41
  62. package/services/GitHubClient.js +0 -40
  63. package/services/NodeReleaseCache.js +0 -41
  64. package/services/NodeResolver.js +0 -44
  65. package/services/NodeScheduleFetcher.js +0 -38
  66. package/services/NodeVersionFetcher.js +0 -39
  67. package/services/OctokitInstance.js +0 -44
  68. package/tsdoc-metadata.json +0 -11
package/README.md CHANGED
@@ -1,63 +1,62 @@
1
1
  # runtime-resolver
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/runtime-resolver)](https://www.npmjs.com/package/runtime-resolver)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A520-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
- [![Effect](https://img.shields.io/badge/Effect-3.x-6644CC)](https://effect.website/)
3
+ [![npm](https://img.shields.io/npm/v/runtime-resolver?label=npm&color=cb3837)](https://www.npmjs.com/package/runtime-resolver)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![Effect](https://img.shields.io/badge/Effect-4.x-6644CC)](https://effect.website/)
8
7
 
9
- Resolve semver-compatible versions of Node.js, Bun and Deno runtimes. Fetches available versions from GitHub with automatic offline fallback via a build-time cache.
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 with a single call
14
- - Filter Node.js results by release phase (current, active-lts, maintenance-lts)
15
- - Control version granularity with increment levels (latest, minor, patch) for all runtimes
16
- - Track data provenance with the `source` field (`"api"` or `"cache"`)
17
- - Offline fallback using bundled version data when GitHub is unreachable
18
- - CLI with structured JSON output for CI/CD pipelines
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
- ## Installation
19
+ ## Install
22
20
 
23
21
  ```bash
24
- npm install runtime-resolver
22
+ npm install -g runtime-resolver
25
23
  ```
26
24
 
27
- The Effect packages (`effect`, `@effect/cli`, `@effect/platform`, `@effect/platform-node`) are declared as peer dependencies. npm 7+ and pnpm auto-install them by default. If you already use Effect in your project, your existing versions will be reused.
25
+ Or run it on demand without installing:
28
26
 
29
- ## Quick Start
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
- Set a `GITHUB_PERSONAL_ACCESS_TOKEN` or `GITHUB_TOKEN` environment variable for authenticated requests. Without one, the resolver falls back to cached data.
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
- ### CLI
33
+ ## Quick start
46
34
 
47
35
  ```bash
48
- npx runtime-resolver --node ">=22" --bun "^1" --deno ">=2" --pretty
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
- Output is structured JSON with a `$schema` reference for editor auto-complete. Print the full schema with `--schema`:
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 --schema
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
- For configuration, API reference, and advanced usage, see [docs](./docs/).
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](./LICENSE)
62
+ [MIT](LICENSE)
@@ -1,37 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { appIdOption, appInstallationIdOption, appPrivateKeyOption, bunDefaultOption, bunOption, denoDefaultOption, denoOption, incrementsOption, nodeDateOption, nodeDefaultOption, nodeOption, nodePhasesOption, prettyOption, resolveHandler, schemaOption, tokenOption } from "../cli/commands/resolve.js";
3
- import { Cause, Console, Effect } from "effect";
4
- import { Command } from "@effect/cli";
5
- import { NodeContext, NodeRuntime } from "@effect/platform-node";
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
- /* v8 ignore start - CLI entry point requires integration testing */
9
- const rootCommand = Command.make("runtime-resolver", {
10
- node: nodeOption,
11
- bun: bunOption,
12
- deno: denoOption,
13
- nodePhases: nodePhasesOption,
14
- increments: incrementsOption,
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.1" })(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.22",
3
+ "version": "1.0.1",
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
- "@octokit/auth-app": "^8.2.0",
47
- "octokit": "^5.0.5",
48
- "semver-effect": "^0.3.1"
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.5",
41
+ "@effected/semver": "~0.2.1",
42
+ "effect": "4.0.0-beta.101"
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 };