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
@@ -1,22 +1,60 @@
1
- import { readFileSync, existsSync } from "fs";
2
- import { join } from "path";
1
+ // @use(kind, contract, limit)
2
+ // @kind(util)
3
+ // @contract(in: projectDir -> out: normalizedConfig)
4
+ // @limit(lines: 90)
5
+ // @teachMe
6
+ // ## ¿Qué hace config.js?
7
+ // Carga prompt-lang.json desde el directorio del proyecto y mergea
8
+ // recursivamente con defaults. Es el punto de entrada para toda
9
+ // la configuración del framework.
10
+ // ## ¿Por qué deep merge?
11
+ // Shallow merge ({...defaults, ...user}) pierde campos anidados.
12
+ // Si el usuario define {annotations: {enabled: false}}, el shallow
13
+ // overwrite elimina annotations.strict. mergeDefaults() preserva
14
+ // los defaults para sub-objetos no especificados.
15
+ // ## ¿Cómo extender?
16
+ // 1. Agrega nuevos defaults en getDefaultConfig()
17
+ // 2. mergeDefaults mergea recursivamente objetos planos (no arrays)
18
+ // 3. detectStack() se auto-corrige según archivos presentes
3
19
 
4
- const CONFIG_FILE = "prompt-lang.json";
20
+ import { readFileSync, existsSync } from "fs"
21
+ import { join } from "path"
22
+
23
+ const CONFIG_FILE = "prompt-lang.json"
5
24
 
6
25
  export function loadConfig(dir = process.cwd()) {
7
- const configPath = join(dir, CONFIG_FILE);
26
+ const configPath = join(dir, CONFIG_FILE)
8
27
  if (!existsSync(configPath)) {
9
- return getDefaultConfig();
28
+ return getDefaultConfig()
10
29
  }
11
30
  try {
12
- const raw = readFileSync(configPath, "utf-8");
13
- return { ...getDefaultConfig(), ...JSON.parse(raw) };
31
+ const raw = readFileSync(configPath, "utf-8")
32
+ return mergeDefaults(getDefaultConfig(), JSON.parse(raw))
14
33
  } catch {
15
- console.warn("⚠️ Error leyendo prompt-lang.json. Usando defaults.");
16
- return getDefaultConfig();
34
+ console.warn("⚠️ Error leyendo prompt-lang.json. Usando defaults.")
35
+ return getDefaultConfig()
17
36
  }
18
37
  }
19
38
 
39
+ function mergeDefaults(defaults, user) {
40
+ const result = { ...defaults }
41
+ for (const key of Object.keys(user)) {
42
+ if (
43
+ user[key] &&
44
+ typeof user[key] === "object" &&
45
+ !Array.isArray(user[key]) &&
46
+ defaults[key] &&
47
+ typeof defaults[key] === "object" &&
48
+ !Array.isArray(defaults[key])
49
+ ) {
50
+ result[key] = mergeDefaults(defaults[key], user[key])
51
+ } else {
52
+ result[key] = user[key]
53
+ }
54
+ }
55
+ return result
56
+ }
57
+
20
58
  export function getDefaultConfig() {
21
59
  return {
22
60
  name: "mi-proyecto",
@@ -43,18 +81,28 @@ export function getDefaultConfig() {
43
81
  ignore: [".env", "*.local", "dist"],
44
82
  output: "contexto.md",
45
83
  },
46
- };
84
+ }
47
85
  }
48
86
 
49
87
  export function detectStack(dir = process.cwd()) {
50
- const has = (file) => existsSync(join(dir, file));
88
+ const has = (file) => existsSync(join(dir, file))
89
+
90
+ let hasReactInDeps = false
91
+ try {
92
+ const pkgPath = join(dir, "package.json")
93
+ if (existsSync(pkgPath)) {
94
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
95
+ hasReactInDeps = !!(pkg.dependencies?.react || pkg.devDependencies?.react)
96
+ }
97
+ } catch {}
98
+
51
99
  return {
52
- react: has("package.json") && existsSync(join(dir, "src", "App.tsx")),
100
+ react: has("package.json") && (existsSync(join(dir, "src", "App.tsx")) || hasReactInDeps),
53
101
  vite: has("vite.config.ts"),
54
102
  tailwind: has("tailwind.config.ts") || has("tailwind.config.js"),
55
103
  ionic: has("ionic.config.json"),
56
104
  tauri: has("src-tauri"),
57
105
  supabase: has("supabase") || has("supabase-config.ts"),
58
106
  typescript: has("tsconfig.json"),
59
- };
107
+ }
60
108
  }
@@ -0,0 +1,203 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
2
+ import { join, relative, dirname } from "path"
3
+ import { getLanguagePath, getLanguage, getLanguageIndex } from "./language-loader.js"
4
+ import { findAllFiles } from "./file-utils.js"
5
+
6
+ const LEARN_ERROR_REGEX =
7
+ /\/\/\s*@learn-error\s+id=([\w-]+)(?:\s+template=([\w-]+))?\s+input='([^']*)'\s+expected=([\w-]+)\s+actual=([\w-]+)\s+fix='([^']*)'(?:\s+category=([\w-]+))?/g
8
+
9
+ export function parseLearnErrors(content, sourceFile = "unknown", langId = "react") {
10
+ const errors = []
11
+ let match
12
+
13
+ while ((match = LEARN_ERROR_REGEX.exec(content)) !== null) {
14
+ const errorId = match[1]
15
+ const explicitTemplate = match[2]
16
+ const input = match[3]
17
+ const expected = match[4]
18
+ const actual = match[5]
19
+ const fix = match[6]
20
+ const category = match[7] || "unclassified"
21
+
22
+ const template = explicitTemplate || resolveTemplateId(errorId, langId)
23
+
24
+ errors.push({
25
+ id: errorId,
26
+ template,
27
+ problem: buildProblemDescription(input, expected, actual),
28
+ solution: fix,
29
+ input,
30
+ expected,
31
+ actual,
32
+ category,
33
+ date: new Date().toISOString().split("T")[0],
34
+ sourceFile,
35
+ })
36
+ }
37
+
38
+ return errors
39
+ }
40
+
41
+ function resolveTemplateId(errorId, langId) {
42
+ const index = getLanguageIndex(langId)
43
+ if (!index?.templates) return errorId.split("-")[0]?.toLowerCase() || "unknown"
44
+
45
+ const prefix = errorId.split("-")[0]?.toLowerCase()
46
+ if (!prefix) return "unknown"
47
+
48
+ // Direct match: template id starts with prefix
49
+ const direct = index.templates.find((t) => t.id.toLowerCase().startsWith(prefix))
50
+ if (direct) return direct.id
51
+
52
+ // Fuzzy match: template id contains prefix
53
+ const fuzzy = index.templates.find((t) => t.id.toLowerCase().includes(prefix))
54
+ if (fuzzy) return fuzzy.id
55
+
56
+ // Match by fixHistory: any error in fixHistory has matching prefix
57
+ const byHistory = index.templates.find((t) =>
58
+ (t.fixHistory || []).some((fid) => fid.toLowerCase().startsWith(prefix))
59
+ )
60
+ if (byHistory) return byHistory.id
61
+
62
+ return prefix
63
+ }
64
+
65
+ export function scanForLearnErrors(sourceDir, langId = "react") {
66
+ const results = []
67
+ const allFiles = findAllFiles(sourceDir)
68
+
69
+ for (const file of allFiles) {
70
+ try {
71
+ const content = readFileSync(file, "utf-8")
72
+ const errors = parseLearnErrors(content, file, langId)
73
+ results.push(...errors)
74
+ } catch {}
75
+ }
76
+
77
+ return results
78
+ }
79
+
80
+ export function persistErrors(errors, langId = "react") {
81
+ const langPath = getLanguagePath(langId)
82
+ if (!langPath) {
83
+ throw new Error(`Language module "${langId}" not found`)
84
+ }
85
+
86
+ const indexPath = join(langPath, "INDEX.json")
87
+ let index = {
88
+ language: langId,
89
+ version: "1.0.0",
90
+ updated: new Date().toISOString().split("T")[0],
91
+ categories: {},
92
+ templates: [],
93
+ errorsLearned: [],
94
+ }
95
+
96
+ if (existsSync(indexPath)) {
97
+ try {
98
+ index = JSON.parse(readFileSync(indexPath, "utf-8"))
99
+ } catch {}
100
+ }
101
+
102
+ if (!index.errorsLearned) index.errorsLearned = []
103
+
104
+ let added = 0
105
+ for (const err of errors) {
106
+ const exists = index.errorsLearned.some((e) => e.id === err.id)
107
+ if (!exists) {
108
+ index.errorsLearned.push(err)
109
+ added++
110
+ }
111
+ }
112
+
113
+ index.updated = new Date().toISOString().split("T")[0]
114
+ writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf-8")
115
+ return added
116
+ }
117
+
118
+ export function generateTestFromError(error, langId = "react") {
119
+ const template = getTestTemplate(langId)
120
+
121
+ const testName = `${error.id}: ${error.problem}`
122
+ const testBody = template
123
+ .replace(/{{TEST_NAME}}/g, testName)
124
+ .replace(/{{INPUT}}/g, error.input)
125
+ .replace(/{{EXPECTED}}/g, error.expected)
126
+ .replace(/{{ACTUAL}}/g, error.actual)
127
+ .replace(/{{FIX}}/g, error.solution)
128
+ .replace(/{{ERROR_ID}}/g, error.id)
129
+ .replace(/{{CATEGORY}}/g, error.category)
130
+
131
+ return testBody
132
+ }
133
+
134
+ export function generateTests(errors, langId = "react") {
135
+ return errors.map((err) => generateTestFromError(err, langId))
136
+ }
137
+
138
+ export function writeTestFile(errors, outputPath, langId = "react") {
139
+ const tests = generateTests(errors, langId)
140
+ const content = [
141
+ "// @generated by openPrompt-Lang qa-gen",
142
+ "// @description: Auto-generated tests from learned errors",
143
+ "// @date: " + new Date().toISOString().split("T")[0],
144
+ `// @errors: ${errors.map((e) => e.id).join(", ")}`,
145
+ "",
146
+ 'import { describe, it, expect } from "vitest"',
147
+ "",
148
+ ...tests,
149
+ "",
150
+ ].join("\n")
151
+
152
+ mkdirSync(dirname(outputPath), { recursive: true })
153
+ writeFileSync(outputPath, content, "utf-8")
154
+ }
155
+
156
+ export function buildProblemDescription(input, expected, actual) {
157
+ return `${input} → expected ${expected}, got ${actual}`
158
+ }
159
+
160
+ // ─── Test templates ──────────────────────────────────────────────────────────
161
+
162
+ function getTestTemplate(langId) {
163
+ const templates = {
164
+ react: `
165
+ describe("{{TEST_NAME}}", () => {
166
+ it("should handle {{INPUT}} correctly", async () => {
167
+ // Learned from error {{ERROR_ID}} ({{CATEGORY}})
168
+ // Problem: {{INPUT}} → expected {{EXPECTED}}, got {{ACTUAL}}
169
+ // Fix applied: {{FIX}}
170
+
171
+ const input = "{{INPUT}}"
172
+ const expected = "{{EXPECTED}}"
173
+
174
+ // TODO: Implement actual test based on your service/hook
175
+ // const result = await yourFunction(input)
176
+ // expect(result.status).toBe(expected)
177
+
178
+ expect(true).toBe(true)
179
+ })
180
+ })
181
+ `,
182
+ vue: `
183
+ describe("{{TEST_NAME}}", () => {
184
+ it("should handle {{INPUT}} correctly", async () => {
185
+ // Learned from error {{ERROR_ID}} ({{CATEGORY}})
186
+ // Problem: {{INPUT}} → expected {{EXPECTED}}, got {{ACTUAL}}
187
+ // Fix applied: {{FIX}}
188
+
189
+ const input = "{{INPUT}}"
190
+ const expected = "{{EXPECTED}}"
191
+
192
+ // TODO: Implement actual test based on your composable
193
+ // const result = await yourFunction(input)
194
+ // expect(result).toBe(expected)
195
+
196
+ expect(true).toBe(true)
197
+ })
198
+ })
199
+ `,
200
+ }
201
+
202
+ return templates[langId] || templates.react
203
+ }
@@ -0,0 +1,119 @@
1
+ // @use(kind, contract, limit)
2
+ // @kind(util)
3
+ // @contract(in: dir, options -> out: string[])
4
+ // @limit(lines: 50)
5
+ // @teachMe
6
+ // ## ¿Qué hace file-utils.js?
7
+ // Unifica los dos findAllFiles duplicados (extract.js y error-learner.js)
8
+ // en una sola función configurable. También provee findComponentFiles
9
+ // que antes estaba en extract.js.
10
+ // ## ¿Por qué unificarlo?
11
+ // Dos implementaciones con diferente lógica de exclusión (una omitía dist,
12
+ // la otra no) causaban bugs inconsistentes entre commands.
13
+
14
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs"
15
+ import { join, extname, basename, dirname } from "path"
16
+
17
+ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]
18
+ const DEFAULT_EXCLUDE = new Set([
19
+ ".git",
20
+ "node_modules",
21
+ "dist",
22
+ "build",
23
+ ".next",
24
+ "coverage",
25
+ ".nyc_output",
26
+ "__pycache__",
27
+ ".cache",
28
+ ])
29
+
30
+ export function findAllFiles(dir, options = {}) {
31
+ const {
32
+ extensions = DEFAULT_EXTENSIONS,
33
+ excludeDirs = DEFAULT_EXCLUDE,
34
+ skipDotDirs = true,
35
+ } = options
36
+
37
+ const results = []
38
+ try {
39
+ const entries = readdirSync(dir, { withFileTypes: true })
40
+ for (const entry of entries) {
41
+ const fullPath = join(dir, entry.name)
42
+
43
+ if (entry.isDirectory()) {
44
+ if (skipDotDirs && entry.name.startsWith(".")) continue
45
+ if (excludeDirs.has(entry.name)) continue
46
+ results.push(...findAllFiles(fullPath, options))
47
+ } else if (entry.isFile()) {
48
+ const ext = extname(entry.name).toLowerCase()
49
+ // null = accept all files, array = filter
50
+ if (!extensions || extensions.includes(ext)) {
51
+ results.push(fullPath)
52
+ }
53
+ }
54
+ }
55
+ } catch {}
56
+ return results
57
+ }
58
+
59
+ export function findComponentFiles(dir) {
60
+ const candidates = findAllFiles(dir, { extensions: [".tsx", ".jsx", ".ts", ".js"] })
61
+ return candidates.filter((f) => {
62
+ try {
63
+ const content = readFileSync(f, "utf-8")
64
+ const isReact =
65
+ content.includes("import React") ||
66
+ content.includes('from "react"') ||
67
+ content.includes("from 'react'")
68
+ const isComponent =
69
+ content.includes("export ") && (content.includes("function ") || content.includes("const "))
70
+ return isReact && isComponent && (f.endsWith(".tsx") || f.endsWith(".jsx"))
71
+ } catch {
72
+ return false
73
+ }
74
+ })
75
+ }
76
+
77
+ export function analyzeImports(dir, files) {
78
+ const importMap = new Map()
79
+
80
+ for (const file of files) {
81
+ try {
82
+ const content = readFileSync(file, "utf-8")
83
+ const imports = []
84
+
85
+ const importRegex = /import\s+(?:\{\s*([^}]+)\}|(\w+))\s+from\s+['"]\.\/([^'"]+)['"]/g
86
+ let match
87
+ while ((match = importRegex.exec(content)) !== null) {
88
+ if (match[1]) {
89
+ const names = match[1]
90
+ .split(",")
91
+ .map((n) => n.trim().replace(/\s+as\s+\w+$/, ""))
92
+ .filter(Boolean)
93
+ imports.push(...names)
94
+ } else if (match[2]) {
95
+ imports.push(match[2])
96
+ }
97
+ }
98
+
99
+ importMap.set(file, imports)
100
+ } catch {
101
+ importMap.set(file, [])
102
+ }
103
+ }
104
+
105
+ return importMap
106
+ }
107
+
108
+ export function countFiles(dir) {
109
+ const counts = { total: 0, ts: 0, tsx: 0, js: 0, jsx: 0, css: 0, other: 0 }
110
+ const allFiles = findAllFiles(dir, { extensions: null }) // null = all files
111
+
112
+ for (const f of allFiles) {
113
+ counts.total++
114
+ const ext = extname(f).slice(1)
115
+ if (counts[ext] !== undefined) counts[ext]++
116
+ else counts.other++
117
+ }
118
+ return counts
119
+ }
@@ -0,0 +1,167 @@
1
+ import { readFileSync, existsSync, readdirSync } from "fs"
2
+ import { join, dirname } from "path"
3
+ import { fileURLToPath } from "url"
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+ const LANGS_DIR = join(__dirname, "..", "templates", "langs")
7
+
8
+ let cachedLanguages = null
9
+
10
+ function discoverLanguages() {
11
+ if (!existsSync(LANGS_DIR)) return []
12
+
13
+ const entries = readdirSync(LANGS_DIR, { withFileTypes: true })
14
+ const languages = []
15
+
16
+ for (const entry of entries) {
17
+ if (!entry.isDirectory()) continue
18
+ const modulePath = join(LANGS_DIR, entry.name, "MODULE.json")
19
+ if (!existsSync(modulePath)) continue
20
+
21
+ try {
22
+ const module = JSON.parse(readFileSync(modulePath, "utf-8"))
23
+ if (module.id && module.name) {
24
+ languages.push({
25
+ ...module,
26
+ _path: join(LANGS_DIR, entry.name),
27
+ })
28
+ }
29
+ } catch {
30
+ continue
31
+ }
32
+ }
33
+
34
+ return languages
35
+ }
36
+
37
+ export function getLanguages(forceRefresh = false) {
38
+ if (!cachedLanguages || forceRefresh) {
39
+ cachedLanguages = discoverLanguages()
40
+ }
41
+ return cachedLanguages
42
+ }
43
+
44
+ export function getLanguage(id) {
45
+ const langs = getLanguages()
46
+ return langs.find((l) => l.id === id) || null
47
+ }
48
+
49
+ export function getLanguagePath(id) {
50
+ const lang = getLanguage(id)
51
+ return lang ? lang._path : null
52
+ }
53
+
54
+ export function getLanguageIndex(id) {
55
+ const langPath = getLanguagePath(id)
56
+ if (!langPath) return null
57
+
58
+ const indexPath = join(langPath, "INDEX.json")
59
+ if (!existsSync(indexPath)) return null
60
+
61
+ try {
62
+ return JSON.parse(readFileSync(indexPath, "utf-8"))
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ export function getTemplate(id, langId) {
69
+ const index = getLanguageIndex(langId)
70
+ if (!index) return null
71
+
72
+ const entry = index.templates?.find((t) => t.id === id)
73
+ if (!entry) return null
74
+
75
+ const langPath = getLanguagePath(langId)
76
+ const templatePath = join(langPath, entry.file)
77
+
78
+ if (!existsSync(templatePath)) return null
79
+
80
+ return {
81
+ metadata: entry,
82
+ content: readFileSync(templatePath, "utf-8"),
83
+ path: templatePath,
84
+ }
85
+ }
86
+
87
+ export function searchTemplates(query, filters = {}) {
88
+ const langs = filters.language ? [getLanguage(filters.language)].filter(Boolean) : getLanguages()
89
+ const results = []
90
+
91
+ const terms = query.toLowerCase().split(/\s+/)
92
+
93
+ for (const lang of langs) {
94
+ const index = getLanguageIndex(lang.id)
95
+ if (!index?.templates) continue
96
+
97
+ for (const tmpl of index.templates) {
98
+ const searchable = [
99
+ tmpl.name,
100
+ tmpl.description,
101
+ ...(tmpl.tags || []),
102
+ ...(tmpl.purposes || []),
103
+ tmpl.category,
104
+ tmpl.id,
105
+ ]
106
+ .filter(Boolean)
107
+ .join(" ")
108
+ .toLowerCase()
109
+
110
+ const match = terms.every((t) => searchable.includes(t))
111
+ if (!match) continue
112
+
113
+ if (filters.category && tmpl.category !== filters.category) continue
114
+ if (filters.darkMode !== undefined && tmpl.darkMode !== filters.darkMode) continue
115
+
116
+ results.push({
117
+ ...tmpl,
118
+ language: lang.id,
119
+ languageName: lang.name,
120
+ })
121
+ }
122
+ }
123
+
124
+ return results
125
+ }
126
+
127
+ export function getTemplateErrors(langId) {
128
+ const index = getLanguageIndex(langId)
129
+ if (!index?.errorsLearned) return []
130
+ return index.errorsLearned
131
+ }
132
+
133
+ export function getErrorsByTemplate(langId, templateId) {
134
+ const index = getLanguageIndex(langId)
135
+ if (!index?.errorsLearned) return []
136
+ return index.errorsLearned.filter((e) => e.template === templateId)
137
+ }
138
+
139
+ export function getStructure(id) {
140
+ const lang = getLanguage(id)
141
+ if (!lang?.structure) return null
142
+ return lang.structure
143
+ }
144
+
145
+ export function getProjectTypes(id) {
146
+ const lang = getLanguage(id)
147
+ if (!lang?.projectTypes) return null
148
+ return lang.projectTypes
149
+ }
150
+
151
+ export function getExtensions(id) {
152
+ const lang = getLanguage(id)
153
+ if (!lang?.extensions) return []
154
+ return lang.extensions
155
+ }
156
+
157
+ export function getUiStyles(id) {
158
+ const lang = getLanguage(id)
159
+ if (!lang?.uiStyles) return ["none"]
160
+ return lang.uiStyles
161
+ }
162
+
163
+ export function getTags(id) {
164
+ const lang = getLanguage(id)
165
+ if (!lang?.tags) return null
166
+ return lang.tags
167
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared utilities for extracting lesson content and tags from templates.
3
+ */
4
+
5
+ export function extractLesson(content) {
6
+ const lines = content.split("\n")
7
+ const lessonLines = []
8
+ let inLesson = false
9
+
10
+ for (const line of lines) {
11
+ if (line.includes("@teachMe")) {
12
+ inLesson = true
13
+ continue
14
+ }
15
+ if (inLesson) {
16
+ const trimmed = line.trim()
17
+ if (!trimmed) continue
18
+ if (!trimmed.startsWith("//")) break
19
+ if (
20
+ /^\/\/\s+@(kind|contract|props|limit|use|deps|compose|platform|state|test|pattern|meta|scope|forbidden|goodPractice|badPractice|template|source|reuse|extracted|learn-error)\b/.test(
21
+ trimmed
22
+ )
23
+ )
24
+ break
25
+ if (trimmed.startsWith("// ##")) {
26
+ lessonLines.push(line.replace(/^\/\/\s*##\s*/, "## ").trim())
27
+ continue
28
+ }
29
+ const cleaned = line.replace(/^\/\/\s*/, "").trim()
30
+ if (cleaned) lessonLines.push(cleaned)
31
+ }
32
+ }
33
+
34
+ return lessonLines.join("\n").trim() || null
35
+ }
36
+
37
+ export function extractTags(content, tagName) {
38
+ const results = []
39
+ const regex = new RegExp(`// @${tagName}:\\s*(.+)`, "g")
40
+ let match
41
+ while ((match = regex.exec(content)) !== null) {
42
+ results.push(match[1].trim())
43
+ }
44
+ return results
45
+ }
@@ -0,0 +1,54 @@
1
+ import { readFileSync, existsSync } from "fs"
2
+
3
+ const builtinTags = JSON.parse(
4
+ readFileSync(new URL("../annotations/tags.json", import.meta.url), "utf-8")
5
+ )
6
+
7
+ function countNesting(line, openChars = "({[", closeChars = ")}]") {
8
+ let depth = 0
9
+ for (const ch of line) {
10
+ if (openChars.includes(ch)) depth++
11
+ if (closeChars.includes(ch)) depth--
12
+ }
13
+ return depth
14
+ }
15
+
16
+ export function openPromptAnnotations(options = {}) {
17
+ const customTags = options.tags || []
18
+ const tags = [...builtinTags, ...customTags]
19
+ const tagPattern = new RegExp(
20
+ `^\\s*@(${tags.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\(`
21
+ )
22
+
23
+ return {
24
+ name: "openprompt-lang",
25
+ enforce: "pre",
26
+
27
+ transform(code, id) {
28
+ if (!/\.([cm]?[jt]sx?|vue)$/.test(id)) return null
29
+
30
+ const lines = code.split("\n")
31
+ const result = []
32
+ let i = 0
33
+
34
+ while (i < lines.length) {
35
+ const trimmed = lines[i].trim()
36
+
37
+ if (tagPattern.test(trimmed)) {
38
+ let depth = countNesting(lines[i])
39
+ while (depth > 0 && i + 1 < lines.length) {
40
+ i++
41
+ depth += countNesting(lines[i])
42
+ }
43
+ i++
44
+ continue
45
+ }
46
+
47
+ result.push(lines[i])
48
+ i++
49
+ }
50
+
51
+ return result.join("\n")
52
+ },
53
+ }
54
+ }