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.
Files changed (34) hide show
  1. package/README.md +26 -2
  2. package/dist/src/274.js +1 -0
  3. package/dist/src/bin.js +27 -22
  4. package/dist/src/daemon.js +15 -10
  5. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +4 -2
  6. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +149 -0
  7. package/package.json +2 -2
  8. package/skills/agent-device/SKILL.md +8 -1
  9. package/src/cli.ts +13 -0
  10. package/src/core/__tests__/capabilities.test.ts +2 -0
  11. package/src/core/capabilities.ts +2 -0
  12. package/src/daemon/handlers/__tests__/replay-heal.test.ts +5 -0
  13. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +219 -0
  14. package/src/daemon/handlers/__tests__/session.test.ts +122 -0
  15. package/src/daemon/handlers/find.ts +23 -3
  16. package/src/daemon/handlers/session.ts +175 -10
  17. package/src/daemon-client.ts +1 -24
  18. package/src/daemon.ts +1 -24
  19. package/src/platforms/__tests__/boot-diagnostics.test.ts +59 -0
  20. package/src/platforms/android/__tests__/index.test.ts +17 -0
  21. package/src/platforms/android/devices.ts +167 -42
  22. package/src/platforms/android/index.ts +101 -14
  23. package/src/platforms/boot-diagnostics.ts +128 -0
  24. package/src/platforms/ios/index.ts +161 -2
  25. package/src/platforms/ios/runner-client.ts +19 -1
  26. package/src/utils/__tests__/exec.test.ts +16 -0
  27. package/src/utils/__tests__/finders.test.ts +34 -0
  28. package/src/utils/__tests__/retry.test.ts +44 -0
  29. package/src/utils/args.ts +9 -1
  30. package/src/utils/exec.ts +39 -0
  31. package/src/utils/finders.ts +27 -9
  32. package/src/utils/retry.ts +143 -13
  33. package/src/utils/version.ts +26 -0
  34. package/dist/src/861.js +0 -1
@@ -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 async function withRetry<T>(
19
- fn: () => Promise<T>,
20
- options: RetryOptions = {},
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 attempts = options.attempts ?? defaultOptions.attempts;
23
- const baseDelayMs = options.baseDelayMs ?? defaultOptions.baseDelayMs;
24
- const maxDelayMs = options.maxDelayMs ?? defaultOptions.maxDelayMs;
25
- const jitter = options.jitter ?? defaultOptions.jitter;
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 <= attempts; attempt += 1) {
103
+ for (let attempt = 1; attempt <= merged.maxAttempts; attempt += 1) {
104
+ if (options.deadline?.isExpired() && attempt > 1) break;
28
105
  try {
29
- return await fn();
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
- if (attempt >= attempts) break;
33
- if (options.shouldRetry && !options.shouldRetry(err, attempt)) break;
34
- const delay = computeDelay(baseDelayMs, maxDelayMs, jitter, attempt);
35
- await sleep(delay);
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};