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,275 @@
1
+ import { handleDaemonRunCommand, handleDaemonStartCommand, handleDaemonStatusCommand, handleDaemonStopCommand, } from "../commands/daemon.js";
2
+ import { handleDoctorCommand } from "../commands/doctor.js";
3
+ import { handleEventsCommand } from "../commands/events.js";
4
+ import { handleInitCommand } from "../commands/init.js";
5
+ import { handleInvokeCommand } from "../commands/invoke.js";
6
+ import { handleKeygenCommand } from "../commands/keygen.js";
7
+ import { handleLinkCommand } from "../commands/link.js";
8
+ import { handleLsCommand } from "../commands/ls.js";
9
+ import { handleMcpCommand } from "../commands/mcp.js";
10
+ import { handleRevokeCommand } from "../commands/revoke.js";
11
+ import { handleToolsCommand } from "../commands/tools.js";
12
+ import { loadConfig } from "../daemon/config.js";
13
+ import { getStateDirPaths, resolveStateDir } from "../daemon/state-dir.js";
14
+ import { usageError } from "../errors.js";
15
+ import { renderEventLine, renderEventsCursorLine } from "../output.js";
16
+ import { getPackageVersion } from "../package-version.js";
17
+ import { ensureDaemonVersionMatches } from "../rpc/client.js";
18
+ import { parseJsonInputOption, parseNonNegativeIntegerOption, parsePositiveIntegerOption, splitOptionalSelector, splitOptionalSelectorAndTarget, splitSelectorAndRequiredTarget, } from "./command-options.js";
19
+ import { createCli } from "./create-cli.js";
20
+ import { executeCommand, executeHostedCommand } from "./runner.js";
21
+ import { systemClock } from "./types.js";
22
+ /** `APPDUCT_DAEMON_RESTART=1` forces a version-mismatch restart for one run — the env-var form
23
+ * of `--daemon-restart`, so an MCP launch config (which passes no CLI flags) can opt in. */
24
+ const DAEMON_RESTART_ENV = "APPDUCT_DAEMON_RESTART";
25
+ const isEnvTruthy = (value) => {
26
+ return value === "1" || value?.toLowerCase() === "true";
27
+ };
28
+ export const runCli = async (argv, options = {}) => {
29
+ const writers = {
30
+ stdout: options.stdout ?? process.stdout,
31
+ stderr: options.stderr ?? process.stderr,
32
+ };
33
+ const clock = options.clock ?? systemClock;
34
+ const cli = createCli();
35
+ try {
36
+ cli.parse(["node", "appduct", ...argv], {
37
+ run: false,
38
+ });
39
+ }
40
+ catch (error) {
41
+ return executeCommand("cli", () => {
42
+ throw error;
43
+ }, {
44
+ json: argv.includes("--json"),
45
+ color: !argv.includes("--no-color"),
46
+ stdout: writers.stdout,
47
+ stderr: writers.stderr,
48
+ clock,
49
+ });
50
+ }
51
+ const matchedCommand = cli.matchedCommandName;
52
+ const parsedOptions = cli.options;
53
+ const parsedArgs = cli.args;
54
+ if (parsedOptions.help || parsedOptions.version) {
55
+ return 0;
56
+ }
57
+ if (!matchedCommand) {
58
+ if (parsedArgs[0]) {
59
+ return executeCommand("cli", () => {
60
+ throw usageError(`Unknown command "${parsedArgs[0]}".`);
61
+ }, {
62
+ json: Boolean(parsedOptions.json),
63
+ color: parsedOptions.color !== false,
64
+ stdout: writers.stdout,
65
+ stderr: writers.stderr,
66
+ clock,
67
+ });
68
+ }
69
+ cli.outputHelp();
70
+ return 0;
71
+ }
72
+ const json = Boolean(parsedOptions.json);
73
+ const color = parsedOptions.color !== false;
74
+ const io = { json, color, stdout: writers.stdout, stderr: writers.stderr, clock };
75
+ const stateDir = resolveStateDir(typeof parsedOptions.stateDir === "string" ? parsedOptions.stateDir : undefined);
76
+ // --- daemon/CLI version drift (issue #30, ARCHITECTURE.md §4 "Version drift") ------------------
77
+ let forceRestart;
78
+ /** Memoized so the config read behind `restartDaemonOnVersionMismatch` happens at most once. */
79
+ const resolveForceRestart = () => {
80
+ forceRestart ??= (async () => {
81
+ let configuredForce = false;
82
+ try {
83
+ configuredForce = (await loadConfig(getStateDirPaths(stateDir))).restartDaemonOnVersionMismatch;
84
+ }
85
+ catch {
86
+ // An unreadable/invalid `config.json` must not turn every command into a config error just
87
+ // because one optional knob lives there; the daemon reports the real problem when it starts.
88
+ }
89
+ // An explicit flag wins outright, in both directions: `--no-daemon-restart` is how an
90
+ // operator overrules a `restartDaemonOnVersionMismatch: true` in their config (or a
91
+ // `APPDUCT_DAEMON_RESTART` exported by a wrapper script) for one command, and silently
92
+ // ignoring it would be the worst kind of surprise for a knob that decides whether their
93
+ // connected devices survive.
94
+ const flag = typeof parsedOptions.daemonRestart === "boolean" ? parsedOptions.daemonRestart : undefined;
95
+ return flag ?? (isEnvTruthy(process.env[DAEMON_RESTART_ENV]) || configuredForce);
96
+ })();
97
+ return forceRestart;
98
+ };
99
+ /**
100
+ * Where the "the running daemon is newer than this client" notice goes. `--json` promises one
101
+ * machine-readable object and nothing else, so in that mode the notice is dropped rather than
102
+ * dribbled onto stderr where it would corrupt a script that captures both streams — the check
103
+ * still behaves identically, it just says nothing. (A structured warning channel would be the
104
+ * better answer; the runner has none today.)
105
+ */
106
+ const cliWarning = json ? () => { } : (message) => void writers.stderr.write(message);
107
+ const versionCheckFor = async (onWarning) => {
108
+ return {
109
+ clientVersion: getPackageVersion(),
110
+ forceRestart: await resolveForceRestart(),
111
+ onWarning,
112
+ };
113
+ };
114
+ /**
115
+ * Wraps a command handler so the daemon's version is verified once, before the command's first
116
+ * RPC. `autoSpawn: false`: with nothing listening there is no drift to find, and any daemon this
117
+ * process spawns afterwards is its own build. Applied to every command that talks to the daemon
118
+ * except `daemon run` (it *is* the daemon), `daemon status` (warns instead — see
119
+ * `commands/daemon.ts`) and `daemon stop` (already the remedy); `keygen`/`doctor` never open a
120
+ * daemon connection at all.
121
+ */
122
+ const guarded = (handler) => {
123
+ return async () => {
124
+ await ensureDaemonVersionMatches({
125
+ stateDir,
126
+ autoSpawn: false,
127
+ checkVersion: await versionCheckFor(cliWarning),
128
+ });
129
+ return handler();
130
+ };
131
+ };
132
+ switch (matchedCommand) {
133
+ case "init":
134
+ return executeCommand("init", () => handleInitCommand({
135
+ scheme: typeof parsedOptions.scheme === "string" ? parsedOptions.scheme : undefined,
136
+ force: Boolean(parsedOptions.force),
137
+ },
138
+ // `init` never reads the state dir, but it must know which directory it is so it can
139
+ // refuse to write a "safe to commit" project config into the daemon's own state.
140
+ { stateDir }), io);
141
+ case "keygen":
142
+ return executeCommand("keygen", () => handleKeygenCommand({
143
+ out: typeof parsedOptions.out === "string" ? parsedOptions.out : undefined,
144
+ force: Boolean(parsedOptions.force),
145
+ }, { stateDir }), io);
146
+ case "link": {
147
+ return executeCommand("link", guarded(() => handleLinkCommand({
148
+ ttlSeconds: parsePositiveIntegerOption(parsedOptions.ttl, "--ttl"),
149
+ scheme: typeof parsedOptions.scheme === "string" ? parsedOptions.scheme : undefined,
150
+ open: typeof parsedOptions.open === "string" ? parsedOptions.open : undefined,
151
+ device: typeof parsedOptions.device === "string" ? parsedOptions.device : undefined,
152
+ // cac camelCases `--bundle-id`; the dashed spelling is kept as a fallback so a
153
+ // parser change can't silently drop the flag.
154
+ bundleId: typeof parsedOptions.bundleId === "string"
155
+ ? parsedOptions.bundleId
156
+ : typeof parsedOptions["bundle-id"] === "string"
157
+ ? parsedOptions["bundle-id"]
158
+ : undefined,
159
+ // Left `undefined` when absent rather than coerced to `false`, so that
160
+ // "--relaunch only applies with --open ios-device" fires on the flag actually being
161
+ // passed and not on every `link` invocation.
162
+ relaunch: parsedOptions.relaunch === true ? true : undefined,
163
+ }, { stateDir })), {
164
+ ...io,
165
+ qr: Boolean(parsedOptions.qr),
166
+ });
167
+ }
168
+ case "ls":
169
+ return executeCommand("ls", guarded(() => handleLsCommand({ stateDir })), io);
170
+ case "tools": {
171
+ const { selector, target, selectorOrTarget } = splitOptionalSelectorAndTarget(parsedArgs, "tools [selector] [name]");
172
+ return executeCommand("tools", guarded(() => handleToolsCommand({ selector: selector ?? selectorOrTarget, name: target }, { stateDir })), { ...io, full: Boolean(parsedOptions.full) });
173
+ }
174
+ case "invoke": {
175
+ const { selector, target: tool } = splitSelectorAndRequiredTarget(parsedArgs, "invoke [selector] <tool> --input '<json>'");
176
+ // SIGINT cancels the in-flight tools.call rather than leaving it running unowned in the app
177
+ // (issue #9) — the listener is torn down once the command settles either way.
178
+ const cancelController = new AbortController();
179
+ const onSigint = () => cancelController.abort();
180
+ process.once("SIGINT", onSigint);
181
+ try {
182
+ return await executeCommand("invoke", guarded(() => handleInvokeCommand({
183
+ selector,
184
+ tool,
185
+ args: parseJsonInputOption(typeof parsedOptions.input === "string" ? parsedOptions.input : undefined),
186
+ timeoutMs: parsePositiveIntegerOption(parsedOptions.timeout, "--timeout"),
187
+ }, { stateDir }, cancelController.signal)), io);
188
+ }
189
+ finally {
190
+ process.off("SIGINT", onSigint);
191
+ }
192
+ }
193
+ case "revoke": {
194
+ const { selector } = splitOptionalSelector(parsedArgs, "revoke [selector]");
195
+ return executeCommand("revoke", guarded(() => handleRevokeCommand({ selector }, { stateDir })), io);
196
+ }
197
+ case "events": {
198
+ const { selector } = splitOptionalSelector(parsedArgs, "events [selector]");
199
+ const since = parseNonNegativeIntegerOption(parsedOptions.since, "--since");
200
+ const follow = Boolean(parsedOptions.follow);
201
+ return executeHostedCommand("events", guarded(() => {
202
+ // Deferred into the wrapped handler (rather than thrown directly in this case body,
203
+ // matching the codebase's existing lax convention for that) so `executeHostedCommand`'s
204
+ // own try/catch renders it as a normal usage_error instead of an uncaught rejection.
205
+ if (since !== undefined && follow) {
206
+ throw usageError('"--since" is a one-shot pull and cannot be combined with "--follow".');
207
+ }
208
+ return handleEventsCommand({ selector, since }, {
209
+ stateDir,
210
+ onEvent: (event) => {
211
+ writers.stdout.write(`${renderEventLine(event, { json, color })}\n`);
212
+ },
213
+ onCursor: (cursor) => {
214
+ writers.stdout.write(`${renderEventsCursorLine(cursor, { json, color })}\n`);
215
+ },
216
+ });
217
+ }), {
218
+ ...io,
219
+ reporter: {
220
+ kind: "interactive",
221
+ onEvent: () => { },
222
+ dispose: () => { },
223
+ },
224
+ });
225
+ }
226
+ case "mcp": {
227
+ return executeHostedCommand("mcp",
228
+ // Unlike every other command the check is threaded into the server itself, not run ahead
229
+ // of it: the MCP server is long-lived and auto-spawns its own daemon, so the check belongs
230
+ // on the startup stream that establishes the connection it keeps (ARCHITECTURE.md §9).
231
+ // The notice always goes to stderr here, `--json` or not: stdout carries MCP protocol
232
+ // frames, and stderr is this server's only log channel (ARCHITECTURE.md §9).
233
+ async () => handleMcpCommand({
234
+ stateDir,
235
+ scheme: typeof parsedOptions.scheme === "string" ? parsedOptions.scheme : undefined,
236
+ checkVersion: await versionCheckFor((message) => void writers.stderr.write(message)),
237
+ }), {
238
+ ...io,
239
+ reporter: {
240
+ kind: "interactive",
241
+ onEvent: () => { },
242
+ dispose: () => { },
243
+ },
244
+ });
245
+ }
246
+ case "doctor": {
247
+ return executeCommand("doctor", () => handleDoctorCommand({
248
+ artifactPath: parsedArgs[0],
249
+ assertPresent: Boolean(parsedOptions.assertPresent),
250
+ assertAbsent: Boolean(parsedOptions.assertAbsent),
251
+ }), io);
252
+ }
253
+ case "daemon": {
254
+ const action = parsedArgs[0];
255
+ switch (action) {
256
+ case "run":
257
+ return executeHostedCommand("daemon run", () => handleDaemonRunCommand({ stateDir, clock }), io);
258
+ case "start":
259
+ return executeCommand("daemon start", guarded(() => handleDaemonStartCommand({ stateDir, clock })), io);
260
+ case "stop":
261
+ return executeCommand("daemon stop", () => handleDaemonStopCommand({ stateDir, clock }), io);
262
+ case "status":
263
+ return executeCommand("daemon status", () => handleDaemonStatusCommand({ stateDir, clock }), io);
264
+ default:
265
+ return executeCommand("daemon", () => {
266
+ throw usageError(`The daemon command requires an action: run, start, stop, or status (got ${action === undefined ? "none" : `"${action}"`}).`);
267
+ }, io);
268
+ }
269
+ }
270
+ default:
271
+ return executeCommand(matchedCommand, () => {
272
+ throw usageError(`Unknown command "${matchedCommand}".`);
273
+ }, io);
274
+ }
275
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Emulator/simulator fast path (ARCHITECTURE.md §8 delivery path 1, §10 `link --open`): delivers a
3
+ * deep link to a booted Android emulator/device or iOS simulator without any human handling a QR
4
+ * code — this is the CI/agent path. All subprocess execution goes through the injectable
5
+ * {@link ExecFn} seam so every branch here is testable without `adb`/`xcrun` installed.
6
+ *
7
+ * {@link detectBootedTargets} is the same enumeration used *before* a target is chosen, so callers
8
+ * that were given no explicit target (notably `appduct_connect` over MCP) can deliver to the one
9
+ * obvious device instead of falling back to a QR code nobody is there to scan.
10
+ *
11
+ * A third, experimental target — `ios-device`, a paired physical iPhone/iPad via `xcrun devicectl`
12
+ * (issue #31) — is explicit opt-in only: it is never auto-detected, it needs the app's bundle id,
13
+ * and its link must carry the LAN address rather than `127.0.0.1` (see {@link usesLoopbackAddress}).
14
+ */
15
+ export type OpenTarget = "android" | "ios-sim" | "ios-device";
16
+ export declare const isOpenTarget: (value: string) => value is OpenTarget;
17
+ /** Human-readable list of every accepted target, for the "must be one of" validation messages the
18
+ * CLI, the shared `mintLink` core and `appduct_connect` each render in their own wording. */
19
+ export declare const OPEN_TARGETS: readonly OpenTarget[];
20
+ /**
21
+ * Whether a link delivered to this target reaches the daemon over loopback, and so must be minted
22
+ * with the `127.0.0.1` address override (`link.ts`, `mcp/connect-tool.ts`). True for `android`
23
+ * (`adb reverse` forwards the port onto the device) and `ios-sim` (the simulator shares the host's
24
+ * network stack); false for `ios-device` — a physical iPhone reaches the daemon only over the LAN,
25
+ * so such a link must carry the machine's real advertised address (issue #31).
26
+ */
27
+ export declare const usesLoopbackAddress: (target: OpenTarget) => boolean;
28
+ /**
29
+ * Whether an advertised address is one that only resolves back to the machine that minted the link.
30
+ * `daemon/address.ts` falls back to `127.0.0.1` when it cannot find a routable interface, and such
31
+ * a link handed to a physical phone points the phone at *itself*: the app connects to nothing, the
32
+ * session is never claimed, and `wait_for_session` blocks for its whole timeout with no clue why.
33
+ * The `ios-device` path checks this after minting and refuses rather than delivering.
34
+ */
35
+ export declare const isLoopbackAddress: (address: string) => boolean;
36
+ /** The one wording for that refusal, shared by the CLI and MCP paths. */
37
+ export declare const loopbackAddressMessage: (address: string) => string;
38
+ export type ExecResult = {
39
+ stdout: string;
40
+ stderr: string;
41
+ };
42
+ /** Injectable process-execution seam; tests stub this instead of shelling out to `adb`/`xcrun`. */
43
+ export type ExecFn = (command: string, args: string[]) => Promise<ExecResult>;
44
+ /** Real subprocess execution via `execFile` (no shell involved — argv is passed verbatim). */
45
+ export declare const defaultExec: ExecFn;
46
+ export type BootedSimulator = {
47
+ udid: string;
48
+ name: string;
49
+ };
50
+ export type PairedIosDevice = {
51
+ udid: string;
52
+ name: string;
53
+ };
54
+ /** The single wording naming both ways to supply a bundle id, so the CLI and MCP paths agree. */
55
+ export declare const MISSING_BUNDLE_ID_MESSAGE: string;
56
+ export declare const isValidBundleId: (value: string) => boolean;
57
+ export declare const invalidBundleIdMessage: (value: string) => string;
58
+ /** One device a link could be delivered to, resolved concretely enough to deliver without a
59
+ * second, racy lookup: `device` is an adb serial or a simulator udid. */
60
+ export type DetectedTarget = {
61
+ target: OpenTarget;
62
+ device: string;
63
+ /** Human-readable form for error messages and agent-facing notes. */
64
+ label: string;
65
+ };
66
+ export type TargetDetection = {
67
+ kind: "single";
68
+ detected: DetectedTarget;
69
+ } | {
70
+ kind: "none";
71
+ } | {
72
+ kind: "ambiguous";
73
+ candidates: DetectedTarget[];
74
+ };
75
+ export type DetectBootedTargetsOptions = {
76
+ exec?: ExecFn;
77
+ env?: NodeJS.ProcessEnv;
78
+ };
79
+ /**
80
+ * Enumerates every booted simulator and attached Android device so a caller with no explicit
81
+ * target can deliver to the single obvious one. `ANDROID_SERIAL`, when set, narrows the Android
82
+ * side to that serial — it is exactly the disambiguation the user has already made.
83
+ *
84
+ * Physical iPhones (`ios-device`) are deliberately *not* enumerated here (issue #31): a paired
85
+ * iPhone is often someone's personal phone rather than a test device, delivery to it may
86
+ * cold-launch the app, and it additionally needs a bundle id the caller has to supply. It is an
87
+ * explicit, opt-in target only.
88
+ */
89
+ export declare const detectBootedTargets: (options?: DetectBootedTargetsOptions) => Promise<TargetDetection>;
90
+ export type OpenTargetOptions = {
91
+ target: OpenTarget;
92
+ /** The composed `<scheme>:///?appduct=<payload>` deep link to deliver. */
93
+ deepLink: string;
94
+ /** The daemon's wss port; used for `adb reverse tcp:<port> tcp:<port>`. */
95
+ wssPort: number;
96
+ /** An adb device serial (`target: "android"`), a simulator udid (`target: "ios-sim"`) or a
97
+ * paired-device udid (`target: "ios-device"`). */
98
+ device?: string;
99
+ /** The installed app's bundle id. Required by (and only used for) `target: "ios-device"`. */
100
+ bundleId?: string;
101
+ /** `target: "ios-device"` only: terminate a running instance first (`--terminate-existing`)
102
+ * instead of launching over it. Off by default; see {@link deliverToOpenTarget}. */
103
+ relaunch?: boolean;
104
+ /** Defaults to `process.env`. */
105
+ env?: NodeJS.ProcessEnv;
106
+ exec?: ExecFn;
107
+ };
108
+ /** Delivers `deepLink` to a booted Android device/emulator, an iOS simulator, or (experimentally)
109
+ * a paired physical iOS device. Throws a clear, actionable error (missing tool, no booted device,
110
+ * ambiguous device, missing bundle id) rather than a raw one. */
111
+ export declare const deliverToOpenTarget: (options: OpenTargetOptions) => Promise<void>;
112
+ //# sourceMappingURL=open-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-target.d.ts","sourceRoot":"","sources":["../../src/cli/open-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAE9D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,KAAK,IAAI,UAErD,CAAC;AAEF;6FAC6F;AAC7F,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAAyC,CAAC;AAExF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,UAAU,KAAG,OAExD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,OAUnD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GAAI,SAAS,MAAM,KAAG,MAI3C,CAAC;AAEf,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,mGAAmG;AACnG,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAE9E,8FAA8F;AAC9F,eAAO,MAAM,WAAW,EAAE,MAWzB,CAAC;AAkCF,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAkH7D,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AA6F7D,iGAAiG;AACjG,eAAO,MAAM,yBAAyB,QAEwB,CAAC;AAe/D,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,OAAwC,CAAC;AAEzF,eAAO,MAAM,sBAAsB,GAAI,OAAO,MAAM,KAAG,MAEzC,CAAC;AAiLf;yEACyE;AACzE,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,cAAc,EAAE,CAAA;CAAE,CAAC;AAcxD,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAS,0BAA+B,KACvC,OAAO,CAAC,eAAe,CAiCzB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,UAAU,CAAC;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB;sDACkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;wFACoF;IACpF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;iEAEiE;AACjE,eAAO,MAAM,mBAAmB,GAAU,SAAS,iBAAiB,KAAG,OAAO,CAAC,IAAI,CAoBlF,CAAC"}