1313 * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --delete # Delete only (skip uninstall — delete only apps with 0 installs)
1414 * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --headed # Show browser window
1515 * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --pattern X # Match apps containing "X" (default: "E2E-")
16+ * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --max-apps 25 # Stop after finding 25 matching apps
17+ * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --max-pages 5 # Stop after scanning 5 dashboard pages
1618 *
1719 * Environment variables (loaded from packages/e2e/.env):
1820 * E2E_ACCOUNT_EMAIL — Shopify account email for login
@@ -59,6 +61,10 @@ export interface CleanupOptions {
5961 headed ?: boolean
6062 /** Organization ID (default: from E2E_ORG_ID env) */
6163 orgId ?: string
64+ /** Stop discovering apps after this many matching apps */
65+ maxApps ?: number
66+ /** Stop discovering apps after this many dashboard pages */
67+ maxPages ?: number
6268}
6369
6470/**
@@ -77,6 +83,8 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise<void> {
7783 console . log ( `[cleanup-apps] Mode: ${ MODE_LABELS [ mode ] } ` )
7884 console . log ( `[cleanup-apps] Org: ${ orgId || '(not set)' } ` )
7985 console . log ( `[cleanup-apps] Pattern: "${ pattern } "` )
86+ if ( opts . maxApps ) console . log ( `[cleanup-apps] Max apps: ${ opts . maxApps } ` )
87+ if ( opts . maxPages ) console . log ( `[cleanup-apps] Max pages: ${ opts . maxPages } ` )
8088 console . log ( '' )
8189
8290 if ( ! email || ! password ) {
@@ -118,7 +126,11 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise<void> {
118126
119127 // Step 3: Find matching apps
120128 console . log ( '[cleanup-apps] Finding matching apps...' )
121- const apps = await findAppsOnDashboard ( page , pattern )
129+ const apps = await findAppsOnDashboard ( page , pattern , {
130+ maxApps : opts . maxApps ,
131+ maxPages : opts . maxPages ,
132+ onlyUninstalled : mode === 'delete' ,
133+ } )
122134 console . log ( `[cleanup-apps] Found ${ apps . length } app(s) matching pattern "${ pattern } "` )
123135 console . log ( '' )
124136
@@ -233,12 +245,15 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise<void> {
233245async function findAppsOnDashboard (
234246 page : Page ,
235247 namePattern : string ,
248+ opts : { maxApps ?: number ; maxPages ?: number ; onlyUninstalled ?: boolean } = { } ,
236249) : Promise < { name : string ; url : string ; installs : number } [ ] > {
237250 const apps : { name : string ; url : string ; installs : number } [ ] = [ ]
238251 let totalSeen = 0
252+ let pagesSeen = 0
239253
240254 // eslint-disable-next-line no-constant-condition
241255 while ( true ) {
256+ pagesSeen ++
242257 // Recover from transient 500/502 before parsing the page
243258 for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
244259 if ( ! ( await refreshIfPageError ( page ) ) ) break
@@ -261,13 +276,18 @@ async function findAppsOnDashboard(
261276
262277 const installMatch = text . match ( / ( \d + ) \s + i n s t a l l / i)
263278 const installs = installMatch ? parseInt ( installMatch [ 1 ] ! , 10 ) : 0
279+ if ( opts . onlyUninstalled && installs > 0 ) continue
264280
265281 const url = href . startsWith ( 'http' ) ? href : `https://dev.shopify.com${ href } `
266282 apps . push ( { name, url, installs} )
283+ if ( opts . maxApps && apps . length >= opts . maxApps ) break
267284 }
268285
269286 console . log ( `[cleanup-apps] ...loaded ${ totalSeen } apps` )
270287
288+ if ( opts . maxApps && apps . length >= opts . maxApps ) break
289+ if ( opts . maxPages && pagesSeen >= opts . maxPages ) break
290+
271291 // Check for next page — navigate via href since the button click may not work
272292 const nextLink = page . locator ( 'a[href*="next_cursor"]' ) . first ( )
273293 if ( ! ( await nextLink . isVisible ( { timeout : BROWSER_TIMEOUT . medium } ) . catch ( ( ) => false ) ) ) break
@@ -392,12 +412,30 @@ async function main() {
392412 pattern = nextArg
393413 }
394414
415+ const maxApps = parsePositiveIntegerOption ( args , '--max-apps' )
416+ const maxPages = parsePositiveIntegerOption ( args , '--max-pages' )
417+
395418 let mode : CleanupMode = 'full'
396419 if ( args . includes ( '--list' ) ) mode = 'list'
397420 else if ( args . includes ( '--uninstall' ) ) mode = 'uninstall'
398421 else if ( args . includes ( '--delete' ) ) mode = 'delete'
399422
400- await cleanupAllApps ( { mode, pattern, headed} )
423+ await cleanupAllApps ( { mode, pattern, headed, maxApps, maxPages} )
424+ }
425+
426+ function parsePositiveIntegerOption ( args : string [ ] , name : string ) : number | undefined {
427+ const optionIndex = args . indexOf ( name )
428+ if ( optionIndex === - 1 ) return undefined
429+
430+ const optionValue = args [ optionIndex + 1 ]
431+ const parsedValue = Number ( optionValue )
432+ if ( ! optionValue || ! Number . isInteger ( parsedValue ) || parsedValue <= 0 ) {
433+ console . error ( `[cleanup-apps] ${ name } requires a positive integer` )
434+ process . exitCode = 1
435+ return undefined
436+ }
437+
438+ return parsedValue
401439}
402440
403441// Run if executed directly (not imported)
0 commit comments