capy-mcp 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brief.js +45 -29
- package/dist/component-discovery.d.ts +25 -0
- package/dist/component-discovery.js +144 -0
- package/dist/design-system.js +45 -8
- package/dist/project.d.ts +7 -0
- package/dist/project.js +118 -24
- package/package.json +1 -1
package/dist/brief.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { detectFramework } from "./framework.js";
|
|
2
2
|
import { buildProjectFacts } from "./project.js";
|
|
3
|
+
import { buildComponentDiscoveryPlan } from "./component-discovery.js";
|
|
3
4
|
const SECTION_ORDER = [
|
|
4
5
|
"Foundations",
|
|
5
6
|
"Colors",
|
|
@@ -17,10 +18,11 @@ const SECTION_ORDER = [
|
|
|
17
18
|
export async function buildPreviewBrief(projectRoot, input) {
|
|
18
19
|
const framework = await detectFramework(projectRoot);
|
|
19
20
|
const projectFacts = await buildProjectFacts(projectRoot, framework);
|
|
20
|
-
const
|
|
21
|
+
const discoveryPlan = await buildComponentDiscoveryPlan(projectRoot, projectFacts);
|
|
22
|
+
const warnings = buildWarnings(framework, input.changedFiles, discoveryPlan.missingFamilyGaps);
|
|
21
23
|
return {
|
|
22
24
|
projectFacts,
|
|
23
|
-
inspectionPlan: buildInspectionPlan(projectFacts),
|
|
25
|
+
inspectionPlan: buildInspectionPlan(projectFacts, discoveryPlan),
|
|
24
26
|
constraints: buildConstraints(framework),
|
|
25
27
|
deliverableSpec: {
|
|
26
28
|
goal: input.task === "update_preview"
|
|
@@ -41,27 +43,35 @@ export async function buildPreviewBrief(projectRoot, input) {
|
|
|
41
43
|
},
|
|
42
44
|
updateStrategy: buildUpdateStrategy(input.changedFiles),
|
|
43
45
|
warnings,
|
|
44
|
-
instructions: buildInstructions(projectFacts, input),
|
|
46
|
+
instructions: buildInstructions(projectFacts, input, discoveryPlan.instruction),
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
|
-
function buildInspectionPlan(projectFacts) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
function buildInspectionPlan(projectFacts, discoveryPlan) {
|
|
50
|
+
// Build app shell targets dynamically from discovered page dirs
|
|
51
|
+
// Use routing style to suggest the right shell files
|
|
52
|
+
const appShellTargets = [];
|
|
53
|
+
for (const pageDir of projectFacts.likelyPageDirs) {
|
|
54
|
+
if (projectFacts.routingStyle === "app-router") {
|
|
55
|
+
appShellTargets.push(`${pageDir}/layout.tsx`);
|
|
56
|
+
appShellTargets.push(`${pageDir}/page.tsx`);
|
|
57
|
+
}
|
|
58
|
+
else if (projectFacts.routingStyle === "pages-router") {
|
|
59
|
+
appShellTargets.push(`${pageDir}/_app.tsx`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Unknown or other routing — suggest both
|
|
63
|
+
appShellTargets.push(`${pageDir}/layout.tsx`);
|
|
64
|
+
appShellTargets.push(`${pageDir}/page.tsx`);
|
|
65
|
+
appShellTargets.push(`${pageDir}/_app.tsx`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Deduplicate
|
|
69
|
+
const uniqueShellTargets = Array.from(new Set(appShellTargets));
|
|
60
70
|
return [
|
|
61
71
|
{
|
|
62
72
|
step: 1,
|
|
63
73
|
action: "Read the app shell and routing entry points first",
|
|
64
|
-
targets:
|
|
74
|
+
targets: uniqueShellTargets.length > 0 ? uniqueShellTargets : projectFacts.likelyPageDirs,
|
|
65
75
|
reason: "Understand the project structure, routing style, and baseline visual language.",
|
|
66
76
|
},
|
|
67
77
|
{
|
|
@@ -72,12 +82,20 @@ function buildInspectionPlan(projectFacts) {
|
|
|
72
82
|
},
|
|
73
83
|
{
|
|
74
84
|
step: 3,
|
|
75
|
-
action: "
|
|
76
|
-
targets:
|
|
77
|
-
|
|
85
|
+
action: "Traverse discovered component directories",
|
|
86
|
+
targets: discoveryPlan.prioritizedFiles.length > 0
|
|
87
|
+
? discoveryPlan.prioritizedFiles
|
|
88
|
+
: projectFacts.likelyComponentDirs,
|
|
89
|
+
reason: "Read the actual components discovered by Capy's scan. These are real files with PascalCase exports, not guesses.",
|
|
78
90
|
},
|
|
79
91
|
{
|
|
80
92
|
step: 4,
|
|
93
|
+
action: "Inspect UI/component directories for anything the scan missed",
|
|
94
|
+
targets: projectFacts.likelyComponentDirs.length > 0 ? projectFacts.likelyComponentDirs : projectFacts.likelyUiDirs,
|
|
95
|
+
reason: "The automatic scan catches most components, but manually inspect directories for any non-standard patterns that were missed.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
step: 5,
|
|
81
99
|
action: "Implement or update the preview route",
|
|
82
100
|
targets: [projectFacts.previewEntryFile],
|
|
83
101
|
reason: "Build a neat, scrollable /preview page that reflects the real app structure.",
|
|
@@ -90,6 +108,9 @@ function buildConstraints(framework) {
|
|
|
90
108
|
"Build a preview page that can support both vertical and horizontal scanning where useful.",
|
|
91
109
|
"Use horizontal specimen rows only when they make scanning easier.",
|
|
92
110
|
"Prefer existing components over creating preview-only components.",
|
|
111
|
+
"Traverse all component directories found by Capy. Every file with a PascalCase export is a component candidate. Read real usage examples before marking a preview section complete.",
|
|
112
|
+
"When you only find hooks, providers, or usage patterns, trace one real usage example and mirror that flow in /preview instead of inventing a fake component.",
|
|
113
|
+
"If a component family is not present in the repo, label it as absent rather than fabricating a preview-only substitute.",
|
|
93
114
|
"Keep the page neat, easy to scan, and aligned with the app's current design language.",
|
|
94
115
|
"Include an icon inventory when the repo exposes app icons clearly enough to catalogue them.",
|
|
95
116
|
"Show colors in a uniform swatch format with normalized 6-character hex labels and click-to-copy affordance.",
|
|
@@ -113,7 +134,7 @@ function buildUpdateStrategy(changedFiles) {
|
|
|
113
134
|
}
|
|
114
135
|
return strategies;
|
|
115
136
|
}
|
|
116
|
-
function buildWarnings(framework, changedFiles) {
|
|
137
|
+
function buildWarnings(framework, changedFiles, discoveryGaps = []) {
|
|
117
138
|
const warnings = [];
|
|
118
139
|
if (framework.confirmationMessage) {
|
|
119
140
|
warnings.push(framework.confirmationMessage);
|
|
@@ -121,18 +142,13 @@ function buildWarnings(framework, changedFiles) {
|
|
|
121
142
|
if (!changedFiles || changedFiles.length === 0) {
|
|
122
143
|
warnings.push("No changedFiles were provided. The agent should inspect git diff or recent edits when performing update_preview.");
|
|
123
144
|
}
|
|
145
|
+
warnings.push(...discoveryGaps);
|
|
124
146
|
return warnings;
|
|
125
147
|
}
|
|
126
|
-
function buildInstructions(projectFacts, input) {
|
|
148
|
+
function buildInstructions(projectFacts, input, discoveryInstruction) {
|
|
127
149
|
const lead = input.task === "update_preview"
|
|
128
150
|
? "Update the existing /preview route incrementally."
|
|
129
151
|
: "Create the /preview route from scratch.";
|
|
130
152
|
const userGoal = input.userGoal ? ` User goal: ${input.userGoal}.` : "";
|
|
131
|
-
return `${lead}${userGoal} Read the app shell first, then global styles, then component directories. After that, implement ${projectFacts.previewEntryFile} as a clean preview surface that supports both vertical and horizontal scanning when useful, includes a dedicated icon section when icons can be discovered, and renders colors as consistent swatches with 6-character hex labels plus click-to-copy behavior using a pointer cursor
|
|
132
|
-
}
|
|
133
|
-
function uniqueCompact(values) {
|
|
134
|
-
return Array.from(new Set(values.filter(Boolean)));
|
|
135
|
-
}
|
|
136
|
-
function firstMatching(values, needle) {
|
|
137
|
-
return values.find((value) => value === needle);
|
|
153
|
+
return `${lead}${userGoal} Read the app shell first, then global styles, then traverse all discovered component directories. After that, implement ${projectFacts.previewEntryFile} as a clean preview surface that supports both vertical and horizontal scanning when useful, includes a dedicated icon section when icons can be discovered, and renders colors as consistent swatches with 6-character hex labels plus click-to-copy behavior using a pointer cursor. ${discoveryInstruction}`;
|
|
138
154
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProjectFacts } from "./types.js";
|
|
2
|
+
interface DiscoveredComponent {
|
|
3
|
+
path: string;
|
|
4
|
+
basename: string;
|
|
5
|
+
exports: string[];
|
|
6
|
+
contents: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ComponentDiscoveryPlan {
|
|
9
|
+
allDiscoveredComponents: DiscoveredComponent[];
|
|
10
|
+
prioritizedFiles: string[];
|
|
11
|
+
discoveredFamilyFacts: string[];
|
|
12
|
+
missingFamilyGaps: string[];
|
|
13
|
+
instruction: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Discover components by scanning the filesystem — no hardcoded family lists.
|
|
17
|
+
*
|
|
18
|
+
* Strategy:
|
|
19
|
+
* 1. Glob for ALL source files within discovered component/UI dirs
|
|
20
|
+
* 2. Extract PascalCase exports from each file
|
|
21
|
+
* 3. Everything with a PascalCase export is a component
|
|
22
|
+
* 4. Classify what we found into loose categories for the agent
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildComponentDiscoveryPlan(projectRoot: string, projectFacts: ProjectFacts): Promise<ComponentDiscoveryPlan>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { basename, join } from "path";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { readText, toPosixPath } from "./files.js";
|
|
4
|
+
/**
|
|
5
|
+
* Discover components by scanning the filesystem — no hardcoded family lists.
|
|
6
|
+
*
|
|
7
|
+
* Strategy:
|
|
8
|
+
* 1. Glob for ALL source files within discovered component/UI dirs
|
|
9
|
+
* 2. Extract PascalCase exports from each file
|
|
10
|
+
* 3. Everything with a PascalCase export is a component
|
|
11
|
+
* 4. Classify what we found into loose categories for the agent
|
|
12
|
+
*/
|
|
13
|
+
export async function buildComponentDiscoveryPlan(projectRoot, projectFacts) {
|
|
14
|
+
const components = await scanForComponents(projectRoot, projectFacts);
|
|
15
|
+
// Group components into loose categories for the agent's benefit
|
|
16
|
+
const prioritizedFiles = components.map((c) => c.path).slice(0, 12);
|
|
17
|
+
const discoveredFamilyFacts = buildDiscoveryFacts(components);
|
|
18
|
+
const missingFamilyGaps = buildGaps(components);
|
|
19
|
+
const instruction = buildInstruction(components, prioritizedFiles);
|
|
20
|
+
return {
|
|
21
|
+
allDiscoveredComponents: components,
|
|
22
|
+
prioritizedFiles,
|
|
23
|
+
discoveredFamilyFacts,
|
|
24
|
+
missingFamilyGaps,
|
|
25
|
+
instruction,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Scan the project for all files that export PascalCase identifiers.
|
|
30
|
+
* Uses the dynamically-discovered component dirs from project.ts,
|
|
31
|
+
* falls back to a broad scan if none were found.
|
|
32
|
+
*/
|
|
33
|
+
async function scanForComponents(projectRoot, projectFacts) {
|
|
34
|
+
// Build glob patterns from discovered dirs
|
|
35
|
+
const searchDirs = new Set([
|
|
36
|
+
...projectFacts.likelyComponentDirs,
|
|
37
|
+
...projectFacts.likelyUiDirs,
|
|
38
|
+
]);
|
|
39
|
+
let patterns;
|
|
40
|
+
if (searchDirs.size > 0) {
|
|
41
|
+
patterns = Array.from(searchDirs).map((dir) => `${dir}/**/*.{ts,tsx,js,jsx}`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Fallback: scan everything if no dirs were discovered
|
|
45
|
+
patterns = ["**/*.{ts,tsx,js,jsx}"];
|
|
46
|
+
}
|
|
47
|
+
const sourceFiles = await glob(patterns, {
|
|
48
|
+
cwd: projectRoot,
|
|
49
|
+
nodir: true,
|
|
50
|
+
ignore: [
|
|
51
|
+
"**/*.test.*",
|
|
52
|
+
"**/*.spec.*",
|
|
53
|
+
"**/*.stories.*",
|
|
54
|
+
"**/*.story.*",
|
|
55
|
+
"**/node_modules/**",
|
|
56
|
+
"**/.next/**",
|
|
57
|
+
"**/dist/**",
|
|
58
|
+
"**/build/**",
|
|
59
|
+
"**/coverage/**",
|
|
60
|
+
"**/.git/**",
|
|
61
|
+
"**/.capy/**",
|
|
62
|
+
"**/preview/**",
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
const components = [];
|
|
66
|
+
for (const file of sourceFiles.map((f) => toPosixPath(f)).sort()) {
|
|
67
|
+
const contents = (await readText(join(projectRoot, file))) ?? "";
|
|
68
|
+
const exports = extractExportCandidates(contents);
|
|
69
|
+
// A file is a component if it has at least one PascalCase export
|
|
70
|
+
if (exports.length > 0) {
|
|
71
|
+
components.push({
|
|
72
|
+
path: file,
|
|
73
|
+
basename: basename(file).replace(/\.[^.]+$/, ""),
|
|
74
|
+
exports,
|
|
75
|
+
contents,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return components;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build discovery facts — a short summary of what was found, organized
|
|
83
|
+
* by directory structure rather than hardcoded families.
|
|
84
|
+
*/
|
|
85
|
+
function buildDiscoveryFacts(components) {
|
|
86
|
+
if (components.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
// Group by directory
|
|
89
|
+
const byDir = new Map();
|
|
90
|
+
for (const comp of components) {
|
|
91
|
+
const slashIndex = comp.path.lastIndexOf("/");
|
|
92
|
+
const dir = slashIndex === -1 ? "." : comp.path.slice(0, slashIndex);
|
|
93
|
+
if (!byDir.has(dir))
|
|
94
|
+
byDir.set(dir, []);
|
|
95
|
+
byDir.get(dir).push(comp.exports[0] ?? comp.basename);
|
|
96
|
+
}
|
|
97
|
+
const facts = [];
|
|
98
|
+
for (const [dir, names] of byDir) {
|
|
99
|
+
const listed = names.slice(0, 5).join(", ");
|
|
100
|
+
const suffix = names.length > 5 ? ` and ${names.length - 5} more` : "";
|
|
101
|
+
facts.push(`${dir}/: ${listed}${suffix}.`);
|
|
102
|
+
}
|
|
103
|
+
return facts.slice(0, 8);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build gap notes — only warn about genuinely concerning gaps,
|
|
107
|
+
* not prescriptive "you should have inputs" type warnings.
|
|
108
|
+
*/
|
|
109
|
+
function buildGaps(components) {
|
|
110
|
+
const gaps = [];
|
|
111
|
+
if (components.length === 0) {
|
|
112
|
+
gaps.push("No components were discovered. The scanner found no files with PascalCase exports in the project. You should manually search src/, components/, ui/, features/, lib/, or similar directories to find UI code.");
|
|
113
|
+
}
|
|
114
|
+
return gaps;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the instruction string for the agent — tell it how to traverse
|
|
118
|
+
* and find things rather than listing specific things to look for.
|
|
119
|
+
*/
|
|
120
|
+
function buildInstruction(components, prioritizedFiles) {
|
|
121
|
+
const traversalGuide = "Traverse the repo's component directories. Every file that exports a PascalCase identifier is a component. Read real usage of each component before building /preview sections. If a component looks like a page section, group it under 'Feature or Page Sections'. If it is a small reusable primitive, group it under the appropriate UI category.";
|
|
122
|
+
const usageGuide = "Use actual component files and real usage examples. If you only find hooks or providers, trace one real usage path and mirror that behavior in /preview instead of inventing a fake specimen.";
|
|
123
|
+
const placementGuide = "Place every component you find into the preview layout based on its actual role. If something does not fit a standard category, create a section for it. Do not invent components that don't exist in the repo, and do not omit components that do.";
|
|
124
|
+
if (prioritizedFiles.length === 0) {
|
|
125
|
+
return `${traversalGuide} ${usageGuide} ${placementGuide}`;
|
|
126
|
+
}
|
|
127
|
+
return `${traversalGuide} Start with ${prioritizedFiles.slice(0, 6).join(", ")}. ${usageGuide} ${placementGuide}`;
|
|
128
|
+
}
|
|
129
|
+
function extractExportCandidates(contents) {
|
|
130
|
+
const exportNames = new Set();
|
|
131
|
+
const patterns = [
|
|
132
|
+
/export\s+function\s+([A-Z][A-Za-z0-9_]*)/g,
|
|
133
|
+
/export\s+const\s+([A-Z][A-Za-z0-9_]*)/g,
|
|
134
|
+
/export\s+class\s+([A-Z][A-Za-z0-9_]*)/g,
|
|
135
|
+
/export\s+default\s+function\s+([A-Z][A-Za-z0-9_]*)/g,
|
|
136
|
+
];
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = pattern.exec(contents)) !== null) {
|
|
140
|
+
exportNames.add(match[1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(exportNames);
|
|
144
|
+
}
|
package/dist/design-system.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { glob } from "glob";
|
|
2
2
|
import { basename, join } from "path";
|
|
3
3
|
import { buildPreviewBrief } from "./brief.js";
|
|
4
|
+
import { buildComponentDiscoveryPlan } from "./component-discovery.js";
|
|
4
5
|
import { readText, toPosixPath, writeText } from "./files.js";
|
|
5
6
|
import { detectFramework } from "./framework.js";
|
|
6
7
|
import { buildProjectFacts } from "./project.js";
|
|
@@ -8,6 +9,7 @@ const DEFAULT_ARTIFACT_PATH = ".capy/design-system.json";
|
|
|
8
9
|
export async function buildDesignSystemArtifact(projectRoot, input = {}) {
|
|
9
10
|
const framework = await detectFramework(projectRoot);
|
|
10
11
|
const projectFacts = await buildProjectFacts(projectRoot, framework);
|
|
12
|
+
const discoveryPlan = await buildComponentDiscoveryPlan(projectRoot, projectFacts);
|
|
11
13
|
const previewBrief = await buildPreviewBrief(projectRoot, {
|
|
12
14
|
task: input.mode === "update" ? "update_preview" : "build_preview",
|
|
13
15
|
changedFiles: input.changedFiles,
|
|
@@ -15,9 +17,9 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
|
|
|
15
17
|
});
|
|
16
18
|
const cssVariables = await collectCssVariables(projectRoot, projectFacts.likelyStyleFiles);
|
|
17
19
|
const components = await collectComponents(projectRoot, projectFacts);
|
|
18
|
-
const readFirst = buildReadFirst(previewBrief, components);
|
|
20
|
+
const readFirst = buildReadFirst(previewBrief, components, discoveryPlan.prioritizedFiles);
|
|
19
21
|
const bridges = await buildUsageBridges(projectRoot, projectFacts.likelyStyleFiles, components);
|
|
20
|
-
const gaps = buildGapNotes(projectFacts, components, cssVariables);
|
|
22
|
+
const gaps = buildGapNotes(projectFacts, components, cssVariables, discoveryPlan.missingFamilyGaps);
|
|
21
23
|
const artifactPath = input.artifactPath ?? DEFAULT_ARTIFACT_PATH;
|
|
22
24
|
return {
|
|
23
25
|
forAgent: {
|
|
@@ -25,7 +27,7 @@ export async function buildDesignSystemArtifact(projectRoot, input = {}) {
|
|
|
25
27
|
? "Update /preview incrementally. Read the files below first, then touch only the sections affected by the changed files unless shared foundations changed."
|
|
26
28
|
: "Build or refine /preview. Read the files below first, then mirror the repo's existing UI language instead of inventing a new one.",
|
|
27
29
|
readFirst,
|
|
28
|
-
facts: buildAgentFacts(projectFacts, components, cssVariables),
|
|
30
|
+
facts: buildAgentFacts(projectFacts, components, cssVariables, discoveryPlan.discoveredFamilyFacts),
|
|
29
31
|
bridges,
|
|
30
32
|
gaps,
|
|
31
33
|
updateHints: previewBrief.updateStrategy,
|
|
@@ -168,7 +170,7 @@ function classifyCssVariable(name) {
|
|
|
168
170
|
function lineNumberAt(contents, index) {
|
|
169
171
|
return contents.slice(0, index).split("\n").length;
|
|
170
172
|
}
|
|
171
|
-
function buildReadFirst(previewBrief, components) {
|
|
173
|
+
function buildReadFirst(previewBrief, components, prioritizedFiles) {
|
|
172
174
|
const seen = new Set();
|
|
173
175
|
const readFirst = [];
|
|
174
176
|
const firstComponentByDir = new Map();
|
|
@@ -181,8 +183,39 @@ function buildReadFirst(previewBrief, components) {
|
|
|
181
183
|
firstComponentByDir.set(directory, component.path);
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
for (const step of previewBrief.inspectionPlan) {
|
|
186
|
+
for (const step of previewBrief.inspectionPlan.slice(0, 2)) {
|
|
185
187
|
for (const target of step.targets) {
|
|
188
|
+
if (!looksLikeProjectPath(target))
|
|
189
|
+
continue;
|
|
190
|
+
const normalizedTarget = firstComponentByDir.get(target) ?? target;
|
|
191
|
+
if (seen.has(normalizedTarget))
|
|
192
|
+
continue;
|
|
193
|
+
seen.add(normalizedTarget);
|
|
194
|
+
readFirst.push({
|
|
195
|
+
path: normalizedTarget,
|
|
196
|
+
reason: step.reason,
|
|
197
|
+
});
|
|
198
|
+
if (readFirst.length >= 8) {
|
|
199
|
+
return readFirst;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const prioritizedFile of prioritizedFiles) {
|
|
204
|
+
if (seen.has(prioritizedFile))
|
|
205
|
+
continue;
|
|
206
|
+
seen.add(prioritizedFile);
|
|
207
|
+
readFirst.push({
|
|
208
|
+
path: prioritizedFile,
|
|
209
|
+
reason: "Likely reusable primitive or usage example. Read this before inventing preview-only specimens.",
|
|
210
|
+
});
|
|
211
|
+
if (readFirst.length >= 8) {
|
|
212
|
+
return readFirst;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const step of previewBrief.inspectionPlan.slice(2)) {
|
|
216
|
+
for (const target of step.targets) {
|
|
217
|
+
if (!looksLikeProjectPath(target))
|
|
218
|
+
continue;
|
|
186
219
|
const normalizedTarget = firstComponentByDir.get(target) ?? target;
|
|
187
220
|
if (seen.has(normalizedTarget))
|
|
188
221
|
continue;
|
|
@@ -198,16 +231,17 @@ function buildReadFirst(previewBrief, components) {
|
|
|
198
231
|
}
|
|
199
232
|
return readFirst;
|
|
200
233
|
}
|
|
201
|
-
function buildAgentFacts(projectFacts, components, cssVariables) {
|
|
234
|
+
function buildAgentFacts(projectFacts, components, cssVariables, discoveredFamilyFacts) {
|
|
202
235
|
return [
|
|
203
236
|
`Use ${projectFacts.framework} conventions.`,
|
|
204
237
|
`Implement /preview at ${projectFacts.previewEntryFile}.`,
|
|
205
238
|
"Use repo-relative paths only.",
|
|
206
239
|
`Found ${components.length} component candidates and ${cssVariables.length} CSS variables.`,
|
|
240
|
+
...discoveredFamilyFacts,
|
|
207
241
|
];
|
|
208
242
|
}
|
|
209
|
-
function buildGapNotes(projectFacts, components, cssVariables) {
|
|
210
|
-
const gaps = [];
|
|
243
|
+
function buildGapNotes(projectFacts, components, cssVariables, discoveryGaps) {
|
|
244
|
+
const gaps = [...discoveryGaps];
|
|
211
245
|
if (components.length === 0) {
|
|
212
246
|
gaps.push("No components were detected. Grep src/components, src/ui, src/features, components, ui, or features before assuming the app has no reusable UI.");
|
|
213
247
|
}
|
|
@@ -261,3 +295,6 @@ function normalizeHex(hex) {
|
|
|
261
295
|
}
|
|
262
296
|
return `#${normalized}`;
|
|
263
297
|
}
|
|
298
|
+
function looksLikeProjectPath(target) {
|
|
299
|
+
return target.includes("/") || /^[A-Za-z0-9._-]+\.(?:tsx|ts|jsx|js|css|scss|sass|less)$/.test(target);
|
|
300
|
+
}
|
package/dist/project.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { FrameworkInfo, ProjectFacts } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build project facts by actually scanning the filesystem instead of
|
|
4
|
+
* checking a hardcoded list of candidate directories.
|
|
5
|
+
*
|
|
6
|
+
* Strategy: glob for all source files, read their exports, then classify
|
|
7
|
+
* directories by what they contain rather than what they're named.
|
|
8
|
+
*/
|
|
2
9
|
export declare function buildProjectFacts(projectRoot: string, framework: FrameworkInfo): Promise<ProjectFacts>;
|
package/dist/project.js
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
|
+
import { basename, dirname } from "path";
|
|
1
2
|
import { glob } from "glob";
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import { readText, toPosixPath } from "./files.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build project facts by actually scanning the filesystem instead of
|
|
6
|
+
* checking a hardcoded list of candidate directories.
|
|
7
|
+
*
|
|
8
|
+
* Strategy: glob for all source files, read their exports, then classify
|
|
9
|
+
* directories by what they contain rather than what they're named.
|
|
10
|
+
*/
|
|
4
11
|
export async function buildProjectFacts(projectRoot, framework) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
const sourceFiles = await glob(["**/*.{ts,tsx,js,jsx}"], {
|
|
13
|
+
cwd: projectRoot,
|
|
14
|
+
nodir: true,
|
|
15
|
+
ignore: [
|
|
16
|
+
"**/*.test.*",
|
|
17
|
+
"**/*.spec.*",
|
|
18
|
+
"**/*.stories.*",
|
|
19
|
+
"**/*.story.*",
|
|
20
|
+
"**/node_modules/**",
|
|
21
|
+
"**/.next/**",
|
|
22
|
+
"**/dist/**",
|
|
23
|
+
"**/build/**",
|
|
24
|
+
"**/coverage/**",
|
|
25
|
+
"**/.git/**",
|
|
26
|
+
"**/.capy/**",
|
|
27
|
+
"**/preview/**",
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
const normalized = sourceFiles.map((f) => toPosixPath(f));
|
|
31
|
+
// Scan files for PascalCase exports to find component directories
|
|
32
|
+
const componentDirs = await findComponentDirs(projectRoot, normalized);
|
|
33
|
+
// Find page/route directories by looking for routing markers
|
|
34
|
+
const pageDirs = findPageDirs(normalized);
|
|
35
|
+
// Find style files — already dynamic via glob
|
|
21
36
|
const styleFiles = await findStyleFiles(projectRoot);
|
|
37
|
+
// UI dirs = union of component + page dirs
|
|
22
38
|
const likelyUiDirs = Array.from(new Set([...componentDirs, ...pageDirs.filter((dir) => !dir.endsWith("/preview"))]));
|
|
23
39
|
return {
|
|
24
40
|
framework: framework.kind,
|
|
@@ -32,14 +48,92 @@ export async function buildProjectFacts(projectRoot, framework) {
|
|
|
32
48
|
likelyUiDirs,
|
|
33
49
|
};
|
|
34
50
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Find component directories by scanning source files for PascalCase exports.
|
|
53
|
+
* A directory is a "component dir" if it contains ≥1 file with a PascalCase export.
|
|
54
|
+
* We deduplicate to the shallowest ancestor that qualifies.
|
|
55
|
+
*/
|
|
56
|
+
async function findComponentDirs(projectRoot, sourceFiles) {
|
|
57
|
+
const dirComponentCount = new Map();
|
|
58
|
+
for (const file of sourceFiles) {
|
|
59
|
+
// Skip files at the root level (no directory)
|
|
60
|
+
if (!file.includes("/"))
|
|
61
|
+
continue;
|
|
62
|
+
const ext = file.split(".").pop() ?? "";
|
|
63
|
+
// Only check .tsx and .jsx files — those are almost always components
|
|
64
|
+
// Also check .ts/.js but require a PascalCase filename as extra signal
|
|
65
|
+
const isTsxJsx = ext === "tsx" || ext === "jsx";
|
|
66
|
+
const fileName = basename(file).replace(/\.[^.]+$/, "");
|
|
67
|
+
const isPascalFileName = /^[A-Z][a-zA-Z0-9]*$/.test(fileName);
|
|
68
|
+
if (!isTsxJsx && !isPascalFileName)
|
|
69
|
+
continue;
|
|
70
|
+
const contents = await readText(`${projectRoot}/${file}`);
|
|
71
|
+
if (!contents)
|
|
72
|
+
continue;
|
|
73
|
+
if (hasPascalCaseExport(contents)) {
|
|
74
|
+
const dir = dirname(file);
|
|
75
|
+
dirComponentCount.set(dir, (dirComponentCount.get(dir) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (dirComponentCount.size === 0)
|
|
79
|
+
return [];
|
|
80
|
+
// Collect all dirs that have components
|
|
81
|
+
const allDirs = Array.from(dirComponentCount.keys()).sort();
|
|
82
|
+
// Deduplicate: if both "src/components" and "src/components/sections" exist,
|
|
83
|
+
// keep the parent "src/components" (the child is already covered by glob patterns).
|
|
84
|
+
// But also keep the child if the parent has 0 direct components (e.g. parent is
|
|
85
|
+
// just an organizer directory).
|
|
86
|
+
const roots = deduplicateToRoots(allDirs);
|
|
87
|
+
return roots;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Given a sorted list of directory paths, return the root-most directories.
|
|
91
|
+
* "src/components/sections" is dropped if "src/components" is in the list.
|
|
92
|
+
*/
|
|
93
|
+
function deduplicateToRoots(sortedDirs) {
|
|
94
|
+
const roots = [];
|
|
95
|
+
for (const dir of sortedDirs) {
|
|
96
|
+
const isChildOfExisting = roots.some((root) => dir.startsWith(root + "/"));
|
|
97
|
+
if (!isChildOfExisting) {
|
|
98
|
+
roots.push(dir);
|
|
40
99
|
}
|
|
41
100
|
}
|
|
42
|
-
return
|
|
101
|
+
return roots;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Find page/route directories by looking for routing convention markers
|
|
105
|
+
* in the actual files instead of checking hardcoded paths.
|
|
106
|
+
*/
|
|
107
|
+
function findPageDirs(sourceFiles) {
|
|
108
|
+
const pageDirSet = new Set();
|
|
109
|
+
// Routing markers that indicate a directory is a page directory
|
|
110
|
+
const routingMarkers = [
|
|
111
|
+
"layout.tsx", "layout.ts", "layout.jsx", "layout.js",
|
|
112
|
+
"page.tsx", "page.ts", "page.jsx", "page.js",
|
|
113
|
+
"_app.tsx", "_app.ts", "_app.jsx", "_app.js",
|
|
114
|
+
"_document.tsx", "_document.ts",
|
|
115
|
+
"+page.svelte", "+layout.svelte",
|
|
116
|
+
];
|
|
117
|
+
for (const file of sourceFiles) {
|
|
118
|
+
const fileName = basename(file);
|
|
119
|
+
if (routingMarkers.includes(fileName)) {
|
|
120
|
+
const dir = dirname(file);
|
|
121
|
+
// We want the top-level routing root, not every nested route
|
|
122
|
+
// e.g. for "src/app/blog/page.tsx" we want "src/app"
|
|
123
|
+
pageDirSet.add(dir);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (pageDirSet.size === 0)
|
|
127
|
+
return [];
|
|
128
|
+
// Find the root-level page dirs (e.g. "src/app" covers "src/app/blog")
|
|
129
|
+
const sorted = Array.from(pageDirSet).sort();
|
|
130
|
+
return deduplicateToRoots(sorted);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if file contents contain a PascalCase export (function, const, or class).
|
|
134
|
+
*/
|
|
135
|
+
function hasPascalCaseExport(contents) {
|
|
136
|
+
return /export\s+(?:default\s+)?(?:function|const|class)\s+[A-Z][A-Za-z0-9_]*/.test(contents);
|
|
43
137
|
}
|
|
44
138
|
async function findStyleFiles(projectRoot) {
|
|
45
139
|
const files = await glob(["**/*.{css,scss,sass,less}"], {
|
package/package.json
CHANGED