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/commands/context.js
CHANGED
|
@@ -1,206 +1,281 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "fs"
|
|
2
|
-
import { join, relative, extname, basename, dirname } from "path"
|
|
3
|
-
import { loadConfig } from "../utils/config.js"
|
|
4
|
-
import { lintFile } from "../utils/annotations.js"
|
|
5
|
-
import chalk from "chalk"
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "fs"
|
|
2
|
+
import { join, relative, extname, basename, dirname } from "path"
|
|
3
|
+
import { loadConfig } from "../utils/config.js"
|
|
4
|
+
import { lintFile } from "../utils/annotations.js"
|
|
5
|
+
import chalk from "chalk"
|
|
6
6
|
|
|
7
7
|
const IGNORED_DIRS = new Set([
|
|
8
|
-
".git",
|
|
9
|
-
"
|
|
10
|
-
|
|
8
|
+
".git",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"dist",
|
|
11
|
+
"build",
|
|
12
|
+
".next",
|
|
13
|
+
".gemini",
|
|
14
|
+
"coverage",
|
|
15
|
+
".nyc_output",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
".cache",
|
|
18
|
+
])
|
|
11
19
|
|
|
12
20
|
const IGNORED_FILES = new Set([
|
|
13
|
-
"package-lock.json",
|
|
14
|
-
"
|
|
15
|
-
|
|
21
|
+
"package-lock.json",
|
|
22
|
+
"yarn.lock",
|
|
23
|
+
".env.local",
|
|
24
|
+
".env",
|
|
25
|
+
"contexto.md",
|
|
26
|
+
"proyecto_completo.md",
|
|
27
|
+
])
|
|
16
28
|
|
|
17
29
|
const IGNORED_EXTENSIONS = new Set([
|
|
18
|
-
".png",
|
|
19
|
-
".
|
|
20
|
-
".
|
|
21
|
-
".
|
|
22
|
-
".
|
|
23
|
-
".
|
|
24
|
-
|
|
30
|
+
".png",
|
|
31
|
+
".jpg",
|
|
32
|
+
".jpeg",
|
|
33
|
+
".gif",
|
|
34
|
+
".ico",
|
|
35
|
+
".svg",
|
|
36
|
+
".webp",
|
|
37
|
+
".pdf",
|
|
38
|
+
".woff",
|
|
39
|
+
".woff2",
|
|
40
|
+
".ttf",
|
|
41
|
+
".eot",
|
|
42
|
+
".otf",
|
|
43
|
+
".mp4",
|
|
44
|
+
".mp3",
|
|
45
|
+
".avi",
|
|
46
|
+
".mov",
|
|
47
|
+
".zip",
|
|
48
|
+
".tar",
|
|
49
|
+
".gz",
|
|
50
|
+
".rar",
|
|
51
|
+
".exe",
|
|
52
|
+
".dll",
|
|
53
|
+
".so",
|
|
54
|
+
".dylib",
|
|
55
|
+
".db",
|
|
56
|
+
".sqlite",
|
|
57
|
+
".sqlite3",
|
|
58
|
+
])
|
|
25
59
|
|
|
26
60
|
const CODE_EXTENSIONS = new Set([
|
|
27
|
-
".ts",
|
|
28
|
-
".
|
|
29
|
-
".
|
|
30
|
-
|
|
61
|
+
".ts",
|
|
62
|
+
".tsx",
|
|
63
|
+
".js",
|
|
64
|
+
".jsx",
|
|
65
|
+
".json",
|
|
66
|
+
".css",
|
|
67
|
+
".html",
|
|
68
|
+
".md",
|
|
69
|
+
".mdx",
|
|
70
|
+
".py",
|
|
71
|
+
".rs",
|
|
72
|
+
".toml",
|
|
73
|
+
".yaml",
|
|
74
|
+
".yml",
|
|
75
|
+
".sh",
|
|
76
|
+
".bash",
|
|
77
|
+
".zsh",
|
|
78
|
+
".env.example",
|
|
79
|
+
])
|
|
31
80
|
|
|
32
81
|
function shouldIgnore(name, relPath, isDir) {
|
|
33
|
-
if (isDir) return IGNORED_DIRS.has(name)
|
|
34
|
-
if (IGNORED_FILES.has(name)) return true
|
|
35
|
-
const ext = extname(name).toLowerCase()
|
|
36
|
-
if (IGNORED_EXTENSIONS.has(ext)) return true
|
|
37
|
-
return false
|
|
82
|
+
if (isDir) return IGNORED_DIRS.has(name)
|
|
83
|
+
if (IGNORED_FILES.has(name)) return true
|
|
84
|
+
const ext = extname(name).toLowerCase()
|
|
85
|
+
if (IGNORED_EXTENSIONS.has(ext)) return true
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function matchesGlob(name, pattern) {
|
|
90
|
+
// Convert simple glob (*.ext) to regex
|
|
91
|
+
if (pattern.includes("*")) {
|
|
92
|
+
const regex = new RegExp(
|
|
93
|
+
"^" +
|
|
94
|
+
pattern
|
|
95
|
+
.split("*")
|
|
96
|
+
.map((s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&"))
|
|
97
|
+
.join(".*") +
|
|
98
|
+
"$"
|
|
99
|
+
)
|
|
100
|
+
return regex.test(name)
|
|
101
|
+
}
|
|
102
|
+
return name.includes(pattern)
|
|
38
103
|
}
|
|
39
104
|
|
|
40
105
|
function generateTree(dirPath, prefix = "", configIgnore = []) {
|
|
41
|
-
let treeStr = ""
|
|
106
|
+
let treeStr = ""
|
|
42
107
|
try {
|
|
43
|
-
const entries = readdirSync(dirPath).sort()
|
|
108
|
+
const entries = readdirSync(dirPath).sort()
|
|
44
109
|
const filtered = entries.filter((e) => {
|
|
45
|
-
if (IGNORED_DIRS.has(e) || IGNORED_FILES.has(e)) return false
|
|
46
|
-
if (configIgnore.some((ig) => e
|
|
47
|
-
return true
|
|
48
|
-
})
|
|
110
|
+
if (IGNORED_DIRS.has(e) || IGNORED_FILES.has(e)) return false
|
|
111
|
+
if (configIgnore.some((ig) => matchesGlob(e, ig))) return false
|
|
112
|
+
return true
|
|
113
|
+
})
|
|
49
114
|
|
|
50
115
|
for (let i = 0; i < filtered.length; i++) {
|
|
51
|
-
const entry = filtered[i]
|
|
52
|
-
const fullPath = join(dirPath, entry)
|
|
53
|
-
const isLast = i === filtered.length - 1
|
|
54
|
-
const connector = isLast ? "└── " : "├── "
|
|
55
|
-
treeStr += `${prefix}${connector}${entry}\n
|
|
116
|
+
const entry = filtered[i]
|
|
117
|
+
const fullPath = join(dirPath, entry)
|
|
118
|
+
const isLast = i === filtered.length - 1
|
|
119
|
+
const connector = isLast ? "└── " : "├── "
|
|
120
|
+
treeStr += `${prefix}${connector}${entry}\n`
|
|
56
121
|
|
|
57
122
|
if (statSync(fullPath).isDirectory()) {
|
|
58
|
-
const extension = isLast ? " " : "│ "
|
|
59
|
-
treeStr += generateTree(fullPath, prefix + extension, configIgnore)
|
|
123
|
+
const extension = isLast ? " " : "│ "
|
|
124
|
+
treeStr += generateTree(fullPath, prefix + extension, configIgnore)
|
|
60
125
|
}
|
|
61
126
|
}
|
|
62
127
|
} catch {
|
|
63
128
|
// PermissionError o similar
|
|
64
129
|
}
|
|
65
|
-
return treeStr
|
|
130
|
+
return treeStr
|
|
66
131
|
}
|
|
67
132
|
|
|
68
133
|
export async function context(options) {
|
|
69
|
-
const baseDir = process.cwd()
|
|
70
|
-
const outputFile = options.output || "contexto.md"
|
|
71
|
-
const config = loadConfig(baseDir)
|
|
72
|
-
const configIgnores = config.extractor?.ignore || []
|
|
134
|
+
const baseDir = process.cwd()
|
|
135
|
+
const outputFile = options.output || "contexto.md"
|
|
136
|
+
const config = loadConfig(baseDir)
|
|
137
|
+
const configIgnores = config.extractor?.ignore || []
|
|
73
138
|
|
|
74
|
-
console.log(chalk.cyan("🔍 Extrayendo contexto del proyecto..."))
|
|
139
|
+
console.log(chalk.cyan("🔍 Extrayendo contexto del proyecto..."))
|
|
75
140
|
|
|
76
|
-
let output = "# Contexto del Proyecto\n\n"
|
|
141
|
+
let output = "# Contexto del Proyecto\n\n"
|
|
77
142
|
|
|
78
143
|
// Incluir AGENTS.md si existe
|
|
79
|
-
const agentsPath = join(baseDir, "AGENTS.md")
|
|
144
|
+
const agentsPath = join(baseDir, "AGENTS.md")
|
|
80
145
|
if (existsSync(agentsPath)) {
|
|
81
|
-
output += "## AGENTS.md (Contexto para IA)\n\n"
|
|
82
|
-
output += readFileSync(agentsPath, "utf-8") + "\n\n"
|
|
146
|
+
output += "## AGENTS.md (Contexto para IA)\n\n"
|
|
147
|
+
output += readFileSync(agentsPath, "utf-8") + "\n\n"
|
|
83
148
|
}
|
|
84
149
|
|
|
85
150
|
// Incluir prompt-lang.json si existe
|
|
86
|
-
const configPath = join(baseDir, "prompt-lang.json")
|
|
151
|
+
const configPath = join(baseDir, "prompt-lang.json")
|
|
87
152
|
if (existsSync(configPath)) {
|
|
88
|
-
output += "## prompt-lang.json (Configuración)\n\n"
|
|
89
|
-
output += "```json\n"
|
|
90
|
-
output += readFileSync(configPath, "utf-8") + "\n"
|
|
91
|
-
output += "```\n\n"
|
|
153
|
+
output += "## prompt-lang.json (Configuración)\n\n"
|
|
154
|
+
output += "```json\n"
|
|
155
|
+
output += readFileSync(configPath, "utf-8") + "\n"
|
|
156
|
+
output += "```\n\n"
|
|
92
157
|
}
|
|
93
158
|
|
|
94
159
|
// Recorrer archivos
|
|
95
|
-
let fileCount = 0
|
|
160
|
+
let fileCount = 0
|
|
96
161
|
const walkDir = (dirPath) => {
|
|
97
|
-
let entries
|
|
162
|
+
let entries
|
|
98
163
|
try {
|
|
99
|
-
entries = readdirSync(dirPath)
|
|
164
|
+
entries = readdirSync(dirPath)
|
|
100
165
|
} catch {
|
|
101
|
-
return
|
|
166
|
+
return
|
|
102
167
|
}
|
|
103
168
|
|
|
104
169
|
for (const entry of entries) {
|
|
105
|
-
const fullPath = join(dirPath, entry)
|
|
106
|
-
let stat
|
|
170
|
+
const fullPath = join(dirPath, entry)
|
|
171
|
+
let stat
|
|
107
172
|
try {
|
|
108
|
-
stat = statSync(fullPath)
|
|
173
|
+
stat = statSync(fullPath)
|
|
109
174
|
} catch {
|
|
110
|
-
continue
|
|
175
|
+
continue
|
|
111
176
|
}
|
|
112
177
|
|
|
113
178
|
if (stat.isDirectory()) {
|
|
114
179
|
if (!shouldIgnore(entry, relative(baseDir, fullPath), true)) {
|
|
115
|
-
walkDir(fullPath)
|
|
180
|
+
walkDir(fullPath)
|
|
116
181
|
}
|
|
117
|
-
continue
|
|
182
|
+
continue
|
|
118
183
|
}
|
|
119
184
|
|
|
120
185
|
// Ignorar archivos
|
|
121
|
-
if (shouldIgnore(entry, relative(baseDir, fullPath), false)) continue
|
|
186
|
+
if (shouldIgnore(entry, relative(baseDir, fullPath), false)) continue
|
|
122
187
|
|
|
123
|
-
const ext = extname(entry).toLowerCase()
|
|
124
|
-
if (!CODE_EXTENSIONS.has(ext)) continue
|
|
188
|
+
const ext = extname(entry).toLowerCase()
|
|
189
|
+
if (!CODE_EXTENSIONS.has(ext)) continue
|
|
125
190
|
|
|
126
191
|
// Ignorar el propio output
|
|
127
|
-
const relPath = relative(baseDir, fullPath)
|
|
128
|
-
if (relPath === outputFile) continue
|
|
192
|
+
const relPath = relative(baseDir, fullPath)
|
|
193
|
+
if (relPath === outputFile) continue
|
|
129
194
|
|
|
130
195
|
// Scope filtering
|
|
131
196
|
if (options.scope) {
|
|
132
197
|
try {
|
|
133
|
-
const content = readFileSync(fullPath, "utf-8")
|
|
134
|
-
if (!content.includes(`@scope`)) continue
|
|
135
|
-
if (!content.includes(`module: ${options.scope}`)) continue
|
|
198
|
+
const content = readFileSync(fullPath, "utf-8")
|
|
199
|
+
if (!content.includes(`@scope`)) continue
|
|
200
|
+
if (!content.includes(`module: ${options.scope}`)) continue
|
|
136
201
|
} catch {
|
|
137
|
-
continue
|
|
202
|
+
continue
|
|
138
203
|
}
|
|
139
204
|
}
|
|
140
205
|
|
|
141
|
-
fileCount
|
|
206
|
+
fileCount++
|
|
142
207
|
|
|
143
|
-
let content
|
|
208
|
+
let content
|
|
144
209
|
try {
|
|
145
|
-
content = readFileSync(fullPath, "utf-8")
|
|
210
|
+
content = readFileSync(fullPath, "utf-8")
|
|
146
211
|
} catch {
|
|
147
|
-
continue
|
|
212
|
+
continue
|
|
148
213
|
}
|
|
149
214
|
|
|
150
215
|
// Detectar lenguaje para el bloque de código
|
|
151
216
|
const langMap = {
|
|
152
|
-
".ts": "typescript",
|
|
153
|
-
".
|
|
154
|
-
".
|
|
155
|
-
".
|
|
156
|
-
".
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
".ts": "typescript",
|
|
218
|
+
".tsx": "tsx",
|
|
219
|
+
".js": "javascript",
|
|
220
|
+
".jsx": "jsx",
|
|
221
|
+
".json": "json",
|
|
222
|
+
".css": "css",
|
|
223
|
+
".html": "html",
|
|
224
|
+
".md": "markdown",
|
|
225
|
+
".mdx": "mdx",
|
|
226
|
+
".py": "python",
|
|
227
|
+
".rs": "rust",
|
|
228
|
+
".toml": "toml",
|
|
229
|
+
".yaml": "yaml",
|
|
230
|
+
".yml": "yaml",
|
|
231
|
+
".sh": "bash",
|
|
232
|
+
}
|
|
233
|
+
const lang = langMap[ext] || ""
|
|
234
|
+
|
|
235
|
+
output += `### Archivo: ${relPath}\n`
|
|
236
|
+
output += `\`\`\`${lang}\n`
|
|
237
|
+
output += content
|
|
238
|
+
if (!content.endsWith("\n")) output += "\n"
|
|
239
|
+
output += "```\n\n"
|
|
165
240
|
|
|
166
241
|
// Si tiene anotaciones PromptLang, mostrarlas como metadata
|
|
167
242
|
if (options.ai !== false) {
|
|
168
243
|
try {
|
|
169
|
-
const { annotations, errors, warnings } = lintFile(content)
|
|
244
|
+
const { annotations, errors, warnings } = lintFile(content)
|
|
170
245
|
if (annotations.length > 0) {
|
|
171
|
-
output += `*Anotaciones PromptLang:*
|
|
246
|
+
output += `*Anotaciones PromptLang:* `
|
|
172
247
|
for (const ann of annotations) {
|
|
173
|
-
output += `\`@${ann.name}
|
|
248
|
+
output += `\`@${ann.name}`
|
|
174
249
|
if (ann.args.length > 0) {
|
|
175
250
|
const argsStr = ann.args
|
|
176
251
|
.map((a) => (a.key ? `${a.key}: ${a.value}` : a.value))
|
|
177
|
-
.join(", ")
|
|
178
|
-
output += `(${argsStr})
|
|
252
|
+
.join(", ")
|
|
253
|
+
output += `(${argsStr})`
|
|
179
254
|
}
|
|
180
|
-
output += "` "
|
|
255
|
+
output += "` "
|
|
181
256
|
}
|
|
182
|
-
output += "\n\n"
|
|
257
|
+
output += "\n\n"
|
|
183
258
|
}
|
|
184
259
|
} catch {
|
|
185
260
|
// Ignorar errores de parseo en la extracción
|
|
186
261
|
}
|
|
187
262
|
}
|
|
188
263
|
}
|
|
189
|
-
}
|
|
264
|
+
}
|
|
190
265
|
|
|
191
|
-
walkDir(baseDir)
|
|
266
|
+
walkDir(baseDir)
|
|
192
267
|
|
|
193
268
|
// Agregar estructura de carpetas al final
|
|
194
|
-
output += "# Estructura de Carpetas\n\n"
|
|
195
|
-
output += "```text\n"
|
|
196
|
-
output += `${basename(baseDir)}/\n
|
|
197
|
-
output += generateTree(baseDir, "", configIgnores)
|
|
198
|
-
output += "```\n"
|
|
269
|
+
output += "# Estructura de Carpetas\n\n"
|
|
270
|
+
output += "```text\n"
|
|
271
|
+
output += `${basename(baseDir)}/\n`
|
|
272
|
+
output += generateTree(baseDir, "", configIgnores)
|
|
273
|
+
output += "```\n"
|
|
199
274
|
|
|
200
275
|
// Escribir archivo
|
|
201
|
-
const outputPath = join(baseDir, outputFile)
|
|
202
|
-
writeFileSync(outputPath, output, "utf-8")
|
|
276
|
+
const outputPath = join(baseDir, outputFile)
|
|
277
|
+
writeFileSync(outputPath, output, "utf-8")
|
|
203
278
|
|
|
204
|
-
console.log(chalk.green(`✅ Contexto extraído: ${fileCount} archivos`))
|
|
205
|
-
console.log(chalk.green(`📄 Output: ${outputFile}`))
|
|
279
|
+
console.log(chalk.green(`✅ Contexto extraído: ${fileCount} archivos`))
|
|
280
|
+
console.log(chalk.green(`📄 Output: ${outputFile}`))
|
|
206
281
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
|
|
2
|
+
import { join, relative, extname, basename, dirname } from "path"
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import { getLanguageIndex, getLanguagePath, getLanguages } from "../utils/language-loader.js"
|
|
5
|
+
import { findAllFiles, findComponentFiles, analyzeImports } from "../utils/file-utils.js"
|
|
6
|
+
|
|
7
|
+
const REUSE_THRESHOLD = 2 // minimum times a component must be imported to qualify
|
|
8
|
+
|
|
9
|
+
export async function extractTemplates(sourceDir, options) {
|
|
10
|
+
const cwd = process.cwd()
|
|
11
|
+
const srcPath = join(cwd, sourceDir || "src")
|
|
12
|
+
const langId = options.lang || "react"
|
|
13
|
+
const minReuse = parseInt(options.minReuse) || REUSE_THRESHOLD
|
|
14
|
+
const dryRun = options.dryRun || false
|
|
15
|
+
|
|
16
|
+
const langPath = getLanguagePath(langId)
|
|
17
|
+
if (!langPath) {
|
|
18
|
+
console.log(chalk.red(`\n❌ Language module "${langId}" not found\n`))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!existsSync(srcPath)) {
|
|
23
|
+
console.log(chalk.red(`\n❌ Source directory not found: ${srcPath}\n`))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.cyan(`\n🔍 Scanning ${srcPath} for reusable components...\n`))
|
|
28
|
+
|
|
29
|
+
// 1. Find all component files
|
|
30
|
+
const componentFiles = findComponentFiles(srcPath)
|
|
31
|
+
if (componentFiles.length === 0) {
|
|
32
|
+
console.log(chalk.yellow(" No component files found.\n"))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.gray(` Found ${componentFiles.length} component files\n`))
|
|
37
|
+
|
|
38
|
+
// 2. Analyze imports to calculate reuse
|
|
39
|
+
const importMap = analyzeImports(srcPath, componentFiles)
|
|
40
|
+
const usageCount = new Map()
|
|
41
|
+
|
|
42
|
+
for (const [, imports] of importMap) {
|
|
43
|
+
for (const imp of imports) {
|
|
44
|
+
usageCount.set(imp, (usageCount.get(imp) || 0) + 1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Rank by reuse percentage
|
|
49
|
+
const totalFiles = componentFiles.length
|
|
50
|
+
const ranked = componentFiles
|
|
51
|
+
.map((f) => {
|
|
52
|
+
const name = basename(f).replace(/\.(tsx|ts)$/, "")
|
|
53
|
+
const imports = usageCount.get(name) || 0
|
|
54
|
+
const reuse = totalFiles > 0 ? (imports / totalFiles) * 100 : 0
|
|
55
|
+
return { file: f, name, imports, reuse }
|
|
56
|
+
})
|
|
57
|
+
.sort((a, b) => b.reuse - a.reuse)
|
|
58
|
+
|
|
59
|
+
// 4. Filter by min reuse
|
|
60
|
+
const candidates = ranked.filter((c) => c.imports >= minReuse)
|
|
61
|
+
|
|
62
|
+
console.log(chalk.blue(`\n Reuse analysis:\n`))
|
|
63
|
+
for (const c of ranked.slice(0, 10)) {
|
|
64
|
+
const bar =
|
|
65
|
+
"█".repeat(Math.round(c.reuse / 5)) + "░".repeat(Math.max(0, 20 - Math.round(c.reuse / 5)))
|
|
66
|
+
const marker = c.imports >= minReuse ? chalk.green("✅") : chalk.gray("⬜")
|
|
67
|
+
console.log(
|
|
68
|
+
` ${marker} ${chalk.bold(c.name)} — ${c.imports}/${totalFiles} imports (${c.reuse.toFixed(0)}%)`
|
|
69
|
+
)
|
|
70
|
+
console.log(` ${bar}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (candidates.length === 0) {
|
|
74
|
+
console.log(
|
|
75
|
+
chalk.yellow(`\n No components meet the minimum reuse threshold (${minReuse}+ imports).\n`)
|
|
76
|
+
)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.green(
|
|
82
|
+
`\n ${candidates.length} components qualify for extraction (≥${minReuse} imports)\n`
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// 5. Extract to language module lib/
|
|
87
|
+
const libDir = join(langPath, "lib")
|
|
88
|
+
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
mkdirSync(libDir, { recursive: true })
|
|
91
|
+
|
|
92
|
+
// Load existing index to update
|
|
93
|
+
const index = getLanguageIndex(langId) || {
|
|
94
|
+
language: langId,
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
updated: new Date().toISOString().split("T")[0],
|
|
97
|
+
categories: {},
|
|
98
|
+
templates: [],
|
|
99
|
+
errorsLearned: [],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Ensure lib category exists
|
|
103
|
+
if (!index.categories) index.categories = {}
|
|
104
|
+
if (!index.categories["lib/extracted"]) {
|
|
105
|
+
index.categories["lib/extracted"] = {
|
|
106
|
+
name: "Extracted from Projects",
|
|
107
|
+
description: "Components automatically extracted from existing projects",
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
const content = readFileSync(c.file, "utf-8")
|
|
113
|
+
const ext = extname(c.file) || ".tsx"
|
|
114
|
+
const destName = `${c.name}.template${ext}`
|
|
115
|
+
const destPath = join(libDir, destName)
|
|
116
|
+
|
|
117
|
+
// Add @extracted annotation if not present
|
|
118
|
+
const annotated = content.startsWith("// @extracted")
|
|
119
|
+
? content
|
|
120
|
+
: `// @extracted\n// @source: ${relative(cwd, c.file)}\n// @reuse: ${c.reuse.toFixed(0)}% (${c.imports}/${totalFiles} files)\n${content}`
|
|
121
|
+
|
|
122
|
+
writeFileSync(destPath, annotated, "utf-8")
|
|
123
|
+
console.log(chalk.green(` ✅ Extracted: ${c.name} → lib/${destName}`))
|
|
124
|
+
|
|
125
|
+
// Add to index
|
|
126
|
+
const existingIdx = index.templates.findIndex((t) => t.id === c.name)
|
|
127
|
+
const entry = {
|
|
128
|
+
id: c.name,
|
|
129
|
+
name: c.name,
|
|
130
|
+
description: `Auto-extracted from ${relative(cwd, c.file)}`,
|
|
131
|
+
category: "lib/extracted",
|
|
132
|
+
purposes: ["reusable"],
|
|
133
|
+
file: `lib/${destName}`,
|
|
134
|
+
testFile: null,
|
|
135
|
+
variants: [],
|
|
136
|
+
sizes: [],
|
|
137
|
+
darkMode: false,
|
|
138
|
+
responsive: false,
|
|
139
|
+
dependencies: [],
|
|
140
|
+
tags: ["extracted"],
|
|
141
|
+
hasTeachMe: false,
|
|
142
|
+
fixHistory: [],
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (existingIdx >= 0) {
|
|
146
|
+
index.templates[existingIdx] = entry
|
|
147
|
+
} else {
|
|
148
|
+
index.templates.push(entry)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Update INDEX.json
|
|
153
|
+
index.updated = new Date().toISOString().split("T")[0]
|
|
154
|
+
writeFileSync(join(langPath, "INDEX.json"), JSON.stringify(index, null, 2), "utf-8")
|
|
155
|
+
console.log(chalk.green(`\n ✅ INDEX.json updated with ${candidates.length} new templates`))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(
|
|
159
|
+
chalk.cyan(`\n Done. ${candidates.length} components extracted to ${relative(cwd, libDir)}/\n`)
|
|
160
|
+
)
|
|
161
|
+
console.log(chalk.gray(` Run: npx openPrompt-Lang component list to see them`))
|
|
162
|
+
console.log(
|
|
163
|
+
chalk.gray(` Run: npx openPrompt-Lang component add <name> --template to use them\n`)
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function extractAnalyze(sourceDir, options) {
|
|
168
|
+
const cwd = process.cwd()
|
|
169
|
+
const srcPath = join(cwd, sourceDir || "src")
|
|
170
|
+
|
|
171
|
+
if (!existsSync(srcPath)) {
|
|
172
|
+
console.log(chalk.red(`\n❌ Source directory not found: ${srcPath}\n`))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(chalk.cyan(`\n📊 Analyzing project structure: ${srcPath}\n`))
|
|
177
|
+
|
|
178
|
+
const allFiles = findAllFiles(srcPath, {
|
|
179
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".css", ".scss"],
|
|
180
|
+
})
|
|
181
|
+
const tsFiles = allFiles.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"))
|
|
182
|
+
const cssFiles = allFiles.filter((f) => f.endsWith(".css") || f.endsWith(".scss"))
|
|
183
|
+
const totalLines = allFiles.reduce((acc, f) => acc + countLines(f), 0)
|
|
184
|
+
|
|
185
|
+
// Component analysis
|
|
186
|
+
const componentFiles = findComponentFiles(srcPath)
|
|
187
|
+
const importMap = analyzeImports(srcPath, componentFiles)
|
|
188
|
+
|
|
189
|
+
let totalImports = 0
|
|
190
|
+
let externalDeps = new Set()
|
|
191
|
+
for (const [, imports] of importMap) {
|
|
192
|
+
totalImports += imports.length
|
|
193
|
+
for (const imp of imports) {
|
|
194
|
+
if (imp.includes("/") || imp.includes(".")) continue // skip relative
|
|
195
|
+
externalDeps.add(imp)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const anyCount = allFiles.filter((f) => {
|
|
200
|
+
try {
|
|
201
|
+
const content = readFileSync(f, "utf-8")
|
|
202
|
+
return content.includes(": any") || content.includes("as any")
|
|
203
|
+
} catch {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
}).length
|
|
207
|
+
|
|
208
|
+
console.log(chalk.cyan(`\n 📊 Project Stats:\n`))
|
|
209
|
+
console.log(` ${chalk.bold("Total files:")} ${allFiles.length}`)
|
|
210
|
+
console.log(` ${chalk.bold("TypeScript:")} ${tsFiles.length}`)
|
|
211
|
+
console.log(` ${chalk.bold("CSS/SCSS:")} ${cssFiles.length}`)
|
|
212
|
+
console.log(` ${chalk.bold("Total lines:")} ${totalLines}`)
|
|
213
|
+
console.log(` ${chalk.bold("Components:")} ${componentFiles.length}`)
|
|
214
|
+
console.log(` ${chalk.bold("Total imports:")} ${totalImports}`)
|
|
215
|
+
console.log(` ${chalk.bold("External deps:")} ${externalDeps.size}`)
|
|
216
|
+
console.log(
|
|
217
|
+
` ${chalk.bold("Files with any:")} ${anyCount} ${anyCount > 0 ? chalk.red("⚠️") : chalk.green("✅")}`
|
|
218
|
+
)
|
|
219
|
+
console.log("")
|
|
220
|
+
|
|
221
|
+
// Top external deps
|
|
222
|
+
const sortedDeps = [...externalDeps].sort()
|
|
223
|
+
if (sortedDeps.length > 0) {
|
|
224
|
+
console.log(chalk.gray(` External dependencies:\n`))
|
|
225
|
+
for (const dep of sortedDeps.slice(0, 15)) {
|
|
226
|
+
console.log(` 📦 ${dep}`)
|
|
227
|
+
}
|
|
228
|
+
if (sortedDeps.length > 15) {
|
|
229
|
+
console.log(chalk.gray(` ... and ${sortedDeps.length - 15} more\n`))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
function countLines(file) {
|
|
237
|
+
try {
|
|
238
|
+
return readFileSync(file, "utf-8").split("\n").length
|
|
239
|
+
} catch {
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/commands/figma.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { writeFileSync, existsSync, mkdirSync } from "fs"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import chalk from "chalk"
|
|
4
|
-
import inquirer from "inquirer"
|
|
5
|
-
import { generateFigmaPrompt } from "../generators/figma-prompt.js"
|
|
1
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import inquirer from "inquirer"
|
|
5
|
+
import { generateFigmaPrompt } from "../generators/figma-prompt.js"
|
|
6
6
|
|
|
7
7
|
export async function figma() {
|
|
8
|
-
console.log(chalk.cyan("\n🎨 Generador de Prompt para Figma\n"))
|
|
8
|
+
console.log(chalk.cyan("\n🎨 Generador de Prompt para Figma\n"))
|
|
9
9
|
|
|
10
10
|
const answers = await inquirer.prompt([
|
|
11
11
|
{
|
|
@@ -44,20 +44,20 @@ export async function figma() {
|
|
|
44
44
|
message: "¿Incluir anti-patrones a evitar?",
|
|
45
45
|
default: true,
|
|
46
46
|
},
|
|
47
|
-
])
|
|
47
|
+
])
|
|
48
48
|
|
|
49
|
-
const promptContent = generateFigmaPrompt(answers)
|
|
49
|
+
const promptContent = generateFigmaPrompt(answers)
|
|
50
50
|
|
|
51
51
|
// Guardar en docs/PROMPTS/
|
|
52
|
-
const promptsDir = join(process.cwd(), "docs", "PROMPTS")
|
|
52
|
+
const promptsDir = join(process.cwd(), "docs", "PROMPTS")
|
|
53
53
|
if (!existsSync(promptsDir)) {
|
|
54
|
-
mkdirSync(promptsDir, { recursive: true })
|
|
54
|
+
mkdirSync(promptsDir, { recursive: true })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const outputPath = join(promptsDir, "figma-prompt.md")
|
|
58
|
-
writeFileSync(outputPath, promptContent, "utf-8")
|
|
57
|
+
const outputPath = join(promptsDir, "figma-prompt.md")
|
|
58
|
+
writeFileSync(outputPath, promptContent, "utf-8")
|
|
59
59
|
|
|
60
|
-
console.log(chalk.green(`\n✅ Prompt Figma generado:`))
|
|
61
|
-
console.log(chalk.cyan(` ${outputPath}\n`))
|
|
62
|
-
console.log(promptContent)
|
|
60
|
+
console.log(chalk.green(`\n✅ Prompt Figma generado:`))
|
|
61
|
+
console.log(chalk.cyan(` ${outputPath}\n`))
|
|
62
|
+
console.log(promptContent)
|
|
63
63
|
}
|