@getcoherent/cli 0.6.0 → 0.6.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sergei Kovtun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -3,29 +3,83 @@ import { z } from "zod";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
4
  import { dirname, resolve } from "path";
5
5
  import { mkdir, writeFile } from "fs/promises";
6
+ import chalk from "chalk";
7
+ var LAYOUT_SYNONYMS = {
8
+ horizontal: "header",
9
+ top: "header",
10
+ nav: "header",
11
+ navbar: "header",
12
+ topbar: "header",
13
+ "top-bar": "header",
14
+ vertical: "sidebar",
15
+ left: "sidebar",
16
+ side: "sidebar",
17
+ drawer: "sidebar",
18
+ full: "both",
19
+ combined: "both",
20
+ empty: "none",
21
+ minimal: "none",
22
+ clean: "none"
23
+ };
24
+ var PAGE_TYPE_SYNONYMS = {
25
+ landing: "marketing",
26
+ public: "marketing",
27
+ home: "marketing",
28
+ website: "marketing",
29
+ static: "marketing",
30
+ application: "app",
31
+ dashboard: "app",
32
+ admin: "app",
33
+ panel: "app",
34
+ console: "app",
35
+ authentication: "auth",
36
+ login: "auth",
37
+ "log-in": "auth",
38
+ register: "auth",
39
+ signin: "auth",
40
+ "sign-in": "auth",
41
+ signup: "auth",
42
+ "sign-up": "auth"
43
+ };
44
+ var COMPONENT_TYPE_SYNONYMS = {
45
+ component: "widget",
46
+ ui: "widget",
47
+ element: "widget",
48
+ block: "widget",
49
+ "page-section": "section",
50
+ hero: "section",
51
+ feature: "section",
52
+ area: "section"
53
+ };
54
+ function normalizeEnum(synonyms) {
55
+ return (v) => {
56
+ const trimmed = v.trim().toLowerCase();
57
+ return synonyms[trimmed] ?? trimmed;
58
+ };
59
+ }
6
60
  var RouteGroupSchema = z.object({
7
61
  id: z.string(),
8
- layout: z.enum(["header", "sidebar", "both", "none"]),
62
+ layout: z.string().transform(normalizeEnum(LAYOUT_SYNONYMS)).pipe(z.enum(["header", "sidebar", "both", "none"])),
9
63
  pages: z.array(z.string())
10
64
  });
11
65
  var PlannedComponentSchema = z.object({
12
66
  name: z.string(),
13
- description: z.string(),
14
- props: z.string(),
15
- usedBy: z.array(z.string()),
16
- type: z.enum(["section", "widget"]),
67
+ description: z.string().default(""),
68
+ props: z.string().default("{}"),
69
+ usedBy: z.array(z.string()).default([]),
70
+ type: z.string().transform(normalizeEnum(COMPONENT_TYPE_SYNONYMS)).pipe(z.enum(["section", "widget"])),
17
71
  shadcnDeps: z.array(z.string()).default([])
18
72
  });
19
73
  var PageNoteSchema = z.object({
20
- type: z.enum(["marketing", "app", "auth"]),
21
- sections: z.array(z.string()),
74
+ type: z.string().transform(normalizeEnum(PAGE_TYPE_SYNONYMS)).pipe(z.enum(["marketing", "app", "auth"])),
75
+ sections: z.array(z.string()).default([]),
22
76
  links: z.record(z.string()).optional()
23
77
  });
24
78
  var ArchitecturePlanSchema = z.object({
25
79
  appName: z.string().optional(),
26
80
  groups: z.array(RouteGroupSchema),
27
- sharedComponents: z.array(PlannedComponentSchema).max(8),
28
- pageNotes: z.record(z.string(), PageNoteSchema)
81
+ sharedComponents: z.array(PlannedComponentSchema).max(8).default([]),
82
+ pageNotes: z.record(z.string(), PageNoteSchema).default({})
29
83
  });
30
84
  function routeToKey(route) {
31
85
  return route.replace(/^\//, "") || "home";
@@ -54,28 +108,29 @@ Rules:
54
108
 
55
109
  Respond with valid JSON matching the schema.`;
56
110
  async function generateArchitecturePlan(pages, userMessage, aiProvider, layoutHint) {
57
- const userPrompt = `${PLAN_SYSTEM_PROMPT}
58
-
59
- Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
111
+ const userPrompt = `Pages: ${pages.map((p) => `${p.name} (${p.route})`).join(", ")}
60
112
 
61
113
  User's request: "${userMessage}"
62
114
 
63
115
  Navigation type requested: ${layoutHint || "auto-detect"}`;
116
+ const warnings = [];
64
117
  for (let attempt = 0; attempt < 2; attempt++) {
65
118
  try {
66
- const raw = await aiProvider.parseModification(userPrompt);
119
+ const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
67
120
  const parsed = ArchitecturePlanSchema.safeParse(raw);
68
- if (parsed.success) return parsed.data;
69
- } catch {
70
- if (attempt === 1) return null;
121
+ if (parsed.success) return { plan: parsed.data, warnings };
122
+ warnings.push(
123
+ `Validation (attempt ${attempt + 1}): ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
124
+ );
125
+ } catch (err) {
126
+ warnings.push(`Error (attempt ${attempt + 1}): ${err instanceof Error ? err.message : String(err)}`);
127
+ if (attempt === 1) return { plan: null, warnings };
71
128
  }
72
129
  }
73
- return null;
130
+ return { plan: null, warnings };
74
131
  }
75
132
  async function updateArchitecturePlan(existingPlan, newPages, userMessage, aiProvider) {
76
- const prompt = `${PLAN_SYSTEM_PROMPT}
77
-
78
- Existing plan:
133
+ const userPrompt = `Existing plan:
79
134
  ${JSON.stringify(existingPlan, null, 2)}
80
135
 
81
136
  New pages to integrate: ${newPages.map((p) => `${p.name} (${p.route})`).join(", ")}
@@ -84,10 +139,13 @@ User's request: "${userMessage}"
84
139
 
85
140
  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
141
  try {
87
- const raw = await aiProvider.parseModification(prompt);
142
+ const raw = await aiProvider.generateJSON(PLAN_SYSTEM_PROMPT, userPrompt);
88
143
  const parsed = ArchitecturePlanSchema.safeParse(raw);
89
144
  if (parsed.success) return parsed.data;
90
- } catch {
145
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
146
+ console.warn(chalk.dim(` Plan update validation failed: ${issues}`));
147
+ } catch (err) {
148
+ console.warn(chalk.dim(` Plan update error: ${err instanceof Error ? err.message : String(err)}`));
91
149
  }
92
150
  const merged = structuredClone(existingPlan);
93
151
  const largestGroup = merged.groups.reduce(
@@ -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-4M2RBSYF.js";
8
+ } from "./chunk-IKHAW6OI.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-FSXSVEYD.js");
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-RFHVT7RC.js");
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-FSXSVEYD.js");
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-RFHVT7RC.js");
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-sm".',
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-sm".',
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-sm".',
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-sm
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-sm
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-sm) > Card with form.
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
- const h1Matches = code.match(/<h1[\s>]/g);
6237
- if (!h1Matches || h1Matches.length === 0) {
6238
- issues.push({
6239
- line: 0,
6240
- type: "NO_H1",
6241
- message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
6242
- severity: "warning"
6243
- });
6244
- } else if (h1Matches.length > 1) {
6245
- issues.push({
6246
- line: 0,
6247
- type: "MULTIPLE_H1",
6248
- message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
6249
- severity: "warning"
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, '<$1 href="/"$2>');
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('added href="/" to <Link>/<a> missing href');
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 normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
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);
@@ -7143,7 +7191,8 @@ async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, mani
7143
7191
  for (const t of sharedTokens) {
7144
7192
  if (pageTokens.includes(t)) overlap++;
7145
7193
  }
7146
- if (overlap >= 12 && sharedTokens.size >= 10) {
7194
+ const overlapRatio = sharedTokens.size > 0 ? overlap / sharedTokens.size : 0;
7195
+ if (overlap >= 20 && overlapRatio >= 0.6) {
7147
7196
  console.log(
7148
7197
  chalk8.yellow(
7149
7198
  `
@@ -7234,8 +7283,8 @@ ${currentCode}
7234
7283
 
7235
7284
  // src/commands/chat/request-parser.ts
7236
7285
  var AUTH_FLOW_PATTERNS = {
7237
- "/login": ["/register", "/forgot-password"],
7238
- "/signin": ["/register", "/forgot-password"],
7286
+ "/login": ["/signup", "/forgot-password"],
7287
+ "/signin": ["/signup", "/forgot-password"],
7239
7288
  "/signup": ["/login"],
7240
7289
  "/register": ["/login"],
7241
7290
  "/forgot-password": ["/login", "/reset-password"],
@@ -7278,14 +7327,19 @@ function extractInternalLinks(code) {
7278
7327
  function inferRelatedPages(plannedPages) {
7279
7328
  const plannedRoutes = new Set(plannedPages.map((p) => p.route));
7280
7329
  const inferred = [];
7281
- for (const { route } of plannedPages) {
7330
+ const queue = [...plannedPages];
7331
+ let i = 0;
7332
+ while (i < queue.length) {
7333
+ const { route } = queue[i++];
7282
7334
  const authRelated = AUTH_FLOW_PATTERNS[route];
7283
7335
  if (authRelated) {
7284
7336
  for (const rel of authRelated) {
7285
7337
  if (!plannedRoutes.has(rel)) {
7286
7338
  const slug = rel.slice(1);
7287
7339
  const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
7288
- inferred.push({ id: slug, name, route: rel });
7340
+ const page = { id: slug, name, route: rel };
7341
+ inferred.push(page);
7342
+ queue.push(page);
7289
7343
  plannedRoutes.add(rel);
7290
7344
  }
7291
7345
  }
@@ -7295,6 +7349,7 @@ function inferRelatedPages(plannedPages) {
7295
7349
  for (const rel of rule.related) {
7296
7350
  if (!plannedRoutes.has(rel.route)) {
7297
7351
  inferred.push(rel);
7352
+ queue.push(rel);
7298
7353
  plannedRoutes.add(rel.route);
7299
7354
  }
7300
7355
  }
@@ -7322,7 +7377,7 @@ function extractPageNamesFromMessage(message) {
7322
7377
  settings: "/settings",
7323
7378
  account: "/account",
7324
7379
  "personal account": "/account",
7325
- registration: "/registration",
7380
+ registration: "/signup",
7326
7381
  signup: "/signup",
7327
7382
  "sign up": "/signup",
7328
7383
  login: "/login",
@@ -7909,7 +7964,13 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7909
7964
  try {
7910
7965
  const ai = await createAIProvider(provider ?? "auto");
7911
7966
  const layoutHint = modCtx.config.navigation?.type || null;
7912
- plan = await generateArchitecturePlan(pageNames, message, ai, layoutHint);
7967
+ const { plan: generatedPlan, warnings: planWarnings } = await generateArchitecturePlan(
7968
+ pageNames,
7969
+ message,
7970
+ ai,
7971
+ layoutHint
7972
+ );
7973
+ plan = generatedPlan;
7913
7974
  if (plan) {
7914
7975
  const groupsSummary = plan.groups.map((g) => `${g.id} (${g.layout}, ${g.pages.length} pages)`).join(", ");
7915
7976
  const sharedSummary = plan.sharedComponents.length > 0 ? plan.sharedComponents.map((c) => `${c.name} \u2192 ${c.usedBy.join(", ")}`).join(" | ") : "";
@@ -7933,6 +7994,9 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7933
7994
  } else {
7934
7995
  spinner.warn("Phase 2/6 \u2014 Plan generation failed (continuing without plan)");
7935
7996
  }
7997
+ for (const w of planWarnings) {
7998
+ console.log(chalk9.dim(` ${w}`));
7999
+ }
7936
8000
  } catch {
7937
8001
  spinner.warn("Phase 2/6 \u2014 Plan generation failed (continuing without plan)");
7938
8002
  }
@@ -7992,7 +8056,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7992
8056
  if (plan && plan.sharedComponents.length > 0) {
7993
8057
  spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
7994
8058
  try {
7995
- const { generateSharedComponentsFromPlan } = await import("./plan-generator-XKMZTEGK.js");
8059
+ const { generateSharedComponentsFromPlan } = await import("./plan-generator-R72I6RNM.js");
7996
8060
  const generated = await generateSharedComponentsFromPlan(
7997
8061
  plan,
7998
8062
  styleContext,
@@ -8048,7 +8112,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
8048
8112
  const isAuth = isAuthRoute(route) || isAuthRoute(name);
8049
8113
  const pageType = plan ? getPageType(route, plan) : inferPageTypeFromRoute(route);
8050
8114
  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-sm". Do NOT use section containers or full-width wrappers. The auth layout provides centering \u2014 just output the card content.' : void 0;
8115
+ 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
8116
  const prompt = [
8053
8117
  `Create ONE page called "${name}" at route "${route}".`,
8054
8118
  `Context: ${message}.`,
@@ -8747,7 +8811,9 @@ function showPreview(requests, results, config2, preflightInstalledNames) {
8747
8811
  const route = page.route || "/";
8748
8812
  console.log(chalk11.white(` \u2728 ${page.name || "Page"}`));
8749
8813
  console.log(chalk11.gray(` Route: ${route}`));
8750
- console.log(chalk11.gray(` Sections: ${page.sections?.length ?? 0}`));
8814
+ const configPage = config2.pages?.find((p) => p.id === page.id || p.route === (page.route || "/"));
8815
+ const sectionCount = configPage?.pageAnalysis?.sections?.length ?? page.sections?.length ?? 0;
8816
+ console.log(chalk11.gray(` Sections: ${sectionCount}`));
8751
8817
  });
8752
8818
  console.log("");
8753
8819
  }
@@ -9285,7 +9351,13 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
9285
9351
  isPage: true
9286
9352
  });
9287
9353
  let codeToWrite = fixedCode;
9288
- const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
9354
+ const currentPlan = projectRoot ? loadPlan(projectRoot) : null;
9355
+ const autoFixCtx = route ? {
9356
+ currentRoute: route,
9357
+ knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
9358
+ linkMap: currentPlan?.pageNotes[routeToKey(route)]?.links
9359
+ } : void 0;
9360
+ const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite, autoFixCtx);
9289
9361
  codeToWrite = autoFixed;
9290
9362
  const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
9291
9363
  if (!hasDashboardPage) {
@@ -9297,7 +9369,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
9297
9369
  }
9298
9370
  const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
9299
9371
  codeToWrite = layoutStripped;
9300
- const currentPlan = projectRoot ? loadPlan(projectRoot) : null;
9372
+ const qualityPageType = currentPlan ? getPageType(route, currentPlan) : inferPageTypeFromRoute(route);
9301
9373
  const pageType = currentPlan ? getPageType(route, currentPlan) : isMarketingRoute(route) ? "marketing" : isAuth ? "auth" : "app";
9302
9374
  if (pageType === "app") {
9303
9375
  const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
@@ -9343,7 +9415,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
9343
9415
  layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout"),
9344
9416
  allShared: manifestForAudit.shared
9345
9417
  });
9346
- let issues = validatePageQuality(codeToWrite);
9418
+ let issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
9347
9419
  const errors = issues.filter((i) => i.severity === "error");
9348
9420
  if (errors.length >= 2 && aiProvider) {
9349
9421
  console.log(
@@ -9364,17 +9436,17 @@ Rules:
9364
9436
  - Keep all existing functionality and layout intact`;
9365
9437
  const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
9366
9438
  if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
9367
- const recheck = validatePageQuality(fixedCode2);
9439
+ const recheck = validatePageQuality(fixedCode2, void 0, qualityPageType);
9368
9440
  const recheckErrors = recheck.filter((i) => i.severity === "error");
9369
9441
  if (recheckErrors.length < errors.length) {
9370
9442
  codeToWrite = fixedCode2;
9371
- const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite);
9443
+ const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite, autoFixCtx);
9372
9444
  if (reFixes.length > 0) {
9373
9445
  codeToWrite = reFixed;
9374
9446
  postFixes.push(...reFixes);
9375
9447
  }
9376
9448
  await writeFile(filePath, codeToWrite);
9377
- issues = validatePageQuality(codeToWrite);
9449
+ issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
9378
9450
  const finalErrors = issues.filter((i) => i.severity === "error").length;
9379
9451
  console.log(chalk12.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
9380
9452
  }
@@ -9495,7 +9567,13 @@ ${pagesCtx}`
9495
9567
  isPage: true
9496
9568
  });
9497
9569
  let codeToWrite = fixedCode;
9498
- const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
9570
+ const currentPlan2 = projectRoot ? loadPlan(projectRoot) : null;
9571
+ const autoFixCtx2 = route ? {
9572
+ currentRoute: route,
9573
+ knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
9574
+ linkMap: currentPlan2?.pageNotes[routeToKey(route)]?.links
9575
+ } : void 0;
9576
+ const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite, autoFixCtx2);
9499
9577
  codeToWrite = autoFixed;
9500
9578
  const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
9501
9579
  if (!hasDashboardPage) {
@@ -9507,7 +9585,7 @@ ${pagesCtx}`
9507
9585
  }
9508
9586
  const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
9509
9587
  codeToWrite = layoutStripped;
9510
- const currentPlan2 = projectRoot ? loadPlan(projectRoot) : null;
9588
+ const qualityPageType2 = currentPlan2 ? getPageType(route, currentPlan2) : inferPageTypeFromRoute(route);
9511
9589
  const pageType2 = currentPlan2 ? getPageType(route, currentPlan2) : isMarketingRoute(route) ? "marketing" : isAuth ? "auth" : "app";
9512
9590
  if (pageType2 === "app") {
9513
9591
  const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
@@ -9553,7 +9631,7 @@ ${pagesCtx}`
9553
9631
  allShared: manifestForAudit.shared,
9554
9632
  layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout")
9555
9633
  });
9556
- const issues = validatePageQuality(codeToWrite);
9634
+ const issues = validatePageQuality(codeToWrite, void 0, qualityPageType2);
9557
9635
  const report = formatIssues(issues);
9558
9636
  if (report) {
9559
9637
  console.log(chalk12.yellow(`
@@ -9569,7 +9647,13 @@ ${pagesCtx}`
9569
9647
  } else {
9570
9648
  try {
9571
9649
  let code = await readFile(absPath);
9572
- const { code: fixed, fixes } = await autoFixCode(code);
9650
+ const currentPlanForElse = projectRoot ? loadPlan(projectRoot) : null;
9651
+ const autoFixCtxElse = route ? {
9652
+ currentRoute: route,
9653
+ knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
9654
+ linkMap: currentPlanForElse?.pageNotes[routeToKey(route)]?.links
9655
+ } : void 0;
9656
+ const { code: fixed, fixes } = await autoFixCode(code, autoFixCtxElse);
9573
9657
  if (fixes.length > 0) {
9574
9658
  code = fixed;
9575
9659
  await writeFile(absPath, code);
@@ -9588,7 +9672,9 @@ ${pagesCtx}`
9588
9672
  allShared: manifest.shared,
9589
9673
  layoutShared: manifest.shared.filter((c) => c.type === "layout")
9590
9674
  });
9591
- const issues = validatePageQuality(code);
9675
+ const currentPlanForQuality = currentPlanForElse;
9676
+ const pageTypeForQuality = currentPlanForQuality ? getPageType(route, currentPlanForQuality) : inferPageTypeFromRoute(route);
9677
+ const issues = validatePageQuality(code, void 0, pageTypeForQuality);
9592
9678
  const report = formatIssues(issues);
9593
9679
  if (report) {
9594
9680
  console.log(chalk12.yellow(`
@@ -10380,8 +10466,16 @@ async function chatCommand(message, options) {
10380
10466
  if (authRelated) authRelated.forEach((l) => allLinkedRoutes.add(l));
10381
10467
  }
10382
10468
  const existingRoutes = new Set(currentConfig.pages.map((p) => p.route).filter(Boolean));
10469
+ const expandedExisting = new Set(existingRoutes);
10470
+ for (const route of existingRoutes) {
10471
+ const canonical = AUTH_SYNONYMS[route] ?? route;
10472
+ expandedExisting.add(canonical);
10473
+ for (const [syn, can] of Object.entries(AUTH_SYNONYMS)) {
10474
+ if (can === canonical) expandedExisting.add(syn);
10475
+ }
10476
+ }
10383
10477
  const missingRoutes = [...allLinkedRoutes].filter((route) => {
10384
- if (existingRoutes.has(route)) return false;
10478
+ if (expandedExisting.has(route)) return false;
10385
10479
  if (existsSync16(routeToFsPath(projectRoot, route, false))) return false;
10386
10480
  if (existsSync16(routeToFsPath(projectRoot, route, true))) return false;
10387
10481
  return true;
@@ -10594,8 +10688,9 @@ async function chatCommand(message, options) {
10594
10688
  const preflightNames = preflightInstalledIds.map((id) => updatedConfig.components.find((c) => c.id === id)?.name).filter(Boolean);
10595
10689
  showPreview(normalizedRequests, results, updatedConfig, preflightNames);
10596
10690
  if (scaffoldedPages.length > 0) {
10691
+ const uniqueScaffolded = [...new Map(scaffoldedPages.map((s) => [s.route, s])).values()];
10597
10692
  console.log(chalk14.cyan("\u{1F517} Auto-scaffolded linked pages:"));
10598
- scaffoldedPages.forEach(({ route, name }) => {
10693
+ uniqueScaffolded.forEach(({ route, name }) => {
10599
10694
  console.log(chalk14.white(` \u2728 ${name} \u2192 ${route}`));
10600
10695
  });
10601
10696
  console.log("");
@@ -11528,7 +11623,7 @@ async function previewCommand() {
11528
11623
  }
11529
11624
  console.log(chalk15.green("\n\u2705 Dependencies installed\n"));
11530
11625
  } else {
11531
- spinner.succeed("Project ready");
11626
+ spinner.succeed("Dependencies installed");
11532
11627
  }
11533
11628
  if (needsGlobalsFix(projectRoot)) {
11534
11629
  spinner.text = "Fixing globals.css...";
@@ -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
  */
@@ -11,7 +11,7 @@ import {
11
11
  routeToKey,
12
12
  savePlan,
13
13
  updateArchitecturePlan
14
- } from "./chunk-4M2RBSYF.js";
14
+ } from "./chunk-IKHAW6OI.js";
15
15
  import "./chunk-3RG5ZIWI.js";
16
16
  export {
17
17
  ArchitecturePlanSchema,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.0",
6
+ "version": "0.6.3",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -33,15 +33,8 @@
33
33
  ],
34
34
  "author": "Coherent Design Method",
35
35
  "license": "MIT",
36
- "scripts": {
37
- "dev": "tsup --watch",
38
- "build": "tsup",
39
- "typecheck": "tsc --noEmit",
40
- "test": "vitest"
41
- },
42
36
  "dependencies": {
43
37
  "@anthropic-ai/sdk": "^0.32.0",
44
- "@getcoherent/core": "workspace:*",
45
38
  "chalk": "^5.3.0",
46
39
  "chokidar": "^4.0.1",
47
40
  "commander": "^11.1.0",
@@ -49,12 +42,19 @@
49
42
  "open": "^10.1.0",
50
43
  "ora": "^7.0.1",
51
44
  "prompts": "^2.4.2",
52
- "zod": "^3.22.4"
45
+ "zod": "^3.22.4",
46
+ "@getcoherent/core": "0.6.3"
53
47
  },
54
48
  "devDependencies": {
55
49
  "@types/node": "^20.11.0",
56
50
  "@types/prompts": "^2.4.9",
57
51
  "tsup": "^8.0.1",
58
52
  "typescript": "^5.3.3"
53
+ },
54
+ "scripts": {
55
+ "dev": "tsup --watch",
56
+ "build": "tsup",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest"
59
59
  }
60
- }
60
+ }