@open-mercato/cli 0.6.3-develop.3894.1.352abf4240 → 0.6.3

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/src/mercato.ts CHANGED
@@ -341,6 +341,122 @@ 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
+
344
460
  type ModuleCommandLookupResult =
345
461
  | {
346
462
  status: 'ok'
@@ -601,18 +717,17 @@ async function runGeneratorSuite(quiet: boolean): Promise<void> {
601
717
  function createGenerateWatchChecksumFn(): () => Promise<string> {
602
718
  return async () => {
603
719
  const { createResolver } = await import('./lib/resolver')
604
- const { calculateStructureChecksum } = await import('./lib/utils')
720
+ const { calculateGenerateWatchStructureChecksum } = await import('./lib/generate-watch-structure')
605
721
  const resolver = createResolver()
606
- const tracked = new Set<string>([
607
- path.join(resolver.getAppDir(), 'src', 'modules.ts'),
608
- path.join(resolver.getAppDir(), 'src', 'modules'),
609
- ])
722
+ const moduleRoots = []
610
723
  for (const entry of resolver.loadEnabledModules()) {
611
724
  const roots = resolver.getModulePaths(entry)
612
- tracked.add(roots.appBase)
613
- tracked.add(roots.pkgBase)
725
+ moduleRoots.push({ appBase: roots.appBase, pkgBase: roots.pkgBase })
614
726
  }
615
- return calculateStructureChecksum(Array.from(tracked))
727
+ return calculateGenerateWatchStructureChecksum({
728
+ modulesFile: path.join(resolver.getAppDir(), 'src', 'modules.ts'),
729
+ moduleRoots,
730
+ })
616
731
  }
617
732
  }
618
733
 
@@ -1690,10 +1805,11 @@ export async function run(argv = process.argv) {
1690
1805
  let processes: ChildProcess[] = []
1691
1806
  let didRetryCorruptedTurbopackCache = false
1692
1807
  let stopping = false
1693
- let envChangePromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1808
+ let devRestartPromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1694
1809
  let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
1695
1810
  let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
1696
1811
  let activeGenerateWatcher: GenerateWatcherHandle | null = null
1812
+ let lastRestartReason: string | null = null
1697
1813
  const generateWatcherMode: GenerateWatcherMode = resolveGenerateWatcherMode(process.env)
1698
1814
  const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
1699
1815
 
@@ -1781,20 +1897,31 @@ export async function run(argv = process.argv) {
1781
1897
  const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1782
1898
 
1783
1899
  const stopEnvWatcher = watchDevEnvFiles(appDir, (filePath) => {
1784
- envChangePromiseResolve?.({
1900
+ devRestartPromiseResolve?.({
1785
1901
  label: 'Environment file change',
1786
1902
  restart: true,
1787
1903
  filePath,
1788
1904
  })
1789
1905
  })
1790
-
1791
- const waitForEnvChange = (): Promise<DevServerRestartResult> =>
1906
+ const waitForDevRestart = (): Promise<DevServerRestartResult> =>
1792
1907
  new Promise((resolve) => {
1793
- envChangePromiseResolve = resolve
1908
+ devRestartPromiseResolve = resolve
1794
1909
  })
1795
1910
 
1796
- const startNextDev = (runtimeEnv: NodeJS.ProcessEnv): Promise<ManagedProcessExitResult> =>
1797
- new Promise((resolve) => {
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
+ )
1798
1925
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
1799
1926
  stdio: ['inherit', 'pipe', 'pipe'],
1800
1927
  env: runtimeEnv,
@@ -1803,11 +1930,18 @@ export async function run(argv = process.argv) {
1803
1930
  processes.push(nextProcess)
1804
1931
 
1805
1932
  let combinedOutput = ''
1933
+ let reportedReady = false
1806
1934
  const appendOutput = (chunk: string) => {
1807
1935
  combinedOutput += chunk
1808
1936
  if (combinedOutput.length > 32_768) {
1809
1937
  combinedOutput = combinedOutput.slice(-32_768)
1810
1938
  }
1939
+ if (!reportedReady && /\bready in\b/i.test(chunk)) {
1940
+ reportedReady = true
1941
+ writeDevSplashRuntimeReady(lastRestartReason ?? undefined)
1942
+ lastRestartReason = null
1943
+ readyResolve()
1944
+ }
1811
1945
  }
1812
1946
 
1813
1947
  nextProcess.stdout?.on('data', (chunk: Buffer | string) => {
@@ -1824,9 +1958,13 @@ export async function run(argv = process.argv) {
1824
1958
  nextProcess.on('exit', async (code, signal) => {
1825
1959
  if (!didRetryCorruptedTurbopackCache && isTurbopackCacheCorruption(combinedOutput)) {
1826
1960
  didRetryCorruptedTurbopackCache = true
1961
+ lastRestartReason = 'corrupted Turbopack dev cache'
1962
+ writeDevSplashRuntimeRestarting(lastRestartReason)
1827
1963
  console.log('[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...')
1828
1964
  removeTurbopackDevCache(appDir)
1829
- return resolve(await startNextDev(runtimeEnv))
1965
+ const restarted = startNextDev(runtimeEnv)
1966
+ restarted.readyPromise.then(readyResolve)
1967
+ return resolve(await restarted.exitPromise)
1830
1968
  }
1831
1969
  resolve({
1832
1970
  label: 'Next.js dev server',
@@ -1835,6 +1973,8 @@ export async function run(argv = process.argv) {
1835
1973
  })
1836
1974
  })
1837
1975
  })
1976
+ return { exitPromise, readyPromise }
1977
+ }
1838
1978
 
1839
1979
  try {
1840
1980
  while (!stopping) {
@@ -1844,62 +1984,92 @@ export async function run(argv = process.argv) {
1844
1984
  const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env)
1845
1985
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1846
1986
  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
+ })
1847
1997
  const managedExitPromises: Promise<DevServerExitResult>[] = [
1848
- startNextDev(runtimeEnv),
1849
- waitForEnvChange(),
1998
+ nextRuntime.exitPromise,
1999
+ restartPromise,
2000
+ backgroundExitPromise,
1850
2001
  ]
1851
2002
 
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)))
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.')
1877
2017
  }
1878
- }
1879
2018
 
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'))
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
+ }
2044
+ }
2045
+
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
+ }
1901
2068
  }
1902
2069
  }
2070
+ nextRuntime.readyPromise.then(() => {
2071
+ void startBackgroundServices()
2072
+ })
1903
2073
 
1904
2074
  if (generateWatcherMode === 'in-process') {
1905
2075
  // Run the structural regeneration watcher inside this process
@@ -1927,11 +2097,15 @@ export async function run(argv = process.argv) {
1927
2097
  }
1928
2098
 
1929
2099
  const firstExit = await Promise.race(managedExitPromises)
2100
+ if (isDevServerRestartResult(firstExit)) {
2101
+ lastRestartReason = `${firstExit.label.toLowerCase()} (${path.basename(firstExit.filePath)})`
2102
+ writeDevSplashRuntimeRestarting(lastRestartReason)
2103
+ }
1930
2104
  await cleanupAndWait()
1931
- envChangePromiseResolve = null
2105
+ devRestartPromiseResolve = null
1932
2106
 
1933
2107
  if (isDevServerRestartResult(firstExit)) {
1934
- console.log(`[server] Detected environment file change (${path.basename(firstExit.filePath)}). Restarting app runtime...`)
2108
+ console.log(`[server] Detected ${firstExit.label.toLowerCase()} (${path.basename(firstExit.filePath)}). Restarting app runtime...`)
1935
2109
  continue
1936
2110
  }
1937
2111