@open-mercato/cli 0.6.8-develop.6891.1.4dca3f1ad3 → 0.6.8-develop.6893.1.7af3b3a72d

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 (106) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/AGENTS.md +6 -1
  3. package/dist/agentic/guides/module-facts.json +113 -113
  4. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys.md +1 -1
  7. package/dist/agentic/guides/modules/attachments.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  9. package/dist/agentic/guides/modules/auth.md +1 -1
  10. package/dist/agentic/guides/modules/business_rules.md +1 -1
  11. package/dist/agentic/guides/modules/catalog.md +1 -1
  12. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  13. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  14. package/dist/agentic/guides/modules/checkout.md +1 -1
  15. package/dist/agentic/guides/modules/communication_channels.md +3 -3
  16. package/dist/agentic/guides/modules/configs.md +1 -1
  17. package/dist/agentic/guides/modules/content.md +1 -1
  18. package/dist/agentic/guides/modules/currencies.md +1 -1
  19. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  20. package/dist/agentic/guides/modules/customers.md +1 -1
  21. package/dist/agentic/guides/modules/dashboards.md +1 -1
  22. package/dist/agentic/guides/modules/data_sync.md +1 -1
  23. package/dist/agentic/guides/modules/design_system.md +1 -1
  24. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  25. package/dist/agentic/guides/modules/directory.md +1 -1
  26. package/dist/agentic/guides/modules/entities.md +1 -1
  27. package/dist/agentic/guides/modules/events.md +1 -1
  28. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  29. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  30. package/dist/agentic/guides/modules/generators.md +1 -1
  31. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  32. package/dist/agentic/guides/modules/integrations.md +1 -1
  33. package/dist/agentic/guides/modules/messages.md +1 -1
  34. package/dist/agentic/guides/modules/notifications.md +1 -1
  35. package/dist/agentic/guides/modules/onboarding.md +1 -1
  36. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  37. package/dist/agentic/guides/modules/perspectives.md +1 -1
  38. package/dist/agentic/guides/modules/planner.md +1 -1
  39. package/dist/agentic/guides/modules/portal.md +1 -1
  40. package/dist/agentic/guides/modules/progress.md +1 -1
  41. package/dist/agentic/guides/modules/query_index.md +1 -1
  42. package/dist/agentic/guides/modules/record_locks.md +1 -1
  43. package/dist/agentic/guides/modules/resources.md +1 -1
  44. package/dist/agentic/guides/modules/sales.md +1 -1
  45. package/dist/agentic/guides/modules/scheduler.md +1 -1
  46. package/dist/agentic/guides/modules/search.md +1 -1
  47. package/dist/agentic/guides/modules/security.md +1 -1
  48. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  49. package/dist/agentic/guides/modules/sso.md +1 -1
  50. package/dist/agentic/guides/modules/staff.md +1 -1
  51. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  52. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  53. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  54. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  55. package/dist/agentic/guides/modules/translations.md +1 -1
  56. package/dist/agentic/guides/modules/webhooks.md +1 -1
  57. package/dist/agentic/guides/modules/wms.md +1 -1
  58. package/dist/agentic/guides/modules/workflows.md +1 -1
  59. package/dist/agentic/guides/upstream/AGENTS.md +2 -1
  60. package/dist/agentic/guides/upstream/manifest.json +2 -2
  61. package/dist/bin.js +20 -34
  62. package/dist/bin.js.map +2 -2
  63. package/dist/lib/cli-bootstrap-mode.js +32 -0
  64. package/dist/lib/cli-bootstrap-mode.js.map +7 -0
  65. package/dist/lib/dev-supervisor-manifest.js +105 -0
  66. package/dist/lib/dev-supervisor-manifest.js.map +7 -0
  67. package/dist/lib/generators/entity-ids.js +11 -24
  68. package/dist/lib/generators/entity-ids.js.map +2 -2
  69. package/dist/lib/generators/module-di.js +10 -23
  70. package/dist/lib/generators/module-di.js.map +2 -2
  71. package/dist/lib/generators/module-entities.js +10 -23
  72. package/dist/lib/generators/module-entities.js.map +2 -2
  73. package/dist/lib/generators/module-registry.js +394 -31
  74. package/dist/lib/generators/module-registry.js.map +2 -2
  75. package/dist/lib/generators/route-manifest-shards.js +170 -0
  76. package/dist/lib/generators/route-manifest-shards.js.map +7 -0
  77. package/dist/lib/queue-worker-supervisor.js.map +2 -2
  78. package/dist/lib/resolver.js +26 -3
  79. package/dist/lib/resolver.js.map +2 -2
  80. package/dist/lib/utils.js +15 -8
  81. package/dist/lib/utils.js.map +2 -2
  82. package/dist/mercato.js +9 -6
  83. package/dist/mercato.js.map +2 -2
  84. package/package.json +6 -6
  85. package/src/__tests__/mercato.test.ts +144 -0
  86. package/src/bin.ts +28 -42
  87. package/src/lib/__tests__/cli-bootstrap-mode.test.ts +22 -0
  88. package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
  89. package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
  90. package/src/lib/__tests__/queue-worker-supervisor.test.ts +33 -0
  91. package/src/lib/__tests__/resolver.enterprise.test.ts +50 -0
  92. package/src/lib/cli-bootstrap-mode.ts +31 -0
  93. package/src/lib/dev-supervisor-manifest.ts +142 -0
  94. package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
  95. package/src/lib/generators/__tests__/module-subset.test.ts +248 -0
  96. package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
  97. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
  98. package/src/lib/generators/entity-ids.ts +10 -26
  99. package/src/lib/generators/module-di.ts +9 -25
  100. package/src/lib/generators/module-entities.ts +9 -25
  101. package/src/lib/generators/module-registry.ts +432 -29
  102. package/src/lib/generators/route-manifest-shards.ts +146 -0
  103. package/src/lib/queue-worker-supervisor.ts +7 -2
  104. package/src/lib/resolver.ts +31 -3
  105. package/src/lib/utils.ts +24 -16
  106. package/src/mercato.ts +15 -6
@@ -0,0 +1,146 @@
1
+ export type RouteManifestShardKind = 'api' | 'backend'
2
+
3
+ export type RouteManifestShardEntry = {
4
+ path: string
5
+ declaration: string
6
+ imports?: string[]
7
+ }
8
+
9
+ export type RouteManifestShardOutput = {
10
+ fileName: string
11
+ content: string
12
+ }
13
+
14
+ type IndexedRouteManifestShardEntry = RouteManifestShardEntry & {
15
+ order: number
16
+ }
17
+
18
+ const DYNAMIC_SHARD_KEY = '__dynamic'
19
+ const ROOT_SHARD_KEY = '__root'
20
+
21
+ function normalizeRoutePath(kind: RouteManifestShardKind, pathname: string): string[] {
22
+ const segments = pathname.split('?')[0].split('/').filter(Boolean)
23
+ if (kind === 'backend' && segments[0]?.toLowerCase() === 'backend') {
24
+ segments.shift()
25
+ }
26
+ return segments
27
+ }
28
+
29
+ export function getRouteManifestShardKey(kind: RouteManifestShardKind, pathname: string): string {
30
+ const firstSegment = normalizeRoutePath(kind, pathname)[0]
31
+ if (!firstSegment) return ROOT_SHARD_KEY
32
+ if (
33
+ firstSegment.startsWith('[')
34
+ || firstSegment.startsWith(':')
35
+ || firstSegment === '*'
36
+ ) {
37
+ return DYNAMIC_SHARD_KEY
38
+ }
39
+ return firstSegment.toLowerCase()
40
+ }
41
+
42
+ function renderArray(entries: RouteManifestShardEntry[]): string {
43
+ if (entries.length === 0) return '[]'
44
+ return `[\n${entries.map((entry) => ` ${entry.declaration}`).join(',\n')}\n]`
45
+ }
46
+
47
+ function uniqueImports(entries: RouteManifestShardEntry[]): string[] {
48
+ return Array.from(new Set(entries.flatMap((entry) => entry.imports ?? [])))
49
+ }
50
+
51
+ function shardStem(kind: RouteManifestShardKind, index: number, key: string): string {
52
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, '_')
53
+ return `${kind}-route-shard.${String(index).padStart(3, '0')}.${safeKey}.generated`
54
+ }
55
+
56
+ function renderShardFile(kind: RouteManifestShardKind, entries: IndexedRouteManifestShardEntry[]): string {
57
+ const typeName = kind === 'api' ? 'ApiRouteManifestEntry' : 'BackendRouteManifestEntry'
58
+ const helperImport = kind === 'backend' ? 'resolvePageRouteMetadata, ' : ''
59
+ const imports = uniqueImports(entries)
60
+ return `// AUTO-GENERATED by mercato generate registry (request-scoped route shard)\nimport { ${helperImport}type ${typeName} } from '@open-mercato/shared/modules/registry'\n${imports.length ? `${imports.join('\n')}\n` : ''}\nexport const routes: ${typeName}[] = ${renderArray(entries)}\nexport const routeOrders = [${entries.map((entry) => entry.order).join(', ')}]\n\nexport default routes\n`
61
+ }
62
+
63
+ function renderLoaderFile(
64
+ kind: RouteManifestShardKind,
65
+ shards: Array<{ key: string; stem: string; orders: number[] }>,
66
+ ): string {
67
+ const isApi = kind === 'api'
68
+ const typeName = kind === 'api' ? 'ApiRouteManifestEntry' : 'BackendRouteManifestEntry'
69
+ const functionName = kind === 'api' ? 'loadApiRouteManifestShard' : 'loadBackendRouteManifestShard'
70
+ const ordinalFunctionName = isApi ? 'loadApiRouteManifestByOrdinal' : 'loadBackendRouteManifestByOrdinal'
71
+ const facadeExportName = isApi ? 'apiRouteFacades' : 'backendRouteFacades'
72
+ const metadataExportName = isApi ? 'apiRouteMetadata' : 'backendRouteMetadata'
73
+ const metadataFileName = isApi ? 'api-route-metadata.generated' : 'backend-route-metadata.generated'
74
+ const shardCases = shards
75
+ .map(({ key, stem }) => ` case ${JSON.stringify(key)}:\n return import('./${stem}').then((entry) => ({ routes: entry.routes, routeOrders: entry.routeOrders }))`)
76
+ .join('\n')
77
+ const hasDynamicShard = shards.some(({ key }) => key === DYNAMIC_SHARD_KEY)
78
+ const routeShardKeys: string[] = []
79
+ for (const shard of shards) {
80
+ for (const order of shard.orders) routeShardKeys[order] = shard.key
81
+ }
82
+ const prefixNormalization = kind === 'backend'
83
+ ? ` if (segments[0]?.toLowerCase() === 'backend') segments.shift()\n`
84
+ : ''
85
+ const loadDynamic = hasDynamicShard
86
+ ? ` const dynamicShard = await loadShard(${JSON.stringify(DYNAMIC_SHARD_KEY)})\n`
87
+ : ` const dynamicShard: LoadedRouteShard = EMPTY_SHARD\n`
88
+
89
+ return `// AUTO-GENERATED by mercato generate registry (request-scoped route loader)\nimport type { ${typeName} } from '@open-mercato/shared/modules/registry'\nimport { ${metadataExportName} } from './${metadataFileName}'\n\ntype LoadedRouteShard = { routes: ${typeName}[]; routeOrders: number[] }\nconst EMPTY_SHARD: LoadedRouteShard = { routes: [], routeOrders: [] }\nconst ROUTE_SHARD_KEYS = ${JSON.stringify(routeShardKeys)}\n\nfunction resolveShardKey(pathname: string): string {\n const segments = pathname.split('?')[0].split('/').filter(Boolean)\n${prefixNormalization} return segments[0]?.toLowerCase() ?? ${JSON.stringify(ROOT_SHARD_KEY)}\n}\n\nasync function loadShard(key: string): Promise<LoadedRouteShard> {\n switch (key) {\n${shardCases ? `${shardCases}\n` : ''} default:\n return EMPTY_SHARD\n }\n}\n\nexport async function ${ordinalFunctionName}(ordinal: number): Promise<${typeName}> {\n const shardKey = ROUTE_SHARD_KEYS[ordinal]\n if (shardKey === undefined) throw new Error('Unknown generated route manifest ordinal: ' + ordinal)\n const shard = await loadShard(shardKey)\n const index = shard.routeOrders.indexOf(ordinal)\n if (index < 0 || !shard.routes[index]) throw new Error('Generated route manifest ordinal is missing from its shard: ' + ordinal)\n return shard.routes[index]\n}\n\nexport const ${facadeExportName}: ${typeName}[] = ${metadataExportName}.map((metadata, ordinal) => ({\n ...metadata,\n load: async () => {\n const route = await ${ordinalFunctionName}(ordinal)\n return route.load()\n },\n}))\n\nexport async function ${functionName}(pathname: string): Promise<${typeName}[]> {\n const exactShard = await loadShard(resolveShardKey(pathname))\n${loadDynamic} return [\n ...exactShard.routes.map((route, index) => ({ route, order: exactShard.routeOrders[index] })),\n ...dynamicShard.routes.map((route, index) => ({ route, order: dynamicShard.routeOrders[index] })),\n ].sort((a, b) => a.order - b.order).map(({ route }) => route)\n}\n`
90
+ }
91
+
92
+ export function renderRouteManifestShardOutputs(
93
+ kind: RouteManifestShardKind,
94
+ entries: RouteManifestShardEntry[],
95
+ ): RouteManifestShardOutput[] {
96
+ const grouped = new Map<string, IndexedRouteManifestShardEntry[]>()
97
+ for (const [order, entry] of entries.entries()) {
98
+ const key = getRouteManifestShardKey(kind, entry.path)
99
+ const existing = grouped.get(key)
100
+ const indexedEntry = { ...entry, order }
101
+ if (existing) existing.push(indexedEntry)
102
+ else grouped.set(key, [indexedEntry])
103
+ }
104
+
105
+ const sortedGroups = Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b))
106
+ const shards = sortedGroups.map(([key, shardEntries], index) => {
107
+ const stem = shardStem(kind, index, key)
108
+ return {
109
+ key,
110
+ stem,
111
+ orders: shardEntries.map((entry) => entry.order),
112
+ output: {
113
+ fileName: `${stem}.ts`,
114
+ content: renderShardFile(kind, shardEntries),
115
+ },
116
+ }
117
+ })
118
+ const loaderFileName = kind === 'api'
119
+ ? 'api-route-shards.generated.ts'
120
+ : 'backend-route-shards.generated.ts'
121
+
122
+ return [
123
+ ...shards.map(({ output }) => output),
124
+ {
125
+ fileName: loaderFileName,
126
+ content: renderLoaderFile(kind, shards),
127
+ },
128
+ ]
129
+ }
130
+
131
+ export function renderRouteMetadataOutput(
132
+ kind: RouteManifestShardKind,
133
+ entries: RouteManifestShardEntry[],
134
+ ): RouteManifestShardOutput {
135
+ const isApi = kind === 'api'
136
+ const manifestType = isApi ? 'ApiRouteManifestEntry' : 'BackendRouteManifestEntry'
137
+ const metadataType = isApi ? 'ApiRouteMetadataEntry' : 'BackendRouteMetadataEntry'
138
+ const exportName = isApi ? 'apiRouteMetadata' : 'backendRouteMetadata'
139
+ const fileName = isApi ? 'api-route-metadata.generated.ts' : 'backend-route-metadata.generated.ts'
140
+ const helperImport = isApi ? '' : 'resolvePageRouteMetadata, '
141
+ const imports = uniqueImports(entries)
142
+ const content = `// AUTO-GENERATED by mercato generate registry (route metadata without component loaders)\nimport { ${helperImport}type ${manifestType} } from '@open-mercato/shared/modules/registry'\n${imports.length ? `${imports.join('\n')}\n` : ''}\nexport type ${metadataType} = Omit<${manifestType}, 'load'>\n\nexport const ${exportName}: ${metadataType}[] = ${renderArray(entries)}\n\nexport default ${exportName}\n`
143
+ return { fileName, content }
144
+ }
145
+
146
+ export const routeManifestShardFilePattern = /^(?:api|backend)-route-shard\..+\.generated\.(?:ts|checksum)$/
@@ -32,6 +32,11 @@ import {
32
32
  import type { ModuleWorker } from '@open-mercato/shared/modules/registry'
33
33
  import type { LazyWorkerSpawnMode } from './auto-spawn-workers'
34
34
 
35
+ export type SupervisorWorkerDescriptor = Pick<
36
+ ModuleWorker,
37
+ 'id' | 'moduleId' | 'queue' | 'concurrency'
38
+ >
39
+
35
40
  export type LazySupervisorSpawnFn = (
36
41
  command: string,
37
42
  args: readonly string[],
@@ -47,7 +52,7 @@ export type LazyWorkerSupervisorOptions = {
47
52
  mercatoBin: string
48
53
  appDir: string
49
54
  runtimeEnv: NodeJS.ProcessEnv
50
- workers: ModuleWorker[]
55
+ workers: readonly SupervisorWorkerDescriptor[]
51
56
  pollMs: number
52
57
  restartOnUnexpectedExit: boolean
53
58
  spawnMode?: LazyWorkerSpawnMode
@@ -89,7 +94,7 @@ type QueueGroup = {
89
94
  workerCount: number
90
95
  }
91
96
 
92
- function groupWorkersByQueue(workers: ModuleWorker[]): QueueGroup[] {
97
+ function groupWorkersByQueue(workers: readonly SupervisorWorkerDescriptor[]): QueueGroup[] {
93
98
  const groups = new Map<string, QueueGroup>()
94
99
  for (const worker of workers) {
95
100
  const existing = groups.get(worker.queue)
@@ -25,6 +25,7 @@ export type CliEnvironment = {
25
25
  export type ModuleEntry = {
26
26
  id: string
27
27
  from?: '@open-mercato/core' | '@app' | string
28
+ devSupervisorRequiresFullBootstrap?: boolean
28
29
  }
29
30
 
30
31
  export type PackageInfo = {
@@ -89,18 +90,45 @@ function pkgRootFor(rootDir: string, from?: string, isMonorepo = true): string {
89
90
  function parseModuleEntryFromObjectLiteral(node: ts.ObjectLiteralExpression): ModuleEntry | null {
90
91
  let id: string | null = null
91
92
  let from: string | null = null
93
+ let devSupervisorRequiresFullBootstrap = false
92
94
  for (const property of node.properties) {
93
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) continue
94
- const key = property.name.text
95
+ if (!ts.isPropertyAssignment(property)) continue
96
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
97
+ ? property.name.text
98
+ : null
95
99
  if (key === 'id' && ts.isStringLiteralLike(property.initializer)) {
96
100
  id = property.initializer.text
97
101
  }
98
102
  if (key === 'from' && ts.isStringLiteralLike(property.initializer)) {
99
103
  from = property.initializer.text
100
104
  }
105
+ if (key === 'overrides') {
106
+ if (!ts.isObjectLiteralExpression(property.initializer)) {
107
+ devSupervisorRequiresFullBootstrap = true
108
+ continue
109
+ }
110
+ for (const overrideProperty of property.initializer.properties) {
111
+ if (ts.isSpreadAssignment(overrideProperty)) {
112
+ devSupervisorRequiresFullBootstrap = true
113
+ break
114
+ }
115
+ if (!ts.isPropertyAssignment(overrideProperty)) continue
116
+ const overrideKey = ts.isIdentifier(overrideProperty.name) || ts.isStringLiteralLike(overrideProperty.name)
117
+ ? overrideProperty.name.text
118
+ : null
119
+ if (overrideKey === 'workers' || overrideKey === 'cli') {
120
+ devSupervisorRequiresFullBootstrap = true
121
+ break
122
+ }
123
+ }
124
+ }
101
125
  }
102
126
  if (!id) return null
103
- return { id, from: from ?? '@open-mercato/core' }
127
+ return {
128
+ id,
129
+ from: from ?? '@open-mercato/core',
130
+ ...(devSupervisorRequiresFullBootstrap ? { devSupervisorRequiresFullBootstrap: true } : {}),
131
+ }
104
132
  }
105
133
 
106
134
  function parseProcessEnvAccess(
package/src/lib/utils.ts CHANGED
@@ -96,18 +96,22 @@ export function writeIfChanged(
96
96
  structure: structureChecksum || '',
97
97
  }
98
98
 
99
- const shouldWrite =
100
- !existingRecord ||
101
- existingRecord.content !== newRecord.content ||
102
- (structureChecksum && existingRecord.structure !== newRecord.structure)
103
-
104
- if (shouldWrite) {
99
+ const outputChanged = !fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content
100
+ if (outputChanged) {
105
101
  ensureDir(filePath)
106
102
  fs.writeFileSync(filePath, content)
103
+ }
104
+
105
+ const checksumChanged =
106
+ !existingRecord ||
107
+ existingRecord.content !== newRecord.content ||
108
+ existingRecord.structure !== newRecord.structure
109
+ if (checksumChanged) {
110
+ ensureDir(checksumPath)
107
111
  writeChecksumRecord(checksumPath, newRecord)
108
- return true
109
112
  }
110
- return false
113
+
114
+ return outputChanged
111
115
  }
112
116
 
113
117
  // Simple comparison without checksum file
@@ -234,18 +238,22 @@ export function writeGeneratedFile(options: {
234
238
  const { outFile, checksumFile, content, structureChecksum, result, quiet } = options
235
239
  const checksum = { content: calculateChecksum(content), structure: structureChecksum }
236
240
  const existing = readChecksumRecord(checksumFile)
237
- const shouldWrite =
238
- !fs.existsSync(outFile) ||
239
- !existing ||
240
- existing.content !== checksum.content ||
241
- existing.structure !== checksum.structure
242
- if (shouldWrite) {
241
+ const outputChanged = !fs.existsSync(outFile) || fs.readFileSync(outFile, 'utf8') !== content
242
+ if (outputChanged) {
243
243
  fs.mkdirSync(path.dirname(outFile), { recursive: true })
244
244
  fs.writeFileSync(outFile, content)
245
- writeChecksumRecord(checksumFile, checksum)
246
245
  result.filesWritten.push(outFile)
247
246
  } else {
248
247
  result.filesUnchanged.push(outFile)
249
248
  }
250
- if (!quiet) logGenerationResult(path.relative(process.cwd(), outFile), shouldWrite)
249
+
250
+ const checksumChanged =
251
+ !existing ||
252
+ existing.content !== checksum.content ||
253
+ existing.structure !== checksum.structure
254
+ if (checksumChanged) {
255
+ fs.mkdirSync(path.dirname(checksumFile), { recursive: true })
256
+ writeChecksumRecord(checksumFile, checksum)
257
+ }
258
+ if (!quiet) logGenerationResult(path.relative(process.cwd(), outFile), outputChanged)
251
259
  }
package/src/mercato.ts CHANGED
@@ -43,6 +43,7 @@ import { acquireServerStartLock } from './lib/server-start-lock'
43
43
  import { assertSingleInstanceStrategies } from './lib/single-instance-strategy-guard'
44
44
  import { createDevEnvReloader, watchDevEnvFiles } from './lib/dev-env-reload'
45
45
  import { quotePostgresIdentifier } from './lib/db/identifiers'
46
+ import { getRegisteredDevSupervisorManifest } from './lib/dev-supervisor-manifest'
46
47
  // Lazy-imported to avoid pulling in `testcontainers` (devDependency) at startup
47
48
  const lazyIntegration = () => import('./lib/testing/integration')
48
49
  import type { ChildProcess } from 'node:child_process'
@@ -1999,6 +2000,10 @@ export async function run(argv = process.argv) {
1999
2000
  let lastRestartReason: string | null = null
2000
2001
  const generateWatcherMode: GenerateWatcherMode = resolveGenerateWatcherMode(process.env)
2001
2002
  const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
2003
+ // The CLI binary registers this primitive manifest before dispatching
2004
+ // `server dev`. Direct `run()` callers keep the existing module-registry
2005
+ // fallback for backward compatibility and focused unit tests.
2006
+ const devSupervisorManifest = getRegisteredDevSupervisorManifest()
2002
2007
 
2003
2008
  function cleanup() {
2004
2009
  console.log('[server] Shutting down...')
@@ -2207,14 +2212,15 @@ export async function run(argv = process.argv) {
2207
2212
  applyEventsSingleDeliveryGuard({ processEnv: process.env, runtimeEnv, autoSpawnWorkersMode })
2208
2213
  const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env)
2209
2214
  const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
2210
- const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
2215
+ const schedulerStartStatus = devSupervisorManifest?.schedulerStartStatus
2216
+ ?? lookupModuleCommand(getCliModules(), 'scheduler', 'start').status
2211
2217
  const embedSchedulerInSharedWorker =
2212
2218
  shouldEmbedLocalSchedulerInSharedWorker(process.env)
2213
2219
  && queueStrategy === 'local'
2214
2220
  && autoSpawnWorkersMode === 'lazy'
2215
2221
  && resolveLazySpawnMode(process.env) === 'shared'
2216
2222
  && autoSpawnSchedulerMode !== 'off'
2217
- && schedulerCommand.status === 'ok'
2223
+ && schedulerStartStatus === 'ok'
2218
2224
  const nextRuntime = startNextDev(runtimeEnv)
2219
2225
  const restartPromise = waitForDevRestart()
2220
2226
  const backgroundStartAbort = new AbortController()
@@ -2248,10 +2254,13 @@ export async function run(argv = process.argv) {
2248
2254
  }
2249
2255
 
2250
2256
  if (autoSpawnWorkersMode !== 'off') {
2251
- const discoveredWorkers = getRegisteredCliWorkers()
2257
+ const discoveredWorkers = devSupervisorManifest?.workers ?? getRegisteredCliWorkers()
2252
2258
  const discoveredWorkerQueues = [...new Set(discoveredWorkers.map((worker) => worker.queue))]
2253
2259
  if (discoveredWorkerQueues.length === 0) {
2254
- 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.')
2260
+ const workerRegistryFile = devSupervisorManifest
2261
+ ? 'dev-supervisor.generated.json'
2262
+ : 'modules.cli.generated.ts'
2263
+ console.error(`[server] AUTO_SPAWN_WORKERS is enabled, but no queues were discovered from CLI modules. Run \`yarn generate\` and verify \`.mercato/generated/${workerRegistryFile}\` contains worker entries. Continuing without auto-spawned workers.`)
2255
2264
  } else if (autoSpawnWorkersMode === 'lazy') {
2256
2265
  const lazySpawnMode = resolveLazySpawnMode(process.env)
2257
2266
  const lazyModeHint = lazySpawnMode === 'shared'
@@ -2299,8 +2308,8 @@ export async function run(argv = process.argv) {
2299
2308
  if (embedSchedulerInSharedWorker && activeLazySupervisor) {
2300
2309
  console.log('[server] Local scheduler will run inside the lazy shared worker process.')
2301
2310
  } else if (autoSpawnSchedulerMode !== 'off' && queueStrategy === 'local') {
2302
- if (schedulerCommand.status !== 'ok') {
2303
- console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
2311
+ if (schedulerStartStatus !== 'ok') {
2312
+ console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand({ status: schedulerStartStatus })}`)
2304
2313
  } else if (autoSpawnSchedulerMode === 'lazy') {
2305
2314
  console.log('[server] Lazy scheduler auto-spawn enabled - scheduler will start when an enabled schedule exists.')
2306
2315
  activeLazySchedulerSupervisor = startLazySchedulerSupervisor({