@executablemd/runtime 0.7.0 → 0.8.1

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,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,22 +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`, `writeTextFile`, `stat`, `glob`,
11
- * `realpath`, `ensureDir`, `rename`, `remove`)
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.
12
14
  * - `API.Fetch` — HTTP requests (`fetch`)
13
15
  * - `API.Env` — the host: variables, platform info, the command that invokes
14
16
  * this xmd, and eval-block compilation
15
17
  * (`cwd`, `env`, `platform`, `command`, `compile`)
16
- * - `Config` — shared execution config (`timeout`)
18
+ * - `API.Service` — scoped attached service startup (`startService`)
19
+ * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
17
20
  *
18
21
  * See `apis.ts` for architecture rationale.
19
22
  * See `@executablemd/runtime/test` for composable test stubs.
20
23
  */
21
24
  export { API } from "./apis.js";
22
- export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, } from "./apis.js";
25
+ export { exec, readTextFile, writeTextFile, stat, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
23
26
  export type { EvalBlock, ResponseHeaders, RuntimeFetchResponse, StatResult } from "./apis.js";
24
- export { findFreePort } from "./find-free-port.js";
25
- export { Config, timeout } from "./config.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";
26
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,6 +20,9 @@
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
  *
@@ -40,6 +43,12 @@ export declare function useStubFs(files: Record<string, string>): Operation<void
40
43
  *
41
44
  * Recognizes `bash -c "echo ..."` and returns the echo'd text as stdout.
42
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.
43
52
  */
44
53
  export declare function useEchoExec(): Operation<void>;
45
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>;