Skip to content

Commit 4ee54bc

Browse files
committed
refactor: simplify daemon failure responses
1 parent 8547cfa commit 4ee54bc

35 files changed

Lines changed: 314 additions & 272 deletions

src/cli.ts

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,13 @@ function readBatchStepFailure(error: Record<string, unknown> | undefined): strin
334334
function writeCommandCliOutput(
335335
command: string,
336336
positionals: string[],
337-
flags: { json?: boolean; verbose?: boolean; snapshotRaw?: boolean; snapshotInteractiveOnly?: boolean; reportJunit?: string },
337+
flags: {
338+
json?: boolean;
339+
verbose?: boolean;
340+
snapshotRaw?: boolean;
341+
snapshotInteractiveOnly?: boolean;
342+
reportJunit?: string;
343+
},
338344
data: Record<string, unknown>,
339345
): number {
340346
if (flags.json) {
@@ -350,10 +356,12 @@ function writeCommandCliOutput(
350356
}
351357

352358
if (command === 'snapshot') {
353-
process.stdout.write(formatSnapshotText(data, {
354-
raw: flags.snapshotRaw,
355-
flatten: flags.snapshotInteractiveOnly,
356-
}));
359+
process.stdout.write(
360+
formatSnapshotText(data, {
361+
raw: flags.snapshotRaw,
362+
flatten: flags.snapshotInteractiveOnly,
363+
}),
364+
);
357365
return 0;
358366
}
359367
if (command === 'test') {
@@ -369,13 +377,28 @@ function writeCommandCliOutput(
369377
}
370378
if (command === 'get') {
371379
const sub = positionals[0];
372-
if (sub === 'text') { process.stdout.write(`${(data as any)?.text ?? ''}\n`); return 0; }
373-
if (sub === 'attrs') { process.stdout.write(`${JSON.stringify((data as any)?.node ?? {}, null, 2)}\n`); return 0; }
380+
if (sub === 'text') {
381+
process.stdout.write(`${(data as any)?.text ?? ''}\n`);
382+
return 0;
383+
}
384+
if (sub === 'attrs') {
385+
process.stdout.write(`${JSON.stringify((data as any)?.node ?? {}, null, 2)}\n`);
386+
return 0;
387+
}
374388
}
375389
if (command === 'find') {
376-
if (typeof (data as any)?.text === 'string') { process.stdout.write(`${(data as any).text}\n`); return 0; }
377-
if (typeof (data as any)?.found === 'boolean') { process.stdout.write(`Found: ${(data as any).found}\n`); return 0; }
378-
if ((data as any)?.node) { process.stdout.write(`${JSON.stringify((data as any).node, null, 2)}\n`); return 0; }
390+
if (typeof (data as any)?.text === 'string') {
391+
process.stdout.write(`${(data as any).text}\n`);
392+
return 0;
393+
}
394+
if (typeof (data as any)?.found === 'boolean') {
395+
process.stdout.write(`Found: ${(data as any).found}\n`);
396+
return 0;
397+
}
398+
if ((data as any)?.node) {
399+
process.stdout.write(`${JSON.stringify((data as any).node, null, 2)}\n`);
400+
return 0;
401+
}
379402
}
380403
if (command === 'is') {
381404
process.stdout.write(`Passed: is ${(data as any)?.predicate ?? 'assertion'}\n`);
@@ -412,9 +435,17 @@ function writeCommandCliOutput(
412435
return 0;
413436
}
414437
if (command === 'clipboard') {
415-
const action = (positionals[0] ?? (typeof data?.action === 'string' ? data.action : '')).toLowerCase();
416-
if (action === 'read') { process.stdout.write(`${typeof data?.text === 'string' ? data.text : ''}\n`); return 0; }
417-
if (action === 'write') { process.stdout.write('Clipboard updated\n'); return 0; }
438+
const action = (
439+
positionals[0] ?? (typeof data?.action === 'string' ? data.action : '')
440+
).toLowerCase();
441+
if (action === 'read') {
442+
process.stdout.write(`${typeof data?.text === 'string' ? data.text : ''}\n`);
443+
return 0;
444+
}
445+
if (action === 'write') {
446+
process.stdout.write('Clipboard updated\n');
447+
return 0;
448+
}
418449
}
419450
if (command === 'network') {
420451
writeNetworkCliOutput(data);
@@ -431,36 +462,47 @@ function writeCommandCliOutput(
431462
}
432463
if (command === 'devices') {
433464
const devices = Array.isArray((data as any).devices) ? (data as any).devices : [];
434-
process.stdout.write(`${devices.map((d: any) => {
435-
const name = d?.name ?? d?.id ?? 'unknown';
436-
const platform = d?.platform ?? 'unknown';
437-
const kind = d?.kind ? ` ${d.kind}` : '';
438-
const target = d?.target ? ` target=${d.target}` : '';
439-
const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
440-
return `${name} (${platform}${kind}${target})${booted}`;
441-
}).join('\n')}\n`);
465+
process.stdout.write(
466+
`${devices
467+
.map((d: any) => {
468+
const name = d?.name ?? d?.id ?? 'unknown';
469+
const platform = d?.platform ?? 'unknown';
470+
const kind = d?.kind ? ` ${d.kind}` : '';
471+
const target = d?.target ? ` target=${d.target}` : '';
472+
const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : '';
473+
return `${name} (${platform}${kind}${target})${booted}`;
474+
})
475+
.join('\n')}\n`,
476+
);
442477
return 0;
443478
}
444479
if (command === 'apps') {
445480
const apps = Array.isArray((data as any).apps) ? (data as any).apps : [];
446-
process.stdout.write(`${apps.map((app: any) => {
447-
if (typeof app === 'string') return app;
448-
if (app && typeof app === 'object') {
449-
const bundleId = app.bundleId ?? app.package;
450-
const name = app.name ?? app.label;
451-
if (name && bundleId) return `${name} (${bundleId})`;
452-
if (bundleId) return String(bundleId);
453-
return JSON.stringify(app);
454-
}
455-
return String(app);
456-
}).join('\n')}\n`);
481+
process.stdout.write(
482+
`${apps
483+
.map((app: any) => {
484+
if (typeof app === 'string') return app;
485+
if (app && typeof app === 'object') {
486+
const bundleId = app.bundleId ?? app.package;
487+
const name = app.name ?? app.label;
488+
if (name && bundleId) return `${name} (${bundleId})`;
489+
if (bundleId) return String(bundleId);
490+
return JSON.stringify(app);
491+
}
492+
return String(app);
493+
})
494+
.join('\n')}\n`,
495+
);
457496
return 0;
458497
}
459498
if (command === 'appstate') {
460499
const platform = (data as any)?.platform;
461500
if (platform === 'ios') {
462-
process.stdout.write(`Foreground app: ${(data as any)?.appName ?? (data as any)?.appBundleId ?? 'unknown'}\n`);
463-
if ((data as any)?.appBundleId) process.stdout.write(`Bundle: ${(data as any).appBundleId}\n`);
501+
process.stdout.write(
502+
`Foreground app: ${(data as any)?.appName ?? (data as any)?.appBundleId ?? 'unknown'}\n`,
503+
);
504+
if ((data as any)?.appBundleId)
505+
process.stdout.write(`Bundle: ${(data as any).appBundleId}\n`);
464506
if ((data as any)?.source) process.stdout.write(`Source: ${(data as any).source}\n`);
465507
return 0;
466508
}
@@ -494,11 +536,18 @@ function writeLogsCliOutput(data: Record<string, unknown>, flags: { json?: boole
494536
.filter(Boolean)
495537
.join(' ');
496538
if (meta && !flags.json) process.stderr.write(`${meta}\n`);
497-
const actionFields = ['started', 'stopped', 'marked', 'cleared', 'restarted', 'removedRotatedFiles'] as const;
539+
const actionFields = [
540+
'started',
541+
'stopped',
542+
'marked',
543+
'cleared',
544+
'restarted',
545+
'removedRotatedFiles',
546+
] as const;
498547
const actionMeta = actionFields
499548
.map((key) => {
500549
const v = data[key];
501-
return v === true ? `${key}=true` : (typeof v === 'number' ? `${key}=${v}` : '');
550+
return v === true ? `${key}=true` : typeof v === 'number' ? `${key}=${v}` : '';
502551
})
503552
.filter(Boolean)
504553
.join(' ');
@@ -523,14 +572,24 @@ function writeNetworkCliOutput(data: Record<string, unknown>): void {
523572
const url = typeof entry.url === 'string' ? entry.url : '<unknown-url>';
524573
const status = typeof entry.status === 'number' ? ` status=${entry.status}` : '';
525574
const timestamp = typeof entry.timestamp === 'string' ? `${entry.timestamp} ` : '';
526-
const durationMs = typeof entry.durationMs === 'number' ? ` durationMs=${entry.durationMs}` : '';
575+
const durationMs =
576+
typeof entry.durationMs === 'number' ? ` durationMs=${entry.durationMs}` : '';
527577
process.stdout.write(`${timestamp}${method} ${url}${status}${durationMs}\n`);
528578
if (typeof entry.headers === 'string') process.stdout.write(` headers: ${entry.headers}\n`);
529-
if (typeof entry.requestBody === 'string') process.stdout.write(` request: ${entry.requestBody}\n`);
530-
if (typeof entry.responseBody === 'string') process.stdout.write(` response: ${entry.responseBody}\n`);
579+
if (typeof entry.requestBody === 'string')
580+
process.stdout.write(` request: ${entry.requestBody}\n`);
581+
if (typeof entry.responseBody === 'string')
582+
process.stdout.write(` response: ${entry.responseBody}\n`);
531583
}
532584
}
533-
const networkMetaFields = ['active', 'state', 'backend', 'include', 'scannedLines', 'matchedLines'] as const;
585+
const networkMetaFields = [
586+
'active',
587+
'state',
588+
'backend',
589+
'include',
590+
'scannedLines',
591+
'matchedLines',
592+
] as const;
534593
const meta = networkMetaFields
535594
.map((key) => (data[key] !== undefined && data[key] !== null ? `${key}=${data[key]}` : ''))
536595
.filter(Boolean)

src/daemon/handlers/find.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { extractNodeText, findNearestHittableAncestor } from '../snapshot-proces
1010
import { parseTimeout } from './parse-utils.ts';
1111
import { readTextForNode } from './interaction-read.ts';
1212
import { captureSnapshot } from './snapshot-capture.ts';
13-
import { errorResponse, sessionNotFoundResponse } from './response.ts';
13+
import { errorResponse } from './response.ts';
1414
import { getActiveAndroidSnapshotFreshness } from '../android-snapshot-freshness.ts';
1515

1616
type FindContext = {
@@ -60,7 +60,7 @@ export async function handleFindCommands(params: {
6060
const isReadOnly =
6161
action === 'exists' || action === 'wait' || action === 'get_text' || action === 'get_attrs';
6262
if (!session && !isReadOnly) {
63-
return sessionNotFoundResponse();
63+
return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
6464
}
6565
const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {}));
6666
if (!session) {

src/daemon/handlers/install-source.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
1313
import { resolveInstallFromSourceResultTarget } from '../../client-shared.ts';
1414
import { AppError, normalizeError } from '../../utils/errors.ts';
1515
import { withSuccessText } from '../../utils/success-text.ts';
16-
import { unsupportedOperationResponse } from './response.ts';
16+
import { errorResponse } from './response.ts';
1717

1818
function normalizePlatform(platform: CommandFlags['platform']): 'ios' | 'android' | undefined {
1919
return platform === 'ios' || platform === 'android' ? platform : undefined;
@@ -74,7 +74,10 @@ export async function handleInstallFromSourceCommand(params: {
7474
flags: req.flags,
7575
});
7676
if (!isCommandSupportedOnDevice('install', device)) {
77-
return unsupportedOperationResponse('install_from_source');
77+
return errorResponse(
78+
'UNSUPPORTED_OPERATION',
79+
'install_from_source is not supported on this device',
80+
);
7881
}
7982

8083
const requestSignal = getRequestSignal(req.meta?.requestId);

src/daemon/handlers/interaction-fill.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { readSnapshotNodesReferenceFrame } from './interaction-touch-reference-f
2121
import { resolveRefTargetWithRectRefresh, type ResolveRefTarget } from './interaction-targeting.ts';
2222
import { unsupportedMacOsDesktopSurfaceInteraction } from './interaction-touch-policy.ts';
2323
import type { RefSnapshotFlagGuardResponse } from './interaction-flags.ts';
24-
import { errorResponse, sessionNotFoundResponse, unsupportedOperationResponse } from './response.ts';
24+
import { errorResponse } from './response.ts';
2525

2626
export async function handleFillCommand(params: {
2727
req: DaemonRequest;
@@ -50,11 +50,11 @@ export async function handleFillCommand(params: {
5050
}
5151
}
5252
if (session && !isCommandSupportedOnDevice('fill', session.device)) {
53-
return unsupportedOperationResponse('fill');
53+
return errorResponse('UNSUPPORTED_OPERATION', 'fill is not supported on this device');
5454
}
5555
if (req.positionals?.[0]?.startsWith('@')) {
5656
if (!session) {
57-
return sessionNotFoundResponse();
57+
return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
5858
}
5959
const invalidRefFlagsResponse = refSnapshotFlagGuardResponse('fill', req.flags);
6060
if (invalidRefFlagsResponse) return invalidRefFlagsResponse;
@@ -83,7 +83,7 @@ export async function handleFillCommand(params: {
8383
captureSnapshotForSession,
8484
resolveRefTarget,
8585
});
86-
if (!resolvedRefFillTarget.ok) return resolvedRefFillTarget.response;
86+
if (!resolvedRefFillTarget.ok) return resolvedRefFillTarget;
8787

8888
const { ref, node, snapshotNodes, point } = resolvedRefFillTarget.target;
8989
const nodeType = node.type ?? '';
@@ -132,7 +132,7 @@ export async function handleFillCommand(params: {
132132
}
133133

134134
if (!session) {
135-
return sessionNotFoundResponse();
135+
return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
136136
}
137137

138138
const selectorArgs = splitSelectorFromArgs(req.positionals ?? [], {
@@ -167,7 +167,10 @@ export async function handleFillCommand(params: {
167167
{ command: req.command },
168168
);
169169
if (!resolved || !resolved.node.rect) {
170-
return errorResponse('COMMAND_FAILED', formatSelectorFailure(chain, resolved?.diagnostics ?? [], { unique: true }));
170+
return errorResponse(
171+
'COMMAND_FAILED',
172+
formatSelectorFailure(chain, resolved?.diagnostics ?? [], { unique: true }),
173+
);
171174
}
172175

173176
const node = resolved.node;

src/daemon/handlers/interaction-flags.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ export function refSnapshotFlagGuardResponse(
1414
): DaemonResponse | null {
1515
const unsupported = unsupportedRefSnapshotFlags(flags);
1616
if (unsupported.length === 0) return null;
17-
return errorResponse('INVALID_ARGS', `${command} @ref does not support ${unsupported.join(', ')}.`);
17+
return errorResponse(
18+
'INVALID_ARGS',
19+
`${command} @ref does not support ${unsupported.join(', ')}.`,
20+
);
1821
}
1922

2023
export type RefSnapshotFlagGuardResponse = typeof refSnapshotFlagGuardResponse;

src/daemon/handlers/interaction-get.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@ import { refSnapshotFlagGuardResponse } from './interaction-flags.ts';
66
import { readTextForNode } from './interaction-read.ts';
77
import { resolveRefTarget } from './interaction-targeting.ts';
88
import { resolveSelectorTarget } from './interaction-selector.ts';
9-
import {
10-
errorResponse,
11-
sessionNotFoundResponse,
12-
unsupportedOperationResponse,
13-
} from './response.ts';
9+
import { errorResponse } from './response.ts';
1410

1511
export async function handleGetCommand(params: InteractionHandlerParams): Promise<DaemonResponse> {
1612
const { req, sessionName, sessionStore, contextFromFlags } = params;
@@ -19,9 +15,9 @@ export async function handleGetCommand(params: InteractionHandlerParams): Promis
1915
return errorResponse('INVALID_ARGS', 'get only supports text or attrs');
2016
}
2117
const session = sessionStore.get(sessionName);
22-
if (!session) return sessionNotFoundResponse();
18+
if (!session) return errorResponse('SESSION_NOT_FOUND', 'No active session. Run open first.');
2319
if (!isCommandSupportedOnDevice('get', session.device)) {
24-
return unsupportedOperationResponse('get');
20+
return errorResponse('UNSUPPORTED_OPERATION', 'get is not supported on this device');
2521
}
2622
const refInput = req.positionals?.[1] ?? '';
2723
if (refInput.startsWith('@')) {
@@ -37,7 +33,7 @@ export async function handleGetCommand(params: InteractionHandlerParams): Promis
3733
invalidRefMessage: 'get text requires a ref like @e2',
3834
notFoundMessage: `Ref ${refInput} not found`,
3935
});
40-
if (!resolvedRefTarget.ok) return resolvedRefTarget.response;
36+
if (!resolvedRefTarget.ok) return resolvedRefTarget;
4137
const { ref, node } = resolvedRefTarget.target;
4238
const selectorChain = buildSelectorChainForNode(node, session.device.platform, {
4339
action: 'get',
@@ -85,7 +81,7 @@ export async function handleGetCommand(params: InteractionHandlerParams): Promis
8581
requireUnique: true,
8682
disambiguateAmbiguous: sub === 'text',
8783
});
88-
if (!resolvedSelectorTarget.ok) return resolvedSelectorTarget.response;
84+
if (!resolvedSelectorTarget.ok) return resolvedSelectorTarget;
8985
const { resolved } = resolvedSelectorTarget;
9086
const node = resolved.node;
9187
const selectorChain = buildSelectorChainForNode(node, session.device.platform, {

0 commit comments

Comments
 (0)