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
|
@@ -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
|
+
}
|