runtime-resolver 0.1.0 → 0.3.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/).
@@ -1,13 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Options } from "@effect/cli";
3
3
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
+ import { Cause, Console, Effect, Layer, Option } from "effect";
4
5
  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";
6
+ import { NodeScheduleFetcherLive, AutoDenoCacheLive, DenoVersionFetcherLive, AutoNodeCacheLive, BunResolver, NodeResolver, NodeVersionFetcherLive, NodeResolverLive, GitHubAutoAuth, BunResolverLive, BunVersionFetcherLive, GitHubClientLive, DenoResolverLive, GitHubTokenAuthFromToken, GitHubAppAuth, AutoBunCacheLive, DenoResolver } from "../711.js";
6
7
  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));
8
+ const makeNodeLayer = (authOverride)=>{
9
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
10
+ const fetchersLayer = Layer.merge(NodeVersionFetcherLive.pipe(Layer.provide(ghLayer)), NodeScheduleFetcherLive.pipe(Layer.provide(ghLayer)));
11
+ return NodeResolverLive.pipe(Layer.provide(AutoNodeCacheLive.pipe(Layer.provide(fetchersLayer))));
12
+ };
13
+ const makeBunLayer = (authOverride)=>{
14
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
15
+ return BunResolverLive.pipe(Layer.provide(AutoBunCacheLive.pipe(Layer.provide(BunVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
16
+ };
17
+ const makeDenoLayer = (authOverride)=>{
18
+ const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
19
+ return DenoResolverLive.pipe(Layer.provide(AutoDenoCacheLive.pipe(Layer.provide(DenoVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
20
+ };
11
21
  const SCHEMA_URL = "https://raw.githubusercontent.com/spencerbeggs/runtime-resolver/main/runtime-resolver.schema.json";
12
22
  const nodeOption = Options.text("node").pipe(Options.optional);
13
23
  const bunOption = Options.text("bun").pipe(Options.optional);
@@ -17,7 +27,6 @@ const incrementsOption = Options.text("increments").pipe(Options.optional);
17
27
  const nodeDefaultOption = Options.text("node-default").pipe(Options.optional);
18
28
  const bunDefaultOption = Options.text("bun-default").pipe(Options.optional);
19
29
  const denoDefaultOption = Options.text("deno-default").pipe(Options.optional);
20
- const freshnessOption = Options.text("freshness").pipe(Options.optional);
21
30
  const nodeDateOption = Options.text("node-date").pipe(Options.optional);
22
31
  const prettyOption = Options.boolean("pretty").pipe(Options.withDefault(false));
23
32
  const schemaOption = Options.boolean("schema").pipe(Options.withDefault(false));
@@ -36,11 +45,6 @@ const VALID_INCREMENTS = [
36
45
  "minor",
37
46
  "patch"
38
47
  ];
39
- const VALID_FRESHNESS = [
40
- "auto",
41
- "api",
42
- "cache"
43
- ];
44
48
  const validatePhases = (raw)=>{
45
49
  const phases = raw.split(",").map((s)=>s.trim());
46
50
  for (const p of phases)if (!VALID_PHASES.includes(p)) throw new Error(`Invalid phase: "${p}". Valid values: ${VALID_PHASES.join(", ")}`);
@@ -50,10 +54,6 @@ const validateIncrements = (raw)=>{
50
54
  if (!VALID_INCREMENTS.includes(raw)) throw new Error(`Invalid increments value: "${raw}". Valid values: ${VALID_INCREMENTS.join(", ")}`);
51
55
  return raw;
52
56
  };
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
57
  const serializeError = (error)=>{
58
58
  if (error && "object" == typeof error && "_tag" in error && "message" in error) {
59
59
  const { _tag, message, ...rest } = error;
@@ -92,7 +92,7 @@ const toError = (name, error)=>[
92
92
  error: serializeError(error)
93
93
  }
94
94
  ];
95
- const resolveNode = (semverRange, phases, increments, defaultVersion, date, freshness, authOverride)=>Effect.gen(function*() {
95
+ const resolveNode = (semverRange, phases, increments, defaultVersion, date, authOverride)=>Effect.gen(function*() {
96
96
  const resolver = yield* NodeResolver;
97
97
  const result = yield* resolver.resolve({
98
98
  semverRange,
@@ -107,14 +107,11 @@ const resolveNode = (semverRange, phases, increments, defaultVersion, date, fres
107
107
  } : {},
108
108
  ...date ? {
109
109
  date
110
- } : {},
111
- ...freshness ? {
112
- freshness
113
110
  } : {}
114
111
  });
115
112
  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*() {
113
+ }).pipe(Effect.provide(makeNodeLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("node", error))));
114
+ const resolveBun = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
118
115
  const resolver = yield* BunResolver;
119
116
  const result = yield* resolver.resolve({
120
117
  semverRange,
@@ -123,14 +120,11 @@ const resolveBun = (semverRange, increments, defaultVersion, freshness, authOver
123
120
  } : {},
124
121
  ...defaultVersion ? {
125
122
  defaultVersion
126
- } : {},
127
- ...freshness ? {
128
- freshness
129
123
  } : {}
130
124
  });
131
125
  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*() {
126
+ }).pipe(Effect.provide(makeBunLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("bun", error))));
127
+ const resolveDeno = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
134
128
  const resolver = yield* DenoResolver;
135
129
  const result = yield* resolver.resolve({
136
130
  semverRange,
@@ -139,13 +133,10 @@ const resolveDeno = (semverRange, increments, defaultVersion, freshness, authOve
139
133
  } : {},
140
134
  ...defaultVersion ? {
141
135
  defaultVersion
142
- } : {},
143
- ...freshness ? {
144
- freshness
145
136
  } : {}
146
137
  });
147
138
  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))));
139
+ }).pipe(Effect.provide(makeDenoLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("deno", error))));
149
140
  const formatResponse = (ok, results, pretty)=>{
150
141
  const response = {
151
142
  $schema: SCHEMA_URL,
@@ -162,7 +153,6 @@ const resolveHandler = (args)=>Effect.gen(function*() {
162
153
  args.deno,
163
154
  args.nodePhases,
164
155
  args.increments,
165
- args.freshness,
166
156
  args.nodeDefault,
167
157
  args.bunDefault,
168
158
  args.denoDefault,
@@ -174,9 +164,7 @@ const resolveHandler = (args)=>Effect.gen(function*() {
174
164
  ];
175
165
  const hasResolveFlags = resolveFlags.some((f)=>Option.isSome(f));
176
166
  if (hasResolveFlags) return void (yield* Console.error("Error: --schema cannot be combined with resolve flags (--node, --bun, --deno, etc.)"));
177
- const { cliJsonSchema } = yield* Effect.promise(()=>import("../235.js").then((mod)=>({
178
- cliJsonSchema: mod.cliJsonSchema
179
- })));
167
+ const { cliJsonSchema } = yield* Effect.promise(()=>import("../json-schema.js").then((m)=>m.json_schema_namespaceObject));
180
168
  yield* Console.log(JSON.stringify(cliJsonSchema, null, 2));
181
169
  return;
182
170
  }
@@ -227,13 +215,6 @@ const resolveHandler = (args)=>Effect.gen(function*() {
227
215
  yield* Console.error(e.message);
228
216
  return;
229
217
  }
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
218
  let nodeDate;
238
219
  if (Option.isSome(args.nodeDate)) {
239
220
  nodeDate = new Date(args.nodeDate.value);
@@ -242,15 +223,15 @@ const resolveHandler = (args)=>Effect.gen(function*() {
242
223
  const tasks = [];
243
224
  if (hasNode) {
244
225
  const nodeDefaultVersion = Option.isSome(args.nodeDefault) ? args.nodeDefault.value : void 0;
245
- tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, validatedFreshness, authLayerOverride));
226
+ tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, authLayerOverride));
246
227
  }
247
228
  if (hasBun) {
248
229
  const bunDefaultVersion = Option.isSome(args.bunDefault) ? args.bunDefault.value : void 0;
249
- tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, validatedFreshness, authLayerOverride));
230
+ tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, authLayerOverride));
250
231
  }
251
232
  if (hasDeno) {
252
233
  const denoDefaultVersion = Option.isSome(args.denoDefault) ? args.denoDefault.value : void 0;
253
- tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, validatedFreshness, authLayerOverride));
234
+ tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, authLayerOverride));
254
235
  }
255
236
  const entries = yield* Effect.all(tasks, {
256
237
  concurrency: "unbounded"
@@ -265,7 +246,6 @@ const rootCommand = Command.make("runtime-resolver", {
265
246
  deno: denoOption,
266
247
  nodePhases: nodePhasesOption,
267
248
  increments: incrementsOption,
268
- freshness: freshnessOption,
269
249
  nodeDefault: nodeDefaultOption,
270
250
  bunDefault: bunDefaultOption,
271
251
  denoDefault: denoDefaultOption,
@@ -279,7 +259,7 @@ const rootCommand = Command.make("runtime-resolver", {
279
259
  }, resolveHandler);
280
260
  const cli = Command.run(rootCommand, {
281
261
  name: "runtime-resolver",
282
- version: "0.1.0"
262
+ version: "0.3.0"
283
263
  });
284
264
  const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer), Effect.catchAllCause((cause)=>{
285
265
  const defects = Cause.defects(cause);