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
package/src/utils/config.js
CHANGED
|
@@ -1,22 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|