@open-mercato/cli 0.5.1-develop.3032.01699048cb → 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/dist/mercato.js +85 -52
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +221 -57
- package/src/mercato.ts +107 -54
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
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.
|
|
63
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
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.
|
|
73
|
+
"@open-mercato/shared": "0.5.1-develop.3036.f02c281f23"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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', '
|
|
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
|
|
408
|
-
if (
|
|
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}" —
|
|
465
|
+
console.log(`⏭️ Skipping "${moduleName}:${commandName}" — ${describeMissingModuleCommand(resolved)}`)
|
|
421
466
|
}
|
|
422
467
|
return false
|
|
423
468
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1750
|
+
env: process.env,
|
|
1708
1751
|
cwd: appDir,
|
|
1709
1752
|
})
|
|
1710
1753
|
processes.push(workerProcess)
|
|
1711
|
-
managedExitPromises.push(waitForManagedProcessExit(workerProcess,
|
|
1754
|
+
managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
|
|
1712
1755
|
}
|
|
1713
1756
|
}
|
|
1714
1757
|
|
|
1715
1758
|
if (autoSpawnScheduler && queueStrategy === 'local') {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
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
|
-
|
|
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
|
}
|