agent-device 0.4.2 → 0.5.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.
- package/README.md +2 -9
- package/dist/src/797.js +1 -1
- package/dist/src/bin.js +5 -5
- package/dist/src/daemon.js +16 -20
- package/package.json +7 -9
- package/skills/agent-device/SKILL.md +3 -6
- package/skills/agent-device/references/permissions.md +3 -15
- package/skills/agent-device/references/snapshot-refs.md +1 -4
- package/dist/bin/axsnapshot +0 -0
- package/ios-runner/AXSnapshot/Package.swift +0 -18
- package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
- package/src/__tests__/cli-close.test.ts +0 -155
- package/src/__tests__/cli-help.test.ts +0 -102
- package/src/bin.ts +0 -3
- package/src/cli.ts +0 -305
- package/src/core/__tests__/capabilities.test.ts +0 -75
- package/src/core/__tests__/dispatch-open.test.ts +0 -25
- package/src/core/__tests__/open-target.test.ts +0 -55
- package/src/core/capabilities.ts +0 -57
- package/src/core/dispatch.ts +0 -382
- package/src/core/open-target.ts +0 -27
- package/src/daemon/__tests__/device-ready.test.ts +0 -52
- package/src/daemon/__tests__/is-predicates.test.ts +0 -68
- package/src/daemon/__tests__/selectors.test.ts +0 -261
- package/src/daemon/__tests__/session-routing.test.ts +0 -108
- package/src/daemon/__tests__/session-selector.test.ts +0 -64
- package/src/daemon/__tests__/session-store.test.ts +0 -142
- package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
- package/src/daemon/action-utils.ts +0 -29
- package/src/daemon/context.ts +0 -48
- package/src/daemon/device-ready.ts +0 -155
- package/src/daemon/handlers/__tests__/find.test.ts +0 -99
- package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
- package/src/daemon/handlers/__tests__/session.test.ts +0 -820
- package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
- package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
- package/src/daemon/handlers/find.ts +0 -324
- package/src/daemon/handlers/interaction.ts +0 -550
- package/src/daemon/handlers/parse-utils.ts +0 -8
- package/src/daemon/handlers/record-trace.ts +0 -154
- package/src/daemon/handlers/session.ts +0 -1137
- package/src/daemon/handlers/snapshot.ts +0 -439
- package/src/daemon/is-predicates.ts +0 -46
- package/src/daemon/selectors.ts +0 -540
- package/src/daemon/session-routing.ts +0 -22
- package/src/daemon/session-selector.ts +0 -39
- package/src/daemon/session-store.ts +0 -296
- package/src/daemon/snapshot-processing.ts +0 -131
- package/src/daemon/types.ts +0 -56
- package/src/daemon-client.ts +0 -272
- package/src/daemon.ts +0 -295
- package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
- package/src/platforms/android/__tests__/index.test.ts +0 -274
- package/src/platforms/android/devices.ts +0 -196
- package/src/platforms/android/index.ts +0 -784
- package/src/platforms/android/ui-hierarchy.ts +0 -312
- package/src/platforms/boot-diagnostics.ts +0 -128
- package/src/platforms/ios/__tests__/index.test.ts +0 -312
- package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
- package/src/platforms/ios/apps.ts +0 -358
- package/src/platforms/ios/ax-snapshot.ts +0 -207
- package/src/platforms/ios/config.ts +0 -28
- package/src/platforms/ios/devicectl.ts +0 -134
- package/src/platforms/ios/devices.ts +0 -100
- package/src/platforms/ios/index.ts +0 -20
- package/src/platforms/ios/runner-client.ts +0 -994
- package/src/platforms/ios/simulator.ts +0 -164
- package/src/utils/__tests__/args.test.ts +0 -239
- package/src/utils/__tests__/daemon-client.test.ts +0 -95
- package/src/utils/__tests__/exec.test.ts +0 -16
- package/src/utils/__tests__/finders.test.ts +0 -34
- package/src/utils/__tests__/keyed-lock.test.ts +0 -55
- package/src/utils/__tests__/process-identity.test.ts +0 -33
- package/src/utils/__tests__/retry.test.ts +0 -44
- package/src/utils/args.ts +0 -239
- package/src/utils/command-schema.ts +0 -622
- package/src/utils/device.ts +0 -84
- package/src/utils/errors.ts +0 -35
- package/src/utils/exec.ts +0 -339
- package/src/utils/finders.ts +0 -101
- package/src/utils/interactive.ts +0 -4
- package/src/utils/interactors.ts +0 -173
- package/src/utils/keyed-lock.ts +0 -14
- package/src/utils/output.ts +0 -204
- package/src/utils/process-identity.ts +0 -100
- package/src/utils/retry.ts +0 -180
- package/src/utils/snapshot.ts +0 -64
- package/src/utils/timeouts.ts +0 -9
- package/src/utils/version.ts +0 -26
package/src/utils/output.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { AppError } from './errors.ts';
|
|
2
|
-
|
|
3
|
-
export type JsonResult =
|
|
4
|
-
| { success: true; data?: Record<string, unknown> }
|
|
5
|
-
| { success: false; error: { code: string; message: string; details?: Record<string, unknown> } };
|
|
6
|
-
|
|
7
|
-
export function printJson(result: JsonResult): void {
|
|
8
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function printHumanError(err: AppError): void {
|
|
12
|
-
const details = err.details ? `\n${JSON.stringify(err.details, null, 2)}` : '';
|
|
13
|
-
process.stderr.write(`Error (${err.code}): ${err.message}${details}\n`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type SnapshotRect = { x: number; y: number; width: number; height: number };
|
|
17
|
-
type SnapshotNode = {
|
|
18
|
-
ref?: string;
|
|
19
|
-
depth?: number;
|
|
20
|
-
type?: string;
|
|
21
|
-
label?: string;
|
|
22
|
-
value?: string;
|
|
23
|
-
identifier?: string;
|
|
24
|
-
rect?: SnapshotRect;
|
|
25
|
-
hittable?: boolean;
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function formatSnapshotText(
|
|
30
|
-
data: Record<string, unknown>,
|
|
31
|
-
options: { raw?: boolean; flatten?: boolean } = {},
|
|
32
|
-
): string {
|
|
33
|
-
const rawNodes = data.nodes;
|
|
34
|
-
const nodes = Array.isArray(rawNodes) ? (rawNodes as SnapshotNode[]) : [];
|
|
35
|
-
const truncated = Boolean(data.truncated);
|
|
36
|
-
const appName = typeof data.appName === 'string' ? data.appName : undefined;
|
|
37
|
-
const appBundleId = typeof data.appBundleId === 'string' ? data.appBundleId : undefined;
|
|
38
|
-
const meta: string[] = [];
|
|
39
|
-
if (appName) meta.push(`Page: ${appName}`);
|
|
40
|
-
if (appBundleId) meta.push(`App: ${appBundleId}`);
|
|
41
|
-
const header = `Snapshot: ${nodes.length} nodes${truncated ? ' (truncated)' : ''}`;
|
|
42
|
-
const prefix = meta.length > 0 ? `${meta.join('\n')}\n` : '';
|
|
43
|
-
if (nodes.length === 0) {
|
|
44
|
-
return `${prefix}${header}\n`;
|
|
45
|
-
}
|
|
46
|
-
if (options.raw) {
|
|
47
|
-
const rawLines = nodes.map((node) => JSON.stringify(node));
|
|
48
|
-
return `${prefix}${header}\n${rawLines.join('\n')}\n`;
|
|
49
|
-
}
|
|
50
|
-
if (options.flatten) {
|
|
51
|
-
const flatLines = nodes.map((node) => formatSnapshotLine(node, 0, false));
|
|
52
|
-
return `${prefix}${header}\n${flatLines.join('\n')}\n`;
|
|
53
|
-
}
|
|
54
|
-
const hiddenGroupDepths: number[] = [];
|
|
55
|
-
const lines: string[] = [];
|
|
56
|
-
for (const node of nodes) {
|
|
57
|
-
const depth = node.depth ?? 0;
|
|
58
|
-
while (hiddenGroupDepths.length > 0 && depth <= hiddenGroupDepths[hiddenGroupDepths.length - 1]) {
|
|
59
|
-
hiddenGroupDepths.pop();
|
|
60
|
-
}
|
|
61
|
-
const label = node.label?.trim() || node.value?.trim() || node.identifier?.trim() || '';
|
|
62
|
-
const type = formatRole(node.type ?? 'Element');
|
|
63
|
-
const isHiddenGroup = type === 'group' && !label;
|
|
64
|
-
if (isHiddenGroup) {
|
|
65
|
-
hiddenGroupDepths.push(depth);
|
|
66
|
-
}
|
|
67
|
-
const adjustedDepth = isHiddenGroup
|
|
68
|
-
? depth
|
|
69
|
-
: Math.max(0, depth - hiddenGroupDepths.length);
|
|
70
|
-
lines.push(formatSnapshotLine(node, adjustedDepth, isHiddenGroup));
|
|
71
|
-
}
|
|
72
|
-
return `${prefix}${header}\n${lines.join('\n')}\n`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatSnapshotLine(node: SnapshotNode, depth: number, hiddenGroup: boolean): string {
|
|
76
|
-
const type = formatRole(node.type ?? 'Element');
|
|
77
|
-
const label = displayLabel(node, type);
|
|
78
|
-
const indent = ' '.repeat(depth);
|
|
79
|
-
const ref = node.ref ? `@${node.ref}` : '';
|
|
80
|
-
const flags = [node.enabled === false ? 'disabled' : null].filter(Boolean).join(', ');
|
|
81
|
-
const flagText = flags ? ` [${flags}]` : '';
|
|
82
|
-
const textPart = label ? ` "${label}"` : '';
|
|
83
|
-
if (hiddenGroup) {
|
|
84
|
-
return `${indent}${ref} [${type}]${flagText}`.trimEnd();
|
|
85
|
-
}
|
|
86
|
-
return `${indent}${ref} [${type}]${textPart}${flagText}`.trimEnd();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function displayLabel(node: SnapshotNode, type: string): string {
|
|
90
|
-
const label = node.label?.trim();
|
|
91
|
-
const value = node.value?.trim();
|
|
92
|
-
if (isEditableRole(type)) {
|
|
93
|
-
// For inputs, prefer current value over placeholder/accessibility label.
|
|
94
|
-
if (value) return value;
|
|
95
|
-
if (label) return label;
|
|
96
|
-
} else if (label) {
|
|
97
|
-
return label;
|
|
98
|
-
}
|
|
99
|
-
if (value) return value;
|
|
100
|
-
const identifier = node.identifier?.trim();
|
|
101
|
-
if (!identifier) return '';
|
|
102
|
-
if (isGenericResourceId(identifier) && (type === 'group' || type === 'image' || type === 'list' || type === 'collection')) {
|
|
103
|
-
return '';
|
|
104
|
-
}
|
|
105
|
-
return identifier;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isEditableRole(type: string): boolean {
|
|
109
|
-
return type === 'text-field' || type === 'text-view' || type === 'search';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function isGenericResourceId(value: string): boolean {
|
|
113
|
-
return /^[\w.]+:id\/[\w.-]+$/i.test(value);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function formatRole(type: string): string {
|
|
117
|
-
const raw = type;
|
|
118
|
-
let normalized = type.replace(/XCUIElementType/gi, '').toLowerCase();
|
|
119
|
-
const isAndroidClass =
|
|
120
|
-
raw.includes('.') &&
|
|
121
|
-
(raw.startsWith('android.') || raw.startsWith('androidx.') || raw.startsWith('com.'));
|
|
122
|
-
if (normalized.startsWith('ax')) {
|
|
123
|
-
normalized = normalized.replace(/^ax/, '');
|
|
124
|
-
}
|
|
125
|
-
if (normalized.includes('.')) {
|
|
126
|
-
normalized = normalized
|
|
127
|
-
.replace(/^android\.widget\./, '')
|
|
128
|
-
.replace(/^android\.view\./, '')
|
|
129
|
-
.replace(/^android\.webkit\./, '')
|
|
130
|
-
.replace(/^androidx\./, '')
|
|
131
|
-
.replace(/^com\.google\.android\./, '')
|
|
132
|
-
.replace(/^com\.android\./, '');
|
|
133
|
-
}
|
|
134
|
-
switch (normalized) {
|
|
135
|
-
case 'application':
|
|
136
|
-
return 'application';
|
|
137
|
-
case 'navigationbar':
|
|
138
|
-
return 'navigation-bar';
|
|
139
|
-
case 'tabbar':
|
|
140
|
-
return 'tab-bar';
|
|
141
|
-
case 'button':
|
|
142
|
-
case 'imagebutton':
|
|
143
|
-
return 'button';
|
|
144
|
-
case 'link':
|
|
145
|
-
return 'link';
|
|
146
|
-
case 'cell':
|
|
147
|
-
return 'cell';
|
|
148
|
-
case 'statictext':
|
|
149
|
-
case 'checkedtextview':
|
|
150
|
-
return 'text';
|
|
151
|
-
case 'textfield':
|
|
152
|
-
case 'edittext':
|
|
153
|
-
return 'text-field';
|
|
154
|
-
case 'textview':
|
|
155
|
-
return isAndroidClass ? 'text' : 'text-view';
|
|
156
|
-
case 'textarea':
|
|
157
|
-
return 'text-view';
|
|
158
|
-
case 'switch':
|
|
159
|
-
return 'switch';
|
|
160
|
-
case 'slider':
|
|
161
|
-
return 'slider';
|
|
162
|
-
case 'image':
|
|
163
|
-
case 'imageview':
|
|
164
|
-
return 'image';
|
|
165
|
-
case 'webview':
|
|
166
|
-
return 'webview';
|
|
167
|
-
case 'framelayout':
|
|
168
|
-
case 'linearlayout':
|
|
169
|
-
case 'relativelayout':
|
|
170
|
-
case 'constraintlayout':
|
|
171
|
-
case 'viewgroup':
|
|
172
|
-
case 'view':
|
|
173
|
-
return 'group';
|
|
174
|
-
case 'listview':
|
|
175
|
-
case 'recyclerview':
|
|
176
|
-
return 'list';
|
|
177
|
-
case 'collectionview':
|
|
178
|
-
return 'collection';
|
|
179
|
-
case 'searchfield':
|
|
180
|
-
return 'search';
|
|
181
|
-
case 'segmentedcontrol':
|
|
182
|
-
return 'segmented-control';
|
|
183
|
-
case 'group':
|
|
184
|
-
return 'group';
|
|
185
|
-
case 'window':
|
|
186
|
-
return 'window';
|
|
187
|
-
case 'checkbox':
|
|
188
|
-
return 'checkbox';
|
|
189
|
-
case 'radio':
|
|
190
|
-
return 'radio';
|
|
191
|
-
case 'menuitem':
|
|
192
|
-
return 'menu-item';
|
|
193
|
-
case 'toolbar':
|
|
194
|
-
return 'toolbar';
|
|
195
|
-
case 'scrollarea':
|
|
196
|
-
case 'scrollview':
|
|
197
|
-
case 'nestedscrollview':
|
|
198
|
-
return 'scroll-area';
|
|
199
|
-
case 'table':
|
|
200
|
-
return 'table';
|
|
201
|
-
default:
|
|
202
|
-
return normalized || 'element';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { runCmdSync } from './exec.ts';
|
|
2
|
-
|
|
3
|
-
const PS_TIMEOUT_MS = 1_000;
|
|
4
|
-
const DAEMON_COMMAND_PATTERNS = [
|
|
5
|
-
/(^|[\/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,
|
|
6
|
-
/(^|[\/\s"'=])src\/daemon\.ts($|[\s"'])/,
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
export function isProcessAlive(pid: number): boolean {
|
|
10
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
11
|
-
try {
|
|
12
|
-
process.kill(pid, 0);
|
|
13
|
-
return true;
|
|
14
|
-
} catch (err) {
|
|
15
|
-
return (err as NodeJS.ErrnoException).code === 'EPERM';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function readProcessStartTime(pid: number): string | null {
|
|
20
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
21
|
-
try {
|
|
22
|
-
const result = runCmdSync('ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
23
|
-
allowFailure: true,
|
|
24
|
-
timeoutMs: PS_TIMEOUT_MS,
|
|
25
|
-
});
|
|
26
|
-
if (result.exitCode !== 0) return null;
|
|
27
|
-
const value = result.stdout.trim();
|
|
28
|
-
return value.length > 0 ? value : null;
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function readProcessCommand(pid: number): string | null {
|
|
35
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
36
|
-
try {
|
|
37
|
-
const result = runCmdSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
38
|
-
allowFailure: true,
|
|
39
|
-
timeoutMs: PS_TIMEOUT_MS,
|
|
40
|
-
});
|
|
41
|
-
if (result.exitCode !== 0) return null;
|
|
42
|
-
const value = result.stdout.trim();
|
|
43
|
-
return value.length > 0 ? value : null;
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isAgentDeviceDaemonCommand(command: string): boolean {
|
|
50
|
-
const normalized = command.toLowerCase().replaceAll('\\', '/');
|
|
51
|
-
if (!normalized.includes('agent-device')) return false;
|
|
52
|
-
return DAEMON_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function isAgentDeviceDaemonProcess(pid: number, expectedStartTime?: string): boolean {
|
|
56
|
-
if (!isProcessAlive(pid)) return false;
|
|
57
|
-
if (expectedStartTime) {
|
|
58
|
-
const actualStartTime = readProcessStartTime(pid);
|
|
59
|
-
if (!actualStartTime || actualStartTime !== expectedStartTime) return false;
|
|
60
|
-
}
|
|
61
|
-
const command = readProcessCommand(pid);
|
|
62
|
-
if (!command) return false;
|
|
63
|
-
return isAgentDeviceDaemonCommand(command);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function trySignalProcess(pid: number, signal: NodeJS.Signals): boolean {
|
|
67
|
-
try {
|
|
68
|
-
process.kill(pid, signal);
|
|
69
|
-
return true;
|
|
70
|
-
} catch (err) {
|
|
71
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
72
|
-
if (code === 'ESRCH' || code === 'EPERM') return false;
|
|
73
|
-
throw err;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function waitForProcessExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
78
|
-
if (!isProcessAlive(pid)) return true;
|
|
79
|
-
const start = Date.now();
|
|
80
|
-
while (Date.now() - start < timeoutMs) {
|
|
81
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
82
|
-
if (!isProcessAlive(pid)) return true;
|
|
83
|
-
}
|
|
84
|
-
return !isProcessAlive(pid);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function stopProcessForTakeover(
|
|
88
|
-
pid: number,
|
|
89
|
-
options: {
|
|
90
|
-
termTimeoutMs: number;
|
|
91
|
-
killTimeoutMs: number;
|
|
92
|
-
expectedStartTime?: string;
|
|
93
|
-
},
|
|
94
|
-
): Promise<void> {
|
|
95
|
-
if (!isAgentDeviceDaemonProcess(pid, options.expectedStartTime)) return;
|
|
96
|
-
if (!trySignalProcess(pid, 'SIGTERM')) return;
|
|
97
|
-
if (await waitForProcessExit(pid, options.termTimeoutMs)) return;
|
|
98
|
-
if (!trySignalProcess(pid, 'SIGKILL')) return;
|
|
99
|
-
await waitForProcessExit(pid, options.killTimeoutMs);
|
|
100
|
-
}
|
package/src/utils/retry.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { AppError } from './errors.ts';
|
|
2
|
-
|
|
3
|
-
type RetryOptions = {
|
|
4
|
-
attempts?: number;
|
|
5
|
-
baseDelayMs?: number;
|
|
6
|
-
maxDelayMs?: number;
|
|
7
|
-
jitter?: number;
|
|
8
|
-
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type RetryPolicy = {
|
|
12
|
-
maxAttempts: number;
|
|
13
|
-
baseDelayMs: number;
|
|
14
|
-
maxDelayMs: number;
|
|
15
|
-
jitter: number;
|
|
16
|
-
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type RetryAttemptContext = {
|
|
20
|
-
attempt: number;
|
|
21
|
-
maxAttempts: number;
|
|
22
|
-
deadline?: Deadline;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type TimeoutProfile = {
|
|
26
|
-
startupMs: number;
|
|
27
|
-
operationMs: number;
|
|
28
|
-
totalMs: number;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type RetryTelemetryEvent = {
|
|
32
|
-
phase?: string;
|
|
33
|
-
event: 'attempt_failed' | 'retry_scheduled' | 'succeeded' | 'exhausted';
|
|
34
|
-
attempt: number;
|
|
35
|
-
maxAttempts: number;
|
|
36
|
-
delayMs?: number;
|
|
37
|
-
elapsedMs?: number;
|
|
38
|
-
remainingMs?: number;
|
|
39
|
-
reason?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export function isEnvTruthy(value: string | undefined): boolean {
|
|
43
|
-
return ['1', 'true', 'yes', 'on'].includes((value ?? '').toLowerCase());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const TIMEOUT_PROFILES: Record<string, TimeoutProfile> = {
|
|
47
|
-
ios_boot: { startupMs: 120_000, operationMs: 20_000, totalMs: 120_000 },
|
|
48
|
-
ios_runner_connect: { startupMs: 120_000, operationMs: 15_000, totalMs: 120_000 },
|
|
49
|
-
android_boot: { startupMs: 60_000, operationMs: 10_000, totalMs: 60_000 },
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const defaultOptions: Required<Pick<RetryOptions, 'attempts' | 'baseDelayMs' | 'maxDelayMs' | 'jitter'>> = {
|
|
53
|
-
attempts: 3,
|
|
54
|
-
baseDelayMs: 200,
|
|
55
|
-
maxDelayMs: 2000,
|
|
56
|
-
jitter: 0.2,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export class Deadline {
|
|
60
|
-
private readonly startedAtMs: number;
|
|
61
|
-
private readonly expiresAtMs: number;
|
|
62
|
-
|
|
63
|
-
private constructor(startedAtMs: number, timeoutMs: number) {
|
|
64
|
-
this.startedAtMs = startedAtMs;
|
|
65
|
-
this.expiresAtMs = startedAtMs + Math.max(0, timeoutMs);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static fromTimeoutMs(timeoutMs: number, nowMs = Date.now()): Deadline {
|
|
69
|
-
return new Deadline(nowMs, timeoutMs);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
remainingMs(nowMs = Date.now()): number {
|
|
73
|
-
return Math.max(0, this.expiresAtMs - nowMs);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
elapsedMs(nowMs = Date.now()): number {
|
|
77
|
-
return Math.max(0, nowMs - this.startedAtMs);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
isExpired(nowMs = Date.now()): boolean {
|
|
81
|
-
return this.remainingMs(nowMs) <= 0;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function retryWithPolicy<T>(
|
|
86
|
-
fn: (context: RetryAttemptContext) => Promise<T>,
|
|
87
|
-
policy: Partial<RetryPolicy> = {},
|
|
88
|
-
options: {
|
|
89
|
-
deadline?: Deadline;
|
|
90
|
-
phase?: string;
|
|
91
|
-
classifyReason?: (error: unknown) => string | undefined;
|
|
92
|
-
onEvent?: (event: RetryTelemetryEvent) => void;
|
|
93
|
-
} = {},
|
|
94
|
-
): Promise<T> {
|
|
95
|
-
const merged: RetryPolicy = {
|
|
96
|
-
maxAttempts: policy.maxAttempts ?? defaultOptions.attempts,
|
|
97
|
-
baseDelayMs: policy.baseDelayMs ?? defaultOptions.baseDelayMs,
|
|
98
|
-
maxDelayMs: policy.maxDelayMs ?? defaultOptions.maxDelayMs,
|
|
99
|
-
jitter: policy.jitter ?? defaultOptions.jitter,
|
|
100
|
-
shouldRetry: policy.shouldRetry,
|
|
101
|
-
};
|
|
102
|
-
let lastError: unknown;
|
|
103
|
-
for (let attempt = 1; attempt <= merged.maxAttempts; attempt += 1) {
|
|
104
|
-
if (options.deadline?.isExpired() && attempt > 1) break;
|
|
105
|
-
try {
|
|
106
|
-
const result = await fn({ attempt, maxAttempts: merged.maxAttempts, deadline: options.deadline });
|
|
107
|
-
options.onEvent?.({
|
|
108
|
-
phase: options.phase,
|
|
109
|
-
event: 'succeeded',
|
|
110
|
-
attempt,
|
|
111
|
-
maxAttempts: merged.maxAttempts,
|
|
112
|
-
elapsedMs: options.deadline?.elapsedMs(),
|
|
113
|
-
remainingMs: options.deadline?.remainingMs(),
|
|
114
|
-
});
|
|
115
|
-
return result;
|
|
116
|
-
} catch (err) {
|
|
117
|
-
lastError = err;
|
|
118
|
-
const reason = options.classifyReason?.(err);
|
|
119
|
-
options.onEvent?.({
|
|
120
|
-
phase: options.phase,
|
|
121
|
-
event: 'attempt_failed',
|
|
122
|
-
attempt,
|
|
123
|
-
maxAttempts: merged.maxAttempts,
|
|
124
|
-
elapsedMs: options.deadline?.elapsedMs(),
|
|
125
|
-
remainingMs: options.deadline?.remainingMs(),
|
|
126
|
-
reason,
|
|
127
|
-
});
|
|
128
|
-
if (attempt >= merged.maxAttempts) break;
|
|
129
|
-
if (merged.shouldRetry && !merged.shouldRetry(err, attempt)) break;
|
|
130
|
-
const delay = computeDelay(merged.baseDelayMs, merged.maxDelayMs, merged.jitter, attempt);
|
|
131
|
-
const boundedDelay = options.deadline ? Math.min(delay, options.deadline.remainingMs()) : delay;
|
|
132
|
-
if (boundedDelay <= 0) break;
|
|
133
|
-
options.onEvent?.({
|
|
134
|
-
phase: options.phase,
|
|
135
|
-
event: 'retry_scheduled',
|
|
136
|
-
attempt,
|
|
137
|
-
maxAttempts: merged.maxAttempts,
|
|
138
|
-
delayMs: boundedDelay,
|
|
139
|
-
elapsedMs: options.deadline?.elapsedMs(),
|
|
140
|
-
remainingMs: options.deadline?.remainingMs(),
|
|
141
|
-
reason,
|
|
142
|
-
});
|
|
143
|
-
await sleep(boundedDelay);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
options.onEvent?.({
|
|
147
|
-
phase: options.phase,
|
|
148
|
-
event: 'exhausted',
|
|
149
|
-
attempt: merged.maxAttempts,
|
|
150
|
-
maxAttempts: merged.maxAttempts,
|
|
151
|
-
elapsedMs: options.deadline?.elapsedMs(),
|
|
152
|
-
remainingMs: options.deadline?.remainingMs(),
|
|
153
|
-
reason: options.classifyReason?.(lastError),
|
|
154
|
-
});
|
|
155
|
-
if (lastError) throw lastError;
|
|
156
|
-
throw new AppError('COMMAND_FAILED', 'retry failed');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export async function withRetry<T>(
|
|
160
|
-
fn: () => Promise<T>,
|
|
161
|
-
options: RetryOptions = {},
|
|
162
|
-
): Promise<T> {
|
|
163
|
-
return retryWithPolicy(() => fn(), {
|
|
164
|
-
maxAttempts: options.attempts,
|
|
165
|
-
baseDelayMs: options.baseDelayMs,
|
|
166
|
-
maxDelayMs: options.maxDelayMs,
|
|
167
|
-
jitter: options.jitter,
|
|
168
|
-
shouldRetry: options.shouldRetry,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function computeDelay(base: number, max: number, jitter: number, attempt: number): number {
|
|
173
|
-
const exp = Math.min(max, base * 2 ** (attempt - 1));
|
|
174
|
-
const jitterAmount = exp * jitter;
|
|
175
|
-
return Math.max(0, exp + (Math.random() * 2 - 1) * jitterAmount);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function sleep(ms: number): Promise<void> {
|
|
179
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
180
|
-
}
|
package/src/utils/snapshot.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
export type Rect = {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type SnapshotOptions = {
|
|
9
|
-
interactiveOnly?: boolean;
|
|
10
|
-
compact?: boolean;
|
|
11
|
-
depth?: number;
|
|
12
|
-
scope?: string;
|
|
13
|
-
raw?: boolean;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type RawSnapshotNode = {
|
|
17
|
-
index: number;
|
|
18
|
-
type?: string;
|
|
19
|
-
label?: string;
|
|
20
|
-
value?: string;
|
|
21
|
-
identifier?: string;
|
|
22
|
-
rect?: Rect;
|
|
23
|
-
enabled?: boolean;
|
|
24
|
-
selected?: boolean;
|
|
25
|
-
hittable?: boolean;
|
|
26
|
-
depth?: number;
|
|
27
|
-
parentIndex?: number;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type SnapshotNode = RawSnapshotNode & {
|
|
31
|
-
ref: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type SnapshotState = {
|
|
35
|
-
nodes: SnapshotNode[];
|
|
36
|
-
createdAt: number;
|
|
37
|
-
truncated?: boolean;
|
|
38
|
-
backend?: 'ax' | 'xctest' | 'android';
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export function attachRefs(nodes: RawSnapshotNode[]): SnapshotNode[] {
|
|
42
|
-
return nodes.map((node, idx) => ({ ...node, ref: `e${idx + 1}` }));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function normalizeRef(input: string): string | null {
|
|
46
|
-
const trimmed = input.trim();
|
|
47
|
-
if (trimmed.startsWith('@')) {
|
|
48
|
-
const ref = trimmed.slice(1);
|
|
49
|
-
return ref ? ref : null;
|
|
50
|
-
}
|
|
51
|
-
if (trimmed.startsWith('e')) return trimmed;
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function findNodeByRef(nodes: SnapshotNode[], ref: string): SnapshotNode | null {
|
|
56
|
-
return nodes.find((node) => node.ref === ref) ?? null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function centerOfRect(rect: Rect): { x: number; y: number } {
|
|
60
|
-
return {
|
|
61
|
-
x: Math.round(rect.x + rect.width / 2),
|
|
62
|
-
y: Math.round(rect.y + rect.height / 2),
|
|
63
|
-
};
|
|
64
|
-
}
|
package/src/utils/timeouts.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function resolveTimeoutMs(raw: string | undefined, fallback: number, min: number): number {
|
|
2
|
-
if (!raw) return fallback;
|
|
3
|
-
const parsed = Number(raw);
|
|
4
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
5
|
-
return Math.max(min, Math.floor(parsed));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Alias for `resolveTimeoutMs` — semantically marks the caller expects seconds. */
|
|
9
|
-
export const resolveTimeoutSeconds = resolveTimeoutMs;
|
package/src/utils/version.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
export function readVersion(): string {
|
|
6
|
-
try {
|
|
7
|
-
const root = findProjectRoot();
|
|
8
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) as {
|
|
9
|
-
version?: string;
|
|
10
|
-
};
|
|
11
|
-
return pkg.version ?? '0.0.0';
|
|
12
|
-
} catch {
|
|
13
|
-
return '0.0.0';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function findProjectRoot(): string {
|
|
18
|
-
const start = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
let current = start;
|
|
20
|
-
for (let i = 0; i < 6; i += 1) {
|
|
21
|
-
const pkgPath = path.join(current, 'package.json');
|
|
22
|
-
if (fs.existsSync(pkgPath)) return current;
|
|
23
|
-
current = path.dirname(current);
|
|
24
|
-
}
|
|
25
|
-
return start;
|
|
26
|
-
}
|