@effected/walker 0.20.1 → 0.22.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/index.d.ts CHANGED
@@ -20,10 +20,16 @@ import { GlobPattern, GlobPatternError, GlobPatternOptions } from "@effected/glo
20
20
  * reserved device name (`CON`, `PRN`, `AUX`, `NUL`, `COM1`–`COM9`,
21
21
  * `LPT1`–`LPT9`), in any case and with or without an extension
22
22
  * (`nul.txt`).
23
+ * - `outside` — `confineReal` only: the candidate's real path, links
24
+ * resolved, lies outside the base's real path.
25
+ * - `unresolvable` — `confineReal` only: resolving the candidate or the base
26
+ * failed for a reason other than absence (a permission error, a link loop,
27
+ * a file used as a directory, a dangling link), so containment cannot be
28
+ * verified. The failure rides in the error's `cause`.
23
29
  *
24
30
  * @public
25
31
  */
26
- export declare const PathEscapeReason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName"]>;
32
+ export declare const PathEscapeReason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName", "outside", "unresolvable"]>;
27
33
  /**
28
34
  * The decoded type of {@link (PathEscapeReason:variable)}.
29
35
  *
@@ -34,7 +40,9 @@ declare const PathEscapeError_base: Schema.Class<PathEscapeError, Schema.TaggedS
34
40
  /** The candidate path, exactly as the caller passed it. */
35
41
  readonly candidate: Schema.String;
36
42
  /** Why the candidate was refused. */
37
- readonly reason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName"]>;
43
+ readonly reason: Schema.Literals<readonly ["nulByte", "absolute", "traversal", "empty", "reservedName", "outside", "unresolvable"]>;
44
+ /** The resolution failure, present when `reason` is `"unresolvable"`. */
45
+ readonly cause: Schema.optionalKey<Schema.Defect>;
38
46
  }>, import("effect/Cause").YieldableError>;
39
47
  /**
40
48
  * An untrusted relative path would leave, or does not name anything under,
@@ -82,7 +90,7 @@ export declare class PathEscapeError extends PathEscapeError_base {
82
90
  * base yields the normalised relative path alone. The check is lexical, not
83
91
  * physical, and symlinks are out of scope: a symlink already inside the
84
92
  * base can still point out of it, so a caller extracting into a directory
85
- * it does not own resolves links separately.
93
+ * it does not own checks the result with `confineReal` as well.
86
94
  *
87
95
  * {@link confineRelative} is defined in terms of this function and adds
88
96
  * only the tracing span.
@@ -139,12 +147,70 @@ export declare const confineRelative: (base: string, candidate: string) => Effec
139
147
  * @public
140
148
  */
141
149
  interface DescendOptions {
142
- /** Absolute directory the pattern is resolved against. Required — walker never reads `process.cwd()`. */
150
+ /**
151
+ * Absolute directory the pattern is resolved against. Required — walker
152
+ * never reads `process.cwd()`.
153
+ *
154
+ * @remarks
155
+ * **A missing `cwd` is an empty result, never a failure**: when `cwd` does
156
+ * not exist, is a file, or is a dangling link, every pattern — literal or
157
+ * magic — answers zero matches (`[]`, or `{ matches: [], unreadable: [] }`
158
+ * under `"record"`), in the `Effect` and the sync forms alike. Zero
159
+ * matches is a normal glob answer, so a caller that must tell "nothing
160
+ * matched" from "there was nothing to look in" checks `cwd` itself first.
161
+ * The same holds for a pattern's literal base below `cwd`. Only an
162
+ * unreadable directory the walk does reach fails, under `"fail"`.
163
+ *
164
+ * **A relative `cwd` — the empty string included — is a defect**: the
165
+ * `Effect` forms die and the sync forms throw. A relative directory would
166
+ * resolve against the process working directory, so the same call would
167
+ * walk different trees from a hook, a CLI and a test runner, and on win32
168
+ * an empty `cwd` joined with a pattern spelled `C:/x` is the drive path
169
+ * `C:\x`. Absoluteness is judged by the injected `Path` (or the sync port's
170
+ * `isAbsolute`), so win32 accepts `C:\repo` and `\\server\share`; a
171
+ * win32 `\repo` names the root of whichever drive the process is on, so
172
+ * it dies as a relative `cwd` does.
173
+ */
143
174
  readonly cwd: string;
175
+ /**
176
+ * Which entry kinds match. `"file"` (the default) answers files only, as
177
+ * `descend` always has; `"directory"` answers directories only; `"any"`
178
+ * answers both.
179
+ *
180
+ * @remarks
181
+ * The kind is the RESOLVED one — links followed, as for files — so a link
182
+ * to a directory matches as a directory, even where `followSymlinks: false`
183
+ * keeps the walk from entering it, and even when its target lies outside
184
+ * `cwd`. Check such an entry with `confineReal` before acting on it. A
185
+ * directory matches as an entry of its parent: `prune`, a boundary and an
186
+ * unentered link withhold what lies below a directory, never the
187
+ * directory itself, so a globstar pattern for `node_modules` finds every
188
+ * `node_modules` under the default `prune` without reading inside one. A directory is tested bare
189
+ * and with a trailing `/`, so the directory-only glob form `dist/`
190
+ * matches, answered as spelled. `cwd` itself is never a match.
191
+ */
192
+ readonly match?: "file" | "directory" | "any";
144
193
  /** Hard cap on directory depth below the pattern's literal prefix. Defaults to 256. */
145
194
  readonly maxDepth?: number;
146
195
  /** Directory names never descended into. Defaults to `["node_modules", ".git"]`; a custom list replaces the default. */
147
196
  readonly prune?: ReadonlyArray<string>;
197
+ /**
198
+ * Marker FILE names that make a directory a boundary — a nested unit (another
199
+ * package, say: `["package.json"]`) whose contents belong to a separate walk.
200
+ *
201
+ * @remarks
202
+ * A directory strictly below `cwd` holding a FILE with one of these names
203
+ * contributes nothing: none of its files match and its subtree is never
204
+ * descended. `cwd` itself is never a boundary, and a DIRECTORY named like a
205
+ * marker does not count. The rule is about where a file sits, not how the
206
+ * walk reached it, so it applies to a walk that starts below `cwd` (the
207
+ * pattern's literal prefix) and to a literal pattern alike: a boundary
208
+ * between `cwd` and the walk base, or between `cwd` and a literal file,
209
+ * also answers zero matches. Each directory pays one stat per marker it
210
+ * actually lists. Defaults to none. Not to be confused with
211
+ * `Walker.ascend`'s `stopAt`, an inclusive upward ceiling.
212
+ */
213
+ readonly boundaries?: ReadonlyArray<string>;
148
214
  /**
149
215
  * What an unreadable directory mid-walk does. `"fail"` (the default) fails
150
216
  * typed — downward enumeration must not silently swallow a subtree, or the
@@ -158,7 +224,9 @@ interface DescendOptions {
158
224
  readonly onUnreadable?: "fail" | "skip";
159
225
  /**
160
226
  * Whether to descend into symlinked directories. Defaults to `false`: a
161
- * symlinked directory is never entered (cycle safety). Under `true` links
227
+ * symlinked directory is never entered (cycle safety) — the walk base, and
228
+ * every directory on the way to it, included: a base reached through a link
229
+ * is an empty result. Under `true` links
162
230
  * are followed with the cycle guard kept underneath, in `@actions/glob`'s
163
231
  * `traversalChain` semantics: each descended directory records its real
164
232
  * path on its own branch's ancestor chain (only the base and each link pay
@@ -208,7 +276,7 @@ interface DescendRecordOptions extends Omit<DescendOptions, "onUnreadable"> {
208
276
  *
209
277
  * @public
210
278
  */
211
- interface UnreadableDirectory {
279
+ interface UnreadableDirectory<C = PlatformError.PlatformError> {
212
280
  /**
213
281
  * The directory's path relative to `cwd`, POSIX separators.
214
282
  *
@@ -220,7 +288,7 @@ interface UnreadableDirectory {
220
288
  */
221
289
  readonly path: string;
222
290
  /** The `readDirectory` (or, for a link under `followSymlinks`, `realPath`) failure, never `NotFound` (a vanished directory is a benign race and is not recorded). */
223
- readonly cause: PlatformError.PlatformError;
291
+ readonly cause: C;
224
292
  }
225
293
  /**
226
294
  * `descend`'s success value under `onUnreadable: "record"`: the matched
@@ -232,11 +300,11 @@ interface UnreadableDirectory {
232
300
  *
233
301
  * @public
234
302
  */
235
- interface DescendResult {
303
+ interface DescendResult<C = PlatformError.PlatformError> {
236
304
  /** Matching FILE paths relative to `cwd`, POSIX separators, sorted — identical in shape to the `"fail"`/`"skip"` success value. */
237
305
  readonly matches: ReadonlyArray<string>;
238
306
  /** Directories that could not be read, each with its cause, in walk order. */
239
- readonly unreadable: ReadonlyArray<UnreadableDirectory>;
307
+ readonly unreadable: ReadonlyArray<UnreadableDirectory<C>>;
240
308
  }
241
309
  declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<"DescendError", {
242
310
  /** The glob pattern's source text. */
@@ -259,6 +327,35 @@ declare const DescendError_base: Schema.Class<DescendError, Schema.TaggedStruct<
259
327
  export declare class DescendError extends DescendError_base {
260
328
  get message(): string;
261
329
  }
330
+ /**
331
+ * One matched entry — a file, or under `match` a directory — with the stat
332
+ * the walk already read for it: `descendEntries`' element. Costs no syscall beyond `descend`'s: the walk stats every entry to
333
+ * classify it, and this keeps the answer instead of discarding it.
334
+ *
335
+ * The type parameter is the stat's type, so the sync walk reuses this shape
336
+ * with its port's stats.
337
+ *
338
+ * @public
339
+ */
340
+ interface DescendEntry<I = FileSystem.File.Info> {
341
+ /** The entry's path relative to `cwd`, POSIX separators. */
342
+ readonly path: string;
343
+ /** `FileSystem.stat` of the file — links followed, so a symlinked file reports its target. */
344
+ readonly info: I;
345
+ }
346
+ /**
347
+ * `descendEntries`' success value under `onUnreadable: "record"`: the matched
348
+ * {@link DescendEntry} values plus every {@link UnreadableDirectory}, exactly
349
+ * as {@link DescendResult} carries them for `descend`.
350
+ *
351
+ * @public
352
+ */
353
+ interface DescendEntriesResult<I = FileSystem.File.Info, C = PlatformError.PlatformError> {
354
+ /** Matching files with their stats, sorted by path. */
355
+ readonly matches: ReadonlyArray<DescendEntry<I>>;
356
+ /** Directories that could not be read, each with its cause, in walk order. */
357
+ readonly unreadable: ReadonlyArray<UnreadableDirectory<C>>;
358
+ }
262
359
  /**
263
360
  * Expand a compiled glob pattern against the filesystem under
264
361
  * `onUnreadable: "record"`, resolving to a {@link DescendResult} — the
@@ -274,15 +371,22 @@ export declare class DescendError extends DescendError_base {
274
371
  export declare function descend(pattern: GlobPattern, options: DescendRecordOptions): Effect.Effect<DescendResult, DescendError, FileSystem.FileSystem | Path.Path>;
275
372
  /**
276
373
  * Expand a compiled glob pattern against the filesystem, returning matching
277
- * FILE paths relative to `cwd` (POSIX separators), sorted by relative path.
374
+ * FILE paths relative to `cwd` (POSIX separators), sorted by relative path —
375
+ * or, under `match: "directory"` or `"any"`, directory paths too.
278
376
  *
279
377
  * @remarks
280
- * A literal pattern (no magic, not negated) fast-paths to a single stat: the
378
+ * A path is answered in the spelling the pattern matched it under: `./src/*.ts`
379
+ * answers `./src/a.ts`, and `src/*.ts` answers `src/a.ts`. A file several
380
+ * brace alternatives match under different spellings (`{src,./src}/*.ts`) is
381
+ * answered once, in the spelling of the earliest of them in pattern order. A
382
+ * literal pattern (no magic, not negated) fast-paths to a single stat: the
281
383
  * result is `[source]` when it resolves to a file, `[]` otherwise — a missing
282
- * path is zero matches, not an error. A magic pattern walks from its literal
283
- * directory prefix (`GlobPattern.enumerationPrefix`); a NEGATED pattern can
284
- * match paths outside that prefix, so it walks from `cwd` itself. A missing
285
- * base directory is likewise an empty result, because zero matches is a
384
+ * path is zero matches, not an error. A magic pattern walks each of its
385
+ * `GlobPattern.enumerationRoots`, reading each directory once however many
386
+ * alternatives spell it; a NEGATED pattern can match paths outside those
387
+ * roots, so it walks from `cwd` itself. A missing `cwd` or
388
+ * base directory (absent, a file, a dangling link) is likewise an empty
389
+ * result — see `DescendOptions.cwd` — because zero matches is a
286
390
  * normal glob answer — as is any pattern that lexically climbs above `cwd`
287
391
  * via `..` segments (walked paths never contain `..`). "Never reads outside
288
392
  * its documented root" holds lexically always, and physically only while
@@ -292,9 +396,10 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
292
396
  * (under the default `onUnreadable: "fail"`) or a walk past `maxDepth` fails,
293
397
  * typed as {@link DescendError}.
294
398
  *
295
- * Only files match. A symlink counts when it stat-resolves to a file
399
+ * Only files match by default (`match` widens that to directories). A symlink
400
+ * counts when it stat-resolves to a file
296
401
  * (`FileSystem.stat` follows links, as node's does); a symlinked directory is
297
- * never descended into by default (cycle safety — detected by a `readLink`
402
+ * never descended into by default (cycle safety — detected by an `lstat`
298
403
  * probe), unless `followSymlinks: true` asks for it, which follows links under
299
404
  * `@actions/glob`'s `traversalChain` cycle guard: a directory is a cycle only
300
405
  * when its real path is already an ancestor of the current branch, so sibling
@@ -332,6 +437,276 @@ export declare function descend(pattern: GlobPattern, options: DescendRecordOpti
332
437
  * @public
333
438
  */
334
439
  export declare function descend(pattern: GlobPattern, options: DescendOptions): Effect.Effect<ReadonlyArray<string>, DescendError, FileSystem.FileSystem | Path.Path>;
440
+ /**
441
+ * `descend` under `onUnreadable: "record"`, keeping each match's stat:
442
+ * resolves to a {@link DescendEntriesResult}.
443
+ *
444
+ * @public
445
+ */
446
+ export declare function descendEntries(pattern: GlobPattern, options: DescendRecordOptions): Effect.Effect<DescendEntriesResult, DescendError, FileSystem.FileSystem | Path.Path>;
447
+ /**
448
+ * `descend`, keeping the stat the walk already read for each matched
449
+ * file: resolves to {@link DescendEntry} values sorted by path.
450
+ *
451
+ * @remarks
452
+ * The same walk, the same matches in the same order, the same errors — only
453
+ * the element type differs, and at no extra syscall (the walk stats every
454
+ * entry to classify it). For a cache signature over a tree, map each entry's
455
+ * `path` and `info.mtime` (or `info.size`) into whatever key the cache needs;
456
+ * the kit does not choose the fingerprint.
457
+ *
458
+ * @example
459
+ * ```ts
460
+ * import { GlobPattern, GlobPatternOptions } from "@effected/glob";
461
+ * import { descendEntries } from "@effected/walker";
462
+ * import { Effect, Option } from "effect";
463
+ *
464
+ * const signature = Effect.gen(function* () {
465
+ * const pattern = yield* GlobPattern.compile("**", GlobPatternOptions.make({ dot: true }));
466
+ * const entries = yield* descendEntries(pattern, { cwd: "/repo/src", boundaries: ["package.json"] });
467
+ * return entries.map((e) => `${e.path}:${Option.match(e.info.mtime, { onNone: () => "", onSome: (d) => d.getTime() })}`).join("|");
468
+ * });
469
+ * ```
470
+ *
471
+ * @public
472
+ */
473
+ export declare function descendEntries(pattern: GlobPattern, options: DescendOptions): Effect.Effect<ReadonlyArray<DescendEntry>, DescendError, FileSystem.FileSystem | Path.Path>;
474
+ /**
475
+ * The stats {@link WalkerSyncFileSystem} answers — a structural subset of
476
+ * `node:fs` `Stats`, so `statSync` / `lstatSync` satisfy it verbatim.
477
+ *
478
+ * @public
479
+ */
480
+ interface WalkerSyncStats {
481
+ /** Whether the entry is a regular file. */
482
+ isFile(): boolean;
483
+ /** Whether the entry is a directory. */
484
+ isDirectory(): boolean;
485
+ /** Whether the entry is a symbolic link (only ever true for `lstat`). */
486
+ isSymbolicLink(): boolean;
487
+ /** Modification time, epoch milliseconds. */
488
+ readonly mtimeMs: number;
489
+ /** Size in bytes. */
490
+ readonly size: number;
491
+ }
492
+ /**
493
+ * The synchronous file operations the sync walk needs, supplied by the
494
+ * consumer — `node:fs` satisfies it (`nodeWalkerFileSystem` from
495
+ * `@effected/walker/node-sync`).
496
+ *
497
+ * @remarks
498
+ * Every member reports failure by throwing a node-shaped error. A `code` of
499
+ * `"ENOENT"` is the benign vanished-path race (empty, never recorded); any
500
+ * other throw from `readDirectory` or `realPath` is an unreadable directory,
501
+ * handled per `onUnreadable`. A throwing `stat` or `lstat` reads as "no
502
+ * entry", exactly as the `Effect` walk reads its failed `stat` and `lstat`.
503
+ *
504
+ * @public
505
+ */
506
+ interface WalkerSyncFileSystem {
507
+ /** The entry names inside the directory at `path` (`readdirSync`). */
508
+ readonly readDirectory: (path: string) => ReadonlyArray<string>;
509
+ /** Stats of `path`, following links (`statSync`). */
510
+ readonly stat: (path: string) => WalkerSyncStats;
511
+ /** Stats of `path` itself, not following a final link (`lstatSync`). */
512
+ readonly lstat: (path: string) => WalkerSyncStats;
513
+ /** The real path of `path`, links resolved (`realpathSync`). */
514
+ readonly realPath: (path: string) => string;
515
+ }
516
+ /**
517
+ * The synchronous path operations the sync forms need — a structural subset
518
+ * of `node:path`, so the module itself (or its `win32` / `posix` variant)
519
+ * satisfies it. It shapes only the absolute paths handed to the
520
+ * {@link WalkerSyncFileSystem}; matching is POSIX-relative by the glob
521
+ * contract.
522
+ *
523
+ * @remarks
524
+ * The members mirror core's `Path` service, so a sync form judges
525
+ * absoluteness, ascends and compares ceilings exactly as its `Effect` twin
526
+ * does under the `Path` layer of the same platform.
527
+ *
528
+ * @public
529
+ */
530
+ interface WalkerSyncPath {
531
+ /** The implementation's separator (`path.sep`). */
532
+ readonly sep: string;
533
+ /** Join segments with the implementation's separator (like `path.join`). */
534
+ readonly join: (...segments: ReadonlyArray<string>) => string;
535
+ /** The parent directory of a path (like `path.dirname`); a fixpoint at the root. */
536
+ readonly dirname: (path: string) => string;
537
+ /** Resolve segments to a normalized path (like `path.resolve`). */
538
+ readonly resolve: (...segments: ReadonlyArray<string>) => string;
539
+ /** Whether a path is absolute under this implementation (like `path.isAbsolute`). */
540
+ readonly isAbsolute: (path: string) => boolean;
541
+ }
542
+ /**
543
+ * The platform half of every sync walk.
544
+ *
545
+ * @public
546
+ */
547
+ interface WalkerSyncOps {
548
+ /** File operations. */
549
+ readonly fileSystem: WalkerSyncFileSystem;
550
+ /** Path joining. */
551
+ readonly path: WalkerSyncPath;
552
+ }
553
+ /**
554
+ * Options for `descendSync` / `descendEntriesSync`: every `DescendOptions`
555
+ * field, plus the platform.
556
+ *
557
+ * @public
558
+ */
559
+ interface DescendSyncOptions extends DescendOptions, WalkerSyncOps {}
560
+ /**
561
+ * {@link DescendSyncOptions} under `onUnreadable: "record"`.
562
+ *
563
+ * @public
564
+ */
565
+ interface DescendSyncRecordOptions extends DescendRecordOptions, WalkerSyncOps {}
566
+ /**
567
+ * `descend` under `onUnreadable: "record"`, synchronously over a caller-supplied port.
568
+ *
569
+ * @public
570
+ */
571
+ export declare function descendSync(pattern: GlobPattern, options: DescendSyncRecordOptions): Result.Result<DescendResult<unknown>, DescendError>;
572
+ /**
573
+ * `descend`, synchronously, over a caller-supplied file port: matching FILE
574
+ * paths relative to `cwd` (POSIX separators), sorted.
575
+ *
576
+ * @remarks
577
+ * The same walk as `descend` — it drives the same traversal, so every rule
578
+ * holds identically: the literal fast path, the enumeration prefix, `prune`,
579
+ * `boundaries`, the symlink posture and per-branch cycle guard under
580
+ * `followSymlinks`, `onUnreadable` and the `maxDepth` failure. Failures are a
581
+ * `Result` failure typed as `DescendError`; an invalid `maxDepth` is a defect
582
+ * and throws. For a caller with no Effect runtime (config evaluation, a Node
583
+ * SEA sidecar); inside Effect, use `descend`.
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * import { GlobPattern } from "@effected/glob";
588
+ * import { descendSync } from "@effected/walker";
589
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
590
+ * import { Result } from "effect";
591
+ *
592
+ * const pattern = Result.getOrThrow(GlobPattern.compileResult("src/**"));
593
+ * const files = descendSync(pattern, { cwd: "/repo", boundaries: ["package.json"], ...nodeWalkerSyncOps });
594
+ * ```
595
+ *
596
+ * @public
597
+ */
598
+ export declare function descendSync(pattern: GlobPattern, options: DescendSyncOptions): Result.Result<ReadonlyArray<string>, DescendError>;
599
+ /**
600
+ * `descendEntries` under `onUnreadable: "record"`, synchronously over a caller-supplied port.
601
+ *
602
+ * @public
603
+ */
604
+ export declare function descendEntriesSync(pattern: GlobPattern, options: DescendSyncRecordOptions): Result.Result<DescendEntriesResult<WalkerSyncStats, unknown>, DescendError>;
605
+ /**
606
+ * `descendEntries`, synchronously: `descendSync`'s matches, each with
607
+ * the stat the walk already read for it.
608
+ *
609
+ * @public
610
+ */
611
+ export declare function descendEntriesSync(pattern: GlobPattern, options: DescendSyncOptions): Result.Result<ReadonlyArray<DescendEntry<WalkerSyncStats>>, DescendError>;
612
+ /**
613
+ * Confine a path to a base directory PHYSICALLY: resolve links in both, and
614
+ * succeed with the candidate's real path only when it lies strictly inside
615
+ * the base's real path.
616
+ *
617
+ * @remarks
618
+ * Reach for it where `confineRelative` stops: a link inside the base that
619
+ * points out of it (a symlinked `dist`, a planted `node_modules` entry), an
620
+ * absolute path an agent or a tool handed over, a `descend` match — a
621
+ * matched link is answered even when its target lies outside `cwd`.
622
+ *
623
+ * The candidate may be absolute (checked as it is) or relative (taken
624
+ * against `base`). It is resolved the way the operating system resolves it:
625
+ * segment by segment, so a `..` after a link climbs out of the link's
626
+ * TARGET — `base/link/../x` is outside when `link` points outside — rather
627
+ * than being collapsed lexically first, as `path.resolve` and node's JS
628
+ * `fs.realpath` would. A path, or the tail of one, that does not exist yet
629
+ * is resolved lexically from its real existing ancestor, so a write target
630
+ * can be checked before it is created — but only once absence is proved: a
631
+ * segment whose real path is not found is checked again without following
632
+ * links, because a dangling link is "not found" too, and a write through one
633
+ * creates its target wherever it points. The base is resolved the same way.
634
+ *
635
+ * Refused, typed as {@link PathEscapeError}: a NUL character (`nulByte`);
636
+ * the empty string or any path whose real location IS the base (`empty`,
637
+ * the same ruling `confineRelative` makes — a caller confining a path names
638
+ * an entry under the base); on win32 a drive-relative `C:x` or a driveless
639
+ * rooted `\x`, which names the current drive (`absolute`), or a segment
640
+ * win32 reads as something else, such as a device name or `.. `
641
+ * (`reservedName`); a real path outside the base's (`outside`); and a
642
+ * resolution that fails for a reason other than absence, such as a
643
+ * permission error, a link loop, or a dangling link — a link whose target
644
+ * does not exist, refused even when that target would lie inside the base
645
+ * (`unresolvable`, with the failure in `cause`) — containment that cannot be
646
+ * verified is refused, never assumed.
647
+ *
648
+ * A relative `base` — the empty string included — is a **defect**: it would
649
+ * be resolved against the process working directory, the silent wrong-root
650
+ * walk `ascend` refuses for a relative `stopAt`. Absoluteness is judged by
651
+ * the injected `Path`, so win32 accepts `C:\repo` and `\\server\share`;
652
+ * a win32 `\repo`, rooted on whichever drive the process is on, is a defect
653
+ * too.
654
+ *
655
+ * Use the returned path, not the candidate: it is the location that was
656
+ * checked. Comparison is exact on the real paths, so on a case-insensitive
657
+ * filesystem a base and a candidate spelled in different case may be refused
658
+ * — the check fails closed. As with any check-then-use on a live filesystem,
659
+ * a link swapped in between the check and the use is not covered.
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * import { confineReal } from "@effected/walker";
664
+ * import { Effect } from "effect";
665
+ *
666
+ * const target = confineReal("/repo", "packages/app/dist").pipe(
667
+ * Effect.catchTag("PathEscapeError", (error) => Effect.die(error)),
668
+ * );
669
+ * // Requires `FileSystem` and `Path` from the platform layer.
670
+ * ```
671
+ *
672
+ * @param base - The absolute directory the candidate must stay under.
673
+ * @param candidate - The path to check: absolute, or relative to `base`.
674
+ * @returns An `Effect` succeeding with the candidate's real path, or failing
675
+ * with {@link PathEscapeError}.
676
+ *
677
+ * @public
678
+ */
679
+ export declare const confineReal: (base: string, candidate: string) => Effect.Effect<string, PathEscapeError, FileSystem.FileSystem | Path.Path>;
680
+ /**
681
+ * {@link confineReal}, synchronously, over a caller-supplied port.
682
+ *
683
+ * @remarks
684
+ * The same resolution from the same code, through the port's `realPath`; a
685
+ * `realPath` throwing `code: "ENOENT"` is absence only when the port's
686
+ * `lstat` throws `ENOENT` too (otherwise a dangling link is there); anything
687
+ * else is `unresolvable`, with the thrown value as the `cause`. A relative `base`
688
+ * throws. For a caller with no Effect runtime; inside Effect, use
689
+ * `confineReal`.
690
+ *
691
+ * @example
692
+ * ```ts
693
+ * import { confineRealSync } from "@effected/walker";
694
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
695
+ * import { Result } from "effect";
696
+ *
697
+ * const checked = confineRealSync("/repo", "/repo/packages/app/dist", nodeWalkerSyncOps);
698
+ * if (Result.isFailure(checked)) console.error(checked.failure.message);
699
+ * ```
700
+ *
701
+ * @param base - The absolute directory the candidate must stay under.
702
+ * @param candidate - The path to check: absolute, or relative to `base`.
703
+ * @param ops - The platform: spread or pass `nodeWalkerSyncOps`.
704
+ * @returns A `Result` succeeding with the candidate's real path, or failing
705
+ * with {@link PathEscapeError}.
706
+ *
707
+ * @public
708
+ */
709
+ export declare const confineRealSync: (base: string, candidate: string, ops: WalkerSyncOps) => Result.Result<string, PathEscapeError>;
335
710
  /**
336
711
  * Options for {@link compileAndExpand}: every {@link DescendOptions} field,
337
712
  * plus the glob options the pattern compiles under.
@@ -472,7 +847,8 @@ interface AscendOptions {
472
847
  *
473
848
  * Only the CEILING is constrained — a relative `start` is fine and ascends
474
849
  * to the relative root. Absoluteness is judged by the injected `Path`
475
- * service, so the win32 layer accepts `C:\repo` and posix does not.
850
+ * service, so the win32 layer accepts `C:\repo` and posix does not; a
851
+ * win32 `\repo`, rooted on whichever drive the process is on, dies too.
476
852
  *
477
853
  * Normalization governs the comparison only: the chain `ascend` returns is
478
854
  * still the lexical one derived from `start`, unrewritten. A ceiling naming
@@ -488,6 +864,37 @@ interface AscendOptions {
488
864
  /** Hard cap on chain length. Defaults to 256. */
489
865
  readonly maxDepth?: number;
490
866
  }
867
+ /**
868
+ * Options for {@link Walker.ascendSync}: every {@link AscendOptions} field,
869
+ * plus the path implementation the chain is computed with.
870
+ *
871
+ * @public
872
+ */
873
+ interface AscendSyncOptions extends AscendOptions {
874
+ /** Path arithmetic — `node:path` satisfies it (`nodeWalkerPath` from `@effected/walker/node-sync`). */
875
+ readonly path: WalkerSyncPath;
876
+ }
877
+ /**
878
+ * Options for the synchronous upward forms that touch the filesystem —
879
+ * {@link Walker.ascendWithinSync} and {@link Walker.ascendToMarkerSync}: the
880
+ * depth cap, plus the platform (spread `nodeWalkerSyncOps` from
881
+ * `@effected/walker/node-sync`).
882
+ *
883
+ * @public
884
+ */
885
+ interface AscendWithinSyncOptions extends Pick<AscendOptions, "maxDepth">, WalkerSyncOps {}
886
+ /**
887
+ * {@link Walker.findUpwardAll}'s answer: the nearest directory holding any
888
+ * candidate, and every candidate that exists there.
889
+ *
890
+ * @public
891
+ */
892
+ interface UpwardMatches {
893
+ /** The directory whose candidates produced the hits. */
894
+ readonly dir: string;
895
+ /** Every existing candidate of `dir`, in the order `candidatesFor` gave them. Never empty. */
896
+ readonly found: readonly [string, ...Array<string>];
897
+ }
491
898
  /**
492
899
  * Upward path traversal primitives: the directory chain from a start path to the
493
900
  * root, and first-match scans over it.
@@ -592,6 +999,171 @@ export declare class Walker {
592
999
  * `pnpm-workspace.yaml`), and its failures are absorbed per directory.
593
1000
  */
594
1001
  static findRoot<E, R>(dirs: ReadonlyArray<string>, isRoot: (dir: string) => Effect.Effect<boolean, E, R>): Effect.Effect<Option.Option<string>, never, R>;
1002
+ /**
1003
+ * {@link Walker.ascend} stopped at the nearest directory that holds any of
1004
+ * `markers`, inclusive: the chain from `start` up to and including the
1005
+ * first directory with a `.git` (or whatever the markers name) entry.
1006
+ *
1007
+ * @remarks
1008
+ * A marker is an entry NAME probed with `FileSystem.exists` in each
1009
+ * directory, nearest first, so any entry kind counts — a `.git` directory,
1010
+ * or the `.git` FILE of a worktree or submodule — and a link counts when it
1011
+ * resolves. The probes stop at the hit: nothing above the directory that
1012
+ * holds a marker is ever touched. With no marker anywhere the chain runs to
1013
+ * the filesystem root, exactly as `ascend(start)` gives it.
1014
+ *
1015
+ * A failed probe is absorbed as "no marker here", as
1016
+ * {@link Walker.firstMatch} absorbs one, so one unreadable directory never
1017
+ * hides a marker above it. An empty `markers` list, or an empty marker
1018
+ * name, is a **defect**: the first would silently ask for the unbounded
1019
+ * walk and the second would match every directory, and both are
1020
+ * statically wrong at the call site. An invalid `maxDepth` dies exactly as
1021
+ * `ascend`'s does.
1022
+ *
1023
+ * There is no `stopAt` here: a marker stop and a lexical ceiling answer the
1024
+ * same question two ways. To bound a marker walk by a ceiling as well,
1025
+ * take the shorter of the two chains.
1026
+ *
1027
+ * @example
1028
+ * ```ts
1029
+ * import { Walker } from "@effected/walker";
1030
+ * import { Effect } from "effect";
1031
+ *
1032
+ * const program = Effect.gen(function* () {
1033
+ * // With `.git` in /repo: ["/repo/packages/app", "/repo/packages", "/repo"]
1034
+ * return yield* Walker.ascendToMarker("/repo/packages/app", [".git"]);
1035
+ * });
1036
+ * // Requires `FileSystem` and `Path` from the platform layer.
1037
+ * ```
1038
+ */
1039
+ static ascendToMarker(start: string, markers: ReadonlyArray<string>, options?: Pick<AscendOptions, "maxDepth">): Effect.Effect<ReadonlyArray<string>, never, FileSystem.FileSystem | Path.Path>;
1040
+ /**
1041
+ * Every existing candidate of the NEAREST directory in `dirs` that has any,
1042
+ * or `Option.none()` — {@link Walker.findUpward} for a caller that must see
1043
+ * a second hit (an ambiguity) rather than have the first one chosen.
1044
+ *
1045
+ * @remarks
1046
+ * Directories are scanned in order, and every candidate of a directory is
1047
+ * probed before the scan ascends; the scan stops after the first directory
1048
+ * with a hit, so nothing above it is probed. `found` keeps the order
1049
+ * `candidatesFor` produced and is never empty. A failed probe is absorbed
1050
+ * as "not there", as in `findUpward`, so not-found and cannot-look are
1051
+ * indistinguishable here too.
1052
+ *
1053
+ * @example
1054
+ * ```ts
1055
+ * import { Walker } from "@effected/walker";
1056
+ * import { Effect, Option } from "effect";
1057
+ *
1058
+ * const program = Effect.gen(function* () {
1059
+ * const dirs = yield* Walker.ascend("/repo/packages/app");
1060
+ * const hits = yield* Walker.findUpwardAll(dirs, (dir) => [`${dir}/tool.config.ts`, `${dir}/tool.config.js`]);
1061
+ * // Two config files in one directory is the caller's ambiguity to report.
1062
+ * return Option.map(hits, ({ found }) => found.length > 1);
1063
+ * });
1064
+ * ```
1065
+ */
1066
+ static findUpwardAll(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>): Effect.Effect<Option.Option<UpwardMatches>, never, FileSystem.FileSystem>;
1067
+ /**
1068
+ * {@link Walker.ascend}, synchronously, over a caller-supplied path
1069
+ * implementation.
1070
+ *
1071
+ * @remarks
1072
+ * The same chain from the same code: lexical, nearest first, `stopAt`
1073
+ * inclusive and compared in resolved form. A relative `stopAt` or an
1074
+ * invalid `maxDepth` is a defect, which here THROWS, since there is no
1075
+ * error channel to die through. For a caller with no Effect runtime (a CLI
1076
+ * helper that runs before one, a bundler-plugin hook); inside Effect, use
1077
+ * `ascend`.
1078
+ *
1079
+ * @example
1080
+ * ```ts
1081
+ * import { Walker } from "@effected/walker";
1082
+ * import { nodeWalkerPath } from "@effected/walker/node-sync";
1083
+ *
1084
+ * const dirs = Walker.ascendSync("/repo/packages/app", { stopAt: "/repo", path: nodeWalkerPath });
1085
+ * // => ["/repo/packages/app", "/repo/packages", "/repo"]
1086
+ * ```
1087
+ */
1088
+ static ascendSync(start: string, options: AscendSyncOptions): ReadonlyArray<string>;
1089
+ /**
1090
+ * {@link Walker.ascendWithin}, synchronously, over a caller-supplied port:
1091
+ * the lexical chain stopped at the nearest directory whose `realPath` is
1092
+ * the ceiling's.
1093
+ *
1094
+ * @remarks
1095
+ * The same walk from the same code as `ascendWithin`: `Option.none()` is
1096
+ * exactly `ascendSync(start)`, a ceiling the chain already spells costs no
1097
+ * `realPath`, a `realPath` that throws is absorbed as "not the ceiling",
1098
+ * and an unresolvable ceiling leaves the lexical chain standing. A
1099
+ * relative ceiling or an invalid `maxDepth` throws.
1100
+ *
1101
+ * @example
1102
+ * ```ts
1103
+ * import { Walker } from "@effected/walker";
1104
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1105
+ * import { Option } from "effect";
1106
+ *
1107
+ * const dirs = Walker.ascendWithinSync("/repo/packages/app", Option.some("/repo"), nodeWalkerSyncOps);
1108
+ * ```
1109
+ */
1110
+ static ascendWithinSync(start: string, ceiling: Option.Option<string>, options: AscendWithinSyncOptions): ReadonlyArray<string>;
1111
+ /**
1112
+ * {@link Walker.ascendToMarker}, synchronously, over a caller-supplied port.
1113
+ *
1114
+ * @remarks
1115
+ * The same walk from the same code. A marker exists when the port's `stat`
1116
+ * (links followed) answers for it; a `stat` that throws, for any reason,
1117
+ * is "no marker here", as the `Effect` form absorbs a failed probe. An
1118
+ * empty `markers` list, an empty marker name or an invalid `maxDepth`
1119
+ * throws.
1120
+ *
1121
+ * @example
1122
+ * ```ts
1123
+ * import { Walker } from "@effected/walker";
1124
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1125
+ *
1126
+ * const dirs = Walker.ascendToMarkerSync("/repo/packages/app", [".git"], nodeWalkerSyncOps);
1127
+ * ```
1128
+ */
1129
+ static ascendToMarkerSync(start: string, markers: ReadonlyArray<string>, options: AscendWithinSyncOptions): ReadonlyArray<string>;
1130
+ /**
1131
+ * {@link Walker.findUpward}, synchronously, over a caller-supplied port: the
1132
+ * first existing candidate, directory-major, or `Option.none()`.
1133
+ *
1134
+ * @remarks
1135
+ * The same scan from the same code. A candidate exists when the port's
1136
+ * `stat` (links followed) answers for it; a `stat` that throws is "not
1137
+ * there". A throwing `candidatesFor` throws before any probe.
1138
+ *
1139
+ * @example
1140
+ * ```ts
1141
+ * import { Walker } from "@effected/walker";
1142
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1143
+ *
1144
+ * const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
1145
+ * const workspace = Walker.findUpwardSync(dirs, (dir) => [`${dir}/pnpm-workspace.yaml`], nodeWalkerSyncOps);
1146
+ * ```
1147
+ */
1148
+ static findUpwardSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<string>;
1149
+ /**
1150
+ * {@link Walker.findUpwardAll}, synchronously, over a caller-supplied port.
1151
+ *
1152
+ * @remarks
1153
+ * The same scan from the same code: every existing candidate of the
1154
+ * nearest directory with any, in candidate order, and nothing above it
1155
+ * probed. A candidate exists when the port's `stat` answers for it.
1156
+ *
1157
+ * @example
1158
+ * ```ts
1159
+ * import { Walker } from "@effected/walker";
1160
+ * import { nodeWalkerSyncOps } from "@effected/walker/node-sync";
1161
+ *
1162
+ * const dirs = Walker.ascendSync("/repo/packages/app", { path: nodeWalkerSyncOps.path });
1163
+ * const hits = Walker.findUpwardAllSync(dirs, (dir) => [`${dir}/a.config.ts`, `${dir}/a.config.js`], nodeWalkerSyncOps);
1164
+ * ```
1165
+ */
1166
+ static findUpwardAllSync(dirs: ReadonlyArray<string>, candidatesFor: (dir: string) => ReadonlyArray<string>, ops: WalkerSyncOps): Option.Option<UpwardMatches>;
595
1167
  }
596
- export type { AscendOptions, CompileAndExpandOptions, DescendOptions, DescendRecordOptions, DescendResult, UnreadableDirectory };
1168
+ export type { AscendOptions, AscendSyncOptions, AscendWithinSyncOptions, CompileAndExpandOptions, DescendEntriesResult, DescendEntry, DescendOptions, DescendRecordOptions, DescendResult, DescendSyncOptions, DescendSyncRecordOptions, UnreadableDirectory, UpwardMatches, WalkerSyncFileSystem, WalkerSyncOps, WalkerSyncPath, WalkerSyncStats };
597
1169
  //# sourceMappingURL=index.d.ts.map