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,438 @@
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
+ import { execFile } from "node:child_process";
16
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
17
+ import { tmpdir } from "node:os";
18
+ import path from "node:path";
19
+ import { connectionError, usageError } from "../errors.js";
20
+ export const isOpenTarget = (value) => {
21
+ return value === "android" || value === "ios-sim" || value === "ios-device";
22
+ };
23
+ /** Human-readable list of every accepted target, for the "must be one of" validation messages the
24
+ * CLI, the shared `mintLink` core and `appduct_connect` each render in their own wording. */
25
+ export const OPEN_TARGETS = ["android", "ios-sim", "ios-device"];
26
+ /**
27
+ * Whether a link delivered to this target reaches the daemon over loopback, and so must be minted
28
+ * with the `127.0.0.1` address override (`link.ts`, `mcp/connect-tool.ts`). True for `android`
29
+ * (`adb reverse` forwards the port onto the device) and `ios-sim` (the simulator shares the host's
30
+ * network stack); false for `ios-device` — a physical iPhone reaches the daemon only over the LAN,
31
+ * so such a link must carry the machine's real advertised address (issue #31).
32
+ */
33
+ export const usesLoopbackAddress = (target) => {
34
+ return target === "android" || target === "ios-sim";
35
+ };
36
+ /**
37
+ * Whether an advertised address is one that only resolves back to the machine that minted the link.
38
+ * `daemon/address.ts` falls back to `127.0.0.1` when it cannot find a routable interface, and such
39
+ * a link handed to a physical phone points the phone at *itself*: the app connects to nothing, the
40
+ * session is never claimed, and `wait_for_session` blocks for its whole timeout with no clue why.
41
+ * The `ios-device` path checks this after minting and refuses rather than delivering.
42
+ */
43
+ export const isLoopbackAddress = (address) => {
44
+ const normalized = address.trim().toLowerCase().replace(/^\[|\]$/gu, "");
45
+ return (normalized === "localhost" ||
46
+ normalized === "::1" ||
47
+ normalized === "0.0.0.0" ||
48
+ normalized === "::" ||
49
+ /^127\./u.test(normalized));
50
+ };
51
+ /** The one wording for that refusal, shared by the CLI and MCP paths. */
52
+ export const loopbackAddressMessage = (address) => `The daemon advertised "${address}", which a physical device cannot reach — it would point the ` +
53
+ "phone at itself and the session would never be claimed. No routable LAN address was detected; " +
54
+ 'set "advertisedIp" in config.json to this machine\'s address on the network the device is on, ' +
55
+ "and retry.";
56
+ /** Real subprocess execution via `execFile` (no shell involved — argv is passed verbatim). */
57
+ export const defaultExec = (command, args) => {
58
+ return new Promise((resolve, reject) => {
59
+ execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
60
+ if (error) {
61
+ reject(Object.assign(error, { stdout, stderr }));
62
+ return;
63
+ }
64
+ resolve({ stdout, stderr });
65
+ });
66
+ });
67
+ };
68
+ const TOOL_INSTALL_HINT = {
69
+ adb: "the Android platform-tools (adb)",
70
+ xcrun: "the Xcode command line tools (xcrun)",
71
+ };
72
+ const run = async (exec, command, args) => {
73
+ try {
74
+ return await exec(command, args);
75
+ }
76
+ catch (error) {
77
+ const err = error;
78
+ if (err.code === "ENOENT") {
79
+ throw connectionError(`"${command}" was not found on PATH. Install ${TOOL_INSTALL_HINT[command] ?? command} and retry.`);
80
+ }
81
+ const stderr = (err.stderr ?? "").trim();
82
+ throw connectionError(`"${command} ${args.join(" ")}" failed${stderr ? `: ${stderr}` : `: ${err.message}`}`);
83
+ }
84
+ };
85
+ const parseBootedSimulators = (stdout) => {
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(stdout);
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ return Object.values(parsed.devices ?? {})
94
+ .flat()
95
+ .filter((entry) => entry.state === "Booted" && typeof entry.udid === "string" && entry.udid.length > 0)
96
+ .map((entry) => ({ udid: entry.udid, name: entry.name ?? entry.udid }));
97
+ };
98
+ const listBootedSimulators = async (exec) => {
99
+ const preflight = await run(exec, "xcrun", ["simctl", "list", "devices", "booted", "--json"]);
100
+ return parseBootedSimulators(preflight.stdout);
101
+ };
102
+ const describeSimulator = (simulator) => `${simulator.name} (${simulator.udid})`;
103
+ const deliverIosSim = async (exec, deepLink, device) => {
104
+ // An explicit udid is honored without a preflight, mirroring android's `--device` passthrough:
105
+ // the caller has already named the device, so re-listing only adds a failure mode.
106
+ if (device) {
107
+ await run(exec, "xcrun", ["simctl", "openurl", device, deepLink]);
108
+ return;
109
+ }
110
+ const booted = await listBootedSimulators(exec);
111
+ if (booted.length === 0) {
112
+ throw usageError("No booted iOS simulator found. Boot a simulator (e.g. via Xcode) and retry.");
113
+ }
114
+ if (booted.length > 1) {
115
+ // Previously this delivered to simctl's `booted` alias regardless, which silently picks one of
116
+ // several booted simulators — the link would land on an arbitrary device and the session would
117
+ // never be claimed, looking exactly like a hung `wait_for_session`. Errors like android does.
118
+ throw usageError(`Multiple iOS simulators are booted (${booted
119
+ .map(describeSimulator)
120
+ .join(", ")}); specify --device <udid>.`);
121
+ }
122
+ // Target the resolved udid rather than the `booted` alias: `booted` is only unambiguous while
123
+ // exactly one simulator is up, and a second one booting between the preflight and this call would
124
+ // otherwise redirect the link to an arbitrary device.
125
+ await run(exec, "xcrun", ["simctl", "openurl", booted[0].udid, deepLink]);
126
+ };
127
+ // --- ios-device (experimental, issue #31) ---
128
+ /**
129
+ * `devicectl` writes its JSON to a *file* (`--json-output <path>`), never to stdout, so this path
130
+ * cannot reuse the stdout-parsing shape `simctl` uses. The file lives in a private `mkdtemp`
131
+ * directory removed in a `finally`, so a failing `devicectl` (or a malformed payload) never leaks
132
+ * a temp file.
133
+ */
134
+ const runDevicectlJson = async (exec, buildArgs) => {
135
+ let dir;
136
+ try {
137
+ dir = await mkdtemp(path.join(tmpdir(), "appduct-devicectl-"));
138
+ }
139
+ catch (error) {
140
+ // A read-only or missing TMPDIR would otherwise surface as a bare `EACCES`/`ENOENT` against a
141
+ // path the user never chose, with nothing saying which knob to turn.
142
+ throw connectionError(`A temporary directory for devicectl's --json-output could not be created in "${tmpdir()}" ` +
143
+ `(${error.message}). Set TMPDIR to a writable directory and retry.`);
144
+ }
145
+ const file = path.join(dir, "devicectl.json");
146
+ try {
147
+ await run(exec, "xcrun", buildArgs(file));
148
+ try {
149
+ return JSON.parse(await readFile(file, "utf8"));
150
+ }
151
+ catch {
152
+ // `devicectl` exited 0 but wrote nothing readable. Treated as an empty listing (the caller
153
+ // renders "no device found") rather than surfacing a JSON syntax error, matching how
154
+ // `parseBootedSimulators` handles unparseable `simctl` output.
155
+ return undefined;
156
+ }
157
+ }
158
+ finally {
159
+ // Swallowed deliberately: a failure to clean up a temp directory must not replace the
160
+ // devicectl error the caller actually needs to read (a `finally` that throws wins).
161
+ await rm(dir, { force: true, recursive: true }).catch(() => { });
162
+ }
163
+ };
164
+ /** `hardwareProperties.platform` values `devicectl` reports for things that are not an iPhone or
165
+ * iPad. A paired Apple Watch or Vision Pro is a real CoreDevice and shows up in the same listing,
166
+ * but cannot run the app and must never make the device choice look ambiguous. */
167
+ const DELIVERABLE_PLATFORM = "ios";
168
+ /**
169
+ * The only `tunnelState` that means "not reachable at all". Deliberately *not* including
170
+ * `disconnected`: a wired, trusted iPhone routinely reports `disconnected` (the tunnel is brought
171
+ * up on demand), so excluding it would drop exactly the device this target exists to reach. This
172
+ * matches the vendored Expo CLI's own `devicectl` integration
173
+ * (`@expo/cli`'s `AppleDevice.js:102`), which keeps `disconnected` and excludes only `unavailable`.
174
+ */
175
+ const UNREACHABLE_TUNNEL_STATE = "unavailable";
176
+ /** Expo's companion condition: a usable device is affirmatively `paired`. */
177
+ const USABLE_PAIRING_STATE = "paired";
178
+ /**
179
+ * `devicectl list devices` lists **every CoreDevice the Mac has ever known**, across platforms and
180
+ * regardless of whether it is plugged in — so the listing has to be filtered, not trusted.
181
+ *
182
+ * Filtering happens here rather than through `devicectl --filter` (an NSPredicate over undocumented
183
+ * keys) so that every rule is exercised by the `ExecFn` tests instead of by a predicate string no
184
+ * test can evaluate.
185
+ *
186
+ * Each rule drops an entry only when the field is **present and disqualifying**; a missing field
187
+ * keeps the device. The field names are undocumented, so a `devicectl` that stops emitting one must
188
+ * degrade to "offer the device and let the launch fail loudly", never to "silently find nothing".
189
+ * That is the one deliberate departure from Expo, which tests `pairingState === "paired"`
190
+ * positively and so would find nothing at all if the key were ever renamed.
191
+ */
192
+ const parsePairedIosDevices = (payload) => {
193
+ const devices = payload?.result?.devices;
194
+ if (!Array.isArray(devices)) {
195
+ return [];
196
+ }
197
+ return devices
198
+ .map((entry) => {
199
+ const platform = entry?.hardwareProperties?.platform;
200
+ if (typeof platform === "string" && platform.toLowerCase() !== DELIVERABLE_PLATFORM) {
201
+ return undefined;
202
+ }
203
+ const tunnelState = entry?.connectionProperties?.tunnelState;
204
+ if (typeof tunnelState === "string" && tunnelState.toLowerCase() === UNREACHABLE_TUNNEL_STATE) {
205
+ return undefined;
206
+ }
207
+ const pairingState = entry?.connectionProperties?.pairingState;
208
+ if (typeof pairingState === "string" && pairingState.toLowerCase() !== USABLE_PAIRING_STATE) {
209
+ return undefined;
210
+ }
211
+ // `hardwareProperties.udid` is what `--device` wants; `identifier` (the CoreDevice UUID) is
212
+ // also accepted by `devicectl`, so a listing that omits the udid is still usable rather than
213
+ // silently dropped. Checked for emptiness rather than `??`, which would keep a `""` udid and
214
+ // never reach the documented fallback.
215
+ const hardwareUdid = entry?.hardwareProperties?.udid;
216
+ const udid = typeof hardwareUdid === "string" && hardwareUdid.length > 0
217
+ ? hardwareUdid
218
+ : entry?.identifier;
219
+ return typeof udid === "string" && udid.length > 0
220
+ ? { udid, name: entry?.deviceProperties?.name ?? udid }
221
+ : undefined;
222
+ })
223
+ .filter((device) => device !== undefined);
224
+ };
225
+ const listPairedIosDevices = async (exec) => {
226
+ const payload = await runDevicectlJson(exec, (file) => [
227
+ "devicectl",
228
+ "list",
229
+ "devices",
230
+ "--timeout",
231
+ "5",
232
+ "--json-output",
233
+ file,
234
+ ]);
235
+ return parsePairedIosDevices(payload);
236
+ };
237
+ const describeIosDevice = (device) => `${device.name} (${device.udid})`;
238
+ /** The single wording naming both ways to supply a bundle id, so the CLI and MCP paths agree. */
239
+ export const MISSING_BUNDLE_ID_MESSAGE = "Delivering to a physical iPhone needs the app's bundle id: pass \"--bundle-id <id>\" (or " +
240
+ '"bundleId" over MCP), or set "iosBundleId" in config.json.';
241
+ /**
242
+ * A bundle id is the **only trailing positional** in the `devicectl device process launch` argv, so
243
+ * a value beginning with `-` would be read by `devicectl` as an option rather than as the app to
244
+ * launch — `--console`, say, silently changing what the command does. Apple's own grammar for a
245
+ * bundle identifier is alphanumerics, `.` and `-`, which never legitimately starts with `-`, so
246
+ * enforcing exactly that shape closes the hole at every entry point.
247
+ *
248
+ * A `--` terminator is deliberately *not* used instead: `devicectl` takes launch arguments after
249
+ * the bundle id, its handling of `--` is undocumented, and none of it can be verified without
250
+ * hardware — whereas this check is exact and fully testable.
251
+ */
252
+ const BUNDLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]*$/u;
253
+ export const isValidBundleId = (value) => BUNDLE_ID_PATTERN.test(value);
254
+ export const invalidBundleIdMessage = (value) => `"${value}" is not a valid iOS bundle id (letters, digits, "." and "-", starting with a letter ` +
255
+ "or digit).";
256
+ /** Same ambiguity rules as `deliverIosSim`: exactly one paired device, or the caller names one. */
257
+ const resolveSingleIosDevice = async (exec) => {
258
+ const paired = await listPairedIosDevices(exec);
259
+ if (paired.length === 0) {
260
+ throw usageError('No connected iOS device was found ("xcrun devicectl list devices" listed none, ignoring ' +
261
+ "disconnected devices and non-iOS ones such as a paired Watch or Vision Pro). Connect the " +
262
+ "device, trust this Mac, and enable Developer Mode on it, then retry.");
263
+ }
264
+ if (paired.length > 1) {
265
+ throw usageError(`Multiple connected iOS devices were found (${paired
266
+ .map(describeIosDevice)
267
+ .join(", ")}); specify --device <udid>.`);
268
+ }
269
+ return paired[0].udid;
270
+ };
271
+ /**
272
+ * Experimental physical-iPhone delivery (issue #31). `xcrun devicectl device process launch
273
+ * --device <udid> --payload-url <url> <bundle-id>` is undocumented by Apple but is the mechanism
274
+ * Xcode 15+ exposes for handing a URL to an installed, dev-signed app on a paired iOS 17+ device.
275
+ * It may cold-launch (rather than foreground) the app; the app side copes either way, because a
276
+ * delivered link supersedes a held session and the initial-URL path is handled.
277
+ *
278
+ * Unlike `ios-sim`, this is never auto-detected — see {@link detectBootedTargets}.
279
+ */
280
+ const deliverIosDevice = async (exec, deepLink, device, bundleId, relaunch) => {
281
+ // Checked before any enumeration: without a bundle id the launch cannot happen at all, so
282
+ // spending a `devicectl list devices` timeout first would only delay the same error.
283
+ if (!bundleId) {
284
+ throw usageError(MISSING_BUNDLE_ID_MESSAGE);
285
+ }
286
+ // Last line of defence: every caller validates too, but this is the one place the value reaches
287
+ // an argv, so it is the one place that must not be bypassable.
288
+ if (!isValidBundleId(bundleId)) {
289
+ throw usageError(invalidBundleIdMessage(bundleId));
290
+ }
291
+ // An explicit udid is honored without a preflight, mirroring the `ios-sim`/`android` passthrough.
292
+ const udid = device ?? (await resolveSingleIosDevice(exec));
293
+ await run(exec, "xcrun", [
294
+ "devicectl",
295
+ "device",
296
+ "process",
297
+ "launch",
298
+ "--device",
299
+ udid,
300
+ // Opt-in only (`--relaunch` / `relaunch: true`). A plain `process launch` is what the vendored
301
+ // Expo CLI does, so it is the better-attested default; what it does when the app is *already
302
+ // running* is the one thing nobody here can verify without hardware. If that turns out to fail
303
+ // rather than deliver the URL, `--terminate-existing` is the escape hatch — it kills the
304
+ // running instance first, which Appduct copes with because a delivered link supersedes a
305
+ // held session.
306
+ ...(relaunch ? ["--terminate-existing"] : []),
307
+ "--payload-url",
308
+ deepLink,
309
+ bundleId,
310
+ ]);
311
+ };
312
+ /** Parses `adb devices` output: header line, then `<serial>\t<state>` per attached device. */
313
+ const parseAdbDevices = (stdout) => {
314
+ return stdout
315
+ .split("\n")
316
+ .slice(1)
317
+ .map((line) => line.trim())
318
+ .filter((line) => line.length > 0)
319
+ .map((line) => {
320
+ const [serial = "", state = ""] = line.split(/\s+/u);
321
+ return { serial, state };
322
+ })
323
+ .filter((entry) => entry.serial.length > 0);
324
+ };
325
+ const listAttachedAndroidDevices = async (exec) => {
326
+ const listing = await run(exec, "adb", ["devices"]);
327
+ return parseAdbDevices(listing.stdout)
328
+ .filter((entry) => entry.state === "device")
329
+ .map((entry) => entry.serial);
330
+ };
331
+ /**
332
+ * Resolves the `-s <serial>` argument, if any. `--device` always wins; otherwise, if
333
+ * `ANDROID_SERIAL` is set, `adb` already honors it with no flag needed from us; otherwise exactly
334
+ * one attached device is required (zero or several is an error naming what's attached).
335
+ */
336
+ const resolveAndroidSerial = async (exec, device, env) => {
337
+ if (device) {
338
+ return device;
339
+ }
340
+ if (env.ANDROID_SERIAL) {
341
+ return undefined;
342
+ }
343
+ const attached = await listAttachedAndroidDevices(exec);
344
+ if (attached.length === 0) {
345
+ throw usageError('No Android device or emulator is attached ("adb devices" listed none).');
346
+ }
347
+ if (attached.length > 1) {
348
+ throw usageError(`Multiple Android devices are attached (${attached.join(", ")}); specify --device <serial> or set ANDROID_SERIAL.`);
349
+ }
350
+ return undefined;
351
+ };
352
+ const withSerial = (args, serial) => {
353
+ return serial ? ["-s", serial, ...args] : args;
354
+ };
355
+ const deliverAndroid = async (exec, deepLink, wssPort, device, env) => {
356
+ const serial = await resolveAndroidSerial(exec, device, env);
357
+ // `adb reverse` before `am start`: the port forward must exist before the app tries to connect,
358
+ // which can happen the instant the deep link is handled.
359
+ await run(exec, "adb", withSerial(["reverse", `tcp:${wssPort}`, `tcp:${wssPort}`], serial));
360
+ await run(exec, "adb", withSerial([
361
+ "shell",
362
+ "am",
363
+ "start",
364
+ "-a",
365
+ "android.intent.action.VIEW",
366
+ "-d",
367
+ // `adb shell` reconstructs everything after "shell" into one string and re-parses it on the
368
+ // device's own shell; single-quoting the URL here (not just for local execFile, which never
369
+ // re-tokenizes argv) protects against that second, remote parse (ARCHITECTURE.md §8).
370
+ `'${deepLink}'`,
371
+ ], serial));
372
+ };
373
+ /** Best-effort enumeration: a missing or failing `xcrun`/`adb` means "no devices of that platform"
374
+ * rather than an error. Detection runs when the caller named *no* target, so a machine without the
375
+ * Android tools installed (or without Xcode) must still reach a clean `{ kind: "none" }` and its
376
+ * QR fallback instead of failing the whole call. */
377
+ const safeList = async (list) => {
378
+ try {
379
+ return await list();
380
+ }
381
+ catch {
382
+ return [];
383
+ }
384
+ };
385
+ /**
386
+ * Enumerates every booted simulator and attached Android device so a caller with no explicit
387
+ * target can deliver to the single obvious one. `ANDROID_SERIAL`, when set, narrows the Android
388
+ * side to that serial — it is exactly the disambiguation the user has already made.
389
+ *
390
+ * Physical iPhones (`ios-device`) are deliberately *not* enumerated here (issue #31): a paired
391
+ * iPhone is often someone's personal phone rather than a test device, delivery to it may
392
+ * cold-launch the app, and it additionally needs a bundle id the caller has to supply. It is an
393
+ * explicit, opt-in target only.
394
+ */
395
+ export const detectBootedTargets = async (options = {}) => {
396
+ const exec = options.exec ?? defaultExec;
397
+ const env = options.env ?? process.env;
398
+ const [simulators, androidSerials] = await Promise.all([
399
+ safeList(() => listBootedSimulators(exec)),
400
+ env.ANDROID_SERIAL
401
+ ? Promise.resolve([env.ANDROID_SERIAL])
402
+ : safeList(() => listAttachedAndroidDevices(exec)),
403
+ ]);
404
+ const candidates = [
405
+ ...simulators.map((simulator) => ({
406
+ target: "ios-sim",
407
+ device: simulator.udid,
408
+ label: describeSimulator(simulator),
409
+ })),
410
+ ...androidSerials.map((serial) => ({
411
+ target: "android",
412
+ device: serial,
413
+ label: serial,
414
+ })),
415
+ ];
416
+ if (candidates.length === 0) {
417
+ return { kind: "none" };
418
+ }
419
+ if (candidates.length === 1) {
420
+ return { kind: "single", detected: candidates[0] };
421
+ }
422
+ return { kind: "ambiguous", candidates };
423
+ };
424
+ /** Delivers `deepLink` to a booted Android device/emulator, an iOS simulator, or (experimentally)
425
+ * a paired physical iOS device. Throws a clear, actionable error (missing tool, no booted device,
426
+ * ambiguous device, missing bundle id) rather than a raw one. */
427
+ export const deliverToOpenTarget = async (options) => {
428
+ const exec = options.exec ?? defaultExec;
429
+ if (options.target === "ios-sim") {
430
+ await deliverIosSim(exec, options.deepLink, options.device);
431
+ return;
432
+ }
433
+ if (options.target === "ios-device") {
434
+ await deliverIosDevice(exec, options.deepLink, options.device, options.bundleId, options.relaunch ?? false);
435
+ return;
436
+ }
437
+ await deliverAndroid(exec, options.deepLink, options.wssPort, options.device, options.env ?? process.env);
438
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * CLI command-result types (ARCHITECTURE.md §10). The v2 command table replaces the v1 shapes that
3
+ * used to live here (`host`/`connect`/`session` — all removed from the v2 command surface) with
4
+ * the thin-client shapes for the v2 command table.
5
+ */
6
+ import type { OpenTarget } from "./open-target.js";
7
+ import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolDescriptor } from "@appduct/shared";
8
+ /**
9
+ * CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
10
+ * `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
11
+ * the daemon's wire `ErrorType` verbatim in `type` (ARCHITECTURE.md §5 — "never re-wrapped"), while
12
+ * CLI-originated failures (bad flags, JSON parse errors, ...) use one of these names directly.
13
+ */
14
+ export type CliErrorType = "usage_error" | "validation_error" | "connection_error" | "session_error" | "tool_error" | "permission_error" | "internal_error" | "inspection_error" | "assertion_error";
15
+ export type CliError = {
16
+ /** A `CliErrorType` for CLI-originated failures, or the verbatim daemon/app `ErrorType` for
17
+ * RPC-originated ones (e.g. `tool_execution_error`) — never re-wrapped under a generic type. */
18
+ type: string;
19
+ message: string;
20
+ details?: unknown;
21
+ };
22
+ export type CommandMeta = {
23
+ command: string;
24
+ timestamp: string;
25
+ duration_ms?: number;
26
+ };
27
+ export type CliSuccessResult<TData> = {
28
+ ok: true;
29
+ data: TData;
30
+ meta?: CommandMeta;
31
+ };
32
+ export type CliFailureResult = {
33
+ ok: false;
34
+ error: CliError;
35
+ meta?: CommandMeta;
36
+ };
37
+ export type CliResult<TData> = CliSuccessResult<TData> | CliFailureResult;
38
+ export type KeygenCommandData = {
39
+ path: string;
40
+ pin: string;
41
+ };
42
+ /**
43
+ * `appduct init` (issue #29). `created`/`changed` are what make the command's idempotency
44
+ * observable to a script: a re-run with the same scheme returns `ok: true` with both `false`, so a
45
+ * caller can tell "nothing to do" from "I just wrote this" without diffing the file itself.
46
+ */
47
+ export type InitCommandData = {
48
+ /** Absolute path of the project config that was written (or already correct). */
49
+ path: string;
50
+ scheme: string;
51
+ /**
52
+ * Where `scheme` came from. These are `init`'s *own* inputs, deliberately named so they cannot
53
+ * be read as `scheme.ts`'s resolution tiers: `init` never consults `APPDUCT_SCHEME` and
54
+ * never walks up to a parent project config (see `commands/init.ts`).
55
+ *
56
+ * - `"--scheme"` — the flag.
57
+ * - `"app.json"` — `<cwd>/app.json`'s `expo.scheme`.
58
+ * - `"android-gradle"` / `"android-manifest"` / `"ios-info-plist"` / `"ios-project-yml"` — one of
59
+ * the static-file native-project probes added for issue #48 (`native-scheme.ts`), tried in
60
+ * that order whenever `app.json` declares nothing. `origin` names the exact file/key.
61
+ * - `"already-recorded"` — the value this file already held, which a plain re-run keeps so it
62
+ * stays idempotent even after the discovered value changes.
63
+ */
64
+ source: "--scheme" | "app.json" | "android-gradle" | "android-manifest" | "ios-info-plist" | "ios-project-yml" | "already-recorded";
65
+ /** The exact file (and key/placeholder) `scheme` was read from, when `source` is a discovery
66
+ * tier rather than `"--scheme"`/`"already-recorded"` — `init`'s human-readable hint surfaces
67
+ * this so a native-probe result says which file to go edit, not just which platform. */
68
+ origin?: string;
69
+ /** The project config did not exist before this run. */
70
+ created: boolean;
71
+ /** This run wrote to the file. `false` on an idempotent re-run. */
72
+ changed: boolean;
73
+ /** Present when the recorded scheme and the value discovery would currently find (`app.json` or
74
+ * a native probe) disagree. The recorded one still wins (a re-run must not start failing because
75
+ * the underlying project file was edited); this says so and names the `--force` invocation that
76
+ * would adopt the other. */
77
+ note?: string;
78
+ /** The MCP server entry to paste into an agent's config — self-contained (it carries `--scheme`)
79
+ * so one machine can serve several apps without editing a global file. */
80
+ mcpServerEntry: {
81
+ command: string;
82
+ args: string[];
83
+ };
84
+ /** Human-facing follow-ups that cannot be inferred from the filesystem (the `auto` import, the
85
+ * MCP paste, how to pair a device). Also carried in `--json` so an agent can relay them. */
86
+ nextSteps: string[];
87
+ };
88
+ export type LinkCommandData = {
89
+ sessionId: string;
90
+ /** Already includes the `pin` query param (see `pin` below) alongside `appduct`. */
91
+ deepLink: string;
92
+ endpoint: AgentEndpoint;
93
+ /** Unix seconds. */
94
+ expiresAt: number;
95
+ /** The daemon's SPKI pin, same value as embedded in `deepLink`'s `pin` query param — surfaced
96
+ * as its own field for `--json` consumers that don't want to re-parse the URL (opt-in
97
+ * hardening dev-mode: native clients trust this only in debug builds with no `cliPins`). */
98
+ pin: string;
99
+ /** Present (and `true`) only once `--open <target>` has successfully delivered the link
100
+ * (ARCHITECTURE.md §10). */
101
+ delivered?: true;
102
+ /** The `--open` target the link was delivered to, alongside `delivered`. */
103
+ target?: OpenTarget;
104
+ };
105
+ /** `appduct ls`: `sessions.list` passthrough, verbatim (ARCHITECTURE.md §10: "--json passthrough"). */
106
+ export type LsCommandData = SessionSummary[];
107
+ /** `appduct tools`: the full list, or a single descriptor when a tool name resolved to a detail lookup. */
108
+ export type ToolsCommandData = ToolDescriptor[] | ToolDescriptor;
109
+ /** `appduct invoke`: the tool's raw result payload, printed as-is. */
110
+ export type InvokeCommandData = unknown;
111
+ export type RevokeCommandData = {
112
+ ok: true;
113
+ };
114
+ /** A single `appduct events` line, in both human and NDJSON (`--json`) rendering. */
115
+ export type EventsCommandLine = EventNotification;
116
+ export type DaemonRunCommandData = {
117
+ daemon: {
118
+ pid: number;
119
+ state_dir: string;
120
+ socket_path: string;
121
+ };
122
+ };
123
+ export type DaemonStartCommandData = {
124
+ daemon: {
125
+ pid: number;
126
+ wss_port: number;
127
+ started_at: string;
128
+ };
129
+ };
130
+ export type DaemonStopCommandData = {
131
+ daemon: {
132
+ ok: true;
133
+ /** How the daemon was told to stop: a clean RPC round-trip, or a SIGTERM fallback. */
134
+ method: "rpc" | "sigterm";
135
+ };
136
+ };
137
+ /** `appduct doctor <artifact>`: artifact-level Appduct inclusion report (docs/tasks/08). */
138
+ export type DoctorCommandData = {
139
+ artifact: string;
140
+ platform: "ios" | "android";
141
+ format: "app" | "ipa" | "apk" | "aab";
142
+ present: boolean;
143
+ /** Which specific markers matched; empty when `present` is `false`. */
144
+ signals: string[];
145
+ /** Present only when `--assert-present`/`--assert-absent` was given. `holds` is always `true` on
146
+ * a successful result — a failed assertion throws instead (see `assertion_error`), so a caller
147
+ * reading only `ok: true` results never needs to re-check this field to know whether the gate
148
+ * passed. */
149
+ assertion?: {
150
+ expected: "present" | "absent";
151
+ holds: true;
152
+ };
153
+ };
154
+ export type DaemonStatusCommandData = {
155
+ daemon: {
156
+ version: string;
157
+ pid: number;
158
+ started_at: string;
159
+ wss_port: number;
160
+ pinned_keys: string[];
161
+ session_count: number;
162
+ };
163
+ /**
164
+ * Present only when the running daemon's version differs from this CLI's (issue #30). `daemon
165
+ * status` deliberately reports drift instead of acting on it — it is the command an operator
166
+ * reaches for to *diagnose* a daemon, so it must never restart the one they are inspecting.
167
+ */
168
+ warning?: string;
169
+ /** Effective policy + audit surfacing (ARCHITECTURE.md §12). */
170
+ policy: {
171
+ default: EffectivePolicyDecision;
172
+ destructive: EffectivePolicyDecision;
173
+ tools?: Record<string, EffectivePolicyDecision>;
174
+ };
175
+ /**
176
+ * `path`/`failed_writes` come from every daemon version. The retention fields are optional
177
+ * because a daemon started before retention existed answers `daemon.status` without them, and a
178
+ * newer CLI can be pointed at exactly that daemon. Absent is reported as absent — omitted from
179
+ * `--json` and skipped in the human rendering — rather than defaulted to `0`, which would state
180
+ * "this daemon retains nothing and has zero day files" when the truth is that it was never
181
+ * asked. `daemon.version` is the field that explains why they are missing.
182
+ */
183
+ audit: {
184
+ path: string;
185
+ failed_writes: number;
186
+ failed_prunes?: number;
187
+ retention_days?: number;
188
+ /** Retained `audit/<YYYY-MM-DD>.jsonl` day files. */
189
+ files?: number;
190
+ /** Total bytes across those files. */
191
+ bytes?: number;
192
+ };
193
+ };
194
+ //# sourceMappingURL=result-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result-types.d.ts","sourceRoot":"","sources":["../../src/cli/result-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * CLI command-result types (ARCHITECTURE.md §10). The v2 command table replaces the v1 shapes that
3
+ * used to live here (`host`/`connect`/`session` — all removed from the v2 command surface) with
4
+ * the thin-client shapes for the v2 command table.
5
+ */
6
+ export {};