@getcoherent/cli 0.1.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 +69 -0
- package/bin/coherent +2 -0
- package/dist/backup-DRQUCHM5.js +11 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-N2S7FM57.js +101 -0
- package/dist/claude-QTRMGJ56.js +363 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10814 -0
- package/dist/openai-provider-VKSRXDLD.js +353 -0
- package/package.json +59 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import "./chunk-3RG5ZIWI.js";
|
|
2
|
+
|
|
3
|
+
// src/utils/openai-provider.ts
|
|
4
|
+
import { validateConfig } from "@getcoherent/core";
|
|
5
|
+
var OpenAIClient = class _OpenAIClient {
|
|
6
|
+
client;
|
|
7
|
+
defaultModel;
|
|
8
|
+
constructor(apiKey, model, OpenAIModule) {
|
|
9
|
+
if (!OpenAIModule) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"OpenAI package not installed. Install it with:\n npm install openai"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
this.client = new OpenAIModule({ apiKey });
|
|
15
|
+
this.defaultModel = model || "gpt-4-turbo-preview";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Factory method for creating OpenAIClient
|
|
19
|
+
*/
|
|
20
|
+
static async create(apiKey, model) {
|
|
21
|
+
const OpenAI = await import("openai").catch(() => null);
|
|
22
|
+
if (!OpenAI) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"OpenAI package not installed. Install it with:\n npm install openai"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return new _OpenAIClient(apiKey, model, OpenAI.default || OpenAI);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate design system config from discovery results
|
|
31
|
+
*/
|
|
32
|
+
async generateConfig(discovery) {
|
|
33
|
+
try {
|
|
34
|
+
const prompt = this.buildConfigPrompt(discovery);
|
|
35
|
+
const response = await this.client.chat.completions.create({
|
|
36
|
+
model: this.defaultModel,
|
|
37
|
+
messages: [
|
|
38
|
+
{
|
|
39
|
+
role: "system",
|
|
40
|
+
content: this.getSystemPrompt()
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
role: "user",
|
|
44
|
+
content: prompt
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
response_format: { type: "json_object" },
|
|
48
|
+
// Force JSON output
|
|
49
|
+
temperature: 0.7,
|
|
50
|
+
max_tokens: 4096
|
|
51
|
+
});
|
|
52
|
+
const content = response.choices[0]?.message?.content;
|
|
53
|
+
if (!content) {
|
|
54
|
+
throw new Error("Empty response from OpenAI API");
|
|
55
|
+
}
|
|
56
|
+
const jsonText = this.extractJSON(content);
|
|
57
|
+
const config = JSON.parse(jsonText);
|
|
58
|
+
return validateConfig(config);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error?.status || error?.message?.includes("OpenAI")) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`OpenAI API error (${error.status}): ${error.message}
|
|
63
|
+
Please check your API key and try again.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (error instanceof Error) {
|
|
67
|
+
throw new Error(`Failed to generate config: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error("Unknown error occurred while generating config");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse modification request from natural language
|
|
74
|
+
*/
|
|
75
|
+
async parseModification(prompt) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await this.client.chat.completions.create({
|
|
78
|
+
model: this.defaultModel,
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: "system",
|
|
82
|
+
content: `You are a design system modification parser.
|
|
83
|
+
Parse natural language requests into structured ModificationRequest JSON.
|
|
84
|
+
Always check component registry before creating new components.
|
|
85
|
+
Return valid JSON only. Use: { "requests": [ ... ], "uxRecommendations": "optional markdown" }
|
|
86
|
+
CRITICAL: All string values in JSON must be on one line. Escape double quotes inside strings with \\". Do not include unescaped newlines or quotes in string values.`
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
role: "user",
|
|
90
|
+
content: prompt
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
response_format: { type: "json_object" },
|
|
94
|
+
temperature: 0.3,
|
|
95
|
+
// Lower temperature for more structured output
|
|
96
|
+
max_tokens: 16384
|
|
97
|
+
});
|
|
98
|
+
if (response.choices[0]?.finish_reason === "length") {
|
|
99
|
+
const err = new Error("AI response truncated (max_tokens reached)");
|
|
100
|
+
err.code = "RESPONSE_TRUNCATED";
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
const content = response.choices[0]?.message?.content;
|
|
104
|
+
if (!content) {
|
|
105
|
+
throw new Error("Empty response from OpenAI API");
|
|
106
|
+
}
|
|
107
|
+
const jsonText = this.extractJSON(content);
|
|
108
|
+
const parsed = JSON.parse(jsonText);
|
|
109
|
+
const requests = Array.isArray(parsed) ? parsed : parsed.requests || parsed.modifications || [];
|
|
110
|
+
if (!Array.isArray(requests)) {
|
|
111
|
+
throw new Error("Expected array of ModificationRequest objects");
|
|
112
|
+
}
|
|
113
|
+
const uxRecommendations = Array.isArray(parsed) ? void 0 : typeof parsed.uxRecommendations === "string" && parsed.uxRecommendations.trim() ? parsed.uxRecommendations.trim() : void 0;
|
|
114
|
+
return { requests, uxRecommendations };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code === "RESPONSE_TRUNCATED") throw error;
|
|
117
|
+
if (error?.status || error?.message?.includes("OpenAI")) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`OpenAI API error (${error.status}): ${error.message}
|
|
120
|
+
Please check your API key and try again.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (error instanceof Error) {
|
|
124
|
+
throw new Error(`Failed to parse modification: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
throw new Error("Unknown error occurred while parsing modification");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Test API connection
|
|
131
|
+
*/
|
|
132
|
+
async testConnection() {
|
|
133
|
+
try {
|
|
134
|
+
await this.client.chat.completions.create({
|
|
135
|
+
model: this.defaultModel,
|
|
136
|
+
messages: [{ role: "user", content: 'Say "OK"' }],
|
|
137
|
+
max_tokens: 10
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build prompt for config generation
|
|
146
|
+
*/
|
|
147
|
+
buildConfigPrompt(discovery) {
|
|
148
|
+
const featuresList = Object.entries(discovery.features).filter(([_, enabled]) => enabled).map(([name]) => name).join(", ") || "none";
|
|
149
|
+
return `Generate a complete DesignSystemConfig JSON for the following project:
|
|
150
|
+
|
|
151
|
+
Project Type: ${discovery.projectType}
|
|
152
|
+
App Type: ${discovery.appType}
|
|
153
|
+
Audience: ${discovery.audience}
|
|
154
|
+
Visual Style: ${discovery.visualStyle}
|
|
155
|
+
Primary Color: ${discovery.primaryColor}
|
|
156
|
+
Dark Mode: ${discovery.darkMode ? "Yes" : "No"}
|
|
157
|
+
Features: ${featuresList}
|
|
158
|
+
${discovery.additionalRequirements ? `Additional Requirements: ${discovery.additionalRequirements}` : ""}
|
|
159
|
+
|
|
160
|
+
Requirements:
|
|
161
|
+
- Use 8pt grid system for spacing (0.25rem, 0.5rem, 1rem, 1.5rem, 2rem, 3rem, 4rem)
|
|
162
|
+
- Ensure WCAG AA contrast (4.5:1 for text) for all colors
|
|
163
|
+
- Generate dark mode colors based on primary color ${discovery.primaryColor}
|
|
164
|
+
- Include commonly needed components for ${discovery.projectType} projects
|
|
165
|
+
- Set appType to "${discovery.appType}" in settings
|
|
166
|
+
- Enable stateManagement if SPA or authentication is needed
|
|
167
|
+
- Use semantic color naming (primary, secondary, success, warning, error, info)
|
|
168
|
+
- Set createdAt and updatedAt to current ISO timestamp
|
|
169
|
+
|
|
170
|
+
Output ONLY valid JSON matching DesignSystemConfig schema. Do not include markdown code blocks or explanations.`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get system prompt
|
|
174
|
+
*/
|
|
175
|
+
getSystemPrompt() {
|
|
176
|
+
return `You are a design system architect expert. Your task is to generate complete, valid DesignSystemConfig JSON objects.
|
|
177
|
+
|
|
178
|
+
Key principles:
|
|
179
|
+
- Always generate valid JSON that matches the DesignSystemConfig schema
|
|
180
|
+
- Use semantic color tokens (primary, secondary, etc.) not hardcoded values
|
|
181
|
+
- Ensure all required fields are present
|
|
182
|
+
- Generate realistic, production-ready configurations
|
|
183
|
+
- Follow 8pt grid system for spacing
|
|
184
|
+
- Ensure WCAG AA contrast compliance
|
|
185
|
+
- Include appropriate components for the project type
|
|
186
|
+
|
|
187
|
+
Return ONLY the JSON object, no markdown, no code blocks, no explanations.`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Extract JSON from response (handles markdown code blocks)
|
|
191
|
+
*/
|
|
192
|
+
extractJSON(text) {
|
|
193
|
+
let jsonText = text.trim();
|
|
194
|
+
if (jsonText.startsWith("```")) {
|
|
195
|
+
const lines = jsonText.split("\n");
|
|
196
|
+
lines.shift();
|
|
197
|
+
if (lines[lines.length - 1].trim() === "```") {
|
|
198
|
+
lines.pop();
|
|
199
|
+
}
|
|
200
|
+
jsonText = lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
return jsonText.trim();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Edit shared component code by instruction (Epic 2).
|
|
206
|
+
*/
|
|
207
|
+
async editSharedComponentCode(currentCode, instruction, componentName) {
|
|
208
|
+
const response = await this.client.chat.completions.create({
|
|
209
|
+
model: this.defaultModel,
|
|
210
|
+
messages: [
|
|
211
|
+
{
|
|
212
|
+
role: "system",
|
|
213
|
+
content: "Return only the raw TSX code, no markdown fences, no explanation."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
role: "user",
|
|
217
|
+
content: `You are a React/Next.js component editor. Update the following component according to the user's instruction.
|
|
218
|
+
|
|
219
|
+
Component name: ${componentName}
|
|
220
|
+
|
|
221
|
+
Current code:
|
|
222
|
+
\`\`\`tsx
|
|
223
|
+
${currentCode}
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
Instruction: ${instruction}
|
|
227
|
+
|
|
228
|
+
Rules: Preserve "use client" if present. Use Tailwind and shadcn/ui patterns. Return ONLY the complete updated component code.`
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
max_tokens: 8192,
|
|
232
|
+
temperature: 0.3
|
|
233
|
+
});
|
|
234
|
+
const content = response.choices[0]?.message?.content;
|
|
235
|
+
if (!content) throw new Error("Empty response from OpenAI");
|
|
236
|
+
return content.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Edit existing page code by instruction. Returns full modified page code.
|
|
240
|
+
*/
|
|
241
|
+
async editPageCode(currentCode, instruction, pageName, designConstraints) {
|
|
242
|
+
const constraintBlock = designConstraints ? `
|
|
243
|
+
Design constraints (follow unless user explicitly overrides):
|
|
244
|
+
${designConstraints}
|
|
245
|
+
` : "";
|
|
246
|
+
const response = await this.client.chat.completions.create({
|
|
247
|
+
model: this.defaultModel,
|
|
248
|
+
messages: [
|
|
249
|
+
{
|
|
250
|
+
role: "system",
|
|
251
|
+
content: "Return only the raw TSX code, no markdown, no comments before or after."
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
role: "user",
|
|
255
|
+
content: `You are a React/Next.js page editor. Modify the existing page according to the user's instruction.
|
|
256
|
+
|
|
257
|
+
Page name: ${pageName}
|
|
258
|
+
${constraintBlock}
|
|
259
|
+
Current code:
|
|
260
|
+
\`\`\`tsx
|
|
261
|
+
${currentCode}
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
Instruction: ${instruction}
|
|
265
|
+
|
|
266
|
+
CRITICAL RULES:
|
|
267
|
+
- Return the COMPLETE modified page code. Do NOT return partial code or snippets.
|
|
268
|
+
- Preserve "use client" if present. Do NOT add export const metadata if "use client" is present.
|
|
269
|
+
- Keep ALL existing content, structure, and functionality UNLESS the instruction says to change it.
|
|
270
|
+
- If the user specifies exact CSS classes or colors \u2014 use them exactly, even if they conflict with design constraints.
|
|
271
|
+
- Use Tailwind CSS and shadcn/ui patterns.
|
|
272
|
+
- Return ONLY the raw TSX code, no markdown fence, no explanation.`
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
max_tokens: 16384,
|
|
276
|
+
temperature: 0.3
|
|
277
|
+
});
|
|
278
|
+
const content = response.choices[0]?.message?.content;
|
|
279
|
+
if (!content) throw new Error("Empty response from OpenAI");
|
|
280
|
+
return content.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Story 2.11: Replace inline block on page with shared component import and usage.
|
|
284
|
+
*/
|
|
285
|
+
async replaceInlineWithShared(pageCode, sharedComponentCode, sharedComponentName, blockHint) {
|
|
286
|
+
const kebab = sharedComponentName.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase()).replace(/^-/, "");
|
|
287
|
+
const hint = blockHint ? ` Identify the block that corresponds to: "${blockHint}".` : "";
|
|
288
|
+
const response = await this.client.chat.completions.create({
|
|
289
|
+
model: this.defaultModel,
|
|
290
|
+
messages: [
|
|
291
|
+
{
|
|
292
|
+
role: "system",
|
|
293
|
+
content: "Return only the raw TSX page code, no markdown fences, no explanation."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
role: "user",
|
|
297
|
+
content: `You are a React/Next.js page editor. Replace an INLINE block on the page with the shared component "${sharedComponentName}".
|
|
298
|
+
|
|
299
|
+
PAGE CODE (find and replace one block):
|
|
300
|
+
\`\`\`tsx
|
|
301
|
+
${pageCode}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
SHARED COMPONENT (use this instead of the inline block):
|
|
305
|
+
\`\`\`tsx
|
|
306
|
+
${sharedComponentCode}
|
|
307
|
+
\`\`\`
|
|
308
|
+
|
|
309
|
+
Tasks:
|
|
310
|
+
1.${hint} Find the inline block that matches or is similar to the shared component.
|
|
311
|
+
2. Add import: import { ${sharedComponentName} } from '@/components/shared/${kebab}'
|
|
312
|
+
3. Replace the inline block with <${sharedComponentName} /> (or with props if needed).
|
|
313
|
+
4. Return the COMPLETE updated page code. Preserve "use client" if present.`
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
max_tokens: 8192,
|
|
317
|
+
temperature: 0.3
|
|
318
|
+
});
|
|
319
|
+
const content = response.choices[0]?.message?.content;
|
|
320
|
+
if (!content) throw new Error("Empty response from OpenAI");
|
|
321
|
+
return content.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
|
|
322
|
+
}
|
|
323
|
+
async extractBlockAsComponent(pageCode, blockHint, componentName) {
|
|
324
|
+
const response = await this.client.chat.completions.create({
|
|
325
|
+
model: this.defaultModel,
|
|
326
|
+
messages: [
|
|
327
|
+
{
|
|
328
|
+
role: "system",
|
|
329
|
+
content: "Return only the raw TSX code for the extracted component, no markdown fences, no explanation."
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
role: "user",
|
|
333
|
+
content: `Extract ONE section from the page code into a standalone React component.
|
|
334
|
+
|
|
335
|
+
PAGE CODE:
|
|
336
|
+
\`\`\`tsx
|
|
337
|
+
${pageCode}
|
|
338
|
+
\`\`\`
|
|
339
|
+
|
|
340
|
+
Block to extract: "${blockHint}". Create component named ${componentName}. Export function ${componentName}(). Include "use client" if it uses hooks. Include needed imports. Return ONLY the component file content.`
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
max_tokens: 4096,
|
|
344
|
+
temperature: 0.3
|
|
345
|
+
});
|
|
346
|
+
const content = response.choices[0]?.message?.content;
|
|
347
|
+
if (!content) throw new Error("Empty response from OpenAI");
|
|
348
|
+
return content.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
export {
|
|
352
|
+
OpenAIClient
|
|
353
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getcoherent/cli",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "CLI interface for Coherent Design Method",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"coherent": "./bin/coherent"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"bin",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/skovtun/coherent-design-method.git",
|
|
22
|
+
"directory": "packages/cli"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"design-system",
|
|
26
|
+
"cli",
|
|
27
|
+
"frontend",
|
|
28
|
+
"ai",
|
|
29
|
+
"code-generation",
|
|
30
|
+
"nextjs",
|
|
31
|
+
"react",
|
|
32
|
+
"coherent"
|
|
33
|
+
],
|
|
34
|
+
"author": "Coherent Design Method",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@getcoherent/core": "0.1.0",
|
|
44
|
+
"@anthropic-ai/sdk": "^0.32.0",
|
|
45
|
+
"chalk": "^5.3.0",
|
|
46
|
+
"commander": "^11.1.0",
|
|
47
|
+
"dotenv": "^16.4.5",
|
|
48
|
+
"open": "^10.1.0",
|
|
49
|
+
"ora": "^7.0.1",
|
|
50
|
+
"prompts": "^2.4.2",
|
|
51
|
+
"zod": "^3.22.4",
|
|
52
|
+
"chokidar": "^4.0.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^20.11.0",
|
|
56
|
+
"tsup": "^8.0.1",
|
|
57
|
+
"typescript": "^5.3.3"
|
|
58
|
+
}
|
|
59
|
+
}
|