@open-mercato/cli 0.6.6-develop.6383.1.27fcc83455 → 0.6.6-develop.6385.1.9a81faa5f0

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 (35) hide show
  1. package/.turbo/turbo-build.log +11 -0
  2. package/build.mjs +70 -2
  3. package/dist/agentic/guides/core.auth.md +3 -0
  4. package/dist/agentic/guides/core.catalog.md +3 -0
  5. package/dist/agentic/guides/core.currencies.md +3 -0
  6. package/dist/agentic/guides/core.customer_accounts.md +3 -0
  7. package/dist/agentic/guides/core.customers.md +3 -0
  8. package/dist/agentic/guides/core.data_sync.md +3 -0
  9. package/dist/agentic/guides/core.integrations.md +3 -0
  10. package/dist/agentic/guides/core.sales.md +3 -0
  11. package/dist/agentic/guides/core.workflows.md +3 -0
  12. package/dist/agentic/guides/module-facts.json +2073 -0
  13. package/dist/agentic/guides/modules/auth.md +64 -0
  14. package/dist/agentic/guides/modules/catalog.md +70 -0
  15. package/dist/agentic/guides/modules/currencies.md +50 -0
  16. package/dist/agentic/guides/modules/customer_accounts.md +76 -0
  17. package/dist/agentic/guides/modules/customers.md +115 -0
  18. package/dist/agentic/guides/modules/data_sync.md +49 -0
  19. package/dist/agentic/guides/modules/integrations.md +49 -0
  20. package/dist/agentic/guides/modules/sales.md +109 -0
  21. package/dist/agentic/guides/modules/workflows.md +73 -0
  22. package/dist/lib/__integration__/TC-INT-008.spec.js +29 -1
  23. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  24. package/dist/lib/agentic-setup.js +87 -0
  25. package/dist/lib/agentic-setup.js.map +2 -2
  26. package/dist/lib/worker-job-handler.js +13 -1
  27. package/dist/lib/worker-job-handler.js.map +2 -2
  28. package/dist/mercato.js +4 -1
  29. package/dist/mercato.js.map +2 -2
  30. package/package.json +5 -5
  31. package/src/lib/__integration__/TC-INT-008.spec.ts +41 -1
  32. package/src/lib/__tests__/worker-job-handler.test.ts +28 -0
  33. package/src/lib/agentic-setup.ts +129 -0
  34. package/src/lib/worker-job-handler.ts +13 -1
  35. package/src/mercato.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.6-develop.6383.1.27fcc83455",
3
+ "version": "0.6.6-develop.6385.1.9a81faa5f0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.4",
61
61
  "@mikro-orm/migrations": "^7.1.4",
62
62
  "@mikro-orm/postgresql": "^7.1.4",
63
- "@open-mercato/queue": "0.6.6-develop.6383.1.27fcc83455",
64
- "@open-mercato/shared": "0.6.6-develop.6383.1.27fcc83455",
63
+ "@open-mercato/queue": "0.6.6-develop.6385.1.9a81faa5f0",
64
+ "@open-mercato/shared": "0.6.6-develop.6385.1.9a81faa5f0",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -71,10 +71,10 @@
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "peerDependencies": {
74
- "@open-mercato/shared": "0.6.6-develop.6383.1.27fcc83455"
74
+ "@open-mercato/shared": "0.6.6-develop.6385.1.9a81faa5f0"
75
75
  },
76
76
  "devDependencies": {
77
- "@open-mercato/shared": "0.6.6-develop.6383.1.27fcc83455",
77
+ "@open-mercato/shared": "0.6.6-develop.6385.1.9a81faa5f0",
78
78
  "@types/jest": "^30.0.0",
79
79
  "jest": "^30.4.2",
80
80
  "ts-jest": "^29.4.11"
@@ -4,6 +4,7 @@ import fs from 'node:fs'
4
4
  import os from 'node:os'
5
5
  import path from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
+ import { MODULE_FACTS_ALLOWLIST } from '../generators/module-facts'
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
10
  const repoRoot = path.resolve(__dirname, '..', '..', '..', '..', '..')
@@ -15,6 +16,11 @@ const standaloneTemplatePackageJsonPath = path.join(repoRoot, 'packages', 'creat
15
16
  const agenticRoot = path.join(repoRoot, 'packages', 'create-app', 'agentic')
16
17
  const packagesRoot = path.join(repoRoot, 'packages')
17
18
 
19
+ // Modules the standalone fixture enables in src/modules.ts. Both are on the
20
+ // fact-sheet allowlist, so agentic:init must ship exactly their fact-sheets
21
+ // (enabled ∩ allowlist — spec D6) and list them in the AGENTS.md marker block.
22
+ const FIXTURE_ENABLED_MODULES = ['customers', 'sales']
23
+
18
24
  function normalizePath(value: string): string {
19
25
  return value.split(path.sep).join('/')
20
26
  }
@@ -57,7 +63,8 @@ function createStandaloneFixture(rootDir: string): string {
57
63
  2,
58
64
  ),
59
65
  )
60
- writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
66
+ const moduleEntries = FIXTURE_ENABLED_MODULES.map((moduleId) => ` { id: '${moduleId}' },`).join('\n')
67
+ writeFile(path.join(appDir, 'src', 'modules.ts'), `export const enabledModules = [\n${moduleEntries}\n]\n`)
61
68
  return appDir
62
69
  }
63
70
 
@@ -167,6 +174,17 @@ function readPlaywrightConfigPathFromCliRunner(): string {
167
174
  function expectedGuideOutputNames(): string[] {
168
175
  const collected = new Set<string>()
169
176
 
177
+ // Static conceptual guides checked into create-app (e.g. module-system.md) are
178
+ // bundled into dist/agentic/guides by the CLI build and copied wholesale.
179
+ const staticGuidesRoot = path.join(agenticRoot, 'guides')
180
+ if (fs.existsSync(staticGuidesRoot)) {
181
+ for (const entry of fs.readdirSync(staticGuidesRoot, { withFileTypes: true })) {
182
+ if (entry.isFile() && entry.name.endsWith('.md')) {
183
+ collected.add(entry.name)
184
+ }
185
+ }
186
+ }
187
+
170
188
  for (const packageName of fs.readdirSync(packagesRoot)) {
171
189
  const packageGuide = path.join(packagesRoot, packageName, 'agentic', 'standalone-guide.md')
172
190
  if (fs.existsSync(packageGuide)) {
@@ -186,6 +204,21 @@ function expectedGuideOutputNames(): string[] {
186
204
  }
187
205
  }
188
206
 
207
+ // Generated fact-sheet artifacts (spec 2026-06-27-ts-morph-module-fact-sheets):
208
+ // the module-facts.json sidecar is copied as-is, fact-sheets are filtered to the
209
+ // fixture's enabled modules, and every allowlisted module whose hand-written
210
+ // core.<module>.md guide no longer exists gets a legacy redirect stub.
211
+ collected.add('module-facts.json')
212
+ for (const moduleId of FIXTURE_ENABLED_MODULES) {
213
+ collected.add(normalizePath(path.join('modules', `${moduleId}.md`)))
214
+ }
215
+ for (const moduleId of MODULE_FACTS_ALLOWLIST) {
216
+ const legacyGuideSource = path.join(packagesRoot, 'core', 'src', 'modules', moduleId, 'agentic', 'standalone-guide.md')
217
+ if (!fs.existsSync(legacyGuideSource)) {
218
+ collected.add(`core.${moduleId}.md`)
219
+ }
220
+ }
221
+
189
222
  return Array.from(collected).sort()
190
223
  }
191
224
 
@@ -230,6 +263,13 @@ test.describe('TC-INT-008: CLI agentic init mirrors standalone scaffolding asset
230
263
  expect(agentsSource).toContain('<!-- CODEX_ENFORCEMENT_RULES_START -->')
231
264
  expect(agentsSource).toContain('.ai/guides/core.md')
232
265
 
266
+ // The Module-Specific Guides marker block lists exactly the enabled modules'
267
+ // fact-sheets (enabled ∩ allowlist), not the full bundled set.
268
+ for (const moduleId of FIXTURE_ENABLED_MODULES) {
269
+ expect(agentsSource).toContain(`.ai/guides/modules/${moduleId}.md`)
270
+ }
271
+ expect(agentsSource).not.toContain('.ai/guides/modules/auth.md')
272
+
233
273
  const specsReadmeSource = fs.readFileSync(path.join(appDir, '.ai', 'specs', 'README.md'), 'utf8')
234
274
  expect(specsReadmeSource).toContain('sample-store')
235
275
 
@@ -1,4 +1,8 @@
1
1
  import type { ModuleWorker } from '@open-mercato/shared/modules/registry'
2
+ import {
3
+ getModuleResourceUsageReport,
4
+ resetModuleResourceUsage,
5
+ } from '@open-mercato/shared/lib/modules/resource-usage'
2
6
  import { createPerJobWorkerHandler, type WorkerJobContainer } from '../worker-job-handler'
3
7
 
4
8
  type FakeContainer = WorkerJobContainer & {
@@ -32,6 +36,14 @@ function makeWorker(id: string, handler: ModuleWorker['handler']): ModuleWorker
32
36
  const baseCtx = { jobId: 'job-1', attemptNumber: 1, queueName: 'test' }
33
37
 
34
38
  describe('createPerJobWorkerHandler', () => {
39
+ beforeEach(() => {
40
+ resetModuleResourceUsage()
41
+ })
42
+
43
+ afterEach(() => {
44
+ resetModuleResourceUsage()
45
+ })
46
+
35
47
  it('creates a fresh container for every job invocation', async () => {
36
48
  const { factory } = makeContainerFactory()
37
49
  const worker = makeWorker('w', jest.fn())
@@ -132,4 +144,20 @@ describe('createPerJobWorkerHandler', () => {
132
144
  await expect(handler({ id: 'a' }, baseCtx)).rejects.toBe(boom)
133
145
  expect(containers[0].em.clear).toHaveBeenCalledTimes(1)
134
146
  })
147
+
148
+ it('attributes worker execution to the owning module', async () => {
149
+ const { factory } = makeContainerFactory()
150
+ const handler = createPerJobWorkerHandler(
151
+ [makeWorker('customers:workers:bulk-update', jest.fn())],
152
+ factory,
153
+ )
154
+
155
+ await handler({ id: 'a' }, baseCtx)
156
+
157
+ const report = getModuleResourceUsageReport()
158
+ expect(report.modules).toHaveLength(1)
159
+ expect(report.modules[0].moduleId).toBe('customers')
160
+ expect(report.modules[0].surfaces[0].surface).toBe('worker')
161
+ expect(report.modules[0].topOperations[0].operation).toBe('customers:workers:bulk-update')
162
+ })
135
163
  })
@@ -9,6 +9,7 @@
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, lstatSync, unlinkSync, readdirSync } from 'node:fs'
10
10
  import { join, dirname, basename } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
+ import { Project, SyntaxKind } from 'ts-morph'
12
13
 
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url))
14
15
  // In the built output (dist/lib/agentic-setup.js), __dirname is dist/lib/.
@@ -52,14 +53,125 @@ function copyFile(srcDir: string, srcRelative: string, destPath: string): void {
52
53
  copyFileSync(srcPath, destPath)
53
54
  }
54
55
 
56
+ // ─── Module fact-sheet selection (mirrors packages/create-app/src/setup/tools/shared.ts) ──
57
+
58
+ // AST-parse the static `enabledModules` array literal in the app's src/modules.ts
59
+ // and collect each entry's `id`. Only the static literal is read (conditional
60
+ // .push()/spread entries are intentionally not seen — see spec D6).
61
+ function readEnabledModuleIds(modulesPath: string): string[] {
62
+ if (!existsSync(modulesPath)) return []
63
+ try {
64
+ const project = new Project({ useInMemoryFileSystem: true })
65
+ const sourceFile = project.createSourceFile('modules.ts', readFileSync(modulesPath, 'utf-8'))
66
+ const declaration = sourceFile.getVariableDeclaration('enabledModules')
67
+ const arrayLiteral = declaration?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
68
+ if (!arrayLiteral) return []
69
+ const ids: string[] = []
70
+ for (const element of arrayLiteral.getElements()) {
71
+ const objectLiteral = element.asKind(SyntaxKind.ObjectLiteralExpression)
72
+ if (!objectLiteral) continue
73
+ const idProperty = objectLiteral.getProperty('id')?.asKind(SyntaxKind.PropertyAssignment)
74
+ const idValue = idProperty?.getInitializerIfKind(SyntaxKind.StringLiteral)?.getLiteralValue()
75
+ if (idValue) ids.push(idValue)
76
+ }
77
+ return ids
78
+ } catch {
79
+ return []
80
+ }
81
+ }
82
+
83
+ // Resolve which per-module fact-sheets to ship: the intersection of the bundled
84
+ // fact-sheets (the D5 allowlist, materialized by build.mjs) with the app's enabled
85
+ // modules. Falls back to the full bundled set when the enabled set cannot be read
86
+ // (R5 — degraded, never empty).
87
+ function selectModuleFactSheets(targetDir: string, modulesSubdir: string): string[] {
88
+ const available = existsSync(modulesSubdir)
89
+ ? readdirSync(modulesSubdir)
90
+ .filter((file) => file.endsWith('.md'))
91
+ .map((file) => file.replace(/\.md$/, ''))
92
+ : []
93
+ if (available.length === 0) return []
94
+ const enabled = new Set(readEnabledModuleIds(join(targetDir, 'src', 'modules.ts')))
95
+ const selected = available.filter((moduleId) => enabled.has(moduleId))
96
+ return selected.length > 0 ? selected : available
97
+ }
98
+
99
+ const MODULE_GUIDES_START = '<!-- om:module-guides:start -->'
100
+ const MODULE_GUIDES_END = '<!-- om:module-guides:end -->'
101
+
102
+ // Read each module's guide label from the bundled `module-facts.json` (emitted by
103
+ // build.mjs from the generator's extraction of each module's own `metadata`). The
104
+ // label falls back description → title → generic, so the CLI never re-declares
105
+ // specific module names or descriptions. A missing/malformed sidecar degrades to an
106
+ // empty map (generic labels), never a throw.
107
+ function readModuleGuideLabels(guidesDir: string): Record<string, string> {
108
+ const factsPath = join(guidesDir, 'module-facts.json')
109
+ if (!existsSync(factsPath)) return {}
110
+ try {
111
+ const parsed = JSON.parse(readFileSync(factsPath, 'utf-8')) as Record<
112
+ string,
113
+ { description?: unknown; title?: unknown }
114
+ >
115
+ const labels: Record<string, string> = {}
116
+ for (const [moduleId, entry] of Object.entries(parsed)) {
117
+ const label =
118
+ (entry && typeof entry.description === 'string' && entry.description) ||
119
+ (entry && typeof entry.title === 'string' && entry.title) ||
120
+ ''
121
+ if (label) labels[moduleId] = label
122
+ }
123
+ return labels
124
+ } catch {
125
+ return {}
126
+ }
127
+ }
128
+
129
+ function renderModuleGuidesBlock(selected: string[], labels: Record<string, string>): string {
130
+ if (selected.length === 0) return '_No module fact-sheets are bundled for this app._'
131
+ const rows = selected.map((moduleId) => {
132
+ const label = labels[moduleId] ?? `Use the ${moduleId} module`
133
+ return `| ${label} | \`.ai/guides/modules/${moduleId}.md\` |`
134
+ })
135
+ return ['| Task | Load |', '|---|---|', ...rows].join('\n')
136
+ }
137
+
138
+ // Regenerate the marker-delimited Module-Specific Guides block in the written
139
+ // AGENTS.md from the selected module set. Replaces strictly between the markers so
140
+ // surrounding prose is untouched and repeat runs are idempotent.
141
+ function injectModuleGuides(
142
+ agentsMdPath: string,
143
+ selected: string[],
144
+ labels: Record<string, string> = {},
145
+ ): void {
146
+ if (!existsSync(agentsMdPath)) return
147
+ const content = readFileSync(agentsMdPath, 'utf-8')
148
+ const startIndex = content.indexOf(MODULE_GUIDES_START)
149
+ const endIndex = content.indexOf(MODULE_GUIDES_END)
150
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
151
+ console.warn(
152
+ `[agentic] Module-Specific Guides markers (${MODULE_GUIDES_START} … ${MODULE_GUIDES_END}) not found in ${agentsMdPath}; the per-module guide list was not generated.`,
153
+ )
154
+ return
155
+ }
156
+ const before = content.slice(0, startIndex + MODULE_GUIDES_START.length)
157
+ const after = content.slice(endIndex)
158
+ const next = `${before}\n${renderModuleGuidesBlock(selected, labels)}\n${after}`
159
+ if (next !== content) writeFileSync(agentsMdPath, next)
160
+ }
161
+
55
162
  // ─── Generators ──────────────────────────────────────────────────────────
56
163
 
57
164
  function generateShared(config: AgenticConfig): void {
58
165
  const { targetDir } = config
59
166
  const srcDir = join(AGENTIC_DIR, 'shared')
60
167
 
168
+ // Resolve which per-module fact-sheets this app gets (enabled ∩ bundled allowlist).
169
+ const selectedModules = selectModuleFactSheets(targetDir, join(GUIDES_DIR, 'modules'))
170
+ const moduleGuideLabels = readModuleGuideLabels(GUIDES_DIR)
171
+
61
172
  // AGENTS.md
62
173
  writeTemplate(srcDir, 'AGENTS.md.template', join(targetDir, 'AGENTS.md'), config)
174
+ injectModuleGuides(join(targetDir, 'AGENTS.md'), selectedModules, moduleGuideLabels)
63
175
 
64
176
  // .ai/ structure
65
177
  writeTemplate(srcDir, 'ai/specs/README.md', join(targetDir, '.ai', 'specs', 'README.md'), config)
@@ -244,6 +356,9 @@ function generateShared(config: AgenticConfig): void {
244
356
 
245
357
  copyFile(srcDir, 'ai/qa/tests/playwright.config.ts', join(targetDir, '.ai', 'qa', 'tests', 'playwright.config.ts'))
246
358
 
359
+ // Package & conceptual guides are copied wholesale (framework-wide). Per-module
360
+ // fact-sheets (.ai/guides/modules/<module>.md) are filtered to the app's enabled
361
+ // module set; the combined module-facts.json sidecar is copied as-is.
247
362
  if (existsSync(GUIDES_DIR)) {
248
363
  const guidesDestDir = join(targetDir, '.ai', 'guides')
249
364
  for (const file of readdirSync(GUIDES_DIR)) {
@@ -253,6 +368,20 @@ function generateShared(config: AgenticConfig): void {
253
368
  ensureDir(destPath)
254
369
  copyFileSync(srcPath, destPath)
255
370
  }
371
+
372
+ const moduleFactsPath = join(GUIDES_DIR, 'module-facts.json')
373
+ if (existsSync(moduleFactsPath)) {
374
+ const destPath = join(guidesDestDir, 'module-facts.json')
375
+ ensureDir(destPath)
376
+ copyFileSync(moduleFactsPath, destPath)
377
+ }
378
+
379
+ const modulesSubdir = join(GUIDES_DIR, 'modules')
380
+ for (const moduleId of selectedModules) {
381
+ const destPath = join(guidesDestDir, 'modules', `${moduleId}.md`)
382
+ ensureDir(destPath)
383
+ copyFileSync(join(modulesSubdir, `${moduleId}.md`), destPath)
384
+ }
256
385
  }
257
386
  }
258
387
 
@@ -1,5 +1,9 @@
1
1
  import type { JobContext, JobHandler } from '@open-mercato/queue'
2
2
  import type { ModuleWorker } from '@open-mercato/shared/modules/registry'
3
+ import {
4
+ inferModuleIdFromResourceId,
5
+ withModuleResourceUsage,
6
+ } from '@open-mercato/shared/lib/modules/resource-usage'
3
7
 
4
8
  export type WorkerJobContainer = {
5
9
  resolve: <T = unknown>(name: string) => T
@@ -30,7 +34,15 @@ export function createPerJobWorkerHandler(
30
34
  const container = await createContainer()
31
35
  try {
32
36
  for (const worker of workers) {
33
- await worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) })
37
+ await withModuleResourceUsage(
38
+ {
39
+ moduleId: worker.moduleId ?? inferModuleIdFromResourceId(worker.id),
40
+ surface: 'worker',
41
+ operation: worker.id,
42
+ resourceId: worker.id,
43
+ },
44
+ () => worker.handler(job, { ...ctx, resolve: container.resolve.bind(container) }),
45
+ )
34
46
  }
35
47
  } finally {
36
48
  try {
package/src/mercato.ts CHANGED
@@ -66,7 +66,10 @@ function getRegisteredCliWorkers(modules: Module[] = getCliModules()): ModuleWor
66
66
  const allWorkers: ModuleWorker[] = []
67
67
  for (const mod of modules) {
68
68
  if (mod.workers) {
69
- allWorkers.push(...mod.workers)
69
+ allWorkers.push(...mod.workers.map((worker) => ({
70
+ ...worker,
71
+ moduleId: worker.moduleId ?? mod.id,
72
+ })))
70
73
  }
71
74
  }
72
75
  return allWorkers