@open-mercato/cli 0.4.2-canary-c02407ff85

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 (51) hide show
  1. package/bin/mercato +21 -0
  2. package/build.mjs +78 -0
  3. package/dist/bin.js +51 -0
  4. package/dist/bin.js.map +7 -0
  5. package/dist/index.js +5 -0
  6. package/dist/index.js.map +7 -0
  7. package/dist/lib/db/commands.js +350 -0
  8. package/dist/lib/db/commands.js.map +7 -0
  9. package/dist/lib/db/index.js +7 -0
  10. package/dist/lib/db/index.js.map +7 -0
  11. package/dist/lib/generators/entity-ids.js +257 -0
  12. package/dist/lib/generators/entity-ids.js.map +7 -0
  13. package/dist/lib/generators/index.js +12 -0
  14. package/dist/lib/generators/index.js.map +7 -0
  15. package/dist/lib/generators/module-di.js +73 -0
  16. package/dist/lib/generators/module-di.js.map +7 -0
  17. package/dist/lib/generators/module-entities.js +104 -0
  18. package/dist/lib/generators/module-entities.js.map +7 -0
  19. package/dist/lib/generators/module-registry.js +1081 -0
  20. package/dist/lib/generators/module-registry.js.map +7 -0
  21. package/dist/lib/resolver.js +205 -0
  22. package/dist/lib/resolver.js.map +7 -0
  23. package/dist/lib/utils.js +161 -0
  24. package/dist/lib/utils.js.map +7 -0
  25. package/dist/mercato.js +1045 -0
  26. package/dist/mercato.js.map +7 -0
  27. package/dist/registry.js +7 -0
  28. package/dist/registry.js.map +7 -0
  29. package/jest.config.cjs +19 -0
  30. package/package.json +71 -0
  31. package/src/__tests__/mercato.test.ts +90 -0
  32. package/src/bin.ts +74 -0
  33. package/src/index.ts +2 -0
  34. package/src/lib/__tests__/resolver.test.ts +101 -0
  35. package/src/lib/__tests__/utils.test.ts +270 -0
  36. package/src/lib/db/__tests__/commands.test.ts +131 -0
  37. package/src/lib/db/commands.ts +431 -0
  38. package/src/lib/db/index.ts +1 -0
  39. package/src/lib/generators/__tests__/generators.test.ts +197 -0
  40. package/src/lib/generators/entity-ids.ts +336 -0
  41. package/src/lib/generators/index.ts +4 -0
  42. package/src/lib/generators/module-di.ts +89 -0
  43. package/src/lib/generators/module-entities.ts +124 -0
  44. package/src/lib/generators/module-registry.ts +1222 -0
  45. package/src/lib/resolver.ts +308 -0
  46. package/src/lib/utils.ts +200 -0
  47. package/src/mercato.ts +1106 -0
  48. package/src/registry.ts +2 -0
  49. package/tsconfig.build.json +4 -0
  50. package/tsconfig.json +12 -0
  51. package/watch.mjs +6 -0
package/src/mercato.ts ADDED
@@ -0,0 +1,1106 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // Note: Generated files and DI container are imported statically to avoid ESM/CJS interop issues.
3
+ // Commands that need to run before generation (e.g., `init`) handle missing modules gracefully.
4
+
5
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
+ import { runWorker } from '@open-mercato/queue/worker'
7
+ import type { Module } from '@open-mercato/shared/modules/registry'
8
+ import { getCliModules, hasCliModules, registerCliModules } from './registry'
9
+ export { getCliModules, hasCliModules, registerCliModules }
10
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
11
+ import type { ChildProcess } from 'node:child_process'
12
+ import path from 'node:path'
13
+ import fs from 'node:fs'
14
+
15
+ let envLoaded = false
16
+
17
+ async function ensureEnvLoaded() {
18
+ if (envLoaded) return
19
+ envLoaded = true
20
+
21
+ // Try to find and load .env from the app directory
22
+ // First, try to find the app directory via resolver
23
+ try {
24
+ const { createResolver } = await import('./lib/resolver.js')
25
+ const resolver = createResolver()
26
+ const appDir = resolver.getAppDir()
27
+
28
+ // Load .env from app directory if it exists
29
+ const envPath = path.join(appDir, '.env')
30
+ if (fs.existsSync(envPath)) {
31
+ const dotenv = await import('dotenv')
32
+ dotenv.config({ path: envPath })
33
+ return
34
+ }
35
+ } catch {
36
+ // Resolver might fail during early init, fall back to default behavior
37
+ }
38
+
39
+ // Fall back to default dotenv behavior (loads from cwd)
40
+ try {
41
+ await import('dotenv/config')
42
+ } catch {}
43
+ }
44
+
45
+ // Helper to run a CLI command directly (without spawning a process)
46
+ async function runModuleCommand(
47
+ allModules: Module[],
48
+ moduleName: string,
49
+ commandName: string,
50
+ args: string[] = []
51
+ ): Promise<void> {
52
+ const mod = allModules.find((m) => m.id === moduleName)
53
+ if (!mod) {
54
+ throw new Error(`Module not found: "${moduleName}"`)
55
+ }
56
+ if (!mod.cli || mod.cli.length === 0) {
57
+ throw new Error(`Module "${moduleName}" has no CLI commands`)
58
+ }
59
+ const cmd = mod.cli.find((c) => c.command === commandName)
60
+ if (!cmd) {
61
+ throw new Error(`Command "${commandName}" not found in module "${moduleName}"`)
62
+ }
63
+ await cmd.run(args)
64
+ }
65
+
66
+ // Build all CLI modules (registered + built-in)
67
+ async function buildAllModules(): Promise<Module[]> {
68
+ const modules = getCliModules()
69
+
70
+ // Load optional app-level CLI commands
71
+ let appCli: any[] = []
72
+ try {
73
+ const dynImport: any = (Function('return import') as any)()
74
+ const app = await dynImport.then((f: any) => f('@/cli')).catch(() => null)
75
+ if (app && Array.isArray(app?.default)) appCli = app.default
76
+ } catch {}
77
+
78
+ const all = modules.slice()
79
+
80
+ if (appCli.length) all.push({ id: 'app', cli: appCli } as any)
81
+
82
+ return all
83
+ }
84
+
85
+ export async function run(argv = process.argv) {
86
+ await ensureEnvLoaded()
87
+ const [, , ...parts] = argv
88
+ const [first, second, ...remaining] = parts
89
+
90
+ // Handle init command directly
91
+ if (first === 'init') {
92
+ const { execSync } = await import('child_process')
93
+
94
+ console.log('🚀 Initializing Open Mercato app...\n')
95
+
96
+ try {
97
+ const initArgs = parts.slice(1).filter(Boolean)
98
+ const reinstall = initArgs.includes('--reinstall') || initArgs.includes('-r')
99
+ const skipExamples = initArgs.includes('--no-examples') || initArgs.includes('--no-exampls')
100
+ const stressTestEnabled =
101
+ initArgs.includes('--stresstest') || initArgs.includes('--stress-test')
102
+ const stressTestLite =
103
+ initArgs.includes('--lite') ||
104
+ initArgs.includes('--stress-lite') ||
105
+ initArgs.some((arg) => arg.startsWith('--payload=lite') || arg.startsWith('--mode=lite'))
106
+ let stressTestCount = 6000
107
+ for (let i = 0; i < initArgs.length; i += 1) {
108
+ const arg = initArgs[i]
109
+ const countPrefixes = ['--count=', '--stress-count=', '--stresstest-count=']
110
+ const matchedPrefix = countPrefixes.find((prefix) => arg.startsWith(prefix))
111
+ if (matchedPrefix) {
112
+ const value = arg.slice(matchedPrefix.length)
113
+ const parsed = Number.parseInt(value, 10)
114
+ if (Number.isFinite(parsed) && parsed > 0) {
115
+ stressTestCount = parsed
116
+ break
117
+ }
118
+ }
119
+ if (arg === '--count' || arg === '--stress-count' || arg === '--stresstest-count' || arg === '-n') {
120
+ const next = initArgs[i + 1]
121
+ if (next && !next.startsWith('-')) {
122
+ const parsed = Number.parseInt(next, 10)
123
+ if (Number.isFinite(parsed) && parsed > 0) {
124
+ stressTestCount = parsed
125
+ break
126
+ }
127
+ }
128
+ }
129
+ if (arg.startsWith('-n=')) {
130
+ const value = arg.slice(3)
131
+ const parsed = Number.parseInt(value, 10)
132
+ if (Number.isFinite(parsed) && parsed > 0) {
133
+ stressTestCount = parsed
134
+ break
135
+ }
136
+ }
137
+ }
138
+ console.log(`🔄 Reinstall mode: ${reinstall ? 'enabled' : 'disabled'}`)
139
+ console.log(`🎨 Example content: ${skipExamples ? 'skipped (--no-examples)' : 'enabled'}`)
140
+ console.log(
141
+ `🏋️ Stress test dataset: ${
142
+ stressTestEnabled
143
+ ? `enabled (target ${stressTestCount} contacts${stressTestLite ? ', lite payload' : ''})`
144
+ : 'disabled'
145
+ }`
146
+ )
147
+
148
+ if (reinstall) {
149
+ // Load env variables so DATABASE_URL is available
150
+ await ensureEnvLoaded()
151
+ console.log('♻️ Reinstall mode enabled: dropping all database tables...')
152
+ const { Client } = await import('pg')
153
+ const dbUrl = process.env.DATABASE_URL
154
+ if (!dbUrl) {
155
+ console.error('DATABASE_URL is not set. Aborting reinstall.')
156
+ return 1
157
+ }
158
+ const client = new Client({ connectionString: dbUrl })
159
+ try {
160
+ await client.connect()
161
+ // Collect all user tables in public schema
162
+ const res = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`)
163
+ const dropTargets = new Set<string>((res.rows || []).map((r: any) => String(r.tablename)))
164
+ for (const forced of ['vector_search', 'vector_search_migrations']) {
165
+ const exists = await client.query(
166
+ `SELECT to_regclass($1) AS regclass`,
167
+ [`public.${forced}`],
168
+ )
169
+ const regclass = (exists as { rows?: Array<{ regclass: string | null }> }).rows?.[0]?.regclass ?? null
170
+ if (regclass) {
171
+ dropTargets.add(forced)
172
+ }
173
+ }
174
+ if (dropTargets.size === 0) {
175
+ console.log(' No tables found in public schema.')
176
+ } else {
177
+ let dropped = 0
178
+ await client.query('BEGIN')
179
+ try {
180
+ for (const t of dropTargets) {
181
+ await client.query(`DROP TABLE IF EXISTS "${t}" CASCADE`)
182
+ dropped += 1
183
+ }
184
+ await client.query('COMMIT')
185
+ console.log(` Dropped ${dropped} tables.`)
186
+ } catch (e) {
187
+ await client.query('ROLLBACK')
188
+ throw e
189
+ }
190
+ }
191
+ } finally {
192
+ try { await client.end() } catch {}
193
+ }
194
+ // Also flush Redis
195
+ try {
196
+ const Redis = (await import('ioredis')).default
197
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'
198
+ const redis = new Redis(redisUrl)
199
+ await redis.flushall()
200
+ await redis.quit()
201
+ console.log(' Redis flushed.')
202
+ } catch {}
203
+ console.log('✅ Database cleared. Proceeding with fresh initialization...\n')
204
+ }
205
+
206
+ // Step 1: Run generators directly (no process spawn)
207
+ console.log('🔧 Preparing modules (registry, entities, DI)...')
208
+ const { createResolver } = await import('./lib/resolver')
209
+ const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi } = await import('./lib/generators')
210
+ const resolver = createResolver()
211
+ await generateEntityIds({ resolver, quiet: true })
212
+ await generateModuleRegistry({ resolver, quiet: true })
213
+ await generateModuleRegistryCli({ resolver, quiet: true })
214
+ await generateModuleEntities({ resolver, quiet: true })
215
+ await generateModuleDi({ resolver, quiet: true })
216
+ console.log('✅ Modules prepared\n')
217
+
218
+ // Step 3: Apply database migrations directly
219
+ console.log('📊 Applying database migrations...')
220
+ const { dbMigrate } = await import('./lib/db')
221
+ await dbMigrate(resolver)
222
+ console.log('✅ Migrations applied\n')
223
+
224
+ // Step 4: Bootstrap to register modules and entity IDs
225
+ // Use the shared dynamicLoader which compiles TypeScript files on-the-fly
226
+ console.log('🔗 Bootstrapping application...')
227
+ const { bootstrapFromAppRoot } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')
228
+ const bootstrapData = await bootstrapFromAppRoot(resolver.getAppDir())
229
+ // Register CLI modules directly (bootstrapFromAppRoot returns the data for this purpose)
230
+ registerCliModules(bootstrapData.modules)
231
+ console.log('✅ Bootstrap complete\n')
232
+
233
+ // Step 5: Build all modules for CLI commands
234
+ const allModules = await buildAllModules()
235
+
236
+ // Step 6: Restore configuration defaults
237
+ console.log('⚙️ Restoring module defaults...')
238
+ await runModuleCommand(allModules, 'configs', 'restore-defaults', [])
239
+ console.log('✅ Module defaults restored\n')
240
+
241
+ // Step 7: Setup RBAC (tenant/org, users, ACLs)
242
+ const findArgValue = (names: string[], fallback: string) => {
243
+ for (const name of names) {
244
+ const match = initArgs.find((arg) => arg.startsWith(name))
245
+ if (match) {
246
+ const value = match.slice(name.length)
247
+ if (value) return value
248
+ }
249
+ }
250
+ return fallback
251
+ }
252
+ const orgName = findArgValue(['--org=', '--orgName='], 'Acme Corp')
253
+ const email = findArgValue(['--email='], 'superadmin@acme.com')
254
+ const password = findArgValue(['--password='], 'secret')
255
+ const roles = findArgValue(['--roles='], 'superadmin,admin,employee')
256
+
257
+ console.log('🔐 Setting up RBAC and users...')
258
+ // Run auth setup command via CLI
259
+ await runModuleCommand(allModules, 'auth', 'setup', [
260
+ '--orgName', orgName,
261
+ '--email', email,
262
+ '--password', password,
263
+ '--roles', roles,
264
+ ])
265
+ // Query DB to get tenant/org IDs using pg directly
266
+ const { Client } = await import('pg')
267
+ const dbUrl = process.env.DATABASE_URL
268
+ const pgClient = new Client({ connectionString: dbUrl })
269
+ await pgClient.connect()
270
+ const orgResult = await pgClient.query(
271
+ `SELECT o.id as org_id, o.tenant_id FROM organizations o
272
+ JOIN users u ON u.organization_id = o.id
273
+ LIMIT 1`
274
+ )
275
+ await pgClient.end()
276
+ const tenantId = orgResult?.rows?.[0]?.tenant_id ?? null
277
+ const orgId = orgResult?.rows?.[0]?.org_id ?? null
278
+ console.log('✅ RBAC setup complete:', { tenantId, organizationId: orgId }, '\n')
279
+
280
+ console.log('🎛️ Seeding feature toggle defaults...')
281
+ await runModuleCommand(allModules, 'feature_toggles', 'seed-defaults', [])
282
+ console.log('🎛️ ✅ Feature toggle defaults seeded\n')
283
+
284
+ if (tenantId) {
285
+ console.log('👥 Seeding tenant-scoped roles...')
286
+ await runModuleCommand(allModules, 'auth', 'seed-roles', ['--tenant', tenantId])
287
+ console.log('🛡️ ✅ Roles seeded\n')
288
+ } else {
289
+ console.log('⚠️ Skipping role seeding because tenant ID was not available.\n')
290
+ }
291
+
292
+ if (orgId && tenantId) {
293
+ if (reinstall) {
294
+ console.log('🧩 Reinstalling custom field definitions...')
295
+ await runModuleCommand(allModules, 'entities', 'reinstall', ['--tenant', tenantId])
296
+ console.log('🧩 ✅ Custom field definitions reinstalled\n')
297
+ }
298
+
299
+ console.log('📚 Seeding customer dictionaries...')
300
+ await runModuleCommand(allModules, 'customers', 'seed-dictionaries', ['--tenant', tenantId, '--org', orgId])
301
+ console.log('📚 ✅ Customer dictionaries seeded\n')
302
+
303
+ console.log('🏠 Seeding staff address types...')
304
+ await runModuleCommand(allModules, 'staff', 'seed-address-types', ['--tenant', tenantId, '--org', orgId])
305
+ console.log('🏠 ✅ Staff address types seeded\n')
306
+
307
+ console.log('🏠 Seeding resources address types...')
308
+ await runModuleCommand(allModules, 'resources', 'seed-address-types', ['--tenant', tenantId, '--org', orgId])
309
+ console.log('🏠 ✅ Resources address types seeded\n')
310
+
311
+ console.log('📚 Seeding currencies...')
312
+ await runModuleCommand(allModules, 'currencies', 'seed', ['--tenant', tenantId, '--org', orgId])
313
+ console.log('📚 ✅ Currencies seeded\n')
314
+
315
+ console.log('📏 Seeding catalog units...')
316
+ await runModuleCommand(allModules, 'catalog', 'seed-units', ['--tenant', tenantId, '--org', orgId])
317
+ console.log('📏 ✅ Catalog units seeded\n')
318
+
319
+ console.log('🗓️ Seeding unavailability reasons...')
320
+ await runModuleCommand(allModules, 'planner', 'seed-unavailability-reasons', ['--tenant', tenantId, '--org', orgId])
321
+ console.log('🗓️ ✅ Unavailability reasons seeded\n')
322
+
323
+ const parsedEncryption = parseBooleanToken(process.env.TENANT_DATA_ENCRYPTION ?? 'yes')
324
+ const encryptionEnabled = parsedEncryption === null ? true : parsedEncryption
325
+ if (encryptionEnabled) {
326
+ console.log('🔒 Seeding encryption defaults...')
327
+ await runModuleCommand(allModules, 'entities', 'seed-encryption', ['--tenant', tenantId, '--org', orgId])
328
+ console.log('🔒 ✅ Encryption defaults seeded\n')
329
+ } else {
330
+ console.log('⚠️ TENANT_DATA_ENCRYPTION disabled; skipping encryption defaults.\n')
331
+ }
332
+
333
+ console.log('🏷️ Seeding catalog price kinds...')
334
+ await runModuleCommand(allModules, 'catalog', 'seed-price-kinds', ['--tenant', tenantId, '--org', orgId])
335
+ console.log('🏷️ ✅ Catalog price kinds seeded\n')
336
+
337
+ console.log('💶 Seeding default tax rates...')
338
+ await runModuleCommand(allModules, 'sales', 'seed-tax-rates', ['--tenant', tenantId, '--org', orgId])
339
+ console.log('🧾 ✅ Tax rates seeded\n')
340
+
341
+ console.log('🚦 Seeding sales statuses...')
342
+ await runModuleCommand(allModules, 'sales', 'seed-statuses', ['--tenant', tenantId, '--org', orgId])
343
+ console.log('🚦 ✅ Sales statuses seeded\n')
344
+
345
+ console.log('⚙️ Seeding adjustment kinds...')
346
+ await runModuleCommand(allModules, 'sales', 'seed-adjustment-kinds', ['--tenant', tenantId, '--org', orgId])
347
+ console.log('⚙️ ✅ Adjustment kinds seeded\n')
348
+
349
+ console.log('🚚 Seeding shipping methods...')
350
+ await runModuleCommand(allModules, 'sales', 'seed-shipping-methods', ['--tenant', tenantId, '--org', orgId])
351
+ console.log('🚚 ✅ Shipping methods seeded\n')
352
+
353
+ console.log('💳 Seeding payment methods...')
354
+ await runModuleCommand(allModules, 'sales', 'seed-payment-methods', ['--tenant', tenantId, '--org', orgId])
355
+ console.log('💳 ✅ Payment methods seeded\n')
356
+
357
+ console.log('🔄 Seeding workflow definitions...')
358
+ try {
359
+ await runModuleCommand(allModules, 'workflows', 'seed-all', ['--tenant', tenantId, '--org', orgId])
360
+ console.log('✅ Workflows and business rules seeded\n')
361
+ } catch (err) {
362
+ console.error('⚠️ Workflow seeding failed (non-fatal):', err)
363
+ }
364
+
365
+ if (skipExamples) {
366
+ console.log('🚫 Example data seeding skipped (--no-examples)\n')
367
+ } else {
368
+ console.log('🛍️ Seeding catalog examples...')
369
+ await runModuleCommand(allModules, 'catalog', 'seed-examples', ['--tenant', tenantId, '--org', orgId])
370
+ console.log('🛍️ ✅ Catalog examples seeded\n')
371
+
372
+ console.log('🏢 Seeding customer examples...')
373
+ await runModuleCommand(allModules, 'customers', 'seed-examples', ['--tenant', tenantId, '--org', orgId])
374
+ console.log('🏢 ✅ Customer examples seeded\n')
375
+
376
+ console.log('🧾 Seeding sales examples...')
377
+ await runModuleCommand(allModules, 'sales', 'seed-examples', ['--tenant', tenantId, '--org', orgId])
378
+ console.log('🧾 ✅ Sales examples seeded\n')
379
+
380
+ console.log('👥 Seeding staff examples...')
381
+ await runModuleCommand(allModules, 'staff', 'seed-examples', ['--tenant', tenantId, '--org', orgId])
382
+ console.log('👥 ✅ Staff examples seeded\n')
383
+
384
+ console.log('📦 Seeding resource capacity units...')
385
+ await runModuleCommand(allModules, 'resources', 'seed-capacity-units', ['--tenant', tenantId, '--org', orgId])
386
+ console.log('📦 ✅ Resource capacity units seeded\n')
387
+
388
+ console.log('🧰 Seeding resource examples...')
389
+ await runModuleCommand(allModules, 'resources', 'seed-examples', ['--tenant', tenantId, '--org', orgId])
390
+ console.log('🧰 ✅ Resource examples seeded\n')
391
+
392
+ console.log('🗓️ Seeding planner availability rulesets...')
393
+ await runModuleCommand(allModules, 'planner', 'seed-availability-rulesets', ['--tenant', tenantId, '--org', orgId])
394
+ console.log('🗓️ ✅ Planner availability rulesets seeded\n')
395
+
396
+ // Optional: seed example todos if the example module is enabled
397
+ const exampleModule = allModules.find((m) => m.id === 'example')
398
+ if (exampleModule && exampleModule.cli) {
399
+ console.log('📝 Seeding example todos...')
400
+ await runModuleCommand(allModules, 'example', 'seed-todos', ['--org', orgId, '--tenant', tenantId])
401
+ console.log('📝 ✅ Example todos seeded\n')
402
+ }
403
+ }
404
+
405
+ if (stressTestEnabled) {
406
+ console.log(
407
+ `🏋️ Seeding stress test customers${stressTestLite ? ' (lite payload)' : ''}...`
408
+ )
409
+ const stressArgs = ['--tenant', tenantId, '--org', orgId, '--count', String(stressTestCount)]
410
+ if (stressTestLite) stressArgs.push('--lite')
411
+ await runModuleCommand(allModules, 'customers', 'seed-stresstest', stressArgs)
412
+ console.log(`✅ Stress test customers seeded (requested ${stressTestCount})\n`)
413
+ }
414
+
415
+ console.log('🧩 Enabling default dashboard widgets...')
416
+ await runModuleCommand(allModules, 'dashboards', 'seed-defaults', ['--tenant', tenantId])
417
+ console.log('✅ Dashboard widgets enabled\n')
418
+
419
+ } else {
420
+ console.log('⚠️ Could not get organization ID or tenant ID, skipping seeding steps\n')
421
+ }
422
+
423
+ console.log('🧠 Building search indexes...')
424
+ const vectorArgs = tenantId
425
+ ? ['--tenant', tenantId, ...(orgId ? ['--org', orgId] : [])]
426
+ : ['--purgeFirst=false']
427
+ await runModuleCommand(allModules, 'search', 'reindex', vectorArgs)
428
+ console.log('✅ Search indexes built\n')
429
+
430
+ console.log('🔍 Rebuilding query indexes...')
431
+ const queryIndexArgs = ['--force', ...(tenantId ? ['--tenant', tenantId] : [])]
432
+ await runModuleCommand(allModules, 'query_index', 'reindex', queryIndexArgs)
433
+ console.log('✅ Query indexes rebuilt\n')
434
+
435
+ // Derive admin/employee only when the provided email is a superadmin email
436
+ const [local, domain] = String(email).split('@')
437
+ const isSuperadminLocal = (local || '').toLowerCase() === 'superadmin' && !!domain
438
+ const adminEmailDerived = isSuperadminLocal ? `admin@${domain}` : null
439
+ const employeeEmailDerived = isSuperadminLocal ? `employee@${domain}` : null
440
+
441
+ // Simplified success message: we know which users were created
442
+ console.log('🎉 App initialization complete!\n')
443
+ console.log('╔══════════════════════════════════════════════════════════════╗')
444
+ console.log('║ 🚀 You\'re now ready to start development! ║')
445
+ console.log('║ ║')
446
+ console.log('║ Start the dev server: ║')
447
+ console.log('║ yarn dev ║')
448
+ console.log('║ ║')
449
+ console.log('║ Users created: ║')
450
+ console.log(`║ 👑 Superadmin: ${email.padEnd(42)} ║`)
451
+ console.log(`║ Password: ${password.padEnd(44)} ║`)
452
+ if (adminEmailDerived) {
453
+ console.log(`║ 🧰 Admin: ${adminEmailDerived.padEnd(42)} ║`)
454
+ console.log(`║ Password: ${password.padEnd(44)} ║`)
455
+ }
456
+ if (employeeEmailDerived) {
457
+ console.log(`║ 👷 Employee: ${employeeEmailDerived.padEnd(42)} ║`)
458
+ console.log(`║ Password: ${password.padEnd(44)} ║`)
459
+ }
460
+ console.log('║ ║')
461
+ console.log('║ Happy coding! ║')
462
+ console.log('╚══════════════════════════════════════════════════════════════╝')
463
+
464
+ return 0
465
+ } catch (error: unknown) {
466
+ if (error instanceof Error) {
467
+ console.error('❌ Initialization failed:', error.message)
468
+ } else {
469
+ console.error('❌ Initialization failed:', error)
470
+ }
471
+ return 1
472
+ }
473
+ }
474
+
475
+ let modName = first
476
+ let cmdName = second
477
+ let rest = remaining
478
+
479
+ if (first === 'reindex') {
480
+ modName = 'query_index'
481
+ cmdName = 'reindex'
482
+ rest = second !== undefined ? [second, ...remaining] : remaining
483
+ }
484
+
485
+ // Handle 'mercato generate' without subcommand - default to 'generate all'
486
+ if (first === 'generate' && !second) {
487
+ cmdName = 'all'
488
+ rest = remaining
489
+ }
490
+
491
+ // Load modules from registered CLI modules
492
+ const modules = getCliModules()
493
+
494
+ // Load optional app-level CLI commands lazily without static import resolution
495
+ let appCli: any[] = []
496
+ try {
497
+ const dynImport: any = (Function('return import') as any)()
498
+ const app = await dynImport.then((f: any) => f('@/cli')).catch(() => null)
499
+ if (app && Array.isArray(app?.default)) appCli = app.default
500
+ } catch {}
501
+ const all = modules.slice()
502
+
503
+ // Built-in CLI module: queue
504
+ all.push({
505
+ id: 'queue',
506
+ cli: [
507
+ {
508
+ command: 'worker',
509
+ run: async (args: string[]) => {
510
+ const isAllQueues = args.includes('--all')
511
+ const queueName = isAllQueues ? null : args[0]
512
+
513
+ // Collect all discovered workers from modules
514
+ type WorkerEntry = {
515
+ id: string
516
+ queue: string
517
+ concurrency: number
518
+ handler: (job: unknown, ctx: unknown) => Promise<void> | void
519
+ }
520
+ const allWorkers: WorkerEntry[] = []
521
+ for (const mod of getCliModules()) {
522
+ const modWorkers = (mod as { workers?: WorkerEntry[] }).workers
523
+ if (modWorkers) {
524
+ allWorkers.push(...modWorkers)
525
+ }
526
+ }
527
+ const discoveredQueues = [...new Set(allWorkers.map((w) => w.queue))]
528
+
529
+ if (!queueName && !isAllQueues) {
530
+ console.error('Usage: mercato queue worker <queueName> | --all')
531
+ console.error('Example: mercato queue worker events')
532
+ console.error('Example: mercato queue worker --all')
533
+ if (discoveredQueues.length > 0) {
534
+ console.error(`Discovered queues: ${discoveredQueues.join(', ')}`)
535
+ }
536
+ return
537
+ }
538
+
539
+ const concurrencyArg = args.find((a) => a.startsWith('--concurrency='))
540
+ const concurrencyOverride = concurrencyArg ? Number(concurrencyArg.split('=')[1]) : undefined
541
+
542
+ if (isAllQueues) {
543
+ // Run workers for all discovered queues
544
+ if (discoveredQueues.length === 0) {
545
+ console.error('[worker] No queues discovered from modules')
546
+ return
547
+ }
548
+
549
+ const container = await createRequestContainer()
550
+ console.log(`[worker] Starting workers for all queues: ${discoveredQueues.join(', ')}`)
551
+
552
+ // Start all queue workers in background mode
553
+ const workerPromises = discoveredQueues.map(async (queue) => {
554
+ const queueWorkers = allWorkers.filter((w) => w.queue === queue)
555
+ const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1)
556
+
557
+ console.log(`[worker] Starting "${queue}" with ${queueWorkers.length} handler(s), concurrency: ${concurrency}`)
558
+
559
+ await runWorker({
560
+ queueName: queue,
561
+ connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
562
+ concurrency,
563
+ background: true,
564
+ handler: async (job, ctx) => {
565
+ for (const worker of queueWorkers) {
566
+ await worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) })
567
+ }
568
+ },
569
+ })
570
+ })
571
+
572
+ await Promise.all(workerPromises)
573
+
574
+ console.log('[worker] All workers started. Press Ctrl+C to stop')
575
+
576
+ // Keep the process alive
577
+ await new Promise(() => {})
578
+ } else {
579
+ // Find workers for this specific queue
580
+ const queueWorkers = allWorkers.filter((w) => w.queue === queueName)
581
+
582
+ if (queueWorkers.length > 0) {
583
+ // Use discovered workers
584
+ const container = await createRequestContainer()
585
+ const concurrency = concurrencyOverride ?? Math.max(...queueWorkers.map((w) => w.concurrency), 1)
586
+
587
+ console.log(`[worker] Found ${queueWorkers.length} worker(s) for queue "${queueName}"`)
588
+
589
+ await runWorker({
590
+ queueName: queueName!,
591
+ connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
592
+ concurrency,
593
+ handler: async (job, ctx) => {
594
+ for (const worker of queueWorkers) {
595
+ await worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) })
596
+ }
597
+ },
598
+ })
599
+ } else {
600
+ console.error(`No workers found for queue "${queueName}"`)
601
+ if (discoveredQueues.length > 0) {
602
+ console.error(`Available queues: ${discoveredQueues.join(', ')}`)
603
+ }
604
+ }
605
+ }
606
+ },
607
+ },
608
+ {
609
+ command: 'clear',
610
+ run: async (args: string[]) => {
611
+ const queueName = args[0]
612
+ if (!queueName) {
613
+ console.error('Usage: mercato queue clear <queueName>')
614
+ return
615
+ }
616
+
617
+ const strategyEnv = process.env.QUEUE_STRATEGY || 'local'
618
+ const { createQueue } = await import('@open-mercato/queue')
619
+
620
+ const queue = strategyEnv === 'async'
621
+ ? createQueue(queueName, 'async', {
622
+ connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
623
+ })
624
+ : createQueue(queueName, 'local')
625
+
626
+ const res = await queue.clear()
627
+ await queue.close()
628
+ console.log(`Cleared queue "${queueName}", removed ${res.removed} jobs`)
629
+ },
630
+ },
631
+ {
632
+ command: 'status',
633
+ run: async (args: string[]) => {
634
+ const queueName = args[0]
635
+ if (!queueName) {
636
+ console.error('Usage: mercato queue status <queueName>')
637
+ return
638
+ }
639
+
640
+ const strategyEnv = process.env.QUEUE_STRATEGY || 'local'
641
+ const { createQueue } = await import('@open-mercato/queue')
642
+
643
+ const queue = strategyEnv === 'async'
644
+ ? createQueue(queueName, 'async', {
645
+ connection: { url: process.env.REDIS_URL || process.env.QUEUE_REDIS_URL },
646
+ })
647
+ : createQueue(queueName, 'local')
648
+
649
+ const counts = await queue.getJobCounts()
650
+ console.log(`Queue "${queueName}" status:`)
651
+ console.log(` Waiting: ${counts.waiting}`)
652
+ console.log(` Active: ${counts.active}`)
653
+ console.log(` Completed: ${counts.completed}`)
654
+ console.log(` Failed: ${counts.failed}`)
655
+ await queue.close()
656
+ },
657
+ },
658
+ ],
659
+ } as any)
660
+
661
+ // Built-in CLI module: events
662
+ all.push({
663
+ id: 'events',
664
+ cli: [
665
+ {
666
+ command: 'emit',
667
+ run: async (args: string[]) => {
668
+ const eventName = args[0]
669
+ if (!eventName) {
670
+ console.error('Usage: mercato events emit <event> [jsonPayload] [--persistent|-p]')
671
+ return
672
+ }
673
+ const persistent = args.includes('--persistent') || args.includes('-p')
674
+ const payloadArg = args[1] && !args[1].startsWith('--') ? args[1] : undefined
675
+ let payload: any = {}
676
+ if (payloadArg) {
677
+ try { payload = JSON.parse(payloadArg) } catch { payload = payloadArg }
678
+ }
679
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
680
+ const container = await createRequestContainer()
681
+ const bus = (container.resolve('eventBus') as any)
682
+ await bus.emit(eventName, payload, { persistent })
683
+ console.log(`Emitted "${eventName}"${persistent ? ' (persistent)' : ''}`)
684
+ },
685
+ },
686
+ {
687
+ command: 'clear',
688
+ run: async () => {
689
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
690
+ const container = await createRequestContainer()
691
+ const bus = (container.resolve('eventBus') as any)
692
+ const res = await bus.clearQueue()
693
+ console.log(`Cleared events queue, removed ${res.removed} events`)
694
+ },
695
+ },
696
+ ],
697
+ } as any)
698
+
699
+ // Built-in CLI module: scaffold
700
+ all.push({
701
+ id: 'scaffold',
702
+ cli: [
703
+ {
704
+ command: 'module',
705
+ run: async (args: string[]) => {
706
+ const name = (args[0] || '').trim()
707
+ if (!name) {
708
+ console.error('Usage: mercato scaffold module <name>')
709
+ return
710
+ }
711
+ const fs = await import('node:fs')
712
+ const path = await import('node:path')
713
+ const { execSync } = await import('node:child_process')
714
+ const base = path.resolve('src/modules', name)
715
+ const folders = ['api', 'backend', 'frontend', 'data', 'subscribers']
716
+ for (const f of folders) fs.mkdirSync(path.join(base, f), { recursive: true })
717
+ const moduleTitle = `${name[0].toUpperCase()}${name.slice(1)}`
718
+ const indexTs = `export const metadata = { title: '${moduleTitle}', group: 'Modules' }\n`
719
+ fs.writeFileSync(path.join(base, 'index.ts'), indexTs, { flag: 'wx' })
720
+ const ceTs = `export const entities = [\n {\n id: '${name}:sample',\n label: '${moduleTitle} Sample',\n description: 'Describe your custom entity',\n showInSidebar: true,\n fields: [\n // { key: 'priority', kind: 'integer', label: 'Priority' },\n ],\n },\n]\n\nexport default entities\n`
721
+ fs.writeFileSync(path.join(base, 'ce.ts'), ceTs, { flag: 'wx' })
722
+ const entitiesTs = `import { Entity, PrimaryKey, Property } from '@mikro-orm/core'\n\n// Add your entities here. Example:\n// @Entity({ tableName: '${name}_items' })\n// export class ${moduleTitle}Item {\n// @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string\n// @Property({ type: 'text' }) title!: string\n// @Property({ name: 'organization_id', type: 'uuid', nullable: true }) organizationId?: string | null\n// @Property({ name: 'tenant_id', type: 'uuid', nullable: true }) tenantId?: string | null\n// @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date()\n// @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() }) updatedAt: Date = new Date()\n// @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null\n// }\n`
723
+ fs.writeFileSync(path.join(base, 'data', 'entities.ts'), entitiesTs, { flag: 'wx' })
724
+ console.log(`Created module at ${path.relative(process.cwd(), base)}`)
725
+ execSync('yarn modules:prepare', { stdio: 'inherit' })
726
+ },
727
+ },
728
+ {
729
+ command: 'entity',
730
+ run: async () => {
731
+ const fs = await import('node:fs')
732
+ const path = await import('node:path')
733
+ const readline = await import('node:readline/promises')
734
+ const { stdin: input, stdout: output } = await import('node:process')
735
+ const { execSync } = await import('node:child_process')
736
+ const rl = readline.createInterface({ input, output })
737
+ try {
738
+ const moduleId = (await rl.question('Module id (folder under src/modules): ')).trim()
739
+ const className = (await rl.question('Entity class name (e.g., Todo): ')).trim()
740
+ const tableName = (await rl.question(`DB table name (default: ${className.toLowerCase()}s): `)).trim() || `${className.toLowerCase()}s`
741
+ const extra = (await rl.question('Additional fields (comma list name:type, e.g., title:text,is_done:boolean): ')).trim()
742
+ const extras = extra
743
+ ? extra.split(',').map(s => s.trim()).filter(Boolean).map(s => {
744
+ const [n,t] = s.split(':').map(x=>x.trim()); return { n, t }
745
+ })
746
+ : []
747
+ const base = path.resolve('src/modules', moduleId, 'data')
748
+ fs.mkdirSync(base, { recursive: true })
749
+ const file = path.join(base, 'entities.ts')
750
+ let content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : `import { Entity, PrimaryKey, Property } from '@mikro-orm/core'\n\n`
751
+ content += `\n@Entity({ tableName: '${tableName}' })\nexport class ${className} {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n`
752
+ for (const f of extras) {
753
+ const n = f.n
754
+ const t = f.t
755
+ if (!n || !t) continue
756
+ const map = {
757
+ text: { ts: 'string', db: 'text' },
758
+ multiline: { ts: 'string', db: 'text' },
759
+ integer: { ts: 'number', db: 'int' },
760
+ float: { ts: 'number', db: 'float' },
761
+ boolean: { ts: 'boolean', db: 'boolean' },
762
+ date: { ts: 'Date', db: 'Date' },
763
+ } as const
764
+ const info = map[t as keyof typeof map]
765
+ const fallback = { ts: 'string', db: 'text' }
766
+ const resolved = info || fallback
767
+ const propName = n.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
768
+ const columnName = n.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)
769
+ const dbType = resolved.db
770
+ const tsType = resolved.ts
771
+ const defaultValue =
772
+ resolved.ts === 'boolean' ? ' = false' :
773
+ resolved.ts === 'Date' ? ' = new Date()' :
774
+ ''
775
+ content += `\n @Property({ name: '${columnName}', type: ${dbType === 'Date' ? 'Date' : `'${dbType}'`}${resolved.ts === 'boolean' ? ', default: false' : ''} })\n ${propName}${tsType === 'number' ? '?: number | null' : tsType === 'boolean' ? ': boolean' : tsType === 'Date' ? ': Date' : '!: string'}${defaultValue}\n`
776
+ }
777
+ content += `}\n`
778
+ fs.writeFileSync(file, content)
779
+ console.log(`Updated ${path.relative(process.cwd(), file)}`)
780
+ console.log('Generating and applying migrations...')
781
+ execSync('yarn modules:prepare', { stdio: 'inherit' })
782
+ execSync('yarn db:generate', { stdio: 'inherit' })
783
+ execSync('yarn db:migrate', { stdio: 'inherit' })
784
+ } finally {
785
+ rl.close()
786
+ }
787
+ },
788
+ },
789
+ {
790
+ command: 'crud',
791
+ run: async (args: string[]) => {
792
+ const fs = await import('node:fs')
793
+ const path = await import('node:path')
794
+ const { execSync } = await import('node:child_process')
795
+ const mod = (args[0] || '').trim()
796
+ const entity = (args[1] || '').trim()
797
+ const routeSeg = (args[2] || '').trim() || `${entity.toLowerCase()}s`
798
+ if (!mod || !entity) {
799
+ console.error('Usage: mercato scaffold crud <moduleId> <EntityClass> [routeSegment]')
800
+ return
801
+ }
802
+ const baseDir = path.resolve('src/modules', mod, 'api', routeSeg)
803
+ fs.mkdirSync(baseDir, { recursive: true })
804
+ const entitySnake = entity.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
805
+ const tmpl = `import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { ${entity} } from '@open-mercato/shared/modules/${mod}/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport ceEntities from '@open-mercato/shared/modules/${mod}/ce'\nimport { buildCustomFieldSelectorsForEntity, extractCustomFieldsFromItem, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'\nimport type { CustomFieldSet } from '@open-mercato/shared/modules/entities'\n\n// Field constants - update these based on your entity's actual fields\nconst F = {\n id: 'id',\n tenant_id: 'tenant_id',\n organization_id: 'organization_id',\n created_at: 'created_at',\n updated_at: 'updated_at',\n deleted_at: 'deleted_at',\n} as const\n\nconst querySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n sortField: z.string().optional().default('id'),\n sortDir: z.enum(['asc','desc']).optional().default('asc'),\n withDeleted: z.coerce.boolean().optional().default(false),\n}).passthrough()\n\nconst createSchema = z.object({}).passthrough()\nconst updateSchema = z.object({ id: z.string().uuid() }).passthrough()\n\ntype Query = z.infer<typeof querySchema>\n\nconst fieldSets: CustomFieldSet[] = []\nconst ceEntity = Array.isArray(ceEntities) ? ceEntities.find((entity) => entity?.id === '${mod}:${entitySnake}') : undefined\nif (ceEntity?.fields?.length) {\n fieldSets.push({ entity: ceEntity.id, fields: ceEntity.fields, source: '${mod}' })\n}\n\nconst cfSel = buildCustomFieldSelectorsForEntity(E.${mod}.${entitySnake}, fieldSets)\nconst sortFieldMap: Record<string, unknown> = { id: F.id, created_at: F.created_at, ...Object.fromEntries(cfSel.keys.map(k => [\`cf_\${k}\`, \`cf:\${k}\`])) }\n\nexport const { metadata, GET, POST, PUT, DELETE } = makeCrudRoute({\n metadata: { GET: { requireAuth: true }, POST: { requireAuth: true }, PUT: { requireAuth: true }, DELETE: { requireAuth: true } },\n orm: { entity: ${entity}, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },\n events: { module: '${mod}', entity: '${entitySnake}', persistent: true },\n indexer: { entityType: E.${mod}.${entitySnake} },\n list: {\n schema: querySchema,\n entityId: E.${mod}.${entitySnake},\n fields: [F.id, F.created_at, ...cfSel.selectors],\n sortFieldMap,\n buildFilters: async (q: Query, ctx) => ({\n ...(await buildCustomFieldFiltersFromQuery({\n entityId: E.${mod}.${entitySnake},\n query: q as any,\n em: ctx.container.resolve('em'),\n tenantId: ctx.auth!.tenantId,\n })),\n }),\n transformItem: (item: any) => ({ id: item.id, created_at: item.created_at, ...extractCustomFieldsFromItem(item, cfSel.keys) }),\n },\n create: { schema: createSchema, mapToEntity: (input: any) => ({}), customFields: { enabled: true, entityId: E.${mod}.${entitySnake}, pickPrefixed: true } },\n update: { schema: updateSchema, applyToEntity: (entity: ${entity}, input: any) => {}, customFields: { enabled: true, entityId: E.${mod}.${entitySnake}, pickPrefixed: true } },\n del: { idFrom: 'query', softDelete: true },\n})\n`
806
+ const file = path.join(baseDir, 'route.ts')
807
+ fs.writeFileSync(file, tmpl, { flag: 'wx' })
808
+ console.log(`Created CRUD route: ${path.relative(process.cwd(), file)}`)
809
+ execSync('yarn modules:prepare', { stdio: 'inherit' })
810
+ },
811
+ },
812
+ ],
813
+ } as any)
814
+
815
+ // Built-in CLI module: generate
816
+ all.push({
817
+ id: 'generate',
818
+ cli: [
819
+ {
820
+ command: 'all',
821
+ run: async (args: string[]) => {
822
+ const { createResolver } = await import('./lib/resolver')
823
+ const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi } = await import('./lib/generators')
824
+ const resolver = createResolver()
825
+ const quiet = args.includes('--quiet') || args.includes('-q')
826
+
827
+ console.log('Running all generators...')
828
+ await generateEntityIds({ resolver, quiet })
829
+ await generateModuleRegistry({ resolver, quiet })
830
+ await generateModuleRegistryCli({ resolver, quiet })
831
+ await generateModuleEntities({ resolver, quiet })
832
+ await generateModuleDi({ resolver, quiet })
833
+ console.log('All generators completed.')
834
+ },
835
+ },
836
+ {
837
+ command: 'entity-ids',
838
+ run: async (args: string[]) => {
839
+ const { createResolver } = await import('./lib/resolver')
840
+ const { generateEntityIds } = await import('./lib/generators')
841
+ const resolver = createResolver()
842
+ await generateEntityIds({ resolver, quiet: args.includes('--quiet') })
843
+ },
844
+ },
845
+ {
846
+ command: 'registry',
847
+ run: async (args: string[]) => {
848
+ const { createResolver } = await import('./lib/resolver')
849
+ const { generateModuleRegistry } = await import('./lib/generators')
850
+ const resolver = createResolver()
851
+ await generateModuleRegistry({ resolver, quiet: args.includes('--quiet') })
852
+ },
853
+ },
854
+ {
855
+ command: 'entities',
856
+ run: async (args: string[]) => {
857
+ const { createResolver } = await import('./lib/resolver')
858
+ const { generateModuleEntities } = await import('./lib/generators')
859
+ const resolver = createResolver()
860
+ await generateModuleEntities({ resolver, quiet: args.includes('--quiet') })
861
+ },
862
+ },
863
+ {
864
+ command: 'di',
865
+ run: async (args: string[]) => {
866
+ const { createResolver } = await import('./lib/resolver')
867
+ const { generateModuleDi } = await import('./lib/generators')
868
+ const resolver = createResolver()
869
+ await generateModuleDi({ resolver, quiet: args.includes('--quiet') })
870
+ },
871
+ },
872
+ ],
873
+ } as any)
874
+
875
+ // Built-in CLI module: db
876
+ all.push({
877
+ id: 'db',
878
+ cli: [
879
+ {
880
+ command: 'generate',
881
+ run: async () => {
882
+ const { createResolver } = await import('./lib/resolver')
883
+ const { dbGenerate } = await import('./lib/db')
884
+ const resolver = createResolver()
885
+ await dbGenerate(resolver)
886
+ },
887
+ },
888
+ {
889
+ command: 'migrate',
890
+ run: async () => {
891
+ const { createResolver } = await import('./lib/resolver')
892
+ const { dbMigrate } = await import('./lib/db')
893
+ const resolver = createResolver()
894
+ await dbMigrate(resolver)
895
+ },
896
+ },
897
+ {
898
+ command: 'greenfield',
899
+ run: async (args: string[]) => {
900
+ const { createResolver } = await import('./lib/resolver')
901
+ const { dbGreenfield } = await import('./lib/db')
902
+ const resolver = createResolver()
903
+ const yes = args.includes('--yes') || args.includes('-y')
904
+ await dbGreenfield(resolver, { yes })
905
+ },
906
+ },
907
+ ],
908
+ } as any)
909
+
910
+ // Built-in CLI module: server (runs Next.js + workers)
911
+ all.push({
912
+ id: 'server',
913
+ cli: [
914
+ {
915
+ command: 'dev',
916
+ run: async () => {
917
+ const { spawn } = await import('child_process')
918
+ const path = await import('path')
919
+ const { createResolver } = await import('./lib/resolver')
920
+ const resolver = createResolver()
921
+ const appDir = resolver.getAppDir()
922
+
923
+ // In monorepo, packages are hoisted to root; in standalone, they're in app's node_modules
924
+ const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir
925
+
926
+ const processes: ChildProcess[] = []
927
+ const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
928
+
929
+ function cleanup() {
930
+ console.log('[server] Shutting down...')
931
+ for (const proc of processes) {
932
+ if (!proc.killed) {
933
+ proc.kill('SIGTERM')
934
+ }
935
+ }
936
+ }
937
+
938
+ process.on('SIGTERM', cleanup)
939
+ process.on('SIGINT', cleanup)
940
+
941
+ console.log('[server] Starting Open Mercato in dev mode...')
942
+
943
+ // Resolve paths relative to where node_modules are located
944
+ const nextBin = path.join(nodeModulesBase, 'node_modules/next/dist/bin/next')
945
+ const mercatoBin = path.join(nodeModulesBase, 'node_modules/@open-mercato/cli/bin/mercato')
946
+
947
+ // Start Next.js dev
948
+ const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
949
+ stdio: 'inherit',
950
+ env: process.env,
951
+ cwd: appDir,
952
+ })
953
+ processes.push(nextProcess)
954
+
955
+ // Start workers if enabled
956
+ if (autoSpawnWorkers) {
957
+ console.log('[server] Starting workers for all queues...')
958
+ const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
959
+ stdio: 'inherit',
960
+ env: process.env,
961
+ cwd: appDir,
962
+ })
963
+ processes.push(workerProcess)
964
+ }
965
+
966
+ // Wait for any process to exit
967
+ await Promise.race(
968
+ processes.map(
969
+ (proc) =>
970
+ new Promise<void>((resolve) => {
971
+ proc.on('exit', () => resolve())
972
+ })
973
+ )
974
+ )
975
+
976
+ cleanup()
977
+ },
978
+ },
979
+ {
980
+ command: 'start',
981
+ run: async () => {
982
+ const { spawn } = await import('child_process')
983
+ const path = await import('path')
984
+ const { createResolver } = await import('./lib/resolver')
985
+ const resolver = createResolver()
986
+ const appDir = resolver.getAppDir()
987
+
988
+ // In monorepo, packages are hoisted to root; in standalone, they're in app's node_modules
989
+ const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir
990
+
991
+ const processes: ChildProcess[] = []
992
+ const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
993
+
994
+ function cleanup() {
995
+ console.log('[server] Shutting down...')
996
+ for (const proc of processes) {
997
+ if (!proc.killed) {
998
+ proc.kill('SIGTERM')
999
+ }
1000
+ }
1001
+ }
1002
+
1003
+ process.on('SIGTERM', cleanup)
1004
+ process.on('SIGINT', cleanup)
1005
+
1006
+ console.log('[server] Starting Open Mercato in production mode...')
1007
+
1008
+ // Resolve paths relative to where node_modules are located
1009
+ const nextBin = path.join(nodeModulesBase, 'node_modules/next/dist/bin/next')
1010
+ const mercatoBin = path.join(nodeModulesBase, 'node_modules/@open-mercato/cli/bin/mercato')
1011
+
1012
+ // Start Next.js production server
1013
+ const nextProcess = spawn('node', [nextBin, 'start'], {
1014
+ stdio: 'inherit',
1015
+ env: process.env,
1016
+ cwd: appDir,
1017
+ })
1018
+ processes.push(nextProcess)
1019
+
1020
+ // Start workers if enabled
1021
+ if (autoSpawnWorkers) {
1022
+ console.log('[server] Starting workers for all queues...')
1023
+ const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
1024
+ stdio: 'inherit',
1025
+ env: process.env,
1026
+ cwd: appDir,
1027
+ })
1028
+ processes.push(workerProcess)
1029
+ }
1030
+
1031
+ // Wait for any process to exit
1032
+ await Promise.race(
1033
+ processes.map(
1034
+ (proc) =>
1035
+ new Promise<void>((resolve) => {
1036
+ proc.on('exit', () => resolve())
1037
+ })
1038
+ )
1039
+ )
1040
+
1041
+ cleanup()
1042
+ },
1043
+ },
1044
+ ],
1045
+ } as any)
1046
+
1047
+ if (appCli.length) all.push({ id: 'app', cli: appCli } as any)
1048
+
1049
+ const quietBanner = process.env.OM_CLI_QUIET === '1'
1050
+ const banner = '🧩 Open Mercato CLI'
1051
+ if (!quietBanner) {
1052
+ const header = [
1053
+ '╔═══════════════════════╗',
1054
+ `║ ${banner.padEnd(21)}║`,
1055
+ '╚═══════════════════════╝',
1056
+ ].join('\n')
1057
+ console.log(header)
1058
+ }
1059
+ const pad = (s: string) => ` ${s}`
1060
+
1061
+ if (!modName || modName === 'help' || modName === '--help' || modName === '-h') {
1062
+ console.log(pad('Usage: ✨ mercato <module> <command> [args]'))
1063
+ const list = all
1064
+ .filter((m) => m.cli && m.cli.length)
1065
+ .map((m) => `• ${m.id}: ${m.cli!.map((c) => `"${c.command}"`).join(', ')}`)
1066
+ if (list.length) {
1067
+ console.log('\n' + pad('Available:'))
1068
+ console.log(list.map(pad).join('\n'))
1069
+ } else {
1070
+ console.log(pad('🌀 No CLI commands available'))
1071
+ }
1072
+ return 0
1073
+ }
1074
+
1075
+ const mod = all.find((m) => m.id === modName)
1076
+ if (!mod) {
1077
+ console.error(`❌ Module not found: "${modName}"`)
1078
+ return 1
1079
+ }
1080
+ if (!mod.cli || mod.cli.length === 0) {
1081
+ console.error(`🚫 Module "${modName}" has no CLI commands`)
1082
+ return 1
1083
+ }
1084
+ if (!cmdName) {
1085
+ console.log(pad(`Commands for "${modName}": ${mod.cli.map((c) => c.command).join(', ')}`))
1086
+ return 1
1087
+ }
1088
+ const cmd = mod.cli.find((c) => c.command === cmdName)
1089
+ if (!cmd) {
1090
+ console.error(`🤔 Unknown command "${cmdName}". Available: ${mod.cli.map((c) => c.command).join(', ')}`)
1091
+ return 1
1092
+ }
1093
+
1094
+ console.log('')
1095
+ const started = Date.now()
1096
+ console.log(`🚀 Running ${modName}:${cmdName} ${rest.join(' ')}`)
1097
+ try {
1098
+ await cmd.run(rest)
1099
+ const ms = Date.now() - started
1100
+ console.log(`⏱️ Done in ${ms}ms`)
1101
+ return 0
1102
+ } catch (e: any) {
1103
+ console.error(`💥 Failed: ${e?.message || e}`)
1104
+ return 1
1105
+ }
1106
+ }