Skip to content

Commit c99c8ba

Browse files
committed
refactor: consolidate push payload source resolution
1 parent 32c7e12 commit c99c8ba

3 files changed

Lines changed: 74 additions & 44 deletions

File tree

src/core/dispatch.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { isDeepLinkTarget } from './open-target.ts';
2020
import type { RawSnapshotNode } from '../utils/snapshot.ts';
2121
import type { CliFlags } from '../utils/command-schema.ts';
2222
import { emitDiagnostic, withDiagnosticTimer } from '../utils/diagnostics.ts';
23-
import { looksLikeInlineJson } from '../utils/json-input.ts';
23+
import { resolvePayloadInput } from '../utils/payload-input.ts';
2424

2525
export type BatchStep = {
2626
command: string;
@@ -550,11 +550,10 @@ function clampIosSwipeDuration(durationMs: number): number {
550550
}
551551

552552
async function readNotificationPayload(payloadArg: string): Promise<Record<string, unknown>> {
553-
const trimmed = payloadArg.trim();
554-
if (!trimmed) {
555-
throw new AppError('INVALID_ARGS', 'push payload cannot be empty');
556-
}
557-
const payloadText = await resolvePushPayloadText(payloadArg, trimmed);
553+
const source = resolvePayloadInput(payloadArg, { subject: 'Push payload' });
554+
const payloadText = source.kind === 'inline'
555+
? source.text
556+
: await readPushPayloadFile(source.path);
558557
try {
559558
const parsed = JSON.parse(payloadText) as unknown;
560559
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -567,26 +566,23 @@ async function readNotificationPayload(payloadArg: string): Promise<Record<strin
567566
}
568567
}
569568

570-
async function resolvePushPayloadText(payloadArg: string, trimmedArg: string): Promise<string> {
571-
const filePayload = await tryReadPushPayloadFile(payloadArg);
572-
if (filePayload !== null) return filePayload;
573-
if (looksLikeInlineJson(trimmedArg)) return trimmedArg;
574-
throw new AppError('INVALID_ARGS', `Push payload file not found: ${payloadArg}`);
575-
}
576-
577-
async function tryReadPushPayloadFile(payloadArg: string): Promise<string | null> {
569+
async function readPushPayloadFile(payloadPath: string): Promise<string> {
578570
try {
579-
return await fs.readFile(payloadArg, 'utf8');
571+
return await fs.readFile(payloadPath, 'utf8');
580572
} catch (error) {
581573
const code = (error as NodeJS.ErrnoException).code;
582-
if (code === 'ENOENT') return null;
574+
if (code === 'ENOENT') {
575+
throw new AppError('INVALID_ARGS', `Push payload file not found: ${payloadPath}`);
576+
}
583577
if (code === 'EISDIR') {
584-
throw new AppError('INVALID_ARGS', `Push payload path is not a file: ${payloadArg}`);
578+
throw new AppError('INVALID_ARGS', `Push payload path is not a file: ${payloadPath}`);
585579
}
586580
if (code === 'EACCES' || code === 'EPERM') {
587-
throw new AppError('INVALID_ARGS', `Push payload file is not readable: ${payloadArg}`);
581+
throw new AppError('INVALID_ARGS', `Push payload file is not readable: ${payloadPath}`);
588582
}
589-
throw new AppError('COMMAND_FAILED', `Unable to read push payload file: ${payloadArg}`, { cause: String(error) });
583+
throw new AppError('COMMAND_FAILED', `Unable to read push payload file: ${payloadPath}`, {
584+
cause: String(error),
585+
});
590586
}
591587
}
592588

src/daemon/handlers/session.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
isClickLikeCommand,
3333
parseReplaySeriesFlags,
3434
} from '../script-utils.ts';
35-
import { looksLikeInlineJson } from '../../utils/json-input.ts';
35+
import { resolvePayloadInput } from '../../utils/payload-input.ts';
3636
import {
3737
appendAppLogMarker,
3838
clearAppLogFiles,
@@ -450,7 +450,7 @@ export async function handleSessionCommands(params: {
450450
ok: false,
451451
error: {
452452
code: 'INVALID_ARGS',
453-
message: 'push requires: push <bundle|package> <payload.json|inline-json>',
453+
message: 'push requires <bundle|package> <payload.json|inline-json>',
454454
},
455455
};
456456
}
@@ -872,29 +872,12 @@ export async function handleSessionCommands(params: {
872872
}
873873

874874
function maybeResolvePushPayloadPath(payloadArg: string, cwd?: string): string {
875-
const trimmed = payloadArg.trim();
876-
const resolvedPath = SessionStore.expandHome(trimmed, cwd);
877-
try {
878-
const stat = fs.statSync(resolvedPath);
879-
if (!stat.isFile()) {
880-
throw new AppError('INVALID_ARGS', `Push payload path is not a file: ${resolvedPath}`);
881-
}
882-
return resolvedPath;
883-
} catch (error) {
884-
const code = (error as NodeJS.ErrnoException).code;
885-
if (code === 'EACCES' || code === 'EPERM') {
886-
throw new AppError('INVALID_ARGS', `Push payload file is not readable: ${resolvedPath}`);
887-
}
888-
if (code && code !== 'ENOENT') {
889-
throw new AppError('COMMAND_FAILED', `Unable to read push payload file: ${resolvedPath}`, {
890-
cause: String(error),
891-
});
892-
}
893-
}
894-
if (looksLikeInlineJson(trimmed)) {
895-
return trimmed;
896-
}
897-
throw new AppError('INVALID_ARGS', `Push payload file not found: ${resolvedPath}`);
875+
const resolved = resolvePayloadInput(payloadArg, {
876+
subject: 'Push payload',
877+
cwd,
878+
expandPath: (value, currentCwd) => SessionStore.expandHome(value, currentCwd),
879+
});
880+
return resolved.kind === 'file' ? resolved.path : resolved.text;
898881
}
899882

900883
async function runBatchCommands(

src/utils/payload-input.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import fs from 'node:fs';
2+
import { AppError } from './errors.ts';
3+
import { looksLikeInlineJson } from './json-input.ts';
4+
5+
export type ResolvedPayloadInput =
6+
| { kind: 'file'; path: string }
7+
| { kind: 'inline'; text: string };
8+
9+
export function resolvePayloadInput(
10+
value: string,
11+
options?: {
12+
subject?: string;
13+
cwd?: string;
14+
expandPath?: (value: string, cwd?: string) => string;
15+
},
16+
): ResolvedPayloadInput {
17+
const subject = options?.subject ?? 'Payload';
18+
const trimmed = value.trim();
19+
if (!trimmed) {
20+
throw new AppError('INVALID_ARGS', `${subject} cannot be empty`);
21+
}
22+
23+
const resolvedPath = options?.expandPath
24+
? options.expandPath(trimmed, options.cwd)
25+
: trimmed;
26+
27+
try {
28+
const stat = fs.statSync(resolvedPath);
29+
if (!stat.isFile()) {
30+
throw new AppError('INVALID_ARGS', `${subject} path is not a file: ${resolvedPath}`);
31+
}
32+
return { kind: 'file', path: resolvedPath };
33+
} catch (error) {
34+
if (error instanceof AppError) throw error;
35+
const code = (error as NodeJS.ErrnoException).code;
36+
if (code === 'EACCES' || code === 'EPERM') {
37+
throw new AppError('INVALID_ARGS', `${subject} file is not readable: ${resolvedPath}`);
38+
}
39+
if (code && code !== 'ENOENT') {
40+
throw new AppError('COMMAND_FAILED', `Unable to read ${subject} file: ${resolvedPath}`, {
41+
cause: String(error),
42+
});
43+
}
44+
}
45+
46+
if (looksLikeInlineJson(trimmed)) {
47+
return { kind: 'inline', text: trimmed };
48+
}
49+
throw new AppError('INVALID_ARGS', `${subject} file not found: ${resolvedPath}`);
50+
}
51+

0 commit comments

Comments
 (0)