agent-device 0.4.0 → 0.4.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 (52) hide show
  1. package/README.md +20 -12
  2. package/dist/src/797.js +1 -0
  3. package/dist/src/bin.js +40 -29
  4. package/dist/src/daemon.js +21 -17
  5. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +8 -2
  6. package/package.json +2 -2
  7. package/skills/agent-device/SKILL.md +23 -14
  8. package/skills/agent-device/references/permissions.md +7 -2
  9. package/skills/agent-device/references/session-management.md +5 -1
  10. package/src/__tests__/cli-close.test.ts +155 -0
  11. package/src/__tests__/cli-help.test.ts +102 -0
  12. package/src/cli.ts +68 -22
  13. package/src/core/__tests__/capabilities.test.ts +2 -1
  14. package/src/core/__tests__/dispatch-open.test.ts +25 -0
  15. package/src/core/__tests__/open-target.test.ts +40 -1
  16. package/src/core/capabilities.ts +1 -1
  17. package/src/core/dispatch.ts +22 -0
  18. package/src/core/open-target.ts +14 -0
  19. package/src/daemon/__tests__/device-ready.test.ts +52 -0
  20. package/src/daemon/__tests__/session-store.test.ts +23 -0
  21. package/src/daemon/device-ready.ts +146 -4
  22. package/src/daemon/handlers/__tests__/session.test.ts +477 -0
  23. package/src/daemon/handlers/session.ts +198 -93
  24. package/src/daemon/handlers/snapshot.ts +210 -185
  25. package/src/daemon/session-store.ts +16 -6
  26. package/src/daemon/types.ts +2 -1
  27. package/src/daemon-client.ts +138 -17
  28. package/src/daemon.ts +99 -9
  29. package/src/platforms/android/__tests__/index.test.ts +118 -1
  30. package/src/platforms/android/index.ts +77 -47
  31. package/src/platforms/ios/__tests__/index.test.ts +292 -4
  32. package/src/platforms/ios/__tests__/runner-client.test.ts +42 -0
  33. package/src/platforms/ios/apps.ts +358 -0
  34. package/src/platforms/ios/config.ts +28 -0
  35. package/src/platforms/ios/devicectl.ts +134 -0
  36. package/src/platforms/ios/devices.ts +15 -2
  37. package/src/platforms/ios/index.ts +20 -455
  38. package/src/platforms/ios/runner-client.ts +171 -69
  39. package/src/platforms/ios/simulator.ts +164 -0
  40. package/src/utils/__tests__/args.test.ts +66 -2
  41. package/src/utils/__tests__/daemon-client.test.ts +95 -0
  42. package/src/utils/__tests__/keyed-lock.test.ts +55 -0
  43. package/src/utils/__tests__/process-identity.test.ts +33 -0
  44. package/src/utils/args.ts +37 -1
  45. package/src/utils/command-schema.ts +58 -27
  46. package/src/utils/interactors.ts +2 -2
  47. package/src/utils/keyed-lock.ts +14 -0
  48. package/src/utils/process-identity.ts +100 -0
  49. package/src/utils/timeouts.ts +9 -0
  50. package/dist/src/274.js +0 -1
  51. package/src/daemon/__tests__/app-state.test.ts +0 -138
  52. package/src/daemon/app-state.ts +0 -65
@@ -1,6 +1,6 @@
1
1
  import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts';
2
2
  import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
3
- import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts';
3
+ import { runIosRunnerCommand, stopIosRunnerSession } from '../../platforms/ios/runner-client.ts';
4
4
  import { snapshotAndroid } from '../../platforms/android/index.ts';
5
5
  import {
6
6
  attachRefs,
@@ -78,45 +78,47 @@ export async function handleSnapshotCommands(params: {
78
78
  }
79
79
  snapshotScope = resolved;
80
80
  }
81
- const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
82
- ...contextFromFlags(
83
- logPath,
84
- { ...req.flags, snapshotScope },
85
- appBundleId,
86
- session?.trace?.outPath,
87
- ),
88
- })) as {
89
- nodes?: RawSnapshotNode[];
90
- truncated?: boolean;
91
- backend?: 'ax' | 'xctest' | 'android';
92
- };
93
- const rawNodes = data?.nodes ?? [];
94
- const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
95
- const snapshot: SnapshotState = {
96
- nodes,
97
- truncated: data?.truncated,
98
- createdAt: Date.now(),
99
- backend: data?.backend,
100
- };
101
- const nextSession: SessionState = session
102
- ? { ...session, snapshot }
103
- : { name: sessionName, device, createdAt: Date.now(), appBundleId, snapshot, actions: [] };
104
- recordIfSession(sessionStore, nextSession, req, {
105
- nodes: nodes.length,
106
- truncated: data?.truncated ?? false,
107
- });
108
- sessionStore.set(sessionName, nextSession);
109
- return {
110
- ok: true,
111
- data: {
81
+ return await withSessionlessRunnerCleanup(session, device, async () => {
82
+ const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
83
+ ...contextFromFlags(
84
+ logPath,
85
+ { ...req.flags, snapshotScope },
86
+ appBundleId,
87
+ session?.trace?.outPath,
88
+ ),
89
+ })) as {
90
+ nodes?: RawSnapshotNode[];
91
+ truncated?: boolean;
92
+ backend?: 'ax' | 'xctest' | 'android';
93
+ };
94
+ const rawNodes = data?.nodes ?? [];
95
+ const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
96
+ const snapshot: SnapshotState = {
112
97
  nodes,
98
+ truncated: data?.truncated,
99
+ createdAt: Date.now(),
100
+ backend: data?.backend,
101
+ };
102
+ const nextSession: SessionState = session
103
+ ? { ...session, snapshot }
104
+ : { name: sessionName, device, createdAt: Date.now(), appBundleId, snapshot, actions: [] };
105
+ recordIfSession(sessionStore, nextSession, req, {
106
+ nodes: nodes.length,
113
107
  truncated: data?.truncated ?? false,
114
- appName: nextSession.appBundleId
115
- ? (nextSession.appName ?? nextSession.appBundleId)
116
- : undefined,
117
- appBundleId: nextSession.appBundleId,
118
- },
119
- };
108
+ });
109
+ sessionStore.set(sessionName, nextSession);
110
+ return {
111
+ ok: true,
112
+ data: {
113
+ nodes,
114
+ truncated: data?.truncated ?? false,
115
+ appName: nextSession.appBundleId
116
+ ? (nextSession.appName ?? nextSession.appBundleId)
117
+ : undefined,
118
+ appBundleId: nextSession.appBundleId,
119
+ },
120
+ };
121
+ });
120
122
  }
121
123
 
122
124
  if (command === 'wait') {
@@ -140,125 +142,127 @@ export async function handleSnapshotCommands(params: {
140
142
  error: { code: 'UNSUPPORTED_OPERATION', message: 'wait is not supported on this device' },
141
143
  };
142
144
  }
143
- let text: string;
144
- let timeoutMs: number | null;
145
- if (parsed.kind === 'selector') {
146
- const timeout = parsed.timeoutMs ?? DEFAULT_TIMEOUT_MS;
147
- const start = Date.now();
148
- while (Date.now() - start < timeout) {
149
- const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
150
- ...contextFromFlags(
151
- logPath,
152
- {
153
- ...req.flags,
154
- snapshotInteractiveOnly: false,
155
- snapshotCompact: false,
156
- },
157
- session?.appBundleId,
158
- session?.trace?.outPath,
159
- ),
160
- })) as {
161
- nodes?: RawSnapshotNode[];
162
- truncated?: boolean;
163
- backend?: 'ax' | 'xctest' | 'android';
164
- };
165
- const rawNodes = data?.nodes ?? [];
166
- const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
167
- if (session) {
168
- session.snapshot = {
169
- nodes,
170
- truncated: data?.truncated,
171
- createdAt: Date.now(),
172
- backend: data?.backend,
145
+ return await withSessionlessRunnerCleanup(session, device, async () => {
146
+ let text: string;
147
+ let timeoutMs: number | null;
148
+ if (parsed.kind === 'selector') {
149
+ const timeout = parsed.timeoutMs ?? DEFAULT_TIMEOUT_MS;
150
+ const start = Date.now();
151
+ while (Date.now() - start < timeout) {
152
+ const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
153
+ ...contextFromFlags(
154
+ logPath,
155
+ {
156
+ ...req.flags,
157
+ snapshotInteractiveOnly: false,
158
+ snapshotCompact: false,
159
+ },
160
+ session?.appBundleId,
161
+ session?.trace?.outPath,
162
+ ),
163
+ })) as {
164
+ nodes?: RawSnapshotNode[];
165
+ truncated?: boolean;
166
+ backend?: 'ax' | 'xctest' | 'android';
173
167
  };
174
- sessionStore.set(sessionName, session);
175
- }
176
- const match = findSelectorChainMatch(nodes, parsed.selector, { platform: device.platform });
177
- if (match) {
178
- recordIfSession(sessionStore, session, req, {
179
- selector: match.selector.raw,
180
- waitedMs: Date.now() - start,
181
- });
182
- return {
183
- ok: true,
184
- data: {
168
+ const rawNodes = data?.nodes ?? [];
169
+ const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
170
+ if (session) {
171
+ session.snapshot = {
172
+ nodes,
173
+ truncated: data?.truncated,
174
+ createdAt: Date.now(),
175
+ backend: data?.backend,
176
+ };
177
+ sessionStore.set(sessionName, session);
178
+ }
179
+ const match = findSelectorChainMatch(nodes, parsed.selector, { platform: device.platform });
180
+ if (match) {
181
+ recordIfSession(sessionStore, session, req, {
185
182
  selector: match.selector.raw,
186
183
  waitedMs: Date.now() - start,
187
- },
188
- };
184
+ });
185
+ return {
186
+ ok: true,
187
+ data: {
188
+ selector: match.selector.raw,
189
+ waitedMs: Date.now() - start,
190
+ },
191
+ };
192
+ }
193
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
189
194
  }
190
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
191
- }
192
- return {
193
- ok: false,
194
- error: {
195
- code: 'COMMAND_FAILED',
196
- message: `wait timed out for selector: ${parsed.selectorExpression}`,
197
- },
198
- };
199
- } else if (parsed.kind === 'ref') {
200
- if (!session?.snapshot) {
201
- return {
202
- ok: false,
203
- error: {
204
- code: 'INVALID_ARGS',
205
- message: 'Ref wait requires an existing snapshot in session.',
206
- },
207
- };
208
- }
209
- const ref = normalizeRef(parsed.rawRef);
210
- if (!ref) {
211
- return {
212
- ok: false,
213
- error: { code: 'INVALID_ARGS', message: `Invalid ref: ${parsed.rawRef}` },
214
- };
215
- }
216
- const node = findNodeByRef(session.snapshot.nodes, ref);
217
- const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
218
- if (!resolved) {
219
195
  return {
220
196
  ok: false,
221
197
  error: {
222
198
  code: 'COMMAND_FAILED',
223
- message: `Ref ${parsed.rawRef} not found or has no label`,
199
+ message: `wait timed out for selector: ${parsed.selectorExpression}`,
224
200
  },
225
201
  };
226
- }
227
- text = resolved;
228
- timeoutMs = parsed.timeoutMs;
229
- } else {
230
- text = parsed.text;
231
- timeoutMs = parsed.timeoutMs;
232
- }
233
- if (!text) {
234
- return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } };
235
- }
236
- const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
237
- const start = Date.now();
238
- while (Date.now() - start < timeout) {
239
- if (device.platform === 'ios') {
240
- const result = (await runIosRunnerCommand(
241
- device,
242
- { command: 'findText', text, appBundleId: session?.appBundleId },
243
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
244
- )) as { found?: boolean };
245
- if (result?.found) {
246
- recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
247
- return { ok: true, data: { text, waitedMs: Date.now() - start } };
202
+ } else if (parsed.kind === 'ref') {
203
+ if (!session?.snapshot) {
204
+ return {
205
+ ok: false,
206
+ error: {
207
+ code: 'INVALID_ARGS',
208
+ message: 'Ref wait requires an existing snapshot in session.',
209
+ },
210
+ };
211
+ }
212
+ const ref = normalizeRef(parsed.rawRef);
213
+ if (!ref) {
214
+ return {
215
+ ok: false,
216
+ error: { code: 'INVALID_ARGS', message: `Invalid ref: ${parsed.rawRef}` },
217
+ };
218
+ }
219
+ const node = findNodeByRef(session.snapshot.nodes, ref);
220
+ const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
221
+ if (!resolved) {
222
+ return {
223
+ ok: false,
224
+ error: {
225
+ code: 'COMMAND_FAILED',
226
+ message: `Ref ${parsed.rawRef} not found or has no label`,
227
+ },
228
+ };
248
229
  }
249
- } else if (device.platform === 'android') {
250
- const androidResult = await snapshotAndroid(device, { scope: text });
251
- if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) {
252
- recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
253
- return { ok: true, data: { text, waitedMs: Date.now() - start } };
230
+ text = resolved;
231
+ timeoutMs = parsed.timeoutMs;
232
+ } else {
233
+ text = parsed.text;
234
+ timeoutMs = parsed.timeoutMs;
235
+ }
236
+ if (!text) {
237
+ return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } };
238
+ }
239
+ const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
240
+ const start = Date.now();
241
+ while (Date.now() - start < timeout) {
242
+ if (device.platform === 'ios') {
243
+ const result = (await runIosRunnerCommand(
244
+ device,
245
+ { command: 'findText', text, appBundleId: session?.appBundleId },
246
+ { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
247
+ )) as { found?: boolean };
248
+ if (result?.found) {
249
+ recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
250
+ return { ok: true, data: { text, waitedMs: Date.now() - start } };
251
+ }
252
+ } else if (device.platform === 'android') {
253
+ const androidResult = await snapshotAndroid(device, { scope: text });
254
+ if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) {
255
+ recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
256
+ return { ok: true, data: { text, waitedMs: Date.now() - start } };
257
+ }
254
258
  }
259
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
255
260
  }
256
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
257
- }
258
- return {
259
- ok: false,
260
- error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` },
261
- };
261
+ return {
262
+ ok: false,
263
+ error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` },
264
+ };
265
+ });
262
266
  }
263
267
 
264
268
  if (command === 'alert') {
@@ -273,37 +277,39 @@ export async function handleSnapshotCommands(params: {
273
277
  },
274
278
  };
275
279
  }
276
- if (action === 'wait') {
277
- const timeout = parseTimeout(req.positionals?.[1]) ?? DEFAULT_TIMEOUT_MS;
278
- const start = Date.now();
279
- while (Date.now() - start < timeout) {
280
- try {
281
- const data = await runIosRunnerCommand(
282
- device,
283
- { command: 'alert', action: 'get', appBundleId: session?.appBundleId },
284
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
285
- );
286
- recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
287
- return { ok: true, data };
288
- } catch {
289
- // keep waiting
280
+ return await withSessionlessRunnerCleanup(session, device, async () => {
281
+ if (action === 'wait') {
282
+ const timeout = parseTimeout(req.positionals?.[1]) ?? DEFAULT_TIMEOUT_MS;
283
+ const start = Date.now();
284
+ while (Date.now() - start < timeout) {
285
+ try {
286
+ const data = await runIosRunnerCommand(
287
+ device,
288
+ { command: 'alert', action: 'get', appBundleId: session?.appBundleId },
289
+ { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
290
+ );
291
+ recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
292
+ return { ok: true, data };
293
+ } catch {
294
+ // keep waiting
295
+ }
296
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
290
297
  }
291
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
298
+ return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
292
299
  }
293
- return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
294
- }
295
- const data = await runIosRunnerCommand(
296
- device,
297
- {
298
- command: 'alert',
299
- action:
300
- action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get',
301
- appBundleId: session?.appBundleId,
302
- },
303
- { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
304
- );
305
- recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
306
- return { ok: true, data };
300
+ const data = await runIosRunnerCommand(
301
+ device,
302
+ {
303
+ command: 'alert',
304
+ action:
305
+ action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get',
306
+ appBundleId: session?.appBundleId,
307
+ },
308
+ { verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
309
+ );
310
+ recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
311
+ return { ok: true, data };
312
+ });
307
313
  }
308
314
 
309
315
  if (command === 'settings') {
@@ -328,18 +334,20 @@ export async function handleSnapshotCommands(params: {
328
334
  },
329
335
  };
330
336
  }
331
- const appBundleId = session?.appBundleId;
332
- const data = await dispatchCommand(
333
- device,
334
- 'settings',
335
- [setting, state, appBundleId ?? ''],
336
- req.flags?.out,
337
- {
338
- ...contextFromFlags(logPath, req.flags, appBundleId, session?.trace?.outPath),
339
- },
340
- );
341
- recordIfSession(sessionStore, session, req, data ?? { setting, state });
342
- return { ok: true, data: data ?? { setting, state } };
337
+ return await withSessionlessRunnerCleanup(session, device, async () => {
338
+ const appBundleId = session?.appBundleId;
339
+ const data = await dispatchCommand(
340
+ device,
341
+ 'settings',
342
+ [setting, state, appBundleId ?? ''],
343
+ req.flags?.out,
344
+ {
345
+ ...contextFromFlags(logPath, req.flags, appBundleId, session?.trace?.outPath),
346
+ },
347
+ );
348
+ recordIfSession(sessionStore, session, req, data ?? { setting, state });
349
+ return { ok: true, data: data ?? { setting, state } };
350
+ });
343
351
  }
344
352
 
345
353
  return null;
@@ -398,6 +406,23 @@ async function resolveSessionDevice(
398
406
  return { session, device };
399
407
  }
400
408
 
409
+ async function withSessionlessRunnerCleanup<T>(
410
+ session: SessionState | undefined,
411
+ device: SessionState['device'],
412
+ task: () => Promise<T>,
413
+ ): Promise<T> {
414
+ const shouldCleanupSessionlessIosRunner = !session && device.platform === 'ios';
415
+ try {
416
+ return await task();
417
+ } finally {
418
+ // Sessionless iOS commands intentionally stop the runner to avoid leaked xcodebuild processes.
419
+ // For multi-command flows, keep an active session via `open` so the runner can be reused.
420
+ if (shouldCleanupSessionlessIosRunner) {
421
+ await stopIosRunnerSession(device.id);
422
+ }
423
+ }
424
+ }
425
+
401
426
  function recordIfSession(
402
427
  sessionStore: SessionStore,
403
428
  session: SessionState | undefined,
@@ -49,6 +49,9 @@ export class SessionStore {
49
49
  if (entry.flags?.noRecord) return;
50
50
  if (entry.flags?.saveScript) {
51
51
  session.recordSession = true;
52
+ if (typeof entry.flags.saveScript === 'string') {
53
+ session.saveScriptPath = SessionStore.expandHome(entry.flags.saveScript);
54
+ }
52
55
  }
53
56
  session.actions.push({
54
57
  ts: Date.now(),
@@ -62,10 +65,9 @@ export class SessionStore {
62
65
  writeSessionLog(session: SessionState): void {
63
66
  try {
64
67
  if (!session.recordSession) return;
65
- if (!fs.existsSync(this.sessionsDir)) fs.mkdirSync(this.sessionsDir, { recursive: true });
66
- const safeName = session.name.replace(/[^a-zA-Z0-9._-]/g, '_');
67
- const timestamp = new Date(session.createdAt).toISOString().replace(/[:.]/g, '-');
68
- const scriptPath = path.join(this.sessionsDir, `${safeName}-${timestamp}.ad`);
68
+ const scriptPath = this.resolveScriptPath(session);
69
+ const scriptDir = path.dirname(scriptPath);
70
+ if (!fs.existsSync(scriptDir)) fs.mkdirSync(scriptDir, { recursive: true });
69
71
  const script = formatScript(session, this.buildOptimizedActions(session));
70
72
  fs.writeFileSync(scriptPath, script);
71
73
  } catch {
@@ -86,6 +88,16 @@ export class SessionStore {
86
88
  return path.resolve(filePath);
87
89
  }
88
90
 
91
+ private resolveScriptPath(session: SessionState): string {
92
+ if (session.saveScriptPath) {
93
+ return SessionStore.expandHome(session.saveScriptPath);
94
+ }
95
+ if (!fs.existsSync(this.sessionsDir)) fs.mkdirSync(this.sessionsDir, { recursive: true });
96
+ const safeName = session.name.replace(/[^a-zA-Z0-9._-]/g, '_');
97
+ const timestamp = new Date(session.createdAt).toISOString().replace(/[:.]/g, '-');
98
+ return path.join(this.sessionsDir, `${safeName}-${timestamp}.ad`);
99
+ }
100
+
89
101
  private buildOptimizedActions(session: SessionState): SessionAction[] {
90
102
  const optimized: SessionAction[] = [];
91
103
  for (const action of session.actions) {
@@ -165,7 +177,6 @@ function sanitizeFlags(flags: CommandFlags | undefined): SessionAction['flags']
165
177
  snapshotScope,
166
178
  snapshotRaw,
167
179
  snapshotBackend,
168
- appsMetadata,
169
180
  relaunch,
170
181
  saveScript,
171
182
  noRecord,
@@ -183,7 +194,6 @@ function sanitizeFlags(flags: CommandFlags | undefined): SessionAction['flags']
183
194
  snapshotScope,
184
195
  snapshotRaw,
185
196
  snapshotBackend,
186
- appsMetadata,
187
197
  relaunch,
188
198
  saveScript,
189
199
  noRecord,
@@ -27,6 +27,7 @@ export type SessionState = {
27
27
  startedAt: number;
28
28
  };
29
29
  recordSession?: boolean;
30
+ saveScriptPath?: string;
30
31
  actions: SessionAction[];
31
32
  recording?: {
32
33
  platform: 'ios' | 'android';
@@ -48,7 +49,7 @@ export type SessionAction = {
48
49
  snapshotScope?: string;
49
50
  snapshotRaw?: boolean;
50
51
  snapshotBackend?: 'ax' | 'xctest';
51
- saveScript?: boolean;
52
+ saveScript?: boolean | string;
52
53
  noRecord?: boolean;
53
54
  };
54
55
  result?: Record<string, unknown>;