@executablemd/runtime 0.11.0 → 0.12.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.
package/esm/api.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module
3
+ *
4
+ * The consumer API of `@executablemd/runtime`: the shared execution
5
+ * configuration a Plugin, a component package or a host reads.
6
+ *
7
+ * `Config` is the same Api the engine and the CLI already install and read;
8
+ * this entrypoint is where a consumer imports it from, so a package depending
9
+ * on runtime reaches its configuration without importing the whole host
10
+ * surface. The root export keeps every one of these names.
11
+ *
12
+ * ```ts
13
+ * import { verbose } from "@executablemd/runtime/api";
14
+ *
15
+ * if (yield* verbose) {
16
+ * // render the detail a quiet run leaves out
17
+ * }
18
+ * ```
19
+ */
20
+ import "./_dnt.polyfills.js";
21
+ export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
22
+ export { asDuration, durationError, parseDuration } from "./duration.js";
package/esm/config.js CHANGED
@@ -26,7 +26,7 @@
26
26
  * `verbose` is the fourth field and is not a timeout. It says whether the
27
27
  * scope reading it renders verbose-only content, it is `false` until something
28
28
  * says otherwise, and it is installed and overridden exactly the way a timeout
29
- * is. It bounds nothing, opens nothing and decides nothing about authority: a
29
+ * is. It bounds nothing, opens nothing and decides no permission: a
30
30
  * component reads it to choose between rendering its content and rendering
31
31
  * nothing, and the host's own presentation — the journal, the event echo, the
32
32
  * testing report — is decided by the command line rather than by this field.
package/esm/files.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * `checkFilePath` is the one exception, and it is deliberately weak: pure path
22
22
  * arithmetic, no filesystem access, and nothing usable comes back — no path, no
23
- * handle, no authority token. `<File>`'s write form calls it to decide whether
23
+ * handle, no capability token. `<File>`'s write form calls it to decide whether
24
24
  * its children may expand at all, and the later `writeTextFile` repeats the
25
25
  * same admission from the same authored path. A check that was skipped,
26
26
  * replaced, or answered by another provider therefore authorizes nothing.
package/esm/launcher.js CHANGED
@@ -27,7 +27,8 @@
27
27
  * document help and inspection free of any of this.
28
28
  */
29
29
  import { createApi } from "@effectionx/context-api";
30
- import { ensure, race, resource, scoped, until, withResolvers } from "effection";
30
+ import { ensure, race, resource, scoped, until } from "effection";
31
+ import { once } from "@effectionx/node/events";
31
32
  import { spawn as spawnChild } from "node:child_process";
32
33
  import process from "node:process";
33
34
  export const NATIVE_LAUNCHER_UNAVAILABLE = "no native launcher is installed — this host does not hand a native agent UI " +
@@ -51,6 +52,10 @@ export const NativeLauncher = createApi("runtime.nativeLauncher", {
51
52
  *launch(_request) {
52
53
  throw new NativeLauncherUnavailableError();
53
54
  },
55
+ // deno-lint-ignore require-yield
56
+ *notify(_text) {
57
+ throw new NativeLauncherUnavailableError();
58
+ },
54
59
  });
55
60
  /** Hold the foreground-terminal lease for the calling scope. */
56
61
  export function reserveTerminal() {
@@ -60,6 +65,10 @@ export function reserveTerminal() {
60
65
  export function flushOutput() {
61
66
  return NativeLauncher.operations.flush();
62
67
  }
68
+ /** Say one thing to whoever is at the terminal this launch reserved. */
69
+ export function notifyTerminal(text) {
70
+ return NativeLauncher.operations.notify(text);
71
+ }
63
72
  /** Run one native UI as a foreground child and report how it ended. */
64
73
  export function nativeLaunch(request) {
65
74
  return NativeLauncher.operations.launch(request);
@@ -117,6 +126,12 @@ export function* installForegroundLauncher(options = {}) {
117
126
  *launch([request]) {
118
127
  return yield* runForeground(request);
119
128
  },
129
+ *notify([text]) {
130
+ // Written and drained, not queued: the next thing to reach this
131
+ // terminal may be a child drawing over it.
132
+ process.stdout.write(`${text}\n`);
133
+ yield* drainStream(process.stdout);
134
+ },
120
135
  }, { at: "min" });
121
136
  }
122
137
  /**
@@ -141,8 +156,6 @@ function runForeground(request) {
141
156
  if (command === undefined) {
142
157
  throw new Error("native launch: command must not be empty");
143
158
  }
144
- const settled = withResolvers();
145
- const failed = withResolvers();
146
159
  let child;
147
160
  // Interrupt, then insist. A cancelled document may not continue — or
148
161
  // finish tearing down — while a child still holds the terminal, so this
@@ -153,28 +166,42 @@ function runForeground(request) {
153
166
  // `inherit` is the whole point: the child reads this terminal and draws on
154
167
  // it directly, so nothing between it and the person using it can buffer,
155
168
  // reorder, capture or journal what passes.
156
- child = spawnChild(command, args, {
169
+ const started = spawnChild(command, args, {
157
170
  cwd: request.cwd,
158
171
  env: request.env,
159
172
  stdio: "inherit",
160
173
  });
161
- child.once("error", (error) => failed.reject(error));
162
- child.once("exit", (code, signal) => {
163
- const outcome = {};
164
- if (code !== null) {
165
- outcome.exitCode = code;
166
- }
167
- if (signal !== null) {
168
- outcome.signal = signal;
169
- }
170
- settled.resolve(outcome);
171
- });
172
- return yield* race([settled.operation, failed.operation]);
174
+ child = started;
175
+ // Raced inline, in the same synchronous run as the spawn, so both arms are
176
+ // attached before the child can report anything — a spawned race attaches
177
+ // a turn later. Whichever loses is halted, which is what detaches it.
178
+ return yield* race([
179
+ (function* () {
180
+ const [code, signal] = yield* once(started, "exit");
181
+ const outcome = {};
182
+ if (code !== null) {
183
+ outcome.exitCode = code;
184
+ }
185
+ if (signal !== null) {
186
+ outcome.signal = signal;
187
+ }
188
+ return outcome;
189
+ })(),
190
+ (function* () {
191
+ const [error] = yield* once(started, "error");
192
+ throw error;
193
+ })(),
194
+ ]);
173
195
  });
174
196
  }
175
197
  /**
176
198
  * End one foreground child and wait for it to be gone.
177
199
  *
200
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
201
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
202
+ * only way to observe that it is released on every settlement path is to hold
203
+ * the child.
204
+ *
178
205
  * Deliberately one promise rather than an Effection race: this runs while the
179
206
  * scope is already being dismantled, and the cheapest correct thing to do
180
207
  * there is to wait on the process's own events instead of starting more
@@ -184,7 +211,7 @@ function runForeground(request) {
184
211
  * is spent — a native UI holding the terminal is not something a cancelled run
185
212
  * can afford to wait on indefinitely.
186
213
  */
187
- function reap(child) {
214
+ export function reap(child) {
188
215
  const pid = child.pid;
189
216
  if (pid === undefined || child.exitCode !== null || child.signalCode !== null) {
190
217
  return Promise.resolve();
@@ -206,6 +233,10 @@ function reap(child) {
206
233
  clearInterval(poll);
207
234
  clearTimeout(escalation);
208
235
  clearTimeout(deadline);
236
+ // The one funnel every settlement goes through — the exit event, the
237
+ // reachability poll, the escalation deadline, and the refusal that
238
+ // rejects — so the handler comes off however this ends.
239
+ child.off("exit", onExit);
209
240
  // Deno's `node:child_process` stops reporting a child's exit once a
210
241
  // signal that child ignored has been delivered, and holds the runtime
211
242
  // open on the handle it will now never settle. Dropping the reference is
@@ -223,7 +254,8 @@ function reap(child) {
223
254
  }
224
255
  resolve();
225
256
  };
226
- child.once("exit", () => done());
257
+ const onExit = () => done();
258
+ child.on("exit", onExit);
227
259
  // Reachability rather than the exit event, because that is the fact this
228
260
  // has to establish and the event is not dependable across runtimes here.
229
261
  const poll = setInterval(() => {
@@ -311,6 +343,10 @@ export function* installControlledLauncher(options = {}) {
311
343
  *flush() {
312
344
  options.onFlush?.();
313
345
  },
346
+ // deno-lint-ignore require-yield
347
+ *notify([text]) {
348
+ options.onNotify?.(text);
349
+ },
314
350
  *launch([request]) {
315
351
  options.record?.(request);
316
352
  if (options.wait) {
package/esm/mod.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * (`cwd`, `env`, `platform`, `command`, `compile`)
19
19
  * - `API.Service` — scoped attached service startup (`startService`)
20
20
  * - `NativeLauncher` — handing one native agent UI the foreground terminal
21
- * (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
21
+ * (`reserveTerminal`, `flushOutput`, `notifyTerminal`, `nativeLaunch`)
22
22
  * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
23
23
  * `verbose`)
24
24
  *
@@ -32,7 +32,7 @@ export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBefo
32
32
  export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
33
33
  export { asDuration, durationError, parseDuration } from "./duration.js";
34
34
  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";
35
- export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, reserveTerminal, } from "./launcher.js";
35
+ export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, notifyTerminal, reserveTerminal, } from "./launcher.js";
36
36
  export { hostFilesHandler, useHostFiles } from "./host-files.js";
37
37
  export { AgentSessionBusy, agentSessionKeyDigest, AgentSessionRecoveryRequired, parseAgentSessionOwnership, serializeAgentSessionOwnership, } from "./agent-session-coordinator.js";
38
38
  export { createDenoAgentSessionCoordinator, hasDenoAgentSessionCoordinator, } from "./deno-agent-session-coordinator.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/runtime",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Runtime host APIs for executable.md documents.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -20,6 +20,12 @@
20
20
  "default": "./esm/mod.js"
21
21
  }
22
22
  },
23
+ "./api": {
24
+ "import": {
25
+ "types": "./types/api.d.ts",
26
+ "default": "./esm/api.js"
27
+ }
28
+ },
23
29
  "./files": {
24
30
  "import": {
25
31
  "types": "./types/files.d.ts",
@@ -38,9 +44,9 @@
38
44
  "@effectionx/context-api": "0.6.0",
39
45
  "@effectionx/fetch": "0.2.1",
40
46
  "@effectionx/fs": "0.3.0",
47
+ "@effectionx/node": "0.2.5",
41
48
  "@effectionx/process": "0.8.1",
42
- "effection": "4.1.0",
43
- "@effectionx/node": "0.2.4"
49
+ "effection": "4.1.0"
44
50
  },
45
51
  "_generatedBy": "dnt@dev"
46
52
  }
package/types/api.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module
3
+ *
4
+ * The consumer API of `@executablemd/runtime`: the shared execution
5
+ * configuration a Plugin, a component package or a host reads.
6
+ *
7
+ * `Config` is the same Api the engine and the CLI already install and read;
8
+ * this entrypoint is where a consumer imports it from, so a package depending
9
+ * on runtime reaches its configuration without importing the whole host
10
+ * surface. The root export keeps every one of these names.
11
+ *
12
+ * ```ts
13
+ * import { verbose } from "@executablemd/runtime/api";
14
+ *
15
+ * if (yield* verbose) {
16
+ * // render the detail a quiet run leaves out
17
+ * }
18
+ * ```
19
+ */
20
+ import "./_dnt.polyfills.js";
21
+ export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
22
+ export type { ConfigApi } from "./config.js";
23
+ export { asDuration, durationError, parseDuration } from "./duration.js";
package/types/config.d.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * `verbose` is the fourth field and is not a timeout. It says whether the
27
27
  * scope reading it renders verbose-only content, it is `false` until something
28
28
  * says otherwise, and it is installed and overridden exactly the way a timeout
29
- * is. It bounds nothing, opens nothing and decides nothing about authority: a
29
+ * is. It bounds nothing, opens nothing and decides no permission: a
30
30
  * component reads it to choose between rendering its content and rendering
31
31
  * nothing, and the host's own presentation — the journal, the event echo, the
32
32
  * testing report — is decided by the command line rather than by this field.
package/types/files.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * `checkFilePath` is the one exception, and it is deliberately weak: pure path
22
22
  * arithmetic, no filesystem access, and nothing usable comes back — no path, no
23
- * handle, no authority token. `<File>`'s write form calls it to decide whether
23
+ * handle, no capability token. `<File>`'s write form calls it to decide whether
24
24
  * its children may expand at all, and the later `writeTextFile` repeats the
25
25
  * same admission from the same authored path. A check that was skipped,
26
26
  * replaced, or answered by another provider therefore authorizes nothing.
@@ -172,10 +172,14 @@ export type FilesDeniableOperation = "temporary-directory";
172
172
  /**
173
173
  * Which contract a provider broke.
174
174
  *
175
- * `authority` — the identity authorizing access is stale, foreign, or gone.
175
+ * `authority` — the identity permitting access is stale, foreign, or gone.
176
176
  * `savepoint` — a nested transaction could not be rolled back or released.
177
177
  * `protocol` — a handler threw, or returned data no consumer can trust.
178
178
  * `teardown` — cleanup failed while the scope was already unwinding.
179
+ *
180
+ * `authority` keeps that spelling because it is a serialized value: it crosses
181
+ * loaded copies and reaches parsers that match it byte for byte, so renaming it
182
+ * would break categorization rather than describe it better.
179
183
  */
180
184
  export type FilesInvariantCategory = "authority" | "savepoint" | "protocol" | "teardown";
181
185
  /**
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { type Api } from "@effectionx/context-api";
30
30
  import type { Operation } from "effection";
31
+ import type { ChildProcess } from "node:child_process";
31
32
  /**
32
33
  * What a provider asks the host to run.
33
34
  *
@@ -55,6 +56,21 @@ export interface NativeLauncherHandler {
55
56
  reserve(): Operation<void>;
56
57
  flush(): Operation<void>;
57
58
  launch(request: NativeLaunchRequest): Operation<NativeLaunchOutcome>;
59
+ /**
60
+ * Show the person one line about the launch itself, on the terminal this
61
+ * launch reserved.
62
+ *
63
+ * Not document output. What a launch has to say — that a turn is about to be
64
+ * spent in their name, what it answered, what it cost — is addressed to
65
+ * whoever is sitting there, and it belongs on the screen the native UI is
66
+ * about to open on rather than in the document's captured text, where a
67
+ * `<File>` would keep it and a replay would print it again.
68
+ *
69
+ * It goes through the launcher for the same reason `flush` does: this is the
70
+ * only thing that knows which terminal a given launch owns, so the root's
71
+ * launch writes to the root terminal.
72
+ */
73
+ notify(text: string): Operation<void>;
58
74
  }
59
75
  export declare const NATIVE_LAUNCHER_UNAVAILABLE: string;
60
76
  export declare class NativeLauncherUnavailableError extends Error {
@@ -66,6 +82,8 @@ export declare const NativeLauncher: Api<NativeLauncherHandler>;
66
82
  export declare function reserveTerminal(): Operation<void>;
67
83
  /** Give the reader everything the document has produced so far. */
68
84
  export declare function flushOutput(): Operation<void>;
85
+ /** Say one thing to whoever is at the terminal this launch reserved. */
86
+ export declare function notifyTerminal(text: string): Operation<void>;
69
87
  /** Run one native UI as a foreground child and report how it ended. */
70
88
  export declare function nativeLaunch(request: NativeLaunchRequest): Operation<NativeLaunchOutcome>;
71
89
  export declare const NO_TERMINAL: string;
@@ -86,6 +104,24 @@ interface ForegroundLauncherOptions {
86
104
  * own its exit status, or continue after the UI closes.
87
105
  */
88
106
  export declare function installForegroundLauncher(options?: ForegroundLauncherOptions): Operation<void>;
107
+ /**
108
+ * End one foreground child and wait for it to be gone.
109
+ *
110
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
111
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
112
+ * only way to observe that it is released on every settlement path is to hold
113
+ * the child.
114
+ *
115
+ * Deliberately one promise rather than an Effection race: this runs while the
116
+ * scope is already being dismantled, and the cheapest correct thing to do
117
+ * there is to wait on the process's own events instead of starting more
118
+ * structured work beside them.
119
+ *
120
+ * A child that ignores the interrupt is killed outright once the grace period
121
+ * is spent — a native UI holding the terminal is not something a cancelled run
122
+ * can afford to wait on indefinitely.
123
+ */
124
+ export declare function reap(child: ChildProcess): Promise<void>;
89
125
  /**
90
126
  * A launcher a host installs when it has no terminal to give away, and no
91
127
  * intention of starting a native UI.
@@ -100,6 +136,8 @@ export interface ControlledLauncherOptions {
100
136
  wait?: (request: NativeLaunchRequest) => Operation<void>;
101
137
  onReserve?: () => void;
102
138
  onFlush?: () => void;
139
+ /** Each line the launch addressed to the terminal, in the order it said them. */
140
+ onNotify?: (text: string) => void;
103
141
  }
104
142
  export declare function installControlledLauncher(options?: ControlledLauncherOptions): Operation<void>;
105
143
  export {};
package/types/mod.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * (`cwd`, `env`, `platform`, `command`, `compile`)
19
19
  * - `API.Service` — scoped attached service startup (`startService`)
20
20
  * - `NativeLauncher` — handing one native agent UI the foreground terminal
21
- * (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
21
+ * (`reserveTerminal`, `flushOutput`, `notifyTerminal`, `nativeLaunch`)
22
22
  * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
23
23
  * `verbose`)
24
24
  *
@@ -37,7 +37,7 @@ export { asDuration, durationError, parseDuration } from "./duration.js";
37
37
  export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";
38
38
  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";
39
39
  export type { FilePathInput, FilesDeniableOperation, FilesErrorData, FilesFailureData, FilesFatalData, FilesFatalFailure, FilesHandler, FilesInvariantCategory, FilesOperation, FilesPhase, FilesReason, FileWriteFailureData, FileWriteInput, FileWritePhase, FileWriteSuccess, FileWriteTarget, GlobInput, } from "./files.js";
40
- export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, reserveTerminal, } from "./launcher.js";
40
+ export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, notifyTerminal, reserveTerminal, } from "./launcher.js";
41
41
  export type { ControlledLauncherOptions, NativeLauncherHandler, NativeLaunchOutcome, NativeLaunchRequest, } from "./launcher.js";
42
42
  export { hostFilesHandler, useHostFiles } from "./host-files.js";
43
43
  export type { HostFilesEvent, HostFilesObserver, HostFilesOptions } from "./host-files.js";