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,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", "node_modules", "dist", "build", ".next", ".gemini",
9
- "coverage", ".nyc_output", "__pycache__", ".cache",
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", "yarn.lock", ".env.local", ".env",
14
- "contexto.md", "proyecto_completo.md",
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", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".webp",
19
- ".pdf", ".woff", ".woff2", ".ttf", ".eot", ".otf",
20
- ".mp4", ".mp3", ".avi", ".mov",
21
- ".zip", ".tar", ".gz", ".rar",
22
- ".exe", ".dll", ".so", ".dylib",
23
- ".db", ".sqlite", ".sqlite3",
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", ".tsx", ".js", ".jsx", ".json", ".css", ".html",
28
- ".md", ".mdx", ".py", ".rs", ".toml", ".yaml", ".yml",
29
- ".sh", ".bash", ".zsh", ".env.example",
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.includes(ig))) return false;
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", ".tsx": "tsx", ".js": "javascript",
153
- ".jsx": "jsx", ".json": "json", ".css": "css",
154
- ".html": "html", ".md": "markdown", ".mdx": "mdx",
155
- ".py": "python", ".rs": "rust", ".toml": "toml",
156
- ".yaml": "yaml", ".yml": "yaml", ".sh": "bash",
157
- };
158
- const lang = langMap[ext] || "";
159
-
160
- output += `### Archivo: ${relPath}\n`;
161
- output += `\`\`\`${lang}\n`;
162
- output += content;
163
- if (!content.endsWith("\n")) output += "\n";
164
- output += "```\n\n";
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
+ }
@@ -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
  }