@executablemd/runtime 0.10.2 → 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/esm/config.js CHANGED
@@ -22,12 +22,21 @@
22
22
  * Installing at `min` is what lets a nested override win: a block's own
23
23
  * `timeout=` outranks the value the command line established for the run.
24
24
  * Omitting a field inherits the enclosing value rather than clearing it.
25
+ *
26
+ * `verbose` is the fourth field and is not a timeout. It says whether the
27
+ * scope reading it renders verbose-only content, it is `false` until something
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
30
+ * component reads it to choose between rendering its content and rendering
31
+ * nothing, and the host's own presentation — the journal, the event echo, the
32
+ * testing report — is decided by the command line rather than by this field.
25
33
  */
26
34
  import { createApi } from "@effectionx/context-api";
27
35
  export const Config = createApi("Config", {
28
36
  timeout: undefined,
29
37
  timeoutExec: undefined,
30
38
  timeoutFetch: undefined,
39
+ verbose: false,
31
40
  });
32
41
  /**
33
42
  * A configured duration is milliseconds or nothing. Anything else fails here,
@@ -56,3 +65,21 @@ export const timeout = validated("timeout", Config.operations.timeout);
56
65
  export const timeoutExec = validated("timeoutExec", Config.operations.timeoutExec);
57
66
  /** The validated default timeout for a Fetch. */
58
67
  export const timeoutFetch = validated("timeoutFetch", Config.operations.timeoutFetch);
68
+ /**
69
+ * The validated verbosity of the reading scope.
70
+ *
71
+ * There is no "absent" verbosity the way there is an absent timeout: a scope
72
+ * either renders verbose-only content or it does not. Anything that is not a
73
+ * boolean — what an untyped JavaScript consumer can still install — fails
74
+ * here, where the reader asked, rather than being read as truthiness by
75
+ * whichever component asked first.
76
+ */
77
+ export const verbose = {
78
+ *[Symbol.iterator]() {
79
+ const configured = yield* Config.operations.verbose;
80
+ if (typeof configured !== "boolean") {
81
+ throw new Error(`Config verbose must be a boolean, got ${String(configured)}`);
82
+ }
83
+ return configured;
84
+ },
85
+ };
package/esm/files.js CHANGED
@@ -83,6 +83,7 @@ const OPERATIONS = [
83
83
  "check-file-path",
84
84
  "read",
85
85
  "delete",
86
+ "ensure-directory",
86
87
  "glob",
87
88
  "temporary-directory",
88
89
  ];
@@ -556,6 +557,10 @@ export const Files = createApi("executablemd.runtime.files", {
556
557
  throw new FilesProviderUnavailableError();
557
558
  },
558
559
  // deno-lint-ignore require-yield
560
+ *ensureDirectory(_input) {
561
+ throw new FilesProviderUnavailableError();
562
+ },
563
+ // deno-lint-ignore require-yield
559
564
  *globFiles(_input) {
560
565
  throw new FilesProviderUnavailableError();
561
566
  },
package/esm/host-files.js CHANGED
@@ -238,6 +238,43 @@ function* removalDestination(input) {
238
238
  return { reason: reasonOf(error) };
239
239
  }
240
240
  }
241
+ /**
242
+ * The directory a `<Dir>` names, which is the one target that may be absolute.
243
+ *
244
+ * Every other operation here refuses an absolute path outright, because a
245
+ * document that writes one is naming a place outside the work it was given.
246
+ * `<Dir>` is the established exception: an absolute `path` has always been used
247
+ * as written, and this operation exists to serve that component. So an absolute
248
+ * target is taken as it stands and is not measured against the working
249
+ * directory — there is no base it was ever relative to.
250
+ *
251
+ * A relative target keeps the ordinary rules: resolved against `cwd`, with both
252
+ * sides canonical, so a working directory reached through a symlink is not read
253
+ * as an escape and a `..` that genuinely leaves still is.
254
+ *
255
+ * The final segment is resolved along with the rest. A directory that already
256
+ * exists behind a symlink is the directory it points at, and entering it is
257
+ * what the document asked for.
258
+ */
259
+ function* directoryDestination(input) {
260
+ try {
261
+ if (isAbsolute(input.path)) {
262
+ return { path: yield* resolveExisting(input.path) };
263
+ }
264
+ if (!within(input.cwd, resolve(input.cwd, input.path))) {
265
+ return { reason: "lexical-escape" };
266
+ }
267
+ const base = (yield* API.Fs.operations.realpath(input.cwd)) ?? input.cwd;
268
+ const path = yield* resolveExisting(resolve(input.cwd, input.path));
269
+ if (!within(base, path)) {
270
+ return { reason: "resolved-escape" };
271
+ }
272
+ return { path };
273
+ }
274
+ catch (error) {
275
+ return { reason: reasonOf(error) };
276
+ }
277
+ }
241
278
  function nonWriteFailure(operation, phase, reason) {
242
279
  return Err(filesFailure({ operation, phase, reason }));
243
280
  }
@@ -472,6 +509,57 @@ export function hostFilesHandler(options = {}) {
472
509
  }
473
510
  return Ok(undefined);
474
511
  }
512
+ /**
513
+ * Make the named path a directory, creating what is missing.
514
+ *
515
+ * Three answers, and the order between them is the contract. An existing
516
+ * directory is success without touching it: nothing is replaced, cleared or
517
+ * written, because the document asked for the directory to exist and it does.
518
+ * An existing entry that is not a directory is a refusal — a file where a
519
+ * directory was asked for is a mistake to report, never a thing to remove.
520
+ * Anything else is created, recursively, along with every missing parent.
521
+ *
522
+ * The target is classified before creation is attempted so the refusal for a
523
+ * non-directory target is decided here rather than left to whatever the
524
+ * platform's `mkdir -p` happens to say. An intermediate non-directory is the
525
+ * platform's to report, and `ENOTDIR` already carries it into the shared
526
+ * vocabulary — so both refusals arrive as `not-directory` and neither carries
527
+ * a host path or a platform message.
528
+ *
529
+ * Creation is direct and persists. There is no rollback and no teardown
530
+ * removal: a later failure of the content that runs inside this directory
531
+ * says nothing about whether the directory should exist.
532
+ */
533
+ function* ensureDirectory(input) {
534
+ if (input.path.length === 0) {
535
+ return nonWriteFailure("ensure-directory", "lexical", "empty-path");
536
+ }
537
+ const target = yield* directoryDestination(input);
538
+ if ("reason" in target) {
539
+ return nonWriteFailure("ensure-directory", "resolution", target.reason);
540
+ }
541
+ notify(observe, { operation: "ensure-directory", phase: "target" });
542
+ try {
543
+ const info = yield* API.Fs.operations.stat(target.path);
544
+ if (info.exists && !info.isDirectory) {
545
+ return nonWriteFailure("ensure-directory", "target", "not-directory");
546
+ }
547
+ if (info.exists) {
548
+ return Ok(undefined);
549
+ }
550
+ }
551
+ catch (error) {
552
+ return nonWriteFailure("ensure-directory", "target", reasonOf(error));
553
+ }
554
+ notify(observe, { operation: "ensure-directory", phase: "access" });
555
+ try {
556
+ yield* API.Fs.operations.ensureDir(target.path);
557
+ }
558
+ catch (error) {
559
+ return nonWriteFailure("ensure-directory", "access", reasonOf(error));
560
+ }
561
+ return Ok(undefined);
562
+ }
475
563
  /**
476
564
  * The regular files under `cwd` that `include` selects and `exclude` does not.
477
565
  *
@@ -550,7 +638,15 @@ export function hostFilesHandler(options = {}) {
550
638
  yield* provide(Ok(canonical));
551
639
  });
552
640
  }
553
- return { checkFilePath, readTextFile, writeTextFile, deleteFile, globFiles, temporaryDirectory };
641
+ return {
642
+ checkFilePath,
643
+ readTextFile,
644
+ writeTextFile,
645
+ deleteFile,
646
+ ensureDirectory,
647
+ globFiles,
648
+ temporaryDirectory,
649
+ };
554
650
  }
555
651
  /**
556
652
  * Remove a temporary directory as its scope ends.
@@ -612,6 +708,9 @@ export function useHostFiles(options = {}) {
612
708
  *deleteFile([input]) {
613
709
  return yield* handler.deleteFile(input);
614
710
  },
711
+ *ensureDirectory([input]) {
712
+ return yield* handler.ensureDirectory(input);
713
+ },
615
714
  *globFiles([input]) {
616
715
  return yield* handler.globFiles(input);
617
716
  },
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 " +
@@ -141,8 +142,6 @@ function runForeground(request) {
141
142
  if (command === undefined) {
142
143
  throw new Error("native launch: command must not be empty");
143
144
  }
144
- const settled = withResolvers();
145
- const failed = withResolvers();
146
145
  let child;
147
146
  // Interrupt, then insist. A cancelled document may not continue — or
148
147
  // finish tearing down — while a child still holds the terminal, so this
@@ -153,28 +152,42 @@ function runForeground(request) {
153
152
  // `inherit` is the whole point: the child reads this terminal and draws on
154
153
  // it directly, so nothing between it and the person using it can buffer,
155
154
  // reorder, capture or journal what passes.
156
- child = spawnChild(command, args, {
155
+ const started = spawnChild(command, args, {
157
156
  cwd: request.cwd,
158
157
  env: request.env,
159
158
  stdio: "inherit",
160
159
  });
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]);
160
+ child = started;
161
+ // Raced inline, in the same synchronous run as the spawn, so both arms are
162
+ // attached before the child can report anything — a spawned race attaches
163
+ // a turn later. Whichever loses is halted, which is what detaches it.
164
+ return yield* race([
165
+ (function* () {
166
+ const [code, signal] = yield* once(started, "exit");
167
+ const outcome = {};
168
+ if (code !== null) {
169
+ outcome.exitCode = code;
170
+ }
171
+ if (signal !== null) {
172
+ outcome.signal = signal;
173
+ }
174
+ return outcome;
175
+ })(),
176
+ (function* () {
177
+ const [error] = yield* once(started, "error");
178
+ throw error;
179
+ })(),
180
+ ]);
173
181
  });
174
182
  }
175
183
  /**
176
184
  * End one foreground child and wait for it to be gone.
177
185
  *
186
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
187
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
188
+ * only way to observe that it is released on every settlement path is to hold
189
+ * the child.
190
+ *
178
191
  * Deliberately one promise rather than an Effection race: this runs while the
179
192
  * scope is already being dismantled, and the cheapest correct thing to do
180
193
  * there is to wait on the process's own events instead of starting more
@@ -184,7 +197,7 @@ function runForeground(request) {
184
197
  * is spent — a native UI holding the terminal is not something a cancelled run
185
198
  * can afford to wait on indefinitely.
186
199
  */
187
- function reap(child) {
200
+ export function reap(child) {
188
201
  const pid = child.pid;
189
202
  if (pid === undefined || child.exitCode !== null || child.signalCode !== null) {
190
203
  return Promise.resolve();
@@ -206,6 +219,10 @@ function reap(child) {
206
219
  clearInterval(poll);
207
220
  clearTimeout(escalation);
208
221
  clearTimeout(deadline);
222
+ // The one funnel every settlement goes through — the exit event, the
223
+ // reachability poll, the escalation deadline, and the refusal that
224
+ // rejects — so the handler comes off however this ends.
225
+ child.off("exit", onExit);
209
226
  // Deno's `node:child_process` stops reporting a child's exit once a
210
227
  // signal that child ignored has been delivered, and holds the runtime
211
228
  // open on the handle it will now never settle. Dropping the reference is
@@ -223,7 +240,8 @@ function reap(child) {
223
240
  }
224
241
  resolve();
225
242
  };
226
- child.once("exit", () => done());
243
+ const onExit = () => done();
244
+ child.on("exit", onExit);
227
245
  // Reachability rather than the exit event, because that is the fact this
228
246
  // has to establish and the event is not dependable across runtimes here.
229
247
  const poll = setInterval(() => {
package/esm/mod.js CHANGED
@@ -19,7 +19,8 @@
19
19
  * - `API.Service` — scoped attached service startup (`startService`)
20
20
  * - `NativeLauncher` — handing one native agent UI the foreground terminal
21
21
  * (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
22
- * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
22
+ * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
23
+ * `verbose`)
23
24
  *
24
25
  * See `apis.ts` for architecture rationale.
25
26
  * See `@executablemd/runtime/test` for composable test stubs.
@@ -28,7 +29,7 @@ import "./_dnt.polyfills.js";
28
29
  export { API } from "./apis.js";
29
30
  export { exec, readTextFile, writeTextFile, stat, lstat, readDirectory, glob, realpath, ensureDir, rename, remove, fetch, cwd, env, platform, command, compile, useQuietProcessOutput, } from "./apis.js";
30
31
  export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
31
- export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
32
+ export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
32
33
  export { asDuration, durationError, parseDuration } from "./duration.js";
33
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";
34
35
  export { flushOutput, installControlledLauncher, installForegroundLauncher, NATIVE_LAUNCHER_UNAVAILABLE, NativeLauncher, NativeLauncherUnavailableError, nativeLaunch, NO_TERMINAL, reserveTerminal, } from "./launcher.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@executablemd/runtime",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "Runtime host APIs for executable.md documents.",
5
5
  "homepage": "https://executable.md",
6
6
  "repository": {
@@ -38,9 +38,9 @@
38
38
  "@effectionx/context-api": "0.6.0",
39
39
  "@effectionx/fetch": "0.2.1",
40
40
  "@effectionx/fs": "0.3.0",
41
+ "@effectionx/node": "0.2.5",
41
42
  "@effectionx/process": "0.8.1",
42
- "effection": "4.1.0",
43
- "@effectionx/node": "0.2.4"
43
+ "effection": "4.1.0"
44
44
  },
45
45
  "_generatedBy": "dnt@dev"
46
46
  }
package/types/config.d.ts CHANGED
@@ -22,6 +22,14 @@
22
22
  * Installing at `min` is what lets a nested override win: a block's own
23
23
  * `timeout=` outranks the value the command line established for the run.
24
24
  * Omitting a field inherits the enclosing value rather than clearing it.
25
+ *
26
+ * `verbose` is the fourth field and is not a timeout. It says whether the
27
+ * scope reading it renders verbose-only content, it is `false` until something
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
30
+ * component reads it to choose between rendering its content and rendering
31
+ * nothing, and the host's own presentation — the journal, the event echo, the
32
+ * testing report — is decided by the command line rather than by this field.
25
33
  */
26
34
  import { type Api } from "@effectionx/context-api";
27
35
  import type { Operation } from "effection";
@@ -32,6 +40,8 @@ export interface ConfigApi {
32
40
  timeoutExec: number | undefined;
33
41
  /** Default timeout for each Fetch, in milliseconds; undefined for none. */
34
42
  timeoutFetch: number | undefined;
43
+ /** Whether this scope renders verbose-only content; false for none. */
44
+ verbose: boolean;
35
45
  }
36
46
  export declare const Config: Api<ConfigApi>;
37
47
  /** The validated run deadline. Read by the run boundary and nothing else. */
@@ -40,3 +50,13 @@ export declare const timeout: Operation<number | undefined>;
40
50
  export declare const timeoutExec: Operation<number | undefined>;
41
51
  /** The validated default timeout for a Fetch. */
42
52
  export declare const timeoutFetch: Operation<number | undefined>;
53
+ /**
54
+ * The validated verbosity of the reading scope.
55
+ *
56
+ * There is no "absent" verbosity the way there is an absent timeout: a scope
57
+ * either renders verbose-only content or it does not. Anything that is not a
58
+ * boolean — what an untyped JavaScript consumer can still install — fails
59
+ * here, where the reader asked, rather than being read as truthiness by
60
+ * whichever component asked first.
61
+ */
62
+ export declare const verbose: Operation<boolean>;
package/types/files.d.ts CHANGED
@@ -68,7 +68,7 @@ export declare const FILES_WRITE_SUCCESS = "executablemd.runtime.files-write-suc
68
68
  */
69
69
  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";
70
70
  /** The operations whose failure carries no commit outcome. */
71
- export type FilesOperation = "check-file-path" | "read" | "delete" | "glob" | "temporary-directory";
71
+ export type FilesOperation = "check-file-path" | "read" | "delete" | "ensure-directory" | "glob" | "temporary-directory";
72
72
  /** Where a non-write operation stopped. */
73
73
  export type FilesPhase = "lexical" | "resolution" | "target" | "access" | "pattern" | "traversal" | "acquire";
74
74
  /** Where a write stopped, which is what decides what may be said about the target. */
@@ -145,6 +145,20 @@ export interface FilesHandler {
145
145
  * that same success.
146
146
  */
147
147
  deleteFile(input: FilePathInput): Operation<Result<void>>;
148
+ /**
149
+ * Make this path name a directory, and answer with nothing.
150
+ *
151
+ * Recursively creates the target and any missing parent. An existing
152
+ * directory is already the answer, so it succeeds without replacing it,
153
+ * clearing it or touching what is in it; a file or another non-directory, at
154
+ * the target or anywhere on the way to it, is a refusal.
155
+ *
156
+ * Mandatory like the rest, and Unit for the same reason `deleteFile` is: a
157
+ * document that asked for a directory to exist has been answered by its
158
+ * existence. Nothing comes back to branch on — no path, no handle, and no
159
+ * word on whether this call is what created it.
160
+ */
161
+ ensureDirectory(input: FilePathInput): Operation<Result<void>>;
148
162
  /** Sorted, deduplicated, POSIX-separated paths of the regular files that match. */
149
163
  globFiles(input: GlobInput): Operation<Result<string[]>>;
150
164
  /**
@@ -74,7 +74,7 @@ import type { FilesHandler } from "./files.js";
74
74
  * observable rather than merely stated.
75
75
  */
76
76
  export interface HostFilesEvent {
77
- readonly operation: "read" | "write" | "delete" | "glob";
77
+ readonly operation: "read" | "write" | "delete" | "ensure-directory" | "glob";
78
78
  readonly phase: "target" | "access" | "parents" | "temporary" | "commit" | "cleanup" | "read-dir";
79
79
  }
80
80
  /** Synchronous, so nothing can run between the observation and the call it precedes. */
@@ -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
  *
@@ -86,6 +87,24 @@ interface ForegroundLauncherOptions {
86
87
  * own its exit status, or continue after the UI closes.
87
88
  */
88
89
  export declare function installForegroundLauncher(options?: ForegroundLauncherOptions): Operation<void>;
90
+ /**
91
+ * End one foreground child and wait for it to be gone.
92
+ *
93
+ * Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
94
+ * `mod.ts`: the listener this installs belongs to a bounded Promise, and the
95
+ * only way to observe that it is released on every settlement path is to hold
96
+ * the child.
97
+ *
98
+ * Deliberately one promise rather than an Effection race: this runs while the
99
+ * scope is already being dismantled, and the cheapest correct thing to do
100
+ * there is to wait on the process's own events instead of starting more
101
+ * structured work beside them.
102
+ *
103
+ * A child that ignores the interrupt is killed outright once the grace period
104
+ * is spent — a native UI holding the terminal is not something a cancelled run
105
+ * can afford to wait on indefinitely.
106
+ */
107
+ export declare function reap(child: ChildProcess): Promise<void>;
89
108
  /**
90
109
  * A launcher a host installs when it has no terminal to give away, and no
91
110
  * intention of starting a native UI.
package/types/mod.d.ts CHANGED
@@ -19,7 +19,8 @@
19
19
  * - `API.Service` — scoped attached service startup (`startService`)
20
20
  * - `NativeLauncher` — handing one native agent UI the foreground terminal
21
21
  * (`reserveTerminal`, `flushOutput`, `nativeLaunch`)
22
- * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`)
22
+ * - `Config` — shared execution config (`timeout`, `timeoutExec`, `timeoutFetch`,
23
+ * `verbose`)
23
24
  *
24
25
  * See `apis.ts` for architecture rationale.
25
26
  * See `@executablemd/runtime/test` for composable test stubs.
@@ -30,7 +31,7 @@ export { exec, readTextFile, writeTextFile, stat, lstat, readDirectory, glob, re
30
31
  export type { DirectoryEntry, EvalBlock, FetchInit, FetchOperation, LinkStatResult, ResponseHeaders, RuntimeFetchResponse, StatResult, } from "./apis.js";
31
32
  export { Service, SERVICE_HOSTNAME, SERVICE_READY_PREFIX, ServiceProcessExitBeforeReadyError, ServiceProtocolDuplicateError, ServiceProtocolHostnameMismatchError, ServiceProtocolIncompatibleError, ServiceProtocolMalformedError, ServiceProtocolTokenMismatchError, ServiceProviderError, ServiceStartupTimeoutError, ServiceTeardownError, ServiceUnexpectedExitError, parseServiceReadyRecord, startService, } from "./service.js";
32
33
  export type { ServiceEndpoint, ServiceHandler, ServiceAttachment, ServiceStartOptions, } from "./service.js";
33
- export { Config, timeout, timeoutExec, timeoutFetch } from "./config.js";
34
+ export { Config, timeout, timeoutExec, timeoutFetch, verbose } from "./config.js";
34
35
  export type { ConfigApi } from "./config.js";
35
36
  export { asDuration, durationError, parseDuration } from "./duration.js";
36
37
  export type { ProcessExecOptions, ProcessOutcome } from "./apis.js";