agent-device 0.3.0 → 0.3.2
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 +26 -2
- package/dist/src/274.js +1 -0
- package/dist/src/bin.js +27 -22
- package/dist/src/daemon.js +15 -10
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +4 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +149 -0
- package/package.json +2 -2
- package/skills/agent-device/SKILL.md +8 -1
- package/src/cli.ts +13 -0
- package/src/core/__tests__/capabilities.test.ts +2 -0
- package/src/core/capabilities.ts +2 -0
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +5 -0
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +219 -0
- package/src/daemon/handlers/__tests__/session.test.ts +122 -0
- package/src/daemon/handlers/find.ts +23 -3
- package/src/daemon/handlers/session.ts +175 -10
- package/src/daemon-client.ts +1 -24
- package/src/daemon.ts +1 -24
- package/src/platforms/__tests__/boot-diagnostics.test.ts +59 -0
- package/src/platforms/android/__tests__/index.test.ts +17 -0
- package/src/platforms/android/devices.ts +167 -42
- package/src/platforms/android/index.ts +101 -14
- package/src/platforms/boot-diagnostics.ts +128 -0
- package/src/platforms/ios/index.ts +161 -2
- package/src/platforms/ios/runner-client.ts +19 -1
- package/src/utils/__tests__/exec.test.ts +16 -0
- package/src/utils/__tests__/finders.test.ts +34 -0
- package/src/utils/__tests__/retry.test.ts +44 -0
- package/src/utils/args.ts +9 -1
- package/src/utils/exec.ts +39 -0
- package/src/utils/finders.ts +27 -9
- package/src/utils/retry.ts +143 -13
- package/src/utils/version.ts +26 -0
- package/dist/src/861.js +0 -1
package/src/utils/retry.ts
CHANGED
|
@@ -8,6 +8,47 @@ type RetryOptions = {
|
|
|
8
8
|
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
9
9
|
};
|
|
10
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
|
+
|
|
11
52
|
const defaultOptions: Required<Pick<RetryOptions, 'attempts' | 'baseDelayMs' | 'maxDelayMs' | 'jitter'>> = {
|
|
12
53
|
attempts: 3,
|
|
13
54
|
baseDelayMs: 200,
|
|
@@ -15,30 +56,119 @@ const defaultOptions: Required<Pick<RetryOptions, 'attempts' | 'baseDelayMs' | '
|
|
|
15
56
|
jitter: 0.2,
|
|
16
57
|
};
|
|
17
58
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
} = {},
|
|
21
94
|
): Promise<T> {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
};
|
|
26
102
|
let lastError: unknown;
|
|
27
|
-
for (let attempt = 1; attempt <=
|
|
103
|
+
for (let attempt = 1; attempt <= merged.maxAttempts; attempt += 1) {
|
|
104
|
+
if (options.deadline?.isExpired() && attempt > 1) break;
|
|
28
105
|
try {
|
|
29
|
-
|
|
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;
|
|
30
116
|
} catch (err) {
|
|
31
117
|
lastError = err;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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);
|
|
36
144
|
}
|
|
37
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
|
+
});
|
|
38
155
|
if (lastError) throw lastError;
|
|
39
156
|
throw new AppError('COMMAND_FAILED', 'retry failed');
|
|
40
157
|
}
|
|
41
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
|
+
|
|
42
172
|
function computeDelay(base: number, max: number, jitter: number, attempt: number): number {
|
|
43
173
|
const exp = Math.min(max, base * 2 ** (attempt - 1));
|
|
44
174
|
const jitterAmount = exp * jitter;
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
}
|
package/dist/src/861.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{spawn as e}from"node:child_process";function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}class n extends Error{constructor(e,n,o,r){super(n),t(this,"code",void 0),t(this,"details",void 0),t(this,"cause",void 0),this.code=e,this.details=o,this.cause=r}}function o(e){return e instanceof n?e:e instanceof Error?new n("UNKNOWN",e.message,void 0,e):new n("UNKNOWN","Unknown error",{err:e})}async function r(t,o,d={}){return new Promise((r,i)=>{let s=e(t,o,{cwd:d.cwd,env:d.env,stdio:["pipe","pipe","pipe"]}),u="",a=d.binaryStdout?Buffer.alloc(0):void 0,c="";d.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==d.stdin&&s.stdin.write(d.stdin),s.stdin.end(),s.stdout.on("data",e=>{d.binaryStdout?a=Buffer.concat([a??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):u+=e}),s.stderr.on("data",e=>{c+=e}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||d.allowFailure?r({stdout:u,stderr:c,exitCode:s,stdoutBuffer:a}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:c,exitCode:s}))})})}async function d(e){try{var t;let{shell:n,args:o}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),d=await r(n,o,{allowFailure:!0});return 0===d.exitCode&&d.stdout.trim().length>0}catch{return!1}}function i(t,n,o={}){e(t,n,{cwd:o.cwd,env:o.env,stdio:"ignore",detached:!0}).unref()}async function s(t,o,r={}){return new Promise((d,i)=>{let s=e(t,o,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"]}),u="",a="",c=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==r.stdin&&s.stdin.write(r.stdin),s.stdin.end(),s.stdout.on("data",e=>{if(r.binaryStdout){c=Buffer.concat([c??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);u+=t,r.onStdoutChunk?.(t)}),s.stderr.on("data",e=>{let t=String(e);a+=t,r.onStderrChunk?.(t)}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||r.allowFailure?d({stdout:u,stderr:a,exitCode:s,stdoutBuffer:c}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:a,exitCode:s}))})})}function u(t,o,r={}){let d=e(t,o,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"]}),i="",s="";d.stdout.setEncoding("utf8"),d.stderr.setEncoding("utf8"),d.stdout.on("data",e=>{i+=e}),d.stderr.on("data",e=>{s+=e});let a=new Promise((e,u)=>{d.on("error",e=>{"ENOENT"===e.code?u(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):u(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),d.on("close",d=>{let a=d??1;0===a||r.allowFailure?e({stdout:i,stderr:s,exitCode:a}):u(new n("COMMAND_FAILED",`${t} exited with code ${a}`,{cmd:t,args:o,stdout:i,stderr:s,exitCode:a}))})});return{child:d,wait:a}}export{fileURLToPath,pathToFileURL}from"node:url";export{default as node_net}from"node:net";export{default as node_fs,promises}from"node:fs";export{default as node_os}from"node:os";export{default as node_path}from"node:path";export{o as asAppError,n as errors_AppError,r as runCmd,u as runCmdBackground,i as runCmdDetached,s as runCmdStreaming,d as whichCmd};
|