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 +48 -0
- package/bin/cli.js +12 -41
- package/bridge.js +23 -17
- package/package.json +13 -1
- package/scanner.js +385 -145
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
|
-
|
|
4
|
-
|
|
5
|
-
import { scanCodebase } from '../scanner.js';
|
|
3
|
+
var nodeVersion = process.version;
|
|
4
|
+
var majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
151
|
+
colors: {},
|
|
152
|
+
cssVariables: [],
|
|
153
|
+
tailwindTheme: null
|
|
30
154
|
};
|
|
31
155
|
|
|
32
|
-
// 1.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
}
|