@open-mercato/cli 0.5.1-develop.3032.01699048cb → 0.5.1-develop.3043.1a796c3920

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.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/shared/AGENTS.md.template +1 -1
  3. package/dist/lib/__integration__/TC-INT-007.spec.js +201 -0
  4. package/dist/lib/__integration__/TC-INT-007.spec.js.map +7 -0
  5. package/dist/lib/dev-env-reload.js +89 -0
  6. package/dist/lib/dev-env-reload.js.map +7 -0
  7. package/dist/lib/generators/extensions/ai-agents.js +218 -0
  8. package/dist/lib/generators/extensions/ai-agents.js.map +7 -0
  9. package/dist/lib/generators/extensions/ai-tools.js +56 -1
  10. package/dist/lib/generators/extensions/ai-tools.js.map +2 -2
  11. package/dist/lib/generators/extensions/index.js +2 -0
  12. package/dist/lib/generators/extensions/index.js.map +2 -2
  13. package/dist/lib/testing/integration-discovery.js +102 -5
  14. package/dist/lib/testing/integration-discovery.js.map +2 -2
  15. package/dist/mercato.js +153 -79
  16. package/dist/mercato.js.map +2 -2
  17. package/package.json +5 -5
  18. package/src/__tests__/mercato.test.ts +301 -25
  19. package/src/lib/__integration__/TC-INT-007.spec.ts +228 -0
  20. package/src/lib/__tests__/dev-env-reload.test.ts +62 -0
  21. package/src/lib/dev-env-reload.ts +110 -0
  22. package/src/lib/generators/__tests__/module-subset.test.ts +14 -0
  23. package/src/lib/generators/__tests__/output-snapshots.test.ts +17 -0
  24. package/src/lib/generators/__tests__/scanner.test.ts +1 -1
  25. package/src/lib/generators/__tests__/structural-contracts.test.ts +26 -0
  26. package/src/lib/generators/extensions/ai-agents.ts +240 -0
  27. package/src/lib/generators/extensions/ai-tools.ts +72 -1
  28. package/src/lib/generators/extensions/index.ts +2 -0
  29. package/src/lib/testing/__tests__/integration-discovery.test.ts +68 -0
  30. package/src/lib/testing/integration-discovery.ts +127 -3
  31. package/src/mercato.ts +190 -83
package/src/mercato.ts CHANGED
@@ -12,6 +12,7 @@ import { resolveInitDerivedSecrets } from './lib/init-secrets'
12
12
  import { parseModuleInstallArgs } from './lib/module-install-args'
13
13
  import { resolveNextBuildIdCandidate } from './lib/next-build-id'
14
14
  import { acquireServerStartLock } from './lib/server-start-lock'
15
+ import { createDevEnvReloader, watchDevEnvFiles } from './lib/dev-env-reload'
15
16
  // Lazy-imported to avoid pulling in `testcontainers` (devDependency) at startup
16
17
  const lazyIntegration = () => import('./lib/testing/integration')
17
18
  import type { ChildProcess } from 'node:child_process'
@@ -19,6 +20,7 @@ import path from 'node:path'
19
20
  import fs from 'node:fs'
20
21
 
21
22
  let envLoaded = false
23
+ const initialProcessEnvironmentEntries = Object.entries(process.env)
22
24
 
23
25
  async function runWithCapturedExitCode(action: () => Promise<void>): Promise<number> {
24
26
  const previousExitCode = process.exitCode
@@ -311,6 +313,24 @@ type ManagedProcessExitResult = {
311
313
  signal: NodeJS.Signals | null
312
314
  }
313
315
 
316
+ type DevServerRestartResult = {
317
+ label: string
318
+ restart: true
319
+ filePath: string
320
+ }
321
+
322
+ type DevServerExitResult = ManagedProcessExitResult | DevServerRestartResult
323
+
324
+ type ModuleCommandLookupResult =
325
+ | {
326
+ status: 'ok'
327
+ module: Module
328
+ command: NonNullable<Module['cli']>[number]
329
+ }
330
+ | {
331
+ status: 'missing-module' | 'missing-cli' | 'missing-command'
332
+ }
333
+
314
334
  function waitForManagedProcessExit(proc: ChildProcess, label: string): Promise<ManagedProcessExitResult> {
315
335
  return new Promise((resolve) => {
316
336
  proc.on('exit', (code, signal) => {
@@ -337,6 +357,54 @@ function createManagedProcessExitError(result: ManagedProcessExitResult): Error
337
357
  return new Error(`[server] ${result.label} exited unexpectedly with ${formatManagedProcessExitStatus(result)}.`)
338
358
  }
339
359
 
360
+ function isDevServerRestartResult(result: DevServerExitResult): result is DevServerRestartResult {
361
+ return 'restart' in result && result.restart === true
362
+ }
363
+
364
+ function formatQueueWorkerLabel(queueNames: string[]): string {
365
+ if (queueNames.length === 0) return 'Queue worker'
366
+ const sorted = [...queueNames].sort((a, b) => a.localeCompare(b))
367
+ const preview = sorted.length > 4 ? `${sorted.slice(0, 4).join(', ')}, +${sorted.length - 4} more` : sorted.join(', ')
368
+ return `Queue worker (${preview})`
369
+ }
370
+
371
+ function lookupModuleCommand(
372
+ allModules: Module[],
373
+ moduleName: string,
374
+ commandName: string,
375
+ ): ModuleCommandLookupResult {
376
+ const mod = allModules.find((entry) => entry.id === moduleName)
377
+ if (!mod) {
378
+ return { status: 'missing-module' }
379
+ }
380
+
381
+ if (!mod.cli || mod.cli.length === 0) {
382
+ return { status: 'missing-cli' }
383
+ }
384
+
385
+ const command = mod.cli.find((entry) => entry.command === commandName)
386
+ if (!command) {
387
+ return { status: 'missing-command' }
388
+ }
389
+
390
+ return {
391
+ status: 'ok',
392
+ module: mod,
393
+ command,
394
+ }
395
+ }
396
+
397
+ function describeMissingModuleCommand(result: Exclude<ModuleCommandLookupResult, { status: 'ok' }>): string {
398
+ switch (result.status) {
399
+ case 'missing-module':
400
+ return 'module not enabled'
401
+ case 'missing-cli':
402
+ return 'module has no CLI commands'
403
+ case 'missing-command':
404
+ return 'command not found'
405
+ }
406
+ }
407
+
340
408
  function ensureNextBuildIdInConfiguredDistDir(appDir: string): void {
341
409
  const configuredDistDir = path.join(appDir, '.mercato', 'next')
342
410
  const configuredBuildIdPath = path.join(configuredDistDir, 'BUILD_ID')
@@ -404,36 +472,25 @@ async function runModuleCommand(
404
472
  args: string[] = [],
405
473
  options: { optional?: boolean; silentOptional?: boolean } = {},
406
474
  ): Promise<boolean> {
407
- const mod = allModules.find((m) => m.id === moduleName)
408
- if (!mod) {
475
+ const resolved = lookupModuleCommand(allModules, moduleName, commandName)
476
+ if (resolved.status !== 'ok') {
409
477
  if (options.optional) {
410
478
  if (!options.silentOptional) {
411
- console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module not enabled`)
479
+ console.log(`⏭️ Skipping "${moduleName}:${commandName}" — ${describeMissingModuleCommand(resolved)}`)
412
480
  }
413
481
  return false
414
482
  }
415
- throw new Error(`Module not found: "${moduleName}"`)
416
- }
417
- if (!mod.cli || mod.cli.length === 0) {
418
- if (options.optional) {
419
- if (!options.silentOptional) {
420
- console.log(`⏭️ Skipping "${moduleName}:${commandName}" — module has no CLI commands`)
421
- }
422
- return false
483
+ switch (resolved.status) {
484
+ case 'missing-module':
485
+ throw new Error(`Module not found: "${moduleName}"`)
486
+ case 'missing-cli':
487
+ throw new Error(`Module "${moduleName}" has no CLI commands`)
488
+ case 'missing-command':
489
+ throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
423
490
  }
424
- throw new Error(`Module "${moduleName}" has no CLI commands`)
425
491
  }
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
433
- }
434
- throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
435
- }
436
- await cmd.run(args)
492
+
493
+ await resolved.command.run(args)
437
494
  return true
438
495
  }
439
496
 
@@ -1599,12 +1656,11 @@ export async function run(argv = process.argv) {
1599
1656
  const appDir = env.appDir
1600
1657
  const nodeModulesBases = Array.from(new Set([env.rootDir, appDir]))
1601
1658
 
1602
- const processes: ChildProcess[] = []
1603
- const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
1604
- const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== 'false'
1605
- const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1606
- const runtimeEnv = buildServerProcessEnvironment(process.env)
1659
+ let processes: ChildProcess[] = []
1607
1660
  let didRetryCorruptedTurbopackCache = false
1661
+ let stopping = false
1662
+ let envChangePromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1663
+ const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
1608
1664
 
1609
1665
  function cleanup() {
1610
1666
  console.log('[server] Shutting down...')
@@ -1622,7 +1678,7 @@ export async function run(argv = process.argv) {
1622
1678
  processes.map(
1623
1679
  (proc) =>
1624
1680
  new Promise<void>((resolve) => {
1625
- if (proc.exitCode !== null) return resolve()
1681
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve()
1626
1682
  proc.on('exit', () => resolve())
1627
1683
  })
1628
1684
  )
@@ -1634,10 +1690,17 @@ export async function run(argv = process.argv) {
1634
1690
  } catch {
1635
1691
  // Lock file may already be removed by Next.js — ignore
1636
1692
  }
1693
+ processes = []
1637
1694
  }
1638
1695
 
1639
- process.on('SIGTERM', cleanup)
1640
- process.on('SIGINT', cleanup)
1696
+ process.on('SIGTERM', () => {
1697
+ stopping = true
1698
+ cleanup()
1699
+ })
1700
+ process.on('SIGINT', () => {
1701
+ stopping = true
1702
+ cleanup()
1703
+ })
1641
1704
 
1642
1705
  console.log('[server] Starting Open Mercato in dev mode...')
1643
1706
 
@@ -1649,7 +1712,20 @@ export async function run(argv = process.argv) {
1649
1712
  const nextBin = resolveInstalledBinary(nodeModulesBases, 'next/dist/bin/next')
1650
1713
  const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1651
1714
 
1652
- const startNextDev = (): Promise<ManagedProcessExitResult> =>
1715
+ const stopEnvWatcher = watchDevEnvFiles(appDir, (filePath) => {
1716
+ envChangePromiseResolve?.({
1717
+ label: 'Environment file change',
1718
+ restart: true,
1719
+ filePath,
1720
+ })
1721
+ })
1722
+
1723
+ const waitForEnvChange = (): Promise<DevServerRestartResult> =>
1724
+ new Promise((resolve) => {
1725
+ envChangePromiseResolve = resolve
1726
+ })
1727
+
1728
+ const startNextDev = (runtimeEnv: NodeJS.ProcessEnv): Promise<ManagedProcessExitResult> =>
1653
1729
  new Promise((resolve) => {
1654
1730
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
1655
1731
  stdio: ['inherit', 'pipe', 'pipe'],
@@ -1682,7 +1758,7 @@ export async function run(argv = process.argv) {
1682
1758
  didRetryCorruptedTurbopackCache = true
1683
1759
  console.log('[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...')
1684
1760
  removeTurbopackDevCache(appDir)
1685
- return resolve(await startNextDev())
1761
+ return resolve(await startNextDev(runtimeEnv))
1686
1762
  }
1687
1763
  resolve({
1688
1764
  label: 'Next.js dev server',
@@ -1692,43 +1768,68 @@ export async function run(argv = process.argv) {
1692
1768
  })
1693
1769
  })
1694
1770
 
1695
- const nextExitPromise = startNextDev()
1696
- const managedExitPromises: Promise<ManagedProcessExitResult>[] = [nextExitPromise]
1771
+ try {
1772
+ while (!stopping) {
1773
+ envReloader.reload()
1774
+ const runtimeEnv = buildServerProcessEnvironment(process.env)
1775
+ const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
1776
+ const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== 'false'
1777
+ const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1778
+ const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
1779
+ const managedExitPromises: Promise<DevServerExitResult>[] = [
1780
+ startNextDev(runtimeEnv),
1781
+ waitForEnvChange(),
1782
+ ]
1783
+
1784
+ // Start workers if enabled
1785
+ if (autoSpawnWorkers) {
1786
+ const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))]
1787
+ if (discoveredWorkerQueues.length === 0) {
1788
+ 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.')
1789
+ } else {
1790
+ console.log('[server] Starting workers for all queues...')
1791
+ const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1792
+ stdio: 'inherit',
1793
+ env: runtimeEnv,
1794
+ cwd: appDir,
1795
+ })
1796
+ processes.push(workerProcess)
1797
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
1798
+ }
1799
+ }
1697
1800
 
1698
- // Start workers if enabled
1699
- if (autoSpawnWorkers) {
1700
- const discoveredWorkerQueues = [...new Set(getRegisteredCliWorkers().map((worker) => worker.queue))]
1701
- if (discoveredWorkerQueues.length === 0) {
1702
- 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.')
1703
- } else {
1704
- console.log('[server] Starting workers for all queues...')
1705
- const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1706
- stdio: 'inherit',
1707
- env: runtimeEnv,
1708
- cwd: appDir,
1709
- })
1710
- processes.push(workerProcess)
1711
- managedExitPromises.push(waitForManagedProcessExit(workerProcess, 'Queue worker'))
1712
- }
1713
- }
1801
+ if (autoSpawnScheduler && queueStrategy === 'local') {
1802
+ if (schedulerCommand.status !== 'ok') {
1803
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
1804
+ } else {
1805
+ console.log('[server] Starting scheduler polling engine...')
1806
+ const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1807
+ stdio: 'inherit',
1808
+ env: runtimeEnv,
1809
+ cwd: appDir,
1810
+ })
1811
+ processes.push(schedulerProcess)
1812
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1813
+ }
1814
+ }
1714
1815
 
1715
- 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'))
1724
- }
1816
+ const firstExit = await Promise.race(managedExitPromises)
1817
+ await cleanupAndWait()
1818
+ envChangePromiseResolve = null
1725
1819
 
1726
- const firstExit = await Promise.race(managedExitPromises)
1820
+ if (isDevServerRestartResult(firstExit)) {
1821
+ console.log(`[server] Detected environment file change (${path.basename(firstExit.filePath)}). Restarting app runtime...`)
1822
+ continue
1823
+ }
1727
1824
 
1728
- await cleanupAndWait()
1825
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1826
+ throw createManagedProcessExitError(firstExit)
1827
+ }
1729
1828
 
1730
- if (!isExpectedManagedExitSignal(firstExit.signal)) {
1731
- throw createManagedProcessExitError(firstExit)
1829
+ stopping = true
1830
+ }
1831
+ } finally {
1832
+ stopEnvWatcher()
1732
1833
  }
1733
1834
  },
1734
1835
  },
@@ -1746,6 +1847,7 @@ export async function run(argv = process.argv) {
1746
1847
  const autoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER !== 'false'
1747
1848
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
1748
1849
  const runtimeEnv = buildServerProcessEnvironment(process.env)
1850
+ const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
1749
1851
  const serverStartLock = acquireServerStartLock(appDir, {
1750
1852
  port: runtimeEnv.PORT ?? process.env.PORT ?? null,
1751
1853
  })
@@ -1753,7 +1855,7 @@ export async function run(argv = process.argv) {
1753
1855
  function cleanup() {
1754
1856
  console.log('[server] Shutting down...')
1755
1857
  for (const proc of processes) {
1756
- if (!proc.killed) {
1858
+ if (!proc.killed && proc.exitCode === null && proc.signalCode === null) {
1757
1859
  proc.kill('SIGTERM')
1758
1860
  }
1759
1861
  }
@@ -1765,7 +1867,7 @@ export async function run(argv = process.argv) {
1765
1867
  processes.map(
1766
1868
  (proc) =>
1767
1869
  new Promise<void>((resolve) => {
1768
- if (proc.exitCode !== null) return resolve()
1870
+ if (proc.exitCode !== null || proc.signalCode !== null) return resolve()
1769
1871
  proc.on('exit', () => resolve())
1770
1872
  })
1771
1873
  )
@@ -1789,6 +1891,9 @@ export async function run(argv = process.argv) {
1789
1891
  cwd: appDir,
1790
1892
  })
1791
1893
  processes.push(nextProcess)
1894
+ const managedExitPromises: Promise<ManagedProcessExitResult>[] = [
1895
+ waitForManagedProcessExit(nextProcess, 'Next.js production server'),
1896
+ ]
1792
1897
 
1793
1898
  // Start workers if enabled
1794
1899
  if (autoSpawnWorkers) {
@@ -1803,30 +1908,32 @@ export async function run(argv = process.argv) {
1803
1908
  cwd: appDir,
1804
1909
  })
1805
1910
  processes.push(workerProcess)
1911
+ managedExitPromises.push(waitForManagedProcessExit(workerProcess, formatQueueWorkerLabel(discoveredWorkerQueues)))
1806
1912
  }
1807
1913
  }
1808
1914
 
1809
1915
  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)
1916
+ if (schedulerCommand.status !== 'ok') {
1917
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
1918
+ } else {
1919
+ console.log('[server] Starting scheduler polling engine...')
1920
+ const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
1921
+ stdio: 'inherit',
1922
+ env: runtimeEnv,
1923
+ cwd: appDir,
1924
+ })
1925
+ processes.push(schedulerProcess)
1926
+ managedExitPromises.push(waitForManagedProcessExit(schedulerProcess, 'Scheduler polling engine'))
1927
+ }
1817
1928
  }
1818
1929
 
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
- )
1930
+ const firstExit = await Promise.race(managedExitPromises)
1828
1931
 
1829
1932
  await cleanupAndWait()
1933
+
1934
+ if (!isExpectedManagedExitSignal(firstExit.signal)) {
1935
+ throw createManagedProcessExitError(firstExit)
1936
+ }
1830
1937
  } finally {
1831
1938
  serverStartLock.release()
1832
1939
  }