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.
- package/README.md +483 -32
- package/bin/cli.js +232 -41
- package/bin/create.js +135 -0
- package/bin/lint.js +20 -21
- package/docs/COMMANDS.md +200 -127
- package/docs/COMMITS/INDEX.md +1 -0
- package/docs/PROMPT_AI_CONTEXT.md +99 -0
- package/docs/langs/dotnet.md +36 -0
- package/docs/langs/java-spring.md +45 -0
- package/docs/langs/python-fastapi.md +35 -0
- package/docs/langs/unity.md +30 -0
- package/docs/langs/vue-nuxt.md +36 -0
- package/package.json +31 -3
- package/scaffolds/.cursorrules +6 -0
- package/scaffolds/AGENTS.md +27 -0
- package/scaffolds/Dockerfile +11 -0
- package/scaffolds/capacitor.config.ts +17 -0
- package/scaffolds/netlify.toml +8 -0
- package/scaffolds/prompt-lang.json +15 -0
- package/scaffolds/railway.json +12 -0
- package/scaffolds/tailwind.config.js +8 -0
- package/scaffolds/tauri.conf.json +26 -0
- package/schemas/language-module.json +116 -0
- package/schemas/prompt-lang.json +38 -3
- package/schemas/structures.json +145 -0
- package/src/ai/prompt-builder.js +184 -0
- package/src/ai/providers.js +247 -0
- package/src/annotations/registry.js +39 -0
- package/src/annotations/tags.json +24 -0
- package/src/commands/ai-gen.js +161 -0
- package/src/commands/component.js +242 -212
- package/src/commands/context.js +184 -109
- package/src/commands/extract.js +242 -0
- package/src/commands/figma.js +15 -15
- package/src/commands/init.js +197 -93
- package/src/commands/integrate.js +406 -0
- package/src/commands/lang.js +148 -0
- package/src/commands/qa-gen.js +139 -0
- package/src/commands/scaffold.js +127 -0
- package/src/commands/suggest.js +24 -14
- package/src/commands/teach.js +110 -0
- package/src/commands/validate.js +143 -83
- package/src/commands/wizard.js +456 -0
- package/src/generators/figma-prompt.js +20 -12
- package/src/language-service/plugin.cjs +94 -0
- package/src/language-service/plugin.d.ts +6 -0
- package/src/mcp-server.js +605 -0
- package/src/templates/langs/react/INDEX.json +262 -0
- package/src/templates/langs/react/MODULE.json +166 -0
- package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
- package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
- package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
- package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
- package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
- package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
- package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
- package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
- package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
- package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
- package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
- package/src/templates/langs/vue/INDEX.json +246 -0
- package/src/templates/langs/vue/MODULE.json +105 -0
- package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
- package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
- package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
- package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
- package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
- package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
- package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
- package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
- package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
- package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
- package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
- package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
- package/src/templates/scripts/log-actividad.sh +32 -0
- package/src/templates/scripts/log-commit.sh +35 -0
- package/src/templates/scripts/log-error.sh +45 -0
- package/src/templates/scripts/validate.sh +23 -0
- package/src/ts-transformer/index.cjs +86 -0
- package/src/utils/ai.js +35 -53
- package/src/utils/annotations.js +260 -214
- package/src/utils/config.js +61 -13
- package/src/utils/error-learner.js +203 -0
- package/src/utils/file-utils.js +119 -0
- package/src/utils/language-loader.js +167 -0
- package/src/utils/template-utils.js +45 -0
- package/src/vite-plugin/index.js +54 -0
- package/vscode-extension/package.json +23 -2
- package/vscode-extension/snippets/promptlang.json +1 -3
- 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
|
+
}
|