Files
node-projectbrowser-app/lib/scanner.js
2026-03-07 23:51:53 +01:00

133 lines
3.6 KiB
JavaScript

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 };