@loadmill/droid-cua 2.43.0 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +123 -8
  2. package/build/index.js +46 -17
  3. package/build/src/cli/device-selector.js +13 -6
  4. package/build/src/cli/headless-execution-config.js +9 -0
  5. package/build/src/cli/headless-report.js +40 -11
  6. package/build/src/cli/headless-runner.js +28 -14
  7. package/build/src/cli/headless-target.js +24 -0
  8. package/build/src/cli/ios-device-command.js +121 -0
  9. package/build/src/cli/ios-device-list.js +32 -0
  10. package/build/src/cli/network-capture-device.js +21 -0
  11. package/build/src/commands/help.js +15 -3
  12. package/build/src/commands/stop.js +1 -0
  13. package/build/src/core/capture-sources.js +4 -0
  14. package/build/src/core/droid-cua-run-usage.js +9 -2
  15. package/build/src/core/execution-context.js +8 -0
  16. package/build/src/core/execution-engine.js +3 -0
  17. package/build/src/core/report/render-html-report.js +299 -94
  18. package/build/src/core/report/run-log-model.js +84 -1
  19. package/build/src/core/report/run-report-model.js +4 -1
  20. package/build/src/core/report/run-report-sections.js +10 -0
  21. package/build/src/core/runtime-config.js +11 -1
  22. package/build/src/core/step-watchdog.js +61 -0
  23. package/build/src/desktop-shared/ios-real-device.js +82 -0
  24. package/build/src/device/factory.js +4 -0
  25. package/build/src/device/ios/actions.js +48 -12
  26. package/build/src/device/ios/appium-server.js +7 -18
  27. package/build/src/device/ios/connection.js +71 -12
  28. package/build/src/device/ios/node-cli-env.js +55 -0
  29. package/build/src/device/ios/real-device/device-state.js +40 -0
  30. package/build/src/device/ios/real-device/devicectl.js +154 -0
  31. package/build/src/device/ios/real-device/readiness.js +439 -0
  32. package/build/src/device/ios/real-device/session.js +176 -0
  33. package/build/src/device/ios/real-device/signing.js +466 -0
  34. package/build/src/device/ios/real-device/wda-provisioning.js +229 -0
  35. package/build/src/modes/execution-mode.js +157 -3
  36. package/build/src/network/certificate-authority.js +21 -1
  37. package/build/src/network/certificate-install.js +38 -0
  38. package/build/src/network/local-proxy.js +21 -1
  39. package/build/src/network/run-network-capture.js +16 -5
  40. package/build/src/prompts/step-watchdog.js +22 -0
  41. package/build/src/utils/desktop-debug.js +2 -2
  42. package/package.json +1 -1
  43. package/skill/droid-cua/SKILL.md +2 -2
  44. package/skill/droid-cua/references/authoring-tests.md +1 -1
  45. package/skill/droid-cua/references/onboarding-and-troubleshooting.md +33 -2
  46. package/skill/droid-cua/references/running-and-healing.md +27 -2
@@ -1,3 +1,5 @@
1
+ import { accessSync, constants as fsConstants, readdirSync } from "fs";
2
+ import os from "os";
1
3
  import path from "path";
2
4
  export const COMMON_NODE_BIN_DIRS = ["/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin"];
3
5
  function prependPathEntries(entries, originalPath) {
@@ -26,3 +28,56 @@ export function makeNodeCliCommandEnv(command) {
26
28
  PATH: prependPathEntries(binDirs, process.env.PATH)
27
29
  };
28
30
  }
31
+ function parseNodeVersion(name) {
32
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(name);
33
+ return match ? match.slice(1).map(Number) : null;
34
+ }
35
+ function compareVersionsDescending(a, b) {
36
+ for (let i = 0; i < 3; i += 1) {
37
+ if (a[i] !== b[i])
38
+ return b[i] - a[i];
39
+ }
40
+ return 0;
41
+ }
42
+ function isExecutable(filePath) {
43
+ try {
44
+ accessSync(filePath, fsConstants.X_OK);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ export function listNvmNodeBinDirs(home = os.homedir()) {
52
+ if (!home)
53
+ return [];
54
+ const versionsDir = path.join(home, ".nvm", "versions", "node");
55
+ let names;
56
+ try {
57
+ names = readdirSync(versionsDir);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ return names
63
+ .map((name) => ({ name, version: parseNodeVersion(name) }))
64
+ .filter((entry) => entry.version)
65
+ .sort((a, b) => compareVersionsDescending(a.version, b.version))
66
+ .map((entry) => path.join(versionsDir, entry.name, "bin"));
67
+ }
68
+ export function resolveNodeCliCommand(command, { pathEnv = process.env.PATH, commonDirs = COMMON_NODE_BIN_DIRS, home = os.homedir() } = {}) {
69
+ const pathDirs = pathEnv
70
+ ?.split(path.delimiter)
71
+ .map((dir) => dir.trim())
72
+ .filter(Boolean) ?? [];
73
+ for (const dir of new Set([...pathDirs, ...commonDirs])) {
74
+ const candidate = path.join(dir, command);
75
+ if (isExecutable(candidate))
76
+ return candidate;
77
+ }
78
+ const nvmBinDir = listNvmNodeBinDirs(home).find((dir) => isExecutable(path.join(dir, "node")));
79
+ if (nvmBinDir && isExecutable(path.join(nvmBinDir, command))) {
80
+ return path.join(nvmBinDir, command);
81
+ }
82
+ return command;
83
+ }
@@ -0,0 +1,40 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const DEFAULT_DIR = path.join(os.homedir(), ".droid-cua", "ios-devices");
5
+ function statePath(dir) {
6
+ return path.join(dir, "state.json");
7
+ }
8
+ async function readAll(dir) {
9
+ try {
10
+ return JSON.parse(await readFile(statePath(dir), "utf8")) || {};
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ }
16
+ function isPlainObject(value) {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ export async function readDeviceState(udid, { dir = DEFAULT_DIR } = {}) {
20
+ const all = await readAll(dir);
21
+ return all[udid] ?? null;
22
+ }
23
+ export async function updateDeviceState(udid, patch, { dir = DEFAULT_DIR } = {}) {
24
+ const all = await readAll(dir);
25
+ const current = all[udid] ?? {};
26
+ const next = { ...current };
27
+ for (const [key, value] of Object.entries(patch)) {
28
+ next[key] = isPlainObject(value) && isPlainObject(current[key]) ? { ...current[key], ...value } : value;
29
+ }
30
+ all[udid] = next;
31
+ await mkdir(dir, { recursive: true });
32
+ const tempPath = `${statePath(dir)}.${process.pid}.${Date.now()}.tmp`;
33
+ await writeFile(tempPath, `${JSON.stringify(all, null, 2)}\n`, "utf8");
34
+ await rename(tempPath, statePath(dir));
35
+ return next;
36
+ }
37
+ export async function didIosVersionChange(udid, currentVersion, { dir = DEFAULT_DIR } = {}) {
38
+ const state = await readDeviceState(udid, { dir });
39
+ return Boolean(state?.lastIosVersion) && state.lastIosVersion !== currentVersion;
40
+ }
@@ -0,0 +1,154 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ const DEFAULT_TIMEOUT_MS = 30_000;
8
+ const INSTALL_TIMEOUT_MS = 180_000;
9
+ let runner = execFileAsync;
10
+ export class DevicectlError extends Error {
11
+ constructor({ kind, message, code = null, domain = null }) {
12
+ super(message);
13
+ this.name = "DevicectlError";
14
+ this.kind = kind;
15
+ this.code = code;
16
+ this.domain = domain;
17
+ }
18
+ }
19
+ export function __setDevicectlRunner(run = null) {
20
+ runner = run || execFileAsync;
21
+ }
22
+ function errorChain(error) {
23
+ const chain = [];
24
+ let current = error;
25
+ while (current && typeof current === "object" && chain.length < 10) {
26
+ chain.push(current);
27
+ const underlying = current.userInfo?.NSUnderlyingError;
28
+ current = underlying?.error ?? underlying;
29
+ }
30
+ return chain;
31
+ }
32
+ function describe(error) {
33
+ return error.userInfo?.NSLocalizedDescription?.string || "";
34
+ }
35
+ function classify(chain) {
36
+ const text = chain.map(describe).join("\n");
37
+ const hasCode = (domain, code) => chain.some((e) => e.domain === domain && e.code === code);
38
+ if (hasCode("com.apple.dt.CoreDeviceError", 10005) || /Developer Mode is disabled/i.test(text)) {
39
+ return "developer-mode-disabled";
40
+ }
41
+ if (/not been explicitly trusted/i.test(text))
42
+ return "untrusted-developer";
43
+ if (/is not installed/i.test(text))
44
+ return "app-not-installed";
45
+ if (/device (is|was) locked|could not be, unlocked/i.test(text))
46
+ return "device-locked";
47
+ if (/device was not found/i.test(text))
48
+ return "device-not-found";
49
+ if (/not paired|unpaired/i.test(text))
50
+ return "not-paired";
51
+ return "unknown";
52
+ }
53
+ export function devicectlErrorFromJson(error) {
54
+ const chain = errorChain(error);
55
+ const descriptions = [...new Set(chain.map(describe).filter(Boolean))];
56
+ return new DevicectlError({
57
+ kind: classify(chain),
58
+ message: descriptions.join(" ") || `devicectl failed (${error.domain} ${error.code})`,
59
+ code: error.code ?? null,
60
+ domain: error.domain ?? null,
61
+ });
62
+ }
63
+ async function readJson(file) {
64
+ try {
65
+ return JSON.parse(await readFile(file, "utf8"));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function runDevicectl(args, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
72
+ const dir = await mkdtemp(path.join(os.tmpdir(), "droid-cua-devicectl-"));
73
+ const jsonPath = path.join(dir, "output.json");
74
+ try {
75
+ let runError = null;
76
+ try {
77
+ await runner("xcrun", ["devicectl", ...args, "--json-output", jsonPath], { timeout: timeoutMs });
78
+ }
79
+ catch (err) {
80
+ runError = err;
81
+ }
82
+ if (runError?.killed || runError?.code === "ETIMEDOUT") {
83
+ throw new DevicectlError({
84
+ kind: "timeout",
85
+ message: `devicectl ${args.slice(0, 3).join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`,
86
+ });
87
+ }
88
+ const output = await readJson(jsonPath);
89
+ if (output?.info?.outcome === "failed" || output?.error) {
90
+ throw devicectlErrorFromJson(output.error || {});
91
+ }
92
+ if (!output || runError) {
93
+ const detail = runError?.stderr?.trim() || runError?.message || "no JSON output";
94
+ throw new DevicectlError({ kind: "unknown", message: `devicectl ${args.slice(0, 3).join(" ")} failed: ${detail}` });
95
+ }
96
+ return output.result ?? {};
97
+ }
98
+ finally {
99
+ await rm(dir, { recursive: true, force: true });
100
+ }
101
+ }
102
+ export async function listDevices() {
103
+ const result = await runDevicectl(["list", "devices"]);
104
+ return (result.devices || []).map((device) => ({
105
+ udid: device.hardwareProperties?.udid,
106
+ name: device.deviceProperties?.name,
107
+ model: device.hardwareProperties?.marketingName,
108
+ iosVersion: device.deviceProperties?.osVersionNumber,
109
+ pairingState: device.connectionProperties?.pairingState,
110
+ tunnelState: device.connectionProperties?.tunnelState,
111
+ transport: device.connectionProperties?.transportType,
112
+ developerModeStatus: device.deviceProperties?.developerModeStatus,
113
+ ddiServicesAvailable: device.deviceProperties?.ddiServicesAvailable === true,
114
+ identifier: device.identifier,
115
+ }));
116
+ }
117
+ export async function getLockState(udid) {
118
+ const result = await runDevicectl(["device", "info", "lockState", "--device", udid]);
119
+ return {
120
+ passcodeRequired: result.passcodeRequired === true,
121
+ unlockedSinceBoot: result.unlockedSinceBoot === true,
122
+ };
123
+ }
124
+ export async function getDdiServices(udid) {
125
+ const result = await runDevicectl(["device", "info", "ddiServices", "--device", udid]);
126
+ return result.ddiMetadata ?? null;
127
+ }
128
+ export async function listApps(udid, { includeAll = false } = {}) {
129
+ const args = ["device", "info", "apps", "--device", udid];
130
+ if (includeAll)
131
+ args.push("--include-all-apps");
132
+ const result = await runDevicectl(args);
133
+ return (result.apps || []).map((app) => ({
134
+ bundleIdentifier: app.bundleIdentifier,
135
+ name: app.name,
136
+ version: app.version,
137
+ }));
138
+ }
139
+ export async function listProcesses(udid) {
140
+ const result = await runDevicectl(["device", "info", "processes", "--device", udid]);
141
+ return (result.runningProcesses || []).map((proc) => ({
142
+ pid: proc.processIdentifier,
143
+ executable: proc.executable,
144
+ }));
145
+ }
146
+ export async function installApp(udid, appPath) {
147
+ const result = await runDevicectl(["device", "install", "app", "--device", udid, appPath], {
148
+ timeoutMs: INSTALL_TIMEOUT_MS,
149
+ });
150
+ return (result.installedApplications || []).map((app) => app.bundleID ?? app.bundleIdentifier);
151
+ }
152
+ export async function terminateProcess(udid, pid) {
153
+ await runDevicectl(["device", "process", "terminate", "--device", udid, "--pid", String(pid)]);
154
+ }