@vostride/agent-qa-appium-runtime 0.0.1 → 0.3.7

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.
package/LICENSE.md ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Pranshu Chittora
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/NOTICE.md ADDED
@@ -0,0 +1,11 @@
1
+ # agent-qa Notice
2
+
3
+ agent-qa is provided by Pranshu Chittora.
4
+
5
+ Copyright 2026 Pranshu Chittora
6
+
7
+ agent-qa is licensed under the Functional Source License, Version 1.1, ALv2 Future License (FSL-1.1-ALv2). See `LICENSE.md` for the full license terms.
8
+
9
+ Under the Grant of Future License in `LICENSE.md`, each version of the Software is also granted under the Apache License, Version 2.0 effective on the second anniversary of the date that version was made available.
10
+
11
+ Third-party dependencies are distributed under their own license terms. This notice covers agent-qa itself and does not replace or modify notices required by third-party dependency owners.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-dark.jpg">
3
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-light.png">
4
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/banner-light.png" alt="agent-qa by vostride — a self-improving QA agent for your software factory" width="100%">
5
+ </picture>
6
+
7
+ <p align="center">
8
+ <strong>English</strong> · <a href="https://github.com/vostride/agent-qa/blob/main/README.zh-CN.md">中文</a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/agent-qa"><img src="https://img.shields.io/npm/v/agent-qa?color=196872" alt="npm version"></a>
13
+ <a href="https://www.npmjs.com/package/agent-qa"><img src="https://img.shields.io/npm/dm/agent-qa?color=196872" alt="npm downloads per month"></a>
14
+ <a href="https://github.com/vostride/agent-qa/stargazers"><img src="https://img.shields.io/github/stars/vostride/agent-qa?color=196872" alt="GitHub stars"></a>
15
+ <a href="https://github.com/vostride/agent-qa/actions/workflows/ci.yml"><img src="https://github.com/vostride/agent-qa/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
16
+ <a href="https://vostride.com/docs/agent-qa"><img src="https://img.shields.io/badge/docs-agent--qa-196872" alt="Documentation"></a>
17
+ <a href="https://github.com/vostride/agent-qa/blob/main/LICENSE.md"><img src="https://img.shields.io/badge/license-FSL--1.1--ALv2-196872" alt="License: FSL-1.1-ALv2"></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://vostride.com/agent-qa">Website</a> · <a href="https://vostride.com/docs/agent-qa">Docs</a> · <a href="https://vostride.com/demo/agent-qa">Demo</a> · <a href="https://vostride.com/blog">Blog</a> · <a href="https://vostride.com/changelog">Changelog</a>
22
+ </p>
23
+
24
+ **Your AI QA engineer, with memory.** Describe a user journey in plain language. agent-qa tests it on web, Android or iOS and keeps the evidence. Use the CLI, the dashboard, or your coding agent through MCP and skills.
25
+
26
+ - **memidex:** remembers facts and working procedures as reviewable files in your repository.
27
+ - **Self-improvement:** validates lessons from runs, retires stale knowledge, and uses your feedback to correct mistakes.
28
+
29
+ | Benchmark | Rank | Published result |
30
+ | --- | --- | --- |
31
+ | [AndroidWorld](https://vostride.com/benchmark/android-world) | **#1** | **100% pass@1** (116/116 tasks) |
32
+ | [AppControlBench](https://vostride.com/benchmark/app-control-bench) | **#1** | **99.6% completion** (114.5/115 points) |
33
+
34
+ Read the full benchmark reports, with run evidence and methodology, on [our blog](https://vostride.com/blog). Both used gpt-6-sol with memory and self-improvement off; AppControlBench gives half credit for partial completion.
35
+
36
+ ## See it in action
37
+
38
+ **Inspect a failure.** The demo catches a cart showing $29 instead of $53, with the failed step and its screenshot side by side.
39
+
40
+ <picture>
41
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-dark.png">
42
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-light.png">
43
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/evidence-light.png" alt="The dashboard shows the deliberate cart-total bug, its failed assertion, and recorded browser evidence" width="100%">
44
+ </picture>
45
+
46
+ **See what memory contributed.** Inspect the knowledge supplied to a run and trace it back to the steps that used it.
47
+
48
+ <picture>
49
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-dark.png">
50
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-light.png">
51
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/knowledge-light.png" alt="The Knowledge tab shows stored facts retrieved for the demo run" width="100%">
52
+ </picture>
53
+
54
+ **Run from your terminal.** Real output from the demo examples: the checkout passes and the test on the deliberate cart-total bug fails.
55
+
56
+ <picture>
57
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-dark.png">
58
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-light.png">
59
+ <img src="https://raw.githubusercontent.com/vostride/agent-qa/main/docs/assets/readme/cli-light.png" alt="CLI output excerpt showing one passed test, one failed test, and eight of nine steps passing" width="100%">
60
+ </picture>
61
+
62
+ ## Get started
63
+
64
+ Requires Node.js 24+.
65
+
66
+ ```sh
67
+ npm install -D agent-qa
68
+ npx agent-qa init
69
+ ```
70
+
71
+ Follow the [quickstart](https://vostride.com/docs/agent-qa/quickstart) to connect a model and run the demo, or [set up with your coding agent](https://vostride.com/docs/agent-qa/agent-quickstart).
72
+
73
+ > [!NOTE]
74
+ > Early stage. Expect bugs. [Report an issue](https://github.com/vostride/agent-qa/issues) when something breaks.
75
+
76
+ ## License
77
+
78
+ [Functional Source License (FSL-1.1-ALv2)](https://github.com/vostride/agent-qa/blob/main/LICENSE.md).
package/dist/index.cjs ADDED
@@ -0,0 +1,268 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="926327cf-3053-5645-bf57-b1fd4889d76c")}catch(e){}}();
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
+ // \0rolldown/runtime.js
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
+ key = keys[i];
14
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
22
+ value: mod,
23
+ enumerable: true
24
+ }) : target, mod));
25
+ let node_child_process = require("node:child_process");
26
+ let node_fs = require("node:fs");
27
+ let node_http = require("node:http");
28
+ let node_net = require("node:net");
29
+ let node_fs_promises = require("node:fs/promises");
30
+ let node_os = require("node:os");
31
+ let node_path = require("node:path");
32
+ let node_module = require("node:module");
33
+ let tree_kill = require("tree-kill");
34
+ tree_kill = __toESM(tree_kill, 1);
35
+ // src/appium-home.ts
36
+ async function prepareAppiumHome(driverFolders) {
37
+ const home = await (0, node_fs_promises.realpath)(await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "agent-qa-appium-")));
38
+ try {
39
+ await (0, node_fs_promises.mkdir)((0, node_path.join)(home, "node_modules"));
40
+ for (const folder of driverFolders) await (0, node_fs_promises.symlink)(folder, (0, node_path.join)(home, "node_modules", (0, node_path.basename)(folder)), "junction");
41
+ } catch (error) {
42
+ await removeAppiumHome(home).catch(() => {});
43
+ throw error;
44
+ }
45
+ return home;
46
+ }
47
+ async function removeAppiumHome(home) {
48
+ await (0, node_fs_promises.rm)(home, {
49
+ recursive: true,
50
+ force: true,
51
+ maxRetries: 3,
52
+ retryDelay: 100
53
+ });
54
+ }
55
+ // src/bundled-appium.ts
56
+ const require$1 = (0, node_module.createRequire)(typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href);
57
+ function resolveBundledAppium() {
58
+ return {
59
+ serverEntry: require$1.resolve("appium/build/lib/main.js"),
60
+ driverFolders: [(0, node_path.dirname)(require$1.resolve("appium-uiautomator2-driver/package.json")), (0, node_path.dirname)(require$1.resolve("appium-xcuitest-driver/package.json"))]
61
+ };
62
+ }
63
+ // src/kill-tree.ts
64
+ function killTree(pid, options = {}) {
65
+ if ((options.platform ?? process.platform) === "linux") {
66
+ let members = [pid];
67
+ try {
68
+ members = linuxTree(pid, options.proc ?? "/proc");
69
+ } catch {}
70
+ for (const member of members) try {
71
+ process.kill(member, "SIGKILL");
72
+ } catch {}
73
+ return Promise.resolve();
74
+ }
75
+ return new Promise((settle) => {
76
+ (0, tree_kill.default)(pid, "SIGKILL", () => settle());
77
+ });
78
+ }
79
+ function linuxTree(root, proc) {
80
+ const children = /* @__PURE__ */ new Map();
81
+ for (const entry of (0, node_fs.readdirSync)(proc)) {
82
+ if (!/^\d+$/u.test(entry)) continue;
83
+ let stat;
84
+ try {
85
+ stat = (0, node_fs.readFileSync)((0, node_path.join)(proc, entry, "stat"), "utf8");
86
+ } catch {
87
+ continue;
88
+ }
89
+ const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
90
+ children.set(parent, [...children.get(parent) ?? [], Number(entry)]);
91
+ }
92
+ const tree = [root];
93
+ for (const member of tree) tree.push(...children.get(member) ?? []);
94
+ return tree;
95
+ }
96
+ // src/start-appium-server.ts
97
+ const LOOPBACK = "127.0.0.1";
98
+ const STARTUP_TIMEOUT_MS = 3e4;
99
+ const POLL_INTERVAL_MS = 500;
100
+ const KEPT_LINES = 10;
101
+ const spawnNode = (command, args, options) => (0, node_child_process.spawn)(command, [...args], options);
102
+ function isAppiumReady(url) {
103
+ return new Promise((settle) => {
104
+ const req = (0, node_http.request)(`${url}/status`, {
105
+ method: "GET",
106
+ timeout: 3e3
107
+ }, (res) => {
108
+ let data = "";
109
+ res.on("data", (chunk) => {
110
+ data += chunk.toString();
111
+ });
112
+ res.on("end", () => {
113
+ try {
114
+ settle(JSON.parse(data)?.value?.ready === true);
115
+ } catch {
116
+ settle(false);
117
+ }
118
+ });
119
+ });
120
+ req.on("error", () => settle(false));
121
+ req.on("timeout", () => {
122
+ req.destroy();
123
+ settle(false);
124
+ });
125
+ req.end();
126
+ });
127
+ }
128
+ function findFreePort() {
129
+ return new Promise((settle, fail) => {
130
+ const server = (0, node_net.createServer)();
131
+ server.unref();
132
+ server.once("error", fail);
133
+ server.listen({
134
+ port: 0,
135
+ host: LOOPBACK
136
+ }, () => {
137
+ const address = server.address();
138
+ const port = typeof address === "object" && address !== null ? address.port : 0;
139
+ server.close(() => settle(port));
140
+ });
141
+ });
142
+ }
143
+ async function startAppiumServer(options = {}) {
144
+ const spawn = options.spawn ?? spawnNode;
145
+ const probe = options.probe ?? isAppiumReady;
146
+ const { serverEntry, driverFolders } = resolveBundledAppium();
147
+ const port = await findFreePort();
148
+ const home = await prepareAppiumHome(driverFolders);
149
+ let child;
150
+ try {
151
+ child = spawn(process.execPath, [
152
+ serverEntry,
153
+ "--address",
154
+ LOOPBACK,
155
+ "--port",
156
+ String(port),
157
+ "--relaxed-security",
158
+ "--deny-insecure",
159
+ "*:cors",
160
+ "--log-no-colors"
161
+ ], {
162
+ env: {
163
+ ...process.env,
164
+ APPIUM_HOME: home
165
+ },
166
+ stdio: [
167
+ "ignore",
168
+ "pipe",
169
+ "pipe"
170
+ ]
171
+ });
172
+ } catch (error) {
173
+ await removeAppiumHome(home).catch(() => {});
174
+ throw error;
175
+ }
176
+ const killOnExit = () => {
177
+ try {
178
+ child.kill("SIGKILL");
179
+ } catch {}
180
+ try {
181
+ (0, node_fs.rmSync)(home, {
182
+ recursive: true,
183
+ force: true
184
+ });
185
+ } catch {}
186
+ };
187
+ process.once("exit", killOnExit);
188
+ child.unref();
189
+ child.stdout?.unref();
190
+ child.stderr?.unref();
191
+ const lastLines = [];
192
+ const forward = (stream) => (chunk) => {
193
+ const text = chunk.toString("utf8");
194
+ options.onOutput?.(text, stream);
195
+ for (const line of text.split(/\r?\n/u)) {
196
+ if (line.trim() === "") continue;
197
+ lastLines.push(line);
198
+ if (lastLines.length > KEPT_LINES) lastLines.shift();
199
+ }
200
+ };
201
+ child.stdout?.on("data", forward("stdout"));
202
+ child.stderr?.on("data", forward("stderr"));
203
+ const withLastLines = (message) => [message, ...lastLines].join("\n");
204
+ let cleaned;
205
+ const cleanUp = () => cleaned ??= removeAppiumHome(home).catch(() => {}).then(() => {
206
+ process.off("exit", killOnExit);
207
+ });
208
+ let wake = () => {};
209
+ let spawnError;
210
+ child.once("error", (error) => {
211
+ spawnError = error;
212
+ wake();
213
+ });
214
+ let closedBy;
215
+ const exited = new Promise((settle) => {
216
+ child.once("close", (code, signal) => {
217
+ closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`;
218
+ wake();
219
+ settle();
220
+ });
221
+ }).then(cleanUp);
222
+ const ended = () => spawnError !== void 0 || closedBy !== void 0;
223
+ const pause = () => new Promise((settle) => {
224
+ if (ended()) return settle();
225
+ const timer = setTimeout(settle, POLL_INTERVAL_MS);
226
+ wake = () => {
227
+ clearTimeout(timer);
228
+ settle();
229
+ };
230
+ });
231
+ let stopping;
232
+ const stop = () => {
233
+ stopping ??= (async () => {
234
+ if (closedBy === void 0) {
235
+ child.ref();
236
+ child.stdout?.ref();
237
+ child.stderr?.ref();
238
+ if (child.pid !== void 0) await killTree(child.pid);
239
+ }
240
+ await exited;
241
+ })();
242
+ return stopping;
243
+ };
244
+ const url = `http://${LOOPBACK}:${port}`;
245
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
246
+ while (!ended() && Date.now() < deadline) {
247
+ if (await probe(url) && !ended()) return {
248
+ url,
249
+ exited,
250
+ stop
251
+ };
252
+ await pause();
253
+ }
254
+ if (spawnError !== void 0) {
255
+ await cleanUp();
256
+ throw spawnError;
257
+ }
258
+ if (closedBy !== void 0) {
259
+ await exited;
260
+ throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`));
261
+ }
262
+ await stop();
263
+ throw new Error(withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`));
264
+ }
265
+ exports.startAppiumServer = startAppiumServer;
266
+
267
+ //# sourceMappingURL=index.cjs.map
268
+ //# debugId=926327cf-3053-5645-bf57-b1fd4889d76c
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/appium-home.ts","../src/bundled-appium.ts","../src/kill-tree.ts","../src/start-appium-server.ts"],"sourcesContent":["import { mkdir, mkdtemp, realpath, rm, symlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { basename, join } from 'node:path'\n\n// A fresh folder has no extension manifest yet, so Appium scans its node_modules and finds the\n// linked drivers at every start. A junction needs no administrator rights on Windows; elsewhere\n// it is an ordinary symlink.\nexport async function prepareAppiumHome(driverFolders: readonly string[]): Promise<string> {\n const home = await realpath(await mkdtemp(join(tmpdir(), 'agent-qa-appium-')))\n try {\n await mkdir(join(home, 'node_modules'))\n for (const folder of driverFolders) {\n await symlink(folder, join(home, 'node_modules', basename(folder)), 'junction')\n }\n } catch (error) {\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n return home\n}\n\n// Removing a link never descends into the folder it points at. The retries cover a file that is\n// still held open for a moment.\nexport async function removeAppiumHome(home: string): Promise<void> {\n await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })\n}\n","import { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\n\nexport interface BundledAppium {\n readonly serverEntry: string\n readonly driverFolders: readonly string[]\n}\n\nconst require = createRequire(typeof __filename === 'string' ? __filename : import.meta.url)\n\n// Resolved from this package's own file: under pnpm only the package that declares a dependency\n// can resolve it, and a project's or the machine's own Appium is never reached.\nexport function resolveBundledAppium(): BundledAppium {\n return {\n serverEntry: require.resolve('appium/build/lib/main.js'),\n driverFolders: [\n dirname(require.resolve('appium-uiautomator2-driver/package.json')),\n dirname(require.resolve('appium-xcuitest-driver/package.json')),\n ],\n }\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport treeKill from 'tree-kill'\n\n// tree-kill finds a Linux process's children by running ps, which slim Linux images do not have,\n// and a missing ps throws in the host process. Linux keeps each process's parent in /proc.\nexport function killTree(\n pid: number,\n options: { readonly platform?: NodeJS.Platform; readonly proc?: string } = {},\n): Promise<void> {\n if ((options.platform ?? process.platform) === 'linux') {\n // When /proc cannot be listed, the process itself is still signalled.\n let members = [pid]\n try {\n members = linuxTree(pid, options.proc ?? '/proc')\n } catch {}\n for (const member of members) {\n try {\n process.kill(member, 'SIGKILL')\n } catch {}\n }\n return Promise.resolve()\n }\n // tree-kill lists the tree before it signals any of it, so the tree is killed only once its\n // callback runs. What the callback reports is not passed on.\n return new Promise((settle) => {\n treeKill(pid, 'SIGKILL', () => settle())\n })\n}\n\nfunction linuxTree(root: number, proc: string): number[] {\n const children = new Map<number, number[]>()\n for (const entry of readdirSync(proc)) {\n if (!/^\\d+$/u.test(entry)) continue\n let stat: string\n try {\n stat = readFileSync(join(proc, entry, 'stat'), 'utf8')\n } catch {\n continue\n }\n // The command name sits in parentheses and may hold spaces and parentheses itself; the\n // parent's id is the second field after the last one.\n const parent = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])\n children.set(parent, [...(children.get(parent) ?? []), Number(entry)])\n }\n const tree = [root]\n for (const member of tree) tree.push(...(children.get(member) ?? []))\n return tree\n}\n","import { spawn as nodeSpawn, type ChildProcessByStdio, type SpawnOptions } from 'node:child_process'\nimport { rmSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { createServer, type Socket } from 'node:net'\n\nimport { prepareAppiumHome, removeAppiumHome } from './appium-home'\nimport { resolveBundledAppium } from './bundled-appium'\nimport { killTree } from './kill-tree'\n\nconst LOOPBACK = '127.0.0.1'\nconst STARTUP_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst KEPT_LINES = 10\n\nexport interface AppiumServer {\n // http://127.0.0.1 and the port the server listens on\n readonly url: string\n // Settles once the server process has ended, by stop() or on its own, and the prepared home\n // is removed. Never rejects.\n readonly exited: Promise<void>\n // Settles once the process tree is killed, the child has closed and the prepared home is\n // removed. Never rejects. Safe to call twice.\n stop(): Promise<void>\n}\n\ninterface ChildOutput {\n on(event: 'data', listener: (chunk: Buffer) => void): unknown\n ref(): unknown\n unref(): unknown\n}\n\ninterface AppiumChild {\n readonly pid?: number | undefined\n readonly stdout: ChildOutput | null\n readonly stderr: ChildOutput | null\n once(event: 'error', listener: (error: Error) => void): unknown\n once(\n event: 'close',\n listener: (code: number | null, signal: NodeJS.Signals | null) => void,\n ): unknown\n kill(signal: NodeJS.Signals): unknown\n ref(): unknown\n unref(): unknown\n}\n\nexport type AppiumSpawn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => AppiumChild\n\nexport interface StartAppiumServerOptions {\n // Every chunk the server prints, for its whole life.\n readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void\n // The two seams a test passes fakes through; the defaults are the real spawn and the real probe.\n readonly spawn?: AppiumSpawn\n readonly probe?: (url: string) => Promise<boolean>\n}\n\n// A child's piped output streams are sockets, which Node types only as Readable.\nconst spawnNode: AppiumSpawn = (command, args, options) =>\n nodeSpawn(command, [...args], options) as ChildProcessByStdio<null, Socket, Socket>\n\nfunction isAppiumReady(url: string): Promise<boolean> {\n return new Promise<boolean>((settle) => {\n const req = request(`${url}/status`, { method: 'GET', timeout: 3000 }, (res) => {\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => {\n try {\n const parsed = JSON.parse(data)\n settle(parsed?.value?.ready === true)\n } catch {\n settle(false)\n }\n })\n })\n req.on('error', () => settle(false))\n req.on('timeout', () => {\n req.destroy()\n settle(false)\n })\n req.end()\n })\n}\n\n// The port the system hands out on loopback, released at once.\nfunction findFreePort(): Promise<number> {\n return new Promise((settle, fail) => {\n const server = createServer()\n server.unref()\n server.once('error', fail)\n server.listen({ port: 0, host: LOOPBACK }, () => {\n const address = server.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n server.close(() => settle(port))\n })\n })\n}\n\nexport async function startAppiumServer(\n options: StartAppiumServerOptions = {},\n): Promise<AppiumServer> {\n const spawn = options.spawn ?? spawnNode\n const probe = options.probe ?? isAppiumReady\n const { serverEntry, driverFolders } = resolveBundledAppium()\n const port = await findFreePort()\n const home = await prepareAppiumHome(driverFolders)\n let child: AppiumChild\n try {\n child = spawn(\n process.execPath,\n [\n serverEntry,\n '--address',\n LOOPBACK,\n '--port',\n String(port),\n '--relaxed-security',\n '--deny-insecure',\n '*:cors',\n '--log-no-colors',\n ],\n { env: { ...process.env, APPIUM_HOME: home }, stdio: ['ignore', 'pipe', 'pipe'] },\n )\n } catch (error) {\n // No child exists yet, so nothing else would remove the home.\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n\n // An exiting process cannot wait for tree-kill to list the tree, so only the server itself is\n // signalled here.\n const killOnExit = (): void => {\n try {\n child.kill('SIGKILL')\n } catch {}\n try {\n rmSync(home, { recursive: true, force: true })\n } catch {}\n }\n process.once('exit', killOnExit)\n // A command that succeeded ends only once nothing holds the event loop, and a child with piped\n // output would hold it for ever, so the exit listener would never run.\n child.unref()\n child.stdout?.unref()\n child.stderr?.unref()\n\n const lastLines: string[] = []\n const forward = (stream: 'stdout' | 'stderr') => (chunk: Buffer) => {\n const text = chunk.toString('utf8')\n options.onOutput?.(text, stream)\n for (const line of text.split(/\\r?\\n/u)) {\n if (line.trim() === '') continue\n lastLines.push(line)\n if (lastLines.length > KEPT_LINES) lastLines.shift()\n }\n }\n child.stdout?.on('data', forward('stdout'))\n child.stderr?.on('data', forward('stderr'))\n const withLastLines = (message: string): string => [message, ...lastLines].join('\\n')\n\n let cleaned: Promise<void> | undefined\n const cleanUp = (): Promise<void> =>\n (cleaned ??= removeAppiumHome(home)\n .catch(() => {})\n .then(() => {\n process.off('exit', killOnExit)\n }))\n\n let wake = (): void => {}\n let spawnError: Error | undefined\n child.once('error', (error) => {\n spawnError = error\n wake()\n })\n let closedBy: string | undefined\n const exited = new Promise<void>((settle) => {\n child.once('close', (code, signal) => {\n closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`\n wake()\n settle()\n })\n }).then(cleanUp)\n const ended = (): boolean => spawnError !== undefined || closedBy !== undefined\n const pause = (): Promise<void> =>\n new Promise((settle) => {\n if (ended()) return settle()\n const timer = setTimeout(settle, POLL_INTERVAL_MS)\n wake = () => {\n clearTimeout(timer)\n settle()\n }\n })\n\n let stopping: Promise<void> | undefined\n const stop = (): Promise<void> => {\n stopping ??= (async () => {\n if (closedBy === undefined) {\n // A process that is only waiting for this stop stays alive until the child has closed.\n child.ref()\n child.stdout?.ref()\n child.stderr?.ref()\n if (child.pid !== undefined) await killTree(child.pid)\n }\n await exited\n })()\n return stopping\n }\n\n const url = `http://${LOOPBACK}:${port}`\n const deadline = Date.now() + STARTUP_TIMEOUT_MS\n while (!ended() && Date.now() < deadline) {\n // Once the child has ended, whatever answers on its port is another process.\n if ((await probe(url)) && !ended()) return { url, exited, stop }\n await pause()\n }\n\n if (spawnError !== undefined) {\n await cleanUp()\n throw spawnError\n }\n if (closedBy !== undefined) {\n await exited\n throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`))\n }\n await stop()\n throw new Error(\n withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`),\n )\n}\n"],"names":["realpath","mkdtemp","join","tmpdir","mkdir","symlink","basename","rm","require","createRequire","dirname","readdirSync","readFileSync","join","nodeSpawn","request","createServer"],"mappings":"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","debugId":"926327cf-3053-5645-bf57-b1fd4889d76c"}
@@ -0,0 +1,29 @@
1
+ import { SpawnOptions } from "node:child_process";
2
+ interface AppiumServer {
3
+ readonly url: string;
4
+ readonly exited: Promise<void>;
5
+ stop(): Promise<void>;
6
+ }
7
+ interface ChildOutput {
8
+ on(event: 'data', listener: (chunk: Buffer) => void): unknown;
9
+ ref(): unknown;
10
+ unref(): unknown;
11
+ }
12
+ interface AppiumChild {
13
+ readonly pid?: number | undefined;
14
+ readonly stdout: ChildOutput | null;
15
+ readonly stderr: ChildOutput | null;
16
+ once(event: 'error', listener: (error: Error) => void): unknown;
17
+ once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
18
+ kill(signal: NodeJS.Signals): unknown;
19
+ ref(): unknown;
20
+ unref(): unknown;
21
+ }
22
+ type AppiumSpawn = (command: string, args: readonly string[], options: SpawnOptions) => AppiumChild;
23
+ interface StartAppiumServerOptions {
24
+ readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void;
25
+ readonly spawn?: AppiumSpawn;
26
+ readonly probe?: (url: string) => Promise<boolean>;
27
+ }
28
+ export declare function startAppiumServer(options?: StartAppiumServerOptions): Promise<AppiumServer>;
29
+ export type { AppiumServer, StartAppiumServerOptions };
@@ -0,0 +1,29 @@
1
+ import { SpawnOptions } from "node:child_process";
2
+ interface AppiumServer {
3
+ readonly url: string;
4
+ readonly exited: Promise<void>;
5
+ stop(): Promise<void>;
6
+ }
7
+ interface ChildOutput {
8
+ on(event: 'data', listener: (chunk: Buffer) => void): unknown;
9
+ ref(): unknown;
10
+ unref(): unknown;
11
+ }
12
+ interface AppiumChild {
13
+ readonly pid?: number | undefined;
14
+ readonly stdout: ChildOutput | null;
15
+ readonly stderr: ChildOutput | null;
16
+ once(event: 'error', listener: (error: Error) => void): unknown;
17
+ once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
18
+ kill(signal: NodeJS.Signals): unknown;
19
+ ref(): unknown;
20
+ unref(): unknown;
21
+ }
22
+ type AppiumSpawn = (command: string, args: readonly string[], options: SpawnOptions) => AppiumChild;
23
+ interface StartAppiumServerOptions {
24
+ readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void;
25
+ readonly spawn?: AppiumSpawn;
26
+ readonly probe?: (url: string) => Promise<boolean>;
27
+ }
28
+ export declare function startAppiumServer(options?: StartAppiumServerOptions): Promise<AppiumServer>;
29
+ export type { AppiumServer, StartAppiumServerOptions };
package/dist/index.js ADDED
@@ -0,0 +1,245 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c772727d-14c3-532a-a31d-8c4b1188089b")}catch(e){}}();
3
+ import { createRequire } from "node:module";
4
+ import { spawn } from "node:child_process";
5
+ import { readFileSync, readdirSync, rmSync } from "node:fs";
6
+ import { request } from "node:http";
7
+ import { createServer } from "node:net";
8
+ import { mkdir, mkdtemp, realpath, rm, symlink } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { basename, dirname, join } from "node:path";
11
+ import treeKill from "tree-kill";
12
+ // src/appium-home.ts
13
+ async function prepareAppiumHome(driverFolders) {
14
+ const home = await realpath(await mkdtemp(join(tmpdir(), "agent-qa-appium-")));
15
+ try {
16
+ await mkdir(join(home, "node_modules"));
17
+ for (const folder of driverFolders) await symlink(folder, join(home, "node_modules", basename(folder)), "junction");
18
+ } catch (error) {
19
+ await removeAppiumHome(home).catch(() => {});
20
+ throw error;
21
+ }
22
+ return home;
23
+ }
24
+ async function removeAppiumHome(home) {
25
+ await rm(home, {
26
+ recursive: true,
27
+ force: true,
28
+ maxRetries: 3,
29
+ retryDelay: 100
30
+ });
31
+ }
32
+ // src/bundled-appium.ts
33
+ const require = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
34
+ function resolveBundledAppium() {
35
+ return {
36
+ serverEntry: require.resolve("appium/build/lib/main.js"),
37
+ driverFolders: [dirname(require.resolve("appium-uiautomator2-driver/package.json")), dirname(require.resolve("appium-xcuitest-driver/package.json"))]
38
+ };
39
+ }
40
+ // src/kill-tree.ts
41
+ function killTree(pid, options = {}) {
42
+ if ((options.platform ?? process.platform) === "linux") {
43
+ let members = [pid];
44
+ try {
45
+ members = linuxTree(pid, options.proc ?? "/proc");
46
+ } catch {}
47
+ for (const member of members) try {
48
+ process.kill(member, "SIGKILL");
49
+ } catch {}
50
+ return Promise.resolve();
51
+ }
52
+ return new Promise((settle) => {
53
+ treeKill(pid, "SIGKILL", () => settle());
54
+ });
55
+ }
56
+ function linuxTree(root, proc) {
57
+ const children = /* @__PURE__ */ new Map();
58
+ for (const entry of readdirSync(proc)) {
59
+ if (!/^\d+$/u.test(entry)) continue;
60
+ let stat;
61
+ try {
62
+ stat = readFileSync(join(proc, entry, "stat"), "utf8");
63
+ } catch {
64
+ continue;
65
+ }
66
+ const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
67
+ children.set(parent, [...children.get(parent) ?? [], Number(entry)]);
68
+ }
69
+ const tree = [root];
70
+ for (const member of tree) tree.push(...children.get(member) ?? []);
71
+ return tree;
72
+ }
73
+ // src/start-appium-server.ts
74
+ const LOOPBACK = "127.0.0.1";
75
+ const STARTUP_TIMEOUT_MS = 3e4;
76
+ const POLL_INTERVAL_MS = 500;
77
+ const KEPT_LINES = 10;
78
+ const spawnNode = (command, args, options) => spawn(command, [...args], options);
79
+ function isAppiumReady(url) {
80
+ return new Promise((settle) => {
81
+ const req = request(`${url}/status`, {
82
+ method: "GET",
83
+ timeout: 3e3
84
+ }, (res) => {
85
+ let data = "";
86
+ res.on("data", (chunk) => {
87
+ data += chunk.toString();
88
+ });
89
+ res.on("end", () => {
90
+ try {
91
+ settle(JSON.parse(data)?.value?.ready === true);
92
+ } catch {
93
+ settle(false);
94
+ }
95
+ });
96
+ });
97
+ req.on("error", () => settle(false));
98
+ req.on("timeout", () => {
99
+ req.destroy();
100
+ settle(false);
101
+ });
102
+ req.end();
103
+ });
104
+ }
105
+ function findFreePort() {
106
+ return new Promise((settle, fail) => {
107
+ const server = createServer();
108
+ server.unref();
109
+ server.once("error", fail);
110
+ server.listen({
111
+ port: 0,
112
+ host: LOOPBACK
113
+ }, () => {
114
+ const address = server.address();
115
+ const port = typeof address === "object" && address !== null ? address.port : 0;
116
+ server.close(() => settle(port));
117
+ });
118
+ });
119
+ }
120
+ async function startAppiumServer(options = {}) {
121
+ const spawn = options.spawn ?? spawnNode;
122
+ const probe = options.probe ?? isAppiumReady;
123
+ const { serverEntry, driverFolders } = resolveBundledAppium();
124
+ const port = await findFreePort();
125
+ const home = await prepareAppiumHome(driverFolders);
126
+ let child;
127
+ try {
128
+ child = spawn(process.execPath, [
129
+ serverEntry,
130
+ "--address",
131
+ LOOPBACK,
132
+ "--port",
133
+ String(port),
134
+ "--relaxed-security",
135
+ "--deny-insecure",
136
+ "*:cors",
137
+ "--log-no-colors"
138
+ ], {
139
+ env: {
140
+ ...process.env,
141
+ APPIUM_HOME: home
142
+ },
143
+ stdio: [
144
+ "ignore",
145
+ "pipe",
146
+ "pipe"
147
+ ]
148
+ });
149
+ } catch (error) {
150
+ await removeAppiumHome(home).catch(() => {});
151
+ throw error;
152
+ }
153
+ const killOnExit = () => {
154
+ try {
155
+ child.kill("SIGKILL");
156
+ } catch {}
157
+ try {
158
+ rmSync(home, {
159
+ recursive: true,
160
+ force: true
161
+ });
162
+ } catch {}
163
+ };
164
+ process.once("exit", killOnExit);
165
+ child.unref();
166
+ child.stdout?.unref();
167
+ child.stderr?.unref();
168
+ const lastLines = [];
169
+ const forward = (stream) => (chunk) => {
170
+ const text = chunk.toString("utf8");
171
+ options.onOutput?.(text, stream);
172
+ for (const line of text.split(/\r?\n/u)) {
173
+ if (line.trim() === "") continue;
174
+ lastLines.push(line);
175
+ if (lastLines.length > KEPT_LINES) lastLines.shift();
176
+ }
177
+ };
178
+ child.stdout?.on("data", forward("stdout"));
179
+ child.stderr?.on("data", forward("stderr"));
180
+ const withLastLines = (message) => [message, ...lastLines].join("\n");
181
+ let cleaned;
182
+ const cleanUp = () => cleaned ??= removeAppiumHome(home).catch(() => {}).then(() => {
183
+ process.off("exit", killOnExit);
184
+ });
185
+ let wake = () => {};
186
+ let spawnError;
187
+ child.once("error", (error) => {
188
+ spawnError = error;
189
+ wake();
190
+ });
191
+ let closedBy;
192
+ const exited = new Promise((settle) => {
193
+ child.once("close", (code, signal) => {
194
+ closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`;
195
+ wake();
196
+ settle();
197
+ });
198
+ }).then(cleanUp);
199
+ const ended = () => spawnError !== void 0 || closedBy !== void 0;
200
+ const pause = () => new Promise((settle) => {
201
+ if (ended()) return settle();
202
+ const timer = setTimeout(settle, POLL_INTERVAL_MS);
203
+ wake = () => {
204
+ clearTimeout(timer);
205
+ settle();
206
+ };
207
+ });
208
+ let stopping;
209
+ const stop = () => {
210
+ stopping ??= (async () => {
211
+ if (closedBy === void 0) {
212
+ child.ref();
213
+ child.stdout?.ref();
214
+ child.stderr?.ref();
215
+ if (child.pid !== void 0) await killTree(child.pid);
216
+ }
217
+ await exited;
218
+ })();
219
+ return stopping;
220
+ };
221
+ const url = `http://${LOOPBACK}:${port}`;
222
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
223
+ while (!ended() && Date.now() < deadline) {
224
+ if (await probe(url) && !ended()) return {
225
+ url,
226
+ exited,
227
+ stop
228
+ };
229
+ await pause();
230
+ }
231
+ if (spawnError !== void 0) {
232
+ await cleanUp();
233
+ throw spawnError;
234
+ }
235
+ if (closedBy !== void 0) {
236
+ await exited;
237
+ throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`));
238
+ }
239
+ await stop();
240
+ throw new Error(withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`));
241
+ }
242
+ export { startAppiumServer };
243
+
244
+ //# sourceMappingURL=index.js.map
245
+ //# debugId=c772727d-14c3-532a-a31d-8c4b1188089b
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/appium-home.ts","../src/bundled-appium.ts","../src/kill-tree.ts","../src/start-appium-server.ts"],"sourcesContent":["import { mkdir, mkdtemp, realpath, rm, symlink } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { basename, join } from 'node:path'\n\n// A fresh folder has no extension manifest yet, so Appium scans its node_modules and finds the\n// linked drivers at every start. A junction needs no administrator rights on Windows; elsewhere\n// it is an ordinary symlink.\nexport async function prepareAppiumHome(driverFolders: readonly string[]): Promise<string> {\n const home = await realpath(await mkdtemp(join(tmpdir(), 'agent-qa-appium-')))\n try {\n await mkdir(join(home, 'node_modules'))\n for (const folder of driverFolders) {\n await symlink(folder, join(home, 'node_modules', basename(folder)), 'junction')\n }\n } catch (error) {\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n return home\n}\n\n// Removing a link never descends into the folder it points at. The retries cover a file that is\n// still held open for a moment.\nexport async function removeAppiumHome(home: string): Promise<void> {\n await rm(home, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })\n}\n","import { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\n\nexport interface BundledAppium {\n readonly serverEntry: string\n readonly driverFolders: readonly string[]\n}\n\nconst require = createRequire(typeof __filename === 'string' ? __filename : import.meta.url)\n\n// Resolved from this package's own file: under pnpm only the package that declares a dependency\n// can resolve it, and a project's or the machine's own Appium is never reached.\nexport function resolveBundledAppium(): BundledAppium {\n return {\n serverEntry: require.resolve('appium/build/lib/main.js'),\n driverFolders: [\n dirname(require.resolve('appium-uiautomator2-driver/package.json')),\n dirname(require.resolve('appium-xcuitest-driver/package.json')),\n ],\n }\n}\n","import { readdirSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport treeKill from 'tree-kill'\n\n// tree-kill finds a Linux process's children by running ps, which slim Linux images do not have,\n// and a missing ps throws in the host process. Linux keeps each process's parent in /proc.\nexport function killTree(\n pid: number,\n options: { readonly platform?: NodeJS.Platform; readonly proc?: string } = {},\n): Promise<void> {\n if ((options.platform ?? process.platform) === 'linux') {\n // When /proc cannot be listed, the process itself is still signalled.\n let members = [pid]\n try {\n members = linuxTree(pid, options.proc ?? '/proc')\n } catch {}\n for (const member of members) {\n try {\n process.kill(member, 'SIGKILL')\n } catch {}\n }\n return Promise.resolve()\n }\n // tree-kill lists the tree before it signals any of it, so the tree is killed only once its\n // callback runs. What the callback reports is not passed on.\n return new Promise((settle) => {\n treeKill(pid, 'SIGKILL', () => settle())\n })\n}\n\nfunction linuxTree(root: number, proc: string): number[] {\n const children = new Map<number, number[]>()\n for (const entry of readdirSync(proc)) {\n if (!/^\\d+$/u.test(entry)) continue\n let stat: string\n try {\n stat = readFileSync(join(proc, entry, 'stat'), 'utf8')\n } catch {\n continue\n }\n // The command name sits in parentheses and may hold spaces and parentheses itself; the\n // parent's id is the second field after the last one.\n const parent = Number(stat.slice(stat.lastIndexOf(')') + 2).split(' ')[1])\n children.set(parent, [...(children.get(parent) ?? []), Number(entry)])\n }\n const tree = [root]\n for (const member of tree) tree.push(...(children.get(member) ?? []))\n return tree\n}\n","import { spawn as nodeSpawn, type ChildProcessByStdio, type SpawnOptions } from 'node:child_process'\nimport { rmSync } from 'node:fs'\nimport { request } from 'node:http'\nimport { createServer, type Socket } from 'node:net'\n\nimport { prepareAppiumHome, removeAppiumHome } from './appium-home'\nimport { resolveBundledAppium } from './bundled-appium'\nimport { killTree } from './kill-tree'\n\nconst LOOPBACK = '127.0.0.1'\nconst STARTUP_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst KEPT_LINES = 10\n\nexport interface AppiumServer {\n // http://127.0.0.1 and the port the server listens on\n readonly url: string\n // Settles once the server process has ended, by stop() or on its own, and the prepared home\n // is removed. Never rejects.\n readonly exited: Promise<void>\n // Settles once the process tree is killed, the child has closed and the prepared home is\n // removed. Never rejects. Safe to call twice.\n stop(): Promise<void>\n}\n\ninterface ChildOutput {\n on(event: 'data', listener: (chunk: Buffer) => void): unknown\n ref(): unknown\n unref(): unknown\n}\n\ninterface AppiumChild {\n readonly pid?: number | undefined\n readonly stdout: ChildOutput | null\n readonly stderr: ChildOutput | null\n once(event: 'error', listener: (error: Error) => void): unknown\n once(\n event: 'close',\n listener: (code: number | null, signal: NodeJS.Signals | null) => void,\n ): unknown\n kill(signal: NodeJS.Signals): unknown\n ref(): unknown\n unref(): unknown\n}\n\nexport type AppiumSpawn = (\n command: string,\n args: readonly string[],\n options: SpawnOptions,\n) => AppiumChild\n\nexport interface StartAppiumServerOptions {\n // Every chunk the server prints, for its whole life.\n readonly onOutput?: (text: string, stream: 'stdout' | 'stderr') => void\n // The two seams a test passes fakes through; the defaults are the real spawn and the real probe.\n readonly spawn?: AppiumSpawn\n readonly probe?: (url: string) => Promise<boolean>\n}\n\n// A child's piped output streams are sockets, which Node types only as Readable.\nconst spawnNode: AppiumSpawn = (command, args, options) =>\n nodeSpawn(command, [...args], options) as ChildProcessByStdio<null, Socket, Socket>\n\nfunction isAppiumReady(url: string): Promise<boolean> {\n return new Promise<boolean>((settle) => {\n const req = request(`${url}/status`, { method: 'GET', timeout: 3000 }, (res) => {\n let data = ''\n res.on('data', (chunk: Buffer) => {\n data += chunk.toString()\n })\n res.on('end', () => {\n try {\n const parsed = JSON.parse(data)\n settle(parsed?.value?.ready === true)\n } catch {\n settle(false)\n }\n })\n })\n req.on('error', () => settle(false))\n req.on('timeout', () => {\n req.destroy()\n settle(false)\n })\n req.end()\n })\n}\n\n// The port the system hands out on loopback, released at once.\nfunction findFreePort(): Promise<number> {\n return new Promise((settle, fail) => {\n const server = createServer()\n server.unref()\n server.once('error', fail)\n server.listen({ port: 0, host: LOOPBACK }, () => {\n const address = server.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n server.close(() => settle(port))\n })\n })\n}\n\nexport async function startAppiumServer(\n options: StartAppiumServerOptions = {},\n): Promise<AppiumServer> {\n const spawn = options.spawn ?? spawnNode\n const probe = options.probe ?? isAppiumReady\n const { serverEntry, driverFolders } = resolveBundledAppium()\n const port = await findFreePort()\n const home = await prepareAppiumHome(driverFolders)\n let child: AppiumChild\n try {\n child = spawn(\n process.execPath,\n [\n serverEntry,\n '--address',\n LOOPBACK,\n '--port',\n String(port),\n '--relaxed-security',\n '--deny-insecure',\n '*:cors',\n '--log-no-colors',\n ],\n { env: { ...process.env, APPIUM_HOME: home }, stdio: ['ignore', 'pipe', 'pipe'] },\n )\n } catch (error) {\n // No child exists yet, so nothing else would remove the home.\n await removeAppiumHome(home).catch(() => {})\n throw error\n }\n\n // An exiting process cannot wait for tree-kill to list the tree, so only the server itself is\n // signalled here.\n const killOnExit = (): void => {\n try {\n child.kill('SIGKILL')\n } catch {}\n try {\n rmSync(home, { recursive: true, force: true })\n } catch {}\n }\n process.once('exit', killOnExit)\n // A command that succeeded ends only once nothing holds the event loop, and a child with piped\n // output would hold it for ever, so the exit listener would never run.\n child.unref()\n child.stdout?.unref()\n child.stderr?.unref()\n\n const lastLines: string[] = []\n const forward = (stream: 'stdout' | 'stderr') => (chunk: Buffer) => {\n const text = chunk.toString('utf8')\n options.onOutput?.(text, stream)\n for (const line of text.split(/\\r?\\n/u)) {\n if (line.trim() === '') continue\n lastLines.push(line)\n if (lastLines.length > KEPT_LINES) lastLines.shift()\n }\n }\n child.stdout?.on('data', forward('stdout'))\n child.stderr?.on('data', forward('stderr'))\n const withLastLines = (message: string): string => [message, ...lastLines].join('\\n')\n\n let cleaned: Promise<void> | undefined\n const cleanUp = (): Promise<void> =>\n (cleaned ??= removeAppiumHome(home)\n .catch(() => {})\n .then(() => {\n process.off('exit', killOnExit)\n }))\n\n let wake = (): void => {}\n let spawnError: Error | undefined\n child.once('error', (error) => {\n spawnError = error\n wake()\n })\n let closedBy: string | undefined\n const exited = new Promise<void>((settle) => {\n child.once('close', (code, signal) => {\n closedBy = signal === null ? `exit code ${code}` : `signal ${signal}`\n wake()\n settle()\n })\n }).then(cleanUp)\n const ended = (): boolean => spawnError !== undefined || closedBy !== undefined\n const pause = (): Promise<void> =>\n new Promise((settle) => {\n if (ended()) return settle()\n const timer = setTimeout(settle, POLL_INTERVAL_MS)\n wake = () => {\n clearTimeout(timer)\n settle()\n }\n })\n\n let stopping: Promise<void> | undefined\n const stop = (): Promise<void> => {\n stopping ??= (async () => {\n if (closedBy === undefined) {\n // A process that is only waiting for this stop stays alive until the child has closed.\n child.ref()\n child.stdout?.ref()\n child.stderr?.ref()\n if (child.pid !== undefined) await killTree(child.pid)\n }\n await exited\n })()\n return stopping\n }\n\n const url = `http://${LOOPBACK}:${port}`\n const deadline = Date.now() + STARTUP_TIMEOUT_MS\n while (!ended() && Date.now() < deadline) {\n // Once the child has ended, whatever answers on its port is another process.\n if ((await probe(url)) && !ended()) return { url, exited, stop }\n await pause()\n }\n\n if (spawnError !== undefined) {\n await cleanUp()\n throw spawnError\n }\n if (closedBy !== undefined) {\n await exited\n throw new Error(withLastLines(`Appium exited before it was ready (${closedBy})`))\n }\n await stop()\n throw new Error(\n withLastLines(`Appium server did not become ready within ${STARTUP_TIMEOUT_MS}ms`),\n )\n}\n"],"names":["nodeSpawn"],"mappings":"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","debugId":"c772727d-14c3-532a-a31d-8c4b1188089b"}
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@vostride/agent-qa-appium-runtime",
3
- "version": "0.0.1",
4
- "main": "index.js",
5
- "files": []
3
+ "version": "0.3.7",
4
+ "private": false,
5
+ "description": "Carries Appium and the UiAutomator2 and XCUITest drivers at pinned versions and starts that Appium server for agent-qa.",
6
+ "keywords": [
7
+ "agent-qa",
8
+ "testing",
9
+ "appium",
10
+ "mobile"
11
+ ],
12
+ "license": "FSL-1.1-ALv2",
13
+ "homepage": "https://github.com/vostride/agent-qa#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/vostride/agent-qa/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/vostride/agent-qa.git",
20
+ "directory": "packages/appium-runtime"
21
+ },
22
+ "engines": {
23
+ "node": ">=24"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ },
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/index.d.cts",
38
+ "default": "./dist/index.cjs"
39
+ }
40
+ }
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "LICENSE.md",
45
+ "NOTICE.md"
46
+ ],
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "dev": "tsdown --watch",
50
+ "test": "vitest run",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist .turbo"
53
+ },
54
+ "dependencies": {
55
+ "appium": "4.0.0-beta.7",
56
+ "appium-uiautomator2-driver": "9.0.2",
57
+ "appium-xcuitest-driver": "13.0.8",
58
+ "tree-kill": "1.2.2"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "~24.19.2",
62
+ "@vostride/agent-qa-arch": "0.3.7",
63
+ "tsdown": "~0.23.4",
64
+ "typescript": "~7.0.2",
65
+ "vitest": "~5.0.5"
66
+ }
6
67
  }