runtime-resolver 0.1.0 → 0.2.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 CHANGED
@@ -2,20 +2,21 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/runtime-resolver)](https://www.npmjs.com/package/runtime-resolver)
4
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/)
5
8
 
6
- 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.
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.
7
10
 
8
11
  ## Features
9
12
 
10
- - Resolve matching versions for Node.js, Bun, and Deno with a single call
13
+ - Resolve matching versions for Node.js, Bun and Deno with a single call
11
14
  - Filter Node.js results by release phase (current, active-lts, maintenance-lts)
12
15
  - Control version granularity with increment levels (latest, minor, patch) for all runtimes
13
- - Resolve a single version from a semver range with `resolveVersion`
14
- - Control data freshness with `freshness` option (`"auto"`, `"api"`, or `"cache"`)
15
16
  - Track data provenance with the `source` field (`"api"` or `"cache"`)
16
- - Input validation with typed `InvalidInputError` for all resolvers
17
17
  - Offline fallback using bundled version data when GitHub is unreachable
18
18
  - CLI with structured JSON output for CI/CD pipelines
19
+ - Full Effect API for custom layer composition and typed error handling
19
20
 
20
21
  ## Installation
21
22
 
@@ -37,8 +38,7 @@ const bun = await resolveBun({ semverRange: ">=1.1", increments: "minor" });
37
38
  const deno = await resolveDeno({ semverRange: ">=2", increments: "minor" });
38
39
  ```
39
40
 
40
- Set a `GITHUB_TOKEN` or `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable
41
- for authenticated requests. Without one, the resolver falls back to cached data.
41
+ Set a `GITHUB_TOKEN` or `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable for authenticated requests. Without one, the resolver falls back to cached data.
42
42
 
43
43
  ### CLI
44
44
 
@@ -52,64 +52,6 @@ Output is structured JSON with a `$schema` reference for editor auto-complete. P
52
52
  npx runtime-resolver --schema
53
53
  ```
54
54
 
55
- #### Useful `jq` recipes
56
-
57
- ```bash
58
- # Get the latest resolved Node.js version
59
- npx runtime-resolver --node ">=22" | jq -r '.results.node.latest'
60
-
61
- # List all matching versions as plain lines
62
- npx runtime-resolver --deno ">=2" | jq -r '.results.deno.versions[]'
63
-
64
- # Exit non-zero when any runtime fails to resolve
65
- npx runtime-resolver --node ">=22" --bun "^1" | jq -e '.ok'
66
-
67
- # Extract only the LTS version for Node.js
68
- npx runtime-resolver --node ">=20" --node-phases active-lts | jq -r '.results.node.lts'
69
-
70
- # Build a comma-separated matrix for GitHub Actions
71
- npx runtime-resolver --node ">=20" --increments minor \
72
- | jq -r '[.results.node.versions[]] | join(",")'
73
- ```
74
-
75
- ### Effect API
76
-
77
- Power users can compose resolvers with custom layers and typed error handling instead of the Promise wrappers.
78
-
79
- ```typescript
80
- import { Effect, Layer } from "effect";
81
- import {
82
- NodeResolver,
83
- NodeResolverLive,
84
- GitHubClientLive,
85
- GitHubTokenAuth,
86
- VersionCacheLive,
87
- } from "runtime-resolver";
88
-
89
- const program = Effect.gen(function* () {
90
- const resolver = yield* NodeResolver;
91
- return yield* resolver.resolve({
92
- semverRange: ">=22",
93
- phases: ["active-lts"],
94
- });
95
- });
96
-
97
- const layer = NodeResolverLive.pipe(
98
- Layer.provide(Layer.merge(
99
- GitHubClientLive.pipe(Layer.provide(GitHubTokenAuth)),
100
- VersionCacheLive,
101
- )),
102
- );
103
-
104
- Effect.runPromise(program.pipe(Effect.provide(layer)));
105
- ```
106
-
107
- All services (`NodeResolver`, `BunResolver`, `DenoResolver`, `GitHubClient`,
108
- `VersionCache`), live layers, auth layers (`GitHubTokenAuth`, `GitHubAppAuth`,
109
- `GitHubAutoAuth`), and typed errors (`AuthenticationError`, `NetworkError`,
110
- `RateLimitError`, `VersionNotFoundError`, etc.) are exported from the package
111
- root.
112
-
113
55
  ## Documentation
114
56
 
115
57
  For configuration, API reference, and advanced usage, see [docs](./docs/).
@@ -2,12 +2,21 @@
2
2
  import { Command, Options } from "@effect/cli";
3
3
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
4
  import { readFileSync } from "node:fs";
5
- import { BunResolver, Console, NodeResolverLive, Layer, GitHubAppAuth, GitHubAutoAuth, BunResolverLive, DenoResolverLive, GitHubClientLive, Cause, Effect, GitHubTokenAuthFromToken, VersionCacheLive, Option, NodeResolver, DenoResolver } from "../969.js";
5
+ import { NodeScheduleFetcherLive, AutoDenoCacheLive, DenoVersionFetcherLive, AutoNodeCacheLive, BunResolver, Console, Option, Layer, GitHubAppAuth, GitHubAutoAuth, BunResolverLive, BunVersionFetcherLive, DenoResolverLive, Cause, Effect, GitHubClientLive, AutoBunCacheLive, GitHubTokenAuthFromToken, NodeResolverLive, NodeVersionFetcherLive, DenoResolver, NodeResolver } from "../701.js";
6
6
  const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
7
- const SharedLayer = Layer.merge(GitHubLayer, VersionCacheLive);
8
- const NodeLayer = NodeResolverLive.pipe(Layer.provide(SharedLayer));
9
- const BunLayer = BunResolverLive.pipe(Layer.provide(SharedLayer));
10
- const DenoLayer = DenoResolverLive.pipe(Layer.provide(SharedLayer));
7
+ const makeNodeLayer = (authOverride)=>{
8
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
9
+ const fetchersLayer = Layer.merge(NodeVersionFetcherLive.pipe(Layer.provide(ghLayer)), NodeScheduleFetcherLive.pipe(Layer.provide(ghLayer)));
10
+ return NodeResolverLive.pipe(Layer.provide(AutoNodeCacheLive.pipe(Layer.provide(fetchersLayer))));
11
+ };
12
+ const makeBunLayer = (authOverride)=>{
13
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
14
+ return BunResolverLive.pipe(Layer.provide(AutoBunCacheLive.pipe(Layer.provide(BunVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
15
+ };
16
+ const makeDenoLayer = (authOverride)=>{
17
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
18
+ return DenoResolverLive.pipe(Layer.provide(AutoDenoCacheLive.pipe(Layer.provide(DenoVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
19
+ };
11
20
  const SCHEMA_URL = "https://raw.githubusercontent.com/spencerbeggs/runtime-resolver/main/runtime-resolver.schema.json";
12
21
  const nodeOption = Options.text("node").pipe(Options.optional);
13
22
  const bunOption = Options.text("bun").pipe(Options.optional);
@@ -17,7 +26,6 @@ const incrementsOption = Options.text("increments").pipe(Options.optional);
17
26
  const nodeDefaultOption = Options.text("node-default").pipe(Options.optional);
18
27
  const bunDefaultOption = Options.text("bun-default").pipe(Options.optional);
19
28
  const denoDefaultOption = Options.text("deno-default").pipe(Options.optional);
20
- const freshnessOption = Options.text("freshness").pipe(Options.optional);
21
29
  const nodeDateOption = Options.text("node-date").pipe(Options.optional);
22
30
  const prettyOption = Options.boolean("pretty").pipe(Options.withDefault(false));
23
31
  const schemaOption = Options.boolean("schema").pipe(Options.withDefault(false));
@@ -36,11 +44,6 @@ const VALID_INCREMENTS = [
36
44
  "minor",
37
45
  "patch"
38
46
  ];
39
- const VALID_FRESHNESS = [
40
- "auto",
41
- "api",
42
- "cache"
43
- ];
44
47
  const validatePhases = (raw)=>{
45
48
  const phases = raw.split(",").map((s)=>s.trim());
46
49
  for (const p of phases)if (!VALID_PHASES.includes(p)) throw new Error(`Invalid phase: "${p}". Valid values: ${VALID_PHASES.join(", ")}`);
@@ -50,10 +53,6 @@ const validateIncrements = (raw)=>{
50
53
  if (!VALID_INCREMENTS.includes(raw)) throw new Error(`Invalid increments value: "${raw}". Valid values: ${VALID_INCREMENTS.join(", ")}`);
51
54
  return raw;
52
55
  };
53
- const validateFreshness = (raw)=>{
54
- if (!VALID_FRESHNESS.includes(raw)) throw new Error(`Invalid freshness value: "${raw}". Valid values: ${VALID_FRESHNESS.join(", ")}`);
55
- return raw;
56
- };
57
56
  const serializeError = (error)=>{
58
57
  if (error && "object" == typeof error && "_tag" in error && "message" in error) {
59
58
  const { _tag, message, ...rest } = error;
@@ -92,7 +91,7 @@ const toError = (name, error)=>[
92
91
  error: serializeError(error)
93
92
  }
94
93
  ];
95
- const resolveNode = (semverRange, phases, increments, defaultVersion, date, freshness, authOverride)=>Effect.gen(function*() {
94
+ const resolveNode = (semverRange, phases, increments, defaultVersion, date, authOverride)=>Effect.gen(function*() {
96
95
  const resolver = yield* NodeResolver;
97
96
  const result = yield* resolver.resolve({
98
97
  semverRange,
@@ -107,14 +106,11 @@ const resolveNode = (semverRange, phases, increments, defaultVersion, date, fres
107
106
  } : {},
108
107
  ...date ? {
109
108
  date
110
- } : {},
111
- ...freshness ? {
112
- freshness
113
109
  } : {}
114
110
  });
115
111
  return toSuccess("node", result);
116
- }).pipe(Effect.provide(authOverride ? NodeResolverLive.pipe(Layer.provide(Layer.merge(GitHubClientLive.pipe(Layer.provide(authOverride)), VersionCacheLive))) : NodeLayer), Effect.catchAll((error)=>Effect.succeed(toError("node", error))));
117
- const resolveBun = (semverRange, increments, defaultVersion, freshness, authOverride)=>Effect.gen(function*() {
112
+ }).pipe(Effect.provide(makeNodeLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("node", error))));
113
+ const resolveBun = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
118
114
  const resolver = yield* BunResolver;
119
115
  const result = yield* resolver.resolve({
120
116
  semverRange,
@@ -123,14 +119,11 @@ const resolveBun = (semverRange, increments, defaultVersion, freshness, authOver
123
119
  } : {},
124
120
  ...defaultVersion ? {
125
121
  defaultVersion
126
- } : {},
127
- ...freshness ? {
128
- freshness
129
122
  } : {}
130
123
  });
131
124
  return toSuccess("bun", result);
132
- }).pipe(Effect.provide(authOverride ? BunResolverLive.pipe(Layer.provide(Layer.merge(GitHubClientLive.pipe(Layer.provide(authOverride)), VersionCacheLive))) : BunLayer), Effect.catchAll((error)=>Effect.succeed(toError("bun", error))));
133
- const resolveDeno = (semverRange, increments, defaultVersion, freshness, authOverride)=>Effect.gen(function*() {
125
+ }).pipe(Effect.provide(makeBunLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("bun", error))));
126
+ const resolveDeno = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
134
127
  const resolver = yield* DenoResolver;
135
128
  const result = yield* resolver.resolve({
136
129
  semverRange,
@@ -139,13 +132,10 @@ const resolveDeno = (semverRange, increments, defaultVersion, freshness, authOve
139
132
  } : {},
140
133
  ...defaultVersion ? {
141
134
  defaultVersion
142
- } : {},
143
- ...freshness ? {
144
- freshness
145
135
  } : {}
146
136
  });
147
137
  return toSuccess("deno", result);
148
- }).pipe(Effect.provide(authOverride ? DenoResolverLive.pipe(Layer.provide(Layer.merge(GitHubClientLive.pipe(Layer.provide(authOverride)), VersionCacheLive))) : DenoLayer), Effect.catchAll((error)=>Effect.succeed(toError("deno", error))));
138
+ }).pipe(Effect.provide(makeDenoLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("deno", error))));
149
139
  const formatResponse = (ok, results, pretty)=>{
150
140
  const response = {
151
141
  $schema: SCHEMA_URL,
@@ -162,7 +152,6 @@ const resolveHandler = (args)=>Effect.gen(function*() {
162
152
  args.deno,
163
153
  args.nodePhases,
164
154
  args.increments,
165
- args.freshness,
166
155
  args.nodeDefault,
167
156
  args.bunDefault,
168
157
  args.denoDefault,
@@ -227,13 +216,6 @@ const resolveHandler = (args)=>Effect.gen(function*() {
227
216
  yield* Console.error(e.message);
228
217
  return;
229
218
  }
230
- let validatedFreshness;
231
- if (Option.isSome(args.freshness)) try {
232
- validatedFreshness = validateFreshness(args.freshness.value);
233
- } catch (e) {
234
- yield* Console.error(e.message);
235
- return;
236
- }
237
219
  let nodeDate;
238
220
  if (Option.isSome(args.nodeDate)) {
239
221
  nodeDate = new Date(args.nodeDate.value);
@@ -242,15 +224,15 @@ const resolveHandler = (args)=>Effect.gen(function*() {
242
224
  const tasks = [];
243
225
  if (hasNode) {
244
226
  const nodeDefaultVersion = Option.isSome(args.nodeDefault) ? args.nodeDefault.value : void 0;
245
- tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, validatedFreshness, authLayerOverride));
227
+ tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, authLayerOverride));
246
228
  }
247
229
  if (hasBun) {
248
230
  const bunDefaultVersion = Option.isSome(args.bunDefault) ? args.bunDefault.value : void 0;
249
- tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, validatedFreshness, authLayerOverride));
231
+ tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, authLayerOverride));
250
232
  }
251
233
  if (hasDeno) {
252
234
  const denoDefaultVersion = Option.isSome(args.denoDefault) ? args.denoDefault.value : void 0;
253
- tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, validatedFreshness, authLayerOverride));
235
+ tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, authLayerOverride));
254
236
  }
255
237
  const entries = yield* Effect.all(tasks, {
256
238
  concurrency: "unbounded"
@@ -265,7 +247,6 @@ const rootCommand = Command.make("runtime-resolver", {
265
247
  deno: denoOption,
266
248
  nodePhases: nodePhasesOption,
267
249
  increments: incrementsOption,
268
- freshness: freshnessOption,
269
250
  nodeDefault: nodeDefaultOption,
270
251
  bunDefault: bunDefaultOption,
271
252
  denoDefault: denoDefaultOption,
@@ -279,7 +260,7 @@ const rootCommand = Command.make("runtime-resolver", {
279
260
  }, resolveHandler);
280
261
  const cli = Command.run(rootCommand, {
281
262
  name: "runtime-resolver",
282
- version: "0.1.0"
263
+ version: "0.2.0"
283
264
  });
284
265
  const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer), Effect.catchAllCause((cause)=>{
285
266
  const defects = Cause.defects(cause);