appduct 0.10.0 → 0.11.1

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 (230) hide show
  1. package/README.md +122 -52
  2. package/dist/artifact-inspect.d.ts +0 -1
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +1250 -2
  5. package/dist/cli/command-options.d.ts +8 -1
  6. package/dist/cli/create-cli.d.ts +0 -1
  7. package/dist/cli/dispatch.d.ts +10 -1
  8. package/dist/cli/envelope.d.ts +19 -0
  9. package/dist/cli/global-flags.d.ts +30 -0
  10. package/dist/cli/open-target.d.ts +25 -8
  11. package/dist/cli/result-types.d.ts +31 -5
  12. package/dist/cli/router.d.ts +78 -0
  13. package/dist/cli/routes/daemon/index.d.ts +9 -0
  14. package/dist/cli/routes/daemon/index.js +848 -0
  15. package/dist/cli/routes/daemon/run.d.ts +3 -0
  16. package/dist/cli/routes/daemon/run.js +3246 -0
  17. package/dist/cli/routes/daemon/start.d.ts +3 -0
  18. package/dist/cli/routes/daemon/start.js +1677 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1685 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1655 -0
  23. package/dist/cli/routes/doctor.d.ts +3 -0
  24. package/dist/cli/routes/doctor.js +1075 -0
  25. package/dist/cli/routes/events.d.ts +3 -0
  26. package/dist/cli/routes/events.js +1915 -0
  27. package/dist/cli/routes/init.d.ts +3 -0
  28. package/dist/cli/routes/init.js +1486 -0
  29. package/dist/cli/routes/invoke.d.ts +3 -0
  30. package/dist/cli/routes/invoke.js +1893 -0
  31. package/dist/cli/routes/keygen.d.ts +3 -0
  32. package/dist/cli/routes/keygen.js +928 -0
  33. package/dist/cli/routes/link.d.ts +3 -0
  34. package/dist/cli/routes/link.js +2714 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1668 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3929 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1684 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1860 -0
  43. package/dist/cli/runner.d.ts +3 -11
  44. package/dist/cli/types.d.ts +9 -6
  45. package/dist/cli/version-guard.d.ts +20 -0
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/client/app-client.d.ts +4 -4
  48. package/dist/client/bootstrap.d.ts +4 -4
  49. package/dist/client/connect.d.ts +0 -1
  50. package/dist/client/errors.d.ts +0 -1
  51. package/dist/client/index.d.ts +1 -2
  52. package/dist/client/index.js +2389 -16
  53. package/dist/commands/daemon/context.d.ts +14 -0
  54. package/dist/commands/daemon/run.d.ts +11 -0
  55. package/dist/commands/daemon/start.d.ts +5 -0
  56. package/dist/commands/daemon/status.d.ts +5 -0
  57. package/dist/commands/daemon/stop.d.ts +5 -0
  58. package/dist/commands/doctor.d.ts +0 -1
  59. package/dist/commands/events.d.ts +0 -1
  60. package/dist/commands/init.d.ts +17 -4
  61. package/dist/commands/invoke.d.ts +0 -1
  62. package/dist/commands/keygen.d.ts +0 -1
  63. package/dist/commands/link.d.ts +1 -2
  64. package/dist/commands/ls.d.ts +0 -1
  65. package/dist/commands/mcp.d.ts +2 -4
  66. package/dist/commands/revoke.d.ts +0 -1
  67. package/dist/commands/tools.d.ts +19 -3
  68. package/dist/daemon/address.d.ts +0 -1
  69. package/dist/daemon/audit.d.ts +6 -11
  70. package/dist/daemon/calls.d.ts +0 -1
  71. package/dist/daemon/config.d.ts +13 -16
  72. package/dist/daemon/daemon.d.ts +0 -1
  73. package/dist/daemon/event-bus.d.ts +0 -1
  74. package/dist/daemon/links.d.ts +0 -1
  75. package/dist/daemon/listener.d.ts +0 -1
  76. package/dist/daemon/log-rotation.d.ts +0 -1
  77. package/dist/daemon/pidfile.d.ts +14 -8
  78. package/dist/daemon/policy.d.ts +0 -1
  79. package/dist/daemon/registry.d.ts +0 -1
  80. package/dist/daemon/rpc-errors.d.ts +16 -0
  81. package/dist/daemon/rpc-server.d.ts +0 -11
  82. package/dist/daemon/sessions.d.ts +0 -1
  83. package/dist/daemon/socket-probe.d.ts +0 -1
  84. package/dist/daemon/state-dir.d.ts +0 -1
  85. package/dist/daemon/timers.d.ts +0 -1
  86. package/dist/daemon/tls.d.ts +0 -1
  87. package/dist/errors.d.ts +0 -1
  88. package/dist/host-certificate.d.ts +0 -1
  89. package/dist/index.d.ts +0 -1
  90. package/dist/index.js +1307 -3
  91. package/dist/key-material.d.ts +0 -1
  92. package/dist/link.d.ts +8 -7
  93. package/dist/mcp/app-tools.d.ts +153 -0
  94. package/dist/mcp/connect-tool.d.ts +14 -8
  95. package/dist/mcp/daemon-tools.d.ts +2 -11
  96. package/dist/mcp/events-tool.d.ts +0 -1
  97. package/dist/mcp/server.d.ts +33 -14
  98. package/dist/native-scheme.d.ts +0 -1
  99. package/dist/output.d.ts +8 -11
  100. package/dist/package-root.d.ts +11 -0
  101. package/dist/package-version.d.ts +0 -1
  102. package/dist/qr-terminal.d.ts +0 -1
  103. package/dist/rpc/client.d.ts +3 -3
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +6 -3
  107. package/dist/artifact-inspect.d.ts.map +0 -1
  108. package/dist/artifact-inspect.js +0 -334
  109. package/dist/bin.d.ts.map +0 -1
  110. package/dist/cli/command-options.d.ts.map +0 -1
  111. package/dist/cli/command-options.js +0 -95
  112. package/dist/cli/create-cli.d.ts.map +0 -1
  113. package/dist/cli/create-cli.js +0 -56
  114. package/dist/cli/dispatch.d.ts.map +0 -1
  115. package/dist/cli/dispatch.js +0 -275
  116. package/dist/cli/open-target.d.ts.map +0 -1
  117. package/dist/cli/open-target.js +0 -438
  118. package/dist/cli/result-types.d.ts.map +0 -1
  119. package/dist/cli/result-types.js +0 -6
  120. package/dist/cli/runner.d.ts.map +0 -1
  121. package/dist/cli/runner.js +0 -111
  122. package/dist/cli/types.d.ts.map +0 -1
  123. package/dist/cli/types.js +0 -10
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -2
  126. package/dist/client/app-client.d.ts.map +0 -1
  127. package/dist/client/app-client.js +0 -223
  128. package/dist/client/bootstrap.d.ts.map +0 -1
  129. package/dist/client/bootstrap.js +0 -108
  130. package/dist/client/connect.d.ts.map +0 -1
  131. package/dist/client/connect.js +0 -38
  132. package/dist/client/errors.d.ts.map +0 -1
  133. package/dist/client/errors.js +0 -40
  134. package/dist/client/index.d.ts.map +0 -1
  135. package/dist/commands/daemon.d.ts +0 -22
  136. package/dist/commands/daemon.d.ts.map +0 -1
  137. package/dist/commands/daemon.js +0 -128
  138. package/dist/commands/doctor.d.ts.map +0 -1
  139. package/dist/commands/doctor.js +0 -42
  140. package/dist/commands/events.d.ts.map +0 -1
  141. package/dist/commands/events.js +0 -76
  142. package/dist/commands/init.d.ts.map +0 -1
  143. package/dist/commands/init.js +0 -231
  144. package/dist/commands/invoke.d.ts.map +0 -1
  145. package/dist/commands/invoke.js +0 -54
  146. package/dist/commands/keygen.d.ts.map +0 -1
  147. package/dist/commands/keygen.js +0 -46
  148. package/dist/commands/link.d.ts.map +0 -1
  149. package/dist/commands/link.js +0 -43
  150. package/dist/commands/ls.d.ts.map +0 -1
  151. package/dist/commands/ls.js +0 -7
  152. package/dist/commands/mcp.d.ts.map +0 -1
  153. package/dist/commands/mcp.js +0 -98
  154. package/dist/commands/revoke.d.ts.map +0 -1
  155. package/dist/commands/revoke.js +0 -7
  156. package/dist/commands/tools.d.ts.map +0 -1
  157. package/dist/commands/tools.js +0 -52
  158. package/dist/daemon/address.d.ts.map +0 -1
  159. package/dist/daemon/address.js +0 -71
  160. package/dist/daemon/audit.d.ts.map +0 -1
  161. package/dist/daemon/audit.js +0 -237
  162. package/dist/daemon/calls.d.ts.map +0 -1
  163. package/dist/daemon/calls.js +0 -158
  164. package/dist/daemon/config.d.ts.map +0 -1
  165. package/dist/daemon/config.js +0 -170
  166. package/dist/daemon/daemon.d.ts.map +0 -1
  167. package/dist/daemon/daemon.js +0 -560
  168. package/dist/daemon/event-bus.d.ts.map +0 -1
  169. package/dist/daemon/event-bus.js +0 -111
  170. package/dist/daemon/links.d.ts.map +0 -1
  171. package/dist/daemon/links.js +0 -118
  172. package/dist/daemon/listener.d.ts.map +0 -1
  173. package/dist/daemon/listener.js +0 -132
  174. package/dist/daemon/log-rotation.d.ts.map +0 -1
  175. package/dist/daemon/log-rotation.js +0 -136
  176. package/dist/daemon/pidfile.d.ts.map +0 -1
  177. package/dist/daemon/pidfile.js +0 -90
  178. package/dist/daemon/policy.d.ts.map +0 -1
  179. package/dist/daemon/policy.js +0 -24
  180. package/dist/daemon/registry.d.ts.map +0 -1
  181. package/dist/daemon/registry.js +0 -31
  182. package/dist/daemon/rpc-server.d.ts.map +0 -1
  183. package/dist/daemon/rpc-server.js +0 -203
  184. package/dist/daemon/sessions.d.ts.map +0 -1
  185. package/dist/daemon/sessions.js +0 -429
  186. package/dist/daemon/socket-probe.d.ts.map +0 -1
  187. package/dist/daemon/socket-probe.js +0 -28
  188. package/dist/daemon/state-dir.d.ts.map +0 -1
  189. package/dist/daemon/state-dir.js +0 -43
  190. package/dist/daemon/timers.d.ts.map +0 -1
  191. package/dist/daemon/timers.js +0 -12
  192. package/dist/daemon/tls.d.ts.map +0 -1
  193. package/dist/daemon/tls.js +0 -79
  194. package/dist/errors.d.ts.map +0 -1
  195. package/dist/errors.js +0 -162
  196. package/dist/host-certificate.d.ts.map +0 -1
  197. package/dist/host-certificate.js +0 -111
  198. package/dist/index.d.ts.map +0 -1
  199. package/dist/key-material.d.ts.map +0 -1
  200. package/dist/key-material.js +0 -42
  201. package/dist/link.d.ts.map +0 -1
  202. package/dist/link.js +0 -117
  203. package/dist/mcp/connect-tool.d.ts.map +0 -1
  204. package/dist/mcp/connect-tool.js +0 -396
  205. package/dist/mcp/daemon-tools.d.ts.map +0 -1
  206. package/dist/mcp/daemon-tools.js +0 -29
  207. package/dist/mcp/events-tool.d.ts.map +0 -1
  208. package/dist/mcp/events-tool.js +0 -322
  209. package/dist/mcp/server.d.ts.map +0 -1
  210. package/dist/mcp/server.js +0 -538
  211. package/dist/mcp/tool-mapping.d.ts +0 -58
  212. package/dist/mcp/tool-mapping.d.ts.map +0 -1
  213. package/dist/mcp/tool-mapping.js +0 -143
  214. package/dist/mcp/tool-namespace.d.ts +0 -40
  215. package/dist/mcp/tool-namespace.d.ts.map +0 -1
  216. package/dist/mcp/tool-namespace.js +0 -65
  217. package/dist/native-scheme.d.ts.map +0 -1
  218. package/dist/native-scheme.js +0 -465
  219. package/dist/output.d.ts.map +0 -1
  220. package/dist/output.js +0 -342
  221. package/dist/package-version.d.ts.map +0 -1
  222. package/dist/package-version.js +0 -47
  223. package/dist/qr-terminal.d.ts.map +0 -1
  224. package/dist/qr-terminal.js +0 -45
  225. package/dist/rpc/client.d.ts.map +0 -1
  226. package/dist/rpc/client.js +0 -819
  227. package/dist/scheme.d.ts.map +0 -1
  228. package/dist/scheme.js +0 -321
  229. package/dist/spki-pin.d.ts.map +0 -1
  230. package/dist/spki-pin.js +0 -14
@@ -1,334 +0,0 @@
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/callstack/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.callstack.appduct` package string in the dex string pool (and its pre-rename spelling,
45
- * `com.callstackincubator.appduct` — every Android marker below is matched against both, so an
46
- * app built against Appduct 0.9.0 or earlier is still detected) (survives as long as
47
- * R8/ProGuard minification+obfuscation isn't applied to it — an aggressive release config with no keep
48
- * rule for this package can rename it away) and the plugin-authored meta-data key names in
49
- * `AndroidManifest.xml` (`com.callstack.appduct.CLI_PINS`/`TRUST`/`ALLOW_PRIVATE_LAN_ONLY`):
50
- * those are XML attribute string values written by the config plugin at prebuild time, not compiled
51
- * identifiers, so R8 never touches them, but they only exist at all if the config plugin ran. Both
52
- * encodings AAPT2 can choose for the manifest string pool (UTF-8 or UTF-16LE) are checked.
53
- *
54
- * On iOS, `present` is an OR across the two real-code-only signals (`ios-core-marker-symbol`,
55
- * `ios-objc-class-symbol`) — specifically so a stripped binary that dropped one literal doesn't flip
56
- * a real inclusion to "absent" just because one check missed — but the Info.plist keys signal cannot
57
- * flip it to `true` by itself, matching Android's marker-only rule below (a stub that ships the same
58
- * plist keys without the real implementation is exactly the failure mode this guards against, even
59
- * though no such stub exists yet as of Phase 1 of docs/tasks/14-native-core-extraction.md).
60
- *
61
- * On Android, `present` is decided by the keep-rule marker alone, not an OR across all three
62
- * signals. The other two are reported in `signals` for corroboration/debugging but cannot flip
63
- * `present` to `true` on their own: `android/build.gradle`'s `APPDUCT_ENABLED`-gated vendored
64
- * source-directory swap (`core` vs `core-noop`, docs/tasks/14-native-core-extraction.md; previously a
65
- * `src/debug`/`src/release-stub` swap, see docs/tasks/00-overview.md's "Revisited
66
- * post-implementation") compiles a genuine no-op `AppductPackage` into a default release build at
67
- * the *same* fully-qualified name the real one uses (required so the shared, non-variant-aware
68
- * `PackageList.java` still resolves), and the config plugin writes the same manifest meta-data
69
- * regardless of variant (Expo mods edit the single merged manifest, not a per-variant one). Both
70
- * fallback signals therefore fire on that harmless stub exactly as they would on the real module, and
71
- * can no longer prove inclusion by themselves.
72
- *
73
- * `appduct doctor` is strictly better than the runtime check it replaces (docs/tasks/00-overview.md
74
- * "What we give up, deliberately"). The keep-rule marker closes the previously-documented gap where a
75
- * bare-RN app with no config plugin and no keep rule could evade both older Android signals under R8
76
- * minification (docs/tasks/10-android-detection-keep-rule.md) — that gap applied to builds produced before
77
- * this library shipped the marker/keep rule; an app that pins an older `@appduct/react-native` version
78
- * still lacks it, and doctor cannot detect inclusion on such a build at all.
79
- */
80
- import { execFile } from "node:child_process";
81
- import { readdir, readFile, stat } from "node:fs/promises";
82
- import path from "node:path";
83
- import { inspectionError, usageError } from "./errors.js";
84
- // --- markers ---
85
- // `@objc(AppductCoreMarker)` -- packages/native/ios/Sources/AppductCore/Real/AppductCoreMarker.swift.
86
- // Compiled only into the real implementation, never the SwiftPM package's Stub/ branch, so unlike
87
- // the Info.plist keys below it cannot exist without the real implementation also being present.
88
- const IOS_CORE_MARKER = "AppductCoreMarker";
89
- const IOS_OBJC_CLASS_MARKER = "RCTNativeAppduct";
90
- const IOS_INFO_PLIST_KEY_MARKERS = ["AppductCliPins", "AppductTrust", "AppductAllowPrivateLanOnly"];
91
- // Fully-qualified name of `AppductNativeMarker`
92
- // (packages/native/android/core/src/main/java/com/callstack/appduct/AppductNativeMarker.kt,
93
- // vendored into packages/react-native/android/core/src/main/java/com/callstack/appduct/AppductNativeMarker.kt
94
- // by scripts/sync-native-core.mjs), kept unminified by packages/native/android/core/consumer-rules.pro
95
- // (vendored to packages/react-native/android/core/consumer-rules.pro the same way). Checked as a dex
96
- // type descriptor (`Lcom/.../AppductNativeMarker;`) — see detectAndroidSignals — which is how the
97
- // class's fully-qualified name is actually encoded in classes.dex.
98
- //
99
- // Each of the three Android markers is matched against BOTH the current `com.callstack.*` namespace
100
- // and the legacy `com.callstackincubator.*` one that every release up to and including 0.9.0
101
- // shipped. `doctor` inspects artifacts it did not build -- including an app built against an older
102
- // Appduct, which is exactly the case a release gate runs into -- and reporting `absent` for an app
103
- // that genuinely bundles Appduct is the one failure mode this tool must never have: it would
104
- // rubber-stamp shipping Appduct to production. docs/CI.md states the rule ("a broken check must
105
- // fail loudly, not rubber-stamp a release"); the cost of honouring it here is one extra buffer
106
- // scan per signal. The legacy entries are load-bearing and must not be dropped while any artifact
107
- // built before the rename can still be inspected.
108
- const ANDROID_KEEP_RULE_MARKER_CLASSES = [
109
- "com/callstack/appduct/AppductNativeMarker",
110
- "com/callstackincubator/appduct/AppductNativeMarker",
111
- ];
112
- const ANDROID_DEX_PACKAGE_MARKERS = [
113
- "com/callstack/appduct",
114
- "com.callstack.appduct",
115
- "com/callstackincubator/appduct",
116
- "com.callstackincubator.appduct",
117
- ];
118
- const ANDROID_MANIFEST_KEY_MARKERS = ["com.callstack.appduct.", "com.callstackincubator.appduct."];
119
- const bufferIncludesAscii = (haystack, needle) => {
120
- return haystack.includes(Buffer.from(needle, "utf8"));
121
- };
122
- const bufferIncludesUtf16le = (haystack, needle) => {
123
- return haystack.includes(Buffer.from(needle, "utf16le"));
124
- };
125
- /** Real subprocess execution via `execFile`, buffered as raw bytes. `maxBuffer` is generous (an
126
- * artifact's full decompressed contents are read into memory at once — see {@link readZipEntries})
127
- * since real `.ipa`/`.apk`/`.aab` files can run into the hundreds of MB. */
128
- export const defaultExecBuffer = (command, args) => {
129
- return new Promise((resolve, reject) => {
130
- execFile(command, args, { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }, (error, stdout, stderr) => {
131
- if (error) {
132
- reject(Object.assign(error, { stdout, stderr }));
133
- return;
134
- }
135
- resolve({ stdout, stderr });
136
- });
137
- });
138
- };
139
- const TOOL_INSTALL_HINT = {
140
- unzip: "Info-ZIP's unzip (present by default on macOS and most Linux CI images)",
141
- };
142
- /** Runs an external tool, translating "not found on PATH" and "ran but failed" into a single
143
- * {@link inspectionError} class — both mean the same thing to a caller: this artifact could not be
144
- * inspected, so no presence/absence claim can be trusted. Never resolves to a result that looks
145
- * like a clean "not present" answer. */
146
- const runToolOrInspectionError = async (exec, command, args, context) => {
147
- try {
148
- const result = await exec(command, args);
149
- return result.stdout;
150
- }
151
- catch (error) {
152
- const err = error;
153
- if (err.code === "ENOENT") {
154
- throw inspectionError(`"${command}" was not found on PATH; cannot inspect ${context}. Install ${TOOL_INSTALL_HINT[command] ?? command} and retry.`);
155
- }
156
- if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
157
- throw inspectionError(`"${command} ${args.join(" ")}" produced more output than this command will buffer while ` +
158
- `inspecting ${context}. The artifact is unusually large, not necessarily corrupt — this is ` +
159
- `a tooling limit, not a presence/absence answer.`);
160
- }
161
- const stderr = err.stderr ? err.stderr.toString("utf8").trim() : "";
162
- throw inspectionError(`"${command} ${args.join(" ")}" failed while inspecting ${context}${stderr ? `: ${stderr}` : `: ${err.message}`}. The artifact may be corrupt or not a valid archive.`);
163
- }
164
- };
165
- /** The container-shape check every zip-format artifact must pass before its contents are trusted
166
- * for a presence/absence answer (see {@link readZipEntries}): an empty, wrong-file-renamed, or
167
- * otherwise not-actually-that-kind-of-artifact zip must never silently read back as "no signals
168
- * found" — it isn't an answer, it's evidence the input wasn't what its extension claimed. */
169
- const EXPECTED_ENTRY_PATTERN = {
170
- ipa: /\.app\//iu,
171
- apk: /(^|\/)AndroidManifest\.xml$/u,
172
- aab: /(^|\/)AndroidManifest\.xml$/u,
173
- };
174
- const assertExpectedZipShape = (format, entries, context) => {
175
- if (entries.length === 0) {
176
- throw inspectionError(`${context} contains no entries — it is not a valid archive.`);
177
- }
178
- if (!entries.some((entry) => EXPECTED_ENTRY_PATTERN[format].test(entry))) {
179
- throw inspectionError(`${context} does not look like a valid .${format}: none of its ${entries.length} entries match ` +
180
- `the expected ${format === "ipa" ? '"*.app/" bundle' : '"AndroidManifest.xml"'} shape. Refusing ` +
181
- `to report absence for an artifact that may simply be the wrong file.`);
182
- }
183
- };
184
- /** Lists a zip archive's entry names via `unzip -Z1` (zipinfo mode: one path per line, nothing
185
- * else) — used only for the {@link assertExpectedZipShape} sanity check, not for detection itself. */
186
- const listZipEntries = async (exec, archivePath, context) => {
187
- const stdout = await runToolOrInspectionError(exec, "unzip", ["-Z1", archivePath], context);
188
- return stdout
189
- .toString("utf8")
190
- .split("\n")
191
- .map((line) => line.trim())
192
- .filter((line) => line.length > 0);
193
- };
194
- /** Streams every entry of a zip-format archive (`.ipa`/`.apk`/`.aab` are all zip containers)
195
- * decompressed and concatenated to one buffer, via `unzip -p`. Deliberately does not attempt to
196
- * enumerate entries and extract selectively first — a single whole-archive pass is simpler, cannot
197
- * miss a signal hiding in an unexpected internal path (embedded framework, AAB's `base/dex/`
198
- * nesting, ...), and correctness matters far more than the marginal speed of a narrower read for a
199
- * command that runs once per release. Validates the archive actually has the expected internal
200
- * shape first ({@link assertExpectedZipShape}) so an empty or mislabeled zip can't read back as a
201
- * clean "no signals found" instead of the "this wasn't a real artifact" it actually is. */
202
- const readZipEntries = async (exec, archivePath, format, context) => {
203
- const entries = await listZipEntries(exec, archivePath, context);
204
- assertExpectedZipShape(format, entries, context);
205
- return runToolOrInspectionError(exec, "unzip", ["-p", archivePath], context);
206
- };
207
- /** Recursively reads every regular file under a `.app` bundle directory into one concatenated
208
- * buffer. Symlinks are skipped (never followed) to avoid loops; `.app` bundles don't legitimately
209
- * need them for this check. */
210
- const readAppDirectory = async (appDirPath, context) => {
211
- const chunks = [];
212
- const walk = async (dir) => {
213
- let entries;
214
- try {
215
- entries = await readdir(dir, { withFileTypes: true });
216
- }
217
- catch (error) {
218
- throw inspectionError(`Could not read "${dir}" while inspecting ${context}: ${error.message}.`);
219
- }
220
- for (const entry of entries) {
221
- if (entry.isSymbolicLink()) {
222
- continue;
223
- }
224
- const entryPath = path.join(dir, entry.name);
225
- if (entry.isDirectory()) {
226
- await walk(entryPath);
227
- continue;
228
- }
229
- if (entry.isFile()) {
230
- try {
231
- chunks.push(await readFile(entryPath));
232
- }
233
- catch (error) {
234
- throw inspectionError(`Could not read "${entryPath}" while inspecting ${context}: ${error.message}.`);
235
- }
236
- }
237
- }
238
- };
239
- await walk(appDirPath);
240
- return Buffer.concat(chunks);
241
- };
242
- // --- format resolution ---
243
- const resolveFormat = (artifactPath) => {
244
- const extension = path.extname(artifactPath).toLowerCase();
245
- switch (extension) {
246
- case ".app":
247
- return { platform: "ios", format: "app" };
248
- case ".ipa":
249
- return { platform: "ios", format: "ipa" };
250
- case ".apk":
251
- return { platform: "android", format: "apk" };
252
- case ".aab":
253
- return { platform: "android", format: "aab" };
254
- default:
255
- throw usageError(`Unrecognized artifact extension "${extension || artifactPath}". Expected one of: .app, .ipa, .apk, .aab.`);
256
- }
257
- };
258
- // --- signal detection ---
259
- const detectIosSignals = (bytes) => {
260
- const signals = [];
261
- // Primary signal: checked first because, unlike the Info.plist keys below, it cannot exist
262
- // without the real implementation (see the file-level doc comment).
263
- if (bufferIncludesAscii(bytes, IOS_CORE_MARKER)) {
264
- signals.push("ios-core-marker-symbol");
265
- }
266
- if (bufferIncludesAscii(bytes, IOS_OBJC_CLASS_MARKER)) {
267
- signals.push("ios-objc-class-symbol");
268
- }
269
- if (IOS_INFO_PLIST_KEY_MARKERS.some((marker) => bufferIncludesAscii(bytes, marker))) {
270
- signals.push("ios-info-plist-keys");
271
- }
272
- return signals;
273
- };
274
- const detectAndroidSignals = (bytes) => {
275
- const signals = [];
276
- // Primary signal: checked first because it's the only one guaranteed to survive R8 minification
277
- // in every supported consumer setup (see the file-level doc comment). Matched as a dex type
278
- // descriptor (`L` + fully-qualified-name-with-slashes + `;`) since that's the actual encoding of
279
- // a class name inside classes.dex, not just a loose substring check.
280
- if (ANDROID_KEEP_RULE_MARKER_CLASSES.some((klass) => bufferIncludesAscii(bytes, `L${klass};`))) {
281
- signals.push("android-keep-rule-marker");
282
- }
283
- if (ANDROID_DEX_PACKAGE_MARKERS.some((marker) => bufferIncludesAscii(bytes, marker))) {
284
- signals.push("android-dex-package-symbol");
285
- }
286
- if (ANDROID_MANIFEST_KEY_MARKERS.some((marker) => bufferIncludesAscii(bytes, marker) || bufferIncludesUtf16le(bytes, marker))) {
287
- signals.push("android-manifest-meta-data-keys");
288
- }
289
- return signals;
290
- };
291
- /** Inspects a built artifact for Appduct's native inclusion. Throws {@link usageError} for an
292
- * unrecognized path/extension (a CLI-usage mistake) and {@link inspectionError} for anything that
293
- * prevents a truthful presence/absence answer (missing tool, unreadable/corrupt artifact) — it
294
- * never resolves successfully with a guessed or default answer. */
295
- export const inspectArtifact = async (artifactPath, options = {}) => {
296
- const exec = options.exec ?? defaultExecBuffer;
297
- const { platform, format } = resolveFormat(artifactPath);
298
- const context = `"${artifactPath}"`;
299
- let stats;
300
- try {
301
- stats = await stat(artifactPath);
302
- }
303
- catch (error) {
304
- throw inspectionError(`Artifact not found at ${context}: ${error.message}.`);
305
- }
306
- let bytes;
307
- if (format === "app") {
308
- if (!stats.isDirectory()) {
309
- throw inspectionError(`Expected ${context} (a ".app" bundle) to be a directory.`);
310
- }
311
- bytes = await readAppDirectory(artifactPath, context);
312
- }
313
- else {
314
- if (!stats.isFile()) {
315
- throw inspectionError(`Expected ${context} to be a file.`);
316
- }
317
- bytes = await readZipEntries(exec, artifactPath, format, context);
318
- }
319
- const signals = platform === "ios" ? detectIosSignals(bytes) : detectAndroidSignals(bytes);
320
- // Android: only the keep-rule marker proves inclusion (see the file-level doc comment) -- the
321
- // other two Android signals are reported but can't flip `present` on their own. iOS mirrors
322
- // this: only the two real-code-only symbols prove inclusion (OR'd together so a stripped binary
323
- // that dropped one doesn't read as absent) -- the Info.plist keys are corroborating only and
324
- // can't flip `present` on their own either.
325
- const present = platform === "android"
326
- ? signals.includes("android-keep-rule-marker")
327
- : signals.includes("ios-core-marker-symbol") || signals.includes("ios-objc-class-symbol");
328
- return {
329
- platform,
330
- format,
331
- present,
332
- signals,
333
- };
334
- };
package/dist/bin.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"command-options.d.ts","sourceRoot":"","sources":["../../src/cli/command-options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;wDACwD;AACxD,eAAO,MAAM,0BAA0B,GAAI,OAAO,OAAO,EAAE,UAAU,MAAM,KAAG,MAAM,GAAG,SAYtF,CAAC;AAEF;;oFAEoF;AACpF,eAAO,MAAM,6BAA6B,GAAI,OAAO,OAAO,EAAE,UAAU,MAAM,KAAG,MAAM,GAAG,SAYzF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,MAAM,EAAE,EACvB,cAAc,MAAM,KACnB;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAcrC,CAAC;AAEF;4BAC4B;AAC5B,eAAO,MAAM,qBAAqB,GAAI,MAAM,SAAS,MAAM,EAAE,EAAE,cAAc,MAAM,KAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAMxG,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,MAAM,EAAE,EACvB,cAAc,MAAM,KACnB;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAcjE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,GAAG,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAkBtF,CAAC"}
@@ -1,95 +0,0 @@
1
- /**
2
- * Small option-parsing helpers shared across the v2 command handlers wired in `dispatch.ts`. Kept
3
- * separate from `dispatch.ts` itself so each parsing rule (numeric flags, the selector/target
4
- * positional split) has one obvious place to test in isolation.
5
- */
6
- import { usageError, validationError } from "../errors.js";
7
- /** Parses a `cac`-provided option value (already a number, a numeric string, or `undefined`) into a
8
- * positive integer, or throws a clear `usage_error`. */
9
- export const parsePositiveIntegerOption = (value, flagName) => {
10
- if (value === undefined) {
11
- return undefined;
12
- }
13
- const parsed = typeof value === "number" ? value : Number(value);
14
- if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
15
- throw usageError(`"${flagName}" must be a positive integer.`);
16
- }
17
- return parsed;
18
- };
19
- /** Parses a `cac`-provided option value into a non-negative integer (0 allowed, unlike
20
- * {@link parsePositiveIntegerOption}), or throws a clear `usage_error`. Used by `events --since`,
21
- * whose cursor `0` is a meaningful "everything retained" value, not an omission. */
22
- export const parseNonNegativeIntegerOption = (value, flagName) => {
23
- if (value === undefined) {
24
- return undefined;
25
- }
26
- const parsed = typeof value === "number" ? value : Number(value);
27
- if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
28
- throw usageError(`"${flagName}" must be a non-negative integer.`);
29
- }
30
- return parsed;
31
- };
32
- /**
33
- * Splits the positional args of a command shaped `<command> [selector] <target>` (e.g. `invoke
34
- * [selector] <tool>`, `tools [selector] <name>`): the last positional is always the required
35
- * target, everything before it (zero or one args) is the optional selector.
36
- */
37
- export const splitSelectorAndRequiredTarget = (args, commandUsage) => {
38
- if (args.length === 0) {
39
- throw usageError(`Usage: ${commandUsage}`);
40
- }
41
- if (args.length > 2) {
42
- throw usageError(`Usage: ${commandUsage} (too many arguments).`);
43
- }
44
- if (args.length === 1) {
45
- return { target: args[0] };
46
- }
47
- return { selector: args[0], target: args[1] };
48
- };
49
- /** Splits the positional args of a command shaped `<command> [selector]`: at most one positional,
50
- * the optional selector. */
51
- export const splitOptionalSelector = (args, commandUsage) => {
52
- if (args.length > 1) {
53
- throw usageError(`Usage: ${commandUsage} (too many arguments).`);
54
- }
55
- return { selector: args[0] };
56
- };
57
- /**
58
- * Splits the positional args of a command shaped `<command> [selector] [target]` (`tools [selector]
59
- * [name]`): with 2 args, `(selector, target)`; with 1, the single arg is ambiguous between "the
60
- * selector" and "the target with the selector omitted" — the caller resolves that (see
61
- * `commands/tools.ts`), so it comes back unlabeled here as `selectorOrTarget`.
62
- */
63
- export const splitOptionalSelectorAndTarget = (args, commandUsage) => {
64
- if (args.length > 2) {
65
- throw usageError(`Usage: ${commandUsage} (too many arguments).`);
66
- }
67
- if (args.length === 2) {
68
- return { selector: args[0], target: args[1] };
69
- }
70
- if (args.length === 1) {
71
- return { selectorOrTarget: args[0] };
72
- }
73
- return {};
74
- };
75
- /**
76
- * Parses the JSON payload for `invoke --input`; never throws a raw `SyntaxError` at the CLI
77
- * boundary. A missing flag is a usage error (nothing was given); a present-but-unparseable or
78
- * wrong-shaped value is a validation error (something was given, and it's invalid).
79
- */
80
- export const parseJsonInputOption = (value) => {
81
- if (value === undefined) {
82
- throw usageError('"--input" is required.');
83
- }
84
- let parsed;
85
- try {
86
- parsed = JSON.parse(value);
87
- }
88
- catch (error) {
89
- throw validationError(`"--input" must be valid JSON: ${error.message}`);
90
- }
91
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
92
- throw validationError('"--input" must be a JSON object.');
93
- }
94
- return parsed;
95
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"create-cli.d.ts","sourceRoot":"","sources":["../../src/cli/create-cli.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,SAAS,yBA2FrB,CAAC"}
@@ -1,56 +0,0 @@
1
- import { cac } from "cac";
2
- import { getPackageVersion } from "../package-version.js";
3
- export const createCli = () => {
4
- const version = getPackageVersion();
5
- const cli = cac("appduct");
6
- cli.option("--json", "Print machine-readable JSON (NDJSON for streaming commands).");
7
- cli.option("--no-color", "Disable terminal color in human-readable output.");
8
- cli.option("--state-dir <path>", "Override the Appduct state directory (default: ~/.appduct).");
9
- cli.option("--daemon-restart", "On a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links.");
10
- cli
11
- .command("init", "Set up the current app directory: write .appduct/config.json and print the MCP snippet.")
12
- .option("--scheme <scheme>", "Deep-link URI scheme to write. Only this flag and <cwd>/app.json are consulted — not " +
13
- "APPDUCT_SCHEME, and no walk-up: init decides what to write here, so it never bakes " +
14
- "an ambient value into a committed file.")
15
- .option("--force", "Replace the scheme already recorded in the project config.");
16
- cli
17
- .command("keygen", "Generate an Appduct host private key and print its app fingerprint.")
18
- .option("--out <path>", "Destination path (default: <state-dir>/key.pem).")
19
- .option("--force", "Overwrite an existing key at the destination path.");
20
- cli
21
- .command("link", "Mint a pending session and print its deep link.")
22
- .option("--ttl <seconds>", "Link time-to-live in seconds (default: from config.json).")
23
- .option("--qr", "Also render the deep link as a terminal QR code.")
24
- .option("--scheme <scheme>", "Deep-link URI scheme (also: APPDUCT_SCHEME; default: app.json's \"expo.scheme\").")
25
- .option("--open <target>", "Deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental).")
26
- .option("--device <id>", "adb serial, simulator udid or paired-device udid to target when --open is ambiguous.")
27
- .option("--bundle-id <id>", "App bundle id for --open ios-device (default: config.json's \"iosBundleId\").")
28
- .option("--relaunch", "With --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing).");
29
- cli.command("ls", "List Appduct sessions.");
30
- cli
31
- .command("tools [selector] [name]", "List a session's tools, or show one tool's full schema.")
32
- .option("--full", "Render full schemas/annotations for every listed tool.");
33
- cli
34
- .command("invoke [selector] [tool]", "Call a tool on a session.")
35
- .option("--input <json>", "Tool input arguments as a JSON object.")
36
- .option("--timeout <ms>", "Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's " +
37
- "own timer, which is the tool's declared timeoutMs (else 10000).");
38
- cli
39
- .command("events [selector]", "Stream session/tool events until interrupted.")
40
- .option("--follow", "Accepted for script readability; the default behavior already follows.")
41
- .option("--since <cursor>", "One-shot: print events retained since this cursor instead of streaming live.");
42
- cli.command("revoke [selector]", "Revoke a session.");
43
- cli
44
- .command("mcp", "Start a stdio MCP server that proxies connected apps' tools to MCP clients.")
45
- .option("--scheme <scheme>", "Deep-link URI scheme for appduct_connect (also: APPDUCT_SCHEME).");
46
- cli
47
- .command("doctor <artifact>", "Report (or assert) whether a built .app/.ipa/.apk/.aab contains Appduct.")
48
- .option("--assert-present", "Exit non-zero (and report) if Appduct is not present in the artifact.")
49
- .option("--assert-absent", "Exit non-zero (and report) if Appduct is present in the artifact.");
50
- // cac only matches a command's first word against argv[0], so "run"/"start"/"stop"/"status"
51
- // are handled as a sub-action of the single "daemon" command rather than four cac commands.
52
- cli.command("daemon [action]", "Manage the Appduct daemon: run, start, stop, or status.");
53
- cli.help();
54
- cli.version(version);
55
- return cli;
56
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,eAAO,MAAM,MAAM,GAAU,MAAM,MAAM,EAAE,EAAE,UAAS,aAAkB,KAAG,OAAO,CAAC,MAAM,CAmYxF,CAAC"}