runtime-resolver 0.3.12 → 0.3.14
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/bin/runtime-resolver.d.ts +1 -0
- package/bin/runtime-resolver.js +29 -261
- package/cli/commands/resolve.js +239 -0
- package/cli/schemas/json-schema.js +12 -0
- package/cli/schemas/response.js +86 -0
- package/data/bun-defaults.js +850 -0
- package/data/deno-defaults.js +1526 -0
- package/data/node-defaults.js +4400 -0
- package/errors/AuthenticationError.js +35 -0
- package/errors/CacheError.js +32 -0
- package/errors/FreshnessError.js +38 -0
- package/errors/InvalidInputError.js +33 -0
- package/errors/NetworkError.js +34 -0
- package/errors/ParseError.js +33 -0
- package/errors/RateLimitError.js +39 -0
- package/errors/VersionNotFoundError.js +37 -0
- package/index.d.ts +2577 -2608
- package/index.js +158 -132
- package/layers/AutoBunCacheLive.js +47 -0
- package/layers/AutoDenoCacheLive.js +47 -0
- package/layers/AutoNodeCacheLive.js +58 -0
- package/layers/BunReleaseCacheLive.js +25 -0
- package/layers/BunResolverLive.js +85 -0
- package/layers/BunVersionFetcherLive.js +55 -0
- package/layers/DenoReleaseCacheLive.js +25 -0
- package/layers/DenoResolverLive.js +84 -0
- package/layers/DenoVersionFetcherLive.js +51 -0
- package/layers/FreshBunCacheLive.js +60 -0
- package/layers/FreshDenoCacheLive.js +60 -0
- package/layers/FreshNodeCacheLive.js +55 -0
- package/layers/GitHubAppAuth.js +58 -0
- package/layers/GitHubAutoAuth.js +70 -0
- package/layers/GitHubClientLive.js +130 -0
- package/layers/GitHubTokenAuth.js +50 -0
- package/layers/NodeReleaseCacheLive.js +77 -0
- package/layers/NodeResolverLive.js +107 -0
- package/layers/NodeScheduleFetcherLive.js +37 -0
- package/layers/NodeVersionFetcherLive.js +55 -0
- package/layers/OfflineBunCacheLive.js +40 -0
- package/layers/OfflineDenoCacheLive.js +40 -0
- package/layers/OfflineNodeCacheLive.js +39 -0
- package/layers/RuntimeCacheLive.js +78 -0
- package/lib/retry.js +27 -0
- package/lib/semver.js +21 -0
- package/package.json +57 -68
- package/schemas/bun-release.js +61 -0
- package/schemas/deno-release.js +61 -0
- package/schemas/github.js +74 -0
- package/schemas/node-release.js +130 -0
- package/schemas/node-schedule.js +110 -0
- package/schemas/node.js +68 -0
- package/services/BunReleaseCache.js +33 -0
- package/services/BunResolver.js +36 -0
- package/services/BunVersionFetcher.js +41 -0
- package/services/DenoReleaseCache.js +33 -0
- package/services/DenoResolver.js +36 -0
- package/services/DenoVersionFetcher.js +41 -0
- package/services/GitHubClient.js +40 -0
- package/services/NodeReleaseCache.js +41 -0
- package/services/NodeResolver.js +44 -0
- package/services/NodeScheduleFetcher.js +38 -0
- package/services/NodeVersionFetcher.js +39 -0
- package/services/OctokitInstance.js +44 -0
- package/tsdoc-metadata.json +11 -11
- package/612.js +0 -23
- package/711.js +0 -7554
- package/json-schema.js +0 -95
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/bin/runtime-resolver.js
CHANGED
|
@@ -1,269 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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";
|
|
3
5
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
|
|
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
|
-
};
|
|
21
|
-
const SCHEMA_URL = "https://raw.githubusercontent.com/spencerbeggs/runtime-resolver/main/runtime-resolver.schema.json";
|
|
22
|
-
const nodeOption = Options.text("node").pipe(Options.optional);
|
|
23
|
-
const bunOption = Options.text("bun").pipe(Options.optional);
|
|
24
|
-
const denoOption = Options.text("deno").pipe(Options.optional);
|
|
25
|
-
const nodePhasesOption = Options.text("node-phases").pipe(Options.optional);
|
|
26
|
-
const incrementsOption = Options.text("increments").pipe(Options.optional);
|
|
27
|
-
const nodeDefaultOption = Options.text("node-default").pipe(Options.optional);
|
|
28
|
-
const bunDefaultOption = Options.text("bun-default").pipe(Options.optional);
|
|
29
|
-
const denoDefaultOption = Options.text("deno-default").pipe(Options.optional);
|
|
30
|
-
const nodeDateOption = Options.text("node-date").pipe(Options.optional);
|
|
31
|
-
const prettyOption = Options.boolean("pretty").pipe(Options.withDefault(false));
|
|
32
|
-
const schemaOption = Options.boolean("schema").pipe(Options.withDefault(false));
|
|
33
|
-
const tokenOption = Options.text("token").pipe(Options.optional);
|
|
34
|
-
const appIdOption = Options.text("app-id").pipe(Options.optional);
|
|
35
|
-
const appPrivateKeyOption = Options.text("app-private-key").pipe(Options.optional);
|
|
36
|
-
const appInstallationIdOption = Options.text("app-installation-id").pipe(Options.optional);
|
|
37
|
-
const VALID_PHASES = [
|
|
38
|
-
"current",
|
|
39
|
-
"active-lts",
|
|
40
|
-
"maintenance-lts",
|
|
41
|
-
"end-of-life"
|
|
42
|
-
];
|
|
43
|
-
const VALID_INCREMENTS = [
|
|
44
|
-
"latest",
|
|
45
|
-
"minor",
|
|
46
|
-
"patch"
|
|
47
|
-
];
|
|
48
|
-
const validatePhases = (raw)=>{
|
|
49
|
-
const phases = raw.split(",").map((s)=>s.trim());
|
|
50
|
-
for (const p of phases)if (!VALID_PHASES.includes(p)) throw new Error(`Invalid phase: "${p}". Valid values: ${VALID_PHASES.join(", ")}`);
|
|
51
|
-
return phases;
|
|
52
|
-
};
|
|
53
|
-
const validateIncrements = (raw)=>{
|
|
54
|
-
if (!VALID_INCREMENTS.includes(raw)) throw new Error(`Invalid increments value: "${raw}". Valid values: ${VALID_INCREMENTS.join(", ")}`);
|
|
55
|
-
return raw;
|
|
56
|
-
};
|
|
57
|
-
const serializeError = (error)=>{
|
|
58
|
-
if (error && "object" == typeof error && "_tag" in error && "message" in error) {
|
|
59
|
-
const { _tag, message, ...rest } = error;
|
|
60
|
-
return {
|
|
61
|
-
_tag,
|
|
62
|
-
message,
|
|
63
|
-
...rest
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
_tag: "UnknownError",
|
|
68
|
-
message: error instanceof Error ? error.message : String(error)
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
const toSuccess = (name, result)=>[
|
|
72
|
-
name,
|
|
73
|
-
{
|
|
74
|
-
ok: true,
|
|
75
|
-
source: result.source,
|
|
76
|
-
versions: [
|
|
77
|
-
...result.versions
|
|
78
|
-
],
|
|
79
|
-
latest: result.latest,
|
|
80
|
-
...void 0 !== result.lts ? {
|
|
81
|
-
lts: result.lts
|
|
82
|
-
} : {},
|
|
83
|
-
...void 0 !== result.default ? {
|
|
84
|
-
default: result.default
|
|
85
|
-
} : {}
|
|
86
|
-
}
|
|
87
|
-
];
|
|
88
|
-
const toError = (name, error)=>[
|
|
89
|
-
name,
|
|
90
|
-
{
|
|
91
|
-
ok: false,
|
|
92
|
-
error: serializeError(error)
|
|
93
|
-
}
|
|
94
|
-
];
|
|
95
|
-
const resolveNode = (semverRange, phases, increments, defaultVersion, date, authOverride)=>Effect.gen(function*() {
|
|
96
|
-
const resolver = yield* NodeResolver;
|
|
97
|
-
const result = yield* resolver.resolve({
|
|
98
|
-
semverRange,
|
|
99
|
-
...phases ? {
|
|
100
|
-
phases
|
|
101
|
-
} : {},
|
|
102
|
-
...increments ? {
|
|
103
|
-
increments
|
|
104
|
-
} : {},
|
|
105
|
-
...defaultVersion ? {
|
|
106
|
-
defaultVersion
|
|
107
|
-
} : {},
|
|
108
|
-
...date ? {
|
|
109
|
-
date
|
|
110
|
-
} : {}
|
|
111
|
-
});
|
|
112
|
-
return toSuccess("node", result);
|
|
113
|
-
}).pipe(Effect.provide(makeNodeLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("node", error))));
|
|
114
|
-
const resolveBun = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
|
|
115
|
-
const resolver = yield* BunResolver;
|
|
116
|
-
const result = yield* resolver.resolve({
|
|
117
|
-
semverRange,
|
|
118
|
-
...increments ? {
|
|
119
|
-
increments
|
|
120
|
-
} : {},
|
|
121
|
-
...defaultVersion ? {
|
|
122
|
-
defaultVersion
|
|
123
|
-
} : {}
|
|
124
|
-
});
|
|
125
|
-
return toSuccess("bun", result);
|
|
126
|
-
}).pipe(Effect.provide(makeBunLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("bun", error))));
|
|
127
|
-
const resolveDeno = (semverRange, increments, defaultVersion, authOverride)=>Effect.gen(function*() {
|
|
128
|
-
const resolver = yield* DenoResolver;
|
|
129
|
-
const result = yield* resolver.resolve({
|
|
130
|
-
semverRange,
|
|
131
|
-
...increments ? {
|
|
132
|
-
increments
|
|
133
|
-
} : {},
|
|
134
|
-
...defaultVersion ? {
|
|
135
|
-
defaultVersion
|
|
136
|
-
} : {}
|
|
137
|
-
});
|
|
138
|
-
return toSuccess("deno", result);
|
|
139
|
-
}).pipe(Effect.provide(makeDenoLayer(authOverride)), Effect.catchAll((error)=>Effect.succeed(toError("deno", error))));
|
|
140
|
-
const formatResponse = (ok, results, pretty)=>{
|
|
141
|
-
const response = {
|
|
142
|
-
$schema: SCHEMA_URL,
|
|
143
|
-
ok,
|
|
144
|
-
results
|
|
145
|
-
};
|
|
146
|
-
return JSON.stringify(response, null, pretty ? 2 : void 0);
|
|
147
|
-
};
|
|
148
|
-
const resolveHandler = (args)=>Effect.gen(function*() {
|
|
149
|
-
if (args.schema) {
|
|
150
|
-
const resolveFlags = [
|
|
151
|
-
args.node,
|
|
152
|
-
args.bun,
|
|
153
|
-
args.deno,
|
|
154
|
-
args.nodePhases,
|
|
155
|
-
args.increments,
|
|
156
|
-
args.nodeDefault,
|
|
157
|
-
args.bunDefault,
|
|
158
|
-
args.denoDefault,
|
|
159
|
-
args.nodeDate,
|
|
160
|
-
args.token,
|
|
161
|
-
args.appId,
|
|
162
|
-
args.appPrivateKey,
|
|
163
|
-
args.appInstallationId
|
|
164
|
-
];
|
|
165
|
-
const hasResolveFlags = resolveFlags.some((f)=>Option.isSome(f));
|
|
166
|
-
if (hasResolveFlags) return void (yield* Console.error("Error: --schema cannot be combined with resolve flags (--node, --bun, --deno, etc.)"));
|
|
167
|
-
const { cliJsonSchema } = yield* Effect.promise(()=>import("../json-schema.js").then((m)=>m.json_schema_namespaceObject));
|
|
168
|
-
yield* Console.log(JSON.stringify(cliJsonSchema, null, 2));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const hasToken = Option.isSome(args.token);
|
|
172
|
-
const hasAppId = Option.isSome(args.appId);
|
|
173
|
-
const hasAppPrivateKey = Option.isSome(args.appPrivateKey);
|
|
174
|
-
const hasAppInstallationId = Option.isSome(args.appInstallationId);
|
|
175
|
-
if (hasToken && (hasAppId || hasAppPrivateKey)) return void (yield* Console.error("Error: --token and --app-id/--app-private-key are mutually exclusive"));
|
|
176
|
-
if (hasAppId !== hasAppPrivateKey) return void (yield* Console.error("Error: --app-id and --app-private-key must both be provided"));
|
|
177
|
-
if (hasAppInstallationId && !hasAppId) return void (yield* Console.error("Error: --app-installation-id requires --app-id and --app-private-key"));
|
|
178
|
-
let resolvedPrivateKey;
|
|
179
|
-
if (hasAppPrivateKey) {
|
|
180
|
-
const keyValue = args.appPrivateKey.value;
|
|
181
|
-
if (keyValue.startsWith("@")) {
|
|
182
|
-
const filePath = keyValue.slice(1);
|
|
183
|
-
try {
|
|
184
|
-
resolvedPrivateKey = readFileSync(filePath, "utf-8");
|
|
185
|
-
} catch {
|
|
186
|
-
yield* Console.error(`Error: Cannot read private key file: ${filePath}`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
} else resolvedPrivateKey = keyValue;
|
|
190
|
-
}
|
|
191
|
-
let authLayerOverride;
|
|
192
|
-
if (hasToken) authLayerOverride = GitHubTokenAuthFromToken(args.token.value);
|
|
193
|
-
else if (hasAppId && resolvedPrivateKey) authLayerOverride = GitHubAppAuth({
|
|
194
|
-
appId: args.appId.value,
|
|
195
|
-
privateKey: resolvedPrivateKey,
|
|
196
|
-
...hasAppInstallationId ? {
|
|
197
|
-
installationId: Number(args.appInstallationId.value)
|
|
198
|
-
} : {}
|
|
199
|
-
});
|
|
200
|
-
const hasNode = Option.isSome(args.node);
|
|
201
|
-
const hasBun = Option.isSome(args.bun);
|
|
202
|
-
const hasDeno = Option.isSome(args.deno);
|
|
203
|
-
if (!hasNode && !hasBun && !hasDeno) return void (yield* Console.error("No runtime specified. Use --node, --bun, or --deno to resolve versions.\nRun with --help for usage information."));
|
|
204
|
-
let validatedPhases;
|
|
205
|
-
if (Option.isSome(args.nodePhases)) try {
|
|
206
|
-
validatedPhases = validatePhases(args.nodePhases.value);
|
|
207
|
-
} catch (e) {
|
|
208
|
-
yield* Console.error(e.message);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
let validatedIncrements;
|
|
212
|
-
if (Option.isSome(args.increments)) try {
|
|
213
|
-
validatedIncrements = validateIncrements(args.increments.value);
|
|
214
|
-
} catch (e) {
|
|
215
|
-
yield* Console.error(e.message);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
let nodeDate;
|
|
219
|
-
if (Option.isSome(args.nodeDate)) {
|
|
220
|
-
nodeDate = new Date(args.nodeDate.value);
|
|
221
|
-
if (Number.isNaN(nodeDate.getTime())) return void (yield* Console.error(`Invalid date: "${args.nodeDate.value}". Use ISO 8601 format (e.g. 2024-01-15).`));
|
|
222
|
-
}
|
|
223
|
-
const tasks = [];
|
|
224
|
-
if (hasNode) {
|
|
225
|
-
const nodeDefaultVersion = Option.isSome(args.nodeDefault) ? args.nodeDefault.value : void 0;
|
|
226
|
-
tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, authLayerOverride));
|
|
227
|
-
}
|
|
228
|
-
if (hasBun) {
|
|
229
|
-
const bunDefaultVersion = Option.isSome(args.bunDefault) ? args.bunDefault.value : void 0;
|
|
230
|
-
tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, authLayerOverride));
|
|
231
|
-
}
|
|
232
|
-
if (hasDeno) {
|
|
233
|
-
const denoDefaultVersion = Option.isSome(args.denoDefault) ? args.denoDefault.value : void 0;
|
|
234
|
-
tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, authLayerOverride));
|
|
235
|
-
}
|
|
236
|
-
const entries = yield* Effect.all(tasks, {
|
|
237
|
-
concurrency: "unbounded"
|
|
238
|
-
});
|
|
239
|
-
const results = Object.fromEntries(entries);
|
|
240
|
-
const hasError = Object.values(results).some((r)=>!r.ok);
|
|
241
|
-
yield* Console.log(formatResponse(!hasError, results, args.pretty));
|
|
242
|
-
});
|
|
6
|
+
|
|
7
|
+
//#region src/cli/index.ts
|
|
8
|
+
/* v8 ignore start - CLI entry point requires integration testing */
|
|
243
9
|
const rootCommand = Command.make("runtime-resolver", {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
259
25
|
}, resolveHandler);
|
|
260
26
|
const cli = Command.run(rootCommand, {
|
|
261
|
-
|
|
262
|
-
|
|
27
|
+
name: "runtime-resolver",
|
|
28
|
+
version: "0.3.14"
|
|
263
29
|
});
|
|
264
|
-
const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer), Effect.catchAllCause((cause)=>{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return Effect.failCause(cause);
|
|
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);
|
|
268
33
|
}));
|
|
269
34
|
NodeRuntime.runMain(main);
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AutoBunCacheLive } from "../../layers/AutoBunCacheLive.js";
|
|
2
|
+
import { AutoDenoCacheLive } from "../../layers/AutoDenoCacheLive.js";
|
|
3
|
+
import { AutoNodeCacheLive } from "../../layers/AutoNodeCacheLive.js";
|
|
4
|
+
import { BunResolver } from "../../services/BunResolver.js";
|
|
5
|
+
import { BunResolverLive } from "../../layers/BunResolverLive.js";
|
|
6
|
+
import { BunVersionFetcherLive } from "../../layers/BunVersionFetcherLive.js";
|
|
7
|
+
import { DenoResolver } from "../../services/DenoResolver.js";
|
|
8
|
+
import { DenoResolverLive } from "../../layers/DenoResolverLive.js";
|
|
9
|
+
import { DenoVersionFetcherLive } from "../../layers/DenoVersionFetcherLive.js";
|
|
10
|
+
import { GitHubAppAuth } from "../../layers/GitHubAppAuth.js";
|
|
11
|
+
import { GitHubAutoAuth } from "../../layers/GitHubAutoAuth.js";
|
|
12
|
+
import { GitHubClientLive } from "../../layers/GitHubClientLive.js";
|
|
13
|
+
import { NodeResolver } from "../../services/NodeResolver.js";
|
|
14
|
+
import { NodeResolverLive } from "../../layers/NodeResolverLive.js";
|
|
15
|
+
import { NodeScheduleFetcherLive } from "../../layers/NodeScheduleFetcherLive.js";
|
|
16
|
+
import { NodeVersionFetcherLive } from "../../layers/NodeVersionFetcherLive.js";
|
|
17
|
+
import { GitHubTokenAuthFromToken } from "../../layers/GitHubTokenAuth.js";
|
|
18
|
+
import { Console, Effect, Layer, Option } from "effect";
|
|
19
|
+
import { Options } from "@effect/cli";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
//#region src/cli/commands/resolve.ts
|
|
23
|
+
/* v8 ignore start */
|
|
24
|
+
const GitHubLayer = GitHubClientLive.pipe(Layer.provide(GitHubAutoAuth));
|
|
25
|
+
const makeNodeLayer = (authOverride) => {
|
|
26
|
+
const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
|
|
27
|
+
const fetchersLayer = Layer.merge(NodeVersionFetcherLive.pipe(Layer.provide(ghLayer)), NodeScheduleFetcherLive.pipe(Layer.provide(ghLayer)));
|
|
28
|
+
return NodeResolverLive.pipe(Layer.provide(AutoNodeCacheLive.pipe(Layer.provide(fetchersLayer))));
|
|
29
|
+
};
|
|
30
|
+
const makeBunLayer = (authOverride) => {
|
|
31
|
+
const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
|
|
32
|
+
return BunResolverLive.pipe(Layer.provide(AutoBunCacheLive.pipe(Layer.provide(BunVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
|
|
33
|
+
};
|
|
34
|
+
const makeDenoLayer = (authOverride) => {
|
|
35
|
+
const ghLayer = authOverride ? GitHubClientLive.pipe(Layer.provide(authOverride)) : GitHubLayer;
|
|
36
|
+
return DenoResolverLive.pipe(Layer.provide(AutoDenoCacheLive.pipe(Layer.provide(DenoVersionFetcherLive.pipe(Layer.provide(ghLayer))))));
|
|
37
|
+
};
|
|
38
|
+
const SCHEMA_URL = "https://raw.githubusercontent.com/spencerbeggs/runtime-resolver/main/runtime-resolver.schema.json";
|
|
39
|
+
const nodeOption = Options.text("node").pipe(Options.optional);
|
|
40
|
+
const bunOption = Options.text("bun").pipe(Options.optional);
|
|
41
|
+
const denoOption = Options.text("deno").pipe(Options.optional);
|
|
42
|
+
const nodePhasesOption = Options.text("node-phases").pipe(Options.optional);
|
|
43
|
+
const incrementsOption = Options.text("increments").pipe(Options.optional);
|
|
44
|
+
const nodeDefaultOption = Options.text("node-default").pipe(Options.optional);
|
|
45
|
+
const bunDefaultOption = Options.text("bun-default").pipe(Options.optional);
|
|
46
|
+
const denoDefaultOption = Options.text("deno-default").pipe(Options.optional);
|
|
47
|
+
const nodeDateOption = Options.text("node-date").pipe(Options.optional);
|
|
48
|
+
const prettyOption = Options.boolean("pretty").pipe(Options.withDefault(false));
|
|
49
|
+
const schemaOption = Options.boolean("schema").pipe(Options.withDefault(false));
|
|
50
|
+
const tokenOption = Options.text("token").pipe(Options.optional);
|
|
51
|
+
const appIdOption = Options.text("app-id").pipe(Options.optional);
|
|
52
|
+
const appPrivateKeyOption = Options.text("app-private-key").pipe(Options.optional);
|
|
53
|
+
const appInstallationIdOption = Options.text("app-installation-id").pipe(Options.optional);
|
|
54
|
+
const VALID_PHASES = [
|
|
55
|
+
"current",
|
|
56
|
+
"active-lts",
|
|
57
|
+
"maintenance-lts",
|
|
58
|
+
"end-of-life"
|
|
59
|
+
];
|
|
60
|
+
const VALID_INCREMENTS = [
|
|
61
|
+
"latest",
|
|
62
|
+
"minor",
|
|
63
|
+
"patch"
|
|
64
|
+
];
|
|
65
|
+
const validatePhases = (raw) => {
|
|
66
|
+
const phases = raw.split(",").map((s) => s.trim());
|
|
67
|
+
for (const p of phases) if (!VALID_PHASES.includes(p)) throw new Error(`Invalid phase: "${p}". Valid values: ${VALID_PHASES.join(", ")}`);
|
|
68
|
+
return phases;
|
|
69
|
+
};
|
|
70
|
+
const validateIncrements = (raw) => {
|
|
71
|
+
if (!VALID_INCREMENTS.includes(raw)) throw new Error(`Invalid increments value: "${raw}". Valid values: ${VALID_INCREMENTS.join(", ")}`);
|
|
72
|
+
return raw;
|
|
73
|
+
};
|
|
74
|
+
const serializeError = (error) => {
|
|
75
|
+
if (error && typeof error === "object" && "_tag" in error && "message" in error) {
|
|
76
|
+
const { _tag, message, ...rest } = error;
|
|
77
|
+
return {
|
|
78
|
+
_tag,
|
|
79
|
+
message,
|
|
80
|
+
...rest
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
_tag: "UnknownError",
|
|
85
|
+
message: error instanceof Error ? error.message : String(error)
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const toSuccess = (name, result) => [name, {
|
|
89
|
+
ok: true,
|
|
90
|
+
source: result.source,
|
|
91
|
+
versions: [...result.versions],
|
|
92
|
+
latest: result.latest,
|
|
93
|
+
...result.lts !== void 0 ? { lts: result.lts } : {},
|
|
94
|
+
...result.default !== void 0 ? { default: result.default } : {}
|
|
95
|
+
}];
|
|
96
|
+
const toError = (name, error) => [name, {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: serializeError(error)
|
|
99
|
+
}];
|
|
100
|
+
const resolveNode = (semverRange, phases, increments, defaultVersion, date, authOverride) => Effect.gen(function* () {
|
|
101
|
+
return toSuccess("node", yield* (yield* NodeResolver).resolve({
|
|
102
|
+
semverRange,
|
|
103
|
+
...phases ? { phases } : {},
|
|
104
|
+
...increments ? { increments } : {},
|
|
105
|
+
...defaultVersion ? { defaultVersion } : {},
|
|
106
|
+
...date ? { date } : {}
|
|
107
|
+
}));
|
|
108
|
+
}).pipe(Effect.provide(makeNodeLayer(authOverride)), Effect.catchAll((error) => Effect.succeed(toError("node", error))));
|
|
109
|
+
const resolveBun = (semverRange, increments, defaultVersion, authOverride) => Effect.gen(function* () {
|
|
110
|
+
return toSuccess("bun", yield* (yield* BunResolver).resolve({
|
|
111
|
+
semverRange,
|
|
112
|
+
...increments ? { increments } : {},
|
|
113
|
+
...defaultVersion ? { defaultVersion } : {}
|
|
114
|
+
}));
|
|
115
|
+
}).pipe(Effect.provide(makeBunLayer(authOverride)), Effect.catchAll((error) => Effect.succeed(toError("bun", error))));
|
|
116
|
+
const resolveDeno = (semverRange, increments, defaultVersion, authOverride) => Effect.gen(function* () {
|
|
117
|
+
return toSuccess("deno", yield* (yield* DenoResolver).resolve({
|
|
118
|
+
semverRange,
|
|
119
|
+
...increments ? { increments } : {},
|
|
120
|
+
...defaultVersion ? { defaultVersion } : {}
|
|
121
|
+
}));
|
|
122
|
+
}).pipe(Effect.provide(makeDenoLayer(authOverride)), Effect.catchAll((error) => Effect.succeed(toError("deno", error))));
|
|
123
|
+
const formatResponse = (ok, results, pretty) => {
|
|
124
|
+
return JSON.stringify({
|
|
125
|
+
$schema: SCHEMA_URL,
|
|
126
|
+
ok,
|
|
127
|
+
results
|
|
128
|
+
}, null, pretty ? 2 : void 0);
|
|
129
|
+
};
|
|
130
|
+
const resolveHandler = (args) => Effect.gen(function* () {
|
|
131
|
+
if (args.schema) {
|
|
132
|
+
if ([
|
|
133
|
+
args.node,
|
|
134
|
+
args.bun,
|
|
135
|
+
args.deno,
|
|
136
|
+
args.nodePhases,
|
|
137
|
+
args.increments,
|
|
138
|
+
args.nodeDefault,
|
|
139
|
+
args.bunDefault,
|
|
140
|
+
args.denoDefault,
|
|
141
|
+
args.nodeDate,
|
|
142
|
+
args.token,
|
|
143
|
+
args.appId,
|
|
144
|
+
args.appPrivateKey,
|
|
145
|
+
args.appInstallationId
|
|
146
|
+
].some((f) => Option.isSome(f))) {
|
|
147
|
+
yield* Console.error("Error: --schema cannot be combined with resolve flags (--node, --bun, --deno, etc.)");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const { cliJsonSchema } = yield* Effect.promise(() => import("../schemas/json-schema.js"));
|
|
151
|
+
yield* Console.log(JSON.stringify(cliJsonSchema, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const hasToken = Option.isSome(args.token);
|
|
155
|
+
const hasAppId = Option.isSome(args.appId);
|
|
156
|
+
const hasAppPrivateKey = Option.isSome(args.appPrivateKey);
|
|
157
|
+
const hasAppInstallationId = Option.isSome(args.appInstallationId);
|
|
158
|
+
if (hasToken && (hasAppId || hasAppPrivateKey)) {
|
|
159
|
+
yield* Console.error("Error: --token and --app-id/--app-private-key are mutually exclusive");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (hasAppId !== hasAppPrivateKey) {
|
|
163
|
+
yield* Console.error("Error: --app-id and --app-private-key must both be provided");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (hasAppInstallationId && !hasAppId) {
|
|
167
|
+
yield* Console.error("Error: --app-installation-id requires --app-id and --app-private-key");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
let resolvedPrivateKey;
|
|
171
|
+
if (hasAppPrivateKey) {
|
|
172
|
+
const keyValue = args.appPrivateKey.value;
|
|
173
|
+
if (keyValue.startsWith("@")) {
|
|
174
|
+
const filePath = keyValue.slice(1);
|
|
175
|
+
try {
|
|
176
|
+
resolvedPrivateKey = readFileSync(filePath, "utf-8");
|
|
177
|
+
} catch {
|
|
178
|
+
yield* Console.error(`Error: Cannot read private key file: ${filePath}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
} else resolvedPrivateKey = keyValue;
|
|
182
|
+
}
|
|
183
|
+
let authLayerOverride;
|
|
184
|
+
if (hasToken) authLayerOverride = GitHubTokenAuthFromToken(args.token.value);
|
|
185
|
+
else if (hasAppId && resolvedPrivateKey) authLayerOverride = GitHubAppAuth({
|
|
186
|
+
appId: args.appId.value,
|
|
187
|
+
privateKey: resolvedPrivateKey,
|
|
188
|
+
...hasAppInstallationId ? { installationId: Number(args.appInstallationId.value) } : {}
|
|
189
|
+
});
|
|
190
|
+
const hasNode = Option.isSome(args.node);
|
|
191
|
+
const hasBun = Option.isSome(args.bun);
|
|
192
|
+
const hasDeno = Option.isSome(args.deno);
|
|
193
|
+
if (!hasNode && !hasBun && !hasDeno) {
|
|
194
|
+
yield* Console.error("No runtime specified. Use --node, --bun, or --deno to resolve versions.\nRun with --help for usage information.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let validatedPhases;
|
|
198
|
+
if (Option.isSome(args.nodePhases)) try {
|
|
199
|
+
validatedPhases = validatePhases(args.nodePhases.value);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
yield* Console.error(e.message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let validatedIncrements;
|
|
205
|
+
if (Option.isSome(args.increments)) try {
|
|
206
|
+
validatedIncrements = validateIncrements(args.increments.value);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
yield* Console.error(e.message);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let nodeDate;
|
|
212
|
+
if (Option.isSome(args.nodeDate)) {
|
|
213
|
+
nodeDate = new Date(args.nodeDate.value);
|
|
214
|
+
if (Number.isNaN(nodeDate.getTime())) {
|
|
215
|
+
yield* Console.error(`Invalid date: "${args.nodeDate.value}". Use ISO 8601 format (e.g. 2024-01-15).`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const tasks = [];
|
|
220
|
+
if (hasNode) {
|
|
221
|
+
const nodeDefaultVersion = Option.isSome(args.nodeDefault) ? args.nodeDefault.value : void 0;
|
|
222
|
+
tasks.push(resolveNode(args.node.value, validatedPhases, validatedIncrements, nodeDefaultVersion, nodeDate, authLayerOverride));
|
|
223
|
+
}
|
|
224
|
+
if (hasBun) {
|
|
225
|
+
const bunDefaultVersion = Option.isSome(args.bunDefault) ? args.bunDefault.value : void 0;
|
|
226
|
+
tasks.push(resolveBun(args.bun.value, validatedIncrements, bunDefaultVersion, authLayerOverride));
|
|
227
|
+
}
|
|
228
|
+
if (hasDeno) {
|
|
229
|
+
const denoDefaultVersion = Option.isSome(args.denoDefault) ? args.denoDefault.value : void 0;
|
|
230
|
+
tasks.push(resolveDeno(args.deno.value, validatedIncrements, denoDefaultVersion, authLayerOverride));
|
|
231
|
+
}
|
|
232
|
+
const entries = yield* Effect.all(tasks, { concurrency: "unbounded" });
|
|
233
|
+
const results = Object.fromEntries(entries);
|
|
234
|
+
const hasError = Object.values(results).some((r) => !r.ok);
|
|
235
|
+
yield* Console.log(formatResponse(!hasError, results, args.pretty));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
export { appIdOption, appInstallationIdOption, appPrivateKeyOption, bunDefaultOption, bunOption, denoDefaultOption, denoOption, incrementsOption, nodeDateOption, nodeDefaultOption, nodeOption, nodePhasesOption, prettyOption, resolveHandler, schemaOption, tokenOption };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CliResponse } from "./response.js";
|
|
2
|
+
import { JSONSchema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/schemas/json-schema.ts
|
|
5
|
+
/**
|
|
6
|
+
* JSON Schema for the CLI response envelope.
|
|
7
|
+
* Used by --schema flag and can be published alongside the package.
|
|
8
|
+
*/
|
|
9
|
+
const cliJsonSchema = JSONSchema.make(CliResponse);
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { cliJsonSchema };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/cli/schemas/response.ts
|
|
4
|
+
/**
|
|
5
|
+
* Structured error in CLI output. Uses _tag to identify the error type.
|
|
6
|
+
* Additional fields vary by error type and are passed through as-is.
|
|
7
|
+
*/
|
|
8
|
+
const CliErrorDetail = Schema.Struct({
|
|
9
|
+
_tag: Schema.String.annotations({ description: "Error type identifier (e.g. RateLimitError, SemverError)" }),
|
|
10
|
+
message: Schema.String.annotations({ description: "Human-readable error message" })
|
|
11
|
+
}).pipe(Schema.extend(Schema.Record({
|
|
12
|
+
key: Schema.String,
|
|
13
|
+
value: Schema.Unknown.annotations({
|
|
14
|
+
description: "Additional error-specific metadata",
|
|
15
|
+
title: "ErrorMetadata"
|
|
16
|
+
})
|
|
17
|
+
})), Schema.annotations({
|
|
18
|
+
identifier: "CliErrorDetail",
|
|
19
|
+
description: "Structured error with type tag, message, and optional metadata"
|
|
20
|
+
}));
|
|
21
|
+
/**
|
|
22
|
+
* Successful resolution for a single runtime.
|
|
23
|
+
*/
|
|
24
|
+
const CliRuntimeSuccess = Schema.Struct({
|
|
25
|
+
ok: Schema.Literal(true).annotations({ description: "Indicates successful resolution" }),
|
|
26
|
+
source: Schema.String.annotations({ description: "Data source: 'api' or 'cache'" }),
|
|
27
|
+
versions: Schema.Array(Schema.String.annotations({ description: "Semver version string" })).annotations({ description: "All resolved versions matching the semver range" }),
|
|
28
|
+
latest: Schema.String.annotations({ description: "Most recent version matching the range" }),
|
|
29
|
+
lts: Schema.optional(Schema.String.annotations({ description: "Current LTS version (Node.js only)" })),
|
|
30
|
+
default: Schema.optional(Schema.String.annotations({ description: "Default recommended version" }))
|
|
31
|
+
}).annotations({
|
|
32
|
+
identifier: "CliRuntimeSuccess",
|
|
33
|
+
description: "Successful version resolution for a single runtime"
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Failed resolution for a single runtime.
|
|
37
|
+
*/
|
|
38
|
+
const CliRuntimeError = Schema.Struct({
|
|
39
|
+
ok: Schema.Literal(false).annotations({ description: "Indicates failed resolution" }),
|
|
40
|
+
error: CliErrorDetail
|
|
41
|
+
}).annotations({
|
|
42
|
+
identifier: "CliRuntimeError",
|
|
43
|
+
description: "Failed version resolution for a single runtime"
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Per-runtime result: success or error.
|
|
47
|
+
*/
|
|
48
|
+
const CliRuntimeResult = Schema.Union(CliRuntimeSuccess, CliRuntimeError).annotations({
|
|
49
|
+
identifier: "CliRuntimeResult",
|
|
50
|
+
description: "Per-runtime resolution result: either success with versions or error with details"
|
|
51
|
+
});
|
|
52
|
+
const CliResultsMap = Schema.Record({
|
|
53
|
+
key: Schema.String.annotations({ description: "Runtime name (node, bun, or deno)" }),
|
|
54
|
+
value: CliRuntimeResult
|
|
55
|
+
}).annotations({ description: "Map of runtime name to resolution result" });
|
|
56
|
+
/**
|
|
57
|
+
* Full CLI success response — all runtimes resolved.
|
|
58
|
+
*/
|
|
59
|
+
const CliSuccessResponse = Schema.Struct({
|
|
60
|
+
ok: Schema.Literal(true).annotations({ description: "True when all requested runtimes resolved successfully" }),
|
|
61
|
+
results: CliResultsMap
|
|
62
|
+
}).annotations({
|
|
63
|
+
identifier: "CliSuccessResponse",
|
|
64
|
+
description: "All requested runtimes resolved successfully"
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Full CLI error response — at least one runtime failed.
|
|
68
|
+
*/
|
|
69
|
+
const CliErrorResponse = Schema.Struct({
|
|
70
|
+
ok: Schema.Literal(false).annotations({ description: "False when one or more requested runtimes failed to resolve" }),
|
|
71
|
+
results: CliResultsMap
|
|
72
|
+
}).annotations({
|
|
73
|
+
identifier: "CliErrorResponse",
|
|
74
|
+
description: "One or more requested runtimes failed to resolve"
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Top-level CLI response envelope.
|
|
78
|
+
*/
|
|
79
|
+
const CliResponse = Schema.Union(CliSuccessResponse, CliErrorResponse).annotations({
|
|
80
|
+
identifier: "CliResponse",
|
|
81
|
+
title: "RuntimeResolverResponse",
|
|
82
|
+
description: "CLI response envelope. Contains an ok discriminator and a results map with per-runtime resolution outcomes."
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CliResponse };
|