@executablemd/runtime 0.6.0 → 0.8.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.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * `API.Files` — the document filesystem boundary.
3
+ *
4
+ * A document names files with a path relative to the contextual working
5
+ * directory, and every one of those operations arrives here. What is on the
6
+ * other side is a provider's choice: `xmd run` installs a host adapter that
7
+ * resolves those paths in the caller's own filesystem, and `xmd workflow`
8
+ * installs one that resolves them in a logical filesystem owned by a database
9
+ * transaction. Neither is named in the components that call this Api, which is
10
+ * what lets one document mean the same thing under both.
11
+ *
12
+ * The operations are **semantic**, not primitive. `writeTextFile` is a whole
13
+ * replacement — admission, resolution, target classification, parent creation,
14
+ * and commit — rather than a sequence a caller assembles, because assembling it
15
+ * from outside is what would let a path admitted by one provider be used by
16
+ * another. `API.Fs` remains the low-level host surface a host adapter is built
17
+ * on; it is not this boundary.
18
+ *
19
+ * `checkFilePath` is the one exception, and it is deliberately weak: pure path
20
+ * arithmetic, no filesystem access, and nothing usable comes back — no path, no
21
+ * handle, no authority token. `<File>`'s write form calls it to decide whether
22
+ * its children may expand at all, and the later `writeTextFile` repeats the
23
+ * same admission from the same authored path. A check that was skipped,
24
+ * replaced, or answered by another provider therefore authorizes nothing.
25
+ *
26
+ * ## Two kinds of failure
27
+ *
28
+ * An ordinary filesystem condition — missing, a directory, permission denied,
29
+ * no space — comes back as `Err(FilesError)` carrying frozen structural data.
30
+ * The consumer reads that data and selects a sentence from a fixed vocabulary;
31
+ * no message, errno code, resolved path, temporary path, or symlink target
32
+ * crosses this boundary. Cancellation is neither of these: it is not caught and
33
+ * never becomes a Result.
34
+ *
35
+ * A provider that is absent, that refuses an operation, or that breaks its own
36
+ * contract is not a filesystem condition. Those **throw**, with fixed
37
+ * diagnostics and no cause, and they end the execution rather than becoming
38
+ * something a document renders. A run whose Files provider is missing must not
39
+ * quietly reach the host instead.
40
+ *
41
+ * ## Why the data is structural
42
+ *
43
+ * Both the failures and the write outcome carry a plain frozen object under a
44
+ * stable `type` tag, and every consumer recognizes them by parsing that tag
45
+ * rather than with `instanceof`. Two copies of this package can be loaded at
46
+ * once — a repository component resolving its own runtime beside the engine's —
47
+ * and `instanceof` answers false across them, which would turn a provider
48
+ * failure into an unrecognized throw exactly when it matters most.
49
+ */
50
+ import { type Api } from "@effectionx/context-api";
51
+ import type { Operation, Result } from "effection";
52
+ /** The stable discriminant on ordinary filesystem failure data. */
53
+ export declare const FILES_ERROR = "executablemd.runtime.files-error/v1";
54
+ /** The stable discriminant on infrastructure failure data. */
55
+ export declare const FILES_FATAL = "executablemd.runtime.files-fatal/v1";
56
+ /** The stable discriminant on a successful write's outcome data. */
57
+ export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-success/v1";
58
+ /**
59
+ * The vocabulary an ordinary failure is reported in.
60
+ *
61
+ * A provider maps whatever its platform produced onto one of these before
62
+ * returning. An unmapped condition becomes `operation-failed`, which is a real
63
+ * answer rather than a placeholder: the consumer has a sentence for it, and the
64
+ * unmapped value itself never crosses the boundary.
65
+ */
66
+ export type FilesReason = "empty-path" | "absolute-path" | "lexical-escape" | "resolved-escape" | "missing" | "directory" | "special-file" | "not-directory" | "permission-denied" | "read-only" | "too-many-symlinks" | "path-too-long" | "no-space" | "quota-exhausted" | "cross-device" | "busy" | "too-many-open-files" | "directory-not-empty" | "invalid-pattern" | "operation-failed";
67
+ /** The operations whose failure carries no commit outcome. */
68
+ export type FilesOperation = "check-file-path" | "read" | "glob" | "temporary-directory";
69
+ /** Where a non-write operation stopped. */
70
+ export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
71
+ /** Where a write stopped, which is what decides what may be said about the target. */
72
+ export type FileWritePhase = "lexical" | "resolution" | "target" | "parents" | "temporary" | "commit" | "cleanup" | "transaction";
73
+ /**
74
+ * What is known about the target afterwards.
75
+ *
76
+ * `commit-unknown` is an answer rather than a missing one: a commit that threw
77
+ * may have run or not, and no provider can tell which from where it stands.
78
+ * What still holds in that case is that the target is one complete version.
79
+ */
80
+ export type FileWriteTarget = "unchanged" | "commit-unknown" | "committed" | "rolled-back";
81
+ export interface FilesFailureData {
82
+ readonly type: typeof FILES_ERROR;
83
+ readonly operation: FilesOperation;
84
+ readonly phase: FilesPhase;
85
+ readonly reason: FilesReason;
86
+ }
87
+ export interface FileWriteFailureData {
88
+ readonly type: typeof FILES_ERROR;
89
+ readonly operation: "write";
90
+ readonly phase: FileWritePhase;
91
+ readonly reason?: FilesReason;
92
+ readonly cleanup?: FilesReason;
93
+ readonly target: FileWriteTarget;
94
+ }
95
+ export type FilesErrorData = FilesFailureData | FileWriteFailureData;
96
+ /**
97
+ * The message every ordinary failure carries.
98
+ *
99
+ * Constant on purpose. A message is the part of an Error that gets printed by
100
+ * accident, and there is nothing safe to put in this one: the authored path
101
+ * belongs to the consumer that wrote it, and everything else belongs to the
102
+ * platform.
103
+ */
104
+ export declare const FILES_ERROR_MESSAGE = "Files operation failed";
105
+ /** An ordinary filesystem failure. What it means is in `data`, never in the message. */
106
+ export declare class FilesError extends Error {
107
+ readonly data: FilesErrorData;
108
+ constructor(data: FilesErrorData);
109
+ }
110
+ export interface FileWriteSuccess {
111
+ readonly type: typeof FILES_WRITE_SUCCESS;
112
+ readonly publication: "host-committed" | "transaction-staged";
113
+ }
114
+ export interface FilePathInput {
115
+ readonly cwd: string;
116
+ readonly path: string;
117
+ }
118
+ export interface FileWriteInput extends FilePathInput {
119
+ readonly content: string;
120
+ }
121
+ export interface GlobInput {
122
+ readonly cwd: string;
123
+ readonly include: string[];
124
+ readonly exclude: string[];
125
+ }
126
+ export interface FilesHandler {
127
+ /**
128
+ * Whether this authored path is admissible at all, decided from the path and
129
+ * `cwd` alone. No filesystem access, and nothing usable comes back.
130
+ */
131
+ checkFilePath(input: FilePathInput): Operation<Result<void>>;
132
+ readTextFile(input: FilePathInput): Operation<Result<string>>;
133
+ writeTextFile(input: FileWriteInput): Operation<Result<FileWriteSuccess>>;
134
+ /** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
135
+ globFiles(input: GlobInput): Operation<Result<string[]>>;
136
+ /**
137
+ * A directory that lives as long as the acquiring scope. A resource, so the
138
+ * caller holds it by acquisition rather than by remembering to remove it.
139
+ */
140
+ temporaryDirectory(): Operation<Result<string>>;
141
+ }
142
+ /** The operations a provider may refuse outright rather than fail at. */
143
+ export type FilesDeniableOperation = "temporary-directory";
144
+ /**
145
+ * Which contract a provider broke.
146
+ *
147
+ * `authority` — the identity authorizing access is stale, foreign, or gone.
148
+ * `savepoint` — a nested transaction could not be rolled back or released.
149
+ * `protocol` — a handler threw, or returned data no consumer can trust.
150
+ * `teardown` — cleanup failed while the scope was already unwinding.
151
+ */
152
+ export type FilesInvariantCategory = "authority" | "savepoint" | "protocol" | "teardown";
153
+ /**
154
+ * Infrastructure failure data.
155
+ *
156
+ * Three kinds, each with fixed fields and nothing derived from the condition
157
+ * that produced it. A category is control data for a consumer deciding what to
158
+ * fence, not text: no diagnostic interpolates it.
159
+ */
160
+ export type FilesFatalData = {
161
+ readonly type: typeof FILES_FATAL;
162
+ readonly kind: "provider-unavailable";
163
+ } | {
164
+ readonly type: typeof FILES_FATAL;
165
+ readonly kind: "operation-denied";
166
+ readonly operation: FilesDeniableOperation;
167
+ } | {
168
+ readonly type: typeof FILES_FATAL;
169
+ readonly kind: "invariant";
170
+ readonly category: FilesInvariantCategory;
171
+ };
172
+ export declare const FILES_PROVIDER_UNAVAILABLE_MESSAGE = "Files provider is not installed";
173
+ export declare const FILES_OPERATION_DENIED_MESSAGE = "Files provider does not support temporary-directory";
174
+ export declare const FILES_INVARIANT_MESSAGE = "Files provider invariant failed";
175
+ /** No Files provider is installed, and there is no host to fall back to. */
176
+ export declare class FilesProviderUnavailableError extends Error {
177
+ readonly data: FilesFatalData;
178
+ constructor();
179
+ }
180
+ /** The installed provider does not implement this operation at all. */
181
+ export declare class FilesOperationDeniedError extends Error {
182
+ readonly data: FilesFatalData;
183
+ constructor(operation: FilesDeniableOperation);
184
+ }
185
+ /** A provider broke its own contract. */
186
+ export declare class FilesInvariantError extends Error {
187
+ readonly data: FilesFatalData;
188
+ constructor(category: FilesInvariantCategory);
189
+ }
190
+ /** An infrastructure failure, recognized structurally rather than by class. */
191
+ export interface FilesFatalFailure extends Error {
192
+ readonly data: FilesFatalData;
193
+ }
194
+ /**
195
+ * The vocabularies, for a provider that reads a failure back out of storage.
196
+ *
197
+ * A transaction-bound provider retains what it refused rather than a serialized
198
+ * error, so restoring one means turning stored text back into the vocabulary.
199
+ * Parsing it here is what keeps one list of reasons and phases: a provider that
200
+ * declared its own copy would be a second list to keep in agreement with this.
201
+ */
202
+ export declare function parseFilesReason(value: unknown): FilesReason | undefined;
203
+ export declare function parseFilesPhase(value: unknown): FilesPhase | undefined;
204
+ export declare function parseFileWritePhase(value: unknown): FileWritePhase | undefined;
205
+ /**
206
+ * The infrastructure failure data this Error carries, if it carries valid data.
207
+ *
208
+ * Every field is checked, the member count with them, and that the object is
209
+ * frozen: extra keys are not the shape this contract describes, and a mutable
210
+ * one is not the shape a constructor here produces. Accepting either would let
211
+ * a provider smuggle a path or a message through under a recognized tag.
212
+ */
213
+ export declare function parseFilesFatal(error: unknown): FilesFatalData | undefined;
214
+ /**
215
+ * Whether this failure satisfies the whole public infrastructure-failure
216
+ * contract, not merely the tag.
217
+ *
218
+ * Recognition decides two different things at once, and the second is why this
219
+ * is stricter than `parseFilesFatal`. A recognized failure is **rethrown by
220
+ * identity** — the object that was thrown is the object a fail-stop records —
221
+ * so recognizing one is a decision to let that exact object travel onward. An
222
+ * Error that carries the right data but also a raw platform message, or a cause
223
+ * chain holding an errno and a path, would then carry all of that past the
224
+ * boundary the reason vocabulary exists to hold.
225
+ *
226
+ * So the whole object has to match what a constructor here produces: the fixed
227
+ * name and diagnostic for its kind, frozen structural data with exactly the
228
+ * fields the kind describes, no cause, and no other enumerable member — string
229
+ * or symbol. Anything else is a candidate that fails the contract, and
230
+ * `invokeFiles` replaces it with a fresh invariant rather than preserving it.
231
+ *
232
+ * Structural throughout, so a failure constructed by a separately loaded copy
233
+ * of this package is recognized on exactly the same terms as one constructed
234
+ * here — `instanceof` answers false across two copies, which is the case this
235
+ * has to survive. That is also why the `name` is checked rather than the class:
236
+ * a second copy's constructor is a different function producing the same name.
237
+ */
238
+ export declare function isFilesFatal(error: unknown): error is FilesFatalFailure;
239
+ /**
240
+ * The infrastructure failure this one is, by identity.
241
+ *
242
+ * The original object comes back rather than a replacement, because a fail-stop
243
+ * that records "the first error" has to record the one that was thrown.
244
+ */
245
+ export declare function asFilesFatal(error: unknown): FilesFatalFailure | undefined;
246
+ /** Build an ordinary non-write failure. */
247
+ export declare function filesFailure(input: {
248
+ operation: FilesOperation;
249
+ phase: FilesPhase;
250
+ reason: FilesReason;
251
+ }): FilesError;
252
+ /**
253
+ * Build a write failure, refusing any combination a consumer could not read.
254
+ *
255
+ * A write's report is the only place a document learns what became of a file it
256
+ * asked to replace, so an invalid combination is a provider bug rather than a
257
+ * value to pass along and interpret later.
258
+ */
259
+ export declare function fileWriteFailure(input: {
260
+ phase: FileWritePhase;
261
+ reason?: FilesReason;
262
+ cleanup?: FilesReason;
263
+ }): FilesError;
264
+ /**
265
+ * The non-write failure data this error carries, if it carries valid data.
266
+ *
267
+ * Malformed data is not fatal here — the consumer already has a sentence for
268
+ * "the operation failed" and nothing about a target is at stake — so this
269
+ * simply declines to recognize it.
270
+ */
271
+ export declare function parseFilesFailure(error: unknown): FilesFailureData | undefined;
272
+ /**
273
+ * The write failure data this error carries, if it carries valid data.
274
+ *
275
+ * Unlike a non-write failure, malformed data here has no safe reading: every
276
+ * sentence a consumer could print makes a claim about whether the file was
277
+ * replaced. A caller treats `undefined` from a write as a protocol invariant
278
+ * rather than inventing a commit state.
279
+ */
280
+ export declare function parseFileWriteFailure(error: unknown): FileWriteFailureData | undefined;
281
+ /** A successful write's outcome. */
282
+ export declare function fileWriteSuccess(publication: FileWriteSuccess["publication"]): FileWriteSuccess;
283
+ /**
284
+ * The write outcome this value is, if it is a valid one.
285
+ *
286
+ * A malformed success is as untrustworthy as a malformed failure: a provider
287
+ * that cannot describe what it did may not have done it, so a caller treats
288
+ * `undefined` here as a protocol invariant too.
289
+ */
290
+ export declare function parseFileWriteSuccess(value: unknown): FileWriteSuccess | undefined;
291
+ /**
292
+ * The document filesystem Api.
293
+ *
294
+ * The terminal handler throws for every operation, including `checkFilePath`.
295
+ * A default that reached the host would make an uninstalled provider
296
+ * indistinguishable from an installed one, and the whole point of the boundary
297
+ * is that a workflow run cannot silently touch the caller's filesystem.
298
+ */
299
+ export declare const Files: Api<FilesHandler>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The host `API.Files` provider — document filesystem access in the caller's
3
+ * own filesystem.
4
+ *
5
+ * This is what `xmd run` installs. A document's relative path is resolved
6
+ * against the contextual working directory and used as an ordinary host path,
7
+ * so a document can hand a file to a tool the caller already has. Everything
8
+ * below is built on the low-level `API.Fs`, which is deliberate: a host that
9
+ * already wraps `API.Fs` to observe or sandbox the engine's own file access
10
+ * keeps seeing a document's access on the same terms.
11
+ *
12
+ * ## What containment means here
13
+ *
14
+ * Access is confined to the contextual directory, judged against the filesystem
15
+ * as this adapter observes it. An empty path, an absolute path, and a lexical
16
+ * `..` escape are refused without touching the filesystem at all; a symlink
17
+ * leading out is refused once resolution can see it.
18
+ *
19
+ * That is sound **while the host pathname namespace is stable**, and every
20
+ * guarantee here is stated on that basis. It is not a sandbox. Another process
21
+ * can replace a directory, symlink, junction, or reparse point between the
22
+ * moment this adapter observes a path and the moment it uses one, and nothing
23
+ * available on the shipped runtimes closes that window without a native
24
+ * dependency. What is contained is the document's own children — the case a
25
+ * document controls — because resolution is deferred until after they run.
26
+ *
27
+ * ## Writes
28
+ *
29
+ * A write goes through a sibling temporary file and a rename. The rename is the
30
+ * commit point: everything before it can fail or be cancelled with the previous
31
+ * file untouched, and once it begins the target holds the complete old file or
32
+ * the complete new one, never a partial write. It is a commit rather than a
33
+ * transaction — a rename that returned is not undone by a later cancellation.
34
+ * The temporary also closes the one hole resolution cannot: a dangling symlink
35
+ * has nothing to resolve, and `rename` replaces the link rather than following
36
+ * it wherever it points.
37
+ *
38
+ * ## What crosses the boundary
39
+ *
40
+ * Nothing from a caught platform error. An errno code *selects* a
41
+ * `FilesReason`, and the reason is all the consumer receives — no message, no
42
+ * code, no resolved path, no temporary path, and no symlink target. A platform
43
+ * error names the path it failed on, and for a write that path can be a
44
+ * temporary the document never chose.
45
+ */
46
+ import type { Operation } from "effection";
47
+ import type { FilesHandler } from "./files.js";
48
+ /**
49
+ * A private step a host operation is about to take.
50
+ *
51
+ * Test-only. Production entrypoints install the adapter without one, so this is
52
+ * neither global state nor a capability: an observer can watch, and the point of
53
+ * watching is to replace part of the tree between an observation and the call
54
+ * that follows it, which is how the stable-namespace limitation is made
55
+ * observable rather than merely stated.
56
+ */
57
+ export interface HostFilesEvent {
58
+ readonly operation: "read" | "write" | "glob";
59
+ readonly phase: "target" | "access" | "parents" | "temporary" | "commit" | "cleanup" | "read-dir";
60
+ }
61
+ /** Synchronous, so nothing can run between the observation and the call it precedes. */
62
+ export type HostFilesObserver = (event: HostFilesEvent) => void;
63
+ export interface HostFilesOptions {
64
+ readonly observe?: HostFilesObserver;
65
+ }
66
+ /**
67
+ * Build a host provider.
68
+ *
69
+ * Exported so a test can drive one operation directly; entrypoints install it
70
+ * with {@link useHostFiles}.
71
+ */
72
+ export declare function hostFilesHandler(options?: HostFilesOptions): FilesHandler;
73
+ /**
74
+ * Install the host provider beneath ordinary middleware.
75
+ *
76
+ * `at: "min"` is what lets a host wrap document filesystem access without
77
+ * replacing it — middleware installed later sees these operations and can
78
+ * delegate to them.
79
+ */
80
+ export declare function useHostFiles(options?: HostFilesOptions): Operation<void>;
package/types/mod.d.ts CHANGED
@@ -5,20 +5,32 @@
5
5
  * `API` is available for middleware (`.around()`).
6
6
  * For normal calls, import operations directly.
7
7
  *
8
- * Six domain APIs:
8
+ * Seven domain APIs:
9
9
  * - `API.Process` — subprocess execution (`exec`)
10
- * - `API.Fs` — filesystem (`readTextFile`, `stat`, `glob`)
10
+ * - `API.Fs` — the low-level host filesystem (`readTextFile`, `writeTextFile`,
11
+ * `stat`, `glob`, `realpath`, `ensureDir`, `rename`, `remove`)
12
+ * - `API.Files` — document filesystem access as whole semantic operations,
13
+ * with no host default. `useHostFiles()` installs the host provider.
11
14
  * - `API.Fetch` — HTTP requests (`fetch`)
12
- * - `API.Env` — environment variables and platform info (`cwd`, `env`, `platform`)
13
- * - `API.Compiler` block compilation (`compile`)
14
- * - `Config` — shared execution config (`timeout`)
15
+ * - `API.Env` — the host: variables, platform info, the command that invokes
16
+ * this xmd, and eval-block compilation
17
+ * (`cwd`, `env`, `platform`, `command`, `compile`)
18
+ * - `API.Service` — scoped attached service startup (`startService`)
19
+ * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
15
20
  *
16
21
  * See `apis.ts` for architecture rationale.
17
22
  * See `@executablemd/runtime/test` for composable test stubs.
18
23
  */
19
24
  export { API } from "./apis.js";
20
- export { exec, readTextFile, stat, glob, fetch, cwd, env, platform, compile } from "./apis.js";
21
- export type { ResponseHeaders, RuntimeFetchResponse, StatResult } from "./apis.js";
22
- export { findFreePort } from "./find-free-port.js";
23
- export { Config, timeout } from "./config.js";
25
+ export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
26
+ export type { EvalBlock, ResponseHeaders, RuntimeFetchResponse, StatResult } from "./apis.js";
27
+ export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
28
+ export type { ServiceEndpoint, ServiceHandler, ServiceAttachment, ServiceStartOptions, } from "./service.js";
29
+ export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
24
30
  export type { ConfigApi } from "./config.js";
31
+ export { asDuration, durationError, parseDuration } from "./duration.js";
32
+ export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";
33
+ export { asFilesFatal, FILES_ERROR, FILES_ERROR_MESSAGE, FILES_FATAL, FILES_INVARIANT_MESSAGE, FILES_OPERATION_DENIED_MESSAGE, FILES_PROVIDER_UNAVAILABLE_MESSAGE, FILES_WRITE_SUCCESS, Files, FilesError, FilesInvariantError, FilesOperationDeniedError, FilesProviderUnavailableError, fileWriteFailure, fileWriteSuccess, filesFailure, isFilesFatal, parseFilesPhase, parseFilesReason, parseFileWriteFailure, parseFileWritePhase, parseFileWriteSuccess, parseFilesFailure, parseFilesFatal, } from "./files.js";
34
+ export type { FilePathInput, FilesDeniableOperation, FilesErrorData, FilesFailureData, FilesFatalData, FilesFatalFailure, FilesHandler, FilesInvariantCategory, FilesOperation, FilesPhase, FilesReason, FileWriteFailureData, FileWriteInput, FileWritePhase, FileWriteSuccess, FileWriteTarget, GlobInput, } from "./files.js";
35
+ export { hostFilesHandler, useHostFiles } from "./host-files.js";
36
+ export type { HostFilesEvent, HostFilesObserver, HostFilesOptions } from "./host-files.js";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Provider-neutral attached-service lifecycle.
3
+ *
4
+ * The shared runtime owns the XMD service handshake shape and validation. A
5
+ * runtime-named host adapter supplies process startup through `API.Service`
6
+ * middleware.
7
+ */
8
+ import { type Api, type Operations } from "@effectionx/context-api";
9
+ import type { Operation } from "effection";
10
+ export declare const SERVICE_READY_PREFIX = "XMD_SERVICE_READY:";
11
+ export declare const SERVICE_HOSTNAME = "127.0.0.1";
12
+ export interface ServiceEndpoint {
13
+ readonly hostname: string;
14
+ readonly port: number;
15
+ }
16
+ export interface ServiceStartOptions {
17
+ readonly command: string;
18
+ readonly cwd?: string;
19
+ readonly startupTimeout?: number;
20
+ }
21
+ export interface ServiceAttachment {
22
+ readonly endpoint: Readonly<ServiceEndpoint>;
23
+ }
24
+ export interface ServiceHandler {
25
+ start(options: ServiceStartOptions): Operation<ServiceAttachment>;
26
+ }
27
+ export declare class ServiceProviderError extends Error {
28
+ name: string;
29
+ constructor();
30
+ }
31
+ export declare class ServiceProtocolMalformedError extends Error {
32
+ name: string;
33
+ constructor();
34
+ }
35
+ export declare class ServiceProtocolIncompatibleError extends Error {
36
+ name: string;
37
+ constructor();
38
+ }
39
+ export declare class ServiceProtocolTokenMismatchError extends Error {
40
+ name: string;
41
+ constructor();
42
+ }
43
+ export declare class ServiceProtocolHostnameMismatchError extends Error {
44
+ name: string;
45
+ constructor();
46
+ }
47
+ export declare class ServiceProtocolDuplicateError extends Error {
48
+ name: string;
49
+ constructor();
50
+ }
51
+ export declare class ServiceStartupTimeoutError extends Error {
52
+ name: string;
53
+ constructor(timeout: number);
54
+ }
55
+ interface ServiceExitStatus {
56
+ readonly code?: number;
57
+ readonly signal?: string;
58
+ }
59
+ export declare class ServiceProcessExitBeforeReadyError extends Error {
60
+ name: string;
61
+ constructor(status: ServiceExitStatus);
62
+ }
63
+ export declare class ServiceUnexpectedExitError extends Error {
64
+ name: string;
65
+ constructor(status: ServiceExitStatus);
66
+ }
67
+ export declare class ServiceTeardownError extends Error {
68
+ name: string;
69
+ constructor(options?: {
70
+ cause?: unknown;
71
+ });
72
+ }
73
+ /** Parse and authenticate one prefix-stripped v1 handshake payload. */
74
+ export declare function parseServiceReadyRecord(payload: string, expectedToken: string): ServiceEndpoint;
75
+ export declare const Service: Api<ServiceHandler>;
76
+ export declare const startService: Operations<ServiceHandler>["start"];
77
+ export {};
@@ -6,5 +6,6 @@
6
6
  * - `useStubFs(files)` — in-memory filesystem
7
7
  * - `useEchoExec()` — simple echo-based exec
8
8
  * - `useFailingExec(exitCode, stderr)` — always-failing exec
9
+ * - `useStubService(endpoint)` — scoped provider-neutral service attachment
9
10
  */
10
- export { useStubFs, useEchoExec, useFailingExec } from "./stubs.js";
11
+ export { useStubFs, useEchoExec, useFailingExec, useStubService } from "./stubs.js";
@@ -20,12 +20,18 @@
20
20
  * ```
21
21
  */
22
22
  import type { Operation } from "effection";
23
+ import type { ServiceEndpoint } from "../service.js";
24
+ /** Install a provider-neutral scoped service attachment stub. */
25
+ export declare function useStubService(endpoint: ServiceEndpoint): Operation<void>;
23
26
  /**
24
27
  * Install an in-memory filesystem stub.
25
28
  *
26
29
  * - `readTextFile` returns content from the `files` map; throws ENOENT for missing keys.
27
30
  * - `stat` returns `{ exists: true, isFile: true }` for keys in the map.
28
31
  * - `glob` throws (not stubbed). Install `API.Fs.around()` directly if needed.
32
+ * - the writing half — `writeTextFile`, `ensureDir`, `rename`, `remove`, and
33
+ * `realpath` — is not stubbed and reaches the real filesystem. A test that
34
+ * exercises a document writing files wants a real temporary directory.
29
35
  *
30
36
  * The `files` object is captured **by reference** — mutating it between
31
37
  * operations changes what `readTextFile`/`stat` see. This is useful for
@@ -37,6 +43,12 @@ export declare function useStubFs(files: Record<string, string>): Operation<void
37
43
  *
38
44
  * Recognizes `bash -c "echo ..."` and returns the echo'd text as stdout.
39
45
  * All other commands return the script text as stdout with exit code 0.
46
+ *
47
+ * It answers like a real child: the text is written to the stdio chain as it
48
+ * "arrives", so whatever encloses the call sees it the way it would see a
49
+ * child's, and it is retained in the outcome only when the caller asked
50
+ * for retention. A stub that always returned strings would let a document
51
+ * render output the contract says was already displayed.
40
52
  */
41
53
  export declare function useEchoExec(): Operation<void>;
42
54
  /**
@@ -1,33 +0,0 @@
1
- /**
2
- * findFreePort — find an available TCP port using the OS.
3
- */
4
- import { race } from "effection";
5
- import { once } from "@effectionx/node";
6
- import { createServer } from "node:net";
7
- /**
8
- * Find an available TCP port by binding to port 0 and reading the
9
- * OS-assigned port number.
10
- */
11
- export function* findFreePort() {
12
- const server = createServer();
13
- const listening = once(server, "listening");
14
- const error = once(server, "error");
15
- server.listen(0);
16
- try {
17
- const rethrowError = {
18
- *[Symbol.iterator]() {
19
- const [err] = yield* error;
20
- throw err;
21
- },
22
- };
23
- yield* race([listening, rethrowError]);
24
- const addr = server.address();
25
- if (!addr || typeof addr !== "object") {
26
- throw new Error("findFreePort: unexpected address format");
27
- }
28
- return addr.port;
29
- }
30
- finally {
31
- server.close();
32
- }
33
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * findFreePort — find an available TCP port using the OS.
3
- */
4
- import type { Operation } from "effection";
5
- /**
6
- * Find an available TCP port by binding to port 0 and reading the
7
- * OS-assigned port number.
8
- */
9
- export declare function findFreePort(): Operation<number>;