first pass

This commit is contained in:
2026-01-26 22:33:55 -06:00
commit fe66be4aad
37 changed files with 3127 additions and 0 deletions

147
cli/detectors/dotnet.js Normal file
View File

@@ -0,0 +1,147 @@
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join, basename } from 'path';
import { glob } from 'glob';
/**
* Detect .NET project type and configuration
*/
export async function detectDotNet(projectPath) {
// Find .csproj files
const csprojFiles = await glob('*.csproj', { cwd: projectPath });
if (csprojFiles.length === 0) {
// Check for .sln file (solution)
const slnFiles = await glob('*.sln', { cwd: projectPath });
if (slnFiles.length > 0) {
return {
type: 'dotnet-solution',
dockerizable: false,
reason: 'Solution files require building individual projects',
template: null
};
}
return null;
}
const csprojFile = csprojFiles[0];
const csprojPath = join(projectPath, csprojFile);
const csprojContent = readFileSync(csprojPath, 'utf-8');
// Detect .NET version
const targetFrameworkMatch = csprojContent.match(/<TargetFramework>([^<]+)<\/TargetFramework>/);
const targetFramework = targetFrameworkMatch ? targetFrameworkMatch[1] : 'net8.0';
// Extract version number (e.g., net9.0 -> 9.0)
const versionMatch = targetFramework.match(/net(\d+\.\d+)/);
const dotnetVersion = versionMatch ? versionMatch[1] : '8.0';
// Detect project type
const isBlazor = csprojContent.includes('Microsoft.AspNetCore.Components') ||
csprojContent.includes('MudBlazor') ||
csprojContent.includes('Blazor');
const isWebAPI = csprojContent.includes('Microsoft.NET.Sdk.Web') &&
!isBlazor;
const isConsole = csprojContent.includes('Microsoft.NET.Sdk') &&
!csprojContent.includes('Microsoft.NET.Sdk.Web');
// Get assembly name for DLL
const assemblyNameMatch = csprojContent.match(/<AssemblyName>([^<]+)<\/AssemblyName>/);
const projectName = basename(csprojFile, '.csproj');
const assemblyName = assemblyNameMatch ? assemblyNameMatch[1] : projectName;
const dllName = `${assemblyName}.dll`;
if (isBlazor) {
return {
type: 'dotnet-blazor',
dockerizable: true,
template: 'dotnet/blazor',
port: 8080,
entryPoint: dllName,
buildCommand: `dotnet publish -c Release`,
description: 'Blazor web application',
csprojFile,
dllName,
dotnetVersion,
targetFramework
};
}
if (isWebAPI) {
return {
type: 'dotnet-webapi',
dockerizable: true,
template: 'dotnet/webapi',
port: 8080,
entryPoint: dllName,
buildCommand: `dotnet publish -c Release`,
description: '.NET Web API',
csprojFile,
dllName,
dotnetVersion,
targetFramework
};
}
if (isConsole) {
return {
type: 'dotnet-console',
dockerizable: true,
template: 'dotnet/console',
port: null,
entryPoint: dllName,
buildCommand: `dotnet publish -c Release`,
description: '.NET Console application',
csprojFile,
dllName,
dotnetVersion,
targetFramework
};
}
// Default to web
return {
type: 'dotnet-generic',
dockerizable: true,
template: 'dotnet/webapi',
port: 8080,
entryPoint: dllName,
buildCommand: `dotnet publish -c Release`,
description: '.NET application',
csprojFile,
dllName,
dotnetVersion,
targetFramework
};
}
/**
* Get additional info about .NET project
*/
export async function getDotNetInfo(projectPath) {
const csprojFiles = await glob('*.csproj', { cwd: projectPath });
if (csprojFiles.length === 0) {
return null;
}
const csprojFile = csprojFiles[0];
const csprojPath = join(projectPath, csprojFile);
const content = readFileSync(csprojPath, 'utf-8');
// Extract package references
const packageRefs = content.match(/<PackageReference\s+Include="([^"]+)"/g) || [];
const packages = packageRefs.map(ref => {
const match = ref.match(/Include="([^"]+)"/);
return match ? match[1] : null;
}).filter(Boolean);
return {
csprojFile,
packages,
hasLaunchSettings: existsSync(join(projectPath, 'Properties', 'launchSettings.json')),
hasAppSettings: existsSync(join(projectPath, 'appsettings.json')),
hasWwwroot: existsSync(join(projectPath, 'wwwroot'))
};
}

126
cli/detectors/index.js Normal file
View File

@@ -0,0 +1,126 @@
import { detectNodeJS, getNodeJSInfo } from './nodejs.js';
import { detectPython, getPythonInfo } from './python.js';
import { detectDotNet, getDotNetInfo } from './dotnet.js';
import { detectStatic, getStaticInfo } from './static.js';
import { basename } from 'path';
/**
* Detect project type by running all detectors
* Returns the first successful detection
*/
export async function detectProject(projectPath) {
const projectName = basename(projectPath);
// Run detectors in order of priority
// Node.js first (most common), then Python, .NET, and finally static
// 1. Node.js detection
const nodeResult = detectNodeJS(projectPath);
if (nodeResult) {
return {
...nodeResult,
projectName,
projectPath
};
}
// 2. Python detection
const pythonResult = detectPython(projectPath);
if (pythonResult) {
return {
...pythonResult,
projectName,
projectPath
};
}
// 3. .NET detection
const dotnetResult = await detectDotNet(projectPath);
if (dotnetResult) {
return {
...dotnetResult,
projectName,
projectPath
};
}
// 4. Static site detection
const staticResult = await detectStatic(projectPath);
if (staticResult) {
return {
...staticResult,
projectName,
projectPath
};
}
// No detection
return {
type: 'unknown',
dockerizable: false,
reason: 'Could not determine project type. No package.json, requirements.txt, .csproj, or index.html found.',
projectName,
projectPath,
template: null
};
}
/**
* Get detailed info about a project
*/
export async function getProjectInfo(projectPath, type) {
switch (true) {
case type.startsWith('nodejs'):
return getNodeJSInfo(projectPath);
case type.startsWith('python'):
return getPythonInfo(projectPath);
case type.startsWith('dotnet'):
return await getDotNetInfo(projectPath);
case type.startsWith('static') || type.startsWith('flutter'):
return await getStaticInfo(projectPath);
default:
return null;
}
}
/**
* Check if a path is a valid project directory
*/
export function isValidProject(projectPath) {
// Exclude common non-project directories
const excludePatterns = [
'node_modules',
'.git',
'.vscode',
'.idea',
'dist',
'build',
'coverage',
'__pycache__',
'venv',
'.venv'
];
const name = basename(projectPath);
return !excludePatterns.includes(name) && !name.startsWith('.');
}
/**
* Get all project types supported
*/
export function getSupportedTypes() {
return [
{ type: 'nodejs-express', description: 'Express.js server', template: 'nodejs/express' },
{ type: 'nodejs-vite-react', description: 'Vite + React SPA', template: 'nodejs/vite-react' },
{ type: 'nodejs-vite-react-ssr', description: 'Vite + React with Express SSR', template: 'nodejs/vite-react-ssr' },
{ type: 'nodejs-generic', description: 'Generic Node.js application', template: 'nodejs/express' },
{ type: 'python-standard', description: 'Standard Python application', template: 'python/standard' },
{ type: 'python-ml-pytorch', description: 'Python ML/AI with PyTorch', template: 'python/ml-pytorch' },
{ type: 'dotnet-blazor', description: '.NET Blazor web application', template: 'dotnet/blazor' },
{ type: 'dotnet-webapi', description: '.NET Web API', template: 'dotnet/webapi' },
{ type: 'static-nginx', description: 'Static website with Nginx', template: 'static/nginx' },
{ type: 'flutter-web', description: 'Flutter web application', template: 'static/nginx' }
];
}
export { detectNodeJS, detectPython, detectDotNet, detectStatic };

210
cli/detectors/nodejs.js Normal file
View File

@@ -0,0 +1,210 @@
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
/**
* Detect Node.js project type and configuration
*/
export function detectNodeJS(projectPath) {
const packageJsonPath = join(projectPath, 'package.json');
if (!existsSync(packageJsonPath)) {
return null;
}
let pkg;
try {
pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
} catch (error) {
return null;
}
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
const scripts = pkg.scripts || {};
// Check for Electron (not dockerizable)
if (deps.electron || devDeps.electron) {
return {
type: 'nodejs-electron',
dockerizable: false,
reason: 'Electron apps are desktop applications and cannot be containerized',
template: null
};
}
// Detect project subtype
const hasVite = !!devDeps.vite;
const hasReact = !!(deps.react || devDeps.react);
const hasExpress = !!deps.express;
const hasSocketIO = !!(deps['socket.io'] || deps['socket.io-client']);
const hasConcurrently = !!devDeps.concurrently;
// Check for server-side rendering setup (Vite + React + Express with concurrent dev)
if (hasVite && hasReact && hasExpress && (hasConcurrently || scripts.dev?.includes('concurrently'))) {
return {
type: 'nodejs-vite-react-ssr',
dockerizable: true,
template: 'nodejs/vite-react-ssr',
port: detectPort(pkg) || 3000,
entryPoint: detectEntryPoint(pkg, projectPath, 'ssr'),
buildCommand: 'npm run build',
description: 'Vite + React with Express SSR'
};
}
// Vite + React SPA (no backend)
if (hasVite && hasReact && !hasExpress) {
return {
type: 'nodejs-vite-react',
dockerizable: true,
template: 'nodejs/vite-react',
port: 80, // Nginx serves static files on port 80
entryPoint: null,
buildCommand: 'npm run build',
description: 'Vite + React SPA (served by Nginx)'
};
}
// Express server (with or without Socket.io)
if (hasExpress) {
return {
type: 'nodejs-express',
dockerizable: true,
template: 'nodejs/express',
port: detectPort(pkg) || 3000,
entryPoint: detectEntryPoint(pkg, projectPath, 'express'),
buildCommand: detectBuildCommand(pkg),
description: hasSocketIO ? 'Express + Socket.io server' : 'Express server'
};
}
// Generic Node.js project (has package.json but no clear type)
if (pkg.main || scripts.start) {
return {
type: 'nodejs-generic',
dockerizable: true,
template: 'nodejs/express',
port: detectPort(pkg) || 3000,
entryPoint: detectEntryPoint(pkg, projectPath, 'generic'),
buildCommand: detectBuildCommand(pkg),
description: 'Generic Node.js application'
};
}
// Has package.json but nothing to run
return {
type: 'nodejs-unknown',
dockerizable: false,
reason: 'No start script or main entry point found',
template: null
};
}
/**
* Detect the default port from package.json or environment
*/
function detectPort(pkg) {
const scripts = pkg.scripts || {};
// Check start script for port
const startScript = scripts.start || '';
const portMatch = startScript.match(/PORT[=\s]+(\d+)/i) ||
startScript.match(/-p\s+(\d+)/) ||
startScript.match(/--port\s+(\d+)/);
if (portMatch) {
return parseInt(portMatch[1], 10);
}
// Check for common port patterns in dev script
const devScript = scripts.dev || '';
const devPortMatch = devScript.match(/:(\d{4})/);
if (devPortMatch) {
return parseInt(devPortMatch[1], 10);
}
// Default based on common frameworks
if (pkg.dependencies?.['next']) return 3000;
if (pkg.devDependencies?.vite) return 5173;
return null;
}
/**
* Detect the entry point file
*/
function detectEntryPoint(pkg, projectPath, type) {
// Check package.json main field
if (pkg.main) {
return pkg.main;
}
// Check for common entry points
const commonEntries = [
'server.js',
'server.mjs',
'src/server.js',
'src/server.mjs',
'src/index.js',
'src/index.mjs',
'index.js',
'app.js',
'src/app.js'
];
for (const entry of commonEntries) {
if (existsSync(join(projectPath, entry))) {
return entry;
}
}
// For SSR projects, look for server files
if (type === 'ssr') {
const ssrEntries = ['server.mjs', 'server.js', 'src/server.mjs', 'src/server.js'];
for (const entry of ssrEntries) {
if (existsSync(join(projectPath, entry))) {
return entry;
}
}
}
// Default
return 'index.js';
}
/**
* Detect if project has a build command
*/
function detectBuildCommand(pkg) {
const scripts = pkg.scripts || {};
if (scripts.build) {
return 'npm run build';
}
return null;
}
/**
* Get additional info about the Node.js project
*/
export function getNodeJSInfo(projectPath) {
const packageJsonPath = join(projectPath, 'package.json');
if (!existsSync(packageJsonPath)) {
return null;
}
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return {
name: pkg.name,
version: pkg.version,
hasLockFile: existsSync(join(projectPath, 'package-lock.json')),
hasYarnLock: existsSync(join(projectPath, 'yarn.lock')),
hasPnpmLock: existsSync(join(projectPath, 'pnpm-lock.yaml')),
nodeVersion: pkg.engines?.node || null,
scripts: Object.keys(pkg.scripts || {}),
dependencies: Object.keys(pkg.dependencies || {}),
devDependencies: Object.keys(pkg.devDependencies || {})
};
}

186
cli/detectors/python.js Normal file
View File

@@ -0,0 +1,186 @@
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
/**
* Detect Python project type and configuration
*/
export function detectPython(projectPath) {
const requirementsPath = join(projectPath, 'requirements.txt');
const pyprojectPath = join(projectPath, 'pyproject.toml');
const setupPath = join(projectPath, 'setup.py');
const hasRequirements = existsSync(requirementsPath);
const hasPyproject = existsSync(pyprojectPath);
const hasSetup = existsSync(setupPath);
if (!hasRequirements && !hasPyproject && !hasSetup) {
return null;
}
// Read requirements to detect project type
let requirements = '';
if (hasRequirements) {
requirements = readFileSync(requirementsPath, 'utf-8').toLowerCase();
}
// Read pyproject.toml for additional info
let pyproject = '';
if (hasPyproject) {
pyproject = readFileSync(pyprojectPath, 'utf-8').toLowerCase();
}
// Detect ML/PyTorch project
const mlIndicators = [
'torch',
'pytorch',
'tensorflow',
'keras',
'transformers',
'opencv',
'faster-whisper',
'whisper',
'scikit-learn',
'numpy'
];
const isML = mlIndicators.some(ind => requirements.includes(ind) || pyproject.includes(ind));
if (isML) {
return {
type: 'python-ml-pytorch',
dockerizable: true,
template: 'python/ml-pytorch',
port: detectPythonPort(projectPath) || 8000,
entryPoint: detectPythonEntryPoint(projectPath),
buildCommand: null,
description: 'Python ML/AI application',
systemDeps: detectSystemDeps(requirements)
};
}
// Check for web frameworks
const hasFlask = requirements.includes('flask') || pyproject.includes('flask');
const hasFastAPI = requirements.includes('fastapi') || pyproject.includes('fastapi');
const hasDjango = requirements.includes('django') || pyproject.includes('django');
let description = 'Python application';
let port = 8000;
if (hasFlask) {
description = 'Flask web application';
port = 5000;
} else if (hasFastAPI) {
description = 'FastAPI web application';
port = 8000;
} else if (hasDjango) {
description = 'Django web application';
port = 8000;
}
return {
type: 'python-standard',
dockerizable: true,
template: 'python/standard',
port: detectPythonPort(projectPath) || port,
entryPoint: detectPythonEntryPoint(projectPath),
buildCommand: null,
description,
framework: hasFlask ? 'flask' : hasFastAPI ? 'fastapi' : hasDjango ? 'django' : null
};
}
/**
* Detect Python entry point
*/
function detectPythonEntryPoint(projectPath) {
const commonEntries = [
'main.py',
'app.py',
'server.py',
'run.py',
'src/main.py',
'src/app.py'
];
for (const entry of commonEntries) {
if (existsSync(join(projectPath, entry))) {
return entry;
}
}
return 'main.py';
}
/**
* Detect port from Python files
*/
function detectPythonPort(projectPath) {
const mainFiles = ['main.py', 'app.py', 'server.py', 'run.py'];
for (const file of mainFiles) {
const filePath = join(projectPath, file);
if (existsSync(filePath)) {
const content = readFileSync(filePath, 'utf-8');
// Look for port definitions
const portMatch = content.match(/port[=\s:]+(\d{4})/i) ||
content.match(/PORT[=\s:]+(\d{4})/i);
if (portMatch) {
return parseInt(portMatch[1], 10);
}
}
}
return null;
}
/**
* Detect system dependencies needed for ML projects
*/
function detectSystemDeps(requirements) {
const deps = [];
if (requirements.includes('opencv') || requirements.includes('cv2')) {
deps.push('libgl1-mesa-glx', 'libglib2.0-0');
}
if (requirements.includes('whisper') || requirements.includes('faster-whisper')) {
deps.push('ffmpeg');
}
if (requirements.includes('soundfile') || requirements.includes('librosa')) {
deps.push('libsndfile1');
}
if (requirements.includes('pillow') || requirements.includes('pil')) {
deps.push('libjpeg-dev', 'zlib1g-dev');
}
return deps;
}
/**
* Get additional info about Python project
*/
export function getPythonInfo(projectPath) {
const requirementsPath = join(projectPath, 'requirements.txt');
const pyprojectPath = join(projectPath, 'pyproject.toml');
const info = {
hasRequirements: existsSync(requirementsPath),
hasPyproject: existsSync(pyprojectPath),
hasSetupPy: existsSync(join(projectPath, 'setup.py')),
hasVenv: existsSync(join(projectPath, 'venv')) || existsSync(join(projectPath, '.venv')),
dependencies: []
};
if (info.hasRequirements) {
const content = readFileSync(requirementsPath, 'utf-8');
info.dependencies = content
.split('\n')
.filter(line => line.trim() && !line.startsWith('#'))
.map(line => line.split('==')[0].split('>=')[0].split('<=')[0].trim());
}
return info;
}

98
cli/detectors/static.js Normal file
View File

@@ -0,0 +1,98 @@
import { existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { glob } from 'glob';
/**
* Detect static site or Flutter web project
*/
export async function detectStatic(projectPath) {
// Check for Flutter project first
const pubspecPath = join(projectPath, 'pubspec.yaml');
if (existsSync(pubspecPath)) {
return {
type: 'flutter-web',
dockerizable: true,
template: 'static/nginx',
port: 80,
entryPoint: null,
buildCommand: 'flutter build web',
description: 'Flutter web application (builds to static files)',
buildDir: 'build/web',
note: 'Run "flutter build web" before Docker build'
};
}
// Check for index.html (static site)
const indexPath = join(projectPath, 'index.html');
const hasIndexHtml = existsSync(indexPath);
// Check for common static site directories
const hasPublicDir = existsSync(join(projectPath, 'public', 'index.html'));
const hasDistDir = existsSync(join(projectPath, 'dist', 'index.html'));
const hasBuildDir = existsSync(join(projectPath, 'build', 'index.html'));
if (!hasIndexHtml && !hasPublicDir && !hasDistDir && !hasBuildDir) {
return null;
}
// Determine the source directory
let sourceDir = '.';
if (hasPublicDir) sourceDir = 'public';
else if (hasDistDir) sourceDir = 'dist';
else if (hasBuildDir) sourceDir = 'build';
// Check for PHP files (simple PHP site)
const phpFiles = await glob('*.php', { cwd: projectPath });
if (phpFiles.length > 0) {
return {
type: 'static-php',
dockerizable: true,
template: 'static/php',
port: 80,
entryPoint: null,
buildCommand: null,
description: 'PHP static site',
sourceDir,
note: 'Uses PHP-FPM with Nginx'
};
}
// Pure static site (HTML/CSS/JS)
return {
type: 'static-nginx',
dockerizable: true,
template: 'static/nginx',
port: 80,
entryPoint: null,
buildCommand: null,
description: 'Static website (served by Nginx)',
sourceDir
};
}
/**
* Get additional info about static site
*/
export async function getStaticInfo(projectPath) {
const info = {
hasIndexHtml: existsSync(join(projectPath, 'index.html')),
hasPackageJson: existsSync(join(projectPath, 'package.json')),
files: {
html: (await glob('**/*.html', { cwd: projectPath, ignore: 'node_modules/**' })).length,
css: (await glob('**/*.css', { cwd: projectPath, ignore: 'node_modules/**' })).length,
js: (await glob('**/*.js', { cwd: projectPath, ignore: 'node_modules/**' })).length,
php: (await glob('**/*.php', { cwd: projectPath, ignore: 'node_modules/**' })).length
},
directories: []
};
// Check for common directories
const dirs = ['public', 'dist', 'build', 'assets', 'css', 'js', 'images'];
for (const dir of dirs) {
if (existsSync(join(projectPath, dir))) {
info.directories.push(dir);
}
}
return info;
}