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.
Files changed (91) hide show
  1. package/README.md +2 -9
  2. package/dist/src/797.js +1 -1
  3. package/dist/src/bin.js +5 -5
  4. package/dist/src/daemon.js +16 -20
  5. package/package.json +7 -9
  6. package/skills/agent-device/SKILL.md +3 -6
  7. package/skills/agent-device/references/permissions.md +3 -15
  8. package/skills/agent-device/references/snapshot-refs.md +1 -4
  9. package/dist/bin/axsnapshot +0 -0
  10. package/ios-runner/AXSnapshot/Package.swift +0 -18
  11. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  12. package/src/__tests__/cli-close.test.ts +0 -155
  13. package/src/__tests__/cli-help.test.ts +0 -102
  14. package/src/bin.ts +0 -3
  15. package/src/cli.ts +0 -305
  16. package/src/core/__tests__/capabilities.test.ts +0 -75
  17. package/src/core/__tests__/dispatch-open.test.ts +0 -25
  18. package/src/core/__tests__/open-target.test.ts +0 -55
  19. package/src/core/capabilities.ts +0 -57
  20. package/src/core/dispatch.ts +0 -382
  21. package/src/core/open-target.ts +0 -27
  22. package/src/daemon/__tests__/device-ready.test.ts +0 -52
  23. package/src/daemon/__tests__/is-predicates.test.ts +0 -68
  24. package/src/daemon/__tests__/selectors.test.ts +0 -261
  25. package/src/daemon/__tests__/session-routing.test.ts +0 -108
  26. package/src/daemon/__tests__/session-selector.test.ts +0 -64
  27. package/src/daemon/__tests__/session-store.test.ts +0 -142
  28. package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
  29. package/src/daemon/action-utils.ts +0 -29
  30. package/src/daemon/context.ts +0 -48
  31. package/src/daemon/device-ready.ts +0 -155
  32. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  33. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  34. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  35. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  36. package/src/daemon/handlers/__tests__/session.test.ts +0 -820
  37. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  38. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  39. package/src/daemon/handlers/find.ts +0 -324
  40. package/src/daemon/handlers/interaction.ts +0 -550
  41. package/src/daemon/handlers/parse-utils.ts +0 -8
  42. package/src/daemon/handlers/record-trace.ts +0 -154
  43. package/src/daemon/handlers/session.ts +0 -1137
  44. package/src/daemon/handlers/snapshot.ts +0 -439
  45. package/src/daemon/is-predicates.ts +0 -46
  46. package/src/daemon/selectors.ts +0 -540
  47. package/src/daemon/session-routing.ts +0 -22
  48. package/src/daemon/session-selector.ts +0 -39
  49. package/src/daemon/session-store.ts +0 -296
  50. package/src/daemon/snapshot-processing.ts +0 -131
  51. package/src/daemon/types.ts +0 -56
  52. package/src/daemon-client.ts +0 -272
  53. package/src/daemon.ts +0 -295
  54. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  55. package/src/platforms/android/__tests__/index.test.ts +0 -274
  56. package/src/platforms/android/devices.ts +0 -196
  57. package/src/platforms/android/index.ts +0 -784
  58. package/src/platforms/android/ui-hierarchy.ts +0 -312
  59. package/src/platforms/boot-diagnostics.ts +0 -128
  60. package/src/platforms/ios/__tests__/index.test.ts +0 -312
  61. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
  62. package/src/platforms/ios/apps.ts +0 -358
  63. package/src/platforms/ios/ax-snapshot.ts +0 -207
  64. package/src/platforms/ios/config.ts +0 -28
  65. package/src/platforms/ios/devicectl.ts +0 -134
  66. package/src/platforms/ios/devices.ts +0 -100
  67. package/src/platforms/ios/index.ts +0 -20
  68. package/src/platforms/ios/runner-client.ts +0 -994
  69. package/src/platforms/ios/simulator.ts +0 -164
  70. package/src/utils/__tests__/args.test.ts +0 -239
  71. package/src/utils/__tests__/daemon-client.test.ts +0 -95
  72. package/src/utils/__tests__/exec.test.ts +0 -16
  73. package/src/utils/__tests__/finders.test.ts +0 -34
  74. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  75. package/src/utils/__tests__/process-identity.test.ts +0 -33
  76. package/src/utils/__tests__/retry.test.ts +0 -44
  77. package/src/utils/args.ts +0 -239
  78. package/src/utils/command-schema.ts +0 -622
  79. package/src/utils/device.ts +0 -84
  80. package/src/utils/errors.ts +0 -35
  81. package/src/utils/exec.ts +0 -339
  82. package/src/utils/finders.ts +0 -101
  83. package/src/utils/interactive.ts +0 -4
  84. package/src/utils/interactors.ts +0 -173
  85. package/src/utils/keyed-lock.ts +0 -14
  86. package/src/utils/output.ts +0 -204
  87. package/src/utils/process-identity.ts +0 -100
  88. package/src/utils/retry.ts +0 -180
  89. package/src/utils/snapshot.ts +0 -64
  90. package/src/utils/timeouts.ts +0 -9
  91. 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
@@ -1,3 +0,0 @@
1
- import { runCli } from './cli.ts';
2
-
3
- runCli(process.argv.slice(2));
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
- });