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
@@ -0,0 +1,184 @@
1
+ import { readFileSync } from "fs"
2
+ import { join } from "path"
3
+ import { getLanguageIndex, getLanguagePath } from "../utils/language-loader.js"
4
+
5
+ export function buildPrompt(description, options = {}) {
6
+ const langId = options.lang || "react"
7
+ const index = getLanguageIndex(langId)
8
+ const profile = options.profile || "mid"
9
+
10
+ const relevantTemplates = findRelevantTemplates(description, index)
11
+
12
+ let templateExamples = ""
13
+ if (relevantTemplates.length > 0) {
14
+ templateExamples = "\n\n## Relevant templates for reference:\n\n"
15
+ for (const t of relevantTemplates.slice(0, 3)) {
16
+ const content = loadTemplateContent(t, langId)
17
+ if (content) {
18
+ templateExamples += `### ${t.id}\n\`\`\`tsx\n${content.slice(0, 800)}\n\`\`\`\n\n`
19
+ }
20
+ }
21
+ }
22
+
23
+ const kind = detectKind(description)
24
+ const componentName = options.name || suggestName(description, kind)
25
+
26
+ const systemPrompt = getSystemPrompt(langId, profile)
27
+
28
+ const userPrompt = [
29
+ `Generate a ${langId} component based on this description:`,
30
+ "",
31
+ description,
32
+ "",
33
+ `## Requirements:`,
34
+ `- Name: ${componentName}`,
35
+ `- Kind: ${kind}`,
36
+ `- Profile: ${profile}`,
37
+ `- Include @use() annotation at the top with the tags you use`,
38
+ `- Include @kind, @props (if component), @contract (if hook/service), @limit, @test annotations`,
39
+ templateExamples,
40
+ `## Language: ${langId}`,
41
+ `## Profile rules:`,
42
+ ...getProfileRules(profile),
43
+ "",
44
+ `Return ONLY the code, no markdown wrapping or explanation.`,
45
+ ].join("\n")
46
+
47
+ return { componentName, kind, systemPrompt, userPrompt, prompt: userPrompt }
48
+ }
49
+
50
+ function findRelevantTemplates(description, index) {
51
+ if (!index?.templates) return []
52
+
53
+ const desc = description.toLowerCase()
54
+ const keywords = desc.split(/\W+/).filter(Boolean)
55
+
56
+ const scored = index.templates.map((t) => {
57
+ const text = [t.name, t.description, ...(t.tags || []), t.category].join(" ").toLowerCase()
58
+ const score = keywords.reduce((sum, kw) => sum + (text.includes(kw) ? 1 : 0), 0)
59
+ return { template: t, score }
60
+ })
61
+
62
+ return scored
63
+ .filter((s) => s.score > 0)
64
+ .sort((a, b) => b.score - a.score)
65
+ .map((s) => s.template)
66
+ }
67
+
68
+ function loadTemplateContent(template, langId) {
69
+ try {
70
+ const langPath = getLanguagePath(langId)
71
+ if (!langPath) return null
72
+ const filePath = join(langPath, template.file)
73
+ return readFileSync(filePath, "utf-8")
74
+ } catch {
75
+ return null
76
+ }
77
+ }
78
+
79
+ function detectKind(description) {
80
+ const desc = description.toLowerCase()
81
+ const words = desc.split(/\W+/).filter(Boolean)
82
+ const wordSet = new Set(words)
83
+
84
+ // Order matters: more specific matches first
85
+ if (hasAny(words, ["page", "screen", "view", "route"])) return "page"
86
+ if (hasAny(words, ["hook", "auth", "session", "fetch", "query", "mutation"])) return "hook"
87
+ if (hasAny(words, ["service", "api", "client", "http", "axios"])) return "service"
88
+ if (hasAny(words, ["store", "context", "provider"])) return "store"
89
+ if (hasAny(words, ["layout", "sidebar", "navbar", "footer", "shell"])) return "layout"
90
+ if (hasAny(words, ["button", "input", "card", "modal", "dialog", "form", "table"]))
91
+ return "component"
92
+
93
+ return "component"
94
+ }
95
+
96
+ function hasAny(words, targets) {
97
+ return targets.some((t) => words.includes(t))
98
+ }
99
+
100
+ const STOP_WORDS = new Set([
101
+ "a",
102
+ "an",
103
+ "the",
104
+ "with",
105
+ "for",
106
+ "and",
107
+ "or",
108
+ "to",
109
+ "of",
110
+ "in",
111
+ "on",
112
+ "at",
113
+ "by",
114
+ "is",
115
+ "it",
116
+ ])
117
+
118
+ function suggestName(description, kind) {
119
+ const words = description
120
+ .split(/\W+/)
121
+ .filter(Boolean)
122
+ .filter((w) => !STOP_WORDS.has(w.toLowerCase()))
123
+ .slice(0, 3)
124
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
125
+
126
+ if (words.length === 0) words.push("Component")
127
+
128
+ const prefixes = {
129
+ hook: "use",
130
+ component: "",
131
+ page: "",
132
+ service: "create",
133
+ store: "use",
134
+ layout: "",
135
+ util: "",
136
+ type: "",
137
+ }
138
+
139
+ const baseName = words.join("")
140
+ const prefix = prefixes[kind] || ""
141
+
142
+ if (prefix) return `${prefix}${baseName}`
143
+ if (baseName.endsWith("Component") || baseName.endsWith("Page") || kind === "hook")
144
+ return baseName
145
+ const suffix = kind === "hook" ? "" : kind === "page" ? "" : "Component"
146
+ return `${baseName}${suffix}`
147
+ }
148
+
149
+ function getSystemPrompt(langId, profile) {
150
+ const base = `You are an expert ${langId} developer using openPrompt-Lang annotations.`
151
+
152
+ const profilePrompts = {
153
+ senior:
154
+ "Generate clean, production-ready code with strict TypeScript, no any types, and full annotations.",
155
+ mid: "Generate balanced code with TypeScript and basic annotations.",
156
+ junior: "Generate simple, well-documented code with plenty of comments and guidance.",
157
+ }
158
+
159
+ return `${base}\n\n${profilePrompts[profile] || profilePrompts.mid}`
160
+ }
161
+
162
+ function getProfileRules(profile) {
163
+ const rules = {
164
+ senior: [
165
+ "- No 'any' types — use 'unknown' + type narrowing",
166
+ "- Every file must have @use(), @kind, @limit annotations",
167
+ "- Extract interfaces/types to separate files if > 5 props",
168
+ "- Include error boundaries for components",
169
+ ],
170
+ mid: [
171
+ "- Prefer specific types over 'any'",
172
+ "- Include @kind and @props annotations",
173
+ "- Basic error handling",
174
+ ],
175
+ junior: [
176
+ "- Add explanatory comments for complex logic",
177
+ "- Include PropTypes or basic type validation",
178
+ "- Add TODO comments for future improvements",
179
+ "- Add console.error in catch blocks",
180
+ ],
181
+ }
182
+
183
+ return rules[profile] || rules.mid
184
+ }
@@ -0,0 +1,247 @@
1
+ // ─── AI Provider abstraction for openPrompt-Lang ────────────────────────────
2
+ // Each provider must implement: generate(prompt, options) => Promise<string>
3
+
4
+ const PROVIDERS = {}
5
+
6
+ export function registerProvider(name, provider) {
7
+ PROVIDERS[name] = provider
8
+ }
9
+
10
+ export function getProvider(name) {
11
+ return PROVIDERS[name]
12
+ }
13
+
14
+ export function listProviders() {
15
+ return Object.keys(PROVIDERS)
16
+ }
17
+
18
+ export async function generateCode(prompt, options = {}) {
19
+ const providerName = options.provider || "check env"
20
+ const resolvedProvider = resolveProvider(providerName)
21
+
22
+ const provider = getProvider(resolvedProvider)
23
+ if (!provider) {
24
+ throw new Error(`Provider "${resolvedProvider}" not available. Install it first.`)
25
+ }
26
+
27
+ return provider.generate(prompt, options)
28
+ }
29
+
30
+ function resolveProvider(preference) {
31
+ if (preference !== "check env") return preference
32
+
33
+ if (process.env.OPENAI_API_KEY || process.env.OPENPROMPT_API_KEY) return "openai"
34
+ if (process.env.ANTHROPIC_API_KEY) return "anthropic"
35
+ if (process.env.OLLAMA_HOST) return "ollama"
36
+
37
+ return "none"
38
+ }
39
+
40
+ // ─── Provider: Ollama (local, free) ─────────────────────────────────────────
41
+
42
+ registerProvider("ollama", {
43
+ name: "Ollama",
44
+ description: "Local LLM via Ollama (free, no API key needed)",
45
+ requiresKey: false,
46
+
47
+ async generate(prompt, options = {}) {
48
+ const host = process.env.OLLAMA_HOST || "http://localhost:11434"
49
+ const model = options.model || "codellama:7b"
50
+
51
+ try {
52
+ const resp = await fetch(`${host}/api/generate`, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({
56
+ model,
57
+ prompt,
58
+ stream: false,
59
+ options: {
60
+ temperature: options.temperature ?? 0.3,
61
+ max_tokens: options.maxTokens ?? 2048,
62
+ },
63
+ }),
64
+ })
65
+
66
+ if (!resp.ok) throw new Error(`Ollama error: ${resp.status}`)
67
+
68
+ const data = await resp.json()
69
+ return data.response || ""
70
+ } catch (err) {
71
+ throw new Error(`Ollama request failed: ${err.message}`)
72
+ }
73
+ },
74
+ })
75
+
76
+ // ─── Provider: OpenAI ────────────────────────────────────────────────────────
77
+
78
+ registerProvider("openai", {
79
+ name: "OpenAI",
80
+ description: "GPT-4o / GPT-4 / GPT-3.5 via OpenAI API",
81
+ requiresKey: true,
82
+
83
+ async generate(prompt, options = {}) {
84
+ const apiKey = process.env.OPENAI_API_KEY || process.env.OPENPROMPT_API_KEY
85
+ if (!apiKey) throw new Error("OPENAI_API_KEY or OPENPROMPT_API_KEY not set")
86
+
87
+ const model = options.model || "gpt-4o"
88
+ const apiUrl = options.apiUrl || "https://api.openai.com/v1/chat/completions"
89
+
90
+ try {
91
+ const resp = await fetch(apiUrl, {
92
+ method: "POST",
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ Authorization: `Bearer ${apiKey}`,
96
+ },
97
+ body: JSON.stringify({
98
+ model,
99
+ messages: [
100
+ {
101
+ role: "system",
102
+ content:
103
+ options.systemPrompt ||
104
+ "You are an expert TypeScript/React developer. Generate clean, production-ready code with openPrompt-Lang annotations.",
105
+ },
106
+ { role: "user", content: prompt },
107
+ ],
108
+ temperature: options.temperature ?? 0.3,
109
+ max_tokens: options.maxTokens ?? 4096,
110
+ }),
111
+ })
112
+
113
+ if (!resp.ok) {
114
+ const err = await resp.text()
115
+ throw new Error(`OpenAI error ${resp.status}: ${err}`)
116
+ }
117
+
118
+ const data = await resp.json()
119
+ return data.choices?.[0]?.message?.content || ""
120
+ } catch (err) {
121
+ throw new Error(`OpenAI request failed: ${err.message}`)
122
+ }
123
+ },
124
+ })
125
+
126
+ // ─── Provider: Anthropic ─────────────────────────────────────────────────────
127
+
128
+ registerProvider("anthropic", {
129
+ name: "Anthropic",
130
+ description: "Claude 3.5 Sonnet / Haiku via Anthropic API",
131
+ requiresKey: true,
132
+
133
+ async generate(prompt, options = {}) {
134
+ const apiKey = process.env.ANTHROPIC_API_KEY
135
+ if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set")
136
+
137
+ const model = options.model || "claude-sonnet-4-20250514"
138
+ const apiUrl = "https://api.anthropic.com/v1/messages"
139
+
140
+ try {
141
+ const resp = await fetch(apiUrl, {
142
+ method: "POST",
143
+ headers: {
144
+ "Content-Type": "application/json",
145
+ "x-api-key": apiKey,
146
+ "anthropic-version": "2023-06-01",
147
+ },
148
+ body: JSON.stringify({
149
+ model,
150
+ max_tokens: options.maxTokens ?? 4096,
151
+ system:
152
+ options.systemPrompt ||
153
+ "You are an expert TypeScript/React developer. Generate clean, production-ready code with openPrompt-Lang annotations.",
154
+ messages: [{ role: "user", content: prompt }],
155
+ }),
156
+ })
157
+
158
+ if (!resp.ok) {
159
+ const err = await resp.text()
160
+ throw new Error(`Anthropic error ${resp.status}: ${err}`)
161
+ }
162
+
163
+ const data = await resp.json()
164
+ return data.content?.[0]?.text || ""
165
+ } catch (err) {
166
+ throw new Error(`Anthropic request failed: ${err.message}`)
167
+ }
168
+ },
169
+ })
170
+
171
+ // ─── Provider: Mock (for testing / dry-run) ──────────────────────────────────
172
+
173
+ registerProvider("mock", {
174
+ name: "Mock",
175
+ description: "Local mock that returns a placeholder component (no API needed)",
176
+ requiresKey: false,
177
+
178
+ async generate(prompt, options = {}) {
179
+ const componentName = options.componentName || "GeneratedComponent"
180
+
181
+ return `// @generated by openPrompt-Lang ai-gen (mock provider)
182
+ // @use(@kind, @props, @limit)
183
+ // @kind(component)
184
+ // @props({ variant: "'primary' | 'secondary'", size: "'sm' | 'md' | 'lg'" })
185
+ // @limit(lines: 60)
186
+ // @test(coverage: 80)
187
+
188
+ import { type VariantProps, cva } from "class-variance-authority"
189
+ import { cn } from "@/lib/utils"
190
+
191
+ const ${componentName}Variants = cva("", {
192
+ variants: {
193
+ variant: {
194
+ primary: "bg-blue-500 text-white hover:bg-blue-600",
195
+ secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
196
+ },
197
+ size: {
198
+ sm: "px-2 py-1 text-sm",
199
+ md: "px-4 py-2 text-base",
200
+ lg: "px-6 py-3 text-lg",
201
+ },
202
+ },
203
+ defaultVariants: {
204
+ variant: "primary",
205
+ size: "md",
206
+ },
207
+ })
208
+
209
+ export interface ${componentName}Props
210
+ extends React.HTMLAttributes<HTMLDivElement>,
211
+ VariantProps<typeof ${componentName}Variants> {}
212
+
213
+ export function ${componentName}({
214
+ className,
215
+ variant,
216
+ size,
217
+ ...props
218
+ }: ${componentName}Props) {
219
+ return (
220
+ <div
221
+ className={cn(${componentName}Variants({ variant, size }), className)}
222
+ {...props}
223
+ />
224
+ )
225
+ }
226
+ `
227
+ },
228
+ })
229
+
230
+ // ─── No provider available ──────────────────────────────────────────────────
231
+
232
+ registerProvider("none", {
233
+ name: "None",
234
+ description: "No AI provider configured",
235
+ requiresKey: false,
236
+
237
+ async generate() {
238
+ throw new Error(
239
+ "No AI provider configured.\n\n" +
240
+ "Set one of:\n" +
241
+ " OPENAI_API_KEY → uses GPT-4o\n" +
242
+ " ANTHROPIC_API_KEY → uses Claude 3.5\n" +
243
+ " OLLAMA_HOST → uses local Ollama (default: http://localhost:11434)\n\n" +
244
+ "Or use --provider mock for a dry-run.\n"
245
+ )
246
+ },
247
+ })
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "fs"
2
+ import { createRequire } from "module"
3
+ import { fileURLToPath } from "url"
4
+ import { dirname, join } from "path"
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const BUILTIN_TAGS = JSON.parse(readFileSync(join(__dirname, "tags.json"), "utf-8"))
8
+
9
+ export function getBuiltinTags() {
10
+ return [...BUILTIN_TAGS]
11
+ }
12
+
13
+ export function getAnnotationTags(langId, projectConfig) {
14
+ const tags = new Set(BUILTIN_TAGS)
15
+
16
+ // Load from language module if available
17
+ if (langId) {
18
+ try {
19
+ const require = createRequire(import.meta.url)
20
+ const modPath = require.resolve(`../templates/langs/${langId}/MODULE.json`)
21
+ const mod = JSON.parse(readFileSync(modPath, "utf-8"))
22
+ if (mod.annotationTags) {
23
+ for (const t of mod.annotationTags) tags.add(t)
24
+ }
25
+ } catch {}
26
+ }
27
+
28
+ // Load from project config
29
+ if (projectConfig?.annotations?.customTags) {
30
+ for (const t of projectConfig.annotations.customTags) tags.add(t)
31
+ }
32
+
33
+ return [...tags]
34
+ }
35
+
36
+ export function makeTagPattern(tags) {
37
+ const escaped = tags.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
38
+ return new RegExp(`^\\s*@(${escaped.join("|")})\\(`)
39
+ }
@@ -0,0 +1,24 @@
1
+ [
2
+ "use",
3
+ "kind",
4
+ "contract",
5
+ "props",
6
+ "state",
7
+ "limit",
8
+ "forbidden",
9
+ "compose",
10
+ "deps",
11
+ "platform",
12
+ "scope",
13
+ "test",
14
+ "meta",
15
+ "pattern",
16
+ "learn-error",
17
+ "goodPractice",
18
+ "badPractice",
19
+ "teachMe",
20
+ "template",
21
+ "source",
22
+ "reuse",
23
+ "extracted"
24
+ ]
@@ -0,0 +1,161 @@
1
+ import { writeFileSync, existsSync, mkdirSync, readFileSync } from "fs"
2
+ import { join, relative } from "path"
3
+ import chalk from "chalk"
4
+ import inquirer from "inquirer"
5
+ import { getLanguageIndex } from "../utils/language-loader.js"
6
+ import { buildPrompt } from "../ai/prompt-builder.js"
7
+ import { generateCode, listProviders, getProvider } from "../ai/providers.js"
8
+
9
+ export async function aiGen(options) {
10
+ const cwd = process.cwd()
11
+ const langId = options.lang || "react"
12
+ const profile = options.profile || "mid"
13
+ const provider =
14
+ options.provider ||
15
+ (process.env.OPENAI_API_KEY ? "openai" : process.env.ANTHROPIC_API_KEY ? "anthropic" : "mock")
16
+
17
+ console.log(chalk.cyan("\nšŸ¤– openPrompt-Lang AI Generator\n"))
18
+ console.log(chalk.gray(` Provider: ${provider}`))
19
+ console.log(chalk.gray(` Language: ${langId}`))
20
+ console.log(chalk.gray(` Profile: ${profile}\n`))
21
+
22
+ // Get description
23
+ let description = options.description
24
+ if (!description) {
25
+ console.log(chalk.blue(" Describe what you want to generate:"))
26
+ const resp = await inquirer.prompt([
27
+ {
28
+ type: "input",
29
+ name: "description",
30
+ message: "Description:",
31
+ validate: (input) => input.trim().length >= 10 || "Describe in at least 10 characters",
32
+ },
33
+ ])
34
+ description = resp.description
35
+ }
36
+
37
+ // Build prompt
38
+ const promptResult = buildPrompt(description, {
39
+ lang: langId,
40
+ profile,
41
+ name: options.name,
42
+ })
43
+
44
+ console.log(chalk.gray(`\n Detected kind: ${promptResult.kind}`))
45
+ console.log(chalk.gray(` Suggested name: ${promptResult.componentName}`))
46
+
47
+ if (options.verbose) {
48
+ console.log(chalk.gray("\n Prompt preview:\n"))
49
+ console.log(chalk.gray(promptResult.prompt.slice(0, 600) + "...\n"))
50
+ }
51
+
52
+ // Generate
53
+ console.log(chalk.blue(`\n Generating with ${provider}...\n`))
54
+
55
+ let code
56
+ try {
57
+ code = await generateCode(promptResult.prompt, {
58
+ provider,
59
+ componentName: promptResult.componentName,
60
+ systemPrompt: promptResult.systemPrompt,
61
+ temperature: options.temperature ?? 0.3,
62
+ maxTokens: options.maxTokens ?? 4096,
63
+ })
64
+ } catch (err) {
65
+ console.log(chalk.red(`\n āŒ Generation failed: ${err.message}\n`))
66
+ console.log(
67
+ chalk.gray(" Available providers:"),
68
+ chalk.cyan(
69
+ listProviders()
70
+ .filter((p) => p !== "none")
71
+ .join(", ")
72
+ )
73
+ )
74
+ console.log(chalk.gray(" Try: --provider mock (for dry-run without API key)\n"))
75
+ return
76
+ }
77
+
78
+ // Clean code (remove markdown code fences if present)
79
+ code = cleanGeneratedCode(code)
80
+
81
+ // Preview
82
+ console.log(chalk.green(" āœ… Generated:\n"))
83
+ console.log(code.slice(0, 500) + (code.length > 500 ? "\n ...\n" : "\n"))
84
+
85
+ // Confirm and write
86
+ const outputDir = options.output ? join(cwd, options.output) : join(cwd, "src", "components")
87
+
88
+ mkdirSync(outputDir, { recursive: true })
89
+ const outputFile = join(outputDir, `${promptResult.componentName}.tsx`)
90
+
91
+ if (existsSync(outputFile) && !options.force) {
92
+ const { overwrite } = await inquirer.prompt([
93
+ {
94
+ type: "confirm",
95
+ name: "overwrite",
96
+ message: `File ${relative(cwd, outputFile)} exists. Overwrite?`,
97
+ default: false,
98
+ },
99
+ ])
100
+ if (!overwrite) {
101
+ console.log(chalk.yellow(" ā­ļø Skipped.\n"))
102
+ return
103
+ }
104
+ }
105
+
106
+ writeFileSync(outputFile, code, "utf-8")
107
+ console.log(chalk.green(` āœ… Written: ${relative(cwd, outputFile)}\n`))
108
+
109
+ // Validate if possible
110
+ if (options.validate) {
111
+ console.log(chalk.blue(" Running validation...\n"))
112
+ const { lintFile } = await import("../utils/annotations.js")
113
+ const result = lintFile(code, true)
114
+ if (result.errors.length > 0) {
115
+ console.log(chalk.yellow(" Validation issues:\n"))
116
+ for (const err of result.errors) console.log(` āš ļø ${err}`)
117
+ console.log("")
118
+ } else {
119
+ console.log(chalk.green(" āœ… Validation passed.\n"))
120
+ }
121
+ }
122
+
123
+ // Suggest next steps
124
+ console.log(chalk.cyan(" šŸ“‹ Next steps:\n"))
125
+ console.log(
126
+ ` npx openPrompt-Lang teach ${promptResult.componentName.toLowerCase()} --lang ${langId}`
127
+ )
128
+ console.log(` npx openPrompt-Lang qa-gen --lang ${langId}`)
129
+ console.log(` npx openPrompt-Lang extract src/ --min-reuse 2\n`)
130
+ }
131
+
132
+ function cleanGeneratedCode(code) {
133
+ // Remove markdown code fences
134
+ code = code.replace(/^```[\w]*\n/gm, "")
135
+ code = code.replace(/```\s*$/gm, "")
136
+ return code.trim()
137
+ }
138
+
139
+ export async function aiProviders() {
140
+ const providers = listProviders()
141
+ console.log(chalk.cyan("\nšŸ”Œ AI Providers:\n"))
142
+ for (const name of providers) {
143
+ const p = getProvider(name)
144
+ const keyStatus = p.requiresKey
145
+ ? name === "openai"
146
+ ? process.env.OPENAI_API_KEY
147
+ ? "āœ… key found"
148
+ : "āŒ no key"
149
+ : name === "anthropic"
150
+ ? process.env.ANTHROPIC_API_KEY
151
+ ? "āœ… key found"
152
+ : "āŒ no key"
153
+ : ""
154
+ : "āœ… no key required"
155
+ console.log(` ${chalk.bold(name)} — ${p.description}`)
156
+ console.log(` ${keyStatus}`)
157
+ console.log("")
158
+ }
159
+
160
+ console.log(chalk.gray(" Set provider with --provider flag or environment variables\n"))
161
+ }