openprompt-lang 0.3.0 → 0.4.0

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 (90) hide show
  1. package/README.md +483 -32
  2. package/bin/cli.js +232 -41
  3. package/bin/create.js +135 -0
  4. package/bin/lint.js +20 -21
  5. package/docs/COMMANDS.md +200 -127
  6. package/docs/COMMITS/INDEX.md +1 -0
  7. package/docs/PROMPT_AI_CONTEXT.md +99 -0
  8. package/docs/langs/dotnet.md +36 -0
  9. package/docs/langs/java-spring.md +45 -0
  10. package/docs/langs/python-fastapi.md +35 -0
  11. package/docs/langs/unity.md +30 -0
  12. package/docs/langs/vue-nuxt.md +36 -0
  13. package/package.json +31 -3
  14. package/scaffolds/.cursorrules +6 -0
  15. package/scaffolds/AGENTS.md +27 -0
  16. package/scaffolds/Dockerfile +11 -0
  17. package/scaffolds/capacitor.config.ts +17 -0
  18. package/scaffolds/netlify.toml +8 -0
  19. package/scaffolds/prompt-lang.json +15 -0
  20. package/scaffolds/railway.json +12 -0
  21. package/scaffolds/tailwind.config.js +8 -0
  22. package/scaffolds/tauri.conf.json +26 -0
  23. package/schemas/language-module.json +116 -0
  24. package/schemas/prompt-lang.json +38 -3
  25. package/schemas/structures.json +145 -0
  26. package/src/ai/prompt-builder.js +184 -0
  27. package/src/ai/providers.js +247 -0
  28. package/src/annotations/registry.js +39 -0
  29. package/src/annotations/tags.json +24 -0
  30. package/src/commands/ai-gen.js +161 -0
  31. package/src/commands/component.js +242 -212
  32. package/src/commands/context.js +184 -109
  33. package/src/commands/extract.js +242 -0
  34. package/src/commands/figma.js +15 -15
  35. package/src/commands/init.js +197 -93
  36. package/src/commands/integrate.js +406 -0
  37. package/src/commands/lang.js +148 -0
  38. package/src/commands/qa-gen.js +139 -0
  39. package/src/commands/scaffold.js +127 -0
  40. package/src/commands/suggest.js +24 -14
  41. package/src/commands/teach.js +110 -0
  42. package/src/commands/validate.js +143 -83
  43. package/src/commands/wizard.js +456 -0
  44. package/src/generators/figma-prompt.js +20 -12
  45. package/src/language-service/plugin.cjs +94 -0
  46. package/src/language-service/plugin.d.ts +6 -0
  47. package/src/mcp-server.js +605 -0
  48. package/src/templates/langs/react/INDEX.json +262 -0
  49. package/src/templates/langs/react/MODULE.json +166 -0
  50. package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
  51. package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
  52. package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
  53. package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
  54. package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
  55. package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
  56. package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
  57. package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
  58. package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
  59. package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
  60. package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
  61. package/src/templates/langs/vue/INDEX.json +246 -0
  62. package/src/templates/langs/vue/MODULE.json +105 -0
  63. package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
  64. package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
  65. package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
  66. package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
  67. package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
  68. package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
  69. package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
  70. package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
  71. package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
  72. package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
  73. package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
  74. package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
  75. package/src/templates/scripts/log-actividad.sh +32 -0
  76. package/src/templates/scripts/log-commit.sh +35 -0
  77. package/src/templates/scripts/log-error.sh +45 -0
  78. package/src/templates/scripts/validate.sh +23 -0
  79. package/src/ts-transformer/index.cjs +86 -0
  80. package/src/utils/ai.js +35 -53
  81. package/src/utils/annotations.js +260 -214
  82. package/src/utils/config.js +61 -13
  83. package/src/utils/error-learner.js +203 -0
  84. package/src/utils/file-utils.js +119 -0
  85. package/src/utils/language-loader.js +167 -0
  86. package/src/utils/template-utils.js +45 -0
  87. package/src/vite-plugin/index.js +54 -0
  88. package/vscode-extension/package.json +23 -2
  89. package/vscode-extension/snippets/promptlang.json +1 -3
  90. package/vscode-extension/syntaxes/annotations-code.tmGrammar.json +15 -0
@@ -0,0 +1,406 @@
1
+ import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "fs"
2
+ import { join } from "path"
3
+ import chalk from "chalk"
4
+ import inquirer from "inquirer"
5
+ import { getLanguageIndex, getStructure } from "../utils/language-loader.js"
6
+
7
+ const CURSOR_RULES_BASE = `// @generated by openPrompt-Lang integrate
8
+ // This file configures Cursor AI behavior for this project.
9
+
10
+ You are an expert TypeScript/React developer using openPrompt-Lang annotations.
11
+
12
+ ## Code Style
13
+ - Use named exports, not default exports
14
+ - Prefer arrow functions for components and hooks
15
+ - Use TypeScript strict mode
16
+ - No \`any\` types — use \`unknown\` + narrowing
17
+
18
+ ## Annotations
19
+ - Every file must start with \`// @use(@kind, @props, @limit, @contract, @deps, @test)\`
20
+ - Components: \`@kind(component)\` + \`@props({ ... })\`
21
+ - Hooks: \`@kind(hook)\` + \`@contract(in: ... -> out: ... @error: ...)\`
22
+ - Pages: \`@kind(page)\` + \`@compose(@subcomponent, @subcomponent)\`
23
+ - Services: \`@kind(service)\` + \`@contract(in: ... -> out: ...)\`
24
+
25
+ ## Limits
26
+ - Components: max 120 lines per file
27
+ - Hooks: max 80 lines per file
28
+ - Pages: max 200 lines per file
29
+ - Extract reusable logic when limits are exceeded
30
+
31
+ ## Learning
32
+ - When you encounter a bug, leave a comment with \`// @learn-error id=...\`
33
+ - Reference templates: run \`npx openPrompt-Lang teach <template-id>\`
34
+ - When in doubt, search the language module: \`npx openPrompt-Lang lang search <query>\`
35
+
36
+ ## Generated Files
37
+ - Files matching \`*.template.*\`, \`*.test.*\`, and \`node_modules/*\` are off-limits for editing
38
+ - Config files (\`prompt-lang.json\`, \`*.config.*\`) should not be modified by the AI
39
+ `
40
+
41
+ export async function integrate(options) {
42
+ const cwd = process.cwd()
43
+ console.log(chalk.cyan("\n🔌 openPrompt-Lang Editor Integration\n"))
44
+
45
+ // Detect existing config
46
+ const configPath = join(cwd, "prompt-lang.json")
47
+ let config = null
48
+ if (existsSync(configPath)) {
49
+ try {
50
+ config = JSON.parse(readFileSync(configPath, "utf-8"))
51
+ console.log(chalk.green(` ✅ Found prompt-lang.json`))
52
+ } catch {
53
+ console.log(chalk.yellow(` ⚠️ prompt-lang.json found but invalid — using defaults`))
54
+ }
55
+ } else {
56
+ console.log(chalk.yellow(" ⚠️ No prompt-lang.json found — using defaults"))
57
+ }
58
+
59
+ const langId = config?.lenguaje || options.lang || detectLanguageFromFiles(cwd)
60
+ const index = getLanguageIndex(langId)
61
+ const structure = getStructure(langId)
62
+
63
+ // Determine what to integrate
64
+ const targets = options.all
65
+ ? ["opencode", "cursor", "vscode"]
66
+ : await askIntegrationTargets(config, langId)
67
+
68
+ const results = []
69
+
70
+ if (targets.includes("opencode")) {
71
+ const r = await setupOpencode(cwd, config, langId, index)
72
+ results.push(r)
73
+ }
74
+
75
+ if (targets.includes("cursor")) {
76
+ const r = await setupCursor(cwd, config, langId, index)
77
+ results.push(r)
78
+ }
79
+
80
+ if (targets.includes("vscode")) {
81
+ const r = await setupVscode(cwd, config, langId)
82
+ results.push(r)
83
+ }
84
+
85
+ console.log(chalk.cyan("\n📋 Integration summary:\n"))
86
+ for (const r of results) {
87
+ const icon = r.status === "created" ? "✅" : r.status === "updated" ? "🔄" : "⏭️"
88
+ console.log(` ${icon} ${r.file} ${r.status !== "skipped" ? "" : `(${r.reason})`}`)
89
+ }
90
+ console.log("")
91
+ }
92
+
93
+ // ─── Opencode integration ────────────────────────────────────────────────────
94
+
95
+ async function setupOpencode(cwd, config, langId, index) {
96
+ const opencodeDir = join(cwd, ".opencode")
97
+ const configFile = join(opencodeDir, "opencode.json")
98
+ const agentDir = join(opencodeDir, "agent")
99
+
100
+ mkdirSync(opencodeDir, { recursive: true })
101
+ mkdirSync(agentDir, { recursive: true })
102
+
103
+ const opencodeConfig = {
104
+ $schema: "https://opencode.ai/config.json",
105
+ mcp: {
106
+ openPrompt: {
107
+ type: "local",
108
+ command: ["npx", "openprompt-lang", "mcp"],
109
+ enabled: true,
110
+ },
111
+ },
112
+ skills: {
113
+ paths: [".opencode/skills"],
114
+ },
115
+ default_agent: "openPrompt",
116
+ instructions: ["AGENTS.md"],
117
+ }
118
+
119
+ const wasExisting = existsSync(configFile)
120
+ writeFileSync(configFile, JSON.stringify(opencodeConfig, null, 2), "utf-8")
121
+ writeFileSync(
122
+ join(agentDir, "openPrompt.md"),
123
+ generateOpenPromptAgentMd(config, langId, index),
124
+ "utf-8"
125
+ )
126
+
127
+ // Generate skill for auto-detection
128
+ const skillsDir = join(opencodeDir, "skills", "openPrompt")
129
+ mkdirSync(skillsDir, { recursive: true })
130
+ writeFileSync(join(skillsDir, "SKILL.md"), generateSkillMd(config, langId, index), "utf-8")
131
+
132
+ return { file: ".opencode/opencode.json", status: wasExisting ? "updated" : "created" }
133
+ }
134
+
135
+ function getOpencodeRules(config, langId, index) {
136
+ const rules = [
137
+ "Always validate annotations with `openPrompt-Lang validate` before committing",
138
+ "Never edit `*.template.*` files directly",
139
+ "When refactoring, run `openPrompt-Lang extract` first to check for reusable patterns",
140
+ ]
141
+
142
+ if (config?.requirements?.auth && config.requirements.auth !== "none") {
143
+ rules.push(
144
+ `Auth provider is ${config.requirements.auth} — use the template \`npx openPrompt-Lang teach hook-useAuth --lang ${langId}\``
145
+ )
146
+ }
147
+
148
+ if (config?.ui?.style) {
149
+ rules.push(`UI library is ${config.ui.style} — prefer components from this library`)
150
+ }
151
+
152
+ if (index?.errorsLearned?.length > 0) {
153
+ rules.push(
154
+ `Module has ${index.errorsLearned.length} learned errors — run \`npx openPrompt-Lang qa-gen\` before deployment`
155
+ )
156
+ }
157
+
158
+ return rules
159
+ }
160
+
161
+ // ─── Cursor integration ──────────────────────────────────────────────────────
162
+
163
+ async function setupCursor(cwd, config, langId, index) {
164
+ const cursorDir = join(cwd, ".cursor")
165
+ const rulesFile = join(cursorDir, "rules")
166
+
167
+ mkdirSync(cursorDir, { recursive: true })
168
+
169
+ const version = config?.version || "0.1.0"
170
+
171
+ let cursorConfig = {}
172
+ const cursorConfigFile = join(cursorDir, "config.json")
173
+ if (existsSync(cursorConfigFile)) {
174
+ try {
175
+ cursorConfig = JSON.parse(readFileSync(cursorConfigFile, "utf-8"))
176
+ } catch {}
177
+ }
178
+
179
+ cursorConfig = {
180
+ ...cursorConfig,
181
+ rules: CURSOR_RULES_BASE,
182
+ project: {
183
+ name: config?.name || cwd.split("/").pop(),
184
+ lenguaje: langId,
185
+ version,
186
+ },
187
+ }
188
+
189
+ writeFileSync(cursorConfigFile, JSON.stringify(cursorConfig, null, 2), "utf-8")
190
+ writeFileSync(rulesFile, CURSOR_RULES_BASE, "utf-8")
191
+
192
+ // Auto-generate .cursorrules at root
193
+ writeFileSync(join(cwd, ".cursorrules"), CURSOR_RULES_BASE, "utf-8")
194
+
195
+ return { file: ".cursor/ + .cursorrules", status: "created" }
196
+ }
197
+
198
+ // ─── VS Code integration ─────────────────────────────────────────────────────
199
+
200
+ async function setupVscode(cwd, config, langId) {
201
+ const vscodeDir = join(cwd, ".vscode")
202
+ mkdirSync(vscodeDir, { recursive: true })
203
+
204
+ const settings = {
205
+ "typescript.preferences.importModuleSpecifier": "relative",
206
+ "typescript.preferences.quoteStyle": "double",
207
+ "editor.formatOnSave": true,
208
+ "editor.codeActionsOnSave": {
209
+ "source.fixAll": "explicit",
210
+ "source.organizeImports": "explicit",
211
+ },
212
+ "files.exclude": {
213
+ "**/.opencode": true,
214
+ "**/tsconfig.tsbuildinfo": true,
215
+ },
216
+ "typescript.tsdk": "node_modules/typescript/lib",
217
+ }
218
+
219
+ const extensions = {
220
+ recommendations: [
221
+ "dbaeumer.vscode-eslint",
222
+ "bradlc.vscode-tailwindcss",
223
+ "esbenp.prettier-vscode",
224
+ ],
225
+ }
226
+
227
+ writeFileSync(join(vscodeDir, "settings.json"), JSON.stringify(settings, null, 2), "utf-8")
228
+ writeFileSync(join(vscodeDir, "extensions.json"), JSON.stringify(extensions, null, 2), "utf-8")
229
+
230
+ return { file: ".vscode/settings.json", status: "created" }
231
+ }
232
+
233
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
234
+
235
+ function generateOpenPromptAgentMd(config, langId, index) {
236
+ const templates = index?.templates || []
237
+
238
+ return `---
239
+ name: openPrompt
240
+ description: Expert in openPrompt-Lang annotations, conventions, and AI-assisted development. Use when working with @kind, @contract, @props, @limit annotations or running openprompt-lang CLI commands.
241
+ mode: primary
242
+ permission:
243
+ edit: allow
244
+ bash:
245
+ git *: allow
246
+ npx openprompt-lang *: allow
247
+ npm *: allow
248
+ "*": ask
249
+ ---
250
+
251
+ You are an expert in **openPrompt-Lang** — a framework of annotations for AI-assisted development.
252
+
253
+ ## Core Rules
254
+ 1. Every source file must declare annotations starting with \`@use()\` at the file top
255
+ 2. Validate before committing: run \`npx openprompt-lang validate\` to check annotation consistency
256
+ 3. Never edit \`*.template.*\` files directly — they are the language module's template library
257
+ 4. Use \`@learn-error\` when encountering bugs — document what you learn so future AI sessions benefit
258
+
259
+ ## Default Workflow (always follow this cycle)
260
+
261
+ When working on ANY task, follow this sequence using MCP tools:
262
+
263
+ \`\`\`
264
+ Step 1: analyze_project → understand structure and health
265
+ Step 2: validate → find annotation errors
266
+ Step 3: lint_file → debug specific file errors
267
+ Step 4: fix + @learn-error → document what you learned
268
+ Step 5: generate_tests → create regression tests
269
+ Step 6: validate → verify fix is clean
270
+ \`\`\`
271
+
272
+ ### Workflow: Debugging annotation errors
273
+ 1. Call \`analyze_project\` to see the big picture
274
+ 2. Call \`validate\` to find all errors
275
+ 3. For each error file, call \`lint_file\` for details
276
+ 4. Fix the issue
277
+ 5. Add \`@learn-error\` annotation documenting the fix
278
+ 6. Call \`generate_tests\` to create regression tests
279
+ 7. Call \`validate\` again to verify
280
+
281
+ ### Workflow: Implementing a new component
282
+ 1. Call \`search_templates\` with the component type (e.g., "button")
283
+ 2. Call \`teach_template\` with the template ID + \`showCode: true\`
284
+ 3. Generate the component following the pattern
285
+ 4. Add appropriate \`@kind\`, \`@contract\`, etc. annotations
286
+ 5. Call \`validate\` before committing
287
+
288
+ ### Workflow: Onboarding to a new project
289
+ 1. Call \`analyze_project\` for structure overview
290
+ 2. Call \`validate\` to check health
291
+ 3. Read \`prompt-lang.json\` and \`AGENTS.md\` from the file system
292
+ 4. Call \`search_templates\` for relevant patterns
293
+ 5. Call \`teach_template\` to learn key patterns
294
+
295
+ ### Workflow: Starting a new project
296
+ 1. Call \`scaffold_project\` to create structure
297
+ 2. Call \`search_templates\` for each component you need
298
+ 3. Call \`teach_template\` to learn each pattern
299
+ 4. Implement following the patterns
300
+ 5. Call \`validate\` before committing
301
+
302
+ ## Available templates
303
+ ${templates.map((t) => `- \`${t.id}\`: ${t.description}${t.hasTeachMe ? " (with @teachMe)" : ""}`).join("\n")}
304
+
305
+ ## Rules
306
+ ${getOpencodeRules(config, langId, index)
307
+ .map((r) => `- ${r}`)
308
+ .join("\n")}
309
+ `
310
+ }
311
+
312
+ function generateSkillMd(config, langId, index) {
313
+ const templates = index?.templates || []
314
+
315
+ return `---
316
+ name: openprompt-lang
317
+ description: |
318
+ Use ONLY when working with openPrompt-Lang annotations (@kind, @contract, @props, @limit, @use, @compose, @deps, @platform, @scope, @state, @forbidden, @pattern, @test, @meta, @learn-error, @goodPractice, @badPractice) or when running openprompt-lang CLI commands (validate, init, scaffold, teach, context, extract, integrate, qa-gen, ai-gen, suggest, wizard, figma, component, lang, mcp).
319
+ ---
320
+
321
+ # openPrompt-Lang Framework
322
+
323
+ ## Overview
324
+ openPrompt-Lang is an annotation-based framework for AI-assisted development.
325
+
326
+ ## Language Module
327
+ **${config?.lenguaje || langId}** — ${index?.language || "Custom"}
328
+ **Templates:** ${templates.length}
329
+ **Learned errors:** ${index?.errorsLearned?.length || 0}
330
+
331
+ ## MCP Tools (available via Opencode)
332
+ - **\`analyze_project\`** — FIRST tool on any unknown project. Returns config, file counts, annotation stats.
333
+ - **\`validate\`** — PRE-COMMIT CHECK. Scan project for annotation errors.
334
+ - **\`lint_file\`** — Debug a single file's annotations in detail.
335
+ - **\`parse_code\`** — Validate annotation syntax inline during code generation.
336
+ - **\`context\`** — Extract project structure with config summary.
337
+ - **\`search_templates\`** — Find templates by keyword.
338
+ - **\`teach_template\`** — Learn from a template (lesson, practices, code).
339
+ - **\`generate_tests\`** — Generate regression tests from @learn-error annotations.
340
+ - **\`scaffold_project\`** — Create project folder structure.
341
+
342
+ ## Default Workflow
343
+ \`\`\`
344
+ 1. analyze_project → 2. validate → 3. lint_file → 4. fix + @learn-error → 5. generate_tests → 6. validate
345
+ \`\`\`
346
+
347
+ ## CLI Commands
348
+ - \`npx openprompt-lang validate [--fix]\` — Validate project structure and annotations
349
+ - \`npx openprompt-lang teach <template-id>\` — Learn from a template
350
+ - \`npx openprompt-lang qa-gen\` — Generate tests from learned errors
351
+ - \`npx openprompt-lang lang search <query>\` — Search templates
352
+ - \`npx openprompt-lang extract <dir>\` — Extract reusable components as templates
353
+ - \`npx openprompt-lang context --output contexto.md\` — Extract project context for AI
354
+
355
+ ## Annotation Syntax
356
+ \`\`\`typescript
357
+ // @use(kind, contract, limit, deps)
358
+ // @kind(hook)
359
+ // @limit(lines: 80)
360
+ // @contract(in: X -> out: Y @error: Z)
361
+ \`\`\`
362
+
363
+ ## @learn-error Format
364
+ \`\`\`typescript
365
+ // @learn-error id=BUTTON-001 input='...' expected=200 actual=500 fix='...' category=validation
366
+ \`\`\`
367
+ `
368
+ }
369
+
370
+ async function askIntegrationTargets(config, langId) {
371
+ const { targets } = await inquirer.prompt([
372
+ {
373
+ type: "checkbox",
374
+ name: "targets",
375
+ message: "Which integrations do you need?",
376
+ choices: [
377
+ { name: "Opencode agent (.opencode/)", value: "opencode", checked: true },
378
+ { name: "Cursor rules (.cursor/ + .cursorrules)", value: "cursor", checked: true },
379
+ { name: "VS Code settings (.vscode/)", value: "vscode", checked: true },
380
+ ],
381
+ },
382
+ ])
383
+
384
+ if (targets.length === 0) {
385
+ console.log(chalk.yellow("\n No integrations selected.\n"))
386
+ return null
387
+ }
388
+
389
+ return targets
390
+ }
391
+
392
+ function detectLanguageFromFiles(cwd) {
393
+ try {
394
+ const entries = readdirSync(cwd, { withFileTypes: true })
395
+
396
+ const hasVue = entries.some((e) => e.name.endsWith(".vue"))
397
+ const hasTsx = entries.some((e) => e.name.endsWith(".tsx"))
398
+ const hasPackageJson = entries.some((e) => e.name === "package.json")
399
+
400
+ if (hasVue) return "vue"
401
+ if (hasTsx || hasPackageJson) return "react"
402
+ return "react"
403
+ } catch {
404
+ return "react"
405
+ }
406
+ }
@@ -0,0 +1,148 @@
1
+ import chalk from "chalk"
2
+ import {
3
+ getLanguages,
4
+ getLanguage,
5
+ getLanguageIndex,
6
+ searchTemplates,
7
+ getTemplateErrors,
8
+ getErrorsByTemplate,
9
+ } from "../utils/language-loader.js"
10
+
11
+ export async function langList() {
12
+ const langs = getLanguages(true)
13
+
14
+ if (langs.length === 0) {
15
+ console.log(chalk.yellow("\n ⚠️ No language modules found in src/templates/langs/\n"))
16
+ return
17
+ }
18
+
19
+ console.log(chalk.cyan("\n📦 Available language modules:\n"))
20
+ for (const lang of langs) {
21
+ const index = getLanguageIndex(lang.id)
22
+ const templateCount = index?.templates?.length || 0
23
+ const hasTeachMe = index?.templates?.filter((t) => t.hasTeachMe).length || 0
24
+ const errorsCount = index?.errorsLearned?.length || 0
25
+
26
+ console.log(` ${chalk.bold(lang.id)} — ${lang.name} v${lang.version}`)
27
+ console.log(` 📁 ${(lang.structure?.folders || []).length} folders`)
28
+ console.log(` 📄 ${templateCount} templates (${hasTeachMe} with @teachMe)`)
29
+ console.log(` 🐛 ${errorsCount} learned errors`)
30
+ console.log(` 🔌 ${(lang.extensions || []).length} extensions`)
31
+ console.log("")
32
+ }
33
+ }
34
+
35
+ export async function langIndex(langId, options) {
36
+ const lang = getLanguage(langId)
37
+ if (!lang) {
38
+ console.log(chalk.red(`\n❌ Language "${langId}" not found\n`))
39
+ return
40
+ }
41
+
42
+ const index = getLanguageIndex(langId)
43
+ if (!index) {
44
+ console.log(chalk.yellow(`\n ⚠️ No INDEX.json for "${langId}"\n`))
45
+ return
46
+ }
47
+
48
+ console.log(chalk.cyan(`\n📚 ${lang.name} — Template Index\n`))
49
+
50
+ const category = options.category
51
+ let templates = index.templates || []
52
+
53
+ if (category) {
54
+ templates = templates.filter((t) => t.category === category)
55
+ const catInfo = index.categories?.[category]
56
+ if (catInfo) {
57
+ console.log(chalk.gray(` Category: ${catInfo.name} — ${catInfo.description}\n`))
58
+ }
59
+ } else {
60
+ console.log(chalk.gray(` Total: ${templates.length} templates\n`))
61
+ }
62
+
63
+ if (templates.length === 0) {
64
+ console.log(chalk.yellow(" No templates found.\n"))
65
+ return
66
+ }
67
+
68
+ // Group by category
69
+ const grouped = {}
70
+ for (const tmpl of templates) {
71
+ const cat = tmpl.category || "uncategorized"
72
+ if (!grouped[cat]) grouped[cat] = []
73
+ grouped[cat].push(tmpl)
74
+ }
75
+
76
+ for (const [cat, tmpls] of Object.entries(grouped)) {
77
+ const catInfo = index.categories?.[cat]
78
+ console.log(chalk.yellow.bold(` ${catInfo?.name || cat}:\n`))
79
+ for (const tmpl of tmpls) {
80
+ const teachBadge = tmpl.hasTeachMe ? chalk.green(" [teach]") : ""
81
+ const darkBadge = tmpl.darkMode ? " 🌙" : ""
82
+ console.log(` 📦 ${chalk.bold(tmpl.id)}${teachBadge}${darkBadge}`)
83
+ console.log(` ${tmpl.description}`)
84
+ if (tmpl.purposes?.length) console.log(` purposes: ${tmpl.purposes.join(", ")}`)
85
+ if (tmpl.tags?.length) console.log(` tags: ${chalk.gray(tmpl.tags.join(", "))}`)
86
+ console.log("")
87
+ }
88
+ }
89
+ }
90
+
91
+ export async function langSearch(query, options) {
92
+ if (!query) {
93
+ console.log(chalk.red("\n❌ Search query required\n"))
94
+ return
95
+ }
96
+
97
+ const results = searchTemplates(query, { language: options.lang })
98
+
99
+ if (results.length === 0) {
100
+ console.log(chalk.yellow(`\n No results for "${query}"\n`))
101
+ return
102
+ }
103
+
104
+ console.log(chalk.cyan(`\n🔍 Search results for "${query}" (${results.length}):\n`))
105
+ for (const r of results) {
106
+ const teachBadge = r.hasTeachMe ? chalk.green(" [teach]") : ""
107
+ console.log(` ${chalk.bold(r.id)}${teachBadge} — ${r.languageName}`)
108
+ console.log(` ${r.description}`)
109
+ console.log(` ${chalk.gray(r.category)} | deps: ${r.dependencies?.join(", ") || "none"}`)
110
+ console.log("")
111
+ }
112
+ }
113
+
114
+ export async function langErrors(langId, options) {
115
+ const lang = getLanguage(langId)
116
+ if (!lang) {
117
+ console.log(chalk.red(`\n❌ Language "${langId}" not found\n`))
118
+ return
119
+ }
120
+
121
+ const templateFilter = options.template
122
+
123
+ let errors
124
+ if (templateFilter) {
125
+ errors = getErrorsByTemplate(langId, templateFilter)
126
+ } else {
127
+ errors = getTemplateErrors(langId)
128
+ }
129
+
130
+ if (errors.length === 0) {
131
+ console.log(chalk.green(`\n ✅ No learned errors for "${templateFilter || langId}"\n`))
132
+ return
133
+ }
134
+
135
+ console.log(
136
+ chalk.cyan(
137
+ `\n🐛 Learned errors${templateFilter ? ` for "${templateFilter}"` : ""} (${errors.length}):\n`
138
+ )
139
+ )
140
+ for (const err of errors) {
141
+ console.log(` ${chalk.red("⚠️ " + err.id)} (${err.category})`)
142
+ console.log(` template: ${err.template}`)
143
+ console.log(` ${chalk.bold("Problem:")} ${err.problem}`)
144
+ console.log(` ${chalk.bold("Fix:")} ${err.solution}`)
145
+ console.log(` ${chalk.gray(err.date)}`)
146
+ console.log("")
147
+ }
148
+ }
@@ -0,0 +1,139 @@
1
+ import { join, relative } from "path"
2
+ import { existsSync, mkdirSync, readFileSync } from "fs"
3
+ import chalk from "chalk"
4
+ import {
5
+ scanForLearnErrors,
6
+ parseLearnErrors,
7
+ persistErrors,
8
+ writeTestFile,
9
+ generateTests,
10
+ } from "../utils/error-learner.js"
11
+ import { getLanguageIndex, getTemplateErrors } from "../utils/language-loader.js"
12
+
13
+ export async function qaGen(options) {
14
+ const cwd = process.cwd()
15
+ const langId = options.lang || "react"
16
+ const sourceDir = options.source ? join(cwd, options.source) : cwd
17
+ const outputDir = options.output ? join(cwd, options.output) : join(cwd, "src", "__tests__")
18
+
19
+ console.log(chalk.cyan("\n🧪 openPrompt-Lang QA Generator\n"))
20
+
21
+ // Phase 1: Scan source for @learn-error annotations
22
+ if (options.scan !== false) {
23
+ console.log(
24
+ chalk.blue(` [1/3] Scanning ${relative(cwd, sourceDir)} for @learn-error annotations...`)
25
+ )
26
+ const foundErrors = scanForLearnErrors(sourceDir, langId)
27
+
28
+ if (foundErrors.length === 0) {
29
+ console.log(chalk.yellow(" No @learn-error annotations found in source.\n"))
30
+ } else {
31
+ console.log(chalk.green(` Found ${foundErrors.length} @learn-error annotations:\n`))
32
+ for (const err of foundErrors) {
33
+ console.log(` 📍 ${chalk.bold(err.id)} — ${err.problem}`)
34
+ console.log(` Fix: ${err.solution}`)
35
+ console.log(` File: ${relative(cwd, err.sourceFile)}`)
36
+ console.log("")
37
+ }
38
+ }
39
+ }
40
+
41
+ // Phase 2: Generate tests from errors
42
+ console.log(chalk.blue(" [2/3] Generating tests from learned errors..."))
43
+
44
+ const index = getLanguageIndex(langId)
45
+ const storedErrors = index?.errorsLearned || []
46
+
47
+ if (storedErrors.length === 0) {
48
+ console.log(chalk.yellow(" No learned errors in language module INDEX.json.\n"))
49
+ console.log(
50
+ chalk.gray(" Tip: Add // @learn-error annotations to your code, then run this again.")
51
+ )
52
+ console.log(
53
+ chalk.gray(
54
+ " Tip: Or add errors directly to src/templates/langs/<lang>/INDEX.json -> errorsLearned\n"
55
+ )
56
+ )
57
+ return
58
+ }
59
+
60
+ // Filter by template if specified
61
+ let errorsToTest = storedErrors
62
+ if (options.template) {
63
+ errorsToTest = storedErrors.filter((e) => e.template === options.template)
64
+ if (errorsToTest.length === 0) {
65
+ console.log(chalk.yellow(` No errors found for template "${options.template}"\n`))
66
+ return
67
+ }
68
+ }
69
+
70
+ console.log(chalk.green(` Using ${errorsToTest.length} learned errors from INDEX.json\n`))
71
+
72
+ for (const err of errorsToTest) {
73
+ console.log(` 🐛 ${chalk.bold(err.id)} (${err.category})`)
74
+ console.log(` ${err.problem}`)
75
+ console.log(` → ${err.solution}`)
76
+ console.log("")
77
+ }
78
+
79
+ // Phase 3: Write test file
80
+ if (!options.dryRun) {
81
+ console.log(chalk.blue(` [3/3] Writing test file...`))
82
+
83
+ mkdirSync(outputDir, { recursive: true })
84
+ const outputFile = join(
85
+ outputDir,
86
+ options.template ? `${options.template}.qa.test.js` : "qa-learned.test.js"
87
+ )
88
+
89
+ writeTestFile(errorsToTest, outputFile, langId)
90
+ console.log(chalk.green(` ✅ Generated: ${relative(cwd, outputFile)}\n`))
91
+ console.log(chalk.cyan(` Run: npx vitest run ${relative(cwd, outputFile)}\n`))
92
+ } else {
93
+ console.log(chalk.gray(" (dry-run — no files written)\n"))
94
+ const testContent = generateTests(errorsToTest, langId)
95
+ console.log(chalk.gray(" Preview:\n"))
96
+ console.log(testContent.join("\n").slice(0, 500) + "...\n")
97
+ }
98
+ }
99
+
100
+ export function qaLearn(options) {
101
+ const langId = options.lang || "react"
102
+
103
+ // Parse @learn-error from a specific file
104
+ if (!options.file) {
105
+ console.log(chalk.red("\n❌ Specify a file with --file or pipe to learn from\n"))
106
+ return
107
+ }
108
+
109
+ const filePath = join(process.cwd(), options.file)
110
+
111
+ if (!existsSync(filePath)) {
112
+ console.log(chalk.red(`\n❌ File not found: ${options.file}\n`))
113
+ return
114
+ }
115
+
116
+ const content = readFileSync(filePath, "utf-8")
117
+ const errors = parseLearnErrors(content, filePath, langId)
118
+
119
+ if (errors.length === 0) {
120
+ console.log(chalk.yellow(`\n No @learn-error annotations found in ${options.file}\n`))
121
+ console.log(
122
+ chalk.gray(" Format: // @learn-error id=X input='...' expected=N actual=N fix='...'\n")
123
+ )
124
+ return
125
+ }
126
+
127
+ console.log(chalk.cyan(`\n📝 Found ${errors.length} errors in ${options.file}\n`))
128
+
129
+ for (const err of errors) {
130
+ console.log(` ${chalk.bold(err.id)}`)
131
+ console.log(` Problem: ${err.problem}`)
132
+ console.log(` Fix: ${err.solution}`)
133
+ console.log(` Category: ${err.category}`)
134
+ console.log("")
135
+ }
136
+
137
+ const added = persistErrors(errors, langId)
138
+ console.log(chalk.green(` ✅ ${added} new errors persisted to ${langId} module INDEX.json\n`))
139
+ }