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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/shared/AGENTS.md.template +1 -1
  3. package/dist/lib/__integration__/TC-INT-007.spec.js +201 -0
  4. package/dist/lib/__integration__/TC-INT-007.spec.js.map +7 -0
  5. package/dist/lib/dev-env-reload.js +89 -0
  6. package/dist/lib/dev-env-reload.js.map +7 -0
  7. package/dist/lib/generators/extensions/ai-agents.js +218 -0
  8. package/dist/lib/generators/extensions/ai-agents.js.map +7 -0
  9. package/dist/lib/generators/extensions/ai-tools.js +56 -1
  10. package/dist/lib/generators/extensions/ai-tools.js.map +2 -2
  11. package/dist/lib/generators/extensions/index.js +2 -0
  12. package/dist/lib/generators/extensions/index.js.map +2 -2
  13. package/dist/lib/testing/integration-discovery.js +102 -5
  14. package/dist/lib/testing/integration-discovery.js.map +2 -2
  15. package/dist/mercato.js +153 -79
  16. package/dist/mercato.js.map +2 -2
  17. package/package.json +5 -5
  18. package/src/__tests__/mercato.test.ts +301 -25
  19. package/src/lib/__integration__/TC-INT-007.spec.ts +228 -0
  20. package/src/lib/__tests__/dev-env-reload.test.ts +62 -0
  21. package/src/lib/dev-env-reload.ts +110 -0
  22. package/src/lib/generators/__tests__/module-subset.test.ts +14 -0
  23. package/src/lib/generators/__tests__/output-snapshots.test.ts +17 -0
  24. package/src/lib/generators/__tests__/scanner.test.ts +1 -1
  25. package/src/lib/generators/__tests__/structural-contracts.test.ts +26 -0
  26. package/src/lib/generators/extensions/ai-agents.ts +240 -0
  27. package/src/lib/generators/extensions/ai-tools.ts +72 -1
  28. package/src/lib/generators/extensions/index.ts +2 -0
  29. package/src/lib/testing/__tests__/integration-discovery.test.ts +68 -0
  30. package/src/lib/testing/integration-discovery.ts +127 -3
  31. package/src/mercato.ts +190 -83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.5.1-develop.3032.01699048cb",
3
+ "version": "0.5.1-develop.3043.1a796c3920",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "@mikro-orm/decorators": "^7.0.13",
60
60
  "@mikro-orm/migrations": "^7.0.13",
61
61
  "@mikro-orm/postgresql": "^7.0.13",
62
- "@open-mercato/queue": "0.5.1-develop.3032.01699048cb",
63
- "@open-mercato/shared": "0.5.1-develop.3032.01699048cb",
62
+ "@open-mercato/queue": "0.5.1-develop.3043.1a796c3920",
63
+ "@open-mercato/shared": "0.5.1-develop.3043.1a796c3920",
64
64
  "cross-spawn": "^7.0.6",
65
65
  "pg": "8.20.0",
66
66
  "semver": "^7.7.4",
@@ -70,10 +70,10 @@
70
70
  "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.5.1-develop.3032.01699048cb"
73
+ "@open-mercato/shared": "0.5.1-develop.3043.1a796c3920"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.5.1-develop.3032.01699048cb",
76
+ "@open-mercato/shared": "0.5.1-develop.3043.1a796c3920",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.3.0",
79
79
  "ts-jest": "^29.4.9"
@@ -1,3 +1,4 @@
1
+ import type { Module } from '@open-mercato/shared/modules/registry'
1
2
  import {
2
3
  registerCliModules,
3
4
  getCliModules,
@@ -6,6 +7,61 @@ import {
6
7
  run,
7
8
  } from '../mercato'
8
9
 
10
+ type MockChildAutoExit = { code: number | null; signal?: NodeJS.Signals | null } | undefined
11
+ type MockChildSpawnRouter = (args: string[]) => MockChildAutoExit
12
+
13
+ function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
14
+ const { EventEmitter } = jest.requireActual('node:events')
15
+
16
+ const createChild = (spawnargs: string[], autoExit?: MockChildAutoExit) => {
17
+ const child = new EventEmitter() as any
18
+ child.stdout = new EventEmitter()
19
+ child.stderr = new EventEmitter()
20
+ child.spawnargs = spawnargs
21
+ child.killed = false
22
+ child.exitCode = null
23
+ child.signalCode = null
24
+ child.kill = jest.fn((signal: NodeJS.Signals = 'SIGTERM') => {
25
+ child.killed = true
26
+ if (child.exitCode !== null || child.signalCode !== null) {
27
+ return true
28
+ }
29
+ child.signalCode = signal
30
+ queueMicrotask(() => {
31
+ child.emit('exit', null, signal)
32
+ })
33
+ return true
34
+ })
35
+
36
+ if (autoExit) {
37
+ queueMicrotask(() => {
38
+ if (child.exitCode !== null || child.signalCode !== null) return
39
+ child.exitCode = autoExit.code
40
+ child.signalCode = autoExit.signal ?? null
41
+ child.emit('exit', child.exitCode, child.signalCode)
42
+ })
43
+ }
44
+
45
+ return child
46
+ }
47
+
48
+ return {
49
+ spawn: jest.fn((_command: string, args: string[]) => createChild(['node', ...args], routeAutoExit(args))),
50
+ }
51
+ }
52
+
53
+ const eventsWorkerFixture: Pick<Module, 'id' | 'workers'> = {
54
+ id: 'events',
55
+ workers: [
56
+ {
57
+ id: 'events.test-worker',
58
+ queue: 'events',
59
+ concurrency: 1,
60
+ handler: jest.fn(),
61
+ },
62
+ ],
63
+ }
64
+
9
65
  describe('mercato CLI module registration', () => {
10
66
  beforeEach(() => {
11
67
  // Reset module state by re-importing
@@ -562,16 +618,19 @@ describe('generate post-step structural cache purge', () => {
562
618
 
563
619
  describe('server dev managed process exits', () => {
564
620
  const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
621
+ const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
565
622
 
566
623
  beforeEach(() => {
567
624
  jest.restoreAllMocks()
568
625
  jest.resetModules()
569
626
  process.env.AUTO_SPAWN_SCHEDULER = 'false'
627
+ process.env.AUTO_SPAWN_WORKERS = 'true'
570
628
  })
571
629
 
572
630
  afterEach(() => {
573
631
  jest.dontMock('child_process')
574
632
  jest.dontMock('node:fs')
633
+ jest.dontMock('../lib/dev-env-reload')
575
634
  jest.dontMock('../lib/generators')
576
635
  jest.dontMock('../lib/resolver')
577
636
  jest.resetModules()
@@ -579,12 +638,220 @@ describe('server dev managed process exits', () => {
579
638
 
580
639
  afterAll(() => {
581
640
  process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
641
+ process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
642
+ })
643
+
644
+ it('skips scheduler auto-start when the module is not enabled', async () => {
645
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
646
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
647
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
648
+ process.env.AUTO_SPAWN_WORKERS = 'false'
649
+
650
+ jest.doMock('node:fs', () => {
651
+ const actual = jest.requireActual('node:fs')
652
+ return {
653
+ ...actual,
654
+ existsSync: jest.fn((candidate: string) =>
655
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
656
+ ),
657
+ unlinkSync: jest.fn(),
658
+ }
659
+ })
660
+ jest.doMock('../lib/generators', () => ({
661
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
662
+ }))
663
+ jest.doMock('../lib/resolver', () => ({
664
+ resolveEnvironment: () => ({
665
+ appDir: '/tmp/test-app',
666
+ rootDir: '/tmp/test-root',
667
+ }),
668
+ createResolver: () => ({}),
669
+ }))
670
+ jest.doMock('child_process', () =>
671
+ buildMockChildProcessModule((args) =>
672
+ args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
673
+ ),
674
+ )
675
+
676
+ const mercato = await import('../mercato')
677
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
678
+
679
+ expect(exitCode).toBe(0)
680
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
681
+ expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
682
+
683
+ consoleErrorSpy.mockRestore()
684
+ consoleLogSpy.mockRestore()
685
+ })
686
+
687
+ it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
688
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
689
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
690
+
691
+ jest.doMock('node:fs', () => {
692
+ const actual = jest.requireActual('node:fs')
693
+ return {
694
+ ...actual,
695
+ existsSync: jest.fn((candidate: string) =>
696
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
697
+ ),
698
+ unlinkSync: jest.fn(),
699
+ }
700
+ })
701
+ jest.doMock('../lib/generators', () => ({
702
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
703
+ }))
704
+ jest.doMock('../lib/resolver', () => ({
705
+ resolveEnvironment: () => ({
706
+ appDir: '/tmp/test-app',
707
+ rootDir: '/tmp/test-root',
708
+ }),
709
+ createResolver: () => ({}),
710
+ }))
711
+ jest.doMock('child_process', () =>
712
+ buildMockChildProcessModule((args) => {
713
+ if (args.slice(1).join(' ') === 'queue worker --all') {
714
+ return { code: 0 }
715
+ }
716
+ return undefined
717
+ }),
718
+ )
719
+
720
+ const mercato = await import('../mercato')
721
+ mercato.registerCliModules([eventsWorkerFixture as Module])
722
+
723
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
724
+
725
+ expect(exitCode).toBe(1)
726
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
727
+ '💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
728
+ )
729
+
730
+ consoleErrorSpy.mockRestore()
731
+ consoleLogSpy.mockRestore()
732
+ })
733
+ })
734
+
735
+ describe('server start managed process exits', () => {
736
+ const originalAutoSpawnScheduler = process.env.AUTO_SPAWN_SCHEDULER
737
+ const originalAutoSpawnWorkers = process.env.AUTO_SPAWN_WORKERS
738
+
739
+ beforeEach(() => {
740
+ jest.restoreAllMocks()
741
+ jest.resetModules()
742
+ process.env.AUTO_SPAWN_SCHEDULER = 'false'
743
+ process.env.AUTO_SPAWN_WORKERS = 'true'
744
+ })
745
+
746
+ afterEach(() => {
747
+ jest.dontMock('child_process')
748
+ jest.dontMock('node:fs')
749
+ jest.dontMock('../lib/resolver')
750
+ jest.dontMock('../lib/server-start-lock')
751
+ jest.resetModules()
752
+ })
753
+
754
+ afterAll(() => {
755
+ process.env.AUTO_SPAWN_SCHEDULER = originalAutoSpawnScheduler
756
+ process.env.AUTO_SPAWN_WORKERS = originalAutoSpawnWorkers
757
+ })
758
+
759
+ it('skips scheduler auto-start when the module is not enabled', async () => {
760
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
761
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
762
+ process.env.AUTO_SPAWN_SCHEDULER = 'true'
763
+ process.env.AUTO_SPAWN_WORKERS = 'false'
764
+
765
+ jest.doMock('node:fs', () => {
766
+ const actual = jest.requireActual('node:fs')
767
+ return {
768
+ ...actual,
769
+ existsSync: jest.fn((candidate: string) =>
770
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
771
+ ),
772
+ }
773
+ })
774
+ jest.doMock('../lib/resolver', () => ({
775
+ resolveEnvironment: () => ({
776
+ appDir: '/tmp/test-app',
777
+ rootDir: '/tmp/test-root',
778
+ }),
779
+ }))
780
+ jest.doMock('../lib/server-start-lock', () => ({
781
+ acquireServerStartLock: jest.fn(() => ({
782
+ release: jest.fn(),
783
+ })),
784
+ }))
785
+ jest.doMock('child_process', () =>
786
+ buildMockChildProcessModule((args) =>
787
+ args[0]?.includes('next/dist/bin/next') ? { code: null, signal: 'SIGTERM' } : undefined,
788
+ ),
789
+ )
790
+
791
+ const mercato = await import('../mercato')
792
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
793
+
794
+ expect(exitCode).toBe(0)
795
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
796
+ expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
797
+
798
+ consoleErrorSpy.mockRestore()
799
+ consoleLogSpy.mockRestore()
582
800
  })
583
801
 
584
802
  it('fails loudly when a managed child exits cleanly but unexpectedly', async () => {
585
803
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
586
804
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
587
805
 
806
+ jest.doMock('node:fs', () => {
807
+ const actual = jest.requireActual('node:fs')
808
+ return {
809
+ ...actual,
810
+ existsSync: jest.fn((candidate: string) =>
811
+ candidate.includes('next/dist/bin/next') || candidate.includes('@open-mercato/cli/bin/mercato'),
812
+ ),
813
+ }
814
+ })
815
+ jest.doMock('../lib/resolver', () => ({
816
+ resolveEnvironment: () => ({
817
+ appDir: '/tmp/test-app',
818
+ rootDir: '/tmp/test-root',
819
+ }),
820
+ }))
821
+ jest.doMock('../lib/server-start-lock', () => ({
822
+ acquireServerStartLock: jest.fn(() => ({
823
+ release: jest.fn(),
824
+ })),
825
+ }))
826
+ jest.doMock('child_process', () =>
827
+ buildMockChildProcessModule((args) => {
828
+ if (args.slice(1).join(' ') === 'queue worker --all') {
829
+ return { code: 0 }
830
+ }
831
+ return undefined
832
+ }),
833
+ )
834
+
835
+ const mercato = await import('../mercato')
836
+ mercato.registerCliModules([eventsWorkerFixture as Module])
837
+
838
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'start'])
839
+
840
+ expect(exitCode).toBe(1)
841
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
842
+ '💥 Failed: [server] Queue worker (events) exited unexpectedly with exit code 0.',
843
+ )
844
+
845
+ consoleErrorSpy.mockRestore()
846
+ consoleLogSpy.mockRestore()
847
+ })
848
+
849
+ it('restarts the managed dev runtime when an app env file changes', async () => {
850
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
851
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
852
+ let envChangeCallback: ((filePath: string) => void) | null = null
853
+ let reloadCount = 0
854
+
588
855
  jest.doMock('node:fs', () => {
589
856
  const actual = jest.requireActual('node:fs')
590
857
  return {
@@ -595,6 +862,19 @@ describe('server dev managed process exits', () => {
595
862
  unlinkSync: jest.fn(),
596
863
  }
597
864
  })
865
+ jest.doMock('../lib/dev-env-reload', () => ({
866
+ createDevEnvReloader: () => ({
867
+ reload: jest.fn(() => {
868
+ reloadCount += 1
869
+ process.env.RESTART_TOKEN = reloadCount === 1 ? 'initial' : 'changed'
870
+ }),
871
+ getWatchedFiles: () => ['/tmp/test-app/.env'],
872
+ }),
873
+ watchDevEnvFiles: jest.fn((_appDir: string, onChange: (filePath: string) => void) => {
874
+ envChangeCallback = onChange
875
+ return jest.fn()
876
+ }),
877
+ }))
598
878
  jest.doMock('../lib/generators', () => ({
599
879
  generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
600
880
  }))
@@ -607,15 +887,14 @@ describe('server dev managed process exits', () => {
607
887
  }))
608
888
  jest.doMock('child_process', () => {
609
889
  const { EventEmitter } = jest.requireActual('node:events')
890
+ let nextSpawnCount = 0
610
891
 
611
892
  const createChild = (
612
- spawnargs: string[],
613
893
  autoExit?: { code: number | null; signal?: NodeJS.Signals | null },
614
894
  ) => {
615
895
  const child = new EventEmitter() as any
616
896
  child.stdout = new EventEmitter()
617
897
  child.stderr = new EventEmitter()
618
- child.spawnargs = spawnargs
619
898
  child.killed = false
620
899
  child.exitCode = null
621
900
  child.signalCode = null
@@ -646,37 +925,34 @@ describe('server dev managed process exits', () => {
646
925
  return {
647
926
  spawn: jest.fn((_command: string, args: string[]) => {
648
927
  if (args[0]?.includes('next/dist/bin/next')) {
649
- return createChild(['node', ...args])
650
- }
651
- if (args.slice(1).join(' ') === 'queue worker --all') {
652
- return createChild(['node', ...args], { code: 0 })
928
+ nextSpawnCount += 1
929
+ if (nextSpawnCount === 1) {
930
+ queueMicrotask(() => envChangeCallback?.('/tmp/test-app/.env'))
931
+ return createChild()
932
+ }
933
+ return createChild({ code: null, signal: 'SIGTERM' })
653
934
  }
654
- return createChild(['node', ...args])
935
+ return createChild()
655
936
  }),
656
937
  }
657
938
  })
658
939
 
659
- const mercato = await import('../mercato')
660
- mercato.registerCliModules([
661
- {
662
- id: 'events',
663
- workers: [
664
- {
665
- queue: 'events',
666
- concurrency: 1,
667
- handler: jest.fn(),
668
- },
669
- ],
670
- } as any,
671
- ])
672
-
673
- const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
940
+ const mercado = await import('../mercato')
941
+ const exitCode = await mercado.run(['node', 'mercato', 'server', 'dev'])
942
+ const { spawn } = await import('child_process')
943
+ const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
944
+ call[1]?.[0]?.includes('next/dist/bin/next'),
945
+ )
674
946
 
675
- expect(exitCode).toBe(1)
676
- expect(consoleErrorSpy).toHaveBeenCalledWith(
677
- '💥 Failed: [server] Queue worker exited unexpectedly with exit code 0.',
947
+ expect(exitCode).toBe(0)
948
+ expect(nextSpawns).toHaveLength(2)
949
+ expect(nextSpawns[0][2].env.RESTART_TOKEN).toBe('initial')
950
+ expect(nextSpawns[1][2].env.RESTART_TOKEN).toBe('changed')
951
+ expect(consoleLogSpy).toHaveBeenCalledWith(
952
+ '[server] Detected environment file change (.env). Restarting app runtime...',
678
953
  )
679
954
 
955
+ delete process.env.RESTART_TOKEN
680
956
  consoleErrorSpy.mockRestore()
681
957
  consoleLogSpy.mockRestore()
682
958
  })
@@ -0,0 +1,228 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { execFileSync } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const repoRoot = path.resolve(__dirname, '..', '..', '..', '..', '..')
10
+ const cliBin = path.join(repoRoot, 'packages', 'cli', 'dist', 'bin.js')
11
+ const fixtureModulePackage = path.join(
12
+ repoRoot,
13
+ 'packages',
14
+ 'cli',
15
+ 'src',
16
+ 'lib',
17
+ '__fixtures__',
18
+ 'official-module-package',
19
+ )
20
+
21
+ function yarnBinary(): string {
22
+ return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
23
+ }
24
+
25
+ function runCommand(command: string, args: string[], cwd: string): string {
26
+ const yarnCacheFolder = path.join(cwd, '.yarn', 'cache')
27
+ return execFileSync(command, args, {
28
+ cwd,
29
+ encoding: 'utf8',
30
+ env: {
31
+ ...process.env,
32
+ FORCE_COLOR: '0',
33
+ NODE_NO_WARNINGS: '1',
34
+ YARN_CACHE_FOLDER: yarnCacheFolder,
35
+ YARN_ENABLE_GLOBAL_CACHE: '0',
36
+ YARN_ENABLE_IMMUTABLE_INSTALLS: '0',
37
+ YARN_NODE_LINKER: 'node-modules',
38
+ },
39
+ })
40
+ }
41
+
42
+ function runMercato(args: string[], cwd: string): string {
43
+ return runCommand(process.execPath, [cliBin, ...args], cwd)
44
+ }
45
+
46
+ function writeFile(filePath: string, content: string): void {
47
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
48
+ fs.writeFileSync(filePath, content)
49
+ }
50
+
51
+ function createCoreWorkspacePackage(rootDir: string, relativeDir: string): string {
52
+ const coreDir = path.join(rootDir, relativeDir)
53
+ writeFile(
54
+ path.join(coreDir, 'package.json'),
55
+ JSON.stringify(
56
+ {
57
+ name: '@open-mercato/core',
58
+ version: '0.4.7',
59
+ type: 'module',
60
+ },
61
+ null,
62
+ 2,
63
+ ),
64
+ )
65
+ return coreDir
66
+ }
67
+
68
+ function createMonorepoFixture(rootDir: string): string {
69
+ writeFile(
70
+ path.join(rootDir, 'package.json'),
71
+ JSON.stringify(
72
+ {
73
+ name: 'cli-module-monorepo-fixture',
74
+ private: true,
75
+ workspaces: ['apps/*', 'packages/*'],
76
+ },
77
+ null,
78
+ 2,
79
+ ),
80
+ )
81
+ writeFile(
82
+ path.join(rootDir, '.yarnrc.yml'),
83
+ ['nodeLinker: node-modules', 'enableGlobalCache: false', 'cacheFolder: ./.yarn/cache', ''].join('\n'),
84
+ )
85
+ createCoreWorkspacePackage(rootDir, path.join('packages', 'core'))
86
+
87
+ const appDir = path.join(rootDir, 'apps', 'mercato')
88
+ writeFile(
89
+ path.join(appDir, 'package.json'),
90
+ JSON.stringify(
91
+ {
92
+ name: '@open-mercato/app',
93
+ version: '0.0.0',
94
+ private: true,
95
+ },
96
+ null,
97
+ 2,
98
+ ),
99
+ )
100
+ writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
101
+ return appDir
102
+ }
103
+
104
+ function createStandaloneFixture(rootDir: string): string {
105
+ writeFile(
106
+ path.join(rootDir, '.yarnrc.yml'),
107
+ ['nodeLinker: node-modules', 'enableGlobalCache: false', 'cacheFolder: ./.yarn/cache', ''].join('\n'),
108
+ )
109
+ createCoreWorkspacePackage(rootDir, path.join('vendor', 'core'))
110
+ writeFile(
111
+ path.join(rootDir, 'package.json'),
112
+ JSON.stringify(
113
+ {
114
+ name: 'cli-module-standalone-fixture',
115
+ private: true,
116
+ dependencies: {
117
+ '@open-mercato/core': 'file:./vendor/core',
118
+ },
119
+ },
120
+ null,
121
+ 2,
122
+ ),
123
+ )
124
+ writeFile(path.join(rootDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
125
+ return rootDir
126
+ }
127
+
128
+ function readFile(filePath: string): string {
129
+ return fs.readFileSync(filePath, 'utf8')
130
+ }
131
+
132
+ test.describe('TC-INT-007: CLI official module install and eject flows', () => {
133
+ test('module add installs and registers a package-backed module in a monorepo app', () => {
134
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-monorepo-'))
135
+
136
+ try {
137
+ const appDir = createMonorepoFixture(rootDir)
138
+ runCommand(yarnBinary(), ['install'], rootDir)
139
+
140
+ runMercato(
141
+ [
142
+ 'module',
143
+ 'add',
144
+ `@open-mercato/test-package@file:${fixtureModulePackage}`,
145
+ ],
146
+ rootDir,
147
+ )
148
+
149
+ const modulesSource = readFile(path.join(appDir, 'src', 'modules.ts'))
150
+ const cssSource = readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))
151
+ const appPackageJson = JSON.parse(readFile(path.join(appDir, 'package.json'))) as {
152
+ dependencies?: Record<string, string>
153
+ }
154
+
155
+ expect(modulesSource).toContain("{ id: 'test_package', from: '@open-mercato/test-package' }")
156
+ expect(cssSource).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
157
+ expect(appPackageJson.dependencies?.['@open-mercato/test-package']).toBeTruthy()
158
+ } finally {
159
+ fs.rmSync(rootDir, { recursive: true, force: true })
160
+ }
161
+ })
162
+
163
+ test('module add --eject copies module source and omits package CSS entries', () => {
164
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-source-'))
165
+
166
+ try {
167
+ createStandaloneFixture(appDir)
168
+ runCommand(yarnBinary(), ['install'], appDir)
169
+
170
+ runMercato(
171
+ [
172
+ 'module',
173
+ 'add',
174
+ `@open-mercato/test-package@file:${fixtureModulePackage}`,
175
+ '--eject',
176
+ ],
177
+ appDir,
178
+ )
179
+
180
+ const modulesSource = readFile(path.join(appDir, 'src', 'modules.ts'))
181
+ const cssSource = readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))
182
+
183
+ expect(modulesSource).toContain("{ id: 'test_package', from: '@app' }")
184
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
185
+ expect(cssSource).toBe('')
186
+ } finally {
187
+ fs.rmSync(appDir, { recursive: true, force: true })
188
+ }
189
+ })
190
+
191
+ test('module enable supports package-backed and ejected flows plus both eject entrypoints', () => {
192
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-eject-'))
193
+
194
+ try {
195
+ createStandaloneFixture(appDir)
196
+ runCommand(yarnBinary(), ['install'], appDir)
197
+ runCommand(
198
+ yarnBinary(),
199
+ ['add', `@open-mercato/test-package@file:${fixtureModulePackage}`],
200
+ appDir,
201
+ )
202
+
203
+ runMercato(['module', 'enable', '@open-mercato/test-package'], appDir)
204
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain(
205
+ "{ id: 'test_package', from: '@open-mercato/test-package' }",
206
+ )
207
+
208
+ writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
209
+ fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
210
+ runMercato(['module', 'enable', '@open-mercato/test-package', '--eject'], appDir)
211
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
212
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
213
+ expect(readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))).toBe('')
214
+
215
+ writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
216
+ fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
217
+ runMercato(['module', 'eject', 'test_package'], appDir)
218
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
219
+
220
+ writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
221
+ fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
222
+ runMercato(['eject', 'test_package'], appDir)
223
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
224
+ } finally {
225
+ fs.rmSync(appDir, { recursive: true, force: true })
226
+ }
227
+ })
228
+ })