@open-mercato/cli 0.6.6-develop.6382.1.4b9b9091ab → 0.6.6-develop.6384.1.f06fc0b42c

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.
@@ -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