@effected/kit 0.0.0-stage → 0.12.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 ADDED
@@ -0,0 +1,509 @@
1
+ import { Context, Duration, Effect, Option, Schema, Scope } from "effect";
2
+ /**
3
+ * The carrier package a tool's bins were installed through.
4
+ *
5
+ * @remarks
6
+ * A carrier (`@scope/plugin`) re-exposes its front ends' bins and passes its
7
+ * own identity down to each front end's `main`. A plain struct rather than a
8
+ * `Schema.Class`, because it travels in JSON envelopes as a plain object and
9
+ * is compared structurally.
10
+ *
11
+ * @public
12
+ */
13
+ export declare const Distribution: Schema.Struct<{
14
+ readonly name: Schema.String;
15
+ readonly version: Schema.String;
16
+ }>;
17
+ /**
18
+ * A decoded {@link (Distribution:variable)}.
19
+ *
20
+ * @public
21
+ */
22
+ export type Distribution = typeof Distribution.Type;
23
+ /**
24
+ * The `distribution` field of a machine-readable envelope: `null` when the
25
+ * front end was installed directly rather than through a carrier.
26
+ *
27
+ * @public
28
+ */
29
+ export declare const DistributionField: Schema.NullOr<Schema.Struct<{
30
+ readonly name: Schema.String;
31
+ readonly version: Schema.String;
32
+ }>>;
33
+ /**
34
+ * The carrier this run was installed through, read anywhere without
35
+ * appearing in `R`.
36
+ *
37
+ * @remarks
38
+ * A `Context.Reference`, not a `Context.Service`: it carries its own default
39
+ * (`Option.none()`), so a direct install needs no provision at all. A front
40
+ * end's `main` provides it once, at the top of the program, with
41
+ * `Effect.provideService(CurrentDistribution, Option.fromNullishOr(options.distribution))`.
42
+ *
43
+ * @public
44
+ */
45
+ export declare const CurrentDistribution: Context.Reference<Option.Option<Distribution>>;
46
+ /**
47
+ * The `via <name> <version>` suffix (prefixed with a space) a `--version`
48
+ * line or a startup log line appends, or `""` for a direct install.
49
+ *
50
+ * @public
51
+ */
52
+ export declare const distributionSuffix: (distribution: Option.Option<Distribution>) => string;
53
+ /**
54
+ * Decorates a `--version` line formatter so its line ends with
55
+ * {@link distributionSuffix}: `formatVersion(name, version)` followed by
56
+ * a space and `via <carrier> <version>` when installed through a carrier,
57
+ * unchanged for a direct install.
58
+ *
59
+ * @remarks
60
+ * Shaped around `effect/cli`'s `CliOutput.Formatter["formatVersion"]`: it
61
+ * wraps whatever formatter is in effect (a coloured default or a custom one)
62
+ * rather than replacing it. The suffix is computed once, when the decorator is
63
+ * applied, because the distribution is known at `main`. Data-last, for `pipe`.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * import { withDistributionVersion } from "@effected/kit";
68
+ * import { Option } from "effect";
69
+ *
70
+ * const formatVersion = withDistributionVersion(Option.some({ name: "@scope/plugin", version: "2.0.0" }))(
71
+ * (name, version) => `${name} ${version}`,
72
+ * );
73
+ * formatVersion("tool", "1.0.0"); // "tool 1.0.0 via @scope/plugin 2.0.0"
74
+ * ```
75
+ *
76
+ * @public
77
+ */
78
+ export declare const withDistributionVersion: (distribution: Option.Option<Distribution>) => ((formatVersion: (name: string, version: string) => string) => (name: string, version: string) => string);
79
+ /**
80
+ * Provides {@link CurrentDistribution} to `self`: `Option.some` of a carrier,
81
+ * `Option.none()` for `null` or `undefined` (a direct install).
82
+ *
83
+ * @remarks
84
+ * The one-line form of what a front end's `main` does once at the top of its
85
+ * program. Data-last, for `pipe`.
86
+ *
87
+ * @public
88
+ */
89
+ export declare const withDistribution: (distribution: Distribution | null | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
90
+ /**
91
+ * Any value as error text, without ever throwing: the one implementation every front end of a tool uses, so a CLI and
92
+ * an MCP server describe the same failure with the same words.
93
+ *
94
+ * @remarks
95
+ * Every member is pure and total. None sanitises its result: text read from a value you do not control may carry
96
+ * escape sequences, so sanitise it before printing it to a terminal. `@effected/cli-kit` publishes the same functions as
97
+ * `Fmt.describe`, `Fmt.causeMessage` and `Fmt.errorText`, which delegate here.
98
+ *
99
+ * The anti-pattern this replaces is the per-site fallback `error instanceof Error ? error.message : String(error)`:
100
+ * it misses an error from another realm and a plain `{ message }` object, reads nothing along the `cause` chain, and
101
+ * throws on a value whose `toString` throws or which has no prototype.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * import { ErrorText } from "@effected/kit"
106
+ *
107
+ * const failure = new Error("Failed to execute statement", { cause: new Error("SQLITE_BUSY: database is locked") })
108
+ * console.log(ErrorText.of(failure))
109
+ * // => "Failed to execute statement: SQLITE_BUSY: database is locked"
110
+ * console.log(ErrorText.of({ code: 7 }))
111
+ * // => "{\"code\":7}"
112
+ * ```
113
+ *
114
+ * @public
115
+ */
116
+ export declare class ErrorText {
117
+ private constructor();
118
+ /**
119
+ * Any value as text, without ever throwing.
120
+ *
121
+ * @remarks
122
+ * A string is itself; a number, boolean, bigint or symbol is `String(value)`; `undefined` and `null` are
123
+ * `"undefined"` and `"null"`. A value with a string `message` (an `Error`, one from another realm, a plain
124
+ * `{ message }`) is that message, even an empty one. Anything else is its JSON, or `String(value)` when it has none
125
+ * (a cycle, a function), or `"[unprintable value]"` when that throws too. A getter that throws is a value with no
126
+ * message.
127
+ *
128
+ * For the messages along an error's `cause` chain, use {@link ErrorText.causeMessage}; for the two combined, use
129
+ * {@link ErrorText.of}.
130
+ *
131
+ * @param value - the value
132
+ */
133
+ static readonly describe: (value: unknown) => string;
134
+ /**
135
+ * Every distinct non-empty message along a value's `cause` chain, nearest first and joined with `: `, or
136
+ * `undefined` when nothing along it has one.
137
+ *
138
+ * @remarks
139
+ * The kit's one guarded cause-message read, the one its own errors render a preserved cause with. A node's message
140
+ * is a thrown string or a non-empty string `message` property, read without `instanceof Error`, so an error from
141
+ * another realm or a plain `{ message }` object counts. A deeper message the joined text already contains is not
142
+ * repeated, and one that contains the joined text replaces it, so a `SqlError` wrapping SQLite's own text reads
143
+ * `Failed to execute statement: SQLITE_BUSY: database is locked`. It is the whole chain, not the innermost message
144
+ * alone. The walk stops after eight nodes, so a cyclic chain terminates.
145
+ *
146
+ * Never throws: a `message` getter that throws leaves that node without text, and a `cause` getter that throws ends
147
+ * the walk with the text found so far, `undefined` when there is none.
148
+ *
149
+ * @param value - the error, or whatever was thrown
150
+ */
151
+ static readonly causeMessage: (value: unknown) => string | undefined;
152
+ /**
153
+ * The text of an unknown failure: {@link ErrorText.causeMessage} when the chain carries a message, otherwise
154
+ * {@link ErrorText.describe}.
155
+ *
156
+ * @remarks
157
+ * Exactly `ErrorText.causeMessage(value) ?? ErrorText.describe(value)`, the fallback every I/O site would otherwise
158
+ * spell out by hand. An error whose message is empty and whose chain carries none is `""`, as `describe` reads it.
159
+ * Never throws.
160
+ *
161
+ * @param value - the error, or whatever was thrown
162
+ */
163
+ static readonly of: (value: unknown) => string;
164
+ }
165
+ /**
166
+ * {@link Exclusive.run}'s required options.
167
+ *
168
+ * @public
169
+ */
170
+ interface ExclusiveRunOptions {
171
+ /**
172
+ * How long the body may run once it holds the permit. Required: a lane
173
+ * that mutates process-global state must not be held forever by a body
174
+ * that never settles. The timeout interrupts the body; it never releases
175
+ * the permit before the body's finalizers have run.
176
+ */
177
+ readonly timeout: Duration.Input;
178
+ }
179
+ /**
180
+ * {@link Exclusive.promise}'s options.
181
+ *
182
+ * @public
183
+ */
184
+ interface ExclusivePromiseOptions<A, E> {
185
+ /** Starts the promise. `signal` aborts when the effect is interrupted. */
186
+ readonly try: (signal: AbortSignal) => PromiseLike<A>;
187
+ /** Maps a rejection, or a synchronous throw from `try`, to the typed failure. */
188
+ readonly catch: (cause: unknown) => E;
189
+ /**
190
+ * On interruption, abort `signal` and resume at once instead of waiting for
191
+ * the promise to settle. Default `false` (abort, then wait). Set it for a
192
+ * promise that ignores its signal, which would otherwise hold the interrupt
193
+ * forever; see {@link Exclusive.promise}.
194
+ */
195
+ readonly abandon?: boolean;
196
+ }
197
+ declare const ExclusiveTimeout_base: Schema.Class<ExclusiveTimeout, Schema.TaggedStruct<"ExclusiveTimeout", {
198
+ readonly timeout: Schema.String;
199
+ readonly message: Schema.String;
200
+ }>, import("effect/Cause").YieldableError>;
201
+ /**
202
+ * The body of an {@link Exclusive.run} call ran past its timeout and was
203
+ * interrupted. Its finalizers had run, and the permit was released, by the
204
+ * time the caller sees this.
205
+ *
206
+ * @remarks
207
+ * Audience: calling code, which usually folds it into its own outcome (a tool
208
+ * result's `{ kind: "timeout" }`). `timeout` is the formatted duration.
209
+ *
210
+ * @public
211
+ */
212
+ export declare class ExclusiveTimeout extends ExclusiveTimeout_base {}
213
+ /**
214
+ * A one-permit lane for work that mutates process-global state (the working
215
+ * directory, the environment, a patched stream), so two requests never
216
+ * interleave it and the state is restored before the next one starts.
217
+ *
218
+ * @remarks
219
+ * Ordering, on every exit path:
220
+ *
221
+ * 1. Waiting for the permit is interruptible: a request cancelled while it is
222
+ * queued leaves the queue and never runs.
223
+ * 2. Once the permit is held, the caller's interruption is masked until the
224
+ * body ends. A transport that maps a client cancel to an interrupt (an MCP
225
+ * server does) only drops the response; the next request still waits for
226
+ * the real work to finish.
227
+ * 3. The required timeout interrupts the body, and only the body.
228
+ * 4. The body runs in its own `Scope`: its finalizers (restoring the working
229
+ * directory or the environment with {@link ProcessScope}, removing a temp
230
+ * directory) run, then the permit is released, then the caller sees the
231
+ * result, an {@link ExclusiveTimeout}, or its own deferred interruption.
232
+ *
233
+ * A body that ignores interruption delays its timeout and the permit release
234
+ * until it settles, as an uninterruptible region does. That is
235
+ * deliberate, because releasing the permit while process state is still being
236
+ * changed is the bug this lane exists to prevent. Run promise work through
237
+ * {@link Exclusive.promise}, whose interruption aborts the signal and then
238
+ * waits for the promise to settle; `Effect.promise` would abandon it. A
239
+ * promise that ignores its signal and never settles therefore hangs the
240
+ * timeout and the permit forever: give it `abandon: true`.
241
+ *
242
+ * **Not reentrant.** A `run` on the same lane from inside a body waits for the
243
+ * permit its own caller holds. It never gets it: the nested call waits until
244
+ * the outer timeout fires, which interrupts the body (and the nested wait with
245
+ * it), so the outer `run` fails with {@link ExclusiveTimeout}. Call the inner
246
+ * work directly instead of through the lane again.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * import { Exclusive, ProcessScope } from "@effected/kit"
251
+ * import { Effect } from "effect"
252
+ *
253
+ * const lane = yield* Exclusive.make
254
+ * const result = yield* lane.run(
255
+ * Effect.gen(function* () {
256
+ * yield* ProcessScope.chdir(process, root)
257
+ * return yield* Exclusive.promise({ try: (signal) => runTool(signal), catch: (cause) => new ToolFailed({ cause }) })
258
+ * }),
259
+ * { timeout: "2 minutes" },
260
+ * )
261
+ * ```
262
+ *
263
+ * @public
264
+ */
265
+ export declare class Exclusive {
266
+ private readonly semaphore;
267
+ private constructor();
268
+ /** A new lane with one permit. Bind it once (a module-level or layer-built value); every call shares it. */
269
+ static readonly make: Effect.Effect<Exclusive>;
270
+ /**
271
+ * A promise whose interruption aborts `signal` and then waits for the
272
+ * promise to settle, rather than abandoning it as `Effect.promise` and
273
+ * `Effect.tryPromise` do. A rejection, or a synchronous throw from `try`,
274
+ * fails through `catch`; a `catch` that itself throws dies with what it
275
+ * threw.
276
+ *
277
+ * @remarks
278
+ * **The default hangs on a promise that ignores its signal.** Waiting is
279
+ * the point: the interrupt (a lane timeout, a cancel) returns only once the
280
+ * promise has settled, so no work is still changing process state when the
281
+ * permit is released. But a promise that never observes `signal` and never
282
+ * settles (a test runner's `start`, a third-party call with no
283
+ * cancellation) holds the interrupt, the lane's timeout and the permit
284
+ * forever.
285
+ *
286
+ * Pass `abandon: true` for such a promise. Interruption still aborts
287
+ * `signal`, then resumes at once without waiting; the promise keeps running
288
+ * unobserved and its eventual result is dropped. Inside {@link Exclusive.run}
289
+ * the body's scope still closes, and its finalizers still run, before the
290
+ * permit is released. What abandoning gives up is the guarantee that the
291
+ * promise itself has stopped: choose it only when the promise cannot be
292
+ * stopped, and restore any state it touches in a finalizer.
293
+ */
294
+ static readonly promise: <A, E>(options: ExclusivePromiseOptions<A, E>) => Effect.Effect<A, E>;
295
+ /**
296
+ * Runs `body` holding this lane's permit, under the ordering in the class
297
+ * remarks. `body` gets its own `Scope`, closed before the permit is released.
298
+ */
299
+ readonly run: <A, E, R>(body: Effect.Effect<A, E, R>, options: ExclusiveRunOptions) => Effect.Effect<A, E | ExclusiveTimeout, Exclude<R, Scope.Scope>>;
300
+ }
301
+ /**
302
+ * The parameter shape {@link LaunchContext.resolve} takes: the process-derived
303
+ * facts a front end resolves once, passed in as values.
304
+ *
305
+ * @remarks
306
+ * Nothing here reads `process`: `argv` and `env` come from the front end's own
307
+ * `main.ts`. That keeps the resolution rule shared and testable while the
308
+ * process read stays at the one place allowed to make it.
309
+ *
310
+ * @public
311
+ */
312
+ interface ResolveInput {
313
+ /**
314
+ * Positional candidates, checked in order. Pass positional arguments only —
315
+ * the parsed positionals of your command, never raw `process.argv.slice(2)`:
316
+ * every non-empty value counts as a candidate, so a `--flag` would become the
317
+ * project directory.
318
+ */
319
+ readonly argv?: ReadonlyArray<string> | undefined;
320
+ /** The environment, usually `process.env`. */
321
+ readonly env: Readonly<Record<string, string | undefined>>;
322
+ /** Env keys to try in order, for example `["OKFIT_PROJECT_DIR", "CLAUDE_PROJECT_DIR"]`. */
323
+ readonly keys: ReadonlyArray<string>;
324
+ }
325
+ /**
326
+ * The parameter shape {@link LaunchContext.projectDir} takes: {@link ResolveInput}
327
+ * plus the `cwd` fallback.
328
+ *
329
+ * @public
330
+ */
331
+ interface ProjectDirInput extends ResolveInput {
332
+ /** The fallback, usually `process.cwd()`. */
333
+ readonly cwd: string;
334
+ }
335
+ /**
336
+ * Resolves where a tool launched by an agent host should treat as its project.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * import { LaunchContext } from "@effected/kit"
341
+ *
342
+ * const dir = LaunchContext.projectDir({
343
+ * argv: [],
344
+ * env: { CLAUDE_PROJECT_DIR: "/work/app" },
345
+ * keys: ["MYTOOL_PROJECT_DIR", "CLAUDE_PROJECT_DIR"],
346
+ * cwd: "/home/me",
347
+ * })
348
+ * // => "/work/app"
349
+ * ```
350
+ *
351
+ * @public
352
+ */
353
+ export declare class LaunchContext {
354
+ private constructor();
355
+ /**
356
+ * Whether a value still carries a literal `${VAR}` placeholder.
357
+ *
358
+ * @remarks
359
+ * An agent host can pass `${CLAUDE_PROJECT_DIR}` through unsubstituted in
360
+ * some launch paths; a path containing a placeholder is never what was meant.
361
+ */
362
+ static readonly isUnsubstituted: (value: string) => boolean;
363
+ /**
364
+ * `value` trimmed, when it names something: `Option.none()` for `undefined`,
365
+ * an empty or whitespace-only string, or a string still carrying a literal
366
+ * `${VAR}` placeholder ({@link LaunchContext.isUnsubstituted}).
367
+ *
368
+ * @remarks
369
+ * The guard every resolver here applies; use it directly for one positional
370
+ * argument, as in `LaunchContext.usable(argv[2])`. `??` would accept an empty
371
+ * string or a literal `${VAR}`, neither of which names a directory or an id.
372
+ */
373
+ static readonly usable: (value: string | undefined) => Option.Option<string>;
374
+ /**
375
+ * The first {@link LaunchContext.usable} `argv` value, then the first usable
376
+ * `env` value in `keys` order; `Option.none()` when none is usable.
377
+ *
378
+ * @remarks
379
+ * The resolver under {@link LaunchContext.projectDir}, without its `cwd`
380
+ * fallback, for any launch-supplied location: a host's plugin data
381
+ * directory, then an app override, with the caller's own fallback after
382
+ * it. Values are trimmed, so a value with surrounding whitespace resolves to
383
+ * the trimmed path.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * import { LaunchContext } from "@effected/kit"
388
+ * import { Option } from "effect"
389
+ *
390
+ * const dataDir = LaunchContext.resolve({ env, keys: ["CLAUDE_PLUGIN_DATA", "MYTOOL_DATA_DIR"] }).pipe(
391
+ * Option.getOrElse(() => `${home}/.mytool`),
392
+ * )
393
+ * ```
394
+ */
395
+ static readonly resolve: (input: ResolveInput) => Option.Option<string>;
396
+ /**
397
+ * The first usable argv value, then the first usable env value in `keys`
398
+ * order, then `cwd`: {@link LaunchContext.resolve} with `cwd` as the
399
+ * fallback.
400
+ */
401
+ static readonly projectDir: (input: ProjectDirInput) => string;
402
+ }
403
+ /**
404
+ * The working-directory half of a process, structurally. Node's `process`
405
+ * satisfies it; pass it in, so nothing here reads `process` itself.
406
+ *
407
+ * @public
408
+ */
409
+ interface ProcessCwdHost {
410
+ /** The current working directory. May throw when it was deleted (`ENOENT`). */
411
+ cwd(): string;
412
+ /** Changes the working directory. Throws when `directory` cannot be entered. */
413
+ chdir(directory: string): void;
414
+ }
415
+ declare const ProcessScopeError_base: Schema.Class<ProcessScopeError, Schema.TaggedStruct<"ProcessScopeError", {
416
+ readonly reason: Schema.Literal<"ChdirFailed">;
417
+ readonly path: Schema.String;
418
+ readonly message: Schema.String;
419
+ readonly cause: Schema.Defect;
420
+ }>, import("effect/Cause").YieldableError>;
421
+ /**
422
+ * A {@link ProcessScope} change could not be made.
423
+ *
424
+ * @remarks
425
+ * Audience: calling code (`reason`, `path`) and an end user (`message`).
426
+ * `"ChdirFailed"` means `chdir` threw for `path`; the original throw is
427
+ * `cause`. Nothing was changed, so nothing is restored.
428
+ *
429
+ * @public
430
+ */
431
+ export declare class ProcessScopeError extends ProcessScopeError_base {}
432
+ /**
433
+ * Scoped changes to process-global state, each restored when the enclosing
434
+ * `Scope` closes, on every exit path.
435
+ *
436
+ * @remarks
437
+ * Pair them with {@link Exclusive}: a scoped restore protects the next request
438
+ * only if no other request can run while the change is live.
439
+ *
440
+ * Both take the state they change as an argument (a {@link ProcessCwdHost},
441
+ * an environment record), so they read no `process` and test against plain
442
+ * doubles. For a temporary directory, use core's
443
+ * `FileSystem.makeTempDirectory` in an `Effect.acquireRelease` whose release
444
+ * ignores a failed removal; `makeTempDirectoryScoped`'s release dies on one,
445
+ * which turns a successful body into a defect.
446
+ *
447
+ * @public
448
+ */
449
+ export declare class ProcessScope {
450
+ private constructor();
451
+ /**
452
+ * Enters `directory` and returns to the previous working directory when the
453
+ * scope closes.
454
+ *
455
+ * @remarks
456
+ * When the previous directory cannot be read (it was deleted, so `cwd()`
457
+ * throws), the change still happens and nothing is restored: there is no
458
+ * valid directory to go back to. A restore that throws is ignored for the
459
+ * same reason.
460
+ */
461
+ static readonly chdir: (host: ProcessCwdHost, directory: string) => Effect.Effect<void, ProcessScopeError, Scope.Scope>;
462
+ /**
463
+ * Applies `patch` to `env` and restores every patched key when the scope
464
+ * closes.
465
+ *
466
+ * @remarks
467
+ * An `undefined` value in `patch` deletes the key, and a key that was absent
468
+ * before is deleted again on restore. Assigning `undefined` to Node's
469
+ * `process.env` would store the string `"undefined"`, so neither direction
470
+ * ever assigns it.
471
+ */
472
+ static readonly env: (env: Record<string, string | undefined>, patch: Readonly<Record<string, string | undefined>>) => Effect.Effect<void, never, Scope.Scope>;
473
+ }
474
+ /**
475
+ * What a caller — usually an agent — should do after a failure.
476
+ *
477
+ * @remarks
478
+ * `hint` is the human-readable instruction. `suggestedTool` and `suggestedArgs`
479
+ * optionally name the tool to call next and the arguments to call it with, so
480
+ * an agent can act without parsing the hint. The optional keys are
481
+ * `optionalKey`: omit them rather than passing an explicit `undefined`, which
482
+ * is rejected instead of silently encoded.
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * import { Remediation } from "@effected/kit"
487
+ *
488
+ * const remediation: Remediation = {
489
+ * hint: "Run the validator on the whole bundle first.",
490
+ * suggestedTool: "validate_bundle",
491
+ * suggestedArgs: { strict: true },
492
+ * }
493
+ * ```
494
+ *
495
+ * @public
496
+ */
497
+ export declare const Remediation: Schema.Struct<{
498
+ readonly hint: Schema.String;
499
+ readonly suggestedTool: Schema.optionalKey<Schema.String>;
500
+ readonly suggestedArgs: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
501
+ }>;
502
+ /**
503
+ * A decoded {@link (Remediation:variable)}.
504
+ *
505
+ * @public
506
+ */
507
+ export type Remediation = typeof Remediation.Type;
508
+ export type { ExclusivePromiseOptions, ExclusiveRunOptions, ProcessCwdHost, ProjectDirInput, ResolveInput };
509
+ //# sourceMappingURL=index.d.ts.map
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { CurrentDistribution, Distribution, DistributionField, distributionSuffix, withDistribution, withDistributionVersion } from "./Distribution.js";
2
+ import { ErrorText } from "./ErrorText.js";
3
+ import { Exclusive, ExclusiveTimeout } from "./Exclusive.js";
4
+ import { LaunchContext } from "./LaunchContext.js";
5
+ import { ProcessScope, ProcessScopeError } from "./ProcessScope.js";
6
+ import { Remediation } from "./Remediation.js";
7
+
8
+ export { CurrentDistribution, Distribution, DistributionField, ErrorText, Exclusive, ExclusiveTimeout, LaunchContext, ProcessScope, ProcessScopeError, Remediation, distributionSuffix, withDistribution, withDistributionVersion };
@@ -0,0 +1,57 @@
1
+ import { Predicate } from "effect";
2
+
3
+ // src/internal/causeMessage.ts
4
+ /** How many nodes of a cause chain the walk reads before stopping, so a cyclic chain cannot spin. */
5
+ const CAUSE_DEPTH = 8;
6
+ /**
7
+ * The message a single node carries: a thrown string, or a non-empty string `message` property. A getter or
8
+ * proxy trap that throws is a node with no message.
9
+ */
10
+ const messageAt = (node) => {
11
+ try {
12
+ const text = Predicate.isString(node) ? node : Predicate.hasProperty(node, "message") && Predicate.isString(node.message) ? node.message : void 0;
13
+ return text === void 0 || text.trim() === "" ? void 0 : text;
14
+ } catch {
15
+ return;
16
+ }
17
+ };
18
+ /** The next link of the chain: a node's `cause`, or `undefined` when it has none or reading it throws. */
19
+ const causeAt = (node) => {
20
+ try {
21
+ return Predicate.hasProperty(node, "cause") ? node.cause : void 0;
22
+ } catch {
23
+ return;
24
+ }
25
+ };
26
+ /**
27
+ * Every distinct non-empty message a preserved cause chain carries, nearest
28
+ * first and joined with `: `, or `undefined` when nothing along it has one.
29
+ *
30
+ * A `Schema.Defect()` field holds whatever was thrown, so the read is guarded
31
+ * rather than assumed: `Predicate.hasProperty` + `Predicate.isString` accepts
32
+ * an `Error`, a cross-realm `Error` that fails `instanceof`, a plain
33
+ * `{ message }` object and a thrown string alike. An empty or whitespace-only
34
+ * message counts as none. The walk follows `cause` links because a wrapper's
35
+ * text is often the generic summary and the driver's text underneath is the
36
+ * actionable detail, yet a wrapper that composes its own context must not
37
+ * lose it either; so nothing is dropped, and a deeper text the nearer one
38
+ * already contains is not repeated. The walk is bounded, so a cycle terminates.
39
+ *
40
+ * It never throws: every property read is guarded, so a `message` getter that throws leaves that link
41
+ * without text, and a `cause` getter that throws ends the walk with what was found so far.
42
+ */
43
+ const causeMessage = (cause) => {
44
+ let message;
45
+ let node = cause;
46
+ for (let depth = 0; depth < CAUSE_DEPTH && node !== void 0 && node !== null; depth++) {
47
+ const text = messageAt(node);
48
+ if (text !== void 0) {
49
+ if (message === void 0 || text.includes(message)) message = text;
50
+ else if (!message.includes(text)) message = `${message}: ${text}`;
51
+ }
52
+ node = causeAt(node);
53
+ }
54
+ return message;
55
+ };
56
+
57
+ export { causeMessage };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
- "name": "@effected/kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@effected/kit",
3
+ "version": "0.12.0",
4
+ "private": false,
5
+ "description": "Platform-free primitives shared by every front end of an Effect v4 tool: distribution identity, remediation, launch context, and transport-neutral process crash guards",
6
+ "keywords": [
7
+ "effect",
8
+ "cli",
9
+ "mcp",
10
+ "carrier",
11
+ "distribution",
12
+ "effected"
13
+ ],
14
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/kit#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/spencerbeggs/effected/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/spencerbeggs/effected.git",
21
+ "directory": "packages/kit"
22
+ },
23
+ "license": "MIT",
24
+ "author": {
25
+ "name": "C. Spencer Beggs",
26
+ "email": "spencer@beggs.codes",
27
+ "url": "https://spencerbeg.gs"
28
+ },
29
+ "sideEffects": false,
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./index.d.ts",
34
+ "import": "./index.js",
35
+ "default": "./index.js"
36
+ },
37
+ "./guard": {
38
+ "types": "./guard.d.ts",
39
+ "import": "./guard.js",
40
+ "default": "./guard.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "peerDependencies": {
45
+ "effect": "~4.1.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=24.11.0"
49
+ }
50
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.60.1"
9
+ }
10
+ ]
11
+ }