@effected/github-actions 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.
@@ -0,0 +1,494 @@
1
+ import { ActionEnvironment } from "./ActionEnvironment.js";
2
+ import { ToolInstaller } from "./ToolInstaller.js";
3
+ import { Context, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
4
+ import { createHash } from "node:crypto";
5
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
6
+ import { PackageManagerPinName } from "@effected/npm";
7
+
8
+ //#region src/PackageManagerInstaller.ts
9
+ /**
10
+ * Raised when a package manager cannot be provisioned on the runner.
11
+ *
12
+ * @public
13
+ */
14
+ var PackageManagerInstallerError = class extends Schema.TaggedErrorClass()("PackageManagerInstallerError", {
15
+ /**
16
+ * `downloadFailed`, `extractFailed` and `cacheFailed` mirror the
17
+ * {@link ToolInstallerError} that caused them, preserved on `cause`
18
+ * (`cacheFailed` also covers a shim that could not be written into the
19
+ * entry). `integrityMismatch` — the downloaded artifact does not hash to
20
+ * what the pin declares; nothing was cached. `integrityMissing` — the pin
21
+ * carries no integrity and the caller asked for `requireIntegrity`.
22
+ * `unsupportedPlatform` — bun publishes no build for this runner's
23
+ * OS/architecture pair. `layoutUnexpected` — the artifact extracted, but
24
+ * its contents are not shaped like the package manager it claims to be.
25
+ */
26
+ reason: Schema.Literals([
27
+ "downloadFailed",
28
+ "extractFailed",
29
+ "integrityMismatch",
30
+ "integrityMissing",
31
+ "unsupportedPlatform",
32
+ "layoutUnexpected",
33
+ "cacheFailed"
34
+ ]),
35
+ /** The package manager being installed. */
36
+ name: PackageManagerPinName,
37
+ /** The exact version being installed, in string form. */
38
+ version: Schema.String,
39
+ /** What was being worked on — a url, a path, or an OS/arch pair. */
40
+ subject: Schema.optionalKey(Schema.String),
41
+ /** The integrity the pin declares (`<algo>.<hex>`), on a mismatch. */
42
+ expected: Schema.optionalKey(Schema.String),
43
+ /** The integrity the artifact actually hashed to, on a mismatch. */
44
+ actual: Schema.optionalKey(Schema.String),
45
+ /** The underlying failure, preserved structurally. */
46
+ cause: Schema.optionalKey(Schema.Defect())
47
+ }) {
48
+ get message() {
49
+ const pin = `${this.name}@${this.version}`;
50
+ switch (this.reason) {
51
+ case "downloadFailed": return `Could not download ${pin}${this.subject === void 0 ? "" : ` from ${this.subject}`}`;
52
+ case "extractFailed": return `Could not extract ${pin}${this.subject === void 0 ? "" : ` (${this.subject})`}`;
53
+ case "integrityMismatch": return `Integrity mismatch for ${pin}: expected ${this.expected}, got ${this.actual}`;
54
+ case "integrityMissing": return `The pin for ${pin} carries no integrity hash and requireIntegrity is set`;
55
+ case "unsupportedPlatform": return `No ${pin} build is published for ${this.subject}`;
56
+ case "layoutUnexpected": return `The ${pin} artifact does not have the expected layout${this.subject === void 0 ? "" : `: ${this.subject}`}`;
57
+ default: return `Could not cache ${pin} into the tool cache${this.subject === void 0 ? "" : `: ${this.subject}`}`;
58
+ }
59
+ }
60
+ };
61
+ /**
62
+ * A package manager the runner's own toolchain already had: nothing was
63
+ * downloaded and nothing was cached, so there is no directory to publish.
64
+ *
65
+ * @remarks
66
+ * `bins` values are bare command names (`npm`, `npx`) resolved through the
67
+ * ambient `PATH` — the toolchain already put them there.
68
+ *
69
+ * @public
70
+ */
71
+ var AmbientPackageManager = class extends Schema.Class("AmbientPackageManager")({
72
+ /** The discriminant: the runner's own toolchain answered. */
73
+ source: Schema.tag("ambient"),
74
+ /** The package-manager name (`npm`, `pnpm`, `yarn` or `bun`). */
75
+ name: PackageManagerPinName,
76
+ /** The exact installed version, in string form. */
77
+ version: Schema.String,
78
+ /** Bin name → the ambient command name that invokes it. */
79
+ bins: Schema.Record(Schema.String, Schema.String)
80
+ }) {};
81
+ /**
82
+ * A package manager living in the runner's tool cache — found there, or
83
+ * installed into it by this call.
84
+ *
85
+ * @remarks
86
+ * `binDir` is the directory a consumer hands to `ActionOutputs.addPath` to
87
+ * make the manager invokable by name in subsequent workflow steps. For the
88
+ * npm-registry managers it is the entry's `.bin` directory of executable
89
+ * shims this installer writes (`#!/bin/sh` exec wrappers, or `.cmd` wrappers
90
+ * on Windows); for bun it is the entry directory itself, because the cached
91
+ * `bun` binary is directly executable and a shim would add nothing but a
92
+ * fork. `bins` still maps each published bin name to the underlying entry —
93
+ * a Node script to run with `node` for npm/pnpm/yarn, the native binary for
94
+ * bun. Calling `addPath` remains deliberately the consumer's move, and note
95
+ * that `addPath` targets *subsequent* steps: a same-process probe must use
96
+ * the absolute paths in `bins` or `binDir`.
97
+ *
98
+ * @public
99
+ */
100
+ var CachedPackageManager = class extends Schema.Class("CachedPackageManager")({
101
+ /** The discriminant: the manager lives in the tool cache. */
102
+ source: Schema.tag("tool-cache"),
103
+ /** The package-manager name (`npm`, `pnpm`, `yarn` or `bun`). */
104
+ name: PackageManagerPinName,
105
+ /** The exact installed version, in string form. */
106
+ version: Schema.String,
107
+ /** The cached entry: `<tool-cache>/<name>/<version>/<arch>`. */
108
+ directory: Schema.String,
109
+ /** The directory to `addPath` — executable shims, or bun's own directory. */
110
+ binDir: Schema.String,
111
+ /** Bin name → the absolute path of the underlying entry point. */
112
+ bins: Schema.Record(Schema.String, Schema.String)
113
+ }) {};
114
+ /**
115
+ * An installed package manager, discriminated by `source`: `ambient` carries
116
+ * no directory, `tool-cache` carries the cached `directory` and the
117
+ * `addPath`-able `binDir`. The union is a `Schema`, so the record round-trips
118
+ * through `ActionState` for a later phase to read back.
119
+ *
120
+ * @remarks
121
+ * **Construct the variant classes, never the union.** The union's inherited
122
+ * `make` typechecks against *both* variants at once, so a wrong field yields
123
+ * a confusing two-branch error instead of "this variant wants X" — reach for
124
+ * {@link AmbientPackageManager}`.make` / {@link CachedPackageManager}`.make`,
125
+ * whose `Schema.tag` fills `source` for you. The same tag is why an
126
+ * overrides-style test helper should be typed
127
+ * `Partial<Omit<typeof CachedPackageManager.Type, "source">>` — a plain
128
+ * `Partial` offers `source`, which `make` auto-fills and rejects as input.
129
+ *
130
+ * @public
131
+ */
132
+ const InstalledPackageManager = Schema.Union([AmbientPackageManager, CachedPackageManager]);
133
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
134
+ /** The shim directory name inside a cached npm-registry-manager entry. */
135
+ const SHIM_DIR = ".bin";
136
+ /**
137
+ * The POSIX shim body: an `exec` wrapper so the shim's process *becomes*
138
+ * node, and `"$@"` so arguments survive quoting intact.
139
+ */
140
+ const posixShim = (target) => `#!/bin/sh\nexec node "${target}" "$@"\n`;
141
+ /** The Windows shim body, CRLF-terminated as cmd expects. */
142
+ const cmdShim = (target) => `@echo off\r\nnode "${target}" %*\r\n`;
143
+ /**
144
+ * The bun release asset name for a runner platform, or `None` when bun
145
+ * publishes no build for it.
146
+ *
147
+ * Asset names verified against the `bun-v1.3.14` release: `bun-{linux,darwin,
148
+ * windows}-{x64,aarch64}.zip`, with the runner's `RUNNER_OS` values mapped to
149
+ * bun's spelling (`macOS` → `darwin`) and the Node arch spelling to bun's
150
+ * (`arm64` → `aarch64`).
151
+ */
152
+ const bunTarget = (runnerOs, arch) => {
153
+ const os = runnerOs.toLowerCase() === "linux" ? "linux" : runnerOs.toLowerCase() === "macos" ? "darwin" : runnerOs.toLowerCase() === "windows" ? "windows" : void 0;
154
+ const cpu = arch === "x64" ? "x64" : arch === "arm64" ? "aarch64" : void 0;
155
+ return os === void 0 || cpu === void 0 ? Option.none() : Option.some(`bun-${os}-${cpu}`);
156
+ };
157
+ /**
158
+ * The registry tarball url for an npm-registry-distributed manager.
159
+ *
160
+ * The yarn split is corepack's own: `<2.0.0` is the `yarn` package, `>=2.0.0`
161
+ * (Berry) ships as `@yarnpkg/cli-dist` — a scoped package, whose tarball
162
+ * basename drops the scope (`/@yarnpkg/cli-dist/-/cli-dist-<v>.tgz`).
163
+ */
164
+ const registryTarballUrl = (name, version, major, registry) => name === "yarn" && major >= 2 ? `${registry}/@yarnpkg/cli-dist/-/cli-dist-${version}.tgz` : `${registry}/${name}/-/${name}-${version}.tgz`;
165
+ /** Normalize a package.json `bin` value into name → relative path entries. */
166
+ const normalizeBins = (bin, fallbackName) => {
167
+ if (typeof bin === "string") return Option.some({ [fallbackName]: bin.replace(/^\.\//, "") });
168
+ if (typeof bin !== "object" || bin === null) return Option.none();
169
+ const entries = {};
170
+ for (const [name, value] of Object.entries(bin)) {
171
+ if (typeof value !== "string") return Option.none();
172
+ entries[name] = value.replace(/^\.\//, "");
173
+ }
174
+ return Object.keys(entries).length === 0 ? Option.none() : Option.some(entries);
175
+ };
176
+ /** Map a `RUNNER_ARCH` value (`X64`, `ARM64`) onto the Node arch spelling. */
177
+ const archFromRunner = (runnerArch) => runnerArch.toLowerCase() === "x64" ? "x64" : runnerArch.toLowerCase() === "arm64" ? "arm64" : runnerArch;
178
+ const make = Effect.gen(function* () {
179
+ const env = yield* ActionEnvironment;
180
+ const fs = yield* FileSystem.FileSystem;
181
+ const path = yield* Path.Path;
182
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
183
+ const installer = yield* ToolInstaller;
184
+ const runnerOs = yield* Effect.map(env.getOptional("RUNNER_OS"), (found) => Option.getOrElse(found, () => ""));
185
+ const arch = yield* Effect.map(env.getOptional("RUNNER_ARCH"), (found) => Option.match(found, {
186
+ onNone: () => process.arch,
187
+ onSome: archFromRunner
188
+ }));
189
+ const windows = runnerOs.toLowerCase() === "windows";
190
+ const bunBinaryName = windows ? "bun.exe" : "bun";
191
+ const shimFileName = (name) => windows ? `${name}.cmd` : name;
192
+ const shimBody = windows ? cmdShim : posixShim;
193
+ const cacheRoot = yield* Effect.map(env.getOptional("RUNNER_TOOL_CACHE"), (found) => Option.getOrElse(found, () => path.join("/tmp", "runner-tool-cache")));
194
+ const finalCachePath = (tool, version) => ToolInstaller.cachePath({
195
+ root: cacheRoot,
196
+ tool,
197
+ version,
198
+ arch: process.arch
199
+ });
200
+ const errorFor = (pin) => (fields) => new PackageManagerInstallerError({
201
+ name: pin.name,
202
+ version: pin.version.toString(),
203
+ ...fields
204
+ });
205
+ /** Map a ToolInstaller failure onto this surface; the reasons line up 1:1. */
206
+ const fromInstaller = (pin) => (cause) => errorFor(pin)({
207
+ reason: cause.reason,
208
+ ...cause.subject === void 0 ? {} : { subject: cause.subject },
209
+ cause
210
+ });
211
+ /** The hex digest of a file's bytes, streamed rather than buffered. */
212
+ const hashFile = (pin, file, algorithm) => Effect.suspend(() => {
213
+ const digest = createHash(algorithm);
214
+ return fs.stream(file).pipe(Stream.runForEach((chunk) => Effect.sync(() => {
215
+ digest.update(chunk);
216
+ })), Effect.map(() => digest.digest("hex")), Effect.mapError((cause) => errorFor(pin)({
217
+ reason: "integrityMismatch",
218
+ subject: file,
219
+ cause
220
+ })));
221
+ });
222
+ /**
223
+ * Verify a downloaded artifact against the pin's integrity, fail-closed.
224
+ *
225
+ * A pin with no integrity proceeds with a logged warning — in-the-wild
226
+ * `devEngines` pins routinely carry none, and the opt-in strictness lives in
227
+ * `requireIntegrity`, checked before anything is downloaded.
228
+ */
229
+ const verifyIntegrity = (pin, file) => Effect.gen(function* () {
230
+ if (pin.integrity === void 0) {
231
+ yield* Effect.logWarning(`The pin ${pin.toString()} carries no integrity hash; the downloaded artifact was not verified.`);
232
+ return;
233
+ }
234
+ const dot = pin.integrity.indexOf(".");
235
+ const algorithm = pin.integrity.slice(0, dot);
236
+ const expectedHex = pin.integrity.slice(dot + 1);
237
+ const actualHex = yield* hashFile(pin, file, algorithm);
238
+ if (actualHex !== expectedHex) return yield* Effect.fail(errorFor(pin)({
239
+ reason: "integrityMismatch",
240
+ subject: file,
241
+ expected: pin.integrity,
242
+ actual: `${algorithm}.${actualHex}`
243
+ }));
244
+ });
245
+ /**
246
+ * The ambient `npm --version`, or `None` when the probe cannot answer.
247
+ *
248
+ * A failed probe is not an error — there is nothing a caller could do about
249
+ * it that the dist path does not already do.
250
+ */
251
+ const ambientNpmVersion = Effect.map(Effect.result(spawner.string(ChildProcess.make("npm", ["--version"]))), (result) => result._tag === "Success" ? Option.some(result.success.trim()) : Option.none());
252
+ const assertFile = (pin, file, subject) => Effect.gen(function* () {
253
+ const stat = yield* Effect.result(fs.stat(file));
254
+ if (stat._tag !== "Success" || stat.success.type !== "File") return yield* Effect.fail(errorFor(pin)({
255
+ reason: "layoutUnexpected",
256
+ subject
257
+ }));
258
+ });
259
+ /**
260
+ * The bin entries a package directory publishes, from its own manifest.
261
+ *
262
+ * Read from the artifact rather than hardcoded per manager, because the
263
+ * layouts genuinely move — corepack's own table has pnpm's entry at
264
+ * `bin/pnpm.cjs` through v10 and `bin/pnpm.mjs` from v11.
265
+ */
266
+ const readPackageBins = (pin, packageDir) => Effect.gen(function* () {
267
+ const manifestPath = path.join(packageDir, "package.json");
268
+ const raw = yield* fs.readFileString(manifestPath).pipe(Effect.mapError((cause) => errorFor(pin)({
269
+ reason: "layoutUnexpected",
270
+ subject: "no package.json",
271
+ cause
272
+ })));
273
+ const manifest = yield* Effect.try({
274
+ try: () => JSON.parse(raw),
275
+ catch: (cause) => errorFor(pin)({
276
+ reason: "layoutUnexpected",
277
+ subject: "unparseable package.json",
278
+ cause
279
+ })
280
+ });
281
+ const bins = Option.getOrUndefined(normalizeBins(manifest.bin, pin.name));
282
+ if (bins === void 0) return yield* Effect.fail(errorFor(pin)({
283
+ reason: "layoutUnexpected",
284
+ subject: "package.json names no bin"
285
+ }));
286
+ for (const [name, relative] of Object.entries(bins)) {
287
+ const target = path.join(packageDir, relative);
288
+ const containment = path.relative(packageDir, target);
289
+ if (containment === ".." || containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment)) return yield* Effect.fail(errorFor(pin)({
290
+ reason: "layoutUnexpected",
291
+ subject: `bin ${name} (${relative}) escapes the package directory`
292
+ }));
293
+ yield* assertFile(pin, target, `bin ${name} (${relative}) is missing`);
294
+ }
295
+ return bins;
296
+ });
297
+ const rootBins = (bins, directory) => Object.fromEntries(Object.entries(bins).map(([name, relative]) => [name, path.join(directory, relative)]));
298
+ /**
299
+ * Write one executable shim per bin into `<into>/.bin`, each invoking
300
+ * `node <finalDirectory>/<relative>`.
301
+ *
302
+ * @remarks
303
+ * `into` and `finalDirectory` are DIFFERENT on the install path on purpose:
304
+ * the shims are written into the *staged* tree so they are part of the entry
305
+ * ToolInstaller renames into place (the cache only ever contains complete
306
+ * entries), while their contents must name the *final* cache path the entry
307
+ * is about to land at. On the cache-hit regeneration path the two coincide.
308
+ * When `skipExisting` is set, a shim already present is left untouched — the
309
+ * regeneration path must not rewrite a shared cache entry another writer owns.
310
+ */
311
+ const writeShims = (pin, into, finalDirectory, bins, options) => Effect.gen(function* () {
312
+ const shimDir = path.join(into, SHIM_DIR);
313
+ const cacheError = (subject) => (cause) => errorFor(pin)({
314
+ reason: "cacheFailed",
315
+ subject,
316
+ cause
317
+ });
318
+ yield* fs.makeDirectory(shimDir, { recursive: true }).pipe(Effect.mapError(cacheError(shimDir)));
319
+ for (const [name, relative] of Object.entries(bins)) {
320
+ const shim = path.join(shimDir, shimFileName(name));
321
+ if (options.skipExisting) {
322
+ const present = yield* Effect.result(fs.stat(shim));
323
+ if (present._tag === "Success" && present.success.type === "File") continue;
324
+ }
325
+ yield* fs.writeFileString(shim, shimBody(path.join(finalDirectory, relative))).pipe(Effect.mapError(cacheError(shim)));
326
+ if (!windows) yield* fs.chmod(shim, 493).pipe(Effect.mapError(cacheError(shim)));
327
+ }
328
+ });
329
+ /** The record for a directory already in the cache, regenerating absent shims. */
330
+ const cachedRecord = (pin, directory) => Effect.gen(function* () {
331
+ if (pin.name === "bun") {
332
+ const binary = path.join(directory, bunBinaryName);
333
+ yield* assertFile(pin, binary, `cached bun binary (${bunBinaryName}) is missing`);
334
+ return CachedPackageManager.make({
335
+ name: pin.name,
336
+ version: pin.version.toString(),
337
+ directory,
338
+ binDir: directory,
339
+ bins: { bun: binary }
340
+ });
341
+ }
342
+ const bins = yield* readPackageBins(pin, directory);
343
+ yield* writeShims(pin, directory, directory, bins, { skipExisting: true });
344
+ return CachedPackageManager.make({
345
+ name: pin.name,
346
+ version: pin.version.toString(),
347
+ directory,
348
+ binDir: path.join(directory, SHIM_DIR),
349
+ bins: rootBins(bins, directory)
350
+ });
351
+ });
352
+ /** Download bun's per-platform zip from GitHub releases and cache the binary. */
353
+ const installBun = (pin) => Effect.gen(function* () {
354
+ const version = pin.version.toString();
355
+ const target = Option.getOrUndefined(bunTarget(runnerOs, arch));
356
+ if (target === void 0) return yield* Effect.fail(errorFor(pin)({
357
+ reason: "unsupportedPlatform",
358
+ subject: `${runnerOs || "unknown"}/${arch}`
359
+ }));
360
+ const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${target}.zip`;
361
+ const archive = yield* installer.download(url).pipe(Effect.mapError(fromInstaller(pin)));
362
+ yield* verifyIntegrity(pin, archive);
363
+ const extracted = yield* installer.extractZip(archive).pipe(Effect.mapError(fromInstaller(pin)));
364
+ const binary = path.join(extracted, target, bunBinaryName);
365
+ yield* assertFile(pin, binary, `${target}/${bunBinaryName} is missing from the zip`);
366
+ if (!windows) yield* fs.chmod(binary, 493).pipe(Effect.mapError((cause) => errorFor(pin)({
367
+ reason: "cacheFailed",
368
+ subject: binary,
369
+ cause
370
+ })));
371
+ const directory = yield* installer.cacheFile(binary, bunBinaryName, pin.name, version).pipe(Effect.mapError(fromInstaller(pin)));
372
+ return CachedPackageManager.make({
373
+ name: pin.name,
374
+ version,
375
+ directory,
376
+ binDir: directory,
377
+ bins: { bun: path.join(directory, bunBinaryName) }
378
+ });
379
+ });
380
+ /** Download a registry tarball, verify, extract, shim, and cache the package dir. */
381
+ const installFromRegistry = (pin, name, registry) => Effect.gen(function* () {
382
+ const version = pin.version.toString();
383
+ const url = registryTarballUrl(name, version, pin.version.major, registry);
384
+ const archive = yield* installer.download(url).pipe(Effect.mapError(fromInstaller(pin)));
385
+ const berry = name === "yarn" && pin.version.major >= 2;
386
+ if (!berry) yield* verifyIntegrity(pin, archive);
387
+ const extracted = yield* installer.extractTar(archive).pipe(Effect.mapError(fromInstaller(pin)));
388
+ const packageDir = path.join(extracted, "package");
389
+ const stat = yield* Effect.result(fs.stat(packageDir));
390
+ if (stat._tag !== "Success" || stat.success.type !== "Directory") return yield* Effect.fail(errorFor(pin)({
391
+ reason: "layoutUnexpected",
392
+ subject: "tarball has no package/ root"
393
+ }));
394
+ const bins = yield* readPackageBins(pin, packageDir);
395
+ if (berry) {
396
+ const cli = path.join(packageDir, "bin/yarn.js");
397
+ yield* assertFile(pin, cli, "bin/yarn.js is missing from the cli-dist tarball");
398
+ yield* verifyIntegrity(pin, cli);
399
+ }
400
+ const destination = finalCachePath(name, version);
401
+ yield* writeShims(pin, packageDir, destination, bins, { skipExisting: false });
402
+ const directory = yield* installer.cacheDir(packageDir, name, version).pipe(Effect.mapError(fromInstaller(pin)));
403
+ if (directory !== destination) return yield* Effect.fail(errorFor(pin)({
404
+ reason: "cacheFailed",
405
+ subject: `cache destination diverged: shims name ${destination}, entry landed at ${directory}`
406
+ }));
407
+ return CachedPackageManager.make({
408
+ name,
409
+ version,
410
+ directory,
411
+ binDir: path.join(directory, SHIM_DIR),
412
+ bins: rootBins(bins, directory)
413
+ });
414
+ });
415
+ return { install: Effect.fn("PackageManagerInstaller.install")(function* (pin, options) {
416
+ const version = pin.version.toString();
417
+ yield* Effect.annotateCurrentSpan({
418
+ name: pin.name,
419
+ version
420
+ });
421
+ if (options?.requireIntegrity === true && pin.integrity === void 0) return yield* Effect.fail(errorFor(pin)({ reason: "integrityMissing" }));
422
+ const cached = yield* installer.find(pin.name, version);
423
+ if (Option.isSome(cached)) return yield* cachedRecord(pin, cached.value);
424
+ if (pin.name === "npm" && options?.allowAmbient !== false) {
425
+ const probed = yield* ambientNpmVersion;
426
+ if (Option.isSome(probed) && probed.value === version) return AmbientPackageManager.make({
427
+ name: pin.name,
428
+ version,
429
+ bins: {
430
+ npm: "npm",
431
+ npx: "npx"
432
+ }
433
+ });
434
+ }
435
+ if (pin.name === "bun") return yield* installBun(pin);
436
+ const registry = (options?.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
437
+ return yield* installFromRegistry(pin, pin.name, registry);
438
+ }) };
439
+ });
440
+ const unimplemented = (member) => {
441
+ throw new Error(`PackageManagerInstaller.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
442
+ };
443
+ /**
444
+ * First-class exact-version package-manager provisioning on a GitHub runner.
445
+ *
446
+ * @remarks
447
+ * Takes a corepack pin (`@effected/npm`'s `PackageManagerPin`) and answers
448
+ * with an installed manager: tool-cache `find` first, then — for npm — an
449
+ * ambient `npm --version` probe (every Node toolchain ships npm), and only
450
+ * then the manager's own dist: npm, pnpm and yarn 1.x from their registry
451
+ * tarballs, yarn 2+ (Berry) from `@yarnpkg/cli-dist`, bun from its
452
+ * per-platform GitHub-release zip. A pin that carries integrity is verified
453
+ * fail-closed against what corepack itself hashes; one that does not proceeds
454
+ * with a logged warning unless `requireIntegrity` says otherwise.
455
+ *
456
+ * Every tool-cache answer carries a `binDir` a consumer can hand straight to
457
+ * `ActionOutputs.addPath`: the npm-registry managers get a `.bin` directory
458
+ * of executable shims written as part of the cached entry, bun's entry is its
459
+ * own `binDir`. Caching goes through {@link ToolInstaller}, so the
460
+ * stage-then-swap invariant holds here for free — shims included, the tool
461
+ * cache only ever contains complete package managers.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * import { ActionOutputs, PackageManagerInstaller } from "@effected/github-actions";
466
+ * import { PackageManagerPin } from "@effected/npm";
467
+ * import { Effect } from "effect";
468
+ *
469
+ * const provision = Effect.gen(function* () {
470
+ * const installer = yield* PackageManagerInstaller;
471
+ * const outputs = yield* ActionOutputs;
472
+ * const pin = yield* PackageManagerPin.parse("pnpm@10.13.1");
473
+ * const installed = yield* installer.install(pin);
474
+ * if (installed.source === "tool-cache") {
475
+ * yield* outputs.addPath(installed.binDir);
476
+ * }
477
+ * });
478
+ * ```
479
+ *
480
+ * @public
481
+ */
482
+ var PackageManagerInstaller = class PackageManagerInstaller extends Context.Service()("@effected/github-actions/PackageManagerInstaller") {
483
+ static layer = Layer.effect(this, make);
484
+ /** A test double. The unstubbed member dies rather than reporting an install that did not happen. */
485
+ static makeTest = (overrides = {}) => ({
486
+ install: () => Effect.sync(() => unimplemented("install")),
487
+ ...overrides
488
+ });
489
+ /** {@link PackageManagerInstaller.makeTest} behind `Layer.succeed`. */
490
+ static layerTest = (overrides = {}) => Layer.succeed(PackageManagerInstaller, PackageManagerInstaller.makeTest(overrides));
491
+ };
492
+
493
+ //#endregion
494
+ export { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError };