@open-mercato/cli 0.5.1-develop.2996.ce62fd491c → 0.5.1-develop.3036.f02c281f23

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.2996.ce62fd491c",
3
+ "version": "0.5.1-develop.3036.f02c281f23",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "@mikro-orm/decorators": "^7.0.13",
60
60
  "@mikro-orm/migrations": "^7.0.13",
61
61
  "@mikro-orm/postgresql": "^7.0.13",
62
- "@open-mercato/queue": "0.5.1-develop.2996.ce62fd491c",
63
- "@open-mercato/shared": "0.5.1-develop.2996.ce62fd491c",
62
+ "@open-mercato/queue": "0.5.1-develop.3036.f02c281f23",
63
+ "@open-mercato/shared": "0.5.1-develop.3036.f02c281f23",
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.2996.ce62fd491c"
73
+ "@open-mercato/shared": "0.5.1-develop.3036.f02c281f23"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.5.1-develop.2996.ce62fd491c",
76
+ "@open-mercato/shared": "0.5.1-develop.3036.f02c281f23",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.3.0",
79
79
  "ts-jest": "^29.4.9"
@@ -1,3 +1,4 @@
1
+ import type { Module } from '@open-mercato/shared/modules/registry'
1
2
  import {
2
3
  registerCliModules,
3
4
  getCliModules,
@@ -6,6 +7,61 @@ import {
6
7
  run,
7
8
  } from '../mercato'
8
9
 
10
+ type MockChildAutoExit = { code: number | null; signal?: NodeJS.Signals | null } | undefined
11
+ type MockChildSpawnRouter = (args: string[]) => MockChildAutoExit
12
+
13
+ function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
14
+ const { EventEmitter } = jest.requireActual('node:events')
15
+
16
+ const createChild = (spawnargs: string[], autoExit?: MockChildAutoExit) => {
17
+ const child = new EventEmitter() as any
18
+ child.stdout = new EventEmitter()
19
+ child.stderr = new EventEmitter()
20
+ child.spawnargs = spawnargs
21
+ child.killed = false
22
+ child.exitCode = null
23
+ child.signalCode = null
24
+ child.kill = jest.fn((signal: NodeJS.Signals = 'SIGTERM') => {
25
+ child.killed = true
26
+ if (child.exitCode !== null || child.signalCode !== null) {
27
+ return true
28
+ }
29
+ child.signalCode = signal
30
+ queueMicrotask(() => {
31
+ child.emit('exit', null, signal)
32
+ })
33
+ return true
34
+ })
35
+
36
+ if (autoExit) {
37
+ queueMicrotask(() => {
38
+ if (child.exitCode !== null || child.signalCode !== null) return
39
+ child.exitCode = autoExit.code
40
+ child.signalCode = autoExit.signal ?? null
41
+ child.emit('exit', child.exitCode, child.signalCode)
42
+ })
43
+ }
44
+
45
+ return child
46
+ }
47
+
48
+ return {
49
+ spawn: jest.fn((_command: string, args: string[]) => createChild(['node', ...args], routeAutoExit(args))),
50
+ }
51
+ }
52
+
53
+ const eventsWorkerFixture: Pick<Module, 'id' | 'workers'> = {
54
+ id: 'events',
55
+ workers: [
56
+ {
57
+ id: 'events.test-worker',
58
+ queue: 'events',
59
+ concurrency: 1,
60
+ handler: jest.fn(),
61
+ },
62
+ ],
63
+ }
64
+
9
65
  describe('mercato CLI module registration', () => {
10
66
  beforeEach(() => {
11
67
  // Reset module state by re-importing
@@ -562,11 +618,13 @@ describe('generate post-step structural cache purge', () => {
562
618
 
563
619
  describe('server dev managed process exits', () => {
564
620
  const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
621
+ const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
565
622
 
566
623
  beforeEach(() => {
567
624
  jest.restoreAllMocks()
568
625
  jest.resetModules()
569
626
  process.env.AUTO_SPAWN_SCHEDULER = 'false'
627
+ process.env.AUTO_SPAWN_WORKERS = 'true'
570
628
  })
571
629
 
572
630
  afterEach(() => {
@@ -579,6 +637,50 @@ describe('server dev managed process exits', () => {
579
637
 
580
638
  afterAll(() => {
581
639
  process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
640
+ process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
641
+ })
642
+
643
+ it('skips scheduler auto-start when the module is not enabled', async () => {
644
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
645
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
646
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
647
+ process.env.AUTO_SPAWN_WORKERS = 'false'
648
+
649
+ jest.doMock('node:fs', () => {
650
+ const actual = jest.requireActual('node:fs')
651
+ return {
652
+ ...actual,
653
+ existsSync: jest.fn((candidate: string) =>
654
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
655
+ ),
656
+ unlinkSync: jest.fn(),
657
+ }
658
+ })
659
+ jest.doMock('../lib/generators', () => ({
660
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
661
+ }))
662
+ jest.doMock('../lib/resolver', () => ({
663
+ resolveEnvironment: () => ({
664
+ appDir: '/tmp/test-app',
665
+ rootDir: '/tmp/test-root',
666
+ }),
667
+ createResolver: () => ({}),
668
+ }))
669
+ jest.doMock('child_process', () =>
670
+ buildMockChildProcessModule((args) =>
671
+ args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
672
+ ),
673
+ )
674
+
675
+ const mercato = await import('../mercato')
676
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
677
+
678
+ expect(exitCode).toBe(0)
679
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
680
+ expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
681
+
682
+ consoleErrorSpy.mockRestore()
683
+ consoleLogSpy.mockRestore()
582
684
  })
583
685
 
584
686
  it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
@@ -605,76 +707,138 @@ describe('server dev managed process exits', () => {
605
707
  }),
606
708
  createResolver: () => ({}),
607
709
  }))
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
- })
710
+ jest.doMock('child_process', () =>
711
+ buildMockChildProcessModule((args) => {
712
+ if (args.slice(1).join(' ') === 'queue worker --all') {
713
+ return { code: 0 }
641
714
  }
715
+ return undefined
716
+ }),
717
+ )
718
+
719
+ const mercato = await import('../mercato')
720
+ mercato.registerCliModules([eventsWorkerFixture as Module])
721
+
722
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
723
+
724
+ expect(exitCode).toBe(1)
725
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
726
+ '💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
727
+ )
728
+
729
+ consoleErrorSpy.mockRestore()
730
+ consoleLogSpy.mockRestore()
731
+ })
732
+ })
733
+
734
+ describe('server start managed process exits', () => {
735
+ const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
736
+ const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
642
737
 
643
- return child
738
+ beforeEach(() => {
739
+ jest.restoreAllMocks()
740
+ jest.resetModules()
741
+ process.env.AUTO_SPAWN_SCHEDULER = 'false'
742
+ process.env.AUTO_SPAWN_WORKERS = 'true'
743
+ })
744
+
745
+ afterEach(() => {
746
+ jest.dontMock('child_process')
747
+ jest.dontMock('node:fs')
748
+ jest.dontMock('../lib/resolver')
749
+ jest.dontMock('../lib/server-start-lock')
750
+ jest.resetModules()
751
+ })
752
+
753
+ afterAll(() => {
754
+ process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
755
+ process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
756
+ })
757
+
758
+ it('skips scheduler auto-start when the module is not enabled', async () => {
759
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
760
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
761
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
762
+ process.env.AUTO_SPAWN_WORKERS = 'false'
763
+
764
+ jest.doMock('node:fs', () => {
765
+ const actual = jest.requireActual('node:fs')
766
+ return {
767
+ ...actual,
768
+ existsSync: jest.fn((candidate: string) =>
769
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
770
+ ),
644
771
  }
772
+ })
773
+ jest.doMock('../lib/resolver', () => ({
774
+ resolveEnvironment: () => ({
775
+ appDir: '/tmp/test-app',
776
+ rootDir: '/tmp/test-root',
777
+ }),
778
+ }))
779
+ jest.doMock('../lib/server-start-lock', () => ({
780
+ acquireServerStartLock: jest.fn(() => ({
781
+ release: jest.fn(),
782
+ })),
783
+ }))
784
+ jest.doMock('child_process', () =>
785
+ buildMockChildProcessModule((args) =>
786
+ args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
787
+ ),
788
+ )
789
+
790
+ const mercato = await import('../mercato')
791
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
792
+
793
+ expect(exitCode).toBe(0)
794
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
795
+ expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
645
796
 
797
+ consoleErrorSpy.mockRestore()
798
+ consoleLogSpy.mockRestore()
799
+ })
800
+
801
+ it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
802
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
803
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
804
+
805
+ jest.doMock('node:fs', () => {
806
+ const actual = jest.requireActual('node:fs')
646
807
  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
- }),
808
+ ...actual,
809
+ existsSync: jest.fn((candidate: string) =>
810
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
811
+ ),
656
812
  }
657
813
  })
814
+ jest.doMock('../lib/resolver', () => ({
815
+ resolveEnvironment: () => ({
816
+ appDir: '/tmp/test-app',
817
+ rootDir: '/tmp/test-root',
818
+ }),
819
+ }))
820
+ jest.doMock('../lib/server-start-lock', () => ({
821
+ acquireServerStartLock: jest.fn(() => ({
822
+ release: jest.fn(),
823
+ })),
824
+ }))
825
+ jest.doMock('child_process', () =>
826
+ buildMockChildProcessModule((args) => {
827
+ if (args.slice(1).join(' ') === 'queue worker --all') {
828
+ return { code: 0 }
829
+ }
830
+ return undefined
831
+ }),
832
+ )
658
833
 
659
834
  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
- ])
835
+ mercato.registerCliModules([eventsWorkerFixture as Module])
672
836
 
673
- const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
837
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
674
838
 
675
839
  expect(exitCode).toBe(1)
676
840
  expect(consoleErrorSpy).toHaveBeenCalledWith(
677
- '💥 Failed: [server] Queue worker exited unexpectedly with exit code 0.',
841
+ '💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
678
842
  )
679
843
 
680
844
  consoleErrorSpy.mockRestore()
package/src/mercato.ts CHANGED
@@ -311,6 +311,16 @@ type ManagedProcessExitResult = {
311
311
  signal: NodeJS.Signals | null
312
312
  }
313
313
 
314
+ type ModuleCommandLookupResult =
315
+ | {
316
+ status: 'ok'
317
+ module: Module
318
+ command: NonNullable<Module['cli']>[number]
319
+ }
320
+ | {
321
+ status: 'missing-module' | 'missing-cli' | 'missing-command'
322
+ }
323
+
314
324
  function waitForManagedProcessExit(proc: ChildProcess, label: string): Promise<ManagedProcessExitResult> {
315
325
  return new Promise((resolve) => {
316
326
  proc.on('exit', (code, signal) => {
@@ -337,6 +347,50 @@ function createManagedProcessExitError(result: ManagedProcessExitResult): Error
337
347
  return new Error(`[server] ${result.label} exited unexpectedly with ${formatManagedProcessExitStatus(result)}.`)
338
348
  }
339
349
 
350
+ function formatQueueWorkerLabel(queueNames: string[]): string {
351
+ if (queueNames.length === 0) return 'Queue worker'
352
+ const sorted = [...queueNames].sort((a, b) => a.localeCompare(b))
353
+ const preview = sorted.length > 4 ? `${sorted.slice(0, 4).join(', ')}, +${sorted.length - 4} more` : sorted.join(', ')
354
+ return `Queue worker (${preview})`
355
+ }
356
+
357
+ function lookupModuleCommand(
358
+ allModules: Module[],
359
+ moduleName: string,
360
+ commandName: string,
361
+ ): ModuleCommandLookupResult {
362
+ const mod = allModules.find((entry) => entry.id === moduleName)
363
+ if (!mod) {
364
+ return { status: 'missing-module' }
365
+ }
366
+
367
+ if (!mod.cli || mod.cli.length === 0) {
368
+ return { status: 'missing-cli' }
369
+ }
370
+
371
+ const command = mod.cli.find((entry) => entry.command === commandName)
372
+ if (!command) {
373
+ return { status: 'missing-command' }
374
+ }
375
+
376
+ return {
377
+ status: 'ok',
378
+ module: mod,
379
+ command,
380
+ }
381
+ }
382
+
383
+ function describeMissingModuleCommand(result: Exclude<ModuleCommandLookupResult, { status: 'ok' }>): string {
384
+ switch (result.status) {
385
+ case 'missing-module':
386
+ return 'module not enabled'
387
+ case 'missing-cli':
388
+ return 'module has no CLI commands'
389
+ case 'missing-command':
390
+ return 'command not found'
391
+ }
392
+ }
393
+
340
394
  function ensureNextBuildIdInConfiguredDistDir(appDir: string): void {
341
395
  const configuredDistDir = path.join(appDir, '.mercato', 'next')
342
396
  const configuredBuildIdPath = path.join(configuredDistDir, 'BUILD_ID')
@@ -404,36 +458,25 @@ async function runModuleCommand(
404
458
  args: string[] = [],
405
459
  options: { optional?: boolean; silentOptional?: boolean } = {},
406
460
  ): Promise<boolean> {
407
- const mod = allModules.find((m) => m.id === moduleName)
408
- if (!mod) {
409
- if (options.optional) {
410
- if (!options.silentOptional) {
411
- console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module not enabled`)
412
- }
413
- return false
414
- }
415
- throw new Error(`Module not found: "${moduleName}"`)
416
- }
417
- if (!mod.cli || mod.cli.length === 0) {
461
+ const resolved = lookupModuleCommand(allModules, moduleName, commandName)
462
+ if (resolved.status !== 'ok') {
418
463
  if (options.optional) {
419
464
  if (!options.silentOptional) {
420
- console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module has no CLI commands`)
465
+ console.log(`⏭️ Skipping "${moduleName}:${commandName}" — ${describeMissingModuleCommand(resolved)}`)
421
466
  }
422
467
  return false
423
468
  }
424
- throw new Error(`Module "${moduleName}" has no CLI commands`)
425
- }
426
- const cmd = mod.cli.find((c) => c.command === commandName)
427
- if (!cmd) {
428
- if (options.optional) {
429
- if (!options.silentOptional) {
430
- console.log(`⏭️ Skipping "${moduleName}:${commandName}" command not found`)
431
- }
432
- return false
469
+ switch (resolved.status) {
470
+ case 'missing-module':
471
+ throw new Error(`Module not found: "${moduleName}"`)
472
+ case 'missing-cli':
473
+ throw new Error(`Module "${moduleName}" has no CLI commands`)
474
+ case 'missing-command':
475
+ throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
433
476
  }
434
- throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
435
477
  }
436
- await cmd.run(args)
478
+
479
+ await resolved.command.run(args)
437
480
  return true
438
481
  }
439
482
 
@@ -1603,8 +1646,8 @@ export async function run(argv = process.argv) {
1603
1646
  const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
1604
1647
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== 'false'
1605
1648
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1606
- const runtimeEnv = buildServerProcessEnvironment(process.env)
1607
1649
  let didRetryCorruptedTurbopackCache = false
1650
+ const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
1608
1651
 
1609
1652
  function cleanup() {
1610
1653
  console.log('[server] Shutting down...')
@@ -1622,7 +1665,7 @@ export async function run(argv = process.argv) {
1622
1665
  processes.map(
1623
1666
  (proc) =>
1624
1667
  new Promise<void>((resolve) => {
1625
- if (proc.exitCode !== null) return resolve()
1668
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve()
1626
1669
  proc.on('exit', () => resolve())
1627
1670
  })
1628
1671
  )
@@ -1653,7 +1696,7 @@ export async function run(argv = process.argv) {
1653
1696
  new Promise((resolve) => {
1654
1697
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
1655
1698
  stdio: ['inherit', 'pipe', 'pipe'],
1656
- env: runtimeEnv,
1699
+ env: process.env,
1657
1700
  cwd: appDir,
1658
1701
  })
1659
1702
  processes.push(nextProcess)
@@ -1704,23 +1747,27 @@ export async function run(argv = process.argv) {
1704
1747
  console.log('[server] Starting workers for all queues...')
1705
1748
  const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1706
1749
  stdio: 'inherit',
1707
- env: runtimeEnv,
1750
+ env: process.env,
1708
1751
  cwd: appDir,
1709
1752
  })
1710
1753
  processes.push(workerProcess)
1711
- managedExitPromises.push(waitForManagedProcessExit(workerProcess, 'Queue worker'))
1754
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
1712
1755
  }
1713
1756
  }
1714
1757
 
1715
1758
  if (autoSpawnScheduler && queueStrategy === 'local') {
1716
- console.log('[server] Starting scheduler polling engine...')
1717
- const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1718
- stdio: 'inherit',
1719
- env: runtimeEnv,
1720
- cwd: appDir,
1721
- })
1722
- processes.push(schedulerProcess)
1723
- managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1759
+ if (schedulerCommand.status !== 'ok') {
1760
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
1761
+ } else {
1762
+ console.log('[server] Starting scheduler polling engine...')
1763
+ const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1764
+ stdio: 'inherit',
1765
+ env: process.env,
1766
+ cwd: appDir,
1767
+ })
1768
+ processes.push(schedulerProcess)
1769
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1770
+ }
1724
1771
  }
1725
1772
 
1726
1773
  const firstExit = await Promise.race(managedExitPromises)
@@ -1746,6 +1793,7 @@ export async function run(argv = process.argv) {
1746
1793
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== 'false'
1747
1794
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1748
1795
  const runtimeEnv = buildServerProcessEnvironment(process.env)
1796
+ const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
1749
1797
  const serverStartLock = acquireServerStartLock(appDir, {
1750
1798
  port: runtimeEnv.PORT ?? process.env.PORT ?? null,
1751
1799
  })
@@ -1753,7 +1801,7 @@ export async function run(argv = process.argv) {
1753
1801
  function cleanup() {
1754
1802
  console.log('[server] Shutting down...')
1755
1803
  for (const proc of processes) {
1756
- if (!proc.killed) {
1804
+ if (!proc.killed && proc.exitCode === null && proc.signalCode === null) {
1757
1805
  proc.kill('SIGTERM')
1758
1806
  }
1759
1807
  }
@@ -1765,7 +1813,7 @@ export async function run(argv = process.argv) {
1765
1813
  processes.map(
1766
1814
  (proc) =>
1767
1815
  new Promise<void>((resolve) => {
1768
- if (proc.exitCode !== null) return resolve()
1816
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve()
1769
1817
  proc.on('exit', () => resolve())
1770
1818
  })
1771
1819
  )
@@ -1789,6 +1837,9 @@ export async function run(argv = process.argv) {
1789
1837
  cwd: appDir,
1790
1838
  })
1791
1839
  processes.push(nextProcess)
1840
+ const managedExitPromises: Promise<ManagedProcessExitResult>[] = [
1841
+ waitForManagedProcessExit(nextProcess, 'Next.js production server'),
1842
+ ]
1792
1843
 
1793
1844
  // Start workers if enabled
1794
1845
  if (autoSpawnWorkers) {
@@ -1803,30 +1854,32 @@ export async function run(argv = process.argv) {
1803
1854
  cwd: appDir,
1804
1855
  })
1805
1856
  processes.push(workerProcess)
1857
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
1806
1858
  }
1807
1859
  }
1808
1860
 
1809
1861
  if (autoSpawnScheduler && queueStrategy === 'local') {
1810
- console.log('[server] Starting scheduler polling engine...')
1811
- const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1812
- stdio: 'inherit',
1813
- env: runtimeEnv,
1814
- cwd: appDir,
1815
- })
1816
- processes.push(schedulerProcess)
1862
+ if (schedulerCommand.status !== 'ok') {
1863
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
1864
+ } else {
1865
+ console.log('[server] Starting scheduler polling engine...')
1866
+ const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1867
+ stdio: 'inherit',
1868
+ env: runtimeEnv,
1869
+ cwd: appDir,
1870
+ })
1871
+ processes.push(schedulerProcess)
1872
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1873
+ }
1817
1874
  }
1818
1875
 
1819
- // Wait for any process to exit
1820
- await Promise.race(
1821
- processes.map(
1822
- (proc) =>
1823
- new Promise<void>((resolve) => {
1824
- proc.on('exit', () => resolve())
1825
- })
1826
- )
1827
- )
1876
+ const firstExit = await Promise.race(managedExitPromises)
1828
1877
 
1829
1878
  await cleanupAndWait()
1879
+
1880
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1881
+ throw createManagedProcessExitError(firstExit)
1882
+ }
1830
1883
  } finally {
1831
1884
  serverStartLock.release()
1832
1885
  }