@getcoherent/cli 0.6.0 → 0.6.1
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/{chunk-4M2RBSYF.js → chunk-INW3BQSX.js} +4 -8
- package/dist/{claude-RFHVT7RC.js → claude-BZ3HSBD3.js} +16 -0
- package/dist/index.js +129 -53
- package/dist/{openai-provider-FSXSVEYD.js → openai-provider-XUI7ZHUR.js} +20 -0
- package/dist/{plan-generator-XKMZTEGK.js → plan-generator-IS3YDZUW.js} +1 -1
- package/package.json +1 -1
|
@@ -54,16 +54,14 @@ Rules:
|
|
|
54
54
|
|
|
55
55
|
Respond with valid JSON matching the schema.`;
|
|
56
56
|
async function generateArchitecturePlan(pages, userMessage, aiProvider, layoutHint) {
|
|
57
|
-
const userPrompt = `${
|
|
58
|
-
|
|
59
|
-
Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
57
|
+
const userPrompt = `Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
60
58
|
|
|
61
59
|
User's request: "${userMessage}"
|
|
62
60
|
|
|
63
61
|
Navigation type requested: ${layoutHint || "auto-detect"}`;
|
|
64
62
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
65
63
|
try {
|
|
66
|
-
const raw = await aiProvider.
|
|
64
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
67
65
|
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
68
66
|
if (parsed.success) return parsed.data;
|
|
69
67
|
} catch {
|
|
@@ -73,9 +71,7 @@ Navigation type requested: ${layoutHint || "auto-detect"}`;
|
|
|
73
71
|
return null;
|
|
74
72
|
}
|
|
75
73
|
async function updateArchitecturePlan(existingPlan, newPages, userMessage, aiProvider) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
Existing plan:
|
|
74
|
+
const userPrompt = `Existing plan:
|
|
79
75
|
${JSON.stringify(existingPlan, null, 2)}
|
|
80
76
|
|
|
81
77
|
New pages to integrate: ${newPages.map((p) => `${p.name} (${p.route})`).join(", ")}
|
|
@@ -84,7 +80,7 @@ User's request: "${userMessage}"
|
|
|
84
80
|
|
|
85
81
|
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.`;
|
|
86
82
|
try {
|
|
87
|
-
const raw = await aiProvider.
|
|
83
|
+
const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
|
|
88
84
|
const parsed = ArchitecturePlanSchema.safeParse(raw);
|
|
89
85
|
if (parsed.success) return parsed.data;
|
|
90
86
|
} catch {
|
|
@@ -129,6 +129,22 @@ Return ONLY the JSON object, no markdown, no code blocks, no explanations.`;
|
|
|
129
129
|
}
|
|
130
130
|
return jsonText.trim();
|
|
131
131
|
}
|
|
132
|
+
async generateJSON(systemPrompt, userPrompt) {
|
|
133
|
+
const response = await this.client.messages.create({
|
|
134
|
+
model: this.defaultModel,
|
|
135
|
+
max_tokens: 16384,
|
|
136
|
+
system: systemPrompt,
|
|
137
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
138
|
+
});
|
|
139
|
+
if (response.stop_reason === "max_tokens") {
|
|
140
|
+
const err = new Error("AI response truncated (max_tokens reached)");
|
|
141
|
+
err.code = "RESPONSE_TRUNCATED";
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
const content = response.content[0];
|
|
145
|
+
if (content.type !== "text") throw new Error("Unexpected response type from Claude API");
|
|
146
|
+
return JSON.parse(this.extractJSON(content.text));
|
|
147
|
+
}
|
|
132
148
|
/**
|
|
133
149
|
* Test API connection
|
|
134
150
|
*/
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
getPageGroup,
|
|
4
4
|
getPageType,
|
|
5
5
|
loadPlan,
|
|
6
|
+
routeToKey,
|
|
6
7
|
savePlan
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-INW3BQSX.js";
|
|
8
9
|
import {
|
|
9
10
|
__require
|
|
10
11
|
} from "./chunk-3RG5ZIWI.js";
|
|
@@ -3839,7 +3840,7 @@ Please set ${envVar} in your environment or .env file.`);
|
|
|
3839
3840
|
}
|
|
3840
3841
|
if (preferredProvider === "openai") {
|
|
3841
3842
|
try {
|
|
3842
|
-
const { OpenAIClient } = await import("./openai-provider-
|
|
3843
|
+
const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
|
|
3843
3844
|
return await OpenAIClient.create(apiKey2, config2?.model);
|
|
3844
3845
|
} catch (error) {
|
|
3845
3846
|
if (error.message?.includes("not installed")) {
|
|
@@ -3853,7 +3854,7 @@ Error: ${error.message}`
|
|
|
3853
3854
|
);
|
|
3854
3855
|
}
|
|
3855
3856
|
} else {
|
|
3856
|
-
const { ClaudeClient } = await import("./claude-
|
|
3857
|
+
const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
|
|
3857
3858
|
return ClaudeClient.create(apiKey2, config2?.model);
|
|
3858
3859
|
}
|
|
3859
3860
|
}
|
|
@@ -3866,7 +3867,7 @@ Error: ${error.message}`
|
|
|
3866
3867
|
switch (provider) {
|
|
3867
3868
|
case "openai":
|
|
3868
3869
|
try {
|
|
3869
|
-
const { OpenAIClient } = await import("./openai-provider-
|
|
3870
|
+
const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
|
|
3870
3871
|
return await OpenAIClient.create(apiKey, config2?.model);
|
|
3871
3872
|
} catch (error) {
|
|
3872
3873
|
if (error.message?.includes("not installed")) {
|
|
@@ -3880,7 +3881,7 @@ Error: ${error.message}`
|
|
|
3880
3881
|
);
|
|
3881
3882
|
}
|
|
3882
3883
|
case "claude":
|
|
3883
|
-
const { ClaudeClient } = await import("./claude-
|
|
3884
|
+
const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
|
|
3884
3885
|
return ClaudeClient.create(apiKey, config2?.model);
|
|
3885
3886
|
default:
|
|
3886
3887
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
@@ -3901,7 +3902,7 @@ var PAGE_TEMPLATES = {
|
|
|
3901
3902
|
login: {
|
|
3902
3903
|
description: "Login page with centered card form",
|
|
3903
3904
|
sections: [
|
|
3904
|
-
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-
|
|
3905
|
+
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-md".',
|
|
3905
3906
|
'Card with CardHeader: CardTitle "Sign in" (text-2xl font-bold), CardDescription "Enter your credentials to access your account" (text-sm text-muted-foreground).',
|
|
3906
3907
|
'CardContent with form: email Input (type="email", placeholder="you@example.com"), password Input (type="password"), a "Forgot password?" link (text-sm text-muted-foreground hover:text-foreground), and a Button "Sign in" (w-full).',
|
|
3907
3908
|
`CardFooter: text "Don't have an account?" with a Sign up link. All text is text-sm text-muted-foreground.`,
|
|
@@ -3922,7 +3923,7 @@ var PAGE_TEMPLATES = {
|
|
|
3922
3923
|
register: {
|
|
3923
3924
|
description: "Registration page with centered card form",
|
|
3924
3925
|
sections: [
|
|
3925
|
-
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-
|
|
3926
|
+
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-md".',
|
|
3926
3927
|
'Card with CardHeader: CardTitle "Create an account" (text-2xl font-bold), CardDescription "Enter your details to get started" (text-sm text-muted-foreground).',
|
|
3927
3928
|
'CardContent with form: name Input, email Input (type="email"), password Input (type="password"), confirm password Input (type="password"), and a Button "Create account" (w-full).',
|
|
3928
3929
|
'CardFooter: text "Already have an account?" with a Sign in link. All text is text-sm text-muted-foreground.',
|
|
@@ -4096,7 +4097,7 @@ var PAGE_TEMPLATES = {
|
|
|
4096
4097
|
"reset-password": {
|
|
4097
4098
|
description: "Reset password page with centered card form",
|
|
4098
4099
|
sections: [
|
|
4099
|
-
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-
|
|
4100
|
+
'Centered layout: outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10". Inner div className="w-full max-w-md".',
|
|
4100
4101
|
'Card with CardHeader: CardTitle "Reset Password" (text-xl), CardDescription.',
|
|
4101
4102
|
'CardContent with form: new password Input (type="password"), confirm password Input (type="password"), Button "Reset password" (w-full).',
|
|
4102
4103
|
'Footer text: "Remember your password?" with Sign in link.',
|
|
@@ -4205,7 +4206,7 @@ LAYOUT PATTERNS:
|
|
|
4205
4206
|
- Stats/KPI grid: grid gap-4 md:grid-cols-2 lg:grid-cols-4
|
|
4206
4207
|
- Card grid (3 col): grid gap-4 md:grid-cols-3
|
|
4207
4208
|
- Full-height page: min-h-svh (not min-h-screen)
|
|
4208
|
-
- Centered form (login/signup): flex min-h-svh flex-col items-center justify-center p-6 md:p-10 \u2192 div w-full max-w-
|
|
4209
|
+
- Centered form (login/signup): flex min-h-svh flex-col items-center justify-center p-6 md:p-10 \u2192 div w-full max-w-md
|
|
4209
4210
|
- Page content wrapper: flex flex-1 flex-col gap-4 p-4 lg:p-6
|
|
4210
4211
|
- Responsive: primary md: and lg:. Use sm:/xl: only when genuinely needed. Avoid 2xl:. NEVER arbitrary like min-[800px].
|
|
4211
4212
|
|
|
@@ -4362,6 +4363,12 @@ var DESIGN_QUALITY_APP = `
|
|
|
4362
4363
|
- Stats grid: grid gap-4 md:grid-cols-2 lg:grid-cols-4
|
|
4363
4364
|
- Content: functional, scannable, data-dense
|
|
4364
4365
|
|
|
4366
|
+
### Toolbars & Filters
|
|
4367
|
+
- Filter row: flex flex-wrap items-center gap-2
|
|
4368
|
+
- Search input: MUST use flex-1 to fill remaining horizontal space
|
|
4369
|
+
- Filters/selects: fixed width (w-[180px] or auto), do NOT flex-grow
|
|
4370
|
+
- On mobile (sm:): search full width, filters wrap to next line
|
|
4371
|
+
|
|
4365
4372
|
NEVER include marketing sections (hero, pricing, testimonials) on app pages.
|
|
4366
4373
|
`;
|
|
4367
4374
|
var DESIGN_QUALITY_AUTH = `
|
|
@@ -4369,21 +4376,29 @@ var DESIGN_QUALITY_AUTH = `
|
|
|
4369
4376
|
|
|
4370
4377
|
### Layout
|
|
4371
4378
|
- Centered card: flex min-h-svh items-center justify-center p-6 md:p-10
|
|
4372
|
-
- Card width: w-full max-w-
|
|
4379
|
+
- Card width: w-full max-w-md
|
|
4373
4380
|
- No navigation, no section containers, no sidebar
|
|
4374
4381
|
- Single focused form with clear CTA
|
|
4375
4382
|
- Card \u2192 CardHeader (title + description) \u2192 CardContent (form) \u2192 CardFooter (link to other auth page)
|
|
4376
4383
|
|
|
4377
4384
|
NEVER include navigation bars, sidebars, or multi-section layouts on auth pages.
|
|
4378
4385
|
`;
|
|
4386
|
+
var DESIGN_QUALITY_CRITICAL = `
|
|
4387
|
+
## CRITICAL CODE RULES (violations will be auto-corrected)
|
|
4388
|
+
- Every lucide-react icon MUST have className="... shrink-0" to prevent flex squishing
|
|
4389
|
+
- Button with asChild wrapping Link: the inner element MUST have className="inline-flex items-center gap-2"
|
|
4390
|
+
- NEVER use raw Tailwind colors (bg-blue-500, text-gray-600). ONLY semantic tokens: bg-primary, text-muted-foreground, etc.
|
|
4391
|
+
- <Link> and <a> MUST always have an href attribute. Never omit href.
|
|
4392
|
+
- CardTitle: NEVER add text-xl, text-2xl, text-lg. CardTitle is text-sm font-medium by default.
|
|
4393
|
+
`;
|
|
4379
4394
|
function getDesignQualityForType(type) {
|
|
4380
4395
|
switch (type) {
|
|
4381
4396
|
case "marketing":
|
|
4382
|
-
return DESIGN_QUALITY_MARKETING;
|
|
4397
|
+
return DESIGN_QUALITY_MARKETING + DESIGN_QUALITY_CRITICAL;
|
|
4383
4398
|
case "app":
|
|
4384
|
-
return DESIGN_QUALITY_APP;
|
|
4399
|
+
return DESIGN_QUALITY_APP + DESIGN_QUALITY_CRITICAL;
|
|
4385
4400
|
case "auth":
|
|
4386
|
-
return DESIGN_QUALITY_AUTH;
|
|
4401
|
+
return DESIGN_QUALITY_AUTH + DESIGN_QUALITY_CRITICAL;
|
|
4387
4402
|
}
|
|
4388
4403
|
}
|
|
4389
4404
|
function inferPageTypeFromRoute(route) {
|
|
@@ -5281,7 +5296,7 @@ pageCode rules (shadcn/ui blocks quality):
|
|
|
5281
5296
|
- Full Next.js App Router page. Imports from '@/components/ui/...' for registry components.
|
|
5282
5297
|
- Follow ALL design constraints above: text-sm base, semantic colors only, restricted spacing, weight-based hierarchy.
|
|
5283
5298
|
- Stat card pattern: Card > CardHeader(flex flex-row items-center justify-between space-y-0 pb-2) > CardTitle(text-sm font-medium) + Icon(size-4 text-muted-foreground) ; CardContent > metric(text-2xl font-bold) + change(text-xs text-muted-foreground).
|
|
5284
|
-
- Login/form pattern: outer div(flex min-h-svh flex-col items-center justify-center p-6 md:p-10) > inner div(w-full max-w-
|
|
5299
|
+
- Login/form pattern: outer div(flex min-h-svh flex-col items-center justify-center p-6 md:p-10) > inner div(w-full max-w-md) > Card with form.
|
|
5285
5300
|
- Dashboard pattern: div(space-y-6) > page header(h1 text-2xl font-bold tracking-tight + p text-sm text-muted-foreground) > stats grid(grid gap-4 md:grid-cols-2 lg:grid-cols-4) > content cards. No <main> wrapper \u2014 the layout provides it.
|
|
5286
5301
|
- No placeholders: real contextual copy only. Use the EXACT text, language, and content from the user's request.
|
|
5287
5302
|
- IMAGES: For avatar/profile photos, use https://i.pravatar.cc/150?u=<unique-seed> (e.g. ?u=sarah.johnson). For hero/product images, use https://picsum.photos/800/400?random=N. Use standard <img> tags with className, NOT Next.js <Image>. Always provide alt text.
|
|
@@ -6063,7 +6078,7 @@ function checkLines(code, pattern, type, message, severity, skipCommentsAndStrin
|
|
|
6063
6078
|
}
|
|
6064
6079
|
return issues;
|
|
6065
6080
|
}
|
|
6066
|
-
function validatePageQuality(code, validRoutes) {
|
|
6081
|
+
function validatePageQuality(code, validRoutes, pageType) {
|
|
6067
6082
|
const issues = [];
|
|
6068
6083
|
const allLines = code.split("\n");
|
|
6069
6084
|
const isTerminalContext = (lineNum) => {
|
|
@@ -6233,21 +6248,23 @@ function validatePageQuality(code, validRoutes) {
|
|
|
6233
6248
|
"warning"
|
|
6234
6249
|
)
|
|
6235
6250
|
);
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
+
if (pageType !== "auth") {
|
|
6252
|
+
const h1Matches = code.match(/<h1[\s>]/g);
|
|
6253
|
+
if (!h1Matches || h1Matches.length === 0) {
|
|
6254
|
+
issues.push({
|
|
6255
|
+
line: 0,
|
|
6256
|
+
type: "NO_H1",
|
|
6257
|
+
message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
|
|
6258
|
+
severity: "warning"
|
|
6259
|
+
});
|
|
6260
|
+
} else if (h1Matches.length > 1) {
|
|
6261
|
+
issues.push({
|
|
6262
|
+
line: 0,
|
|
6263
|
+
type: "MULTIPLE_H1",
|
|
6264
|
+
message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
|
|
6265
|
+
severity: "warning"
|
|
6266
|
+
});
|
|
6267
|
+
}
|
|
6251
6268
|
}
|
|
6252
6269
|
const headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
|
|
6253
6270
|
const hasCardContext = /\bCard\b|\bCardTitle\b|\bCardHeader\b/.test(code);
|
|
@@ -6421,6 +6438,24 @@ function validatePageQuality(code, validRoutes) {
|
|
|
6421
6438
|
issues.push(...detectComponentIssues(code));
|
|
6422
6439
|
return issues;
|
|
6423
6440
|
}
|
|
6441
|
+
function resolveHref(linkText, context) {
|
|
6442
|
+
if (!context) return "/";
|
|
6443
|
+
const text = linkText.trim().toLowerCase();
|
|
6444
|
+
if (context.linkMap) {
|
|
6445
|
+
for (const [label, route] of Object.entries(context.linkMap)) {
|
|
6446
|
+
if (label.toLowerCase() === text) return route;
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
if (context.knownRoutes) {
|
|
6450
|
+
const cleaned = text.replace(/^(back\s+to|go\s+to|view\s+all|see\s+all|return\s+to)\s+/i, "").trim();
|
|
6451
|
+
for (const route of context.knownRoutes) {
|
|
6452
|
+
const slug = route.split("/").filter(Boolean).pop() || "";
|
|
6453
|
+
const routeName = slug.replace(/[-_]/g, " ");
|
|
6454
|
+
if (routeName && cleaned === routeName) return route;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
return "/";
|
|
6458
|
+
}
|
|
6424
6459
|
function replaceRawColors(classes, colorMap) {
|
|
6425
6460
|
let changed = false;
|
|
6426
6461
|
let result = classes;
|
|
@@ -6513,7 +6548,7 @@ function replaceRawColors(classes, colorMap) {
|
|
|
6513
6548
|
});
|
|
6514
6549
|
return { result, changed };
|
|
6515
6550
|
}
|
|
6516
|
-
async function autoFixCode(code) {
|
|
6551
|
+
async function autoFixCode(code, context) {
|
|
6517
6552
|
const fixes = [];
|
|
6518
6553
|
let fixed = code;
|
|
6519
6554
|
const beforeQuoteFix = fixed;
|
|
@@ -6900,9 +6935,14 @@ ${selectImport}`
|
|
|
6900
6935
|
fixes.push("added inline-flex to Button asChild children (base-ui compat)");
|
|
6901
6936
|
}
|
|
6902
6937
|
const beforeLinkHrefFix = fixed;
|
|
6903
|
-
fixed = fixed.replace(/<(Link|a)\b(?![^>]*\bhref\s*=)([^>]*)>/g,
|
|
6938
|
+
fixed = fixed.replace(/<(Link|a)\b(?![^>]*\bhref\s*=)([^>]*)>([\s\S]*?)<\/\1>/g, (_match, tag, attrs, children) => {
|
|
6939
|
+
const textContent = children.replace(/<[^>]*>/g, "").trim();
|
|
6940
|
+
const href = resolveHref(textContent, context);
|
|
6941
|
+
return `<${tag} href="${href}"${attrs}>${children}</${tag}>`;
|
|
6942
|
+
});
|
|
6943
|
+
fixed = fixed.replace(/<(Link|a)\b(?![^>]*\bhref\s*=)([^>]*)\/?>/g, '<$1 href="/"$2>');
|
|
6904
6944
|
if (fixed !== beforeLinkHrefFix) {
|
|
6905
|
-
fixes.push(
|
|
6945
|
+
fixes.push("added href to <Link>/<a> missing href");
|
|
6906
6946
|
}
|
|
6907
6947
|
const { code: fixedByRules, fixes: ruleFixes } = applyComponentRules(fixed);
|
|
6908
6948
|
if (ruleFixes.length > 0) {
|
|
@@ -7084,8 +7124,16 @@ function routeToRelPath(route, isAuthOrPlan) {
|
|
|
7084
7124
|
if (isMarketingRoute(route)) return `app/${slug}/page.tsx`;
|
|
7085
7125
|
return `app/(app)/${slug}/page.tsx`;
|
|
7086
7126
|
}
|
|
7127
|
+
var AUTH_SYNONYMS = {
|
|
7128
|
+
"/register": "/signup",
|
|
7129
|
+
"/registration": "/signup",
|
|
7130
|
+
"/sign-up": "/signup",
|
|
7131
|
+
"/signin": "/login",
|
|
7132
|
+
"/sign-in": "/login"
|
|
7133
|
+
};
|
|
7087
7134
|
function deduplicatePages(pages) {
|
|
7088
|
-
const
|
|
7135
|
+
const canonicalize = (route) => AUTH_SYNONYMS[route] || route;
|
|
7136
|
+
const normalize = (route) => canonicalize(route).replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
|
|
7089
7137
|
const seen = /* @__PURE__ */ new Map();
|
|
7090
7138
|
return pages.filter((page, idx) => {
|
|
7091
7139
|
const norm = normalize(page.route);
|
|
@@ -7234,8 +7282,8 @@ ${currentCode}
|
|
|
7234
7282
|
|
|
7235
7283
|
// src/commands/chat/request-parser.ts
|
|
7236
7284
|
var AUTH_FLOW_PATTERNS = {
|
|
7237
|
-
"/login": ["/
|
|
7238
|
-
"/signin": ["/
|
|
7285
|
+
"/login": ["/signup", "/forgot-password"],
|
|
7286
|
+
"/signin": ["/signup", "/forgot-password"],
|
|
7239
7287
|
"/signup": ["/login"],
|
|
7240
7288
|
"/register": ["/login"],
|
|
7241
7289
|
"/forgot-password": ["/login", "/reset-password"],
|
|
@@ -7278,14 +7326,19 @@ function extractInternalLinks(code) {
|
|
|
7278
7326
|
function inferRelatedPages(plannedPages) {
|
|
7279
7327
|
const plannedRoutes = new Set(plannedPages.map((p) => p.route));
|
|
7280
7328
|
const inferred = [];
|
|
7281
|
-
|
|
7329
|
+
const queue = [...plannedPages];
|
|
7330
|
+
let i = 0;
|
|
7331
|
+
while (i < queue.length) {
|
|
7332
|
+
const { route } = queue[i++];
|
|
7282
7333
|
const authRelated = AUTH_FLOW_PATTERNS[route];
|
|
7283
7334
|
if (authRelated) {
|
|
7284
7335
|
for (const rel of authRelated) {
|
|
7285
7336
|
if (!plannedRoutes.has(rel)) {
|
|
7286
7337
|
const slug = rel.slice(1);
|
|
7287
7338
|
const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
7288
|
-
|
|
7339
|
+
const page = { id: slug, name, route: rel };
|
|
7340
|
+
inferred.push(page);
|
|
7341
|
+
queue.push(page);
|
|
7289
7342
|
plannedRoutes.add(rel);
|
|
7290
7343
|
}
|
|
7291
7344
|
}
|
|
@@ -7295,6 +7348,7 @@ function inferRelatedPages(plannedPages) {
|
|
|
7295
7348
|
for (const rel of rule.related) {
|
|
7296
7349
|
if (!plannedRoutes.has(rel.route)) {
|
|
7297
7350
|
inferred.push(rel);
|
|
7351
|
+
queue.push(rel);
|
|
7298
7352
|
plannedRoutes.add(rel.route);
|
|
7299
7353
|
}
|
|
7300
7354
|
}
|
|
@@ -7322,7 +7376,7 @@ function extractPageNamesFromMessage(message) {
|
|
|
7322
7376
|
settings: "/settings",
|
|
7323
7377
|
account: "/account",
|
|
7324
7378
|
"personal account": "/account",
|
|
7325
|
-
registration: "/
|
|
7379
|
+
registration: "/signup",
|
|
7326
7380
|
signup: "/signup",
|
|
7327
7381
|
"sign up": "/signup",
|
|
7328
7382
|
login: "/login",
|
|
@@ -7992,7 +8046,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
7992
8046
|
if (plan && plan.sharedComponents.length > 0) {
|
|
7993
8047
|
spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
|
|
7994
8048
|
try {
|
|
7995
|
-
const { generateSharedComponentsFromPlan } = await import("./plan-generator-
|
|
8049
|
+
const { generateSharedComponentsFromPlan } = await import("./plan-generator-IS3YDZUW.js");
|
|
7996
8050
|
const generated = await generateSharedComponentsFromPlan(
|
|
7997
8051
|
plan,
|
|
7998
8052
|
styleContext,
|
|
@@ -8048,7 +8102,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
8048
8102
|
const isAuth = isAuthRoute(route) || isAuthRoute(name);
|
|
8049
8103
|
const pageType = plan ? getPageType(route, plan) : inferPageTypeFromRoute(route);
|
|
8050
8104
|
const designConstraints = getDesignQualityForType(pageType);
|
|
8051
|
-
const authNote = isAuth ? 'For this auth page: use centered card layout with outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10" and inner div className="w-full max-w-
|
|
8105
|
+
const authNote = isAuth ? 'For this auth page: use centered card layout with outer div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10" and inner div className="w-full max-w-md". Do NOT use section containers or full-width wrappers. The auth layout provides centering \u2014 just output the card content.' : void 0;
|
|
8052
8106
|
const prompt = [
|
|
8053
8107
|
`Create ONE page called "${name}" at route "${route}".`,
|
|
8054
8108
|
`Context: ${message}.`,
|
|
@@ -8747,7 +8801,9 @@ function showPreview(requests, results, config2, preflightInstalledNames) {
|
|
|
8747
8801
|
const route = page.route || "/";
|
|
8748
8802
|
console.log(chalk11.white(` \u2728 ${page.name || "Page"}`));
|
|
8749
8803
|
console.log(chalk11.gray(` Route: ${route}`));
|
|
8750
|
-
|
|
8804
|
+
const configPage = config2.pages?.find((p) => p.id === page.id || p.route === (page.route || "/"));
|
|
8805
|
+
const sectionCount = configPage?.pageAnalysis?.sections?.length ?? page.sections?.length ?? 0;
|
|
8806
|
+
console.log(chalk11.gray(` Sections: ${sectionCount}`));
|
|
8751
8807
|
});
|
|
8752
8808
|
console.log("");
|
|
8753
8809
|
}
|
|
@@ -9285,7 +9341,13 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
9285
9341
|
isPage: true
|
|
9286
9342
|
});
|
|
9287
9343
|
let codeToWrite = fixedCode;
|
|
9288
|
-
const
|
|
9344
|
+
const currentPlan = projectRoot ? loadPlan(projectRoot) : null;
|
|
9345
|
+
const autoFixCtx = route ? {
|
|
9346
|
+
currentRoute: route,
|
|
9347
|
+
knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
|
|
9348
|
+
linkMap: currentPlan?.pageNotes[routeToKey(route)]?.links
|
|
9349
|
+
} : void 0;
|
|
9350
|
+
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite, autoFixCtx);
|
|
9289
9351
|
codeToWrite = autoFixed;
|
|
9290
9352
|
const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
|
|
9291
9353
|
if (!hasDashboardPage) {
|
|
@@ -9297,7 +9359,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
9297
9359
|
}
|
|
9298
9360
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
9299
9361
|
codeToWrite = layoutStripped;
|
|
9300
|
-
const
|
|
9362
|
+
const qualityPageType = currentPlan ? getPageType(route, currentPlan) : inferPageTypeFromRoute(route);
|
|
9301
9363
|
const pageType = currentPlan ? getPageType(route, currentPlan) : isMarketingRoute(route) ? "marketing" : isAuth ? "auth" : "app";
|
|
9302
9364
|
if (pageType === "app") {
|
|
9303
9365
|
const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
|
|
@@ -9343,7 +9405,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
9343
9405
|
layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout"),
|
|
9344
9406
|
allShared: manifestForAudit.shared
|
|
9345
9407
|
});
|
|
9346
|
-
let issues = validatePageQuality(codeToWrite);
|
|
9408
|
+
let issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
9347
9409
|
const errors = issues.filter((i) => i.severity === "error");
|
|
9348
9410
|
if (errors.length >= 2 && aiProvider) {
|
|
9349
9411
|
console.log(
|
|
@@ -9364,17 +9426,17 @@ Rules:
|
|
|
9364
9426
|
- Keep all existing functionality and layout intact`;
|
|
9365
9427
|
const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
|
|
9366
9428
|
if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
|
|
9367
|
-
const recheck = validatePageQuality(fixedCode2);
|
|
9429
|
+
const recheck = validatePageQuality(fixedCode2, void 0, qualityPageType);
|
|
9368
9430
|
const recheckErrors = recheck.filter((i) => i.severity === "error");
|
|
9369
9431
|
if (recheckErrors.length < errors.length) {
|
|
9370
9432
|
codeToWrite = fixedCode2;
|
|
9371
|
-
const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite);
|
|
9433
|
+
const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite, autoFixCtx);
|
|
9372
9434
|
if (reFixes.length > 0) {
|
|
9373
9435
|
codeToWrite = reFixed;
|
|
9374
9436
|
postFixes.push(...reFixes);
|
|
9375
9437
|
}
|
|
9376
9438
|
await writeFile(filePath, codeToWrite);
|
|
9377
|
-
issues = validatePageQuality(codeToWrite);
|
|
9439
|
+
issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
9378
9440
|
const finalErrors = issues.filter((i) => i.severity === "error").length;
|
|
9379
9441
|
console.log(chalk12.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
|
|
9380
9442
|
}
|
|
@@ -9495,7 +9557,13 @@ ${pagesCtx}`
|
|
|
9495
9557
|
isPage: true
|
|
9496
9558
|
});
|
|
9497
9559
|
let codeToWrite = fixedCode;
|
|
9498
|
-
const
|
|
9560
|
+
const currentPlan2 = projectRoot ? loadPlan(projectRoot) : null;
|
|
9561
|
+
const autoFixCtx2 = route ? {
|
|
9562
|
+
currentRoute: route,
|
|
9563
|
+
knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
|
|
9564
|
+
linkMap: currentPlan2?.pageNotes[routeToKey(route)]?.links
|
|
9565
|
+
} : void 0;
|
|
9566
|
+
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite, autoFixCtx2);
|
|
9499
9567
|
codeToWrite = autoFixed;
|
|
9500
9568
|
const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
|
|
9501
9569
|
if (!hasDashboardPage) {
|
|
@@ -9507,7 +9575,7 @@ ${pagesCtx}`
|
|
|
9507
9575
|
}
|
|
9508
9576
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
9509
9577
|
codeToWrite = layoutStripped;
|
|
9510
|
-
const
|
|
9578
|
+
const qualityPageType2 = currentPlan2 ? getPageType(route, currentPlan2) : inferPageTypeFromRoute(route);
|
|
9511
9579
|
const pageType2 = currentPlan2 ? getPageType(route, currentPlan2) : isMarketingRoute(route) ? "marketing" : isAuth ? "auth" : "app";
|
|
9512
9580
|
if (pageType2 === "app") {
|
|
9513
9581
|
const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
|
|
@@ -9553,7 +9621,7 @@ ${pagesCtx}`
|
|
|
9553
9621
|
allShared: manifestForAudit.shared,
|
|
9554
9622
|
layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout")
|
|
9555
9623
|
});
|
|
9556
|
-
const issues = validatePageQuality(codeToWrite);
|
|
9624
|
+
const issues = validatePageQuality(codeToWrite, void 0, qualityPageType2);
|
|
9557
9625
|
const report = formatIssues(issues);
|
|
9558
9626
|
if (report) {
|
|
9559
9627
|
console.log(chalk12.yellow(`
|
|
@@ -9569,7 +9637,13 @@ ${pagesCtx}`
|
|
|
9569
9637
|
} else {
|
|
9570
9638
|
try {
|
|
9571
9639
|
let code = await readFile(absPath);
|
|
9572
|
-
const
|
|
9640
|
+
const currentPlanForElse = projectRoot ? loadPlan(projectRoot) : null;
|
|
9641
|
+
const autoFixCtxElse = route ? {
|
|
9642
|
+
currentRoute: route,
|
|
9643
|
+
knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
|
|
9644
|
+
linkMap: currentPlanForElse?.pageNotes[routeToKey(route)]?.links
|
|
9645
|
+
} : void 0;
|
|
9646
|
+
const { code: fixed, fixes } = await autoFixCode(code, autoFixCtxElse);
|
|
9573
9647
|
if (fixes.length > 0) {
|
|
9574
9648
|
code = fixed;
|
|
9575
9649
|
await writeFile(absPath, code);
|
|
@@ -9588,7 +9662,9 @@ ${pagesCtx}`
|
|
|
9588
9662
|
allShared: manifest.shared,
|
|
9589
9663
|
layoutShared: manifest.shared.filter((c) => c.type === "layout")
|
|
9590
9664
|
});
|
|
9591
|
-
const
|
|
9665
|
+
const currentPlanForQuality = currentPlanForElse;
|
|
9666
|
+
const pageTypeForQuality = currentPlanForQuality ? getPageType(route, currentPlanForQuality) : inferPageTypeFromRoute(route);
|
|
9667
|
+
const issues = validatePageQuality(code, void 0, pageTypeForQuality);
|
|
9592
9668
|
const report = formatIssues(issues);
|
|
9593
9669
|
if (report) {
|
|
9594
9670
|
console.log(chalk12.yellow(`
|
|
@@ -122,6 +122,26 @@ Please check your API key and try again.`
|
|
|
122
122
|
throw new Error("Unknown error occurred while parsing modification");
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
async generateJSON(systemPrompt, userPrompt) {
|
|
126
|
+
const response = await this.client.chat.completions.create({
|
|
127
|
+
model: this.defaultModel,
|
|
128
|
+
messages: [
|
|
129
|
+
{ role: "system", content: systemPrompt },
|
|
130
|
+
{ role: "user", content: userPrompt }
|
|
131
|
+
],
|
|
132
|
+
response_format: { type: "json_object" },
|
|
133
|
+
temperature: 0.3,
|
|
134
|
+
max_tokens: 16384
|
|
135
|
+
});
|
|
136
|
+
if (response.choices[0]?.finish_reason === "length") {
|
|
137
|
+
const err = new Error("AI response truncated (max_tokens reached)");
|
|
138
|
+
err.code = "RESPONSE_TRUNCATED";
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
const content = response.choices[0]?.message?.content;
|
|
142
|
+
if (!content) throw new Error("Empty response from OpenAI API");
|
|
143
|
+
return JSON.parse(this.extractJSON(content));
|
|
144
|
+
}
|
|
125
145
|
/**
|
|
126
146
|
* Test API connection
|
|
127
147
|
*/
|