yap2app 1.1.0 → 1.1.2

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/bridge.js CHANGED
@@ -1,11 +1,17 @@
1
- #!/usr/bin/env node
2
-
3
1
  import http from 'http';
4
- import fs from 'fs/promises';
5
2
  import fsSync from 'fs';
6
3
  import path from 'path';
4
+ import util from 'util';
7
5
  import { scanCodebase } from './scanner.js';
8
6
 
7
+ const fs = fsSync.promises || {
8
+ readdir: util.promisify(fsSync.readdir),
9
+ readFile: util.promisify(fsSync.readFile),
10
+ writeFile: util.promisify(fsSync.writeFile),
11
+ mkdir: util.promisify(fsSync.mkdir),
12
+ stat: util.promisify(fsSync.stat)
13
+ };
14
+
9
15
  const PORT = process.env.YAP2APP_PORT || 10420;
10
16
  const PROJECT_ROOT = process.cwd();
11
17
 
@@ -91,11 +97,13 @@ const server = http.createServer(async (req, res) => {
91
97
  return;
92
98
  }
93
99
 
94
- // 3. GET /api/context (Shallow Codebase Scanner)
95
- if (req.method === 'GET' && req.url === '/api/context') {
100
+ // 3. GET /api/context (Shallow Codebase Scanner with Optional Prompt Query)
101
+ if (req.method === 'GET' && req.url.startsWith('/api/context')) {
96
102
  try {
97
- console.log(`[Yap2App Bridge] 🔍 Scanning shallow codebase context in: ${PROJECT_ROOT}`);
98
- const context = await scanCodebase(PROJECT_ROOT);
103
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
104
+ const promptQuery = parsedUrl.searchParams.get('q') || '';
105
+ console.log(`[Yap2App Bridge] 🔍 Scanning shallow codebase context in: ${PROJECT_ROOT}${promptQuery ? ` (ranking for query: "${promptQuery}")` : ''}`);
106
+ const context = await scanCodebase(PROJECT_ROOT, promptQuery);
99
107
  res.writeHead(200, headers);
100
108
  res.end(JSON.stringify(context));
101
109
  } catch (e) {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "yap2app",
3
- "version": "1.1.0",
4
- "description": "Autonomous Vibe-to-Prod Localhost Bridge & Model Context Protocol (MCP) Server",
5
- "main": "bridge.js",
3
+ "version": "1.1.2",
6
4
  "type": "module",
5
+ "description": "Autonomous Vibe-to-Prod Localhost Bridge & Model Context Protocol (MCP) Server",
6
+ "main": "bin/cli.bundle.js",
7
7
  "bin": {
8
8
  "yap2app": "./bin/cli.js"
9
9
  },
package/scanner.js CHANGED
@@ -1,6 +1,14 @@
1
- import fs from 'fs/promises';
2
1
  import fsSync from 'fs';
3
2
  import path from 'path';
3
+ import util from 'util';
4
+
5
+ const fs = fsSync.promises || {
6
+ readdir: util.promisify(fsSync.readdir),
7
+ readFile: util.promisify(fsSync.readFile),
8
+ writeFile: util.promisify(fsSync.writeFile),
9
+ mkdir: util.promisify(fsSync.mkdir),
10
+ stat: util.promisify(fsSync.stat)
11
+ };
4
12
 
5
13
  const IGNORED_DIRS = new Set([
6
14
  'node_modules',
@@ -28,18 +36,32 @@ async function walkDir(dir, baseDir, maxDepth = 6, currentDepth = 0) {
28
36
  if (currentDepth > maxDepth) return [];
29
37
  let results = [];
30
38
  try {
31
- const entries = await fs.readdir(dir, { withFileTypes: true });
32
- for (const entry of entries) {
33
- const fullPath = path.join(dir, entry.name);
39
+ const rawEntries = await fs.readdir(dir);
40
+ for (const rawEntry of rawEntries) {
41
+ const entryName = typeof rawEntry === 'string' ? rawEntry : rawEntry.name;
42
+ const fullPath = path.join(dir, entryName);
34
43
  const relativePath = path.relative(baseDir, fullPath);
35
44
 
36
- if (entry.isDirectory()) {
37
- if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
45
+ let isDir = false;
46
+ let isFile = false;
47
+ if (typeof rawEntry === 'object' && rawEntry.isDirectory && rawEntry.isFile) {
48
+ isDir = rawEntry.isDirectory();
49
+ isFile = rawEntry.isFile();
50
+ } else {
51
+ try {
52
+ const st = fsSync.statSync(fullPath);
53
+ isDir = st.isDirectory();
54
+ isFile = st.isFile();
55
+ } catch {}
56
+ }
57
+
58
+ if (isDir) {
59
+ if (!IGNORED_DIRS.has(entryName) && !entryName.startsWith('.')) {
38
60
  const sub = await walkDir(fullPath, baseDir, maxDepth, currentDepth + 1);
39
61
  results = results.concat(sub);
40
62
  }
41
- } else if (entry.isFile()) {
42
- const ext = path.extname(entry.name).toLowerCase();
63
+ } else if (isFile) {
64
+ const ext = path.extname(entryName).toLowerCase();
43
65
  if ([
44
66
  '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.hbs', '.handlebars',
45
67
  '.html', '.css', '.scss', '.sass', '.less', '.vm', '.ftl', '.jsp', '.php',
@@ -49,7 +71,7 @@ async function walkDir(dir, baseDir, maxDepth = 6, currentDepth = 0) {
49
71
  ].includes(ext)) {
50
72
  results.push({
51
73
  relative: relativePath,
52
- name: entry.name,
74
+ name: entryName,
53
75
  ext,
54
76
  dir: path.relative(baseDir, dir)
55
77
  });
@@ -84,11 +106,33 @@ function parsePackageJson(content) {
84
106
  * Returns null if no verified signature matches with high confidence.
85
107
  */
86
108
  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'))) {
109
+ // 1. Google Brightspot CMS / Glue-Bundle / Handlebars Frontend
110
+ const hasHbsFiles = (extensionHistogram['.hbs'] || 0) > 0 || (extensionHistogram['.handlebars'] || 0) > 0;
111
+ const hasBrightspotConfigs = allFiles.some(f =>
112
+ f.name === 'louhi.json' ||
113
+ f.name === '_fields.config.json' ||
114
+ f.name === '_data.json' ||
115
+ f.name === 'brightspot.json' ||
116
+ f.name.includes('_config.json') ||
117
+ f.relative.includes('styleguide/') ||
118
+ f.relative.includes('brightspot')
119
+ );
120
+ const hasBrightspotManifest = manifests['louhi.json'] ||
121
+ manifests['_fields.config.json'] ||
122
+ (manifests['package.json'] && (
123
+ manifests['package.json'].includes('google-marketing-bundle-glue') ||
124
+ manifests['package.json'].includes('brightspot') ||
125
+ manifests['package.json'].includes('handlebars')
126
+ )) ||
127
+ (manifests['pom.xml'] && (
128
+ manifests['pom.xml'].includes('brightspot') ||
129
+ manifests['pom.xml'].includes('psddev')
130
+ ));
131
+
132
+ if (hasHbsFiles || hasBrightspotConfigs || hasBrightspotManifest) {
89
133
  return {
90
134
  framework: 'brightspot_glue',
91
- displayName: 'Google Brightspot CMS / Glue-Bundle'
135
+ displayName: 'Google Brightspot CMS / Handlebars'
92
136
  };
93
137
  }
94
138
 
@@ -116,28 +160,36 @@ function detectVerifiedFramework(manifests, allFiles, extensionHistogram) {
116
160
  return { framework: 'angular', displayName: 'Angular Project' };
117
161
  }
118
162
 
119
- // 4. Python Ecosystem
163
+ // 4. Standalone UI file distribution inspection
164
+ const htmlCount = extensionHistogram['.html'] || 0;
165
+ const scssCount = (extensionHistogram['.scss'] || 0) + (extensionHistogram['.css'] || 0);
166
+ const jsCount = (extensionHistogram['.js'] || 0) + (extensionHistogram['.ts'] || 0);
167
+
168
+ // 5. Python Ecosystem (Only if no primary UI files found)
120
169
  if (manifests['requirements.txt'] || manifests['pyproject.toml']) {
121
170
  const pyReqs = (manifests['requirements.txt'] || '') + (manifests['pyproject.toml'] || '');
122
171
  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' };
172
+ if (/fastapi/i.test(pyReqs) && htmlCount === 0 && scssCount === 0) return { framework: 'fastapi', displayName: 'FastAPI' };
173
+ if (/flask/i.test(pyReqs) && htmlCount === 0 && scssCount === 0) return { framework: 'flask', displayName: 'Flask' };
125
174
  }
126
175
 
127
- // 5. PHP Ecosystem
176
+ // 6. PHP Ecosystem
128
177
  if (manifests['composer.json']) {
129
178
  if (manifests['composer.json'].includes('laravel/framework')) return { framework: 'laravel', displayName: 'Laravel' };
130
179
  if (manifests['composer.json'].includes('symfony/')) return { framework: 'symfony', displayName: 'Symfony' };
131
180
  }
132
181
 
133
- // 6. Java Ecosystem
182
+ // 7. Java Ecosystem
134
183
  if (manifests['pom.xml'] || manifests['build.gradle']) {
135
184
  const jContent = (manifests['pom.xml'] || '') + (manifests['build.gradle'] || '');
136
185
  if (jContent.includes('spring-boot')) return { framework: 'spring_boot', displayName: 'Spring Boot' };
137
186
  }
138
187
 
139
- // If not 100% verified, return null (do NOT guess or display inaccurate tags)
140
- return null;
188
+ // Fallback to Plain HTML / CSS / JS for standard web projects
189
+ return {
190
+ framework: 'plain_html_css_js',
191
+ displayName: 'Plain HTML / CSS / JS'
192
+ };
141
193
  }
142
194
 
143
195
  /**
@@ -243,6 +295,119 @@ async function extractUtilitiesAndIcons(projectRoot, allFiles, manifests) {
243
295
  return uiEcosystem;
244
296
  }
245
297
 
298
+ /**
299
+ * Deep Design System & SCSS Mixins Extraction.
300
+ * Detects whether the workspace uses Google Glue, Tailwind CSS, Bootstrap, or custom SCSS mixins,
301
+ * and catalogs reusable project mixins, import paths, and class conventions.
302
+ */
303
+ async function extractDesignSystemAndMixins(projectRoot, allFiles, manifests) {
304
+ const designSystem = {
305
+ name: 'custom_bem',
306
+ display_name: 'Custom BEM / SCSS',
307
+ is_glue: false,
308
+ is_tailwind: false,
309
+ is_bootstrap: false,
310
+ class_prefix: '',
311
+ mixins_available: [],
312
+ import_paths: [],
313
+ guidelines: []
314
+ };
315
+
316
+ // 1. Detect Google Glue Design System
317
+ const hasGluePackage = Boolean(
318
+ manifests['package.json'] && (
319
+ manifests['package.json'].includes('@google/glue') ||
320
+ manifests['package.json'].includes('google-marketing-bundle-glue') ||
321
+ manifests['package.json'].includes('glue')
322
+ )
323
+ );
324
+ const hasGlueFiles = allFiles.some(f =>
325
+ f.name.toLowerCase().includes('glue') ||
326
+ f.relative.toLowerCase().includes('glue/')
327
+ );
328
+
329
+ // Check code samples/style files for glue references
330
+ let hasGlueClasses = false;
331
+ const styleAndTemplateFiles = allFiles.filter(f =>
332
+ ['.scss', '.css', '.hbs', '.handlebars', '.html'].includes(f.ext)
333
+ ).slice(0, 25);
334
+
335
+ for (const f of styleAndTemplateFiles) {
336
+ try {
337
+ const fullP = path.join(projectRoot, f.relative);
338
+ const content = await fs.readFile(fullP, 'utf8');
339
+ if (content.includes('glue-') || content.includes('@include glue-') || content.includes('@import "glue') || content.includes("@import 'glue")) {
340
+ hasGlueClasses = true;
341
+ break;
342
+ }
343
+ } catch {}
344
+ }
345
+
346
+ if (hasGluePackage || hasGlueFiles || hasGlueClasses) {
347
+ designSystem.name = 'google_glue';
348
+ designSystem.display_name = 'Google Glue Web Design System';
349
+ designSystem.is_glue = true;
350
+ designSystem.class_prefix = 'glue-';
351
+ designSystem.guidelines = [
352
+ 'MUST use official Google Glue HTML classes: glue-headline (glue-headline--1, glue-headline--2, glue-headline--3), glue-button (glue-button--primary, glue-button--secondary), glue-card (glue-card--elevated), glue-grid, glue-mod-elevated, glue-mod-color-primary, glue-eyebrow, glue-body.',
353
+ 'In SCSS, MUST @import existing Glue stylesheets and use Glue mixins (@include glue-typography(...), @include glue-elevation(...), @include glue-button(...)) instead of creating redundant custom mixins, custom classes, or ad-hoc variables.',
354
+ 'Maintain strict Google Glue design system alignment and BEM modifier syntax.'
355
+ ];
356
+ } else if (
357
+ (manifests['package.json'] && manifests['package.json'].includes('tailwindcss')) ||
358
+ allFiles.some(f => f.name.startsWith('tailwind.config'))
359
+ ) {
360
+ designSystem.name = 'tailwind';
361
+ designSystem.display_name = 'Tailwind CSS';
362
+ designSystem.is_tailwind = true;
363
+ designSystem.guidelines = [
364
+ 'Use standard Tailwind utility classes directly on HTML elements.',
365
+ 'Avoid custom ad-hoc CSS rules when standard Tailwind classes can be used.'
366
+ ];
367
+ } else if (
368
+ (manifests['package.json'] && manifests['package.json'].includes('bootstrap')) ||
369
+ allFiles.some(f => f.name.includes('bootstrap'))
370
+ ) {
371
+ designSystem.name = 'bootstrap';
372
+ designSystem.display_name = 'Bootstrap';
373
+ designSystem.is_bootstrap = true;
374
+ designSystem.guidelines = [
375
+ 'Use Bootstrap utility classes and components (container, row, col, btn, card, etc.).'
376
+ ];
377
+ }
378
+
379
+ // 2. Scan SCSS Files for Available Mixins, Functions, and Import Paths
380
+ const scssFiles = allFiles.filter(f => f.ext === '.scss').slice(0, 35);
381
+ for (const sf of scssFiles) {
382
+ try {
383
+ const fullP = path.join(projectRoot, sf.relative);
384
+ const content = await fs.readFile(fullP, 'utf8');
385
+
386
+ // Extract @mixin declarations
387
+ const mixinMatches = content.matchAll(/@mixin\s+([a-zA-Z0-9_-]+)\s*(\([^)]*\))?/g);
388
+ for (const m of mixinMatches) {
389
+ if (designSystem.mixins_available.length < 35) {
390
+ designSystem.mixins_available.push({
391
+ name: m[1],
392
+ signature: m[0].trim(),
393
+ file: sf.relative
394
+ });
395
+ }
396
+ }
397
+
398
+ // If this file contains mixins or variables, record as reusable import path
399
+ if (sf.name.startsWith('_') || sf.relative.includes('mixin') || sf.relative.includes('variable') || sf.relative.includes('theme') || sf.relative.includes('styleguide') || sf.relative.includes('glue')) {
400
+ const importPath = sf.relative.replace(/\.scss$/, '').replace(/^_/, '');
401
+ if (!designSystem.import_paths.includes(importPath) && designSystem.import_paths.length < 20) {
402
+ designSystem.import_paths.push(importPath);
403
+ }
404
+ }
405
+ } catch {}
406
+ }
407
+
408
+ return designSystem;
409
+ }
410
+
246
411
  /**
247
412
  * Extracts reusable UI components with their export signatures and interface snippets.
248
413
  */
@@ -316,13 +481,100 @@ function extractPathAliases(manifests) {
316
481
  }
317
482
  } catch {}
318
483
  }
319
- return '@/;';
484
+ return '@/';
485
+ }
486
+
487
+ /**
488
+ * Deterministic Routing Topology Detection.
489
+ * Categorizes the project architecture into known routing paradigms.
490
+ */
491
+ function detectRoutingTopology(projectRoot, allFiles, manifests) {
492
+ // 1. Brightspot CMS / Glue-Bundle
493
+ const isBrightspot = allFiles.some(f =>
494
+ f.name === 'louhi.json' ||
495
+ f.name === '_fields.config.json' ||
496
+ f.name === 'brightspot.json' ||
497
+ f.relative.includes('styleguide/') ||
498
+ f.relative.includes('brightspot')
499
+ ) || Boolean(manifests['louhi.json'] || manifests['_fields.config.json']);
500
+
501
+ if (isBrightspot) {
502
+ return 'brightspot-glue';
503
+ }
504
+
505
+ // 2. Next.js App Router (src/app or app/)
506
+ const hasAppRouter = allFiles.some(f =>
507
+ (f.relative.startsWith('app/') || f.relative.startsWith('src/app/')) &&
508
+ (f.name.startsWith('page.') || f.name.startsWith('layout.') || f.name.startsWith('route.'))
509
+ );
510
+ if (hasAppRouter) {
511
+ return 'nextjs-app-router';
512
+ }
513
+
514
+ // 3. Next.js Pages Router (src/pages or pages/)
515
+ const hasPagesRouter = allFiles.some(f =>
516
+ (f.relative.startsWith('pages/') || f.relative.startsWith('src/pages/')) &&
517
+ (f.name.startsWith('_app.') || f.name.startsWith('_document.') || f.name.startsWith('index.'))
518
+ );
519
+ if (hasPagesRouter) {
520
+ return 'nextjs-pages-router';
521
+ }
522
+
523
+ // 4. Default Single Page Application / Static Architecture
524
+ return 'standard-spa';
525
+ }
526
+
527
+ /**
528
+ * Prompt-Aware Lexical Ranking of Reusable Components.
529
+ * Ranks components by semantic/lexical overlap with a given prompt or keywords,
530
+ * returning the top 15 most relevant components.
531
+ */
532
+ export function rankComponentsByRelevance(components = [], prompt = '') {
533
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
534
+ return components.slice(0, 15);
535
+ }
536
+
537
+ // Extract alphanumeric tokens from prompt (lowercased, min length 2)
538
+ const promptTokens = prompt
539
+ .toLowerCase()
540
+ .replace(/[^a-z0-9\s]/g, ' ')
541
+ .split(/\s+/)
542
+ .filter(t => t.length >= 2);
543
+
544
+ if (promptTokens.length === 0) {
545
+ return components.slice(0, 15);
546
+ }
547
+
548
+ const scored = components.map(comp => {
549
+ const compName = (typeof comp === 'string' ? comp : comp.name || comp.path || '').toLowerCase();
550
+ const compPath = (typeof comp === 'object' && comp.path ? comp.path : '').toLowerCase();
551
+ const compSig = (typeof comp === 'object' && comp.signature ? comp.signature : '').toLowerCase();
552
+ const compProps = (typeof comp === 'object' && comp.props ? comp.props : '').toLowerCase();
553
+
554
+ const targetText = `${compName} ${compPath} ${compSig} ${compProps}`;
555
+ let score = 0;
556
+
557
+ for (const token of promptTokens) {
558
+ if (compName.includes(token)) score += 10;
559
+ if (compPath.includes(token)) score += 5;
560
+ if (compSig.includes(token)) score += 3;
561
+ if (compProps.includes(token)) score += 2;
562
+ if (targetText.includes(token)) score += 1;
563
+ }
564
+
565
+ return { comp, score };
566
+ });
567
+
568
+ // Sort descending by score, maintaining original order on ties
569
+ scored.sort((a, b) => b.score - a.score);
570
+
571
+ return scored.slice(0, 15).map(item => item.comp);
320
572
  }
321
573
 
322
574
  /**
323
575
  * AI-Driven Universal Codebase Analyzer for Yap2App.
324
576
  */
325
- export async function scanCodebase(targetDir = process.cwd()) {
577
+ export async function scanCodebase(targetDir = process.cwd(), promptQuery = '') {
326
578
  const projectRoot = path.resolve(targetDir);
327
579
  const projectName = path.basename(projectRoot) || 'local-project';
328
580
 
@@ -375,8 +627,9 @@ export async function scanCodebase(targetDir = process.cwd()) {
375
627
  } catch (e) {}
376
628
  }
377
629
 
378
- // 5. Deep Catalog of Component Signatures & Primitives
379
- const componentSignatures = await extractComponentSignatures(projectRoot, allFiles);
630
+ // 5. Deep Catalog of Component Signatures & Primitives (Ranked to top 15)
631
+ const rawComponentSignatures = await extractComponentSignatures(projectRoot, allFiles);
632
+ const componentSignatures = rankComponentsByRelevance(rawComponentSignatures, promptQuery);
380
633
  const componentNames = componentSignatures.map(c => c.name);
381
634
  const realComponents = componentSignatures.map(c => c.path);
382
635
 
@@ -386,15 +639,22 @@ export async function scanCodebase(targetDir = process.cwd()) {
386
639
  // 7. Extract UI Library Ecosystem & Helpers
387
640
  const uiEcosystem = await extractUtilitiesAndIcons(projectRoot, allFiles, manifestsFound);
388
641
 
389
- // 8. Deterministic Framework Detection (Strict Verification)
642
+ // 8. Extract Deep Design System & Reusable SCSS Mixins
643
+ const designSystem = await extractDesignSystemAndMixins(projectRoot, allFiles, manifestsFound);
644
+ uiEcosystem.design_system = designSystem;
645
+
646
+ // 9. Deterministic Framework Detection (Strict Verification)
390
647
  const verified = detectVerifiedFramework(manifestsFound, allFiles, extensionHistogram);
391
648
  const inferredFramework = verified ? verified.framework : null;
392
649
  const frameworkDisplayName = verified ? verified.displayName : null;
393
650
 
394
- // 9. Path Aliases
651
+ // 10. Detect Routing Topology
652
+ const routingTopology = detectRoutingTopology(projectRoot, allFiles, manifestsFound);
653
+
654
+ // 11. Path Aliases
395
655
  const pathAlias = extractPathAliases(manifestsFound);
396
656
 
397
- // 10. Extract Routes & Pages
657
+ // 12. Extract Routes & Pages
398
658
  const routesDiscovered = allFiles
399
659
  .filter(f => f.relative.includes('pages/') || f.relative.includes('app/') || f.name.startsWith('page.'))
400
660
  .map(f => f.relative)
@@ -405,12 +665,14 @@ export async function scanCodebase(targetDir = process.cwd()) {
405
665
  project_root: projectRoot,
406
666
  framework: inferredFramework,
407
667
  framework_display_name: frameworkDisplayName,
668
+ routing_topology: routingTopology,
408
669
  extension_histogram: extensionHistogram,
409
670
  manifests: manifestsFound,
410
671
  codebase_samples: codeSamples,
411
672
  existing_components: realComponents,
412
673
  component_signatures: componentSignatures,
413
674
  component_names: componentNames,
675
+ design_system: designSystem,
414
676
  ui_ecosystem: uiEcosystem,
415
677
  routes_discovered: routesDiscovered,
416
678
  discovered_files: discoveredFiles.slice(0, 80),