@open-mercato/cli 0.4.9-develop-db9ecc46fc → 0.4.9-develop-d989387b7a

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 (58) hide show
  1. package/dist/agentic/shared/AGENTS.md.template +2 -0
  2. package/dist/agentic/shared/ai/skills/eject-and-customize/SKILL.md +3 -1
  3. package/dist/bin.js +1 -0
  4. package/dist/bin.js.map +2 -2
  5. package/dist/lib/__fixtures__/official-module-package/src/index.js +1 -0
  6. package/dist/lib/__fixtures__/official-module-package/src/index.js.map +7 -0
  7. package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js +10 -0
  8. package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js.map +7 -0
  9. package/dist/lib/eject.js +30 -38
  10. package/dist/lib/eject.js.map +2 -2
  11. package/dist/lib/generators/index.js +2 -0
  12. package/dist/lib/generators/index.js.map +2 -2
  13. package/dist/lib/generators/module-package-sources.js +45 -0
  14. package/dist/lib/generators/module-package-sources.js.map +7 -0
  15. package/dist/lib/module-install-args.js +40 -0
  16. package/dist/lib/module-install-args.js.map +7 -0
  17. package/dist/lib/module-install.js +157 -0
  18. package/dist/lib/module-install.js.map +7 -0
  19. package/dist/lib/module-package.js +245 -0
  20. package/dist/lib/module-package.js.map +7 -0
  21. package/dist/lib/modules-config.js +255 -0
  22. package/dist/lib/modules-config.js.map +7 -0
  23. package/dist/lib/resolver.js +19 -5
  24. package/dist/lib/resolver.js.map +2 -2
  25. package/dist/lib/testing/integration-discovery.js +20 -9
  26. package/dist/lib/testing/integration-discovery.js.map +2 -2
  27. package/dist/lib/testing/integration.js +86 -47
  28. package/dist/lib/testing/integration.js.map +2 -2
  29. package/dist/mercato.js +120 -43
  30. package/dist/mercato.js.map +3 -3
  31. package/package.json +5 -4
  32. package/src/__tests__/mercato.test.ts +6 -1
  33. package/src/bin.ts +1 -0
  34. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/index.js +2 -0
  35. package/src/lib/__fixtures__/official-module-package/package.json +33 -0
  36. package/src/lib/__fixtures__/official-module-package/src/index.ts +1 -0
  37. package/src/lib/__fixtures__/official-module-package/src/modules/test_package/index.ts +6 -0
  38. package/src/lib/__fixtures__/official-module-package/src/modules/test_package/widgets/injection/test/widget.tsx +3 -0
  39. package/src/lib/__tests__/eject.test.ts +107 -1
  40. package/src/lib/__tests__/module-install-args.test.ts +35 -0
  41. package/src/lib/__tests__/module-install.test.ts +217 -0
  42. package/src/lib/__tests__/module-package.test.ts +215 -0
  43. package/src/lib/__tests__/modules-config.test.ts +104 -0
  44. package/src/lib/__tests__/resolve-environment.test.ts +141 -0
  45. package/src/lib/eject.ts +45 -55
  46. package/src/lib/generators/__tests__/generators.test.ts +11 -0
  47. package/src/lib/generators/__tests__/module-package-sources.test.ts +121 -0
  48. package/src/lib/generators/index.ts +1 -0
  49. package/src/lib/generators/module-package-sources.ts +59 -0
  50. package/src/lib/module-install-args.ts +50 -0
  51. package/src/lib/module-install.ts +234 -0
  52. package/src/lib/module-package.ts +355 -0
  53. package/src/lib/modules-config.ts +393 -0
  54. package/src/lib/resolver.ts +46 -4
  55. package/src/lib/testing/__tests__/integration-discovery.test.ts +30 -0
  56. package/src/lib/testing/integration-discovery.ts +23 -8
  57. package/src/lib/testing/integration.ts +97 -57
  58. package/src/mercato.ts +128 -49
@@ -1,12 +1,13 @@
1
1
  import { GenericContainer } from 'testcontainers'
2
2
  import { spawn, type ChildProcess, type StdioOptions } from 'node:child_process'
3
3
  import { createServer } from 'node:net'
4
+ import { existsSync, readFileSync } from 'node:fs'
4
5
  import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
5
6
  import { createHash } from 'node:crypto'
6
7
  import path from 'node:path'
7
8
  import { createInterface, type Interface } from 'node:readline/promises'
8
9
  import { stdin as input, stdout as output } from 'node:process'
9
- import { createResolver } from '../resolver'
10
+ import { resolveEnvironment } from '../resolver'
10
11
  import { discoverIntegrationSpecFiles as discoverIntegrationSpecFilesShared } from './integration-discovery'
11
12
  import { resolveDockerHostFromContext, runCommandAndCapture } from './runtime-utils'
12
13
 
@@ -162,8 +163,50 @@ const PLAYWRIGHT_QUICK_FAILURE_THRESHOLD = 6
162
163
  const PLAYWRIGHT_QUICK_FAILURE_MAX_DURATION_MS = 1_500
163
164
  const PLAYWRIGHT_HEALTH_PROBE_INTERVAL_MS = 3_000
164
165
  const ANSI_ESCAPE_REGEX = /\x1b\[[0-?]*[ -/]*[@-~]/g // NOSONAR — ANSI escape sequence pattern
165
- const resolver = createResolver()
166
- const projectRootDirectory = resolver.getRootDir()
166
+ const env = resolveEnvironment()
167
+ const projectRootDirectory = env.rootDir
168
+ const appDirectory = env.appDir
169
+ const corePackageRootDirectory = env.packageRoot('@open-mercato/core')
170
+ const uiPackageRootDirectory = env.packageRoot('@open-mercato/ui')
171
+
172
+ function resolveFirstExistingPath(...candidates: string[]): string | null {
173
+ for (const candidate of candidates) {
174
+ if (existsSync(candidate)) {
175
+ return candidate
176
+ }
177
+ }
178
+ return null
179
+ }
180
+
181
+ function collectExistingPaths(candidates: Array<string | null | undefined>): string[] {
182
+ const collected = new Set<string>()
183
+ for (const candidate of candidates) {
184
+ if (candidate && existsSync(candidate)) {
185
+ collected.add(candidate)
186
+ }
187
+ }
188
+ return Array.from(collected)
189
+ }
190
+
191
+ function readPackageScripts(packageRoot: string): Record<string, string> {
192
+ try {
193
+ const raw = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8')) as {
194
+ scripts?: Record<string, string>
195
+ }
196
+ return raw.scripts ?? {}
197
+ } catch {
198
+ return {}
199
+ }
200
+ }
201
+
202
+ const projectScripts = readPackageScripts(projectRootDirectory)
203
+ const appNextConfigPath = resolveFirstExistingPath(
204
+ path.join(appDirectory, 'next.config.ts'),
205
+ path.join(appDirectory, 'next.config.js'),
206
+ path.join(appDirectory, 'next.config.mjs'),
207
+ )
208
+ const APP_MODULES_CHECKSUM_PATH = path.join(appDirectory, '.mercato', 'generated', 'modules.generated.checksum')
209
+ const PROJECT_SUPPORTS_PACKAGE_BUILDS = typeof projectScripts['build:packages'] === 'string'
167
210
  const EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.json')
168
211
  const EPHEMERAL_ENV_LOCK_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.lock')
169
212
  const LEGACY_EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.md')
@@ -172,26 +215,26 @@ const PLAYWRIGHT_INTEGRATION_CONFIG_PATH = '.ai/qa/tests/playwright.config.ts'
172
215
  const PLAYWRIGHT_RESULTS_JSON_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'test-results', 'results.json')
173
216
  const LEGACY_INTEGRATION_TEST_ROOT = path.join(projectRootDirectory, '.ai', 'qa', 'tests')
174
217
  const APP_BUILD_ARTIFACTS = [
175
- path.join(projectRootDirectory, 'apps', 'mercato', '.mercato', 'next', 'BUILD_ID'),
176
- path.join(projectRootDirectory, 'apps', 'mercato', '.mercato', 'generated', 'modules.generated.ts'),
177
- path.join(projectRootDirectory, 'packages', 'core', 'dist', 'index.js'),
178
- path.join(projectRootDirectory, 'packages', 'ui', 'dist', 'index.js'),
218
+ path.join(appDirectory, '.mercato', 'next', 'BUILD_ID'),
219
+ path.join(appDirectory, '.mercato', 'generated', 'modules.generated.ts'),
220
+ path.join(corePackageRootDirectory, 'dist', 'index.js'),
221
+ path.join(uiPackageRootDirectory, 'dist', 'index.js'),
179
222
  ]
180
- const APP_BUILD_INPUT_PATHS = [
181
- path.join(projectRootDirectory, 'apps', 'mercato', 'src'),
182
- path.join(projectRootDirectory, 'apps', 'mercato', 'package.json'),
183
- path.join(projectRootDirectory, 'apps', 'mercato', 'next.config.ts'),
184
- path.join(projectRootDirectory, 'apps', 'mercato', 'tsconfig.json'),
185
- path.join(projectRootDirectory, 'packages', 'core', 'src'),
186
- path.join(projectRootDirectory, 'packages', 'core', 'package.json'),
187
- path.join(projectRootDirectory, 'packages', 'core', 'tsconfig.json'),
188
- path.join(projectRootDirectory, 'packages', 'ui', 'src'),
189
- path.join(projectRootDirectory, 'packages', 'ui', 'package.json'),
190
- path.join(projectRootDirectory, 'packages', 'ui', 'tsconfig.json'),
223
+ const APP_BUILD_INPUT_PATHS = collectExistingPaths([
224
+ path.join(appDirectory, 'src'),
225
+ path.join(appDirectory, 'package.json'),
226
+ appNextConfigPath,
227
+ path.join(appDirectory, 'tsconfig.json'),
228
+ resolveFirstExistingPath(path.join(corePackageRootDirectory, 'src'), path.join(corePackageRootDirectory, 'dist')),
229
+ path.join(corePackageRootDirectory, 'package.json'),
230
+ path.join(corePackageRootDirectory, 'tsconfig.json'),
231
+ resolveFirstExistingPath(path.join(uiPackageRootDirectory, 'src'), path.join(uiPackageRootDirectory, 'dist')),
232
+ path.join(uiPackageRootDirectory, 'package.json'),
233
+ path.join(uiPackageRootDirectory, 'tsconfig.json'),
191
234
  path.join(projectRootDirectory, 'package.json'),
192
235
  path.join(projectRootDirectory, 'tsconfig.base.json'),
193
236
  path.join(projectRootDirectory, 'yarn.lock'),
194
- ]
237
+ ])
195
238
  const EXPECTED_TEST_FOLDERS = ['auth', 'catalog', 'crm', 'sales', 'admin', 'api', 'integration'] as const
196
239
  const FOLDER_TO_CATEGORY_CODE: Record<string, string> = {
197
240
  admin: 'ADMIN',
@@ -295,8 +338,9 @@ function runYarnCommand(
295
338
  args: string[],
296
339
  environment: NodeJS.ProcessEnv,
297
340
  opts: { silent?: boolean } = {},
341
+ cwd: string = projectRootDirectory,
298
342
  ): Promise<void> {
299
- return runYarnRawCommand(['run', ...args], environment, opts)
343
+ return runYarnRawCommand(['run', ...args], environment, opts, cwd)
300
344
  }
301
345
 
302
346
  async function runTimedStep<T>(
@@ -543,11 +587,12 @@ function runYarnRawCommand(
543
587
  commandArgs: string[],
544
588
  environment: NodeJS.ProcessEnv,
545
589
  opts: { silent?: boolean } = {},
590
+ cwd: string = projectRootDirectory,
546
591
  ): Promise<void> {
547
592
  return new Promise((resolve, reject) => {
548
593
  const outputMode: StdioOptions = opts.silent ? ['ignore', 'pipe', 'pipe'] : 'inherit'
549
594
  const command: ChildProcess = spawn(resolveYarnBinary(), commandArgs, {
550
- cwd: projectRootDirectory,
595
+ cwd,
551
596
  env: environment,
552
597
  stdio: outputMode,
553
598
  })
@@ -593,24 +638,15 @@ function runNpxCommand(args: string[], environment: NodeJS.ProcessEnv): Promise<
593
638
  })
594
639
  }
595
640
 
596
- function runYarnWorkspaceCommand(
597
- workspaceName: string,
598
- commandName: string,
599
- commandArgs: string[],
600
- environment: NodeJS.ProcessEnv,
601
- opts: { silent?: boolean } = {},
602
- ): Promise<void> {
603
- return runYarnRawCommand(['workspace', workspaceName, commandName, ...commandArgs], environment, opts)
604
- }
605
-
606
641
  function startYarnRawCommand(
607
642
  commandArgs: string[],
608
643
  environment: NodeJS.ProcessEnv,
609
644
  opts: { silent?: boolean } = {},
645
+ cwd: string = projectRootDirectory,
610
646
  ): ChildProcess {
611
647
  const outputMode: StdioOptions = opts.silent ? ['ignore', 'pipe', 'pipe'] : 'inherit'
612
648
  const processHandle: ChildProcess = spawn(resolveYarnBinary(), commandArgs, {
613
- cwd: projectRootDirectory,
649
+ cwd,
614
650
  env: environment,
615
651
  stdio: outputMode,
616
652
  })
@@ -621,14 +657,13 @@ function startYarnRawCommand(
621
657
  return processHandle
622
658
  }
623
659
 
624
- function startYarnWorkspaceCommand(
625
- workspaceName: string,
626
- commandName: string,
627
- commandArgs: string[],
660
+ function startYarnCommand(
661
+ args: string[],
628
662
  environment: NodeJS.ProcessEnv,
629
663
  opts: { silent?: boolean } = {},
664
+ cwd: string = projectRootDirectory,
630
665
  ): ChildProcess {
631
- return startYarnRawCommand(['workspace', workspaceName, commandName, ...commandArgs], environment, opts)
666
+ return startYarnRawCommand(['run', ...args], environment, opts, cwd)
632
667
  }
633
668
 
634
669
  async function assertContainerRuntimeAvailable(): Promise<void> {
@@ -2501,7 +2536,6 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2501
2536
  }
2502
2537
  }
2503
2538
 
2504
- const appWorkspace = '@open-mercato/app'
2505
2539
  const shouldUseIsolatedPort = shouldUseIsolatedPortForFreshEnvironment({
2506
2540
  reuseExisting: options.reuseExisting,
2507
2541
  existingStateBeforeReuseAttempt,
@@ -2607,9 +2641,9 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2607
2641
  console.log(`[${options.logPrefix}] Ephemeral database ready at ${databaseHost}:${databasePort}`)
2608
2642
  console.log(`[${options.logPrefix}] Initializing application data (includes migrations)...`)
2609
2643
  await runTimedStep(options.logPrefix, 'Initializing application data', { expectedSeconds: 45 }, async () =>
2610
- runYarnWorkspaceCommand(appWorkspace, 'initialize', [], commandEnvironment, {
2644
+ runYarnCommand(['initialize'], commandEnvironment, {
2611
2645
  silent: !options.verbose,
2612
- }))
2646
+ }, appDirectory))
2613
2647
 
2614
2648
  if (!needsBuild) {
2615
2649
  console.log(
@@ -2621,32 +2655,38 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2621
2655
  } else {
2622
2656
  console.log(`[${options.logPrefix}] Build artifacts missing, stale, or out of date; rebuilding artifacts.`)
2623
2657
  }
2624
- console.log(`[${options.logPrefix}] Building packages...`)
2625
- await runTimedStep(options.logPrefix, 'Building packages', { expectedSeconds: 20 }, async () =>
2626
- runYarnCommand(['build:packages'], commandEnvironment, {
2627
- silent: !options.verbose,
2628
- }))
2658
+ if (PROJECT_SUPPORTS_PACKAGE_BUILDS) {
2659
+ console.log(`[${options.logPrefix}] Building packages...`)
2660
+ await runTimedStep(options.logPrefix, 'Building packages', { expectedSeconds: 20 }, async () =>
2661
+ runYarnCommand(['build:packages'], commandEnvironment, {
2662
+ silent: !options.verbose,
2663
+ }))
2664
+ } else {
2665
+ console.log(`[${options.logPrefix}] Skipping package build step (no build:packages script at project root).`)
2666
+ }
2629
2667
 
2630
2668
  console.log(`[${options.logPrefix}] Regenerating module artifacts...`)
2631
- await rm(path.join(projectRootDirectory, 'apps', 'mercato', '.mercato', 'generated', 'modules.generated.checksum'), {
2669
+ await rm(APP_MODULES_CHECKSUM_PATH, {
2632
2670
  force: true,
2633
2671
  })
2634
2672
  await runTimedStep(options.logPrefix, 'Regenerating module artifacts', { expectedSeconds: 8 }, async () =>
2635
2673
  runYarnCommand(['generate'], commandEnvironment, {
2636
2674
  silent: !options.verbose,
2637
- }))
2638
-
2639
- console.log(`[${options.logPrefix}] Rebuilding packages after generation...`)
2640
- await runTimedStep(options.logPrefix, 'Rebuilding packages after generation', { expectedSeconds: 20 }, async () =>
2641
- runYarnCommand(['build:packages'], commandEnvironment, {
2642
- silent: !options.verbose,
2643
- }))
2675
+ }, appDirectory))
2676
+
2677
+ if (PROJECT_SUPPORTS_PACKAGE_BUILDS) {
2678
+ console.log(`[${options.logPrefix}] Rebuilding packages after generation...`)
2679
+ await runTimedStep(options.logPrefix, 'Rebuilding packages after generation', { expectedSeconds: 20 }, async () =>
2680
+ runYarnCommand(['build:packages'], commandEnvironment, {
2681
+ silent: !options.verbose,
2682
+ }))
2683
+ }
2644
2684
 
2645
2685
  console.log(`[${options.logPrefix}] Building application...`)
2646
2686
  await runTimedStep(options.logPrefix, 'Building application', { expectedSeconds: 76 }, async () =>
2647
- runYarnWorkspaceCommand(appWorkspace, 'build', [], commandEnvironment, {
2687
+ runYarnCommand(['build'], commandEnvironment, {
2648
2688
  silent: !options.verbose,
2649
- }))
2689
+ }, appDirectory))
2650
2690
  }
2651
2691
 
2652
2692
  if (shouldPersistBuildCache && sourceFingerprintValue) {
@@ -2654,9 +2694,9 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
2654
2694
  }
2655
2695
 
2656
2696
  console.log(`[${options.logPrefix}] Starting application on ${applicationBaseUrl}...`)
2657
- const startedAppProcess = startYarnWorkspaceCommand(appWorkspace, 'start', [], commandEnvironment, {
2697
+ const startedAppProcess = startYarnCommand(['start'], commandEnvironment, {
2658
2698
  silent: !options.verbose,
2659
- })
2699
+ }, appDirectory)
2660
2700
  applicationProcess = startedAppProcess
2661
2701
 
2662
2702
  await runTimedStep(
package/src/mercato.ts CHANGED
@@ -11,6 +11,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
11
11
  import { getSslConfig } from '@open-mercato/shared/lib/db/ssl'
12
12
  import { getRedisUrl } from '@open-mercato/shared/lib/redis/connection'
13
13
  import { resolveInitDerivedSecrets } from './lib/init-secrets'
14
+ import { parseModuleInstallArgs } from './lib/module-install-args'
14
15
  // Lazy-imported to avoid pulling in `testcontainers` (devDependency) at startup
15
16
  const lazyIntegration = () => import('./lib/testing/integration')
16
17
  import type { ChildProcess } from 'node:child_process'
@@ -53,6 +54,51 @@ async function ensureEnvLoaded() {
53
54
  } catch {}
54
55
  }
55
56
 
57
+ function resolveInstalledBinary(baseDirs: string[], relativeBinPath: string): string {
58
+ const checked = new Set<string>()
59
+ for (const baseDir of baseDirs) {
60
+ const candidate = path.join(baseDir, 'node_modules', relativeBinPath)
61
+ checked.add(candidate)
62
+ if (fs.existsSync(candidate)) return candidate
63
+ }
64
+ throw new Error(
65
+ `Could not find installed binary "${relativeBinPath}". Checked: ${Array.from(checked).join(', ')}`,
66
+ )
67
+ }
68
+
69
+ async function handleDirectEjectCommand(args: string[]): Promise<number> {
70
+ const { createResolver } = await import('./lib/resolver')
71
+ const { listEjectableModules, ejectModule } = await import('./lib/eject')
72
+ const resolver = createResolver()
73
+ const commandArgs = args.filter(Boolean)
74
+ const isList = commandArgs.includes('--list') || commandArgs.includes('-l')
75
+ const moduleId = isList ? undefined : commandArgs.find((arg) => !arg.startsWith('-'))
76
+
77
+ if (isList || !moduleId) {
78
+ const ejectable = listEjectableModules(resolver)
79
+ if (ejectable.length === 0) {
80
+ console.log('No ejectable modules found.')
81
+ } else {
82
+ console.log('Ejectable modules:\n')
83
+ for (const mod of ejectable) {
84
+ const desc = mod.description ? ` — ${mod.description}` : ''
85
+ console.log(` ${mod.id} (from: ${mod.from})${desc}`)
86
+ }
87
+ console.log('\nUsage: yarn mercato eject <moduleId>')
88
+ }
89
+ return 0
90
+ }
91
+
92
+ console.log(`Ejecting module "${moduleId}"...`)
93
+ ejectModule(resolver, moduleId)
94
+ console.log(`\n✅ Module "${moduleId}" ejected successfully!\n`)
95
+ console.log('Next steps:')
96
+ console.log(' 1. Run generators: yarn mercato generate all')
97
+ console.log(` 2. Customize: edit src/modules/${moduleId}/`)
98
+ console.log(' 3. Start dev: yarn dev')
99
+ return 0
100
+ }
101
+
56
102
  // Helper to run a CLI command directly (without spawning a process)
57
103
  async function runModuleCommand(
58
104
  allModules: Module[],
@@ -274,13 +320,14 @@ export async function run(argv = process.argv) {
274
320
  // Step 1: Run generators directly (no process spawn)
275
321
  console.log('🔧 Preparing modules (registry, entities, DI)...')
276
322
  const { createResolver } = await import('./lib/resolver')
277
- const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi, generateOpenApi } = await import('./lib/generators')
323
+ const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi } = await import('./lib/generators')
278
324
  const resolver = createResolver()
279
325
  await generateEntityIds({ resolver, quiet: true })
280
326
  await generateModuleRegistry({ resolver, quiet: true })
281
327
  await generateModuleRegistryCli({ resolver, quiet: true })
282
328
  await generateModuleEntities({ resolver, quiet: true })
283
329
  await generateModuleDi({ resolver, quiet: true })
330
+ await generateModulePackageSources({ resolver, quiet: true })
284
331
  await generateOpenApi({ resolver, quiet: true })
285
332
  console.log('✅ Modules prepared\n')
286
333
 
@@ -524,39 +571,72 @@ export async function run(argv = process.argv) {
524
571
  return exitCode
525
572
  }
526
573
 
527
- // Handle eject command directly (bootstrap-free)
528
- if (first === 'eject') {
574
+ if (first === 'module') {
529
575
  try {
530
- const { createResolver } = await import('./lib/resolver')
531
- const { listEjectableModules, ejectModule } = await import('./lib/eject')
532
- const resolver = createResolver()
576
+ const subcommand = second
577
+ const commandArgs = remaining.filter(Boolean)
578
+
579
+ if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
580
+ console.log('Usage: yarn mercato module <add|enable|eject> ...')
581
+ console.log(' yarn mercato module add <packageSpec> [--module <moduleId>] [--eject]')
582
+ console.log(' yarn mercato module enable <packageName> [--module <moduleId>] [--eject]')
583
+ console.log(' yarn mercato module eject <moduleId>')
584
+ return 0
585
+ }
533
586
 
534
- const isList = second === '--list' || second === '-l'
535
- const moduleId = !isList ? second : undefined
587
+ if (subcommand === 'add') {
588
+ const { createResolver } = await import('./lib/resolver')
589
+ const { addOfficialModule } = await import('./lib/module-install')
590
+ const { packageSpec, eject, moduleId } = parseModuleInstallArgs(commandArgs)
536
591
 
537
- if (isList || !moduleId) {
538
- const ejectable = listEjectableModules(resolver)
539
- if (ejectable.length === 0) {
540
- console.log('No ejectable modules found.')
541
- } else {
542
- console.log('Ejectable modules:\n')
543
- for (const mod of ejectable) {
544
- const desc = mod.description ? ` — ${mod.description}` : ''
545
- console.log(` ${mod.id} (from: ${mod.from})${desc}`)
546
- }
547
- console.log('\nUsage: yarn mercato eject <moduleId>')
592
+ if (!packageSpec) {
593
+ console.error('Usage: yarn mercato module add <packageSpec> [--module <moduleId>] [--eject]')
594
+ return 1
548
595
  }
596
+
597
+ const result = await addOfficialModule(createResolver(), packageSpec, eject, moduleId ?? undefined)
598
+ console.log(`\n✅ Module "${result.moduleId}" enabled from ${result.from}.\n`)
599
+ console.log('Next steps:')
600
+ console.log(' 1. Review generated files if needed: .mercato/generated/')
601
+ console.log(' 2. Start dev: yarn dev')
549
602
  return 0
550
603
  }
551
604
 
552
- console.log(`Ejecting module "${moduleId}"...`)
553
- ejectModule(resolver, moduleId)
554
- console.log(`\n✅ Module "${moduleId}" ejected successfully!\n`)
555
- console.log('Next steps:')
556
- console.log(' 1. Run generators: yarn mercato generate all')
557
- console.log(` 2. Customize: edit src/modules/${moduleId}/`)
558
- console.log(' 3. Start dev: yarn dev')
559
- return 0
605
+ if (subcommand === 'enable') {
606
+ const packageName = commandArgs.find((arg) => !arg.startsWith('-'))
607
+ if (!packageName) {
608
+ console.error('Usage: yarn mercato module enable <packageName> [--module <moduleId>] [--eject]')
609
+ return 1
610
+ }
611
+
612
+ const { createResolver } = await import('./lib/resolver')
613
+ const { enableOfficialModule } = await import('./lib/module-install')
614
+ const { moduleId, eject } = parseModuleInstallArgs(commandArgs)
615
+ const result = await enableOfficialModule(createResolver(), packageName, moduleId ?? undefined, eject)
616
+ console.log(`\n✅ Module "${result.moduleId}" enabled from ${result.from}.\n`)
617
+ console.log('Next steps:')
618
+ console.log(' 1. Review generated files if needed: .mercato/generated/')
619
+ console.log(' 2. Start dev: yarn dev')
620
+ return 0
621
+ }
622
+
623
+ if (subcommand === 'eject') {
624
+ return handleDirectEjectCommand(commandArgs)
625
+ }
626
+
627
+ console.error(`Unknown module subcommand "${subcommand}".`)
628
+ return 1
629
+ } catch (error: unknown) {
630
+ const message = error instanceof Error ? error.message : String(error)
631
+ console.error(`❌ Module command failed: ${message}`)
632
+ return 1
633
+ }
634
+ }
635
+
636
+ // Handle eject command directly (bootstrap-free)
637
+ if (first === 'eject') {
638
+ try {
639
+ return handleDirectEjectCommand(parts.slice(1))
560
640
  } catch (error: unknown) {
561
641
  const message = error instanceof Error ? error.message : String(error)
562
642
  console.error(`❌ Eject failed: ${message}`)
@@ -880,7 +960,7 @@ export async function run(argv = process.argv) {
880
960
  command: 'all',
881
961
  run: async (args: string[]) => {
882
962
  const { createResolver } = await import('./lib/resolver')
883
- const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi, generateOpenApi } = await import('./lib/generators')
963
+ const { generateEntityIds, generateModuleRegistry, generateModuleRegistryCli, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi } = await import('./lib/generators')
884
964
  const resolver = createResolver()
885
965
  const quiet = args.includes('--quiet') || args.includes('-q')
886
966
 
@@ -890,6 +970,7 @@ export async function run(argv = process.argv) {
890
970
  await generateModuleRegistryCli({ resolver, quiet })
891
971
  await generateModuleEntities({ resolver, quiet })
892
972
  await generateModuleDi({ resolver, quiet })
973
+ await generateModulePackageSources({ resolver, quiet })
893
974
  await generateOpenApi({ resolver, quiet })
894
975
  console.log('All generators completed.')
895
976
  },
@@ -907,9 +988,10 @@ export async function run(argv = process.argv) {
907
988
  command: 'registry',
908
989
  run: async (args: string[]) => {
909
990
  const { createResolver } = await import('./lib/resolver')
910
- const { generateModuleRegistry } = await import('./lib/generators')
991
+ const { generateModulePackageSources, generateModuleRegistry } = await import('./lib/generators')
911
992
  const resolver = createResolver()
912
993
  await generateModuleRegistry({ resolver, quiet: args.includes('--quiet') })
994
+ await generateModulePackageSources({ resolver, quiet: args.includes('--quiet') })
913
995
  },
914
996
  },
915
997
  {
@@ -976,13 +1058,10 @@ export async function run(argv = process.argv) {
976
1058
  command: 'dev',
977
1059
  run: async () => {
978
1060
  const { spawn } = await import('child_process')
979
- const path = await import('path')
980
- const { createResolver } = await import('./lib/resolver')
981
- const resolver = createResolver()
982
- const appDir = resolver.getAppDir()
983
-
984
- // In monorepo, packages are hoisted to root; in standalone, they're in app's node_modules
985
- const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir
1061
+ const { resolveEnvironment } = await import('./lib/resolver')
1062
+ const env = resolveEnvironment()
1063
+ const appDir = env.appDir
1064
+ const nodeModulesBases = Array.from(new Set([env.rootDir, appDir]))
986
1065
 
987
1066
  const processes: ChildProcess[] = []
988
1067
  const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
@@ -1003,9 +1082,13 @@ export async function run(argv = process.argv) {
1003
1082
 
1004
1083
  console.log('[server] Starting Open Mercato in dev mode...')
1005
1084
 
1006
- // Resolve paths relative to where node_modules are located
1007
- const nextBin = path.join(nodeModulesBase, 'node_modules/next/dist/bin/next')
1008
- const mercatoBin = path.join(nodeModulesBase, 'node_modules/@open-mercato/cli/bin/mercato')
1085
+ // Ensure module-package-sources.css exists before Next.js starts
1086
+ const { createResolver: createResolverForSources } = await import('./lib/resolver')
1087
+ const { generateModulePackageSources } = await import('./lib/generators')
1088
+ await generateModulePackageSources({ resolver: createResolverForSources(), quiet: true })
1089
+
1090
+ const nextBin = resolveInstalledBinary(nodeModulesBases, 'next/dist/bin/next')
1091
+ const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1009
1092
 
1010
1093
  // Start Next.js dev
1011
1094
  const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
@@ -1053,13 +1136,10 @@ export async function run(argv = process.argv) {
1053
1136
  command: 'start',
1054
1137
  run: async () => {
1055
1138
  const { spawn } = await import('child_process')
1056
- const path = await import('path')
1057
- const { createResolver } = await import('./lib/resolver')
1058
- const resolver = createResolver()
1059
- const appDir = resolver.getAppDir()
1060
-
1061
- // In monorepo, packages are hoisted to root; in standalone, they're in app's node_modules
1062
- const nodeModulesBase = resolver.isMonorepo() ? resolver.getRootDir() : appDir
1139
+ const { resolveEnvironment } = await import('./lib/resolver')
1140
+ const env = resolveEnvironment()
1141
+ const appDir = env.appDir
1142
+ const nodeModulesBases = Array.from(new Set([env.rootDir, appDir]))
1063
1143
 
1064
1144
  const processes: ChildProcess[] = []
1065
1145
  const autoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS !== 'false'
@@ -1080,9 +1160,8 @@ export async function run(argv = process.argv) {
1080
1160
 
1081
1161
  console.log('[server] Starting Open Mercato in production mode...')
1082
1162
 
1083
- // Resolve paths relative to where node_modules are located
1084
- const nextBin = path.join(nodeModulesBase, 'node_modules/next/dist/bin/next')
1085
- const mercatoBin = path.join(nodeModulesBase, 'node_modules/@open-mercato/cli/bin/mercato')
1163
+ const nextBin = resolveInstalledBinary(nodeModulesBases, 'next/dist/bin/next')
1164
+ const mercatoBin = resolveInstalledBinary(nodeModulesBases, '@open-mercato/cli/bin/mercato')
1086
1165
 
1087
1166
  // Start Next.js production server
1088
1167
  const nextProcess = spawn('node', [nextBin, 'start'], {