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.
- package/README.md +20 -12
- package/dist/src/797.js +1 -0
- package/dist/src/bin.js +40 -29
- package/dist/src/daemon.js +21 -17
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +8 -2
- package/package.json +2 -2
- package/skills/agent-device/SKILL.md +23 -14
- package/skills/agent-device/references/permissions.md +7 -2
- package/skills/agent-device/references/session-management.md +5 -1
- package/src/__tests__/cli-close.test.ts +155 -0
- package/src/__tests__/cli-help.test.ts +102 -0
- package/src/cli.ts +68 -22
- package/src/core/__tests__/capabilities.test.ts +2 -1
- package/src/core/__tests__/dispatch-open.test.ts +25 -0
- package/src/core/__tests__/open-target.test.ts +40 -1
- package/src/core/capabilities.ts +1 -1
- package/src/core/dispatch.ts +22 -0
- package/src/core/open-target.ts +14 -0
- package/src/daemon/__tests__/device-ready.test.ts +52 -0
- package/src/daemon/__tests__/session-store.test.ts +23 -0
- package/src/daemon/device-ready.ts +146 -4
- package/src/daemon/handlers/__tests__/session.test.ts +477 -0
- package/src/daemon/handlers/session.ts +198 -93
- package/src/daemon/handlers/snapshot.ts +210 -185
- package/src/daemon/session-store.ts +16 -6
- package/src/daemon/types.ts +2 -1
- package/src/daemon-client.ts +138 -17
- package/src/daemon.ts +99 -9
- package/src/platforms/android/__tests__/index.test.ts +118 -1
- package/src/platforms/android/index.ts +77 -47
- package/src/platforms/ios/__tests__/index.test.ts +292 -4
- package/src/platforms/ios/__tests__/runner-client.test.ts +42 -0
- package/src/platforms/ios/apps.ts +358 -0
- package/src/platforms/ios/config.ts +28 -0
- package/src/platforms/ios/devicectl.ts +134 -0
- package/src/platforms/ios/devices.ts +15 -2
- package/src/platforms/ios/index.ts +20 -455
- package/src/platforms/ios/runner-client.ts +171 -69
- package/src/platforms/ios/simulator.ts +164 -0
- package/src/utils/__tests__/args.test.ts +66 -2
- package/src/utils/__tests__/daemon-client.test.ts +95 -0
- package/src/utils/__tests__/keyed-lock.test.ts +55 -0
- package/src/utils/__tests__/process-identity.test.ts +33 -0
- package/src/utils/args.ts +37 -1
- package/src/utils/command-schema.ts +58 -27
- package/src/utils/interactors.ts +2 -2
- package/src/utils/keyed-lock.ts +14 -0
- package/src/utils/process-identity.ts +100 -0
- package/src/utils/timeouts.ts +9 -0
- package/dist/src/274.js +0 -1
- package/src/daemon/__tests__/app-state.test.ts +0 -138
- 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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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: `
|
|
199
|
+
message: `wait timed out for selector: ${parsed.selectorExpression}`,
|
|
224
200
|
},
|
|
225
201
|
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
298
|
+
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
|
|
292
299
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
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,
|
package/src/daemon/types.ts
CHANGED
|
@@ -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>;
|