@open-mercato/cli 0.6.3 → 0.6.4-canary.3910.1.0ff5e9e9be

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.3",
3
+ "version": "0.6.4-canary.3910.1.0ff5e9e9be",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,21 +59,21 @@
59
59
  "@mikro-orm/decorators": "^7.1.1",
60
60
  "@mikro-orm/migrations": "^7.1.1",
61
61
  "@mikro-orm/postgresql": "^7.1.1",
62
- "@open-mercato/queue": "0.6.3",
63
- "@open-mercato/shared": "0.6.3",
62
+ "@open-mercato/queue": "0.6.4-canary.3910.1.0ff5e9e9be",
63
+ "@open-mercato/shared": "0.6.4-canary.3910.1.0ff5e9e9be",
64
64
  "cross-spawn": "^7.0.6",
65
65
  "pg": "8.21.0",
66
66
  "semver": "^7.8.1",
67
- "testcontainers": "^12.0.1",
67
+ "testcontainers": "^12.0.0",
68
68
  "ts-morph": "^28.0.0",
69
69
  "tsx": "^4.22.3",
70
70
  "typescript": "^6.0.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.6.3"
73
+ "@open-mercato/shared": "0.6.4-canary.3910.1.0ff5e9e9be"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.6.3",
76
+ "@open-mercato/shared": "0.6.4-canary.3910.1.0ff5e9e9be",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.4.2",
79
79
  "ts-jest": "^29.4.11"
@@ -85,5 +85,6 @@
85
85
  "type": "git",
86
86
  "url": "https://github.com/open-mercato/open-mercato",
87
87
  "directory": "packages/cli"
88
- }
88
+ },
89
+ "stableVersion": "0.6.3"
89
90
  }
@@ -37,18 +37,9 @@ function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
37
37
  if (autoExit) {
38
38
  queueMicrotask(() => {
39
39
  if (child.exitCode !== null || child.signalCode !== null) return
40
- if (pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
41
- child.stdout.emit('data', '✓ Ready in 1ms\n')
42
- }
43
- queueMicrotask(() => {
44
- child.exitCode = autoExit.code
45
- child.signalCode = autoExit.signal ?? null
46
- child.emit('exit', child.exitCode, child.signalCode)
47
- })
48
- })
49
- } else if (pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
50
- queueMicrotask(() => {
51
- child.stdout.emit('data', '✓ Ready in 1ms\n')
40
+ child.exitCode = autoExit.code
41
+ child.signalCode = autoExit.signal ?? null
42
+ child.emit('exit', child.exitCode, child.signalCode)
52
43
  })
53
44
  }
54
45
 
@@ -1058,7 +1049,6 @@ describe('server start managed process exits', () => {
1058
1049
  envChangeCallback = onChange
1059
1050
  return jest.fn()
1060
1051
  }),
1061
- watchDevRuntimeFiles: jest.fn(() => jest.fn()),
1062
1052
  }))
1063
1053
  jest.doMock('../lib/generators', () => ({
1064
1054
  generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
- import { createDevEnvReloader, resolveDevEnvFilePaths, watchDevRuntimeFiles } from '../dev-env-reload'
4
+ import { createDevEnvReloader, resolveDevEnvFilePaths } from '../dev-env-reload'
5
5
  import { normalizeTestPath } from './path-helpers'
6
6
 
7
7
  describe('dev env reload helpers', () => {
@@ -60,24 +60,4 @@ describe('dev env reload helpers', () => {
60
60
  expect(environment.SHELL_VALUE).toBe('shell-value')
61
61
  expect(environment.REMOVED_LATER).toBeUndefined()
62
62
  })
63
-
64
- it('watches generated runtime files when explicitly requested', async () => {
65
- const generatedDir = path.join(appDir, '.mercato', 'generated')
66
- fs.mkdirSync(generatedDir, { recursive: true })
67
- const generatedFile = path.join(generatedDir, 'backend-routes.generated.ts')
68
- fs.writeFileSync(generatedFile, 'export const routes = []\n')
69
-
70
- let stop = () => {}
71
- const seen = new Promise<string>((resolve) => {
72
- stop = watchDevRuntimeFiles(appDir, (filePath) => {
73
- stop()
74
- resolve(filePath)
75
- }, { debounceMs: 10 })
76
- })
77
-
78
- await new Promise((resolve) => setTimeout(resolve, 25))
79
- fs.writeFileSync(generatedFile, 'export const routes = [1]\n')
80
-
81
- await expect(seen.finally(stop)).resolves.toBe(generatedFile)
82
- })
83
63
  })
@@ -2,8 +2,6 @@ import dotenv from 'dotenv'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
 
5
- const RUNTIME_GRAPH_FILE_PATTERN = /\.generated(?:\.[a-z0-9]+)?(?:\.tsx?|\.checksum)$/i
6
-
7
5
  const DEV_ENV_PRIORITY = [
8
6
  '.env',
9
7
  '.env.development',
@@ -110,48 +108,3 @@ export function watchDevEnvFiles(
110
108
  }
111
109
  }
112
110
  }
113
-
114
- export function watchDevRuntimeFiles(
115
- appDir: string,
116
- onChange: (filePath: string) => void,
117
- options: { debounceMs?: number } = {},
118
- ): () => void {
119
- const debounceMs = options.debounceMs ?? 250
120
- const generatedDir = path.join(appDir, '.mercato', 'generated')
121
- const timers = new Map<string, NodeJS.Timeout>()
122
-
123
- if (!fs.existsSync(generatedDir)) {
124
- return () => {}
125
- }
126
-
127
- let watcher: fs.FSWatcher | null = null
128
- try {
129
- watcher = fs.watch(generatedDir, (eventType, fileName) => {
130
- if (eventType !== 'change' && eventType !== 'rename') return
131
- const name = String(fileName ?? '')
132
- if (!name) return
133
- if (!RUNTIME_GRAPH_FILE_PATTERN.test(name)) return
134
-
135
- const filePath = path.join(generatedDir, name)
136
- const existingTimer = timers.get(filePath)
137
- if (existingTimer) {
138
- clearTimeout(existingTimer)
139
- }
140
-
141
- timers.set(filePath, setTimeout(() => {
142
- timers.delete(filePath)
143
- onChange(filePath)
144
- }, debounceMs))
145
- })
146
- } catch {
147
- return () => {}
148
- }
149
-
150
- return () => {
151
- for (const timer of timers.values()) {
152
- clearTimeout(timer)
153
- }
154
- timers.clear()
155
- watcher?.close()
156
- }
157
- }
@@ -2949,13 +2949,6 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2949
2949
  // on `DEMO_MODE !== 'false'` in the backend layout, so opting out here
2950
2950
  // only affects the integration test runtime — dev + prod stay unchanged.
2951
2951
  DEMO_MODE: 'false',
2952
- // Disable the feature-toggle resolution cache. Tests flip overrides
2953
- // (e.g. example_customers_sync enabled/bidirectional) between cases; the
2954
- // 1-minute resolution cache can serve a stale value across set/clear
2955
- // under fast churn, which made the example.todo.* persistent subscribers
2956
- // skip enqueueing inbound sync jobs (TC-CRM-028 inbound trio flake).
2957
- // Fresh DB reads make flag-gated sync deterministic in tests only.
2958
- OM_FEATURE_TOGGLES_CACHE_DISABLED: '1',
2959
2952
  OM_CLI_QUIET: '1',
2960
2953
  MERCATO_QUIET: '1',
2961
2954
  QUEUE_BASE_DIR: EPHEMERAL_QUEUE_BASE_DIR,
package/src/mercato.ts CHANGED
@@ -341,122 +341,6 @@ type DevServerRestartResult = {
341
341
 
342
342
  type DevServerExitResult = ManagedProcessExitResult | DevServerRestartResult
343
343
 
344
- function resolveDevRuntimeBaseUrl(environment: NodeJS.ProcessEnv = process.env): string {
345
- const configured =
346
- environment.APP_URL
347
- ?? environment.NEXT_PUBLIC_APP_URL
348
- ?? environment.NEXTAUTH_URL
349
- if (configured?.trim()) {
350
- return configured.trim().replace(/\/+$/, '')
351
- }
352
- return `http://localhost:${environment.PORT?.trim() || '3000'}`
353
- }
354
-
355
- function writeDevSplashChildState(state: Record<string, unknown>): void {
356
- if (process.env.OM_DEV_SPLASH_RUNTIME_WRAPPER === '1') return
357
- const stateFile = process.env.OM_DEV_SPLASH_CHILD_STATE_FILE
358
- if (!stateFile?.trim()) return
359
-
360
- try {
361
- fs.mkdirSync(path.dirname(stateFile), { recursive: true })
362
- fs.writeFileSync(stateFile, `${JSON.stringify({
363
- mode: process.env.OM_DEV_SPLASH_MODE || 'dev',
364
- failed: false,
365
- failureLines: [],
366
- failureCommand: null,
367
- ...state,
368
- }, null, 2)}\n`)
369
- } catch {
370
- // Splash state is best-effort; terminal logs remain authoritative.
371
- }
372
- }
373
-
374
- function writeDevSplashRuntimeStarting(detail = 'Starting Next.js dev server'): void {
375
- writeDevSplashChildState({
376
- phase: 'Preparing app runtime',
377
- detail,
378
- ready: false,
379
- readyUrl: null,
380
- loginUrl: null,
381
- progressLabel: 'Launching app runtime',
382
- activity: detail,
383
- })
384
- }
385
-
386
- function resolveSplashProgressFallback(): { current: number; total: number } {
387
- const current = Number.parseInt(process.env.OM_DEV_SPLASH_STAGE_CURRENT ?? '', 10)
388
- const total = Number.parseInt(process.env.OM_DEV_SPLASH_STAGE_TOTAL ?? '', 10)
389
- if (Number.isFinite(current) && Number.isFinite(total) && total > 0) {
390
- return { current, total }
391
- }
392
- if (process.env.OM_DEV_SPLASH_MODE === 'greenfield' || process.env.OM_DEV_SPLASH_MODE === 'setup') {
393
- return { current: 5, total: 5 }
394
- }
395
- return { current: 3, total: 3 }
396
- }
397
-
398
- function writeDevSplashRuntimeRestarting(reason: string): void {
399
- const progress = resolveSplashProgressFallback()
400
- writeDevSplashChildState({
401
- phase: 'App runtime is restarting',
402
- detail: `Reason: ${reason}`,
403
- ready: false,
404
- readyUrl: null,
405
- loginUrl: null,
406
- progressCurrent: progress.current,
407
- progressTotal: progress.total,
408
- progressLabel: 'Restarting app runtime',
409
- activity: `App runtime restart: ${reason}`,
410
- })
411
- }
412
-
413
- function writeDevSplashRuntimeReady(reason?: string): void {
414
- const readyUrl = resolveDevRuntimeBaseUrl()
415
- const progress = resolveSplashProgressFallback()
416
- writeDevSplashChildState({
417
- phase: 'App is ready',
418
- detail: reason ? `Restart completed after ${reason}` : 'Next.js dev server is ready',
419
- ready: true,
420
- readyUrl,
421
- loginUrl: `${readyUrl}/login`,
422
- progressCurrent: progress.current,
423
- progressTotal: progress.total,
424
- progressPercent: 100,
425
- progressLabel: 'App is ready',
426
- activity: reason ? `Restart completed after ${reason}` : 'App runtime is ready',
427
- })
428
- }
429
-
430
- function resolveDevWarmupReadyTimeoutMs(environment: NodeJS.ProcessEnv = process.env): number {
431
- const parsed = Number.parseInt(environment.OM_DEV_WARMUP_READY_TIMEOUT_MS ?? '', 10)
432
- if (Number.isFinite(parsed) && parsed >= 0) return parsed
433
- return 300_000
434
- }
435
-
436
- async function waitForDevWarmupReadyFile(
437
- filePath: string | undefined,
438
- options: {
439
- timeoutMs?: number
440
- signal?: AbortSignal
441
- } = {},
442
- ): Promise<'ready' | 'timeout' | 'aborted'> {
443
- const normalized = filePath?.trim()
444
- if (!normalized) return 'ready'
445
- const timeoutMs = options.timeoutMs ?? resolveDevWarmupReadyTimeoutMs()
446
- const startedAt = Date.now()
447
-
448
- while (true) {
449
- if (options.signal?.aborted) return 'aborted'
450
- try {
451
- if (fs.existsSync(normalized)) return 'ready'
452
- } catch {
453
- // Keep polling; the runtime wrapper owns this best-effort marker.
454
- }
455
- if (timeoutMs >= 0 && Date.now() - startedAt >= timeoutMs) return 'timeout'
456
- await new Promise((resolve) => setTimeout(resolve, 250))
457
- }
458
- }
459
-
460
344
  type ModuleCommandLookupResult =
461
345
  | {
462
346
  status: 'ok'
@@ -717,17 +601,18 @@ async function runGeneratorSuite(quiet: boolean): Promise<void> {
717
601
  function createGenerateWatchChecksumFn(): () => Promise<string> {
718
602
  return async () => {
719
603
  const { createResolver } = await import('./lib/resolver')
720
- const { calculateGenerateWatchStructureChecksum } = await import('./lib/generate-watch-structure')
604
+ const { calculateStructureChecksum } = await import('./lib/utils')
721
605
  const resolver = createResolver()
722
- const moduleRoots = []
606
+ const tracked = new Set<string>([
607
+ path.join(resolver.getAppDir(), 'src', 'modules.ts'),
608
+ path.join(resolver.getAppDir(), 'src', 'modules'),
609
+ ])
723
610
  for (const entry of resolver.loadEnabledModules()) {
724
611
  const roots = resolver.getModulePaths(entry)
725
- moduleRoots.push({ appBase: roots.appBase, pkgBase: roots.pkgBase })
612
+ tracked.add(roots.appBase)
613
+ tracked.add(roots.pkgBase)
726
614
  }
727
- return calculateGenerateWatchStructureChecksum({
728
- modulesFile: path.join(resolver.getAppDir(), 'src', 'modules.ts'),
729
- moduleRoots,
730
- })
615
+ return calculateStructureChecksum(Array.from(tracked))
731
616
  }
732
617
  }
733
618
 
@@ -1805,11 +1690,10 @@ export async function run(argv = process.argv) {
1805
1690
  let processes: ChildProcess[] = []
1806
1691
  let didRetryCorruptedTurbopackCache = false
1807
1692
  let stopping = false
1808
- let devRestartPromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1693
+ let envChangePromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1809
1694
  let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
1810
1695
  let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
1811
1696
  let activeGenerateWatcher: GenerateWatcherHandle | null = null
1812
- let lastRestartReason: string | null = null
1813
1697
  const generateWatcherMode: GenerateWatcherMode = resolveGenerateWatcherMode(process.env)
1814
1698
  const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
1815
1699
 
@@ -1897,31 +1781,20 @@ export async function run(argv = process.argv) {
1897
1781
  const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1898
1782
 
1899
1783
  const stopEnvWatcher = watchDevEnvFiles(appDir, (filePath) => {
1900
- devRestartPromiseResolve?.({
1784
+ envChangePromiseResolve?.({
1901
1785
  label: 'Environment file change',
1902
1786
  restart: true,
1903
1787
  filePath,
1904
1788
  })
1905
1789
  })
1906
- const waitForDevRestart = (): Promise<DevServerRestartResult> =>
1790
+
1791
+ const waitForEnvChange = (): Promise<DevServerRestartResult> =>
1907
1792
  new Promise((resolve) => {
1908
- devRestartPromiseResolve = resolve
1793
+ envChangePromiseResolve = resolve
1909
1794
  })
1910
1795
 
1911
- const startNextDev = (runtimeEnv: NodeJS.ProcessEnv): {
1912
- exitPromise: Promise<ManagedProcessExitResult>
1913
- readyPromise: Promise<void>
1914
- } => {
1915
- let readyResolve: () => void = () => undefined
1916
- const readyPromise = new Promise<void>((resolve) => {
1917
- readyResolve = resolve
1918
- })
1919
- const exitPromise = new Promise<ManagedProcessExitResult>((resolve) => {
1920
- writeDevSplashRuntimeStarting(
1921
- lastRestartReason
1922
- ? `Restarting Next.js dev server. Reason: ${lastRestartReason}`
1923
- : 'Starting Next.js dev server',
1924
- )
1796
+ const startNextDev = (runtimeEnv: NodeJS.ProcessEnv): Promise<ManagedProcessExitResult> =>
1797
+ new Promise((resolve) => {
1925
1798
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
1926
1799
  stdio: ['inherit', 'pipe', 'pipe'],
1927
1800
  env: runtimeEnv,
@@ -1930,18 +1803,11 @@ export async function run(argv = process.argv) {
1930
1803
  processes.push(nextProcess)
1931
1804
 
1932
1805
  let combinedOutput = ''
1933
- let reportedReady = false
1934
1806
  const appendOutput = (chunk: string) => {
1935
1807
  combinedOutput += chunk
1936
1808
  if (combinedOutput.length > 32_768) {
1937
1809
  combinedOutput = combinedOutput.slice(-32_768)
1938
1810
  }
1939
- if (!reportedReady && /\bready in\b/i.test(chunk)) {
1940
- reportedReady = true
1941
- writeDevSplashRuntimeReady(lastRestartReason ?? undefined)
1942
- lastRestartReason = null
1943
- readyResolve()
1944
- }
1945
1811
  }
1946
1812
 
1947
1813
  nextProcess.stdout?.on('data', (chunk: Buffer | string) => {
@@ -1958,13 +1824,9 @@ export async function run(argv = process.argv) {
1958
1824
  nextProcess.on('exit', async (code, signal) => {
1959
1825
  if (!didRetryCorruptedTurbopackCache && isTurbopackCacheCorruption(combinedOutput)) {
1960
1826
  didRetryCorruptedTurbopackCache = true
1961
- lastRestartReason = 'corrupted Turbopack dev cache'
1962
- writeDevSplashRuntimeRestarting(lastRestartReason)
1963
1827
  console.log('[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...')
1964
1828
  removeTurbopackDevCache(appDir)
1965
- const restarted = startNextDev(runtimeEnv)
1966
- restarted.readyPromise.then(readyResolve)
1967
- return resolve(await restarted.exitPromise)
1829
+ return resolve(await startNextDev(runtimeEnv))
1968
1830
  }
1969
1831
  resolve({
1970
1832
  label: 'Next.js dev server',
@@ -1973,8 +1835,6 @@ export async function run(argv = process.argv) {
1973
1835
  })
1974
1836
  })
1975
1837
  })
1976
- return { exitPromise, readyPromise }
1977
- }
1978
1838
 
1979
1839
  try {
1980
1840
  while (!stopping) {
@@ -1984,92 +1844,62 @@ export async function run(argv = process.argv) {
1984
1844
  const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env)
1985
1845
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1986
1846
  const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
1987
- const nextRuntime = startNextDev(runtimeEnv)
1988
- const restartPromise = waitForDevRestart()
1989
- const backgroundStartAbort = new AbortController()
1990
- const cancelBackgroundStart = () => backgroundStartAbort.abort()
1991
- nextRuntime.exitPromise.finally(cancelBackgroundStart)
1992
- restartPromise.then(cancelBackgroundStart)
1993
- let backgroundExitResolve: (result: ManagedProcessExitResult) => void = () => undefined
1994
- const backgroundExitPromise = new Promise<ManagedProcessExitResult>((resolve) => {
1995
- backgroundExitResolve = resolve
1996
- })
1997
1847
  const managedExitPromises: Promise<DevServerExitResult>[] = [
1998
- nextRuntime.exitPromise,
1999
- restartPromise,
2000
- backgroundExitPromise,
1848
+ startNextDev(runtimeEnv),
1849
+ waitForEnvChange(),
2001
1850
  ]
2002
1851
 
2003
- const startBackgroundServices = async () => {
2004
- if (stopping || backgroundStartAbort.signal.aborted) return
2005
-
2006
- // Keep first-route compilation responsive: greenfield setup can
2007
- // leave vector/fulltext jobs ready. When the dev wrapper is
2008
- // active, wait for its /login + /backend warmup marker before
2009
- // workers and scheduler begin consuming CPU and database I/O.
2010
- const warmupReady = await waitForDevWarmupReadyFile(process.env.OM_DEV_WARMUP_READY_FILE, {
2011
- timeoutMs: resolveDevWarmupReadyTimeoutMs(process.env),
2012
- signal: backgroundStartAbort.signal,
2013
- })
2014
- if (warmupReady === 'aborted' || stopping || backgroundStartAbort.signal.aborted) return
2015
- if (warmupReady === 'timeout') {
2016
- console.warn('[server] Timed out waiting for dev warmup marker; starting background services anyway.')
2017
- }
2018
-
2019
- if (autoSpawnWorkersMode !== 'off') {
2020
- const discoveredWorkers = getRegisteredCliWorkers()
2021
- const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))]
2022
- if (discoveredWorkerQueues.length === 0) {
2023
- console.error('[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.')
2024
- } else if (autoSpawnWorkersMode === 'lazy') {
2025
- console.log(`[server] Lazy worker auto-spawn enabled — workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`)
2026
- activeLazySupervisor = startLazyWorkerSupervisor({
2027
- mercatoBin,
2028
- appDir,
2029
- runtimeEnv,
2030
- workers: discoveredWorkers,
2031
- pollMs: resolveLazyPollMs(process.env),
2032
- restartOnUnexpectedExit: resolveLazyRestart(process.env),
2033
- })
2034
- } else {
2035
- console.log('[server] Eager worker auto-spawn enabled - starting workers for all queues...')
2036
- const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
2037
- stdio: 'inherit',
2038
- env: runtimeEnv,
2039
- cwd: appDir,
2040
- })
2041
- processes.push(workerProcess)
2042
- waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)).then(backgroundExitResolve)
2043
- }
1852
+ // Start workers if enabled
1853
+ if (autoSpawnWorkersMode !== 'off') {
1854
+ const discoveredWorkers = getRegisteredCliWorkers()
1855
+ const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))]
1856
+ if (discoveredWorkerQueues.length === 0) {
1857
+ console.error('[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run `yarn generate` and verify `.mercato/generated/modules.cli.generated.ts` contains worker entries. Continuing without auto-spawned workers.')
1858
+ } else if (autoSpawnWorkersMode === 'lazy') {
1859
+ console.log(`[server] Lazy worker auto-spawn enabled — workers will start on first job (${discoveredWorkerQueues.length} queue(s) watched).`)
1860
+ activeLazySupervisor = startLazyWorkerSupervisor({
1861
+ mercatoBin,
1862
+ appDir,
1863
+ runtimeEnv,
1864
+ workers: discoveredWorkers,
1865
+ pollMs: resolveLazyPollMs(process.env),
1866
+ restartOnUnexpectedExit: resolveLazyRestart(process.env),
1867
+ })
1868
+ } else {
1869
+ console.log('[server] Eager worker auto-spawn enabled - starting workers for all queues...')
1870
+ const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1871
+ stdio: 'inherit',
1872
+ env: runtimeEnv,
1873
+ cwd: appDir,
1874
+ })
1875
+ processes.push(workerProcess)
1876
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
2044
1877
  }
1878
+ }
2045
1879
 
2046
- if (autoSpawnSchedulerMode !== 'off' && queueStrategy === 'local') {
2047
- if (schedulerCommand.status !== 'ok') {
2048
- console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
2049
- } else if (autoSpawnSchedulerMode === 'lazy') {
2050
- console.log('[server] Lazy scheduler auto-spawn enabled - scheduler will start when an enabled schedule exists.')
2051
- activeLazySchedulerSupervisor = startLazySchedulerSupervisor({
2052
- mercatoBin,
2053
- appDir,
2054
- runtimeEnv,
2055
- pollMs: resolveLazySchedulerPollMs(process.env),
2056
- restartOnUnexpectedExit: resolveLazySchedulerRestart(process.env),
2057
- })
2058
- } else {
2059
- console.log('[server] Eager scheduler auto-spawn enabled - starting scheduler polling engine...')
2060
- const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
2061
- stdio: 'inherit',
2062
- env: runtimeEnv,
2063
- cwd: appDir,
2064
- })
2065
- processes.push(schedulerProcess)
2066
- waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine').then(backgroundExitResolve)
2067
- }
1880
+ if (autoSpawnSchedulerMode !== 'off' && queueStrategy === 'local') {
1881
+ if (schedulerCommand.status !== 'ok') {
1882
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
1883
+ } else if (autoSpawnSchedulerMode === 'lazy') {
1884
+ console.log('[server] Lazy scheduler auto-spawn enabled - scheduler will start when an enabled schedule exists.')
1885
+ activeLazySchedulerSupervisor = startLazySchedulerSupervisor({
1886
+ mercatoBin,
1887
+ appDir,
1888
+ runtimeEnv,
1889
+ pollMs: resolveLazySchedulerPollMs(process.env),
1890
+ restartOnUnexpectedExit: resolveLazySchedulerRestart(process.env),
1891
+ })
1892
+ } else {
1893
+ console.log('[server] Eager scheduler auto-spawn enabled - starting scheduler polling engine...')
1894
+ const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1895
+ stdio: 'inherit',
1896
+ env: runtimeEnv,
1897
+ cwd: appDir,
1898
+ })
1899
+ processes.push(schedulerProcess)
1900
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
2068
1901
  }
2069
1902
  }
2070
- nextRuntime.readyPromise.then(() => {
2071
- void startBackgroundServices()
2072
- })
2073
1903
 
2074
1904
  if (generateWatcherMode === 'in-process') {
2075
1905
  // Run the structural regeneration watcher inside this process
@@ -2097,15 +1927,11 @@ export async function run(argv = process.argv) {
2097
1927
  }
2098
1928
 
2099
1929
  const firstExit = await Promise.race(managedExitPromises)
2100
- if (isDevServerRestartResult(firstExit)) {
2101
- lastRestartReason = `${firstExit.label.toLowerCase()} (${path.basename(firstExit.filePath)})`
2102
- writeDevSplashRuntimeRestarting(lastRestartReason)
2103
- }
2104
1930
  await cleanupAndWait()
2105
- devRestartPromiseResolve = null
1931
+ envChangePromiseResolve = null
2106
1932
 
2107
1933
  if (isDevServerRestartResult(firstExit)) {
2108
- console.log(`[server] Detected ${firstExit.label.toLowerCase()} (${path.basename(firstExit.filePath)}). Restarting app runtime...`)
1934
+ console.log(`[server] Detected environment file change (${path.basename(firstExit.filePath)}). Restarting app runtime...`)
2109
1935
  continue
2110
1936
  }
2111
1937