appduct 0.9.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.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/bin.js +2 -0
  4. package/dist/artifact-inspect.d.ts +110 -0
  5. package/dist/artifact-inspect.d.ts.map +1 -0
  6. package/dist/artifact-inspect.js +315 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +4 -0
  10. package/dist/cli/command-options.d.ts +44 -0
  11. package/dist/cli/command-options.d.ts.map +1 -0
  12. package/dist/cli/command-options.js +95 -0
  13. package/dist/cli/create-cli.d.ts +2 -0
  14. package/dist/cli/create-cli.d.ts.map +1 -0
  15. package/dist/cli/create-cli.js +56 -0
  16. package/dist/cli/dispatch.d.ts +3 -0
  17. package/dist/cli/dispatch.d.ts.map +1 -0
  18. package/dist/cli/dispatch.js +275 -0
  19. package/dist/cli/open-target.d.ts +112 -0
  20. package/dist/cli/open-target.d.ts.map +1 -0
  21. package/dist/cli/open-target.js +438 -0
  22. package/dist/cli/result-types.d.ts +194 -0
  23. package/dist/cli/result-types.d.ts.map +1 -0
  24. package/dist/cli/result-types.js +6 -0
  25. package/dist/cli/runner.d.ts +35 -0
  26. package/dist/cli/runner.d.ts.map +1 -0
  27. package/dist/cli/runner.js +111 -0
  28. package/dist/cli/types.d.ts +20 -0
  29. package/dist/cli/types.d.ts.map +1 -0
  30. package/dist/cli/types.js +10 -0
  31. package/dist/cli.d.ts +4 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +2 -0
  34. package/dist/client/app-client.d.ts +126 -0
  35. package/dist/client/app-client.d.ts.map +1 -0
  36. package/dist/client/app-client.js +223 -0
  37. package/dist/client/bootstrap.d.ts +48 -0
  38. package/dist/client/bootstrap.d.ts.map +1 -0
  39. package/dist/client/bootstrap.js +108 -0
  40. package/dist/client/connect.d.ts +17 -0
  41. package/dist/client/connect.d.ts.map +1 -0
  42. package/dist/client/connect.js +38 -0
  43. package/dist/client/errors.d.ts +26 -0
  44. package/dist/client/errors.d.ts.map +1 -0
  45. package/dist/client/errors.js +40 -0
  46. package/dist/client/index.d.ts +19 -0
  47. package/dist/client/index.d.ts.map +1 -0
  48. package/dist/client/index.js +16 -0
  49. package/dist/commands/daemon.d.ts +22 -0
  50. package/dist/commands/daemon.d.ts.map +1 -0
  51. package/dist/commands/daemon.js +128 -0
  52. package/dist/commands/doctor.d.ts +23 -0
  53. package/dist/commands/doctor.d.ts.map +1 -0
  54. package/dist/commands/doctor.js +42 -0
  55. package/dist/commands/events.d.ts +37 -0
  56. package/dist/commands/events.d.ts.map +1 -0
  57. package/dist/commands/events.js +76 -0
  58. package/dist/commands/init.d.ts +51 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +231 -0
  61. package/dist/commands/invoke.d.ts +22 -0
  62. package/dist/commands/invoke.d.ts.map +1 -0
  63. package/dist/commands/invoke.js +54 -0
  64. package/dist/commands/keygen.d.ts +23 -0
  65. package/dist/commands/keygen.d.ts.map +1 -0
  66. package/dist/commands/keygen.js +46 -0
  67. package/dist/commands/link.d.ts +30 -0
  68. package/dist/commands/link.d.ts.map +1 -0
  69. package/dist/commands/link.js +43 -0
  70. package/dist/commands/ls.d.ts +9 -0
  71. package/dist/commands/ls.d.ts.map +1 -0
  72. package/dist/commands/ls.js +7 -0
  73. package/dist/commands/mcp.d.ts +49 -0
  74. package/dist/commands/mcp.d.ts.map +1 -0
  75. package/dist/commands/mcp.js +98 -0
  76. package/dist/commands/revoke.d.ts +12 -0
  77. package/dist/commands/revoke.d.ts.map +1 -0
  78. package/dist/commands/revoke.js +7 -0
  79. package/dist/commands/tools.d.ts +23 -0
  80. package/dist/commands/tools.d.ts.map +1 -0
  81. package/dist/commands/tools.js +52 -0
  82. package/dist/daemon/address.d.ts +30 -0
  83. package/dist/daemon/address.d.ts.map +1 -0
  84. package/dist/daemon/address.js +71 -0
  85. package/dist/daemon/audit.d.ts +107 -0
  86. package/dist/daemon/audit.d.ts.map +1 -0
  87. package/dist/daemon/audit.js +237 -0
  88. package/dist/daemon/calls.d.ts +84 -0
  89. package/dist/daemon/calls.d.ts.map +1 -0
  90. package/dist/daemon/calls.js +158 -0
  91. package/dist/daemon/config.d.ts +79 -0
  92. package/dist/daemon/config.d.ts.map +1 -0
  93. package/dist/daemon/config.js +170 -0
  94. package/dist/daemon/daemon.d.ts +43 -0
  95. package/dist/daemon/daemon.d.ts.map +1 -0
  96. package/dist/daemon/daemon.js +560 -0
  97. package/dist/daemon/event-bus.d.ts +49 -0
  98. package/dist/daemon/event-bus.d.ts.map +1 -0
  99. package/dist/daemon/event-bus.js +111 -0
  100. package/dist/daemon/links.d.ts +54 -0
  101. package/dist/daemon/links.d.ts.map +1 -0
  102. package/dist/daemon/links.js +118 -0
  103. package/dist/daemon/listener.d.ts +34 -0
  104. package/dist/daemon/listener.d.ts.map +1 -0
  105. package/dist/daemon/listener.js +132 -0
  106. package/dist/daemon/log-rotation.d.ts +82 -0
  107. package/dist/daemon/log-rotation.d.ts.map +1 -0
  108. package/dist/daemon/log-rotation.js +136 -0
  109. package/dist/daemon/pidfile.d.ts +38 -0
  110. package/dist/daemon/pidfile.d.ts.map +1 -0
  111. package/dist/daemon/pidfile.js +90 -0
  112. package/dist/daemon/policy.d.ts +22 -0
  113. package/dist/daemon/policy.d.ts.map +1 -0
  114. package/dist/daemon/policy.js +24 -0
  115. package/dist/daemon/registry.d.ts +19 -0
  116. package/dist/daemon/registry.d.ts.map +1 -0
  117. package/dist/daemon/registry.js +31 -0
  118. package/dist/daemon/rpc-server.d.ts +54 -0
  119. package/dist/daemon/rpc-server.d.ts.map +1 -0
  120. package/dist/daemon/rpc-server.js +203 -0
  121. package/dist/daemon/sessions.d.ts +86 -0
  122. package/dist/daemon/sessions.d.ts.map +1 -0
  123. package/dist/daemon/sessions.js +429 -0
  124. package/dist/daemon/socket-probe.d.ts +16 -0
  125. package/dist/daemon/socket-probe.d.ts.map +1 -0
  126. package/dist/daemon/socket-probe.js +28 -0
  127. package/dist/daemon/state-dir.d.ts +31 -0
  128. package/dist/daemon/state-dir.d.ts.map +1 -0
  129. package/dist/daemon/state-dir.js +43 -0
  130. package/dist/daemon/timers.d.ts +16 -0
  131. package/dist/daemon/timers.d.ts.map +1 -0
  132. package/dist/daemon/timers.js +12 -0
  133. package/dist/daemon/tls.d.ts +58 -0
  134. package/dist/daemon/tls.d.ts.map +1 -0
  135. package/dist/daemon/tls.js +79 -0
  136. package/dist/errors.d.ts +28 -0
  137. package/dist/errors.d.ts.map +1 -0
  138. package/dist/errors.js +162 -0
  139. package/dist/host-certificate.d.ts +8 -0
  140. package/dist/host-certificate.d.ts.map +1 -0
  141. package/dist/host-certificate.js +111 -0
  142. package/dist/index.d.ts +4 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +3 -0
  145. package/dist/key-material.d.ts +14 -0
  146. package/dist/key-material.d.ts.map +1 -0
  147. package/dist/key-material.js +42 -0
  148. package/dist/link.d.ts +76 -0
  149. package/dist/link.d.ts.map +1 -0
  150. package/dist/link.js +117 -0
  151. package/dist/mcp/connect-tool.d.ts +115 -0
  152. package/dist/mcp/connect-tool.d.ts.map +1 -0
  153. package/dist/mcp/connect-tool.js +396 -0
  154. package/dist/mcp/daemon-tools.d.ts +13 -0
  155. package/dist/mcp/daemon-tools.d.ts.map +1 -0
  156. package/dist/mcp/daemon-tools.js +29 -0
  157. package/dist/mcp/events-tool.d.ts +111 -0
  158. package/dist/mcp/events-tool.d.ts.map +1 -0
  159. package/dist/mcp/events-tool.js +322 -0
  160. package/dist/mcp/server.d.ts +58 -0
  161. package/dist/mcp/server.d.ts.map +1 -0
  162. package/dist/mcp/server.js +538 -0
  163. package/dist/mcp/tool-mapping.d.ts +58 -0
  164. package/dist/mcp/tool-mapping.d.ts.map +1 -0
  165. package/dist/mcp/tool-mapping.js +143 -0
  166. package/dist/mcp/tool-namespace.d.ts +40 -0
  167. package/dist/mcp/tool-namespace.d.ts.map +1 -0
  168. package/dist/mcp/tool-namespace.js +65 -0
  169. package/dist/native-scheme.d.ts +59 -0
  170. package/dist/native-scheme.d.ts.map +1 -0
  171. package/dist/native-scheme.js +465 -0
  172. package/dist/output.d.ts +28 -0
  173. package/dist/output.d.ts.map +1 -0
  174. package/dist/output.js +342 -0
  175. package/dist/package-version.d.ts +30 -0
  176. package/dist/package-version.d.ts.map +1 -0
  177. package/dist/package-version.js +47 -0
  178. package/dist/qr-terminal.d.ts +13 -0
  179. package/dist/qr-terminal.d.ts.map +1 -0
  180. package/dist/qr-terminal.js +45 -0
  181. package/dist/rpc/client.d.ts +137 -0
  182. package/dist/rpc/client.d.ts.map +1 -0
  183. package/dist/rpc/client.js +819 -0
  184. package/dist/scheme.d.ts +181 -0
  185. package/dist/scheme.d.ts.map +1 -0
  186. package/dist/scheme.js +321 -0
  187. package/dist/spki-pin.d.ts +3 -0
  188. package/dist/spki-pin.d.ts.map +1 -0
  189. package/dist/spki-pin.js +14 -0
  190. package/package.json +65 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `link()`/`waitForSession()` (issue #8): the bootstrap half of the client, so a test's
3
+ * `globalSetup` can pair a simulator/emulator without shelling out to `appduct link --open`.
4
+ * `link()` delegates to the same `mintLink` core `commands/link.ts` uses (`../link.js`) so the
5
+ * deep-link shape can't drift between the CLI and this package; `waitForSession()` mirrors
6
+ * `mcp/connect-tool.ts`'s `handleWaitForSessionTool`.
7
+ */
8
+ import { RPC_METHODS } from "@appduct/shared";
9
+ import {} from "../cli/open-target.js";
10
+ import { resolveStateDir } from "../daemon/state-dir.js";
11
+ import { mintLink } from "../link.js";
12
+ import { openDaemonStream, DaemonRpcError } from "../rpc/client.js";
13
+ import { makeAppClient } from "./app-client.js";
14
+ import { AppductError, toAppductError } from "./errors.js";
15
+ const DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS = 120_000;
16
+ /** Mints a session link via `link.create` and composes the deep link. With `target` set, delivers
17
+ * it directly to a booted emulator/simulator instead of leaving delivery to the caller. */
18
+ export const link = async (options = {}) => {
19
+ try {
20
+ return await mintLink({
21
+ stateDir: resolveStateDir(options.stateDir),
22
+ spawn: options.spawn,
23
+ autoSpawn: options.autoSpawn,
24
+ ttlSeconds: options.ttlSeconds,
25
+ target: options.target,
26
+ device: options.device,
27
+ bundleId: options.bundleId,
28
+ relaunch: options.relaunch,
29
+ scheme: options.scheme,
30
+ cwd: options.cwd,
31
+ exec: options.exec,
32
+ env: options.env,
33
+ schemeEnv: options.schemeEnv,
34
+ });
35
+ }
36
+ catch (error) {
37
+ throw toAppductError(error);
38
+ }
39
+ };
40
+ /** Waits for `sessionId` (from {@link link}) to be claimed by a device, then returns a connected
41
+ * {@link AppClient} for it — resolves immediately if it's already claimed. */
42
+ export const waitForSession = async (sessionId, options = {}) => {
43
+ const stateDir = resolveStateDir(options.stateDir);
44
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS;
45
+ let stream;
46
+ try {
47
+ stream = await openDaemonStream({ stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
48
+ }
49
+ catch (error) {
50
+ throw toAppductError(error);
51
+ }
52
+ try {
53
+ // The session may already be claimed by the time this runs — resolve immediately rather than
54
+ // waiting for an event that already happened.
55
+ try {
56
+ await stream.call(RPC_METHODS.sessionsDescribe, { selector: sessionId });
57
+ return makeAppClient(stream, sessionId);
58
+ }
59
+ catch (error) {
60
+ if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") {
61
+ throw error;
62
+ }
63
+ }
64
+ await new Promise((resolve, reject) => {
65
+ let settled = false;
66
+ const settle = (fn) => {
67
+ if (settled) {
68
+ return;
69
+ }
70
+ settled = true;
71
+ clearTimeout(timer);
72
+ unsubscribeNotification();
73
+ unsubscribeClose();
74
+ fn();
75
+ };
76
+ const timer = setTimeout(() => {
77
+ settle(() => reject(new AppductError("timeout", `Timed out after ${timeoutMs}ms waiting for session "${sessionId}" to be claimed.`)));
78
+ }, timeoutMs);
79
+ const unsubscribeClose = stream.onClose(() => {
80
+ settle(() => reject(new AppductError("connection_error", `The connection to the Appduct daemon closed while waiting for session "${sessionId}" to be claimed.`)));
81
+ });
82
+ // Registered *before* `events.subscribe` is sent (not after awaiting it) — a
83
+ // `session_claimed` notification that arrives in the same TCP chunk as the subscribe
84
+ // response is dispatched synchronously within that chunk's processing, before an `await`
85
+ // continuation below would get a chance to run; registering the listener first means it's
86
+ // always in place in time to see it.
87
+ const unsubscribeNotification = stream.onNotification((payload) => {
88
+ if (settled) {
89
+ return;
90
+ }
91
+ const event = payload;
92
+ if (event.kind === "session_claimed" && event.sessionId === sessionId) {
93
+ settle(resolve);
94
+ }
95
+ });
96
+ stream
97
+ .call(RPC_METHODS.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] })
98
+ .catch((error) => {
99
+ settle(() => reject(toAppductError(error)));
100
+ });
101
+ });
102
+ return makeAppClient(stream, sessionId);
103
+ }
104
+ catch (error) {
105
+ stream.close();
106
+ throw toAppductError(error);
107
+ }
108
+ };
@@ -0,0 +1,17 @@
1
+ import { type SpawnFn } from "../rpc/client.js";
2
+ import { type AppClient, type ToolMap } from "./app-client.js";
3
+ export type ConnectOptions = {
4
+ /** Session id or alias; omitted selects the sole active/suspended session (rejects with
5
+ * `"no_session"`/`"ambiguous_session"` otherwise — the same rule the CLI/MCP surfaces use). */
6
+ selector?: string;
7
+ /** Defaults to `APPDUCT_STATE_DIR`, else `~/.appduct` — the same resolution the CLI uses. */
8
+ stateDir?: string;
9
+ /** Injectable daemon-spawn seam (tests only); real callers never need this. */
10
+ spawn?: SpawnFn;
11
+ /** Auto-spawn a daemon on a missing connection. Defaults to `true`. */
12
+ autoSpawn?: boolean;
13
+ requestTimeoutMs?: number;
14
+ };
15
+ /** Opens a persistent connection to the daemon and resolves `options.selector` to one session. */
16
+ export declare const connect: <TTools = ToolMap>(options?: ConnectOptions) => Promise<AppClient<TTools>>;
17
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/client/connect.ts"],"names":[],"mappings":"AAUA,OAAO,EAAuC,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG9E,MAAM,MAAM,cAAc,GAAG;IAC3B;mGAC+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,OAAO,GAAU,MAAM,GAAG,OAAO,EAC5C,UAAS,cAAmB,KAC3B,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CA0B3B,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `connect()` (issue #8): the entry point for a test runner that wants to drive an already-claimed
3
+ * session without shelling out to the CLI. Auto-spawns the daemon (same seam `rpc/client.ts` gives
4
+ * the CLI/MCP server) and resolves `selector` up front via `sessions.describe` so a bad/ambiguous
5
+ * selector fails fast with a typed {@link AppductError} rather than surfacing on the first
6
+ * `call()`.
7
+ */
8
+ import { RPC_METHODS } from "@appduct/shared";
9
+ import { resolveStateDir } from "../daemon/state-dir.js";
10
+ import { openDaemonStream } from "../rpc/client.js";
11
+ import { makeAppClient } from "./app-client.js";
12
+ import { toAppductError } from "./errors.js";
13
+ /** Opens a persistent connection to the daemon and resolves `options.selector` to one session. */
14
+ export const connect = async (options = {}) => {
15
+ const stateDir = resolveStateDir(options.stateDir);
16
+ let stream;
17
+ try {
18
+ stream = await openDaemonStream({
19
+ stateDir,
20
+ spawn: options.spawn,
21
+ autoSpawn: options.autoSpawn,
22
+ requestTimeoutMs: options.requestTimeoutMs,
23
+ });
24
+ }
25
+ catch (error) {
26
+ throw toAppductError(error);
27
+ }
28
+ try {
29
+ const detail = await stream.call(RPC_METHODS.sessionsDescribe, {
30
+ selector: options.selector,
31
+ });
32
+ return makeAppClient(stream, detail.sessionId);
33
+ }
34
+ catch (error) {
35
+ stream.close();
36
+ throw toAppductError(error);
37
+ }
38
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Client-facing error type (issue #8): the wire `error.data.type` (ARCHITECTURE.md §5) surfaced as
3
+ * a typed exception instead of a `DaemonRpcError`/stderr string a test would have to pattern-match.
4
+ * `type` preserves the wire value verbatim — the same non-rewrapping rule `errors.ts`'s
5
+ * `toCliError` already enforces for the CLI — so `expect(...).rejects.toMatchObject({ type:
6
+ * "policy_denied" })` works without this package's internal error classes leaking into test code.
7
+ */
8
+ import type { ErrorType } from "@appduct/shared";
9
+ /** The wire `ErrorType` union, plus three client-only buckets for conditions the daemon never
10
+ * produced: `"connection_error"` (daemon unreachable/unspawnable, or the connection dropped mid-
11
+ * wait), `"timeout"` (a client-side wait — `waitForEvent`, `waitForSession` — gave up; distinct
12
+ * from the wire `"tool_timeout"` so a test can tell "the app's tool timed out" apart from "my wait
13
+ * gave up"), and `"client_error"` (anything else, e.g. a failed emulator/simulator delivery from
14
+ * {@link link}, or a malformed JSON-RPC error with no `data.type`). */
15
+ export type AppductErrorType = ErrorType | "connection_error" | "timeout" | "client_error";
16
+ export declare class AppductError extends Error {
17
+ readonly type: AppductErrorType;
18
+ readonly details?: unknown;
19
+ /** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
20
+ readonly code?: number;
21
+ constructor(type: AppductErrorType, message: string, details?: unknown, code?: number);
22
+ }
23
+ /** Converts any error thrown by the RPC layer into a {@link AppductError}, preserving the wire
24
+ * `data.type` verbatim when present. Idempotent on an already-converted error. */
25
+ export declare const toAppductError: (error: unknown) => AppductError;
26
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD;;;;;uEAKuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,GAAG,cAAc,CAAC;AAE3F,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEX,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM;CAOtF;AAED;kFACkF;AAClF,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,YAiC/C,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { isAppductCliError } from "../errors.js";
2
+ import { DaemonRpcError, isDaemonUnreachableError } from "../rpc/client.js";
3
+ export class AppductError extends Error {
4
+ type;
5
+ details;
6
+ /** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
7
+ code;
8
+ constructor(type, message, details, code) {
9
+ super(message);
10
+ this.name = "AppductError";
11
+ this.type = type;
12
+ this.details = details;
13
+ this.code = code;
14
+ }
15
+ }
16
+ /** Converts any error thrown by the RPC layer into a {@link AppductError}, preserving the wire
17
+ * `data.type` verbatim when present. Idempotent on an already-converted error. */
18
+ export const toAppductError = (error) => {
19
+ if (error instanceof AppductError) {
20
+ return error;
21
+ }
22
+ const withCause = (result) => {
23
+ result.cause = error;
24
+ return result;
25
+ };
26
+ if (error instanceof DaemonRpcError) {
27
+ const type = error.data?.type ?? "client_error";
28
+ return withCause(new AppductError(type, error.message, error.data?.details, error.code));
29
+ }
30
+ if (isDaemonUnreachableError(error)) {
31
+ return withCause(new AppductError("connection_error", error instanceof Error ? error.message : "The Appduct daemon is unreachable."));
32
+ }
33
+ if (isAppductCliError(error)) {
34
+ return withCause(new AppductError("client_error", error.message, error.details));
35
+ }
36
+ if (error instanceof Error) {
37
+ return withCause(new AppductError("client_error", error.message));
38
+ }
39
+ return new AppductError("client_error", "An unexpected error occurred.", error);
40
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `appduct/client` (issue #8): a first-class programmatic client for test runners — a thin typed
3
+ * wrapper over the same daemon RPC the CLI and MCP server use, so a Jest/Vitest/Detox spec can
4
+ * `connect()`/`call()` instead of spawning `appduct invoke ... --json` and parsing stdout.
5
+ *
6
+ * ```ts
7
+ * import { connect } from "appduct/client";
8
+ *
9
+ * const app = await connect();
10
+ * const { total } = await app.call("sum", { a: 2, b: 3 });
11
+ * await app.close();
12
+ * ```
13
+ */
14
+ export { connect, type ConnectOptions } from "./connect.js";
15
+ export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
16
+ export { AppductError, type AppductErrorType } from "./errors.js";
17
+ export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
18
+ export type { AgentEndpoint, ErrorType, ToolDescriptor } from "@appduct/shared";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACrH,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,SAAS,EACT,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,OAAO,EACP,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `appduct/client` (issue #8): a first-class programmatic client for test runners — a thin typed
3
+ * wrapper over the same daemon RPC the CLI and MCP server use, so a Jest/Vitest/Detox spec can
4
+ * `connect()`/`call()` instead of spawning `appduct invoke ... --json` and parsing stdout.
5
+ *
6
+ * ```ts
7
+ * import { connect } from "appduct/client";
8
+ *
9
+ * const app = await connect();
10
+ * const { total } = await app.call("sum", { a: 2, b: 3 });
11
+ * await app.close();
12
+ * ```
13
+ */
14
+ export { connect } from "./connect.js";
15
+ export { link, waitForSession } from "./bootstrap.js";
16
+ export { AppductError } from "./errors.js";
@@ -0,0 +1,22 @@
1
+ import type { CliResult, DaemonRunCommandData, DaemonStartCommandData, DaemonStatusCommandData, DaemonStopCommandData } from "../cli/result-types.js";
2
+ import type { Clock } from "../cli/types.js";
3
+ import { type SpawnFn } from "../rpc/client.js";
4
+ export type DaemonCommandContext = {
5
+ stateDir: string;
6
+ clock?: Clock;
7
+ spawn?: SpawnFn;
8
+ warn?: (message: string) => void;
9
+ /** Test seam: the version `daemon status` compares the daemon against; defaults to this
10
+ * package's own version. */
11
+ clientVersion?: string;
12
+ };
13
+ export type DaemonRunHostedResult = {
14
+ result: CliResult<DaemonRunCommandData>;
15
+ completion: Promise<void>;
16
+ stop: () => void;
17
+ };
18
+ export declare const handleDaemonRunCommand: (context: DaemonCommandContext) => Promise<DaemonRunHostedResult>;
19
+ export declare const handleDaemonStartCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStartCommandData>>;
20
+ export declare const handleDaemonStopCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStopCommandData>>;
21
+ export declare const handleDaemonStatusCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStatusCommandData>>;
22
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/commands/daemon.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAM7C,OAAO,EAAuD,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAErG,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;gCAC4B;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,SAAS,CAAC,oBAAoB,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,sBAAsB,GACjC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,qBAAqB,CAiC/B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAiB3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAyB1C,CAAC;AAQF,eAAO,MAAM,yBAAyB,GACpC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,uBAAuB,CAAC,CA2D5C,CAAC"}
@@ -0,0 +1,128 @@
1
+ import { RPC_METHODS } from "@appduct/shared";
2
+ import { startDaemon } from "../daemon/daemon.js";
3
+ import { DaemonAlreadyRunningError, readPidFromFile } from "../daemon/pidfile.js";
4
+ import { getStateDirPaths } from "../daemon/state-dir.js";
5
+ import { connectionError } from "../errors.js";
6
+ import { getPackageVersion } from "../package-version.js";
7
+ import { callDaemon, isDaemonUnreachableError, isSameVersion } from "../rpc/client.js";
8
+ export const handleDaemonRunCommand = async (context) => {
9
+ let daemon;
10
+ try {
11
+ daemon = await startDaemon({
12
+ stateDir: context.stateDir,
13
+ clock: context.clock,
14
+ warn: context.warn,
15
+ });
16
+ }
17
+ catch (error) {
18
+ if (error instanceof DaemonAlreadyRunningError) {
19
+ throw connectionError(error.message, { pid: error.pid });
20
+ }
21
+ throw error;
22
+ }
23
+ return {
24
+ result: {
25
+ ok: true,
26
+ data: {
27
+ daemon: {
28
+ pid: process.pid,
29
+ state_dir: daemon.paths.root,
30
+ socket_path: daemon.paths.socketPath,
31
+ },
32
+ },
33
+ },
34
+ completion: daemon.exited,
35
+ stop: () => {
36
+ void daemon.shutdown();
37
+ },
38
+ };
39
+ };
40
+ export const handleDaemonStartCommand = async (context) => {
41
+ const status = await callDaemon(RPC_METHODS.daemonStatus, {}, { stateDir: context.stateDir, autoSpawn: true, spawn: context.spawn });
42
+ return {
43
+ ok: true,
44
+ data: {
45
+ daemon: {
46
+ pid: status.pid,
47
+ wss_port: status.wssPort,
48
+ started_at: status.startedAt,
49
+ },
50
+ },
51
+ };
52
+ };
53
+ export const handleDaemonStopCommand = async (context) => {
54
+ try {
55
+ await callDaemon(RPC_METHODS.daemonShutdown, {}, { stateDir: context.stateDir, autoSpawn: false });
56
+ return { ok: true, data: { daemon: { ok: true, method: "rpc" } } };
57
+ }
58
+ catch (error) {
59
+ if (!isDaemonUnreachableError(error)) {
60
+ throw error;
61
+ }
62
+ const paths = getStateDirPaths(context.stateDir);
63
+ const pid = await readPidFromFile(paths.pidFilePath);
64
+ if (pid === undefined) {
65
+ throw connectionError("No Appduct daemon is running.");
66
+ }
67
+ process.kill(pid, "SIGTERM");
68
+ return { ok: true, data: { daemon: { ok: true, method: "sigterm" } } };
69
+ }
70
+ };
71
+ /** Drops the entries whose value is `undefined`, so spreading the result adds only keys that
72
+ * carry an actual answer. */
73
+ const definedOnly = (fields) => {
74
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
75
+ };
76
+ export const handleDaemonStatusCommand = async (context) => {
77
+ const status = await callDaemon(RPC_METHODS.daemonStatus, {}, { stateDir: context.stateDir, autoSpawn: true, spawn: context.spawn });
78
+ // The wire type declares the retention fields required, as it must for any daemon built from
79
+ // this tree — but the daemon on the other end of the socket may predate them, and a running
80
+ // daemon outlives the CLI upgrade that would replace it. Reading them through a partial view
81
+ // keeps that possibility in the types instead of in a comment, and leaves each one `undefined`
82
+ // rather than `0` when it was never reported.
83
+ const retention = status.audit;
84
+ const clientVersion = context.clientVersion ?? getPackageVersion();
85
+ // Guarded the same way the version check guards it (`rpc/client.ts`): a `daemon.status` without
86
+ // `sessions` would otherwise throw from the middle of the command that exists to diagnose a
87
+ // daemon — the one command that must keep working against a daemon it does not fully understand.
88
+ const sessionCount = Array.isArray(status.sessions) ? status.sessions.length : 0;
89
+ const pendingLinkCount = typeof status.pendingLinks === "number" ? status.pendingLinks : 0;
90
+ // Names the same cost the restart path names (`rpc/client.ts`), so the diagnosis an operator
91
+ // reads here and the error they hit on the next command tell one consistent story: an unclaimed
92
+ // link is destroyed by a restart just as surely as a session is.
93
+ const atStake = `${sessionCount} session(s) and ${pendingLinkCount} unclaimed link(s)`;
94
+ const warning = isSameVersion(status.version, clientVersion)
95
+ ? undefined
96
+ : `Version drift: this daemon is ${status.version} but the CLI is ${clientVersion}. ` +
97
+ `Newer RPC methods will fail against it — run "appduct daemon stop" (this drops ${atStake}) ` +
98
+ `and the next command will start a matching daemon, or pass "--daemon-restart" to any command to replace it in place.`;
99
+ return {
100
+ ok: true,
101
+ data: {
102
+ warning,
103
+ daemon: {
104
+ version: status.version,
105
+ pid: status.pid,
106
+ started_at: status.startedAt,
107
+ wss_port: status.wssPort,
108
+ pinned_keys: status.pinnedKeys,
109
+ session_count: sessionCount,
110
+ },
111
+ policy: status.policy,
112
+ audit: {
113
+ path: status.audit.path,
114
+ failed_writes: status.audit.failedWrites,
115
+ // Spread so an unreported field is a *missing key*, not a key holding `undefined`. The two
116
+ // serialize identically, but only the first is honestly shaped: a caller can ask whether
117
+ // the daemon answered, and `--json` consumers see the same absence in-process as on the
118
+ // wire.
119
+ ...definedOnly({
120
+ failed_prunes: retention.failedPrunes,
121
+ retention_days: retention.retentionDays,
122
+ files: retention.files,
123
+ bytes: retention.bytes,
124
+ }),
125
+ },
126
+ },
127
+ };
128
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `appduct doctor <artifact>` (docs/tasks/08-appduct-doctor.md): the release-gate replacement
3
+ * for the runtime `debuggable` check removed elsewhere in opt-in hardening — see
4
+ * `artifact-inspect.ts`'s doc comment for the detection strategy and why it never collapses a
5
+ * missing-tool or unreadable-artifact case into "absent".
6
+ *
7
+ * This handler owns only the CLI-facing concerns: option validation (mutually exclusive
8
+ * `--assert-present`/`--assert-absent`) and turning a failed assertion into a distinct
9
+ * {@link assertionError} exit, separate from `inspectionError`'s "couldn't tell" exit. All artifact
10
+ * inspection itself lives in `artifact-inspect.ts`, unit-testable without going through the CLI.
11
+ */
12
+ import type { CliResult, DoctorCommandData } from "../cli/result-types.js";
13
+ import { type ExecBufferFn } from "../artifact-inspect.js";
14
+ export type DoctorCommandOptions = {
15
+ artifactPath?: string;
16
+ assertPresent?: boolean;
17
+ assertAbsent?: boolean;
18
+ };
19
+ export type DoctorCommandContext = {
20
+ exec?: ExecBufferFn;
21
+ };
22
+ export declare const handleDoctorCommand: (options: DoctorCommandOptions, context?: DoctorCommandContext) => Promise<CliResult<DoctorCommandData>>;
23
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG5E,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,UAAS,oBAAyB,KACjC,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAyCtC,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `appduct doctor <artifact>` (docs/tasks/08-appduct-doctor.md): the release-gate replacement
3
+ * for the runtime `debuggable` check removed elsewhere in opt-in hardening — see
4
+ * `artifact-inspect.ts`'s doc comment for the detection strategy and why it never collapses a
5
+ * missing-tool or unreadable-artifact case into "absent".
6
+ *
7
+ * This handler owns only the CLI-facing concerns: option validation (mutually exclusive
8
+ * `--assert-present`/`--assert-absent`) and turning a failed assertion into a distinct
9
+ * {@link assertionError} exit, separate from `inspectionError`'s "couldn't tell" exit. All artifact
10
+ * inspection itself lives in `artifact-inspect.ts`, unit-testable without going through the CLI.
11
+ */
12
+ import { inspectArtifact } from "../artifact-inspect.js";
13
+ import { assertionError, usageError } from "../errors.js";
14
+ export const handleDoctorCommand = async (options, context = {}) => {
15
+ if (!options.artifactPath) {
16
+ throw usageError("Usage: appduct doctor <path-to-artifact> [--assert-present | --assert-absent]");
17
+ }
18
+ if (options.assertPresent && options.assertAbsent) {
19
+ throw usageError("--assert-present and --assert-absent are mutually exclusive.");
20
+ }
21
+ const inspection = await inspectArtifact(options.artifactPath, { exec: context.exec });
22
+ const data = {
23
+ artifact: options.artifactPath,
24
+ platform: inspection.platform,
25
+ format: inspection.format,
26
+ present: inspection.present,
27
+ signals: inspection.signals,
28
+ };
29
+ if (options.assertPresent && !inspection.present) {
30
+ throw assertionError(`Expected Appduct to be present in "${options.artifactPath}", but no inclusion signal was found.`, data);
31
+ }
32
+ if (options.assertAbsent && inspection.present) {
33
+ throw assertionError(`Expected Appduct to be absent from "${options.artifactPath}", but found: ${inspection.signals.join(", ")}.`, data);
34
+ }
35
+ if (options.assertPresent || options.assertAbsent) {
36
+ data.assertion = {
37
+ expected: options.assertPresent ? "present" : "absent",
38
+ holds: true,
39
+ };
40
+ }
41
+ return { ok: true, data };
42
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` over a persistent
3
+ * connection and streams notifications until interrupted (Ctrl-C) or the connection drops. This is
4
+ * the one command whose output isn't a single rendered `CliResult` — each event is written as it
5
+ * arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
6
+ * with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
7
+ *
8
+ * `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
9
+ * of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
10
+ * is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
11
+ * "hosted command" here just completes immediately instead of running until interrupted.
12
+ */
13
+ import { type EventNotification } from "@appduct/shared";
14
+ import type { CliResult } from "../cli/result-types.js";
15
+ import { type SpawnFn } from "../rpc/client.js";
16
+ export type EventsCommandOptions = {
17
+ selector?: string;
18
+ /** Exclusive lower bound on `EventNotification.seq`; switches this command to a one-shot
19
+ * `events.since` pull instead of a live `events.subscribe` stream. */
20
+ since?: number;
21
+ };
22
+ export type EventsCommandContext = {
23
+ stateDir: string;
24
+ spawn?: SpawnFn;
25
+ onEvent: (event: EventNotification) => void;
26
+ /** `--since` mode only: called once with the pull's resulting cursor, so a scripted caller
27
+ * doesn't have to reconstruct it by maxing `seq` over the printed lines (impossible when the
28
+ * response is empty — the whole point of a cursor is knowing where to resume from either way). */
29
+ onCursor?: (cursor: number) => void;
30
+ };
31
+ export type EventsHostedResult = {
32
+ result: CliResult<never>;
33
+ completion: Promise<void>;
34
+ stop: () => void;
35
+ };
36
+ export declare const handleEventsCommand: (options: EventsCommandOptions, context: EventsCommandContext) => Promise<EventsHostedResult>;
37
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/commands/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAe,KAAK,iBAAiB,EAAsD,MAAM,iBAAiB,CAAC;AAE1H,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC5C;;sGAEkG;IAClG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AA4BF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,SAAS,oBAAoB,KAC5B,OAAO,CAAC,kBAAkB,CAkD5B,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` over a persistent
3
+ * connection and streams notifications until interrupted (Ctrl-C) or the connection drops. This is
4
+ * the one command whose output isn't a single rendered `CliResult` — each event is written as it
5
+ * arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
6
+ * with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
7
+ *
8
+ * `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
9
+ * of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
10
+ * is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
11
+ * "hosted command" here just completes immediately instead of running until interrupted.
12
+ */
13
+ import { RPC_METHODS } from "@appduct/shared";
14
+ import { openDaemonStream } from "../rpc/client.js";
15
+ const NEVER_RESULT = { ok: true, data: undefined };
16
+ const handleEventsSinceCommand = async (options, context) => {
17
+ const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
18
+ try {
19
+ const since = await stream.call(RPC_METHODS.eventsSince, {
20
+ selector: options.selector,
21
+ since: options.since,
22
+ });
23
+ for (const event of since.events) {
24
+ context.onEvent(event);
25
+ }
26
+ context.onCursor?.(since.cursor);
27
+ }
28
+ finally {
29
+ stream.close();
30
+ }
31
+ return { result: NEVER_RESULT, completion: Promise.resolve(), stop: () => { } };
32
+ };
33
+ export const handleEventsCommand = async (options, context) => {
34
+ if (options.since !== undefined) {
35
+ return handleEventsSinceCommand(options, context);
36
+ }
37
+ const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
38
+ try {
39
+ await stream.call(RPC_METHODS.eventsSubscribe, {
40
+ sessionSelector: options.selector,
41
+ });
42
+ }
43
+ catch (error) {
44
+ stream.close();
45
+ throw error;
46
+ }
47
+ let resolveCompletion;
48
+ const completion = new Promise((resolve) => {
49
+ resolveCompletion = resolve;
50
+ });
51
+ const unsubscribeNotifications = stream.onNotification((payload) => {
52
+ context.onEvent(payload);
53
+ });
54
+ let unsubscribeClose = () => { };
55
+ let stopped = false;
56
+ const stop = () => {
57
+ if (stopped) {
58
+ return;
59
+ }
60
+ stopped = true;
61
+ unsubscribeNotifications();
62
+ unsubscribeClose();
63
+ stream.close();
64
+ resolveCompletion();
65
+ };
66
+ // The daemon going away mid-stream (shutdown, crash) must end the command gracefully rather than
67
+ // hang forever waiting for a Ctrl-C that will never resolve anything.
68
+ unsubscribeClose = stream.onClose(stop);
69
+ return {
70
+ // Never rendered: the reporter passed to `executeHostedCommand` suppresses the bootstrap
71
+ // render for this command (see the doc comment above).
72
+ result: NEVER_RESULT,
73
+ completion,
74
+ stop,
75
+ };
76
+ };