@effected/github-actions 0.14.0 → 0.16.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/ActionInput.js CHANGED
@@ -176,6 +176,37 @@ var ActionInput = class ActionInput {
176
176
  }));
177
177
  }
178
178
  /**
179
+ * An input that must be one of a closed set of strings, typed as their
180
+ * union.
181
+ *
182
+ * @remarks
183
+ * The match is **exact**: no trimming and no case folding, so `" pr"` and
184
+ * `"PR"` are both rejected for `["commit", "pr"]`. An action manifest
185
+ * declares the set in `options:`-style prose and the workflow author copies
186
+ * a value verbatim; the one thing worth tolerating there is nothing, because
187
+ * a tolerated near-miss is a value the manifest never named. Absent or `""`
188
+ * is **missing data** (see {@link ActionInput.string}) — `Config.withDefault`
189
+ * for an optional input. A present value outside the set fails carrying its
190
+ * `actual`, naming the input, the value and the allowed set, and is never
191
+ * swallowed by a default.
192
+ *
193
+ * Core's `Config.Literals` is the same idea over `Config.schema`; this one
194
+ * exists so the failure reads like every other `ActionInput` failure —
195
+ * naming the input by its action name, not its `INPUT_` variable.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { ActionInput } from "@effected/github-actions";
200
+ * import { Config } from "effect";
201
+ *
202
+ * // Config<"commit" | "pr">
203
+ * const mode = ActionInput.literals("mode", ["commit", "pr"]).pipe(Config.withDefault("commit"));
204
+ * ```
205
+ */
206
+ static literals(name, allowed) {
207
+ return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => allowed.includes(raw) ? Effect.succeed(raw) : Effect.fail(configError(`Input "${name}" must be one of: ${allowed.join(" | ")} — received "${raw}"`, raw))));
208
+ }
209
+ /**
179
210
  * An integer input.
180
211
  *
181
212
  * @remarks
@@ -306,6 +337,12 @@ var ActionInput = class ActionInput {
306
337
  * `Config.option` for an optional input. A present value that is not valid
307
338
  * JSON, or does not satisfy the schema, fails carrying its `actual` and is
308
339
  * never swallowed by a default.
340
+ *
341
+ * There is no `schemaOption`: `ActionInput.schema(name, S).pipe(Config.option)`
342
+ * yields `Config<Option<A>>` with missing → `None` and a malformed value
343
+ * still failing — core's documented `Config.option` behaviour ("validation
344
+ * errors ... still propagate", `Config.ts`), which is exactly the
345
+ * missing-versus-malformed split above.
309
346
  */
310
347
  static schema(name, schema) {
311
348
  return Config.String(inputVariable(name)).pipe(Config.mapEffect((raw) => {
@@ -352,7 +389,11 @@ var ActionInput = class ActionInput {
352
389
  * to input-name entries too: `{ "flag": "" }` is an unsupplied input.
353
390
  *
354
391
  * Taking the environment as an argument rather than reading `process.env`
355
- * is what makes inputs testable without mutating the test process.
392
+ * is what makes inputs testable without mutating the test process. The
393
+ * `process.env` default is a sanctioned exception to the package rule
394
+ * stated on `ChildEnv` (ambient process state is never read behind a
395
+ * caller's back) — a default the caller overrides by passing a value, the
396
+ * same class as `DetachedProcess.spawn`'s `base`.
356
397
  *
357
398
  * **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
358
399
  * beneath this provider or {@link ActionInput.providerOver}, it uppercases
package/Artifact.js CHANGED
@@ -6,10 +6,11 @@ import { CONFLICT, field, isOk, stringField, twirpCall, twirpFailureFields } fro
6
6
  import { digestFileHex } from "./internal/digest.js";
7
7
  import { isWindowsRunner } from "./internal/runner.js";
8
8
  import { spawnOnce } from "./internal/spawn.js";
9
+ import { unzipCommand, zipCommand, zipManifest } from "./internal/archiveCommands.js";
9
10
  import { Context, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
10
11
  import { HttpClient } from "effect/unstable/http";
11
12
  import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
12
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
13
+ import { ChildProcessSpawner } from "effect/unstable/process";
13
14
 
14
15
  //#region src/Artifact.ts
15
16
  /**
@@ -195,31 +196,34 @@ const make = (transfer) => Effect.gen(function* () {
195
196
  artifact,
196
197
  cause
197
198
  }));
198
- const zip = (files, root, destination, level, artifact) => {
199
+ const zip = (files, root, destination, level, artifact) => Effect.gen(function* () {
199
200
  const relative = files.map((file) => path.relative(root, file));
200
- return windows ? archive(ChildProcess.make("pwsh", [
201
- "-NoProfile",
202
- "-NonInteractive",
203
- "-Command",
204
- `Compress-Archive -Path ${relative.map((file) => `'${file.replaceAll("'", "''")}'`).join(",")} -DestinationPath '${destination.replaceAll("'", "''")}' -Force`
205
- ], { cwd: root }), artifact) : archive(ChildProcess.make("zip", [
206
- `-${Math.min(9, Math.max(0, Math.trunc(level)))}`,
207
- "-qr",
201
+ const unrepresentable = relative.find((file) => file.includes("\n") || file.includes("\r"));
202
+ if (unrepresentable !== void 0) return yield* Effect.fail(new ArtifactError({
203
+ reason: "invalidOptions",
204
+ artifact,
205
+ detail: `a file path may not contain a line break: ${JSON.stringify(unrepresentable)}`
206
+ }));
207
+ const manifest = path.join(path.dirname(destination), "artifact.manifest");
208
+ if (windows) yield* fs.writeFileString(manifest, zipManifest(relative)).pipe(Effect.mapError((cause) => new ArtifactError({
209
+ reason: "archiveFailed",
210
+ artifact,
211
+ cause
212
+ })));
213
+ yield* archive(zipCommand({
214
+ windows,
215
+ root,
216
+ files: relative,
217
+ manifest,
208
218
  destination,
209
- ...relative
210
- ], { cwd: root }), artifact);
211
- };
212
- const unzip = (source, destination, artifact) => windows ? archive(ChildProcess.make("pwsh", [
213
- "-NoProfile",
214
- "-NonInteractive",
215
- "-Command",
216
- `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${source.replaceAll("'", "''")}', '${destination.replaceAll("'", "''")}')`
217
- ]), artifact) : archive(ChildProcess.make("unzip", [
218
- "-oq",
219
+ level
220
+ }), artifact);
221
+ });
222
+ const unzip = (source, destination, artifact) => archive(unzipCommand({
223
+ windows,
219
224
  source,
220
- "-d",
221
225
  destination
222
- ]), artifact);
226
+ }), artifact);
223
227
  return {
224
228
  upload: Effect.fn("Artifact.upload")(function* (name, files, rootDirectory, options) {
225
229
  yield* Effect.annotateCurrentSpan({ name });
package/CacheKey.js CHANGED
@@ -353,18 +353,22 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
353
353
  * failure mode a cache key has.
354
354
  *
355
355
  * Candidates are matched by their path **relative to the workspace**, and a
356
- * literal that climbs above it (`../lockfile`) is dropped, which is what
357
- * makes "never hash a file outside the workspace" structural rather than a
358
- * rule someone has to remember. Directories are excluded: a directory
356
+ * literal that climbs above it (`../lockfile`) is dropped lexically — no
357
+ * literal reaches outside the workspace through path text. The walk is
358
+ * another matter: under `followSymlinks` a symlinked directory whose target
359
+ * lives outside the workspace IS descended, and its files DO contribute to
360
+ * the key — parity with `@actions/glob`, which follows links out of the
361
+ * tree too. Directories are excluded: a directory
359
362
  * called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
360
363
  * would fail rather than being ignored. An absent literal is a miss; any
361
364
  * other failure to read one is a typed `CacheKeyReadError`, because a key
362
365
  * derived from an incomplete file set is wrong in a way nothing reports.
363
366
  *
364
- * **One knowing divergence from the runner's `hashFiles()`:** `descend`
365
- * never enters a symlinked directory (cycle safety), where `@actions/glob`
366
- * follows links by default. A file reachable only through a symlinked
367
- * directory does not contribute to the key.
367
+ * Symlinked directories are followed (`descend` under
368
+ * `followSymlinks: true`), matching `@actions/glob`'s default
369
+ * `followSymbolicLinks: true` — a file reachable only through a symlinked
370
+ * directory contributes to the key, and `descend`'s real-path cycle guard
371
+ * keeps link loops finite.
368
372
  *
369
373
  * The answer is sorted, so a caller cannot make its key depend on the order
370
374
  * the filesystem happened to report.
@@ -396,7 +400,8 @@ var CacheKey = class CacheKey extends Schema.Class("CacheKey")(Schema.Struct({
396
400
  if (wildcard.negated) continue;
397
401
  const found = yield* descend(wildcard, {
398
402
  cwd: workspace,
399
- prune: []
403
+ prune: [],
404
+ followSymlinks: true
400
405
  }).pipe(Effect.mapError((cause) => new CacheKeyReadError({
401
406
  path: path.join(workspace, cause.path),
402
407
  cause
package/ChildEnv.js CHANGED
@@ -29,6 +29,16 @@
29
29
  * ambient process — the same species as `Secret`'s statics, one concern per
30
30
  * member.
31
31
  *
32
+ * **The package rule, stated once: ambient process state is never read behind
33
+ * a caller's back.** `process.env`, `process.arch` and `process.platform` are
34
+ * read by `ActionEnvironment` (once, at layer construction) and otherwise
35
+ * taken as an argument — which is what makes every platform branch
36
+ * exercisable from a test on any host. The sanctioned exceptions are
37
+ * *defaults a caller can override by passing the value*: `ActionInput.provider`'s
38
+ * `env = process.env` and `DetachedProcess.spawn`'s `base`. A structural test
39
+ * (`__test__/ambientReads.test.ts`) holds the list closed; a new default is
40
+ * added there with its reason, never silently.
41
+ *
32
42
  * @public
33
43
  */
34
44
  var ChildEnv = class ChildEnv {
@@ -1,4 +1,5 @@
1
1
  import { unstubbed } from "./internal/unstubbed.js";
2
+ import { isErrno } from "./internal/fsProbe.js";
2
3
  import { Effect, Schedule, Schema } from "effect";
3
4
  import { HttpClient } from "effect/unstable/http";
4
5
  import { ChildProcessSpawner } from "effect/unstable/process";
@@ -114,8 +115,6 @@ cause: Schema.optionalKey(Schema.Defect()) }) {
114
115
  * @public
115
116
  */
116
117
  const ProcessId = Schema.Number.pipe(Schema.check(Schema.makeFilter((value) => Number.isInteger(value) && value > 0 ? void 0 : "Expected a positive integer process id")), Schema.brand("ProcessId"));
117
- /** Whether a thrown value is a Node errno error with the given code. */
118
- const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
119
118
  /** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
120
119
  const dies = unstubbed("DetachedProcess.makeTestOps");
121
120
  /**
@@ -174,7 +173,7 @@ var DetachedProcess = class DetachedProcess {
174
173
  const child = spawn(options.command, [...options.args ?? []], {
175
174
  ...options.cwd === void 0 ? {} : { cwd: options.cwd },
176
175
  env: {
177
- ...process.env,
176
+ ...options.base ?? process.env,
178
177
  ...options.env
179
178
  },
180
179
  detached: true,
@@ -1,7 +1,7 @@
1
1
  import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { unstubbed } from "./internal/unstubbed.js";
3
3
  import { digestFileHex } from "./internal/digest.js";
4
- import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
4
+ import { isWindowsRunner } from "./internal/runner.js";
5
5
  import { typeAt } from "./internal/fsProbe.js";
6
6
  import { PNPM_EXE_PREFIX, detectMusl, isNodeScript, pnpmExeTarget, strongestSri } from "./internal/pnpmExe.js";
7
7
  import { ToolInstaller } from "./ToolInstaller.js";
@@ -223,13 +223,6 @@ const make = Effect.gen(function* () {
223
223
  const bunBinaryName = windows ? "bun.exe" : "bun";
224
224
  const shimFileName = (name) => windows ? `${name}.cmd` : name;
225
225
  const shimBody = windows ? cmdShim : posixShim;
226
- const cacheRoot = yield* toolCacheRoot(env, path);
227
- const finalCachePath = (tool, version) => ToolInstaller.cachePath({
228
- root: cacheRoot,
229
- tool,
230
- version,
231
- arch: process.arch
232
- });
233
226
  const errorFor = (pin) => (fields) => new PackageManagerInstallerError({
234
227
  name: pin.name,
235
228
  version: pin.version.toString(),
@@ -551,13 +544,9 @@ const make = Effect.gen(function* () {
551
544
  yield* assertFile(pin, cli, "bin/yarn.js is missing from the cli-dist tarball");
552
545
  yield* verifyIntegrity(pin, cli);
553
546
  }
554
- const destination = finalCachePath(name, version);
547
+ const destination = installer.cachePath(name, version);
555
548
  yield* writeShims(pin, packageDir, destination, bins, { skipExisting: false });
556
549
  const directory = yield* installer.cacheDir(packageDir, name, version).pipe(Effect.mapError(fromInstaller(pin)));
557
- if (directory !== destination) return yield* Effect.fail(errorFor(pin)({
558
- reason: "cacheFailed",
559
- subject: `cache destination diverged: shims name ${destination}, entry landed at ${directory}`
560
- }));
561
550
  return CachedPackageManager.make({
562
551
  name,
563
552
  version,
package/ToolInstaller.js CHANGED
@@ -2,10 +2,11 @@ import { ActionEnvironment } from "./ActionEnvironment.js";
2
2
  import { unstubbed } from "./internal/unstubbed.js";
3
3
  import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
4
4
  import { spawnOnce } from "./internal/spawn.js";
5
- import { typeAt } from "./internal/fsProbe.js";
5
+ import { tarExtractCommand, unzipCommand } from "./internal/archiveCommands.js";
6
+ import { isErrno, typeAt } from "./internal/fsProbe.js";
6
7
  import { Context, Effect, FileSystem, Layer, Option, Path, Schedule, Schema, Stream } from "effect";
7
8
  import { HttpClient } from "effect/unstable/http";
8
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
9
+ import { ChildProcessSpawner } from "effect/unstable/process";
9
10
 
10
11
  //#region src/ToolInstaller.ts
11
12
  /**
@@ -135,6 +136,29 @@ const make = Effect.gen(function* () {
135
136
  subject: tool,
136
137
  cause
137
138
  })));
139
+ /**
140
+ * Move a source tree into the staging directory, consuming the source.
141
+ *
142
+ * @remarks
143
+ * A rename first: on the same filesystem it is O(1) however large the
144
+ * toolchain is, where a recursive copy of a hundreds-of-MB tree is the
145
+ * dominant cost of an install. The staging directory is removed before the
146
+ * rename because Windows refuses to rename onto an existing directory; its
147
+ * mktemp name stays reserved in practice. The copy fallback is taken ONLY
148
+ * for `EXDEV` — a cross-filesystem move, the one case a rename cannot do —
149
+ * and every other rename failure is reported as-is: a copy after a
150
+ * permission error would turn a real failure into a slow success. The
151
+ * fallback removes the source afterwards so the contract is the same on
152
+ * both paths; that removal is best-effort, since the entry is already
153
+ * complete and a leftover temp directory is not worth failing the install.
154
+ */
155
+ const moveIntoStaging = (source, staging) => fs.remove(staging, {
156
+ recursive: true,
157
+ force: true
158
+ }).pipe(Effect.flatMap(() => fs.rename(source, staging)), Effect.catchIf((error) => isErrno(error.cause, "EXDEV"), () => fs.copy(source, staging, { overwrite: true }).pipe(Effect.andThen(Effect.ignore(fs.remove(source, {
159
+ recursive: true,
160
+ force: true
161
+ }))))));
138
162
  /** A staging directory beside the cache, so the swap is a rename and not a copy. */
139
163
  const staged = (tool, use) => Effect.gen(function* () {
140
164
  yield* fs.makeDirectory(root, { recursive: true });
@@ -198,32 +222,24 @@ const make = Effect.gen(function* () {
198
222
  });
199
223
  return {
200
224
  find,
225
+ cachePath,
201
226
  download,
202
227
  extractTar: Effect.fn("ToolInstaller.extractTar")(function* (archive, options) {
203
228
  const destination = yield* destinationFor(options);
204
- const flags = options?.flags === void 0 || options.flags.length === 0 ? ["xzf"] : [...options.flags];
205
- yield* extractWith(ChildProcess.make("tar", [
206
- ...flags,
229
+ yield* extractWith(tarExtractCommand({
207
230
  archive,
208
- "-C",
209
- destination
210
- ]), archive);
231
+ destination,
232
+ flags: options?.flags
233
+ }), archive);
211
234
  return destination;
212
235
  }),
213
236
  extractZip: Effect.fn("ToolInstaller.extractZip")(function* (archive, options) {
214
237
  const destination = yield* destinationFor(options);
215
- const command = windows ? ChildProcess.make("pwsh", [
216
- "-NoProfile",
217
- "-NonInteractive",
218
- "-Command",
219
- `$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${archive.replaceAll("'", "''")}', '${destination.replaceAll("'", "''")}', $true) } catch { [Console]::Error.WriteLine($_.Exception.ToString()); exit 1 }`
220
- ]) : ChildProcess.make("unzip", [
221
- "-oq",
222
- archive,
223
- "-d",
238
+ yield* extractWith(unzipCommand({
239
+ windows,
240
+ source: archive,
224
241
  destination
225
- ]);
226
- yield* extractWith(command, archive);
242
+ }), archive);
227
243
  return destination;
228
244
  }),
229
245
  cacheDir: Effect.fn("ToolInstaller.cacheDir")(function* (source, tool, version) {
@@ -231,7 +247,7 @@ const make = Effect.gen(function* () {
231
247
  tool,
232
248
  version
233
249
  });
234
- return yield* staged(tool, (staging) => fs.copy(source, staging, { overwrite: true }).pipe(Effect.mapError((cause) => new ToolInstallerError({
250
+ return yield* staged(tool, (staging) => moveIntoStaging(source, staging).pipe(Effect.mapError((cause) => new ToolInstallerError({
235
251
  reason: "cacheFailed",
236
252
  subject: tool,
237
253
  cause
@@ -266,6 +282,15 @@ const make = Effect.gen(function* () {
266
282
  });
267
283
  const dies = unstubbed("ToolInstaller.makeTest");
268
284
  /**
285
+ * The root {@link ToolInstaller.makeTest}'s `cachePath` answers under: the
286
+ * same resolution as `make`'s (`internal/runner.ts`), read from the ambient
287
+ * environment because a double has no `ActionEnvironment` to ask. The literal
288
+ * `/tmp/runner-tool-cache` is what `make`'s `path.join("/tmp", "runner-tool-cache")`
289
+ * spells on POSIX, which is the only place a test double runs. Test-double
290
+ * only; allowlisted as such in `__test__/ambientReads.test.ts`.
291
+ */
292
+ const testRoot = () => process.env.RUNNER_TOOL_CACHE ?? "/tmp/runner-tool-cache";
293
+ /**
269
294
  * Download, extract and cache a toolchain in the runner's tool cache.
270
295
  *
271
296
  * @remarks
@@ -311,9 +336,28 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
311
336
  * runner's `RUNNER_ARCH` (`X64`) for the same reason.
312
337
  */
313
338
  static cachePath = (options) => `${options.root}/${options.tool}/${options.version}/${options.arch}`;
314
- /** A test double. Unstubbed members die rather than reporting a tool that is not there. */
339
+ /**
340
+ * A test double. Unstubbed members die rather than reporting a tool that is
341
+ * not there.
342
+ *
343
+ * @remarks
344
+ * `cachePath` is the one member with a default rather than a death: it is
345
+ * pure and total, and a caller composing it into shim contents would
346
+ * otherwise have to stub it in every test. The default is the static layout
347
+ * over `RUNNER_TOOL_CACHE`, or the same off-runner root `make` resolves
348
+ * (`internal/runner.ts`). **That is a read of the ambient environment**,
349
+ * sanctioned here only because a double has no `ActionEnvironment` to ask —
350
+ * test-double-only, and listed as such in the package's ambient-read
351
+ * allowlist (`__test__/ambientReads.test.ts`).
352
+ */
315
353
  static makeTest = (overrides = {}) => ({
316
354
  find: () => dies("find"),
355
+ cachePath: (tool, version) => ToolInstaller.cachePath({
356
+ root: testRoot(),
357
+ tool,
358
+ version,
359
+ arch: process.arch
360
+ }),
317
361
  download: () => dies("download"),
318
362
  extractTar: () => dies("extractTar"),
319
363
  extractZip: () => dies("extractZip"),
package/index.d.ts CHANGED
@@ -1319,18 +1319,22 @@ export declare class CacheKey extends CacheKey_base {
1319
1319
  * failure mode a cache key has.
1320
1320
  *
1321
1321
  * Candidates are matched by their path **relative to the workspace**, and a
1322
- * literal that climbs above it (`../lockfile`) is dropped, which is what
1323
- * makes "never hash a file outside the workspace" structural rather than a
1324
- * rule someone has to remember. Directories are excluded: a directory
1322
+ * literal that climbs above it (`../lockfile`) is dropped lexically — no
1323
+ * literal reaches outside the workspace through path text. The walk is
1324
+ * another matter: under `followSymlinks` a symlinked directory whose target
1325
+ * lives outside the workspace IS descended, and its files DO contribute to
1326
+ * the key — parity with `@actions/glob`, which follows links out of the
1327
+ * tree too. Directories are excluded: a directory
1325
1328
  * called `notes.txt` matches `**\/*.txt` and is not a file, and hashing it
1326
1329
  * would fail rather than being ignored. An absent literal is a miss; any
1327
1330
  * other failure to read one is a typed `CacheKeyReadError`, because a key
1328
1331
  * derived from an incomplete file set is wrong in a way nothing reports.
1329
1332
  *
1330
- * **One knowing divergence from the runner's `hashFiles()`:** `descend`
1331
- * never enters a symlinked directory (cycle safety), where `@actions/glob`
1332
- * follows links by default. A file reachable only through a symlinked
1333
- * directory does not contribute to the key.
1333
+ * Symlinked directories are followed (`descend` under
1334
+ * `followSymlinks: true`), matching `@actions/glob`'s default
1335
+ * `followSymbolicLinks: true` — a file reachable only through a symlinked
1336
+ * directory contributes to the key, and `descend`'s real-path cycle guard
1337
+ * keeps link loops finite.
1334
1338
  *
1335
1339
  * The answer is sorted, so a caller cannot make its key depend on the order
1336
1340
  * the filesystem happened to report.
@@ -1646,6 +1650,35 @@ export declare class ActionInput {
1646
1650
  * a malformed `dry-run` quietly reading as `false`.
1647
1651
  */
1648
1652
  static boolean(name: string): Config.Config<boolean>;
1653
+ /**
1654
+ * An input that must be one of a closed set of strings, typed as their
1655
+ * union.
1656
+ *
1657
+ * @remarks
1658
+ * The match is **exact**: no trimming and no case folding, so `" pr"` and
1659
+ * `"PR"` are both rejected for `["commit", "pr"]`. An action manifest
1660
+ * declares the set in `options:`-style prose and the workflow author copies
1661
+ * a value verbatim; the one thing worth tolerating there is nothing, because
1662
+ * a tolerated near-miss is a value the manifest never named. Absent or `""`
1663
+ * is **missing data** (see {@link ActionInput.string}) — `Config.withDefault`
1664
+ * for an optional input. A present value outside the set fails carrying its
1665
+ * `actual`, naming the input, the value and the allowed set, and is never
1666
+ * swallowed by a default.
1667
+ *
1668
+ * Core's `Config.Literals` is the same idea over `Config.schema`; this one
1669
+ * exists so the failure reads like every other `ActionInput` failure —
1670
+ * naming the input by its action name, not its `INPUT_` variable.
1671
+ *
1672
+ * @example
1673
+ * ```ts
1674
+ * import { ActionInput } from "@effected/github-actions";
1675
+ * import { Config } from "effect";
1676
+ *
1677
+ * // Config<"commit" | "pr">
1678
+ * const mode = ActionInput.literals("mode", ["commit", "pr"]).pipe(Config.withDefault("commit"));
1679
+ * ```
1680
+ */
1681
+ static literals<const L extends readonly [string, ...Array<string>]>(name: string, allowed: L): Config.Config<L[number]>;
1649
1682
  /**
1650
1683
  * An integer input.
1651
1684
  *
@@ -1740,6 +1773,12 @@ export declare class ActionInput {
1740
1773
  * `Config.option` for an optional input. A present value that is not valid
1741
1774
  * JSON, or does not satisfy the schema, fails carrying its `actual` and is
1742
1775
  * never swallowed by a default.
1776
+ *
1777
+ * There is no `schemaOption`: `ActionInput.schema(name, S).pipe(Config.option)`
1778
+ * yields `Config<Option<A>>` with missing → `None` and a malformed value
1779
+ * still failing — core's documented `Config.option` behaviour ("validation
1780
+ * errors ... still propagate", `Config.ts`), which is exactly the
1781
+ * missing-versus-malformed split above.
1743
1782
  */
1744
1783
  static schema<A, I>(name: string, schema: Schema.Codec<A, I>): Config.Config<A>;
1745
1784
  /**
@@ -1776,7 +1815,11 @@ export declare class ActionInput {
1776
1815
  * to input-name entries too: `{ "flag": "" }` is an unsupplied input.
1777
1816
  *
1778
1817
  * Taking the environment as an argument rather than reading `process.env`
1779
- * is what makes inputs testable without mutating the test process.
1818
+ * is what makes inputs testable without mutating the test process. The
1819
+ * `process.env` default is a sanctioned exception to the package rule
1820
+ * stated on `ChildEnv` (ambient process state is never read behind a
1821
+ * caller's back) — a default the caller overrides by passing a value, the
1822
+ * same class as `DetachedProcess.spawn`'s `base`.
1780
1823
  *
1781
1824
  * **A bare `ConfigProvider.fromEnv` cannot serve input-name keys.** Composed
1782
1825
  * beneath this provider or {@link ActionInput.providerOver}, it uppercases
@@ -2233,8 +2276,9 @@ interface UploadOptions {
2233
2276
  * The zlib level, 0–9, defaulting to 6 as `@actions/artifact` does.
2234
2277
  *
2235
2278
  * @remarks
2236
- * Out-of-range values are clamped. No effect on Windows, where
2237
- * `Compress-Archive` has no numeric equivalent.
2279
+ * Out-of-range values are clamped. On Windows the level maps onto .NET's
2280
+ * `CompressionLevel`: `0` is `NoCompression`, `1..3` `Fastest`, `4..8`
2281
+ * `Optimal`, `9` `SmallestSize`.
2238
2282
  */
2239
2283
  readonly compressionLevel?: number | undefined;
2240
2284
  }
@@ -3068,9 +3112,9 @@ interface PathPrependOptions {
3068
3112
  * The environment the child will inherit — on a runner, `process.env`.
3069
3113
  *
3070
3114
  * @remarks
3071
- * Explicit rather than defaulted, matching this package's rule that ambient
3072
- * process state is never read behind a caller's back — which is also what
3073
- * makes the Windows casing branch exercisable from a test on any host.
3115
+ * Explicit rather than defaulted, per the package rule stated on
3116
+ * {@link ChildEnv} — which is also what makes the Windows casing branch
3117
+ * exercisable from a test on any host.
3074
3118
  */
3075
3119
  readonly base: Readonly<Record<string, string | undefined>>;
3076
3120
  /**
@@ -3109,6 +3153,16 @@ interface PathPrependOptions {
3109
3153
  * ambient process — the same species as `Secret`'s statics, one concern per
3110
3154
  * member.
3111
3155
  *
3156
+ * **The package rule, stated once: ambient process state is never read behind
3157
+ * a caller's back.** `process.env`, `process.arch` and `process.platform` are
3158
+ * read by `ActionEnvironment` (once, at layer construction) and otherwise
3159
+ * taken as an argument — which is what makes every platform branch
3160
+ * exercisable from a test on any host. The sanctioned exceptions are
3161
+ * *defaults a caller can override by passing the value*: `ActionInput.provider`'s
3162
+ * `env = process.env` and `DetachedProcess.spawn`'s `base`. A structural test
3163
+ * (`__test__/ambientReads.test.ts`) holds the list closed; a new default is
3164
+ * added there with its reason, never silently.
3165
+ *
3112
3166
  * @public
3113
3167
  */
3114
3168
  export declare class ChildEnv {
@@ -3328,15 +3382,32 @@ interface DetachedSpawnOptions {
3328
3382
  readonly logFile: string;
3329
3383
  /** The working directory. */
3330
3384
  readonly cwd?: string | undefined;
3385
+ /**
3386
+ * The environment the child inherits, before `env` is merged over it.
3387
+ *
3388
+ * @remarks
3389
+ * Defaults to the parent's `process.env` — **the one sanctioned ambient
3390
+ * fallback in this module**, the same class of default as
3391
+ * `ActionInput.provider`'s `env = process.env`, and an exception to the
3392
+ * package rule stated on `ChildEnv` (ambient process state is never read
3393
+ * behind a caller's back) precisely because passing a value here is how a
3394
+ * caller opts out of it. Supply it to give the child a controlled block —
3395
+ * a test proving exactly what a worker sees, or a worker that must not
3396
+ * inherit the action's secrets — remembering that a base without `PATH`
3397
+ * costs the child its runtime. An `undefined` value is dropped by Node's
3398
+ * spawn, never stringified.
3399
+ */
3400
+ readonly base?: Readonly<Record<string, string | undefined>> | undefined;
3331
3401
  /**
3332
3402
  * Environment additions.
3333
3403
  *
3334
3404
  * @remarks
3335
- * **Merged over the parent's environment, not substituted for it.** A bare
3336
- * environment costs the child `PATH`, which usually means it cannot find its
3337
- * own runtime and dies before writing a word to the log — a failure that
3338
- * looks like a spawn bug and is not. Secrets belong here only via
3339
- * `Secret.forChildEnv`, which masks them on the way out.
3405
+ * **Merged over `base` (the parent's environment by default), not
3406
+ * substituted for it.** A bare environment costs the child `PATH`, which
3407
+ * usually means it cannot find its own runtime and dies before writing a
3408
+ * word to the log — a failure that looks like a spawn bug and is not.
3409
+ * Secrets belong here only via `Secret.forChildEnv`, which masks them on
3410
+ * the way out.
3340
3411
  */
3341
3412
  readonly env?: Readonly<Record<string, string>> | undefined;
3342
3413
  }
@@ -4393,13 +4464,38 @@ interface ToolInstallerShape {
4393
4464
  * say) to a foreign entry is a real shipped bug.
4394
4465
  */
4395
4466
  readonly find: (tool: string, version: string) => Effect.Effect<Option.Option<string>>;
4467
+ /**
4468
+ * Where this installer's `cacheDir` / `cacheFile` will land `tool@version`:
4469
+ * the final `<root>/<tool>/<version>/<arch>`, over the root this layer
4470
+ * resolved at construction.
4471
+ *
4472
+ * @remarks
4473
+ * Pure — no IO, nothing is created — and exposed so a caller that must
4474
+ * write the final path INTO the staged tree before the swap (a shim that
4475
+ * names its own cached entry) reads the one answer `cacheDir` is about to
4476
+ * use, instead of deriving root and arch a second time and guarding the
4477
+ * two against drifting apart. {@link ToolInstaller.cachePath} is the same
4478
+ * layout as a static function of an explicit root.
4479
+ */
4480
+ readonly cachePath: (tool: string, version: string) => string;
4396
4481
  /** Download a url to a temporary file, retrying what is worth retrying. */
4397
4482
  readonly download: (url: string, options?: ToolDownloadOptions) => Effect.Effect<string, ToolInstallerError>;
4398
4483
  /** Extract a tarball. Returns the directory its contents landed in. */
4399
4484
  readonly extractTar: (archive: string, options?: ExtractOptions) => Effect.Effect<string, ToolInstallerError>;
4400
4485
  /** Extract a zip. Returns the directory its contents landed in. */
4401
4486
  readonly extractZip: (archive: string, options?: ExtractOptions) => Effect.Effect<string, ToolInstallerError>;
4402
- /** Install a directory into the tool cache. Returns its cached path. */
4487
+ /**
4488
+ * Install a directory into the tool cache. Returns its cached path.
4489
+ *
4490
+ * @remarks
4491
+ * **`source` is consumed.** On success it no longer exists at its original
4492
+ * path: the tree is renamed into the cache when the two share a filesystem
4493
+ * (`RUNNER_TEMP` and `RUNNER_TOOL_CACHE` do on hosted runners, making the
4494
+ * install O(1) rather than a recursive copy of the whole toolchain), and
4495
+ * otherwise copied and then removed. Write everything the cached entry must
4496
+ * contain — shims, overlaid binaries — into `source` *before* this call,
4497
+ * and read nothing from it afterwards.
4498
+ */
4403
4499
  readonly cacheDir: (source: string, tool: string, version: string) => Effect.Effect<string, ToolInstallerError>;
4404
4500
  /** Install a single file into the tool cache under `name`. Returns the cached directory. */
4405
4501
  readonly cacheFile: (source: string, name: string, tool: string, version: string) => Effect.Effect<string, ToolInstallerError>;
@@ -4475,7 +4571,20 @@ export declare class ToolInstaller extends ToolInstaller_base {
4475
4571
  readonly version: string;
4476
4572
  readonly arch: string;
4477
4573
  }) => string;
4478
- /** A test double. Unstubbed members die rather than reporting a tool that is not there. */
4574
+ /**
4575
+ * A test double. Unstubbed members die rather than reporting a tool that is
4576
+ * not there.
4577
+ *
4578
+ * @remarks
4579
+ * `cachePath` is the one member with a default rather than a death: it is
4580
+ * pure and total, and a caller composing it into shim contents would
4581
+ * otherwise have to stub it in every test. The default is the static layout
4582
+ * over `RUNNER_TOOL_CACHE`, or the same off-runner root `make` resolves
4583
+ * (`internal/runner.ts`). **That is a read of the ambient environment**,
4584
+ * sanctioned here only because a double has no `ActionEnvironment` to ask —
4585
+ * test-double-only, and listed as such in the package's ambient-read
4586
+ * allowlist (`__test__/ambientReads.test.ts`).
4587
+ */
4479
4588
  static readonly makeTest: (overrides?: Partial<ToolInstallerShape>) => ToolInstallerShape;
4480
4589
  /** {@link ToolInstaller.makeTest} behind `Layer.succeed`. */
4481
4590
  static readonly layerTest: (overrides?: Partial<ToolInstallerShape>) => Layer.Layer<ToolInstaller>;
@@ -0,0 +1,153 @@
1
+ import { ChildProcess } from "effect/unstable/process";
2
+
3
+ //#region src/internal/archiveCommands.ts
4
+ /**
5
+ * The archiver command lines this package spawns, spelled once.
6
+ *
7
+ * @remarks
8
+ * `Artifact` (pack and unpack) and `ToolInstaller` (unpack only) each spawn
9
+ * the same three shapes — a zip, an unzip, a tar extraction — and each has a
10
+ * Windows half that no test on a POSIX host can execute. Two hand-written
11
+ * copies drifted: `ToolInstaller.extractZip` learned the overwrite overload
12
+ * and the stderr capture below, `Artifact.download` did not, and a download
13
+ * into a non-empty directory kept failing on Windows with an EMPTY stderr.
14
+ * This module is the one spelling; the tests pin its text verbatim, which
15
+ * is the only proof the Windows halves get.
16
+ *
17
+ * **The pwsh unzip is deliberately belt-and-braces.** The THREE-argument
18
+ * `ExtractToDirectory(src, dest, $true)` overload overwrites existing files —
19
+ * the two-argument one refuses, which is what turned a repeated extraction
20
+ * into a hard failure — and the `$ErrorActionPreference = 'Stop'` plus
21
+ * try/catch writes the actual .NET exception text plainly to stderr and
22
+ * exits 1. pwsh's own error rendering does not reliably reach a captured
23
+ * stream, and an empty complaint costs a source dive to even hypothesize
24
+ * about.
25
+ *
26
+ * **The pwsh zip drives .NET's `ZipFile` directly, not `Compress-Archive`.**
27
+ * Microsoft's own reference for `Compress-Archive -Path` states both faults:
28
+ * handed individual file paths it stores every entry under its bare file
29
+ * name — directory structure is kept only when `-Path` names a directory —
30
+ * so `dir\b.txt` landed as `b.txt`, same-named files in different
31
+ * directories collided, and a Windows artifact was structurally different
32
+ * from the POSIX `zip -qr` one; and `-Path` expands wildcards, so a literal
33
+ * `report[1].txt` matched nothing and failed the upload. Stating every entry
34
+ * name explicitly through `CreateEntryFromFile(zip, source, entryName)`
35
+ * removes both: the source is `Path.Combine(root, rel)`, taken literally,
36
+ * and the entry name is `rel` with `\` turned to `/`, which is what `zip`
37
+ * records on POSIX.
38
+ *
39
+ * **The Windows script is constant-size: the file list travels in a manifest
40
+ * file, never in `-Command`.** `CreateProcessW` caps the whole command line
41
+ * at 32,767 characters, and inlining one `CreateEntryFromFile` statement per
42
+ * file (~214 characters each) hit that ceiling at roughly 150 files with
43
+ * `ENAMETOOLONG` — fewer than the `Compress-Archive` form it replaced carried.
44
+ * The script now reads {@link zipManifest}'s output (one relative path per
45
+ * line) with `File.ReadAllLines` and loops. **The two branches deliberately
46
+ * differ in mechanism**: POSIX `zip` takes the files as argv, whose ceiling
47
+ * (`ARG_MAX`, megabytes on the hosted runners) is far larger, so the only
48
+ * file-count ceiling left is that POSIX argv limit. The manifest is written by
49
+ * the caller (`Artifact.zip`) as UTF-8 WITHOUT a BOM, which is what
50
+ * `ReadAllLines` reads by default. Its BOM detection would consume a BOM
51
+ * rather than leak it into the first path (measured), so this is a
52
+ * plain-bytes preference, not a correctness guard.
53
+ *
54
+ * **Every `CreateEntryFromFile` is assigned to `$null`.** Unassigned, pwsh
55
+ * writes the returned `ZipArchiveEntry` to stdout — roughly 430 bytes of
56
+ * formatted object per file — and `internal/spawn.ts` interleaves stdout with
57
+ * stderr into the captured output, which is what `ArtifactError.stderr`
58
+ * carries. Left in, that chatter buried the one line that matters, the .NET
59
+ * exception, under kilobytes of entry listings.
60
+ *
61
+ * Every path handed to PowerShell is single-quoted, and a single quote inside
62
+ * one is doubled — the only escape a single-quoted PowerShell literal has.
63
+ *
64
+ * Pure: no filesystem, no environment, no spawn. `internal/spawn.ts` is the
65
+ * execution half. Reaches `effect/unstable/process` alone, so it is safe for
66
+ * any module to import (`__test__/reachability.test.ts`).
67
+ *
68
+ * @internal
69
+ */
70
+ /** A PowerShell single-quoted literal: `'` becomes `''`, nothing else is special. */
71
+ const pwshLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
72
+ /** A non-interactive pwsh invocation of one command line. */
73
+ const pwsh = (script, options) => ChildProcess.make("pwsh", [
74
+ "-NoProfile",
75
+ "-NonInteractive",
76
+ "-Command",
77
+ script
78
+ ], options);
79
+ /** {@link ZipCommandOptions.level} clamped to `zip`'s `0..9` and truncated. */
80
+ const clampLevel = (level) => Math.min(9, Math.max(0, Math.trunc(level)));
81
+ /** The .NET `CompressionLevel` member a clamped zlib level maps onto. */
82
+ const dotnetCompressionLevel = (level) => level === 0 ? "NoCompression" : level <= 3 ? "Fastest" : level <= 8 ? "Optimal" : "SmallestSize";
83
+ /**
84
+ * The manifest the Windows zip script reads: `files` one per line, `\n`-joined,
85
+ * with a trailing newline. Pure; the caller writes it (UTF-8, no BOM).
86
+ *
87
+ * @remarks
88
+ * A path containing `\n` or `\r` is unrepresentable — it would split into two
89
+ * entries — and this function does not check; `Artifact.zip` rejects such a
90
+ * file as `invalidOptions`, naming it, before anything is written.
91
+ *
92
+ * @internal
93
+ */
94
+ const zipManifest = (files) => files.map((file) => `${file}\n`).join("");
95
+ /**
96
+ * Pack `files` into a zip: `zip -<level> -qr <destination> <files...>` with
97
+ * `root` as the working directory, or on Windows a pwsh script that opens
98
+ * `destination` with `ZipFile.Open(..., Create)` after a `File.Delete` (the
99
+ * overwrite) and adds one `CreateEntryFromFile` per line of `manifest`,
100
+ * naming each entry explicitly — under `$ErrorActionPreference = 'Stop'` and
101
+ * a stderr-writing try/catch, and with the archive disposed in a `finally` so
102
+ * a failed entry cannot leave the handle open (the module doc says why not
103
+ * `Compress-Archive`, and why the list travels in a file).
104
+ *
105
+ * @internal
106
+ */
107
+ const zipCommand = (options) => {
108
+ const level = clampLevel(options.level);
109
+ if (!options.windows) return ChildProcess.make("zip", [
110
+ `-${level}`,
111
+ "-qr",
112
+ options.destination,
113
+ ...options.files
114
+ ], { cwd: options.root });
115
+ const destination = pwshLiteral(options.destination);
116
+ const root = pwshLiteral(options.root);
117
+ const manifest = pwshLiteral(options.manifest);
118
+ const compression = `[System.IO.Compression.CompressionLevel]::${dotnetCompressionLevel(level)}`;
119
+ return pwsh(`\$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression; Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.File]::Delete(${destination}); $zip = [System.IO.Compression.ZipFile]::Open(${destination}, [System.IO.Compression.ZipArchiveMode]::Create); try { foreach ($rel in [System.IO.File]::ReadAllLines(${manifest})) { if ($rel -ne '') { $null = [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, [System.IO.Path]::Combine(${root}, $rel), $rel.Replace('\\', '/'), ${compression}) } } } finally { \$zip.Dispose() } } catch { [Console]::Error.WriteLine(\$_.Exception.ToString()); exit 1 }`, { cwd: options.root });
120
+ };
121
+ /**
122
+ * Unpack a zip into `destination`, overwriting: `unzip -oq <source> -d
123
+ * <destination>`, or the three-argument `ExtractToDirectory` overload under
124
+ * `$ErrorActionPreference = 'Stop'` and a stderr-writing try/catch on
125
+ * Windows (the module doc says why both halves are load-bearing).
126
+ *
127
+ * @internal
128
+ */
129
+ const unzipCommand = (options) => options.windows ? pwsh(`\$ErrorActionPreference = 'Stop'; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory(${pwshLiteral(options.source)}, ${pwshLiteral(options.destination)}, $true) } catch { [Console]::Error.WriteLine(\$_.Exception.ToString()); exit 1 }`) : ChildProcess.make("unzip", [
130
+ "-oq",
131
+ options.source,
132
+ "-d",
133
+ options.destination
134
+ ]);
135
+ /**
136
+ * Extract a tarball: `tar <flags> <archive> -C <destination>`. The same on
137
+ * every platform — bsdtar ships on the Windows runner image — so unlike the
138
+ * zip pair there is no `windows` switch.
139
+ *
140
+ * @internal
141
+ */
142
+ const tarExtractCommand = (options) => {
143
+ const flags = options.flags === void 0 || options.flags.length === 0 ? ["xzf"] : [...options.flags];
144
+ return ChildProcess.make("tar", [
145
+ ...flags,
146
+ options.archive,
147
+ "-C",
148
+ options.destination
149
+ ]);
150
+ };
151
+
152
+ //#endregion
153
+ export { tarExtractCommand, unzipCommand, zipCommand, zipManifest };
@@ -6,11 +6,12 @@ import { createHash } from "node:crypto";
6
6
  * The sanctioned `node:crypto` digests, spelled once.
7
7
  *
8
8
  * @remarks
9
- * Core `Crypto` is RNG-only — no digest, no HMAC — so this package's licence
10
- * for a `node:` import covers hashing (`CLAUDE.md`). Keeping every `createHash`
11
- * behind this module is what makes that licence auditable in one place rather
12
- * than six, and keeps the streamed file digest — the shape a multi-gigabyte
13
- * toolchain archive needs — from being re-derived per caller.
9
+ * Core `Crypto` offers one-shot SHA digests over bytes already in memory, but
10
+ * no HMAC and no incremental hasher, so this package's licence for a `node:`
11
+ * import covers hashing (see the Module concept in `okf/`). Keeping every
12
+ * `createHash` behind this module is what makes that licence auditable in one
13
+ * place rather than six, and keeps the streamed file digest — the shape a
14
+ * multi-gigabyte toolchain archive needs — from being re-derived per caller.
14
15
  *
15
16
  * @internal
16
17
  */
@@ -9,6 +9,14 @@ import { Effect } from "effect";
9
9
  * @internal
10
10
  */
11
11
  const typeAt = (fs, path) => Effect.map(Effect.option(fs.stat(path)), (info) => info._tag === "Some" ? info.value.type : void 0);
12
+ /**
13
+ * Whether a thrown value is a Node errno error with the given code — the
14
+ * platform's `SystemError` keeps the raw exception as its `cause`, and codes
15
+ * it maps to no named tag (`EXDEV`, `ESRCH`) are only recoverable from there.
16
+ *
17
+ * @internal
18
+ */
19
+ const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
12
20
 
13
21
  //#endregion
14
- export { typeAt };
22
+ export { isErrno, typeAt };
@@ -19,9 +19,9 @@ const isWindowsRunner = (env) => Effect.map(env.getOptional("RUNNER_OS"), (found
19
19
  * Resolved at layer construction, never at import: the source package read
20
20
  * the variable into a module-level constant, which fixed the root before any
21
21
  * layer could say otherwise and made it impossible to point a test elsewhere.
22
- * `PackageManagerInstaller` resolves the same root because the shims it
23
- * writes into a staged entry must name the FINAL cache path; sharing the
24
- * resolution is what keeps its post-swap equality check a formality.
22
+ * `ToolInstaller` is its only reader: a consumer that needs the final cache
23
+ * path (`PackageManagerInstaller`, for its shims) asks the installer's own
24
+ * `cachePath` member rather than resolving the root a second time.
25
25
  *
26
26
  * @internal
27
27
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
6
6
  "keywords": [
@@ -39,18 +39,18 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.11.0",
43
- "@effected/glob": "^0.7.0",
44
- "@effected/markdown": "^0.11.0",
45
- "@effected/npm": "^0.15.0",
46
- "@effected/sbom": "^0.7.0",
47
- "@effected/semver": "^0.8.0",
48
- "@effected/templates": "^0.7.0",
49
- "@effected/walker": "^0.10.0"
42
+ "@effected/github": "^0.12.0",
43
+ "@effected/glob": "^0.8.0",
44
+ "@effected/markdown": "^0.12.0",
45
+ "@effected/npm": "^0.16.0",
46
+ "@effected/sbom": "^0.8.0",
47
+ "@effected/semver": "^0.9.0",
48
+ "@effected/templates": "^0.8.0",
49
+ "@effected/walker": "^0.12.0"
50
50
  },
51
51
  "peerDependencies": {
52
- "@effect/platform-node": "4.0.0-rc.116",
53
- "effect": "4.0.0-rc.116"
52
+ "@effect/platform-node": "4.0.0-rc.117",
53
+ "effect": "4.0.0-rc.117"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">=24.11.0"