@effected/workspaces 0.28.0-next-20260927071821 → 0.28.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.
@@ -482,7 +482,19 @@ var ConfigDependencyHooks = class ConfigDependencyHooks extends Context.Service(
482
482
  * with `reason: "integrityUnavailable"` — nothing is fetched in either case
483
483
  * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
484
484
  * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
485
- * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
485
+ * registry access, and is bounded at two minutes.
486
+ *
487
+ * The scratch workspace fetches through the same registries a
488
+ * `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
489
+ * registries, mirrors, auth) is copied into it as-is, with `${VAR}`
490
+ * references left for pnpm to expand, never read or logged, and removed
491
+ * with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
492
+ * `registries` keys are carried into the scratch's. That is the CURRENT
493
+ * checkout's registry config for both sides of a diff — a base ref's
494
+ * `.npmrc` is not read through git — and relative paths inside the `.npmrc`
495
+ * (a `cafile=./ca.pem`) resolve against the scratch, not the root.
496
+ *
497
+ * An empty `configDependencies`, or one whose dependencies all
486
498
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
487
499
  * segment in a dependency name fails typed **before** any spawn; a
488
500
  * load failure in the child — a syntax error, a throwing top level, an
package/LockfileReader.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
2
3
  import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
3
4
  import { PackageManagerDetector } from "./PackageManagerName.js";
@@ -60,7 +61,7 @@ var LockfileReader = class LockfileReader extends Context.Service()("@effected/w
60
61
  const fs = yield* FileSystem.FileSystem;
61
62
  const path = yield* Path.Path;
62
63
  const init = Effect.gen(function* () {
63
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
64
+ const root = yield* findLayerRoot(roots, options);
64
65
  const format = (yield* detector.detect(root)).name;
65
66
  const lockfilePath = path.join(root, filenameFor(format));
66
67
  const content = yield* fs.readFileString(lockfilePath).pipe(Effect.mapError((cause) => new LockfileReadError({
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
2
3
  import { inlineCatalogs, merge, normalize, rangeOf } from "./internal/catalogs.js";
3
4
  import { ConfigDependencyHooks, NoPeerDependencyRules } from "./ConfigDependencyHooks.js";
@@ -342,7 +343,7 @@ var WorkspaceCatalogs = class WorkspaceCatalogs extends Context.Service()("@effe
342
343
  }));
343
344
  }));
344
345
  const assemble = Effect.gen(function* () {
345
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
346
+ const root = yield* findLayerRoot(roots, options);
346
347
  const lockfileOutputs = yield* lockfiles.read().pipe(Effect.map((lockfile) => ({
347
348
  catalogs: CatalogSet.fromLockfile(lockfile),
348
349
  importerVersions: importerVersionsOf(lockfile)
@@ -1,4 +1,5 @@
1
1
  import { enumerate } from "./internal/enumerate.js";
2
+ import { findLayerRoot } from "./internal/layerRoot.js";
2
3
  import { readPatterns } from "./internal/patterns.js";
3
4
  import { WorkspacePackage } from "./WorkspacePackage.js";
4
5
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
@@ -256,8 +257,7 @@ var WorkspaceDiscovery = class WorkspaceDiscovery extends Context.Service()("@ef
256
257
  };
257
258
  }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
258
259
  const discover = Effect.gen(function* () {
259
- const stopAt = options?.stopAt;
260
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd(), stopAt === void 0 ? void 0 : { stopAt }));
260
+ const root = yield* findLayerRoot(roots, options);
261
261
  return yield* discoverAt(root);
262
262
  });
263
263
  const [resolveOnce, invalidate] = yield* Effect.cachedInvalidateWithTTL(discover, Duration.infinity);
@@ -1,3 +1,4 @@
1
+ import { findLayerRoot } from "./internal/layerRoot.js";
1
2
  import { manifestPatternsOf, pnpmPatternsOf } from "./internal/patterns.js";
2
3
  import { WorkspaceRoot } from "./WorkspaceRoot.js";
3
4
  import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
@@ -210,7 +211,7 @@ var WorkspaceSnapshots = class WorkspaceSnapshots extends Context.Service()("@ef
210
211
  const atCaches = /* @__PURE__ */ new Map();
211
212
  return {
212
213
  at: Effect.fn("WorkspaceSnapshots.at")(function* (ref) {
213
- const root = yield* Effect.suspend(() => roots.find(options?.cwd ?? process.cwd()));
214
+ const root = yield* findLayerRoot(roots, options);
214
215
  const key = `${root}\0${ref}`;
215
216
  let memo = atCaches.get(key);
216
217
  if (memo === void 0) {
package/Workspaces.js CHANGED
@@ -48,7 +48,8 @@ const localExecLayer = (options) => Layer.effect(LocalExec, Effect.gen(function*
48
48
  const detector = yield* PackageManagerDetector;
49
49
  return { context: Effect.gen(function* () {
50
50
  const cwd = options?.cwd ?? globalThis.process?.cwd?.() ?? "/";
51
- const root = yield* roots.find(cwd).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
51
+ const stopAt = options?.stopAt;
52
+ const root = yield* roots.find(cwd, stopAt === void 0 ? void 0 : { stopAt }).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
52
53
  if (Option.isNone(root)) return Option.none();
53
54
  const detected = yield* detector.detect(root.value).pipe(Effect.asSome, Effect.catchTag("PackageManagerDetectionError", () => Effect.succeed(Option.none())), Effect.mapError((cause) => new LocalExecError({
54
55
  directory: root.value,
@@ -289,7 +290,10 @@ var Workspaces = class {
289
290
  * verbatim.
290
291
  *
291
292
  * `directory` is the resolved **workspace root**, not the caller's cwd: a
292
- * project-local launcher has to run where the workspace is.
293
+ * project-local launcher has to run where the workspace is. `stopAt` caps
294
+ * that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
295
+ * ceiling with no root at or below it is the same `Option.none()` as no
296
+ * root at all.
293
297
  *
294
298
  * A consumer with no monorepo never needs this layer, and therefore never
295
299
  * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
package/index.d.ts CHANGED
@@ -560,6 +560,11 @@ interface WorkspaceDiscoveryOptions {
560
560
  * directory the caller names, which a single layer-level ceiling cannot
561
561
  * sensibly bound, so they ascend unbounded as before.
562
562
  *
563
+ * `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
564
+ * take the same option; give them the same
565
+ * value when wiring by hand, or let a `Workspaces.*` composite forward one
566
+ * `stopAt` to all of them, so no service adopts a root another refused.
567
+ *
563
568
  * @defaultValue no ceiling — the ascent runs to the filesystem root.
564
569
  */
565
570
  readonly stopAt?: string | undefined;
@@ -1252,7 +1257,19 @@ export declare class ConfigDependencyHooks extends ConfigDependencyHooks_base {
1252
1257
  * with `reason: "integrityUnavailable"` — nothing is fetched in either case
1253
1258
  * — and a fetch that fails with `reason: "fetchFailed"`, keeping the
1254
1259
  * not-installed remediation. The fetch needs `pnpm` 11 or 12 on `PATH` and
1255
- * registry access, and is bounded at two minutes. An empty `configDependencies`, or one whose dependencies all
1260
+ * registry access, and is bounded at two minutes.
1261
+ *
1262
+ * The scratch workspace fetches through the same registries a
1263
+ * `pnpm install` in the workspace would: the workspace root's `.npmrc` (scoped
1264
+ * registries, mirrors, auth) is copied into it as-is, with `${VAR}`
1265
+ * references left for pnpm to expand, never read or logged, and removed
1266
+ * with the scratch; and the root `pnpm-workspace.yaml`'s `registry` and
1267
+ * `registries` keys are carried into the scratch's. That is the CURRENT
1268
+ * checkout's registry config for both sides of a diff — a base ref's
1269
+ * `.npmrc` is not read through git — and relative paths inside the `.npmrc`
1270
+ * (a `cafile=./ca.pem`) resolve against the scratch, not the root.
1271
+ *
1272
+ * An empty `configDependencies`, or one whose dependencies all
1256
1273
  * ship no pnpmfile, returns the seed without spawning anything; a `..` path
1257
1274
  * segment in a dependency name fails typed **before** any spawn; a
1258
1275
  * load failure in the child — a syntax error, a throwing top level, an
@@ -2054,6 +2071,21 @@ interface LockfileReaderOptions {
2054
2071
  * @defaultValue `process.cwd()`, read lazily on first use.
2055
2072
  */
2056
2073
  readonly cwd?: string;
2074
+ /**
2075
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
2076
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
2077
+ *
2078
+ * @remarks
2079
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
2080
+ * root is found at or below the ceiling, `read` fails with
2081
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
2082
+ * than adopting an enclosing directory's workspace. Pass the same value as
2083
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
2084
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
2085
+ *
2086
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
2087
+ */
2088
+ readonly stopAt?: string | undefined;
2057
2089
  }
2058
2090
  declare const LockfileReader_base: Context.ServiceClass<LockfileReader, "@effected/workspaces/LockfileReader", LockfileReaderShape>;
2059
2091
  /**
@@ -3131,6 +3163,21 @@ interface WorkspaceCatalogsOptions {
3131
3163
  * @defaultValue `process.cwd()`, read lazily on first use.
3132
3164
  */
3133
3165
  readonly cwd?: string;
3166
+ /**
3167
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
3168
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
3169
+ *
3170
+ * @remarks
3171
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3172
+ * root is found at or below the ceiling, catalog assembly fails with
3173
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3174
+ * than adopting an enclosing directory's workspace. Pass the same value as
3175
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3176
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3177
+ *
3178
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3179
+ */
3180
+ readonly stopAt?: string | undefined;
3134
3181
  }
3135
3182
  declare const WorkspaceCatalogs_base: Context.ServiceClass<WorkspaceCatalogs, "@effected/workspaces/WorkspaceCatalogs", WorkspaceCatalogsShape>;
3136
3183
  /**
@@ -3684,6 +3731,21 @@ interface WorkspaceSnapshotsOptions {
3684
3731
  * first call is honoured.
3685
3732
  */
3686
3733
  readonly cwd?: string;
3734
+ /**
3735
+ * A ceiling for the root ascent from `cwd`, passed straight through to the
3736
+ * `stopAt` of {@link WorkspaceRoot}'s `find`.
3737
+ *
3738
+ * @remarks
3739
+ * Inclusive, and resolved to an absolute path exactly as `cwd` is. When no
3740
+ * root is found at or below the ceiling, `at(ref)` fails with
3741
+ * {@link WorkspaceRootNotFoundError} carrying the resolved `stopAt` rather
3742
+ * than adopting an enclosing directory's workspace. Pass the same value as
3743
+ * {@link WorkspaceDiscoveryOptions.stopAt} so every service agrees on the
3744
+ * root; the `Workspaces.*` composites forward one `stopAt` to all of them.
3745
+ *
3746
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3747
+ */
3748
+ readonly stopAt?: string | undefined;
3687
3749
  /**
3688
3750
  * Catalogs every snapshot this service produces carries as its
3689
3751
  * `seededCatalogs` — consulted only where the snapshot's own catalogs cannot
@@ -3851,9 +3913,17 @@ export declare class WorkspaceSnapshots extends WorkspaceSnapshots_base {
3851
3913
  /**
3852
3914
  * Options shared by the composite layers.
3853
3915
  *
3916
+ * @remarks
3917
+ * Derived from the option shapes of the services a composite builds —
3918
+ * {@link WorkspaceDiscoveryOptions}, {@link LockfileReaderOptions} and
3919
+ * {@link WorkspaceCatalogsOptions} — and handed to each of them unchanged, so a
3920
+ * root-resolving option one service grows cannot silently stop at the
3921
+ * composite. `cwd` and `stopAt` are re-declared here only to document them as
3922
+ * the one concern they are across the composite.
3923
+ *
3854
3924
  * @public
3855
3925
  */
3856
- interface WorkspacesOptions {
3926
+ interface WorkspacesOptions extends WorkspaceDiscoveryOptions, LockfileReaderOptions, WorkspaceCatalogsOptions {
3857
3927
  /**
3858
3928
  * The directory every root-consuming service resolves the workspace root
3859
3929
  * from — one explicit concern, applied uniformly.
@@ -3861,8 +3931,23 @@ interface WorkspacesOptions {
3861
3931
  * @defaultValue `process.cwd()`, read lazily on first use.
3862
3932
  */
3863
3933
  readonly cwd?: string;
3864
- /** Descent cap for segment-crossing `packages:` patterns. Defaults to 32. */
3865
- readonly maxDepth?: number;
3934
+ /**
3935
+ * A ceiling for the root ascent from `cwd`, forwarded to EVERY
3936
+ * root-resolving service the composite builds — {@link WorkspaceDiscovery},
3937
+ * {@link LockfileReader}, {@link WorkspaceCatalogs} and, on the git
3938
+ * composites, {@link WorkspaceSnapshots}.
3939
+ *
3940
+ * @remarks
3941
+ * Inclusive and resolved to an absolute path, as {@link WorkspaceRoot}'s
3942
+ * `find` does. Pass `stopAt: cwd` for a checkout nested inside someone
3943
+ * else's workspace: every service then fails with
3944
+ * {@link WorkspaceRootNotFoundError} consistently, instead of discovery
3945
+ * refusing the enclosing workspace while the lockfile and catalog reads
3946
+ * adopt it. A checkout that is itself a workspace root still resolves.
3947
+ *
3948
+ * @defaultValue no ceiling — the ascent runs to the filesystem root.
3949
+ */
3950
+ readonly stopAt?: string | undefined;
3866
3951
  }
3867
3952
  /**
3868
3953
  * Options for the composites that additionally provide {@link WorkspaceSnapshots}
@@ -4108,7 +4193,10 @@ export declare class Workspaces {
4108
4193
  * verbatim.
4109
4194
  *
4110
4195
  * `directory` is the resolved **workspace root**, not the caller's cwd: a
4111
- * project-local launcher has to run where the workspace is.
4196
+ * project-local launcher has to run where the workspace is. `stopAt` caps
4197
+ * that ascent exactly as {@link WorkspacesOptions.stopAt} does, and a
4198
+ * ceiling with no root at or below it is the same `Option.none()` as no
4199
+ * root at all.
4112
4200
  *
4113
4201
  * A consumer with no monorepo never needs this layer, and therefore never
4114
4202
  * installs this package — `LocalExec.layerNone` and `LocalExec.layerFor`
@@ -4137,6 +4225,7 @@ export declare class Workspaces {
4137
4225
  */
4138
4226
  static readonly localExecLayer: (options?: {
4139
4227
  readonly cwd?: string;
4228
+ readonly stopAt?: string | undefined;
4140
4229
  }) => Layer.Layer<LocalExec, never, PackageManagerDetector | WorkspaceRoot>;
4141
4230
  /**
4142
4231
  * Resolve every `catalog:` and `workspace:` specifier in one `Manifest`
@@ -1,10 +1,11 @@
1
1
  import { carries, manifestVersion, messageOf, sideLabel } from "./configDependencyShared.js";
2
2
  import { splitConfigDependencySpec } from "./configDependencySpecGrammar.js";
3
- import { Duration, Effect } from "effect";
3
+ import { Duration, Effect, Predicate } from "effect";
4
4
  import { IntegrityHash } from "@effected/npm";
5
+ import { Yaml } from "@effected/yaml";
5
6
  import { Run } from "@effected/commands";
6
7
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
7
- import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
8
+ import { copyFile, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
9
10
  import { basename, dirname, join } from "node:path";
10
11
 
@@ -47,12 +48,36 @@ const expectedIntegrity = (request) => Effect.gen(function* () {
47
48
  if (integrity === void 0) return yield* Effect.fail(fetchFailure("integrityUnavailable", `config dependency ${key} must be fetched, but ${sideLabel(side)} records no integrity for it (no inline integrity in pnpm-workspace.yaml and no pnpm-lock.yaml entry for that version), so a fetched copy could not be verified and nothing was fetched.`));
48
49
  return integrity;
49
50
  });
51
+ /** The {@link RegistrySettings} of a parsed `pnpm-workspace.yaml` document. */
52
+ const registrySettingsOf = (document) => {
53
+ if (!Predicate.isObject(document)) return { registries: {} };
54
+ const registries = {};
55
+ if (Predicate.isObject(document.registries)) {
56
+ for (const [scope, url] of Object.entries(document.registries)) if (typeof url === "string") registries[scope] = url;
57
+ }
58
+ return typeof document.registry === "string" ? {
59
+ registry: document.registry,
60
+ registries
61
+ } : { registries };
62
+ };
50
63
  /**
51
- * The scratch workspace's `pnpm-workspace.yaml`. Every scalar is a
64
+ * The scratch workspace's `pnpm-workspace.yaml`: the one config dependency,
65
+ * plus the declaring workspace's registry settings. Every key and scalar is a
52
66
  * JSON-quoted string, which YAML reads verbatim, so no declared text can
53
67
  * change the document's structure.
54
68
  */
55
- const scratchWorkspaceYaml = (name, version) => `configDependencies:\n ${JSON.stringify(name)}: ${JSON.stringify(version)}\n`;
69
+ const scratchWorkspaceYaml = (name, version, settings = { registries: {} }) => {
70
+ const lines = ["configDependencies:", ` ${JSON.stringify(name)}: ${JSON.stringify(version)}`];
71
+ if (settings.registry !== void 0) lines.push(`registry: ${JSON.stringify(settings.registry)}`);
72
+ const scopes = Object.entries(settings.registries);
73
+ if (scopes.length > 0) {
74
+ lines.push("registries:");
75
+ for (const [scope, url] of scopes) lines.push(` ${JSON.stringify(scope)}: ${JSON.stringify(url)}`);
76
+ }
77
+ return `${lines.join("\n")}\n`;
78
+ };
79
+ /** Whether `error` is a node filesystem "no such file" failure. */
80
+ const isNotFound = (error) => Predicate.isObject(error) && error.code === "ENOENT";
56
81
  /**
57
82
  * The scratch workspace's `pnpm-lock.yaml`: the env preamble pnpm 11 and 12
58
83
  * write for a lone config dependency, pinned to `integrity`, followed by the
@@ -123,14 +148,22 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
123
148
  catch: (cause) => failed(`${what} failed: ${messageOf(cause)}`, cause)
124
149
  });
125
150
  const integrity = yield* expectedIntegrity(request);
151
+ const rootYaml = yield* io("reading the workspace pnpm-workspace.yaml", () => readFile(join(request.root, "pnpm-workspace.yaml"), "utf8").catch((error) => {
152
+ if (isNotFound(error)) return void 0;
153
+ throw error;
154
+ }));
155
+ const settings = rootYaml === void 0 ? registrySettingsOf(void 0) : registrySettingsOf(yield* Yaml.parse(rootYaml).pipe(Effect.mapError((cause) => failed(`reading the workspace pnpm-workspace.yaml failed: ${messageOf(cause)}`, cause))));
126
156
  const scratch = yield* Effect.acquireRelease(io("creating a scratch workspace", () => mkdtemp(join(tmpdir(), "effected-config-dependency-"))), (dir) => Effect.promise(() => rm(dir, {
127
157
  recursive: true,
128
158
  force: true
129
159
  }).catch(() => void 0)));
130
160
  const lockfile = scratchLockfile(name, version, integrity);
131
161
  yield* io("writing the scratch workspace", async () => {
132
- await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version));
162
+ await writeFile(join(scratch, "pnpm-workspace.yaml"), scratchWorkspaceYaml(name, version, settings));
133
163
  await writeFile(join(scratch, "pnpm-lock.yaml"), lockfile);
164
+ await copyFile(join(request.root, ".npmrc"), join(scratch, ".npmrc")).catch((error) => {
165
+ if (!isNotFound(error)) throw error;
166
+ });
134
167
  });
135
168
  const command = ChildProcess.make("pnpm", fetchArgs(scratch, request.stores), { cwd: scratch });
136
169
  yield* Run.text(command, { timeout: FETCH_TIMEOUT }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.mapError((cause) => failed(`fetching ${key} with \`pnpm ${fetchArgs("<scratch>", request.stores).join(" ")}\` failed: ${messageOf(cause)}`, cause)));
@@ -143,4 +176,4 @@ const makeFetchConfigDependency = (spawner) => (request) => Effect.scoped(Effect
143
176
  }));
144
177
 
145
178
  //#endregion
146
- export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, storeDirArgument };
179
+ export { expectedIntegrity, fetchArgs, makeFetchConfigDependency, registrySettingsOf, scratchWorkspaceYaml, storeDirArgument };
@@ -266,6 +266,7 @@ const resolveDirectory = (root, entry, stores, locks, options) => Effect.gen(fun
266
266
  if (options.fetch === void 0) return yield* Effect.fail(notInstalled(Option.none()));
267
267
  return {
268
268
  dir: yield* options.fetch({
269
+ root,
269
270
  name,
270
271
  version: declared,
271
272
  spec: entry.spec,
@@ -0,0 +1,14 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/layerRoot.ts
4
+ /**
5
+ * Resolve the workspace root a layer's options name. `Effect.suspend` so the
6
+ * ambient `process.cwd()` is read at first use, not at layer construction.
7
+ */
8
+ const findLayerRoot = (roots, options) => Effect.suspend(() => {
9
+ const stopAt = options?.stopAt;
10
+ return roots.find(options?.cwd ?? process.cwd(), stopAt === void 0 ? void 0 : { stopAt });
11
+ });
12
+
13
+ //#endregion
14
+ export { findLayerRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/workspaces",
3
- "version": "0.28.0-next-20260927071821",
3
+ "version": "0.28.0",
4
4
  "private": false,
5
5
  "description": "Monorepo workspace tooling as Effect services — root discovery, package enumeration, the dependency graph, package-manager detection, pnpm catalog resolution, lockfile IO and git-based change detection.",
6
6
  "keywords": [
@@ -55,9 +55,9 @@
55
55
  "@effected/git": "^0.17.0",
56
56
  "@effected/glob": "^0.8.0",
57
57
  "@effected/jsonc": "^0.13.0",
58
- "@effected/lockfiles": "^0.12.0-next-20260927071821",
59
- "@effected/npm": "^0.17.0-next-20260927071821",
60
- "@effected/package-json": "^0.18.0-next-20260927071821",
58
+ "@effected/lockfiles": "^0.12.0",
59
+ "@effected/npm": "^0.17.0",
60
+ "@effected/package-json": "^0.18.0",
61
61
  "@effected/semver": "^0.9.0",
62
62
  "@effected/walker": "^0.12.0",
63
63
  "@effected/yaml": "^0.17.0",
package/testing.d.ts CHANGED
@@ -652,6 +652,11 @@ interface WorkspaceDiscoveryOptions {
652
652
  * directory the caller names, which a single layer-level ceiling cannot
653
653
  * sensibly bound, so they ascend unbounded as before.
654
654
  *
655
+ * `LockfileReader`, `WorkspaceCatalogs` and `WorkspaceSnapshots` each
656
+ * take the same option; give them the same
657
+ * value when wiring by hand, or let a `Workspaces.*` composite forward one
658
+ * `stopAt` to all of them, so no service adopts a root another refused.
659
+ *
655
660
  * @defaultValue no ceiling — the ascent runs to the filesystem root.
656
661
  */
657
662
  readonly stopAt?: string | undefined;