const fs = require('fs'); const path = require('path'); const { parseEnvFile } = require('./env-parser'); const BASE_DIR = path.resolve(__dirname, '../../'); const SKIP_DIRS = new Set([ 'backups', 'varia', 'wn-golem-starter', 'summercms.io', 'docs', 'projects-browser' ]); function extractPortFromUrl(url) { if (!url) return null; try { const u = new URL(url); const port = parseInt(u.port, 10); return port || null; } catch { return null; } } function detectFrontend(dirPath) { try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory() || !entry.name.startsWith('vue-')) continue; const frontendDir = path.join(dirPath, entry.name); const pkgPath = path.join(frontendDir, 'package.json'); if (!fs.existsSync(pkgPath)) continue; try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); if (!pkg.scripts?.dev) continue; } catch { continue; } const hasPnpmLock = fs.existsSync(path.join(frontendDir, 'pnpm-lock.yaml')); return { dir: entry.name, fullPath: frontendDir, packageManager: hasPnpmLock ? 'pnpm' : 'npm', }; } } catch {} return null; } function scanProjects() { const projects = []; const entries = fs.readdirSync(BASE_DIR, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue; const dirPath = path.join(BASE_DIR, entry.name); // Check if artisan exists directly if (fs.existsSync(path.join(dirPath, 'artisan'))) { projects.push(buildProjectMeta(dirPath, entry.name)); continue; } // Check one level deeper try { const subEntries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const sub of subEntries) { if (!sub.isDirectory() || sub.name.startsWith('.')) continue; const subPath = path.join(dirPath, sub.name); if (fs.existsSync(path.join(subPath, 'artisan'))) { projects.push(buildProjectMeta(subPath, entry.name)); } } } catch {} } // Sort by slug for deterministic port assignment projects.sort((a, b) => a.slug.localeCompare(b.slug)); // Assign API ports to projects without one let nextPort = 8100; const usedPorts = new Set(projects.filter(p => p.port).map(p => p.port)); for (const project of projects) { if (!project.port) { while (usedPorts.has(nextPort)) nextPort++; project.port = nextPort; project.portSource = 'assigned'; usedPorts.add(nextPort); nextPort++; } } // Assign frontend ports deterministically from 3010+ let nextFrontendPort = 3010; for (const project of projects) { if (project.frontend) { project.frontend.port = nextFrontendPort++; } } return projects; } function buildProjectMeta(dirPath, slug) { const envPath = path.join(dirPath, '.env'); const hasEnv = fs.existsSync(envPath); const env = hasEnv ? parseEnvFile(envPath) : {}; const port = extractPortFromUrl(env.APP_URL); // Make SQLite path relative if it starts with the project path let dbName = env.DB_DATABASE || null; if (dbName && dbName.startsWith(dirPath)) { dbName = path.relative(dirPath, dbName); } const frontend = detectFrontend(dirPath); const hasDocs = fs.existsSync(path.join(dirPath, 'docs')); return { slug, name: slug, path: dirPath, port: port, portSource: port ? '.env' : null, hasEnv, dbType: env.DB_CONNECTION || null, dbName, frontend, hasDocs, }; } module.exports = { scanProjects };