yap2app 1.0.0 → 1.1.0

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/bin/cli-esm.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startBridge } from '../bridge.js';
4
+ import { runMcpServer } from '../mcp.js';
5
+ import { scanCodebase } from '../scanner.js';
6
+
7
+ const command = process.argv[2] || 'start';
8
+
9
+ async function main() {
10
+ if (command === 'mcp') {
11
+ // Run stdio MCP server for Cursor / Claude Desktop / VS Code
12
+ await runMcpServer();
13
+ } else if (command === 'scan') {
14
+ // Run standalone shallow scanner and print summary
15
+ console.log(`\nšŸ” Yap2App Codebase Context Scanner:`);
16
+ const context = await scanCodebase(process.cwd());
17
+ console.log(`\nProject: ${context.project_name}`);
18
+ console.log(`Framework: ${context.framework} (${context.router_type})`);
19
+ console.log(`Path Alias: ${context.path_alias}`);
20
+ console.log(`Dependencies Detected: ${context.dependencies.length}`);
21
+ console.log(`UI Components Catalog: ${context.component_names.join(', ') || 'None'}`);
22
+ console.log(`\nFull Context JSON:\n`, JSON.stringify(context, null, 2));
23
+ } else if (command === 'start' || command === 'bridge' || command === 'server') {
24
+ // Start Localhost Bridge for Web Studio UI
25
+ startBridge();
26
+ } else if (command === 'help' || command === '--help' || command === '-h') {
27
+ console.log(`
28
+ ⚔ Yap2App CLI & MCP Toolkit ⚔
29
+
30
+ Usage:
31
+ npx yap2app [command]
32
+
33
+ Commands:
34
+ start | bridge Start the Localhost Bridge with W3C PNA for 1-click Web Studio Sync (Default)
35
+ mcp Run the Model Context Protocol (MCP) Server over stdio for Cursor/Claude
36
+ scan Run a shallow codebase scan and print detected standards & UI primitives
37
+ help Display this help guide
38
+ `);
39
+ } else {
40
+ // Default fallback to bridge
41
+ startBridge();
42
+ }
43
+ }
44
+
45
+ main().catch(err => {
46
+ console.error("Yap2App CLI Error:", err);
47
+ process.exit(1);
48
+ });
package/bin/cli.js CHANGED
@@ -1,48 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { startBridge } from '../bridge.js';
4
- import { runMcpServer } from '../mcp.js';
5
- import { scanCodebase } from '../scanner.js';
3
+ var nodeVersion = process.version;
4
+ var majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
6
5
 
7
- const command = process.argv[2] || 'start';
8
-
9
- async function main() {
10
- if (command === 'mcp') {
11
- // Run stdio MCP server for Cursor / Claude Desktop / VS Code
12
- await runMcpServer();
13
- } else if (command === 'scan') {
14
- // Run standalone shallow scanner and print summary
15
- console.log(`\nšŸ” Yap2App Codebase Context Scanner:`);
16
- const context = await scanCodebase(process.cwd());
17
- console.log(`\nProject: ${context.project_name}`);
18
- console.log(`Framework: ${context.framework} (${context.router_type})`);
19
- console.log(`Path Alias: ${context.path_alias}`);
20
- console.log(`Dependencies Detected: ${context.dependencies.length}`);
21
- console.log(`UI Components Catalog: ${context.component_names.join(', ') || 'None'}`);
22
- console.log(`\nFull Context JSON:\n`, JSON.stringify(context, null, 2));
23
- } else if (command === 'start' || command === 'bridge' || command === 'server') {
24
- // Start Localhost Bridge for Web Studio UI
25
- startBridge();
26
- } else if (command === 'help' || command === '--help' || command === '-h') {
27
- console.log(`
28
- ⚔ Yap2App CLI & MCP Toolkit ⚔
29
-
30
- Usage:
31
- npx @yap2app/cli [command]
32
-
33
- Commands:
34
- start | bridge Start the Localhost Bridge with W3C PNA for 1-click Web Studio Sync (Default)
35
- mcp Run the Model Context Protocol (MCP) Server over stdio for Cursor/Claude
36
- scan Run a shallow codebase scan and print detected standards & UI primitives
37
- help Display this help guide
38
- `);
39
- } else {
40
- // Default fallback to bridge
41
- startBridge();
42
- }
6
+ if (majorVersion < 18) {
7
+ console.error('\n============================================================');
8
+ console.error('āŒ Yap2App requires Node.js v18+ (Current: ' + nodeVersion + ')');
9
+ console.error('šŸ‘‰ Please switch to a modern Node version by running:');
10
+ console.error(' nvm use 20 (or: nvm use 18)');
11
+ console.error('============================================================\n');
12
+ process.exit(1);
43
13
  }
44
14
 
45
- main().catch(err => {
46
- console.error("Yap2App CLI Error:", err);
15
+ // Dynamically import the ES module entrypoint for Node 18+
16
+ import('./cli-esm.js').catch(function(err) {
17
+ console.error('Yap2App Execution Error:', err);
47
18
  process.exit(1);
48
19
  });
package/bridge.js CHANGED
@@ -9,14 +9,18 @@ import { scanCodebase } from './scanner.js';
9
9
  const PORT = process.env.YAP2APP_PORT || 10420;
10
10
  const PROJECT_ROOT = process.cwd();
11
11
 
12
- // Bulletproof W3C Private Network Access (PNA) and CORS Headers
13
- const HEADERS = {
14
- 'Access-Control-Allow-Origin': '*',
15
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
16
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Access-Control-Request-Private-Network, X-Requested-With',
17
- 'Access-Control-Allow-Private-Network': 'true',
18
- 'Content-Type': 'application/json'
19
- };
12
+ // Bulletproof W3C Private Network Access (PNA) and Dynamic Origin CORS Headers
13
+ function getCorsHeaders(req) {
14
+ const origin = req?.headers?.origin || '*';
15
+ return {
16
+ 'Access-Control-Allow-Origin': origin,
17
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, DELETE, HEAD',
18
+ 'Access-Control-Allow-Headers': req?.headers?.['access-control-request-headers'] || 'Content-Type, Authorization, Access-Control-Request-Private-Network, X-Requested-With, *',
19
+ 'Access-Control-Allow-Private-Network': 'true',
20
+ 'Access-Control-Max-Age': '86400',
21
+ 'Content-Type': 'application/json'
22
+ };
23
+ }
20
24
 
21
25
  /**
22
26
  * Computes a lightweight line-by-line diff between original disk file and new content.
@@ -64,16 +68,18 @@ function computeDiff(originalText, newText) {
64
68
  }
65
69
 
66
70
  const server = http.createServer(async (req, res) => {
71
+ const headers = getCorsHeaders(req);
72
+
67
73
  // 1. Handle Preflight OPTIONS requests (PNA + CORS)
68
74
  if (req.method === 'OPTIONS') {
69
- res.writeHead(204, HEADERS);
75
+ res.writeHead(204, headers);
70
76
  res.end();
71
77
  return;
72
78
  }
73
79
 
74
80
  // 2. Health & Root Info
75
81
  if (req.method === 'GET' && (req.url === '/' || req.url === '/health' || req.url === '/api/health')) {
76
- res.writeHead(200, HEADERS);
82
+ res.writeHead(200, headers);
77
83
  res.end(JSON.stringify({
78
84
  status: 'healthy',
79
85
  service: 'Yap2App-Localhost-Bridge',
@@ -90,11 +96,11 @@ const server = http.createServer(async (req, res) => {
90
96
  try {
91
97
  console.log(`[Yap2App Bridge] šŸ” Scanning shallow codebase context in: ${PROJECT_ROOT}`);
92
98
  const context = await scanCodebase(PROJECT_ROOT);
93
- res.writeHead(200, HEADERS);
99
+ res.writeHead(200, headers);
94
100
  res.end(JSON.stringify(context));
95
101
  } catch (e) {
96
102
  console.error('[Yap2App Bridge] Context scan error:', e);
97
- res.writeHead(500, HEADERS);
103
+ res.writeHead(500, headers);
98
104
  res.end(JSON.stringify({ error: e.message }));
99
105
  }
100
106
  return;
@@ -129,7 +135,7 @@ const server = http.createServer(async (req, res) => {
129
135
  });
130
136
  }
131
137
 
132
- res.writeHead(200, HEADERS);
138
+ res.writeHead(200, headers);
133
139
  res.end(JSON.stringify({
134
140
  success: true,
135
141
  files_count: diffResults.length,
@@ -137,7 +143,7 @@ const server = http.createServer(async (req, res) => {
137
143
  }));
138
144
  } catch (e) {
139
145
  console.error('[Yap2App Bridge] Diff error:', e);
140
- res.writeHead(500, HEADERS);
146
+ res.writeHead(500, headers);
141
147
  res.end(JSON.stringify({ error: e.message }));
142
148
  }
143
149
  });
@@ -166,7 +172,7 @@ const server = http.createServer(async (req, res) => {
166
172
  written.push(file.path);
167
173
  }
168
174
 
169
- res.writeHead(200, HEADERS);
175
+ res.writeHead(200, headers);
170
176
  res.end(JSON.stringify({
171
177
  success: true,
172
178
  written_count: written.length,
@@ -176,7 +182,7 @@ const server = http.createServer(async (req, res) => {
176
182
  }));
177
183
  } catch (e) {
178
184
  console.error('[Yap2App Bridge] Write to disk error:', e);
179
- res.writeHead(500, HEADERS);
185
+ res.writeHead(500, headers);
180
186
  res.end(JSON.stringify({ error: e.message }));
181
187
  }
182
188
  });
@@ -184,7 +190,7 @@ const server = http.createServer(async (req, res) => {
184
190
  }
185
191
 
186
192
  // 404 Fallback
187
- res.writeHead(404, HEADERS);
193
+ res.writeHead(404, headers);
188
194
  res.end(JSON.stringify({ error: 'Endpoint Not Found' }));
189
195
  });
190
196
 
package/package.json CHANGED
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "yap2app",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Autonomous Vibe-to-Prod Localhost Bridge & Model Context Protocol (MCP) Server",
5
5
  "main": "bridge.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "yap2app": "./bin/cli.js"
9
9
  },
10
+ "files": [
11
+ "bin",
12
+ "bridge.js",
13
+ "mcp.js",
14
+ "scanner.js",
15
+ "v2p.js",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
10
22
  "scripts": {
11
23
  "start": "node ./bin/cli.js start",
12
24
  "bridge": "node ./bin/cli.js bridge",
package/scanner.js CHANGED
@@ -2,180 +2,420 @@ import fs from 'fs/promises';
2
2
  import fsSync from 'fs';
3
3
  import path from 'path';
4
4
 
5
+ const IGNORED_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ '.next',
9
+ '.nuxt',
10
+ 'dist',
11
+ 'build',
12
+ 'out',
13
+ '.vscode',
14
+ '.idea',
15
+ 'venv',
16
+ '.venv',
17
+ '__pycache__',
18
+ '.pytest_cache',
19
+ 'coverage',
20
+ '.turbo',
21
+ 'wheels'
22
+ ]);
23
+
5
24
  /**
6
- * Shallow Codebase Scanner for Yap2App.
7
- * Rapidly inspects package.json, tsconfig.json, and component directories
8
- * to extract project standards, path aliases, and reusable UI primitives.
25
+ * Universal recursive file walker across project hierarchies up to depth 6.
9
26
  */
10
- export async function scanCodebase(targetDir = process.cwd()) {
11
- const projectRoot = path.resolve(targetDir);
12
- const projectName = path.basename(projectRoot) || 'local-project';
27
+ async function walkDir(dir, baseDir, maxDepth = 6, currentDepth = 0) {
28
+ if (currentDepth > maxDepth) return [];
29
+ let results = [];
30
+ try {
31
+ const entries = await fs.readdir(dir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ const fullPath = path.join(dir, entry.name);
34
+ const relativePath = path.relative(baseDir, fullPath);
35
+
36
+ if (entry.isDirectory()) {
37
+ if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
38
+ const sub = await walkDir(fullPath, baseDir, maxDepth, currentDepth + 1);
39
+ results = results.concat(sub);
40
+ }
41
+ } else if (entry.isFile()) {
42
+ const ext = path.extname(entry.name).toLowerCase();
43
+ if ([
44
+ '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.hbs', '.handlebars',
45
+ '.html', '.css', '.scss', '.sass', '.less', '.vm', '.ftl', '.jsp', '.php',
46
+ '.blade.php', '.twig', '.erb', '.liquid', '.astro',
47
+ '.py', '.go', '.rs', '.java', '.kt', '.rb', '.cs',
48
+ '.json', '.yaml', '.yml', '.toml', '.xml'
49
+ ].includes(ext)) {
50
+ results.push({
51
+ relative: relativePath,
52
+ name: entry.name,
53
+ ext,
54
+ dir: path.relative(baseDir, dir)
55
+ });
56
+ }
57
+ }
58
+ }
59
+ } catch (e) {
60
+ // Ignore unreadable paths
61
+ }
62
+ return results;
63
+ }
64
+
65
+ /**
66
+ * Parses package.json safely and returns dependencies dictionary.
67
+ */
68
+ function parsePackageJson(content) {
69
+ try {
70
+ const pkg = JSON.parse(content);
71
+ return {
72
+ name: pkg.name,
73
+ dependencies: pkg.dependencies || {},
74
+ devDependencies: pkg.devDependencies || {},
75
+ allDeps: { ...(pkg.devDependencies || {}), ...(pkg.dependencies || {}) }
76
+ };
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Deterministic, 100% verified framework detection.
84
+ * Returns null if no verified signature matches with high confidence.
85
+ */
86
+ function detectVerifiedFramework(manifests, allFiles, extensionHistogram) {
87
+ // 1. Google Brightspot / Glue-Bundle
88
+ if (manifests['louhi.json'] || manifests['_fields.config.json'] || (manifests['package.json'] && manifests['package.json'].includes('google-marketing-bundle-glue'))) {
89
+ return {
90
+ framework: 'brightspot_glue',
91
+ displayName: 'Google Brightspot CMS / Glue-Bundle'
92
+ };
93
+ }
94
+
95
+ // 2. Node / JS Ecosystem (Exact dependency inspection)
96
+ if (manifests['package.json']) {
97
+ const pkg = parsePackageJson(manifests['package.json']);
98
+ if (pkg && pkg.allDeps) {
99
+ const deps = pkg.allDeps;
100
+ if (deps['next']) return { framework: 'nextjs', displayName: `Next.js (${deps['next'].replace(/[\^~]/g, '')})` };
101
+ if (deps['@angular/core']) return { framework: 'angular', displayName: `Angular (${deps['@angular/core'].replace(/[\^~]/g, '')})` };
102
+ if (deps['nuxt'] || deps['nuxt3']) return { framework: 'nuxt', displayName: 'Nuxt' };
103
+ if (deps['vue']) return { framework: 'vue', displayName: `Vue (${deps['vue'].replace(/[\^~]/g, '')})` };
104
+ if (deps['@sveltejs/kit'] || deps['svelte']) return { framework: 'svelte', displayName: 'Svelte' };
105
+ if (deps['astro']) return { framework: 'astro', displayName: 'Astro' };
106
+ if (deps['@remix-run/react']) return { framework: 'remix', displayName: 'Remix' };
107
+ if (deps['react']) return { framework: 'react', displayName: `React (${deps['react'].replace(/[\^~]/g, '')})` };
108
+ if (deps['express']) return { framework: 'express', displayName: 'Express.js' };
109
+ if (deps['hono']) return { framework: 'hono', displayName: 'Hono' };
110
+ if (deps['fastify']) return { framework: 'fastify', displayName: 'Fastify' };
111
+ }
112
+ }
113
+
114
+ // 3. Angular project config
115
+ if (manifests['angular.json']) {
116
+ return { framework: 'angular', displayName: 'Angular Project' };
117
+ }
118
+
119
+ // 4. Python Ecosystem
120
+ if (manifests['requirements.txt'] || manifests['pyproject.toml']) {
121
+ const pyReqs = (manifests['requirements.txt'] || '') + (manifests['pyproject.toml'] || '');
122
+ if (/django/i.test(pyReqs)) return { framework: 'django', displayName: 'Django' };
123
+ if (/fastapi/i.test(pyReqs)) return { framework: 'fastapi', displayName: 'FastAPI' };
124
+ if (/flask/i.test(pyReqs)) return { framework: 'flask', displayName: 'Flask' };
125
+ }
126
+
127
+ // 5. PHP Ecosystem
128
+ if (manifests['composer.json']) {
129
+ if (manifests['composer.json'].includes('laravel/framework')) return { framework: 'laravel', displayName: 'Laravel' };
130
+ if (manifests['composer.json'].includes('symfony/')) return { framework: 'symfony', displayName: 'Symfony' };
131
+ }
132
+
133
+ // 6. Java Ecosystem
134
+ if (manifests['pom.xml'] || manifests['build.gradle']) {
135
+ const jContent = (manifests['pom.xml'] || '') + (manifests['build.gradle'] || '');
136
+ if (jContent.includes('spring-boot')) return { framework: 'spring_boot', displayName: 'Spring Boot' };
137
+ }
138
+
139
+ // If not 100% verified, return null (do NOT guess or display inaccurate tags)
140
+ return null;
141
+ }
13
142
 
14
- let dependencies = [];
15
- let devDependencies = [];
16
- let framework = 'react';
17
- let routerType = 'spa';
18
- let iconLibrary = 'lucide-react';
19
- let stylingSystem = 'tailwindcss';
20
- let baseAlias = '@/;';
21
- baseAlias = '@/;'.replace(';', ''); // '@/ '
22
- let existingComponents = [];
23
- let componentNames = [];
24
- let designTokens = {
25
- primary: '#2563EB',
26
- neutral: '#0F172A',
143
+ /**
144
+ * Extracts design tokens, CSS variables, Tailwind configurations, and color palettes.
145
+ */
146
+ async function extractDesignTokens(projectRoot, allFiles) {
147
+ const tokens = {
148
+ primary: '#1A73E8',
149
+ neutral: '#202124',
27
150
  background: '#FFFFFF',
28
- font: 'Plus Jakarta Sans',
29
- radius: '0.75rem'
151
+ colors: {},
152
+ cssVariables: [],
153
+ tailwindTheme: null
30
154
  };
31
155
 
32
- // 1. Shallow Scan: package.json
33
- try {
34
- const pkgPath = path.join(projectRoot, 'package.json');
35
- if (fsSync.existsSync(pkgPath)) {
36
- const pkgJson = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
37
- dependencies = Object.keys(pkgJson.dependencies || {});
38
- devDependencies = Object.keys(pkgJson.devDependencies || {});
39
- const allDeps = new Set([...dependencies, ...devDependencies]);
40
-
41
- // Detect Framework
42
- if (allDeps.has('next')) {
43
- framework = 'nextjs';
44
- const hasAppDir = fsSync.existsSync(path.join(projectRoot, 'app')) || fsSync.existsSync(path.join(projectRoot, 'src', 'app'));
45
- routerType = hasAppDir ? 'app_router' : 'pages_router';
46
- } else if (allDeps.has('vite')) {
47
- framework = 'vite_react';
48
- routerType = 'spa';
49
- } else if (allDeps.has('@remix-run/react')) {
50
- framework = 'remix';
51
- } else if (allDeps.has('vue') || allDeps.has('nuxt')) {
52
- framework = 'vue';
53
- } else if (allDeps.has('svelte') || allDeps.has('@sveltejs/kit')) {
54
- framework = 'svelte';
55
- }
156
+ // 1. Inspect Tailwind configuration
157
+ const tailwindConfigFile = allFiles.find(f => f.name.startsWith('tailwind.config'));
158
+ if (tailwindConfigFile) {
159
+ try {
160
+ const fullP = path.join(projectRoot, tailwindConfigFile.relative);
161
+ const content = await fs.readFile(fullP, 'utf8');
162
+ tokens.tailwindTheme = content.slice(0, 1000);
163
+ } catch {}
164
+ }
165
+
166
+ // 2. Extract CSS/SCSS variables & Hex Palettes
167
+ const styleFiles = allFiles.filter(f =>
168
+ ['.css', '.scss', '.less'].includes(f.ext) ||
169
+ f.name.includes('theme') ||
170
+ f.name.includes('_colorPalette') ||
171
+ f.name.includes('variables')
172
+ ).slice(0, 8);
56
173
 
57
- // Detect Icons
58
- if (allDeps.has('lucide-react')) {
59
- iconLibrary = 'lucide-react';
60
- } else if (allDeps.has('@heroicons/react')) {
61
- iconLibrary = '@heroicons/react';
62
- } else if (allDeps.has('@tabler/icons-react')) {
63
- iconLibrary = '@tabler/icons-react';
64
- } else if (allDeps.has('react-icons')) {
65
- iconLibrary = 'react-icons';
174
+ for (const sf of styleFiles) {
175
+ try {
176
+ const fullP = path.join(projectRoot, sf.relative);
177
+ const content = await fs.readFile(fullP, 'utf8');
178
+
179
+ // Extract CSS custom properties: --color-primary: #... or --bg-...
180
+ const varMatches = content.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g);
181
+ for (const m of varMatches) {
182
+ const varName = m[1].trim();
183
+ const varVal = m[2].trim();
184
+ if (tokens.cssVariables.length < 35) {
185
+ tokens.cssVariables.push({ name: varName, value: varVal });
186
+ }
187
+ if (/primary|brand|accent/i.test(varName) && /^#[0-9a-fA-F]{3,8}$/.test(varVal)) {
188
+ tokens.primary = varVal;
189
+ }
66
190
  }
67
191
 
68
- // Detect Styling
69
- if (allDeps.has('tailwindcss') || allDeps.has('@tailwindcss/vite')) {
70
- stylingSystem = 'tailwindcss';
192
+ // Extract Hex colors
193
+ const hexMatches = content.matchAll(/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g);
194
+ for (const hm of hexMatches) {
195
+ const hex = hm[0].toUpperCase();
196
+ tokens.colors[hex] = (tokens.colors[hex] || 0) + 1;
71
197
  }
72
- }
73
- } catch (e) {
74
- console.warn('[Yap2App Scanner] package.json scan skipped:', e.message);
198
+ } catch {}
75
199
  }
76
200
 
77
- // 2. Shallow Scan: tsconfig.json or jsconfig.json (for Path Aliases)
78
- try {
79
- let tsconfigRaw = null;
80
- const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
81
- const jsconfigPath = path.join(projectRoot, 'jsconfig.json');
82
-
83
- if (fsSync.existsSync(tsconfigPath)) {
84
- tsconfigRaw = await fs.readFile(tsconfigPath, 'utf8');
85
- } else if (fsSync.existsSync(jsconfigPath)) {
86
- tsconfigRaw = await fs.readFile(jsconfigPath, 'utf8');
201
+ return tokens;
202
+ }
203
+
204
+ /**
205
+ * Extracts UI libraries (Radix, Lucide, Tailwind, MUI) and utility functions (cn, clsx).
206
+ */
207
+ async function extractUtilitiesAndIcons(projectRoot, allFiles, manifests) {
208
+ const uiEcosystem = {
209
+ ui_library: null,
210
+ icon_library: null,
211
+ utility_helpers: [],
212
+ detected_icons: []
213
+ };
214
+
215
+ // Inspect package dependencies
216
+ if (manifests['package.json']) {
217
+ const pkg = parsePackageJson(manifests['package.json']);
218
+ if (pkg && pkg.allDeps) {
219
+ const deps = pkg.allDeps;
220
+ if (deps['lucide-react']) uiEcosystem.icon_library = 'lucide-react';
221
+ else if (deps['@heroicons/react']) uiEcosystem.icon_library = '@heroicons/react';
222
+ else if (deps['react-icons']) uiEcosystem.icon_library = 'react-icons';
223
+ else if (deps['@tabler/icons-react']) uiEcosystem.icon_library = '@tabler/icons-react';
224
+
225
+ if (deps['@radix-ui/react-dialog'] || deps['@radix-ui/react-slot']) uiEcosystem.ui_library = 'shadcn/ui & Radix UI';
226
+ else if (deps['@mui/material']) uiEcosystem.ui_library = 'Material UI (MUI)';
227
+ else if (deps['@chakra-ui/react']) uiEcosystem.ui_library = 'Chakra UI';
228
+ else if (deps['antd']) uiEcosystem.ui_library = 'Ant Design';
87
229
  }
230
+ }
231
+
232
+ // Detect cn/clsx utility files
233
+ const utilFile = allFiles.find(f =>
234
+ f.relative.includes('utils') || f.name === 'cn.ts' || f.name === 'cn.js' || f.name === 'utils.ts'
235
+ );
236
+ if (utilFile) {
237
+ uiEcosystem.utility_helpers.push({
238
+ name: 'cn / classnames helper',
239
+ path: utilFile.relative
240
+ });
241
+ }
242
+
243
+ return uiEcosystem;
244
+ }
88
245
 
89
- if (tsconfigRaw) {
90
- const cleaned = tsconfigRaw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
91
- const config = JSON.parse(cleaned);
92
- const paths = config.compilerOptions?.paths || {};
246
+ /**
247
+ * Extracts reusable UI components with their export signatures and interface snippets.
248
+ */
249
+ async function extractComponentSignatures(projectRoot, allFiles) {
250
+ const catalog = [];
251
+ const candidateExtensions = ['.hbs', '.tsx', '.jsx', '.vue', '.svelte', '.vm', '.ftl', '.html'];
252
+
253
+ const componentCandidates = allFiles.filter(file => {
254
+ const baseName = path.basename(file.name, file.ext);
255
+ return candidateExtensions.includes(file.ext) &&
256
+ !baseName.startsWith('_') &&
257
+ !baseName.includes('.test') &&
258
+ !baseName.includes('.spec') &&
259
+ !baseName.includes('.story') &&
260
+ !['index', 'main', 'app', 'layout'].includes(baseName.toLowerCase());
261
+ }).slice(0, 45);
262
+
263
+ for (const file of componentCandidates) {
264
+ const baseName = path.basename(file.name, file.ext);
265
+ try {
266
+ const fullP = path.join(projectRoot, file.relative);
267
+ const rawCode = await fs.readFile(fullP, 'utf8');
93
268
 
94
- const aliasKeys = Object.keys(paths);
95
- if (aliasKeys.length > 0) {
96
- const primaryKey = aliasKeys.find(k => k.startsWith('@/') || k.startsWith('~/*') || k === '@/*') || aliasKeys[0];
97
- baseAlias = primaryKey.replace(/\*$/, '');
269
+ // Extract Interface/Props or parameters if present
270
+ let propsSnippet = '';
271
+ const interfaceMatch = rawCode.match(/(?:interface|type)\s+[\w]+\s*=?\s*\{[\s\S]*?\}/);
272
+ if (interfaceMatch) {
273
+ propsSnippet = interfaceMatch[0].slice(0, 350);
98
274
  }
275
+
276
+ // Extract function signature: export function ComponentName(props: ...)
277
+ let signatureSnippet = '';
278
+ const sigMatch = rawCode.match(/export\s+(?:default\s+)?(?:function|const)\s+([A-Z]\w+)[^{]*/);
279
+ if (sigMatch) {
280
+ signatureSnippet = sigMatch[0].trim().slice(0, 150);
281
+ }
282
+
283
+ catalog.push({
284
+ name: baseName,
285
+ path: file.relative,
286
+ extension: file.ext,
287
+ signature: signatureSnippet || null,
288
+ props: propsSnippet || null
289
+ });
290
+ } catch {
291
+ catalog.push({
292
+ name: baseName,
293
+ path: file.relative,
294
+ extension: file.ext,
295
+ signature: null,
296
+ props: null
297
+ });
99
298
  }
100
- } catch (e) {
101
- console.warn('[Yap2App Scanner] tsconfig scan fallback to default alias (@/):', e.message);
102
299
  }
103
300
 
104
- // 3. Shallow Scan: Component Inventory in standard directories
105
- const candidateDirs = [
106
- path.join(projectRoot, 'src', 'components', 'ui'),
107
- path.join(projectRoot, 'components', 'ui'),
108
- path.join(projectRoot, 'src', 'components'),
109
- path.join(projectRoot, 'components'),
110
- path.join(projectRoot, 'src', 'ui'),
111
- path.join(projectRoot, 'ui')
112
- ];
301
+ return catalog;
302
+ }
303
+
304
+ /**
305
+ * Extracts tsconfig path aliases (e.g. @/ -> src/).
306
+ */
307
+ function extractPathAliases(manifests) {
308
+ if (manifests['tsconfig.json']) {
309
+ try {
310
+ const cleaned = manifests['tsconfig.json'].replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
311
+ const parsed = JSON.parse(cleaned);
312
+ const paths = parsed.compilerOptions?.paths || {};
313
+ const firstKey = Object.keys(paths)[0];
314
+ if (firstKey) {
315
+ return firstKey.replace('*', '');
316
+ }
317
+ } catch {}
318
+ }
319
+ return '@/;';
320
+ }
321
+
322
+ /**
323
+ * AI-Driven Universal Codebase Analyzer for Yap2App.
324
+ */
325
+ export async function scanCodebase(targetDir = process.cwd()) {
326
+ const projectRoot = path.resolve(targetDir);
327
+ const projectName = path.basename(projectRoot) || 'local-project';
328
+
329
+ // 1. Traverse actual workspace files
330
+ const allFiles = await walkDir(projectRoot, projectRoot, 6, 0);
331
+ const discoveredFiles = allFiles.map(f => f.relative);
332
+
333
+ // 2. Compute File Extension Histogram
334
+ const extensionHistogram = {};
335
+ for (const f of allFiles) {
336
+ if (f.ext) {
337
+ extensionHistogram[f.ext] = (extensionHistogram[f.ext] || 0) + 1;
338
+ }
339
+ }
113
340
 
114
- const foundFiles = new Set();
341
+ // 3. Extract Manifest Snippets
342
+ const manifestFiles = [
343
+ 'package.json', 'louhi.json', '_fields.config.json', 'pom.xml', 'build.gradle',
344
+ 'requirements.txt', 'pyproject.toml', 'Gemfile', 'Cargo.toml', 'composer.json',
345
+ 'tsconfig.json', 'angular.json', 'nuxt.config.ts', 'svelte.config.js', 'astro.config.mjs'
346
+ ];
115
347
 
116
- for (const dir of candidateDirs) {
117
- if (fsSync.existsSync(dir)) {
348
+ const manifestsFound = {};
349
+ for (const mf of manifestFiles) {
350
+ const match = allFiles.find(f => f.name === mf);
351
+ if (match) {
118
352
  try {
119
- const files = await fs.readdir(dir);
120
- for (const file of files) {
121
- if (file.endsWith('.tsx') || file.endsWith('.jsx') || file.endsWith('.vue') || file.endsWith('.svelte')) {
122
- const baseName = file.replace(/\.[^/.]+$/, '');
123
- if (!foundFiles.has(baseName.toLowerCase()) && !baseName.startsWith('index') && !baseName.includes('.test') && !baseName.includes('.stories')) {
124
- foundFiles.add(baseName.toLowerCase());
125
-
126
- const pascalName = baseName
127
- .split(/[-_]/)
128
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
129
- .join('');
130
-
131
- componentNames.push(pascalName);
132
-
133
- const isUiDir = dir.includes('ui');
134
- const relativeImport = isUiDir ? `${baseAlias}components/ui/${baseName}` : `${baseAlias}components/${baseName}`;
135
- existingComponents.push(relativeImport);
136
- }
137
- }
138
- }
139
- } catch (e) {
140
- // Ignore read errors
141
- }
353
+ const fullP = path.join(projectRoot, match.relative);
354
+ const content = await fs.readFile(fullP, 'utf8');
355
+ manifestsFound[mf] = content.slice(0, 1500);
356
+ } catch (e) {}
142
357
  }
143
358
  }
144
359
 
145
- // Fallback sensible UI defaults if project has no local UI components yet
146
- if (existingComponents.length === 0) {
147
- componentNames = ['Button', 'Card', 'Input', 'Badge', 'Dialog', 'Tabs'];
148
- existingComponents = [
149
- `${baseAlias}components/ui/button`,
150
- `${baseAlias}components/ui/card`,
151
- `${baseAlias}components/ui/input`,
152
- `${baseAlias}components/ui/badge`,
153
- `${baseAlias}components/ui/dialog`,
154
- `${baseAlias}components/ui/tabs`
155
- ];
156
- }
157
-
158
- // 4. Custom Architectural Guidelines tailored to detected stack
159
- const customGuidelines = `STRICT ENTERPRISE STANDARDS:
160
- - Re-use detected local UI primitives (${componentNames.slice(0, 8).join(', ')}) from ${baseAlias}components/ui/
161
- - Use ${iconLibrary} for all icon rendering
162
- - Target Framework: ${framework.toUpperCase()} (${routerType})
163
- - Styling: Modern Tailwind CSS with Clean White/Slate enterprise palette
164
- - Full WCAG 2.2 AAA accessibility compliance (aria-labels, keyboard focus rings, role attributes)
165
- - Zero-Trust input sanitization (no direct dangerouslySetInnerHTML)`;
360
+ // 4. Extract Real Code Samples from the User's Workspace for Few-Shot Grounding
361
+ const codeSamples = [];
362
+ const candidateSampleExtensions = ['.hbs', '.tsx', '.jsx', '.vue', '.svelte', '.vm', '.ftl', '.jsp', '.html', '.blade.php', '.liquid'];
363
+ const sampleFiles = allFiles.filter(f => candidateSampleExtensions.includes(f.ext) && !f.name.startsWith('_') && !f.name.includes('.test.')).slice(0, 4);
364
+
365
+ for (const sf of sampleFiles) {
366
+ try {
367
+ const fullP = path.join(projectRoot, sf.relative);
368
+ const rawCode = await fs.readFile(fullP, 'utf8');
369
+ const lines = rawCode.split('\n').slice(0, 50).join('\n');
370
+ codeSamples.push({
371
+ file_path: sf.relative,
372
+ extension: sf.ext,
373
+ snippet: lines
374
+ });
375
+ } catch (e) {}
376
+ }
377
+
378
+ // 5. Deep Catalog of Component Signatures & Primitives
379
+ const componentSignatures = await extractComponentSignatures(projectRoot, allFiles);
380
+ const componentNames = componentSignatures.map(c => c.name);
381
+ const realComponents = componentSignatures.map(c => c.path);
382
+
383
+ // 6. Extract Real Design Tokens & Theme Config
384
+ const designTokens = await extractDesignTokens(projectRoot, allFiles);
385
+
386
+ // 7. Extract UI Library Ecosystem & Helpers
387
+ const uiEcosystem = await extractUtilitiesAndIcons(projectRoot, allFiles, manifestsFound);
388
+
389
+ // 8. Deterministic Framework Detection (Strict Verification)
390
+ const verified = detectVerifiedFramework(manifestsFound, allFiles, extensionHistogram);
391
+ const inferredFramework = verified ? verified.framework : null;
392
+ const frameworkDisplayName = verified ? verified.displayName : null;
393
+
394
+ // 9. Path Aliases
395
+ const pathAlias = extractPathAliases(manifestsFound);
396
+
397
+ // 10. Extract Routes & Pages
398
+ const routesDiscovered = allFiles
399
+ .filter(f => f.relative.includes('pages/') || f.relative.includes('app/') || f.name.startsWith('page.'))
400
+ .map(f => f.relative)
401
+ .slice(0, 15);
166
402
 
167
403
  return {
168
404
  project_name: projectName,
169
405
  project_root: projectRoot,
170
- framework,
171
- router_type: routerType,
172
- path_alias: baseAlias.endsWith('/') ? baseAlias : baseAlias + '/',
173
- dependencies: [...dependencies, ...devDependencies],
174
- existing_components: existingComponents,
406
+ framework: inferredFramework,
407
+ framework_display_name: frameworkDisplayName,
408
+ extension_histogram: extensionHistogram,
409
+ manifests: manifestsFound,
410
+ codebase_samples: codeSamples,
411
+ existing_components: realComponents,
412
+ component_signatures: componentSignatures,
175
413
  component_names: componentNames,
176
- design_tokens: designTokens,
177
- icon_library: iconLibrary,
178
- styling_system: stylingSystem,
179
- custom_guidelines: customGuidelines
414
+ ui_ecosystem: uiEcosystem,
415
+ routes_discovered: routesDiscovered,
416
+ discovered_files: discoveredFiles.slice(0, 80),
417
+ total_files_count: discoveredFiles.length,
418
+ path_alias: pathAlias,
419
+ design_tokens: designTokens
180
420
  };
181
421
  }