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