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.
Files changed (91) hide show
  1. package/README.md +2 -9
  2. package/dist/src/797.js +1 -1
  3. package/dist/src/bin.js +5 -5
  4. package/dist/src/daemon.js +16 -20
  5. package/package.json +7 -9
  6. package/skills/agent-device/SKILL.md +3 -6
  7. package/skills/agent-device/references/permissions.md +3 -15
  8. package/skills/agent-device/references/snapshot-refs.md +1 -4
  9. package/dist/bin/axsnapshot +0 -0
  10. package/ios-runner/AXSnapshot/Package.swift +0 -18
  11. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  12. package/src/__tests__/cli-close.test.ts +0 -155
  13. package/src/__tests__/cli-help.test.ts +0 -102
  14. package/src/bin.ts +0 -3
  15. package/src/cli.ts +0 -305
  16. package/src/core/__tests__/capabilities.test.ts +0 -75
  17. package/src/core/__tests__/dispatch-open.test.ts +0 -25
  18. package/src/core/__tests__/open-target.test.ts +0 -55
  19. package/src/core/capabilities.ts +0 -57
  20. package/src/core/dispatch.ts +0 -382
  21. package/src/core/open-target.ts +0 -27
  22. package/src/daemon/__tests__/device-ready.test.ts +0 -52
  23. package/src/daemon/__tests__/is-predicates.test.ts +0 -68
  24. package/src/daemon/__tests__/selectors.test.ts +0 -261
  25. package/src/daemon/__tests__/session-routing.test.ts +0 -108
  26. package/src/daemon/__tests__/session-selector.test.ts +0 -64
  27. package/src/daemon/__tests__/session-store.test.ts +0 -142
  28. package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
  29. package/src/daemon/action-utils.ts +0 -29
  30. package/src/daemon/context.ts +0 -48
  31. package/src/daemon/device-ready.ts +0 -155
  32. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  33. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  34. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  35. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  36. package/src/daemon/handlers/__tests__/session.test.ts +0 -820
  37. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  38. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  39. package/src/daemon/handlers/find.ts +0 -324
  40. package/src/daemon/handlers/interaction.ts +0 -550
  41. package/src/daemon/handlers/parse-utils.ts +0 -8
  42. package/src/daemon/handlers/record-trace.ts +0 -154
  43. package/src/daemon/handlers/session.ts +0 -1137
  44. package/src/daemon/handlers/snapshot.ts +0 -439
  45. package/src/daemon/is-predicates.ts +0 -46
  46. package/src/daemon/selectors.ts +0 -540
  47. package/src/daemon/session-routing.ts +0 -22
  48. package/src/daemon/session-selector.ts +0 -39
  49. package/src/daemon/session-store.ts +0 -296
  50. package/src/daemon/snapshot-processing.ts +0 -131
  51. package/src/daemon/types.ts +0 -56
  52. package/src/daemon-client.ts +0 -272
  53. package/src/daemon.ts +0 -295
  54. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  55. package/src/platforms/android/__tests__/index.test.ts +0 -274
  56. package/src/platforms/android/devices.ts +0 -196
  57. package/src/platforms/android/index.ts +0 -784
  58. package/src/platforms/android/ui-hierarchy.ts +0 -312
  59. package/src/platforms/boot-diagnostics.ts +0 -128
  60. package/src/platforms/ios/__tests__/index.test.ts +0 -312
  61. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
  62. package/src/platforms/ios/apps.ts +0 -358
  63. package/src/platforms/ios/ax-snapshot.ts +0 -207
  64. package/src/platforms/ios/config.ts +0 -28
  65. package/src/platforms/ios/devicectl.ts +0 -134
  66. package/src/platforms/ios/devices.ts +0 -100
  67. package/src/platforms/ios/index.ts +0 -20
  68. package/src/platforms/ios/runner-client.ts +0 -994
  69. package/src/platforms/ios/simulator.ts +0 -164
  70. package/src/utils/__tests__/args.test.ts +0 -239
  71. package/src/utils/__tests__/daemon-client.test.ts +0 -95
  72. package/src/utils/__tests__/exec.test.ts +0 -16
  73. package/src/utils/__tests__/finders.test.ts +0 -34
  74. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  75. package/src/utils/__tests__/process-identity.test.ts +0 -33
  76. package/src/utils/__tests__/retry.test.ts +0 -44
  77. package/src/utils/args.ts +0 -239
  78. package/src/utils/command-schema.ts +0 -622
  79. package/src/utils/device.ts +0 -84
  80. package/src/utils/errors.ts +0 -35
  81. package/src/utils/exec.ts +0 -339
  82. package/src/utils/finders.ts +0 -101
  83. package/src/utils/interactive.ts +0 -4
  84. package/src/utils/interactors.ts +0 -173
  85. package/src/utils/keyed-lock.ts +0 -14
  86. package/src/utils/output.ts +0 -204
  87. package/src/utils/process-identity.ts +0 -100
  88. package/src/utils/retry.ts +0 -180
  89. package/src/utils/snapshot.ts +0 -64
  90. package/src/utils/timeouts.ts +0 -9
  91. package/src/utils/version.ts +0 -26
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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;
@@ -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
- }