@open-mercato/cli 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1

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.6.1-develop.3291.1.6fad645fd0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "@mikro-orm/decorators": "^7.0.15",
60
60
  "@mikro-orm/migrations": "^7.0.15",
61
61
  "@mikro-orm/postgresql": "^7.0.15",
62
- "@open-mercato/queue": "0.6.1-develop.3291.1.6fad645fd0",
63
- "@open-mercato/shared": "0.6.1-develop.3291.1.6fad645fd0",
62
+ "@open-mercato/queue": "0.6.1",
63
+ "@open-mercato/shared": "0.6.1",
64
64
  "cross-spawn": "^7.0.6",
65
65
  "pg": "8.20.0",
66
66
  "semver": "^7.8.0",
@@ -70,10 +70,10 @@
70
70
  "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.6.1-develop.3291.1.6fad645fd0"
73
+ "@open-mercato/shared": "0.6.1"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.6.1-develop.3291.1.6fad645fd0",
76
+ "@open-mercato/shared": "0.6.1",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.4.2",
79
79
  "ts-jest": "^29.4.9"
@@ -85,6 +85,5 @@
85
85
  "type": "git",
86
86
  "url": "https://github.com/open-mercato/open-mercato",
87
87
  "directory": "packages/cli"
88
- },
89
- "stableVersion": "0.6.0"
88
+ }
90
89
  }
@@ -62,6 +62,16 @@ const eventsWorkerFixture: Pick<Module, 'id' | 'workers'> = {
62
62
  ],
63
63
  }
64
64
 
65
+ const schedulerCliFixture: Pick<Module, 'id' | 'cli'> = {
66
+ id: 'scheduler',
67
+ cli: [
68
+ {
69
+ command: 'start',
70
+ run: jest.fn(),
71
+ },
72
+ ],
73
+ }
74
+
65
75
  describe('mercato CLI module registration', () => {
66
76
  beforeEach(() => {
67
77
  // Reset module state by re-importing
@@ -620,6 +630,7 @@ describe('server dev managed process exits', () => {
620
630
  const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
621
631
  const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
622
632
  const originalLazy = process.env.OM_AUTO_SPAWN_WORKERS_LAZY
633
+ const originalLazyScheduler = process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
623
634
 
624
635
  beforeEach(() => {
625
636
  jest.restoreAllMocks()
@@ -627,6 +638,7 @@ describe('server dev managed process exits', () => {
627
638
  process.env.AUTO_SPAWN_SCHEDULER = 'false'
628
639
  process.env.AUTO_SPAWN_WORKERS = 'true'
629
640
  delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
641
+ delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
630
642
  })
631
643
 
632
644
  afterEach(() => {
@@ -636,8 +648,10 @@ describe('server dev managed process exits', () => {
636
648
  jest.dontMock('../lib/generators')
637
649
  jest.dontMock('../lib/resolver')
638
650
  jest.dontMock('../lib/queue-worker-supervisor')
651
+ jest.dontMock('../lib/scheduler-supervisor')
639
652
  jest.resetModules()
640
653
  delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
654
+ delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
641
655
  })
642
656
 
643
657
  afterAll(() => {
@@ -645,6 +659,8 @@ describe('server dev managed process exits', () => {
645
659
  process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
646
660
  if (originalLazy === undefined) delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
647
661
  else process.env.OM_AUTO_SPAWN_WORKERS_LAZY = originalLazy
662
+ if (originalLazyScheduler === undefined) delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
663
+ else process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY = originalLazyScheduler
648
664
  })
649
665
 
650
666
  it('skips scheduler auto-start when the module is not enabled', async () => {
@@ -801,12 +817,79 @@ describe('server dev managed process exits', () => {
801
817
  consoleErrorSpy.mockRestore()
802
818
  consoleLogSpy.mockRestore()
803
819
  })
820
+
821
+ it('starts the lazy scheduler supervisor instead of the scheduler process when OM_AUTO_SPAWN_SCHEDULER_LAZY=true', async () => {
822
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
823
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
824
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
825
+ process.env.AUTO_SPAWN_WORKERS = 'false'
826
+ process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY = 'true'
827
+
828
+ jest.doMock('node:fs', () => {
829
+ const actual = jest.requireActual('node:fs')
830
+ return {
831
+ ...actual,
832
+ existsSync: jest.fn((candidate: string) =>
833
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
834
+ ),
835
+ unlinkSync: jest.fn(),
836
+ }
837
+ })
838
+ jest.doMock('../lib/generators', () => ({
839
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
840
+ }))
841
+ jest.doMock('../lib/resolver', () => ({
842
+ resolveEnvironment: () => ({
843
+ appDir: '/tmp/test-app',
844
+ rootDir: '/tmp/test-root',
845
+ }),
846
+ createResolver: () => ({}),
847
+ }))
848
+
849
+ const schedulerClose = jest.fn().mockResolvedValue(undefined)
850
+ const startLazySchedulerSupervisor = jest.fn(() => ({
851
+ started: false,
852
+ getActiveChild: () => undefined,
853
+ close: schedulerClose,
854
+ done: Promise.resolve(),
855
+ }))
856
+ jest.doMock('../lib/scheduler-supervisor', () => ({
857
+ startLazySchedulerSupervisor,
858
+ }))
859
+
860
+ jest.doMock('child_process', () =>
861
+ buildMockChildProcessModule((args) =>
862
+ args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
863
+ ),
864
+ )
865
+
866
+ const mercado = await import('../mercato')
867
+ mercado.registerCliModules([schedulerCliFixture as Module])
868
+
869
+ const exitCode = await mercado.run(['node', 'mercato', 'server', 'dev'])
870
+
871
+ expect(exitCode).toBe(0)
872
+ expect(startLazySchedulerSupervisor).toHaveBeenCalledTimes(1)
873
+ const supervisorCall = startLazySchedulerSupervisor.mock.calls[0][0]
874
+ expect(supervisorCall.pollMs).toBe(1000)
875
+ expect(supervisorCall.restartOnUnexpectedExit).toBe(true)
876
+ expect(schedulerClose).toHaveBeenCalled()
877
+
878
+ const { spawn } = await import('child_process')
879
+ const allSpawnCalls = (spawn as jest.Mock).mock.calls.map((call) => call[1] as string[])
880
+ const schedulerSpawn = allSpawnCalls.find((args) => args.slice(1).join(' ') === 'scheduler start')
881
+ expect(schedulerSpawn).toBeUndefined()
882
+
883
+ consoleErrorSpy.mockRestore()
884
+ consoleLogSpy.mockRestore()
885
+ })
804
886
  })
805
887
 
806
888
  describe('server start managed process exits', () => {
807
889
  const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
808
890
  const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
809
891
  const originalLazy = process.env.OM_AUTO_SPAWN_WORKERS_LAZY
892
+ const originalLazyScheduler = process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
810
893
 
811
894
  beforeEach(() => {
812
895
  jest.restoreAllMocks()
@@ -814,6 +897,7 @@ describe('server start managed process exits', () => {
814
897
  process.env.AUTO_SPAWN_SCHEDULER = 'false'
815
898
  process.env.AUTO_SPAWN_WORKERS = 'true'
816
899
  delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
900
+ delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
817
901
  })
818
902
 
819
903
  afterEach(() => {
@@ -822,8 +906,10 @@ describe('server start managed process exits', () => {
822
906
  jest.dontMock('../lib/resolver')
823
907
  jest.dontMock('../lib/server-start-lock')
824
908
  jest.dontMock('../lib/queue-worker-supervisor')
909
+ jest.dontMock('../lib/scheduler-supervisor')
825
910
  jest.resetModules()
826
911
  delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
912
+ delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
827
913
  })
828
914
 
829
915
  afterAll(() => {
@@ -831,6 +917,8 @@ describe('server start managed process exits', () => {
831
917
  process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
832
918
  if (originalLazy === undefined) delete process.env.OM_AUTO_SPAWN_WORKERS_LAZY
833
919
  else process.env.OM_AUTO_SPAWN_WORKERS_LAZY = originalLazy
920
+ if (originalLazyScheduler === undefined) delete process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY
921
+ else process.env.OM_AUTO_SPAWN_SCHEDULER_LAZY = originalLazyScheduler
834
922
  })
835
923
 
836
924
  it('skips scheduler auto-start when the module is not enabled', async () => {
@@ -0,0 +1,63 @@
1
+ import {
2
+ resolveAutoSpawnSchedulerEnabled,
3
+ resolveAutoSpawnSchedulerLazy,
4
+ resolveAutoSpawnSchedulerMode,
5
+ resolveLazySchedulerPollMs,
6
+ resolveLazySchedulerRestart,
7
+ } from '../auto-spawn-scheduler'
8
+
9
+ describe('auto scheduler spawn env resolution', () => {
10
+ it('defaults to eager scheduler auto-spawn', () => {
11
+ expect(resolveAutoSpawnSchedulerMode({})).toBe('eager')
12
+ expect(resolveAutoSpawnSchedulerEnabled({})).toBe(true)
13
+ expect(resolveAutoSpawnSchedulerLazy({})).toBe(false)
14
+ })
15
+
16
+ it('returns off when AUTO_SPAWN_SCHEDULER=false', () => {
17
+ expect(resolveAutoSpawnSchedulerMode({ AUTO_SPAWN_SCHEDULER: 'false' })).toBe('off')
18
+ })
19
+
20
+ it('returns off when OM_AUTO_SPAWN_SCHEDULER=false and legacy unset', () => {
21
+ expect(resolveAutoSpawnSchedulerMode({ OM_AUTO_SPAWN_SCHEDULER: 'false' })).toBe('off')
22
+ })
23
+
24
+ it('legacy AUTO_SPAWN_SCHEDULER=true wins over OM_AUTO_SPAWN_SCHEDULER=false', () => {
25
+ expect(resolveAutoSpawnSchedulerMode({
26
+ AUTO_SPAWN_SCHEDULER: 'true',
27
+ OM_AUTO_SPAWN_SCHEDULER: 'false',
28
+ })).toBe('eager')
29
+ })
30
+
31
+ it('returns lazy when scheduler is enabled and OM_AUTO_SPAWN_SCHEDULER_LAZY=true', () => {
32
+ expect(resolveAutoSpawnSchedulerMode({
33
+ AUTO_SPAWN_SCHEDULER: 'true',
34
+ OM_AUTO_SPAWN_SCHEDULER_LAZY: 'true',
35
+ })).toBe('lazy')
36
+ })
37
+
38
+ it('lazy is ignored when AUTO_SPAWN_SCHEDULER=false', () => {
39
+ expect(resolveAutoSpawnSchedulerMode({
40
+ AUTO_SPAWN_SCHEDULER: 'false',
41
+ OM_AUTO_SPAWN_SCHEDULER_LAZY: 'true',
42
+ })).toBe('off')
43
+ })
44
+
45
+ it('treats invalid AUTO_SPAWN_SCHEDULER values as default', () => {
46
+ expect(resolveAutoSpawnSchedulerMode({ AUTO_SPAWN_SCHEDULER: 'maybe' })).toBe('eager')
47
+ })
48
+
49
+ it('resolves and clamps the lazy scheduler poll interval', () => {
50
+ expect(resolveLazySchedulerPollMs({})).toBe(1000)
51
+ expect(resolveLazySchedulerPollMs({ OM_AUTO_SPAWN_SCHEDULER_LAZY_POLL_MS: '500' })).toBe(500)
52
+ expect(resolveLazySchedulerPollMs({ OM_AUTO_SPAWN_SCHEDULER_LAZY_POLL_MS: '50' })).toBe(250)
53
+ expect(resolveLazySchedulerPollMs({ OM_AUTO_SPAWN_SCHEDULER_LAZY_POLL_MS: 'abc' })).toBe(1000)
54
+ expect(resolveLazySchedulerPollMs({ OM_AUTO_SPAWN_SCHEDULER_LAZY_POLL_MS: '0' })).toBe(1000)
55
+ })
56
+
57
+ it('defaults lazy scheduler restart to enabled', () => {
58
+ expect(resolveLazySchedulerRestart({})).toBe(true)
59
+ expect(resolveLazySchedulerRestart({ OM_AUTO_SPAWN_SCHEDULER_LAZY_RESTART: 'false' })).toBe(false)
60
+ expect(resolveLazySchedulerRestart({ OM_AUTO_SPAWN_SCHEDULER_LAZY_RESTART: 'true' })).toBe(true)
61
+ expect(resolveLazySchedulerRestart({ OM_AUTO_SPAWN_SCHEDULER_LAZY_RESTART: 'maybe' })).toBe(true)
62
+ })
63
+ })
@@ -0,0 +1,199 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import type { ChildProcess } from 'node:child_process'
3
+ import {
4
+ startLazySchedulerSupervisor,
5
+ type LazySchedulerSupervisorProbeFn,
6
+ type LazySchedulerSupervisorSpawnFn,
7
+ } from '../scheduler-supervisor'
8
+
9
+ type FakeChild = ChildProcess & {
10
+ triggerExit: (code: number | null, signal?: NodeJS.Signals | null) => void
11
+ }
12
+
13
+ function createFakeChild(): FakeChild {
14
+ const child = new EventEmitter() as unknown as FakeChild
15
+ ;(child as any).stdout = null
16
+ ;(child as any).stderr = null
17
+ ;(child as any).pid = Math.floor(Math.random() * 100000) + 1
18
+ ;(child as any).killed = false
19
+ ;(child as any).exitCode = null
20
+ ;(child as any).signalCode = null
21
+ ;(child as any).kill = jest.fn((signal: NodeJS.Signals = 'SIGTERM') => {
22
+ if ((child as any).killed) return true
23
+ ;(child as any).killed = true
24
+ ;(child as any).signalCode = signal
25
+ process.nextTick(() => child.emit('exit', null, signal))
26
+ return true
27
+ })
28
+ child.triggerExit = (code, signal = null) => {
29
+ ;(child as any).exitCode = code
30
+ ;(child as any).signalCode = signal
31
+ child.emit('exit', code, signal)
32
+ }
33
+ return child
34
+ }
35
+
36
+ const silentLogger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() }
37
+
38
+ async function flushAsync(times = 5): Promise<void> {
39
+ for (let i = 0; i < times; i++) {
40
+ // eslint-disable-next-line no-await-in-loop
41
+ await Promise.resolve()
42
+ }
43
+ }
44
+
45
+ describe('startLazySchedulerSupervisor', () => {
46
+ it('does not spawn the scheduler while no enabled schedules exist', async () => {
47
+ const spawnFn = jest.fn() as unknown as jest.MockedFunction<LazySchedulerSupervisorSpawnFn>
48
+ const probeFn = jest.fn(async () => ({
49
+ enabledSchedules: 0,
50
+ dueSchedules: 0,
51
+ error: false,
52
+ })) as jest.MockedFunction<LazySchedulerSupervisorProbeFn>
53
+
54
+ const handle = startLazySchedulerSupervisor({
55
+ mercatoBin: '/tmp/mercato',
56
+ appDir: '/tmp/app',
57
+ runtimeEnv: { ...process.env },
58
+ pollMs: 250,
59
+ restartOnUnexpectedExit: true,
60
+ spawnFn,
61
+ probeFn,
62
+ logger: silentLogger,
63
+ })
64
+
65
+ await flushAsync(20)
66
+ expect(probeFn).toHaveBeenCalled()
67
+ expect(spawnFn).not.toHaveBeenCalled()
68
+ expect(handle.started).toBe(false)
69
+
70
+ await handle.close()
71
+ })
72
+
73
+ it('spawns the existing scheduler command after an enabled schedule appears', async () => {
74
+ const child = createFakeChild()
75
+ const spawnFn = jest.fn(() => child) as unknown as jest.MockedFunction<LazySchedulerSupervisorSpawnFn>
76
+ const probeFn = jest.fn(async () => ({
77
+ enabledSchedules: 1,
78
+ dueSchedules: 0,
79
+ error: false,
80
+ })) as jest.MockedFunction<LazySchedulerSupervisorProbeFn>
81
+
82
+ const handle = startLazySchedulerSupervisor({
83
+ mercatoBin: '/tmp/mercato',
84
+ appDir: '/tmp/app',
85
+ runtimeEnv: { ...process.env },
86
+ pollMs: 250,
87
+ restartOnUnexpectedExit: false,
88
+ spawnFn,
89
+ probeFn,
90
+ logger: silentLogger,
91
+ })
92
+
93
+ await flushAsync(20)
94
+
95
+ expect(spawnFn).toHaveBeenCalledTimes(1)
96
+ const [command, args] = spawnFn.mock.calls[0]
97
+ expect(command).toBe('node')
98
+ expect(args).toEqual(['/tmp/mercato', 'scheduler', 'start'])
99
+ expect(handle.started).toBe(true)
100
+
101
+ await flushAsync(20)
102
+ expect(spawnFn).toHaveBeenCalledTimes(1)
103
+
104
+ await handle.close()
105
+ })
106
+
107
+ it('does not spawn when the probe reports an error', async () => {
108
+ const spawnFn = jest.fn() as unknown as jest.MockedFunction<LazySchedulerSupervisorSpawnFn>
109
+ const probeFn = jest.fn(async () => ({
110
+ enabledSchedules: 0,
111
+ dueSchedules: 0,
112
+ error: true,
113
+ errorMessage: 'database unavailable',
114
+ })) as jest.MockedFunction<LazySchedulerSupervisorProbeFn>
115
+
116
+ const handle = startLazySchedulerSupervisor({
117
+ mercatoBin: '/tmp/mercato',
118
+ appDir: '/tmp/app',
119
+ runtimeEnv: { ...process.env },
120
+ pollMs: 250,
121
+ restartOnUnexpectedExit: true,
122
+ spawnFn,
123
+ probeFn,
124
+ logger: silentLogger,
125
+ })
126
+
127
+ await flushAsync(20)
128
+ expect(spawnFn).not.toHaveBeenCalled()
129
+
130
+ await handle.close()
131
+ })
132
+
133
+ it('terminates the scheduler child on close()', async () => {
134
+ const child = createFakeChild()
135
+ const spawnFn = jest.fn(() => child) as unknown as jest.MockedFunction<LazySchedulerSupervisorSpawnFn>
136
+ const probeFn = jest.fn(async () => ({
137
+ enabledSchedules: 1,
138
+ dueSchedules: 1,
139
+ error: false,
140
+ })) as jest.MockedFunction<LazySchedulerSupervisorProbeFn>
141
+
142
+ const handle = startLazySchedulerSupervisor({
143
+ mercatoBin: '/tmp/mercato',
144
+ appDir: '/tmp/app',
145
+ runtimeEnv: { ...process.env },
146
+ pollMs: 250,
147
+ restartOnUnexpectedExit: true,
148
+ spawnFn,
149
+ probeFn,
150
+ logger: silentLogger,
151
+ })
152
+
153
+ await flushAsync(20)
154
+ await handle.close()
155
+
156
+ expect((child as any).kill).toHaveBeenCalledWith('SIGTERM')
157
+ })
158
+
159
+ it('restarts an unexpectedly exited scheduler only while schedules still exist', async () => {
160
+ const firstChild = createFakeChild()
161
+ const secondChild = createFakeChild()
162
+ const spawnFn = jest
163
+ .fn()
164
+ .mockImplementationOnce(() => firstChild)
165
+ .mockImplementationOnce(() => secondChild) as unknown as jest.MockedFunction<LazySchedulerSupervisorSpawnFn>
166
+
167
+ let enabledSchedules = 1
168
+ const probeFn = jest.fn(async () => ({
169
+ enabledSchedules,
170
+ dueSchedules: enabledSchedules,
171
+ error: false,
172
+ })) as jest.MockedFunction<LazySchedulerSupervisorProbeFn>
173
+
174
+ const handle = startLazySchedulerSupervisor({
175
+ mercatoBin: '/tmp/mercato',
176
+ appDir: '/tmp/app',
177
+ runtimeEnv: { ...process.env },
178
+ pollMs: 250,
179
+ restartOnUnexpectedExit: true,
180
+ spawnFn,
181
+ probeFn,
182
+ logger: silentLogger,
183
+ })
184
+
185
+ await flushAsync(20)
186
+ expect(spawnFn).toHaveBeenCalledTimes(1)
187
+
188
+ firstChild.triggerExit(1, null)
189
+ await flushAsync(40)
190
+ expect(spawnFn).toHaveBeenCalledTimes(2)
191
+
192
+ enabledSchedules = 0
193
+ secondChild.triggerExit(1, null)
194
+ await flushAsync(40)
195
+ expect(spawnFn).toHaveBeenCalledTimes(2)
196
+
197
+ await handle.close()
198
+ })
199
+ })
@@ -0,0 +1,39 @@
1
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
2
+
3
+ export type AutoSpawnSchedulerMode = 'off' | 'eager' | 'lazy'
4
+
5
+ export type AutoSpawnSchedulerEnvSource = Pick<NodeJS.ProcessEnv, string> | Record<string, string | undefined>
6
+
7
+ const DEFAULT_LAZY_SCHEDULER_POLL_MS = 1000
8
+ const MIN_LAZY_SCHEDULER_POLL_MS = 250
9
+
10
+ export function resolveAutoSpawnSchedulerEnabled(env: AutoSpawnSchedulerEnvSource = process.env): boolean {
11
+ const legacy = parseBooleanToken(env.AUTO_SPAWN_SCHEDULER)
12
+ if (legacy !== null) return legacy
13
+ const aliased = parseBooleanToken(env.OM_AUTO_SPAWN_SCHEDULER)
14
+ if (aliased !== null) return aliased
15
+ return true
16
+ }
17
+
18
+ export function resolveAutoSpawnSchedulerLazy(env: AutoSpawnSchedulerEnvSource = process.env): boolean {
19
+ return parseBooleanToken(env.OM_AUTO_SPAWN_SCHEDULER_LAZY) === true
20
+ }
21
+
22
+ export function resolveAutoSpawnSchedulerMode(env: AutoSpawnSchedulerEnvSource = process.env): AutoSpawnSchedulerMode {
23
+ if (!resolveAutoSpawnSchedulerEnabled(env)) return 'off'
24
+ if (resolveAutoSpawnSchedulerLazy(env)) return 'lazy'
25
+ return 'eager'
26
+ }
27
+
28
+ export function resolveLazySchedulerPollMs(env: AutoSpawnSchedulerEnvSource = process.env): number {
29
+ const raw = env.OM_AUTO_SPAWN_SCHEDULER_LAZY_POLL_MS
30
+ if (typeof raw !== 'string' || raw.trim() === '') return DEFAULT_LAZY_SCHEDULER_POLL_MS
31
+ const parsed = Number(raw)
32
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_LAZY_SCHEDULER_POLL_MS
33
+ return Math.max(MIN_LAZY_SCHEDULER_POLL_MS, Math.floor(parsed))
34
+ }
35
+
36
+ export function resolveLazySchedulerRestart(env: AutoSpawnSchedulerEnvSource = process.env): boolean {
37
+ const parsed = parseBooleanToken(env.OM_AUTO_SPAWN_SCHEDULER_LAZY_RESTART)
38
+ return parsed === null ? true : parsed
39
+ }