@open-mercato/cli 0.5.1-develop.2744.9c8be0dd93 → 0.5.1-develop.2762.90c271efe2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.5.1-develop.2744.9c8be0dd93",
3
+ "version": "0.5.1-develop.2762.90c271efe2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "@mikro-orm/decorators": "^7.0.10",
60
60
  "@mikro-orm/migrations": "^7.0.10",
61
61
  "@mikro-orm/postgresql": "^7.0.10",
62
- "@open-mercato/queue": "0.5.1-develop.2744.9c8be0dd93",
63
- "@open-mercato/shared": "0.5.1-develop.2744.9c8be0dd93",
62
+ "@open-mercato/queue": "0.5.1-develop.2762.90c271efe2",
63
+ "@open-mercato/shared": "0.5.1-develop.2762.90c271efe2",
64
64
  "cross-spawn": "^7.0.6",
65
65
  "pg": "8.20.0",
66
66
  "semver": "^7.7.4",
@@ -70,10 +70,10 @@
70
70
  "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.5.1-develop.2744.9c8be0dd93"
73
+ "@open-mercato/shared": "0.5.1-develop.2762.90c271efe2"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.5.1-develop.2744.9c8be0dd93",
76
+ "@open-mercato/shared": "0.5.1-develop.2762.90c271efe2",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.3.0",
79
79
  "ts-jest": "^29.4.9"
@@ -350,6 +350,109 @@ describe('init command failure output', () => {
350
350
  consoleErrorSpy.mockRestore()
351
351
  consoleLogSpy.mockRestore()
352
352
  })
353
+
354
+ it('keeps init successful when lean presets disable optional modules', async () => {
355
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
356
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
357
+
358
+ const configsRestoreDefaults = jest.fn().mockResolvedValue(undefined)
359
+ const authSetup = jest.fn().mockResolvedValue(undefined)
360
+ const authSeedRoles = jest.fn().mockResolvedValue(undefined)
361
+ const entitiesSeedEncryption = jest.fn().mockResolvedValue(undefined)
362
+ const queryIndexReindex = jest.fn().mockResolvedValue(undefined)
363
+
364
+ jest.doMock('child_process', () => ({
365
+ execSync: jest.fn(),
366
+ }))
367
+ jest.doMock('pg', () => ({
368
+ Client: jest.fn().mockImplementation(() => ({
369
+ connect: jest.fn().mockResolvedValue(undefined),
370
+ query: jest.fn().mockResolvedValue({
371
+ rows: [{ org_id: 'org-1', tenant_id: 'tenant-1' }],
372
+ }),
373
+ end: jest.fn().mockResolvedValue(undefined),
374
+ })),
375
+ }))
376
+ jest.doMock('../lib/generators', () => ({
377
+ generateEntityIds: jest.fn().mockResolvedValue(undefined),
378
+ generateModuleRegistry: jest.fn().mockResolvedValue(undefined),
379
+ generateModuleRegistryApp: jest.fn().mockResolvedValue(undefined),
380
+ generateModuleRegistryCli: jest.fn().mockResolvedValue(undefined),
381
+ generateModuleEntities: jest.fn().mockResolvedValue(undefined),
382
+ generateModuleDi: jest.fn().mockResolvedValue(undefined),
383
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
384
+ generateOpenApi: jest.fn().mockResolvedValue(undefined),
385
+ }))
386
+ jest.doMock('../lib/db', () => ({
387
+ dbMigrate: jest.fn().mockResolvedValue(undefined),
388
+ }))
389
+ jest.doMock('../lib/resolver', () => ({
390
+ createResolver: () => ({
391
+ getAppDir: () => '/tmp/test-app',
392
+ }),
393
+ }))
394
+ jest.doMock('@open-mercato/shared/lib/bootstrap/dynamicLoader', () => ({
395
+ bootstrapFromAppRoot: jest.fn().mockResolvedValue({
396
+ modules: [
397
+ {
398
+ id: 'configs',
399
+ cli: [{ command: 'restore-defaults', run: configsRestoreDefaults }],
400
+ },
401
+ {
402
+ id: 'auth',
403
+ cli: [
404
+ { command: 'setup', run: authSetup },
405
+ { command: 'seed-roles', run: authSeedRoles },
406
+ ],
407
+ },
408
+ {
409
+ id: 'entities',
410
+ cli: [{ command: 'seed-encryption', run: entitiesSeedEncryption }],
411
+ },
412
+ {
413
+ id: 'query_index',
414
+ cli: [{ command: 'reindex', run: queryIndexReindex }],
415
+ },
416
+ ],
417
+ }),
418
+ }))
419
+ jest.doMock('@open-mercato/shared/lib/di/container', () => ({
420
+ createRequestContainer: jest.fn().mockResolvedValue({
421
+ resolve: jest.fn().mockReturnValue({}),
422
+ }),
423
+ }))
424
+ jest.doMock(
425
+ '@open-mercato/core/modules/auth/lib/setup-app',
426
+ () => ({
427
+ ensureCustomRoleAcls: jest.fn().mockResolvedValue(undefined),
428
+ }),
429
+ { virtual: true },
430
+ )
431
+
432
+ const mercato = await import('../mercato')
433
+ const exitCode = await mercato.run(['node', 'mercato', 'init'])
434
+
435
+ expect(exitCode).toBe(0)
436
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
437
+ expect(consoleLogSpy).toHaveBeenCalledWith(
438
+ '⏭️ Skipping "feature_toggles:seed-defaults" — module not enabled',
439
+ )
440
+ expect(consoleLogSpy).toHaveBeenCalledWith(
441
+ '⏭️ Skipping "dashboards:seed-defaults" — module not enabled',
442
+ )
443
+ expect(consoleLogSpy).toHaveBeenCalledWith(
444
+ '⏭️ Skipping "dashboards:enable-analytics-widgets" — module not enabled',
445
+ )
446
+ expect(consoleLogSpy).toHaveBeenCalledWith(
447
+ '⏭️ Skipping "search:reindex" — module not enabled',
448
+ )
449
+ expect(configsRestoreDefaults).toHaveBeenCalled()
450
+ expect(authSetup).toHaveBeenCalled()
451
+ expect(queryIndexReindex).toHaveBeenCalledWith(['--force', '--tenant', 'tenant-1'])
452
+
453
+ consoleErrorSpy.mockRestore()
454
+ consoleLogSpy.mockRestore()
455
+ })
353
456
  })
354
457
 
355
458
  describe('generate post-step structural cache purge', () => {
@@ -456,3 +559,125 @@ describe('generate post-step structural cache purge', () => {
456
559
  consoleLogSpy.mockRestore()
457
560
  })
458
561
  })
562
+
563
+ describe('server dev managed process exits', () => {
564
+ const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
565
+
566
+ beforeEach(() => {
567
+ jest.restoreAllMocks()
568
+ jest.resetModules()
569
+ process.env.AUTO_SPAWN_SCHEDULER = 'false'
570
+ })
571
+
572
+ afterEach(() => {
573
+ jest.dontMock('child_process')
574
+ jest.dontMock('node:fs')
575
+ jest.dontMock('../lib/generators')
576
+ jest.dontMock('../lib/resolver')
577
+ jest.resetModules()
578
+ })
579
+
580
+ afterAll(() => {
581
+ process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
582
+ })
583
+
584
+ it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
585
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
586
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
587
+
588
+ jest.doMock('node:fs', () => {
589
+ const actual = jest.requireActual('node:fs')
590
+ return {
591
+ ...actual,
592
+ existsSync: jest.fn((candidate: string) =>
593
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
594
+ ),
595
+ unlinkSync: jest.fn(),
596
+ }
597
+ })
598
+ jest.doMock('../lib/generators', () => ({
599
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
600
+ }))
601
+ jest.doMock('../lib/resolver', () => ({
602
+ resolveEnvironment: () => ({
603
+ appDir: '/tmp/test-app',
604
+ rootDir: '/tmp/test-root',
605
+ }),
606
+ createResolver: () => ({}),
607
+ }))
608
+ jest.doMock('child_process', () => {
609
+ const { EventEmitter } = jest.requireActual('node:events')
610
+
611
+ const createChild = (
612
+ spawnargs: string[],
613
+ autoExit?: { code: number | null; signal?: NodeJS.Signals | null },
614
+ ) => {
615
+ const child = new EventEmitter() as any
616
+ child.stdout = new EventEmitter()
617
+ child.stderr = new EventEmitter()
618
+ child.spawnargs = spawnargs
619
+ child.killed = false
620
+ child.exitCode = null
621
+ child.signalCode = null
622
+ child.kill = jest.fn((signal: NodeJS.Signals = 'SIGTERM') => {
623
+ child.killed = true
624
+ if (child.exitCode !== null || child.signalCode !== null) {
625
+ return true
626
+ }
627
+ child.signalCode = signal
628
+ queueMicrotask(() => {
629
+ child.emit('exit', null, signal)
630
+ })
631
+ return true
632
+ })
633
+
634
+ if (autoExit) {
635
+ queueMicrotask(() => {
636
+ if (child.exitCode !== null || child.signalCode !== null) return
637
+ child.exitCode = autoExit.code
638
+ child.signalCode = autoExit.signal ?? null
639
+ child.emit('exit', child.exitCode, child.signalCode)
640
+ })
641
+ }
642
+
643
+ return child
644
+ }
645
+
646
+ return {
647
+ spawn: jest.fn((_command: string, args: string[]) => {
648
+ if (args[0]?.includes('next/dist/bin/next')) {
649
+ return createChild(['node', ...args])
650
+ }
651
+ if (args.slice(1).join(' ') === 'queue worker --all') {
652
+ return createChild(['node', ...args], { code: 0 })
653
+ }
654
+ return createChild(['node', ...args])
655
+ }),
656
+ }
657
+ })
658
+
659
+ const mercato = await import('../mercato')
660
+ mercato.registerCliModules([
661
+ {
662
+ id: 'events',
663
+ workers: [
664
+ {
665
+ queue: 'events',
666
+ concurrency: 1,
667
+ handler: jest.fn(),
668
+ },
669
+ ],
670
+ } as any,
671
+ ])
672
+
673
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
674
+
675
+ expect(exitCode).toBe(1)
676
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
677
+ '💥 Failed: [server] Queue worker exited unexpectedly with exit code 0.',
678
+ )
679
+
680
+ consoleErrorSpy.mockRestore()
681
+ consoleLogSpy.mockRestore()
682
+ })
683
+ })
@@ -217,6 +217,8 @@ const EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ep
217
217
  const EPHEMERAL_ENV_LOCK_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.lock')
218
218
  const LEGACY_EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.md')
219
219
  const EPHEMERAL_BUILD_CACHE_STATE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-build-cache.json')
220
+ const EPHEMERAL_CACHE_DB_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-cache.sqlite')
221
+ const EPHEMERAL_QUEUE_BASE_DIR = path.join(appDirectory, '.mercato', 'queue')
220
222
  const PLAYWRIGHT_INTEGRATION_CONFIG_PATH = '.ai/qa/tests/playwright.config.ts'
221
223
  const PLAYWRIGHT_RESULTS_JSON_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'test-results', 'results.json')
222
224
  const LEGACY_INTEGRATION_TEST_ROOT = path.join(projectRootDirectory, '.ai', 'qa', 'tests')
@@ -2855,9 +2857,13 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2855
2857
  const databaseHost = databaseContainer.getHost()
2856
2858
  const databasePort = databaseContainer.getMappedPort(5432)
2857
2859
  const databaseUrl = `postgres://${databaseUser}:${databasePassword}@${databaseHost}:${databasePort}/${databaseName}`
2860
+ await rm(EPHEMERAL_CACHE_DB_PATH, { force: true }).catch(() => undefined)
2861
+ await rm(EPHEMERAL_QUEUE_BASE_DIR, { recursive: true, force: true }).catch(() => undefined)
2858
2862
  const enterpriseModulesFlag = process.env.OM_ENABLE_ENTERPRISE_MODULES ?? 'false'
2859
2863
  const commandEnvironment = buildEnvironment({
2860
2864
  DATABASE_URL: databaseUrl,
2865
+ CACHE_STRATEGY: 'sqlite',
2866
+ CACHE_SQLITE_PATH: EPHEMERAL_CACHE_DB_PATH,
2861
2867
  BASE_URL: applicationBaseUrl,
2862
2868
  APP_URL: applicationBaseUrl,
2863
2869
  NEXT_PUBLIC_APP_URL: applicationBaseUrl,
@@ -2889,6 +2895,7 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2889
2895
  AUTO_SPAWN_SCHEDULER: 'false',
2890
2896
  OM_CLI_QUIET: '1',
2891
2897
  MERCATO_QUIET: '1',
2898
+ QUEUE_BASE_DIR: EPHEMERAL_QUEUE_BASE_DIR,
2892
2899
  NODE_NO_WARNINGS: '1',
2893
2900
  PORT: String(applicationPort),
2894
2901
  PW_CAPTURE_SCREENSHOTS: options.captureScreenshots ? '1' : '0',
package/src/mercato.ts CHANGED
@@ -305,6 +305,38 @@ function buildServerProcessEnvironment(environment: NodeJS.ProcessEnv): NodeJS.P
305
305
  return runtimeEnv
306
306
  }
307
307
 
308
+ type ManagedProcessExitResult = {
309
+ label: string
310
+ code: number | null
311
+ signal: NodeJS.Signals | null
312
+ }
313
+
314
+ function waitForManagedProcessExit(proc: ChildProcess, label: string): Promise<ManagedProcessExitResult> {
315
+ return new Promise((resolve) => {
316
+ proc.on('exit', (code, signal) => {
317
+ resolve({ label, code, signal })
318
+ })
319
+ })
320
+ }
321
+
322
+ function isExpectedManagedExitSignal(signal: NodeJS.Signals | null): boolean {
323
+ return signal === 'SIGINT' || signal === 'SIGTERM'
324
+ }
325
+
326
+ function formatManagedProcessExitStatus(result: ManagedProcessExitResult): string {
327
+ if (typeof result.code === 'number') {
328
+ return `exit code ${result.code}`
329
+ }
330
+ if (result.signal) {
331
+ return `signal ${result.signal}`
332
+ }
333
+ return 'an unknown status'
334
+ }
335
+
336
+ function createManagedProcessExitError(result: ManagedProcessExitResult): Error {
337
+ return new Error(`[server] ${result.label} exited unexpectedly with ${formatManagedProcessExitStatus(result)}.`)
338
+ }
339
+
308
340
  function ensureNextBuildIdInConfiguredDistDir(appDir: string): void {
309
341
  const configuredDistDir = path.join(appDir, '.mercato', 'next')
310
342
  const configuredBuildIdPath = path.join(configuredDistDir, 'BUILD_ID')
@@ -371,14 +403,14 @@ async function runModuleCommand(
371
403
  commandName: string,
372
404
  args: string[] = [],
373
405
  options: { optional?: boolean; silentOptional?: boolean } = {},
374
- ): Promise<void> {
406
+ ): Promise<boolean> {
375
407
  const mod = allModules.find((m) => m.id === moduleName)
376
408
  if (!mod) {
377
409
  if (options.optional) {
378
410
  if (!options.silentOptional) {
379
411
  console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module not enabled`)
380
412
  }
381
- return
413
+ return false
382
414
  }
383
415
  throw new Error(`Module not found: "${moduleName}"`)
384
416
  }
@@ -387,7 +419,7 @@ async function runModuleCommand(
387
419
  if (!options.silentOptional) {
388
420
  console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module has no CLI commands`)
389
421
  }
390
- return
422
+ return false
391
423
  }
392
424
  throw new Error(`Module "${moduleName}" has no CLI commands`)
393
425
  }
@@ -397,11 +429,12 @@ async function runModuleCommand(
397
429
  if (!options.silentOptional) {
398
430
  console.log(`⏭️ Skipping "${moduleName}:${commandName}" — command not found`)
399
431
  }
400
- return
432
+ return false
401
433
  }
402
434
  throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
403
435
  }
404
436
  await cmd.run(args)
437
+ return true
405
438
  }
406
439
 
407
440
  async function runPostGenerateStructuralCachePurge(quiet: boolean): Promise<void> {
@@ -760,8 +793,11 @@ export async function run(argv = process.argv) {
760
793
  console.log('✅ RBAC setup complete:', { tenantId, organizationId: orgId }, '\n')
761
794
 
762
795
  console.log('🎛️ Seeding feature toggle defaults...')
763
- await runModuleCommand(allModules, 'feature_toggles', 'seed-defaults', [])
764
- console.log('🎛️ ✅ Feature toggle defaults seeded\n')
796
+ if (await runModuleCommand(allModules, 'feature_toggles', 'seed-defaults', [], { optional: true })) {
797
+ console.log('🎛️ ✅ Feature toggle defaults seeded\n')
798
+ } else {
799
+ console.log('')
800
+ }
765
801
 
766
802
  if (tenantId) {
767
803
  console.log('👥 Seeding tenant-scoped roles...')
@@ -828,22 +864,31 @@ export async function run(argv = process.argv) {
828
864
  )
829
865
  const stressArgs = ['--tenant', tenantId, '--org', orgId, '--count', String(stressTestCount)]
830
866
  if (stressTestLite) stressArgs.push('--lite')
831
- await runModuleCommand(allModules, 'customers', 'seed-stresstest', stressArgs, { optional: true })
832
- console.log(`✅ Stress test customers seeded (requested ${stressTestCount})\n`)
867
+ if (await runModuleCommand(allModules, 'customers', 'seed-stresstest', stressArgs, { optional: true })) {
868
+ console.log(`✅ Stress test customers seeded (requested ${stressTestCount})\n`)
869
+ } else {
870
+ console.log('')
871
+ }
833
872
  }
834
873
 
835
874
  console.log('🧩 Enabling default dashboard widgets...')
836
- await runModuleCommand(allModules, 'dashboards', 'seed-defaults', ['--tenant', tenantId], { optional: true })
837
- console.log('✅ Dashboard widgets enabled\n')
875
+ if (await runModuleCommand(allModules, 'dashboards', 'seed-defaults', ['--tenant', tenantId], { optional: true })) {
876
+ console.log('✅ Dashboard widgets enabled\n')
877
+ } else {
878
+ console.log('')
879
+ }
838
880
 
839
881
  console.log('📊 Enabling analytics widgets for admin and employee roles...')
840
- await runModuleCommand(allModules, 'dashboards', 'enable-analytics-widgets', [
882
+ if (await runModuleCommand(allModules, 'dashboards', 'enable-analytics-widgets', [
841
883
  '--tenant',
842
884
  tenantId,
843
885
  '--roles',
844
886
  'admin,employee',
845
- ])
846
- console.log('✅ Analytics widgets enabled for roles\n')
887
+ ], { optional: true })) {
888
+ console.log('✅ Analytics widgets enabled for roles\n')
889
+ } else {
890
+ console.log('')
891
+ }
847
892
 
848
893
  } else {
849
894
  console.log('⚠️ Could not get organization ID or tenant ID, skipping seeding steps\n')
@@ -853,13 +898,19 @@ export async function run(argv = process.argv) {
853
898
  const vectorArgs = tenantId
854
899
  ? ['--tenant', tenantId, ...(orgId ? ['--org', orgId] : [])]
855
900
  : ['--purgeFirst=false']
856
- await runModuleCommand(allModules, 'search', 'reindex', vectorArgs, { optional: true })
857
- console.log('✅ Search indexes built\n')
901
+ if (await runModuleCommand(allModules, 'search', 'reindex', vectorArgs, { optional: true })) {
902
+ console.log('✅ Search indexes built\n')
903
+ } else {
904
+ console.log('')
905
+ }
858
906
 
859
907
  console.log('🔍 Rebuilding query indexes...')
860
908
  const queryIndexArgs = ['--force', ...(tenantId ? ['--tenant', tenantId] : [])]
861
- await runModuleCommand(allModules, 'query_index', 'reindex', queryIndexArgs, { optional: true })
862
- console.log('✅ Query indexes rebuilt\n')
909
+ if (await runModuleCommand(allModules, 'query_index', 'reindex', queryIndexArgs, { optional: true })) {
910
+ console.log('✅ Query indexes rebuilt\n')
911
+ } else {
912
+ console.log('')
913
+ }
863
914
 
864
915
  const adminPasswordOverride = derivedSecrets.adminPassword
865
916
  const employeePasswordOverride = derivedSecrets.employeePassword
@@ -1598,11 +1649,11 @@ export async function run(argv = process.argv) {
1598
1649
  const nextBin = resolveInstalledBinary(nodeModulesBases, 'next/dist/bin/next')
1599
1650
  const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1600
1651
 
1601
- const startNextDev = (): Promise<void> =>
1652
+ const startNextDev = (): Promise<ManagedProcessExitResult> =>
1602
1653
  new Promise((resolve) => {
1603
1654
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
1604
1655
  stdio: ['inherit', 'pipe', 'pipe'],
1605
- env: process.env,
1656
+ env: runtimeEnv,
1606
1657
  cwd: appDir,
1607
1658
  })
1608
1659
  processes.push(nextProcess)
@@ -1626,19 +1677,23 @@ export async function run(argv = process.argv) {
1626
1677
  appendOutput(text)
1627
1678
  })
1628
1679
 
1629
- nextProcess.on('exit', async () => {
1680
+ nextProcess.on('exit', async (code, signal) => {
1630
1681
  if (!didRetryCorruptedTurbopackCache && isTurbopackCacheCorruption(combinedOutput)) {
1631
1682
  didRetryCorruptedTurbopackCache = true
1632
1683
  console.log('[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...')
1633
1684
  removeTurbopackDevCache(appDir)
1634
- await startNextDev()
1635
- return resolve()
1685
+ return resolve(await startNextDev())
1636
1686
  }
1637
- resolve()
1687
+ resolve({
1688
+ label: 'Next.js dev server',
1689
+ code,
1690
+ signal,
1691
+ })
1638
1692
  })
1639
1693
  })
1640
1694
 
1641
1695
  const nextExitPromise = startNextDev()
1696
+ const managedExitPromises: Promise<ManagedProcessExitResult>[] = [nextExitPromise]
1642
1697
 
1643
1698
  // Start workers if enabled
1644
1699
  if (autoSpawnWorkers) {
@@ -1649,10 +1704,11 @@ export async function run(argv = process.argv) {
1649
1704
  console.log('[server] Starting workers for all queues...')
1650
1705
  const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1651
1706
  stdio: 'inherit',
1652
- env: process.env,
1707
+ env: runtimeEnv,
1653
1708
  cwd: appDir,
1654
1709
  })
1655
1710
  processes.push(workerProcess)
1711
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, 'Queue worker'))
1656
1712
  }
1657
1713
  }
1658
1714
 
@@ -1660,28 +1716,20 @@ export async function run(argv = process.argv) {
1660
1716
  console.log('[server] Starting scheduler polling engine...')
1661
1717
  const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1662
1718
  stdio: 'inherit',
1663
- env: process.env,
1719
+ env: runtimeEnv,
1664
1720
  cwd: appDir,
1665
1721
  })
1666
1722
  processes.push(schedulerProcess)
1723
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1667
1724
  }
1668
1725
 
1669
- // Wait for any process to exit
1670
- await Promise.race(
1671
- [
1672
- nextExitPromise,
1673
- ...processes
1674
- .filter((proc) => proc.spawnargs[1] !== nextBin)
1675
- .map(
1676
- (proc) =>
1677
- new Promise<void>((resolve) => {
1678
- proc.on('exit', () => resolve())
1679
- })
1680
- ),
1681
- ]
1682
- )
1726
+ const firstExit = await Promise.race(managedExitPromises)
1683
1727
 
1684
1728
  await cleanupAndWait()
1729
+
1730
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1731
+ throw createManagedProcessExitError(firstExit)
1732
+ }
1685
1733
  },
1686
1734
  },
1687
1735
  {