@getcoherent/cli 0.6.12 → 0.6.14
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 +21 -0
- package/dist/ai-classifier-EGXPFJLN.js +42 -0
- package/dist/ai-provider-HUQO64P3.js +13 -0
- package/dist/chunk-25JRF5MA.js +124 -0
- package/dist/{chunk-CLPILU3Z.js → chunk-5AHG4NNX.js} +10 -292
- package/dist/chunk-N6H73ROO.js +1244 -0
- package/dist/chunk-WRDWFCQJ.js +323 -0
- package/dist/component-extractor-VYJLT5NR.js +56 -0
- package/dist/design-constraints-EIP2XM7T.js +43 -0
- package/dist/index.js +857 -1914
- package/dist/{plan-generator-QUESV7GS.js → plan-generator-H55WEIY2.js} +2 -1
- package/dist/quality-validator-3K5BMJSR.js +15 -0
- package/dist/reuse-validator-HC4LZEKF.js +74 -0
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAIProvider
|
|
3
|
+
} from "./chunk-25JRF5MA.js";
|
|
4
|
+
import {
|
|
5
|
+
autoFixCode,
|
|
6
|
+
checkDesignConsistency,
|
|
7
|
+
formatIssues,
|
|
8
|
+
validatePageQuality,
|
|
9
|
+
verifyIncrementalEdit
|
|
10
|
+
} from "./chunk-N6H73ROO.js";
|
|
11
|
+
import {
|
|
12
|
+
generateArchitecturePlan,
|
|
13
|
+
getPageGroup,
|
|
14
|
+
getPageType,
|
|
15
|
+
loadPlan,
|
|
16
|
+
routeToKey,
|
|
17
|
+
savePlan
|
|
18
|
+
} from "./chunk-WRDWFCQJ.js";
|
|
1
19
|
import {
|
|
2
20
|
CORE_CONSTRAINTS,
|
|
3
21
|
DESIGN_QUALITY,
|
|
@@ -5,16 +23,10 @@ import {
|
|
|
5
23
|
DESIGN_THINKING,
|
|
6
24
|
INTERACTION_PATTERNS,
|
|
7
25
|
VISUAL_DEPTH,
|
|
8
|
-
generateArchitecturePlan,
|
|
9
26
|
getDesignQualityForType,
|
|
10
|
-
getPageGroup,
|
|
11
|
-
getPageType,
|
|
12
27
|
inferPageTypeFromRoute,
|
|
13
|
-
loadPlan,
|
|
14
|
-
routeToKey,
|
|
15
|
-
savePlan,
|
|
16
28
|
selectContextualRules
|
|
17
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-5AHG4NNX.js";
|
|
18
30
|
import {
|
|
19
31
|
__require
|
|
20
32
|
} from "./chunk-3RG5ZIWI.js";
|
|
@@ -2454,9 +2466,17 @@ function buildSharedComponentsListForClaude(manifest) {
|
|
|
2454
2466
|
if (!manifest.shared || manifest.shared.length === 0) {
|
|
2455
2467
|
return "No shared components yet. Register with: coherent components shared add <name> --type layout|section|widget";
|
|
2456
2468
|
}
|
|
2457
|
-
const order = {
|
|
2469
|
+
const order = {
|
|
2470
|
+
layout: 0,
|
|
2471
|
+
navigation: 1,
|
|
2472
|
+
"data-display": 2,
|
|
2473
|
+
form: 3,
|
|
2474
|
+
feedback: 4,
|
|
2475
|
+
section: 5,
|
|
2476
|
+
widget: 6
|
|
2477
|
+
};
|
|
2458
2478
|
const sorted = [...manifest.shared].sort(
|
|
2459
|
-
(a, b) => order[a.type] - order[b.type] || a.name.localeCompare(b.name)
|
|
2479
|
+
(a, b) => (order[a.type] ?? 9) - (order[b.type] ?? 9) || a.name.localeCompare(b.name)
|
|
2460
2480
|
);
|
|
2461
2481
|
return sorted.map((e) => {
|
|
2462
2482
|
const used = e.usedIn.length === 0 ? "\u2014" : e.usedIn.length === 1 && e.usedIn[0] === "app/layout.tsx" ? "layout" : e.usedIn.length + " files";
|
|
@@ -2941,14 +2961,29 @@ function buildSharedComponentsList(manifest) {
|
|
|
2941
2961
|
When you create reusable blocks (headers, footers, repeated sections),
|
|
2942
2962
|
register them: coherent components shared add <Name> --type layout|section|widget`;
|
|
2943
2963
|
}
|
|
2944
|
-
const
|
|
2964
|
+
const typeOrder = {
|
|
2965
|
+
layout: 0,
|
|
2966
|
+
navigation: 1,
|
|
2967
|
+
"data-display": 2,
|
|
2968
|
+
form: 3,
|
|
2969
|
+
feedback: 4,
|
|
2970
|
+
section: 5,
|
|
2971
|
+
widget: 6
|
|
2972
|
+
};
|
|
2945
2973
|
const sorted = [...manifest.shared].sort(
|
|
2946
|
-
(a, b) =>
|
|
2974
|
+
(a, b) => (typeOrder[a.type] ?? 9) - (typeOrder[b.type] ?? 9) || a.name.localeCompare(b.name)
|
|
2947
2975
|
);
|
|
2948
2976
|
const lines = sorted.map((entry) => {
|
|
2949
2977
|
const usedIn = entry.usedIn.length === 0 ? "(not used yet)" : entry.usedIn.length === 1 && entry.usedIn[0] === "app/layout.tsx" ? "app/layout.tsx (all pages)" : entry.usedIn.join(", ");
|
|
2950
|
-
|
|
2951
|
-
|
|
2978
|
+
const importPath = entry.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
|
|
2979
|
+
const parts = [
|
|
2980
|
+
`- ${entry.id} ${entry.name} (${entry.type})${entry.description ? ` \u2014 ${entry.description}` : ""}`,
|
|
2981
|
+
` Import: import { ${entry.name} } from '@/components/shared/${importPath}'`
|
|
2982
|
+
];
|
|
2983
|
+
if (entry.propsInterface) parts.push(` Props: ${entry.propsInterface}`);
|
|
2984
|
+
if (entry.usageExample) parts.push(` Usage: ${entry.usageExample}`);
|
|
2985
|
+
parts.push(` Used in: ${usedIn}`);
|
|
2986
|
+
return parts.join("\n");
|
|
2952
2987
|
});
|
|
2953
2988
|
return `Currently registered shared components:
|
|
2954
2989
|
|
|
@@ -3811,7 +3846,7 @@ async function createAppRouteGroupLayout(projectPath) {
|
|
|
3811
3846
|
}
|
|
3812
3847
|
|
|
3813
3848
|
// src/commands/chat.ts
|
|
3814
|
-
import
|
|
3849
|
+
import chalk13 from "chalk";
|
|
3815
3850
|
import ora2 from "ora";
|
|
3816
3851
|
import { resolve as resolve10, relative as relative2, join as join11 } from "path";
|
|
3817
3852
|
import { existsSync as existsSync17, readFileSync as readFileSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "fs";
|
|
@@ -3822,129 +3857,12 @@ import {
|
|
|
3822
3857
|
CLI_VERSION as CLI_VERSION2,
|
|
3823
3858
|
getTemplateForPageType as getTemplateForPageType2,
|
|
3824
3859
|
loadManifest as loadManifest8,
|
|
3825
|
-
saveManifest as saveManifest2
|
|
3860
|
+
saveManifest as saveManifest2,
|
|
3861
|
+
updateEntry
|
|
3826
3862
|
} from "@getcoherent/core";
|
|
3827
3863
|
|
|
3828
3864
|
// src/agents/modifier.ts
|
|
3829
|
-
import chalk6 from "chalk";
|
|
3830
|
-
|
|
3831
|
-
// src/utils/ai-provider.ts
|
|
3832
3865
|
import chalk5 from "chalk";
|
|
3833
|
-
function detectAIProvider() {
|
|
3834
|
-
if (process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY) {
|
|
3835
|
-
return "openai";
|
|
3836
|
-
}
|
|
3837
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
3838
|
-
return "claude";
|
|
3839
|
-
}
|
|
3840
|
-
return "claude";
|
|
3841
|
-
}
|
|
3842
|
-
function hasAnyAPIKey() {
|
|
3843
|
-
return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY || process.env.GITHUB_COPILOT_OPENAI_API_KEY);
|
|
3844
|
-
}
|
|
3845
|
-
function getAPIKey(provider) {
|
|
3846
|
-
switch (provider) {
|
|
3847
|
-
case "openai":
|
|
3848
|
-
return process.env.OPENAI_API_KEY || process.env.CURSOR_OPENAI_API_KEY || process.env.GITHUB_COPILOT_OPENAI_API_KEY;
|
|
3849
|
-
case "claude":
|
|
3850
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
3851
|
-
case "auto":
|
|
3852
|
-
const detected = detectAIProvider();
|
|
3853
|
-
return getAPIKey(detected);
|
|
3854
|
-
default:
|
|
3855
|
-
return void 0;
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
function showAPIKeyHelp() {
|
|
3859
|
-
console.log(chalk5.red("\n\u274C No API key found\n"));
|
|
3860
|
-
console.log("To use Coherent, you need an AI provider API key.\n");
|
|
3861
|
-
console.log(chalk5.cyan("\u250C\u2500 Quick Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
3862
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3863
|
-
console.log(chalk5.cyan("\u2502 Option 1: Claude (Anthropic) \u2502"));
|
|
3864
|
-
console.log(chalk5.gray("\u2502 Get key: https://console.anthropic.com/ \u2502"));
|
|
3865
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3866
|
-
console.log(chalk5.cyan("\u2502 Copy and run this command: \u2502"));
|
|
3867
|
-
console.log(chalk5.cyan("\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502"));
|
|
3868
|
-
console.log(chalk5.white('\u2502 \u2502 echo "ANTHROPIC_API_KEY=sk-..." > .env \u2502 \u2502'));
|
|
3869
|
-
console.log(chalk5.cyan("\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502"));
|
|
3870
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3871
|
-
console.log(chalk5.cyan("\u2502 Option 2: OpenAI (ChatGPT) \u2502"));
|
|
3872
|
-
console.log(chalk5.gray("\u2502 Get key: https://platform.openai.com/ \u2502"));
|
|
3873
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3874
|
-
console.log(chalk5.cyan("\u2502 Copy and run this command: \u2502"));
|
|
3875
|
-
console.log(chalk5.cyan("\u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502"));
|
|
3876
|
-
console.log(chalk5.white('\u2502 \u2502 echo "OPENAI_API_KEY=sk-..." > .env \u2502 \u2502'));
|
|
3877
|
-
console.log(chalk5.cyan("\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502"));
|
|
3878
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3879
|
-
console.log(chalk5.cyan("\u2502 Then run: coherent init \u2502"));
|
|
3880
|
-
console.log(chalk5.cyan("\u2502 \u2502"));
|
|
3881
|
-
console.log(chalk5.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"));
|
|
3882
|
-
console.log(chalk5.gray("Note: If using Cursor, your CURSOR_OPENAI_API_KEY"));
|
|
3883
|
-
console.log(chalk5.gray("will be detected automatically.\n"));
|
|
3884
|
-
}
|
|
3885
|
-
async function createAIProvider(preferredProvider = "auto", config2) {
|
|
3886
|
-
if (!hasAnyAPIKey() && !config2?.apiKey) {
|
|
3887
|
-
showAPIKeyHelp();
|
|
3888
|
-
throw new Error("API key required");
|
|
3889
|
-
}
|
|
3890
|
-
if (preferredProvider !== "auto") {
|
|
3891
|
-
const apiKey2 = config2?.apiKey || getAPIKey(preferredProvider);
|
|
3892
|
-
if (!apiKey2) {
|
|
3893
|
-
const providerName = preferredProvider === "openai" ? "OpenAI" : "Anthropic Claude";
|
|
3894
|
-
const envVar = preferredProvider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
3895
|
-
showAPIKeyHelp();
|
|
3896
|
-
throw new Error(`${providerName} API key not found.
|
|
3897
|
-
Please set ${envVar} in your environment or .env file.`);
|
|
3898
|
-
}
|
|
3899
|
-
if (preferredProvider === "openai") {
|
|
3900
|
-
try {
|
|
3901
|
-
const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
|
|
3902
|
-
return await OpenAIClient.create(apiKey2, config2?.model);
|
|
3903
|
-
} catch (error) {
|
|
3904
|
-
if (error.message?.includes("not installed")) {
|
|
3905
|
-
throw error;
|
|
3906
|
-
}
|
|
3907
|
-
throw new Error(
|
|
3908
|
-
`OpenAI provider requires "openai" package. Install it with:
|
|
3909
|
-
npm install openai
|
|
3910
|
-
Or use Claude provider instead.
|
|
3911
|
-
Error: ${error.message}`
|
|
3912
|
-
);
|
|
3913
|
-
}
|
|
3914
|
-
} else {
|
|
3915
|
-
const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
|
|
3916
|
-
return ClaudeClient.create(apiKey2, config2?.model);
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
const provider = detectAIProvider();
|
|
3920
|
-
const apiKey = config2?.apiKey || getAPIKey(provider);
|
|
3921
|
-
if (!apiKey) {
|
|
3922
|
-
showAPIKeyHelp();
|
|
3923
|
-
throw new Error("API key required");
|
|
3924
|
-
}
|
|
3925
|
-
switch (provider) {
|
|
3926
|
-
case "openai":
|
|
3927
|
-
try {
|
|
3928
|
-
const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
|
|
3929
|
-
return await OpenAIClient.create(apiKey, config2?.model);
|
|
3930
|
-
} catch (error) {
|
|
3931
|
-
if (error.message?.includes("not installed")) {
|
|
3932
|
-
throw error;
|
|
3933
|
-
}
|
|
3934
|
-
throw new Error(
|
|
3935
|
-
`OpenAI provider requires "openai" package. Install it with:
|
|
3936
|
-
npm install openai
|
|
3937
|
-
Or use Claude provider instead.
|
|
3938
|
-
Error: ${error.message}`
|
|
3939
|
-
);
|
|
3940
|
-
}
|
|
3941
|
-
case "claude":
|
|
3942
|
-
const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
|
|
3943
|
-
return ClaudeClient.create(apiKey, config2?.model);
|
|
3944
|
-
default:
|
|
3945
|
-
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
3946
|
-
}
|
|
3947
|
-
}
|
|
3948
3866
|
|
|
3949
3867
|
// src/agents/page-templates.ts
|
|
3950
3868
|
var PAGE_TEMPLATES = {
|
|
@@ -4246,13 +4164,14 @@ async function parseModification(message, context, provider = "auto", options) {
|
|
|
4246
4164
|
enhancedMessage = expandPageRequest(pageName, message);
|
|
4247
4165
|
if (enhancedMessage !== message) {
|
|
4248
4166
|
isExpandedPageRequest = true;
|
|
4249
|
-
console.log(
|
|
4167
|
+
console.log(chalk5.cyan("\u{1F4A1} Expanding request with best practices..."));
|
|
4250
4168
|
}
|
|
4251
4169
|
}
|
|
4252
4170
|
}
|
|
4253
4171
|
const prompt = buildModificationPrompt(enhancedMessage, context.config, componentRegistry, {
|
|
4254
4172
|
isExpandedPageRequest,
|
|
4255
|
-
sharedComponentsSummary: options?.sharedComponentsSummary
|
|
4173
|
+
sharedComponentsSummary: options?.sharedComponentsSummary,
|
|
4174
|
+
tieredComponentsPrompt: options?.tieredComponentsPrompt
|
|
4256
4175
|
});
|
|
4257
4176
|
const raw = await ai.parseModification(prompt);
|
|
4258
4177
|
const requestsArray = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
@@ -4318,7 +4237,14 @@ Rules:
|
|
|
4318
4237
|
function buildModificationPrompt(message, config2, componentRegistry, options) {
|
|
4319
4238
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4320
4239
|
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" : "";
|
|
4321
|
-
const sharedSection = options?.
|
|
4240
|
+
const sharedSection = options?.tieredComponentsPrompt ? `
|
|
4241
|
+
|
|
4242
|
+
## SHARED COMPONENTS (MANDATORY REUSE)
|
|
4243
|
+
|
|
4244
|
+
${options.tieredComponentsPrompt}
|
|
4245
|
+
|
|
4246
|
+
For editing an existing shared component use type "modify-layout-block" with target "CID-XXX" or name.
|
|
4247
|
+
` : options?.sharedComponentsSummary ? `
|
|
4322
4248
|
|
|
4323
4249
|
## SHARED COMPONENTS (MANDATORY REUSE)
|
|
4324
4250
|
|
|
@@ -4594,8 +4520,10 @@ Return valid JSON only, no markdown code fence. Use this shape:
|
|
|
4594
4520
|
{ "requests": [ ... array of ModificationRequest ... ], "uxRecommendations": "optional markdown or omit key" }
|
|
4595
4521
|
Legacy: returning only a JSON array of requests is still accepted.`;
|
|
4596
4522
|
}
|
|
4597
|
-
function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType) {
|
|
4523
|
+
function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType, tieredComponentsPrompt) {
|
|
4598
4524
|
const designConstraints = pageType ? getDesignQualityForType(pageType) : "";
|
|
4525
|
+
const sharedNote = tieredComponentsPrompt || (sharedComponentsSummary ? `Available shared components:
|
|
4526
|
+
${sharedComponentsSummary}` : "");
|
|
4599
4527
|
return [
|
|
4600
4528
|
`Generate complete pageCode for a page called "${pageName}" at route "${route}".`,
|
|
4601
4529
|
`Output valid TSX with a default export React component.`,
|
|
@@ -4604,8 +4532,7 @@ function buildLightweightPagePrompt(pageName, route, styleContext, sharedCompone
|
|
|
4604
4532
|
designConstraints,
|
|
4605
4533
|
styleContext ? `Follow this style context:
|
|
4606
4534
|
${styleContext}` : "",
|
|
4607
|
-
|
|
4608
|
-
${sharedComponentsSummary}` : ""
|
|
4535
|
+
sharedNote
|
|
4609
4536
|
].filter(Boolean).join("\n\n");
|
|
4610
4537
|
}
|
|
4611
4538
|
async function checkComponentReuse(requests, componentManager) {
|
|
@@ -4805,7 +4732,7 @@ import { appendFile } from "fs/promises";
|
|
|
4805
4732
|
// src/utils/backup.ts
|
|
4806
4733
|
import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6, readdirSync, rmSync as rmSync2, statSync } from "fs";
|
|
4807
4734
|
import { join as join9, relative, dirname as dirname4 } from "path";
|
|
4808
|
-
import
|
|
4735
|
+
import chalk6 from "chalk";
|
|
4809
4736
|
var DEBUG = process.env.COHERENT_DEBUG === "1";
|
|
4810
4737
|
var BACKUP_DIR = ".coherent/backups";
|
|
4811
4738
|
var MAX_BACKUPS = 5;
|
|
@@ -4952,7 +4879,7 @@ function restoreDirectory(currentDir, backupRoot, projectRoot) {
|
|
|
4952
4879
|
function logBackupCreated(backupPath) {
|
|
4953
4880
|
if (backupPath) {
|
|
4954
4881
|
const name = backupPath.split("/").pop();
|
|
4955
|
-
console.log(
|
|
4882
|
+
console.log(chalk6.dim(` \u{1F4BE} Backup saved: .coherent/backups/${name}`));
|
|
4956
4883
|
}
|
|
4957
4884
|
}
|
|
4958
4885
|
|
|
@@ -5028,1248 +4955,11 @@ function fixGlobalsCss(projectRoot, config2) {
|
|
|
5028
4955
|
writeFileSync7(layoutPath, layoutContent, "utf-8");
|
|
5029
4956
|
}
|
|
5030
4957
|
|
|
5031
|
-
// src/utils/component-rules.ts
|
|
5032
|
-
function extractJsxElementProps(code, openTagStart) {
|
|
5033
|
-
let i = openTagStart;
|
|
5034
|
-
if (code[i] !== "<") return null;
|
|
5035
|
-
i++;
|
|
5036
|
-
let braceDepth = 0;
|
|
5037
|
-
let inSingleQuote = false;
|
|
5038
|
-
let inDoubleQuote = false;
|
|
5039
|
-
let inTemplateLiteral = false;
|
|
5040
|
-
let escaped = false;
|
|
5041
|
-
while (i < code.length) {
|
|
5042
|
-
const ch = code[i];
|
|
5043
|
-
if (escaped) {
|
|
5044
|
-
escaped = false;
|
|
5045
|
-
i++;
|
|
5046
|
-
continue;
|
|
5047
|
-
}
|
|
5048
|
-
if (ch === "\\" && (inSingleQuote || inDoubleQuote || inTemplateLiteral)) {
|
|
5049
|
-
escaped = true;
|
|
5050
|
-
i++;
|
|
5051
|
-
continue;
|
|
5052
|
-
}
|
|
5053
|
-
if (!inDoubleQuote && !inTemplateLiteral && ch === "'" && braceDepth > 0) {
|
|
5054
|
-
inSingleQuote = !inSingleQuote;
|
|
5055
|
-
} else if (!inSingleQuote && !inTemplateLiteral && ch === '"') {
|
|
5056
|
-
inDoubleQuote = !inDoubleQuote;
|
|
5057
|
-
} else if (!inSingleQuote && !inDoubleQuote && ch === "`") {
|
|
5058
|
-
inTemplateLiteral = !inTemplateLiteral;
|
|
5059
|
-
}
|
|
5060
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
5061
|
-
if (ch === "{") braceDepth++;
|
|
5062
|
-
else if (ch === "}") braceDepth--;
|
|
5063
|
-
else if (ch === ">" && braceDepth === 0) {
|
|
5064
|
-
return code.slice(openTagStart, i + 1);
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
i++;
|
|
5068
|
-
}
|
|
5069
|
-
return null;
|
|
5070
|
-
}
|
|
5071
|
-
var NAV_STYLE_SIGNAL = /text-muted-foreground/;
|
|
5072
|
-
var buttonMissingGhostVariant = {
|
|
5073
|
-
id: "button-missing-ghost-variant",
|
|
5074
|
-
component: "Button",
|
|
5075
|
-
detect(code) {
|
|
5076
|
-
const issues = [];
|
|
5077
|
-
const buttonRe = /<Button\s/g;
|
|
5078
|
-
let match;
|
|
5079
|
-
while ((match = buttonRe.exec(code)) !== null) {
|
|
5080
|
-
const props = extractJsxElementProps(code, match.index);
|
|
5081
|
-
if (!props) continue;
|
|
5082
|
-
if (/\bvariant\s*=/.test(props)) continue;
|
|
5083
|
-
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5084
|
-
const line = code.slice(0, match.index).split("\n").length;
|
|
5085
|
-
issues.push({
|
|
5086
|
-
line,
|
|
5087
|
-
type: "BUTTON_MISSING_VARIANT",
|
|
5088
|
-
message: '<Button> with navigation-style classes (text-muted-foreground) but no variant \u2014 add variant="ghost"',
|
|
5089
|
-
severity: "error"
|
|
5090
|
-
});
|
|
5091
|
-
}
|
|
5092
|
-
return issues;
|
|
5093
|
-
},
|
|
5094
|
-
fix(code) {
|
|
5095
|
-
let result = code;
|
|
5096
|
-
let applied = false;
|
|
5097
|
-
const buttonRe = /<Button\s/g;
|
|
5098
|
-
let match;
|
|
5099
|
-
let offset = 0;
|
|
5100
|
-
while ((match = buttonRe.exec(code)) !== null) {
|
|
5101
|
-
const adjustedIndex = match.index + offset;
|
|
5102
|
-
const props = extractJsxElementProps(result, adjustedIndex);
|
|
5103
|
-
if (!props) continue;
|
|
5104
|
-
if (/\bvariant\s*=/.test(props)) continue;
|
|
5105
|
-
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5106
|
-
const insertPos = adjustedIndex + "<Button".length;
|
|
5107
|
-
const insertion = "\n" + getIndent(result, adjustedIndex) + ' variant="ghost"';
|
|
5108
|
-
result = result.slice(0, insertPos) + insertion + result.slice(insertPos);
|
|
5109
|
-
offset += insertion.length;
|
|
5110
|
-
applied = true;
|
|
5111
|
-
}
|
|
5112
|
-
return {
|
|
5113
|
-
code: result,
|
|
5114
|
-
applied,
|
|
5115
|
-
description: 'added variant="ghost" to Button with nav-style classes'
|
|
5116
|
-
};
|
|
5117
|
-
}
|
|
5118
|
-
};
|
|
5119
|
-
function getIndent(code, pos) {
|
|
5120
|
-
const lineStart = code.lastIndexOf("\n", pos);
|
|
5121
|
-
const lineContent = code.slice(lineStart + 1, pos);
|
|
5122
|
-
const indentMatch = lineContent.match(/^(\s*)/);
|
|
5123
|
-
return indentMatch ? indentMatch[1] : "";
|
|
5124
|
-
}
|
|
5125
|
-
var buttonWFullGhostJustify = {
|
|
5126
|
-
id: "button-w-full-ghost-justify",
|
|
5127
|
-
component: "Button",
|
|
5128
|
-
detect(code) {
|
|
5129
|
-
const issues = [];
|
|
5130
|
-
const buttonRe = /<Button\s/g;
|
|
5131
|
-
let match;
|
|
5132
|
-
while ((match = buttonRe.exec(code)) !== null) {
|
|
5133
|
-
const props = extractJsxElementProps(code, match.index);
|
|
5134
|
-
if (!props) continue;
|
|
5135
|
-
const hasGhost = /variant\s*=\s*["']ghost["']/.test(props);
|
|
5136
|
-
const hasWFull = /w-full/.test(props);
|
|
5137
|
-
const hasJustifyStart = /justify-start/.test(props);
|
|
5138
|
-
if (hasGhost && hasWFull && !hasJustifyStart) {
|
|
5139
|
-
issues.push({
|
|
5140
|
-
line: 0,
|
|
5141
|
-
type: "button-w-full-ghost-justify",
|
|
5142
|
-
severity: "warning",
|
|
5143
|
-
message: 'Button with w-full and variant="ghost" should include justify-start for proper text alignment'
|
|
5144
|
-
});
|
|
5145
|
-
}
|
|
5146
|
-
}
|
|
5147
|
-
return issues;
|
|
5148
|
-
},
|
|
5149
|
-
fix(code) {
|
|
5150
|
-
let applied = false;
|
|
5151
|
-
const result = code.replace(
|
|
5152
|
-
/(<Button\s[^>]*variant\s*=\s*["']ghost["'][^>]*className\s*=\s*["'][^"']*)(w-full)([^"']*["'])/g,
|
|
5153
|
-
(full, before, wFull, after) => {
|
|
5154
|
-
if (full.includes("justify-start")) return full;
|
|
5155
|
-
applied = true;
|
|
5156
|
-
return `${before}${wFull} justify-start${after}`;
|
|
5157
|
-
}
|
|
5158
|
-
);
|
|
5159
|
-
return {
|
|
5160
|
-
code: result,
|
|
5161
|
-
applied,
|
|
5162
|
-
description: "Added justify-start to ghost Button with w-full"
|
|
5163
|
-
};
|
|
5164
|
-
}
|
|
5165
|
-
};
|
|
5166
|
-
var rules = [buttonMissingGhostVariant, buttonWFullGhostJustify];
|
|
5167
|
-
function detectComponentIssues(code) {
|
|
5168
|
-
const issues = [];
|
|
5169
|
-
for (const rule of rules) {
|
|
5170
|
-
issues.push(...rule.detect(code));
|
|
5171
|
-
}
|
|
5172
|
-
return issues;
|
|
5173
|
-
}
|
|
5174
|
-
function applyComponentRules(code) {
|
|
5175
|
-
const fixes = [];
|
|
5176
|
-
let result = code;
|
|
5177
|
-
for (const rule of rules) {
|
|
5178
|
-
const { code: fixed, applied, description } = rule.fix(result);
|
|
5179
|
-
if (applied) {
|
|
5180
|
-
result = fixed;
|
|
5181
|
-
fixes.push(description);
|
|
5182
|
-
}
|
|
5183
|
-
}
|
|
5184
|
-
return { code: result, fixes };
|
|
5185
|
-
}
|
|
5186
|
-
|
|
5187
|
-
// src/utils/quality-validator.ts
|
|
5188
|
-
var RAW_COLOR_RE = /(?:(?:hover|focus|active|group-hover|focus-visible|focus-within):)?(?:bg|text|border|ring|outline|from|to|via)-(gray|blue|red|green|yellow|purple|pink|indigo|orange|slate|zinc|stone|neutral|emerald|teal|cyan|sky|violet|fuchsia|rose|amber|lime)-\d+/g;
|
|
5189
|
-
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
5190
|
-
var TEXT_BASE_RE = /\btext-base\b/g;
|
|
5191
|
-
var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
|
|
5192
|
-
var SM_BREAKPOINT_RE = /\bsm:/g;
|
|
5193
|
-
var XL_BREAKPOINT_RE = /\bxl:/g;
|
|
5194
|
-
var XXL_BREAKPOINT_RE = /\b2xl:/g;
|
|
5195
|
-
var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
|
|
5196
|
-
var RAW_BUTTON_RE = /<button\b/g;
|
|
5197
|
-
var RAW_INPUT_RE = /<input\b/g;
|
|
5198
|
-
var RAW_SELECT_RE = /<select\b/g;
|
|
5199
|
-
var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
|
|
5200
|
-
var NATIVE_TABLE_RE = /<table\b/g;
|
|
5201
|
-
var PLACEHOLDER_PATTERNS = [
|
|
5202
|
-
/>\s*Lorem ipsum\b/i,
|
|
5203
|
-
/>\s*Card content\s*</i,
|
|
5204
|
-
/>\s*Your (?:text|content) here\s*</i,
|
|
5205
|
-
/>\s*Description\s*</,
|
|
5206
|
-
/>\s*Title\s*</,
|
|
5207
|
-
/placeholder\s*text/i
|
|
5208
|
-
];
|
|
5209
|
-
var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
|
|
5210
|
-
var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
|
|
5211
|
-
var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
|
|
5212
|
-
var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
|
|
5213
|
-
function isInsideCommentOrString(line, matchIndex) {
|
|
5214
|
-
const commentIdx = line.indexOf("//");
|
|
5215
|
-
if (commentIdx !== -1 && commentIdx < matchIndex) return true;
|
|
5216
|
-
let inSingle = false;
|
|
5217
|
-
let inDouble = false;
|
|
5218
|
-
let inTemplate = false;
|
|
5219
|
-
for (let i = 0; i < matchIndex; i++) {
|
|
5220
|
-
const ch = line[i];
|
|
5221
|
-
const prev = i > 0 ? line[i - 1] : "";
|
|
5222
|
-
if (prev === "\\") continue;
|
|
5223
|
-
if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
|
|
5224
|
-
if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
|
|
5225
|
-
if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
|
|
5226
|
-
}
|
|
5227
|
-
return inSingle || inDouble || inTemplate;
|
|
5228
|
-
}
|
|
5229
|
-
function checkLines(code, pattern, type, message, severity, skipCommentsAndStrings = false) {
|
|
5230
|
-
const issues = [];
|
|
5231
|
-
const lines = code.split("\n");
|
|
5232
|
-
let inBlockComment = false;
|
|
5233
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5234
|
-
const line = lines[i];
|
|
5235
|
-
if (skipCommentsAndStrings) {
|
|
5236
|
-
if (inBlockComment) {
|
|
5237
|
-
const endIdx = line.indexOf("*/");
|
|
5238
|
-
if (endIdx !== -1) {
|
|
5239
|
-
inBlockComment = false;
|
|
5240
|
-
}
|
|
5241
|
-
continue;
|
|
5242
|
-
}
|
|
5243
|
-
const blockStart = line.indexOf("/*");
|
|
5244
|
-
if (blockStart !== -1 && !line.includes("*/")) {
|
|
5245
|
-
inBlockComment = true;
|
|
5246
|
-
continue;
|
|
5247
|
-
}
|
|
5248
|
-
let m;
|
|
5249
|
-
pattern.lastIndex = 0;
|
|
5250
|
-
while ((m = pattern.exec(line)) !== null) {
|
|
5251
|
-
if (!isInsideCommentOrString(line, m.index)) {
|
|
5252
|
-
issues.push({ line: i + 1, type, message, severity });
|
|
5253
|
-
break;
|
|
5254
|
-
}
|
|
5255
|
-
}
|
|
5256
|
-
} else {
|
|
5257
|
-
pattern.lastIndex = 0;
|
|
5258
|
-
if (pattern.test(line)) {
|
|
5259
|
-
issues.push({ line: i + 1, type, message, severity });
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5262
|
-
}
|
|
5263
|
-
return issues;
|
|
5264
|
-
}
|
|
5265
|
-
function validatePageQuality(code, validRoutes, pageType) {
|
|
5266
|
-
const issues = [];
|
|
5267
|
-
const allLines = code.split("\n");
|
|
5268
|
-
const isTerminalContext = (lineNum) => {
|
|
5269
|
-
const start = Math.max(0, lineNum - 20);
|
|
5270
|
-
const nearby = allLines.slice(start, lineNum).join(" ");
|
|
5271
|
-
if (/font-mono/.test(allLines[lineNum - 1] || "")) return true;
|
|
5272
|
-
if (/bg-zinc-950|bg-zinc-900/.test(nearby) && /font-mono/.test(nearby)) return true;
|
|
5273
|
-
return false;
|
|
5274
|
-
};
|
|
5275
|
-
issues.push(
|
|
5276
|
-
...checkLines(
|
|
5277
|
-
code,
|
|
5278
|
-
RAW_COLOR_RE,
|
|
5279
|
-
"RAW_COLOR",
|
|
5280
|
-
"Raw Tailwind color detected \u2014 use semantic tokens (bg-primary, text-muted-foreground, etc.)",
|
|
5281
|
-
"error"
|
|
5282
|
-
).filter((issue) => !isTerminalContext(issue.line))
|
|
5283
|
-
);
|
|
5284
|
-
issues.push(
|
|
5285
|
-
...checkLines(
|
|
5286
|
-
code,
|
|
5287
|
-
HEX_IN_CLASS_RE,
|
|
5288
|
-
"HEX_IN_CLASS",
|
|
5289
|
-
"Hex color in className \u2014 use CSS variables via semantic tokens",
|
|
5290
|
-
"error"
|
|
5291
|
-
)
|
|
5292
|
-
);
|
|
5293
|
-
issues.push(
|
|
5294
|
-
...checkLines(code, TEXT_BASE_RE, "TEXT_BASE", "text-base detected \u2014 use text-sm as base font size", "warning")
|
|
5295
|
-
);
|
|
5296
|
-
issues.push(
|
|
5297
|
-
...checkLines(code, HEAVY_SHADOW_RE, "HEAVY_SHADOW", "Heavy shadow detected \u2014 use shadow-sm or none", "warning")
|
|
5298
|
-
);
|
|
5299
|
-
issues.push(
|
|
5300
|
-
...checkLines(
|
|
5301
|
-
code,
|
|
5302
|
-
SM_BREAKPOINT_RE,
|
|
5303
|
-
"SM_BREAKPOINT",
|
|
5304
|
-
"sm: breakpoint \u2014 consider if md:/lg: is sufficient",
|
|
5305
|
-
"info"
|
|
5306
|
-
)
|
|
5307
|
-
);
|
|
5308
|
-
issues.push(
|
|
5309
|
-
...checkLines(
|
|
5310
|
-
code,
|
|
5311
|
-
XL_BREAKPOINT_RE,
|
|
5312
|
-
"XL_BREAKPOINT",
|
|
5313
|
-
"xl: breakpoint \u2014 consider if md:/lg: is sufficient",
|
|
5314
|
-
"info"
|
|
5315
|
-
)
|
|
5316
|
-
);
|
|
5317
|
-
issues.push(
|
|
5318
|
-
...checkLines(
|
|
5319
|
-
code,
|
|
5320
|
-
XXL_BREAKPOINT_RE,
|
|
5321
|
-
"XXL_BREAKPOINT",
|
|
5322
|
-
"2xl: breakpoint \u2014 rarely needed, consider xl: instead",
|
|
5323
|
-
"warning"
|
|
5324
|
-
)
|
|
5325
|
-
);
|
|
5326
|
-
issues.push(
|
|
5327
|
-
...checkLines(
|
|
5328
|
-
code,
|
|
5329
|
-
LARGE_CARD_TITLE_RE,
|
|
5330
|
-
"LARGE_CARD_TITLE",
|
|
5331
|
-
"Large text on CardTitle \u2014 use text-sm font-medium",
|
|
5332
|
-
"warning"
|
|
5333
|
-
)
|
|
5334
|
-
);
|
|
5335
|
-
const codeLines = code.split("\n");
|
|
5336
|
-
issues.push(
|
|
5337
|
-
...checkLines(
|
|
5338
|
-
code,
|
|
5339
|
-
RAW_BUTTON_RE,
|
|
5340
|
-
"NATIVE_BUTTON",
|
|
5341
|
-
"Native <button> \u2014 use Button from @/components/ui/button",
|
|
5342
|
-
"error",
|
|
5343
|
-
true
|
|
5344
|
-
).filter((issue) => {
|
|
5345
|
-
const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
|
|
5346
|
-
if (nearby.includes("aria-label")) return false;
|
|
5347
|
-
if (/onClick=\{.*copy/i.test(nearby)) return false;
|
|
5348
|
-
return true;
|
|
5349
|
-
})
|
|
5350
|
-
);
|
|
5351
|
-
issues.push(
|
|
5352
|
-
...checkLines(
|
|
5353
|
-
code,
|
|
5354
|
-
RAW_SELECT_RE,
|
|
5355
|
-
"NATIVE_SELECT",
|
|
5356
|
-
"Native <select> \u2014 use Select from @/components/ui/select",
|
|
5357
|
-
"error",
|
|
5358
|
-
true
|
|
5359
|
-
)
|
|
5360
|
-
);
|
|
5361
|
-
issues.push(
|
|
5362
|
-
...checkLines(
|
|
5363
|
-
code,
|
|
5364
|
-
NATIVE_CHECKBOX_RE,
|
|
5365
|
-
"NATIVE_CHECKBOX",
|
|
5366
|
-
'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
|
|
5367
|
-
"error",
|
|
5368
|
-
true
|
|
5369
|
-
)
|
|
5370
|
-
);
|
|
5371
|
-
issues.push(
|
|
5372
|
-
...checkLines(
|
|
5373
|
-
code,
|
|
5374
|
-
NATIVE_TABLE_RE,
|
|
5375
|
-
"NATIVE_TABLE",
|
|
5376
|
-
"Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
|
|
5377
|
-
"warning",
|
|
5378
|
-
true
|
|
5379
|
-
)
|
|
5380
|
-
);
|
|
5381
|
-
const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5382
|
-
if (!hasInputImport) {
|
|
5383
|
-
issues.push(
|
|
5384
|
-
...checkLines(
|
|
5385
|
-
code,
|
|
5386
|
-
RAW_INPUT_RE,
|
|
5387
|
-
"RAW_INPUT",
|
|
5388
|
-
"Raw <input> element \u2014 import and use Input from @/components/ui/input",
|
|
5389
|
-
"warning",
|
|
5390
|
-
true
|
|
5391
|
-
)
|
|
5392
|
-
);
|
|
5393
|
-
}
|
|
5394
|
-
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
5395
|
-
const lines = code.split("\n");
|
|
5396
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5397
|
-
if (pattern.test(lines[i])) {
|
|
5398
|
-
issues.push({
|
|
5399
|
-
line: i + 1,
|
|
5400
|
-
type: "PLACEHOLDER",
|
|
5401
|
-
message: "Placeholder content detected \u2014 use real contextual content",
|
|
5402
|
-
severity: "error"
|
|
5403
|
-
});
|
|
5404
|
-
}
|
|
5405
|
-
}
|
|
5406
|
-
}
|
|
5407
|
-
const hasGrid = /\bgrid\b/.test(code);
|
|
5408
|
-
const hasResponsive = /\bmd:|lg:/.test(code);
|
|
5409
|
-
if (hasGrid && !hasResponsive) {
|
|
5410
|
-
issues.push({
|
|
5411
|
-
line: 0,
|
|
5412
|
-
type: "NO_RESPONSIVE",
|
|
5413
|
-
message: "Grid layout without responsive breakpoints (md: or lg:)",
|
|
5414
|
-
severity: "warning"
|
|
5415
|
-
});
|
|
5416
|
-
}
|
|
5417
|
-
issues.push(
|
|
5418
|
-
...checkLines(
|
|
5419
|
-
code,
|
|
5420
|
-
IMG_WITHOUT_ALT_RE,
|
|
5421
|
-
"MISSING_ALT",
|
|
5422
|
-
'<img> without alt attribute \u2014 add descriptive alt or alt="" for decorative images',
|
|
5423
|
-
"error"
|
|
5424
|
-
)
|
|
5425
|
-
);
|
|
5426
|
-
issues.push(
|
|
5427
|
-
...checkLines(
|
|
5428
|
-
code,
|
|
5429
|
-
GENERIC_BUTTON_LABELS,
|
|
5430
|
-
"GENERIC_BUTTON_TEXT",
|
|
5431
|
-
'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
|
|
5432
|
-
"warning"
|
|
5433
|
-
)
|
|
5434
|
-
);
|
|
5435
|
-
if (pageType !== "auth") {
|
|
5436
|
-
const h1Matches = code.match(/<h1[\s>]/g);
|
|
5437
|
-
if (!h1Matches || h1Matches.length === 0) {
|
|
5438
|
-
issues.push({
|
|
5439
|
-
line: 0,
|
|
5440
|
-
type: "NO_H1",
|
|
5441
|
-
message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
|
|
5442
|
-
severity: "warning"
|
|
5443
|
-
});
|
|
5444
|
-
} else if (h1Matches.length > 1) {
|
|
5445
|
-
issues.push({
|
|
5446
|
-
line: 0,
|
|
5447
|
-
type: "MULTIPLE_H1",
|
|
5448
|
-
message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
|
|
5449
|
-
severity: "warning"
|
|
5450
|
-
});
|
|
5451
|
-
}
|
|
5452
|
-
}
|
|
5453
|
-
const headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
|
|
5454
|
-
const hasCardContext = /\bCard\b|\bCardTitle\b|\bCardHeader\b/.test(code);
|
|
5455
|
-
for (let i = 1; i < headingLevels.length; i++) {
|
|
5456
|
-
if (headingLevels[i] > headingLevels[i - 1] + 1) {
|
|
5457
|
-
issues.push({
|
|
5458
|
-
line: 0,
|
|
5459
|
-
type: "SKIPPED_HEADING",
|
|
5460
|
-
message: `Heading level skipped: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]} \u2014 don't skip levels`,
|
|
5461
|
-
severity: hasCardContext ? "info" : "warning"
|
|
5462
|
-
});
|
|
5463
|
-
break;
|
|
5464
|
-
}
|
|
5465
|
-
}
|
|
5466
|
-
const hasLabelImport = /import\s.*Label.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5467
|
-
const inputCount = (code.match(INPUT_TAG_RE) || []).length;
|
|
5468
|
-
const labelForCount = (code.match(LABEL_FOR_RE) || []).length;
|
|
5469
|
-
if (hasLabelImport && inputCount > 0 && labelForCount === 0) {
|
|
5470
|
-
issues.push({
|
|
5471
|
-
line: 0,
|
|
5472
|
-
type: "MISSING_LABEL",
|
|
5473
|
-
message: "Inputs found but no Label with htmlFor \u2014 every input must have a visible label",
|
|
5474
|
-
severity: "error"
|
|
5475
|
-
});
|
|
5476
|
-
}
|
|
5477
|
-
if (!hasLabelImport && inputCount > 0 && !/<label\b/i.test(code)) {
|
|
5478
|
-
issues.push({
|
|
5479
|
-
line: 0,
|
|
5480
|
-
type: "MISSING_LABEL",
|
|
5481
|
-
message: "Inputs found but no Label component \u2014 import Label and add htmlFor on each input",
|
|
5482
|
-
severity: "error"
|
|
5483
|
-
});
|
|
5484
|
-
}
|
|
5485
|
-
const hasPlaceholder = /placeholder\s*=/.test(code);
|
|
5486
|
-
if (hasPlaceholder && inputCount > 0 && labelForCount === 0 && !/<label\b/i.test(code) && !/<Label\b/.test(code)) {
|
|
5487
|
-
issues.push({
|
|
5488
|
-
line: 0,
|
|
5489
|
-
type: "PLACEHOLDER_ONLY_LABEL",
|
|
5490
|
-
message: "Inputs use placeholder only \u2014 add visible Label with htmlFor (placeholder is not a substitute)",
|
|
5491
|
-
severity: "error"
|
|
5492
|
-
});
|
|
5493
|
-
}
|
|
5494
|
-
const hasInteractive = /<Button\b|<button\b|<a\b/.test(code);
|
|
5495
|
-
const hasFocusVisible = /focus-visible:/.test(code);
|
|
5496
|
-
const usesShadcnButton = /import\s.*Button.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5497
|
-
if (hasInteractive && !hasFocusVisible && !usesShadcnButton) {
|
|
5498
|
-
issues.push({
|
|
5499
|
-
line: 0,
|
|
5500
|
-
type: "MISSING_FOCUS_VISIBLE",
|
|
5501
|
-
message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
|
|
5502
|
-
severity: "info"
|
|
5503
|
-
});
|
|
5504
|
-
}
|
|
5505
|
-
const hasTableOrList = /<Table\b|<table\b|\.map\s*\(|<ul\b|<ol\b/.test(code);
|
|
5506
|
-
const hasEmptyCheck = /\.length\s*[=!]==?\s*0|\.length\s*>\s*0|\.length\s*<\s*1|No\s+\w+\s+found|empty|no results|EmptyState|empty state/i.test(
|
|
5507
|
-
code
|
|
5508
|
-
);
|
|
5509
|
-
if (hasTableOrList && !hasEmptyCheck) {
|
|
5510
|
-
issues.push({
|
|
5511
|
-
line: 0,
|
|
5512
|
-
type: "NO_EMPTY_STATE",
|
|
5513
|
-
message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
|
|
5514
|
-
severity: "warning"
|
|
5515
|
-
});
|
|
5516
|
-
}
|
|
5517
|
-
const hasDataFetching = /fetch\s*\(|useQuery|useSWR|useEffect\s*\([^)]*fetch|getData|loadData/i.test(code);
|
|
5518
|
-
const hasLoadingPattern = /skeleton|Skeleton|spinner|Spinner|isLoading|loading|Loading/.test(code);
|
|
5519
|
-
if (hasDataFetching && !hasLoadingPattern) {
|
|
5520
|
-
issues.push({
|
|
5521
|
-
line: 0,
|
|
5522
|
-
type: "NO_LOADING_STATE",
|
|
5523
|
-
message: "Page with data fetching but no loading/skeleton pattern \u2014 add skeleton or spinner",
|
|
5524
|
-
severity: "warning"
|
|
5525
|
-
});
|
|
5526
|
-
}
|
|
5527
|
-
const hasGenericError = /Something went wrong|"Error"|'Error'|>Error<\//.test(code) || /error\.message\s*\|\|\s*["']Error["']/.test(code);
|
|
5528
|
-
if (hasGenericError) {
|
|
5529
|
-
issues.push({
|
|
5530
|
-
line: 0,
|
|
5531
|
-
type: "EMPTY_ERROR_MESSAGE",
|
|
5532
|
-
message: "Generic error message detected \u2014 use what happened + why + what to do next",
|
|
5533
|
-
severity: "warning"
|
|
5534
|
-
});
|
|
5535
|
-
}
|
|
5536
|
-
const hasDestructive = /variant\s*=\s*["']destructive["']|Delete|Remove/.test(code);
|
|
5537
|
-
const hasConfirm = /AlertDialog|Dialog.*confirm|confirm\s*\(|onConfirm|are you sure/i.test(code);
|
|
5538
|
-
if (hasDestructive && !hasConfirm) {
|
|
5539
|
-
issues.push({
|
|
5540
|
-
line: 0,
|
|
5541
|
-
type: "DESTRUCTIVE_NO_CONFIRM",
|
|
5542
|
-
message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
|
|
5543
|
-
severity: "warning"
|
|
5544
|
-
});
|
|
5545
|
-
}
|
|
5546
|
-
const hasFormSubmit = /<form\b|onSubmit|type\s*=\s*["']submit["']/.test(code);
|
|
5547
|
-
const hasFeedback = /toast|success|error|Saved|Saving|saving|setError|setSuccess/i.test(code);
|
|
5548
|
-
if (hasFormSubmit && !hasFeedback) {
|
|
5549
|
-
issues.push({
|
|
5550
|
-
line: 0,
|
|
5551
|
-
type: "FORM_NO_FEEDBACK",
|
|
5552
|
-
message: 'Form with submit but no success/error feedback pattern \u2014 add "Saving..." then "Saved" or error',
|
|
5553
|
-
severity: "info"
|
|
5554
|
-
});
|
|
5555
|
-
}
|
|
5556
|
-
const hasNav = /<nav\b|NavLink|navigation|sidebar.*link|Sidebar.*link/i.test(code);
|
|
5557
|
-
const hasActiveState = /pathname|active|current|aria-current|data-active/.test(code);
|
|
5558
|
-
if (hasNav && !hasActiveState) {
|
|
5559
|
-
issues.push({
|
|
5560
|
-
line: 0,
|
|
5561
|
-
type: "NAV_NO_ACTIVE_STATE",
|
|
5562
|
-
message: "Navigation without active/current page indicator \u2014 add active state for current route",
|
|
5563
|
-
severity: "info"
|
|
5564
|
-
});
|
|
5565
|
-
}
|
|
5566
|
-
if (validRoutes && validRoutes.length > 0) {
|
|
5567
|
-
const routeSet = new Set(validRoutes);
|
|
5568
|
-
routeSet.add("#");
|
|
5569
|
-
const lines = code.split("\n");
|
|
5570
|
-
const linkHrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
|
|
5571
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5572
|
-
let match;
|
|
5573
|
-
while ((match = linkHrefRe.exec(lines[i])) !== null) {
|
|
5574
|
-
const target = match[1];
|
|
5575
|
-
if (target === "/" || target.startsWith("/design-system") || target.startsWith("/api") || target.startsWith("/#"))
|
|
5576
|
-
continue;
|
|
5577
|
-
if (!routeSet.has(target)) {
|
|
5578
|
-
issues.push({
|
|
5579
|
-
line: i + 1,
|
|
5580
|
-
type: "BROKEN_INTERNAL_LINK",
|
|
5581
|
-
message: `Link to "${target}" \u2014 route does not exist in project`,
|
|
5582
|
-
severity: "warning"
|
|
5583
|
-
});
|
|
5584
|
-
}
|
|
5585
|
-
}
|
|
5586
|
-
}
|
|
5587
|
-
}
|
|
5588
|
-
const linkBlockRe = /<(?:Link|a)\b[^>]*>[\s\S]*?<\/(?:Link|a)>/g;
|
|
5589
|
-
let linkMatch;
|
|
5590
|
-
while ((linkMatch = linkBlockRe.exec(code)) !== null) {
|
|
5591
|
-
const block = linkMatch[0];
|
|
5592
|
-
if (/<(?:Button|button)\b/.test(block) && !/asChild/.test(block)) {
|
|
5593
|
-
issues.push({
|
|
5594
|
-
line: 0,
|
|
5595
|
-
type: "NESTED_INTERACTIVE",
|
|
5596
|
-
message: "Button inside Link without asChild \u2014 causes DOM nesting error. Use <Button asChild><Link>...</Link></Button> instead",
|
|
5597
|
-
severity: "error"
|
|
5598
|
-
});
|
|
5599
|
-
break;
|
|
5600
|
-
}
|
|
5601
|
-
}
|
|
5602
|
-
const nestedAnchorRe = /<a\b[^>]*>[\s\S]*?<a\b/;
|
|
5603
|
-
if (nestedAnchorRe.test(code)) {
|
|
5604
|
-
issues.push({
|
|
5605
|
-
line: 0,
|
|
5606
|
-
type: "NESTED_INTERACTIVE",
|
|
5607
|
-
message: "Nested <a> tags \u2014 causes DOM nesting error. Remove inner anchor or restructure",
|
|
5608
|
-
severity: "error"
|
|
5609
|
-
});
|
|
5610
|
-
}
|
|
5611
|
-
const linkWithoutHrefRe = /<(?:Link|a)\b(?![^>]*\bhref\s*=)[^>]*>/g;
|
|
5612
|
-
let linkNoHrefMatch;
|
|
5613
|
-
while ((linkNoHrefMatch = linkWithoutHrefRe.exec(code)) !== null) {
|
|
5614
|
-
const matchLine = code.slice(0, linkNoHrefMatch.index).split("\n").length;
|
|
5615
|
-
issues.push({
|
|
5616
|
-
line: matchLine,
|
|
5617
|
-
type: "LINK_MISSING_HREF",
|
|
5618
|
-
message: "<Link> or <a> without href prop \u2014 causes Next.js runtime error. Add href attribute.",
|
|
5619
|
-
severity: "error"
|
|
5620
|
-
});
|
|
5621
|
-
}
|
|
5622
|
-
issues.push(...detectComponentIssues(code));
|
|
5623
|
-
return issues;
|
|
5624
|
-
}
|
|
5625
|
-
function resolveHref(linkText, context) {
|
|
5626
|
-
if (!context) return "/";
|
|
5627
|
-
const text = linkText.trim().toLowerCase();
|
|
5628
|
-
if (context.linkMap) {
|
|
5629
|
-
for (const [label, route] of Object.entries(context.linkMap)) {
|
|
5630
|
-
if (label.toLowerCase() === text) return route;
|
|
5631
|
-
}
|
|
5632
|
-
}
|
|
5633
|
-
if (context.knownRoutes) {
|
|
5634
|
-
const cleaned = text.replace(/^(back\s+to|go\s+to|view\s+all|see\s+all|return\s+to)\s+/i, "").trim();
|
|
5635
|
-
for (const route of context.knownRoutes) {
|
|
5636
|
-
const slug = route.split("/").filter(Boolean).pop() || "";
|
|
5637
|
-
const routeName = slug.replace(/[-_]/g, " ");
|
|
5638
|
-
if (routeName && cleaned === routeName) return route;
|
|
5639
|
-
}
|
|
5640
|
-
}
|
|
5641
|
-
return "/";
|
|
5642
|
-
}
|
|
5643
|
-
function replaceRawColors(classes, colorMap) {
|
|
5644
|
-
let changed = false;
|
|
5645
|
-
let result = classes;
|
|
5646
|
-
const accentColorRe = /\b((?:(?:hover|focus|active|group-hover|focus-visible|focus-within):)?)(bg|text|border|ring|outline|from|to|via)-(emerald|blue|violet|indigo|purple|teal|cyan|sky|rose|amber|red|green|yellow|pink|orange|fuchsia|lime)-(\d+)\b/g;
|
|
5647
|
-
result = result.replace(accentColorRe, (m, statePrefix, prefix, color, shade) => {
|
|
5648
|
-
const bare = m.replace(statePrefix, "");
|
|
5649
|
-
if (colorMap[bare]) {
|
|
5650
|
-
changed = true;
|
|
5651
|
-
return statePrefix + colorMap[bare];
|
|
5652
|
-
}
|
|
5653
|
-
const n = parseInt(shade);
|
|
5654
|
-
const isDestructive = color === "red";
|
|
5655
|
-
if (prefix === "bg") {
|
|
5656
|
-
if (n >= 500 && n <= 700) {
|
|
5657
|
-
changed = true;
|
|
5658
|
-
return statePrefix + (isDestructive ? "bg-destructive" : "bg-primary");
|
|
5659
|
-
}
|
|
5660
|
-
if (n >= 100 && n <= 200) {
|
|
5661
|
-
changed = true;
|
|
5662
|
-
return statePrefix + (isDestructive ? "bg-destructive/10" : "bg-primary/10");
|
|
5663
|
-
}
|
|
5664
|
-
if (n >= 300 && n <= 400) {
|
|
5665
|
-
changed = true;
|
|
5666
|
-
return statePrefix + (isDestructive ? "bg-destructive/20" : "bg-primary/20");
|
|
5667
|
-
}
|
|
5668
|
-
if (n >= 800) {
|
|
5669
|
-
changed = true;
|
|
5670
|
-
return statePrefix + "bg-muted";
|
|
5671
|
-
}
|
|
5672
|
-
}
|
|
5673
|
-
if (prefix === "text") {
|
|
5674
|
-
if (n >= 400 && n <= 600) {
|
|
5675
|
-
changed = true;
|
|
5676
|
-
return statePrefix + (isDestructive ? "text-destructive" : "text-primary");
|
|
5677
|
-
}
|
|
5678
|
-
if (n >= 100 && n <= 300) {
|
|
5679
|
-
changed = true;
|
|
5680
|
-
return statePrefix + "text-foreground";
|
|
5681
|
-
}
|
|
5682
|
-
if (n >= 700) {
|
|
5683
|
-
changed = true;
|
|
5684
|
-
return statePrefix + "text-foreground";
|
|
5685
|
-
}
|
|
5686
|
-
}
|
|
5687
|
-
if (prefix === "border" || prefix === "ring" || prefix === "outline") {
|
|
5688
|
-
changed = true;
|
|
5689
|
-
return statePrefix + (isDestructive ? `${prefix}-destructive` : `${prefix}-primary`);
|
|
5690
|
-
}
|
|
5691
|
-
if (prefix === "from" || prefix === "to" || prefix === "via") {
|
|
5692
|
-
changed = true;
|
|
5693
|
-
if (n >= 100 && n <= 300)
|
|
5694
|
-
return statePrefix + (isDestructive ? `${prefix}-destructive/20` : `${prefix}-primary/20`);
|
|
5695
|
-
return statePrefix + (isDestructive ? `${prefix}-destructive` : `${prefix}-primary`);
|
|
5696
|
-
}
|
|
5697
|
-
return m;
|
|
5698
|
-
});
|
|
5699
|
-
const neutralColorRe = /\b((?:(?:hover|focus|active|group-hover|focus-visible|focus-within):)?)(bg|text|border|ring|outline)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
|
|
5700
|
-
result = result.replace(neutralColorRe, (m, statePrefix, prefix, _color, shade) => {
|
|
5701
|
-
const bare = m.replace(statePrefix, "");
|
|
5702
|
-
if (colorMap[bare]) {
|
|
5703
|
-
changed = true;
|
|
5704
|
-
return statePrefix + colorMap[bare];
|
|
5705
|
-
}
|
|
5706
|
-
const n = parseInt(shade);
|
|
5707
|
-
if (prefix === "bg") {
|
|
5708
|
-
if (n >= 800) {
|
|
5709
|
-
changed = true;
|
|
5710
|
-
return statePrefix + "bg-background";
|
|
5711
|
-
}
|
|
5712
|
-
if (n >= 100 && n <= 300) {
|
|
5713
|
-
changed = true;
|
|
5714
|
-
return statePrefix + "bg-muted";
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
if (prefix === "text") {
|
|
5718
|
-
if (n >= 100 && n <= 300) {
|
|
5719
|
-
changed = true;
|
|
5720
|
-
return statePrefix + "text-foreground";
|
|
5721
|
-
}
|
|
5722
|
-
if (n >= 400 && n <= 600) {
|
|
5723
|
-
changed = true;
|
|
5724
|
-
return statePrefix + "text-muted-foreground";
|
|
5725
|
-
}
|
|
5726
|
-
}
|
|
5727
|
-
if (prefix === "border" || prefix === "ring" || prefix === "outline") {
|
|
5728
|
-
changed = true;
|
|
5729
|
-
return statePrefix + `${prefix === "border" ? "border-border" : `${prefix}-ring`}`;
|
|
5730
|
-
}
|
|
5731
|
-
return m;
|
|
5732
|
-
});
|
|
5733
|
-
return { result, changed };
|
|
5734
|
-
}
|
|
5735
|
-
async function autoFixCode(code, context) {
|
|
5736
|
-
const fixes = [];
|
|
5737
|
-
let fixed = code;
|
|
5738
|
-
const beforeQuoteFix = fixed;
|
|
5739
|
-
fixed = fixed.replace(/\\'(\s*[}\],])/g, "'$1");
|
|
5740
|
-
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
5741
|
-
if (fixed !== beforeQuoteFix) {
|
|
5742
|
-
fixes.push("fixed escaped closing quotes in strings");
|
|
5743
|
-
}
|
|
5744
|
-
const beforeEntityFix = fixed;
|
|
5745
|
-
const isInsideAttrValue = (line, idx) => {
|
|
5746
|
-
let inQuote = false;
|
|
5747
|
-
let inAttr = false;
|
|
5748
|
-
for (let i = 0; i < idx; i++) {
|
|
5749
|
-
if (line[i] === "=" && line[i + 1] === '"') {
|
|
5750
|
-
inAttr = true;
|
|
5751
|
-
inQuote = true;
|
|
5752
|
-
i++;
|
|
5753
|
-
} else if (inAttr && line[i] === '"') {
|
|
5754
|
-
inAttr = false;
|
|
5755
|
-
inQuote = false;
|
|
5756
|
-
}
|
|
5757
|
-
}
|
|
5758
|
-
return inQuote;
|
|
5759
|
-
};
|
|
5760
|
-
fixed = fixed.split("\n").map((line) => {
|
|
5761
|
-
let l = line;
|
|
5762
|
-
l = l.replace(/<=/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "<=");
|
|
5763
|
-
l = l.replace(/>=/g, (m, offset) => isInsideAttrValue(line, offset) ? m : ">=");
|
|
5764
|
-
l = l.replace(/&&/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "&&");
|
|
5765
|
-
l = l.replace(
|
|
5766
|
-
/([\w)\]])\s*<\s*([\w(])/g,
|
|
5767
|
-
(m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} < ${p2}`
|
|
5768
|
-
);
|
|
5769
|
-
l = l.replace(
|
|
5770
|
-
/([\w)\]])\s*>\s*([\w(])/g,
|
|
5771
|
-
(m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} > ${p2}`
|
|
5772
|
-
);
|
|
5773
|
-
return l;
|
|
5774
|
-
}).join("\n");
|
|
5775
|
-
if (fixed !== beforeEntityFix) {
|
|
5776
|
-
fixes.push("Fixed syntax issues");
|
|
5777
|
-
}
|
|
5778
|
-
const beforeLtFix = fixed;
|
|
5779
|
-
fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1<$2");
|
|
5780
|
-
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$2");
|
|
5781
|
-
if (fixed !== beforeLtFix) {
|
|
5782
|
-
fixes.push("escaped < in JSX text content");
|
|
5783
|
-
}
|
|
5784
|
-
if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
|
|
5785
|
-
fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
|
|
5786
|
-
fixes.push("text-base \u2192 text-sm");
|
|
5787
|
-
}
|
|
5788
|
-
if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
|
|
5789
|
-
fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
|
|
5790
|
-
fixes.push("large text in CardTitle \u2192 removed");
|
|
5791
|
-
}
|
|
5792
|
-
if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
|
|
5793
|
-
fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
|
|
5794
|
-
fixes.push("heavy shadow \u2192 shadow-sm");
|
|
5795
|
-
}
|
|
5796
|
-
const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
|
|
5797
|
-
const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
|
|
5798
|
-
fixed
|
|
5799
|
-
);
|
|
5800
|
-
const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
|
|
5801
|
-
if ((hasHooks || hasEvents) && !hasUseClient) {
|
|
5802
|
-
fixed = `'use client'
|
|
5803
|
-
|
|
5804
|
-
${fixed}`;
|
|
5805
|
-
fixes.push('added "use client" (client features detected)');
|
|
5806
|
-
}
|
|
5807
|
-
if (/^['"]use client['"]/.test(fixed.trim()) && /\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/.test(fixed)) {
|
|
5808
|
-
const metaMatch = fixed.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
|
|
5809
|
-
if (metaMatch) {
|
|
5810
|
-
const start = fixed.indexOf(metaMatch[0]);
|
|
5811
|
-
const open = fixed.indexOf("{", start);
|
|
5812
|
-
let depth = 1, i = open + 1;
|
|
5813
|
-
while (i < fixed.length && depth > 0) {
|
|
5814
|
-
if (fixed[i] === "{") depth++;
|
|
5815
|
-
else if (fixed[i] === "}") depth--;
|
|
5816
|
-
i++;
|
|
5817
|
-
}
|
|
5818
|
-
const tail = fixed.slice(i);
|
|
5819
|
-
const semi = tail.match(/^\s*;/);
|
|
5820
|
-
const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
|
|
5821
|
-
fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
|
|
5822
|
-
fixes.push('removed metadata export (conflicts with "use client")');
|
|
5823
|
-
}
|
|
5824
|
-
}
|
|
5825
|
-
const lines = fixed.split("\n");
|
|
5826
|
-
let hasReplacedButton = false;
|
|
5827
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5828
|
-
if (!/<button\b/.test(lines[i])) continue;
|
|
5829
|
-
if (lines[i].includes("aria-label")) continue;
|
|
5830
|
-
if (/onClick=\{.*copy/i.test(lines[i])) continue;
|
|
5831
|
-
const block = lines.slice(i, i + 5).join(" ");
|
|
5832
|
-
if (block.includes("aria-label") || /onClick=\{.*copy/i.test(block)) continue;
|
|
5833
|
-
lines[i] = lines[i].replace(/<button\b/g, "<Button");
|
|
5834
|
-
hasReplacedButton = true;
|
|
5835
|
-
}
|
|
5836
|
-
if (hasReplacedButton) {
|
|
5837
|
-
fixed = lines.join("\n");
|
|
5838
|
-
fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
|
|
5839
|
-
return "</Button>";
|
|
5840
|
-
});
|
|
5841
|
-
const openCount = (fixed.match(/<Button\b/g) || []).length;
|
|
5842
|
-
const closeCount = (fixed.match(/<\/Button>/g) || []).length;
|
|
5843
|
-
if (closeCount > openCount) {
|
|
5844
|
-
let excess = closeCount - openCount;
|
|
5845
|
-
fixed = fixed.replace(/<\/Button>/g, (m) => {
|
|
5846
|
-
if (excess > 0) {
|
|
5847
|
-
excess--;
|
|
5848
|
-
return "</button>";
|
|
5849
|
-
}
|
|
5850
|
-
return m;
|
|
5851
|
-
});
|
|
5852
|
-
}
|
|
5853
|
-
const hasButtonImport = /import\s.*\bButton\b.*from\s+['"]@\/components\/ui\/button['"]/.test(fixed);
|
|
5854
|
-
if (!hasButtonImport) {
|
|
5855
|
-
const lastImportIdx = fixed.lastIndexOf("\nimport ");
|
|
5856
|
-
if (lastImportIdx !== -1) {
|
|
5857
|
-
const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
|
|
5858
|
-
fixed = fixed.slice(0, lineEnd + 1) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(lineEnd + 1);
|
|
5859
|
-
} else {
|
|
5860
|
-
const insertAfter = hasUseClient ? fixed.indexOf("\n") + 1 : 0;
|
|
5861
|
-
fixed = fixed.slice(0, insertAfter) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(insertAfter);
|
|
5862
|
-
}
|
|
5863
|
-
}
|
|
5864
|
-
fixes.push("<button> \u2192 <Button> (with import)");
|
|
5865
|
-
}
|
|
5866
|
-
const colorMap = {
|
|
5867
|
-
"bg-zinc-950": "bg-background",
|
|
5868
|
-
"bg-zinc-900": "bg-background",
|
|
5869
|
-
"bg-slate-950": "bg-background",
|
|
5870
|
-
"bg-slate-900": "bg-background",
|
|
5871
|
-
"bg-gray-950": "bg-background",
|
|
5872
|
-
"bg-gray-900": "bg-background",
|
|
5873
|
-
"bg-zinc-800": "bg-muted",
|
|
5874
|
-
"bg-slate-800": "bg-muted",
|
|
5875
|
-
"bg-gray-800": "bg-muted",
|
|
5876
|
-
"bg-zinc-100": "bg-muted",
|
|
5877
|
-
"bg-slate-100": "bg-muted",
|
|
5878
|
-
"bg-gray-100": "bg-muted",
|
|
5879
|
-
"bg-white": "bg-background",
|
|
5880
|
-
"bg-black": "bg-background",
|
|
5881
|
-
"text-white": "text-foreground",
|
|
5882
|
-
"text-black": "text-foreground",
|
|
5883
|
-
"text-zinc-100": "text-foreground",
|
|
5884
|
-
"text-zinc-200": "text-foreground",
|
|
5885
|
-
"text-slate-100": "text-foreground",
|
|
5886
|
-
"text-gray-100": "text-foreground",
|
|
5887
|
-
"text-zinc-400": "text-muted-foreground",
|
|
5888
|
-
"text-zinc-500": "text-muted-foreground",
|
|
5889
|
-
"text-slate-400": "text-muted-foreground",
|
|
5890
|
-
"text-slate-500": "text-muted-foreground",
|
|
5891
|
-
"text-gray-400": "text-muted-foreground",
|
|
5892
|
-
"text-gray-500": "text-muted-foreground",
|
|
5893
|
-
"border-zinc-700": "border-border",
|
|
5894
|
-
"border-zinc-800": "border-border",
|
|
5895
|
-
"border-slate-700": "border-border",
|
|
5896
|
-
"border-gray-700": "border-border",
|
|
5897
|
-
"border-zinc-200": "border-border",
|
|
5898
|
-
"border-slate-200": "border-border",
|
|
5899
|
-
"border-gray-200": "border-border"
|
|
5900
|
-
};
|
|
5901
|
-
const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
|
|
5902
|
-
const isInsideTerminalBlock = (offset) => {
|
|
5903
|
-
const preceding = fixed.slice(Math.max(0, offset - 600), offset);
|
|
5904
|
-
if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
|
|
5905
|
-
if (!/font-mono/.test(preceding)) return false;
|
|
5906
|
-
const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
|
|
5907
|
-
const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
|
|
5908
|
-
return lastTerminal > lastClose;
|
|
5909
|
-
};
|
|
5910
|
-
let hadColorFix = false;
|
|
5911
|
-
fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
|
|
5912
|
-
if (isCodeContext(classes)) return fullMatch;
|
|
5913
|
-
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
5914
|
-
const { result, changed } = replaceRawColors(classes, colorMap);
|
|
5915
|
-
if (changed) hadColorFix = true;
|
|
5916
|
-
if (result !== classes) return `className="${result}"`;
|
|
5917
|
-
return fullMatch;
|
|
5918
|
-
});
|
|
5919
|
-
fixed = fixed.replace(/(?:cn|clsx|cva)\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g, (fullMatch, args) => {
|
|
5920
|
-
const replaced = args.replace(/"([^"]*)"/g, (_qm, inner) => {
|
|
5921
|
-
const { result, changed } = replaceRawColors(inner, colorMap);
|
|
5922
|
-
if (changed) hadColorFix = true;
|
|
5923
|
-
return `"${result}"`;
|
|
5924
|
-
});
|
|
5925
|
-
if (replaced !== args) return fullMatch.replace(args, replaced);
|
|
5926
|
-
return fullMatch;
|
|
5927
|
-
});
|
|
5928
|
-
fixed = fixed.replace(/className='([^']*)'/g, (fullMatch, classes, offset) => {
|
|
5929
|
-
if (isCodeContext(classes)) return fullMatch;
|
|
5930
|
-
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
5931
|
-
const { result, changed } = replaceRawColors(classes, colorMap);
|
|
5932
|
-
if (changed) hadColorFix = true;
|
|
5933
|
-
if (result !== classes) return `className='${result}'`;
|
|
5934
|
-
return fullMatch;
|
|
5935
|
-
});
|
|
5936
|
-
fixed = fixed.replace(/className=\{`([^`]*)`\}/g, (fullMatch, inner) => {
|
|
5937
|
-
const { result, changed } = replaceRawColors(inner, colorMap);
|
|
5938
|
-
if (changed) hadColorFix = true;
|
|
5939
|
-
if (result !== inner) return `className={\`${result}\`}`;
|
|
5940
|
-
return fullMatch;
|
|
5941
|
-
});
|
|
5942
|
-
if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
|
|
5943
|
-
const selectRe = /<select\b[^>]*>([\s\S]*?)<\/select>/g;
|
|
5944
|
-
let hadSelectFix = false;
|
|
5945
|
-
fixed = fixed.replace(selectRe, (_match, inner) => {
|
|
5946
|
-
const options = [];
|
|
5947
|
-
const optionRe = /<option\s+value="([^"]*)"[^>]*>([^<]*)<\/option>/g;
|
|
5948
|
-
let optMatch;
|
|
5949
|
-
while ((optMatch = optionRe.exec(inner)) !== null) {
|
|
5950
|
-
options.push({ value: optMatch[1], label: optMatch[2] });
|
|
5951
|
-
}
|
|
5952
|
-
if (options.length === 0) return _match;
|
|
5953
|
-
hadSelectFix = true;
|
|
5954
|
-
const items = options.map((o) => ` <SelectItem value="${o.value}">${o.label}</SelectItem>`).join("\n");
|
|
5955
|
-
return `<Select>
|
|
5956
|
-
<SelectTrigger>
|
|
5957
|
-
<SelectValue placeholder="Select..." />
|
|
5958
|
-
</SelectTrigger>
|
|
5959
|
-
<SelectContent>
|
|
5960
|
-
${items}
|
|
5961
|
-
</SelectContent>
|
|
5962
|
-
</Select>`;
|
|
5963
|
-
});
|
|
5964
|
-
if (hadSelectFix) {
|
|
5965
|
-
fixes.push("<select> \u2192 shadcn Select");
|
|
5966
|
-
const selectImport = `import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'`;
|
|
5967
|
-
if (!/from\s+['"]@\/components\/ui\/select['"]/.test(fixed)) {
|
|
5968
|
-
const replaced = fixed.replace(
|
|
5969
|
-
/(import\s+\{[^}]*\}\s+from\s+['"]@\/components\/ui\/[^'"]+['"])/,
|
|
5970
|
-
`$1
|
|
5971
|
-
${selectImport}`
|
|
5972
|
-
);
|
|
5973
|
-
if (replaced !== fixed) {
|
|
5974
|
-
fixed = replaced;
|
|
5975
|
-
} else {
|
|
5976
|
-
fixed = selectImport + "\n" + fixed;
|
|
5977
|
-
}
|
|
5978
|
-
}
|
|
5979
|
-
}
|
|
5980
|
-
const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
5981
|
-
if (lucideImportMatch) {
|
|
5982
|
-
let lucideExports = null;
|
|
5983
|
-
try {
|
|
5984
|
-
const { createRequire } = await import("module");
|
|
5985
|
-
const require2 = createRequire(process.cwd() + "/package.json");
|
|
5986
|
-
const lr = require2("lucide-react");
|
|
5987
|
-
lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
5988
|
-
} catch {
|
|
5989
|
-
}
|
|
5990
|
-
if (lucideExports) {
|
|
5991
|
-
const nonLucideImports = /* @__PURE__ */ new Set();
|
|
5992
|
-
for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from\s*["'](?!lucide-react)([^"']+)["']/g)) {
|
|
5993
|
-
m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => nonLucideImports.add(n));
|
|
5994
|
-
}
|
|
5995
|
-
const iconNames = lucideImportMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
5996
|
-
const duplicates = iconNames.filter((name) => nonLucideImports.has(name));
|
|
5997
|
-
let newImport = lucideImportMatch[1];
|
|
5998
|
-
for (const dup of duplicates) {
|
|
5999
|
-
newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
|
|
6000
|
-
fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
|
|
6001
|
-
}
|
|
6002
|
-
const invalid = iconNames.filter((name) => !lucideExports.has(name) && !nonLucideImports.has(name));
|
|
6003
|
-
if (invalid.length > 0) {
|
|
6004
|
-
const fallback = "Circle";
|
|
6005
|
-
for (const bad of invalid) {
|
|
6006
|
-
const re = new RegExp(`\\b${bad}\\b`, "g");
|
|
6007
|
-
newImport = newImport.replace(re, fallback);
|
|
6008
|
-
fixed = fixed.replace(re, fallback);
|
|
6009
|
-
}
|
|
6010
|
-
fixes.push(`invalid lucide icons \u2192 ${fallback}: ${invalid.join(", ")}`);
|
|
6011
|
-
}
|
|
6012
|
-
if (duplicates.length > 0 || invalid.length > 0) {
|
|
6013
|
-
const importedNames = [
|
|
6014
|
-
...new Set(
|
|
6015
|
-
newImport.split(",").map((s) => s.trim()).filter(Boolean)
|
|
6016
|
-
)
|
|
6017
|
-
];
|
|
6018
|
-
const originalImportLine = lucideImportMatch[0];
|
|
6019
|
-
fixed = fixed.replace(originalImportLine, `import { ${importedNames.join(", ")} } from "lucide-react"`);
|
|
6020
|
-
}
|
|
6021
|
-
}
|
|
6022
|
-
}
|
|
6023
|
-
const lucideImportMatch2 = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
6024
|
-
if (lucideImportMatch2) {
|
|
6025
|
-
let lucideExports2 = null;
|
|
6026
|
-
try {
|
|
6027
|
-
const { createRequire } = await import("module");
|
|
6028
|
-
const req = createRequire(process.cwd() + "/package.json");
|
|
6029
|
-
const lr = req("lucide-react");
|
|
6030
|
-
lucideExports2 = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
6031
|
-
} catch {
|
|
6032
|
-
}
|
|
6033
|
-
if (lucideExports2) {
|
|
6034
|
-
const allImportedNames = /* @__PURE__ */ new Set();
|
|
6035
|
-
for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
|
|
6036
|
-
m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => allImportedNames.add(n));
|
|
6037
|
-
}
|
|
6038
|
-
for (const m of fixed.matchAll(/import\s+([A-Z]\w+)\s+from/g)) {
|
|
6039
|
-
allImportedNames.add(m[1]);
|
|
6040
|
-
}
|
|
6041
|
-
const lucideImported = new Set(
|
|
6042
|
-
lucideImportMatch2[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
6043
|
-
);
|
|
6044
|
-
const jsxIconRefs = [...new Set([...fixed.matchAll(/<([A-Z][a-zA-Z]*Icon)\s/g)].map((m) => m[1]))];
|
|
6045
|
-
const missing = [];
|
|
6046
|
-
for (const ref of jsxIconRefs) {
|
|
6047
|
-
if (allImportedNames.has(ref)) continue;
|
|
6048
|
-
if (fixed.includes(`function ${ref}`) || fixed.includes(`const ${ref}`)) continue;
|
|
6049
|
-
const baseName = ref.replace(/Icon$/, "");
|
|
6050
|
-
if (lucideExports2.has(ref)) {
|
|
6051
|
-
missing.push(ref);
|
|
6052
|
-
lucideImported.add(ref);
|
|
6053
|
-
} else if (lucideExports2.has(baseName)) {
|
|
6054
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6055
|
-
fixed = fixed.replace(re, baseName);
|
|
6056
|
-
missing.push(baseName);
|
|
6057
|
-
lucideImported.add(baseName);
|
|
6058
|
-
fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
|
|
6059
|
-
} else {
|
|
6060
|
-
const fallback = "Circle";
|
|
6061
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6062
|
-
fixed = fixed.replace(re, fallback);
|
|
6063
|
-
lucideImported.add(fallback);
|
|
6064
|
-
fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
|
|
6065
|
-
}
|
|
6066
|
-
}
|
|
6067
|
-
if (missing.length > 0) {
|
|
6068
|
-
const allNames = [...lucideImported];
|
|
6069
|
-
const origLine = lucideImportMatch2[0];
|
|
6070
|
-
fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
|
|
6071
|
-
fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
|
|
6072
|
-
}
|
|
6073
|
-
}
|
|
6074
|
-
}
|
|
6075
|
-
const lucideNamesMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
6076
|
-
if (lucideNamesMatch) {
|
|
6077
|
-
const lucideNames = new Set(
|
|
6078
|
-
lucideNamesMatch[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
6079
|
-
);
|
|
6080
|
-
const beforeShrinkFix = fixed;
|
|
6081
|
-
for (const iconName of lucideNames) {
|
|
6082
|
-
const iconRe = new RegExp(`(<${iconName}\\s[^>]*className=")([^"]*)(")`, "g");
|
|
6083
|
-
fixed = fixed.replace(iconRe, (_m, pre, classes, post) => {
|
|
6084
|
-
if (/\bshrink-0\b/.test(classes)) return _m;
|
|
6085
|
-
return `${pre}${classes} shrink-0${post}`;
|
|
6086
|
-
});
|
|
6087
|
-
}
|
|
6088
|
-
if (fixed !== beforeShrinkFix) {
|
|
6089
|
-
fixes.push("added shrink-0 to icons");
|
|
6090
|
-
}
|
|
6091
|
-
}
|
|
6092
|
-
const linkWithButtonRe = /(<Link\b[^>]*>)\s*(<Button\b(?![^>]*asChild)[^>]*>)([\s\S]*?)<\/Button>\s*<\/Link>/g;
|
|
6093
|
-
const beforeLinkFix = fixed;
|
|
6094
|
-
fixed = fixed.replace(linkWithButtonRe, (_match, linkOpen, buttonOpen, inner) => {
|
|
6095
|
-
const hrefMatch = linkOpen.match(/href="([^"]*)"/);
|
|
6096
|
-
const href = hrefMatch ? hrefMatch[1] : "/";
|
|
6097
|
-
const buttonWithAsChild = buttonOpen.replace("<Button", "<Button asChild");
|
|
6098
|
-
return `${buttonWithAsChild}<Link href="${href}">${inner.trim()}</Link></Button>`;
|
|
6099
|
-
});
|
|
6100
|
-
if (fixed !== beforeLinkFix) {
|
|
6101
|
-
fixes.push("Link>Button \u2192 Button asChild>Link (DOM nesting fix)");
|
|
6102
|
-
}
|
|
6103
|
-
const beforeAsChildFlex = fixed;
|
|
6104
|
-
fixed = fixed.replace(
|
|
6105
|
-
/(<Button\b[^>]*\basChild\b[^>]*>)\s*(<(?:Link|a)\b)([^>]*)(>)/g,
|
|
6106
|
-
(_match, btnOpen, childTag, childProps, close) => {
|
|
6107
|
-
if (/\binline-flex\b/.test(childProps)) return _match;
|
|
6108
|
-
if (/className="([^"]*)"/.test(childProps)) {
|
|
6109
|
-
const merged = childProps.replace(
|
|
6110
|
-
/className="([^"]*)"/,
|
|
6111
|
-
(_cm, classes) => `className="inline-flex items-center gap-2 ${classes}"`
|
|
6112
|
-
);
|
|
6113
|
-
return `${btnOpen}${childTag}${merged}${close}`;
|
|
6114
|
-
}
|
|
6115
|
-
return `${btnOpen}${childTag} className="inline-flex items-center gap-2"${close}`;
|
|
6116
|
-
}
|
|
6117
|
-
);
|
|
6118
|
-
if (fixed !== beforeAsChildFlex) {
|
|
6119
|
-
fixes.push("added inline-flex to Button asChild children (base-ui compat)");
|
|
6120
|
-
}
|
|
6121
|
-
const beforeLinkHrefFix = fixed;
|
|
6122
|
-
fixed = fixed.replace(/<(Link|a)\b(?![^>]*\bhref\s*=)([^>]*)>([\s\S]*?)<\/\1>/g, (_match, tag, attrs, children) => {
|
|
6123
|
-
const textContent = children.replace(/<[^>]*>/g, "").trim();
|
|
6124
|
-
const href = resolveHref(textContent, context);
|
|
6125
|
-
return `<${tag} href="${href}"${attrs}>${children}</${tag}>`;
|
|
6126
|
-
});
|
|
6127
|
-
fixed = fixed.replace(/<(Link|a)\b(?![^>]*\bhref\s*=)([^>]*)\/?>/g, '<$1 href="/"$2>');
|
|
6128
|
-
if (fixed !== beforeLinkHrefFix) {
|
|
6129
|
-
fixes.push("added href to <Link>/<a> missing href");
|
|
6130
|
-
}
|
|
6131
|
-
const { code: fixedByRules, fixes: ruleFixes } = applyComponentRules(fixed);
|
|
6132
|
-
if (ruleFixes.length > 0) {
|
|
6133
|
-
fixed = fixedByRules;
|
|
6134
|
-
fixes.push(...ruleFixes);
|
|
6135
|
-
}
|
|
6136
|
-
const beforeTabsFix = fixed;
|
|
6137
|
-
fixed = fixed.replace(
|
|
6138
|
-
/(<TabsTrigger\b[^>]*className=")([^"]*)(")/g,
|
|
6139
|
-
(_m, pre, classes, post) => {
|
|
6140
|
-
const cleaned = classes.replace(/\b(border-input|border\b|outline\b)\s*/g, "").trim();
|
|
6141
|
-
if (cleaned !== classes.trim()) return `${pre}${cleaned}${post}`;
|
|
6142
|
-
return _m;
|
|
6143
|
-
}
|
|
6144
|
-
);
|
|
6145
|
-
if (fixed !== beforeTabsFix) {
|
|
6146
|
-
fixes.push("stripped border from TabsTrigger (shadcn handles active state)");
|
|
6147
|
-
}
|
|
6148
|
-
const beforeJunkFix = fixed;
|
|
6149
|
-
fixed = fixed.replace(/className="([^"]*)"/g, (_match, classes) => {
|
|
6150
|
-
const cleaned = classes.split(/\s+/).filter((c) => c !== "-0").join(" ");
|
|
6151
|
-
if (cleaned !== classes.trim()) return `className="${cleaned}"`;
|
|
6152
|
-
return _match;
|
|
6153
|
-
});
|
|
6154
|
-
if (fixed !== beforeJunkFix) {
|
|
6155
|
-
fixes.push("removed junk classes (-0)");
|
|
6156
|
-
}
|
|
6157
|
-
fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
|
|
6158
|
-
const cleaned = inner.replace(/\s{2,}/g, " ").trim();
|
|
6159
|
-
return `className="${cleaned}"`;
|
|
6160
|
-
});
|
|
6161
|
-
let imgCounter = 1;
|
|
6162
|
-
const beforeImgFix = fixed;
|
|
6163
|
-
fixed = fixed.replace(/["']\/api\/placeholder\/(\d+)\/(\d+)["']/g, (_m, w, h) => {
|
|
6164
|
-
return `"https://picsum.photos/${w}/${h}?random=${imgCounter++}"`;
|
|
6165
|
-
});
|
|
6166
|
-
fixed = fixed.replace(/["']\/placeholder-avatar[^"']*["']/g, () => {
|
|
6167
|
-
return `"https://i.pravatar.cc/150?u=user${imgCounter++}"`;
|
|
6168
|
-
});
|
|
6169
|
-
fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
|
|
6170
|
-
const height = h || w;
|
|
6171
|
-
return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
|
|
6172
|
-
});
|
|
6173
|
-
fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
|
|
6174
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
6175
|
-
});
|
|
6176
|
-
fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
|
|
6177
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
6178
|
-
});
|
|
6179
|
-
if (fixed !== beforeImgFix) {
|
|
6180
|
-
fixes.push("placeholder images \u2192 working URLs (picsum/pravatar)");
|
|
6181
|
-
}
|
|
6182
|
-
return { code: fixed, fixes };
|
|
6183
|
-
}
|
|
6184
|
-
function formatIssues(issues) {
|
|
6185
|
-
if (issues.length === 0) return "";
|
|
6186
|
-
const errors = issues.filter((i) => i.severity === "error");
|
|
6187
|
-
const warnings = issues.filter((i) => i.severity === "warning");
|
|
6188
|
-
const infos = issues.filter((i) => i.severity === "info");
|
|
6189
|
-
const lines = [];
|
|
6190
|
-
if (errors.length > 0) {
|
|
6191
|
-
lines.push(` \u274C ${errors.length} error(s):`);
|
|
6192
|
-
for (const e of errors) {
|
|
6193
|
-
lines.push(` L${e.line}: [${e.type}] ${e.message}`);
|
|
6194
|
-
}
|
|
6195
|
-
}
|
|
6196
|
-
if (warnings.length > 0) {
|
|
6197
|
-
lines.push(` \u26A0\uFE0F ${warnings.length} warning(s):`);
|
|
6198
|
-
for (const w of warnings) {
|
|
6199
|
-
lines.push(` L${w.line}: [${w.type}] ${w.message}`);
|
|
6200
|
-
}
|
|
6201
|
-
}
|
|
6202
|
-
if (infos.length > 0) {
|
|
6203
|
-
lines.push(` \u2139\uFE0F ${infos.length} info:`);
|
|
6204
|
-
for (const i of infos) {
|
|
6205
|
-
lines.push(` L${i.line}: [${i.type}] ${i.message}`);
|
|
6206
|
-
}
|
|
6207
|
-
}
|
|
6208
|
-
return lines.join("\n");
|
|
6209
|
-
}
|
|
6210
|
-
function checkDesignConsistency(code) {
|
|
6211
|
-
const warnings = [];
|
|
6212
|
-
const hexPattern = /\[#[0-9a-fA-F]{3,8}\]/g;
|
|
6213
|
-
for (const match of code.matchAll(hexPattern)) {
|
|
6214
|
-
warnings.push({
|
|
6215
|
-
type: "hardcoded-color",
|
|
6216
|
-
message: `Hardcoded color ${match[0]} \u2014 use a design token (e.g., bg-primary) instead`
|
|
6217
|
-
});
|
|
6218
|
-
}
|
|
6219
|
-
const spacingPattern = /[pm][trblxy]?-\[\d+px\]/g;
|
|
6220
|
-
for (const match of code.matchAll(spacingPattern)) {
|
|
6221
|
-
warnings.push({
|
|
6222
|
-
type: "arbitrary-spacing",
|
|
6223
|
-
message: `Arbitrary spacing ${match[0]} \u2014 use Tailwind spacing scale instead`
|
|
6224
|
-
});
|
|
6225
|
-
}
|
|
6226
|
-
return warnings;
|
|
6227
|
-
}
|
|
6228
|
-
function verifyIncrementalEdit(before, after) {
|
|
6229
|
-
const issues = [];
|
|
6230
|
-
const hookPattern = /\buse[A-Z]\w+\s*\(/;
|
|
6231
|
-
if (hookPattern.test(after) && !after.includes("'use client'") && !after.includes('"use client"')) {
|
|
6232
|
-
issues.push({
|
|
6233
|
-
type: "missing-use-client",
|
|
6234
|
-
message: 'Code uses React hooks but missing "use client" directive'
|
|
6235
|
-
});
|
|
6236
|
-
}
|
|
6237
|
-
if (!after.includes("export default")) {
|
|
6238
|
-
issues.push({
|
|
6239
|
-
type: "missing-default-export",
|
|
6240
|
-
message: "Missing default export \u2014 page component must have a default export"
|
|
6241
|
-
});
|
|
6242
|
-
}
|
|
6243
|
-
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
6244
|
-
const beforeImports = /* @__PURE__ */ new Set();
|
|
6245
|
-
const afterImports = /* @__PURE__ */ new Set();
|
|
6246
|
-
for (const match of before.matchAll(importRegex)) {
|
|
6247
|
-
match[1].split(",").forEach((s) => beforeImports.add(s.trim()));
|
|
6248
|
-
}
|
|
6249
|
-
for (const match of after.matchAll(importRegex)) {
|
|
6250
|
-
match[1].split(",").forEach((s) => afterImports.add(s.trim()));
|
|
6251
|
-
}
|
|
6252
|
-
for (const symbol of beforeImports) {
|
|
6253
|
-
if (!afterImports.has(symbol) && symbol.length > 0) {
|
|
6254
|
-
const codeWithoutImports = after.replace(/^import\s+.*$/gm, "");
|
|
6255
|
-
const symbolRegex = new RegExp(`\\b${symbol}\\b`);
|
|
6256
|
-
if (symbolRegex.test(codeWithoutImports)) {
|
|
6257
|
-
issues.push({
|
|
6258
|
-
type: "missing-import",
|
|
6259
|
-
symbol,
|
|
6260
|
-
message: `Import for "${symbol}" was removed but symbol is still used in code`
|
|
6261
|
-
});
|
|
6262
|
-
}
|
|
6263
|
-
}
|
|
6264
|
-
}
|
|
6265
|
-
return issues;
|
|
6266
|
-
}
|
|
6267
|
-
|
|
6268
4958
|
// src/commands/chat/utils.ts
|
|
6269
4959
|
import { resolve as resolve5 } from "path";
|
|
6270
4960
|
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
6271
4961
|
import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
|
|
6272
|
-
import
|
|
4962
|
+
import chalk7 from "chalk";
|
|
6273
4963
|
var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
|
|
6274
4964
|
var MIN_ANCHOR_PAGE_CODE_CHARS = 120;
|
|
6275
4965
|
var AUTH_ROUTE_SLUGS = /* @__PURE__ */ new Set([
|
|
@@ -6370,7 +5060,7 @@ async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, mani
|
|
|
6370
5060
|
if (hasImport) continue;
|
|
6371
5061
|
if (plannedForPage) {
|
|
6372
5062
|
console.log(
|
|
6373
|
-
|
|
5063
|
+
chalk7.yellow(
|
|
6374
5064
|
`
|
|
6375
5065
|
\u26A0 Page "${pageName}" should use shared component ${e.name} (per architecture plan) but it's not imported. Import from @/components/shared/${kebab}`
|
|
6376
5066
|
)
|
|
@@ -6380,7 +5070,7 @@ async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, mani
|
|
|
6380
5070
|
const sameNameAsTag = new RegExp(`<\\/?${e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s>]`).test(pageCode);
|
|
6381
5071
|
if (sameNameAsTag) {
|
|
6382
5072
|
console.log(
|
|
6383
|
-
|
|
5073
|
+
chalk7.yellow(
|
|
6384
5074
|
`
|
|
6385
5075
|
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
6386
5076
|
)
|
|
@@ -6399,7 +5089,7 @@ async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, mani
|
|
|
6399
5089
|
const overlapRatio = sharedTokens.size > 0 ? overlap / sharedTokens.size : 0;
|
|
6400
5090
|
if (overlap >= 20 && overlapRatio >= 0.6) {
|
|
6401
5091
|
console.log(
|
|
6402
|
-
|
|
5092
|
+
chalk7.yellow(
|
|
6403
5093
|
`
|
|
6404
5094
|
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
6405
5095
|
)
|
|
@@ -6449,10 +5139,10 @@ ${currentCode}
|
|
|
6449
5139
|
\`\`\`` : "";
|
|
6450
5140
|
return `Modify the shared component ${entry.name} (${entry.id}, file: ${entry.file}): ${message}. Read the current code below and apply the requested changes. Return the full updated component code as pageCode.${codeSnippet}`;
|
|
6451
5141
|
}
|
|
6452
|
-
console.log(
|
|
5142
|
+
console.log(chalk7.yellow(`
|
|
6453
5143
|
\u26A0\uFE0F Component "${target}" not found in shared components.`));
|
|
6454
|
-
console.log(
|
|
6455
|
-
console.log(
|
|
5144
|
+
console.log(chalk7.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
|
|
5145
|
+
console.log(chalk7.dim(" Proceeding with message as-is...\n"));
|
|
6456
5146
|
}
|
|
6457
5147
|
if (options.page) {
|
|
6458
5148
|
const target = options.page;
|
|
@@ -6474,10 +5164,10 @@ ${currentCode}
|
|
|
6474
5164
|
\`\`\`` : "";
|
|
6475
5165
|
return `Update page "${page.name}" (id: ${page.id}, route: ${page.route}, file: ${relPath}): ${message}. Read the current code below and apply the requested changes.${codeSnippet}`;
|
|
6476
5166
|
}
|
|
6477
|
-
console.log(
|
|
5167
|
+
console.log(chalk7.yellow(`
|
|
6478
5168
|
\u26A0\uFE0F Page "${target}" not found.`));
|
|
6479
|
-
console.log(
|
|
6480
|
-
console.log(
|
|
5169
|
+
console.log(chalk7.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
|
|
5170
|
+
console.log(chalk7.dim(" Proceeding with message as-is...\n"));
|
|
6481
5171
|
}
|
|
6482
5172
|
if (options.token) {
|
|
6483
5173
|
const target = options.token;
|
|
@@ -6997,7 +5687,7 @@ async function pMap(items, fn, concurrency = 3) {
|
|
|
6997
5687
|
}
|
|
6998
5688
|
|
|
6999
5689
|
// src/commands/chat/split-generator.ts
|
|
7000
|
-
import
|
|
5690
|
+
import chalk8 from "chalk";
|
|
7001
5691
|
function buildExistingPagesContext(config2) {
|
|
7002
5692
|
const pages = config2.pages || [];
|
|
7003
5693
|
const analyzed = pages.filter((p) => p.pageAnalysis);
|
|
@@ -7094,6 +5784,43 @@ Before implementing any section, check this list. Import and use matching compon
|
|
|
7094
5784
|
|
|
7095
5785
|
${sharedComponentsSummary}`;
|
|
7096
5786
|
}
|
|
5787
|
+
var RELEVANT_TYPES = {
|
|
5788
|
+
app: ["data-display", "form", "navigation", "feedback"],
|
|
5789
|
+
auth: ["form", "feedback"],
|
|
5790
|
+
marketing: ["section", "layout"]
|
|
5791
|
+
};
|
|
5792
|
+
function buildTieredComponentsPrompt(manifest, pageType) {
|
|
5793
|
+
if (manifest.shared.length === 0) return void 0;
|
|
5794
|
+
const relevantTypes = new Set(RELEVANT_TYPES[pageType] || RELEVANT_TYPES.app);
|
|
5795
|
+
const level1Lines = manifest.shared.map((e) => {
|
|
5796
|
+
const desc = e.description ? ` \u2014 ${e.description}` : "";
|
|
5797
|
+
return `- ${e.id} ${e.name} (${e.type})${desc}`;
|
|
5798
|
+
});
|
|
5799
|
+
const relevantComponents = manifest.shared.filter((e) => relevantTypes.has(e.type));
|
|
5800
|
+
const level2Blocks = relevantComponents.filter((e) => e.propsInterface || e.usageExample).map((e) => {
|
|
5801
|
+
const importPath = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
|
|
5802
|
+
const lines = [`### ${e.name} (${e.id})`];
|
|
5803
|
+
if (e.propsInterface) lines.push(`Props: ${e.propsInterface}`);
|
|
5804
|
+
if (e.usageExample) lines.push(`Usage: ${e.usageExample}`);
|
|
5805
|
+
lines.push(`Import: import { ${e.name} } from '@/components/shared/${importPath}'`);
|
|
5806
|
+
return lines.join("\n");
|
|
5807
|
+
});
|
|
5808
|
+
const sections = [
|
|
5809
|
+
`SHARED COMPONENTS \u2014 MANDATORY REUSE:`,
|
|
5810
|
+
`Before implementing any section, check this list. Import and use matching components. Do NOT re-implement these patterns inline.`,
|
|
5811
|
+
``,
|
|
5812
|
+
`Available components:`,
|
|
5813
|
+
...level1Lines
|
|
5814
|
+
];
|
|
5815
|
+
if (level2Blocks.length > 0) {
|
|
5816
|
+
sections.push(``, `Components to use on this page (detailed API):`, ...level2Blocks);
|
|
5817
|
+
}
|
|
5818
|
+
sections.push(
|
|
5819
|
+
``,
|
|
5820
|
+
`If you need a component from the list above that isn't detailed below, import it by path \u2014 the system will validate usage post-generation.`
|
|
5821
|
+
);
|
|
5822
|
+
return sections.join("\n");
|
|
5823
|
+
}
|
|
7097
5824
|
function formatPlanSummary(plan) {
|
|
7098
5825
|
if (plan.groups.length === 0) return "";
|
|
7099
5826
|
const groupLines = plan.groups.map((g) => ` Group "${g.id}" (layout: ${g.layout}): ${g.pages.join(", ")}`);
|
|
@@ -7229,9 +5956,9 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
7229
5956
|
const sharedSummary = plan.sharedComponents.length > 0 ? plan.sharedComponents.map((c) => `${c.name} \u2192 ${c.usedBy.join(", ")}`).join(" | ") : "";
|
|
7230
5957
|
const totalPages = plan.groups.reduce((sum, g) => sum + g.pages.length, 0);
|
|
7231
5958
|
spinner.succeed(`Phase 2/6 \u2014 Architecture plan created`);
|
|
7232
|
-
console.log(
|
|
7233
|
-
if (sharedSummary) console.log(
|
|
7234
|
-
console.log(
|
|
5959
|
+
console.log(chalk8.dim(` Groups: ${groupsSummary}`));
|
|
5960
|
+
if (sharedSummary) console.log(chalk8.dim(` Shared: ${sharedSummary}`));
|
|
5961
|
+
console.log(chalk8.dim(` Total: ${totalPages} pages, ${plan.sharedComponents.length} shared components`));
|
|
7235
5962
|
if (plan.sharedComponents.length > 0 && parseOpts.projectRoot) {
|
|
7236
5963
|
const allDeps = new Set(plan.sharedComponents.flatMap((c) => c.shadcnDeps));
|
|
7237
5964
|
if (allDeps.size > 0) {
|
|
@@ -7248,7 +5975,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
7248
5975
|
spinner.warn("Phase 2/6 \u2014 Plan generation failed (continuing without plan)");
|
|
7249
5976
|
}
|
|
7250
5977
|
for (const w of planWarnings) {
|
|
7251
|
-
console.log(
|
|
5978
|
+
console.log(chalk8.dim(` ${w}`));
|
|
7252
5979
|
}
|
|
7253
5980
|
} catch {
|
|
7254
5981
|
spinner.warn("Phase 2/6 \u2014 Plan generation failed (continuing without plan)");
|
|
@@ -7309,7 +6036,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
7309
6036
|
if (plan && plan.sharedComponents.length > 0) {
|
|
7310
6037
|
spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
|
|
7311
6038
|
try {
|
|
7312
|
-
const { generateSharedComponentsFromPlan } = await import("./plan-generator-
|
|
6039
|
+
const { generateSharedComponentsFromPlan } = await import("./plan-generator-H55WEIY2.js");
|
|
7313
6040
|
const generated = await generateSharedComponentsFromPlan(
|
|
7314
6041
|
plan,
|
|
7315
6042
|
styleContext,
|
|
@@ -7353,6 +6080,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
7353
6080
|
spinner.start(`Phase 5/6 \u2014 Generating ${remainingPages.length} pages in parallel...`);
|
|
7354
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.";
|
|
7355
6082
|
const sharedComponentsNote = buildSharedComponentsNote(parseOpts.sharedComponentsSummary);
|
|
6083
|
+
const currentManifest = projectRoot ? await loadManifest5(projectRoot) : null;
|
|
7356
6084
|
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="#".`;
|
|
7357
6085
|
const alignmentNote = 'CRITICAL LAYOUT RULE: Every <section> must wrap its content in a container div matching the header width. Use the EXACT same container classes as shown in the style context (e.g. className="container max-w-6xl px-4" or className="max-w-6xl mx-auto px-4"). Inner content can use narrower max-w for text centering, but the outer section container MUST match.';
|
|
7358
6086
|
const planSummaryNote = plan ? formatPlanSummary(plan) : "";
|
|
@@ -7373,6 +6101,7 @@ ${existingAppPageCode}
|
|
|
7373
6101
|
const pageType = plan ? getPageType(route, plan) : inferPageTypeFromRoute(route);
|
|
7374
6102
|
const designConstraints = getDesignQualityForType(pageType);
|
|
7375
6103
|
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;
|
|
6104
|
+
const tieredNote = currentManifest ? buildTieredComponentsPrompt(currentManifest, pageType) : void 0;
|
|
7376
6105
|
const prompt = [
|
|
7377
6106
|
`Create ONE page called "${name}" at route "${route}".`,
|
|
7378
6107
|
`Context: ${message}.`,
|
|
@@ -7380,7 +6109,7 @@ ${existingAppPageCode}
|
|
|
7380
6109
|
`PAGE TYPE: ${pageType}`,
|
|
7381
6110
|
designConstraints,
|
|
7382
6111
|
sharedLayoutNote,
|
|
7383
|
-
sharedComponentsNote,
|
|
6112
|
+
tieredNote || sharedComponentsNote,
|
|
7384
6113
|
routeNote,
|
|
7385
6114
|
alignmentNote,
|
|
7386
6115
|
authNote,
|
|
@@ -7413,12 +6142,14 @@ ${existingAppPageCode}
|
|
|
7413
6142
|
const pageRoute = page.route || `/${pageName.toLowerCase()}`;
|
|
7414
6143
|
try {
|
|
7415
6144
|
const retryPageType = plan ? getPageType(pageRoute, plan) : inferPageTypeFromRoute(pageRoute);
|
|
6145
|
+
const retryTieredNote = currentManifest ? buildTieredComponentsPrompt(currentManifest, retryPageType) : void 0;
|
|
7416
6146
|
const lightweightPrompt = buildLightweightPagePrompt(
|
|
7417
6147
|
pageName,
|
|
7418
6148
|
pageRoute,
|
|
7419
6149
|
styleContext || "",
|
|
7420
6150
|
parseOpts.sharedComponentsSummary,
|
|
7421
|
-
retryPageType
|
|
6151
|
+
retryPageType,
|
|
6152
|
+
retryTieredNote
|
|
7422
6153
|
);
|
|
7423
6154
|
const retryResult = await parseModification(lightweightPrompt, modCtx, provider, {
|
|
7424
6155
|
...parseOpts,
|
|
@@ -7549,7 +6280,7 @@ function extractAppNameFromPrompt(prompt) {
|
|
|
7549
6280
|
import { resolve as resolve8 } from "path";
|
|
7550
6281
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
7551
6282
|
import { dirname as dirname6 } from "path";
|
|
7552
|
-
import
|
|
6283
|
+
import chalk11 from "chalk";
|
|
7553
6284
|
import {
|
|
7554
6285
|
getTemplateForPageType,
|
|
7555
6286
|
loadManifest as loadManifest6,
|
|
@@ -7570,7 +6301,7 @@ import {
|
|
|
7570
6301
|
TailwindConfigGenerator
|
|
7571
6302
|
} from "@getcoherent/core";
|
|
7572
6303
|
import { integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout2, generateSharedComponent as generateSharedComponent3 } from "@getcoherent/core";
|
|
7573
|
-
import
|
|
6304
|
+
import chalk9 from "chalk";
|
|
7574
6305
|
|
|
7575
6306
|
// src/utils/file-hashes.ts
|
|
7576
6307
|
import { createHash } from "crypto";
|
|
@@ -7674,7 +6405,7 @@ async function canOverwriteShared(projectRoot, componentFile, storedHashes) {
|
|
|
7674
6405
|
if (!storedHash) return true;
|
|
7675
6406
|
const edited = await isManuallyEdited(filePath, storedHash);
|
|
7676
6407
|
if (edited) {
|
|
7677
|
-
console.log(
|
|
6408
|
+
console.log(chalk9.yellow(` \u26A0 Skipping ${componentFile} \u2014 manually edited since last generation`));
|
|
7678
6409
|
}
|
|
7679
6410
|
return !edited;
|
|
7680
6411
|
}
|
|
@@ -7737,7 +6468,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
|
|
|
7737
6468
|
await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type, options.navChanged);
|
|
7738
6469
|
} catch (err) {
|
|
7739
6470
|
if (process.env.COHERENT_DEBUG === "1") {
|
|
7740
|
-
console.log(
|
|
6471
|
+
console.log(chalk9.dim("Layout integration warning:", err));
|
|
7741
6472
|
}
|
|
7742
6473
|
}
|
|
7743
6474
|
}
|
|
@@ -7879,7 +6610,7 @@ async function regenerateFiles(modified, config2, projectRoot, options = { navCh
|
|
|
7879
6610
|
});
|
|
7880
6611
|
const sharedInstalled = await scanAndInstallSharedDeps(projectRoot);
|
|
7881
6612
|
if (sharedInstalled.length > 0 && process.env.COHERENT_DEBUG === "1") {
|
|
7882
|
-
console.log(
|
|
6613
|
+
console.log(chalk9.dim(` Auto-installed shared deps: ${sharedInstalled.join(", ")}`));
|
|
7883
6614
|
}
|
|
7884
6615
|
}
|
|
7885
6616
|
if (componentIds.size > 0) {
|
|
@@ -7936,7 +6667,7 @@ function extractBalancedTag(source, tagName) {
|
|
|
7936
6667
|
}
|
|
7937
6668
|
|
|
7938
6669
|
// src/commands/chat/reporting.ts
|
|
7939
|
-
import
|
|
6670
|
+
import chalk10 from "chalk";
|
|
7940
6671
|
function extractImportsFrom(code, fromPath) {
|
|
7941
6672
|
const escaped = fromPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7942
6673
|
const regex = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"\`]${escaped}[^'"\`]*['"\`]`, "g");
|
|
@@ -7958,27 +6689,27 @@ function printPostGenerationReport(opts) {
|
|
|
7958
6689
|
const iconCount = extractImportsFrom(code, "lucide-react").length;
|
|
7959
6690
|
const hasInstalled = postFixes.some((f) => f.startsWith("Installed:"));
|
|
7960
6691
|
const syntaxStatus = postFixes.length > 0 ? postFixes.some((f) => f.includes("metadata")) ? "fixed (escaped metadata quotes) \u2714" : "fixed \u2714" : "valid \u2714";
|
|
7961
|
-
console.log(
|
|
6692
|
+
console.log(chalk10.green(`
|
|
7962
6693
|
\u2705 Page "${pageTitle}" ${action} at ${filePath}
|
|
7963
6694
|
`));
|
|
7964
6695
|
if (uiComponents.length > 0) {
|
|
7965
|
-
console.log(
|
|
6696
|
+
console.log(chalk10.dim(` Components: ${uiComponents.join(", ")} (from @/components/ui)`));
|
|
7966
6697
|
}
|
|
7967
6698
|
if (inCodeShared.length > 0) {
|
|
7968
|
-
console.log(
|
|
6699
|
+
console.log(chalk10.dim(` Shared: ${inCodeShared.map((s) => `${s.id} (${s.name})`).join(", ")}`));
|
|
7969
6700
|
}
|
|
7970
6701
|
if (layoutShared.length > 0) {
|
|
7971
|
-
console.log(
|
|
6702
|
+
console.log(chalk10.dim(` Layout: ${layoutShared.map((l) => `${l.id} (${l.name})`).join(", ")} via layout.tsx`));
|
|
7972
6703
|
}
|
|
7973
6704
|
if (iconCount > 0) {
|
|
7974
|
-
console.log(
|
|
6705
|
+
console.log(chalk10.dim(` Icons: ${iconCount} from lucide-react`));
|
|
7975
6706
|
}
|
|
7976
6707
|
if (hasInstalled) {
|
|
7977
|
-
console.log(
|
|
6708
|
+
console.log(chalk10.dim(" Dependencies: installed \u2714"));
|
|
7978
6709
|
}
|
|
7979
|
-
console.log(
|
|
6710
|
+
console.log(chalk10.dim(` Syntax: ${syntaxStatus}`));
|
|
7980
6711
|
if (route) {
|
|
7981
|
-
console.log(
|
|
6712
|
+
console.log(chalk10.cyan(`
|
|
7982
6713
|
Preview: http://localhost:3000${route}`));
|
|
7983
6714
|
}
|
|
7984
6715
|
console.log("");
|
|
@@ -7986,35 +6717,35 @@ function printPostGenerationReport(opts) {
|
|
|
7986
6717
|
function printSharedComponentReport(opts) {
|
|
7987
6718
|
const { id, name, file, instruction, postFixes = [] } = opts;
|
|
7988
6719
|
const syntaxStatus = postFixes.length > 0 ? "fixed \u2714" : "valid \u2714";
|
|
7989
|
-
console.log(
|
|
6720
|
+
console.log(chalk10.green(`
|
|
7990
6721
|
\u2705 Updated ${id} (${name}) at ${file}
|
|
7991
6722
|
`));
|
|
7992
6723
|
if (instruction) {
|
|
7993
6724
|
const snippet = instruction.length > 60 ? instruction.slice(0, 57) + "..." : instruction;
|
|
7994
|
-
console.log(
|
|
6725
|
+
console.log(chalk10.dim(` Changed: ${snippet}`));
|
|
7995
6726
|
}
|
|
7996
|
-
console.log(
|
|
7997
|
-
console.log(
|
|
6727
|
+
console.log(chalk10.dim(" Affects: all pages via layout.tsx"));
|
|
6728
|
+
console.log(chalk10.dim(` Syntax: ${syntaxStatus}`));
|
|
7998
6729
|
console.log("");
|
|
7999
6730
|
}
|
|
8000
6731
|
function printLinkSharedReport(opts) {
|
|
8001
6732
|
const { sharedId, sharedName, pageTarget, route, postFixes = [] } = opts;
|
|
8002
6733
|
const syntaxStatus = postFixes.length > 0 ? "fixed \u2714" : "valid \u2714";
|
|
8003
|
-
console.log(
|
|
6734
|
+
console.log(chalk10.green(`
|
|
8004
6735
|
\u2705 Linked ${sharedId} (${sharedName}) to page "${pageTarget}"
|
|
8005
6736
|
`));
|
|
8006
|
-
console.log(
|
|
8007
|
-
console.log(
|
|
6737
|
+
console.log(chalk10.dim(` Syntax: ${syntaxStatus}`));
|
|
6738
|
+
console.log(chalk10.cyan(` Preview: http://localhost:3000${route}`));
|
|
8008
6739
|
console.log("");
|
|
8009
6740
|
}
|
|
8010
6741
|
function printPromoteAndLinkReport(opts) {
|
|
8011
6742
|
const { id, name, file, usedInFiles, postFixes = [] } = opts;
|
|
8012
6743
|
const syntaxStatus = postFixes.length > 0 ? "fixed \u2714" : "valid \u2714";
|
|
8013
|
-
console.log(
|
|
6744
|
+
console.log(chalk10.green(`
|
|
8014
6745
|
\u2705 Created ${id} (${name}) at ${file}
|
|
8015
6746
|
`));
|
|
8016
|
-
console.log(
|
|
8017
|
-
console.log(
|
|
6747
|
+
console.log(chalk10.dim(` Linked to: ${usedInFiles.length} page(s)`));
|
|
6748
|
+
console.log(chalk10.dim(` Syntax: ${syntaxStatus}`));
|
|
8018
6749
|
console.log("");
|
|
8019
6750
|
}
|
|
8020
6751
|
function showPreview(requests, results, config2, preflightInstalledNames) {
|
|
@@ -8032,23 +6763,23 @@ function showPreview(requests, results, config2, preflightInstalledNames) {
|
|
|
8032
6763
|
const modifiedSharedComponents = successfulPairs.filter(({ request }) => request.type === "modify-layout-block");
|
|
8033
6764
|
const modifiedPages = successfulPairs.filter(({ request }) => request.type === "update-page");
|
|
8034
6765
|
const tokenChanges = successfulPairs.filter(({ request }) => request.type === "update-token");
|
|
8035
|
-
console.log(
|
|
6766
|
+
console.log(chalk10.bold.cyan("\n\u{1F4CB} Changes Applied:\n"));
|
|
8036
6767
|
if (preflightInstalledNames && preflightInstalledNames.length > 0) {
|
|
8037
|
-
console.log(
|
|
6768
|
+
console.log(chalk10.cyan("\u{1F50D} Pre-flight check: Installed missing components:"));
|
|
8038
6769
|
preflightInstalledNames.forEach((name) => {
|
|
8039
|
-
console.log(
|
|
6770
|
+
console.log(chalk10.green(` \u2728 Auto-installed ${name}`));
|
|
8040
6771
|
});
|
|
8041
6772
|
console.log("");
|
|
8042
6773
|
}
|
|
8043
6774
|
if (addedComponents.length > 0) {
|
|
8044
6775
|
const names = addedComponents.map(({ request }) => request.changes.name).filter(Boolean);
|
|
8045
|
-
console.log(
|
|
8046
|
-
console.log(
|
|
6776
|
+
console.log(chalk10.green("\u{1F4E6} Components:"));
|
|
6777
|
+
console.log(chalk10.white(` \u2728 Auto-installed: ${names.join(", ")}`));
|
|
8047
6778
|
}
|
|
8048
6779
|
if (customComponents.length > 0) {
|
|
8049
6780
|
const names = customComponents.map(({ request }) => request.changes.name).filter(Boolean);
|
|
8050
|
-
if (addedComponents.length === 0) console.log(
|
|
8051
|
-
console.log(
|
|
6781
|
+
if (addedComponents.length === 0) console.log(chalk10.green("\u{1F4E6} Components:"));
|
|
6782
|
+
console.log(chalk10.white(` \u2728 Created: ${names.join(", ")}`));
|
|
8052
6783
|
}
|
|
8053
6784
|
const usedComponentIds = /* @__PURE__ */ new Set();
|
|
8054
6785
|
addedPages.forEach(({ request }) => {
|
|
@@ -8063,73 +6794,73 @@ function showPreview(requests, results, config2, preflightInstalledNames) {
|
|
|
8063
6794
|
]);
|
|
8064
6795
|
const reusedIds = [...usedComponentIds].filter((id) => !newComponentIds.has(id));
|
|
8065
6796
|
if (reusedIds.length > 0) {
|
|
8066
|
-
if (addedComponents.length === 0 && customComponents.length === 0) console.log(
|
|
8067
|
-
console.log(
|
|
6797
|
+
if (addedComponents.length === 0 && customComponents.length === 0) console.log(chalk10.green("\u{1F4E6} Components:"));
|
|
6798
|
+
console.log(chalk10.white(` \u{1F504} Reused: ${reusedIds.join(", ")}`));
|
|
8068
6799
|
}
|
|
8069
6800
|
if (addedComponents.length > 0 || customComponents.length > 0 || reusedIds.length > 0) {
|
|
8070
6801
|
console.log("");
|
|
8071
6802
|
}
|
|
8072
6803
|
if (addedPages.length > 0) {
|
|
8073
|
-
console.log(
|
|
6804
|
+
console.log(chalk10.green("\u{1F4C4} Pages Created:"));
|
|
8074
6805
|
addedPages.forEach(({ request }) => {
|
|
8075
6806
|
const page = request.changes;
|
|
8076
6807
|
const route = page.route || "/";
|
|
8077
|
-
console.log(
|
|
8078
|
-
console.log(
|
|
6808
|
+
console.log(chalk10.white(` \u2728 ${page.name || "Page"}`));
|
|
6809
|
+
console.log(chalk10.gray(` Route: ${route}`));
|
|
8079
6810
|
const configPage = config2.pages?.find((p) => p.id === page.id || p.route === (page.route || "/"));
|
|
8080
6811
|
const sectionCount = configPage?.pageAnalysis?.sections?.length ?? page.sections?.length ?? 0;
|
|
8081
|
-
console.log(
|
|
6812
|
+
console.log(chalk10.gray(` Sections: ${sectionCount}`));
|
|
8082
6813
|
});
|
|
8083
6814
|
console.log("");
|
|
8084
6815
|
}
|
|
8085
6816
|
if (modifiedComponents.length > 0 || modifiedSharedComponents.length > 0 || modifiedPages.length > 0 || tokenChanges.length > 0) {
|
|
8086
|
-
console.log(
|
|
6817
|
+
console.log(chalk10.yellow("\u{1F527} Modified:"));
|
|
8087
6818
|
modifiedComponents.forEach(({ result }) => {
|
|
8088
|
-
console.log(
|
|
6819
|
+
console.log(chalk10.white(` \u2022 ${result.message}`));
|
|
8089
6820
|
});
|
|
8090
6821
|
modifiedSharedComponents.forEach(({ result }) => {
|
|
8091
|
-
console.log(
|
|
6822
|
+
console.log(chalk10.white(` \u2022 ${result.message}`));
|
|
8092
6823
|
});
|
|
8093
6824
|
modifiedPages.forEach(({ result }) => {
|
|
8094
|
-
console.log(
|
|
6825
|
+
console.log(chalk10.white(` \u2022 ${result.message}`));
|
|
8095
6826
|
});
|
|
8096
6827
|
tokenChanges.forEach(({ result }) => {
|
|
8097
|
-
console.log(
|
|
6828
|
+
console.log(chalk10.white(` \u2022 ${result.message}`));
|
|
8098
6829
|
});
|
|
8099
6830
|
console.log("");
|
|
8100
6831
|
}
|
|
8101
6832
|
if (failedPairs.length > 0) {
|
|
8102
|
-
console.log(
|
|
6833
|
+
console.log(chalk10.red("\u274C Failed modifications:"));
|
|
8103
6834
|
failedPairs.forEach(({ result }) => {
|
|
8104
|
-
console.log(
|
|
6835
|
+
console.log(chalk10.gray(` \u2716 ${result.message}`));
|
|
8105
6836
|
});
|
|
8106
6837
|
console.log("");
|
|
8107
6838
|
}
|
|
8108
6839
|
const successCount = successfulPairs.length;
|
|
8109
6840
|
const totalCount = results.length;
|
|
8110
6841
|
if (successCount === totalCount) {
|
|
8111
|
-
console.log(
|
|
6842
|
+
console.log(chalk10.green.bold(`\u2705 Success! ${successCount} modification(s) applied
|
|
8112
6843
|
`));
|
|
8113
6844
|
} else {
|
|
8114
|
-
console.log(
|
|
6845
|
+
console.log(chalk10.yellow.bold(`\u26A0\uFE0F Partial success: ${successCount}/${totalCount} modification(s) applied
|
|
8115
6846
|
`));
|
|
8116
6847
|
}
|
|
8117
6848
|
if (addedPages.length > 0) {
|
|
8118
6849
|
const firstPage = addedPages[0].request.changes;
|
|
8119
6850
|
const route = firstPage?.route || "/";
|
|
8120
|
-
console.log(
|
|
8121
|
-
console.log(
|
|
8122
|
-
console.log(
|
|
8123
|
-
console.log(
|
|
6851
|
+
console.log(chalk10.cyan("\u{1F680} What's next:\n"));
|
|
6852
|
+
console.log(chalk10.white(" \u{1F4FA} View in browser:"));
|
|
6853
|
+
console.log(chalk10.cyan(" coherent preview"));
|
|
6854
|
+
console.log(chalk10.gray(` \u2192 Opens http://localhost:3000${route}
|
|
8124
6855
|
`));
|
|
8125
|
-
console.log(
|
|
8126
|
-
console.log(
|
|
8127
|
-
console.log(
|
|
6856
|
+
console.log(chalk10.white(" \u{1F3A8} Customize:"));
|
|
6857
|
+
console.log(chalk10.cyan(' coherent chat "make buttons rounded"'));
|
|
6858
|
+
console.log(chalk10.cyan(` coherent chat "add hero section to ${firstPage?.name ?? "page"}"`));
|
|
8128
6859
|
console.log("");
|
|
8129
6860
|
} else if (successCount > 0) {
|
|
8130
|
-
console.log(
|
|
8131
|
-
console.log(
|
|
8132
|
-
console.log(
|
|
6861
|
+
console.log(chalk10.cyan("\u{1F680} What's next:\n"));
|
|
6862
|
+
console.log(chalk10.white(" \u{1F4FA} Preview changes:"));
|
|
6863
|
+
console.log(chalk10.cyan(" coherent preview\n"));
|
|
8133
6864
|
}
|
|
8134
6865
|
}
|
|
8135
6866
|
function getChangeDescription(request, config2) {
|
|
@@ -8278,8 +7009,8 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8278
7009
|
const newCode = await ai.editSharedComponentCode(currentCode, instruction, resolved.name);
|
|
8279
7010
|
const { fixedCode, fixes } = await validateAndFixGeneratedCode(projectRoot, newCode, { isPage: false });
|
|
8280
7011
|
if (fixes.length > 0) {
|
|
8281
|
-
console.log(
|
|
8282
|
-
fixes.forEach((f) => console.log(
|
|
7012
|
+
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7013
|
+
fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8283
7014
|
}
|
|
8284
7015
|
await writeFile(fullPath, fixedCode);
|
|
8285
7016
|
printSharedComponentReport({
|
|
@@ -8352,8 +7083,8 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8352
7083
|
const newPageCode = await ai.replaceInlineWithShared(pageCode, sharedCode, resolved.name, changes?.blockHint);
|
|
8353
7084
|
const { fixedCode, fixes } = await validateAndFixGeneratedCode(projectRoot, newPageCode, { isPage: true });
|
|
8354
7085
|
if (fixes.length > 0) {
|
|
8355
|
-
console.log(
|
|
8356
|
-
fixes.forEach((f) => console.log(
|
|
7086
|
+
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7087
|
+
fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8357
7088
|
}
|
|
8358
7089
|
await writeFile(pageFilePath, fixedCode);
|
|
8359
7090
|
const manifest = await loadManifest6(projectRoot);
|
|
@@ -8455,8 +7186,8 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8455
7186
|
const newCode = await ai.replaceInlineWithShared(linkPageCode, sharedCode, created.name, blockHint);
|
|
8456
7187
|
const { fixedCode, fixes } = await validateAndFixGeneratedCode(projectRoot, newCode, { isPage: true });
|
|
8457
7188
|
if (fixes.length > 0) {
|
|
8458
|
-
console.log(
|
|
8459
|
-
fixes.forEach((f) => console.log(
|
|
7189
|
+
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7190
|
+
fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8460
7191
|
}
|
|
8461
7192
|
await writeFile(fullPath, fixedCode);
|
|
8462
7193
|
usedInFiles.push(relPath);
|
|
@@ -8547,7 +7278,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8547
7278
|
const aiPageCode = typeof page.pageCode === "string" && page.pageCode.trim() !== "" ? page.pageCode : void 0;
|
|
8548
7279
|
if (aiPageCode) {
|
|
8549
7280
|
finalPageCode = aiPageCode;
|
|
8550
|
-
if (DEBUG2) console.log(
|
|
7281
|
+
if (DEBUG2) console.log(chalk11.dim(` [pageCode] Using AI-generated pageCode (user content priority)`));
|
|
8551
7282
|
} else {
|
|
8552
7283
|
const inferredType = page.pageType || inferPageType(page.route || "", page.name || "");
|
|
8553
7284
|
if (inferredType) {
|
|
@@ -8562,19 +7293,19 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8562
7293
|
const content = page.structuredContent || getDefaultContent(inferredType, page.name || pageName);
|
|
8563
7294
|
finalPageCode = templateFn(content, opts);
|
|
8564
7295
|
if (DEBUG2)
|
|
8565
|
-
console.log(
|
|
7296
|
+
console.log(chalk11.dim(` [template] Used "${inferredType}" template (inferred from route/name)`));
|
|
8566
7297
|
} catch {
|
|
8567
|
-
if (DEBUG2) console.log(
|
|
7298
|
+
if (DEBUG2) console.log(chalk11.dim(` [template] Failed for "${inferredType}"`));
|
|
8568
7299
|
}
|
|
8569
7300
|
}
|
|
8570
7301
|
}
|
|
8571
7302
|
}
|
|
8572
7303
|
if (!finalPageCode) {
|
|
8573
|
-
console.log(
|
|
7304
|
+
console.log(chalk11.yellow(`
|
|
8574
7305
|
\u26A0\uFE0F Page "${page.name || page.id}" has no generated code \u2014 it will appear empty.`));
|
|
8575
|
-
console.log(
|
|
7306
|
+
console.log(chalk11.dim(" This usually means the AI did not produce pageCode for this page."));
|
|
8576
7307
|
console.log(
|
|
8577
|
-
|
|
7308
|
+
chalk11.dim(
|
|
8578
7309
|
' Try running: coherent chat "regenerate the ' + (page.name || page.id) + ' page with full content"'
|
|
8579
7310
|
)
|
|
8580
7311
|
);
|
|
@@ -8646,8 +7377,8 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8646
7377
|
const allFixes = [...postFixes, ...autoFixes];
|
|
8647
7378
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
8648
7379
|
if (allFixes.length > 0) {
|
|
8649
|
-
console.log(
|
|
8650
|
-
allFixes.forEach((f) => console.log(
|
|
7380
|
+
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7381
|
+
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8651
7382
|
}
|
|
8652
7383
|
await writeFile(filePath, codeToWrite);
|
|
8653
7384
|
const pageIdx = dsm.getConfig().pages.findIndex((p) => p.id === page.id);
|
|
@@ -8684,7 +7415,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
8684
7415
|
const errors = issues.filter((i) => i.severity === "error");
|
|
8685
7416
|
if (errors.length >= 2 && aiProvider) {
|
|
8686
7417
|
console.log(
|
|
8687
|
-
|
|
7418
|
+
chalk11.yellow(`
|
|
8688
7419
|
\u{1F504} ${errors.length} quality errors \u2014 attempting AI fix for ${page.name || page.id}...`)
|
|
8689
7420
|
);
|
|
8690
7421
|
try {
|
|
@@ -8713,7 +7444,7 @@ Rules:
|
|
|
8713
7444
|
await writeFile(filePath, codeToWrite);
|
|
8714
7445
|
issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
|
|
8715
7446
|
const finalErrors = issues.filter((i) => i.severity === "error").length;
|
|
8716
|
-
console.log(
|
|
7447
|
+
console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
|
|
8717
7448
|
}
|
|
8718
7449
|
}
|
|
8719
7450
|
}
|
|
@@ -8722,15 +7453,15 @@ Rules:
|
|
|
8722
7453
|
}
|
|
8723
7454
|
const report = formatIssues(issues);
|
|
8724
7455
|
if (report) {
|
|
8725
|
-
console.log(
|
|
7456
|
+
console.log(chalk11.yellow(`
|
|
8726
7457
|
\u{1F50D} Quality check for ${page.name || page.id}:`));
|
|
8727
|
-
console.log(
|
|
7458
|
+
console.log(chalk11.dim(report));
|
|
8728
7459
|
}
|
|
8729
7460
|
const consistency = checkDesignConsistency(codeToWrite);
|
|
8730
7461
|
if (consistency.length > 0) {
|
|
8731
|
-
console.log(
|
|
7462
|
+
console.log(chalk11.yellow(`
|
|
8732
7463
|
\u{1F3A8} Design consistency for ${page.name || page.id}:`));
|
|
8733
|
-
consistency.forEach((w) => console.log(
|
|
7464
|
+
consistency.forEach((w) => console.log(chalk11.dim(` \u26A0 [${w.type}] ${w.message}`)));
|
|
8734
7465
|
}
|
|
8735
7466
|
}
|
|
8736
7467
|
}
|
|
@@ -8745,9 +7476,9 @@ Rules:
|
|
|
8745
7476
|
const changes = request.changes;
|
|
8746
7477
|
const instruction = originalMessage || (typeof changes?.instruction === "string" ? changes.instruction : void 0);
|
|
8747
7478
|
let resolvedPageCode = typeof changes?.pageCode === "string" && changes.pageCode.trim() !== "" ? changes.pageCode : void 0;
|
|
8748
|
-
if (DEBUG2 && instruction) console.log(
|
|
7479
|
+
if (DEBUG2 && instruction) console.log(chalk11.dim(` [update-page] instruction: ${instruction.slice(0, 120)}...`));
|
|
8749
7480
|
if (DEBUG2 && resolvedPageCode)
|
|
8750
|
-
console.log(
|
|
7481
|
+
console.log(chalk11.dim(` [update-page] has pageCode (${resolvedPageCode.length} chars)`));
|
|
8751
7482
|
const configChanges = { ...changes };
|
|
8752
7483
|
delete configChanges.pageCode;
|
|
8753
7484
|
delete configChanges.pageType;
|
|
@@ -8771,12 +7502,12 @@ Rules:
|
|
|
8771
7502
|
try {
|
|
8772
7503
|
currentCode = await readFile(absPath);
|
|
8773
7504
|
} catch {
|
|
8774
|
-
if (DEBUG2) console.log(
|
|
7505
|
+
if (DEBUG2) console.log(chalk11.dim(` [update-page] Could not read current file at ${absPath}`));
|
|
8775
7506
|
}
|
|
8776
7507
|
if (currentCode) {
|
|
8777
7508
|
const ai = await createAIProvider(aiProvider ?? "auto");
|
|
8778
7509
|
if (ai.editPageCode) {
|
|
8779
|
-
console.log(
|
|
7510
|
+
console.log(chalk11.dim(" \u270F\uFE0F Applying changes to existing page..."));
|
|
8780
7511
|
const coreRules = CORE_CONSTRAINTS;
|
|
8781
7512
|
const pageRoute = pageDef.route || `/${pageDef.id}`;
|
|
8782
7513
|
const pageType = inferPageTypeFromRoute(pageRoute);
|
|
@@ -8799,15 +7530,15 @@ ${contextualRules}
|
|
|
8799
7530
|
${routeRules}
|
|
8800
7531
|
${pagesCtx}`
|
|
8801
7532
|
);
|
|
8802
|
-
if (DEBUG2) console.log(
|
|
7533
|
+
if (DEBUG2) console.log(chalk11.dim(` [update-page] AI returned ${resolvedPageCode.length} chars`));
|
|
8803
7534
|
const editIssues = verifyIncrementalEdit(currentCode, resolvedPageCode);
|
|
8804
7535
|
if (editIssues.length > 0) {
|
|
8805
|
-
console.log(
|
|
7536
|
+
console.log(chalk11.yellow(`
|
|
8806
7537
|
\u26A0 Incremental edit issues for ${pageDef.name || pageDef.id}:`));
|
|
8807
|
-
editIssues.forEach((issue) => console.log(
|
|
7538
|
+
editIssues.forEach((issue) => console.log(chalk11.dim(` [${issue.type}] ${issue.message}`)));
|
|
8808
7539
|
}
|
|
8809
7540
|
} else {
|
|
8810
|
-
console.log(
|
|
7541
|
+
console.log(chalk11.yellow(" \u26A0 AI provider does not support editPageCode"));
|
|
8811
7542
|
}
|
|
8812
7543
|
}
|
|
8813
7544
|
}
|
|
@@ -8865,8 +7596,8 @@ ${pagesCtx}`
|
|
|
8865
7596
|
const allFixes = [...postFixes, ...autoFixes];
|
|
8866
7597
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
8867
7598
|
if (allFixes.length > 0) {
|
|
8868
|
-
console.log(
|
|
8869
|
-
allFixes.forEach((f) => console.log(
|
|
7599
|
+
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7600
|
+
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8870
7601
|
}
|
|
8871
7602
|
await writeFile(absPath, codeToWrite);
|
|
8872
7603
|
const updatePageIdx = dsm.getConfig().pages.findIndex((p) => p.id === pageDef.id);
|
|
@@ -8902,15 +7633,15 @@ ${pagesCtx}`
|
|
|
8902
7633
|
const issues = validatePageQuality(codeToWrite, void 0, qualityPageType2);
|
|
8903
7634
|
const report = formatIssues(issues);
|
|
8904
7635
|
if (report) {
|
|
8905
|
-
console.log(
|
|
7636
|
+
console.log(chalk11.yellow(`
|
|
8906
7637
|
\u{1F50D} Quality check for ${pageDef.name || pageDef.id}:`));
|
|
8907
|
-
console.log(
|
|
7638
|
+
console.log(chalk11.dim(report));
|
|
8908
7639
|
}
|
|
8909
7640
|
const consistency = checkDesignConsistency(codeToWrite);
|
|
8910
7641
|
if (consistency.length > 0) {
|
|
8911
|
-
console.log(
|
|
7642
|
+
console.log(chalk11.yellow(`
|
|
8912
7643
|
\u{1F3A8} Design consistency for ${pageDef.name || pageDef.id}:`));
|
|
8913
|
-
consistency.forEach((w) => console.log(
|
|
7644
|
+
consistency.forEach((w) => console.log(chalk11.dim(` \u26A0 [${w.type}] ${w.message}`)));
|
|
8914
7645
|
}
|
|
8915
7646
|
} else {
|
|
8916
7647
|
try {
|
|
@@ -8925,8 +7656,8 @@ ${pagesCtx}`
|
|
|
8925
7656
|
if (fixes.length > 0) {
|
|
8926
7657
|
code = fixed;
|
|
8927
7658
|
await writeFile(absPath, code);
|
|
8928
|
-
console.log(
|
|
8929
|
-
fixes.forEach((f) => console.log(
|
|
7659
|
+
console.log(chalk11.dim(" \u{1F527} Auto-fixes applied:"));
|
|
7660
|
+
fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
8930
7661
|
}
|
|
8931
7662
|
const relFilePath = routeToRelPath(route, isAuth);
|
|
8932
7663
|
const manifest = await loadManifest6(projectRoot);
|
|
@@ -8945,9 +7676,9 @@ ${pagesCtx}`
|
|
|
8945
7676
|
const issues = validatePageQuality(code, void 0, pageTypeForQuality);
|
|
8946
7677
|
const report = formatIssues(issues);
|
|
8947
7678
|
if (report) {
|
|
8948
|
-
console.log(
|
|
7679
|
+
console.log(chalk11.yellow(`
|
|
8949
7680
|
\u{1F50D} Quality check for ${pageDef.name || pageDef.id}:`));
|
|
8950
|
-
console.log(
|
|
7681
|
+
console.log(chalk11.dim(report));
|
|
8951
7682
|
}
|
|
8952
7683
|
} catch {
|
|
8953
7684
|
}
|
|
@@ -9070,7 +7801,7 @@ function hasNavChanged(before, after) {
|
|
|
9070
7801
|
}
|
|
9071
7802
|
|
|
9072
7803
|
// src/commands/chat/interactive.ts
|
|
9073
|
-
import
|
|
7804
|
+
import chalk12 from "chalk";
|
|
9074
7805
|
import { resolve as resolve9 } from "path";
|
|
9075
7806
|
import { existsSync as existsSync16, readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
9076
7807
|
import { DesignSystemManager as DesignSystemManager6, ComponentManager as ComponentManager4, loadManifest as loadManifest7 } from "@getcoherent/core";
|
|
@@ -9088,7 +7819,7 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9088
7819
|
const validProviders = ["claude", "openai", "auto"];
|
|
9089
7820
|
const provider = (options.provider || "auto").toLowerCase();
|
|
9090
7821
|
if (!validProviders.includes(provider)) {
|
|
9091
|
-
console.error(
|
|
7822
|
+
console.error(chalk12.red(`
|
|
9092
7823
|
\u274C Invalid provider: ${options.provider}`));
|
|
9093
7824
|
process.exit(1);
|
|
9094
7825
|
}
|
|
@@ -9103,13 +7834,13 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9103
7834
|
} catch (e) {
|
|
9104
7835
|
if (DEBUG3) console.error("Failed to load REPL history:", e);
|
|
9105
7836
|
}
|
|
9106
|
-
console.log(
|
|
9107
|
-
console.log(
|
|
9108
|
-
console.log(
|
|
7837
|
+
console.log(chalk12.cyan("\n\u{1F3A8} Coherent Interactive Mode"));
|
|
7838
|
+
console.log(chalk12.dim(" Type your requests, or use built-in commands."));
|
|
7839
|
+
console.log(chalk12.dim(' Type "help" for available commands, "exit" to quit.\n'));
|
|
9109
7840
|
const rl = createInterface({
|
|
9110
7841
|
input: process.stdin,
|
|
9111
7842
|
output: process.stdout,
|
|
9112
|
-
prompt:
|
|
7843
|
+
prompt: chalk12.cyan("Coherent> "),
|
|
9113
7844
|
history,
|
|
9114
7845
|
historySize: 200
|
|
9115
7846
|
});
|
|
@@ -9123,37 +7854,45 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9123
7854
|
const lower = input.toLowerCase();
|
|
9124
7855
|
if (lower === "exit" || lower === "quit" || lower === "q") {
|
|
9125
7856
|
saveHistory();
|
|
9126
|
-
console.log(
|
|
7857
|
+
console.log(chalk12.dim("\nBye!\n"));
|
|
9127
7858
|
rl.close();
|
|
9128
7859
|
process.exit(0);
|
|
9129
7860
|
}
|
|
9130
7861
|
if (lower === "help") {
|
|
9131
|
-
console.log(
|
|
9132
|
-
console.log(
|
|
9133
|
-
console.log(
|
|
9134
|
-
console.log(
|
|
9135
|
-
console.log(
|
|
9136
|
-
console.log(
|
|
9137
|
-
console.log(
|
|
9138
|
-
console.log(
|
|
9139
|
-
console.log(
|
|
9140
|
-
console.log(
|
|
9141
|
-
console.log(
|
|
7862
|
+
console.log(chalk12.bold("\n Built-in commands:"));
|
|
7863
|
+
console.log(chalk12.white(" components") + chalk12.dim(" \u2014 list shared and UI components"));
|
|
7864
|
+
console.log(chalk12.white(" pages") + chalk12.dim(" \u2014 list all pages"));
|
|
7865
|
+
console.log(chalk12.white(" tokens") + chalk12.dim(" \u2014 show design tokens"));
|
|
7866
|
+
console.log(chalk12.white(" status") + chalk12.dim(" \u2014 project summary"));
|
|
7867
|
+
console.log(chalk12.white(" help") + chalk12.dim(" \u2014 this help"));
|
|
7868
|
+
console.log(chalk12.white(" exit") + chalk12.dim(" \u2014 quit interactive mode"));
|
|
7869
|
+
console.log(chalk12.bold("\n Target shortcuts:"));
|
|
7870
|
+
console.log(chalk12.white(" @ComponentName <msg>") + chalk12.dim(" \u2014 target a shared component"));
|
|
7871
|
+
console.log(chalk12.white(" @/route <msg>") + chalk12.dim(" \u2014 target a page by route"));
|
|
7872
|
+
console.log(chalk12.dim("\n Anything else is sent to AI as a modification request.\n"));
|
|
9142
7873
|
rl.prompt();
|
|
9143
7874
|
return;
|
|
9144
7875
|
}
|
|
9145
7876
|
if (lower === "components" || lower === "list components" || lower.includes("what components")) {
|
|
9146
7877
|
const manifest = await loadManifest7(projectRoot);
|
|
9147
7878
|
if (manifest.shared.length === 0) {
|
|
9148
|
-
console.log(
|
|
7879
|
+
console.log(chalk12.gray("\n No shared components yet.\n"));
|
|
9149
7880
|
} else {
|
|
9150
7881
|
console.log("");
|
|
9151
|
-
const order = {
|
|
7882
|
+
const order = {
|
|
7883
|
+
layout: 0,
|
|
7884
|
+
navigation: 1,
|
|
7885
|
+
"data-display": 2,
|
|
7886
|
+
form: 3,
|
|
7887
|
+
feedback: 4,
|
|
7888
|
+
section: 5,
|
|
7889
|
+
widget: 6
|
|
7890
|
+
};
|
|
9152
7891
|
const sorted = [...manifest.shared].sort((a, b) => (order[a.type] ?? 9) - (order[b.type] ?? 9));
|
|
9153
7892
|
sorted.forEach((entry) => {
|
|
9154
|
-
const usage = entry.usedIn.includes("app/layout.tsx") ?
|
|
7893
|
+
const usage = entry.usedIn.includes("app/layout.tsx") ? chalk12.green("all pages") : entry.usedIn.length > 0 ? chalk12.gray(entry.usedIn.join(", ")) : chalk12.gray("unused");
|
|
9155
7894
|
console.log(
|
|
9156
|
-
` ${
|
|
7895
|
+
` ${chalk12.cyan(entry.id.padEnd(8))} ${chalk12.white(entry.name.padEnd(18))} ${chalk12.gray(entry.type.padEnd(9))} ${usage}`
|
|
9157
7896
|
);
|
|
9158
7897
|
});
|
|
9159
7898
|
console.log("");
|
|
@@ -9164,11 +7903,11 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9164
7903
|
if (lower === "pages" || lower === "list pages" || lower.includes("what pages")) {
|
|
9165
7904
|
const currentConfig = dsm.getConfig();
|
|
9166
7905
|
if (currentConfig.pages.length === 0) {
|
|
9167
|
-
console.log(
|
|
7906
|
+
console.log(chalk12.gray("\n No pages yet.\n"));
|
|
9168
7907
|
} else {
|
|
9169
7908
|
console.log("");
|
|
9170
7909
|
currentConfig.pages.forEach((p) => {
|
|
9171
|
-
console.log(` ${
|
|
7910
|
+
console.log(` ${chalk12.white(p.name.padEnd(22))} ${chalk12.gray(p.route)}`);
|
|
9172
7911
|
});
|
|
9173
7912
|
console.log("");
|
|
9174
7913
|
}
|
|
@@ -9178,10 +7917,10 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9178
7917
|
if (lower === "status") {
|
|
9179
7918
|
const currentConfig = dsm.getConfig();
|
|
9180
7919
|
const manifest = await loadManifest7(projectRoot);
|
|
9181
|
-
console.log(
|
|
7920
|
+
console.log(chalk12.bold(`
|
|
9182
7921
|
${currentConfig.name || "Coherent Project"}`));
|
|
9183
7922
|
console.log(
|
|
9184
|
-
|
|
7923
|
+
chalk12.dim(
|
|
9185
7924
|
` Pages: ${currentConfig.pages.length} | Shared components: ${manifest.shared.length} | UI components: ${cm.getAllComponents().length}
|
|
9186
7925
|
`
|
|
9187
7926
|
)
|
|
@@ -9192,21 +7931,21 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9192
7931
|
if (lower === "tokens" || lower === "show tokens" || lower === "design tokens") {
|
|
9193
7932
|
const currentConfig = dsm.getConfig();
|
|
9194
7933
|
const t = currentConfig.tokens;
|
|
9195
|
-
console.log(
|
|
9196
|
-
console.log(
|
|
7934
|
+
console.log(chalk12.bold("\n Design Tokens\n"));
|
|
7935
|
+
console.log(chalk12.cyan(" Colors (light)"));
|
|
9197
7936
|
for (const [k, v] of Object.entries(t.colors.light)) {
|
|
9198
|
-
console.log(` ${
|
|
7937
|
+
console.log(` ${chalk12.white(k.padEnd(14))} ${chalk12.gray(v)}`);
|
|
9199
7938
|
}
|
|
9200
|
-
console.log(
|
|
9201
|
-
console.log(` ${
|
|
9202
|
-
console.log(` ${
|
|
9203
|
-
console.log(
|
|
7939
|
+
console.log(chalk12.cyan("\n Typography"));
|
|
7940
|
+
console.log(` ${chalk12.white("sans".padEnd(14))} ${chalk12.gray(t.typography.fontFamily.sans)}`);
|
|
7941
|
+
console.log(` ${chalk12.white("mono".padEnd(14))} ${chalk12.gray(t.typography.fontFamily.mono)}`);
|
|
7942
|
+
console.log(chalk12.cyan("\n Spacing"));
|
|
9204
7943
|
for (const [k, v] of Object.entries(t.spacing)) {
|
|
9205
|
-
console.log(` ${
|
|
7944
|
+
console.log(` ${chalk12.white(k.padEnd(14))} ${chalk12.gray(v)}`);
|
|
9206
7945
|
}
|
|
9207
|
-
console.log(
|
|
7946
|
+
console.log(chalk12.cyan("\n Radius"));
|
|
9208
7947
|
for (const [k, v] of Object.entries(t.radius)) {
|
|
9209
|
-
console.log(` ${
|
|
7948
|
+
console.log(` ${chalk12.white(k.padEnd(14))} ${chalk12.gray(v)}`);
|
|
9210
7949
|
}
|
|
9211
7950
|
console.log("");
|
|
9212
7951
|
rl.prompt();
|
|
@@ -9228,7 +7967,7 @@ async function interactiveChat(options, chatCommandFn) {
|
|
|
9228
7967
|
await dsm.load();
|
|
9229
7968
|
} catch (err) {
|
|
9230
7969
|
if (!err._printed) {
|
|
9231
|
-
console.error(
|
|
7970
|
+
console.error(chalk12.red(`
|
|
9232
7971
|
Error: ${err.message}
|
|
9233
7972
|
`));
|
|
9234
7973
|
}
|
|
@@ -9260,8 +7999,8 @@ async function chatCommand(message, options) {
|
|
|
9260
7999
|
process.exit(1);
|
|
9261
8000
|
};
|
|
9262
8001
|
if (!message) {
|
|
9263
|
-
console.error(
|
|
9264
|
-
console.log(
|
|
8002
|
+
console.error(chalk13.red('\n\u274C No message provided. Use: coherent chat "your request"\n'));
|
|
8003
|
+
console.log(chalk13.dim(" Or use interactive mode: coherent chat -i\n"));
|
|
9265
8004
|
bail("No message provided");
|
|
9266
8005
|
}
|
|
9267
8006
|
const spinner = ora2("Processing your request...").start();
|
|
@@ -9271,7 +8010,7 @@ async function chatCommand(message, options) {
|
|
|
9271
8010
|
const migrationGuard = join11(projectRoot, ".coherent", "migration-in-progress");
|
|
9272
8011
|
if (existsSync17(migrationGuard)) {
|
|
9273
8012
|
spinner.fail("Migration in progress");
|
|
9274
|
-
console.error(
|
|
8013
|
+
console.error(chalk13.red("\n\u274C A migration is in progress. Run `coherent migrate --rollback` to undo first."));
|
|
9275
8014
|
bail("Migration in progress");
|
|
9276
8015
|
}
|
|
9277
8016
|
const validProviders = ["claude", "openai", "auto"];
|
|
@@ -9281,20 +8020,20 @@ async function chatCommand(message, options) {
|
|
|
9281
8020
|
releaseLock = await acquireProjectLock(projectRoot);
|
|
9282
8021
|
if (!validProviders.includes(provider)) {
|
|
9283
8022
|
spinner.fail("Invalid provider");
|
|
9284
|
-
console.error(
|
|
8023
|
+
console.error(chalk13.red(`
|
|
9285
8024
|
\u274C Invalid provider: ${options.provider}`));
|
|
9286
|
-
console.log(
|
|
8025
|
+
console.log(chalk13.dim(`Valid options: ${validProviders.join(", ")}`));
|
|
9287
8026
|
bail(`Invalid provider: ${options.provider}`);
|
|
9288
8027
|
}
|
|
9289
8028
|
spinner.text = "Loading design system configuration...";
|
|
9290
8029
|
const config2 = await loadConfig(configPath);
|
|
9291
8030
|
if (config2.coherentVersion && config2.coherentVersion !== CLI_VERSION2) {
|
|
9292
8031
|
spinner.stop();
|
|
9293
|
-
console.log(
|
|
9294
|
-
console.log(
|
|
9295
|
-
console.log(
|
|
9296
|
-
console.log(
|
|
9297
|
-
console.log(
|
|
8032
|
+
console.log(chalk13.yellow("\n\u26A0\uFE0F Version mismatch detected\n"));
|
|
8033
|
+
console.log(chalk13.gray(" Project created with: ") + chalk13.white(`v${config2.coherentVersion}`));
|
|
8034
|
+
console.log(chalk13.gray(" Current CLI version: ") + chalk13.white(`v${CLI_VERSION2}`));
|
|
8035
|
+
console.log(chalk13.cyan("\n \u{1F4A1} Run `coherent update` to apply latest changes to your project.\n"));
|
|
8036
|
+
console.log(chalk13.dim(" Continuing anyway...\n"));
|
|
9298
8037
|
spinner.start("Loading design system configuration...");
|
|
9299
8038
|
}
|
|
9300
8039
|
if (needsGlobalsFix(projectRoot)) {
|
|
@@ -9314,6 +8053,64 @@ async function chatCommand(message, options) {
|
|
|
9314
8053
|
const pm = new PageManager3(config2, cm);
|
|
9315
8054
|
spinner.succeed("Configuration loaded");
|
|
9316
8055
|
message = await resolveTargetFlags(message, options, config2, projectRoot);
|
|
8056
|
+
if (options.newComponent) {
|
|
8057
|
+
const componentName = options.newComponent;
|
|
8058
|
+
spinner.start(`Creating shared component: ${componentName}...`);
|
|
8059
|
+
const { createAIProvider: createAIProvider2 } = await import("./ai-provider-HUQO64P3.js");
|
|
8060
|
+
const { generateSharedComponent: generateSharedComponent7 } = await import("@getcoherent/core");
|
|
8061
|
+
const { autoFixCode: autoFixCode2 } = await import("./quality-validator-3K5BMJSR.js");
|
|
8062
|
+
const { extractPropsInterface, extractDependencies } = await import("./component-extractor-VYJLT5NR.js");
|
|
8063
|
+
const aiProvider = await createAIProvider2(provider ?? "auto");
|
|
8064
|
+
const prompt = `Generate a React component called "${componentName}". Description: ${message}.
|
|
8065
|
+
Use shadcn/ui components and Tailwind CSS semantic tokens. Export the component as a named export.
|
|
8066
|
+
Include a TypeScript props interface.
|
|
8067
|
+
Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${componentName}", "pageCode": "...full TSX..." } }] }`;
|
|
8068
|
+
const raw = await aiProvider.parseModification(prompt);
|
|
8069
|
+
const requests2 = Array.isArray(raw) ? raw : raw?.requests ?? [];
|
|
8070
|
+
const codeMatch = requests2.find(
|
|
8071
|
+
(r) => r.changes?.pageCode
|
|
8072
|
+
);
|
|
8073
|
+
const rawCode = codeMatch?.changes?.pageCode || "";
|
|
8074
|
+
if (!rawCode) {
|
|
8075
|
+
spinner.fail(`Could not generate component ${componentName}`);
|
|
8076
|
+
releaseLock?.();
|
|
8077
|
+
return;
|
|
8078
|
+
}
|
|
8079
|
+
const { code: fixedCode } = await autoFixCode2(rawCode);
|
|
8080
|
+
const props = extractPropsInterface(fixedCode);
|
|
8081
|
+
const deps = extractDependencies(fixedCode);
|
|
8082
|
+
const componentType = options.type || "section";
|
|
8083
|
+
const genResult = await generateSharedComponent7(projectRoot, {
|
|
8084
|
+
name: componentName,
|
|
8085
|
+
type: componentType,
|
|
8086
|
+
code: fixedCode,
|
|
8087
|
+
description: message,
|
|
8088
|
+
propsInterface: props ?? void 0,
|
|
8089
|
+
dependencies: deps,
|
|
8090
|
+
source: "manual"
|
|
8091
|
+
});
|
|
8092
|
+
if (!options.type) {
|
|
8093
|
+
try {
|
|
8094
|
+
const { classifyComponents } = await import("./ai-classifier-EGXPFJLN.js");
|
|
8095
|
+
const classifications = await classifyComponents(
|
|
8096
|
+
[{ name: componentName, signature: props || componentName }],
|
|
8097
|
+
async (p) => JSON.stringify(await aiProvider.generateJSON("You are a component classifier.", p))
|
|
8098
|
+
);
|
|
8099
|
+
if (classifications.length > 0) {
|
|
8100
|
+
let manifest2 = await loadManifest8(projectRoot);
|
|
8101
|
+
manifest2 = updateEntry(manifest2, genResult.id, {
|
|
8102
|
+
type: classifications[0].type,
|
|
8103
|
+
description: classifications[0].description || message
|
|
8104
|
+
});
|
|
8105
|
+
await saveManifest2(projectRoot, manifest2);
|
|
8106
|
+
}
|
|
8107
|
+
} catch {
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
spinner.succeed(`Created ${genResult.name} (${genResult.id}) at ${genResult.file}`);
|
|
8111
|
+
releaseLock?.();
|
|
8112
|
+
return;
|
|
8113
|
+
}
|
|
9317
8114
|
const isPageGenRequest = /\bpages?\s*[:)]/i.test(message) || /\bcreate\b.*\bpage/i.test(message) || message.length > 200;
|
|
9318
8115
|
if (!isPageGenRequest) {
|
|
9319
8116
|
if (/switch to dark mode|default to dark|make.*dark.*(default|theme)|dark theme/i.test(message)) {
|
|
@@ -9321,9 +8118,9 @@ async function chatCommand(message, options) {
|
|
|
9321
8118
|
const done = await setDefaultDarkTheme(projectRoot);
|
|
9322
8119
|
spinner.stop();
|
|
9323
8120
|
if (done) {
|
|
9324
|
-
console.log(
|
|
8121
|
+
console.log(chalk13.green("\n\u2705 Default theme set to dark. Reload the app to see changes.\n"));
|
|
9325
8122
|
} else {
|
|
9326
|
-
console.log(
|
|
8123
|
+
console.log(chalk13.yellow("\n\u26A0\uFE0F Could not update layout (app/layout.tsx not found).\n"));
|
|
9327
8124
|
}
|
|
9328
8125
|
return;
|
|
9329
8126
|
}
|
|
@@ -9339,10 +8136,10 @@ async function chatCommand(message, options) {
|
|
|
9339
8136
|
dsm.updateConfig(cfg);
|
|
9340
8137
|
dsm.save();
|
|
9341
8138
|
spinner.stop();
|
|
9342
|
-
console.log(
|
|
8139
|
+
console.log(chalk13.green("\n\u2705 Default theme set to light. Reload the app to see changes.\n"));
|
|
9343
8140
|
} catch {
|
|
9344
8141
|
spinner.stop();
|
|
9345
|
-
console.log(
|
|
8142
|
+
console.log(chalk13.yellow("\n\u26A0\uFE0F Could not update layout (app/layout.tsx not found).\n"));
|
|
9346
8143
|
}
|
|
9347
8144
|
return;
|
|
9348
8145
|
}
|
|
@@ -9352,7 +8149,7 @@ async function chatCommand(message, options) {
|
|
|
9352
8149
|
const { created, id } = await ensureThemeToggle(projectRoot);
|
|
9353
8150
|
spinner.stop();
|
|
9354
8151
|
console.log(
|
|
9355
|
-
|
|
8152
|
+
chalk13.green(
|
|
9356
8153
|
`
|
|
9357
8154
|
\u2705 ${created ? `Created ${id} (ThemeToggle) and added to layout` : "ThemeToggle already present; layout updated"}.
|
|
9358
8155
|
`
|
|
@@ -9360,7 +8157,7 @@ async function chatCommand(message, options) {
|
|
|
9360
8157
|
);
|
|
9361
8158
|
} catch (e) {
|
|
9362
8159
|
spinner.fail("Failed to add theme toggle");
|
|
9363
|
-
if (e instanceof Error) console.error(
|
|
8160
|
+
if (e instanceof Error) console.error(chalk13.red("\n\u274C " + e.message + "\n"));
|
|
9364
8161
|
}
|
|
9365
8162
|
return;
|
|
9366
8163
|
}
|
|
@@ -9376,12 +8173,12 @@ async function chatCommand(message, options) {
|
|
|
9376
8173
|
manifest = { ...manifest, shared: validShared };
|
|
9377
8174
|
await saveManifest2(project.root, manifest);
|
|
9378
8175
|
if (DEBUG4) {
|
|
9379
|
-
console.log(
|
|
8176
|
+
console.log(chalk13.dim(`[pre-gen] Cleaned ${cleaned} orphaned component(s) from manifest`));
|
|
9380
8177
|
}
|
|
9381
8178
|
}
|
|
9382
8179
|
const sharedComponentsSummary = buildSharedComponentsSummary(manifest);
|
|
9383
8180
|
if (DEBUG4 && sharedComponentsSummary) {
|
|
9384
|
-
console.log(
|
|
8181
|
+
console.log(chalk13.dim("[add-page] sharedComponentsSummary in prompt:\n" + sharedComponentsSummary));
|
|
9385
8182
|
}
|
|
9386
8183
|
let requests;
|
|
9387
8184
|
let uxRecommendations;
|
|
@@ -9488,10 +8285,10 @@ async function chatCommand(message, options) {
|
|
|
9488
8285
|
}
|
|
9489
8286
|
if (requests.length === 0) {
|
|
9490
8287
|
spinner.fail("No modifications found in your request");
|
|
9491
|
-
console.log(
|
|
9492
|
-
console.log(
|
|
9493
|
-
console.log(
|
|
9494
|
-
console.log(
|
|
8288
|
+
console.log(chalk13.yellow("\n\u{1F4A1} Try being more specific, e.g.:"));
|
|
8289
|
+
console.log(chalk13.dim(' - "make buttons blue"'));
|
|
8290
|
+
console.log(chalk13.dim(' - "add a pricing page"'));
|
|
8291
|
+
console.log(chalk13.dim(' - "change primary color to green"'));
|
|
9495
8292
|
return;
|
|
9496
8293
|
}
|
|
9497
8294
|
spinner.succeed(`Parsed ${requests.length} modification(s)`);
|
|
@@ -9499,11 +8296,11 @@ async function chatCommand(message, options) {
|
|
|
9499
8296
|
normalizedRequests = normalizedRequests.map((req) => {
|
|
9500
8297
|
const result = normalizeRequest(req, dsm.getConfig());
|
|
9501
8298
|
if ("error" in result) {
|
|
9502
|
-
console.log(
|
|
8299
|
+
console.log(chalk13.yellow(` \u26A0 Skipped: ${result.error}`));
|
|
9503
8300
|
return null;
|
|
9504
8301
|
}
|
|
9505
8302
|
if (result.type !== req.type) {
|
|
9506
|
-
console.log(
|
|
8303
|
+
console.log(chalk13.dim(` \u2139 Adjusted: ${req.type} \u2192 ${result.type} (target: ${req.target})`));
|
|
9507
8304
|
}
|
|
9508
8305
|
return result;
|
|
9509
8306
|
}).filter((r) => r !== null);
|
|
@@ -9559,13 +8356,13 @@ async function chatCommand(message, options) {
|
|
|
9559
8356
|
}
|
|
9560
8357
|
}
|
|
9561
8358
|
if (DEBUG4) {
|
|
9562
|
-
console.log(
|
|
8359
|
+
console.log(chalk13.gray(`
|
|
9563
8360
|
[DEBUG] Pre-flight analysis for page "${page.name || page.route}": `));
|
|
9564
|
-
console.log(
|
|
8361
|
+
console.log(chalk13.gray(` Page sections: ${page.sections?.length || 0}`));
|
|
9565
8362
|
if (page.sections?.[0]?.props?.fields) {
|
|
9566
|
-
console.log(
|
|
8363
|
+
console.log(chalk13.gray(` First section has ${page.sections[0].props.fields.length} fields`));
|
|
9567
8364
|
page.sections[0].props.fields.forEach((f, i) => {
|
|
9568
|
-
console.log(
|
|
8365
|
+
console.log(chalk13.gray(` Field ${i}: component=${f.component}`));
|
|
9569
8366
|
});
|
|
9570
8367
|
}
|
|
9571
8368
|
}
|
|
@@ -9588,8 +8385,8 @@ async function chatCommand(message, options) {
|
|
|
9588
8385
|
const INVALID_COMPONENT_IDS = /* @__PURE__ */ new Set(["ui", "shared", "lib", "utils", "hooks", "app", "components"]);
|
|
9589
8386
|
for (const id of INVALID_COMPONENT_IDS) allNeededComponentIds.delete(id);
|
|
9590
8387
|
if (DEBUG4) {
|
|
9591
|
-
console.log(
|
|
9592
|
-
console.log(
|
|
8388
|
+
console.log(chalk13.gray("\n[DEBUG] Pre-flight analysis (consolidated):"));
|
|
8389
|
+
console.log(chalk13.gray(` All needed components: ${Array.from(allNeededComponentIds).join(", ")}`));
|
|
9593
8390
|
console.log("");
|
|
9594
8391
|
}
|
|
9595
8392
|
const missingComponents = [];
|
|
@@ -9597,59 +8394,59 @@ async function chatCommand(message, options) {
|
|
|
9597
8394
|
const isRegistered = !!cm.read(componentId);
|
|
9598
8395
|
const filePath = join11(projectRoot, "components", "ui", `${componentId}.tsx`);
|
|
9599
8396
|
const fileExists = existsSync17(filePath);
|
|
9600
|
-
if (DEBUG4) console.log(
|
|
8397
|
+
if (DEBUG4) console.log(chalk13.gray(` Checking ${componentId}: registered=${isRegistered} file=${fileExists}`));
|
|
9601
8398
|
if (!isRegistered || !fileExists) {
|
|
9602
8399
|
missingComponents.push(componentId);
|
|
9603
8400
|
}
|
|
9604
8401
|
}
|
|
9605
8402
|
if (missingComponents.length > 0) {
|
|
9606
8403
|
spinner.stop();
|
|
9607
|
-
console.log(
|
|
8404
|
+
console.log(chalk13.cyan("\n\u{1F50D} Pre-flight check: Installing missing components...\n"));
|
|
9608
8405
|
const provider2 = getComponentProvider();
|
|
9609
8406
|
for (const componentId of missingComponents) {
|
|
9610
8407
|
if (DEBUG4) {
|
|
9611
|
-
console.log(
|
|
9612
|
-
console.log(
|
|
8408
|
+
console.log(chalk13.gray(` [DEBUG] Trying to install: ${componentId}`));
|
|
8409
|
+
console.log(chalk13.gray(` [DEBUG] provider.has(${componentId}): ${provider2.has(componentId)}`));
|
|
9613
8410
|
}
|
|
9614
8411
|
if (provider2.has(componentId)) {
|
|
9615
8412
|
try {
|
|
9616
8413
|
const result = await provider2.installComponent(componentId, projectRoot);
|
|
9617
|
-
if (DEBUG4) console.log(
|
|
8414
|
+
if (DEBUG4) console.log(chalk13.gray(` [DEBUG] installComponent result: ${result.success}`));
|
|
9618
8415
|
if (result.success && result.componentDef) {
|
|
9619
8416
|
if (!cm.read(componentId)) {
|
|
9620
8417
|
if (DEBUG4)
|
|
9621
8418
|
console.log(
|
|
9622
|
-
|
|
8419
|
+
chalk13.gray(` [DEBUG] Registering ${result.componentDef.id} (${result.componentDef.name})`)
|
|
9623
8420
|
);
|
|
9624
8421
|
const regResult = await cm.register(result.componentDef);
|
|
9625
8422
|
if (DEBUG4) {
|
|
9626
8423
|
console.log(
|
|
9627
|
-
|
|
8424
|
+
chalk13.gray(
|
|
9628
8425
|
` [DEBUG] Register result: ${regResult.success ? "SUCCESS" : "FAILED"}${!regResult.success && regResult.message ? ` - ${regResult.message}` : ""}`
|
|
9629
8426
|
)
|
|
9630
8427
|
);
|
|
9631
8428
|
}
|
|
9632
8429
|
if (regResult.success) {
|
|
9633
8430
|
preflightInstalledIds.push(result.componentDef.id);
|
|
9634
|
-
console.log(
|
|
8431
|
+
console.log(chalk13.green(` \u2728 Auto-installed ${result.componentDef.name} component`));
|
|
9635
8432
|
dsm.updateConfig(regResult.config);
|
|
9636
8433
|
cm.updateConfig(regResult.config);
|
|
9637
8434
|
pm.updateConfig(regResult.config);
|
|
9638
8435
|
}
|
|
9639
8436
|
} else {
|
|
9640
8437
|
preflightInstalledIds.push(result.componentDef.id);
|
|
9641
|
-
console.log(
|
|
8438
|
+
console.log(chalk13.green(` \u2728 Re-installed ${result.componentDef.name} component (file was missing)`));
|
|
9642
8439
|
}
|
|
9643
8440
|
}
|
|
9644
8441
|
} catch (error) {
|
|
9645
|
-
console.log(
|
|
9646
|
-
console.log(
|
|
8442
|
+
console.log(chalk13.red(` \u274C Failed to install ${componentId}:`));
|
|
8443
|
+
console.log(chalk13.red(` ${error instanceof Error ? error.message : error}`));
|
|
9647
8444
|
if (error instanceof Error && error.stack) {
|
|
9648
|
-
console.log(
|
|
8445
|
+
console.log(chalk13.gray(` ${error.stack.split("\n")[1]}`));
|
|
9649
8446
|
}
|
|
9650
8447
|
}
|
|
9651
8448
|
} else {
|
|
9652
|
-
console.log(
|
|
8449
|
+
console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
|
|
9653
8450
|
}
|
|
9654
8451
|
}
|
|
9655
8452
|
console.log("");
|
|
@@ -9660,11 +8457,11 @@ async function chatCommand(message, options) {
|
|
|
9660
8457
|
const toInstallNpm = [...neededPkgs].filter((p) => !installedPkgs.has(p));
|
|
9661
8458
|
if (toInstallNpm.length > 0) {
|
|
9662
8459
|
spinner.stop();
|
|
9663
|
-
console.log(
|
|
8460
|
+
console.log(chalk13.cyan(`
|
|
9664
8461
|
\u{1F4E6} Auto-installing missing dependencies: ${toInstallNpm.join(", ")}
|
|
9665
8462
|
`));
|
|
9666
8463
|
const ok = await installPackages(projectRoot, toInstallNpm);
|
|
9667
|
-
if (!ok) console.log(
|
|
8464
|
+
if (!ok) console.log(chalk13.yellow(` Run manually: npm install ${toInstallNpm.join(" ")}
|
|
9668
8465
|
`));
|
|
9669
8466
|
spinner.start("Applying modifications...");
|
|
9670
8467
|
}
|
|
@@ -9675,7 +8472,7 @@ async function chatCommand(message, options) {
|
|
|
9675
8472
|
if (componentId && preflightComponentIds.has(componentId)) {
|
|
9676
8473
|
if (DEBUG4) {
|
|
9677
8474
|
console.log(
|
|
9678
|
-
|
|
8475
|
+
chalk13.gray(`[DEBUG] Filtered duplicate add-component: ${componentId} (already installed in pre-flight)`)
|
|
9679
8476
|
);
|
|
9680
8477
|
}
|
|
9681
8478
|
return false;
|
|
@@ -9684,11 +8481,11 @@ async function chatCommand(message, options) {
|
|
|
9684
8481
|
return true;
|
|
9685
8482
|
});
|
|
9686
8483
|
if (DEBUG4 && preflightComponentIds.size > 0) {
|
|
9687
|
-
console.log(
|
|
8484
|
+
console.log(chalk13.gray(`[DEBUG] Remaining requests after filtering: ${normalizedRequests.length}`));
|
|
9688
8485
|
}
|
|
9689
8486
|
try {
|
|
9690
8487
|
createBackup(projectRoot);
|
|
9691
|
-
if (DEBUG4) console.log(
|
|
8488
|
+
if (DEBUG4) console.log(chalk13.dim("[backup] Created snapshot"));
|
|
9692
8489
|
} catch {
|
|
9693
8490
|
}
|
|
9694
8491
|
const navBefore = takeNavSnapshot(
|
|
@@ -9711,6 +8508,26 @@ async function chatCommand(message, options) {
|
|
|
9711
8508
|
break;
|
|
9712
8509
|
}
|
|
9713
8510
|
}
|
|
8511
|
+
try {
|
|
8512
|
+
const { validateReuse } = await import("./reuse-validator-HC4LZEKF.js");
|
|
8513
|
+
const { inferPageTypeFromRoute: inferPageTypeFromRoute2 } = await import("./design-constraints-EIP2XM7T.js");
|
|
8514
|
+
const manifest2 = await loadManifest8(projectRoot);
|
|
8515
|
+
if (manifest2.shared.length > 0) {
|
|
8516
|
+
for (const request of normalizedRequests) {
|
|
8517
|
+
if (request.type !== "add-page") continue;
|
|
8518
|
+
const changes = request.changes;
|
|
8519
|
+
const pageCode = changes?.pageCode;
|
|
8520
|
+
if (!pageCode) continue;
|
|
8521
|
+
const route = changes.route || "";
|
|
8522
|
+
const pageType = inferPageTypeFromRoute2(route);
|
|
8523
|
+
const warnings = validateReuse(manifest2, pageCode, pageType);
|
|
8524
|
+
for (const w of warnings) {
|
|
8525
|
+
console.log(chalk13.yellow(` \u26A0 ${w.message}`));
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
} catch {
|
|
8530
|
+
}
|
|
9714
8531
|
const currentConfig = dsm.getConfig();
|
|
9715
8532
|
const autoScaffoldEnabled = currentConfig.settings.autoScaffold === true;
|
|
9716
8533
|
const scaffoldedPages = [];
|
|
@@ -9751,10 +8568,10 @@ async function chatCommand(message, options) {
|
|
|
9751
8568
|
const SCAFFOLD_AI_LIMIT = 10;
|
|
9752
8569
|
if (missingRoutes.length > 0 && missingRoutes.length <= SCAFFOLD_AI_LIMIT) {
|
|
9753
8570
|
spinner.stop();
|
|
9754
|
-
console.log(
|
|
8571
|
+
console.log(chalk13.cyan(`
|
|
9755
8572
|
\u{1F517} Auto-scaffolding ${missingRoutes.length} linked page(s)...`));
|
|
9756
8573
|
console.log(
|
|
9757
|
-
|
|
8574
|
+
chalk13.dim(
|
|
9758
8575
|
` (${missingRoutes.length} additional AI call(s) \u2014 disable with settings.autoScaffold: false in config)
|
|
9759
8576
|
`
|
|
9760
8577
|
)
|
|
@@ -9791,7 +8608,7 @@ async function chatCommand(message, options) {
|
|
|
9791
8608
|
}
|
|
9792
8609
|
} catch (err) {
|
|
9793
8610
|
scaffoldSpinner.warn(` Could not scaffold "${pageName}" (${linkedRoute}) \u2014 skipped`);
|
|
9794
|
-
if (DEBUG4) console.log(
|
|
8611
|
+
if (DEBUG4) console.log(chalk13.dim(` ${err instanceof Error ? err.message : "unknown error"}`));
|
|
9795
8612
|
}
|
|
9796
8613
|
}
|
|
9797
8614
|
console.log("");
|
|
@@ -9799,7 +8616,7 @@ async function chatCommand(message, options) {
|
|
|
9799
8616
|
} else if (missingRoutes.length > SCAFFOLD_AI_LIMIT) {
|
|
9800
8617
|
spinner.stop();
|
|
9801
8618
|
console.log(
|
|
9802
|
-
|
|
8619
|
+
chalk13.yellow(
|
|
9803
8620
|
`
|
|
9804
8621
|
\u26A0 Found ${missingRoutes.length} linked pages \u2014 creating placeholder pages (too many for AI generation).`
|
|
9805
8622
|
)
|
|
@@ -9828,7 +8645,7 @@ async function chatCommand(message, options) {
|
|
|
9828
8645
|
scaffoldedPages.push({ route: linkedRoute, name: `${pageName} (placeholder)` });
|
|
9829
8646
|
}
|
|
9830
8647
|
console.log(
|
|
9831
|
-
|
|
8648
|
+
chalk13.cyan(` Created ${missingRoutes.length} placeholder pages. Use \`coherent chat\` to fill them.
|
|
9832
8649
|
`)
|
|
9833
8650
|
);
|
|
9834
8651
|
spinner.start("Finalizing...");
|
|
@@ -9856,9 +8673,9 @@ async function chatCommand(message, options) {
|
|
|
9856
8673
|
}
|
|
9857
8674
|
}
|
|
9858
8675
|
if (linkIssues.length > 0) {
|
|
9859
|
-
console.log(
|
|
8676
|
+
console.log(chalk13.yellow("\n\u{1F517} Broken internal links:"));
|
|
9860
8677
|
for (const { page, message: message2 } of linkIssues) {
|
|
9861
|
-
console.log(
|
|
8678
|
+
console.log(chalk13.dim(` ${page}: ${message2}`));
|
|
9862
8679
|
}
|
|
9863
8680
|
}
|
|
9864
8681
|
}
|
|
@@ -9871,7 +8688,7 @@ async function chatCommand(message, options) {
|
|
|
9871
8688
|
if (latestConfig.theme.defaultMode !== targetMode) {
|
|
9872
8689
|
latestConfig.theme.defaultMode = targetMode;
|
|
9873
8690
|
dsm.updateConfig(latestConfig);
|
|
9874
|
-
if (DEBUG4) console.log(
|
|
8691
|
+
if (DEBUG4) console.log(chalk13.dim(` [theme] Set defaultMode to "${targetMode}"`));
|
|
9875
8692
|
}
|
|
9876
8693
|
const layoutPath = resolve10(projectRoot, "app", "layout.tsx");
|
|
9877
8694
|
try {
|
|
@@ -9879,11 +8696,11 @@ async function chatCommand(message, options) {
|
|
|
9879
8696
|
if (targetMode === "dark" && !layoutCode.includes('className="dark"')) {
|
|
9880
8697
|
layoutCode = layoutCode.replace(/<html\s+lang="en"/, '<html lang="en" className="dark"');
|
|
9881
8698
|
await writeFile(layoutPath, layoutCode);
|
|
9882
|
-
console.log(
|
|
8699
|
+
console.log(chalk13.dim(` \u{1F319} Applied dark theme to layout`));
|
|
9883
8700
|
} else if (targetMode === "light" && layoutCode.includes('className="dark"')) {
|
|
9884
8701
|
layoutCode = layoutCode.replace(' className="dark"', "");
|
|
9885
8702
|
await writeFile(layoutPath, layoutCode);
|
|
9886
|
-
console.log(
|
|
8703
|
+
console.log(chalk13.dim(` \u2600\uFE0F Applied light theme to layout`));
|
|
9887
8704
|
}
|
|
9888
8705
|
} catch {
|
|
9889
8706
|
}
|
|
@@ -9915,7 +8732,7 @@ async function chatCommand(message, options) {
|
|
|
9915
8732
|
}
|
|
9916
8733
|
const finalDeps = await scanAndInstallSharedDeps(projectRoot);
|
|
9917
8734
|
if (finalDeps.length > 0) {
|
|
9918
|
-
console.log(
|
|
8735
|
+
console.log(chalk13.dim(` Auto-installed shared deps: ${finalDeps.join(", ")}`));
|
|
9919
8736
|
}
|
|
9920
8737
|
try {
|
|
9921
8738
|
fixGlobalsCss(projectRoot, updatedConfig);
|
|
@@ -9939,7 +8756,53 @@ async function chatCommand(message, options) {
|
|
|
9939
8756
|
}
|
|
9940
8757
|
await saveHashes(projectRoot, updatedHashes);
|
|
9941
8758
|
} catch {
|
|
9942
|
-
if (DEBUG4) console.log(
|
|
8759
|
+
if (DEBUG4) console.log(chalk13.dim("[hashes] Could not save file hashes"));
|
|
8760
|
+
}
|
|
8761
|
+
try {
|
|
8762
|
+
const { extractPropsInterface, extractDependencies, extractUsageExample } = await import("./component-extractor-VYJLT5NR.js");
|
|
8763
|
+
let currentManifest = await loadManifest8(projectRoot);
|
|
8764
|
+
let manifestChanged = false;
|
|
8765
|
+
for (const entry of currentManifest.shared) {
|
|
8766
|
+
const fullPath = resolve10(projectRoot, entry.file);
|
|
8767
|
+
if (!existsSync17(fullPath)) continue;
|
|
8768
|
+
const code = readFileSync12(fullPath, "utf-8");
|
|
8769
|
+
const props = extractPropsInterface(code);
|
|
8770
|
+
const deps = extractDependencies(code);
|
|
8771
|
+
if (props && props !== entry.propsInterface || deps.length !== (entry.dependencies?.length ?? 0)) {
|
|
8772
|
+
currentManifest = updateEntry(currentManifest, entry.id, {
|
|
8773
|
+
propsInterface: props ?? entry.propsInterface,
|
|
8774
|
+
dependencies: deps
|
|
8775
|
+
});
|
|
8776
|
+
manifestChanged = true;
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
const pageFiles = Array.from(allModified).filter((f) => f.startsWith("app/") && f.endsWith("page.tsx"));
|
|
8780
|
+
for (const pageFile of pageFiles) {
|
|
8781
|
+
const fullPath = resolve10(projectRoot, pageFile);
|
|
8782
|
+
if (!existsSync17(fullPath)) continue;
|
|
8783
|
+
const pageCode = readFileSync12(fullPath, "utf-8");
|
|
8784
|
+
for (const entry of currentManifest.shared) {
|
|
8785
|
+
const isUsed = pageCode.includes(`from '@/components/shared/`) && (pageCode.includes(`{ ${entry.name} }`) || pageCode.includes(`{ ${entry.name},`));
|
|
8786
|
+
if (isUsed && !entry.usedIn.includes(pageFile)) {
|
|
8787
|
+
currentManifest = updateEntry(currentManifest, entry.id, {
|
|
8788
|
+
usedIn: [...entry.usedIn, pageFile]
|
|
8789
|
+
});
|
|
8790
|
+
manifestChanged = true;
|
|
8791
|
+
if (!entry.usageExample) {
|
|
8792
|
+
const usage = extractUsageExample(pageCode, entry.name);
|
|
8793
|
+
if (usage) {
|
|
8794
|
+
currentManifest = updateEntry(currentManifest, entry.id, { usageExample: usage });
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
}
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
if (manifestChanged) {
|
|
8801
|
+
await saveManifest2(projectRoot, currentManifest);
|
|
8802
|
+
if (DEBUG4) console.log(chalk13.dim("[auto-sync] Manifest updated"));
|
|
8803
|
+
}
|
|
8804
|
+
} catch {
|
|
8805
|
+
if (DEBUG4) console.log(chalk13.dim("[auto-sync] Skipped"));
|
|
9943
8806
|
}
|
|
9944
8807
|
const successfulPairs = normalizedRequests.map((request, index) => ({ request, result: results[index] })).filter(({ result }) => result.success);
|
|
9945
8808
|
if (successfulPairs.length > 0) {
|
|
@@ -9957,9 +8820,9 @@ async function chatCommand(message, options) {
|
|
|
9957
8820
|
showPreview(normalizedRequests, results, updatedConfig, preflightNames);
|
|
9958
8821
|
if (scaffoldedPages.length > 0) {
|
|
9959
8822
|
const uniqueScaffolded = [...new Map(scaffoldedPages.map((s) => [s.route, s])).values()];
|
|
9960
|
-
console.log(
|
|
8823
|
+
console.log(chalk13.cyan("\u{1F517} Auto-scaffolded linked pages:"));
|
|
9961
8824
|
uniqueScaffolded.forEach(({ route, name }) => {
|
|
9962
|
-
console.log(
|
|
8825
|
+
console.log(chalk13.white(` \u2728 ${name} \u2192 ${route}`));
|
|
9963
8826
|
});
|
|
9964
8827
|
console.log("");
|
|
9965
8828
|
}
|
|
@@ -9981,58 +8844,58 @@ ${uxRecommendations}
|
|
|
9981
8844
|
);
|
|
9982
8845
|
}
|
|
9983
8846
|
await appendFile(recPath, section);
|
|
9984
|
-
console.log(
|
|
8847
|
+
console.log(chalk13.cyan("\n\u{1F4CB} UX Recommendations:"));
|
|
9985
8848
|
for (const line of uxRecommendations.split("\n").filter(Boolean)) {
|
|
9986
|
-
console.log(
|
|
8849
|
+
console.log(chalk13.dim(` ${line}`));
|
|
9987
8850
|
}
|
|
9988
|
-
console.log(
|
|
8851
|
+
console.log(chalk13.dim(" \u2192 Saved to /design-system/docs/recommendations"));
|
|
9989
8852
|
} catch (e) {
|
|
9990
8853
|
console.log(
|
|
9991
|
-
|
|
8854
|
+
chalk13.yellow("\n\u26A0\uFE0F Could not write recommendations.md: " + (e instanceof Error ? e.message : String(e)))
|
|
9992
8855
|
);
|
|
9993
|
-
console.log(
|
|
8856
|
+
console.log(chalk13.dim("Recommendations:\n") + uxRecommendations);
|
|
9994
8857
|
}
|
|
9995
8858
|
}
|
|
9996
8859
|
} catch (error) {
|
|
9997
8860
|
spinner.fail("Chat command failed");
|
|
9998
|
-
console.error(
|
|
8861
|
+
console.error(chalk13.red("\n\u2716 Chat command failed"));
|
|
9999
8862
|
const zodError = error;
|
|
10000
8863
|
const issues = zodError.issues || error.errors;
|
|
10001
8864
|
if (issues && Array.isArray(issues)) {
|
|
10002
|
-
console.log(
|
|
8865
|
+
console.log(chalk13.yellow("\n\u26A0\uFE0F AI generated incomplete data. Missing or invalid fields:"));
|
|
10003
8866
|
issues.forEach((err) => {
|
|
10004
|
-
console.log(
|
|
8867
|
+
console.log(chalk13.gray(` \u2022 ${err.path.join(".")}: ${err.message}`));
|
|
10005
8868
|
});
|
|
10006
|
-
console.log(
|
|
10007
|
-
console.log(
|
|
10008
|
-
console.log(
|
|
8869
|
+
console.log(chalk13.cyan("\n\u{1F4A1} Try being more specific, e.g.:"));
|
|
8870
|
+
console.log(chalk13.white(' coherent chat "add a dashboard page with hero section using Button component"'));
|
|
8871
|
+
console.log(chalk13.white(' coherent chat "add pricing page"'));
|
|
10009
8872
|
} else if (error instanceof Error) {
|
|
10010
|
-
console.error(
|
|
8873
|
+
console.error(chalk13.red(error.message));
|
|
10011
8874
|
if (error.message.includes("Unterminated string") || error.message.includes("Unexpected end of JSON") || error.message.includes("Failed to parse modification") && error.message.includes("JSON")) {
|
|
10012
8875
|
console.log(
|
|
10013
|
-
|
|
8876
|
+
chalk13.yellow("\n\u{1F4A1} The AI response was too large or contained invalid JSON. Try splitting your request:")
|
|
10014
8877
|
);
|
|
10015
|
-
console.log(
|
|
10016
|
-
console.log(
|
|
10017
|
-
console.log(
|
|
8878
|
+
console.log(chalk13.white(' coherent chat "add dashboard page with stats and recent activity"'));
|
|
8879
|
+
console.log(chalk13.white(' coherent chat "add account page"'));
|
|
8880
|
+
console.log(chalk13.white(' coherent chat "add settings page"'));
|
|
10018
8881
|
} else if (error.message.includes("API key not found") || error.message.includes("ANTHROPIC_API_KEY") || error.message.includes("OPENAI_API_KEY")) {
|
|
10019
8882
|
const isOpenAI = error.message.includes("OpenAI") || typeof provider !== "undefined" && provider === "openai";
|
|
10020
8883
|
const providerName = isOpenAI ? "OpenAI" : "Anthropic Claude";
|
|
10021
8884
|
const envVar = isOpenAI ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
10022
8885
|
const url = isOpenAI ? "https://platform.openai.com" : "https://console.anthropic.com";
|
|
10023
|
-
console.log(
|
|
10024
|
-
console.log(
|
|
10025
|
-
console.log(
|
|
10026
|
-
console.log(
|
|
10027
|
-
console.log(
|
|
10028
|
-
console.log(
|
|
8886
|
+
console.log(chalk13.yellow("\n\u{1F4A1} Setup Instructions:"));
|
|
8887
|
+
console.log(chalk13.dim(` 1. Get your ${providerName} API key from: ${url}`));
|
|
8888
|
+
console.log(chalk13.dim(" 2. Create a .env file in the current directory:"));
|
|
8889
|
+
console.log(chalk13.cyan(` echo "${envVar}=your_key_here" > .env`));
|
|
8890
|
+
console.log(chalk13.dim(" 3. Or export it in your shell:"));
|
|
8891
|
+
console.log(chalk13.cyan(` export ${envVar}=your_key_here`));
|
|
10029
8892
|
if (isOpenAI) {
|
|
10030
|
-
console.log(
|
|
10031
|
-
console.log(
|
|
8893
|
+
console.log(chalk13.dim('\n Also ensure "openai" package is installed:'));
|
|
8894
|
+
console.log(chalk13.cyan(" npm install openai"));
|
|
10032
8895
|
}
|
|
10033
8896
|
}
|
|
10034
8897
|
} else {
|
|
10035
|
-
console.error(
|
|
8898
|
+
console.error(chalk13.red("Unknown error occurred"));
|
|
10036
8899
|
}
|
|
10037
8900
|
console.log("");
|
|
10038
8901
|
if (options._throwOnError) {
|
|
@@ -10045,7 +8908,7 @@ ${uxRecommendations}
|
|
|
10045
8908
|
}
|
|
10046
8909
|
|
|
10047
8910
|
// src/commands/preview.ts
|
|
10048
|
-
import
|
|
8911
|
+
import chalk14 from "chalk";
|
|
10049
8912
|
import ora3 from "ora";
|
|
10050
8913
|
import { spawn } from "child_process";
|
|
10051
8914
|
import { existsSync as existsSync20, rmSync as rmSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync10, readdirSync as readdirSync6 } from "fs";
|
|
@@ -10122,9 +8985,15 @@ function extractExportedComponentNames(code) {
|
|
|
10122
8985
|
}
|
|
10123
8986
|
function inferComponentType(name, code) {
|
|
10124
8987
|
const lower = name.toLowerCase();
|
|
10125
|
-
if (/header|footer|
|
|
8988
|
+
if (/header|footer|layout|appbar|topbar/.test(lower)) return "layout";
|
|
8989
|
+
if (/sidebar|nav|menu|breadcrumb|tabs/.test(lower)) return "navigation";
|
|
8990
|
+
if (/table|chart|stats|metric|list|grid|card/.test(lower)) return "data-display";
|
|
8991
|
+
if (/form|input|filter|search|select|picker/.test(lower)) return "form";
|
|
8992
|
+
if (/alert|toast|modal|dialog|notification|error|snackbar/.test(lower)) return "feedback";
|
|
10126
8993
|
if (/section|hero|pricing|testimonial|features|cta|banner/.test(lower)) return "section";
|
|
10127
8994
|
if (/<header[\s>]/.test(code) || /<footer[\s>]/.test(code)) return "layout";
|
|
8995
|
+
if (/<nav[\s>]/.test(code)) return "navigation";
|
|
8996
|
+
if (/<form[\s>]/.test(code)) return "form";
|
|
10128
8997
|
if (/<section[\s>]/.test(code)) return "section";
|
|
10129
8998
|
return "widget";
|
|
10130
8999
|
}
|
|
@@ -10334,7 +9203,9 @@ function reconcileComponents(projectRoot, manifest) {
|
|
|
10334
9203
|
file: comp.file,
|
|
10335
9204
|
usedIn: comp.usedIn,
|
|
10336
9205
|
description: `Auto-registered by sync from ${comp.file}`,
|
|
10337
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9206
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9207
|
+
dependencies: [],
|
|
9208
|
+
source: "extracted"
|
|
10338
9209
|
});
|
|
10339
9210
|
m.nextId++;
|
|
10340
9211
|
result.added.push({ id, name: comp.name, file: comp.file, type: comp.type });
|
|
@@ -10435,13 +9306,13 @@ async function handleFileChange(projectRoot, filePath) {
|
|
|
10435
9306
|
return;
|
|
10436
9307
|
}
|
|
10437
9308
|
const config2 = getWatcherConfig(projectRoot);
|
|
10438
|
-
const
|
|
9309
|
+
const chalk33 = (await import("chalk")).default;
|
|
10439
9310
|
if (config2.autoInstall) {
|
|
10440
9311
|
const missing = findMissingPackagesInCode(content, projectRoot);
|
|
10441
9312
|
if (missing.length > 0) {
|
|
10442
9313
|
const ok = await installPackages(projectRoot, missing);
|
|
10443
9314
|
if (ok) {
|
|
10444
|
-
console.log(
|
|
9315
|
+
console.log(chalk33.cyan(`
|
|
10445
9316
|
\u{1F527} Auto-installed: ${missing.join(", ")} (needed by ${relativePath})`));
|
|
10446
9317
|
}
|
|
10447
9318
|
}
|
|
@@ -10450,12 +9321,12 @@ async function handleFileChange(projectRoot, filePath) {
|
|
|
10450
9321
|
const fixed = sanitizeMetadataStrings(ensureUseClientIfNeeded(content));
|
|
10451
9322
|
if (fixed !== content) {
|
|
10452
9323
|
writeFileSync9(filePath, fixed, "utf-8");
|
|
10453
|
-
console.log(
|
|
9324
|
+
console.log(chalk33.cyan(` \u{1F527} Auto-fixed syntax in ${relativePath}`));
|
|
10454
9325
|
}
|
|
10455
9326
|
}
|
|
10456
9327
|
if (config2.warnNativeElements && hasNativeElements(content)) {
|
|
10457
|
-
console.log(
|
|
10458
|
-
console.log(
|
|
9328
|
+
console.log(chalk33.yellow(` \u26A0 ${relativePath}: uses native HTML elements (<button>, <select>, etc.)`));
|
|
9329
|
+
console.log(chalk33.dim(" Use components from @/components/ui/ instead"));
|
|
10459
9330
|
}
|
|
10460
9331
|
if (config2.warnSharedReuse) {
|
|
10461
9332
|
let manifest;
|
|
@@ -10468,8 +9339,8 @@ async function handleFileChange(projectRoot, filePath) {
|
|
|
10468
9339
|
const dupes = findInlineDuplicatesOfShared(content, manifest);
|
|
10469
9340
|
for (const d of dupes) {
|
|
10470
9341
|
const importPath = d.file.replace(/\.tsx$/, "").replace(/^components\/shared\//, "");
|
|
10471
|
-
console.log(
|
|
10472
|
-
console.log(
|
|
9342
|
+
console.log(chalk33.yellow(` \u26A0 ${relativePath}: has inline code similar to ${d.cid} (${d.name})`));
|
|
9343
|
+
console.log(chalk33.dim(` Consider: import { ${d.name} } from "@/components/shared/${importPath}"`));
|
|
10473
9344
|
}
|
|
10474
9345
|
}
|
|
10475
9346
|
}
|
|
@@ -10478,7 +9349,7 @@ async function handleFileDelete(projectRoot, filePath) {
|
|
|
10478
9349
|
const relativePath = relative4(projectRoot, filePath).replace(/\\/g, "/");
|
|
10479
9350
|
if (!relativePath.startsWith("components/") || relativePath.startsWith("components/ui/")) return;
|
|
10480
9351
|
try {
|
|
10481
|
-
const
|
|
9352
|
+
const chalk33 = (await import("chalk")).default;
|
|
10482
9353
|
const manifest = await loadManifest9(projectRoot);
|
|
10483
9354
|
const orphaned = manifest.shared.find((s) => s.file === relativePath);
|
|
10484
9355
|
if (orphaned) {
|
|
@@ -10487,7 +9358,7 @@ async function handleFileDelete(projectRoot, filePath) {
|
|
|
10487
9358
|
shared: manifest.shared.filter((s) => s.id !== orphaned.id)
|
|
10488
9359
|
};
|
|
10489
9360
|
await saveManifest3(projectRoot, cleaned);
|
|
10490
|
-
console.log(
|
|
9361
|
+
console.log(chalk33.cyan(`
|
|
10491
9362
|
\u{1F5D1} Auto-removed ${orphaned.id} (${orphaned.name}) \u2014 file deleted`));
|
|
10492
9363
|
await writeCursorRules(projectRoot);
|
|
10493
9364
|
}
|
|
@@ -10499,7 +9370,7 @@ async function detectNewComponent(projectRoot, filePath) {
|
|
|
10499
9370
|
if (!relativePath.startsWith("components/") || relativePath.startsWith("components/ui/")) return;
|
|
10500
9371
|
if (!relativePath.endsWith(".tsx") && !relativePath.endsWith(".jsx")) return;
|
|
10501
9372
|
try {
|
|
10502
|
-
const
|
|
9373
|
+
const chalk33 = (await import("chalk")).default;
|
|
10503
9374
|
const manifest = await loadManifest9(projectRoot);
|
|
10504
9375
|
const alreadyRegistered = manifest.shared.some((s) => s.file === relativePath);
|
|
10505
9376
|
if (alreadyRegistered) return;
|
|
@@ -10508,9 +9379,9 @@ async function detectNewComponent(projectRoot, filePath) {
|
|
|
10508
9379
|
if (exports.length > 0) {
|
|
10509
9380
|
const alreadyByName = exports.every((n) => manifest.shared.some((s) => s.name === n));
|
|
10510
9381
|
if (!alreadyByName) {
|
|
10511
|
-
console.log(
|
|
9382
|
+
console.log(chalk33.cyan(`
|
|
10512
9383
|
\u2139 New component detected: ${exports[0]} in ${relativePath}`));
|
|
10513
|
-
console.log(
|
|
9384
|
+
console.log(chalk33.dim(" Register with: coherent sync"));
|
|
10514
9385
|
}
|
|
10515
9386
|
}
|
|
10516
9387
|
} catch {
|
|
@@ -10597,17 +9468,17 @@ function clearStaleCache(projectRoot) {
|
|
|
10597
9468
|
const nextDir = join14(projectRoot, ".next");
|
|
10598
9469
|
if (existsSync20(nextDir)) {
|
|
10599
9470
|
rmSync3(nextDir, { recursive: true, force: true });
|
|
10600
|
-
console.log(
|
|
9471
|
+
console.log(chalk14.dim(" \u2714 Cleared stale build cache"));
|
|
10601
9472
|
}
|
|
10602
9473
|
}
|
|
10603
9474
|
async function preflightDependencyCheck(projectRoot) {
|
|
10604
9475
|
const missing = await findMissingPackages(projectRoot);
|
|
10605
9476
|
if (missing.length === 0) return;
|
|
10606
|
-
console.log(
|
|
9477
|
+
console.log(chalk14.cyan(`
|
|
10607
9478
|
Auto-installing missing dependencies: ${missing.join(", ")}`));
|
|
10608
9479
|
const ok = await installPackages(projectRoot, missing);
|
|
10609
|
-
if (ok) console.log(
|
|
10610
|
-
else console.log(
|
|
9480
|
+
if (ok) console.log(chalk14.dim(" \u2714 Installed"));
|
|
9481
|
+
else console.log(chalk14.yellow(` Run manually: npm install ${missing.join(" ")}`));
|
|
10611
9482
|
}
|
|
10612
9483
|
async function listPageFiles(appDir) {
|
|
10613
9484
|
const out = [];
|
|
@@ -10631,7 +9502,7 @@ async function validateSyntax(projectRoot) {
|
|
|
10631
9502
|
const fixed = fixUnescapedLtInJsx(sanitizeMetadataStrings(ensureUseClientIfNeeded(content)));
|
|
10632
9503
|
if (fixed !== content) {
|
|
10633
9504
|
writeFileSync10(file, fixed, "utf-8");
|
|
10634
|
-
console.log(
|
|
9505
|
+
console.log(chalk14.dim(` \u2714 Auto-fixed syntax: ${file.replace(projectRoot, ".").replace(/^\.[/\\]/, "")}`));
|
|
10635
9506
|
}
|
|
10636
9507
|
}
|
|
10637
9508
|
}
|
|
@@ -10673,7 +9544,7 @@ async function fixMissingComponentExports(projectRoot) {
|
|
|
10673
9544
|
try {
|
|
10674
9545
|
const result = await provider.installComponent(componentId, projectRoot);
|
|
10675
9546
|
if (result.success) {
|
|
10676
|
-
console.log(
|
|
9547
|
+
console.log(chalk14.dim(` \u2714 Installed missing ${componentId}.tsx`));
|
|
10677
9548
|
}
|
|
10678
9549
|
} catch {
|
|
10679
9550
|
}
|
|
@@ -10685,7 +9556,7 @@ async function fixMissingComponentExports(projectRoot) {
|
|
|
10685
9556
|
mkdirSync10(uiDir, { recursive: true });
|
|
10686
9557
|
const newContent = await generator.generate(def);
|
|
10687
9558
|
writeFileSync10(componentFile, newContent, "utf-8");
|
|
10688
|
-
console.log(
|
|
9559
|
+
console.log(chalk14.dim(` \u2714 Created missing ${componentId}.tsx`));
|
|
10689
9560
|
} catch {
|
|
10690
9561
|
}
|
|
10691
9562
|
}
|
|
@@ -10708,7 +9579,7 @@ async function fixMissingComponentExports(projectRoot) {
|
|
|
10708
9579
|
try {
|
|
10709
9580
|
const result = await provider.installComponent(componentId, projectRoot, { force: true });
|
|
10710
9581
|
if (result.success) {
|
|
10711
|
-
console.log(
|
|
9582
|
+
console.log(chalk14.dim(` \u2714 Reinstalled ${componentId}.tsx (added missing exports: ${missing.join(", ")})`));
|
|
10712
9583
|
}
|
|
10713
9584
|
} catch {
|
|
10714
9585
|
}
|
|
@@ -10718,7 +9589,7 @@ async function fixMissingComponentExports(projectRoot) {
|
|
|
10718
9589
|
try {
|
|
10719
9590
|
const newContent = await generator.generate(def);
|
|
10720
9591
|
writeFileSync10(componentFile, newContent, "utf-8");
|
|
10721
|
-
console.log(
|
|
9592
|
+
console.log(chalk14.dim(` \u2714 Regenerated ${componentId}.tsx (added missing exports: ${missing.join(", ")})`));
|
|
10722
9593
|
} catch {
|
|
10723
9594
|
}
|
|
10724
9595
|
}
|
|
@@ -10777,14 +9648,14 @@ async function healthCheck(port) {
|
|
|
10777
9648
|
try {
|
|
10778
9649
|
const res = await fetch(`http://localhost:${port}`);
|
|
10779
9650
|
if (res.status === 200) {
|
|
10780
|
-
console.log(
|
|
9651
|
+
console.log(chalk14.green(`
|
|
10781
9652
|
\u2705 Preview healthy at http://localhost:${port}`));
|
|
10782
9653
|
} else {
|
|
10783
|
-
console.log(
|
|
9654
|
+
console.log(chalk14.yellow(`
|
|
10784
9655
|
\u26A0 Preview returned ${res.status}. Run: coherent fix`));
|
|
10785
9656
|
}
|
|
10786
9657
|
} catch {
|
|
10787
|
-
console.log(
|
|
9658
|
+
console.log(chalk14.yellow(`
|
|
10788
9659
|
\u26A0 Preview not responding. Run: coherent fix`));
|
|
10789
9660
|
}
|
|
10790
9661
|
}
|
|
@@ -10823,29 +9694,29 @@ function launchWithMonitoring(projectRoot, restarts) {
|
|
|
10823
9694
|
const shadcnId = extractShadcnComponentFromModuleNotFound(msg);
|
|
10824
9695
|
if (shadcnId && !installingSet.has(shadcnId)) {
|
|
10825
9696
|
installingSet.add(shadcnId);
|
|
10826
|
-
console.log(
|
|
9697
|
+
console.log(chalk14.yellow(`
|
|
10827
9698
|
\u26A0 Missing component detected: ${shadcnId}`));
|
|
10828
|
-
console.log(
|
|
9699
|
+
console.log(chalk14.cyan(" Auto-installing..."));
|
|
10829
9700
|
autoInstallShadcnComponent(shadcnId, projectRoot).then((ok) => {
|
|
10830
9701
|
if (ok) {
|
|
10831
|
-
console.log(
|
|
9702
|
+
console.log(chalk14.green(` \u2714 Installed ${shadcnId}.tsx. Restarting...`));
|
|
10832
9703
|
intentionalRestart = true;
|
|
10833
9704
|
server.kill("SIGTERM");
|
|
10834
9705
|
launchWithMonitoring(projectRoot, restarts + 1).then(resolvePromise).catch(rejectPromise);
|
|
10835
9706
|
} else {
|
|
10836
|
-
console.log(
|
|
9707
|
+
console.log(chalk14.red(` \u2716 Could not install ${shadcnId}. Run: npx shadcn@latest add ${shadcnId}`));
|
|
10837
9708
|
}
|
|
10838
9709
|
});
|
|
10839
9710
|
} else if (!shadcnId) {
|
|
10840
9711
|
const pkg = extractPackageFromModuleNotFound(msg);
|
|
10841
9712
|
if (pkg && !installingSet.has(pkg)) {
|
|
10842
9713
|
installingSet.add(pkg);
|
|
10843
|
-
console.log(
|
|
9714
|
+
console.log(chalk14.yellow(`
|
|
10844
9715
|
\u26A0 Missing package detected: ${pkg}`));
|
|
10845
|
-
console.log(
|
|
9716
|
+
console.log(chalk14.cyan(" Auto-installing..."));
|
|
10846
9717
|
installPackages(projectRoot, [pkg]).then((ok) => {
|
|
10847
9718
|
if (ok) {
|
|
10848
|
-
console.log(
|
|
9719
|
+
console.log(chalk14.green(` \u2714 Installed ${pkg}. Restarting...`));
|
|
10849
9720
|
intentionalRestart = true;
|
|
10850
9721
|
server.kill("SIGTERM");
|
|
10851
9722
|
launchWithMonitoring(projectRoot, restarts + 1).then(resolvePromise).catch(rejectPromise);
|
|
@@ -10855,19 +9726,19 @@ function launchWithMonitoring(projectRoot, restarts) {
|
|
|
10855
9726
|
}
|
|
10856
9727
|
}
|
|
10857
9728
|
if (msg.includes("Failed to compile")) {
|
|
10858
|
-
console.log(
|
|
10859
|
-
console.log(
|
|
10860
|
-
console.log(
|
|
9729
|
+
console.log(chalk14.yellow("\n\u26A0 Compilation error detected."));
|
|
9730
|
+
console.log(chalk14.dim(' Hint: run "coherent fix" in another terminal to auto-fix'));
|
|
9731
|
+
console.log(chalk14.dim(' Or: coherent chat "fix the broken page"'));
|
|
10861
9732
|
}
|
|
10862
9733
|
});
|
|
10863
9734
|
server.on("exit", (code) => {
|
|
10864
9735
|
if (intentionalRestart) return;
|
|
10865
9736
|
if (code !== 0 && code !== null) {
|
|
10866
|
-
console.log(
|
|
9737
|
+
console.log(chalk14.red(`
|
|
10867
9738
|
\u274C Dev server exited with code ${code}`));
|
|
10868
|
-
console.log(
|
|
9739
|
+
console.log(chalk14.dim(' Check the output above. Fix and run "coherent preview" again.\n'));
|
|
10869
9740
|
} else {
|
|
10870
|
-
console.log(
|
|
9741
|
+
console.log(chalk14.dim("\n\u{1F44B} Dev server stopped"));
|
|
10871
9742
|
}
|
|
10872
9743
|
process.exit(code ?? 0);
|
|
10873
9744
|
});
|
|
@@ -10876,7 +9747,7 @@ function launchWithMonitoring(projectRoot, restarts) {
|
|
|
10876
9747
|
});
|
|
10877
9748
|
const shutdown = () => {
|
|
10878
9749
|
closeWatcher();
|
|
10879
|
-
console.log(
|
|
9750
|
+
console.log(chalk14.dim("\n\n\u{1F6D1} Stopping dev server..."));
|
|
10880
9751
|
server.kill("SIGTERM");
|
|
10881
9752
|
};
|
|
10882
9753
|
process.on("SIGINT", shutdown);
|
|
@@ -10888,9 +9759,9 @@ async function openBrowser(url) {
|
|
|
10888
9759
|
const open = await import("open");
|
|
10889
9760
|
await open.default(url);
|
|
10890
9761
|
} catch (error) {
|
|
10891
|
-
console.log(
|
|
9762
|
+
console.log(chalk14.yellow(`
|
|
10892
9763
|
\u26A0\uFE0F Could not open browser automatically`));
|
|
10893
|
-
console.log(
|
|
9764
|
+
console.log(chalk14.dim(` Please open ${url} manually`));
|
|
10894
9765
|
}
|
|
10895
9766
|
}
|
|
10896
9767
|
function startDevServer(projectRoot) {
|
|
@@ -10921,8 +9792,8 @@ async function previewCommand() {
|
|
|
10921
9792
|
try {
|
|
10922
9793
|
if (!checkProjectInitialized(projectRoot)) {
|
|
10923
9794
|
spinner.fail("Project not initialized");
|
|
10924
|
-
console.log(
|
|
10925
|
-
console.log(
|
|
9795
|
+
console.log(chalk14.red("\n\u274C Project not found"));
|
|
9796
|
+
console.log(chalk14.dim('Run "coherent init" first to create a project.'));
|
|
10926
9797
|
process.exit(1);
|
|
10927
9798
|
}
|
|
10928
9799
|
spinner.text = "Checking dependencies...";
|
|
@@ -10930,16 +9801,16 @@ async function previewCommand() {
|
|
|
10930
9801
|
spinner.warn("Dependencies not installed");
|
|
10931
9802
|
const pm = getPackageManager(projectRoot);
|
|
10932
9803
|
const installCommand = pm === "pnpm" ? "pnpm install" : "npm install";
|
|
10933
|
-
console.log(
|
|
10934
|
-
console.log(
|
|
9804
|
+
console.log(chalk14.yellow("\n\u26A0\uFE0F Dependencies not installed"));
|
|
9805
|
+
console.log(chalk14.cyan(`
|
|
10935
9806
|
Running ${installCommand}...
|
|
10936
9807
|
`));
|
|
10937
9808
|
const ok = await runInstall(projectRoot);
|
|
10938
9809
|
if (!ok) {
|
|
10939
|
-
console.error(
|
|
9810
|
+
console.error(chalk14.red('\n\u274C Install failed. Fix errors above and run "coherent preview" again.\n'));
|
|
10940
9811
|
process.exit(1);
|
|
10941
9812
|
}
|
|
10942
|
-
console.log(
|
|
9813
|
+
console.log(chalk14.green("\n\u2705 Dependencies installed\n"));
|
|
10943
9814
|
} else {
|
|
10944
9815
|
spinner.succeed("Dependencies installed");
|
|
10945
9816
|
}
|
|
@@ -10961,11 +9832,11 @@ async function previewCommand() {
|
|
|
10961
9832
|
const globalsContent = readFileSync15(globalsPath, "utf-8");
|
|
10962
9833
|
const cssIssues = validateV4GlobalsCss(globalsContent);
|
|
10963
9834
|
if (cssIssues.length > 0) {
|
|
10964
|
-
console.log(
|
|
9835
|
+
console.log(chalk14.yellow("\n\u26A0\uFE0F globals.css validation warnings:"));
|
|
10965
9836
|
for (const issue of cssIssues) {
|
|
10966
|
-
console.log(
|
|
9837
|
+
console.log(chalk14.yellow(` \u2022 ${issue}`));
|
|
10967
9838
|
}
|
|
10968
|
-
console.log(
|
|
9839
|
+
console.log(chalk14.dim(' Run "coherent chat" to regenerate globals.css\n'));
|
|
10969
9840
|
}
|
|
10970
9841
|
}
|
|
10971
9842
|
}
|
|
@@ -10976,27 +9847,27 @@ async function previewCommand() {
|
|
|
10976
9847
|
await fixMissingComponentExports(projectRoot);
|
|
10977
9848
|
await backfillPageAnalysis(projectRoot);
|
|
10978
9849
|
spinner.succeed("Project ready");
|
|
10979
|
-
console.log(
|
|
10980
|
-
console.log(
|
|
9850
|
+
console.log(chalk14.dim(" \u{1F4A1} Edited files manually? Run `coherent sync` to update the Design System.\n"));
|
|
9851
|
+
console.log(chalk14.blue("\n\u{1F680} Starting Next.js dev server...\n"));
|
|
10981
9852
|
await launchWithMonitoring(projectRoot, 0);
|
|
10982
9853
|
} catch (error) {
|
|
10983
9854
|
spinner.fail("Failed to start dev server");
|
|
10984
9855
|
if (error instanceof Error) {
|
|
10985
|
-
console.error(
|
|
9856
|
+
console.error(chalk14.red(`
|
|
10986
9857
|
\u274C ${error.message}`));
|
|
10987
9858
|
if (error.message.includes("package.json")) {
|
|
10988
|
-
console.log(
|
|
10989
|
-
console.log(
|
|
9859
|
+
console.log(chalk14.yellow("\n\u{1F4A1} Tip: Make sure you're in a Coherent project directory."));
|
|
9860
|
+
console.log(chalk14.dim(' Run "coherent init" to create a new project.'));
|
|
10990
9861
|
}
|
|
10991
9862
|
} else {
|
|
10992
|
-
console.error(
|
|
9863
|
+
console.error(chalk14.red("Unknown error occurred"));
|
|
10993
9864
|
}
|
|
10994
9865
|
process.exit(1);
|
|
10995
9866
|
}
|
|
10996
9867
|
}
|
|
10997
9868
|
|
|
10998
9869
|
// src/commands/export.ts
|
|
10999
|
-
import
|
|
9870
|
+
import chalk15 from "chalk";
|
|
11000
9871
|
import ora4 from "ora";
|
|
11001
9872
|
import { spawn as spawn2 } from "child_process";
|
|
11002
9873
|
import { existsSync as existsSync21, rmSync as rmSync4, readdirSync as readdirSync7 } from "fs";
|
|
@@ -11275,7 +10146,7 @@ async function exportCommand(options = {}) {
|
|
|
11275
10146
|
const missingDeps = await findMissingDepsInExport(outputDir);
|
|
11276
10147
|
if (missingDeps.length > 0) {
|
|
11277
10148
|
console.log(
|
|
11278
|
-
|
|
10149
|
+
chalk15.yellow(
|
|
11279
10150
|
"\n\u26A0\uFE0F Warning: exported code imports packages not in package.json: " + missingDeps.join(", ") + "\n Add them to dependencies and run npm install in the export dir.\n"
|
|
11280
10151
|
)
|
|
11281
10152
|
);
|
|
@@ -11291,7 +10162,7 @@ async function exportCommand(options = {}) {
|
|
|
11291
10162
|
spinner.succeed("Dependencies installed");
|
|
11292
10163
|
await ensureReadmeDeploySection(outputDir);
|
|
11293
10164
|
await patchNextConfigForExport(outputDir);
|
|
11294
|
-
console.log(
|
|
10165
|
+
console.log(chalk15.dim("\n Tip: run `coherent check` before export to catch quality issues.\n"));
|
|
11295
10166
|
let buildOk = false;
|
|
11296
10167
|
if (doBuild) {
|
|
11297
10168
|
spinner.start("Running next build...");
|
|
@@ -11301,7 +10172,7 @@ async function exportCommand(options = {}) {
|
|
|
11301
10172
|
spinner.succeed("Build: success");
|
|
11302
10173
|
} catch (e) {
|
|
11303
10174
|
spinner.fail("Build failed");
|
|
11304
|
-
if (e instanceof Error) console.error(
|
|
10175
|
+
if (e instanceof Error) console.error(chalk15.red(e.message));
|
|
11305
10176
|
}
|
|
11306
10177
|
} else {
|
|
11307
10178
|
buildOk = true;
|
|
@@ -11309,23 +10180,23 @@ async function exportCommand(options = {}) {
|
|
|
11309
10180
|
const pageCount = await countPages(outputDir);
|
|
11310
10181
|
const componentCount = countComponents(outputDir);
|
|
11311
10182
|
spinner.stop();
|
|
11312
|
-
console.log(
|
|
11313
|
-
console.log(
|
|
11314
|
-
console.log(
|
|
11315
|
-
console.log(
|
|
10183
|
+
console.log(chalk15.green("\n\u2705 Exported to " + outputDir + "\n"));
|
|
10184
|
+
console.log(chalk15.blue(" Pages: " + pageCount));
|
|
10185
|
+
console.log(chalk15.blue(" Components: " + componentCount + " (base + shared)"));
|
|
10186
|
+
console.log(chalk15.blue(" Build: " + (doBuild ? buildOk ? "success" : "failed" : "skipped (--no-build)")));
|
|
11316
10187
|
console.log("");
|
|
11317
|
-
console.log(
|
|
11318
|
-
console.log(
|
|
10188
|
+
console.log(chalk15.dim(" Deploy: npx vercel " + outputDir));
|
|
10189
|
+
console.log(chalk15.dim(" or: npx netlify deploy --dir " + outputDir + "/.next"));
|
|
11319
10190
|
console.log("");
|
|
11320
10191
|
} catch (error) {
|
|
11321
10192
|
spinner.fail("Export failed");
|
|
11322
|
-
if (error instanceof Error) console.error(
|
|
10193
|
+
if (error instanceof Error) console.error(chalk15.red("\n\u274C " + error.message));
|
|
11323
10194
|
process.exit(1);
|
|
11324
10195
|
}
|
|
11325
10196
|
}
|
|
11326
10197
|
|
|
11327
10198
|
// src/commands/status.ts
|
|
11328
|
-
import
|
|
10199
|
+
import chalk16 from "chalk";
|
|
11329
10200
|
import { basename } from "path";
|
|
11330
10201
|
import { DesignSystemManager as DesignSystemManager9 } from "@getcoherent/core";
|
|
11331
10202
|
function countTokens(tokens) {
|
|
@@ -11349,58 +10220,58 @@ async function statusCommand() {
|
|
|
11349
10220
|
try {
|
|
11350
10221
|
const project = findConfig();
|
|
11351
10222
|
if (!project) {
|
|
11352
|
-
console.log(
|
|
10223
|
+
console.log(chalk16.yellow("\u26A0\uFE0F Not in a Coherent project\n"));
|
|
11353
10224
|
console.log("Initialize a project:");
|
|
11354
|
-
console.log(
|
|
10225
|
+
console.log(chalk16.white(" $ coherent init\n"));
|
|
11355
10226
|
return;
|
|
11356
10227
|
}
|
|
11357
|
-
console.log(
|
|
11358
|
-
console.log(
|
|
11359
|
-
console.log(
|
|
10228
|
+
console.log(chalk16.cyan("\n\u2728 Current Project\n"));
|
|
10229
|
+
console.log(chalk16.gray("\u{1F4C1} Location: ") + chalk16.white(project.root));
|
|
10230
|
+
console.log(chalk16.gray("\u{1F4C4} Config: ") + chalk16.white(basename(project.configPath)));
|
|
11360
10231
|
console.log("");
|
|
11361
10232
|
try {
|
|
11362
10233
|
const manager = new DesignSystemManager9(project.configPath);
|
|
11363
10234
|
await manager.load();
|
|
11364
10235
|
const config2 = manager.getConfig();
|
|
11365
|
-
console.log(
|
|
10236
|
+
console.log(chalk16.cyan("\u{1F4CA} Statistics:\n"));
|
|
11366
10237
|
const pageCount = Array.isArray(config2.pages) ? config2.pages.length : Object.keys(config2.pages || {}).length;
|
|
11367
|
-
console.log(
|
|
10238
|
+
console.log(chalk16.gray(" Pages: ") + chalk16.white(String(pageCount)));
|
|
11368
10239
|
const componentCount = Array.isArray(config2.components) ? config2.components.length : Object.keys(config2.components || {}).length;
|
|
11369
|
-
console.log(
|
|
10240
|
+
console.log(chalk16.gray(" Components: ") + chalk16.white(String(componentCount)));
|
|
11370
10241
|
const tokenCount = countTokens(config2.tokens);
|
|
11371
|
-
console.log(
|
|
10242
|
+
console.log(chalk16.gray(" Design tokens: ") + chalk16.white(String(tokenCount)));
|
|
11372
10243
|
console.log("");
|
|
11373
10244
|
const recent = readRecentChanges(project.root);
|
|
11374
10245
|
if (recent.length > 0) {
|
|
11375
|
-
console.log(
|
|
10246
|
+
console.log(chalk16.cyan("\u{1F4DD} Recent changes:\n"));
|
|
11376
10247
|
recent.slice(0, 5).forEach((change) => {
|
|
11377
10248
|
const ago = formatTimeAgo(change.timestamp);
|
|
11378
|
-
console.log(
|
|
10249
|
+
console.log(chalk16.gray(" \u2022 ") + chalk16.white(change.description) + chalk16.gray(` (${ago})`));
|
|
11379
10250
|
});
|
|
11380
10251
|
console.log("");
|
|
11381
10252
|
}
|
|
11382
|
-
console.log(
|
|
11383
|
-
console.log(
|
|
11384
|
-
console.log(
|
|
11385
|
-
console.log(
|
|
10253
|
+
console.log(chalk16.cyan("\u{1F680} Quick actions:\n"));
|
|
10254
|
+
console.log(chalk16.white(' $ coherent chat "add new page"'));
|
|
10255
|
+
console.log(chalk16.white(" $ coherent preview"));
|
|
10256
|
+
console.log(chalk16.white(" $ coherent export"));
|
|
11386
10257
|
console.log("");
|
|
11387
10258
|
} catch (error) {
|
|
11388
|
-
console.error(
|
|
10259
|
+
console.error(chalk16.red("Error loading config:"));
|
|
11389
10260
|
if (error instanceof Error) {
|
|
11390
|
-
console.error(
|
|
10261
|
+
console.error(chalk16.red(` ${error.message}`));
|
|
11391
10262
|
} else {
|
|
11392
|
-
console.error(
|
|
10263
|
+
console.error(chalk16.red(" Unknown error"));
|
|
11393
10264
|
}
|
|
11394
10265
|
console.log("");
|
|
11395
10266
|
}
|
|
11396
10267
|
} catch (error) {
|
|
11397
|
-
console.error(
|
|
10268
|
+
console.error(chalk16.red("\u274C Command failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
11398
10269
|
process.exit(1);
|
|
11399
10270
|
}
|
|
11400
10271
|
}
|
|
11401
10272
|
|
|
11402
10273
|
// src/commands/regenerate-docs.ts
|
|
11403
|
-
import
|
|
10274
|
+
import chalk17 from "chalk";
|
|
11404
10275
|
import ora5 from "ora";
|
|
11405
10276
|
import { DesignSystemManager as DesignSystemManager10 } from "@getcoherent/core";
|
|
11406
10277
|
import { ProjectScaffolder as ProjectScaffolder2 } from "@getcoherent/core";
|
|
@@ -11408,9 +10279,9 @@ async function regenerateDocsCommand() {
|
|
|
11408
10279
|
try {
|
|
11409
10280
|
const project = findConfig();
|
|
11410
10281
|
if (!project) {
|
|
11411
|
-
console.log(
|
|
10282
|
+
console.log(chalk17.yellow("\u26A0\uFE0F Not in a Coherent project\n"));
|
|
11412
10283
|
console.log("Run this command from a project root that has design-system.config.ts");
|
|
11413
|
-
console.log(
|
|
10284
|
+
console.log(chalk17.white(" $ coherent init # in an empty folder first\n"));
|
|
11414
10285
|
process.exit(1);
|
|
11415
10286
|
}
|
|
11416
10287
|
const spinner = ora5("Regenerating documentation pages...").start();
|
|
@@ -11422,23 +10293,23 @@ async function regenerateDocsCommand() {
|
|
|
11422
10293
|
await scaffolder.generateDocsPages();
|
|
11423
10294
|
spinner.succeed("Documentation pages updated");
|
|
11424
10295
|
console.log(
|
|
11425
|
-
|
|
10296
|
+
chalk17.gray(
|
|
11426
10297
|
"\nUpdated: app/design-system/docs/ (layout, page, components, tokens, for-designers, recommendations)\n"
|
|
11427
10298
|
)
|
|
11428
10299
|
);
|
|
11429
10300
|
} catch (err) {
|
|
11430
10301
|
spinner.fail("Failed to regenerate docs");
|
|
11431
|
-
console.error(
|
|
10302
|
+
console.error(chalk17.red(err instanceof Error ? err.message : String(err)));
|
|
11432
10303
|
process.exit(1);
|
|
11433
10304
|
}
|
|
11434
10305
|
} catch (error) {
|
|
11435
|
-
console.error(
|
|
10306
|
+
console.error(chalk17.red("\u274C Command failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
11436
10307
|
process.exit(1);
|
|
11437
10308
|
}
|
|
11438
10309
|
}
|
|
11439
10310
|
|
|
11440
10311
|
// src/commands/fix.ts
|
|
11441
|
-
import
|
|
10312
|
+
import chalk18 from "chalk";
|
|
11442
10313
|
import { readdirSync as readdirSync8, readFileSync as readFileSync16, existsSync as existsSync22, writeFileSync as writeFileSync11, rmSync as rmSync5, mkdirSync as mkdirSync7 } from "fs";
|
|
11443
10314
|
import { resolve as resolve13, join as join16 } from "path";
|
|
11444
10315
|
import {
|
|
@@ -11490,31 +10361,31 @@ async function fixCommand(opts = {}) {
|
|
|
11490
10361
|
const fixes = [];
|
|
11491
10362
|
const remaining = [];
|
|
11492
10363
|
if (dryRun) {
|
|
11493
|
-
console.log(
|
|
10364
|
+
console.log(chalk18.cyan("\ncoherent fix --dry-run\n"));
|
|
11494
10365
|
} else {
|
|
11495
|
-
console.log(
|
|
10366
|
+
console.log(chalk18.cyan("\ncoherent fix\n"));
|
|
11496
10367
|
}
|
|
11497
10368
|
if (!skipCache) {
|
|
11498
10369
|
const nextDir = join16(projectRoot, ".next");
|
|
11499
10370
|
if (existsSync22(nextDir)) {
|
|
11500
10371
|
if (!dryRun) rmSync5(nextDir, { recursive: true, force: true });
|
|
11501
10372
|
fixes.push("Cleared build cache");
|
|
11502
|
-
console.log(
|
|
10373
|
+
console.log(chalk18.green(" \u2714 Cleared build cache"));
|
|
11503
10374
|
}
|
|
11504
10375
|
}
|
|
11505
10376
|
const missingPkgs = await findMissingPackages(projectRoot);
|
|
11506
10377
|
if (missingPkgs.length > 0) {
|
|
11507
10378
|
if (dryRun) {
|
|
11508
10379
|
fixes.push(`Would install packages: ${missingPkgs.join(", ")}`);
|
|
11509
|
-
console.log(
|
|
10380
|
+
console.log(chalk18.green(` \u2714 Would install packages: ${missingPkgs.join(", ")}`));
|
|
11510
10381
|
} else {
|
|
11511
10382
|
const ok = await installPackages(projectRoot, missingPkgs);
|
|
11512
10383
|
if (ok) {
|
|
11513
10384
|
fixes.push(`Installed missing packages: ${missingPkgs.join(", ")}`);
|
|
11514
|
-
console.log(
|
|
10385
|
+
console.log(chalk18.green(` \u2714 Installed missing packages: ${missingPkgs.join(", ")}`));
|
|
11515
10386
|
} else {
|
|
11516
10387
|
remaining.push(`Failed to install: ${missingPkgs.join(", ")}. Run: npm install ${missingPkgs.join(" ")}`);
|
|
11517
|
-
console.log(
|
|
10388
|
+
console.log(chalk18.yellow(` \u26A0 Could not install: ${missingPkgs.join(", ")}`));
|
|
11518
10389
|
}
|
|
11519
10390
|
}
|
|
11520
10391
|
}
|
|
@@ -11551,7 +10422,7 @@ async function fixCommand(opts = {}) {
|
|
|
11551
10422
|
if (toInstall.length > 0) {
|
|
11552
10423
|
if (dryRun) {
|
|
11553
10424
|
fixes.push(`Would install components: ${toInstall.join(", ")}`);
|
|
11554
|
-
console.log(
|
|
10425
|
+
console.log(chalk18.green(` \u2714 Would install components: ${toInstall.join(", ")}`));
|
|
11555
10426
|
} else {
|
|
11556
10427
|
let installed = 0;
|
|
11557
10428
|
for (const componentId of toInstall) {
|
|
@@ -11580,14 +10451,14 @@ async function fixCommand(opts = {}) {
|
|
|
11580
10451
|
installed++;
|
|
11581
10452
|
} catch (err) {
|
|
11582
10453
|
console.log(
|
|
11583
|
-
|
|
10454
|
+
chalk18.yellow(` \u26A0 Failed to install ${componentId}: ${err instanceof Error ? err.message : "unknown"}`)
|
|
11584
10455
|
);
|
|
11585
10456
|
}
|
|
11586
10457
|
}
|
|
11587
10458
|
if (installed > 0) {
|
|
11588
10459
|
await dsm.save();
|
|
11589
10460
|
fixes.push(`Installed missing components: ${toInstall.join(", ")}`);
|
|
11590
|
-
console.log(
|
|
10461
|
+
console.log(chalk18.green(` \u2714 Installed missing components: ${toInstall.join(", ")}`));
|
|
11591
10462
|
}
|
|
11592
10463
|
}
|
|
11593
10464
|
}
|
|
@@ -11607,7 +10478,7 @@ async function fixCommand(opts = {}) {
|
|
|
11607
10478
|
if (syntaxFixed > 0) {
|
|
11608
10479
|
const verb = dryRun ? "Would fix" : "Fixed";
|
|
11609
10480
|
fixes.push(`${verb} syntax in ${syntaxFixed} file(s)`);
|
|
11610
|
-
console.log(
|
|
10481
|
+
console.log(chalk18.green(` \u2714 ${verb} syntax: ${syntaxFixed} file(s) (use client, metadata, quotes)`));
|
|
11611
10482
|
}
|
|
11612
10483
|
if (!skipQuality) {
|
|
11613
10484
|
let qualityFixCount = 0;
|
|
@@ -11625,7 +10496,7 @@ async function fixCommand(opts = {}) {
|
|
|
11625
10496
|
const uniqueFixes = [...new Set(qualityFixDetails)];
|
|
11626
10497
|
const verb = dryRun ? "Would fix" : "Fixed";
|
|
11627
10498
|
fixes.push(`${verb} quality in ${qualityFixCount} file(s)`);
|
|
11628
|
-
console.log(
|
|
10499
|
+
console.log(chalk18.green(` \u2714 ${verb} ${uniqueFixes.length} quality issue type(s): ${uniqueFixes.join(", ")}`));
|
|
11629
10500
|
}
|
|
11630
10501
|
}
|
|
11631
10502
|
let totalErrors = 0;
|
|
@@ -11670,13 +10541,13 @@ async function fixCommand(opts = {}) {
|
|
|
11670
10541
|
if (dryRun) {
|
|
11671
10542
|
fixes.push(`Would update ${o.id} path to ${newPath}`);
|
|
11672
10543
|
} else {
|
|
11673
|
-
console.log(
|
|
10544
|
+
console.log(chalk18.green(` \u2714 Updated ${o.id} (${o.name}) path \u2192 ${newPath}`));
|
|
11674
10545
|
}
|
|
11675
10546
|
} else {
|
|
11676
10547
|
if (dryRun) {
|
|
11677
10548
|
fixes.push(`Would remove orphaned ${o.id} (${o.name})`);
|
|
11678
10549
|
} else {
|
|
11679
|
-
console.log(
|
|
10550
|
+
console.log(chalk18.green(` \u2714 Removed orphaned ${o.id} (${o.name}) \u2014 file missing`));
|
|
11680
10551
|
}
|
|
11681
10552
|
}
|
|
11682
10553
|
}
|
|
@@ -11689,7 +10560,7 @@ async function fixCommand(opts = {}) {
|
|
|
11689
10560
|
entry.usedIn = fullActual;
|
|
11690
10561
|
manifestModified = true;
|
|
11691
10562
|
if (!dryRun) {
|
|
11692
|
-
console.log(
|
|
10563
|
+
console.log(chalk18.green(` \u2714 Updated ${entry.id} usedIn: ${fullActual.join(", ") || "none"}`));
|
|
11693
10564
|
}
|
|
11694
10565
|
}
|
|
11695
10566
|
}
|
|
@@ -11704,10 +10575,12 @@ async function fixCommand(opts = {}) {
|
|
|
11704
10575
|
file: comp.file,
|
|
11705
10576
|
usedIn: comp.usedIn,
|
|
11706
10577
|
description: "Auto-registered by fix",
|
|
11707
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10578
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10579
|
+
dependencies: [],
|
|
10580
|
+
source: "extracted"
|
|
11708
10581
|
});
|
|
11709
10582
|
manifest.nextId++;
|
|
11710
|
-
console.log(
|
|
10583
|
+
console.log(chalk18.green(` \u2714 Registered ${id} (${comp.name}) from ${comp.file}`));
|
|
11711
10584
|
} else {
|
|
11712
10585
|
fixes.push(`Would register ${comp.name} from ${comp.file}`);
|
|
11713
10586
|
}
|
|
@@ -11727,33 +10600,33 @@ async function fixCommand(opts = {}) {
|
|
|
11727
10600
|
} catch {
|
|
11728
10601
|
}
|
|
11729
10602
|
if (fixes.length === 0 && totalErrors === 0 && totalWarnings === 0 && remaining.length === 0) {
|
|
11730
|
-
console.log(
|
|
11731
|
-
console.log(
|
|
10603
|
+
console.log(chalk18.green("\n \u2705 Everything looks good \u2014 no issues found\n"));
|
|
10604
|
+
console.log(chalk18.cyan(" Run: coherent preview\n"));
|
|
11732
10605
|
return;
|
|
11733
10606
|
}
|
|
11734
10607
|
if (fixes.length > 0) console.log("");
|
|
11735
10608
|
if (totalErrors > 0 || totalWarnings > 0 || remaining.length > 0) {
|
|
11736
|
-
console.log(
|
|
11737
|
-
console.log(
|
|
10609
|
+
console.log(chalk18.dim(" \u2500".repeat(25)));
|
|
10610
|
+
console.log(chalk18.yellow(`
|
|
11738
10611
|
Remaining (need manual fix or AI):`));
|
|
11739
10612
|
for (const { path: path4, report } of fileIssues) {
|
|
11740
|
-
console.log(
|
|
10613
|
+
console.log(chalk18.dim(` \u{1F4C4} ${path4}`));
|
|
11741
10614
|
console.log(report);
|
|
11742
10615
|
}
|
|
11743
10616
|
for (const r of remaining) {
|
|
11744
|
-
console.log(
|
|
10617
|
+
console.log(chalk18.yellow(` \u26A0 ${r}`));
|
|
11745
10618
|
}
|
|
11746
10619
|
console.log("");
|
|
11747
10620
|
const parts = [];
|
|
11748
|
-
if (totalErrors > 0) parts.push(
|
|
11749
|
-
if (totalWarnings > 0) parts.push(
|
|
10621
|
+
if (totalErrors > 0) parts.push(chalk18.red(`\u274C ${totalErrors} error(s)`));
|
|
10622
|
+
if (totalWarnings > 0) parts.push(chalk18.yellow(`\u26A0 ${totalWarnings} warning(s)`));
|
|
11750
10623
|
if (parts.length > 0) console.log(` ${parts.join(" ")}`);
|
|
11751
10624
|
}
|
|
11752
|
-
console.log(
|
|
10625
|
+
console.log(chalk18.cyan("\n Run: coherent preview\n"));
|
|
11753
10626
|
}
|
|
11754
10627
|
|
|
11755
10628
|
// src/commands/check.ts
|
|
11756
|
-
import
|
|
10629
|
+
import chalk19 from "chalk";
|
|
11757
10630
|
import { resolve as resolve14 } from "path";
|
|
11758
10631
|
import { readdirSync as readdirSync9, readFileSync as readFileSync17, statSync as statSync3, existsSync as existsSync23 } from "fs";
|
|
11759
10632
|
import { loadManifest as loadManifest11 } from "@getcoherent/core";
|
|
@@ -11801,7 +10674,7 @@ async function checkCommand(opts = {}) {
|
|
|
11801
10674
|
const appDir = resolve14(projectRoot, "app");
|
|
11802
10675
|
const files = findTsxFiles(appDir);
|
|
11803
10676
|
result.pages.total = files.length;
|
|
11804
|
-
if (!opts.json) console.log(
|
|
10677
|
+
if (!opts.json) console.log(chalk19.cyan("\n \u{1F4C4} Pages") + chalk19.dim(` (${files.length} scanned)
|
|
11805
10678
|
`));
|
|
11806
10679
|
const autoFixableTypes = /* @__PURE__ */ new Set([
|
|
11807
10680
|
"RAW_COLOR",
|
|
@@ -11831,7 +10704,7 @@ async function checkCommand(opts = {}) {
|
|
|
11831
10704
|
result.autoFixable += fileAutoFixable;
|
|
11832
10705
|
if (filteredIssues.length === 0) {
|
|
11833
10706
|
result.pages.clean++;
|
|
11834
|
-
if (!opts.json) console.log(
|
|
10707
|
+
if (!opts.json) console.log(chalk19.green(` \u2714 ${relativePath}`) + chalk19.dim(" \u2014 clean"));
|
|
11835
10708
|
continue;
|
|
11836
10709
|
}
|
|
11837
10710
|
if (errors > 0) result.pages.withErrors++;
|
|
@@ -11844,9 +10717,9 @@ async function checkCommand(opts = {}) {
|
|
|
11844
10717
|
});
|
|
11845
10718
|
if (!opts.json) {
|
|
11846
10719
|
const parts = [];
|
|
11847
|
-
if (errors > 0) parts.push(
|
|
11848
|
-
if (warnings > 0) parts.push(
|
|
11849
|
-
console.log(
|
|
10720
|
+
if (errors > 0) parts.push(chalk19.red(`${errors} error(s)`));
|
|
10721
|
+
if (warnings > 0) parts.push(chalk19.yellow(`${warnings} warning(s)`));
|
|
10722
|
+
console.log(chalk19.yellow(` \u26A0 ${relativePath}`) + chalk19.dim(` \u2014 ${parts.join(", ")}`));
|
|
11850
10723
|
console.log(formatIssues(filteredIssues));
|
|
11851
10724
|
}
|
|
11852
10725
|
}
|
|
@@ -11872,15 +10745,15 @@ async function checkCommand(opts = {}) {
|
|
|
11872
10745
|
}
|
|
11873
10746
|
}
|
|
11874
10747
|
if (!opts.json && result.links.broken.length > 0) {
|
|
11875
|
-
console.log(
|
|
11876
|
-
\u{1F517} Internal Links`) +
|
|
10748
|
+
console.log(chalk19.yellow(`
|
|
10749
|
+
\u{1F517} Internal Links`) + chalk19.dim(` (${result.links.total} scanned)
|
|
11877
10750
|
`));
|
|
11878
10751
|
for (const b of result.links.broken) {
|
|
11879
|
-
console.log(
|
|
10752
|
+
console.log(chalk19.red(` \u2717 ${b.file}:${b.line}`) + chalk19.dim(` \u2192 ${b.href} (route does not exist)`));
|
|
11880
10753
|
}
|
|
11881
10754
|
} else if (!opts.json && result.links.total > 0) {
|
|
11882
|
-
console.log(
|
|
11883
|
-
\u{1F517} Internal Links`) +
|
|
10755
|
+
console.log(chalk19.green(`
|
|
10756
|
+
\u{1F517} Internal Links`) + chalk19.dim(` \u2014 all ${result.links.total} links resolve \u2713`));
|
|
11884
10757
|
}
|
|
11885
10758
|
try {
|
|
11886
10759
|
const manifest = await loadManifest11(project.root);
|
|
@@ -11889,7 +10762,7 @@ async function checkCommand(opts = {}) {
|
|
|
11889
10762
|
const fullPath = resolve14(project.root, entry.file);
|
|
11890
10763
|
if (!existsSync23(fullPath)) {
|
|
11891
10764
|
result.pages.withErrors++;
|
|
11892
|
-
if (!opts.json) console.log(
|
|
10765
|
+
if (!opts.json) console.log(chalk19.red(`
|
|
11893
10766
|
\u2717 Missing shared component file: ${entry.id} (${entry.file})`));
|
|
11894
10767
|
}
|
|
11895
10768
|
}
|
|
@@ -11901,8 +10774,8 @@ async function checkCommand(opts = {}) {
|
|
|
11901
10774
|
try {
|
|
11902
10775
|
const manifest = await loadManifest11(projectRoot);
|
|
11903
10776
|
if (!opts.json && manifest.shared.length > 0) {
|
|
11904
|
-
console.log(
|
|
11905
|
-
\u{1F9E9} Shared Components`) +
|
|
10777
|
+
console.log(chalk19.cyan(`
|
|
10778
|
+
\u{1F9E9} Shared Components`) + chalk19.dim(` (${manifest.shared.length} registered)
|
|
11906
10779
|
`));
|
|
11907
10780
|
}
|
|
11908
10781
|
let consistent = 0;
|
|
@@ -11916,8 +10789,8 @@ async function checkCommand(opts = {}) {
|
|
|
11916
10789
|
if (!fileExists) {
|
|
11917
10790
|
_orphaned++;
|
|
11918
10791
|
if (!opts.json) {
|
|
11919
|
-
console.log(
|
|
11920
|
-
console.log(
|
|
10792
|
+
console.log(chalk19.red(` \u274C ${entry.id} (${entry.name}) \u2014 file missing: ${entry.file}`));
|
|
10793
|
+
console.log(chalk19.dim(` Fix: coherent fix or coherent sync`));
|
|
11921
10794
|
}
|
|
11922
10795
|
continue;
|
|
11923
10796
|
}
|
|
@@ -11928,11 +10801,11 @@ async function checkCommand(opts = {}) {
|
|
|
11928
10801
|
_nameMismatch++;
|
|
11929
10802
|
if (!opts.json) {
|
|
11930
10803
|
console.log(
|
|
11931
|
-
|
|
10804
|
+
chalk19.yellow(
|
|
11932
10805
|
` \u26A0 ${entry.id} \u2014 manifest name "${entry.name}" doesn't match export "${actualExports[0]}"`
|
|
11933
10806
|
)
|
|
11934
10807
|
);
|
|
11935
|
-
console.log(
|
|
10808
|
+
console.log(chalk19.dim(` Fix: coherent sync`));
|
|
11936
10809
|
}
|
|
11937
10810
|
}
|
|
11938
10811
|
} catch {
|
|
@@ -11947,35 +10820,35 @@ async function checkCommand(opts = {}) {
|
|
|
11947
10820
|
if (totalUsage === 0) {
|
|
11948
10821
|
unused++;
|
|
11949
10822
|
if (!opts.json) {
|
|
11950
|
-
console.log(
|
|
11951
|
-
console.log(
|
|
10823
|
+
console.log(chalk19.blue(` \u2139 ${entry.id} (${entry.name}) \u2014 registered but not used anywhere`));
|
|
10824
|
+
console.log(chalk19.dim(` Remove: coherent components shared remove ${entry.id}`));
|
|
11952
10825
|
}
|
|
11953
10826
|
} else {
|
|
11954
10827
|
consistent++;
|
|
11955
10828
|
const usageDesc = inLayout ? `layout + ${actualUsedIn.length} page(s)` : `${actualUsedIn.length} page(s)`;
|
|
11956
10829
|
if (!opts.json) {
|
|
11957
|
-
const staleNote = isStale ?
|
|
11958
|
-
console.log(
|
|
10830
|
+
const staleNote = isStale ? chalk19.yellow(" [usedIn stale]") : "";
|
|
10831
|
+
console.log(chalk19.green(` \u2714 ${entry.id} (${entry.name})`) + chalk19.dim(` \u2014 ${usageDesc}`) + staleNote);
|
|
11959
10832
|
}
|
|
11960
10833
|
}
|
|
11961
10834
|
}
|
|
11962
10835
|
const unregistered = findUnregisteredComponents(projectRoot, manifest);
|
|
11963
10836
|
if (unregistered.length > 0 && !opts.json) {
|
|
11964
|
-
console.log(
|
|
10837
|
+
console.log(chalk19.cyan(`
|
|
11965
10838
|
\u{1F4E6} Unregistered components found:`));
|
|
11966
10839
|
for (const comp of unregistered) {
|
|
11967
|
-
console.log(
|
|
11968
|
-
console.log(
|
|
10840
|
+
console.log(chalk19.blue(` \u2139 ${comp.name}`) + chalk19.dim(` \u2014 ${comp.file} (not in manifest)`));
|
|
10841
|
+
console.log(chalk19.dim(` Register: coherent sync`));
|
|
11969
10842
|
}
|
|
11970
10843
|
}
|
|
11971
10844
|
const inlineDupes = findInlineDuplicates(projectRoot, manifest);
|
|
11972
10845
|
if (inlineDupes.length > 0 && !opts.json) {
|
|
11973
|
-
console.log(
|
|
10846
|
+
console.log(chalk19.cyan(`
|
|
11974
10847
|
\u{1F50D} Inline duplicates:`));
|
|
11975
10848
|
for (const dup of inlineDupes) {
|
|
11976
|
-
console.log(
|
|
10849
|
+
console.log(chalk19.yellow(` \u26A0 ${dup.pageFile}`) + chalk19.dim(` has inline ${dup.componentName}`));
|
|
11977
10850
|
console.log(
|
|
11978
|
-
|
|
10851
|
+
chalk19.dim(
|
|
11979
10852
|
` Use shared: import { ${dup.componentName} } from "@/${dup.sharedFile.replace(".tsx", "")}"`
|
|
11980
10853
|
)
|
|
11981
10854
|
);
|
|
@@ -11998,28 +10871,62 @@ async function checkCommand(opts = {}) {
|
|
|
11998
10871
|
} catch {
|
|
11999
10872
|
}
|
|
12000
10873
|
}
|
|
10874
|
+
if (!skipShared) {
|
|
10875
|
+
try {
|
|
10876
|
+
const { validateReuse } = await import("./reuse-validator-HC4LZEKF.js");
|
|
10877
|
+
const { inferPageTypeFromRoute: inferPageTypeFromRoute2 } = await import("./design-constraints-EIP2XM7T.js");
|
|
10878
|
+
const manifest = await loadManifest11(projectRoot);
|
|
10879
|
+
const appDir = resolve14(projectRoot, "app");
|
|
10880
|
+
const pageFiles = existsSync23(appDir) ? findTsxFiles(appDir) : [];
|
|
10881
|
+
if (manifest.shared.length > 0 && pageFiles.length > 0) {
|
|
10882
|
+
const reuseWarnings = [];
|
|
10883
|
+
for (const file of pageFiles) {
|
|
10884
|
+
const code = readFileSync17(file, "utf-8");
|
|
10885
|
+
const relativePath = file.replace(projectRoot + "/", "");
|
|
10886
|
+
const route = "/" + relativePath.replace(/^app\//, "").replace(/\/page\.tsx$/, "").replace(/^\(.*?\)\//, "");
|
|
10887
|
+
const pageType = inferPageTypeFromRoute2(route);
|
|
10888
|
+
const warnings = validateReuse(manifest, code, pageType);
|
|
10889
|
+
for (const w of warnings) {
|
|
10890
|
+
reuseWarnings.push({ file: relativePath, message: w.message });
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
if (reuseWarnings.length > 0 && !opts.json) {
|
|
10894
|
+
console.log(chalk19.yellow(`
|
|
10895
|
+
\u{1F504} Reuse Warnings`) + chalk19.dim(` (${reuseWarnings.length} found)
|
|
10896
|
+
`));
|
|
10897
|
+
for (const w of reuseWarnings.slice(0, 10)) {
|
|
10898
|
+
console.log(chalk19.yellow(` \u26A0 ${w.file}:`) + chalk19.dim(` ${w.message}`));
|
|
10899
|
+
}
|
|
10900
|
+
if (reuseWarnings.length > 10) {
|
|
10901
|
+
console.log(chalk19.dim(` ... and ${reuseWarnings.length - 10} more`));
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
}
|
|
10905
|
+
} catch {
|
|
10906
|
+
}
|
|
10907
|
+
}
|
|
12001
10908
|
if (opts.json) {
|
|
12002
10909
|
console.log(JSON.stringify(result, null, 2));
|
|
12003
10910
|
return;
|
|
12004
10911
|
}
|
|
12005
|
-
console.log(
|
|
10912
|
+
console.log(chalk19.dim("\n " + "\u2500".repeat(50)));
|
|
12006
10913
|
const summaryParts = [];
|
|
12007
10914
|
if (!skipPages) {
|
|
12008
|
-
summaryParts.push(`${
|
|
12009
|
-
if (result.pages.withErrors > 0) summaryParts.push(
|
|
12010
|
-
if (result.pages.withWarnings > 0) summaryParts.push(
|
|
10915
|
+
summaryParts.push(`${chalk19.green(`${result.pages.clean} clean`)} pages`);
|
|
10916
|
+
if (result.pages.withErrors > 0) summaryParts.push(chalk19.red(`${result.pages.withErrors} with errors`));
|
|
10917
|
+
if (result.pages.withWarnings > 0) summaryParts.push(chalk19.yellow(`${result.pages.withWarnings} with warnings`));
|
|
12011
10918
|
}
|
|
12012
10919
|
if (!skipShared && result.shared.total > 0) {
|
|
12013
10920
|
summaryParts.push(`${result.shared.consistent} healthy shared`);
|
|
12014
10921
|
if (result.shared.unused > 0) summaryParts.push(`${result.shared.unused} unused`);
|
|
12015
10922
|
}
|
|
12016
10923
|
if (result.links.broken.length > 0) {
|
|
12017
|
-
summaryParts.push(
|
|
10924
|
+
summaryParts.push(chalk19.red(`${result.links.broken.length} broken link(s)`));
|
|
12018
10925
|
}
|
|
12019
10926
|
console.log(`
|
|
12020
10927
|
${summaryParts.join(" | ")}`);
|
|
12021
10928
|
if (result.autoFixable > 0) {
|
|
12022
|
-
console.log(
|
|
10929
|
+
console.log(chalk19.cyan(`
|
|
12023
10930
|
Auto-fixable: ${result.autoFixable} issues. Run: coherent fix`));
|
|
12024
10931
|
}
|
|
12025
10932
|
console.log("");
|
|
@@ -12028,21 +10935,21 @@ async function checkCommand(opts = {}) {
|
|
|
12028
10935
|
}
|
|
12029
10936
|
|
|
12030
10937
|
// src/commands/repair.ts
|
|
12031
|
-
import
|
|
10938
|
+
import chalk20 from "chalk";
|
|
12032
10939
|
async function repairCommand() {
|
|
12033
|
-
console.log(
|
|
10940
|
+
console.log(chalk20.dim(" \u2139\uFE0F `coherent repair` is deprecated \u2014 use `coherent fix` instead\n"));
|
|
12034
10941
|
await fixCommand();
|
|
12035
10942
|
}
|
|
12036
10943
|
|
|
12037
10944
|
// src/commands/doctor.ts
|
|
12038
|
-
import
|
|
10945
|
+
import chalk21 from "chalk";
|
|
12039
10946
|
async function doctorCommand() {
|
|
12040
|
-
console.log(
|
|
10947
|
+
console.log(chalk21.dim(" \u2139\uFE0F `coherent doctor` is deprecated \u2014 use `coherent fix` instead\n"));
|
|
12041
10948
|
await fixCommand();
|
|
12042
10949
|
}
|
|
12043
10950
|
|
|
12044
10951
|
// src/commands/rules.ts
|
|
12045
|
-
import
|
|
10952
|
+
import chalk22 from "chalk";
|
|
12046
10953
|
async function rulesCommand() {
|
|
12047
10954
|
try {
|
|
12048
10955
|
const result = await regenerateCursorRules();
|
|
@@ -12053,31 +10960,31 @@ async function rulesCommand() {
|
|
|
12053
10960
|
if (result.sharedCount !== void 0) parts.push(`${result.sharedCount} shared components`);
|
|
12054
10961
|
if (result.tokenKeys !== void 0) parts.push(`${result.tokenKeys} design token keys`);
|
|
12055
10962
|
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
12056
|
-
console.log(
|
|
10963
|
+
console.log(chalk22.green(`\u2714 Updated .cursorrules and CLAUDE.md${summary}
|
|
12057
10964
|
`));
|
|
12058
10965
|
} catch (error) {
|
|
12059
|
-
console.error(
|
|
10966
|
+
console.error(chalk22.red("\u274C Command failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
12060
10967
|
process.exit(1);
|
|
12061
10968
|
}
|
|
12062
10969
|
}
|
|
12063
10970
|
|
|
12064
10971
|
// src/commands/validate.ts
|
|
12065
|
-
import
|
|
10972
|
+
import chalk23 from "chalk";
|
|
12066
10973
|
async function validateCommand() {
|
|
12067
|
-
console.log(
|
|
10974
|
+
console.log(chalk23.dim(" \u2139\uFE0F `coherent validate` is deprecated \u2014 use `coherent check` instead\n"));
|
|
12068
10975
|
await checkCommand({ pages: true });
|
|
12069
10976
|
}
|
|
12070
10977
|
|
|
12071
10978
|
// src/commands/audit.ts
|
|
12072
|
-
import
|
|
10979
|
+
import chalk24 from "chalk";
|
|
12073
10980
|
async function auditCommand(options) {
|
|
12074
|
-
console.log(
|
|
10981
|
+
console.log(chalk24.dim(" \u2139\uFE0F `coherent audit` is deprecated \u2014 use `coherent check` instead\n"));
|
|
12075
10982
|
await checkCommand({ shared: true, json: options.json });
|
|
12076
10983
|
}
|
|
12077
10984
|
|
|
12078
10985
|
// src/commands/components.ts
|
|
12079
10986
|
import { Command } from "commander";
|
|
12080
|
-
import
|
|
10987
|
+
import chalk25 from "chalk";
|
|
12081
10988
|
import {
|
|
12082
10989
|
DesignSystemManager as DesignSystemManager12,
|
|
12083
10990
|
ComponentManager as ComponentManager7,
|
|
@@ -12128,19 +11035,27 @@ function createComponentsCommand() {
|
|
|
12128
11035
|
console.log(JSON.stringify({ shared: manifest.shared, ui: installed2 }, null, 2));
|
|
12129
11036
|
return;
|
|
12130
11037
|
}
|
|
12131
|
-
console.log(
|
|
11038
|
+
console.log(chalk25.bold("\n\u{1F4E6} Shared Components"));
|
|
12132
11039
|
if (manifest.shared.length === 0) {
|
|
12133
|
-
console.log(
|
|
11040
|
+
console.log(chalk25.gray(" None yet. Generate pages with header/footer to create them.\n"));
|
|
12134
11041
|
} else {
|
|
12135
|
-
const order = {
|
|
11042
|
+
const order = {
|
|
11043
|
+
layout: 0,
|
|
11044
|
+
navigation: 1,
|
|
11045
|
+
"data-display": 2,
|
|
11046
|
+
form: 3,
|
|
11047
|
+
feedback: 4,
|
|
11048
|
+
section: 5,
|
|
11049
|
+
widget: 6
|
|
11050
|
+
};
|
|
12136
11051
|
const sorted = [...manifest.shared].sort(
|
|
12137
11052
|
(a, b) => (order[a.type] ?? 9) - (order[b.type] ?? 9) || a.name.localeCompare(b.name)
|
|
12138
11053
|
);
|
|
12139
11054
|
console.log("");
|
|
12140
11055
|
sorted.forEach((entry) => {
|
|
12141
|
-
const usage = entry.usedIn.length === 0 ?
|
|
11056
|
+
const usage = entry.usedIn.length === 0 ? chalk25.gray("unused") : entry.usedIn.includes("app/layout.tsx") ? chalk25.green("all pages") : chalk25.gray(entry.usedIn.join(", "));
|
|
12142
11057
|
console.log(
|
|
12143
|
-
` ${
|
|
11058
|
+
` ${chalk25.cyan(entry.id.padEnd(8))} ${chalk25.white(entry.name.padEnd(18))} ${chalk25.gray(entry.type.padEnd(9))} ${usage}`
|
|
12144
11059
|
);
|
|
12145
11060
|
});
|
|
12146
11061
|
console.log("");
|
|
@@ -12149,24 +11064,24 @@ function createComponentsCommand() {
|
|
|
12149
11064
|
const availableShadcn = listShadcnComponents();
|
|
12150
11065
|
const installedIds = new Set(installed.map((c) => c.id));
|
|
12151
11066
|
const notInstalled = availableShadcn.filter((id) => !installedIds.has(id));
|
|
12152
|
-
console.log(
|
|
11067
|
+
console.log(chalk25.bold("\u{1F9E9} UI Components (shadcn)"));
|
|
12153
11068
|
if (installed.length === 0) {
|
|
12154
|
-
console.log(
|
|
11069
|
+
console.log(chalk25.gray(" None installed yet.\n"));
|
|
12155
11070
|
} else {
|
|
12156
11071
|
const names = installed.map((c) => c.name).sort();
|
|
12157
|
-
console.log(
|
|
11072
|
+
console.log(chalk25.green(` Installed (${names.length}): `) + chalk25.white(names.join(", ")));
|
|
12158
11073
|
}
|
|
12159
11074
|
if (notInstalled.length > 0) {
|
|
12160
|
-
console.log(
|
|
11075
|
+
console.log(chalk25.gray(` Available (${notInstalled.length}): `) + chalk25.gray(notInstalled.join(", ")));
|
|
12161
11076
|
}
|
|
12162
11077
|
console.log("");
|
|
12163
|
-
console.log(
|
|
12164
|
-
console.log(
|
|
12165
|
-
console.log(
|
|
11078
|
+
console.log(chalk25.cyan("\u{1F4A1} Commands:"));
|
|
11079
|
+
console.log(chalk25.white(' coherent chat "add a testimonial component"'));
|
|
11080
|
+
console.log(chalk25.white(' coherent chat --component "Header" "add a search button"'));
|
|
12166
11081
|
console.log("");
|
|
12167
11082
|
});
|
|
12168
11083
|
cmd.command("add <name>").description("Install a specific component").action(async (name) => {
|
|
12169
|
-
console.log(
|
|
11084
|
+
console.log(chalk25.yellow(`
|
|
12170
11085
|
\u{1F4A1} Use: coherent chat "add ${name} component"
|
|
12171
11086
|
`));
|
|
12172
11087
|
});
|
|
@@ -12179,25 +11094,35 @@ function createComponentsCommand() {
|
|
|
12179
11094
|
console.log(JSON.stringify(manifest, null, 2));
|
|
12180
11095
|
return;
|
|
12181
11096
|
}
|
|
12182
|
-
console.log(
|
|
11097
|
+
console.log(chalk25.bold("\n\u{1F4E6} Shared Components\n"));
|
|
12183
11098
|
if (manifest.shared.length === 0) {
|
|
12184
|
-
console.log(
|
|
12185
|
-
console.log(
|
|
11099
|
+
console.log(chalk25.yellow(" No shared components yet.\n"));
|
|
11100
|
+
console.log(chalk25.gray(' Create via chat: coherent chat "add a page with header and footer"\n'));
|
|
12186
11101
|
return;
|
|
12187
11102
|
}
|
|
12188
|
-
const order = {
|
|
12189
|
-
|
|
11103
|
+
const order = {
|
|
11104
|
+
layout: 0,
|
|
11105
|
+
navigation: 1,
|
|
11106
|
+
"data-display": 2,
|
|
11107
|
+
form: 3,
|
|
11108
|
+
feedback: 4,
|
|
11109
|
+
section: 5,
|
|
11110
|
+
widget: 6
|
|
11111
|
+
};
|
|
11112
|
+
const sorted = [...manifest.shared].sort(
|
|
11113
|
+
(a, b) => (order[a.type] ?? 9) - (order[b.type] ?? 9) || a.name.localeCompare(b.name)
|
|
11114
|
+
);
|
|
12190
11115
|
sorted.forEach((entry) => {
|
|
12191
|
-
const usedIn = entry.usedIn.length === 0 ?
|
|
12192
|
-
console.log(
|
|
11116
|
+
const usedIn = entry.usedIn.length === 0 ? chalk25.gray("(not used yet)") : entry.usedIn.length === 1 && entry.usedIn[0] === "app/layout.tsx" ? chalk25.gray("layout.tsx (all pages)") : chalk25.gray(`used in: ${entry.usedIn.join(", ")}`);
|
|
11117
|
+
console.log(chalk25.cyan(` ${entry.id}`), chalk25.white(entry.name), chalk25.gray(entry.type));
|
|
12193
11118
|
if (opts.verbose) {
|
|
12194
|
-
console.log(
|
|
12195
|
-
if (entry.description) console.log(
|
|
11119
|
+
console.log(chalk25.gray(` file: ${entry.file}`));
|
|
11120
|
+
if (entry.description) console.log(chalk25.gray(` ${entry.description}`));
|
|
12196
11121
|
}
|
|
12197
|
-
console.log(
|
|
11122
|
+
console.log(chalk25.gray(` ${usedIn}`));
|
|
12198
11123
|
console.log("");
|
|
12199
11124
|
});
|
|
12200
|
-
console.log(
|
|
11125
|
+
console.log(chalk25.cyan("\u{1F4A1} Modify by ID:"), chalk25.white('coherent chat "in CID-001 add a search button"\n'));
|
|
12201
11126
|
});
|
|
12202
11127
|
sharedCmd.command("add <name>").description("Create a shared component (layout/section/widget) and register in manifest").option("-t, --type <type>", "Type: layout | section | widget", "layout").option("-d, --description <desc>", "Description").action(async (name, opts) => {
|
|
12203
11128
|
const project = findConfig();
|
|
@@ -12209,12 +11134,12 @@ function createComponentsCommand() {
|
|
|
12209
11134
|
description: opts.description,
|
|
12210
11135
|
usedIn: type === "layout" ? ["app/layout.tsx"] : []
|
|
12211
11136
|
});
|
|
12212
|
-
console.log(
|
|
11137
|
+
console.log(chalk25.green(`
|
|
12213
11138
|
\u2705 Created ${result.id} (${result.name}) at ${result.file}
|
|
12214
11139
|
`));
|
|
12215
11140
|
if (type === "layout") {
|
|
12216
11141
|
const updated = await integrateSharedLayoutIntoRootLayout3(project.root);
|
|
12217
|
-
if (updated) console.log(
|
|
11142
|
+
if (updated) console.log(chalk25.cyan(" Updated app/layout.tsx to use shared layout components.\n"));
|
|
12218
11143
|
}
|
|
12219
11144
|
const sharedPagePath = resolve15(project.root, "app/design-system/shared/page.tsx");
|
|
12220
11145
|
if (!existsSync24(sharedPagePath)) {
|
|
@@ -12224,23 +11149,23 @@ function createComponentsCommand() {
|
|
|
12224
11149
|
const config2 = dsm.getConfig();
|
|
12225
11150
|
const written = await writeDesignSystemFiles(project.root, config2, { sharedOnly: true });
|
|
12226
11151
|
if (written.length > 0) {
|
|
12227
|
-
console.log(
|
|
11152
|
+
console.log(chalk25.cyan(" Added Design System shared pages: /design-system/shared\n"));
|
|
12228
11153
|
}
|
|
12229
11154
|
} catch (e) {
|
|
12230
|
-
if (process.env.COHERENT_DEBUG === "1") console.error(
|
|
11155
|
+
if (process.env.COHERENT_DEBUG === "1") console.error(chalk25.dim("DS shared pages write failed:"), e);
|
|
12231
11156
|
}
|
|
12232
11157
|
}
|
|
12233
11158
|
try {
|
|
12234
11159
|
await writeCursorRules(project.root);
|
|
12235
11160
|
} catch (e) {
|
|
12236
|
-
if (process.env.COHERENT_DEBUG === "1") console.error(
|
|
11161
|
+
if (process.env.COHERENT_DEBUG === "1") console.error(chalk25.dim("Could not update .cursorrules:"), e);
|
|
12237
11162
|
}
|
|
12238
11163
|
});
|
|
12239
11164
|
return cmd;
|
|
12240
11165
|
}
|
|
12241
11166
|
|
|
12242
11167
|
// src/commands/import-cmd.ts
|
|
12243
|
-
import
|
|
11168
|
+
import chalk26 from "chalk";
|
|
12244
11169
|
import ora6 from "ora";
|
|
12245
11170
|
import { writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
|
|
12246
11171
|
import { resolve as resolve16, join as join18, dirname as dirname9 } from "path";
|
|
@@ -12345,25 +11270,25 @@ function createImportCommand() {
|
|
|
12345
11270
|
}
|
|
12346
11271
|
async function importFigmaAction(urlOrKey, opts) {
|
|
12347
11272
|
if (typeof urlOrKey !== "string" || !urlOrKey.trim()) {
|
|
12348
|
-
console.error(
|
|
12349
|
-
console.log(
|
|
11273
|
+
console.error(chalk26.red("\n\u274C Figma URL or file key is required.\n"));
|
|
11274
|
+
console.log(chalk26.dim(" Usage: coherent import figma <url-or-key> --token <your-token>\n"));
|
|
12350
11275
|
process.exit(1);
|
|
12351
11276
|
}
|
|
12352
11277
|
const token = opts.token ?? process.env.FIGMA_ACCESS_TOKEN ?? process.env.FIGMA_TOKEN;
|
|
12353
11278
|
if (!token || typeof token !== "string") {
|
|
12354
|
-
console.error(
|
|
12355
|
-
console.log(
|
|
12356
|
-
console.log(
|
|
12357
|
-
console.log(
|
|
11279
|
+
console.error(chalk26.red("\n\u274C Figma token required.\n"));
|
|
11280
|
+
console.log(chalk26.dim(" Use: coherent import figma <url-or-key> --token <your-token>"));
|
|
11281
|
+
console.log(chalk26.dim(" Or set FIGMA_ACCESS_TOKEN or FIGMA_TOKEN in your environment.\n"));
|
|
11282
|
+
console.log(chalk26.dim(" Get a token: Figma \u2192 Settings \u2192 Personal access tokens.\n"));
|
|
12358
11283
|
process.exit(1);
|
|
12359
11284
|
}
|
|
12360
11285
|
const generatePages = opts.pages !== false;
|
|
12361
11286
|
const dryRun = Boolean(opts.dryRun);
|
|
12362
11287
|
const fileKey = FigmaClient.extractFileKey(urlOrKey);
|
|
12363
11288
|
if (!fileKey) {
|
|
12364
|
-
console.error(
|
|
12365
|
-
console.log(
|
|
12366
|
-
console.log(
|
|
11289
|
+
console.error(chalk26.red("\n\u274C Invalid Figma URL or file key.\n"));
|
|
11290
|
+
console.log(chalk26.dim(" Use a URL like: https://www.figma.com/file/ABC123/MyDesign"));
|
|
11291
|
+
console.log(chalk26.dim(" Or the file key: ABC123\n"));
|
|
12367
11292
|
process.exit(1);
|
|
12368
11293
|
}
|
|
12369
11294
|
const project = findConfig();
|
|
@@ -12540,7 +11465,7 @@ export const config = ${JSON.stringify(fullConfig, null, 2)} as const
|
|
|
12540
11465
|
try {
|
|
12541
11466
|
await writeCursorRules(projectRoot);
|
|
12542
11467
|
} catch (e) {
|
|
12543
|
-
if (process.env.COHERENT_DEBUG === "1") console.error(
|
|
11468
|
+
if (process.env.COHERENT_DEBUG === "1") console.error(chalk26.dim("Could not update .cursorrules:"), e);
|
|
12544
11469
|
}
|
|
12545
11470
|
} else {
|
|
12546
11471
|
stats.filesWritten.push(DESIGN_SYSTEM_CONFIG_PATH);
|
|
@@ -12550,7 +11475,7 @@ export const config = ${JSON.stringify(fullConfig, null, 2)} as const
|
|
|
12550
11475
|
} catch (err) {
|
|
12551
11476
|
spinner.fail("Import failed");
|
|
12552
11477
|
const message = err instanceof Error ? err.message : String(err);
|
|
12553
|
-
console.error(
|
|
11478
|
+
console.error(chalk26.red("\n\u274C " + message + "\n"));
|
|
12554
11479
|
process.exit(1);
|
|
12555
11480
|
}
|
|
12556
11481
|
}
|
|
@@ -12558,36 +11483,36 @@ function printReport(stats, opts) {
|
|
|
12558
11483
|
const { dryRun, generatePages, fileName } = opts;
|
|
12559
11484
|
console.log("");
|
|
12560
11485
|
if (dryRun) {
|
|
12561
|
-
console.log(
|
|
11486
|
+
console.log(chalk26.yellow("\u2550\u2550\u2550 Dry run (no files written) \u2550\u2550\u2550"));
|
|
12562
11487
|
console.log("");
|
|
12563
11488
|
}
|
|
12564
|
-
console.log(
|
|
11489
|
+
console.log(chalk26.green("\u2705 Figma import complete"));
|
|
12565
11490
|
console.log("");
|
|
12566
|
-
console.log(
|
|
12567
|
-
console.log(
|
|
12568
|
-
console.log(
|
|
12569
|
-
console.log(
|
|
12570
|
-
console.log(
|
|
11491
|
+
console.log(chalk26.cyan(" Statistics"));
|
|
11492
|
+
console.log(chalk26.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
11493
|
+
console.log(chalk26.blue(` File: ${fileName}`));
|
|
11494
|
+
console.log(chalk26.blue(` Color styles: ${stats.colorStyles}`));
|
|
11495
|
+
console.log(chalk26.blue(` Text styles: ${stats.textStyles}`));
|
|
12571
11496
|
console.log(
|
|
12572
|
-
|
|
11497
|
+
chalk26.blue(` Components: ${stats.componentsTotal} (${stats.baseCount} \u2192 base, ${stats.sharedCount} \u2192 shared)`)
|
|
12573
11498
|
);
|
|
12574
|
-
console.log(
|
|
12575
|
-
console.log(
|
|
12576
|
-
console.log(
|
|
12577
|
-
console.log(
|
|
11499
|
+
console.log(chalk26.blue(` Pages: ${stats.pagesGenerated}${!generatePages ? " (skipped by --no-pages)" : ""}`));
|
|
11500
|
+
console.log(chalk26.blue(` design-system.config: ${stats.configUpdated ? "updated" : "\u2014"}`));
|
|
11501
|
+
console.log(chalk26.blue(` Layout (Header/Footer): ${stats.layoutIntegrated ? "integrated" : "\u2014"}`));
|
|
11502
|
+
console.log(chalk26.blue(` DS viewer files: ${stats.dsFilesWritten}`));
|
|
12578
11503
|
console.log(
|
|
12579
|
-
|
|
11504
|
+
chalk26.blue(` Total files ${dryRun ? "that would be written" : "written"}: ${stats.filesWritten.length}`)
|
|
12580
11505
|
);
|
|
12581
11506
|
console.log("");
|
|
12582
11507
|
if (stats.filesWritten.length > 0 && stats.filesWritten.length <= 30) {
|
|
12583
|
-
console.log(
|
|
12584
|
-
stats.filesWritten.forEach((f) => console.log(
|
|
11508
|
+
console.log(chalk26.dim(" Files:"));
|
|
11509
|
+
stats.filesWritten.forEach((f) => console.log(chalk26.dim(` ${f}`)));
|
|
12585
11510
|
console.log("");
|
|
12586
11511
|
}
|
|
12587
11512
|
}
|
|
12588
11513
|
|
|
12589
11514
|
// src/commands/ds.ts
|
|
12590
|
-
import
|
|
11515
|
+
import chalk27 from "chalk";
|
|
12591
11516
|
import ora7 from "ora";
|
|
12592
11517
|
import { DesignSystemManager as DesignSystemManager14 } from "@getcoherent/core";
|
|
12593
11518
|
async function dsRegenerateCommand() {
|
|
@@ -12602,16 +11527,16 @@ async function dsRegenerateCommand() {
|
|
|
12602
11527
|
const config2 = dsm.getConfig();
|
|
12603
11528
|
const written = await writeDesignSystemFiles(project.root, config2);
|
|
12604
11529
|
spinner.succeed(`Regenerated ${written.length} Design System file(s)`);
|
|
12605
|
-
console.log(
|
|
12606
|
-
console.log(
|
|
11530
|
+
console.log(chalk27.gray(" app/design-system/* and app/api/design-system/*\n"));
|
|
11531
|
+
console.log(chalk27.cyan(" Open /design-system in the app to view.\n"));
|
|
12607
11532
|
} catch (error) {
|
|
12608
|
-
console.error(
|
|
11533
|
+
console.error(chalk27.red("\u274C Command failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
12609
11534
|
process.exit(1);
|
|
12610
11535
|
}
|
|
12611
11536
|
}
|
|
12612
11537
|
|
|
12613
11538
|
// src/commands/update.ts
|
|
12614
|
-
import
|
|
11539
|
+
import chalk28 from "chalk";
|
|
12615
11540
|
import ora8 from "ora";
|
|
12616
11541
|
import { readFileSync as readFileSync18, existsSync as existsSync26 } from "fs";
|
|
12617
11542
|
import { join as join19 } from "path";
|
|
@@ -12663,14 +11588,14 @@ async function updateCommand(opts) {
|
|
|
12663
11588
|
const projectVersion = config2.coherentVersion || "0.0.0";
|
|
12664
11589
|
if (compareSemver(projectVersion, CLI_VERSION4) === 0) {
|
|
12665
11590
|
spinner.succeed("Project is already up to date");
|
|
12666
|
-
console.log(
|
|
11591
|
+
console.log(chalk28.gray(` Version: v${CLI_VERSION4}
|
|
12667
11592
|
`));
|
|
12668
11593
|
return;
|
|
12669
11594
|
}
|
|
12670
11595
|
if (compareSemver(projectVersion, CLI_VERSION4) > 0) {
|
|
12671
11596
|
spinner.warn("Project was created with a newer CLI version");
|
|
12672
|
-
console.log(
|
|
12673
|
-
console.log(
|
|
11597
|
+
console.log(chalk28.yellow(` Project: v${projectVersion} \u2192 CLI: v${CLI_VERSION4}`));
|
|
11598
|
+
console.log(chalk28.yellow(" Update your CLI: npm install -g @getcoherent/cli@latest\n"));
|
|
12674
11599
|
return;
|
|
12675
11600
|
}
|
|
12676
11601
|
const report = {
|
|
@@ -12716,36 +11641,36 @@ async function updateCommand(opts) {
|
|
|
12716
11641
|
}
|
|
12717
11642
|
function printReport2(report) {
|
|
12718
11643
|
const from = report.fromVersion ? `v${report.fromVersion}` : "unknown";
|
|
12719
|
-
console.log(
|
|
11644
|
+
console.log(chalk28.green(`
|
|
12720
11645
|
\u2714 Project updated: ${from} \u2192 v${report.toVersion}
|
|
12721
11646
|
`));
|
|
12722
11647
|
if (report.overlayFiles > 0) {
|
|
12723
|
-
console.log(
|
|
11648
|
+
console.log(chalk28.white(` \u2714 Regenerated platform overlay (${report.overlayFiles} files)`));
|
|
12724
11649
|
}
|
|
12725
11650
|
if (report.migrationsApplied.length > 0) {
|
|
12726
11651
|
for (const desc of report.migrationsApplied) {
|
|
12727
|
-
console.log(
|
|
11652
|
+
console.log(chalk28.white(` \u2714 Migrated config: ${desc}`));
|
|
12728
11653
|
}
|
|
12729
11654
|
}
|
|
12730
11655
|
if (report.rulesUpdated) {
|
|
12731
|
-
console.log(
|
|
11656
|
+
console.log(chalk28.white(" \u2714 Updated .cursorrules and CLAUDE.md"));
|
|
12732
11657
|
}
|
|
12733
11658
|
if (report.missingCssVars.length > 0) {
|
|
12734
11659
|
console.log("");
|
|
12735
|
-
console.log(
|
|
11660
|
+
console.log(chalk28.yellow(" \u26A0 New CSS variables available in globals.css:"));
|
|
12736
11661
|
for (const v of report.missingCssVars.slice(0, 10)) {
|
|
12737
|
-
console.log(
|
|
11662
|
+
console.log(chalk28.gray(` ${v}`));
|
|
12738
11663
|
}
|
|
12739
11664
|
if (report.missingCssVars.length > 10) {
|
|
12740
|
-
console.log(
|
|
11665
|
+
console.log(chalk28.gray(` ... and ${report.missingCssVars.length - 10} more`));
|
|
12741
11666
|
}
|
|
12742
11667
|
console.log("");
|
|
12743
|
-
console.log(
|
|
12744
|
-
console.log(
|
|
11668
|
+
console.log(chalk28.cyan(" To add them automatically:"));
|
|
11669
|
+
console.log(chalk28.white(" coherent update --patch-globals\n"));
|
|
12745
11670
|
}
|
|
12746
11671
|
console.log("");
|
|
12747
|
-
console.log(
|
|
12748
|
-
console.log(
|
|
11672
|
+
console.log(chalk28.dim(" Your pages and components were NOT modified."));
|
|
11673
|
+
console.log(chalk28.dim(" Run `coherent check` to check existing pages against new rules.\n"));
|
|
12749
11674
|
}
|
|
12750
11675
|
var EXPECTED_CSS_VARS = [
|
|
12751
11676
|
"--background",
|
|
@@ -12829,7 +11754,7 @@ function patchGlobalsCss(projectRoot, missingVars) {
|
|
|
12829
11754
|
}
|
|
12830
11755
|
|
|
12831
11756
|
// src/commands/undo.ts
|
|
12832
|
-
import
|
|
11757
|
+
import chalk29 from "chalk";
|
|
12833
11758
|
async function undoCommand(options) {
|
|
12834
11759
|
try {
|
|
12835
11760
|
const project = findConfig();
|
|
@@ -12838,41 +11763,41 @@ async function undoCommand(options) {
|
|
|
12838
11763
|
const backups = listBackups(projectRoot);
|
|
12839
11764
|
if (options.list) {
|
|
12840
11765
|
if (backups.length === 0) {
|
|
12841
|
-
console.log(
|
|
11766
|
+
console.log(chalk29.yellow("No backups found."));
|
|
12842
11767
|
return;
|
|
12843
11768
|
}
|
|
12844
|
-
console.log(
|
|
11769
|
+
console.log(chalk29.bold("\n\u{1F4E6} Available backups:\n"));
|
|
12845
11770
|
for (const b of backups) {
|
|
12846
11771
|
const date = new Date(b.timestamp);
|
|
12847
11772
|
const timeStr = date.toLocaleString();
|
|
12848
|
-
console.log(
|
|
12849
|
-
console.log(
|
|
11773
|
+
console.log(chalk29.white(` ${b.name}`));
|
|
11774
|
+
console.log(chalk29.dim(` ${timeStr} \u2014 ${b.files} file(s)`));
|
|
12850
11775
|
console.log();
|
|
12851
11776
|
}
|
|
12852
11777
|
return;
|
|
12853
11778
|
}
|
|
12854
11779
|
if (backups.length === 0) {
|
|
12855
|
-
console.log(
|
|
11780
|
+
console.log(chalk29.yellow("No backups found. Nothing to undo."));
|
|
12856
11781
|
return;
|
|
12857
11782
|
}
|
|
12858
11783
|
const latest = backups[0];
|
|
12859
11784
|
const ok = restoreBackup(projectRoot, latest.name);
|
|
12860
11785
|
if (!ok) {
|
|
12861
|
-
console.log(
|
|
11786
|
+
console.log(chalk29.red("Failed to restore backup."));
|
|
12862
11787
|
return;
|
|
12863
11788
|
}
|
|
12864
|
-
console.log(
|
|
12865
|
-
console.log(
|
|
12866
|
-
console.log(
|
|
12867
|
-
console.log(
|
|
11789
|
+
console.log(chalk29.green("\n\u2705 Restored to previous state:\n"));
|
|
11790
|
+
console.log(chalk29.dim(` Snapshot: ${new Date(latest.timestamp).toLocaleString()}`));
|
|
11791
|
+
console.log(chalk29.dim(` Files: ${latest.files} restored`));
|
|
11792
|
+
console.log(chalk29.cyan("\n Run: coherent preview\n"));
|
|
12868
11793
|
} catch (error) {
|
|
12869
|
-
console.error(
|
|
11794
|
+
console.error(chalk29.red("\u274C Undo failed:"), error instanceof Error ? error.message : "Unknown error");
|
|
12870
11795
|
process.exit(1);
|
|
12871
11796
|
}
|
|
12872
11797
|
}
|
|
12873
11798
|
|
|
12874
11799
|
// src/commands/sync.ts
|
|
12875
|
-
import
|
|
11800
|
+
import chalk30 from "chalk";
|
|
12876
11801
|
import ora9 from "ora";
|
|
12877
11802
|
import { existsSync as existsSync27, readFileSync as readFileSync19 } from "fs";
|
|
12878
11803
|
import { join as join20, relative as relative5, dirname as dirname10 } from "path";
|
|
@@ -13102,6 +12027,18 @@ function extractReusablePatterns(code) {
|
|
|
13102
12027
|
}
|
|
13103
12028
|
return [...classCounts.entries()].filter(([, count]) => count >= 2).map(([pattern, count]) => ({ pattern, count, sample: pattern.slice(0, 80) })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
13104
12029
|
}
|
|
12030
|
+
function mergeReusablePatternsToStylePatterns(patterns, existing) {
|
|
12031
|
+
const result = { ...existing };
|
|
12032
|
+
for (const p of patterns) {
|
|
12033
|
+
if (p.pattern.includes("rounded") && p.pattern.includes("border") && !result.card) {
|
|
12034
|
+
result.card = p.pattern;
|
|
12035
|
+
}
|
|
12036
|
+
if (p.pattern.includes("py-") && p.pattern.includes("px-") && !result.section) {
|
|
12037
|
+
result.section = p.pattern;
|
|
12038
|
+
}
|
|
12039
|
+
}
|
|
12040
|
+
return result;
|
|
12041
|
+
}
|
|
13105
12042
|
async function syncCommand(options = {}) {
|
|
13106
12043
|
const project = findConfig();
|
|
13107
12044
|
if (!project) return exitNotCoherent();
|
|
@@ -13110,7 +12047,7 @@ async function syncCommand(options = {}) {
|
|
|
13110
12047
|
const doTokens = runAll || options.tokens === true;
|
|
13111
12048
|
const doComponents = runAll || options.components === true;
|
|
13112
12049
|
const doPatterns = runAll || options.patterns === true;
|
|
13113
|
-
if (dryRun) console.log(
|
|
12050
|
+
if (dryRun) console.log(chalk30.yellow(" [dry-run] No files will be written\n"));
|
|
13114
12051
|
const spinner = ora9("Scanning project files...").start();
|
|
13115
12052
|
try {
|
|
13116
12053
|
const appDir = join20(project.root, "app");
|
|
@@ -13251,84 +12188,87 @@ async function syncCommand(options = {}) {
|
|
|
13251
12188
|
spinner.succeed("Updated .cursorrules and CLAUDE.md");
|
|
13252
12189
|
}
|
|
13253
12190
|
console.log("");
|
|
13254
|
-
console.log(
|
|
12191
|
+
console.log(chalk30.green(`\u2705 Design System ${dryRun ? "analyzed" : "synced"} with actual code
|
|
13255
12192
|
`));
|
|
13256
|
-
console.log(
|
|
12193
|
+
console.log(chalk30.blue("\u{1F4C4} Pages:"));
|
|
13257
12194
|
for (const page of discoveredPages) {
|
|
13258
12195
|
const a = analyzePageCode(page.code);
|
|
13259
12196
|
const comps = Object.entries(a.componentUsage || {}).filter(([, c]) => c > 0).map(([n]) => n);
|
|
13260
|
-
console.log(
|
|
13261
|
-
if (comps.length > 0) console.log(
|
|
13262
|
-
if (a.sections?.length) console.log(
|
|
12197
|
+
console.log(chalk30.gray(` ${page.route} \u2014 ${page.name}`));
|
|
12198
|
+
if (comps.length > 0) console.log(chalk30.gray(` Components: ${comps.join(", ")}`));
|
|
12199
|
+
if (a.sections?.length) console.log(chalk30.gray(` Sections: ${a.sections.map((s) => s.name).join(", ")}`));
|
|
13263
12200
|
}
|
|
13264
12201
|
if (doTokens && extractedTokens) {
|
|
13265
12202
|
console.log("");
|
|
13266
|
-
console.log(
|
|
12203
|
+
console.log(chalk30.blue("\u{1F3A8} Design Tokens (from globals.css):"));
|
|
13267
12204
|
const lc = Object.keys(extractedTokens.colors.light).length;
|
|
13268
12205
|
const dc = Object.keys(extractedTokens.colors.dark).length;
|
|
13269
|
-
console.log(
|
|
13270
|
-
console.log(
|
|
13271
|
-
if (extractedTokens.radius) console.log(
|
|
12206
|
+
console.log(chalk30.gray(` Light: ${lc} variables | Dark: ${dc} variables`));
|
|
12207
|
+
console.log(chalk30.gray(` Default mode: ${extractedTokens.defaultMode}`));
|
|
12208
|
+
if (extractedTokens.radius) console.log(chalk30.gray(` Border radius: ${extractedTokens.radius}`));
|
|
13272
12209
|
}
|
|
13273
12210
|
if (doComponents && reconcileResult) {
|
|
13274
12211
|
console.log("");
|
|
13275
|
-
console.log(
|
|
12212
|
+
console.log(chalk30.blue("\u{1F9E9} Shared Components:"));
|
|
13276
12213
|
for (const r of reconcileResult.removed) {
|
|
13277
|
-
console.log(
|
|
12214
|
+
console.log(chalk30.red(` \u{1F5D1} Removed ${r.id} (${r.name}) \u2014 ${r.reason}`));
|
|
13278
12215
|
}
|
|
13279
12216
|
for (const u of reconcileResult.updated) {
|
|
13280
|
-
console.log(
|
|
12217
|
+
console.log(chalk30.cyan(` \u{1F4DD} Updated ${u.id} ${u.field}: ${u.from} \u2192 ${u.to}`));
|
|
13281
12218
|
}
|
|
13282
12219
|
for (const a of reconcileResult.added) {
|
|
13283
|
-
console.log(
|
|
12220
|
+
console.log(chalk30.green(` \u2728 Added ${a.id} (${a.name}) \u2014 ${a.file} (${a.type})`));
|
|
13284
12221
|
}
|
|
13285
12222
|
for (const w of reconcileResult.warnings) {
|
|
13286
|
-
console.log(
|
|
13287
|
-
console.log(
|
|
12223
|
+
console.log(chalk30.yellow(` \u26A0 ${w.message}`));
|
|
12224
|
+
console.log(chalk30.dim(` ${w.suggestion}`));
|
|
13288
12225
|
}
|
|
13289
12226
|
if (reconcileResult.removed.length === 0 && reconcileResult.updated.length === 0 && reconcileResult.added.length === 0 && reconcileResult.warnings.length === 0) {
|
|
13290
|
-
console.log(
|
|
12227
|
+
console.log(chalk30.gray(" All components consistent \u2713"));
|
|
13291
12228
|
}
|
|
13292
12229
|
}
|
|
13293
12230
|
if (doPatterns && Object.keys(stylePatterns).length > 0) {
|
|
13294
12231
|
console.log("");
|
|
13295
|
-
console.log(
|
|
13296
|
-
if (stylePatterns.card) console.log(
|
|
13297
|
-
if (stylePatterns.section) console.log(
|
|
13298
|
-
if (stylePatterns.terminal) console.log(
|
|
13299
|
-
if (stylePatterns.iconContainer) console.log(
|
|
12232
|
+
console.log(chalk30.blue("\u{1F4D0} Style Patterns:"));
|
|
12233
|
+
if (stylePatterns.card) console.log(chalk30.gray(` Cards: ${stylePatterns.card.slice(0, 80)}`));
|
|
12234
|
+
if (stylePatterns.section) console.log(chalk30.gray(` Sections: ${stylePatterns.section}`));
|
|
12235
|
+
if (stylePatterns.terminal) console.log(chalk30.gray(` Terminal: ${stylePatterns.terminal.slice(0, 80)}`));
|
|
12236
|
+
if (stylePatterns.iconContainer) console.log(chalk30.gray(` Icons: ${stylePatterns.iconContainer.slice(0, 80)}`));
|
|
13300
12237
|
if (stylePatterns.heroHeadline)
|
|
13301
|
-
console.log(
|
|
12238
|
+
console.log(chalk30.gray(` Hero headline: ${stylePatterns.heroHeadline.slice(0, 80)}`));
|
|
13302
12239
|
if (stylePatterns.sectionTitle)
|
|
13303
|
-
console.log(
|
|
12240
|
+
console.log(chalk30.gray(` Section title: ${stylePatterns.sectionTitle.slice(0, 80)}`));
|
|
13304
12241
|
}
|
|
13305
12242
|
const tokenUsage = extractActualTokenUsage(allPageCode);
|
|
13306
12243
|
if (tokenUsage.colors.length > 0) {
|
|
13307
12244
|
console.log("");
|
|
13308
|
-
console.log(
|
|
12245
|
+
console.log(chalk30.blue("\u{1F3F7}\uFE0F Actual token usage (from classNames):"));
|
|
13309
12246
|
console.log(
|
|
13310
|
-
|
|
12247
|
+
chalk30.gray(
|
|
13311
12248
|
` Colors: ${tokenUsage.colors.slice(0, 12).join(", ")}${tokenUsage.colors.length > 12 ? ` (+${tokenUsage.colors.length - 12})` : ""}`
|
|
13312
12249
|
)
|
|
13313
12250
|
);
|
|
13314
12251
|
console.log(
|
|
13315
|
-
|
|
12252
|
+
chalk30.gray(
|
|
13316
12253
|
` Typography: ${tokenUsage.typography.slice(0, 8).join(", ")}${tokenUsage.typography.length > 8 ? ` (+${tokenUsage.typography.length - 8})` : ""}`
|
|
13317
12254
|
)
|
|
13318
12255
|
);
|
|
13319
|
-
console.log(
|
|
12256
|
+
console.log(chalk30.gray(` Radius: ${tokenUsage.borderRadius.join(", ")}`));
|
|
13320
12257
|
}
|
|
13321
12258
|
const reusable = extractReusablePatterns(allPageCode);
|
|
13322
12259
|
if (reusable.length > 0) {
|
|
12260
|
+
if (!dryRun) {
|
|
12261
|
+
config2.stylePatterns = mergeReusablePatternsToStylePatterns(reusable, config2.stylePatterns || {});
|
|
12262
|
+
}
|
|
13323
12263
|
console.log("");
|
|
13324
|
-
console.log(
|
|
12264
|
+
console.log(chalk30.blue(`\u{1F501} Repeating patterns (${reusable.length} \u2014 potential reusable components):`));
|
|
13325
12265
|
for (const p of reusable.slice(0, 5)) {
|
|
13326
|
-
console.log(
|
|
12266
|
+
console.log(chalk30.gray(` \xD7${p.count}: ${p.sample}${p.sample.length < p.pattern.length ? "..." : ""}`));
|
|
13327
12267
|
}
|
|
13328
12268
|
}
|
|
13329
12269
|
console.log("");
|
|
13330
12270
|
if (!dryRun) {
|
|
13331
|
-
console.log(
|
|
12271
|
+
console.log(chalk30.cyan(" Open /design-system in the app to see the updated view."));
|
|
13332
12272
|
}
|
|
13333
12273
|
console.log("");
|
|
13334
12274
|
} catch (err) {
|
|
@@ -13339,7 +12279,7 @@ async function syncCommand(options = {}) {
|
|
|
13339
12279
|
}
|
|
13340
12280
|
|
|
13341
12281
|
// src/commands/migrate.ts
|
|
13342
|
-
import
|
|
12282
|
+
import chalk31 from "chalk";
|
|
13343
12283
|
import ora10 from "ora";
|
|
13344
12284
|
import { existsSync as existsSync28, mkdirSync as mkdirSync8, cpSync, rmSync as rmSync6, writeFileSync as writeFileSync12, readFileSync as readFileSync20, readdirSync as readdirSync10 } from "fs";
|
|
13345
12285
|
import { join as join21 } from "path";
|
|
@@ -13416,13 +12356,13 @@ async function migrateAction(options) {
|
|
|
13416
12356
|
}
|
|
13417
12357
|
const guard = guardPath(projectRoot);
|
|
13418
12358
|
if (existsSync28(guard)) {
|
|
13419
|
-
console.log(
|
|
13420
|
-
console.log(
|
|
12359
|
+
console.log(chalk31.yellow("A migration is already in progress."));
|
|
12360
|
+
console.log(chalk31.dim("Run `coherent migrate --rollback` to undo, or delete .coherent/migration-in-progress"));
|
|
13421
12361
|
return;
|
|
13422
12362
|
}
|
|
13423
12363
|
const uiDir = join21(projectRoot, "components", "ui");
|
|
13424
12364
|
if (!existsSync28(uiDir)) {
|
|
13425
|
-
console.log(
|
|
12365
|
+
console.log(chalk31.yellow("No components/ui directory found. Nothing to migrate."));
|
|
13426
12366
|
return;
|
|
13427
12367
|
}
|
|
13428
12368
|
const provider = getComponentProvider();
|
|
@@ -13430,16 +12370,16 @@ async function migrateAction(options) {
|
|
|
13430
12370
|
const files = readdirSync10(uiDir).filter((f) => f.endsWith(".tsx"));
|
|
13431
12371
|
const migratable = files.map((f) => f.replace(".tsx", "")).filter((id) => managedIds.has(id));
|
|
13432
12372
|
if (migratable.length === 0) {
|
|
13433
|
-
console.log(
|
|
12373
|
+
console.log(chalk31.green("All components are already up to date."));
|
|
13434
12374
|
return;
|
|
13435
12375
|
}
|
|
13436
|
-
console.log(
|
|
12376
|
+
console.log(chalk31.cyan(`
|
|
13437
12377
|
Found ${migratable.length} component(s) to migrate:`));
|
|
13438
12378
|
for (const id of migratable) {
|
|
13439
|
-
console.log(
|
|
12379
|
+
console.log(chalk31.dim(` - ${id}`));
|
|
13440
12380
|
}
|
|
13441
12381
|
if (options.dryRun) {
|
|
13442
|
-
console.log(
|
|
12382
|
+
console.log(chalk31.yellow("\n[dry-run] No changes applied."));
|
|
13443
12383
|
return;
|
|
13444
12384
|
}
|
|
13445
12385
|
const spinner = ora10("Migrating components...").start();
|
|
@@ -13456,12 +12396,12 @@ Found ${migratable.length} component(s) to migrate:`));
|
|
|
13456
12396
|
if (result.success) {
|
|
13457
12397
|
migrated++;
|
|
13458
12398
|
} else {
|
|
13459
|
-
console.warn(
|
|
12399
|
+
console.warn(chalk31.yellow(` \u26A0 Failed to migrate ${id}`));
|
|
13460
12400
|
}
|
|
13461
12401
|
}
|
|
13462
12402
|
clearGuard(projectRoot);
|
|
13463
12403
|
spinner.succeed(`Migrated ${migrated}/${migratable.length} components to real shadcn/ui`);
|
|
13464
|
-
console.log(
|
|
12404
|
+
console.log(chalk31.dim(` Backup saved to: ${backup}`));
|
|
13465
12405
|
} catch (err) {
|
|
13466
12406
|
spinner.fail("Migration failed \u2014 rolling back");
|
|
13467
12407
|
rollback(projectRoot);
|
|
@@ -13473,7 +12413,7 @@ Found ${migratable.length} component(s) to migrate:`));
|
|
|
13473
12413
|
import { existsSync as existsSync29, mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync13 } from "fs";
|
|
13474
12414
|
import { join as join22 } from "path";
|
|
13475
12415
|
import { homedir } from "os";
|
|
13476
|
-
import
|
|
12416
|
+
import chalk32 from "chalk";
|
|
13477
12417
|
import { CLI_VERSION as CLI_VERSION5 } from "@getcoherent/core";
|
|
13478
12418
|
var DEBUG5 = process.env.COHERENT_DEBUG === "1";
|
|
13479
12419
|
var PACKAGE_NAME = "@getcoherent/cli";
|
|
@@ -13544,8 +12484,8 @@ async function checkForUpdates() {
|
|
|
13544
12484
|
}
|
|
13545
12485
|
function printUpdateNotice(latest) {
|
|
13546
12486
|
console.log(
|
|
13547
|
-
|
|
13548
|
-
\u2B06 Update available: v${CLI_VERSION5} \u2192 v${latest}`) +
|
|
12487
|
+
chalk32.yellow(`
|
|
12488
|
+
\u2B06 Update available: v${CLI_VERSION5} \u2192 v${latest}`) + chalk32.dim(`
|
|
13549
12489
|
Run: npm update -g ${PACKAGE_NAME}
|
|
13550
12490
|
`)
|
|
13551
12491
|
);
|
|
@@ -13564,7 +12504,10 @@ program.name("coherent").description(
|
|
|
13564
12504
|
"Coherent Design Method \u2014 AI-powered design system generator\nby Sergei Kovtun \xB7 https://www.linkedin.com/in/sergeikovtun/"
|
|
13565
12505
|
).version(CLI_VERSION6);
|
|
13566
12506
|
program.command("init").argument("[name]", "Project directory name (created if it does not exist)").description("Initialize a new Coherent project").action(initCommand);
|
|
13567
|
-
program.command("chat").description("Modify design system via conversation").argument("[message]", "Modification request").option("--provider <provider>", "AI provider: claude|openai|auto", "auto").option("--component <name>", "Target a specific component by name or CID").option("--page <name>", "Target a specific page by name, id, or route").option("--token <name>", "Target a specific design token").option("-
|
|
12507
|
+
program.command("chat").description("Modify design system via conversation").argument("[message]", "Modification request").option("--provider <provider>", "AI provider: claude|openai|auto", "auto").option("--component <name>", "Target a specific component by name or CID").option("--page <name>", "Target a specific page by name, id, or route").option("--token <name>", "Target a specific design token").option("--new-component <name>", "Create a new shared component with the given name").option(
|
|
12508
|
+
"--type <type>",
|
|
12509
|
+
"Component type for --new-component: layout, navigation, data-display, form, feedback, section"
|
|
12510
|
+
).option("-i, --interactive", "Interactive chat mode").action(chatCommand);
|
|
13568
12511
|
program.command("preview").description("Launch dev server for preview").action(previewCommand);
|
|
13569
12512
|
program.command("check").description("Show all problems: page quality, shared components, internal links").option("--json", "Output as JSON").option("--pages", "Only check pages").option("--shared", "Only check shared components").action((opts) => checkCommand(opts));
|
|
13570
12513
|
program.command("fix").description("Auto-fix everything: cache, deps, components, syntax, quality").option("--dry-run", "Show what would be fixed without writing").option("--no-cache", "Skip cache clearing").option("--no-quality", "Skip quality auto-fixes").action((opts) => fixCommand(opts));
|