@getcoherent/cli 0.6.13 → 0.6.15
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/dist/ai-classifier-EGXPFJLN.js +42 -0
- package/dist/ai-provider-CGSIYFZT.js +13 -0
- package/dist/{chunk-CLPILU3Z.js → chunk-5AHG4NNX.js} +10 -292
- package/dist/chunk-N6H73ROO.js +1244 -0
- package/dist/chunk-PVJJ2YXP.js +325 -0
- package/dist/chunk-SPQZBQYY.js +124 -0
- package/dist/{claude-BZ3HSBD3.js → claude-K5B5ITAT.js} +1 -1
- package/dist/component-extractor-VYJLT5NR.js +56 -0
- package/dist/design-constraints-EIP2XM7T.js +43 -0
- package/dist/index.js +1349 -1993
- package/dist/{openai-provider-XUI7ZHUR.js → openai-provider-4KGARVC3.js} +1 -1
- package/dist/{plan-generator-QUESV7GS.js → plan-generator-ITHYNYJI.js} +2 -1
- package/dist/quality-validator-3K5BMJSR.js +15 -0
- package/dist/reuse-validator-HC4LZEKF.js +74 -0
- package/package.json +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CORE_CONSTRAINTS,
|
|
3
|
+
getDesignQualityForType,
|
|
4
|
+
inferPageTypeFromRoute
|
|
5
|
+
} from "./chunk-5AHG4NNX.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/chat/plan-generator.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { generateSharedComponent } from "@getcoherent/core";
|
|
13
|
+
var LAYOUT_SYNONYMS = {
|
|
14
|
+
horizontal: "header",
|
|
15
|
+
top: "header",
|
|
16
|
+
nav: "header",
|
|
17
|
+
navbar: "header",
|
|
18
|
+
topbar: "header",
|
|
19
|
+
"top-bar": "header",
|
|
20
|
+
vertical: "sidebar",
|
|
21
|
+
left: "sidebar",
|
|
22
|
+
side: "sidebar",
|
|
23
|
+
drawer: "sidebar",
|
|
24
|
+
full: "both",
|
|
25
|
+
combined: "both",
|
|
26
|
+
empty: "none",
|
|
27
|
+
minimal: "none",
|
|
28
|
+
clean: "none"
|
|
29
|
+
};
|
|
30
|
+
var PAGE_TYPE_SYNONYMS = {
|
|
31
|
+
landing: "marketing",
|
|
32
|
+
public: "marketing",
|
|
33
|
+
home: "marketing",
|
|
34
|
+
website: "marketing",
|
|
35
|
+
static: "marketing",
|
|
36
|
+
application: "app",
|
|
37
|
+
dashboard: "app",
|
|
38
|
+
admin: "app",
|
|
39
|
+
panel: "app",
|
|
40
|
+
console: "app",
|
|
41
|
+
authentication: "auth",
|
|
42
|
+
login: "auth",
|
|
43
|
+
"log-in": "auth",
|
|
44
|
+
register: "auth",
|
|
45
|
+
signin: "auth",
|
|
46
|
+
"sign-in": "auth",
|
|
47
|
+
signup: "auth",
|
|
48
|
+
"sign-up": "auth"
|
|
49
|
+
};
|
|
50
|
+
var COMPONENT_TYPE_SYNONYMS = {
|
|
51
|
+
component: "widget",
|
|
52
|
+
ui: "widget",
|
|
53
|
+
element: "widget",
|
|
54
|
+
block: "widget",
|
|
55
|
+
"page-section": "section",
|
|
56
|
+
hero: "section",
|
|
57
|
+
feature: "section",
|
|
58
|
+
area: "section",
|
|
59
|
+
nav: "navigation",
|
|
60
|
+
navbar: "navigation",
|
|
61
|
+
sidebar: "navigation",
|
|
62
|
+
menu: "navigation",
|
|
63
|
+
"data display": "data-display",
|
|
64
|
+
table: "data-display",
|
|
65
|
+
chart: "data-display",
|
|
66
|
+
card: "data-display",
|
|
67
|
+
stats: "data-display",
|
|
68
|
+
input: "form",
|
|
69
|
+
filter: "form",
|
|
70
|
+
search: "form",
|
|
71
|
+
error: "feedback",
|
|
72
|
+
alert: "feedback",
|
|
73
|
+
toast: "feedback",
|
|
74
|
+
notification: "feedback",
|
|
75
|
+
modal: "feedback",
|
|
76
|
+
dialog: "feedback"
|
|
77
|
+
};
|
|
78
|
+
function normalizeEnum(synonyms) {
|
|
79
|
+
return (v) => {
|
|
80
|
+
const trimmed = v.trim().toLowerCase();
|
|
81
|
+
return synonyms[trimmed] ?? trimmed;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
var RouteGroupSchema = z.object({
|
|
85
|
+
id: z.string(),
|
|
86
|
+
layout: z.string().transform(normalizeEnum(LAYOUT_SYNONYMS)).pipe(z.enum(["header", "sidebar", "both", "none"])),
|
|
87
|
+
pages: z.array(z.string())
|
|
88
|
+
});
|
|
89
|
+
var PlannedComponentSchema = z.object({
|
|
90
|
+
name: z.string(),
|
|
91
|
+
description: z.string().default(""),
|
|
92
|
+
props: z.string().default("{}"),
|
|
93
|
+
usedBy: z.array(z.string()).default([]),
|
|
94
|
+
type: z.string().transform(normalizeEnum(COMPONENT_TYPE_SYNONYMS)).pipe(z.enum(["layout", "navigation", "data-display", "form", "feedback", "section", "widget"])).catch("section"),
|
|
95
|
+
shadcnDeps: z.array(z.string()).default([])
|
|
96
|
+
});
|
|
97
|
+
var PageNoteSchema = z.object({
|
|
98
|
+
type: z.string().transform(normalizeEnum(PAGE_TYPE_SYNONYMS)).pipe(z.enum(["marketing", "app", "auth"])),
|
|
99
|
+
sections: z.array(z.string()).default([]),
|
|
100
|
+
links: z.record(z.string()).optional()
|
|
101
|
+
});
|
|
102
|
+
var ArchitecturePlanSchema = z.object({
|
|
103
|
+
appName: z.string().optional(),
|
|
104
|
+
groups: z.array(RouteGroupSchema),
|
|
105
|
+
sharedComponents: z.array(PlannedComponentSchema).max(8).default([]),
|
|
106
|
+
pageNotes: z.record(z.string(), PageNoteSchema).default({})
|
|
107
|
+
});
|
|
108
|
+
function routeToKey(route) {
|
|
109
|
+
return route.replace(/^\//, "") || "home";
|
|
110
|
+
}
|
|
111
|
+
function getPageGroup(route, plan) {
|
|
112
|
+
return plan.groups.find((g) => g.pages.includes(route));
|
|
113
|
+
}
|
|
114
|
+
function getPageType(route, plan) {
|
|
115
|
+
return plan.pageNotes[routeToKey(route)]?.type ?? inferPageTypeFromRoute(route);
|
|
116
|
+
}
|
|
117
|
+
var PLAN_SYSTEM_PROMPT = `You are a UI architect. Given a list of pages for a web application, create a Component Architecture Plan as JSON.
|
|
118
|
+
|
|
119
|
+
Your task:
|
|
120
|
+
1. Group pages by navigation context (e.g., public marketing pages, authenticated app pages, auth flows)
|
|
121
|
+
2. Identify reusable UI components that appear on 2+ pages
|
|
122
|
+
3. Describe each page's sections and cross-page links
|
|
123
|
+
|
|
124
|
+
Rules:
|
|
125
|
+
- Each group gets a layout type: "header" (horizontal nav), "sidebar" (vertical nav), "both", or "none" (no nav)
|
|
126
|
+
- Shared components must be genuinely reusable (appear on 2+ pages). Do NOT create a shared component for patterns used on only one page.
|
|
127
|
+
- Page types: "marketing" (landing, features, pricing \u2014 spacious, section-based), "app" (dashboard, settings \u2014 compact, data-dense), "auth" (login, register \u2014 centered card form)
|
|
128
|
+
- Component props should be a TypeScript-like interface string
|
|
129
|
+
- shadcnDeps lists the shadcn/ui atoms the component will need (e.g., "card", "badge", "avatar")
|
|
130
|
+
- Cross-page links: map link labels to target routes (e.g., {"Sign in": "/login"})
|
|
131
|
+
- Maximum 8 shared components
|
|
132
|
+
|
|
133
|
+
Respond with EXACTLY this JSON structure (use these exact field names):
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
"appName": "MyApp",
|
|
137
|
+
"groups": [
|
|
138
|
+
{ "id": "public", "layout": "header", "pages": ["/", "/pricing"] },
|
|
139
|
+
{ "id": "app", "layout": "sidebar", "pages": ["/dashboard", "/settings"] },
|
|
140
|
+
{ "id": "auth", "layout": "none", "pages": ["/login", "/register"] }
|
|
141
|
+
],
|
|
142
|
+
"sharedComponents": [
|
|
143
|
+
{
|
|
144
|
+
"name": "StatCard",
|
|
145
|
+
"description": "Displays a single metric with label and value",
|
|
146
|
+
"props": "{ label: string; value: string; icon?: React.ReactNode }",
|
|
147
|
+
"usedBy": ["/dashboard", "/projects"],
|
|
148
|
+
"type": "widget",
|
|
149
|
+
"shadcnDeps": ["card"]
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"pageNotes": {
|
|
153
|
+
"home": { "type": "marketing", "sections": ["Hero", "Features", "Pricing"], "links": { "Sign in": "/login" } },
|
|
154
|
+
"dashboard": { "type": "app", "sections": ["Stats row", "Recent tasks", "Activity feed"] },
|
|
155
|
+
"login": { "type": "auth", "sections": ["Login form"] }
|
|
156
|
+
}
|
|
157
|
+
}`;
|
|
158
|
+
async function generateArchitecturePlan(pages, userMessage, aiProvider, layoutHint) {
|
|
159
|
+
const userPrompt = `Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
160
|
+
|
|
161
|
+
User's request: "${userMessage}"
|
|
162
|
+
|
|
163
|
+
Navigation type requested: ${layoutHint || "auto-detect"}`;
|
|
164
|
+
const warnings = [];
|
|
165
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
168
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
169
|
+
if (parsed.success) return { plan: parsed.data, warnings };
|
|
170
|
+
warnings.push(
|
|
171
|
+
`Validation (attempt ${attempt + 1}): ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
172
|
+
);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
warnings.push(`Error (attempt ${attempt + 1}): ${err instanceof Error ? err.message : String(err)}`);
|
|
175
|
+
if (attempt === 1) return { plan: null, warnings };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { plan: null, warnings };
|
|
179
|
+
}
|
|
180
|
+
async function updateArchitecturePlan(existingPlan, newPages, userMessage, aiProvider) {
|
|
181
|
+
const userPrompt = `Existing plan:
|
|
182
|
+
${JSON.stringify(existingPlan, null, 2)}
|
|
183
|
+
|
|
184
|
+
New pages to integrate: ${newPages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
185
|
+
|
|
186
|
+
User's request: "${userMessage}"
|
|
187
|
+
|
|
188
|
+
Update the existing plan to include these new pages. Keep all existing groups, components, and pageNotes. Add the new pages to appropriate groups and add pageNotes for them.`;
|
|
189
|
+
try {
|
|
190
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
191
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
192
|
+
if (parsed.success) return parsed.data;
|
|
193
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
194
|
+
console.warn(chalk.dim(` Plan update validation failed: ${issues}`));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn(chalk.dim(` Plan update error: ${err instanceof Error ? err.message : String(err)}`));
|
|
197
|
+
}
|
|
198
|
+
const merged = structuredClone(existingPlan);
|
|
199
|
+
const largestGroup = merged.groups.reduce(
|
|
200
|
+
(best, g) => g.pages.length > (best?.pages.length ?? 0) ? g : best,
|
|
201
|
+
merged.groups[0]
|
|
202
|
+
);
|
|
203
|
+
for (const page of newPages) {
|
|
204
|
+
const alreadyPlaced = merged.groups.some((g) => g.pages.includes(page.route));
|
|
205
|
+
if (!alreadyPlaced && largestGroup) {
|
|
206
|
+
largestGroup.pages.push(page.route);
|
|
207
|
+
}
|
|
208
|
+
const key = routeToKey(page.route);
|
|
209
|
+
if (!merged.pageNotes[key]) {
|
|
210
|
+
merged.pageNotes[key] = { type: "app", sections: [] };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return merged;
|
|
214
|
+
}
|
|
215
|
+
var cachedPlan = null;
|
|
216
|
+
function savePlan(projectRoot, plan) {
|
|
217
|
+
cachedPlan = null;
|
|
218
|
+
const dir = resolve(projectRoot, ".coherent");
|
|
219
|
+
mkdirSync(dir, { recursive: true });
|
|
220
|
+
writeFileSync(resolve(dir, "plan.json"), JSON.stringify(plan, null, 2));
|
|
221
|
+
}
|
|
222
|
+
function loadPlan(projectRoot) {
|
|
223
|
+
const planPath = resolve(projectRoot, ".coherent", "plan.json");
|
|
224
|
+
if (cachedPlan?.path === planPath) return cachedPlan.plan;
|
|
225
|
+
if (!existsSync(planPath)) return null;
|
|
226
|
+
try {
|
|
227
|
+
const raw = JSON.parse(readFileSync(planPath, "utf-8"));
|
|
228
|
+
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
229
|
+
if (!parsed.success) return null;
|
|
230
|
+
cachedPlan = { path: planPath, plan: parsed.data };
|
|
231
|
+
return parsed.data;
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function toKebabCase(name) {
|
|
237
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
238
|
+
}
|
|
239
|
+
async function generateSharedComponentsFromPlan(plan, styleContext, projectRoot, aiProvider) {
|
|
240
|
+
if (plan.sharedComponents.length === 0) return [];
|
|
241
|
+
const componentSpecs = plan.sharedComponents.map(
|
|
242
|
+
(c) => `- ${c.name}: ${c.description}. Props: ${c.props}. Type: ${c.type}. shadcn deps: ${c.shadcnDeps.join(", ") || "none"}`
|
|
243
|
+
).join("\n");
|
|
244
|
+
const designRules = `${CORE_CONSTRAINTS}
|
|
245
|
+
${getDesignQualityForType("app")}`;
|
|
246
|
+
const prompt = `Generate React components as separate files. For EACH component below, return an add-page request with name and pageCode fields.
|
|
247
|
+
|
|
248
|
+
Components to generate:
|
|
249
|
+
${componentSpecs}
|
|
250
|
+
|
|
251
|
+
Style context: ${styleContext || "default"}
|
|
252
|
+
|
|
253
|
+
${designRules}
|
|
254
|
+
|
|
255
|
+
Requirements:
|
|
256
|
+
- Each component MUST use a NAMED export: \`export function ComponentName\` (NOT export default)
|
|
257
|
+
- Use shadcn/ui imports from @/components/ui/*
|
|
258
|
+
- Use Tailwind CSS classes matching the style context
|
|
259
|
+
- TypeScript with proper props interface
|
|
260
|
+
- Each component is a standalone file
|
|
261
|
+
|
|
262
|
+
Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentName", pageCode: "..." } }, ...] }`;
|
|
263
|
+
const results = [];
|
|
264
|
+
try {
|
|
265
|
+
const raw = await aiProvider.parseModification(prompt);
|
|
266
|
+
const requests = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
267
|
+
for (const comp of plan.sharedComponents) {
|
|
268
|
+
const match = requests.find(
|
|
269
|
+
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
270
|
+
);
|
|
271
|
+
const code = match?.changes?.pageCode;
|
|
272
|
+
if (code && (code.includes("export function") || code.includes("export default"))) {
|
|
273
|
+
const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
|
|
274
|
+
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
275
|
+
results.push({ name: comp.name, code: fixedCode, file });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
for (const comp of plan.sharedComponents) {
|
|
280
|
+
try {
|
|
281
|
+
const singlePrompt = `Generate a React component: ${comp.name} \u2014 ${comp.description}. Props: ${comp.props}. shadcn deps: ${comp.shadcnDeps.join(", ") || "none"}. Style: ${styleContext || "default"}. Return { requests: [{ type: "add-page", changes: { name: "${comp.name}", pageCode: "..." } }] }`;
|
|
282
|
+
const raw = await aiProvider.parseModification(singlePrompt);
|
|
283
|
+
const requests = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
284
|
+
const match = requests.find(
|
|
285
|
+
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
286
|
+
);
|
|
287
|
+
const code = match?.changes?.pageCode;
|
|
288
|
+
if (code && (code.includes("export function") || code.includes("export default"))) {
|
|
289
|
+
const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
|
|
290
|
+
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
291
|
+
results.push({ name: comp.name, code: fixedCode, file });
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const comp of results) {
|
|
298
|
+
const planned = plan.sharedComponents.find((c) => c.name === comp.name);
|
|
299
|
+
await generateSharedComponent(projectRoot, {
|
|
300
|
+
name: comp.name,
|
|
301
|
+
type: planned?.type ?? "section",
|
|
302
|
+
code: comp.code,
|
|
303
|
+
description: planned?.description,
|
|
304
|
+
usedIn: planned?.usedBy ?? [],
|
|
305
|
+
source: "generated",
|
|
306
|
+
overwrite: true
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return results;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export {
|
|
313
|
+
RouteGroupSchema,
|
|
314
|
+
PlannedComponentSchema,
|
|
315
|
+
PageNoteSchema,
|
|
316
|
+
ArchitecturePlanSchema,
|
|
317
|
+
routeToKey,
|
|
318
|
+
getPageGroup,
|
|
319
|
+
getPageType,
|
|
320
|
+
generateArchitecturePlan,
|
|
321
|
+
updateArchitecturePlan,
|
|
322
|
+
savePlan,
|
|
323
|
+
loadPlan,
|
|
324
|
+
generateSharedComponentsFromPlan
|
|
325
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/utils/ai-provider.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
function detectAIProvider() {
|
|
4
|
+
if (process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY) {
|
|
5
|
+
return "openai";
|
|
6
|
+
}
|
|
7
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
8
|
+
return "claude";
|
|
9
|
+
}
|
|
10
|
+
return "claude";
|
|
11
|
+
}
|
|
12
|
+
function hasAnyAPIKey() {
|
|
13
|
+
return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY || process.env.GITHUB_COPILOT_OPENAI_API_KEY);
|
|
14
|
+
}
|
|
15
|
+
function getAPIKey(provider) {
|
|
16
|
+
switch (provider) {
|
|
17
|
+
case "openai":
|
|
18
|
+
return process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY || process.env.GITHUB_COPILOT_OPENAI_API_KEY;
|
|
19
|
+
case "claude":
|
|
20
|
+
return process.env.ANTHROPIC_API_KEY;
|
|
21
|
+
case "auto":
|
|
22
|
+
const detected = detectAIProvider();
|
|
23
|
+
return getAPIKey(detected);
|
|
24
|
+
default:
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function showAPIKeyHelp() {
|
|
29
|
+
console.log(chalk.red("\n\u274C No API key found\n"));
|
|
30
|
+
console.log("To use Coherent, you need an AI provider API key.\n");
|
|
31
|
+
console.log(chalk.cyan("\u250C\u2500 Quick Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
32
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
33
|
+
console.log(chalk.cyan("\u2502 Option 1: Claude (Anthropic) \u2502"));
|
|
34
|
+
console.log(chalk.gray("\u2502 Get key: https://console.anthropic.com/ \u2502"));
|
|
35
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
36
|
+
console.log(chalk.cyan("\u2502 Copy and run this command: \u2502"));
|
|
37
|
+
console.log(chalk.cyan("\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502"));
|
|
38
|
+
console.log(chalk.white('\u2502 \u2502 echo "ANTHROPIC_API_KEY=sk-..." > .env \u2502 \u2502'));
|
|
39
|
+
console.log(chalk.cyan("\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502"));
|
|
40
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
41
|
+
console.log(chalk.cyan("\u2502 Option 2: OpenAI (ChatGPT) \u2502"));
|
|
42
|
+
console.log(chalk.gray("\u2502 Get key: https://platform.openai.com/ \u2502"));
|
|
43
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
44
|
+
console.log(chalk.cyan("\u2502 Copy and run this command: \u2502"));
|
|
45
|
+
console.log(chalk.cyan("\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502"));
|
|
46
|
+
console.log(chalk.white('\u2502 \u2502 echo "OPENAI_API_KEY=sk-..." > .env \u2502 \u2502'));
|
|
47
|
+
console.log(chalk.cyan("\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502"));
|
|
48
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
49
|
+
console.log(chalk.cyan("\u2502 Then run: coherent init \u2502"));
|
|
50
|
+
console.log(chalk.cyan("\u2502 \u2502"));
|
|
51
|
+
console.log(chalk.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"));
|
|
52
|
+
console.log(chalk.gray("Note: If using Cursor, your CURSOR_OPENAI_API_KEY"));
|
|
53
|
+
console.log(chalk.gray("will be detected automatically.\n"));
|
|
54
|
+
}
|
|
55
|
+
async function createAIProvider(preferredProvider = "auto", config) {
|
|
56
|
+
if (!hasAnyAPIKey() && !config?.apiKey) {
|
|
57
|
+
showAPIKeyHelp();
|
|
58
|
+
throw new Error("API key required");
|
|
59
|
+
}
|
|
60
|
+
if (preferredProvider !== "auto") {
|
|
61
|
+
const apiKey2 = config?.apiKey || getAPIKey(preferredProvider);
|
|
62
|
+
if (!apiKey2) {
|
|
63
|
+
const providerName = preferredProvider === "openai" ? "OpenAI" : "Anthropic Claude";
|
|
64
|
+
const envVar = preferredProvider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
65
|
+
showAPIKeyHelp();
|
|
66
|
+
throw new Error(`${providerName} API key not found.
|
|
67
|
+
Please set ${envVar} in your environment or .env file.`);
|
|
68
|
+
}
|
|
69
|
+
if (preferredProvider === "openai") {
|
|
70
|
+
try {
|
|
71
|
+
const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
|
|
72
|
+
return await OpenAIClient.create(apiKey2, config?.model);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.message?.includes("not installed")) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(
|
|
78
|
+
`OpenAI provider requires "openai" package. Install it with:
|
|
79
|
+
npm install openai
|
|
80
|
+
Or use Claude provider instead.
|
|
81
|
+
Error: ${error.message}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
|
|
86
|
+
return ClaudeClient.create(apiKey2, config?.model);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const provider = detectAIProvider();
|
|
90
|
+
const apiKey = config?.apiKey || getAPIKey(provider);
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
showAPIKeyHelp();
|
|
93
|
+
throw new Error("API key required");
|
|
94
|
+
}
|
|
95
|
+
switch (provider) {
|
|
96
|
+
case "openai":
|
|
97
|
+
try {
|
|
98
|
+
const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
|
|
99
|
+
return await OpenAIClient.create(apiKey, config?.model);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.message?.includes("not installed")) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(
|
|
105
|
+
`OpenAI provider requires "openai" package. Install it with:
|
|
106
|
+
npm install openai
|
|
107
|
+
Or use Claude provider instead.
|
|
108
|
+
Error: ${error.message}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
case "claude":
|
|
112
|
+
const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
|
|
113
|
+
return ClaudeClient.create(apiKey, config?.model);
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
detectAIProvider,
|
|
121
|
+
hasAnyAPIKey,
|
|
122
|
+
getAPIKey,
|
|
123
|
+
createAIProvider
|
|
124
|
+
};
|
|
@@ -399,7 +399,7 @@ Rules:
|
|
|
399
399
|
- Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
|
|
400
400
|
- Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
|
|
401
401
|
|
|
402
|
-
Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
|
|
402
|
+
Each component object: "name" (PascalCase), "type" ("layout"|"navigation"|"data-display"|"form"|"feedback"|"section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
|
|
403
403
|
|
|
404
404
|
If no repeating patterns found: { "components": [] }`
|
|
405
405
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import "./chunk-3RG5ZIWI.js";
|
|
2
|
+
|
|
3
|
+
// src/utils/component-extractor.ts
|
|
4
|
+
function extractExportedComponentName(code) {
|
|
5
|
+
const patterns = [
|
|
6
|
+
/export\s+(?:default\s+)?function\s+([A-Z][a-zA-Z0-9]*)/,
|
|
7
|
+
/export\s+const\s+([A-Z][a-zA-Z0-9]*)\s*[=:]/
|
|
8
|
+
];
|
|
9
|
+
for (const pattern of patterns) {
|
|
10
|
+
const match = code.match(pattern);
|
|
11
|
+
if (match) return match[1];
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function extractPropsInterface(code) {
|
|
16
|
+
const interfaceMatch = code.match(/(?:interface|type)\s+Props\s*=?\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/);
|
|
17
|
+
if (interfaceMatch) {
|
|
18
|
+
const fields = interfaceMatch[1].split("\n").map((l) => l.trim().replace(/;?\s*$/, "")).filter((l) => l && !l.startsWith("//"));
|
|
19
|
+
return `{ ${fields.join("; ")} }`;
|
|
20
|
+
}
|
|
21
|
+
const inlineMatch = code.match(/\}\s*:\s*(\{[^)]+\})/);
|
|
22
|
+
if (inlineMatch) return inlineMatch[1].replace(/\s+/g, " ").trim();
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function extractDependencies(code) {
|
|
26
|
+
const deps = [];
|
|
27
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
30
|
+
const source = match[1];
|
|
31
|
+
if (source.startsWith("@/components/ui/")) {
|
|
32
|
+
deps.push(source.replace("@/", ""));
|
|
33
|
+
} else if (!source.startsWith(".") && !source.startsWith("@/")) {
|
|
34
|
+
deps.push(source);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...new Set(deps)];
|
|
38
|
+
}
|
|
39
|
+
function extractUsageExample(pageCode, componentName) {
|
|
40
|
+
const selfClosingRegex = new RegExp(`<${componentName}\\s[^>]*/>`);
|
|
41
|
+
const selfMatch = pageCode.match(selfClosingRegex);
|
|
42
|
+
if (selfMatch) return selfMatch[0];
|
|
43
|
+
const openingRegex = new RegExp(`<${componentName}[\\s>][^]*?</${componentName}>`);
|
|
44
|
+
const openMatch = pageCode.match(openingRegex);
|
|
45
|
+
if (openMatch) {
|
|
46
|
+
const full = openMatch[0];
|
|
47
|
+
return full.length > 200 ? full.slice(0, 200) + "..." : full;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
extractDependencies,
|
|
53
|
+
extractExportedComponentName,
|
|
54
|
+
extractPropsInterface,
|
|
55
|
+
extractUsageExample
|
|
56
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CORE_CONSTRAINTS,
|
|
3
|
+
DESIGN_CONSTRAINTS,
|
|
4
|
+
DESIGN_QUALITY,
|
|
5
|
+
DESIGN_QUALITY_COMMON,
|
|
6
|
+
DESIGN_THINKING,
|
|
7
|
+
INTERACTION_PATTERNS,
|
|
8
|
+
RULES_CARDS_LAYOUT,
|
|
9
|
+
RULES_COMPONENTS_MISC,
|
|
10
|
+
RULES_CONTENT,
|
|
11
|
+
RULES_DATA_DISPLAY,
|
|
12
|
+
RULES_FEEDBACK,
|
|
13
|
+
RULES_FORMS,
|
|
14
|
+
RULES_NAVIGATION,
|
|
15
|
+
RULES_OVERLAYS,
|
|
16
|
+
RULES_SHADCN_APIS,
|
|
17
|
+
VISUAL_DEPTH,
|
|
18
|
+
getDesignQualityForType,
|
|
19
|
+
inferPageTypeFromRoute,
|
|
20
|
+
selectContextualRules
|
|
21
|
+
} from "./chunk-5AHG4NNX.js";
|
|
22
|
+
import "./chunk-3RG5ZIWI.js";
|
|
23
|
+
export {
|
|
24
|
+
CORE_CONSTRAINTS,
|
|
25
|
+
DESIGN_CONSTRAINTS,
|
|
26
|
+
DESIGN_QUALITY,
|
|
27
|
+
DESIGN_QUALITY_COMMON,
|
|
28
|
+
DESIGN_THINKING,
|
|
29
|
+
INTERACTION_PATTERNS,
|
|
30
|
+
RULES_CARDS_LAYOUT,
|
|
31
|
+
RULES_COMPONENTS_MISC,
|
|
32
|
+
RULES_CONTENT,
|
|
33
|
+
RULES_DATA_DISPLAY,
|
|
34
|
+
RULES_FEEDBACK,
|
|
35
|
+
RULES_FORMS,
|
|
36
|
+
RULES_NAVIGATION,
|
|
37
|
+
RULES_OVERLAYS,
|
|
38
|
+
RULES_SHADCN_APIS,
|
|
39
|
+
VISUAL_DEPTH,
|
|
40
|
+
getDesignQualityForType,
|
|
41
|
+
inferPageTypeFromRoute,
|
|
42
|
+
selectContextualRules
|
|
43
|
+
};
|