capy-mcp 1.0.6 → 1.0.8

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/cli.js CHANGED
File without changes
@@ -3,7 +3,6 @@ interface DiscoveredComponent {
3
3
  path: string;
4
4
  basename: string;
5
5
  exports: string[];
6
- contents: string;
7
6
  }
8
7
  export interface ComponentDiscoveryPlan {
9
8
  allDiscoveredComponents: DiscoveredComponent[];
@@ -36,14 +36,12 @@ async function scanForComponents(projectRoot, projectFacts) {
36
36
  ...projectFacts.likelyComponentDirs,
37
37
  ...projectFacts.likelyUiDirs,
38
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}"];
39
+ // If no component dirs were discovered, return empty instead of
40
+ // falling back to a repo-wide scan that reads every file's contents.
41
+ if (searchDirs.size === 0) {
42
+ return [];
46
43
  }
44
+ const patterns = Array.from(searchDirs).map((dir) => `${dir}/**/*.{ts,tsx,js,jsx}`);
47
45
  const sourceFiles = await glob(patterns, {
48
46
  cwd: projectRoot,
49
47
  nodir: true,
@@ -72,7 +70,6 @@ async function scanForComponents(projectRoot, projectFacts) {
72
70
  path: file,
73
71
  basename: basename(file).replace(/\.[^.]+$/, ""),
74
72
  exports,
75
- contents,
76
73
  });
77
74
  }
78
75
  }
package/dist/project.js CHANGED
@@ -28,14 +28,15 @@ export async function buildProjectFacts(projectRoot, framework) {
28
28
  ],
29
29
  });
30
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
31
+ // Find page/route directories FIRST we need these to exclude from component dirs
34
32
  const pageDirs = findPageDirs(normalized);
33
+ // Scan files for PascalCase exports to find component directories
34
+ // Exclude page dirs so route files don't inflate the component list
35
+ const componentDirs = await findComponentDirs(projectRoot, normalized, pageDirs);
35
36
  // Find style files — already dynamic via glob
36
37
  const styleFiles = await findStyleFiles(projectRoot);
37
- // UI dirs = union of component + page dirs
38
- const likelyUiDirs = Array.from(new Set([...componentDirs, ...pageDirs.filter((dir) => !dir.endsWith("/preview"))]));
38
+ // UI dirs = component dirs only (page dirs are routes, not reusable UI)
39
+ const likelyUiDirs = Array.from(new Set([...componentDirs]));
39
40
  return {
40
41
  framework: framework.kind,
41
42
  routingStyle: framework.routingStyle,
@@ -49,42 +50,45 @@ export async function buildProjectFacts(projectRoot, framework) {
49
50
  };
50
51
  }
51
52
  /**
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.
53
+ * Find component directories by scanning .tsx/.jsx source files for
54
+ * PascalCase exports that look like React components.
55
+ *
56
+ * Key rules:
57
+ * - Only .tsx/.jsx files qualify (plain .ts/.js are utilities, not components)
58
+ * - Files must contain JSX or React signals (not just any PascalCase export)
59
+ * - Page/route directories are excluded (those are routes, not reusable UI)
55
60
  */
56
- async function findComponentDirs(projectRoot, sourceFiles) {
61
+ async function findComponentDirs(projectRoot, sourceFiles, pageDirs) {
57
62
  const dirComponentCount = new Map();
58
63
  for (const file of sourceFiles) {
59
64
  // Skip files at the root level (no directory)
60
65
  if (!file.includes("/"))
61
66
  continue;
67
+ // Skip files inside page/route dirs — those are routes, not components
68
+ const dir = dirname(file);
69
+ if (isInsidePageDir(dir, pageDirs))
70
+ continue;
62
71
  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";
72
+ // ONLY check .tsx and .jsx files — plain .ts/.js are utilities/hooks/config
73
+ if (ext !== "tsx" && ext !== "jsx")
74
+ continue;
75
+ // Skip common non-component page/route files by name
66
76
  const fileName = basename(file).replace(/\.[^.]+$/, "");
67
- const isPascalFileName = /^[A-Z][a-zA-Z0-9]*$/.test(fileName);
68
- if (!isTsxJsx && !isPascalFileName)
77
+ if (isRouteFileName(fileName))
69
78
  continue;
70
79
  const contents = await readText(`${projectRoot}/${file}`);
71
80
  if (!contents)
72
81
  continue;
82
+ // For .tsx/.jsx files, a PascalCase export is sufficient signal —
83
+ // the file extension itself indicates component intent
73
84
  if (hasPascalCaseExport(contents)) {
74
- const dir = dirname(file);
75
85
  dirComponentCount.set(dir, (dirComponentCount.get(dir) ?? 0) + 1);
76
86
  }
77
87
  }
78
88
  if (dirComponentCount.size === 0)
79
89
  return [];
80
- // Collect all dirs that have components
81
90
  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;
91
+ return deduplicateToRoots(allDirs);
88
92
  }
89
93
  /**
90
94
  * Given a sorted list of directory paths, return the root-most directories.
@@ -135,6 +139,41 @@ function findPageDirs(sourceFiles) {
135
139
  function hasPascalCaseExport(contents) {
136
140
  return /export\s+(?:default\s+)?(?:function|const|class)\s+[A-Z][A-Za-z0-9_]*/.test(contents);
137
141
  }
142
+ /**
143
+ * Check if a file looks like a React component (contains JSX or React imports).
144
+ * This prevents pure utility files with PascalCase class exports from being
145
+ * treated as components.
146
+ */
147
+ function looksLikeComponent(contents) {
148
+ // Contains JSX-like syntax: <Component, <div, <>, etc.
149
+ if (/<[A-Za-z][A-Za-z0-9.]*[\s/>]/.test(contents))
150
+ return true;
151
+ if (/<>/.test(contents))
152
+ return true;
153
+ // Imports React or uses React APIs
154
+ if (/from\s+['"]react['"]/.test(contents))
155
+ return true;
156
+ if (/React\.createElement/.test(contents))
157
+ return true;
158
+ return false;
159
+ }
160
+ /**
161
+ * Check if a directory is inside (or equal to) any of the known page dirs.
162
+ */
163
+ function isInsidePageDir(dir, pageDirs) {
164
+ return pageDirs.some((pageDir) => dir === pageDir || dir.startsWith(pageDir + "/"));
165
+ }
166
+ /**
167
+ * Common route/page filenames that should not be treated as components.
168
+ */
169
+ function isRouteFileName(name) {
170
+ const routeNames = new Set([
171
+ "layout", "page", "loading", "error", "not-found",
172
+ "template", "default", "route", "middleware",
173
+ "_app", "_document", "_error",
174
+ ]);
175
+ return routeNames.has(name);
176
+ }
138
177
  async function findStyleFiles(projectRoot) {
139
178
  const files = await glob(["**/*.{css,scss,sass,less}"], {
140
179
  cwd: projectRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capy-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "MCP server that inspects a repo, returns a structured /preview brief, and writes a design-system JSON artifact for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",