@@ -8,13 +8,14 @@ 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' ;
@@ -429,6 +430,28 @@ export async function dispatchCommand(
429430 await setAndroidSetting ( device , setting , state ) ;
430431 return { setting, state } ;
431432 }
433+ case 'push' : {
434+ const target = positionals [ 0 ] ?. trim ( ) ;
435+ const payloadArg = positionals [ 1 ] ?. trim ( ) ;
436+ if ( ! target || ! payloadArg ) {
437+ throw new AppError (
438+ 'INVALID_ARGS' ,
439+ 'push requires <bundle|package> <payload.json|inline-json>' ,
440+ ) ;
441+ }
442+ const payload = await readNotificationPayload ( payloadArg ) ;
443+ if ( device . platform === 'ios' ) {
444+ await pushIosNotification ( device , target , payload ) ;
445+ return { platform : 'ios' , bundleId : target } ;
446+ }
447+ const androidResult = await pushAndroidNotification ( device , target , payload ) ;
448+ return {
449+ platform : 'android' ,
450+ package : target ,
451+ action : androidResult . action ,
452+ extrasCount : androidResult . extrasCount ,
453+ } ;
454+ }
432455 case 'snapshot' : {
433456 if ( device . platform === 'ios' ) {
434457 const result = ( await withDiagnosticTimer (
@@ -516,6 +539,51 @@ function clampIosSwipeDuration(durationMs: number): number {
516539 return Math . min ( 60 , Math . max ( 16 , Math . round ( durationMs ) ) ) ;
517540}
518541
542+ async function readNotificationPayload ( payloadArg : string ) : Promise < Record < string , unknown > > {
543+ const trimmed = payloadArg . trim ( ) ;
544+ if ( ! trimmed ) {
545+ throw new AppError ( 'INVALID_ARGS' , 'push payload cannot be empty' ) ;
546+ }
547+ const payloadText = await resolvePushPayloadText ( payloadArg , trimmed ) ;
548+ try {
549+ const parsed = JSON . parse ( payloadText ) as unknown ;
550+ if ( ! parsed || typeof parsed !== 'object' || Array . isArray ( parsed ) ) {
551+ throw new AppError ( 'INVALID_ARGS' , 'push payload must be a JSON object' ) ;
552+ }
553+ return parsed as Record < string , unknown > ;
554+ } catch ( error ) {
555+ if ( error instanceof AppError ) throw error ;
556+ throw new AppError ( 'INVALID_ARGS' , `Invalid push payload JSON: ${ payloadArg } ` ) ;
557+ }
558+ }
559+
560+ function looksLikeInlineJson ( value : string ) : boolean {
561+ return ( value . startsWith ( '{' ) && value . endsWith ( '}' ) ) || ( value . startsWith ( '[' ) && value . endsWith ( ']' ) ) ;
562+ }
563+
564+ async function resolvePushPayloadText ( payloadArg : string , trimmedArg : string ) : Promise < string > {
565+ const filePayload = await tryReadPushPayloadFile ( payloadArg ) ;
566+ if ( filePayload !== null ) return filePayload ;
567+ if ( looksLikeInlineJson ( trimmedArg ) ) return trimmedArg ;
568+ throw new AppError ( 'INVALID_ARGS' , `Push payload file not found: ${ payloadArg } ` ) ;
569+ }
570+
571+ async function tryReadPushPayloadFile ( payloadArg : string ) : Promise < string | null > {
572+ try {
573+ return await fs . readFile ( payloadArg , 'utf8' ) ;
574+ } catch ( error ) {
575+ const code = ( error as NodeJS . ErrnoException ) . code ;
576+ if ( code === 'ENOENT' ) return null ;
577+ if ( code === 'EISDIR' ) {
578+ throw new AppError ( 'INVALID_ARGS' , `Push payload path is not a file: ${ payloadArg } ` ) ;
579+ }
580+ if ( code === 'EACCES' || code === 'EPERM' ) {
581+ throw new AppError ( 'INVALID_ARGS' , `Push payload file is not readable: ${ payloadArg } ` ) ;
582+ }
583+ throw new AppError ( 'COMMAND_FAILED' , `Unable to read push payload file: ${ payloadArg } ` , { cause : String ( error ) } ) ;
584+ }
585+ }
586+
519587export function shouldUseIosTapSeries (
520588 device : DeviceInfo ,
521589 count : number ,
0 commit comments