@open-mercato/cli 0.10.1-develop.7983.1.fd91207efe → 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
@@ -0,0 +1,145 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import ts from 'typescript-js'
4
+ import { normalizeModuleLocator, type ModuleIdentity } from '@open-mercato/shared/modules/module-locators'
5
+ import type { ModuleEntry, PackageResolver } from './resolver'
6
+ import { readEnabledModules } from './modules-config'
7
+
8
+ function readJson(filePath: string): Record<string, unknown> {
9
+ try {
10
+ const value: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'))
11
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
12
+ } catch {
13
+ return {}
14
+ }
15
+ }
16
+
17
+ function readMetadataLocator(moduleBase: string): string | undefined {
18
+ const indexPath = ['index.ts', 'index.js'].map((name) => path.join(moduleBase, name)).find((candidate) => fs.existsSync(candidate))
19
+ if (!indexPath) return undefined
20
+ const source = ts.createSourceFile(indexPath, fs.readFileSync(indexPath, 'utf8'), ts.ScriptTarget.Latest, true)
21
+ let locator: string | undefined
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'metadata' && node.initializer) {
24
+ let initializer = node.initializer
25
+ while (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer) || ts.isParenthesizedExpression(initializer)) initializer = initializer.expression
26
+ if (ts.isObjectLiteralExpression(initializer)) {
27
+ for (const property of initializer.properties) {
28
+ if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) && property.name.text === 'locator' && ts.isStringLiteralLike(property.initializer)) locator = property.initializer.text
29
+ }
30
+ }
31
+ }
32
+ ts.forEachChild(node, visit)
33
+ }
34
+ visit(source)
35
+ return locator
36
+ }
37
+
38
+ function sourceLocator(value: unknown): string | undefined {
39
+ if (typeof value !== 'string') return undefined
40
+ const locator = normalizeModuleLocator(value)
41
+ return locator?.startsWith('npm:') || locator?.startsWith('github:') ? locator : undefined
42
+ }
43
+
44
+ function installedDependencyLocator(specifier: unknown, canonicalName: string | undefined): string | undefined {
45
+ if (typeof specifier !== 'string') return undefined
46
+ if (/^(?:github:|github\.com\/|(?:git\+)?https?:\/\/github\.com\/|git@github\.com:|(?:git\+)?ssh:\/\/|[a-z0-9][a-z0-9-]*\/[a-z0-9._-]+(?:[#@]|$))/i.test(specifier)) {
47
+ const locator = sourceLocator(specifier)
48
+ return locator?.startsWith('github:') ? locator : undefined
49
+ }
50
+ if (specifier.startsWith('npm:')) {
51
+ const aliasTarget = specifier.match(/^npm:(@[^/]+\/[^@]+|[^@]+)(?:@.+)?$/)?.[1]
52
+ return aliasTarget ? sourceLocator(`npm:${aliasTarget}`) : undefined
53
+ }
54
+ if (!canonicalName || /^(?:[a-z][a-z0-9+.-]*:|[./]|https?:)/i.test(specifier)) return undefined
55
+ return sourceLocator(`npm:${canonicalName}`)
56
+ }
57
+
58
+ function assertDeclaredIdentity(entry: ModuleEntry, observedLocator: string | undefined, canonicalName: string | undefined): void {
59
+ const binding = entry.from ?? '@open-mercato/core'
60
+ if (entry.packageName && canonicalName && entry.packageName !== canonicalName) {
61
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared package '${entry.packageName}' disagrees with installed package '${canonicalName}'. Restore the intended binding or correct src/modules.ts, then regenerate.`)
62
+ }
63
+ const declaredLocator = sourceLocator(entry.locator)
64
+ if (declaredLocator && observedLocator && declaredLocator !== observedLocator) {
65
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared origin '${declaredLocator}' disagrees with observed origin '${observedLocator}'. Disable the old provider before replacing it, then regenerate.`)
66
+ }
67
+ if (declaredLocator?.startsWith('npm:') && canonicalName && declaredLocator !== sourceLocator(`npm:${canonicalName}`)) {
68
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared origin '${declaredLocator}' disagrees with installed package '${canonicalName}'. Correct src/modules.ts to select the intended provider, then regenerate.`)
69
+ }
70
+ }
71
+
72
+ function readModuleLocators(
73
+ entry: ModuleEntry,
74
+ resolver: PackageResolver,
75
+ appManifest: Record<string, unknown>,
76
+ packageManifests: Map<string, Record<string, unknown>>,
77
+ ): string[] {
78
+ const from = entry.from ?? '@open-mercato/core'
79
+ if (from === '@app') {
80
+ const retainedLocator = sourceLocator(entry.locator ?? readMetadataLocator(resolver.getModulePaths(entry).appBase))
81
+ assertDeclaredIdentity(entry, retainedLocator, entry.packageName)
82
+ return retainedLocator ? [retainedLocator] : []
83
+ }
84
+
85
+ let manifest = packageManifests.get(from)
86
+ if (!manifest) {
87
+ manifest = readJson(path.join(resolver.getPackageRoot(from), 'package.json'))
88
+ packageManifests.set(from, manifest)
89
+ }
90
+ const canonicalName = typeof manifest.name === 'string' ? manifest.name : undefined
91
+ const observedLocators = new Set<string>()
92
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
93
+ const dependencies = appManifest[section]
94
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
95
+ const specifier = (dependencies as Record<string, unknown>)[from]
96
+ const locator = installedDependencyLocator(specifier, canonicalName)
97
+ if (locator?.startsWith('npm:') && canonicalName !== from && typeof specifier === 'string' && !specifier.startsWith('npm:')) {
98
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': installed package '${canonicalName}' does not match the dependency binding. Restore the intended package or declare an explicit npm alias, then regenerate.`)
99
+ }
100
+ if (locator) observedLocators.add(locator)
101
+ }
102
+ if (observedLocators.size > 1) {
103
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': dependency sections declare conflicting origins. Keep one provider for this binding in package.json, then regenerate.`)
104
+ }
105
+ const observedLocator = [...observedLocators][0]
106
+ if (observedLocator?.startsWith('npm:') && canonicalName && observedLocator !== sourceLocator(`npm:${canonicalName}`)) {
107
+ throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': dependency origin '${observedLocator}' disagrees with installed package '${canonicalName}'. Restore the intended package through the package manager, then regenerate.`)
108
+ }
109
+ assertDeclaredIdentity(entry, observedLocator, canonicalName)
110
+ const locator = observedLocator ?? sourceLocator(entry.locator)
111
+ return locator ? [locator] : []
112
+ }
113
+
114
+ export function getModuleLocators(entry: ModuleEntry, resolver: PackageResolver): string[] {
115
+ return readModuleLocators(entry, resolver, readJson(path.join(resolver.getAppDir(), 'package.json')), new Map())
116
+ }
117
+
118
+ export function assertUniqueEnabledModuleIds(enabledModules: readonly ModuleEntry[], resolver: PackageResolver): void {
119
+ const byId = new Map<string, ModuleEntry>()
120
+ for (const entry of enabledModules) {
121
+ const existing = byId.get(entry.id)
122
+ if (existing) {
123
+ const describe = (provider: ModuleEntry): string => {
124
+ const binding = provider.from ?? '@open-mercato/core'
125
+ const source = sourceLocator(provider.locator)
126
+ return binding === '@app'
127
+ ? `@app (${resolver.getModulePaths(provider).appBase})${source ? `, origin ${source}` : ''}`
128
+ : `binding '${binding}'${source ? `, origin ${source}` : ''}`
129
+ }
130
+ throw new Error(`[generate] Duplicate enabled module ID '${entry.id}': ${describe(existing)} and ${describe(entry)}. Keep one provider in src/modules.ts, then run yarn mercato generate. Installing under another package alias does not allow both module IDs to run.`)
131
+ }
132
+ byId.set(entry.id, entry)
133
+ }
134
+ }
135
+
136
+ export function getEnabledModuleIdentities(resolver: PackageResolver, enabledModules: readonly ModuleEntry[] = resolver.loadEnabledModules()): ModuleIdentity[] {
137
+ const modulesPath = typeof resolver.getModulesConfigPath === 'function'
138
+ ? resolver.getModulesConfigPath()
139
+ : path.join(resolver.getAppDir(), 'src', 'modules.ts')
140
+ if (fs.existsSync(modulesPath)) assertUniqueEnabledModuleIds(readEnabledModules(modulesPath), resolver)
141
+ assertUniqueEnabledModuleIds(enabledModules, resolver)
142
+ const appManifest = readJson(path.join(resolver.getAppDir(), 'package.json'))
143
+ const packageManifests = new Map<string, Record<string, unknown>>()
144
+ return enabledModules.map((entry) => ({ id: entry.id, locators: readModuleLocators(entry, resolver, appManifest, packageManifests) }))
145
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { execFileSync } from 'node:child_process'
5
+ import spawn from 'cross-spawn'
6
+ import { assertModulePackageExports, isModulePackageExportError } from './module-package'
7
+
8
+ export type InspectedModuleTarget = {
9
+ packageName: string
10
+ packageSpec: string
11
+ moduleIds: string[]
12
+ locator?: string
13
+ version?: string
14
+ }
15
+
16
+ type ArtifactInspection = { moduleIds: string[]; version?: string }
17
+
18
+ function record(value: unknown): Record<string, unknown> {
19
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
20
+ }
21
+
22
+ function inspectFiles(manifest: Record<string, unknown>, packageName: string, files: string[], requiredModuleIds: readonly string[], readSource: (relativePath: string) => string): ArtifactInspection {
23
+ if (manifest.name !== packageName) throw new Error(`Inspected package does not match "${packageName}". No package was installed.`)
24
+ const moduleIds = [...new Set(files.flatMap((file) => {
25
+ const match = /^src\/modules\/([a-z0-9]+(?:_[a-z0-9]+)*)\/index\.ts$/.exec(file)
26
+ return match && files.includes(`dist/modules/${match[1]}/index.js`) ? [match[1]] : []
27
+ }))]
28
+ const checkedModuleIds = requiredModuleIds.length ? requiredModuleIds : moduleIds.length === 1 ? moduleIds : []
29
+ for (const moduleId of checkedModuleIds) {
30
+ if (!moduleIds.includes(moduleId)) throw new Error(`Package "${packageName}" is missing source or compiled descriptors for "${moduleId}". No package was installed.`)
31
+ assertModulePackageExports(manifest, packageName, moduleId, files, readSource)
32
+ }
33
+ if (moduleIds.length === 0) throw new Error(`Package "${packageName}" is missing source or compiled module descriptors. Build the package before retrying. No package was installed.`)
34
+ return { moduleIds, ...(typeof manifest.version === 'string' ? { version: manifest.version } : {}) }
35
+ }
36
+
37
+ function inspectArchive(archive: string, packageName: string, requiredModuleIds: readonly string[]): ArtifactInspection {
38
+ const manifest = record(JSON.parse(execFileSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8', timeout: 15_000 })))
39
+ const files = execFileSync('tar', ['-tf', archive], { encoding: 'utf8', timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }).split('\n').filter((file) => file.startsWith('package/')).map((file) => file.slice('package/'.length))
40
+ return inspectFiles(manifest, packageName, files, requiredModuleIds, (relativePath) => execFileSync('tar', ['-xOf', archive, `package/${relativePath}`], { encoding: 'utf8', timeout: 15_000, maxBuffer: 1024 * 1024 }))
41
+ }
42
+
43
+ function packageFiles(packageRoot: string, relativeDir = ''): string[] {
44
+ return fs.readdirSync(path.join(packageRoot, relativeDir), { withFileTypes: true }).flatMap((entry) => {
45
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) return []
46
+ const relativePath = path.posix.join(relativeDir, entry.name)
47
+ return entry.isDirectory() ? packageFiles(packageRoot, relativePath) : entry.isFile() ? [relativePath] : []
48
+ })
49
+ }
50
+
51
+ function isModernYarnProject(appDir: string): boolean {
52
+ let directory = appDir
53
+ while (true) {
54
+ if (fs.existsSync(path.join(directory, '.yarnrc.yml'))) return true
55
+ const manifestPath = path.join(directory, 'package.json')
56
+ if (fs.existsSync(manifestPath)) {
57
+ const manifest = record(JSON.parse(fs.readFileSync(manifestPath, 'utf8')))
58
+ if (typeof manifest.packageManager === 'string' && /^yarn@(?:[2-9]|[1-9][0-9]+)\./.test(manifest.packageManager)) return true
59
+ }
60
+ const parent = path.dirname(directory)
61
+ if (parent === directory) return false
62
+ directory = parent
63
+ }
64
+ }
65
+
66
+ function yarnNpmConfiguration(appDir: string, packageName: string, temporaryDir: string): string[] {
67
+ if (!isModernYarnProject(appDir)) return []
68
+ const settings = new Map<string, unknown>()
69
+ for (const setting of ['npmRegistryServer', 'npmScopes', 'npmRegistries', 'npmAuthToken', 'npmAuthIdent']) {
70
+ const result = spawn.sync(process.platform === 'win32' ? 'yarn.cmd' : 'yarn', ['config', 'get', setting, '--json', '--no-redacted'], {
71
+ cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000,
72
+ })
73
+ if (result.error || result.status !== 0) throw new Error('Cannot inspect project package registry configuration')
74
+ settings.set(setting, JSON.parse(result.stdout))
75
+ }
76
+ const scope = packageName.startsWith('@') ? packageName.slice(1).split('/')[0] : undefined
77
+ const scopeSettings = scope ? record(record(settings.get('npmScopes'))[scope]) : {}
78
+ const registry = typeof scopeSettings.npmRegistryServer === 'string' ? scopeSettings.npmRegistryServer : settings.get('npmRegistryServer')
79
+ if (typeof registry !== 'string') throw new Error('Missing project package registry')
80
+ const registryUrl = new URL(registry)
81
+ if (!['http:', 'https:'].includes(registryUrl.protocol) || registryUrl.username || registryUrl.password) throw new Error('Invalid project package registry')
82
+ const registryKey = `//${registryUrl.host}${registryUrl.pathname}`.replace(/\/$/, '')
83
+ const registries = record(settings.get('npmRegistries'))
84
+ const registrySettings = record(registries[registry.replace(/\/$/, '')] ?? registries[registryKey] ?? registries[`${registryKey}/`])
85
+ const auth = scopeSettings.npmAuthToken || scopeSettings.npmAuthIdent ? scopeSettings
86
+ : registrySettings.npmAuthToken || registrySettings.npmAuthIdent ? registrySettings
87
+ : { npmAuthToken: settings.get('npmAuthToken'), npmAuthIdent: settings.get('npmAuthIdent') }
88
+ const userConfig = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig ?? path.join(os.homedir(), '.npmrc')
89
+ const existingConfig = fs.existsSync(userConfig) ? fs.readFileSync(userConfig, 'utf8') : ''
90
+ const lines = [existingConfig, `registry=${JSON.stringify(registry)}`]
91
+ if (scope) lines.push(`@${scope}:registry=${JSON.stringify(registry)}`)
92
+ if (typeof auth.npmAuthToken === 'string' && auth.npmAuthToken) lines.push(`${registryKey}/:_authToken=${JSON.stringify(auth.npmAuthToken)}`)
93
+ else if (typeof auth.npmAuthIdent === 'string' && auth.npmAuthIdent) lines.push(`${registryKey}/:_auth=${Buffer.from(auth.npmAuthIdent).toString('base64')}`)
94
+ const configPath = path.join(temporaryDir, 'inspection.npmrc')
95
+ fs.writeFileSync(configPath, `${lines.join('\n')}\n`, { mode: 0o600 })
96
+ return ['--userconfig', configPath, '--registry', registry, ...(scope ? [`--@${scope}:registry=${registry}`] : [])]
97
+ }
98
+
99
+ export function inspectNpmModuleTarget(packageSpec: string, packageName: string, appDir: string, requiredModuleIds: readonly string[] = []): InspectedModuleTarget {
100
+ const filePrefix = `${packageName}@file:`
101
+ if (packageSpec.startsWith(filePrefix)) {
102
+ const packageRoot = path.resolve(appDir, packageSpec.slice(filePrefix.length))
103
+ if (fs.statSync(packageRoot).isFile()) return { packageName, packageSpec, ...inspectArchive(packageRoot, packageName, requiredModuleIds) }
104
+ const manifest = record(JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')))
105
+ const inspected = inspectFiles(manifest, packageName, packageFiles(packageRoot), requiredModuleIds, (relativePath) => fs.readFileSync(path.join(packageRoot, relativePath), 'utf8'))
106
+ return { packageName, packageSpec, ...inspected }
107
+ }
108
+
109
+ const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-module-inspection-'))
110
+ let inspectionStep = 'read the project registry configuration'
111
+ try {
112
+ const configurationArgs = yarnNpmConfiguration(appDir, packageName, temporaryDir)
113
+ inspectionStep = 'fetch the exact npm artifact'
114
+ const packedProcess = spawn.sync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['pack', packageSpec, '--ignore-scripts', '--json', '--pack-destination', temporaryDir, ...configurationArgs], {
115
+ cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
116
+ })
117
+ if (packedProcess.error || packedProcess.status !== 0) throw new Error('Artifact inspection failed')
118
+ inspectionStep = 'parse the npm artifact response'
119
+ const packages: unknown = JSON.parse(packedProcess.stdout)
120
+ const packed = record(Array.isArray(packages) ? packages[0] : undefined)
121
+ if (packed.name !== packageName || typeof packed.version !== 'string' || !/^[0-9A-Za-z.+-]+$/.test(packed.version)
122
+ || typeof packed.filename !== 'string' || path.basename(packed.filename) !== packed.filename) throw new Error('Invalid packed manifest')
123
+ inspectionStep = 'validate the packed module artifact'
124
+ const inspected = inspectArchive(path.join(temporaryDir, packed.filename), packageName, requiredModuleIds)
125
+ if (inspected.version !== packed.version) throw new Error('Packed artifact version disagrees with resolution')
126
+ return { packageName, packageSpec, ...inspected, locator: `npm:${packageName}` }
127
+ } catch (error) {
128
+ const safeDetail = isModulePackageExportError(error) && error instanceof Error ? ` ${error.message}` : ''
129
+ throw new Error(`Cannot inspect the exact module artifact for "${packageName}" while trying to ${inspectionStep}.${safeDetail} Check project registry credentials, package access and shipped src/dist module descriptors before retrying. No package was installed.`)
130
+ } finally {
131
+ fs.rmSync(temporaryDir, { recursive: true, force: true })
132
+ }
133
+ }
@@ -8,9 +8,13 @@ import {
8
8
  validateEjectBoundaries,
9
9
  type ValidatedOfficialModulePackage,
10
10
  } from './module-package'
11
- import { assertModuleNotGatedOff, ensureModuleRegistration } from './modules-config'
11
+ import { assertModuleNotGatedOff, prepareModuleRegistration, prepareModuleRegistrationSource } from './modules-config'
12
12
  import type { PackageResolver } from './resolver'
13
13
  import { resolveSpawnCommand } from './spawn'
14
+ import { parseModuleLocator, resolveModuleRequirement } from '@open-mercato/shared/modules/module-locators'
15
+ import { getEnabledModuleIdentities, getModuleLocators } from './module-identity'
16
+ import { resolveGithubModuleTarget } from './module-github'
17
+ import { inspectNpmModuleTarget, type InspectedModuleTarget } from './module-inspection'
14
18
 
15
19
  export type ModuleCommandResult = {
16
20
  moduleId: string
@@ -91,7 +95,7 @@ function readFileIfExists(filePath: string): string | null {
91
95
  return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null
92
96
  }
93
97
 
94
- function isDeclaredAppDependency(appDir: string, packageName: string): boolean {
98
+ function isDeclaredAppDependency(appDir: string, packageName: string, packageSpecifier?: string): boolean {
95
99
  const packageJsonContent = readFileIfExists(path.join(appDir, 'package.json'))
96
100
  if (!packageJsonContent) {
97
101
  return false
@@ -105,10 +109,13 @@ function isDeclaredAppDependency(appDir: string, packageName: string): boolean {
105
109
  throw new Error(`App package.json at ${path.join(appDir, 'package.json')} is not valid JSON. ${message}`)
106
110
  }
107
111
 
108
- return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some((field) => {
112
+ const declarations = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].flatMap((field) => {
109
113
  const dependencies = packageJson[field]
110
114
  return typeof dependencies === 'object' && dependencies !== null && packageName in dependencies
115
+ ? [(dependencies as Record<string, unknown>)[packageName]]
116
+ : []
111
117
  })
118
+ return declarations.length > 0 && (packageSpecifier === undefined || declarations.every((declaration) => declaration === packageSpecifier))
112
119
  }
113
120
 
114
121
  function isGatedOffError(error: unknown): boolean {
@@ -187,7 +194,7 @@ function assertThirdPartyAllowed(packageName: string, allowThirdParty: boolean):
187
194
  }
188
195
 
189
196
  function assertSupportedPackageSpec(packageSpec: string, allowThirdParty: boolean): string {
190
- const trimmed = packageSpec.trim()
197
+ const trimmed = packageSpec.trim().replace(/^npm:/, '')
191
198
 
192
199
  if (!SAFE_PACKAGE_SPEC_PATTERN.test(trimmed)) {
193
200
  throw new Error('Unsupported package spec. Provide a valid npm package name with an optional tag/version or file: locator.')
@@ -217,12 +224,71 @@ function validateBeforeRegistration(
217
224
  const appModuleDir = path.join(resolver.getAppDir(), 'src', 'modules', modulePackage.metadata.moduleId)
218
225
 
219
226
  if (eject && fs.existsSync(appModuleDir)) {
220
- throw new Error(`Destination directory already exists: ${appModuleDir}. Remove it first or rerun without --eject.`)
227
+ throw new Error(`Destination directory already exists: ${appModuleDir}. Preserve local source and choose a different destination or resolve the conflict manually.`)
221
228
  }
222
229
 
223
230
  return appModuleDir
224
231
  }
225
232
 
233
+ function assertRequiredModulesEnabled(
234
+ modulePackage: ValidatedOfficialModulePackage,
235
+ resolver: PackageResolver,
236
+ ): void {
237
+ const enabledModules = getEnabledModuleIdentities(resolver)
238
+ const missingModuleIds = Array.from(new Set(modulePackage.moduleInfo.requires ?? [])).filter(
239
+ (requirement) => !resolveModuleRequirement(requirement, enabledModules),
240
+ )
241
+ if (missingModuleIds.length === 0) return
242
+
243
+ throw new Error(
244
+ [
245
+ `Module "${modulePackage.metadata.moduleId}" requires module(s) that are not enabled: ${missingModuleIds.join(', ')}.`,
246
+ 'Enable the missing module(s) in src/modules.ts first, then rerun this command.',
247
+ ...missingModuleIds.filter((requirement) => parseModuleLocator(requirement)?.kind !== 'module').map(
248
+ (requirement) => `Install ${requirement}: npx create-mercato-module install ${shellQuote(requirement)}`,
249
+ ),
250
+ ].join('\n'),
251
+ )
252
+ }
253
+
254
+ function shellQuote(value: string): string {
255
+ return "'" + value.replace(/'/g, "'\\''") + "'"
256
+ }
257
+
258
+ function assertNoModuleCollision(
259
+ resolver: PackageResolver,
260
+ moduleId: string,
261
+ packageName: string,
262
+ locator?: string,
263
+ ): void {
264
+ const registrations = resolver.loadEnabledModules().filter((entry) => entry.id === moduleId)
265
+ if (registrations.length > 1) {
266
+ throw new Error(`Module name collision: "${moduleId}" is registered multiple times from ${registrations.map((entry) => entry.locator ?? entry.from ?? '@open-mercato/core').join(', ')}. Resolve duplicate registrations before retrying.`)
267
+ }
268
+ const existing = registrations[0]
269
+ if (existing && (existing.from ?? '@open-mercato/core') !== packageName) {
270
+ throw new Error(`Module name collision: "${moduleId}" is already registered from "${existing.locator ?? existing.from ?? '@open-mercato/core'}"; requested source is "${locator ?? packageName}". Two sources cannot share one module id. Remove or rename the conflicting module explicitly before retrying.`)
271
+ }
272
+ const existingGithub = existing ? getModuleLocators(existing, resolver).find((value) => value.startsWith('github:')) : undefined
273
+ const requestedIdentity = locator ? parseModuleLocator(locator)?.identity : undefined
274
+ if (existingGithub && requestedIdentity && existingGithub !== requestedIdentity) {
275
+ throw new Error(`Module name collision: "${moduleId}" is already registered from "${existingGithub}"; requested source is "${locator}". Remove or rename the conflicting module explicitly before retrying.`)
276
+ }
277
+ if (!existing && fs.existsSync(path.join(resolver.getAppDir(), 'src', 'modules', moduleId))) {
278
+ throw new Error(`Module name collision: local src/modules/${moduleId} already exists; requested source is "${locator ?? packageName}". Remove or rename the conflicting module explicitly before retrying.`)
279
+ }
280
+ }
281
+
282
+ function assertBindingPreserved(resolver: PackageResolver, target: InspectedModuleTarget): void {
283
+ const consumers = resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === target.packageName)
284
+ for (const entry of consumers) {
285
+ const locators = getModuleLocators(entry, resolver)
286
+ if (!target.locator || !locators.includes(target.locator) || !target.moduleIds.includes(entry.id)) {
287
+ throw new Error(`Package binding collision: "${target.packageName}" supplies enabled module "${entry.id}" from "${locators.join(', ') || 'unknown origin'}". Requested "${target.locator ?? 'unknown origin'}" must preserve its source and module layout. Disable affected entries explicitly before replacing this binding. No package was installed.`)
288
+ }
289
+ }
290
+ }
291
+
226
292
  function prepareRegistrationSource(
227
293
  modulePackage: ValidatedOfficialModulePackage,
228
294
  resolver: PackageResolver,
@@ -256,19 +322,38 @@ async function registerResolvedOfficialModule(
256
322
  modulePackage: ValidatedOfficialModulePackage,
257
323
  packageName: string,
258
324
  eject: boolean,
325
+ requestedLocator?: string,
259
326
  ): Promise<ModuleCommandResult> {
260
327
  assertModuleNotGatedOff(resolver.getModulesConfigPath(), modulePackage.metadata.moduleId)
261
- const from = prepareRegistrationSource(modulePackage, resolver, packageName, eject)
262
-
263
- const registration = ensureModuleRegistration(
264
- resolver.getModulesConfigPath(),
328
+ const locators = getModuleLocators({ id: modulePackage.metadata.moduleId, from: packageName }, resolver)
329
+ if (requestedLocator && locators.length > 0 && !locators.includes(requestedLocator)) {
330
+ throw new Error(`Installed package "${packageName}" disagrees with inspected origin "${requestedLocator}". It is installed but not enabled. Restore the binding and lockfile through your package manager before retrying.`)
331
+ }
332
+ const locator = requestedLocator ?? locators[0]
333
+ assertNoModuleCollision(resolver, modulePackage.metadata.moduleId, packageName, locator)
334
+ assertRequiredModulesEnabled(modulePackage, resolver)
335
+ const existing = resolver.loadEnabledModules().find((entry) => entry.id === modulePackage.metadata.moduleId)
336
+ const from = eject ? '@app' : packageName
337
+ const provenance = {
338
+ ...(locator ? { locator } : {}),
339
+ ...(eject || modulePackage.packageName !== packageName ? { packageName: modulePackage.packageName } : {}),
340
+ }
341
+ const modulesPath = resolver.getModulesConfigPath()
342
+ const modulesSource = fs.readFileSync(modulesPath, 'utf8')
343
+ const registration = existing && eject
344
+ ? { changed: true, source: prepareModuleRegistrationSource(modulesSource, modulePackage.metadata.moduleId, from, provenance) }
345
+ : prepareModuleRegistration(
346
+ modulesSource,
265
347
  {
266
348
  id: modulePackage.metadata.moduleId,
267
349
  from,
350
+ ...(locator || eject || modulePackage.packageName !== packageName ? provenance : {}),
268
351
  },
269
352
  )
270
353
 
354
+ prepareRegistrationSource(modulePackage, resolver, packageName, eject)
271
355
  if (registration.changed) {
356
+ fs.writeFileSync(modulesPath, registration.source)
272
357
  await runGeneratorsWithRegistrationNotice(resolver, modulePackage.metadata.moduleId)
273
358
  }
274
359
 
@@ -287,24 +372,81 @@ export async function addOfficialModule(
287
372
  moduleId?: string,
288
373
  allowThirdParty = false,
289
374
  ): Promise<ModuleCommandResult> {
290
- const safePackageSpec = assertSupportedPackageSpec(packageSpec, allowThirdParty)
291
- const packageName = parsePackageNameFromSpec(safePackageSpec)
292
- assertPackageName(packageName)
293
-
375
+ const input = packageSpec.trim()
376
+ getEnabledModuleIdentities(resolver)
377
+ const isGithub = !input.startsWith('@') && !input.startsWith('npm:') && (
378
+ input.includes('/') && !input.includes('@file:') || /^(?:github:|git@|(?:git\+)?ssh:)/i.test(input)
379
+ )
380
+ let safePackageSpec: string
381
+ let packageName: string
382
+ let target: InspectedModuleTarget
383
+ if (isGithub) {
384
+ if (!allowThirdParty) {
385
+ throw new Error('GitHub repository installs require --allow-third-party, independently of the declared npm package scope.')
386
+ }
387
+ target = resolveGithubModuleTarget(input, resolver.getAppDir(), { moduleId, enabledModules: resolver.loadEnabledModules() })
388
+ assertThirdPartyAllowed(target.packageName, allowThirdParty)
389
+ const selectedModuleId = moduleId ?? (target.moduleIds.length === 1 ? target.moduleIds[0] : undefined)
390
+ if (!selectedModuleId || !target.moduleIds.includes(selectedModuleId)) {
391
+ throw new Error(`Select a module with --module <moduleId>. Available in ${target.locator ?? target.packageName}: ${target.moduleIds.join(', ')}. No package was installed.`)
392
+ }
393
+ assertNoModuleCollision(resolver, selectedModuleId, target.packageName, target.locator)
394
+ safePackageSpec = target.packageSpec
395
+ packageName = target.packageName
396
+ } else {
397
+ safePackageSpec = assertSupportedPackageSpec(input, allowThirdParty)
398
+ const parsedName = parsePackageNameFromSpec(safePackageSpec)
399
+ assertPackageName(parsedName)
400
+ packageName = parsedName
401
+ if (moduleId) assertNoModuleCollision(resolver, moduleId, packageName, safePackageSpec.startsWith(`${packageName}@file:`) ? undefined : `npm:${packageName}`)
402
+ const requiredModuleIds = resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === packageName).map((entry) => entry.id)
403
+ target = inspectNpmModuleTarget(safePackageSpec, packageName, resolver.getAppDir(), [...new Set([...(moduleId ? [moduleId] : []), ...requiredModuleIds])])
404
+ }
405
+ const selectedModuleId = moduleId ?? (target.moduleIds.length === 1 ? target.moduleIds[0] : undefined)
406
+ if (!selectedModuleId || !target.moduleIds.includes(selectedModuleId)) {
407
+ throw new Error(`Select a module with --module <moduleId>. Available in ${target.locator ?? target.packageName}: ${target.moduleIds.join(', ')}. No package was installed.`)
408
+ }
409
+ assertNoModuleCollision(resolver, selectedModuleId, packageName, target.locator)
410
+ const existing = resolver.loadEnabledModules().find((entry) => entry.id === selectedModuleId)
411
+ if (!eject && existing && !target.locator
412
+ && target.packageSpec.startsWith(`${packageName}@file:`)
413
+ && getModuleLocators(existing, resolver).length === 0
414
+ && isDeclaredAppDependency(resolver.getAppDir(), packageName, target.packageSpec.slice(packageName.length + 1))) {
415
+ return enableOfficialModule(resolver, packageName, selectedModuleId, false, allowThirdParty)
416
+ }
417
+ assertBindingPreserved(resolver, target)
418
+ const modulesSource = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
419
+ const from = eject ? '@app' : packageName
420
+ if (existing && eject) prepareModuleRegistrationSource(modulesSource, selectedModuleId, from, { locator: target.locator, packageName })
421
+ else prepareModuleRegistration(modulesSource, { id: selectedModuleId, from, locator: target.locator })
422
+ if (eject && fs.existsSync(path.join(resolver.getAppDir(), 'src', 'modules', selectedModuleId))) {
423
+ throw new Error(`Destination directory already exists for "${selectedModuleId}". Preserve local source and resolve the conflict manually. No package was installed.`)
424
+ }
425
+ safePackageSpec = target.packageSpec
426
+ assertModuleNotGatedOff(resolver.getModulesConfigPath(), selectedModuleId)
294
427
  const wasDeclaredBefore = isDeclaredAppDependency(resolver.getAppDir(), packageName)
295
428
  const modulesConfigBefore = readFileIfExists(resolver.getModulesConfigPath())
296
-
297
429
  await installPackageSpec(resolver, safePackageSpec)
298
430
 
299
431
  try {
300
- const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, moduleId)
301
- return await registerResolvedOfficialModule(resolver, modulePackage, packageName, eject)
432
+ const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, selectedModuleId)
433
+ if (modulePackage.packageName !== packageName) throw new Error('Installed manifest differs from inspected package name.')
434
+ if (target.version && modulePackage.packageJson.version !== target.version) {
435
+ throw new Error(`Installed version "${modulePackage.packageJson.version ?? 'unknown'}" differs from inspected version "${target.version}". No registration or source edits were made.`)
436
+ }
437
+ for (const entry of resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === packageName)) {
438
+ resolveInstalledOfficialModulePackage(resolver, packageName, entry.id)
439
+ }
440
+ return await registerResolvedOfficialModule(resolver, modulePackage, packageName, eject, target.locator)
302
441
  } catch (error) {
303
442
  const moduleWasRegistered = readFileIfExists(resolver.getModulesConfigPath()) !== modulesConfigBefore
304
- if (wasDeclaredBefore || moduleWasRegistered || isGatedOffError(error)) {
305
- throw error
306
- }
307
- throw withLeftoverPackageHint(error, resolver, packageName)
443
+ if (wasDeclaredBefore || moduleWasRegistered || isGatedOffError(error)) throw error
444
+ const message = error instanceof Error ? error.message : String(error)
445
+ throw withLeftoverPackageHint(
446
+ new Error(`Package "${packageName}" is installed; enablement did not complete. Review src/modules.ts and restore the package binding/lockfile through the package manager if needed. ${message}`),
447
+ resolver,
448
+ packageName,
449
+ )
308
450
  }
309
451
  }
310
452
 
@@ -315,8 +457,14 @@ export async function enableOfficialModule(
315
457
  eject = false,
316
458
  allowThirdParty = false,
317
459
  ): Promise<ModuleCommandResult> {
318
- assertThirdPartyAllowed(packageName, allowThirdParty)
319
-
460
+ if (!SAFE_PACKAGE_SPEC_PATTERN.test(packageName) || parsePackageNameFromSpec(packageName) !== packageName) {
461
+ throw new Error('Unsupported installed package binding. Use the installed npm package name or alias, without a locator, path or version selector.')
462
+ }
463
+ getEnabledModuleIdentities(resolver)
320
464
  const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, moduleId)
465
+ assertThirdPartyAllowed(modulePackage.packageName, allowThirdParty)
466
+ if (!allowThirdParty && getModuleLocators({ id: modulePackage.metadata.moduleId, from: packageName }, resolver).some((locator) => locator.startsWith('github:'))) {
467
+ throw new Error('GitHub repository installs require --allow-third-party, independently of the declared npm package scope.')
468
+ }
321
469
  return registerResolvedOfficialModule(resolver, modulePackage, packageName, eject)
322
470
  }