Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/detector/node/file-system.mjs
1import fs from 'node:fs';2import path from 'node:path';34// ---------------------------------------------------------------------------5// File walker6// ---------------------------------------------------------------------------78const SKIP_DIRS = new Set([9'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output',10'.svelte-kit', '__pycache__', '.turbo', '.vercel',11]);1213const SCANNABLE_EXTENSIONS = new Set([14'.html', '.htm', '.css', '.scss', '.sass', '.less',15'.jsx', '.tsx', '.js', '.ts',16'.vue', '.svelte', '.astro',17]);1819const HTML_EXTENSIONS = new Set(['.html', '.htm']);2021function walkDir(dir) {22const files = [];23let entries;24try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }25for (const entry of entries) {26if (SKIP_DIRS.has(entry.name)) continue;27const full = path.join(dir, entry.name);28if (entry.isDirectory()) files.push(...walkDir(full));29else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);30}31return files;32}333435// ---------------------------------------------------------------------------36// Import graph (multi-file awareness)37// ---------------------------------------------------------------------------3839function resolveImport(specifier, fromDir, fileSet) {40if (!/^[./]/.test(specifier)) return null; // skip bare specifiers41const base = path.resolve(fromDir, specifier);42if (fileSet.has(base)) return base;43for (const ext of SCANNABLE_EXTENSIONS) {44const withExt = base + ext;45if (fileSet.has(withExt)) return withExt;46}47// index file convention48for (const ext of SCANNABLE_EXTENSIONS) {49const indexFile = path.join(base, 'index' + ext);50if (fileSet.has(indexFile)) return indexFile;51}52return null;53}5455function buildImportGraph(files) {56const fileSet = new Set(files);57const graph = new Map();5859for (const file of files) {60const content = fs.readFileSync(file, 'utf-8');61const dir = path.dirname(file);62const imports = new Set();6364// ES imports: import ... from '...' and import '...'65const esRe = /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g;66let m;67while ((m = esRe.exec(content)) !== null) {68const resolved = resolveImport(m[1], dir, fileSet);69if (resolved) imports.add(resolved);70}7172// CSS @import73const cssRe = /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g;74while ((m = cssRe.exec(content)) !== null) {75const resolved = resolveImport(m[1], dir, fileSet);76if (resolved) imports.add(resolved);77}7879// SCSS @use / @forward80const scssRe = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;81while ((m = scssRe.exec(content)) !== null) {82const resolved = resolveImport(m[1], dir, fileSet);83if (resolved) imports.add(resolved);84}8586graph.set(file, imports);87}88return graph;89}9091// ---------------------------------------------------------------------------92// Framework dev server detection93// ---------------------------------------------------------------------------9495const FRAMEWORK_CONFIGS = [96{ name: 'Next.js', files: ['next.config.js', 'next.config.mjs', 'next.config.ts'], defaultPort: 3000,97portRe: /port\s*[:=]\s*(\d+)/,98fingerprint: { header: 'x-powered-by', value: /next/i } },99{ name: 'SvelteKit', files: ['svelte.config.js', 'svelte.config.ts'], defaultPort: 5173,100portRe: /port\s*[:=]\s*(\d+)/,101fingerprint: { header: 'x-sveltekit-page', value: null } },102{ name: 'Nuxt', files: ['nuxt.config.js', 'nuxt.config.ts'], defaultPort: 3000,103portRe: /port\s*[:=]\s*(\d+)/,104fingerprint: { header: 'x-powered-by', value: /nuxt/i } },105{ name: 'Vite', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'], defaultPort: 5173,106portRe: /port\s*[:=]\s*(\d+)/,107fingerprint: { body: /@vite\/client/ } },108{ name: 'Astro', files: ['astro.config.js', 'astro.config.ts', 'astro.config.mjs'], defaultPort: 4321,109portRe: /port\s*[:=]\s*(\d+)/,110fingerprint: { body: /astro/i } },111{ name: 'Angular', files: ['angular.json'], defaultPort: 4200,112portRe: /"port"\s*:\s*(\d+)/,113fingerprint: { body: /ng-version/i } },114{ name: 'Remix', files: ['remix.config.js', 'remix.config.ts'], defaultPort: 3000,115portRe: /port\s*[:=]\s*(\d+)/,116fingerprint: { header: 'x-powered-by', value: /remix/i } },117];118119function detectFrameworkConfig(dir) {120let entries;121try { entries = fs.readdirSync(dir); } catch { return null; }122const entrySet = new Set(entries);123124for (const cfg of FRAMEWORK_CONFIGS) {125const match = cfg.files.find(f => entrySet.has(f));126if (!match) continue;127128const configPath = path.join(dir, match);129let port = cfg.defaultPort;130try {131const content = fs.readFileSync(configPath, 'utf-8');132const portMatch = content.match(cfg.portRe);133if (portMatch) port = parseInt(portMatch[1], 10);134} catch { /* use default */ }135136return { name: cfg.name, port, configPath, fingerprint: cfg.fingerprint };137}138return null;139}140141/**142* Check if a port is listening and optionally verify it matches the expected framework.143* Returns { listening: true, matched: true/false } or { listening: false }.144*/145async function isPortListening(port, fingerprint = null) {146if (!fingerprint) {147// Simple TCP probe fallback148const net = await import('node:net');149return new Promise((resolve) => {150const sock = net.default.createConnection({ port, host: '127.0.0.1' });151sock.setTimeout(500);152sock.on('connect', () => { sock.destroy(); resolve({ listening: true, matched: true }); });153sock.on('error', () => resolve({ listening: false }));154sock.on('timeout', () => { sock.destroy(); resolve({ listening: false }); });155});156}157158// HTTP probe with fingerprint matching159try {160const controller = new AbortController();161const timeout = setTimeout(() => controller.abort(), 2000);162const res = await fetch(`http://localhost:${port}/`, { signal: controller.signal, redirect: 'follow' });163clearTimeout(timeout);164165// Check header fingerprint166if (fingerprint.header) {167const val = res.headers.get(fingerprint.header);168if (val && (!fingerprint.value || fingerprint.value.test(val))) {169return { listening: true, matched: true };170}171}172173// Check body fingerprint174if (fingerprint.body) {175const body = await res.text();176if (fingerprint.body.test(body)) {177return { listening: true, matched: true };178}179}180181// Port is listening but doesn't match the expected framework182return { listening: true, matched: false };183} catch {184return { listening: false };185}186}187188export {189SKIP_DIRS,190SCANNABLE_EXTENSIONS,191HTML_EXTENSIONS,192walkDir,193resolveImport,194buildImportGraph,195FRAMEWORK_CONFIGS,196detectFrameworkConfig,197isPortListening,198};199