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.
- package/README.md +2 -9
- package/dist/src/797.js +1 -1
- package/dist/src/bin.js +5 -5
- package/dist/src/daemon.js +16 -20
- package/package.json +7 -9
- package/skills/agent-device/SKILL.md +3 -6
- package/skills/agent-device/references/permissions.md +3 -15
- package/skills/agent-device/references/snapshot-refs.md +1 -4
- package/dist/bin/axsnapshot +0 -0
- package/ios-runner/AXSnapshot/Package.swift +0 -18
- package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
- package/src/__tests__/cli-close.test.ts +0 -155
- package/src/__tests__/cli-help.test.ts +0 -102
- package/src/bin.ts +0 -3
- package/src/cli.ts +0 -305
- package/src/core/__tests__/capabilities.test.ts +0 -75
- package/src/core/__tests__/dispatch-open.test.ts +0 -25
- package/src/core/__tests__/open-target.test.ts +0 -55
- package/src/core/capabilities.ts +0 -57
- package/src/core/dispatch.ts +0 -382
- package/src/core/open-target.ts +0 -27
- package/src/daemon/__tests__/device-ready.test.ts +0 -52
- package/src/daemon/__tests__/is-predicates.test.ts +0 -68
- package/src/daemon/__tests__/selectors.test.ts +0 -261
- package/src/daemon/__tests__/session-routing.test.ts +0 -108
- package/src/daemon/__tests__/session-selector.test.ts +0 -64
- package/src/daemon/__tests__/session-store.test.ts +0 -142
- package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
- package/src/daemon/action-utils.ts +0 -29
- package/src/daemon/context.ts +0 -48
- package/src/daemon/device-ready.ts +0 -155
- package/src/daemon/handlers/__tests__/find.test.ts +0 -99
- package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
- package/src/daemon/handlers/__tests__/session.test.ts +0 -820
- package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
- package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
- package/src/daemon/handlers/find.ts +0 -324
- package/src/daemon/handlers/interaction.ts +0 -550
- package/src/daemon/handlers/parse-utils.ts +0 -8
- package/src/daemon/handlers/record-trace.ts +0 -154
- package/src/daemon/handlers/session.ts +0 -1137
- package/src/daemon/handlers/snapshot.ts +0 -439
- package/src/daemon/is-predicates.ts +0 -46
- package/src/daemon/selectors.ts +0 -540
- package/src/daemon/session-routing.ts +0 -22
- package/src/daemon/session-selector.ts +0 -39
- package/src/daemon/session-store.ts +0 -296
- package/src/daemon/snapshot-processing.ts +0 -131
- package/src/daemon/types.ts +0 -56
- package/src/daemon-client.ts +0 -272
- package/src/daemon.ts +0 -295
- package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
- package/src/platforms/android/__tests__/index.test.ts +0 -274
- package/src/platforms/android/devices.ts +0 -196
- package/src/platforms/android/index.ts +0 -784
- package/src/platforms/android/ui-hierarchy.ts +0 -312
- package/src/platforms/boot-diagnostics.ts +0 -128
- package/src/platforms/ios/__tests__/index.test.ts +0 -312
- package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
- package/src/platforms/ios/apps.ts +0 -358
- package/src/platforms/ios/ax-snapshot.ts +0 -207
- package/src/platforms/ios/config.ts +0 -28
- package/src/platforms/ios/devicectl.ts +0 -134
- package/src/platforms/ios/devices.ts +0 -100
- package/src/platforms/ios/index.ts +0 -20
- package/src/platforms/ios/runner-client.ts +0 -994
- package/src/platforms/ios/simulator.ts +0 -164
- package/src/utils/__tests__/args.test.ts +0 -239
- package/src/utils/__tests__/daemon-client.test.ts +0 -95
- package/src/utils/__tests__/exec.test.ts +0 -16
- package/src/utils/__tests__/finders.test.ts +0 -34
- package/src/utils/__tests__/keyed-lock.test.ts +0 -55
- package/src/utils/__tests__/process-identity.test.ts +0 -33
- package/src/utils/__tests__/retry.test.ts +0 -44
- package/src/utils/args.ts +0 -239
- package/src/utils/command-schema.ts +0 -622
- package/src/utils/device.ts +0 -84
- package/src/utils/errors.ts +0 -35
- package/src/utils/exec.ts +0 -339
- package/src/utils/finders.ts +0 -101
- package/src/utils/interactive.ts +0 -4
- package/src/utils/interactors.ts +0 -173
- package/src/utils/keyed-lock.ts +0 -14
- package/src/utils/output.ts +0 -204
- package/src/utils/process-identity.ts +0 -100
- package/src/utils/retry.ts +0 -180
- package/src/utils/snapshot.ts +0 -64
- package/src/utils/timeouts.ts +0 -9
- package/src/utils/version.ts +0 -26
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { runCli } from '../cli.ts';
|
|
4
|
-
import { AppError } from '../utils/errors.ts';
|
|
5
|
-
import type { DaemonResponse } from '../daemon-client.ts';
|
|
6
|
-
|
|
7
|
-
class ExitSignal extends Error {
|
|
8
|
-
public readonly code: number;
|
|
9
|
-
|
|
10
|
-
constructor(code: number) {
|
|
11
|
-
super(`EXIT_${code}`);
|
|
12
|
-
this.code = code;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type RunResult = {
|
|
17
|
-
code: number | null;
|
|
18
|
-
stdout: string;
|
|
19
|
-
stderr: string;
|
|
20
|
-
daemonCalls: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
async function runCliCapture(argv: string[]): Promise<RunResult> {
|
|
24
|
-
let daemonCalls = 0;
|
|
25
|
-
let stdout = '';
|
|
26
|
-
let stderr = '';
|
|
27
|
-
let code: number | null = null;
|
|
28
|
-
|
|
29
|
-
const originalExit = process.exit;
|
|
30
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
31
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
32
|
-
|
|
33
|
-
(process as any).exit = ((nextCode?: number) => {
|
|
34
|
-
throw new ExitSignal(nextCode ?? 0);
|
|
35
|
-
}) as typeof process.exit;
|
|
36
|
-
(process.stdout as any).write = ((chunk: unknown) => {
|
|
37
|
-
stdout += String(chunk);
|
|
38
|
-
return true;
|
|
39
|
-
}) as typeof process.stdout.write;
|
|
40
|
-
(process.stderr as any).write = ((chunk: unknown) => {
|
|
41
|
-
stderr += String(chunk);
|
|
42
|
-
return true;
|
|
43
|
-
}) as typeof process.stderr.write;
|
|
44
|
-
|
|
45
|
-
const sendToDaemon = async (): Promise<DaemonResponse> => {
|
|
46
|
-
daemonCalls += 1;
|
|
47
|
-
throw new AppError('COMMAND_FAILED', 'Failed to start daemon', {
|
|
48
|
-
infoPath: '/tmp/daemon.json',
|
|
49
|
-
hint: 'stale daemon info',
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
await runCli(argv, { sendToDaemon });
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (error instanceof ExitSignal) code = error.code;
|
|
57
|
-
else throw error;
|
|
58
|
-
} finally {
|
|
59
|
-
process.exit = originalExit;
|
|
60
|
-
process.stdout.write = originalStdoutWrite;
|
|
61
|
-
process.stderr.write = originalStderrWrite;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { code, stdout, stderr, daemonCalls };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function runCliCaptureWithErrorDetails(
|
|
68
|
-
argv: string[],
|
|
69
|
-
details: Record<string, unknown>,
|
|
70
|
-
message = 'Failed to start daemon',
|
|
71
|
-
): Promise<RunResult> {
|
|
72
|
-
let daemonCalls = 0;
|
|
73
|
-
let stdout = '';
|
|
74
|
-
let stderr = '';
|
|
75
|
-
let code: number | null = null;
|
|
76
|
-
|
|
77
|
-
const originalExit = process.exit;
|
|
78
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
79
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
80
|
-
|
|
81
|
-
(process as any).exit = ((nextCode?: number) => {
|
|
82
|
-
throw new ExitSignal(nextCode ?? 0);
|
|
83
|
-
}) as typeof process.exit;
|
|
84
|
-
(process.stdout as any).write = ((chunk: unknown) => {
|
|
85
|
-
stdout += String(chunk);
|
|
86
|
-
return true;
|
|
87
|
-
}) as typeof process.stdout.write;
|
|
88
|
-
(process.stderr as any).write = ((chunk: unknown) => {
|
|
89
|
-
stderr += String(chunk);
|
|
90
|
-
return true;
|
|
91
|
-
}) as typeof process.stderr.write;
|
|
92
|
-
|
|
93
|
-
const sendToDaemon = async (): Promise<DaemonResponse> => {
|
|
94
|
-
daemonCalls += 1;
|
|
95
|
-
throw new AppError('COMMAND_FAILED', message, details);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
await runCli(argv, { sendToDaemon });
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error instanceof ExitSignal) code = error.code;
|
|
102
|
-
else throw error;
|
|
103
|
-
} finally {
|
|
104
|
-
process.exit = originalExit;
|
|
105
|
-
process.stdout.write = originalStdoutWrite;
|
|
106
|
-
process.stderr.write = originalStderrWrite;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { code, stdout, stderr, daemonCalls };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
test('close treats daemon startup failure as no-op', async () => {
|
|
113
|
-
const result = await runCliCapture(['close']);
|
|
114
|
-
assert.equal(result.code, null);
|
|
115
|
-
assert.equal(result.daemonCalls, 1);
|
|
116
|
-
assert.equal(result.stdout, '');
|
|
117
|
-
assert.equal(result.stderr, '');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('close --json treats daemon startup failure as no-op success', async () => {
|
|
121
|
-
const result = await runCliCapture(['close', '--json']);
|
|
122
|
-
assert.equal(result.code, null);
|
|
123
|
-
assert.equal(result.daemonCalls, 1);
|
|
124
|
-
const payload = JSON.parse(result.stdout);
|
|
125
|
-
assert.equal(payload.success, true);
|
|
126
|
-
assert.equal(payload.data.closed, 'session');
|
|
127
|
-
assert.equal(payload.data.source, 'no-daemon');
|
|
128
|
-
assert.equal(result.stderr, '');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('close treats lock-only daemon startup failure as no-op', async () => {
|
|
132
|
-
const result = await runCliCaptureWithErrorDetails(['close'], {
|
|
133
|
-
lockPath: '/tmp/daemon.lock',
|
|
134
|
-
hint: 'stale daemon lock',
|
|
135
|
-
});
|
|
136
|
-
assert.equal(result.code, null);
|
|
137
|
-
assert.equal(result.daemonCalls, 1);
|
|
138
|
-
assert.equal(result.stdout, '');
|
|
139
|
-
assert.equal(result.stderr, '');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('close treats structured daemon startup failure as no-op without relying on message text', async () => {
|
|
143
|
-
const result = await runCliCaptureWithErrorDetails(
|
|
144
|
-
['close'],
|
|
145
|
-
{
|
|
146
|
-
kind: 'daemon_startup_failed',
|
|
147
|
-
lockPath: '/tmp/daemon.lock',
|
|
148
|
-
},
|
|
149
|
-
'daemon bootstrap failed',
|
|
150
|
-
);
|
|
151
|
-
assert.equal(result.code, null);
|
|
152
|
-
assert.equal(result.daemonCalls, 1);
|
|
153
|
-
assert.equal(result.stdout, '');
|
|
154
|
-
assert.equal(result.stderr, '');
|
|
155
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { runCli } from '../cli.ts';
|
|
4
|
-
import type { DaemonResponse } from '../daemon-client.ts';
|
|
5
|
-
|
|
6
|
-
class ExitSignal extends Error {
|
|
7
|
-
public readonly code: number;
|
|
8
|
-
|
|
9
|
-
constructor(code: number) {
|
|
10
|
-
super(`EXIT_${code}`);
|
|
11
|
-
this.code = code;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type RunResult = {
|
|
16
|
-
code: number | null;
|
|
17
|
-
stdout: string;
|
|
18
|
-
stderr: string;
|
|
19
|
-
daemonCalls: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
async function runCliCapture(argv: string[]): Promise<RunResult> {
|
|
23
|
-
let daemonCalls = 0;
|
|
24
|
-
let stdout = '';
|
|
25
|
-
let stderr = '';
|
|
26
|
-
let code: number | null = null;
|
|
27
|
-
|
|
28
|
-
const originalExit = process.exit;
|
|
29
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
30
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
31
|
-
|
|
32
|
-
(process as any).exit = ((nextCode?: number) => {
|
|
33
|
-
throw new ExitSignal(nextCode ?? 0);
|
|
34
|
-
}) as typeof process.exit;
|
|
35
|
-
(process.stdout as any).write = ((chunk: unknown) => {
|
|
36
|
-
stdout += String(chunk);
|
|
37
|
-
return true;
|
|
38
|
-
}) as typeof process.stdout.write;
|
|
39
|
-
(process.stderr as any).write = ((chunk: unknown) => {
|
|
40
|
-
stderr += String(chunk);
|
|
41
|
-
return true;
|
|
42
|
-
}) as typeof process.stderr.write;
|
|
43
|
-
|
|
44
|
-
const sendToDaemon = async (): Promise<DaemonResponse> => {
|
|
45
|
-
daemonCalls += 1;
|
|
46
|
-
return { ok: true, data: {} };
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
await runCli(argv, { sendToDaemon });
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (error instanceof ExitSignal) code = error.code;
|
|
53
|
-
else throw error;
|
|
54
|
-
} finally {
|
|
55
|
-
process.exit = originalExit;
|
|
56
|
-
process.stdout.write = originalStdoutWrite;
|
|
57
|
-
process.stderr.write = originalStderrWrite;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { code, stdout, stderr, daemonCalls };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
test('help appstate prints command help and skips daemon dispatch', async () => {
|
|
64
|
-
const result = await runCliCapture(['help', 'appstate']);
|
|
65
|
-
assert.equal(result.code, 0);
|
|
66
|
-
assert.equal(result.daemonCalls, 0);
|
|
67
|
-
assert.match(result.stdout, /Show foreground app\/activity/);
|
|
68
|
-
assert.doesNotMatch(result.stdout, /Command flags:/);
|
|
69
|
-
assert.match(result.stdout, /Global flags:/);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('appstate --help prints command help and skips daemon dispatch', async () => {
|
|
73
|
-
const result = await runCliCapture(['appstate', '--help']);
|
|
74
|
-
assert.equal(result.code, 0);
|
|
75
|
-
assert.equal(result.daemonCalls, 0);
|
|
76
|
-
assert.match(result.stdout, /Usage:\n agent-device appstate/);
|
|
77
|
-
assert.match(result.stdout, /Global flags:/);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('help unknown command prints error plus global usage and skips daemon dispatch', async () => {
|
|
81
|
-
const result = await runCliCapture(['help', 'not-a-command']);
|
|
82
|
-
assert.equal(result.code, 1);
|
|
83
|
-
assert.equal(result.daemonCalls, 0);
|
|
84
|
-
assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: not-a-command/);
|
|
85
|
-
assert.match(result.stdout, /Commands:/);
|
|
86
|
-
assert.match(result.stdout, /Flags:/);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test('unknown command --help prints error plus global usage and skips daemon dispatch', async () => {
|
|
90
|
-
const result = await runCliCapture(['not-a-command', '--help']);
|
|
91
|
-
assert.equal(result.code, 1);
|
|
92
|
-
assert.equal(result.daemonCalls, 0);
|
|
93
|
-
assert.match(result.stderr, /Error \(INVALID_ARGS\): Unknown command: not-a-command/);
|
|
94
|
-
assert.match(result.stdout, /Commands:/);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('help rejects multiple positional commands and skips daemon dispatch', async () => {
|
|
98
|
-
const result = await runCliCapture(['help', 'appstate', 'extra']);
|
|
99
|
-
assert.equal(result.code, 1);
|
|
100
|
-
assert.equal(result.daemonCalls, 0);
|
|
101
|
-
assert.match(result.stderr, /Error \(INVALID_ARGS\): help accepts at most one command/);
|
|
102
|
-
});
|
package/src/bin.ts
DELETED
package/src/cli.ts
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { parseArgs, toDaemonFlags, usage, usageForCommand } from './utils/args.ts';
|
|
2
|
-
import { asAppError, AppError } from './utils/errors.ts';
|
|
3
|
-
import { formatSnapshotText, printHumanError, printJson } from './utils/output.ts';
|
|
4
|
-
import { readVersion } from './utils/version.ts';
|
|
5
|
-
import { pathToFileURL } from 'node:url';
|
|
6
|
-
import { sendToDaemon } from './daemon-client.ts';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import os from 'node:os';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
|
|
11
|
-
type CliDeps = {
|
|
12
|
-
sendToDaemon: typeof sendToDaemon;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const DEFAULT_CLI_DEPS: CliDeps = {
|
|
16
|
-
sendToDaemon,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS): Promise<void> {
|
|
20
|
-
const parsed = parseArgs(argv);
|
|
21
|
-
for (const warning of parsed.warnings) {
|
|
22
|
-
process.stderr.write(`Warning: ${warning}\n`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (parsed.flags.version) {
|
|
26
|
-
process.stdout.write(`${readVersion()}\n`);
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const isHelpAlias = parsed.command === 'help';
|
|
31
|
-
const isHelpFlag = parsed.flags.help;
|
|
32
|
-
if (isHelpAlias || isHelpFlag) {
|
|
33
|
-
if (isHelpAlias && parsed.positionals.length > 1) {
|
|
34
|
-
printHumanError(new AppError('INVALID_ARGS', 'help accepts at most one command.'));
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
const helpTarget = isHelpAlias ? parsed.positionals[0] : parsed.command;
|
|
38
|
-
if (!helpTarget) {
|
|
39
|
-
process.stdout.write(`${usage()}\n`);
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
const commandHelp = usageForCommand(helpTarget);
|
|
43
|
-
if (commandHelp) {
|
|
44
|
-
process.stdout.write(commandHelp);
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
printHumanError(new AppError('INVALID_ARGS', `Unknown command: ${helpTarget}`));
|
|
48
|
-
process.stdout.write(`${usage()}\n`);
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!parsed.command) {
|
|
53
|
-
process.stdout.write(`${usage()}\n`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const { command, positionals, flags } = parsed;
|
|
58
|
-
const daemonFlags = toDaemonFlags(flags);
|
|
59
|
-
const sessionName = flags.session ?? process.env.AGENT_DEVICE_SESSION ?? 'default';
|
|
60
|
-
const logTailStopper = flags.verbose && !flags.json ? startDaemonLogTail() : null;
|
|
61
|
-
try {
|
|
62
|
-
if (command === 'session') {
|
|
63
|
-
const sub = positionals[0] ?? 'list';
|
|
64
|
-
if (sub !== 'list') {
|
|
65
|
-
throw new AppError('INVALID_ARGS', 'session only supports list');
|
|
66
|
-
}
|
|
67
|
-
const response = await deps.sendToDaemon({
|
|
68
|
-
session: sessionName,
|
|
69
|
-
command: 'session_list',
|
|
70
|
-
positionals: [],
|
|
71
|
-
flags: daemonFlags,
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) throw new AppError(response.error.code as any, response.error.message);
|
|
74
|
-
if (flags.json) printJson({ success: true, data: response.data ?? {} });
|
|
75
|
-
else process.stdout.write(`${JSON.stringify(response.data ?? {}, null, 2)}\n`);
|
|
76
|
-
if (logTailStopper) logTailStopper();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const response = await deps.sendToDaemon({
|
|
81
|
-
session: sessionName,
|
|
82
|
-
command: command!,
|
|
83
|
-
positionals,
|
|
84
|
-
flags: daemonFlags,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (response.ok) {
|
|
88
|
-
if (flags.json) {
|
|
89
|
-
printJson({ success: true, data: response.data ?? {} });
|
|
90
|
-
if (logTailStopper) logTailStopper();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (command === 'snapshot') {
|
|
94
|
-
process.stdout.write(
|
|
95
|
-
formatSnapshotText((response.data ?? {}) as Record<string, unknown>, {
|
|
96
|
-
raw: flags.snapshotRaw,
|
|
97
|
-
flatten: flags.snapshotInteractiveOnly,
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
if (logTailStopper) logTailStopper();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (command === 'get') {
|
|
104
|
-
const sub = positionals[0];
|
|
105
|
-
if (sub === 'text') {
|
|
106
|
-
const text = (response.data as any)?.text ?? '';
|
|
107
|
-
process.stdout.write(`${text}\n`);
|
|
108
|
-
if (logTailStopper) logTailStopper();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (sub === 'attrs') {
|
|
112
|
-
const node = (response.data as any)?.node ?? {};
|
|
113
|
-
process.stdout.write(`${JSON.stringify(node, null, 2)}\n`);
|
|
114
|
-
if (logTailStopper) logTailStopper();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (command === 'find') {
|
|
119
|
-
const data = response.data as any;
|
|
120
|
-
if (typeof data?.text === 'string') {
|
|
121
|
-
process.stdout.write(`${data.text}\n`);
|
|
122
|
-
if (logTailStopper) logTailStopper();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (typeof data?.found === 'boolean') {
|
|
126
|
-
process.stdout.write(`Found: ${data.found}\n`);
|
|
127
|
-
if (logTailStopper) logTailStopper();
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (data?.node) {
|
|
131
|
-
process.stdout.write(`${JSON.stringify(data.node, null, 2)}\n`);
|
|
132
|
-
if (logTailStopper) logTailStopper();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (command === 'is') {
|
|
137
|
-
const predicate = (response.data as any)?.predicate ?? 'assertion';
|
|
138
|
-
process.stdout.write(`Passed: is ${predicate}\n`);
|
|
139
|
-
if (logTailStopper) logTailStopper();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (command === 'boot') {
|
|
143
|
-
const platform = (response.data as any)?.platform ?? 'unknown';
|
|
144
|
-
const device = (response.data as any)?.device ?? (response.data as any)?.id ?? 'unknown';
|
|
145
|
-
process.stdout.write(`Boot ready: ${device} (${platform})\n`);
|
|
146
|
-
if (logTailStopper) logTailStopper();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
if (command === 'click') {
|
|
150
|
-
const ref = (response.data as any)?.ref ?? '';
|
|
151
|
-
const x = (response.data as any)?.x;
|
|
152
|
-
const y = (response.data as any)?.y;
|
|
153
|
-
if (ref && typeof x === 'number' && typeof y === 'number') {
|
|
154
|
-
process.stdout.write(`Clicked @${ref} (${x}, ${y})\n`);
|
|
155
|
-
}
|
|
156
|
-
if (logTailStopper) logTailStopper();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (response.data && typeof response.data === 'object') {
|
|
160
|
-
const data = response.data as Record<string, unknown>;
|
|
161
|
-
if (command === 'devices') {
|
|
162
|
-
const devices = Array.isArray((data as any).devices) ? (data as any).devices : [];
|
|
163
|
-
const lines = devices.map((d: any) => {
|
|
164
|
-
const name = d?.name ?? d?.id ?? 'unknown';
|
|
165
|
-
const platform = d?.platform ?? 'unknown';
|
|
166
|
-
const kind = d?.kind ? ` ${d.kind}` : '';
|
|
167
|
-
const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
|
|
168
|
-
return `${name} (${platform}${kind})${booted}`;
|
|
169
|
-
});
|
|
170
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
171
|
-
if (logTailStopper) logTailStopper();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (command === 'apps') {
|
|
175
|
-
const apps = Array.isArray((data as any).apps) ? (data as any).apps : [];
|
|
176
|
-
const lines = apps.map((app: any) => {
|
|
177
|
-
if (typeof app === 'string') return app;
|
|
178
|
-
if (app && typeof app === 'object') {
|
|
179
|
-
const bundleId = app.bundleId ?? app.package;
|
|
180
|
-
const name = app.name ?? app.label;
|
|
181
|
-
if (name && bundleId) return `${name} (${bundleId})`;
|
|
182
|
-
if (bundleId) return String(bundleId);
|
|
183
|
-
return JSON.stringify(app);
|
|
184
|
-
}
|
|
185
|
-
return String(app);
|
|
186
|
-
});
|
|
187
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
188
|
-
if (logTailStopper) logTailStopper();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (command === 'appstate') {
|
|
192
|
-
const platform = (data as any)?.platform;
|
|
193
|
-
const appBundleId = (data as any)?.appBundleId;
|
|
194
|
-
const appName = (data as any)?.appName;
|
|
195
|
-
const source = (data as any)?.source;
|
|
196
|
-
const pkg = (data as any)?.package;
|
|
197
|
-
const activity = (data as any)?.activity;
|
|
198
|
-
if (platform === 'ios') {
|
|
199
|
-
process.stdout.write(`Foreground app: ${appName ?? appBundleId ?? 'unknown'}\n`);
|
|
200
|
-
if (appBundleId) process.stdout.write(`Bundle: ${appBundleId}\n`);
|
|
201
|
-
if (source) process.stdout.write(`Source: ${source}\n`);
|
|
202
|
-
if (logTailStopper) logTailStopper();
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (platform === 'android') {
|
|
206
|
-
process.stdout.write(`Foreground app: ${pkg ?? 'unknown'}\n`);
|
|
207
|
-
if (activity) process.stdout.write(`Activity: ${activity}\n`);
|
|
208
|
-
if (logTailStopper) logTailStopper();
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (logTailStopper) logTailStopper();
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
throw new AppError(response.error.code as any, response.error.message, response.error.details);
|
|
218
|
-
} catch (err) {
|
|
219
|
-
const appErr = asAppError(err);
|
|
220
|
-
if (command === 'close' && isDaemonStartupFailure(appErr)) {
|
|
221
|
-
if (flags.json) {
|
|
222
|
-
printJson({ success: true, data: { closed: 'session', source: 'no-daemon' } });
|
|
223
|
-
}
|
|
224
|
-
if (logTailStopper) logTailStopper();
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (flags.json) {
|
|
228
|
-
printJson({
|
|
229
|
-
success: false,
|
|
230
|
-
error: { code: appErr.code, message: appErr.message, details: appErr.details },
|
|
231
|
-
});
|
|
232
|
-
} else {
|
|
233
|
-
printHumanError(appErr);
|
|
234
|
-
if (flags.verbose) {
|
|
235
|
-
try {
|
|
236
|
-
const logPath = path.join(os.homedir(), '.agent-device', 'daemon.log');
|
|
237
|
-
if (fs.existsSync(logPath)) {
|
|
238
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
239
|
-
const lines = content.split('\n');
|
|
240
|
-
const tail = lines.slice(Math.max(0, lines.length - 200)).join('\n');
|
|
241
|
-
if (tail.trim().length > 0) {
|
|
242
|
-
process.stderr.write(`\n[daemon log]\n${tail}\n`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} catch {
|
|
246
|
-
// ignore
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (logTailStopper) logTailStopper();
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function isDaemonStartupFailure(error: AppError): boolean {
|
|
256
|
-
if (error.code !== 'COMMAND_FAILED') return false;
|
|
257
|
-
if (error.details?.kind === 'daemon_startup_failed') return true;
|
|
258
|
-
if (!error.message.toLowerCase().includes('failed to start daemon')) return false;
|
|
259
|
-
return typeof error.details?.infoPath === 'string' || typeof error.details?.lockPath === 'string';
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const isDirectRun = pathToFileURL(process.argv[1] ?? '').href === import.meta.url;
|
|
263
|
-
if (isDirectRun) {
|
|
264
|
-
runCli(process.argv.slice(2)).catch((err) => {
|
|
265
|
-
const appErr = asAppError(err);
|
|
266
|
-
printHumanError(appErr);
|
|
267
|
-
process.exit(1);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function startDaemonLogTail(): (() => void) | null {
|
|
272
|
-
try {
|
|
273
|
-
const logPath = path.join(os.homedir(), '.agent-device', 'daemon.log');
|
|
274
|
-
let offset = 0;
|
|
275
|
-
let stopped = false;
|
|
276
|
-
const interval = setInterval(() => {
|
|
277
|
-
if (stopped) return;
|
|
278
|
-
if (!fs.existsSync(logPath)) return;
|
|
279
|
-
try {
|
|
280
|
-
const stats = fs.statSync(logPath);
|
|
281
|
-
if (stats.size < offset) offset = 0;
|
|
282
|
-
if (stats.size <= offset) return;
|
|
283
|
-
const fd = fs.openSync(logPath, 'r');
|
|
284
|
-
try {
|
|
285
|
-
const buffer = Buffer.alloc(stats.size - offset);
|
|
286
|
-
fs.readSync(fd, buffer, 0, buffer.length, offset);
|
|
287
|
-
offset = stats.size;
|
|
288
|
-
if (buffer.length > 0) {
|
|
289
|
-
process.stdout.write(buffer.toString('utf8'));
|
|
290
|
-
}
|
|
291
|
-
} finally {
|
|
292
|
-
fs.closeSync(fd);
|
|
293
|
-
}
|
|
294
|
-
} catch {
|
|
295
|
-
// Best-effort tailing should not crash CLI flow.
|
|
296
|
-
}
|
|
297
|
-
}, 200);
|
|
298
|
-
return () => {
|
|
299
|
-
stopped = true;
|
|
300
|
-
clearInterval(interval);
|
|
301
|
-
};
|
|
302
|
-
} catch {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { isCommandSupportedOnDevice } from '../capabilities.ts';
|
|
4
|
-
import type { DeviceInfo } from '../../utils/device.ts';
|
|
5
|
-
|
|
6
|
-
const iosSimulator: DeviceInfo = {
|
|
7
|
-
platform: 'ios',
|
|
8
|
-
id: 'sim-1',
|
|
9
|
-
name: 'iPhone',
|
|
10
|
-
kind: 'simulator',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const iosDevice: DeviceInfo = {
|
|
14
|
-
platform: 'ios',
|
|
15
|
-
id: 'dev-1',
|
|
16
|
-
name: 'iPhone',
|
|
17
|
-
kind: 'device',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const androidDevice: DeviceInfo = {
|
|
21
|
-
platform: 'android',
|
|
22
|
-
id: 'and-1',
|
|
23
|
-
name: 'Pixel',
|
|
24
|
-
kind: 'device',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
test('iOS simulator-only commands reject iOS devices and Android', () => {
|
|
28
|
-
for (const cmd of ['alert', 'pinch']) {
|
|
29
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosSimulator), true, `${cmd} on iOS sim`);
|
|
30
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosDevice), false, `${cmd} on iOS device`);
|
|
31
|
-
assert.equal(isCommandSupportedOnDevice(cmd, androidDevice), false, `${cmd} on Android`);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('simulator-only iOS commands with Android support reject iOS devices', () => {
|
|
36
|
-
for (const cmd of ['reinstall', 'record', 'settings', 'swipe']) {
|
|
37
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosSimulator), true, `${cmd} on iOS sim`);
|
|
38
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosDevice), false, `${cmd} on iOS device`);
|
|
39
|
-
assert.equal(isCommandSupportedOnDevice(cmd, androidDevice), true, `${cmd} on Android`);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('core commands support iOS simulator, iOS device, and Android', () => {
|
|
44
|
-
for (const cmd of [
|
|
45
|
-
'app-switcher',
|
|
46
|
-
'apps',
|
|
47
|
-
'back',
|
|
48
|
-
'boot',
|
|
49
|
-
'click',
|
|
50
|
-
'close',
|
|
51
|
-
'fill',
|
|
52
|
-
'find',
|
|
53
|
-
'focus',
|
|
54
|
-
'get',
|
|
55
|
-
'home',
|
|
56
|
-
'long-press',
|
|
57
|
-
'open',
|
|
58
|
-
'press',
|
|
59
|
-
'screenshot',
|
|
60
|
-
'scroll',
|
|
61
|
-
'scrollintoview',
|
|
62
|
-
'snapshot',
|
|
63
|
-
'type',
|
|
64
|
-
'wait',
|
|
65
|
-
]) {
|
|
66
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosSimulator), true, `${cmd} on iOS sim`);
|
|
67
|
-
assert.equal(isCommandSupportedOnDevice(cmd, iosDevice), true, `${cmd} on iOS device`);
|
|
68
|
-
assert.equal(isCommandSupportedOnDevice(cmd, androidDevice), true, `${cmd} on Android`);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('unknown commands default to supported', () => {
|
|
73
|
-
assert.equal(isCommandSupportedOnDevice('some-future-cmd', iosSimulator), true);
|
|
74
|
-
assert.equal(isCommandSupportedOnDevice('some-future-cmd', androidDevice), true);
|
|
75
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { dispatchCommand } from '../dispatch.ts';
|
|
4
|
-
import { AppError } from '../../utils/errors.ts';
|
|
5
|
-
import type { DeviceInfo } from '../../utils/device.ts';
|
|
6
|
-
|
|
7
|
-
test('dispatch open rejects URL as first argument when second URL is provided', async () => {
|
|
8
|
-
const device: DeviceInfo = {
|
|
9
|
-
platform: 'ios',
|
|
10
|
-
id: 'sim-1',
|
|
11
|
-
name: 'iPhone 15',
|
|
12
|
-
kind: 'simulator',
|
|
13
|
-
booted: true,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
await assert.rejects(
|
|
17
|
-
() => dispatchCommand(device, 'open', ['myapp://first', 'myapp://second']),
|
|
18
|
-
(error: unknown) => {
|
|
19
|
-
assert.equal(error instanceof AppError, true);
|
|
20
|
-
assert.equal((error as AppError).code, 'INVALID_ARGS');
|
|
21
|
-
assert.match((error as AppError).message, /requires an app target as the first argument/i);
|
|
22
|
-
return true;
|
|
23
|
-
},
|
|
24
|
-
);
|
|
25
|
-
});
|