@getcoherent/cli 0.6.14 → 0.6.16
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-provider-HUQO64P3.js → ai-provider-CGSIYFZT.js} +1 -1
- package/dist/{chunk-WRDWFCQJ.js → chunk-PVJJ2YXP.js} +7 -5
- package/dist/{chunk-25JRF5MA.js → chunk-SPQZBQYY.js} +4 -4
- package/dist/{claude-BZ3HSBD3.js → claude-K5B5ITAT.js} +1 -1
- package/dist/index.js +573 -103
- package/dist/{openai-provider-XUI7ZHUR.js → openai-provider-4KGARVC3.js} +1 -1
- package/dist/{plan-generator-H55WEIY2.js → plan-generator-ITHYNYJI.js} +1 -1
- package/package.json +10 -10
- package/LICENSE +0 -21
|
@@ -253,7 +253,7 @@ Style context: ${styleContext || "default"}
|
|
|
253
253
|
${designRules}
|
|
254
254
|
|
|
255
255
|
Requirements:
|
|
256
|
-
- Each component MUST
|
|
256
|
+
- Each component MUST use a NAMED export: \`export function ComponentName\` (NOT export default)
|
|
257
257
|
- Use shadcn/ui imports from @/components/ui/*
|
|
258
258
|
- Use Tailwind CSS classes matching the style context
|
|
259
259
|
- TypeScript with proper props interface
|
|
@@ -269,9 +269,10 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
|
|
|
269
269
|
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
270
270
|
);
|
|
271
271
|
const code = match?.changes?.pageCode;
|
|
272
|
-
if (code && code.includes("export default")) {
|
|
272
|
+
if (code && (code.includes("export function") || code.includes("export default"))) {
|
|
273
|
+
const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
|
|
273
274
|
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
274
|
-
results.push({ name: comp.name, code, file });
|
|
275
|
+
results.push({ name: comp.name, code: fixedCode, file });
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
} catch {
|
|
@@ -284,9 +285,10 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
|
|
|
284
285
|
(r) => r.type === "add-page" && r.changes?.name === comp.name
|
|
285
286
|
);
|
|
286
287
|
const code = match?.changes?.pageCode;
|
|
287
|
-
if (code && code.includes("export default")) {
|
|
288
|
+
if (code && (code.includes("export function") || code.includes("export default"))) {
|
|
289
|
+
const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
|
|
288
290
|
const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
|
|
289
|
-
results.push({ name: comp.name, code, file });
|
|
291
|
+
results.push({ name: comp.name, code: fixedCode, file });
|
|
290
292
|
}
|
|
291
293
|
} catch {
|
|
292
294
|
}
|
|
@@ -68,7 +68,7 @@ Please set ${envVar} in your environment or .env file.`);
|
|
|
68
68
|
}
|
|
69
69
|
if (preferredProvider === "openai") {
|
|
70
70
|
try {
|
|
71
|
-
const { OpenAIClient } = await import("./openai-provider-
|
|
71
|
+
const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
|
|
72
72
|
return await OpenAIClient.create(apiKey2, config?.model);
|
|
73
73
|
} catch (error) {
|
|
74
74
|
if (error.message?.includes("not installed")) {
|
|
@@ -82,7 +82,7 @@ Error: ${error.message}`
|
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
} else {
|
|
85
|
-
const { ClaudeClient } = await import("./claude-
|
|
85
|
+
const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
|
|
86
86
|
return ClaudeClient.create(apiKey2, config?.model);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -95,7 +95,7 @@ Error: ${error.message}`
|
|
|
95
95
|
switch (provider) {
|
|
96
96
|
case "openai":
|
|
97
97
|
try {
|
|
98
|
-
const { OpenAIClient } = await import("./openai-provider-
|
|
98
|
+
const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
|
|
99
99
|
return await OpenAIClient.create(apiKey, config?.model);
|
|
100
100
|
} catch (error) {
|
|
101
101
|
if (error.message?.includes("not installed")) {
|
|
@@ -109,7 +109,7 @@ Error: ${error.message}`
|
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
case "claude":
|
|
112
|
-
const { ClaudeClient } = await import("./claude-
|
|
112
|
+
const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
|
|
113
113
|
return ClaudeClient.create(apiKey, config?.model);
|
|
114
114
|
default:
|
|
115
115
|
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
@@ -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
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createAIProvider
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SPQZBQYY.js";
|
|
4
4
|
import {
|
|
5
5
|
autoFixCode,
|
|
6
6
|
checkDesignConsistency,
|
|
@@ -14,8 +14,9 @@ import {
|
|
|
14
14
|
getPageType,
|
|
15
15
|
loadPlan,
|
|
16
16
|
routeToKey,
|
|
17
|
-
savePlan
|
|
18
|
-
|
|
17
|
+
savePlan,
|
|
18
|
+
updateArchitecturePlan
|
|
19
|
+
} from "./chunk-PVJJ2YXP.js";
|
|
19
20
|
import {
|
|
20
21
|
CORE_CONSTRAINTS,
|
|
21
22
|
DESIGN_QUALITY,
|
|
@@ -2464,7 +2465,7 @@ import { loadManifest } from "@getcoherent/core";
|
|
|
2464
2465
|
import { DesignSystemManager } from "@getcoherent/core";
|
|
2465
2466
|
function buildSharedComponentsListForClaude(manifest) {
|
|
2466
2467
|
if (!manifest.shared || manifest.shared.length === 0) {
|
|
2467
|
-
return "No shared components yet. Register with: coherent components shared add <name> --type layout|section|widget";
|
|
2468
|
+
return "No shared components yet. Register with: coherent components shared add <name> --type layout|navigation|data-display|form|feedback|section|widget";
|
|
2468
2469
|
}
|
|
2469
2470
|
const order = {
|
|
2470
2471
|
layout: 0,
|
|
@@ -2959,7 +2960,7 @@ function buildSharedComponentsList(manifest) {
|
|
|
2959
2960
|
if (!manifest.shared || manifest.shared.length === 0) {
|
|
2960
2961
|
return `No shared components registered yet.
|
|
2961
2962
|
When you create reusable blocks (headers, footers, repeated sections),
|
|
2962
|
-
register them: coherent components shared add <Name> --type layout|section|widget`;
|
|
2963
|
+
register them: coherent components shared add <Name> --type layout|navigation|data-display|form|feedback|section|widget`;
|
|
2963
2964
|
}
|
|
2964
2965
|
const typeOrder = {
|
|
2965
2966
|
layout: 0,
|
|
@@ -3857,7 +3858,7 @@ import {
|
|
|
3857
3858
|
CLI_VERSION as CLI_VERSION2,
|
|
3858
3859
|
getTemplateForPageType as getTemplateForPageType2,
|
|
3859
3860
|
loadManifest as loadManifest8,
|
|
3860
|
-
saveManifest as
|
|
3861
|
+
saveManifest as saveManifest3,
|
|
3861
3862
|
updateEntry
|
|
3862
3863
|
} from "@getcoherent/core";
|
|
3863
3864
|
|
|
@@ -4101,7 +4102,7 @@ function detectPageType(pageName) {
|
|
|
4101
4102
|
const normalized = pageName.toLowerCase();
|
|
4102
4103
|
if (/dashboard|admin|overview/.test(normalized)) return "dashboard";
|
|
4103
4104
|
if (/login|signin|sign-in/.test(normalized)) return "login";
|
|
4104
|
-
if (/
|
|
4105
|
+
if (/regist(?:er|ration)|signup|sign.?up/.test(normalized)) return "register";
|
|
4105
4106
|
if (/pricing|plans|subscription/.test(normalized)) return "pricing";
|
|
4106
4107
|
if (/about|company/.test(normalized)) return "about";
|
|
4107
4108
|
if (/contact|support|help/.test(normalized)) return "contact";
|
|
@@ -4171,7 +4172,8 @@ async function parseModification(message, context, provider = "auto", options) {
|
|
|
4171
4172
|
const prompt = buildModificationPrompt(enhancedMessage, context.config, componentRegistry, {
|
|
4172
4173
|
isExpandedPageRequest,
|
|
4173
4174
|
sharedComponentsSummary: options?.sharedComponentsSummary,
|
|
4174
|
-
tieredComponentsPrompt: options?.tieredComponentsPrompt
|
|
4175
|
+
tieredComponentsPrompt: options?.tieredComponentsPrompt,
|
|
4176
|
+
reusePlanDirective: options?.reusePlanDirective
|
|
4175
4177
|
});
|
|
4176
4178
|
const raw = await ai.parseModification(prompt);
|
|
4177
4179
|
const requestsArray = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
@@ -4237,7 +4239,14 @@ Rules:
|
|
|
4237
4239
|
function buildModificationPrompt(message, config2, componentRegistry, options) {
|
|
4238
4240
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4239
4241
|
const expandedHint = options?.isExpandedPageRequest === true ? "\nIMPORTANT: The user request has been expanded with best practices. Use ALL the details provided when generating sections and content.\n\n" : "";
|
|
4240
|
-
const sharedSection = options?.
|
|
4242
|
+
const sharedSection = options?.reusePlanDirective ? `
|
|
4243
|
+
|
|
4244
|
+
## COMPONENT REUSE DIRECTIVE
|
|
4245
|
+
|
|
4246
|
+
${options.reusePlanDirective}
|
|
4247
|
+
|
|
4248
|
+
For editing an existing shared component use type "modify-layout-block" with target "CID-XXX" or name.
|
|
4249
|
+
` : options?.tieredComponentsPrompt ? `
|
|
4241
4250
|
|
|
4242
4251
|
## SHARED COMPONENTS (MANDATORY REUSE)
|
|
4243
4252
|
|
|
@@ -4520,9 +4529,9 @@ Return valid JSON only, no markdown code fence. Use this shape:
|
|
|
4520
4529
|
{ "requests": [ ... array of ModificationRequest ... ], "uxRecommendations": "optional markdown or omit key" }
|
|
4521
4530
|
Legacy: returning only a JSON array of requests is still accepted.`;
|
|
4522
4531
|
}
|
|
4523
|
-
function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType, tieredComponentsPrompt) {
|
|
4532
|
+
function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType, tieredComponentsPrompt, reusePlanDirective) {
|
|
4524
4533
|
const designConstraints = pageType ? getDesignQualityForType(pageType) : "";
|
|
4525
|
-
const sharedNote = tieredComponentsPrompt || (sharedComponentsSummary ? `Available shared components:
|
|
4534
|
+
const sharedNote = reusePlanDirective || tieredComponentsPrompt || (sharedComponentsSummary ? `Available shared components:
|
|
4526
4535
|
${sharedComponentsSummary}` : "");
|
|
4527
4536
|
return [
|
|
4528
4537
|
`Generate complete pageCode for a page called "${pageName}" at route "${route}".`,
|
|
@@ -5028,7 +5037,7 @@ var AUTH_SYNONYMS = {
|
|
|
5028
5037
|
};
|
|
5029
5038
|
function deduplicatePages(pages) {
|
|
5030
5039
|
const canonicalize = (route) => AUTH_SYNONYMS[route] || route;
|
|
5031
|
-
const normalize = (route) => canonicalize(route).replace(/\/$/, "")
|
|
5040
|
+
const normalize = (route) => canonicalize(route).replace(/\/$/, "");
|
|
5032
5041
|
const seen = /* @__PURE__ */ new Map();
|
|
5033
5042
|
return pages.filter((page, idx) => {
|
|
5034
5043
|
const norm = normalize(page.route);
|
|
@@ -5050,10 +5059,10 @@ function extractComponentIdsFromCode(code) {
|
|
|
5050
5059
|
return ids;
|
|
5051
5060
|
}
|
|
5052
5061
|
async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, manifest, plan) {
|
|
5053
|
-
const
|
|
5054
|
-
if (
|
|
5062
|
+
const reusable = manifest.shared.filter((e) => e.type !== "layout");
|
|
5063
|
+
if (reusable.length === 0) return;
|
|
5055
5064
|
const plannedForPage = plan ? new Set(plan.sharedComponents.filter((c) => c.usedBy.includes(route)).map((c) => c.name)) : null;
|
|
5056
|
-
for (const e of
|
|
5065
|
+
for (const e of reusable) {
|
|
5057
5066
|
if (plannedForPage && !plannedForPage.has(e.name)) continue;
|
|
5058
5067
|
const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
|
|
5059
5068
|
const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
|
|
@@ -5298,6 +5307,52 @@ function extractPageNamesFromMessage(message) {
|
|
|
5298
5307
|
}
|
|
5299
5308
|
return pages;
|
|
5300
5309
|
}
|
|
5310
|
+
function detectExplicitRootPage(message, pageNames) {
|
|
5311
|
+
const lower = message.toLowerCase();
|
|
5312
|
+
const w = "[a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u04010-9_]";
|
|
5313
|
+
const patterns = [
|
|
5314
|
+
new RegExp(
|
|
5315
|
+
`(?:main|start|home|root|first|entry|primary|\u0441\u0442\u0430\u0440\u0442\u043E\u0432${w}*|\u0433\u043B\u0430\u0432\u043D${w}*|\u043D\u0430\u0447\u0430\u043B\u044C\u043D${w}*)\\s*(?:page|screen|view|\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*|\u044D\u043A\u0440\u0430\u043D${w}*)\\s*(?:is|should be|:|\u2014|\u2013|-|\u0431\u0443\u0434\u0435\u0442|\u044D\u0442\u043E)\\s*([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+)`,
|
|
5316
|
+
"i"
|
|
5317
|
+
),
|
|
5318
|
+
new RegExp(
|
|
5319
|
+
`([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+)\\s*(?:as|for|\u043A\u0430\u043A|\u0432 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435)\\s*(?:the\\s+)?(?:main|start|home|root|primary|\u0441\u0442\u0430\u0440\u0442\u043E\u0432${w}*|\u0433\u043B\u0430\u0432\u043D${w}*)\\s*(?:page|screen|\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*)`,
|
|
5320
|
+
"i"
|
|
5321
|
+
),
|
|
5322
|
+
new RegExp(
|
|
5323
|
+
`(?:start|begin|\u043D\u0430\u0447\u0430\u0442${w}*|\u043D\u0430\u0447\u0438\u043D${w}*)\\s*(?:with|from|\u0441|\u0441\u043E)\\s*(?:a\\s+|an\\s+)?([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+?)(?:\\s+page|\\s+screen|\\s+form|\\s+\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*|\\s+\u0444\u043E\u0440\u043C${w}*)?(?:\\s*$|[,.])`,
|
|
5324
|
+
"i"
|
|
5325
|
+
)
|
|
5326
|
+
];
|
|
5327
|
+
for (const pattern of patterns) {
|
|
5328
|
+
const match = lower.match(pattern);
|
|
5329
|
+
if (match) {
|
|
5330
|
+
const keyword = match[1].trim();
|
|
5331
|
+
const found = pageNames.find(
|
|
5332
|
+
(p) => p.name.toLowerCase().includes(keyword) || keyword.includes(p.name.toLowerCase()) || p.id.includes(keyword.replace(/\s+/g, "-"))
|
|
5333
|
+
);
|
|
5334
|
+
if (found) return found.id;
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
return null;
|
|
5338
|
+
}
|
|
5339
|
+
var NON_MARKETING_ROUTES = /* @__PURE__ */ new Set([
|
|
5340
|
+
"/login",
|
|
5341
|
+
"/signin",
|
|
5342
|
+
"/signup",
|
|
5343
|
+
"/register",
|
|
5344
|
+
"/forgot-password",
|
|
5345
|
+
"/reset-password",
|
|
5346
|
+
"/dashboard",
|
|
5347
|
+
"/settings",
|
|
5348
|
+
"/account",
|
|
5349
|
+
"/tasks",
|
|
5350
|
+
"/profile"
|
|
5351
|
+
]);
|
|
5352
|
+
function isAppOnlyRequest(pageNames) {
|
|
5353
|
+
if (pageNames.length === 0) return false;
|
|
5354
|
+
return pageNames.every((p) => NON_MARKETING_ROUTES.has(p.route) || p.route.startsWith("/dashboard"));
|
|
5355
|
+
}
|
|
5301
5356
|
function normalizeRequest(request, config2) {
|
|
5302
5357
|
const changes = request.changes;
|
|
5303
5358
|
const VALID_TYPES = [
|
|
@@ -5567,7 +5622,9 @@ import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync
|
|
|
5567
5622
|
import { resolve as resolve6 } from "path";
|
|
5568
5623
|
import { z } from "zod";
|
|
5569
5624
|
import {
|
|
5625
|
+
SharedComponentTypeSchema,
|
|
5570
5626
|
loadManifest as loadManifest5,
|
|
5627
|
+
saveManifest,
|
|
5571
5628
|
generateSharedComponent as generateSharedComponent2
|
|
5572
5629
|
} from "@getcoherent/core";
|
|
5573
5630
|
|
|
@@ -5688,6 +5745,140 @@ async function pMap(items, fn, concurrency = 3) {
|
|
|
5688
5745
|
|
|
5689
5746
|
// src/commands/chat/split-generator.ts
|
|
5690
5747
|
import chalk8 from "chalk";
|
|
5748
|
+
|
|
5749
|
+
// src/utils/reuse-planner.ts
|
|
5750
|
+
var SECTION_TYPE_MAP = {
|
|
5751
|
+
stats: ["data-display", "widget"],
|
|
5752
|
+
metrics: ["data-display", "widget"],
|
|
5753
|
+
kpi: ["data-display", "widget"],
|
|
5754
|
+
list: ["data-display"],
|
|
5755
|
+
table: ["data-display"],
|
|
5756
|
+
items: ["data-display"],
|
|
5757
|
+
form: ["form"],
|
|
5758
|
+
filter: ["form"],
|
|
5759
|
+
search: ["form"],
|
|
5760
|
+
nav: ["navigation"],
|
|
5761
|
+
menu: ["navigation"],
|
|
5762
|
+
tabs: ["navigation"],
|
|
5763
|
+
card: ["widget", "data-display"],
|
|
5764
|
+
grid: ["widget", "data-display"],
|
|
5765
|
+
chart: ["data-display"],
|
|
5766
|
+
graph: ["data-display"],
|
|
5767
|
+
alert: ["feedback"],
|
|
5768
|
+
toast: ["feedback"],
|
|
5769
|
+
banner: ["feedback"]
|
|
5770
|
+
};
|
|
5771
|
+
function sectionToComponentTypes(section) {
|
|
5772
|
+
const lower = section.toLowerCase();
|
|
5773
|
+
const types = /* @__PURE__ */ new Set();
|
|
5774
|
+
for (const [keyword, componentTypes] of Object.entries(SECTION_TYPE_MAP)) {
|
|
5775
|
+
if (lower.includes(keyword)) {
|
|
5776
|
+
componentTypes.forEach((t) => types.add(t));
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
return [...types];
|
|
5780
|
+
}
|
|
5781
|
+
function componentFilenameToImportPath(file) {
|
|
5782
|
+
const withoutExt = file.replace(/\.tsx?$/, "");
|
|
5783
|
+
return `@/${withoutExt}`;
|
|
5784
|
+
}
|
|
5785
|
+
function buildReusePlan(input) {
|
|
5786
|
+
const { pageName, sections, manifest } = input;
|
|
5787
|
+
const reuse = [];
|
|
5788
|
+
const usedComponents = /* @__PURE__ */ new Set();
|
|
5789
|
+
for (const section of sections) {
|
|
5790
|
+
const matchingTypes = sectionToComponentTypes(section);
|
|
5791
|
+
if (matchingTypes.length === 0) continue;
|
|
5792
|
+
for (const entry of manifest.shared) {
|
|
5793
|
+
if (usedComponents.has(entry.id)) continue;
|
|
5794
|
+
if (!matchingTypes.includes(entry.type)) continue;
|
|
5795
|
+
reuse.push({
|
|
5796
|
+
component: entry.name,
|
|
5797
|
+
targetSection: section,
|
|
5798
|
+
reason: entry.usedIn.length > 0 ? `Used on ${entry.usedIn.length} page(s)` : `Matches section type (${entry.type})`,
|
|
5799
|
+
importPath: componentFilenameToImportPath(entry.file),
|
|
5800
|
+
usageExample: entry.usageExample || `<${entry.name} />`
|
|
5801
|
+
});
|
|
5802
|
+
usedComponents.add(entry.id);
|
|
5803
|
+
break;
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
const reusePatterns = extractCodePatterns(input.existingPageCode, sections);
|
|
5807
|
+
return { pageName, reuse, createNew: [], reusePatterns };
|
|
5808
|
+
}
|
|
5809
|
+
function extractCodePatterns(existingPageCode, _sections) {
|
|
5810
|
+
const patterns = [];
|
|
5811
|
+
const gridPatterns = /* @__PURE__ */ new Map();
|
|
5812
|
+
for (const [route, code] of Object.entries(existingPageCode)) {
|
|
5813
|
+
const gridMatches = code.match(/className="[^"]*grid[^"]*"/g) || [];
|
|
5814
|
+
for (const match of gridMatches) {
|
|
5815
|
+
const cls = match.replace(/className="|"/g, "");
|
|
5816
|
+
if (!gridPatterns.has(cls)) gridPatterns.set(cls, []);
|
|
5817
|
+
gridPatterns.get(cls).push(route);
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
for (const [pattern, pages] of gridPatterns) {
|
|
5821
|
+
if (pages.length >= 1 && pattern.includes("grid-cols")) {
|
|
5822
|
+
patterns.push({
|
|
5823
|
+
pattern,
|
|
5824
|
+
sourcePages: pages,
|
|
5825
|
+
targetSection: "Layout grid"
|
|
5826
|
+
});
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
return patterns;
|
|
5830
|
+
}
|
|
5831
|
+
function buildReusePlanDirective(plan) {
|
|
5832
|
+
if (plan.reuse.length === 0 && plan.createNew.length === 0 && plan.reusePatterns.length === 0) {
|
|
5833
|
+
return "";
|
|
5834
|
+
}
|
|
5835
|
+
const lines = [`COMPONENT REUSE PLAN FOR THIS PAGE:`];
|
|
5836
|
+
if (plan.reuse.length > 0) {
|
|
5837
|
+
lines.push("", "MUST USE (import these \u2014 do NOT re-implement):");
|
|
5838
|
+
for (const r of plan.reuse) {
|
|
5839
|
+
lines.push(` - ${r.component} from ${r.importPath} \u2014 for "${r.targetSection}" section`);
|
|
5840
|
+
if (r.usageExample) {
|
|
5841
|
+
lines.push(` Example: ${r.usageExample}`);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
if (plan.createNew.length > 0) {
|
|
5846
|
+
lines.push("", "CREATE NEW (no existing match):");
|
|
5847
|
+
for (const c of plan.createNew) {
|
|
5848
|
+
lines.push(` - ${c.name} \u2014 ${c.reason} (suggest type: ${c.suggestedType})`);
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
if (plan.reusePatterns.length > 0) {
|
|
5852
|
+
lines.push("", "LAYOUT PATTERNS (copy from existing pages for visual consistency):");
|
|
5853
|
+
for (const p of plan.reusePatterns) {
|
|
5854
|
+
lines.push(` - ${p.targetSection}: className="${p.pattern}"`);
|
|
5855
|
+
lines.push(` (source: ${p.sourcePages.join(", ")})`);
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
return lines.join("\n");
|
|
5859
|
+
}
|
|
5860
|
+
function verifyReusePlan(generatedCode, plan) {
|
|
5861
|
+
const passed = [];
|
|
5862
|
+
const missed = [];
|
|
5863
|
+
for (const entry of plan.reuse) {
|
|
5864
|
+
const isImported = generatedCode.includes(entry.importPath) || generatedCode.includes(`{ ${entry.component} }`) || generatedCode.includes(`{ ${entry.component},`);
|
|
5865
|
+
if (isImported) {
|
|
5866
|
+
passed.push(entry);
|
|
5867
|
+
} else {
|
|
5868
|
+
missed.push(entry);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
let retryDirective;
|
|
5872
|
+
if (missed.length > 0) {
|
|
5873
|
+
const lines = missed.map(
|
|
5874
|
+
(m) => `CRITICAL: Your previous output failed to import ${m.component} from ${m.importPath}. You MUST import and use this component for the "${m.targetSection}" section. Do NOT re-implement it inline.`
|
|
5875
|
+
);
|
|
5876
|
+
retryDirective = lines.join("\n");
|
|
5877
|
+
}
|
|
5878
|
+
return { passed, missed, retryDirective };
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5881
|
+
// src/commands/chat/split-generator.ts
|
|
5691
5882
|
function buildExistingPagesContext(config2) {
|
|
5692
5883
|
const pages = config2.pages || [];
|
|
5693
5884
|
const analyzed = pages.filter((p) => p.pageAnalysis);
|
|
@@ -5758,7 +5949,7 @@ function extractStyleContext(pageCode) {
|
|
|
5758
5949
|
return `STYLE CONTEXT (match these patterns exactly for visual consistency with the anchor page):
|
|
5759
5950
|
${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
5760
5951
|
}
|
|
5761
|
-
var VALID_NAV_TYPES = /* @__PURE__ */ new Set(["header", "sidebar", "both"]);
|
|
5952
|
+
var VALID_NAV_TYPES = /* @__PURE__ */ new Set(["header", "sidebar", "both", "none"]);
|
|
5762
5953
|
function parseNavTypeFromPlan(planResult) {
|
|
5763
5954
|
const nav = planResult.navigation;
|
|
5764
5955
|
if (nav && typeof nav.type === "string" && VALID_NAV_TYPES.has(nav.type)) {
|
|
@@ -5882,6 +6073,52 @@ function readExistingAppPageForReference(projectRoot, plan) {
|
|
|
5882
6073
|
}
|
|
5883
6074
|
return null;
|
|
5884
6075
|
}
|
|
6076
|
+
function buildLayoutNote(layoutType) {
|
|
6077
|
+
switch (layoutType) {
|
|
6078
|
+
case "sidebar":
|
|
6079
|
+
return "This page uses a SIDEBAR layout. The sidebar navigation is already rendered by the group layout. Do NOT create your own sidebar or side navigation. Start with the main content area directly. The page content appears to the right of the sidebar.";
|
|
6080
|
+
case "both":
|
|
6081
|
+
return "This page has both a sidebar and a header rendered by the group layout. Do NOT include any site-wide header, nav, sidebar, or footer in this page. Start with the main content directly.";
|
|
6082
|
+
case "none":
|
|
6083
|
+
return "This page has no shared navigation from the layout. Include any needed navigation within the page itself.";
|
|
6084
|
+
default:
|
|
6085
|
+
return "Header and Footer are shared components rendered by the root layout. Do NOT include any site-wide <header>, <nav>, or <footer> in this page. Start with the main content directly.";
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
function buildAnchorPagePrompt(homePage, message, allPagesList, allRoutes, plan) {
|
|
6089
|
+
const pageType = detectPageType(homePage.name) || detectPageType(homePage.route);
|
|
6090
|
+
const authPageTypes = /* @__PURE__ */ new Set(["login", "register", "reset-password"]);
|
|
6091
|
+
const isAuth = isAuthRoute(homePage.route) || isAuthRoute(homePage.name) || authPageTypes.has(pageType || "");
|
|
6092
|
+
if (isAuth) {
|
|
6093
|
+
return `Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This is the application's entry point \u2014 a clean, centered authentication form. Generate complete pageCode. Do NOT include site-wide <header>, <nav>, or <footer> \u2014 this page has its own minimal layout. Make it visually polished with proper form validation UI \u2014 this page sets the design direction for the entire site. Do not generate other pages.`;
|
|
6094
|
+
}
|
|
6095
|
+
const groupLayout = plan?.groups.find((g) => g.pages.includes(homePage.route))?.layout;
|
|
6096
|
+
if (groupLayout === "sidebar" || pageType === "dashboard") {
|
|
6097
|
+
return `Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This REPLACES the default placeholder page \u2014 generate a complete application page. Generate complete pageCode. Do NOT include a sidebar or top navigation \u2014 these are handled by the layout. Focus on the main content area. Make it visually polished \u2014 this page sets the design direction for the entire site. Do not generate other pages.`;
|
|
6098
|
+
}
|
|
6099
|
+
return `Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This REPLACES the default placeholder page \u2014 generate a complete, content-rich landing page for the project described above. Generate complete pageCode. Include a branded site-wide <header> with navigation links to ALL these pages: ${allPagesList}. Use these EXACT routes in navigation: ${allRoutes}. Include a <footer> at the bottom. Make it visually polished \u2014 this page sets the design direction for the entire site. Do not generate other pages.`;
|
|
6100
|
+
}
|
|
6101
|
+
function getGroupLayoutForRoute(route, plan) {
|
|
6102
|
+
if (!plan) return void 0;
|
|
6103
|
+
const group = plan.groups.find((g) => g.pages.includes(route));
|
|
6104
|
+
return group?.layout;
|
|
6105
|
+
}
|
|
6106
|
+
var manifestLock = Promise.resolve();
|
|
6107
|
+
async function updateManifestSafe(projectRoot, fn) {
|
|
6108
|
+
const timeoutMs = 5e3;
|
|
6109
|
+
const update = manifestLock.then(async () => {
|
|
6110
|
+
const m = await loadManifest5(projectRoot);
|
|
6111
|
+
const updated = fn(m);
|
|
6112
|
+
await saveManifest(projectRoot, updated);
|
|
6113
|
+
});
|
|
6114
|
+
manifestLock = update.catch(() => {
|
|
6115
|
+
});
|
|
6116
|
+
await Promise.race([
|
|
6117
|
+
update,
|
|
6118
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("manifest sync timeout")), timeoutMs))
|
|
6119
|
+
]).catch(() => {
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
5885
6122
|
async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
|
|
5886
6123
|
let pageNames = [];
|
|
5887
6124
|
spinner.start("Phase 1/6 \u2014 Planning pages...");
|
|
@@ -5924,7 +6161,11 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
5924
6161
|
(p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
|
|
5925
6162
|
);
|
|
5926
6163
|
const isFreshProject = userPages.length === 0;
|
|
5927
|
-
|
|
6164
|
+
const explicitRootId = detectExplicitRootPage(message, pageNames);
|
|
6165
|
+
if (explicitRootId) {
|
|
6166
|
+
const rootPage = pageNames.find((p) => p.id === explicitRootId);
|
|
6167
|
+
if (rootPage) rootPage.route = "/";
|
|
6168
|
+
} else if (!isAppOnlyRequest(pageNames) && (isFreshProject || impliesFullWebsite(message))) {
|
|
5928
6169
|
pageNames.unshift({ name: "Home", id: "home", route: "/" });
|
|
5929
6170
|
}
|
|
5930
6171
|
}
|
|
@@ -5943,14 +6184,16 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
5943
6184
|
spinner.start("Phase 2/6 \u2014 Generating architecture plan...");
|
|
5944
6185
|
try {
|
|
5945
6186
|
const ai = await createAIProvider(provider ?? "auto");
|
|
5946
|
-
const
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
message,
|
|
5950
|
-
|
|
5951
|
-
layoutHint
|
|
5952
|
-
|
|
5953
|
-
|
|
6187
|
+
const cachedPlan = loadPlan(parseOpts.projectRoot);
|
|
6188
|
+
let planWarnings = [];
|
|
6189
|
+
if (cachedPlan) {
|
|
6190
|
+
plan = await updateArchitecturePlan(cachedPlan, pageNames, message, ai);
|
|
6191
|
+
} else {
|
|
6192
|
+
const layoutHint = modCtx.config.navigation?.type || null;
|
|
6193
|
+
const result = await generateArchitecturePlan(pageNames, message, ai, layoutHint);
|
|
6194
|
+
plan = result.plan;
|
|
6195
|
+
planWarnings = result.warnings;
|
|
6196
|
+
}
|
|
5954
6197
|
if (plan) {
|
|
5955
6198
|
const groupsSummary = plan.groups.map((g) => `${g.id} (${g.layout}, ${g.pages.length} pages)`).join(", ");
|
|
5956
6199
|
const sharedSummary = plan.sharedComponents.length > 0 ? plan.sharedComponents.map((c) => `${c.name} \u2192 ${c.usedBy.join(", ")}`).join(" | ") : "";
|
|
@@ -6001,12 +6244,8 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6001
6244
|
if (!reusedExistingAnchor) {
|
|
6002
6245
|
spinner.start(`Phase 3/6 \u2014 Generating ${homePage.name} page (sets design direction)...`);
|
|
6003
6246
|
try {
|
|
6004
|
-
const
|
|
6005
|
-
|
|
6006
|
-
modCtx,
|
|
6007
|
-
provider,
|
|
6008
|
-
parseOpts
|
|
6009
|
-
);
|
|
6247
|
+
const anchorPrompt = buildAnchorPagePrompt(homePage, message, allPagesList, allRoutes, plan);
|
|
6248
|
+
const homeResult = await parseModification(anchorPrompt, modCtx, provider, parseOpts);
|
|
6010
6249
|
const codePage = homeResult.requests.find((r) => r.type === "add-page");
|
|
6011
6250
|
if (codePage) {
|
|
6012
6251
|
homeRequest = codePage;
|
|
@@ -6036,7 +6275,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6036
6275
|
if (plan && plan.sharedComponents.length > 0) {
|
|
6037
6276
|
spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
|
|
6038
6277
|
try {
|
|
6039
|
-
const { generateSharedComponentsFromPlan } = await import("./plan-generator-
|
|
6278
|
+
const { generateSharedComponentsFromPlan } = await import("./plan-generator-ITHYNYJI.js");
|
|
6040
6279
|
const generated = await generateSharedComponentsFromPlan(
|
|
6041
6280
|
plan,
|
|
6042
6281
|
styleContext,
|
|
@@ -6078,7 +6317,6 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6078
6317
|
return { requests: homeRequest ? [homeRequest] : [], plan };
|
|
6079
6318
|
}
|
|
6080
6319
|
spinner.start(`Phase 5/6 \u2014 Generating ${remainingPages.length} pages in parallel...`);
|
|
6081
|
-
const sharedLayoutNote = "Header and Footer are shared components rendered by the root layout. Do NOT include any site-wide <header>, <nav>, or <footer> in this page. Start with the main content directly.";
|
|
6082
6320
|
const sharedComponentsNote = buildSharedComponentsNote(parseOpts.sharedComponentsSummary);
|
|
6083
6321
|
const currentManifest = projectRoot ? await loadManifest5(projectRoot) : null;
|
|
6084
6322
|
const routeNote = `EXISTING ROUTES in this project: ${allRoutes}. All internal links MUST point to one of these routes. If a target doesn't exist, use href="#".`;
|
|
@@ -6092,6 +6330,23 @@ ${existingAppPageCode}
|
|
|
6092
6330
|
\`\`\`
|
|
6093
6331
|
` : "";
|
|
6094
6332
|
const existingPagesContext = buildExistingPagesContext(modCtx.config);
|
|
6333
|
+
const existingPageCode = {};
|
|
6334
|
+
if (projectRoot) {
|
|
6335
|
+
const appDir = resolve6(projectRoot, "app");
|
|
6336
|
+
if (existsSync14(appDir)) {
|
|
6337
|
+
const pageFiles = readdirSync2(appDir, { recursive: true }).filter(
|
|
6338
|
+
(f) => typeof f === "string" && f.endsWith("page.tsx")
|
|
6339
|
+
);
|
|
6340
|
+
for (const pf of pageFiles) {
|
|
6341
|
+
try {
|
|
6342
|
+
const code = readFileSync9(resolve6(appDir, pf), "utf-8");
|
|
6343
|
+
const route = "/" + pf.replace(/\/page\.tsx$/, "").replace(/\(.*?\)\//g, "");
|
|
6344
|
+
existingPageCode[route === "/" ? "/" : route] = code;
|
|
6345
|
+
} catch {
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6095
6350
|
const AI_CONCURRENCY = 3;
|
|
6096
6351
|
let phase5Done = 0;
|
|
6097
6352
|
const remainingRequests = await pMap(
|
|
@@ -6101,15 +6356,45 @@ ${existingAppPageCode}
|
|
|
6101
6356
|
const pageType = plan ? getPageType(route, plan) : inferPageTypeFromRoute(route);
|
|
6102
6357
|
const designConstraints = getDesignQualityForType(pageType);
|
|
6103
6358
|
const authNote = isAuth ? 'For this auth page: the auth layout already provides centering (flex items-center justify-center min-h-svh). Do NOT add your own centering wrapper or min-h-svh. Just output a div with className="w-full max-w-md" containing the Card. Do NOT use section containers or full-width wrappers.' : void 0;
|
|
6359
|
+
const layoutForPage = getGroupLayoutForRoute(route, plan);
|
|
6360
|
+
const layoutNote = buildLayoutNote(layoutForPage);
|
|
6104
6361
|
const tieredNote = currentManifest ? buildTieredComponentsPrompt(currentManifest, pageType) : void 0;
|
|
6362
|
+
const pageKey = route.replace(/^\//, "") || "home";
|
|
6363
|
+
const pageSections = plan?.pageNotes?.[pageKey]?.sections || [];
|
|
6364
|
+
let reusePlanDirective = "";
|
|
6365
|
+
let currentReusePlan = null;
|
|
6366
|
+
if (currentManifest && currentManifest.shared.length > 0) {
|
|
6367
|
+
try {
|
|
6368
|
+
currentReusePlan = buildReusePlan({
|
|
6369
|
+
pageName: name,
|
|
6370
|
+
pageType,
|
|
6371
|
+
sections: pageSections,
|
|
6372
|
+
manifest: currentManifest,
|
|
6373
|
+
existingPageCode,
|
|
6374
|
+
userRequest: message
|
|
6375
|
+
});
|
|
6376
|
+
reusePlanDirective = buildReusePlanDirective(currentReusePlan);
|
|
6377
|
+
if (currentReusePlan.reuse.length > 0 || currentReusePlan.createNew.length > 0) {
|
|
6378
|
+
const parts = [];
|
|
6379
|
+
if (currentReusePlan.reuse.length > 0)
|
|
6380
|
+
parts.push(`REUSE: ${currentReusePlan.reuse.map((r) => r.component).join(", ")}`);
|
|
6381
|
+
if (currentReusePlan.createNew.length > 0)
|
|
6382
|
+
parts.push(`CREATE: ${currentReusePlan.createNew.map((c) => c.name).join(", ")}`);
|
|
6383
|
+
if (currentReusePlan.reusePatterns.length > 0)
|
|
6384
|
+
parts.push(`${currentReusePlan.reusePatterns.length} pattern(s)`);
|
|
6385
|
+
console.log(chalk8.dim(` \u{1F504} Reuse Plan for "${name}": ${parts.join(" | ")}`));
|
|
6386
|
+
}
|
|
6387
|
+
} catch {
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6105
6390
|
const prompt = [
|
|
6106
6391
|
`Create ONE page called "${name}" at route "${route}".`,
|
|
6107
6392
|
`Context: ${message}.`,
|
|
6108
6393
|
`Generate complete pageCode for this single page only. Do not generate other pages.`,
|
|
6109
6394
|
`PAGE TYPE: ${pageType}`,
|
|
6110
6395
|
designConstraints,
|
|
6111
|
-
|
|
6112
|
-
tieredNote || sharedComponentsNote,
|
|
6396
|
+
layoutNote,
|
|
6397
|
+
reusePlanDirective || tieredNote || sharedComponentsNote,
|
|
6113
6398
|
routeNote,
|
|
6114
6399
|
alignmentNote,
|
|
6115
6400
|
authNote,
|
|
@@ -6122,7 +6407,47 @@ ${existingAppPageCode}
|
|
|
6122
6407
|
const result = await parseModification(prompt, modCtx, provider, parseOpts);
|
|
6123
6408
|
phase5Done++;
|
|
6124
6409
|
spinner.text = `Phase 5/6 \u2014 ${phase5Done}/${remainingPages.length} pages generated...`;
|
|
6125
|
-
|
|
6410
|
+
let codePage = result.requests.find((r) => r.type === "add-page");
|
|
6411
|
+
if (currentReusePlan && currentReusePlan.reuse.length > 0 && codePage) {
|
|
6412
|
+
const pageCode = codePage.changes?.pageCode;
|
|
6413
|
+
if (pageCode) {
|
|
6414
|
+
const verification = verifyReusePlan(pageCode, currentReusePlan);
|
|
6415
|
+
if (verification.passed.length > 0) {
|
|
6416
|
+
console.log(
|
|
6417
|
+
chalk8.dim(` \u2713 Reuse verified for "${name}": ${verification.passed.map((p) => p.component).join(", ")}`)
|
|
6418
|
+
);
|
|
6419
|
+
}
|
|
6420
|
+
if (verification.missed.length > 0 && verification.retryDirective) {
|
|
6421
|
+
console.log(
|
|
6422
|
+
chalk8.yellow(
|
|
6423
|
+
` \u26A0 Missed reuse in "${name}": ${verification.missed.map((m) => m.component).join(", ")} \u2014 retrying...`
|
|
6424
|
+
)
|
|
6425
|
+
);
|
|
6426
|
+
try {
|
|
6427
|
+
const retryPrompt = [prompt, verification.retryDirective].join("\n\n");
|
|
6428
|
+
const retryResult = await parseModification(retryPrompt, modCtx, provider, parseOpts);
|
|
6429
|
+
const retryPage = retryResult.requests.find((r) => r.type === "add-page");
|
|
6430
|
+
if (retryPage) codePage = retryPage;
|
|
6431
|
+
} catch {
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
if (projectRoot && codePage && currentManifest) {
|
|
6437
|
+
const finalPageCode = codePage.changes?.pageCode;
|
|
6438
|
+
if (finalPageCode) {
|
|
6439
|
+
await updateManifestSafe(projectRoot, (m) => {
|
|
6440
|
+
const updatedShared = m.shared.map((entry) => {
|
|
6441
|
+
const isUsed = finalPageCode.includes(`{ ${entry.name} }`) || finalPageCode.includes(`{ ${entry.name},`);
|
|
6442
|
+
if (isUsed && !entry.usedIn.includes(route)) {
|
|
6443
|
+
return { ...entry, usedIn: [...entry.usedIn, route] };
|
|
6444
|
+
}
|
|
6445
|
+
return entry;
|
|
6446
|
+
});
|
|
6447
|
+
return { ...m, shared: updatedShared };
|
|
6448
|
+
});
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6126
6451
|
return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
|
|
6127
6452
|
} catch {
|
|
6128
6453
|
phase5Done++;
|
|
@@ -6170,7 +6495,7 @@ ${existingAppPageCode}
|
|
|
6170
6495
|
}
|
|
6171
6496
|
var SharedExtractionItemSchema = z.object({
|
|
6172
6497
|
name: z.string().min(2).max(50),
|
|
6173
|
-
type:
|
|
6498
|
+
type: SharedComponentTypeSchema,
|
|
6174
6499
|
description: z.string().max(200).default(""),
|
|
6175
6500
|
propsInterface: z.string().default("{}"),
|
|
6176
6501
|
code: z.string()
|
|
@@ -6216,7 +6541,8 @@ async function extractSharedComponents(homePageCode, projectRoot, aiProvider) {
|
|
|
6216
6541
|
const provider = getComponentProvider();
|
|
6217
6542
|
for (const item of filtered) {
|
|
6218
6543
|
try {
|
|
6219
|
-
|
|
6544
|
+
let { code: fixedCode } = await autoFixCode(item.code);
|
|
6545
|
+
fixedCode = fixedCode.replace(/export default function (\w+)/g, "export function $1");
|
|
6220
6546
|
const shadcnImports = [...fixedCode.matchAll(/from\s+["']@\/components\/ui\/(.+?)["']/g)];
|
|
6221
6547
|
for (const match of shadcnImports) {
|
|
6222
6548
|
await provider.installComponent(match[1], projectRoot);
|
|
@@ -6284,7 +6610,7 @@ import chalk11 from "chalk";
|
|
|
6284
6610
|
import {
|
|
6285
6611
|
getTemplateForPageType,
|
|
6286
6612
|
loadManifest as loadManifest6,
|
|
6287
|
-
saveManifest,
|
|
6613
|
+
saveManifest as saveManifest2,
|
|
6288
6614
|
updateUsedIn,
|
|
6289
6615
|
findSharedComponentByIdOrName,
|
|
6290
6616
|
generateSharedComponent as generateSharedComponent4
|
|
@@ -6394,7 +6720,9 @@ async function regeneratePage(pageId, config2, projectRoot) {
|
|
|
6394
6720
|
const code = await generator.generate(page, appType);
|
|
6395
6721
|
const route = page.route || "/";
|
|
6396
6722
|
const isAuth = isAuthRoute(route) || isAuthRoute(page.name || page.id || "");
|
|
6397
|
-
const
|
|
6723
|
+
const { loadPlan: loadPlanForPath } = await import("./plan-generator-ITHYNYJI.js");
|
|
6724
|
+
const planForPath = loadPlanForPath(projectRoot);
|
|
6725
|
+
const filePath = routeToFsPath(projectRoot, route, planForPath || isAuth);
|
|
6398
6726
|
await mkdir3(dirname5(filePath), { recursive: true });
|
|
6399
6727
|
await writeFile(filePath, code);
|
|
6400
6728
|
}
|
|
@@ -6409,7 +6737,9 @@ async function canOverwriteShared(projectRoot, componentFile, storedHashes) {
|
|
|
6409
6737
|
}
|
|
6410
6738
|
return !edited;
|
|
6411
6739
|
}
|
|
6412
|
-
async function regenerateLayout(config2, projectRoot, options = {
|
|
6740
|
+
async function regenerateLayout(config2, projectRoot, options = {
|
|
6741
|
+
navChanged: false
|
|
6742
|
+
}) {
|
|
6413
6743
|
const appType = config2.settings.appType || "multi-page";
|
|
6414
6744
|
const generator = new PageGenerator(config2);
|
|
6415
6745
|
const initialized = config2.settings.initialized !== false;
|
|
@@ -6465,7 +6795,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
|
|
|
6465
6795
|
try {
|
|
6466
6796
|
await integrateSharedLayoutIntoRootLayout2(projectRoot);
|
|
6467
6797
|
await ensureAuthRouteGroup(projectRoot);
|
|
6468
|
-
await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type, options.navChanged);
|
|
6798
|
+
await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type, options.navChanged, options.groupLayouts);
|
|
6469
6799
|
} catch (err) {
|
|
6470
6800
|
if (process.env.COHERENT_DEBUG === "1") {
|
|
6471
6801
|
console.log(chalk9.dim("Layout integration warning:", err));
|
|
@@ -6494,18 +6824,20 @@ async function scanAndInstallSharedDeps(projectRoot) {
|
|
|
6494
6824
|
}
|
|
6495
6825
|
return [...new Set(installed)];
|
|
6496
6826
|
}
|
|
6497
|
-
async function ensureAppRouteGroupLayout(projectRoot, navType, forceUpdate = false) {
|
|
6827
|
+
async function ensureAppRouteGroupLayout(projectRoot, navType, forceUpdate = false, groupLayouts) {
|
|
6828
|
+
const effectiveNavType = groupLayouts?.["app"] || navType;
|
|
6498
6829
|
const layoutPath = resolve7(projectRoot, "app", "(app)", "layout.tsx");
|
|
6499
6830
|
if (existsSync15(layoutPath) && !forceUpdate) return;
|
|
6500
6831
|
const { mkdir: mkdirAsync } = await import("fs/promises");
|
|
6501
6832
|
await mkdirAsync(resolve7(projectRoot, "app", "(app)"), { recursive: true });
|
|
6502
|
-
const code = buildAppLayoutCode(
|
|
6833
|
+
const code = buildAppLayoutCode(effectiveNavType);
|
|
6503
6834
|
await writeFile(layoutPath, code);
|
|
6504
6835
|
}
|
|
6505
6836
|
function buildAppLayoutCode(navType) {
|
|
6506
6837
|
const hasSidebar = navType === "sidebar" || navType === "both";
|
|
6507
6838
|
if (hasSidebar) {
|
|
6508
|
-
return `import {
|
|
6839
|
+
return `import { AppSidebar } from '@/components/shared/sidebar'
|
|
6840
|
+
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
|
6509
6841
|
|
|
6510
6842
|
export default function AppLayout({
|
|
6511
6843
|
children,
|
|
@@ -6513,12 +6845,14 @@ export default function AppLayout({
|
|
|
6513
6845
|
children: React.ReactNode
|
|
6514
6846
|
}) {
|
|
6515
6847
|
return (
|
|
6516
|
-
<
|
|
6517
|
-
<
|
|
6518
|
-
<
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6848
|
+
<SidebarProvider>
|
|
6849
|
+
<AppSidebar />
|
|
6850
|
+
<SidebarInset>
|
|
6851
|
+
<main className="flex-1 px-4 sm:px-6 lg:px-8 py-6">
|
|
6852
|
+
{children}
|
|
6853
|
+
</main>
|
|
6854
|
+
</SidebarInset>
|
|
6855
|
+
</SidebarProvider>
|
|
6522
6856
|
)
|
|
6523
6857
|
}
|
|
6524
6858
|
`;
|
|
@@ -6538,7 +6872,8 @@ export default function AppLayout({
|
|
|
6538
6872
|
}
|
|
6539
6873
|
function buildGroupLayoutCode(layout, _pages) {
|
|
6540
6874
|
if (layout === "sidebar" || layout === "both") {
|
|
6541
|
-
return `import {
|
|
6875
|
+
return `import { AppSidebar } from '@/components/shared/sidebar'
|
|
6876
|
+
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
|
6542
6877
|
|
|
6543
6878
|
export default function GroupLayout({
|
|
6544
6879
|
children,
|
|
@@ -6546,12 +6881,14 @@ export default function GroupLayout({
|
|
|
6546
6881
|
children: React.ReactNode
|
|
6547
6882
|
}) {
|
|
6548
6883
|
return (
|
|
6549
|
-
<
|
|
6550
|
-
<
|
|
6551
|
-
<
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6884
|
+
<SidebarProvider>
|
|
6885
|
+
<AppSidebar />
|
|
6886
|
+
<SidebarInset>
|
|
6887
|
+
<main className="flex-1 px-4 sm:px-6 lg:px-8 py-6">
|
|
6888
|
+
{children}
|
|
6889
|
+
</main>
|
|
6890
|
+
</SidebarInset>
|
|
6891
|
+
</SidebarProvider>
|
|
6555
6892
|
)
|
|
6556
6893
|
}
|
|
6557
6894
|
`;
|
|
@@ -6583,15 +6920,32 @@ export default function GroupLayout({
|
|
|
6583
6920
|
}
|
|
6584
6921
|
`;
|
|
6585
6922
|
}
|
|
6586
|
-
async function ensurePlanGroupLayouts(projectRoot, plan) {
|
|
6923
|
+
async function ensurePlanGroupLayouts(projectRoot, plan, storedHashes = {}, config2) {
|
|
6587
6924
|
const { mkdir: mkdirAsync } = await import("fs/promises");
|
|
6925
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
6588
6926
|
for (const group of plan.groups) {
|
|
6589
6927
|
const groupDir = resolve7(projectRoot, "app", `(${group.id})`);
|
|
6590
6928
|
await mkdirAsync(groupDir, { recursive: true });
|
|
6591
6929
|
const layoutPath = resolve7(groupDir, "layout.tsx");
|
|
6930
|
+
const relPath = `app/(${group.id})/layout.tsx`;
|
|
6931
|
+
if (existsSync15(layoutPath)) {
|
|
6932
|
+
const currentContent = readFileSync10(layoutPath, "utf-8");
|
|
6933
|
+
const currentHash = createHash2("md5").update(currentContent).digest("hex");
|
|
6934
|
+
const storedHash = storedHashes[relPath];
|
|
6935
|
+
if (storedHash && storedHash !== currentHash) {
|
|
6936
|
+
continue;
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6592
6939
|
const code = buildGroupLayoutCode(group.layout, group.pages);
|
|
6593
6940
|
await writeFile(layoutPath, code);
|
|
6594
6941
|
}
|
|
6942
|
+
if (config2) {
|
|
6943
|
+
const layouts = {};
|
|
6944
|
+
for (const group of plan.groups) {
|
|
6945
|
+
layouts[group.id] = group.layout;
|
|
6946
|
+
}
|
|
6947
|
+
config2.groupLayouts = layouts;
|
|
6948
|
+
}
|
|
6595
6949
|
}
|
|
6596
6950
|
async function regenerateFiles(modified, config2, projectRoot, options = { navChanged: false }) {
|
|
6597
6951
|
const componentIds = /* @__PURE__ */ new Set();
|
|
@@ -7058,7 +7412,8 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7058
7412
|
modified: []
|
|
7059
7413
|
};
|
|
7060
7414
|
}
|
|
7061
|
-
const
|
|
7415
|
+
const readPlan = projectRoot ? loadPlan(projectRoot) : null;
|
|
7416
|
+
const pageFilePath = routeToFsPath(projectRoot, route, readPlan || false);
|
|
7062
7417
|
let pageCode;
|
|
7063
7418
|
try {
|
|
7064
7419
|
pageCode = await readFile(pageFilePath);
|
|
@@ -7093,7 +7448,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7093
7448
|
const filePathRel = routePath ? `app/${routePath}/page.tsx` : "app/page.tsx";
|
|
7094
7449
|
if (!usedIn.includes(filePathRel)) {
|
|
7095
7450
|
const nextManifest = updateUsedIn(manifest, resolved.id, [...usedIn, filePathRel]);
|
|
7096
|
-
await
|
|
7451
|
+
await saveManifest2(projectRoot, nextManifest);
|
|
7097
7452
|
}
|
|
7098
7453
|
printLinkSharedReport({
|
|
7099
7454
|
sharedId: resolved.id,
|
|
@@ -7194,7 +7549,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7194
7549
|
}
|
|
7195
7550
|
const manifest = await loadManifest6(projectRoot);
|
|
7196
7551
|
const nextManifest = updateUsedIn(manifest, created.id, usedInFiles);
|
|
7197
|
-
await
|
|
7552
|
+
await saveManifest2(projectRoot, nextManifest);
|
|
7198
7553
|
printPromoteAndLinkReport({
|
|
7199
7554
|
id: created.id,
|
|
7200
7555
|
name: created.name,
|
|
@@ -7341,13 +7696,13 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7341
7696
|
if (isAuth) {
|
|
7342
7697
|
await ensureAuthRouteGroup(projectRoot);
|
|
7343
7698
|
}
|
|
7344
|
-
const
|
|
7699
|
+
const currentPlan = projectRoot ? loadPlan(projectRoot) : null;
|
|
7700
|
+
const filePath = routeToFsPath(projectRoot, route, currentPlan || isAuth);
|
|
7345
7701
|
await mkdir4(dirname6(filePath), { recursive: true });
|
|
7346
7702
|
const { fixedCode, fixes: postFixes } = await validateAndFixGeneratedCode(projectRoot, finalPageCode, {
|
|
7347
7703
|
isPage: true
|
|
7348
7704
|
});
|
|
7349
7705
|
let codeToWrite = fixedCode;
|
|
7350
|
-
const currentPlan = projectRoot ? loadPlan(projectRoot) : null;
|
|
7351
7706
|
const autoFixCtx = route ? {
|
|
7352
7707
|
currentRoute: route,
|
|
7353
7708
|
knownRoutes: dsm.getConfig().pages.map((p) => p.route).filter(Boolean),
|
|
@@ -7413,44 +7768,55 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7413
7768
|
});
|
|
7414
7769
|
let issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
7415
7770
|
const errors = issues.filter((i) => i.severity === "error");
|
|
7416
|
-
|
|
7771
|
+
const MAX_QUALITY_FIX_ATTEMPTS = 2;
|
|
7772
|
+
let currentErrors = errors;
|
|
7773
|
+
for (let attempt = 0; attempt < MAX_QUALITY_FIX_ATTEMPTS && currentErrors.length > 0; attempt++) {
|
|
7774
|
+
if (!aiProvider) break;
|
|
7417
7775
|
console.log(
|
|
7418
|
-
chalk11.yellow(
|
|
7419
|
-
|
|
7776
|
+
chalk11.yellow(
|
|
7777
|
+
`
|
|
7778
|
+
\u{1F504} ${currentErrors.length} quality errors \u2014 attempting AI fix${attempt > 0 ? ` (retry ${attempt + 1})` : ""} for ${page.name || page.id}...`
|
|
7779
|
+
)
|
|
7420
7780
|
);
|
|
7421
7781
|
try {
|
|
7422
7782
|
const ai = await createAIProvider(aiProvider);
|
|
7423
|
-
if (ai.editPageCode)
|
|
7424
|
-
|
|
7425
|
-
|
|
7783
|
+
if (!ai.editPageCode) break;
|
|
7784
|
+
const errorList = currentErrors.map((e) => `Line ${e.line}: [${e.type}] ${e.message}`).join("\n");
|
|
7785
|
+
const instruction = `Fix these quality issues:
|
|
7426
7786
|
${errorList}
|
|
7427
7787
|
|
|
7428
7788
|
Rules:
|
|
7429
7789
|
- Replace raw Tailwind colors (bg-emerald-500, text-zinc-400, etc.) with semantic tokens (bg-primary, text-muted-foreground, bg-muted, etc.)
|
|
7790
|
+
- Replace placeholder content ("Lorem ipsum", "John Doe", "user@example.com") with realistic contextual content
|
|
7430
7791
|
- Ensure heading hierarchy (h1 \u2192 h2 \u2192 h3, no skipping)
|
|
7431
7792
|
- Add Label components for form inputs
|
|
7432
7793
|
- Keep all existing functionality and layout intact`;
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
}
|
|
7444
|
-
await writeFile(filePath, codeToWrite);
|
|
7445
|
-
issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
7446
|
-
const finalErrors = issues.filter((i) => i.severity === "error").length;
|
|
7447
|
-
console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
|
|
7794
|
+
const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
|
|
7795
|
+
if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
|
|
7796
|
+
const recheck = validatePageQuality(fixedCode2, void 0, qualityPageType);
|
|
7797
|
+
const recheckErrors = recheck.filter((i) => i.severity === "error");
|
|
7798
|
+
if (recheckErrors.length < currentErrors.length) {
|
|
7799
|
+
codeToWrite = fixedCode2;
|
|
7800
|
+
const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite, autoFixCtx);
|
|
7801
|
+
if (reFixes.length > 0) {
|
|
7802
|
+
codeToWrite = reFixed;
|
|
7803
|
+
postFixes.push(...reFixes);
|
|
7448
7804
|
}
|
|
7805
|
+
await writeFile(filePath, codeToWrite);
|
|
7806
|
+
currentErrors = recheckErrors;
|
|
7807
|
+
console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${currentErrors.length} errors`));
|
|
7808
|
+
if (currentErrors.length === 0) break;
|
|
7809
|
+
} else {
|
|
7810
|
+
break;
|
|
7449
7811
|
}
|
|
7812
|
+
} else {
|
|
7813
|
+
break;
|
|
7450
7814
|
}
|
|
7451
7815
|
} catch {
|
|
7816
|
+
break;
|
|
7452
7817
|
}
|
|
7453
7818
|
}
|
|
7819
|
+
issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
7454
7820
|
const report = formatIssues(issues);
|
|
7455
7821
|
if (report) {
|
|
7456
7822
|
console.log(chalk11.yellow(`
|
|
@@ -7496,7 +7862,8 @@ Rules:
|
|
|
7496
7862
|
if (pageDef?.route) {
|
|
7497
7863
|
const route = pageDef.route;
|
|
7498
7864
|
const isAuth = isAuthRoute(route) || isAuthRoute(pageDef.name || pageDef.id || "");
|
|
7499
|
-
const
|
|
7865
|
+
const updatePlan = projectRoot ? loadPlan(projectRoot) : null;
|
|
7866
|
+
const absPath = routeToFsPath(projectRoot, route, updatePlan || isAuth);
|
|
7500
7867
|
if (!resolvedPageCode && instruction) {
|
|
7501
7868
|
let currentCode;
|
|
7502
7869
|
try {
|
|
@@ -7630,7 +7997,54 @@ ${pagesCtx}`
|
|
|
7630
7997
|
allShared: manifestForAudit.shared,
|
|
7631
7998
|
layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout")
|
|
7632
7999
|
});
|
|
7633
|
-
|
|
8000
|
+
let issues = validatePageQuality(codeToWrite, void 0, qualityPageType2);
|
|
8001
|
+
let qualityErrors = issues.filter((i) => i.severity === "error");
|
|
8002
|
+
const MAX_UPDATE_QUALITY_FIX_ATTEMPTS = 2;
|
|
8003
|
+
for (let attempt = 0; attempt < MAX_UPDATE_QUALITY_FIX_ATTEMPTS && qualityErrors.length > 0; attempt++) {
|
|
8004
|
+
if (!aiProvider) break;
|
|
8005
|
+
try {
|
|
8006
|
+
const ai = await createAIProvider(aiProvider);
|
|
8007
|
+
if (!ai.editPageCode) break;
|
|
8008
|
+
const errorList = qualityErrors.map((e) => `Line ${e.line}: [${e.type}] ${e.message}`).join("\n");
|
|
8009
|
+
const fixInstruction = `Fix these quality issues:
|
|
8010
|
+
${errorList}
|
|
8011
|
+
|
|
8012
|
+
Rules:
|
|
8013
|
+
- Replace raw Tailwind colors with semantic tokens (bg-primary, text-muted-foreground, etc.)
|
|
8014
|
+
- Replace placeholder content with realistic contextual content
|
|
8015
|
+
- Ensure heading hierarchy
|
|
8016
|
+
- Keep all existing functionality and layout intact`;
|
|
8017
|
+
const qFixedCode = await ai.editPageCode(
|
|
8018
|
+
codeToWrite,
|
|
8019
|
+
fixInstruction,
|
|
8020
|
+
pageDef.name || pageDef.id || "Page"
|
|
8021
|
+
);
|
|
8022
|
+
if (qFixedCode && qFixedCode.length > 100 && /export\s+(default\s+)?function/.test(qFixedCode)) {
|
|
8023
|
+
const recheck = validatePageQuality(qFixedCode, void 0, qualityPageType2);
|
|
8024
|
+
const recheckErrors = recheck.filter((i) => i.severity === "error");
|
|
8025
|
+
if (recheckErrors.length < qualityErrors.length) {
|
|
8026
|
+
codeToWrite = qFixedCode;
|
|
8027
|
+
const { code: reFixed } = await autoFixCode(codeToWrite, autoFixCtx2);
|
|
8028
|
+
codeToWrite = reFixed;
|
|
8029
|
+
await writeFile(absPath, codeToWrite);
|
|
8030
|
+
qualityErrors = recheckErrors;
|
|
8031
|
+
console.log(
|
|
8032
|
+
chalk11.green(
|
|
8033
|
+
` \u2714 Quality fix: ${qualityErrors.length + (qualityErrors.length - recheckErrors.length)} \u2192 ${recheckErrors.length} errors`
|
|
8034
|
+
)
|
|
8035
|
+
);
|
|
8036
|
+
if (qualityErrors.length === 0) break;
|
|
8037
|
+
} else {
|
|
8038
|
+
break;
|
|
8039
|
+
}
|
|
8040
|
+
} else {
|
|
8041
|
+
break;
|
|
8042
|
+
}
|
|
8043
|
+
} catch {
|
|
8044
|
+
break;
|
|
8045
|
+
}
|
|
8046
|
+
}
|
|
8047
|
+
issues = validatePageQuality(codeToWrite, void 0, qualityPageType2);
|
|
7634
8048
|
const report = formatIssues(issues);
|
|
7635
8049
|
if (report) {
|
|
7636
8050
|
console.log(chalk11.yellow(`
|
|
@@ -8056,7 +8470,7 @@ async function chatCommand(message, options) {
|
|
|
8056
8470
|
if (options.newComponent) {
|
|
8057
8471
|
const componentName = options.newComponent;
|
|
8058
8472
|
spinner.start(`Creating shared component: ${componentName}...`);
|
|
8059
|
-
const { createAIProvider: createAIProvider2 } = await import("./ai-provider-
|
|
8473
|
+
const { createAIProvider: createAIProvider2 } = await import("./ai-provider-CGSIYFZT.js");
|
|
8060
8474
|
const { generateSharedComponent: generateSharedComponent7 } = await import("@getcoherent/core");
|
|
8061
8475
|
const { autoFixCode: autoFixCode2 } = await import("./quality-validator-3K5BMJSR.js");
|
|
8062
8476
|
const { extractPropsInterface, extractDependencies } = await import("./component-extractor-VYJLT5NR.js");
|
|
@@ -8102,7 +8516,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8102
8516
|
type: classifications[0].type,
|
|
8103
8517
|
description: classifications[0].description || message
|
|
8104
8518
|
});
|
|
8105
|
-
await
|
|
8519
|
+
await saveManifest3(projectRoot, manifest2);
|
|
8106
8520
|
}
|
|
8107
8521
|
} catch {
|
|
8108
8522
|
}
|
|
@@ -8171,7 +8585,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8171
8585
|
if (validShared.length !== manifest.shared.length) {
|
|
8172
8586
|
const cleaned = manifest.shared.length - validShared.length;
|
|
8173
8587
|
manifest = { ...manifest, shared: validShared };
|
|
8174
|
-
await
|
|
8588
|
+
await saveManifest3(project.root, manifest);
|
|
8175
8589
|
if (DEBUG4) {
|
|
8176
8590
|
console.log(chalk13.dim(`[pre-gen] Cleaned ${cleaned} orphaned component(s) from manifest`));
|
|
8177
8591
|
}
|
|
@@ -8184,7 +8598,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8184
8598
|
let uxRecommendations;
|
|
8185
8599
|
const SPLIT_THRESHOLD = 4;
|
|
8186
8600
|
const parseOpts = { sharedComponentsSummary, projectRoot };
|
|
8187
|
-
const modCtx = { config:
|
|
8601
|
+
const modCtx = { config: dsm.getConfig(), componentManager: cm };
|
|
8188
8602
|
const multiPageHint = /\b(pages?|sections?)\s*[:]\s*\w/i.test(message) || (message.match(
|
|
8189
8603
|
/\b(?:registration|about|catal|account|contact|pricing|dashboard|settings|login|sign.?up|blog|portfolio|features)\b/gi
|
|
8190
8604
|
) || []).length >= SPLIT_THRESHOLD;
|
|
@@ -8194,7 +8608,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8194
8608
|
requests = splitResult.requests;
|
|
8195
8609
|
if (splitResult.plan && projectRoot) {
|
|
8196
8610
|
savePlan(projectRoot, splitResult.plan);
|
|
8197
|
-
await ensurePlanGroupLayouts(projectRoot, splitResult.plan);
|
|
8611
|
+
await ensurePlanGroupLayouts(projectRoot, splitResult.plan, storedHashes, dsm.getConfig());
|
|
8198
8612
|
}
|
|
8199
8613
|
uxRecommendations = void 0;
|
|
8200
8614
|
} catch {
|
|
@@ -8233,8 +8647,24 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8233
8647
|
}
|
|
8234
8648
|
}
|
|
8235
8649
|
} else {
|
|
8650
|
+
let reusePlanDirective;
|
|
8651
|
+
try {
|
|
8652
|
+
const singlePageManifest = await loadManifest8(projectRoot);
|
|
8653
|
+
if (singlePageManifest.shared.length > 0) {
|
|
8654
|
+
const reusePlan = buildReusePlan({
|
|
8655
|
+
pageName: "page",
|
|
8656
|
+
pageType: inferPageTypeFromRoute("/"),
|
|
8657
|
+
sections: [],
|
|
8658
|
+
manifest: singlePageManifest,
|
|
8659
|
+
existingPageCode: {},
|
|
8660
|
+
userRequest: message
|
|
8661
|
+
});
|
|
8662
|
+
reusePlanDirective = buildReusePlanDirective(reusePlan) || void 0;
|
|
8663
|
+
}
|
|
8664
|
+
} catch {
|
|
8665
|
+
}
|
|
8236
8666
|
try {
|
|
8237
|
-
const result = await parseModification(message, modCtx, provider, parseOpts);
|
|
8667
|
+
const result = await parseModification(message, modCtx, provider, { ...parseOpts, reusePlanDirective });
|
|
8238
8668
|
requests = result.requests;
|
|
8239
8669
|
uxRecommendations = result.uxRecommendations;
|
|
8240
8670
|
const pagesWithoutCode = requests.filter(
|
|
@@ -8272,7 +8702,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8272
8702
|
requests = splitResult.requests;
|
|
8273
8703
|
if (splitResult.plan && projectRoot) {
|
|
8274
8704
|
savePlan(projectRoot, splitResult.plan);
|
|
8275
|
-
await ensurePlanGroupLayouts(projectRoot, splitResult.plan);
|
|
8705
|
+
await ensurePlanGroupLayouts(projectRoot, splitResult.plan, storedHashes, dsm.getConfig());
|
|
8276
8706
|
}
|
|
8277
8707
|
uxRecommendations = void 0;
|
|
8278
8708
|
} catch {
|
|
@@ -8798,7 +9228,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
|
|
|
8798
9228
|
}
|
|
8799
9229
|
}
|
|
8800
9230
|
if (manifestChanged) {
|
|
8801
|
-
await
|
|
9231
|
+
await saveManifest3(projectRoot, currentManifest);
|
|
8802
9232
|
if (DEBUG4) console.log(chalk13.dim("[auto-sync] Manifest updated"));
|
|
8803
9233
|
}
|
|
8804
9234
|
} catch {
|
|
@@ -8969,7 +9399,7 @@ import { DesignSystemManager as DesignSystemManager8, ComponentGenerator as Comp
|
|
|
8969
9399
|
// src/utils/file-watcher.ts
|
|
8970
9400
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync19 } from "fs";
|
|
8971
9401
|
import { relative as relative4, join as join13 } from "path";
|
|
8972
|
-
import { loadManifest as loadManifest9, saveManifest as
|
|
9402
|
+
import { loadManifest as loadManifest9, saveManifest as saveManifest4 } from "@getcoherent/core";
|
|
8973
9403
|
|
|
8974
9404
|
// src/utils/component-integrity.ts
|
|
8975
9405
|
import { existsSync as existsSync18, readFileSync as readFileSync13, readdirSync as readdirSync5 } from "fs";
|
|
@@ -9357,7 +9787,7 @@ async function handleFileDelete(projectRoot, filePath) {
|
|
|
9357
9787
|
...manifest,
|
|
9358
9788
|
shared: manifest.shared.filter((s) => s.id !== orphaned.id)
|
|
9359
9789
|
};
|
|
9360
|
-
await
|
|
9790
|
+
await saveManifest4(projectRoot, cleaned);
|
|
9361
9791
|
console.log(chalk33.cyan(`
|
|
9362
9792
|
\u{1F5D1} Auto-removed ${orphaned.id} (${orphaned.name}) \u2014 file deleted`));
|
|
9363
9793
|
await writeCursorRules(projectRoot);
|
|
@@ -9594,6 +10024,41 @@ async function fixMissingComponentExports(projectRoot) {
|
|
|
9594
10024
|
}
|
|
9595
10025
|
}
|
|
9596
10026
|
}
|
|
10027
|
+
const neededSharedExports = /* @__PURE__ */ new Map();
|
|
10028
|
+
for (const file of pages) {
|
|
10029
|
+
const content = readFileSync15(file, "utf-8");
|
|
10030
|
+
const sharedImportRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/shared\/([^'"]+)['"]/g;
|
|
10031
|
+
let sm;
|
|
10032
|
+
while ((sm = sharedImportRe.exec(content)) !== null) {
|
|
10033
|
+
const names = sm[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
10034
|
+
const componentId = sm[2];
|
|
10035
|
+
if (!neededSharedExports.has(componentId)) neededSharedExports.set(componentId, /* @__PURE__ */ new Set());
|
|
10036
|
+
for (const name of names) neededSharedExports.get(componentId).add(name);
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
10039
|
+
for (const [componentId, needed] of neededSharedExports) {
|
|
10040
|
+
const componentFile = join14(sharedDir, `${componentId}.tsx`);
|
|
10041
|
+
if (!existsSync20(componentFile)) continue;
|
|
10042
|
+
let content = readFileSync15(componentFile, "utf-8");
|
|
10043
|
+
const exportRe = /export\s+(?:const|function|class)\s+(\w+)|export\s*\{([^}]+)\}/g;
|
|
10044
|
+
const existingExports = /* @__PURE__ */ new Set();
|
|
10045
|
+
let em;
|
|
10046
|
+
while ((em = exportRe.exec(content)) !== null) {
|
|
10047
|
+
if (em[1]) existingExports.add(em[1]);
|
|
10048
|
+
if (em[2])
|
|
10049
|
+
em[2].split(",").map(
|
|
10050
|
+
(s) => s.trim().split(/\s+as\s+/).pop()
|
|
10051
|
+
).filter(Boolean).forEach((n) => existingExports.add(n));
|
|
10052
|
+
}
|
|
10053
|
+
const missing = [...needed].filter((n) => !existingExports.has(n));
|
|
10054
|
+
if (missing.length === 0) continue;
|
|
10055
|
+
const defaultExportMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
10056
|
+
if (defaultExportMatch && missing.includes(defaultExportMatch[1])) {
|
|
10057
|
+
content = content.replace(/export\s+default\s+function\s+(\w+)/, "export function $1");
|
|
10058
|
+
writeFileSync10(componentFile, content, "utf-8");
|
|
10059
|
+
console.log(chalk14.dim(` \u2714 Fixed export in ${componentId}.tsx (default \u2192 named)`));
|
|
10060
|
+
}
|
|
10061
|
+
}
|
|
9597
10062
|
}
|
|
9598
10063
|
async function backfillPageAnalysis(projectRoot) {
|
|
9599
10064
|
const configPath = join14(projectRoot, "design-system.config.ts");
|
|
@@ -10318,7 +10783,7 @@ import {
|
|
|
10318
10783
|
PageManager as PageManager4,
|
|
10319
10784
|
ComponentGenerator as ComponentGenerator4,
|
|
10320
10785
|
loadManifest as loadManifest10,
|
|
10321
|
-
saveManifest as
|
|
10786
|
+
saveManifest as saveManifest5
|
|
10322
10787
|
} from "@getcoherent/core";
|
|
10323
10788
|
function extractComponentIdsFromCode2(code) {
|
|
10324
10789
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -10587,7 +11052,7 @@ async function fixCommand(opts = {}) {
|
|
|
10587
11052
|
manifestModified = true;
|
|
10588
11053
|
}
|
|
10589
11054
|
if (manifestModified && !dryRun) {
|
|
10590
|
-
await
|
|
11055
|
+
await saveManifest5(project.root, manifest);
|
|
10591
11056
|
fixes.push("Shared component manifest updated");
|
|
10592
11057
|
}
|
|
10593
11058
|
for (const entry of manifest.shared) {
|
|
@@ -11124,10 +11589,15 @@ function createComponentsCommand() {
|
|
|
11124
11589
|
});
|
|
11125
11590
|
console.log(chalk25.cyan("\u{1F4A1} Modify by ID:"), chalk25.white('coherent chat "in CID-001 add a search button"\n'));
|
|
11126
11591
|
});
|
|
11127
|
-
sharedCmd.command("add <name>").description("Create a shared component (layout/section/widget) and register in manifest").option(
|
|
11592
|
+
sharedCmd.command("add <name>").description("Create a shared component (layout/section/widget) and register in manifest").option(
|
|
11593
|
+
"-t, --type <type>",
|
|
11594
|
+
"Type: layout | navigation | data-display | form | feedback | section | widget",
|
|
11595
|
+
"layout"
|
|
11596
|
+
).option("-d, --description <desc>", "Description").action(async (name, opts) => {
|
|
11128
11597
|
const project = findConfig();
|
|
11129
11598
|
if (!project) exitNotCoherent();
|
|
11130
|
-
const
|
|
11599
|
+
const validTypes = ["layout", "navigation", "data-display", "form", "feedback", "section", "widget"];
|
|
11600
|
+
const type = validTypes.includes(opts.type ?? "") ? opts.type : "layout";
|
|
11131
11601
|
const result = await generateSharedComponent5(project.root, {
|
|
11132
11602
|
name: name.trim(),
|
|
11133
11603
|
type,
|
|
@@ -11803,7 +12273,7 @@ import { existsSync as existsSync27, readFileSync as readFileSync19 } from "fs";
|
|
|
11803
12273
|
import { join as join20, relative as relative5, dirname as dirname10 } from "path";
|
|
11804
12274
|
import { readdir as readdir4, readFile as readFile7 } from "fs/promises";
|
|
11805
12275
|
import { DesignSystemManager as DesignSystemManager16 } from "@getcoherent/core";
|
|
11806
|
-
import { loadManifest as loadManifest13, saveManifest as
|
|
12276
|
+
import { loadManifest as loadManifest13, saveManifest as saveManifest6, findSharedComponent } from "@getcoherent/core";
|
|
11807
12277
|
function extractTokensFromProject(projectRoot) {
|
|
11808
12278
|
const lightColors = {};
|
|
11809
12279
|
const darkColors = {};
|
|
@@ -12099,7 +12569,7 @@ async function syncCommand(options = {}) {
|
|
|
12099
12569
|
const { manifest: reconciledManifest, result: rr } = reconcileComponents(project.root, manifest);
|
|
12100
12570
|
reconcileResult = rr;
|
|
12101
12571
|
if (!dryRun) {
|
|
12102
|
-
await
|
|
12572
|
+
await saveManifest6(project.root, reconciledManifest);
|
|
12103
12573
|
}
|
|
12104
12574
|
detectedComponents = await detectCustomComponents(project.root, allPageCode);
|
|
12105
12575
|
for (const comp of detectedComponents) {
|
|
@@ -365,7 +365,7 @@ Rules:
|
|
|
365
365
|
- Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
|
|
366
366
|
- Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
|
|
367
367
|
|
|
368
|
-
Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
|
|
368
|
+
Each component object: "name" (PascalCase), "type" ("layout"|"navigation"|"data-display"|"form"|"feedback"|"section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
|
|
369
369
|
|
|
370
370
|
If no repeating patterns found: { "components": [] }`
|
|
371
371
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.6.
|
|
6
|
+
"version": "0.6.16",
|
|
7
7
|
"description": "CLI interface for Coherent Design Method",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.js",
|
|
@@ -33,8 +33,15 @@
|
|
|
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
|
+
},
|
|
36
42
|
"dependencies": {
|
|
37
43
|
"@anthropic-ai/sdk": "^0.32.0",
|
|
44
|
+
"@getcoherent/core": "workspace:*",
|
|
38
45
|
"chalk": "^5.3.0",
|
|
39
46
|
"chokidar": "^4.0.1",
|
|
40
47
|
"commander": "^11.1.0",
|
|
@@ -42,19 +49,12 @@
|
|
|
42
49
|
"open": "^10.1.0",
|
|
43
50
|
"ora": "^7.0.1",
|
|
44
51
|
"prompts": "^2.4.2",
|
|
45
|
-
"zod": "^3.22.4"
|
|
46
|
-
"@getcoherent/core": "0.6.14"
|
|
52
|
+
"zod": "^3.22.4"
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
49
55
|
"@types/node": "^20.11.0",
|
|
50
56
|
"@types/prompts": "^2.4.9",
|
|
51
57
|
"tsup": "^8.0.1",
|
|
52
58
|
"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
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|