133 lines
3.6 KiB
JavaScript
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 };
|