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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Szymon Chmal and Callstack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ [![Appduct][appduct-banner]][repo]
2
+
3
+ ### Drive app tools without shipping debug UI
4
+
5
+ [![MIT license][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
6
+
7
+ The **`appduct`** package is the operator/agent side of Appduct: a long-lived **daemon** owning a TLS-terminated `wss://` listener and any number of device sessions, a **CLI** and an **MCP server** that are thin RPC clients of it, and the **key management** tying it together — so **developers, QA, and agents** steer app state from a terminal or an MCP client **instead of a hidden debug menu**.
8
+
9
+ ## Why use this package
10
+
11
+ - **One daemon, many devices**: it auto-spawns on first use, serves every device on one `wss://` port, and survives Metro reloads, backgrounding, and network flaps by suspending and resuming sessions rather than dying with them.
12
+ - **Same surface for humans and agents**: the CLI (`tools`, `invoke`, `events`, ...) and `appduct mcp` use the same RPC methods — an agent sees the tools a human operator does.
13
+ - **Hardened control plane**: the CLI/MCP never touch sockets, keys, or state files directly; everything goes through a Unix-domain-socket RPC surface gated by filesystem permissions ([docs/SECURITY.md][security]).
14
+ - **Fits production-minded apps**: the app only exposes what you register; trust boundaries are pins + TLS, and production deployments can gate tools with policy and audit every call ([docs/ARCHITECTURE.md][architecture] §12).
15
+
16
+ ## Getting started
17
+
18
+ ```bash
19
+ npm install -g appduct
20
+ appduct link --scheme myapp --qr
21
+ ```
22
+
23
+ `link` needs your app's deep-link scheme: pass `--scheme` (the app's `expo.scheme`, or its bare-RN equivalent), or set `"scheme"` once in `~/.appduct/config.json` and omit the flag. Without either, the command exits with a usage error.
24
+
25
+ Scan the QR (or open the deep link) in a Appduct-enabled app, then:
26
+
27
+ ```bash
28
+ appduct tools
29
+ appduct invoke sum --input '{"a":2,"b":3}'
30
+ ```
31
+
32
+ That is the whole loop. There is no host process to start: `appduct` auto-spawns its daemon the first time any command needs it.
33
+
34
+ ## Commands
35
+
36
+ | Command | Role |
37
+ | --- | --- |
38
+ | `appduct init [--scheme <s>] [--force]` | set up an app directory: write `.appduct/config.json`, print the MCP snippet |
39
+ | `appduct keygen [--out <path>] [--force]` | generate a daemon private key, print its app pin |
40
+ | `appduct link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--bundle-id <id>] [--scheme <s>]` | mint a pending session and print its deep link |
41
+ | `appduct ls` | list sessions: alias, state, device, tool count |
42
+ | `appduct tools [selector] [name] [--full]` | list a session's tools, or show one tool's full schema |
43
+ | `appduct invoke [selector] <tool> --input '<json>' [--timeout <ms>]` | call a tool. `--timeout` (clamped to 1 000–600 000 ms) can **shorten** the deadline but cannot extend it past the app's own timer: the app aborts the handler at the tool's declared `timeoutMs`, or 10 s for a tool that declares none, whatever the caller asks for. Give a slow tool more room by declaring `timeoutMs` on its registration |
44
+ | `appduct events [selector] [--follow] [--since <cursor>]` | stream session/tool events (default), or one-shot pull everything retained since `<cursor>` (`--since`); `--json` emits NDJSON |
45
+ | `appduct revoke [selector]` | revoke a session |
46
+ | `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
47
+ | `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
48
+ | `appduct doctor <artifact> [--assert-present\|--assert-absent]` | **release-gate step**: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
49
+
50
+ Every command that targets a session accepts an optional `selector` (a session id or an alias from `appduct ls`); omit it when exactly one session is active. Global flags: `--json` (machine-readable output), `--no-color`, `--state-dir <path>` (default `~/.appduct`), `--daemon-restart` (on a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links; `APPDUCT_DAEMON_RESTART=1` and `config.json`'s `restartDaemonOnVersionMismatch` do the same for every command, and `--no-daemon-restart` overrules both for one). Run `appduct <command> --help` for the exact flags of any command, or `appduct --help` for the full list.
51
+
52
+ ### The deep-link scheme
53
+
54
+ `appduct link`, `appduct mcp` and the MCP `appduct_connect` tool all resolve the scheme the same way, first match wins:
55
+
56
+ 1. `--scheme <s>`
57
+ 2. the `APPDUCT_SCHEME` environment variable
58
+ 3. the nearest `.appduct/config.json` that declares a `scheme`, walking up from the working directory — one without that key does not stop the walk, and the walk skips `~/.appduct` and the state dir in use so global config is never mistaken for a project's
59
+ 4. `scheme` in `<state-dir>/config.json`
60
+ 5. a static-file project probe, tried in this order (no walk-up — an app root is where you run these commands):
61
+ 1. `<cwd>/app.json`'s `expo.scheme` — a string, or the first entry of an array
62
+ 2. **Android**: `app/build.gradle.kts` or `app/build.gradle`'s `appductScheme` manifest placeholder (`manifestPlaceholders["appductScheme"] = "myapp"` or `manifestPlaceholders.appductScheme = "myapp"`), then `app/src/main/AndroidManifest.xml`'s first `<data android:scheme>` inside an intent filter that also declares `android.intent.action.VIEW`
63
+ 3. **iOS**: any `Info.plist` up to two levels below the app root (excluding `Pods`, `build`, `node_modules`, `DerivedData`) for the first `CFBundleURLSchemes` entry, then xcodegen's `project.yml` for the same key under `info.properties.CFBundleURLTypes`
64
+ 6. otherwise an error naming every location above
65
+
66
+ So in an Expo app root, none of it needs configuring; a plain Xcode or Gradle app root needs no configuring either, once its `Info.plist`/`AndroidManifest.xml`/`build.gradle` already declares a `CFBundleURLTypes`/intent-filter/`appductScheme` for deep links. `app.config.js` / `app.config.ts` are never executed to read a scheme, and neither is `xcodebuild`, `plutil` or a Gradle evaluation for the native probes — every one of them is a plain, defensively-parsed read of a static project file (a binary-encoded `Info.plist` is reported as unreadable rather than decoded). Dynamic-config projects, or anything these probes can't make sense of, should use `--scheme`, `APPDUCT_SCHEME`, or `appduct init --scheme <s>`.
67
+
68
+ **Disagreement is never guessed away.** If more than one native probe resolves to a *different* scheme (say, an `android/` and an `ios/` tree in the same repo declaring different values), resolution fails with a usage error naming every conflicting source instead of picking one.
69
+
70
+ `appduct init`, run in an app root, writes `.appduct/config.json` (mode `0600`, in a `0700` directory) with the discovered scheme and prints the MCP server entry to paste plus the `import "@appduct/react-native/auto"` reminder. It never generates keys or touches daemon state.
71
+
72
+ **`init` is not the resolver.** It consults exactly two *kinds* of source — `--scheme`, and whatever the same static-file discovery step 5 above runs (`<cwd>/app.json`, then the native Android/iOS probes) — plus whatever the file already records. It deliberately ignores `APPDUCT_SCHEME` and does not walk up to a parent `.appduct/config.json`, because it is deciding what to *write here*: inheriting either would bake an ambient value into a committed file (a shell variable that happened to be exported, or a parent project's scheme silently copied into a sub-package). The precedence list above is what *reads* the result. `--json`'s `source` field reports which input won: `"--scheme"`, `"app.json"`, one of the four native-probe names (`"android-gradle"`, `"android-manifest"`, `"ios-info-plist"`, `"ios-project-yml"`), or `"already-recorded"`. When it is a discovery tier, `origin` names the exact file (and key/placeholder) the value came from, and the printed hint says so too — "Scheme … was read from …" — rather than only naming the platform.
73
+
74
+ It also refuses to run where `<cwd>/.appduct` would be the daemon's own state directory (`~/.appduct`, or a `--state-dir`/`APPDUCT_STATE_DIR` you point at it) — that directory holds `key.pem` and the audit log, and a config written there is not something to commit.
75
+
76
+ It is idempotent, and safe to re-run:
77
+
78
+ - A plain re-run keeps whatever scheme is already recorded. If discovery (`app.json` or a native probe) has since come to declare a different value, the result carries a `note` saying so rather than failing — a command documented as safe to re-run must not start erroring because somebody renamed a scheme.
79
+ - `--scheme <different>` needs `--force` to replace a recorded scheme; `--force` on its own re-adopts `app.json`'s value.
80
+ - `--force` merges into the existing JSON rather than truncating it.
81
+
82
+ A project `.appduct/config.json` holds client-side settings only — it can never redirect the state directory, key or policy (`--state-dir` / `APPDUCT_STATE_DIR` do that). The reverse is worth stating too: **do not point `--state-dir` at a project `.appduct/` directory you commit.** The state dir is where the daemon keeps `key.pem` and its audit log, and a committed one would publish a private key. The two directories share a name and nothing else; the walk-up that finds a project config deliberately skips `~/.appduct` and whatever state dir is in use, so neither can be mistaken for the other.
83
+
84
+ `appduct mcp` is the one exception to step 6: it starts even with no scheme, because it is still useful for proxying tools to a session paired another way. Only `appduct_connect` fails, and its error names every location tried.
85
+
86
+ ### Delivering the link to a device
87
+
88
+ `--open` hands the freshly minted deep link straight to a device, so nothing has to scan a QR code:
89
+
90
+ | `--open` | Mechanism | Address in the link |
91
+ | --- | --- | --- |
92
+ | `android` | `adb reverse tcp:<port> tcp:<port>`, then `adb shell am start -a android.intent.action.VIEW -d '<link>'` | `127.0.0.1` (the port is forwarded onto the device) |
93
+ | `ios-sim` | `xcrun simctl openurl <udid> <link>` | `127.0.0.1` (the simulator shares the host's network) |
94
+ | `ios-device` **(experimental)** | `xcrun devicectl device process launch --device <udid> [--terminate-existing] --payload-url <link> <bundle-id>` | the machine's detected LAN address — there is no `adb reverse` equivalent on iOS |
95
+
96
+ With no `--device`, every target requires exactly one device and errors naming each candidate rather than making an arbitrary pick: `android` counts attached devices (`ANDROID_SERIAL` disambiguates), `ios-sim` counts booted simulators, and `ios-device` counts *connected iOS* devices — `devicectl` lists every CoreDevice the Mac has ever paired, so disconnected phones and non-iOS ones (a paired Watch or Vision Pro) are filtered out before the count.
97
+
98
+ #### `--open ios-device` (experimental)
99
+
100
+ A wired iPhone or iPad used to have no automated path at all — only a human scanning the QR code. `--open ios-device` closes that gap through `xcrun devicectl`, but it is **experimental**: `devicectl`'s `--payload-url` flag is not documented by Apple, and it cannot be verified in CI, so treat a failure here as a reason to fall back to the QR flow rather than as a bug in your app.
101
+
102
+ It needs all of:
103
+
104
+ - **iOS 17 or newer** on the device, and **Xcode 15 or newer** on the host (`devicectl` does not exist before that). For iOS 16 and below, use the QR/deep-link flow.
105
+ - The device **paired and trusted** by this Mac, with **Developer Mode** enabled on it (Settings → Privacy & Security → Developer Mode).
106
+ - A **development-signed build of your app already installed** — `devicectl` launches an installed app; it does not install one.
107
+ - The phone and this machine **on the same network**, reachable at the address the link advertises. `appduct link` prints it on its `Endpoint` line (`--json`: `endpoint.address`); `advertisedIp` in `config.json` overrides the detection. If no routable address is found, detection falls back to `127.0.0.1` — which a phone cannot reach — so `ios-device` refuses to deliver such a link and tells you to set `advertisedIp` rather than leaving you with a session that is never claimed.
108
+ - The app's **bundle id**, from `iosBundleId` in `config.json` or `--bundle-id <id>` on the command line. Without one the command fails with a usage error before minting anything. It must look like a bundle id — letters, digits, `.` and `-` — since it is passed to `devicectl` as a trailing positional, where a value starting with `-` would be read as an option instead.
109
+
110
+ ```bash
111
+ appduct link --scheme myapp --open ios-device --bundle-id com.example.myapp
112
+ ```
113
+
114
+ Two behaviours worth knowing:
115
+
116
+ - **What happens when the app is already running is unverified.** The delivery is a plain `process launch`, which is what Expo's own `devicectl` integration does. Whether that foregrounds a running app with the URL, cold-launches it, or fails outright has not been observed on hardware. If delivery to an already-running app does nothing, pass **`--relaunch`** (`relaunch: true` over MCP): it adds `--terminate-existing`, killing the running instance first. Appduct copes with the restart either way — a delivered link supersedes a held session.
117
+ - **A physical iPhone is never auto-detected.** `appduct_connect` called without a `target` only ever considers booted simulators and attached Android devices; `appduct link` without `--open` does not look for a device at all. A paired iPhone is often someone's personal phone, so it has to be asked for explicitly.
118
+
119
+ ## MCP setup (Claude Code, Cursor, and similar)
120
+
121
+ Add `appduct mcp` to the agent's MCP server config — no separate server process to manage; it auto-spawns the daemon the same way the CLI does:
122
+
123
+ ```json
124
+ {
125
+ "mcpServers": {
126
+ "appduct": {
127
+ "command": "appduct",
128
+ "args": ["mcp"]
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ An MCP server is usually launched with a working directory you don't control, so discovery from `app.json` may not apply. On a machine with more than one app, make the entry self-contained by naming the scheme in it — which is exactly what `appduct init` prints:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "appduct": {
140
+ "command": "appduct",
141
+ "args": ["mcp", "--scheme", "myapp"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Once configured, the connected app's tools appear as MCP tools automatically: `tools/list` mirrors the live registry (namespaced `<alias>__<name>` when more than one session is active), and `tools/call` proxies straight to the app with progress and errors preserved.
148
+
149
+ Four built-in tools cover what an agent can't do through the app's own registry. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there is nothing to deliver to. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `bundleId` (or `iosBundleId` in `config.json`) and the [prerequisites above](#--open-ios-device-experimental); it is experimental and never auto-detected, so an agent has to ask for it by name.
150
+
151
+ The other two give an agent a pull surface over `postEvent()`-pushed app events: `appduct_events` drains everything retained since a cursor, and `appduct_wait_for_event` blocks for a matching event (checking what's already retained before waiting live), rejecting with `tool_timeout` if none arrives in time.
152
+
153
+ ## Test runners: `appduct/client`
154
+
155
+ A thin typed wrapper over the same daemon RPC the CLI and MCP server use — for a Jest/Vitest/Detox spec that drives a running app without spawning `appduct invoke ... --json` and parsing stdout:
156
+
157
+ ```ts
158
+ import { connect } from "appduct/client";
159
+
160
+ // auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
161
+ const app = await connect();
162
+
163
+ await app.tools(); // ToolDescriptor[]
164
+ const { total } = await app.call("sum", { a: 2, b: 3 });
165
+ const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 });
166
+ app.close();
167
+ ```
168
+
169
+ Errors surface as a `AppductError` whose `type` preserves the daemon's wire error type verbatim, so a test can assert on it directly instead of string-matching stderr:
170
+
171
+ ```ts
172
+ await expect(app.call("checkout", {})).rejects.toMatchObject({ type: "policy_denied" });
173
+ ```
174
+
175
+ Pair a simulator/emulator in a `globalSetup` without shelling out via `link`/`waitForSession`:
176
+
177
+ ```ts
178
+ import { link, waitForSession } from "appduct/client";
179
+
180
+ const { sessionId } = await link({ target: "ios-sim" });
181
+ const app = await waitForSession(sessionId, { timeoutMs: 60_000 });
182
+ ```
183
+
184
+ `app.call`'s tool name and args/result types can't be inferred automatically — tools are registered by the connected app at runtime — so declare your own tool map once for typed calls throughout a suite:
185
+
186
+ ```ts
187
+ type Tools = {
188
+ sum: { args: { a: number; b: number }; result: { total: number } };
189
+ };
190
+
191
+ const app = await connect<Tools>();
192
+ const { total } = await app.call("sum", { a: 2, b: 3 }); // typed
193
+ ```
194
+
195
+ `waitForEvent` first drains the daemon's per-session retained buffer for an already-arrived match before falling back to a live wait, so it's safe to call after the action that emits the event. Pass `since` (the `cursor` from a previous `app.events()`/`waitForEvent()` call) to skip events already handled:
196
+
197
+ ```ts
198
+ const { events, cursor } = await app.events(); // pull: what already happened
199
+ const next = await app.waitForEvent("checkout_done", { since: cursor });
200
+ ```
201
+
202
+ For everything else, the package exports `runCli` and the command handlers from [`src/index.ts`](https://github.com/callstackincubator/appduct/blob/main/packages/appduct/src/index.ts), so you can embed the same behavior in Node or Bun scripts without shelling out.
203
+
204
+ ## Keys and pins
205
+
206
+ You can skip this entirely while your app has no `cliPins` configured — the zero-config default, in **any** build type. The daemon auto-generates its own `key.pem` the first time it starts if one isn't already there (mode `0600`) and prints its `sha256/...` fingerprint on that first run; `appduct link` carries that fingerprint on the deep link for the app to pick up. See [`docs/SECURITY.md`][security]'s "Trust modes" for what that does and does not protect.
207
+
208
+ For a build that should trust only a key you embedded ahead of time, generate one explicitly:
209
+
210
+ ```bash
211
+ appduct keygen
212
+ ```
213
+
214
+ The command writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by default (override with `--out`; add `--force` to overwrite) and prints the exact `sha256/...` SPKI fingerprint your app should place into `cliPins`. It runs non-interactively — safe to call from CI or a setup script.
215
+
216
+ `appduct link`'s deep link is `<scheme>:///?appduct=<payload>&pin=<sha256/...>`. The `appduct` param is the existing binary v2 bootstrap payload (address, session id, token, expiry — unchanged); `pin` is a separate, out-of-band query param carrying the daemon's current SPKI fingerprint for apps that want to pick it up. An app build with embedded `cliPins` ignores `pin` outright — embedded pins always win, in every build type. A build with no embedded pins trusts it for that one session.
217
+
218
+ ## Daemon lifecycle
219
+
220
+ The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It writes state to `<state-dir>/` (`daemon.sock`, `daemon.pid`, `daemon.log`, `daemon.log.1`, `key.pem`, `config.json`, `audit/`), holds a single-instance lock via the pidfile, and runs independently of any one device's connection, so a reload or crash on the device side never costs you the daemon process.
221
+
222
+ Use `appduct daemon status` to see what's running (version, pid, `wssPort`, pinned keys, live sessions, effective policy, and the audit log's retention window, file count, size, and failure counters) and `appduct daemon stop` to shut it down explicitly.
223
+
224
+ Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon *newer* than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
225
+
226
+ Neither log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, and a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned. Both are `config.json` keys ([ARCHITECTURE.md §3][architecture]).
227
+
228
+ ## Release gate: `appduct doctor`
229
+
230
+ Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [docs/BUILD-VARIANTS.md][build-variants]) — there is no runtime `debuggable` check to catch a pipeline that forgot to exclude the package. `doctor` is the replacement: an artifact-level assertion you run against the thing you're about to ship, not the config you think produced it.
231
+
232
+ ```bash
233
+ appduct doctor ./build/MyApp.ipa --assert-absent
234
+ appduct doctor ./build/app-release.apk --assert-absent
235
+ ```
236
+
237
+ Exit codes, what it inspects per platform, the marker-only detection rules on both platforms, and the CI wiring are in [docs/CI.md][ci].
238
+
239
+ ## Related packages
240
+
241
+ - **[@appduct/react-native](https://github.com/callstackincubator/appduct/blob/main/packages/react-native/README.md)** — native app client + Expo plugin.
242
+ - **[@appduct/shared](https://github.com/callstackincubator/appduct/blob/main/packages/shared/README.md)** — wire protocol v2 types used by this package and the React Native client.
243
+
244
+ ## Documentation
245
+
246
+ - [Architecture][architecture]
247
+ - [Wire protocol][protocol]
248
+ - [Security model & key rotation][security]
249
+ - [Build variants][build-variants]
250
+ - [CI and the release gate][ci]
251
+ - [Monorepo README](https://github.com/callstackincubator/appduct/blob/main/README.md)
252
+
253
+ ## Made with ❤️ at Callstack
254
+
255
+ `appduct` is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. [Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
256
+
257
+ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥
258
+
259
+ [appduct-banner]: https://img.shields.io/badge/Appduct-callstack%2Fincubator-111827?style=for-the-badge&logo=github&logoColor=white
260
+ [repo]: https://github.com/callstackincubator/appduct
261
+ [callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=appduct&utm_term=readme-with-love
262
+ [architecture]: https://github.com/callstackincubator/appduct/blob/main/docs/ARCHITECTURE.md
263
+ [protocol]: https://github.com/callstackincubator/appduct/blob/main/docs/PROTOCOL.md
264
+ [security]: https://github.com/callstackincubator/appduct/blob/main/docs/SECURITY.md
265
+ [build-variants]: https://github.com/callstackincubator/appduct/blob/main/docs/BUILD-VARIANTS.md
266
+ [ci]: https://github.com/callstackincubator/appduct/blob/main/docs/CI.md
267
+ [license-badge]: https://img.shields.io/npm/l/appduct?style=for-the-badge
268
+ [license]: https://github.com/callstackincubator/appduct/blob/main/LICENSE
269
+ [npm-downloads-badge]: https://img.shields.io/npm/dm/appduct?style=for-the-badge
270
+ [npm-downloads]: https://www.npmjs.com/package/appduct
271
+ [prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
272
+ [prs-welcome]: https://github.com/callstackincubator/appduct/pulls
273
+ [chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
274
+ [chat]: https://discord.gg/xgGt7KAjxv
package/bin.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./dist/bin.js";
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `appduct doctor` (docs/tasks/08-appduct-doctor.md): the artifact-level replacement for the
3
+ * runtime `debuggable`/`#if DEBUG` gate removed elsewhere in opt-in hardening. Given a built
4
+ * `.app`/`.ipa`/`.apk`/`.aab`, decide whether Appduct's native code actually shipped inside it —
5
+ * not whether the config that's supposed to have produced it looks right.
6
+ *
7
+ * The one property everything here is built around: **never report "absent" because a tool was
8
+ * missing or the artifact couldn't be read.** That failure mode is strictly worse than not having
9
+ * this check at all, because it turns a release gate into a rubber stamp that always exits 0 once
10
+ * `unzip` happens to be missing from a runner's image. Every path that can't produce a truthful
11
+ * answer throws {@link inspectionError} — a distinct, non-zero exit — instead of falling through to
12
+ * `present: false`.
13
+ *
14
+ * Detection strategy, and why no single signal is authoritative on either platform:
15
+ *
16
+ * - iOS: `present` is decided by real-code-only symbols alone, exactly like Android below —
17
+ * `AppductCoreMarker` (`packages/native/ios/Sources/AppductCore/Real/AppductCoreMarker.swift`,
18
+ * docs/tasks/14-native-core-extraction.md), an `@objc` class compiled only into the real
19
+ * implementation, never into the SwiftPM package's `Stub/` branch, so its presence cannot be
20
+ * confused with a stub build. `RCTNativeAppduct` (`RCT_EXPORT_MODULE`, see
21
+ * `packages/react-native/ios/RCTNativeAppduct.mm`) is kept as a second real-code signal for
22
+ * artifacts built before the marker existed. Both are Objective-C runtime metadata — they live in
23
+ * `__objc_classname`/`__objc_data`, and `strip`/release optimization leaves them alone because
24
+ * removing them would break `+[NSObject class]`-based dispatch. (A build that dead-strips the whole
25
+ * translation unit for lack of `-ObjC`/`-force_load` could still drop them — these signals assume
26
+ * the module actually links into the binary, which is the thing being checked in the first
27
+ * place.) The plugin-authored `Info.plist` keys
28
+ * (`AppductCliPins`/`AppductTrust`/`AppductAllowPrivateLanOnly`, docs/tasks/00-overview.md
29
+ * "Native config keys the plugin writes") are reported as a corroborating signal only and can no
30
+ * longer flip `present` to `true` on their own — an app author can write these keys by hand (or a
31
+ * future stub could ship them) without the real implementation being present, exactly the reason
32
+ * the Android signals below aren't all treated as equally authoritative either.
33
+ * - Android: the primary signal is `AppductNativeMarker`
34
+ * (`packages/native/android/core/src/main/java/com/callstackincubator/appduct/AppductNativeMarker.kt`,
35
+ * vendored into `@appduct/react-native` at `android/core/src/main/java/...`), a marker class with no
36
+ * other purpose. Its fully-qualified name is kept unminified and unremoved by a `-keep` rule in
37
+ * `consumer-rules.pro` (same vendoring path), shipped to every consuming app via `consumerProguardFiles`
38
+ * (see `../build.gradle`) — R8 applies it regardless of whether the app used the Expo config plugin or
39
+ * bare-RN autolinking, and regardless of whether the app authored any keep rules of its own
40
+ * (docs/tasks/10-android-detection-keep-rule.md). This is the only Android signal guaranteed to survive
41
+ * minification in every supported consumer setup.
42
+ *
43
+ * Two more signals are kept as fallbacks for artifacts built before this marker existed: the
44
+ * `com.callstackincubator.appduct` package string in the dex string pool (survives as long as
45
+ * R8/ProGuard minification+obfuscation isn't applied to it — an aggressive release config with no keep
46
+ * rule for this package can rename it away) and the plugin-authored meta-data key names in
47
+ * `AndroidManifest.xml` (`com.callstackincubator.appduct.CLI_PINS`/`TRUST`/`ALLOW_PRIVATE_LAN_ONLY`):
48
+ * those are XML attribute string values written by the config plugin at prebuild time, not compiled
49
+ * identifiers, so R8 never touches them, but they only exist at all if the config plugin ran. Both
50
+ * encodings AAPT2 can choose for the manifest string pool (UTF-8 or UTF-16LE) are checked.
51
+ *
52
+ * On iOS, `present` is an OR across the two real-code-only signals (`ios-core-marker-symbol`,
53
+ * `ios-objc-class-symbol`) — specifically so a stripped binary that dropped one literal doesn't flip
54
+ * a real inclusion to "absent" just because one check missed — but the Info.plist keys signal cannot
55
+ * flip it to `true` by itself, matching Android's marker-only rule below (a stub that ships the same
56
+ * plist keys without the real implementation is exactly the failure mode this guards against, even
57
+ * though no such stub exists yet as of Phase 1 of docs/tasks/14-native-core-extraction.md).
58
+ *
59
+ * On Android, `present` is decided by the keep-rule marker alone, not an OR across all three
60
+ * signals. The other two are reported in `signals` for corroboration/debugging but cannot flip
61
+ * `present` to `true` on their own: `android/build.gradle`'s `APPDUCT_ENABLED`-gated vendored
62
+ * source-directory swap (`core` vs `core-noop`, docs/tasks/14-native-core-extraction.md; previously a
63
+ * `src/debug`/`src/release-stub` swap, see docs/tasks/00-overview.md's "Revisited
64
+ * post-implementation") compiles a genuine no-op `AppductPackage` into a default release build at
65
+ * the *same* fully-qualified name the real one uses (required so the shared, non-variant-aware
66
+ * `PackageList.java` still resolves), and the config plugin writes the same manifest meta-data
67
+ * regardless of variant (Expo mods edit the single merged manifest, not a per-variant one). Both
68
+ * fallback signals therefore fire on that harmless stub exactly as they would on the real module, and
69
+ * can no longer prove inclusion by themselves.
70
+ *
71
+ * `appduct doctor` is strictly better than the runtime check it replaces (docs/tasks/00-overview.md
72
+ * "What we give up, deliberately"). The keep-rule marker closes the previously-documented gap where a
73
+ * bare-RN app with no config plugin and no keep rule could evade both older Android signals under R8
74
+ * minification (docs/tasks/10-android-detection-keep-rule.md) — that gap applied to builds produced before
75
+ * this library shipped the marker/keep rule; an app that pins an older `@appduct/react-native` version
76
+ * still lacks it, and doctor cannot detect inclusion on such a build at all.
77
+ */
78
+ export type ArtifactPlatform = "ios" | "android";
79
+ export type ArtifactFormat = "app" | "ipa" | "apk" | "aab";
80
+ export type DetectionSignal = "ios-core-marker-symbol" | "ios-objc-class-symbol" | "ios-info-plist-keys" | "android-keep-rule-marker" | "android-dex-package-symbol" | "android-manifest-meta-data-keys";
81
+ export type ArtifactInspection = {
82
+ platform: ArtifactPlatform;
83
+ format: ArtifactFormat;
84
+ present: boolean;
85
+ /** Which specific markers matched — empty when `present` is `false`. Surfaced for auditability,
86
+ * not just a boolean: a report that says *why* is checkable by a human without re-running the
87
+ * tool with more verbosity. */
88
+ signals: DetectionSignal[];
89
+ };
90
+ export type ExecBufferResult = {
91
+ stdout: Buffer;
92
+ stderr: Buffer;
93
+ };
94
+ /** Injectable subprocess seam; tests stub this to simulate a missing `unzip` (ENOENT) or a
95
+ * corrupt-archive failure without needing either condition to be true on the machine running the
96
+ * tests. */
97
+ export type ExecBufferFn = (command: string, args: string[]) => Promise<ExecBufferResult>;
98
+ /** Real subprocess execution via `execFile`, buffered as raw bytes. `maxBuffer` is generous (an
99
+ * artifact's full decompressed contents are read into memory at once — see {@link readZipEntries})
100
+ * since real `.ipa`/`.apk`/`.aab` files can run into the hundreds of MB. */
101
+ export declare const defaultExecBuffer: ExecBufferFn;
102
+ export type InspectArtifactOptions = {
103
+ exec?: ExecBufferFn;
104
+ };
105
+ /** Inspects a built artifact for Appduct's native inclusion. Throws {@link usageError} for an
106
+ * unrecognized path/extension (a CLI-usage mistake) and {@link inspectionError} for anything that
107
+ * prevents a truthful presence/absence answer (missing tool, unreadable/corrupt artifact) — it
108
+ * never resolves successfully with a guessed or default answer. */
109
+ export declare const inspectArtifact: (artifactPath: string, options?: InspectArtifactOptions) => Promise<ArtifactInspection>;
110
+ //# sourceMappingURL=artifact-inspect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-inspect.d.ts","sourceRoot":"","sources":["../src/artifact-inspect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AAQH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,SAAS,CAAC;AACjD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE3D,MAAM,MAAM,eAAe,GACvB,wBAAwB,GACxB,uBAAuB,GACvB,qBAAqB,GACrB,0BAA0B,GAC1B,4BAA4B,GAC5B,iCAAiC,CAAC;AAEtC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB;;mCAE+B;IAC/B,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B,CAAC;AAkCF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;YAEY;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;AAE1F;;4EAE4E;AAC5E,eAAO,MAAM,iBAAiB,EAAE,YAgB/B,CAAC;AA8NF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAEF;;;mEAGmE;AACnE,eAAO,MAAM,eAAe,GAC1B,cAAc,MAAM,EACpB,UAAS,sBAA2B,KACnC,OAAO,CAAC,kBAAkB,CA+C5B,CAAC"}