@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 ADDED
@@ -0,0 +1,69 @@
1
+ # @coherent/cli
2
+
3
+ CLI for **Coherent Design Method** — generate and maintain Next.js apps with a single design-system config.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @coherent/cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # In an empty directory (or new repo)
15
+ coherent init
16
+ npm install
17
+ coherent preview
18
+ ```
19
+
20
+ - **App:** http://localhost:3000
21
+ - **Design System:** http://localhost:3000/design-system
22
+ - **Docs:** http://localhost:3000/design-system/docs
23
+
24
+ ## Commands
25
+
26
+ | Command | Description |
27
+ |---------|-------------|
28
+ | `coherent init` | Create project: config, app, design-system viewer, docs. Non-interactive (optional provider flag). |
29
+ | `coherent chat "<request>"` | Parse NL, update config, regenerate pages/components/nav (e.g. add page, change tokens). |
30
+ | `coherent preview` | Start Next.js dev server. |
31
+ | `coherent export` | Production build; optional Vercel/Netlify config. |
32
+ | `coherent status` | Print config summary (pages, components). |
33
+ | `coherent components` | List registered components. |
34
+
35
+ ## Two Workflows
36
+
37
+ After `coherent init` you can:
38
+
39
+ 1. **Work in Cursor/IDE** — Edit `design-system.config.ts`, `app/`, `components/`; hot reload. Best for fine-grained control.
40
+ 2. **Use `coherent chat`** — e.g. `coherent chat "add pricing page"` to scaffold pages/components and sync config/nav. Best for fast generation.
41
+
42
+ **Tip:** Use chat for structure, then Cursor for details. Commit before each `coherent chat` and run `git diff` after.
43
+
44
+ ## Examples (chat)
45
+
46
+ ```bash
47
+ coherent chat "add dashboard page with stats cards"
48
+ coherent chat "add Button, Card, Input from shadcn"
49
+ coherent chat "add contact page with form"
50
+ coherent chat "change primary color to #6366f1"
51
+ ```
52
+
53
+ ## API Key
54
+
55
+ For `coherent init` (discovery) and `coherent chat` you need an AI provider key:
56
+
57
+ - **Anthropic (default):** `ANTHROPIC_API_KEY`
58
+ - **OpenAI:** `OPENAI_API_KEY`
59
+
60
+ Put in `.env` in the project root or export in the shell. In Cursor, keys are often auto-detected.
61
+
62
+ ## Docs
63
+
64
+ - Root repo: [README](../../README.md) and [QUICK_REFERENCE.md](../../QUICK_REFERENCE.md)
65
+ - Project context and workflows: [CONTEXT.md](../../CONTEXT.md) §7
66
+
67
+ ## License
68
+
69
+ MIT
package/bin/coherent ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js')
@@ -0,0 +1,11 @@
1
+ import {
2
+ createBackup,
3
+ listBackups,
4
+ restoreBackup
5
+ } from "./chunk-N2S7FM57.js";
6
+ import "./chunk-3RG5ZIWI.js";
7
+ export {
8
+ createBackup,
9
+ listBackups,
10
+ restoreBackup
11
+ };
@@ -0,0 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ export {
9
+ __require
10
+ };
@@ -0,0 +1,101 @@
1
+ // src/utils/backup.ts
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync, copyFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ var BACKUP_DIR = ".coherent/backups";
5
+ function findFiles(dir, suffix) {
6
+ if (!existsSync(dir)) return [];
7
+ const results = [];
8
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
9
+ const full = join(dir, entry.name);
10
+ if (entry.isDirectory()) {
11
+ results.push(...findFiles(full, suffix));
12
+ } else if (entry.name.endsWith(suffix)) {
13
+ results.push(full);
14
+ }
15
+ }
16
+ return results;
17
+ }
18
+ var MAX_BACKUPS = 10;
19
+ function getBackupRoot(projectRoot) {
20
+ return join(projectRoot, BACKUP_DIR);
21
+ }
22
+ function createBackup(projectRoot, message) {
23
+ const backupRoot = getBackupRoot(projectRoot);
24
+ const id = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
25
+ const backupDir = join(backupRoot, id);
26
+ mkdirSync(backupDir, { recursive: true });
27
+ const filesToBackup = [
28
+ "design-system.config.ts",
29
+ "app/layout.tsx",
30
+ ...findFiles(join(projectRoot, "app"), "page.tsx").map((f) => f.slice(projectRoot.length + 1)),
31
+ ...findFiles(join(projectRoot, "components", "shared"), ".tsx").map((f) => f.slice(projectRoot.length + 1))
32
+ ];
33
+ const backedUp = [];
34
+ for (const relPath of filesToBackup) {
35
+ const src = join(projectRoot, relPath);
36
+ if (!existsSync(src)) continue;
37
+ const dest = join(backupDir, relPath);
38
+ mkdirSync(dirname(dest), { recursive: true });
39
+ copyFileSync(src, dest);
40
+ backedUp.push(relPath);
41
+ }
42
+ const manifest = {
43
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
44
+ message,
45
+ files: backedUp
46
+ };
47
+ writeFileSync(join(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2));
48
+ pruneOldBackups(backupRoot);
49
+ return id;
50
+ }
51
+ function restoreBackup(projectRoot, backupId) {
52
+ const backupRoot = getBackupRoot(projectRoot);
53
+ if (!existsSync(backupRoot)) return null;
54
+ const id = backupId || getLatestBackupId(backupRoot);
55
+ if (!id) return null;
56
+ const backupDir = join(backupRoot, id);
57
+ const manifestPath = join(backupDir, "manifest.json");
58
+ if (!existsSync(manifestPath)) return null;
59
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
60
+ for (const relPath of manifest.files) {
61
+ const src = join(backupDir, relPath);
62
+ const dest = join(projectRoot, relPath);
63
+ if (!existsSync(src)) continue;
64
+ mkdirSync(dirname(dest), { recursive: true });
65
+ copyFileSync(src, dest);
66
+ }
67
+ return manifest;
68
+ }
69
+ function listBackups(projectRoot) {
70
+ const backupRoot = getBackupRoot(projectRoot);
71
+ if (!existsSync(backupRoot)) return [];
72
+ const dirs = readdirSync(backupRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
73
+ const results = [];
74
+ for (const id of dirs) {
75
+ const manifestPath = join(backupRoot, id, "manifest.json");
76
+ if (!existsSync(manifestPath)) continue;
77
+ try {
78
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
79
+ results.push({ id, manifest });
80
+ } catch {
81
+ }
82
+ }
83
+ return results;
84
+ }
85
+ function getLatestBackupId(backupRoot) {
86
+ const dirs = readdirSync(backupRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
87
+ return dirs[dirs.length - 1];
88
+ }
89
+ function pruneOldBackups(backupRoot) {
90
+ const dirs = readdirSync(backupRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
91
+ while (dirs.length > MAX_BACKUPS) {
92
+ const oldest = dirs.shift();
93
+ rmSync(join(backupRoot, oldest), { recursive: true, force: true });
94
+ }
95
+ }
96
+
97
+ export {
98
+ createBackup,
99
+ restoreBackup,
100
+ listBackups
101
+ };
@@ -0,0 +1,363 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
3
+ // src/utils/claude.ts
4
+ import Anthropic from "@anthropic-ai/sdk";
5
+ import { validateConfig } from "@getcoherent/core";
6
+ var ClaudeClient = class _ClaudeClient {
7
+ client;
8
+ defaultModel;
9
+ constructor(apiKey, model) {
10
+ const key = apiKey || process.env.ANTHROPIC_API_KEY;
11
+ if (!key) {
12
+ throw new Error(
13
+ "ANTHROPIC_API_KEY not found in environment.\nPlease set it in your .env file or export it:\n export ANTHROPIC_API_KEY=your_key_here"
14
+ );
15
+ }
16
+ this.client = new Anthropic({ apiKey: key });
17
+ this.defaultModel = model || process.env.CLAUDE_MODEL || "claude-sonnet-4-20250514";
18
+ }
19
+ /**
20
+ * Factory method for creating ClaudeClient
21
+ */
22
+ static create(apiKey, model) {
23
+ return new _ClaudeClient(apiKey, model);
24
+ }
25
+ /**
26
+ * Generate design system config from discovery results
27
+ */
28
+ async generateConfig(discovery) {
29
+ try {
30
+ const prompt = this.buildConfigPrompt(discovery);
31
+ const response = await this.client.messages.create({
32
+ model: this.defaultModel,
33
+ max_tokens: 4096,
34
+ messages: [
35
+ {
36
+ role: "user",
37
+ content: prompt
38
+ }
39
+ ],
40
+ system: this.getSystemPrompt()
41
+ });
42
+ const content = response.content[0];
43
+ if (content.type !== "text") {
44
+ throw new Error("Unexpected response type from Claude API");
45
+ }
46
+ const jsonText = this.extractJSON(content.text);
47
+ const config = JSON.parse(jsonText);
48
+ return validateConfig(config);
49
+ } catch (error) {
50
+ if (error instanceof Anthropic.APIError) {
51
+ if (error.status === 404 && error.error?.type === "not_found_error") {
52
+ throw new Error(
53
+ `\u274C Model not found: ${this.defaultModel}
54
+
55
+ The specified Claude model is not available.
56
+ Try setting a different model:
57
+ export CLAUDE_MODEL=claude-sonnet-4-20250514
58
+ Or use the default model by removing CLAUDE_MODEL from your environment.`
59
+ );
60
+ }
61
+ throw new Error(
62
+ `Claude 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
+ * Build prompt for config generation
74
+ */
75
+ buildConfigPrompt(discovery) {
76
+ const featuresList = Object.entries(discovery.features).filter(([_, enabled]) => enabled).map(([name]) => name).join(", ") || "none";
77
+ return `Generate a complete DesignSystemConfig JSON for the following project:
78
+
79
+ Project Type: ${discovery.projectType}
80
+ App Type: ${discovery.appType}
81
+ Audience: ${discovery.audience}
82
+ Visual Style: ${discovery.visualStyle}
83
+ Primary Color: ${discovery.primaryColor}
84
+ Dark Mode: ${discovery.darkMode ? "Yes" : "No"}
85
+ Features: ${featuresList}
86
+ ${discovery.additionalRequirements ? `Additional Requirements: ${discovery.additionalRequirements}` : ""}
87
+
88
+ Requirements:
89
+ - Use 8pt grid system for spacing (0.25rem, 0.5rem, 1rem, 1.5rem, 2rem, 3rem, 4rem)
90
+ - Ensure WCAG AA contrast (4.5:1 for text) for all colors
91
+ - Generate dark mode colors based on primary color ${discovery.primaryColor}
92
+ - Include commonly needed components for ${discovery.projectType} projects
93
+ - Set appType to "${discovery.appType}" in settings
94
+ - Enable stateManagement if SPA or authentication is needed
95
+ - Use semantic color naming (primary, secondary, success, warning, error, info)
96
+ - Set createdAt and updatedAt to current ISO timestamp
97
+
98
+ Output ONLY valid JSON matching DesignSystemConfig schema. Do not include markdown code blocks or explanations.`;
99
+ }
100
+ /**
101
+ * Get system prompt for Claude
102
+ */
103
+ getSystemPrompt() {
104
+ return `You are a design system architect expert. Your task is to generate complete, valid DesignSystemConfig JSON objects.
105
+
106
+ Key principles:
107
+ - Always generate valid JSON that matches the DesignSystemConfig schema
108
+ - Use semantic color tokens (primary, secondary, etc.) not hardcoded values
109
+ - Ensure all required fields are present
110
+ - Generate realistic, production-ready configurations
111
+ - Follow 8pt grid system for spacing
112
+ - Ensure WCAG AA contrast compliance
113
+ - Include appropriate components for the project type
114
+
115
+ Return ONLY the JSON object, no markdown, no code blocks, no explanations.`;
116
+ }
117
+ /**
118
+ * Extract JSON from Claude's response (handles markdown code blocks)
119
+ */
120
+ extractJSON(text) {
121
+ let jsonText = text.trim();
122
+ if (jsonText.startsWith("```")) {
123
+ const lines = jsonText.split("\n");
124
+ lines.shift();
125
+ if (lines[lines.length - 1].trim() === "```") {
126
+ lines.pop();
127
+ }
128
+ jsonText = lines.join("\n");
129
+ }
130
+ return jsonText.trim();
131
+ }
132
+ /**
133
+ * Test API connection
134
+ */
135
+ async testConnection() {
136
+ try {
137
+ await this.client.messages.create({
138
+ model: this.defaultModel,
139
+ max_tokens: 10,
140
+ messages: [
141
+ {
142
+ role: "user",
143
+ content: 'Say "OK"'
144
+ }
145
+ ]
146
+ });
147
+ return true;
148
+ } catch (error) {
149
+ return false;
150
+ }
151
+ }
152
+ /**
153
+ * Parse modification request from natural language
154
+ */
155
+ async parseModification(prompt) {
156
+ try {
157
+ const response = await this.client.messages.create({
158
+ model: this.defaultModel,
159
+ max_tokens: 16384,
160
+ messages: [
161
+ {
162
+ role: "user",
163
+ content: prompt
164
+ }
165
+ ],
166
+ system: `You are a design system modification parser.
167
+ Parse natural language requests into structured ModificationRequest JSON.
168
+ Always check component registry before creating new components.
169
+ Return valid JSON only, no markdown. Use: { "requests": [ ... ], "uxRecommendations": "optional markdown" }
170
+ 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.`
171
+ });
172
+ if (response.stop_reason === "max_tokens") {
173
+ const err = new Error("AI response truncated (max_tokens reached)");
174
+ err.code = "RESPONSE_TRUNCATED";
175
+ throw err;
176
+ }
177
+ const content = response.content[0];
178
+ if (content.type !== "text") {
179
+ throw new Error("Unexpected response type from Claude API");
180
+ }
181
+ const jsonText = this.extractJSON(content.text);
182
+ const parsed = JSON.parse(jsonText);
183
+ if (Array.isArray(parsed)) {
184
+ return { requests: parsed };
185
+ }
186
+ const requests = parsed?.requests;
187
+ if (!Array.isArray(requests)) {
188
+ throw new Error('Expected "requests" array in response');
189
+ }
190
+ return {
191
+ requests,
192
+ uxRecommendations: typeof parsed.uxRecommendations === "string" && parsed.uxRecommendations.trim() ? parsed.uxRecommendations.trim() : void 0
193
+ };
194
+ } catch (error) {
195
+ if (error?.code === "RESPONSE_TRUNCATED") {
196
+ throw error;
197
+ }
198
+ if (error instanceof Anthropic.APIError) {
199
+ if (error.status === 404 && error.error?.type === "not_found_error") {
200
+ throw new Error(
201
+ `\u274C Model not found: ${this.defaultModel}
202
+
203
+ The specified Claude model is not available.
204
+ Try setting a different model:
205
+ export CLAUDE_MODEL=claude-sonnet-4-20250514
206
+ Or use the default model by removing CLAUDE_MODEL from your environment.`
207
+ );
208
+ }
209
+ throw new Error(
210
+ `Claude API error (${error.status}): ${error.message}
211
+ Please check your API key and try again.`
212
+ );
213
+ }
214
+ if (error instanceof Error) {
215
+ throw new Error(`Failed to parse modification: ${error.message}`);
216
+ }
217
+ throw new Error("Unknown error occurred while parsing modification");
218
+ }
219
+ }
220
+ /**
221
+ * Edit shared component code by instruction (Epic 2).
222
+ */
223
+ async editSharedComponentCode(currentCode, instruction, componentName) {
224
+ const response = await this.client.messages.create({
225
+ model: this.defaultModel,
226
+ max_tokens: 8192,
227
+ messages: [
228
+ {
229
+ role: "user",
230
+ content: `You are a React/Next.js component editor. Update the following component according to the user's instruction.
231
+
232
+ Component name: ${componentName}
233
+
234
+ Current code:
235
+ \`\`\`tsx
236
+ ${currentCode}
237
+ \`\`\`
238
+
239
+ Instruction: ${instruction}
240
+
241
+ Rules: Preserve "use client" if present. Use Tailwind and shadcn/ui patterns. Return ONLY the complete updated component code, no markdown fence, no explanation.`
242
+ }
243
+ ],
244
+ system: "Return only the raw TSX code, no markdown, no comments before or after."
245
+ });
246
+ const content = response.content[0];
247
+ if (content.type !== "text") throw new Error("Unexpected response type");
248
+ return content.text.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
249
+ }
250
+ /**
251
+ * Edit existing page code by instruction. Returns full modified page code.
252
+ */
253
+ async editPageCode(currentCode, instruction, pageName, designConstraints) {
254
+ const constraintBlock = designConstraints ? `
255
+ Design constraints (follow unless user explicitly overrides):
256
+ ${designConstraints}
257
+ ` : "";
258
+ const response = await this.client.messages.create({
259
+ model: this.defaultModel,
260
+ max_tokens: 16384,
261
+ messages: [
262
+ {
263
+ role: "user",
264
+ content: `You are a React/Next.js page editor. Modify the existing page according to the user's instruction.
265
+
266
+ Page name: ${pageName}
267
+ ${constraintBlock}
268
+ Current code:
269
+ \`\`\`tsx
270
+ ${currentCode}
271
+ \`\`\`
272
+
273
+ Instruction: ${instruction}
274
+
275
+ CRITICAL RULES:
276
+ - Return the COMPLETE modified page code. Do NOT return partial code or snippets.
277
+ - Preserve "use client" if present. Do NOT add export const metadata if "use client" is present.
278
+ - Keep ALL existing content, structure, and functionality UNLESS the instruction says to change it.
279
+ - If the user specifies exact CSS classes or colors \u2014 use them exactly, even if they conflict with design constraints.
280
+ - Use Tailwind CSS and shadcn/ui patterns.
281
+ - Return ONLY the raw TSX code, no markdown fence, no explanation.`
282
+ }
283
+ ],
284
+ system: "Return only the raw TSX code, no markdown, no comments before or after."
285
+ });
286
+ const content = response.content[0];
287
+ if (content.type !== "text") throw new Error("Unexpected response type");
288
+ return content.text.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
289
+ }
290
+ /**
291
+ * Story 2.11: Replace inline block on page with shared component import and usage.
292
+ */
293
+ async replaceInlineWithShared(pageCode, sharedComponentCode, sharedComponentName, blockHint) {
294
+ const hint = blockHint ? ` Identify the block that corresponds to: "${blockHint}".` : "";
295
+ const response = await this.client.messages.create({
296
+ model: this.defaultModel,
297
+ max_tokens: 8192,
298
+ messages: [
299
+ {
300
+ role: "user",
301
+ content: `You are a React/Next.js page editor. Replace an INLINE block on the page with the shared component "${sharedComponentName}".
302
+
303
+ PAGE CODE (find and replace one block):
304
+ \`\`\`tsx
305
+ ${pageCode}
306
+ \`\`\`
307
+
308
+ SHARED COMPONENT (use this instead of the inline block):
309
+ \`\`\`tsx
310
+ ${sharedComponentCode}
311
+ \`\`\`
312
+
313
+ Tasks:
314
+ 1.${hint} Find the inline block that matches or is similar to the shared component (e.g. same structure: hero, CTA, card section).
315
+ 2. Add an import at the top: import { ${sharedComponentName} } from '@/components/shared/${sharedComponentName.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase()).replace(/^-/, "")}'
316
+ (Use kebab-case file name: HeroSection \u2192 hero-section, PricingCard \u2192 pricing-card.)
317
+ 3. Replace the inline block with <${sharedComponentName} /> (or with props if the shared component accepts them and the page needs different values).
318
+ 4. Return the COMPLETE updated page code. Preserve "use client" if present. No markdown fence, no explanation.`
319
+ }
320
+ ],
321
+ system: "Return only the raw TSX page code, no markdown, no comments before or after."
322
+ });
323
+ const content = response.content[0];
324
+ if (content.type !== "text") throw new Error("Unexpected response type");
325
+ return content.text.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
326
+ }
327
+ /**
328
+ * Story 2.11 B2: Extract a block from page code as a standalone React component.
329
+ */
330
+ async extractBlockAsComponent(pageCode, blockHint, componentName) {
331
+ const response = await this.client.messages.create({
332
+ model: this.defaultModel,
333
+ max_tokens: 4096,
334
+ messages: [
335
+ {
336
+ role: "user",
337
+ content: `You are a React/Next.js refactoring assistant. Extract ONE section/block from the page code into a standalone React component.
338
+
339
+ PAGE CODE:
340
+ \`\`\`tsx
341
+ ${pageCode}
342
+ \`\`\`
343
+
344
+ Block to extract: "${blockHint}"
345
+
346
+ Requirements:
347
+ 1. Find the block that matches "${blockHint}" (e.g. CTA section, hero, feature grid).
348
+ 2. Create a new component named ${componentName} that contains ONLY that block's JSX and logic.
349
+ 3. The component must be self-contained: export function ${componentName}() { ... } with "use client" if it uses hooks.
350
+ 4. Use the same imports (Button, Card, etc.) inside the component - include any needed import statements at the top.
351
+ 5. Return ONLY the complete component file content (no markdown fence, no explanation).`
352
+ }
353
+ ],
354
+ system: "Return only the raw TSX code for the extracted component, no markdown, no explanation."
355
+ });
356
+ const content = response.content[0];
357
+ if (content.type !== "text") throw new Error("Unexpected response type");
358
+ return content.text.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
359
+ }
360
+ };
361
+ export {
362
+ ClaudeClient
363
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }