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,465 +0,0 @@
1
- /**
2
- * Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps
3
- * (docs/tasks/20-cli-native-scheme-discovery.md, phase 3 of issue #48).
4
- *
5
- * `scheme.ts`'s `resolveScheme` treats this module as one more thing to try after `<cwd>/app.json`
6
- * — it never walks up (an app root is where these commands run, same rule as `app.json`), and,
7
- * like every other source in that file, **nothing here executes project code**: no `plutil`, no
8
- * `xcodebuild`, no Gradle evaluation. Every probe is a plain read of a well-known project file,
9
- * parsed defensively:
10
- *
11
- * - Android: `app/build.gradle.kts` / `app/build.gradle` for a `manifestPlaceholders["appductScheme"]`
12
- * (or `.appductScheme =`) assignment, then `app/src/main/AndroidManifest.xml` for the first
13
- * `<data android:scheme>` inside an intent filter that also declares
14
- * `android.intent.action.VIEW`.
15
- * - iOS: any `Info.plist` up to two levels below the app root (excluding `Pods`, `build`,
16
- * `node_modules` and `DerivedData` — vendored/build-output trees that are not the project's own
17
- * config, and can be enormous) for the first `CFBundleURLSchemes` entry, then `project.yml`
18
- * (xcodegen) for the same key under `info.properties.CFBundleURLTypes`.
19
- *
20
- * A binary-encoded `Info.plist` (Xcode can write either encoding) is reported as unreadable rather
21
- * than decoded — `plutil -convert xml1` would work, but that is exactly the "run a tool against
22
- * project state" this module exists to avoid, so a binary plist just falls through to `--scheme`
23
- * like any other file this can't make sense of.
24
- *
25
- * **Disagreement is not guessed away.** If more than one probe below resolves to a *different*
26
- * scheme (e.g. a monorepo whose `android/` and `ios/` declare different values), discovery throws
27
- * a usage error naming every conflicting source instead of picking one — silently choosing would
28
- * mean `appduct link` opens the wrong app on whichever platform lost.
29
- */
30
- import { readdir, readFile, stat } from "node:fs/promises";
31
- import { join } from "node:path";
32
- import { firstConfiguredScheme, isValidScheme } from "./scheme.js";
33
- import { usageError } from "./errors.js";
34
- // --- shared file-reading caps ---
35
- /** A `build.gradle`/`AndroidManifest.xml`/`Info.plist`/`project.yml` this large is not a file
36
- * these probes are meant to read — treated the same as "not found" rather than buffering an
37
- * arbitrarily large file into memory for a single regex/parse pass. */
38
- const MAX_PROBE_FILE_BYTES = 2 * 1024 * 1024;
39
- /** Depth-limited walk cap for the `Info.plist` search: 0 = the app root itself, 1 = its immediate
40
- * subdirectories, 2 = one level below that (where `MyApp.xcodeproj`'s sibling `Info.plist` and a
41
- * typical `MyApp/Info.plist` both live). */
42
- const MAX_PLIST_WALK_DEPTH = 2;
43
- /** Defensive cap on directory entries visited by the `Info.plist` walk, so a pathological tree
44
- * (a huge `node_modules` that slipped past the exclusion, a symlink farm) can't turn a static-file
45
- * probe into an unbounded scan. */
46
- const MAX_WALK_ENTRIES = 5000;
47
- /** Directories the `Info.plist` walk never descends into: vendored/build-output trees, not the
48
- * project's own config, and often enormous. */
49
- const EXCLUDED_WALK_DIRS = new Set(["Pods", "build", "node_modules", "DerivedData"]);
50
- /** Reads a file as text, capped at {@link MAX_PROBE_FILE_BYTES}. A missing file, a directory where
51
- * a file was expected, or a permission error are all "nothing usable here" — a caller falls
52
- * through to the next probe rather than treating any of these as a hard failure. */
53
- const readCappedText = async (path) => {
54
- const buffer = await readCappedBuffer(path);
55
- if (!buffer.ok) {
56
- return buffer;
57
- }
58
- return { ok: true, text: buffer.data.toString("utf8") };
59
- };
60
- const readCappedBuffer = async (path) => {
61
- let size;
62
- try {
63
- const stats = await stat(path);
64
- if (!stats.isFile()) {
65
- return { ok: false, reason: "not-found" };
66
- }
67
- size = stats.size;
68
- }
69
- catch {
70
- return { ok: false, reason: "not-found" };
71
- }
72
- if (size > MAX_PROBE_FILE_BYTES) {
73
- return { ok: false, reason: "too-large" };
74
- }
75
- try {
76
- return { ok: true, data: await readFile(path) };
77
- }
78
- catch {
79
- return { ok: false, reason: "unreadable" };
80
- }
81
- };
82
- /** A probe-reported scheme still has to be a legal URI scheme — the same rule every other source
83
- * in `scheme.ts` enforces, so a typo'd placeholder fails with a clear message instead of composing
84
- * an unopenable deep link three commands later. */
85
- const requireValidNativeScheme = (value, origin) => {
86
- if (!isValidScheme(value)) {
87
- throw usageError(`Invalid deep-link scheme ${JSON.stringify(value)} from ${origin}: a scheme must start with ` +
88
- 'a letter and contain only letters, digits, "+", "-" or "." (for example "myapp") — do not ' +
89
- 'include "://".');
90
- }
91
- return value;
92
- };
93
- // --- Android: build.gradle(.kts) "appductScheme" placeholder ---
94
- /**
95
- * Matches both Groovy DSL (`manifestPlaceholders["appductScheme"] = "myapp"`) and Kotlin DSL
96
- * (`manifestPlaceholders["appductScheme"] = "myapp"` or the property form
97
- * `manifestPlaceholders.appductScheme = "myapp"`) — issue #48's sketch shows the bracket form
98
- * for both, but the property form is common enough in real `build.gradle.kts` files to accept too.
99
- */
100
- const GRADLE_PLACEHOLDER_PATTERN = /manifestPlaceholders\s*(?:\[\s*["']appductScheme["']\s*\]|\.\s*appductScheme)\s*=\s*["']([^"']*)["']/u;
101
- const extractGradleScheme = (text) => {
102
- const match = GRADLE_PLACEHOLDER_PATTERN.exec(text);
103
- const value = match?.[1];
104
- return value !== undefined && value.length > 0 ? value : undefined;
105
- };
106
- const probeAndroidGradle = async (root) => {
107
- const candidates = [join(root, "app", "build.gradle.kts"), join(root, "app", "build.gradle")];
108
- const notes = [];
109
- for (const candidate of candidates) {
110
- const read = await readCappedText(candidate);
111
- if (!read.ok) {
112
- if (read.reason === "too-large") {
113
- notes.push(`${candidate} (too large, skipped)`);
114
- }
115
- continue;
116
- }
117
- const scheme = extractGradleScheme(read.text);
118
- if (scheme !== undefined) {
119
- requireValidNativeScheme(scheme, `"appductScheme" in ${candidate}`);
120
- return {
121
- result: {
122
- source: "android-gradle",
123
- scheme,
124
- origin: `"appductScheme" in ${candidate}`,
125
- },
126
- tried: [describeAndroidGradleTried(candidates, notes)],
127
- };
128
- }
129
- }
130
- return { tried: [describeAndroidGradleTried(candidates, notes)] };
131
- };
132
- const describeAndroidGradleTried = (candidates, notes) => {
133
- const base = `${candidates.join(", ")} ("appductScheme" manifest placeholder)`;
134
- return notes.length === 0 ? base : `${base} — ${notes.join(", ")}`;
135
- };
136
- // --- Android: AndroidManifest.xml <data android:scheme> in a VIEW intent filter ---
137
- const INTENT_FILTER_PATTERN = /<intent-filter\b[^>]*>([\s\S]*?)<\/intent-filter>/giu;
138
- const VIEW_ACTION_PATTERN = /<action\b[^>]*\bandroid:name\s*=\s*["']android\.intent\.action\.VIEW["']/iu;
139
- const DATA_SCHEME_PATTERN = /<data\b[^>]*\bandroid:scheme\s*=\s*["']([^"']*)["']/iu;
140
- /** The first `<data android:scheme>` inside an intent filter that also declares
141
- * `android.intent.action.VIEW` — a filter for some other action (`SEND`, `MAIN`, ...) is not a
142
- * deep-link scheme declaration and is skipped even if it happens to carry a `<data>` tag. */
143
- const extractManifestScheme = (xml) => {
144
- for (const match of xml.matchAll(INTENT_FILTER_PATTERN)) {
145
- const block = match[1] ?? "";
146
- if (!VIEW_ACTION_PATTERN.test(block)) {
147
- continue;
148
- }
149
- const dataMatch = DATA_SCHEME_PATTERN.exec(block);
150
- const scheme = dataMatch?.[1];
151
- if (scheme !== undefined && scheme.length > 0) {
152
- return scheme;
153
- }
154
- }
155
- return undefined;
156
- };
157
- const probeAndroidManifest = async (root) => {
158
- const manifestPath = join(root, "app", "src", "main", "AndroidManifest.xml");
159
- const read = await readCappedText(manifestPath);
160
- const description = `${manifestPath} (first <data android:scheme> in a VIEW intent-filter)`;
161
- if (!read.ok) {
162
- return {
163
- tried: [read.reason === "too-large" ? `${description} — too large, skipped` : description],
164
- };
165
- }
166
- const scheme = extractManifestScheme(read.text);
167
- if (scheme === undefined) {
168
- return { tried: [description] };
169
- }
170
- requireValidNativeScheme(scheme, `"android:scheme" in ${manifestPath}`);
171
- return {
172
- result: {
173
- source: "android-manifest",
174
- scheme,
175
- origin: `"android:scheme" in ${manifestPath}`,
176
- },
177
- tried: [description],
178
- };
179
- };
180
- const XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
181
- const decodeXmlEntities = (text) => {
182
- return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/gu, (whole, entity) => {
183
- if (entity.startsWith("#x")) {
184
- const code = Number.parseInt(entity.slice(2), 16);
185
- return Number.isNaN(code) ? whole : String.fromCodePoint(code);
186
- }
187
- if (entity.startsWith("#")) {
188
- const code = Number.parseInt(entity.slice(1), 10);
189
- return Number.isNaN(code) ? whole : String.fromCodePoint(code);
190
- }
191
- return XML_ENTITIES[entity] ?? whole;
192
- });
193
- };
194
- /**
195
- * Turns an XML plist into a tree of tags — deliberately not a general/validating XML parser (no
196
- * DTD, no namespaces, no attribute parsing beyond stripping them): plist's grammar is small and
197
- * fixed (`dict`/`array`/`key`/`string`/`integer`/`real`/`true`/`false`/`date`/`data`), and being
198
- * tolerant of the ways a hand-written or generated `Info.plist` can still deviate slightly (a
199
- * stray unescaped `&`, mismatched nesting) matters more here than rejecting a technically-invalid
200
- * document.
201
- */
202
- const tokenizePlistXml = (xml) => {
203
- const cleaned = xml
204
- .replace(/<\?[\s\S]*?\?>/gu, "")
205
- .replace(/<!DOCTYPE[\s\S]*?>/giu, "")
206
- .replace(/<!--[\s\S]*?-->/gu, "");
207
- const root = { tag: "#root", children: [], text: "" };
208
- const stack = [root];
209
- const tagOrTextPattern = /<(\/?)([a-zA-Z][\w.:-]*)\b[^>]*?(\/?)>|([^<]+)/gu;
210
- for (const match of cleaned.matchAll(tagOrTextPattern)) {
211
- const [, closing, name, selfClosing, text] = match;
212
- // `stack` always has `root` at index 0 and is only ever popped down to length 1, so the top
213
- // is never actually undefined — the non-null assertion just satisfies noUncheckedIndexedAccess.
214
- const top = stack[stack.length - 1];
215
- if (text !== undefined) {
216
- top.text += decodeXmlEntities(text);
217
- continue;
218
- }
219
- if (closing) {
220
- if (stack.length > 1) {
221
- stack.pop();
222
- }
223
- continue;
224
- }
225
- const node = { tag: name ?? "", children: [], text: "" };
226
- top.children.push(node);
227
- if (!selfClosing) {
228
- stack.push(node);
229
- }
230
- }
231
- return root;
232
- };
233
- const plistNodeToValue = (node) => {
234
- switch (node.tag) {
235
- case "true":
236
- return true;
237
- case "false":
238
- return false;
239
- case "integer": {
240
- const value = Number.parseInt(node.text.trim(), 10);
241
- return Number.isNaN(value) ? node.text.trim() : value;
242
- }
243
- case "real": {
244
- const value = Number.parseFloat(node.text.trim());
245
- return Number.isNaN(value) ? node.text.trim() : value;
246
- }
247
- case "array":
248
- return node.children.map(plistNodeToValue);
249
- case "dict": {
250
- const result = {};
251
- for (let index = 0; index < node.children.length; index += 2) {
252
- const keyNode = node.children[index];
253
- const valueNode = node.children[index + 1];
254
- if (keyNode?.tag === "key" && valueNode !== undefined) {
255
- result[keyNode.text.trim()] = plistNodeToValue(valueNode);
256
- }
257
- }
258
- return result;
259
- }
260
- default:
261
- // "string", and every leaf type this probe has no specific use for (data, date): the text
262
- // content is the closest thing to a value, and is simply never read for those tags.
263
- return node.text.trim();
264
- }
265
- };
266
- /** `undefined` for anything that isn't a well-formed `<plist>…</plist>` document (including a
267
- * binary plist, whose bytes tokenize into garbage tag names rather than throwing). */
268
- const parsePlistXml = (xml) => {
269
- const root = tokenizePlistXml(xml);
270
- const plistNode = root.children.find((child) => child.tag === "plist");
271
- const valueNode = plistNode?.children[0];
272
- return valueNode ? plistNodeToValue(valueNode) : undefined;
273
- };
274
- /** Apple's magic number for a binary-encoded plist (`bplist00`/`bplist01`/...) — checked on the
275
- * raw bytes before any text decoding is attempted, since a binary plist's bytes are not valid
276
- * UTF-8 and decoding them first would just produce mangled tag soup instead of a clean signal. */
277
- const isBinaryPlist = (buffer) => {
278
- return buffer.length >= 6 && buffer.subarray(0, 6).toString("latin1") === "bplist";
279
- };
280
- /** The first non-empty `CFBundleURLSchemes` entry across every `CFBundleURLTypes` dict, in array
281
- * order — mirrors `firstConfiguredScheme`'s "first entry wins" rule, extended to "first dict that
282
- * actually has one wins" since an app can declare more than one `CFBundleURLTypes` entry (custom
283
- * scheme plus a universal-link-only entry with no `CFBundleURLSchemes` at all, say). */
284
- const firstPlistUrlScheme = (cfBundleURLTypes) => {
285
- if (!Array.isArray(cfBundleURLTypes)) {
286
- return undefined;
287
- }
288
- for (const entry of cfBundleURLTypes) {
289
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
290
- continue;
291
- }
292
- const scheme = firstConfiguredScheme(entry.CFBundleURLSchemes);
293
- if (scheme !== undefined) {
294
- return scheme;
295
- }
296
- }
297
- return undefined;
298
- };
299
- /**
300
- * Every file named `filename` from `root` down to `maxDepth` levels, nearest first (ties broken by
301
- * path) — the deterministic order that decides which `Info.plist` is "the first one" when several
302
- * exist. Symlinks are never followed (matches `artifact-inspect.ts`'s `readAppDirectory`, same
303
- * reasoning: avoids loops, and a plist worth discovering is never reached only through one).
304
- */
305
- const walkForFilename = async (root, filename, maxDepth) => {
306
- const hits = [];
307
- let visited = 0;
308
- const walk = async (dir, depth) => {
309
- let entries;
310
- try {
311
- entries = await readdir(dir, { withFileTypes: true });
312
- }
313
- catch {
314
- return;
315
- }
316
- for (const entry of entries) {
317
- if (visited >= MAX_WALK_ENTRIES) {
318
- return;
319
- }
320
- visited += 1;
321
- if (entry.isSymbolicLink()) {
322
- continue;
323
- }
324
- if (entry.isDirectory()) {
325
- if (EXCLUDED_WALK_DIRS.has(entry.name)) {
326
- continue;
327
- }
328
- if (depth < maxDepth) {
329
- await walk(join(dir, entry.name), depth + 1);
330
- }
331
- continue;
332
- }
333
- if (entry.isFile() && entry.name === filename) {
334
- hits.push({ path: join(dir, entry.name), depth });
335
- }
336
- }
337
- };
338
- await walk(root, 0);
339
- hits.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
340
- return hits;
341
- };
342
- const probeIosInfoPlist = async (root) => {
343
- const files = await walkForFilename(root, "Info.plist", MAX_PLIST_WALK_DEPTH);
344
- const describeSearch = (notes) => {
345
- const base = files.length === 0
346
- ? `Info.plist up to ${MAX_PLIST_WALK_DEPTH} levels below ${root} (excluding Pods/build/` +
347
- "node_modules/DerivedData) — none found"
348
- : `${files.map((file) => file.path).join(", ")} (first "CFBundleURLSchemes" entry)`;
349
- return notes.length === 0 ? base : `${base} — ${notes.join(", ")}`;
350
- };
351
- const notes = [];
352
- for (const file of files) {
353
- const read = await readCappedBuffer(file.path);
354
- if (!read.ok) {
355
- notes.push(`${file.path} (${read.reason === "too-large" ? "too large, skipped" : "unreadable"})`);
356
- continue;
357
- }
358
- if (isBinaryPlist(read.data)) {
359
- notes.push(`${file.path} (binary plist — unreadable, use --scheme)`);
360
- continue;
361
- }
362
- const value = parsePlistXml(read.data.toString("utf8"));
363
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
364
- continue;
365
- }
366
- const scheme = firstPlistUrlScheme(value.CFBundleURLTypes);
367
- if (scheme === undefined) {
368
- continue;
369
- }
370
- requireValidNativeScheme(scheme, `"CFBundleURLSchemes" in ${file.path}`);
371
- return {
372
- result: {
373
- source: "ios-info-plist",
374
- scheme,
375
- origin: `"CFBundleURLSchemes" in ${file.path}`,
376
- },
377
- tried: [describeSearch(notes)],
378
- };
379
- }
380
- return { tried: [describeSearch(notes)] };
381
- };
382
- // --- iOS: xcodegen's project.yml (info.properties.CFBundleURLTypes) ---
383
- /**
384
- * Regex-based rather than a general YAML parser: `project.yml`'s `CFBundleURLTypes` shape is
385
- * exactly the plist one (an array of dicts, each with a `CFBundleURLSchemes` array or scalar), and
386
- * a full YAML grammar (anchors, flow vs. block collections, multiline scalars) is far more than
387
- * this one key needs. Matches both the inline flow form (`CFBundleURLSchemes: [myapp]`) and the
388
- * block sequence form (`CFBundleURLSchemes:\n - myapp`), quoted or not.
389
- *
390
- * Deliberately *not* restricted to the legal scheme character set here (unlike the plist/Expo
391
- * path, which parses the value as a generic string first): capturing everything up to the
392
- * unambiguous YAML boundary (`]`, `,`, a quote, or the end of the line) and letting
393
- * {@link requireValidNativeScheme} reject it is what lets a genuinely malformed value (say,
394
- * `myapp://`) surface the same "invalid deep-link scheme" error the other three probes give,
395
- * instead of silently truncating it into something that looks valid.
396
- */
397
- const PROJECT_YML_SCHEME_PATTERN = /CFBundleURLSchemes\s*:\s*(?:\[\s*["']?([^"'\],\r\n]*)["']?|\r?\n[ \t]*-[ \t]*["']?([^"'\r\n]*)["']?)/u;
398
- const extractProjectYmlScheme = (text) => {
399
- const match = PROJECT_YML_SCHEME_PATTERN.exec(text);
400
- const raw = (match?.[1] ?? match?.[2])?.trim();
401
- return raw !== undefined && raw.length > 0 ? raw : undefined;
402
- };
403
- const probeIosProjectYml = async (root) => {
404
- const path = join(root, "project.yml");
405
- const description = `${path} ("info.properties.CFBundleURLTypes" > "CFBundleURLSchemes")`;
406
- const read = await readCappedText(path);
407
- if (!read.ok) {
408
- return {
409
- tried: [read.reason === "too-large" ? `${description} — too large, skipped` : description],
410
- };
411
- }
412
- const scheme = extractProjectYmlScheme(read.text);
413
- if (scheme === undefined) {
414
- return { tried: [description] };
415
- }
416
- requireValidNativeScheme(scheme, `"CFBundleURLSchemes" in ${path}`);
417
- return {
418
- result: { source: "ios-project-yml", scheme, origin: `"CFBundleURLSchemes" in ${path}` },
419
- tried: [description],
420
- };
421
- };
422
- // --- orchestration ---
423
- /** In the order the file-level doc comment (and issue #48) describes: Android before iOS, and
424
- * within each platform the build config before the manifest/project file that mirrors it. */
425
- const PROBES = [
426
- probeAndroidGradle,
427
- probeAndroidManifest,
428
- probeIosInfoPlist,
429
- probeIosProjectYml,
430
- ];
431
- /**
432
- * Runs every static-file probe against `root` (never walking up — same rule as `app.json`) and
433
- * decides the result:
434
- *
435
- * - No probe resolves a scheme: `result` is `undefined`.
436
- * - Every probe that resolves one agrees: that scheme wins, attributed to whichever probe ran
437
- * first (the order in {@link PROBES}).
438
- * - Two probes resolve *different* schemes: throws a usage error naming both sources and their
439
- * values, since guessing which one is right would silently target the wrong app on one
440
- * platform.
441
- *
442
- * `tried` always has one entry per probe, hit or miss, so a "no scheme anywhere" error can list
443
- * every location this looked without the caller needing to know which probes exist.
444
- */
445
- export const discoverNativeScheme = async (root) => {
446
- const tried = [];
447
- const results = [];
448
- for (const probe of PROBES) {
449
- const outcome = await probe(root);
450
- tried.push(...outcome.tried);
451
- if (outcome.result !== undefined) {
452
- results.push(outcome.result);
453
- }
454
- }
455
- if (results.length === 0) {
456
- return { tried };
457
- }
458
- const distinctSchemes = new Set(results.map((entry) => entry.scheme));
459
- if (distinctSchemes.size > 1) {
460
- throw usageError("Conflicting deep-link schemes found in this project: " +
461
- results.map((entry) => `${entry.origin} says ${JSON.stringify(entry.scheme)}`).join(", but ") +
462
- ". Appduct will not guess which one is right — pass --scheme <scheme> to pick one.");
463
- }
464
- return { result: results[0], tried };
465
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,iBAAiB,EAA4C,MAAM,iBAAiB,CAAC;AAE5H,OAAO,KAAK,EAEV,SAAS,EAcV,MAAM,uBAAuB,CAAC;AAK/B,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,iGAAiG;IACjG,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,sGAAsG;IACtG,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAmWF,eAAO,MAAM,YAAY,GACvB,QAAQ,SAAS,CAAC,OAAO,CAAC,EAC1B,SAAS,aAAa,KACrB;IACD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CA+BjB,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,eAAe,GAC1B,OAAO,iBAAiB,EACxB,SAAS;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,KACzC,MAWF,CAAC;AAEF;;6EAE6E;AAC7E,eAAO,MAAM,sBAAsB,GAAI,QAAQ,MAAM,EAAE,SAAS;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,KAAG,MAOnG,CAAC"}