@@ -8,17 +8,19 @@ import {
88 backAndroid ,
99 ensureAdb ,
1010 homeAndroid ,
11+ pushAndroidNotification ,
1112 setAndroidSetting ,
1213 snapshotAndroid ,
1314} from '../platforms/android/index.ts' ;
1415import { listIosDevices } from '../platforms/ios/devices.ts' ;
1516import { getInteractor , type RunnerContext } from '../utils/interactors.ts' ;
1617import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts' ;
17- import { setIosSetting } from '../platforms/ios/index.ts' ;
18+ import { pushIosNotification , setIosSetting } from '../platforms/ios/index.ts' ;
1819import { isDeepLinkTarget } from './open-target.ts' ;
1920import type { RawSnapshotNode } from '../utils/snapshot.ts' ;
2021import type { CliFlags } from '../utils/command-schema.ts' ;
2122import { emitDiagnostic , withDiagnosticTimer } from '../utils/diagnostics.ts' ;
23+ import { resolvePayloadInput } from '../utils/payload-input.ts' ;
2224
2325export type BatchStep = {
2426 command : string ;
@@ -438,6 +440,28 @@ export async function dispatchCommand(
438440 await setAndroidSetting ( device , setting , state , appBundleId ?? context ?. appBundleId , permissionOptions ) ;
439441 return { setting, state } ;
440442 }
443+ case 'push' : {
444+ const target = positionals [ 0 ] ?. trim ( ) ;
445+ const payloadArg = positionals [ 1 ] ?. trim ( ) ;
446+ if ( ! target || ! payloadArg ) {
447+ throw new AppError (
448+ 'INVALID_ARGS' ,
449+ 'push requires <bundle|package> <payload.json|inline-json>' ,
450+ ) ;
451+ }
452+ const payload = await readNotificationPayload ( payloadArg ) ;
453+ if ( device . platform === 'ios' ) {
454+ await pushIosNotification ( device , target , payload ) ;
455+ return { platform : 'ios' , bundleId : target } ;
456+ }
457+ const androidResult = await pushAndroidNotification ( device , target , payload ) ;
458+ return {
459+ platform : 'android' ,
460+ package : target ,
461+ action : androidResult . action ,
462+ extrasCount : androidResult . extrasCount ,
463+ } ;
464+ }
441465 case 'snapshot' : {
442466 if ( device . platform === 'ios' ) {
443467 const result = ( await withDiagnosticTimer (
@@ -525,6 +549,43 @@ function clampIosSwipeDuration(durationMs: number): number {
525549 return Math . min ( 60 , Math . max ( 16 , Math . round ( durationMs ) ) ) ;
526550}
527551
552+ async function readNotificationPayload ( payloadArg : string ) : Promise < Record < string , unknown > > {
553+ const source = resolvePayloadInput ( payloadArg , { subject : 'Push payload' } ) ;
554+ const payloadText = source . kind === 'inline'
555+ ? source . text
556+ : await readPushPayloadFile ( source . path ) ;
557+ try {
558+ const parsed = JSON . parse ( payloadText ) as unknown ;
559+ if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed ) ) {
560+ throw new AppError ( 'INVALID_ARGS' , 'push payload must be a JSON object' ) ;
561+ }
562+ return parsed as Record < string , unknown > ;
563+ } catch ( error ) {
564+ if ( error instanceof AppError ) throw error ;
565+ throw new AppError ( 'INVALID_ARGS' , `Invalid push payload JSON: ${ payloadArg } ` ) ;
566+ }
567+ }
568+
569+ async function readPushPayloadFile ( payloadPath : string ) : Promise < string > {
570+ try {
571+ return await fs . readFile ( payloadPath , 'utf8' ) ;
572+ } catch ( error ) {
573+ const code = ( error as NodeJS . ErrnoException ) . code ;
574+ if ( code === 'ENOENT' ) {
575+ throw new AppError ( 'INVALID_ARGS' , `Push payload file not found: ${ payloadPath } ` ) ;
576+ }
577+ if ( code === 'EISDIR' ) {
578+ throw new AppError ( 'INVALID_ARGS' , `Push payload path is not a file: ${ payloadPath } ` ) ;
579+ }
580+ if ( code === 'EACCES' || code === 'EPERM' ) {
581+ throw new AppError ( 'INVALID_ARGS' , `Push payload file is not readable: ${ payloadPath } ` ) ;
582+ }
583+ throw new AppError ( 'COMMAND_FAILED' , `Unable to read push payload file: ${ payloadPath } ` , {
584+ cause : String ( error ) ,
585+ } ) ;
586+ }
587+ }
588+
528589export function shouldUseIosTapSeries (
529590 device : DeviceInfo ,
530591 count : number ,
0 commit comments