@effected/workspaces 0.26.0 → 0.28.0-next-20260927062352

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/testing.d.ts CHANGED
@@ -3,6 +3,8 @@ import { Context, Duration, Effect, FileSystem, Layer, Option, Path, Schema } fr
3
3
  import { GlobPattern } from "@effected/glob";
4
4
  import { WorkspaceManifest } from "@effected/lockfiles";
5
5
  import { Package } from "@effected/package-json";
6
+ import { CommandOutput } from "@effected/commands";
7
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
6
8
  //#region src/LayerPolicy.d.ts
7
9
  declare const LayerPolicyError_base: Schema.Class<LayerPolicyError, Schema.TaggedStruct<"LayerPolicyError", {
8
10
  /** `read`: the file could not be read. `json`: it is not JSON. `decode`: it is not a `LayerPolicy`. */
@@ -883,12 +885,66 @@ export declare class WorkspaceDiscovery extends WorkspaceDiscovery_base {
883
885
  type PackSource = "source" | {
884
886
  readonly directory: string;
885
887
  };
888
+ /**
889
+ * What decides the set of packages a packed install packs: the options
890
+ * {@link PackedInstall.closure} and {@link PackedInstall.run} share.
891
+ *
892
+ * @remarks
893
+ * The run's options satisfy this interface, so
894
+ * `PackedInstall.closure(options.carrier, options)` answers for exactly the
895
+ * run those options describe.
896
+ *
897
+ * @public
898
+ */
899
+ interface PackedInstallClosureOptions {
900
+ /**
901
+ * The other workspace packages to pack and override; `"auto"` is the
902
+ * carrier's transitive runtime workspace dependencies (`dependencies`,
903
+ * `optionalDependencies`, `peerDependencies`). `PackedInstall.closure`
904
+ * defaults it to `"auto"`.
905
+ */
906
+ readonly closure?: ReadonlyArray<string> | "auto" | undefined;
907
+ /**
908
+ * Packages from outside the workspace that replace registry resolution in
909
+ * every scratch consumer: package name to a local package directory or a
910
+ * `.tgz`, with or without a `file:` prefix.
911
+ *
912
+ * @remarks
913
+ * The dogfood case: a closure member imports a surface of a dependency
914
+ * that exists only in a sibling checkout's unreleased build, and the
915
+ * registry's copy lacks it. A directory is `npm pack`ed, so it must be
916
+ * publish-ready (for an effected-bundled package, its
917
+ * `dist/prod/npm/pkg`); a `.tgz` is used as it is. Either way the packed
918
+ * manifest must be named as its key, carry no specifier only a workspace
919
+ * could resolve, and it joins `PackedInstallResult.tarballs`. Each
920
+ * consumer steers the package to that tarball through the same override
921
+ * field as the closure (npm's and bun's `overrides`, Yarn's `resolutions`,
922
+ * pnpm's `overrides` in `pnpm-workspace.yaml`), so the carrier's
923
+ * transitive references resolve to it too, whatever range they ask for.
924
+ *
925
+ * A relative path resolves against the workspace root. A key naming the
926
+ * carrier or a closure member, a path that is neither a directory with a
927
+ * `package.json` nor a `.tgz` file, or a manifest named otherwise fails
928
+ * `InvalidOverride`. Entries here win over `workspaceOverrides` ones.
929
+ */
930
+ readonly overrides?: Readonly<Record<string, string>> | undefined;
931
+ /**
932
+ * Also read the workspace root's `pnpm-workspace.yaml` and take every
933
+ * `overrides:` entry of the dogfood link shape,
934
+ * `"@scope/name": "file:<dir>"`, as if passed through `overrides`: the
935
+ * linked sibling builds the workspace itself installs. Entries that are not `file:`, or
936
+ * whose key carries a selector (`a>b`, `a@1`), are left out. A root with
937
+ * no readable `pnpm-workspace.yaml`, or one that is not YAML, fails
938
+ * `InvalidOverride`.
939
+ */
940
+ readonly workspaceOverrides?: boolean | undefined;
941
+ }
886
942
  /**
887
943
  * Options for {@link PackedInstall.run}.
888
944
  *
889
945
  * @public
890
946
  */
891
- interface PackedInstallOptions {
947
+ interface PackedInstallOptions extends PackedInstallClosureOptions {
892
948
  /** The carrier: a direct dependency of each scratch consumer, beside only `consumerDependencies`. */
893
949
  readonly carrier: string;
894
950
  /** The other workspace packages to pack and override; `"auto"` is the carrier's transitive runtime workspace dependencies. */
@@ -906,8 +962,46 @@ interface PackedInstallOptions {
906
962
  * published artifact; see {@link PackSource} for what `"source"` costs.
907
963
  */
908
964
  readonly packFrom?: PackSource | undefined;
909
- /** Bin names every consumer must expose, executable, in `node_modules/.bin`. */
965
+ /**
966
+ * Bin names every consumer must expose, executable, in `node_modules/.bin`.
967
+ *
968
+ * @remarks
969
+ * By default only the carrier may declare them. Whatever this lists, a
970
+ * packed package other than the carrier that declares one of the
971
+ * carrier's own bin names fails `BinConflict` before any install, unless
972
+ * `allowSharedBins` is set: under a flat layout (npm, bun,
973
+ * Yarn's `node-modules` linker) either package can take the `.bin` slot,
974
+ * so the bin check and every bin run could pass on the wrong package.
975
+ *
976
+ * The check compares packed packages only (the carrier, the closure and
977
+ * `overrides`), reading each manifest's `bin` field; it does not read
978
+ * `directories.bin`, and a dependency installed from the registry that
979
+ * declares the same bin name goes undetected.
980
+ */
910
981
  readonly bins: ReadonlyArray<string>;
982
+ /**
983
+ * Skip the `BinConflict` check: the front ends declare the carrier's bin
984
+ * names too, deliberately.
985
+ *
986
+ * @remarks
987
+ * A supported, permanent choice for a carrier whose front ends are also
988
+ * installed and run on their own, and a stopgap for one migrating to
989
+ * carrier-only bins. Its cost is provenance: under npm, bun and Yarn's
990
+ * `node-modules` linker, either package can take the `.bin` slot (npm and
991
+ * bun were observed to link the package whose name sorts first, which for
992
+ * a `cli` front end and a `plugin` carrier is the front end; Yarn 1 and 4
993
+ * were observed to keep the carrier's, the consumer's direct dependency),
994
+ * and the bin that runs then carries no distribution identity: no
995
+ * `--version` suffix.
996
+ * When every shared bin calls the same `main()`, nothing else changes.
997
+ *
998
+ * Every expected bin in `bins` is still verified present and executable,
999
+ * but that check, and `runBin`, can pass on the front end's bin. Prove the
1000
+ * carrier's own shim with `InstalledConsumer.runCarrierBin`, and read who
1001
+ * owns the slot with `InstalledConsumer.binProvenance`. Defaults to
1002
+ * `false`, so sharing is an explicit choice.
1003
+ */
1004
+ readonly allowSharedBins?: boolean | undefined;
911
1005
  /** The environment for every spawn: pass `process.env` from the test file. The parent manager's context is stripped. */
912
1006
  readonly env: Readonly<Record<string, string | undefined>>;
913
1007
  /**
@@ -922,19 +1016,26 @@ interface PackedInstallOptions {
922
1016
  * while npm and bun hoist it and pass. That is a property of the consumer,
923
1017
  * not of the pack.
924
1018
  *
925
- * An entry naming a packed package (the carrier or a closure member) is
926
- * written as that package's `file:` tarball, whatever spec you pass, so
927
- * any range will do: the packed tarball always wins. npm fails an install
928
- * whose direct spec differs from its override (`EOVERRIDE`), so a range
929
- * written through unchanged would break every npm consumer.
1019
+ * An entry naming a packed package (the carrier, a closure member or an
1020
+ * `overrides` package) is written as that package's `file:` tarball,
1021
+ * whatever spec you pass, so any range will do: the packed tarball always
1022
+ * wins. npm fails an install whose direct spec differs from its override
1023
+ * (`EOVERRIDE`), so a range written through unchanged would break every
1024
+ * npm consumer.
930
1025
  */
931
1026
  readonly consumerDependencies?: Readonly<Record<string, string>> | undefined;
932
1027
  /** Ceiling on each install. Defaults to four minutes. Expiry fails `InstallFailed` with a message naming the manager and this duration. */
933
1028
  readonly installTimeout?: Duration.Input | undefined;
1029
+ /**
1030
+ * Ceiling on each package's pack. Defaults to two minutes. Expiry fails
1031
+ * `PackFailed` with a message naming the package and this duration. Pass
1032
+ * the same value to {@link PackedInstall.timeoutBudget}.
1033
+ */
1034
+ readonly packTimeout?: Duration.Input | undefined;
934
1035
  }
935
1036
  declare const PackedInstallError_base: Schema.Class<PackedInstallError, Schema.TaggedStruct<"PackedInstallError", {
936
1037
  /** What failed. */
937
- readonly reason: Schema.Literals<readonly ["UnsupportedPlatform", "NoManagerAvailable", "ManagerUnavailable", "UnknownPackage", "PackSourceMissing", "PackFailed", "UnresolvedProtocol", "InstallFailed", "MissingBin", "Discovery", "Io"]>;
1038
+ readonly reason: Schema.Literals<readonly ["UnsupportedPlatform", "NoManagerAvailable", "ManagerUnavailable", "UnknownPackage", "PackSourceMissing", "PackFailed", "UnresolvedProtocol", "InvalidOverride", "BinConflict", "InstallFailed", "MissingBin", "BinFailed", "UnownedBin", "Discovery", "Io"]>;
938
1039
  /** One line, naming the package or manager involved. */
939
1040
  readonly message: Schema.String;
940
1041
  /** The package manager involved. */
@@ -952,6 +1053,44 @@ declare const PackedInstallError_base: Schema.Class<PackedInstallError, Schema.T
952
1053
  * @public
953
1054
  */
954
1055
  export declare class PackedInstallError extends PackedInstallError_base {}
1056
+ /**
1057
+ * Options for {@link InstalledConsumer.command}: how the bin's environment
1058
+ * and working directory are built.
1059
+ *
1060
+ * @public
1061
+ */
1062
+ interface BinCommandOptions {
1063
+ /**
1064
+ * Variables layered over the environment the install ran under, such as an
1065
+ * `XDG_DATA_HOME` inside `PackedInstallResult.scratch`. A key set to
1066
+ * `undefined` removes that variable. They are applied after the parent
1067
+ * manager's context is stripped, so an explicit entry always wins: pass
1068
+ * `CI: "true"` to run a bin as if under CI.
1069
+ */
1070
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
1071
+ /** The working directory. Defaults to the consumer's `directory`. */
1072
+ readonly cwd?: string | undefined;
1073
+ }
1074
+ /**
1075
+ * Options for {@link InstalledConsumer.runBin}.
1076
+ *
1077
+ * @public
1078
+ */
1079
+ interface RunBinOptions extends BinCommandOptions {
1080
+ /** Ceiling on the run. Defaults to one minute. Expiry fails `BinFailed` with a message naming the bin and this duration. */
1081
+ readonly timeout?: Duration.Input | undefined;
1082
+ }
1083
+ /**
1084
+ * Which installed package a `node_modules/.bin` symlink resolves into.
1085
+ *
1086
+ * @public
1087
+ */
1088
+ interface BinProvenance {
1089
+ /** The `name` from the nearest `package.json` above the link's target. */
1090
+ readonly package: string;
1091
+ /** The link's target, realpath'd. */
1092
+ readonly target: string;
1093
+ }
955
1094
  declare const InstalledConsumer_base: Schema.Class<InstalledConsumer, Schema.Struct<{
956
1095
  /** The package manager that installed it. */
957
1096
  readonly manager: Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>;
@@ -959,6 +1098,18 @@ declare const InstalledConsumer_base: Schema.Class<InstalledConsumer, Schema.Str
959
1098
  readonly managerVersion: Schema.String;
960
1099
  /** The consumer project directory (realpath'd). */
961
1100
  readonly directory: Schema.String;
1101
+ /**
1102
+ * The scrubbed environment the install ran under, which {@link InstalledConsumer.runBin}
1103
+ * starts from. Redacted, so printing a consumer never prints a token.
1104
+ * `PackedInstall.run` always sets it; a hand-made consumer without it runs
1105
+ * its bins under only `RunBinOptions.env`.
1106
+ */
1107
+ readonly env: Schema.optionalKey<Schema.Redacted<Schema.$Record<Schema.String, Schema.String>>>;
1108
+ /**
1109
+ * The carrier the consumer depends on, which {@link InstalledConsumer.carrierCommand}
1110
+ * resolves bins through. `PackedInstall.run` always sets it.
1111
+ */
1112
+ readonly carrier: Schema.optionalKey<Schema.String>;
962
1113
  }>, {}>;
963
1114
  /**
964
1115
  * One scratch project, outside the workspace, with the carrier installed.
@@ -968,6 +1119,134 @@ declare const InstalledConsumer_base: Schema.Class<InstalledConsumer, Schema.Str
968
1119
  export declare class InstalledConsumer extends InstalledConsumer_base {
969
1120
  /** The installed bin `name`, in `node_modules/.bin`. POSIX: `PackedInstall` runs only there. */
970
1121
  binPath(name: string): string;
1122
+ /**
1123
+ * Which installed package the `node_modules/.bin` entry `name` resolves into.
1124
+ *
1125
+ * @remarks
1126
+ * Under a flat layout (npm, bun, and Yarn with the `node-modules` linker
1127
+ * this run configures), a hoisted bin of the same name from another package
1128
+ * can shadow the carrier's, and running it cannot tell you which one ran.
1129
+ * Those managers write each `.bin` entry as a symlink, so this reads the
1130
+ * link, realpaths its target, and walks up to the nearest `package.json`
1131
+ * with a string `name`, staying inside the consumer directory (itself
1132
+ * realpath'd first, so a `/var` alias of `/private/var` or a trailing
1133
+ * slash does not move the bound). A manifest that is valid JSON but not an
1134
+ * object, or has no string `name` (a `dist/package.json` carrying only
1135
+ * `type`), is passed over. Assert `package` is your carrier.
1136
+ *
1137
+ * `undefined` means one thing: the entry exists and is not a symlink.
1138
+ * pnpm writes `.bin` entries as shell shims, whose target this does not
1139
+ * parse; its isolated layout links only the consumer's direct dependencies
1140
+ * at the top level, so a shadowing bin needs a direct dependency there.
1141
+ *
1142
+ * An entry that does not exist, or a link whose target does not, fails
1143
+ * `MissingBin`. A link into no named package inside the consumer fails
1144
+ * `UnownedBin`, naming the target. Any other read failure, a
1145
+ * `package.json` that is not JSON included, fails `Io`.
1146
+ *
1147
+ * @param name - The bin, as named in `node_modules/.bin`.
1148
+ */
1149
+ binProvenance(name: string): Effect.Effect<BinProvenance | undefined, PackedInstallError, FileSystem.FileSystem | Path.Path>;
1150
+ /**
1151
+ * The command {@link InstalledConsumer.runBin} spawns for the bin `name`,
1152
+ * for a caller that drives the child itself, such as `McpProbe.initialize`
1153
+ * from `@effected/mcp/testing`.
1154
+ *
1155
+ * @remarks
1156
+ * {@link InstalledConsumer.binPath}, from the consumer's directory, under
1157
+ * the install's scrubbed environment with `options.env` layered over it
1158
+ * after the scrub, and nothing inherited beyond that. `runBin` builds its
1159
+ * command here and then ignores stdin; this one leaves stdin as the
1160
+ * spawner's default pipe, so a probe can write to it. Spawn it inside the
1161
+ * scope that ran `PackedInstall.run`: the scratch directory is removed
1162
+ * when that scope closes.
1163
+ *
1164
+ * @example
1165
+ * ```ts
1166
+ * import { McpProbe } from "@effected/mcp/testing";
1167
+ * import type { InstalledConsumer } from "@effected/workspaces/testing";
1168
+ *
1169
+ * declare const consumer: InstalledConsumer;
1170
+ * declare const xdg: string;
1171
+ *
1172
+ * const probe = McpProbe.initialize(consumer.command("my-tool-mcp", [], { env: { XDG_DATA_HOME: xdg } }));
1173
+ * ```
1174
+ *
1175
+ * @param name - The bin, as named in `node_modules/.bin`.
1176
+ * @param args - Its arguments.
1177
+ * @param options - Extra environment and working directory.
1178
+ */
1179
+ command(name: string, args?: ReadonlyArray<string>, options?: BinCommandOptions): ChildProcess.StandardCommand;
1180
+ /**
1181
+ * Run the installed bin `name` to completion and collect what it wrote.
1182
+ *
1183
+ * @remarks
1184
+ * Spawns {@link InstalledConsumer.command} with stdin ignored: the bin
1185
+ * from the consumer's directory, under the install's scrubbed environment
1186
+ * with `options.env` layered over it after the scrub, and nothing
1187
+ * inherited beyond that. A
1188
+ * non-zero exit is a result, read from `exitCode`, never a failure. A bin
1189
+ * that cannot spawn, outlives `options.timeout` or floods its output fails
1190
+ * `BinFailed` naming the manager and the bin. Run it inside the scope that
1191
+ * ran `PackedInstall.run`: the scratch directory is removed when that
1192
+ * scope closes.
1193
+ *
1194
+ * @param name - The bin, as named in `node_modules/.bin`.
1195
+ * @param args - Its arguments.
1196
+ * @param options - Extra environment, working directory and ceiling.
1197
+ */
1198
+ runBin(name: string, args?: ReadonlyArray<string>, options?: RunBinOptions): Effect.Effect<CommandOutput, PackedInstallError, ChildProcessSpawner.ChildProcessSpawner>;
1199
+ /**
1200
+ * The command that runs the CARRIER's own bin `name`, whichever package
1201
+ * won the `node_modules/.bin` slot.
1202
+ *
1203
+ * @remarks
1204
+ * With `allowSharedBins`, a flat layout (npm, bun, Yarn's `node-modules`
1205
+ * linker) can link a front end's same-named bin into `.bin`, so
1206
+ * {@link InstalledConsumer.runBin} proves only that SOME package's bin runs.
1207
+ * This reads the carrier's installed `node_modules/<carrier>/package.json`,
1208
+ * takes `name` from its `bin` map (a `bin` string answers to the unscoped
1209
+ * package name), and returns `node <that file> ...args`: the carrier's shim
1210
+ * itself, run by the `node` on the environment's `PATH`.
1211
+ *
1212
+ * Running it through `node` has three limits. It assumes a Node script, so
1213
+ * a non-Node shim misruns. It drops any flags in the shim's shebang (a
1214
+ * `#!/usr/bin/env -S node --enable-source-maps` runs without them). And it
1215
+ * bypasses the file's executable bit, so under shared bins it does not
1216
+ * prove the carrier's shim is executable, only that it runs: the manager
1217
+ * that linked a front end's bin into the slot may never have made the
1218
+ * carrier's target executable. Environment and working directory are built as
1219
+ * {@link InstalledConsumer.command} builds them, stdin left open for a probe
1220
+ * such as `McpProbe.initialize`.
1221
+ *
1222
+ * A consumer with no `carrier`, a carrier that is not installed or does not
1223
+ * declare `name`, or a declared file that does not exist fails
1224
+ * `MissingBin`; a manifest that cannot be read or is not JSON fails `Io`.
1225
+ *
1226
+ * @param name - The bin, as the carrier's `bin` map names it.
1227
+ * @param args - Its arguments.
1228
+ * @param options - Extra environment and working directory.
1229
+ */
1230
+ carrierCommand(name: string, args?: ReadonlyArray<string>, options?: BinCommandOptions): Effect.Effect<ChildProcess.StandardCommand, PackedInstallError, FileSystem.FileSystem | Path.Path>;
1231
+ /**
1232
+ * Run the carrier's own bin `name` to completion, whichever package won
1233
+ * the `node_modules/.bin` slot.
1234
+ *
1235
+ * @remarks
1236
+ * Spawns {@link InstalledConsumer.carrierCommand} with stdin ignored, and
1237
+ * reports as {@link InstalledConsumer.runBin} does: a non-zero exit is a
1238
+ * result, and a bin that cannot spawn or outlives `options.timeout` (one
1239
+ * minute by default) fails `BinFailed`. Use it beside `runBin` under
1240
+ * `allowSharedBins`: `runBin` proves what a user typing the bin name gets,
1241
+ * this proves the carrier's shim itself works, within `carrierCommand`'s
1242
+ * limits (a Node script, shebang flags dropped, executable bit not
1243
+ * checked).
1244
+ *
1245
+ * @param name - The bin, as the carrier's `bin` map names it.
1246
+ * @param args - Its arguments.
1247
+ * @param options - Extra environment, working directory and ceiling.
1248
+ */
1249
+ runCarrierBin(name: string, args?: ReadonlyArray<string>, options?: RunBinOptions): Effect.Effect<CommandOutput, PackedInstallError, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
971
1250
  }
972
1251
  declare const PackedInstallResult_base: Schema.Class<PackedInstallResult, Schema.Struct<{
973
1252
  /** One consumer per available manager, in the order requested. */
@@ -976,6 +1255,13 @@ declare const PackedInstallResult_base: Schema.Class<PackedInstallResult, Schema
976
1255
  readonly unavailable: Schema.$Array<Schema.Literals<readonly ["npm", "pnpm", "yarn", "bun"]>>;
977
1256
  /** Every packed package: name to absolute tarball path. */
978
1257
  readonly tarballs: Schema.$Record<Schema.String, Schema.String>;
1258
+ /**
1259
+ * The scratch root (realpath'd) holding the tarballs and every consumer.
1260
+ * It is removed when the scope that ran `PackedInstall.run` closes, so a
1261
+ * directory made under it, such as an `XDG_DATA_HOME` for the bins, is
1262
+ * cleaned up with it.
1263
+ */
1264
+ readonly scratch: Schema.String;
979
1265
  }>, {}>;
980
1266
  /**
981
1267
  * What a packed install produced.
@@ -983,6 +1269,32 @@ declare const PackedInstallResult_base: Schema.Class<PackedInstallResult, Schema
983
1269
  * @public
984
1270
  */
985
1271
  export declare class PackedInstallResult extends PackedInstallResult_base {}
1272
+ /**
1273
+ * What {@link PackedInstall.timeoutBudget} adds up.
1274
+ *
1275
+ * @public
1276
+ */
1277
+ interface PackedInstallBudget {
1278
+ /** The managers the run requests; one listed twice counts once. */
1279
+ readonly managers: ReadonlyArray<PackageManagerName>;
1280
+ /** The run's `installTimeout`. Defaults to four minutes, as the run does. */
1281
+ readonly installTimeout?: Duration.Input | undefined;
1282
+ /** The run's `packTimeout`. Defaults to two minutes, as the run does. */
1283
+ readonly packTimeout?: Duration.Input | undefined;
1284
+ /**
1285
+ * What the run packs: a count, or the names {@link PackedInstall.closure}
1286
+ * returns for the run's options, which are exactly the keys of
1287
+ * `PackedInstallResult.tarballs`.
1288
+ */
1289
+ readonly packages: number | ReadonlyArray<string>;
1290
+ /**
1291
+ * What the test does with each consumer afterwards, such as its bin runs'
1292
+ * ceilings. Defaults to one minute: one {@link InstalledConsumer.runBin}
1293
+ * at its default ceiling. Add a minute per further `runBin`, and pass
1294
+ * `"0 seconds"` for a test that only installs.
1295
+ */
1296
+ readonly perConsumer?: Duration.Input | undefined;
1297
+ }
986
1298
  /**
987
1299
  * Prove a carrier's bins reach a consumer that is not part of the workspace,
988
1300
  * once per available package manager.
@@ -994,8 +1306,8 @@ export declare class PackedInstallResult extends PackedInstallResult_base {}
994
1306
  * pin in the repo cannot refuse it), it writes a consumer whose only direct
995
1307
  * dependency is the carrier tarball, steers the closure to its tarballs
996
1308
  * through the manager's own override field, pins the probed version (as
997
- * `packageManager`, or for pnpm as `devEngines.packageManager` with `onFail:
998
- * "ignore"`, since pnpm resolves a `packageManager` pin from the registry even
1309
+ * `packageManager`, or for pnpm as `devEngines.packageManager` with
1310
+ * `onFail: "ignore"`, since pnpm resolves a `packageManager` pin from the registry even
999
1311
  * when it names the running version), installs with lifecycle scripts skipped,
1000
1312
  * and checks every
1001
1313
  * expected bin is present and executable. A packed manifest that still
@@ -1003,18 +1315,18 @@ export declare class PackedInstallResult extends PackedInstallResult_base {}
1003
1315
  * fails `UnresolvedProtocol` before any install.
1004
1316
  *
1005
1317
  * It asserts nothing about what the bins DO: run them from the test through
1006
- * `InstalledConsumer.binPath`, for an MCP bin with `McpProbe` from
1007
- * `@effected/mcp/testing`, inside the same scope, because the scratch
1008
- * directory is removed when the scope closes. POSIX only.
1318
+ * `InstalledConsumer.runBin`, or for an MCP bin through `McpProbe` from
1319
+ * `@effected/mcp/testing` at `InstalledConsumer.binPath`, inside the same
1320
+ * scope, because the scratch directory (`PackedInstallResult.scratch`) is
1321
+ * removed when the scope closes. Size the test's outer timeout with
1322
+ * {@link PackedInstall.timeoutBudget}. POSIX only.
1009
1323
  *
1010
1324
  * @example
1011
1325
  * ```ts
1012
1326
  * import { NodeServices } from "@effect/platform-node";
1013
- * import { Run } from "@effected/commands";
1014
1327
  * import { Workspaces } from "@effected/workspaces";
1015
1328
  * import { PackedInstall } from "@effected/workspaces/testing";
1016
1329
  * import { Effect, Layer } from "effect";
1017
- * import { ChildProcess } from "effect/unstable/process";
1018
1330
  *
1019
1331
  * const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
1020
1332
  *
@@ -1028,12 +1340,11 @@ export declare class PackedInstallResult extends PackedInstallResult_base {}
1028
1340
  * bins: ["my-tool"],
1029
1341
  * env: process.env,
1030
1342
  * });
1031
- * const env = PackedInstall.scrubEnv(process.env);
1343
+ * // Keep the tool's data inside the scratch root, removed with it.
1344
+ * const env = { XDG_DATA_HOME: `${result.scratch}/xdg` };
1032
1345
  * for (const consumer of result.consumers) {
1033
- * const version = yield* Run.text(
1034
- * ChildProcess.make(consumer.binPath("my-tool"), ["--version"], { env, extendEnv: false }),
1035
- * );
1036
- * console.log(consumer.manager, version.trim());
1346
+ * const { stdout, exitCode } = yield* consumer.runBin("my-tool", ["--version"], { env });
1347
+ * console.log(consumer.manager, exitCode, stdout.trim());
1037
1348
  * }
1038
1349
  * }).pipe(Effect.scoped, Effect.provide(Live));
1039
1350
  * ```
@@ -1044,8 +1355,115 @@ export declare class PackedInstall {
1044
1355
  private constructor();
1045
1356
  /** `env` without undefined values or the parent manager's context: the environment the installs run under. Reuse it to run the installed bins. */
1046
1357
  static readonly scrubEnv: (env: Readonly<Record<string, string | undefined>>) => Record<string, string>;
1358
+ /**
1359
+ * The ceiling a test's outer timeout should cover so that every one of the
1360
+ * run's own ceilings fires first, as a named `PackedInstallError`, rather
1361
+ * than the outer guard's `TimeoutError` that names nothing.
1362
+ *
1363
+ * @remarks
1364
+ * The worst case of the run's own ceilings, taken in sequence as the run
1365
+ * takes them: each manager's `--version` probe (30 seconds), install
1366
+ * (`installTimeout`) and `perConsumer` (one minute by default, one `runBin`
1367
+ * at its default ceiling), plus each package's pack
1368
+ * (`packTimeout`, two minutes by default) and manifest read (30 seconds),
1369
+ * plus 30 seconds for the untimed steps and one minute for cleanup:
1370
+ * removing the scratch root when the scope closes, and killing a child
1371
+ * whose ceiling interrupted it. It reads the same defaults the run does.
1372
+ * Pass `packages` as the names {@link PackedInstall.closure} returns to
1373
+ * size it before the run. A vitest test timeout must sit above it, since
1374
+ * vitest's own guard should not pre-empt the Effect's.
1375
+ *
1376
+ * @example
1377
+ * ```ts
1378
+ * import { PackedInstall } from "@effected/workspaces/testing";
1379
+ * import { Duration } from "effect";
1380
+ *
1381
+ * const budget = PackedInstall.timeoutBudget({
1382
+ * managers: ["npm", "pnpm", "yarn", "bun"],
1383
+ * installTimeout: "3 minutes",
1384
+ * packTimeout: "30 seconds",
1385
+ * packages: ["my-tool", "my-tool-cli", "my-tool-core"],
1386
+ * perConsumer: "2 minutes",
1387
+ * });
1388
+ * console.log(Duration.format(budget));
1389
+ * // => 26m 30s
1390
+ * ```
1391
+ */
1392
+ static readonly timeoutBudget: (budget: PackedInstallBudget) => Duration.Duration;
1393
+ /**
1394
+ * The packages a run packs, in the order it packs them, without packing
1395
+ * anything: the carrier, the rest of the closure, then the `overrides`
1396
+ * packages by name.
1397
+ *
1398
+ * @remarks
1399
+ * The same implementation {@link PackedInstall.run} plans with, so for the
1400
+ * same workspace and options the result equals the keys of
1401
+ * `PackedInstallResult.tarballs`, in order. `options.closure` defaults to
1402
+ * `"auto"`, and the run's own options object is accepted as it is. It
1403
+ * fails as the run would before packing: `UnknownPackage`, `Discovery`,
1404
+ * `InvalidOverride` or `Io`. Use it to size
1405
+ * {@link PackedInstall.timeoutBudget} before the test is declared.
1406
+ *
1407
+ * @example
1408
+ * ```ts
1409
+ * import { NodeServices } from "@effect/platform-node";
1410
+ * import { Workspaces } from "@effected/workspaces";
1411
+ * import { PackedInstall } from "@effected/workspaces/testing";
1412
+ * import { Effect, Layer } from "effect";
1413
+ *
1414
+ * const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
1415
+ * const packages = await Effect.runPromise(PackedInstall.closure("my-tool").pipe(Effect.provide(Live)));
1416
+ * const budget = PackedInstall.timeoutBudget({ managers: ["npm", "pnpm"], packages });
1417
+ * ```
1418
+ *
1419
+ * @param carrier - The carrier, as the run's `carrier`.
1420
+ * @param options - The closure and override options, as the run takes them.
1421
+ */
1422
+ static readonly closure: (carrier: string, options?: PackedInstallClosureOptions) => Effect.Effect<ReadonlyArray<string>, PackedInstallError, WorkspaceDiscovery | FileSystem.FileSystem | Path.Path>;
1423
+ /**
1424
+ * {@link PackedInstall.timeoutBudget} for exactly the run `options`
1425
+ * describes, planned from the options themselves.
1426
+ *
1427
+ * @remarks
1428
+ * Plans the closure with {@link PackedInstall.closure} (the run's own
1429
+ * planner, `overrides` and `workspaceOverrides` included) and takes
1430
+ * `managers`, `installTimeout` and `packTimeout` from the same object, so
1431
+ * nothing is written twice. `perConsumer` is the one input a run's options
1432
+ * do not carry. It is an Effect because planning reads the workspace; in a
1433
+ * vitest file, await it at module evaluation, since a test's timeout is
1434
+ * fixed when the test is declared. Fails as `closure` does.
1435
+ *
1436
+ * @example
1437
+ * ```ts
1438
+ * import { NodeServices } from "@effect/platform-node";
1439
+ * import { Workspaces } from "@effected/workspaces";
1440
+ * import type { PackedInstallOptions } from "@effected/workspaces/testing";
1441
+ * import { PackedInstall } from "@effected/workspaces/testing";
1442
+ * import { Duration, Effect, Layer } from "effect";
1443
+ *
1444
+ * const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
1445
+ * const RUN: PackedInstallOptions = {
1446
+ * carrier: "my-tool",
1447
+ * closure: "auto",
1448
+ * managers: ["npm", "pnpm"],
1449
+ * bins: ["my-tool"],
1450
+ * env: process.env,
1451
+ * packTimeout: "30 seconds",
1452
+ * };
1453
+ * const budget = await Effect.runPromise(
1454
+ * PackedInstall.timeoutBudgetFor(RUN, { perConsumer: "1 minute" }).pipe(Effect.provide(Live)),
1455
+ * );
1456
+ * const TEST_TIMEOUT_MS = Duration.toMillis(budget) + 60_000;
1457
+ * ```
1458
+ *
1459
+ * @param options - The run's options, the same object passed to `run`.
1460
+ * @param extra - What the test does with each consumer afterwards.
1461
+ */
1462
+ static readonly timeoutBudgetFor: (options: PackedInstallOptions, extra?: {
1463
+ readonly perConsumer?: Duration.Input | undefined;
1464
+ }) => Effect.Effect<Duration.Duration, PackedInstallError, WorkspaceDiscovery | FileSystem.FileSystem | Path.Path>;
1047
1465
  /** Pack, then install under every available manager. */
1048
- static readonly run: (options: PackedInstallOptions) => Effect.Effect<PackedInstallResult, PackedInstallError, import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path | import("effect/Scope").Scope | WorkspaceDiscovery>;
1466
+ static readonly run: (options: PackedInstallOptions) => Effect.Effect<PackedInstallResult, PackedInstallError, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path | import("effect/Scope").Scope | WorkspaceDiscovery>;
1049
1467
  }
1050
1468
  //#endregion
1051
1469
  //#region src/SourceBoundary.d.ts
@@ -1068,6 +1486,17 @@ export declare class PackedInstall {
1068
1486
  * core's `Console` service. `{ forbidImports }` forbids an import whose specifier
1069
1487
  * equals an entry, is a subpath of one, or starts with an entry's text before a
1070
1488
  * trailing `*` (so `"node:*"` and `"@effect/platform*"` are prefixes).
1489
+ * `{ forbidTokens }` forbids each entry's exact text wherever it appears in
1490
+ * code (see {@link SourceBoundary.check} for how a token is matched).
1491
+ *
1492
+ * To confine a token to named files rather than forbid it everywhere, forbid
1493
+ * it and waive the rule for those files through
1494
+ * {@link ScanOptions.allowRules}: the `"forbidTokens"` key names the files, and
1495
+ * `SourceScan.waived` then reports every use inside them, so a
1496
+ * confinement that no longer matches anything shows up there. The house rule
1497
+ * "`process.env.__PACKAGE_VERSION__` only in `version.ts`" is
1498
+ * `rules: [{ forbidTokens: ["process.env.__PACKAGE_VERSION__"] }]` with
1499
+ * `allowRules: { forbidTokens: ["version.ts"] }`.
1071
1500
  *
1072
1501
  * `"node:*"` matches only the `node:` spelling: a bare built-in such as `"fs"`
1073
1502
  * is not caught. To forbid both, spread Node's own list from the test file,
@@ -1080,6 +1509,8 @@ export declare class PackedInstall {
1080
1509
  */
1081
1510
  type BoundaryRule = "process" | "node:process" | "stdout-write" | "console" | "console-stdout" | {
1082
1511
  readonly forbidImports: ReadonlyArray<string>;
1512
+ } | {
1513
+ readonly forbidTokens: ReadonlyArray<string>;
1083
1514
  };
1084
1515
  /**
1085
1516
  * Options for the `process` rule.
@@ -1090,7 +1521,9 @@ interface ReferenceOptions {
1090
1521
  /**
1091
1522
  * Exact tokens exempt from the `process` rule, each starting at the
1092
1523
  * reference (`"process.env.MY_CONSTANT"`). `process.env.__PACKAGE_VERSION__`
1093
- * is always exempt: the bundler substitutes it at build time.
1524
+ * is always exempt: the bundler substitutes it at build time. To hold such a
1525
+ * token to named files, forbid it with a `{ forbidTokens }` rule as well (see
1526
+ * {@link BoundaryRule}).
1094
1527
  */
1095
1528
  readonly ignoreTokens?: ReadonlyArray<string> | undefined;
1096
1529
  }
@@ -1117,8 +1550,8 @@ declare const Offence_base: Schema.Class<Offence, Schema.Struct<{
1117
1550
  /** The 1-based column, in UTF-16 code units. */
1118
1551
  readonly column: Schema.Number;
1119
1552
  /** The rule broken. */
1120
- readonly rule: Schema.Literals<readonly ["process", "node:process", "stdout-write", "console", "console-stdout", "forbidImports"]>;
1121
- /** What matched: the identifier, the call, or the import specifier. */
1553
+ readonly rule: Schema.Literals<readonly ["process", "node:process", "stdout-write", "console", "console-stdout", "forbidImports", "forbidTokens"]>;
1554
+ /** What matched: the identifier, the call, the import specifier, or the token. */
1122
1555
  readonly detail: Schema.String;
1123
1556
  }>, {}>;
1124
1557
  /**
@@ -1132,7 +1565,8 @@ export declare class Offence extends Offence_base {
1132
1565
  }
1133
1566
  /**
1134
1567
  * The rule an {@link Offence} names: every string {@link BoundaryRule}, plus
1135
- * `"forbidImports"` for any `{ forbidImports }` rule. These are the keys of
1568
+ * `"forbidImports"` for any `{ forbidImports }` rule and `"forbidTokens"` for
1569
+ * any `{ forbidTokens }` rule. These are the keys of
1136
1570
  * {@link ScanOptions.allowRules}.
1137
1571
  *
1138
1572
  * @public
@@ -1178,8 +1612,10 @@ interface ScanOptions extends ReferenceOptions {
1178
1612
  * rule. A matching file is still checked against every other rule, and
1179
1613
  * each offence a glob waives is reported in `SourceScan.waived`
1180
1614
  * rather than dropped. The `"forbidImports"` key covers every
1181
- * `{ forbidImports }` rule. A file `allow` matches is exempt from every
1182
- * rule, so nothing in it is waived.
1615
+ * `{ forbidImports }` rule, and the `"forbidTokens"` key every
1616
+ * `{ forbidTokens }` rule, which is how a token is confined to named files.
1617
+ * A file `allow` matches is exempt from every rule, so nothing in it is
1618
+ * waived.
1183
1619
  */
1184
1620
  readonly allowRules?: { readonly [R in OffenceRule]?: ReadonlyArray<string> | undefined; } | undefined;
1185
1621
  /**
@@ -1221,6 +1657,10 @@ interface ScanOptions extends ReferenceOptions {
1221
1657
  *
1222
1658
  * - JSX text reads as code;
1223
1659
  *
1660
+ * - a variable named `yield` or `await` in a sloppy-mode script reads as the
1661
+ * keyword, so a `/` after it opens a regex (module and strict code reserve
1662
+ * both words);
1663
+ *
1224
1664
  * - `forbidImports: ["node:*"]` does not catch a bare built-in such as
1225
1665
  * `"fs"` (see {@link BoundaryRule}).
1226
1666
  *
@@ -1250,7 +1690,22 @@ export declare class SourceBoundary {
1250
1690
  static readonly importSpecifiers: (text: string) => ReadonlyArray<string>;
1251
1691
  /** Whether `text` imports the Node built-in `module`, in either its `node:` or bare form. */
1252
1692
  static readonly importsNode: (text: string, module: string) => boolean;
1253
- /** Every place `text` breaks one of `rules`, in source order, attributed to `file`. */
1693
+ /**
1694
+ * Every place `text` breaks one of `rules`, in source order, attributed to `file`.
1695
+ *
1696
+ * @remarks
1697
+ * A `{ forbidTokens }` entry matches its exact text in code only: comments,
1698
+ * strings, template text and regex bodies are blanked first, so a token that
1699
+ * itself contains a string literal never matches, and whitespace inside a
1700
+ * token must match byte for byte. A token that starts with an identifier
1701
+ * character does not match inside a longer identifier or after `#`, and one
1702
+ * that ends with one does not match when an identifier character follows.
1703
+ * A member access is still a match: `globalThis.process.env.X` contains the
1704
+ * token `process.env.X`. A token listed twice is reported once. A token with
1705
+ * no identifier character at either end (`"=>"`, `"?."`) has no edge guard,
1706
+ * so it matches inside longer punctuation, and consecutive matches of it can
1707
+ * overlap.
1708
+ */
1254
1709
  static readonly check: (file: string, text: string, rules: ReadonlyArray<BoundaryRule>, options?: ReferenceOptions) => ReadonlyArray<Offence>;
1255
1710
  /**
1256
1711
  * Check every source file under `root` against `rules`.
@@ -1378,5 +1833,5 @@ export declare class WorkspaceLayering {
1378
1833
  static readonly checkWorkspace: (policy: LayerPolicy) => Effect.Effect<LayeringReport, WorkspaceDiscoveryFailure, WorkspaceDiscovery>;
1379
1834
  }
1380
1835
  //#endregion
1381
- export type { BoundaryFixture, BoundaryRule, DependencyDiff, FindWorkspaceRootOptions, LayeringGraph, OffenceRule, PackSource, PackedInstallOptions, ReferenceOptions, ScanOptions, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceLookupFailure, WorkspaceRootShape };
1836
+ export type { BinCommandOptions, BinProvenance, BoundaryFixture, BoundaryRule, DependencyDiff, FindWorkspaceRootOptions, LayeringGraph, OffenceRule, PackSource, PackedInstallBudget, PackedInstallClosureOptions, PackedInstallOptions, ReferenceOptions, RunBinOptions, ScanOptions, WorkspaceDiscoveryFailure, WorkspaceDiscoveryOptions, WorkspaceDiscoveryShape, WorkspaceLookupFailure, WorkspaceRootShape };
1382
1837
  //# sourceMappingURL=testing.d.ts.map