@open-mercato/cli 0.10.1-develop.7990.1.82d0a183c5 → 0.10.1-develop.7994.1.c9f31972aa

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 (162) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/guides/module-facts.json +162 -162
  3. package/dist/agentic/guides/module-facts.v2.json +162 -162
  4. package/dist/agentic/guides/module-tool.md +21 -0
  5. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  6. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  7. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  8. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  9. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  10. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  11. package/dist/agentic/guides/modules/auth/index.md +1 -1
  12. package/dist/agentic/guides/modules/availability/index.md +1 -1
  13. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  14. package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
  15. package/dist/agentic/guides/modules/cart/index.md +1 -1
  16. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  17. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  20. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  21. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  22. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  24. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  25. package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
  26. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  27. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  28. package/dist/agentic/guides/modules/configs/index.md +1 -1
  29. package/dist/agentic/guides/modules/content/index.md +1 -1
  30. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  31. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  32. package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
  33. package/dist/agentic/guides/modules/customers/index.md +1 -1
  34. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  35. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  36. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  37. package/dist/agentic/guides/modules/devices/index.md +1 -1
  38. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  39. package/dist/agentic/guides/modules/directory/index.md +1 -1
  40. package/dist/agentic/guides/modules/documents/index.md +1 -1
  41. package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
  42. package/dist/agentic/guides/modules/entities/index.md +1 -1
  43. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  44. package/dist/agentic/guides/modules/events/index.md +1 -1
  45. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  46. package/dist/agentic/guides/modules/forms/index.md +1 -1
  47. package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
  48. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  49. package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
  50. package/dist/agentic/guides/modules/generators/index.md +1 -1
  51. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  52. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  53. package/dist/agentic/guides/modules/messages/index.md +1 -1
  54. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  55. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  56. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  57. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  58. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  59. package/dist/agentic/guides/modules/planner/index.md +1 -1
  60. package/dist/agentic/guides/modules/portal/index.md +1 -1
  61. package/dist/agentic/guides/modules/progress/index.md +1 -1
  62. package/dist/agentic/guides/modules/promotions/index.md +1 -1
  63. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  64. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  65. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  66. package/dist/agentic/guides/modules/resources/index.md +1 -1
  67. package/dist/agentic/guides/modules/sales/index.md +1 -1
  68. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  69. package/dist/agentic/guides/modules/search/index.md +1 -1
  70. package/dist/agentic/guides/modules/security/index.md +1 -1
  71. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  72. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  73. package/dist/agentic/guides/modules/sso/index.md +1 -1
  74. package/dist/agentic/guides/modules/staff/index.md +1 -1
  75. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  76. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  77. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  78. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  79. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  80. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  81. package/dist/agentic/guides/modules/translations/index.md +1 -1
  82. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  83. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  84. package/dist/agentic/guides/modules/wms/index.md +1 -1
  85. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  86. package/dist/agentic/guides/reference-module-facts.json +1 -1
  87. package/dist/agentic/guides/upstream/manifest.json +1 -1
  88. package/dist/agentic/shared/ai/harness/cases.json +23 -9
  89. package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
  90. package/dist/lib/__integration__/TC-INT-007.spec.js +207 -19
  91. package/dist/lib/__integration__/TC-INT-007.spec.js.map +2 -2
  92. package/dist/lib/__integration__/TC-INT-009.spec.js +267 -0
  93. package/dist/lib/__integration__/TC-INT-009.spec.js.map +7 -0
  94. package/dist/lib/eject.js +25 -9
  95. package/dist/lib/eject.js.map +2 -2
  96. package/dist/lib/generators/entity-ids.js +2 -0
  97. package/dist/lib/generators/entity-ids.js.map +2 -2
  98. package/dist/lib/generators/module-di.js +2 -0
  99. package/dist/lib/generators/module-di.js.map +2 -2
  100. package/dist/lib/generators/module-entities.js +2 -0
  101. package/dist/lib/generators/module-entities.js.map +2 -2
  102. package/dist/lib/generators/module-package-sources.js +6 -4
  103. package/dist/lib/generators/module-package-sources.js.map +2 -2
  104. package/dist/lib/generators/module-registry.js +32 -86
  105. package/dist/lib/generators/module-registry.js.map +2 -2
  106. package/dist/lib/generators/openapi.js +2 -0
  107. package/dist/lib/generators/openapi.js.map +2 -2
  108. package/dist/lib/generators/source-exports.js +73 -0
  109. package/dist/lib/generators/source-exports.js.map +7 -0
  110. package/dist/lib/init-summary.js +44 -0
  111. package/dist/lib/init-summary.js.map +7 -0
  112. package/dist/lib/module-github.js +98 -0
  113. package/dist/lib/module-github.js.map +7 -0
  114. package/dist/lib/module-identity.js +131 -0
  115. package/dist/lib/module-identity.js.map +7 -0
  116. package/dist/lib/module-inspection.js +124 -0
  117. package/dist/lib/module-inspection.js.map +7 -0
  118. package/dist/lib/module-install.js +150 -21
  119. package/dist/lib/module-install.js.map +3 -3
  120. package/dist/lib/module-package.js +205 -15
  121. package/dist/lib/module-package.js.map +2 -2
  122. package/dist/lib/modules-config.js +69 -30
  123. package/dist/lib/modules-config.js.map +2 -2
  124. package/dist/lib/resolver.js +33 -34
  125. package/dist/lib/resolver.js.map +2 -2
  126. package/dist/mercato.js +14 -23
  127. package/dist/mercato.js.map +2 -2
  128. package/package.json +7 -7
  129. package/src/__tests__/mercato.test.ts +14 -4
  130. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/widgets/injection/test/widget.js +1 -0
  131. package/src/lib/__integration__/TC-INT-007.spec.ts +219 -19
  132. package/src/lib/__integration__/TC-INT-009.spec.ts +279 -0
  133. package/src/lib/__tests__/eject.test.ts +98 -4
  134. package/src/lib/__tests__/init-summary.test.ts +50 -0
  135. package/src/lib/__tests__/module-github.test.ts +108 -0
  136. package/src/lib/__tests__/module-identity.test.ts +144 -0
  137. package/src/lib/__tests__/module-inspection.test.ts +195 -0
  138. package/src/lib/__tests__/module-install.test.ts +399 -10
  139. package/src/lib/__tests__/module-package.test.ts +154 -0
  140. package/src/lib/__tests__/modules-config.test.ts +55 -0
  141. package/src/lib/__tests__/resolver.packages.test.ts +29 -0
  142. package/src/lib/eject.ts +28 -10
  143. package/src/lib/generators/__tests__/module-id-collisions.test.ts +146 -0
  144. package/src/lib/generators/__tests__/module-package-sources.test.ts +19 -2
  145. package/src/lib/generators/__tests__/module-requirements.test.ts +56 -0
  146. package/src/lib/generators/entity-ids.ts +2 -0
  147. package/src/lib/generators/module-di.ts +2 -0
  148. package/src/lib/generators/module-entities.ts +2 -0
  149. package/src/lib/generators/module-package-sources.ts +7 -5
  150. package/src/lib/generators/module-registry.ts +33 -115
  151. package/src/lib/generators/openapi.ts +3 -0
  152. package/src/lib/generators/source-exports.ts +93 -0
  153. package/src/lib/init-summary.ts +54 -0
  154. package/src/lib/module-github.ts +109 -0
  155. package/src/lib/module-identity.ts +145 -0
  156. package/src/lib/module-inspection.ts +133 -0
  157. package/src/lib/module-install.ts +170 -22
  158. package/src/lib/module-package.ts +243 -16
  159. package/src/lib/modules-config.ts +80 -32
  160. package/src/lib/resolver.ts +35 -39
  161. package/src/mercato.ts +14 -24
  162. package/src/types/cross-spawn.d.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.10.1-develop.7990.1.82d0a183c5",
3
+ "version": "0.10.1-develop.7994.1.c9f31972aa",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,10 +60,10 @@
60
60
  "@mikro-orm/decorators": "^7.1.14",
61
61
  "@mikro-orm/migrations": "^7.1.14",
62
62
  "@mikro-orm/postgresql": "^7.1.14",
63
- "@open-mercato/events": "0.10.1-develop.7990.1.82d0a183c5",
64
- "@open-mercato/queue": "0.10.1-develop.7990.1.82d0a183c5",
65
- "@open-mercato/shared": "0.10.1-develop.7990.1.82d0a183c5",
66
- "@open-mercato/telemetry": "0.10.1-develop.7990.1.82d0a183c5",
63
+ "@open-mercato/events": "0.10.1-develop.7994.1.c9f31972aa",
64
+ "@open-mercato/queue": "0.10.1-develop.7994.1.c9f31972aa",
65
+ "@open-mercato/shared": "0.10.1-develop.7994.1.c9f31972aa",
66
+ "@open-mercato/telemetry": "0.10.1-develop.7994.1.c9f31972aa",
67
67
  "cross-spawn": "^7.0.6",
68
68
  "minisearch": "7.2.0",
69
69
  "pg": "8.23.0",
@@ -75,10 +75,10 @@
75
75
  "typescript-js": "npm:typescript@6.0.3"
76
76
  },
77
77
  "peerDependencies": {
78
- "@open-mercato/shared": "0.10.1-develop.7990.1.82d0a183c5"
78
+ "@open-mercato/shared": "0.10.1-develop.7994.1.c9f31972aa"
79
79
  },
80
80
  "devDependencies": {
81
- "@open-mercato/shared": "0.10.1-develop.7990.1.82d0a183c5",
81
+ "@open-mercato/shared": "0.10.1-develop.7994.1.c9f31972aa",
82
82
  "@types/jest": "^30.0.0",
83
83
  "jest": "^30.4.2",
84
84
  "ts-jest": "^29.4.12"
@@ -1125,8 +1125,8 @@ describe('init command failure output', () => {
1125
1125
  expect(authSetup).toHaveBeenCalledWith(expect.arrayContaining(['--include-demo-users']))
1126
1126
  expect(consoleLogSpy).toHaveBeenCalledWith('👤 Demo accounts (admin@/employee@): enabled')
1127
1127
  const loggedLines = consoleLogSpy.mock.calls.map((args) => args.join(' '))
1128
- expect(loggedLines.some((line) => line.includes('🧰 Admin:'))).toBe(true)
1129
- expect(loggedLines.some((line) => line.includes('👷 Employee:'))).toBe(true)
1128
+ expect(loggedLines.some((line) => line.includes('🧰 Admin'))).toBe(true)
1129
+ expect(loggedLines.some((line) => line.includes('👷 Employee'))).toBe(true)
1130
1130
 
1131
1131
  consoleErrorSpy.mockRestore()
1132
1132
  consoleLogSpy.mockRestore()
@@ -1148,8 +1148,8 @@ describe('init command failure output', () => {
1148
1148
  '👤 Demo accounts (admin@/employee@): skipped (--no-demo-users)',
1149
1149
  )
1150
1150
  const loggedLines = consoleLogSpy.mock.calls.map((args) => args.join(' '))
1151
- expect(loggedLines.some((line) => line.includes('🧰 Admin:'))).toBe(false)
1152
- expect(loggedLines.some((line) => line.includes('👷 Employee:'))).toBe(false)
1151
+ expect(loggedLines.some((line) => line.includes('🧰 Admin'))).toBe(false)
1152
+ expect(loggedLines.some((line) => line.includes('👷 Employee'))).toBe(false)
1153
1153
 
1154
1154
  consoleErrorSpy.mockRestore()
1155
1155
  consoleLogSpy.mockRestore()
@@ -1735,6 +1735,11 @@ describe('server dev managed process exits', () => {
1735
1735
  expect(exitCode).toBe(0)
1736
1736
  expect(consoleErrorSpy).not.toHaveBeenCalled()
1737
1737
  expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
1738
+ const { spawn } = await import('child_process')
1739
+ const nextSpawn = (spawn as jest.Mock).mock.calls.find((call) =>
1740
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next') && call[1]?.includes('dev'),
1741
+ )
1742
+ expect(nextSpawn?.[2]?.env.NODE_ENV).toBe('development')
1738
1743
 
1739
1744
  consoleErrorSpy.mockRestore()
1740
1745
  consoleLogSpy.mockRestore()
@@ -2477,6 +2482,11 @@ describe('server start managed process exits', () => {
2477
2482
  expect(exitCode).toBe(0)
2478
2483
  expect(consoleErrorSpy).not.toHaveBeenCalled()
2479
2484
  expect(consoleLogSpy).toHaveBeenCalledWith('[server] Skipping scheduler auto-start — module not enabled')
2485
+ const { spawn } = await import('child_process')
2486
+ const nextSpawn = (spawn as jest.Mock).mock.calls.find((call) =>
2487
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next') && call[1]?.includes('start'),
2488
+ )
2489
+ expect(nextSpawn?.[2]?.env.NODE_ENV).toBe('production')
2480
2490
 
2481
2491
  consoleErrorSpy.mockRestore()
2482
2492
  consoleLogSpy.mockRestore()
@@ -22,21 +22,33 @@ function yarnBinary(): string {
22
22
  return process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
23
23
  }
24
24
 
25
- function runCommand(command: string, args: string[], cwd: string): string {
25
+ function runCommand(command: string, args: string[], cwd: string, environment: NodeJS.ProcessEnv = {}): string {
26
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
- })
27
+ try {
28
+ return execFileSync(command, args, {
29
+ cwd,
30
+ encoding: 'utf8',
31
+ env: {
32
+ ...process.env,
33
+ FORCE_COLOR: '0',
34
+ NODE_NO_WARNINGS: '1',
35
+ YARN_CACHE_FOLDER: yarnCacheFolder,
36
+ YARN_ENABLE_GLOBAL_CACHE: '0',
37
+ YARN_ENABLE_IMMUTABLE_INSTALLS: '0',
38
+ YARN_NODE_LINKER: 'node-modules',
39
+ ...environment,
40
+ },
41
+ })
42
+ } catch (error) {
43
+ if (!error || typeof error !== 'object') throw error
44
+ const errorRecord = error as Record<string, unknown>
45
+ const outputs = ['stdout', 'stderr'].flatMap((streamName) => {
46
+ const output = errorRecord[streamName]
47
+ return typeof output === 'string' || Buffer.isBuffer(output) ? [output.toString().trim()] : []
48
+ }).filter(Boolean)
49
+ const message = error instanceof Error ? error.message : String(error)
50
+ throw new Error(outputs.length > 0 ? `${message}\n${outputs.join('\n')}` : message)
51
+ }
40
52
  }
41
53
 
42
54
  function runMercato(args: string[], cwd: string): string {
@@ -77,6 +89,7 @@ const commentedModulesSource = [
77
89
  ].join('\n')
78
90
 
79
91
  function createMonorepoFixture(rootDir: string): string {
92
+ writeFile(path.join(rootDir, 'yarn.lock'), '')
80
93
  writeFile(
81
94
  path.join(rootDir, 'package.json'),
82
95
  JSON.stringify(
@@ -113,6 +126,7 @@ function createMonorepoFixture(rootDir: string): string {
113
126
  }
114
127
 
115
128
  function createStandaloneFixture(rootDir: string): string {
129
+ writeFile(path.join(rootDir, 'yarn.lock'), '')
116
130
  writeFile(
117
131
  path.join(rootDir, '.yarnrc.yml'),
118
132
  ['nodeLinker: node-modules', 'enableGlobalCache: false', 'cacheFolder: ./.yarn/cache', ''].join('\n'),
@@ -196,14 +210,196 @@ test.describe('TC-INT-007: CLI official module install and eject flows', () => {
196
210
  const modulesSource = readFile(path.join(appDir, 'src', 'modules.ts'))
197
211
  const cssSource = readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))
198
212
 
199
- expect(modulesSource).toContain("{ id: 'test_package', from: '@app' }")
213
+ expect(modulesSource).toMatch(/id: 'test_package', from: '@app'/)
214
+ expect(modulesSource).not.toContain('locator:')
215
+ expect(modulesSource).toContain("packageName: '@open-mercato/test-package'")
200
216
  expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
217
+ expect(readFile(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(readFile(path.join(fixtureModulePackage, 'src', 'modules', 'test_package', 'index.ts')))
201
218
  expect(cssSource).toBe('')
202
219
  } finally {
203
220
  fs.rmSync(appDir, { recursive: true, force: true })
204
221
  }
205
222
  })
206
223
 
224
+ test('module add reports dependencies that must be enabled before registration', () => {
225
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-dependencies-'))
226
+
227
+ try {
228
+ createStandaloneFixture(appDir)
229
+ const packageDir = path.join(appDir, 'fixture-module-package')
230
+ fs.cpSync(fixtureModulePackage, packageDir, { recursive: true })
231
+ writeFile(
232
+ path.join(packageDir, 'src', 'modules', 'test_package', 'index.ts'),
233
+ "export const metadata = { id: 'test_package', requires: ['customers'], ejectable: true }\n",
234
+ )
235
+ runCommand(yarnBinary(), ['install'], appDir)
236
+
237
+ expect(() => runMercato(
238
+ ['module', 'add', '@open-mercato/test-package@file:./fixture-module-package'],
239
+ appDir,
240
+ )).toThrow(/requires module\(s\) that are not enabled: customers/)
241
+
242
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toBe('export const enabledModules = []\n')
243
+ } finally {
244
+ fs.rmSync(appDir, { recursive: true, force: true })
245
+ }
246
+ })
247
+
248
+ test('module add resolves canonical requirements from retained ejected origins', () => {
249
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-canonical-requires-'))
250
+
251
+ try {
252
+ createStandaloneFixture(appDir)
253
+ const packageDir = path.join(appDir, 'fixture-module-package')
254
+ fs.cpSync(fixtureModulePackage, packageDir, { recursive: true })
255
+ const requirements = [
256
+ '@pkarw/marketing_consents@preview',
257
+ 'npm:@pkarw/marketing_consents@1.0.0',
258
+ 'github:pkarw/marketing-consents',
259
+ 'npm:consent-helper@next',
260
+ 'github:pkarw/consent-tools#main',
261
+ 'pkarw/consent-tools',
262
+ 'https://github.com/pkarw/consent-tools.git#release',
263
+ 'ssh://git@github.com/pkarw/consent-tools.git#main',
264
+ 'git@github.com:pkarw/consent-tools.git',
265
+ 'github:pkarw/installed-tools',
266
+ 'npm:@open-mercato/core@preview',
267
+ ]
268
+ writeFile(
269
+ path.join(packageDir, 'src', 'modules', 'test_package', 'index.ts'),
270
+ `export const metadata = { id: 'test_package', requires: ${JSON.stringify(requirements)}, ejectable: true }\n`,
271
+ )
272
+ const entries = [
273
+ { id: 'marketing_consents', from: '@app', locator: 'github:pkarw/marketing-consents', packageName: '@pkarw/marketing_consents' },
274
+ { id: 'npm_consents', from: '@app', locator: 'npm:@pkarw/marketing_consents', packageName: '@pkarw/marketing_consents' },
275
+ { id: 'npm_core', from: '@app', locator: 'npm:@open-mercato/core', packageName: '@open-mercato/core' },
276
+ { id: 'consent_helper', from: '@app', locator: 'npm:consent-helper' },
277
+ { id: 'consent_tools', from: '@app', locator: 'github:pkarw/consent-tools' },
278
+ ]
279
+ for (const entry of entries) {
280
+ writeFile(
281
+ path.join(appDir, 'src', 'modules', entry.id, 'index.ts'),
282
+ `export const metadata = { id: '${entry.id}' }\n`,
283
+ )
284
+ }
285
+ const corePackageDir = path.join(appDir, 'vendor', 'core')
286
+ writeFile(
287
+ path.join(corePackageDir, 'package.json'),
288
+ JSON.stringify({ name: '@open-mercato/core', version: '0.4.7', type: 'module', repository: { type: 'git', url: 'https://github.com/pkarw/installed-tools.git' } }),
289
+ )
290
+ writeFile(
291
+ path.join(corePackageDir, 'src', 'modules', 'repository_tools', 'index.ts'),
292
+ "export const metadata = { id: 'repository_tools' }\n",
293
+ )
294
+ writeFile(
295
+ path.join(corePackageDir, 'dist', 'modules', 'repository_tools', 'index.js'),
296
+ "export const metadata = { id: 'repository_tools' }\n",
297
+ )
298
+ const enabledEntries = [...entries, { id: 'repository_tools', from: '@open-mercato/core', locator: 'github:pkarw/installed-tools' }]
299
+ writeFile(path.join(appDir, 'src', 'modules.ts'), `export const enabledModules = ${JSON.stringify(enabledEntries)}\n`)
300
+ runCommand(yarnBinary(), ['install'], appDir)
301
+
302
+ runMercato(['module', 'add', 'npm:@open-mercato/test-package@file:./fixture-module-package'], appDir)
303
+
304
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("id: 'test_package'")
305
+ } finally {
306
+ fs.rmSync(appDir, { recursive: true, force: true })
307
+ }
308
+ })
309
+
310
+ test('same-name modules from different origins do not satisfy canonical requirements', () => {
311
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-wrong-origin-'))
312
+
313
+ try {
314
+ createStandaloneFixture(appDir)
315
+ const packageDir = path.join(appDir, 'fixture-module-package')
316
+ fs.cpSync(fixtureModulePackage, packageDir, { recursive: true })
317
+ writeFile(
318
+ path.join(packageDir, 'src', 'modules', 'test_package', 'index.ts'),
319
+ "export const metadata = { id: 'test_package', requires: ['@pkarw/marketing_consents', 'github:pkarw/consent-tools'], ejectable: true }\n",
320
+ )
321
+ const modulesSource = `export const enabledModules = [
322
+ { id: 'marketing_consents', from: '@app', locator: 'npm:@open-mercato/marketing_consents' },
323
+ { id: 'consent_tools', from: '@app', locator: 'github:another-owner/consent-tools' },
324
+ ]\n`
325
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
326
+ runCommand(yarnBinary(), ['install'], appDir)
327
+
328
+ expect(() => runMercato(
329
+ ['module', 'add', '@open-mercato/test-package@file:./fixture-module-package', '--eject'],
330
+ appDir,
331
+ )).toThrow(/@pkarw\/marketing_consents[\s\S]*github:pkarw\/consent-tools[\s\S]*npx create-mercato-module install/)
332
+
333
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toBe(modulesSource)
334
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package'))).toBe(false)
335
+ } finally {
336
+ fs.rmSync(appDir, { recursive: true, force: true })
337
+ }
338
+ })
339
+
340
+ test('module add rejects a same-ID source collision before ejecting', () => {
341
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-module-collision-'))
342
+
343
+ try {
344
+ createStandaloneFixture(appDir)
345
+ const modulesSource = "export const enabledModules = [{ id: 'test_package', from: '@app', locator: 'github:another-owner/test-package' }]\n"
346
+ const localSource = "export const metadata = { id: 'test_package', locator: 'github:another-owner/test-package' }\n"
347
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
348
+ writeFile(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'), localSource)
349
+ runCommand(yarnBinary(), ['install'], appDir)
350
+
351
+ expect(() => runMercato(
352
+ ['module', 'add', `@open-mercato/test-package@file:${fixtureModulePackage}`, '--eject'],
353
+ appDir,
354
+ )).toThrow(/(?=[\s\S]*test_package)(?=[\s\S]*(?:collision|already|conflict))(?=[\s\S]*another-owner)(?=[\s\S]*@open-mercato\/test-package)/i)
355
+
356
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toBe(modulesSource)
357
+ expect(readFile(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(localSource)
358
+ } finally {
359
+ fs.rmSync(appDir, { recursive: true, force: true })
360
+ }
361
+ })
362
+
363
+ test('GitHub locators detect a runtime ID collision before invoking Yarn', () => {
364
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-github-collision-'))
365
+ try {
366
+ createStandaloneFixture(appDir)
367
+ const modulesSource = "export const enabledModules = [{ id: 'test_package', from: '@app', locator: 'github:other/test-package' }]\n"
368
+ writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
369
+ const originalManifest = readFile(path.join(appDir, 'package.json'))
370
+ const executableDir = path.join(appDir, 'bin')
371
+ const packageManifest = Buffer.from(JSON.stringify({ name: '@pkarw/test-package' })).toString('base64')
372
+ const ghPath = path.join(executableDir, 'gh')
373
+ writeFile(ghPath, `#!/usr/bin/env node
374
+ const endpoint = process.argv[3]
375
+ const sha = 'a'.repeat(40)
376
+ const value = endpoint === 'user' ? { login: 'pkarw' }
377
+ : endpoint.includes('/commits/') ? { sha }
378
+ : endpoint.includes('/contents/') ? { encoding: 'base64', content: ${JSON.stringify(packageManifest)} }
379
+ : endpoint.includes('/git/trees/') ? { tree: [{ path: 'src/modules/test_package/index.ts' }] }
380
+ : { default_branch: 'main' }
381
+ process.stdout.write(JSON.stringify(value))
382
+ `)
383
+ const yarnPath = path.join(executableDir, 'yarn')
384
+ const marker = path.join(appDir, 'yarn-invoked')
385
+ writeFile(yarnPath, "#!/usr/bin/env node\nrequire('node:fs').writeFileSync(process.env.COLLISION_YARN_MARKER, 'invoked')\nprocess.exit(1)\n")
386
+ fs.chmodSync(ghPath, 0o755)
387
+ fs.chmodSync(yarnPath, 0o755)
388
+ for (const locator of ['github:pkarw/test-package', 'https://github.com/pkarw/test-package.git', 'git@github.com:pkarw/test-package.git', 'github:test-package']) {
389
+ expect(() => runCommand(process.execPath, [cliBin, 'module', 'add', locator, '--allow-third-party'], appDir, {
390
+ PATH: executableDir + path.delimiter + process.env.PATH,
391
+ COLLISION_YARN_MARKER: marker,
392
+ })).toThrow(/Module name collision[\s\S]*test_package[\s\S]*github:other\/test-package[\s\S]*github:pkarw\/test-package/)
393
+ }
394
+ expect(fs.existsSync(marker)).toBe(false)
395
+ expect(readFile(path.join(appDir, 'package.json'))).toBe(originalManifest)
396
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toBe(modulesSource)
397
+ expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package'))).toBe(false)
398
+ } finally {
399
+ fs.rmSync(appDir, { recursive: true, force: true })
400
+ }
401
+ })
402
+
207
403
  test('module enable supports package-backed and ejected flows plus both eject entrypoints', () => {
208
404
  const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-cli-standalone-eject-'))
209
405
 
@@ -218,25 +414,29 @@ test.describe('TC-INT-007: CLI official module install and eject flows', () => {
218
414
 
219
415
  runMercato(['module', 'enable', '@open-mercato/test-package'], appDir)
220
416
  expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain(
221
- "{ id: 'test_package', from: '@open-mercato/test-package' }",
417
+ "id: 'test_package', from: '@open-mercato/test-package'",
222
418
  )
223
419
 
224
420
  writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
225
421
  fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
226
422
  runMercato(['module', 'enable', '@open-mercato/test-package', '--eject'], appDir)
227
- expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
423
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("id: 'test_package', from: '@app'")
228
424
  expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'test_package', 'index.ts'))).toBe(true)
229
425
  expect(readFile(path.join(appDir, '.mercato', 'generated', 'module-package-sources.css'))).toBe('')
230
426
 
231
427
  writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
232
428
  fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
233
429
  runMercato(['module', 'eject', 'test_package'], appDir)
234
- expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
430
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("packageName: '@open-mercato/test-package'")
431
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).not.toContain("locator: 'npm:@open-mercato/test-package'")
432
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("id: 'test_package', from: '@app'")
235
433
 
236
434
  writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n")
237
435
  fs.rmSync(path.join(appDir, 'src', 'modules', 'test_package'), { recursive: true, force: true })
238
436
  runMercato(['eject', 'test_package'], appDir)
239
- expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("{ id: 'test_package', from: '@app' }")
437
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("packageName: '@open-mercato/test-package'")
438
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).not.toContain("locator: 'npm:@open-mercato/test-package'")
439
+ expect(readFile(path.join(appDir, 'src', 'modules.ts'))).toContain("id: 'test_package', from: '@app'")
240
440
  } finally {
241
441
  fs.rmSync(appDir, { recursive: true, force: true })
242
442
  }