UNPKG

sourcewizard

Version:

SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration

1,363 lines (1,362 loc) 74 kB
import { promises as fs } from "fs"; import * as path from "path"; import { spawn } from "child_process"; // Common directories to ignore even if not in .gitignore const DEFAULT_IGNORE_PATTERNS = [ "node_modules", ".git", ".svn", ".hg", "target", // Rust/Java "build", "dist", "out", ".next", ".nuxt", "coverage", ".cache", "tmp", "temp", ".tmp", ".temp", "__pycache__", "*.pyc", ".pytest_cache", ".venv", "venv", "env", "Pods", // iOS "DerivedData", // iOS "vendor", // PHP/Go ]; export async function detectRepo(repoPath) { const defaultContext = { name: "unknown", }; try { const repoInfo = await analyzeRepositoryRecursive(repoPath); return { ...defaultContext, ...repoInfo, }; } catch (error) { console.error("Error detecting repository:", error); return defaultContext; } } async function analyzeRepositoryRecursive(repoPath) { const context = {}; // Root name always comes from directory name context.name = path.basename(repoPath); // Parse gitignore patterns const gitignorePatterns = await parseGitignorePatterns(repoPath); // Recursively find all packages in the repository const packages = await findAllPackages(repoPath, gitignorePatterns); // Convert ALL packages to TargetInfo (including root packages) const targets = await convertPackagesToTargets(packages, repoPath); if (Object.keys(targets).length > 0) { context.targets = targets; } return context; } async function parseGitignorePatterns(repoPath) { const patterns = []; // Add default ignore patterns for (const pattern of DEFAULT_IGNORE_PATTERNS) { patterns.push({ pattern, isNegated: false, isDirectory: pattern.endsWith("/") || !pattern.includes("."), }); } // Parse .gitignore files const gitignoreFiles = [ path.join(repoPath, ".gitignore"), path.join(repoPath, ".git", "info", "exclude"), ]; for (const gitignoreFile of gitignoreFiles) { try { const content = await fs.readFile(gitignoreFile, "utf-8"); const lines = content.split("\n"); for (let line of lines) { line = line.trim(); // Skip empty lines and comments if (!line || line.startsWith("#")) continue; const isNegated = line.startsWith("!"); if (isNegated) { line = line.substring(1); } const isDirectory = line.endsWith("/"); if (isDirectory) { line = line.substring(0, line.length - 1); } patterns.push({ pattern: line, isNegated, isDirectory, }); } } catch { // .gitignore file doesn't exist or can't be read } } return patterns; } function shouldIgnorePath(filePath, repoPath, patterns) { const relativePath = path.relative(repoPath, filePath); const pathParts = relativePath.split(path.sep); let ignored = false; for (const pattern of patterns) { const matches = matchesGitignorePattern(relativePath, pathParts, pattern); if (matches) { ignored = !pattern.isNegated; } } return ignored; } function matchesGitignorePattern(relativePath, pathParts, pattern) { const { pattern: pat, isDirectory } = pattern; // Simple glob matching if (pat === "*") return true; // Exact match if (relativePath === pat) return true; // Check if any path component matches for (const part of pathParts) { if (part === pat) return true; // Simple wildcard matching if (pat.includes("*")) { const regex = new RegExp(pat.replace(/\*/g, ".*")); if (regex.test(part)) return true; } } // Directory-specific matching if (isDirectory) { return pathParts.includes(pat); } // Pattern with path separators if (pat.includes("/")) { return relativePath.includes(pat); } return false; } async function findAllPackages(repoPath, gitignorePatterns) { const packages = []; await scanDirectory(repoPath, repoPath, packages, gitignorePatterns); return packages; } async function scanDirectory(currentPath, repoPath, packages, patterns) { try { // Check if this directory should be ignored if (shouldIgnorePath(currentPath, repoPath, patterns)) { return; } const entries = await fs.readdir(currentPath, { withFileTypes: true }); // Check for package files in current directory const packageFiles = [ { file: "package.json", type: "node", language: "javascript" }, { file: "Cargo.toml", type: "rust", language: "rust" }, { file: "go.mod", type: "go", language: "go" }, { file: "pom.xml", type: "java-maven", language: "java" }, { file: "build.gradle", type: "java-gradle", language: "java" }, { file: "build.gradle.kts", type: "java-gradle", language: "kotlin", }, { file: "composer.json", type: "php", language: "php" }, { file: "Gemfile", type: "ruby", language: "ruby" }, { file: "setup.py", type: "python", language: "python" }, { file: "pyproject.toml", type: "python", language: "python" }, { file: "requirements.txt", type: "python", language: "python" }, { file: "Pipfile", type: "python", language: "python" }, ]; for (const { file, type, language } of packageFiles) { const filePath = path.join(currentPath, file); try { await fs.access(filePath); // Found a package file, analyze it const packageInfo = await analyzePackageFile(filePath, currentPath, repoPath, type, language); if (packageInfo) { packages.push(packageInfo); } // For Python, we might have multiple package files, so don't break early if (type !== "python") { break; // Only one package type per directory (except Python) } } catch { // File doesn't exist } } // Recursively scan subdirectories for (const entry of entries) { if (entry.isDirectory()) { const subPath = path.join(currentPath, entry.name); await scanDirectory(subPath, repoPath, packages, patterns); } } } catch (error) { // Directory access error, skip console.debug(`Skipping directory ${currentPath}: ${error}`); } } async function analyzePackageFile(filePath, packagePath, repoPath, type, language) { try { const relativePath = path.relative(repoPath, packagePath); switch (type) { case "node": { const content = await fs.readFile(filePath, "utf-8"); let packageJson; try { packageJson = JSON.parse(content); } catch (parseError) { throw new Error(`JSON syntax error in ${filePath}: ${parseError instanceof Error ? parseError.message : parseError}`); } return { path: packagePath, relativePath: relativePath || "//", type, configFile: "package.json", name: packageJson.name, language: await detectJavaScriptLanguage(packageJson, packagePath), framework: detectFramework(packageJson), packageManager: await detectPackageManager(packagePath, repoPath), }; } case "rust": { const content = await fs.readFile(filePath, "utf-8"); const nameMatch = content.match(/name\s*=\s*"([^"]+)"/); return { path: packagePath, relativePath: relativePath || "//", type, configFile: "Cargo.toml", name: nameMatch?.[1], language: "rust", packageManager: "cargo", }; } case "go": { const content = await fs.readFile(filePath, "utf-8"); const moduleMatch = content.match(/module\s+(.+)/); const moduleName = moduleMatch?.[1] || "unknown"; return { path: packagePath, relativePath: relativePath || "//", type, configFile: "go.mod", name: moduleName.split("/").pop(), language: "go", packageManager: "go", }; } case "java-maven": { const content = await fs.readFile(filePath, "utf-8"); const nameMatch = content.match(/<artifactId>([^<]+)<\/artifactId>/); return { path: packagePath, relativePath: relativePath || "//", type, configFile: "pom.xml", name: nameMatch?.[1], language: "java", framework: "maven", packageManager: "maven", }; } case "java-gradle": { const content = await fs.readFile(filePath, "utf-8"); const nameMatch = content.match(/rootProject\.name\s*=\s*['"]([^'"]+)['"]/); return { path: packagePath, relativePath: relativePath || "//", type, configFile: path.basename(filePath), name: nameMatch?.[1] || path.basename(packagePath), language: filePath.endsWith(".kts") ? "kotlin" : "java", framework: "gradle", packageManager: "gradle", }; } case "php": { const content = await fs.readFile(filePath, "utf-8"); const composer = JSON.parse(content); return { path: packagePath, relativePath: relativePath || "//", type, configFile: "composer.json", name: composer.name, language: "php", framework: detectPHPFramework(composer), packageManager: "composer", }; } case "ruby": { return { path: packagePath, relativePath: relativePath || "//", type, configFile: "Gemfile", name: path.basename(packagePath), language: "ruby", framework: await detectRubyFramework(packagePath), packageManager: "bundle", }; } case "python": { let name = path.basename(packagePath); if (filePath.endsWith("setup.py")) { const content = await fs.readFile(filePath, "utf-8"); const nameMatch = content.match(/name\s*=\s*['""]([^'"]*)['"]/); if (nameMatch) name = nameMatch[1]; } else if (filePath.endsWith("pyproject.toml")) { const content = await fs.readFile(filePath, "utf-8"); const nameMatch = content.match(/name\s*=\s*"([^"]+)"/); if (nameMatch) name = nameMatch[1]; } return { path: packagePath, relativePath: relativePath || "//", type, configFile: path.basename(filePath), name, language: "python", framework: await detectPythonFramework(packagePath), packageManager: "pip", }; } default: return null; } } catch (error) { console.error(`Error analyzing package file ${filePath}: ${error}`); return null; } } async function determinePrimaryPackage(repoPath, packages) { // First, look for a package in the root directory const rootPackage = packages.find((pkg) => pkg.relativePath === "//" || pkg.relativePath === ""); if (rootPackage) { return rootPackage; } // If no root package, find the most common language/type const typeCounts = packages.reduce((acc, pkg) => { acc[pkg.type] = (acc[pkg.type] || 0) + 1; return acc; }, {}); const mostCommonType = Object.entries(typeCounts).sort(([, a], [, b]) => b - a)[0]?.[0]; if (mostCommonType) { return packages.find((pkg) => pkg.type === mostCommonType) || packages[0]; } return packages[0] || null; } function determinePrimaryLanguage(packages) { if (packages.length === 0) return undefined; // Count languages by frequency const languageCounts = packages.reduce((acc, pkg) => { acc[pkg.language] = (acc[pkg.language] || 0) + 1; return acc; }, {}); // Return most common language const mostCommonLanguage = Object.entries(languageCounts).sort(([, a], [, b]) => b - a)[0]?.[0]; return mostCommonLanguage || packages[0].language; } // This function is no longer needed since we don't populate root context from packages async function collectAllEnvFiles(repoPath, packages) { const envFileMap = new Map(); // Only check each package's own directory for env files for (const pkg of packages) { const envFiles = await detectEnvFiles(pkg.path); if (envFiles.length > 0) { envFileMap.set(pkg.relativePath, envFiles); } } return envFileMap; } async function detectDependencyFilesWithPaths(pkg, repoPath) { const files = []; const dependencyFileMap = { node: ["package.json"], python: ["requirements.txt", "setup.py", "pyproject.toml", "Pipfile"], go: ["go.mod"], rust: ["Cargo.toml"], "java-maven": ["pom.xml"], "java-gradle": ["build.gradle", "build.gradle.kts"], php: ["composer.json"], ruby: ["Gemfile"], }; const possibleFiles = dependencyFileMap[pkg.type] || []; for (const file of possibleFiles) { try { const filePath = path.join(pkg.path, file); await fs.access(filePath); // Create path relative to repo root const relativePath = path.relative(repoPath, filePath); files.push(relativePath.startsWith(".") ? relativePath : `./${relativePath}`); } catch { // File doesn't exist } } return files; } function getPackageEnvFiles(pkg, allEnvFiles, repoPath) { const envFiles = []; const packageEnvFiles = allEnvFiles.get(pkg.relativePath); if (packageEnvFiles) { for (const envFile of packageEnvFiles) { // Construct proper relative path from repo root const fullPath = pkg.relativePath === "//" ? `./${envFile}` : `./${path.join(pkg.relativePath, envFile)}`; envFiles.push(fullPath); } } return envFiles; } async function convertPackagesToTargets(packages, repoPath) { const targets = {}; // First, collect all env files in the repository for inheritance const allEnvFiles = await collectAllEnvFiles(repoPath, packages); for (const pkg of packages) { const isRoot = pkg.relativePath === "//" || pkg.relativePath === ""; const targetPath = isRoot ? "//" : pkg.relativePath; const targetName = pkg.name || path.basename(pkg.path); // Detect dependency files with full paths const dependencyFiles = await detectDependencyFilesWithPaths(pkg, repoPath); // Get env files for this package only const envFiles = getPackageEnvFiles(pkg, allEnvFiles, repoPath); // Get version from package const version = await getPackageVersion(pkg); // Create the main package target const targetKey = isRoot ? `:${targetName}` : `${targetPath}:${targetName}`; const packageActions = await generatePackageActions(pkg, targetPath); targets[targetKey] = { name: targetName, path: targetPath, language: pkg.language, version, framework: pkg.framework, package_manager: pkg.packageManager, dependency_files: dependencyFiles, env_files: envFiles, target_type: "package", actions: packageActions, }; // Detect entrypoint scripts for this package const entrypoints = await detectEntrypointScripts(pkg, repoPath); // Create additional targets for each entrypoint script for (const entrypoint of entrypoints) { // Calculate the script's directory relative to repo root const scriptDir = path.dirname(entrypoint.relativePath); const scriptPath = scriptDir === "." ? "//" : scriptDir.replace(/^\.\//, ""); // Create target key based on script location, not package location const scriptTargetKey = scriptPath === "//" ? `:${entrypoint.name}` : `${scriptPath}:${entrypoint.name}`; // Detect internal dependencies for the script const internalDeps = await detectInternalDependencies(entrypoint, pkg, repoPath); // Generate script-specific actions const scriptActions = await generateScriptActions(entrypoint, pkg, scriptPath); targets[scriptTargetKey] = { name: entrypoint.name, path: scriptPath, language: pkg.language, version, framework: pkg.framework, package_manager: pkg.packageManager, dependency_files: dependencyFiles, env_files: envFiles, entrypoint: entrypoint.relativePath, target_type: "script", internal_dependencies: internalDeps.length > 0 ? internalDeps : undefined, actions: scriptActions, }; } // Create additional targets for each package.json script (Node.js packages only) if (pkg.type === "node") { await addPackageScriptTargets(pkg, targetName, targetPath, isRoot, targets, packageActions); } } return targets; } async function addPackageScriptTargets(pkg, targetName, targetPath, isRoot, targets, packageActions) { try { // Read package.json to get scripts const packageJsonPath = path.join(pkg.path, "package.json"); const packageContent = await fs.readFile(packageJsonPath, "utf-8"); let packageJson; try { packageJson = JSON.parse(packageContent); } catch (parseError) { throw new Error(`JSON syntax error in ${packageJsonPath}: ${parseError instanceof Error ? parseError.message : parseError}`); } if (!packageJson.scripts) { return; // No scripts to create targets for } // Create a target for each script for (const [scriptName, scriptCode] of Object.entries(packageJson.scripts)) { if (typeof scriptCode !== 'string') continue; // Create script target key in format: <pkg-path>:<pkg-name>-<script-name> const scriptTargetKey = isRoot ? `:${targetName}-${scriptName}` : `${targetPath}:${targetName}-${scriptName}`; // Create script-specific actions that just run this one script const packageManager = pkg.packageManager || "npm"; const runCommand = getRunCommand(packageManager); const scriptCommand = `${runCommand} ${scriptName}`; // Copy actions from the main package target const scriptActions = { build: [...packageActions.build], test: [...packageActions.test], deploy: [...packageActions.deploy], dev: [{ command: scriptCommand }], lint: [...packageActions.lint], format: [...packageActions.format], install: [...packageActions.install], clean: [...packageActions.clean], check: [...packageActions.check], add: [...packageActions.add], }; targets[scriptTargetKey] = { name: `${targetName}-${scriptName}`, path: targetPath, language: pkg.language, framework: pkg.framework, package_manager: pkg.packageManager, dependency_files: [], // Script targets don't need dependency files env_files: [], // Script targets don't need env files target_type: "script", actions: scriptActions, }; } } catch (error) { // Log errors reading package.json for script targets console.error(`Could not read package.json for script targets at ${pkg.path}: ${error}`); } } async function detectEntrypointScripts(pkg, repoPath) { switch (pkg.type) { case "python": return await detectPythonEntrypoints(pkg, repoPath); case "node": return await detectNodeEntrypoints(pkg, repoPath); case "go": return await detectGoEntrypoints(pkg, repoPath); case "rust": return await detectRustEntrypoints(pkg, repoPath); case "java-maven": case "java-gradle": return await detectJavaEntrypoints(pkg, repoPath); default: return []; } } async function detectPythonEntrypoints(pkg, repoPath) { const entrypoints = []; try { const files = await findPythonFiles(pkg.path); for (const file of files) { try { const content = await fs.readFile(file, "utf-8"); // Check if the file contains if __name__ == "__main__": pattern const hasMainBlock = /if\s+__name__\s*==\s*["']__main__["']\s*:/.test(content); if (hasMainBlock) { const fileName = path.basename(file, ".py"); const relativePath = path.relative(repoPath, file); entrypoints.push({ name: fileName, path: file, relativePath: relativePath.startsWith(".") ? relativePath : `./${relativePath}`, }); } } catch (error) { // Skip files that can't be read console.debug(`Skipping file ${file}: ${error}`); } } } catch (error) { console.debug(`Error detecting Python entrypoints in ${pkg.path}: ${error}`); } return entrypoints; } async function findPythonFiles(dirPath) { const pythonFiles = []; // Common directories to ignore when scanning for Python entrypoints const ignoreDirectories = [ "__pycache__", ".pytest_cache", "tests", "test", ".git", // Virtual environment directories "venv", "env", ".venv", ".env", "virtualenv", "ENV", // Other common directories to skip "node_modules", ".tox", "dist", "build", "*.egg-info", ".coverage", "htmlcov", ]; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isFile() && entry.name.endsWith(".py")) { // Skip common non-entrypoint files if (!["__init__.py", "setup.py", "conftest.py"].includes(entry.name)) { pythonFiles.push(fullPath); } } else if (entry.isDirectory()) { // Check if this directory should be ignored const shouldIgnore = ignoreDirectories.some((pattern) => { if (pattern.includes("*")) { // Simple glob matching for patterns like "*.egg-info" const regex = new RegExp(pattern.replace(/\*/g, ".*")); return regex.test(entry.name); } return entry.name === pattern; }); if (!shouldIgnore) { const subFiles = await findPythonFiles(fullPath); pythonFiles.push(...subFiles); } } } } catch (error) { console.debug(`Error reading directory ${dirPath}: ${error}`); } return pythonFiles; } // Stub functions for other languages - to be implemented later async function detectNodeEntrypoints(pkg, repoPath) { // TODO: Implement Node.js entrypoint detection // Could look for files with require.main === module or specific patterns return []; } async function detectGoEntrypoints(pkg, repoPath) { // TODO: Implement Go entrypoint detection // Could look for main() functions in main packages return []; } async function detectRustEntrypoints(pkg, repoPath) { // TODO: Implement Rust entrypoint detection // Could look for main.rs files or bin entries in Cargo.toml return []; } async function detectJavaEntrypoints(pkg, repoPath) { // TODO: Implement Java entrypoint detection // Could look for public static void main methods return []; } async function detectInternalDependencies(entrypoint, pkg, repoPath) { switch (pkg.type) { case "python": return await detectPythonInternalDependencies(entrypoint, pkg, repoPath); case "node": return await detectNodeInternalDependencies(entrypoint, pkg, repoPath); case "go": return await detectGoInternalDependencies(entrypoint, pkg, repoPath); case "rust": return await detectRustInternalDependencies(entrypoint, pkg, repoPath); case "java-maven": case "java-gradle": return await detectJavaInternalDependencies(entrypoint, pkg, repoPath); default: return []; } } async function generatePackageActions(pkg, targetPath) { const actions = { build: [], test: [], deploy: [], dev: [], lint: [], format: [], install: [], clean: [], check: [], add: [], }; await addPackageActions(pkg, actions); return actions; } async function generateScriptActions(entrypoint, pkg, targetPath) { const actions = { build: [], test: [], deploy: [], dev: [], lint: [], format: [], install: [], clean: [], check: [], add: [], }; // Generate language-specific script actions switch (pkg.type) { case "python": await addPythonScriptActions(entrypoint, actions, targetPath); break; case "node": await addNodeScriptActions(entrypoint, actions, targetPath); break; case "go": await addGoScriptActions(entrypoint, actions, targetPath); break; case "rust": await addRustScriptActions(entrypoint, actions, targetPath); break; case "java-maven": case "java-gradle": await addJavaScriptActions(entrypoint, actions, targetPath); break; default: break; } return actions; } async function addPythonScriptActions(entrypoint, actions, targetPath) { const scriptPath = entrypoint.relativePath; // Run action - execute the Python script actions.dev.push({ command: `python ${scriptPath}`, }); // Test action - try to run pytest on the script if it looks like a test if (entrypoint.name.includes("test") || entrypoint.name.startsWith("test_")) { actions.test.push({ command: `python -m pytest ${scriptPath}`, }); } else { // For non-test scripts, add a basic validation run actions.test.push({ command: `python -m py_compile ${scriptPath}`, }); } // Install dependencies if requirements.txt exists in the target path actions.install.push({ command: "pip install -r requirements.txt", }); // Lint action actions.lint.push({ command: `python -m flake8 ${scriptPath}`, }); // Format action actions.format.push({ command: `python -m black ${scriptPath}`, }); // Deploy action - leave empty for now } // Stub functions for other languages - to be implemented later async function addNodeScriptActions(entrypoint, actions, targetPath) { const scriptPath = entrypoint.relativePath; // TODO: Implement Node.js script actions actions.dev.push({ command: `node ${scriptPath}`, }); // Deploy action - leave empty for now } async function addGoScriptActions(entrypoint, actions, targetPath) { const scriptPath = entrypoint.relativePath; // TODO: Implement Go script actions actions.dev.push({ command: `go run ${scriptPath}`, }); // Deploy action - leave empty for now } async function addRustScriptActions(entrypoint, actions, targetPath) { // TODO: Implement Rust script actions // Deploy action - leave empty for now } async function addJavaScriptActions(entrypoint, actions, targetPath) { // TODO: Implement Java script actions // Deploy action - leave empty for now } async function detectPythonInternalDependencies(entrypoint, pkg, repoPath) { const internalDeps = []; try { const content = await fs.readFile(entrypoint.path, "utf-8"); const imports = parsePythonImports(content); for (const importModule of imports) { const internalPath = await resolveInternalPythonModule(importModule, entrypoint.path, repoPath); if (internalPath) { internalDeps.push(internalPath); } } } catch (error) { console.debug(`Error detecting internal dependencies for ${entrypoint.path}: ${error}`); } return Array.from(new Set(internalDeps)); // Remove duplicates } function parsePythonImports(content) { const imports = []; const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith("#")) continue; // Parse "import module" statements const importMatch = trimmed.match(/^import\s+([a-zA-Z_][a-zA-Z0-9_.]*)/); if (importMatch) { imports.push(importMatch[1]); continue; } // Parse "from module import ..." statements const fromImportMatch = trimmed.match(/^from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import/); if (fromImportMatch) { imports.push(fromImportMatch[1]); continue; } // Handle relative imports "from .module import ..." or "from ..module import ..." const relativeImportMatch = trimmed.match(/^from\s+(\.{1,}[a-zA-Z_][a-zA-Z0-9_.]*)\s+import/); if (relativeImportMatch) { imports.push(relativeImportMatch[1]); continue; } } return imports; } async function resolveInternalPythonModule(moduleName, scriptPath, repoPath) { // Handle relative imports if (moduleName.startsWith(".")) { const scriptDir = path.dirname(scriptPath); const relativePath = resolveRelativePythonImport(moduleName, scriptDir, repoPath); return relativePath; } // Check if it's an internal module by looking for corresponding files const possiblePaths = [ // Look for module.py in current directory and parent directories path.join(path.dirname(scriptPath), `${moduleName}.py`), path.join(repoPath, `${moduleName}.py`), // Look for module/__init__.py path.join(path.dirname(scriptPath), moduleName, "__init__.py"), path.join(repoPath, moduleName, "__init__.py"), // Look for nested modules (e.g., package.module -> package/module.py) ...moduleName.split(".").reduce((paths, part, index, parts) => { const subPath = parts.slice(0, index + 1).join("/"); paths.push(path.join(path.dirname(scriptPath), `${subPath}.py`), path.join(repoPath, `${subPath}.py`), path.join(path.dirname(scriptPath), subPath, "__init__.py"), path.join(repoPath, subPath, "__init__.py")); return paths; }, []), ]; for (const possiblePath of possiblePaths) { try { await fs.access(possiblePath); const relativePath = path.relative(repoPath, possiblePath); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } catch { // File doesn't exist, continue } } return null; // Not an internal module } function resolveRelativePythonImport(relativePath, scriptDir, repoPath) { const dots = relativePath.match(/^\.+/)?.[0] || ""; const modulePart = relativePath.substring(dots.length); let targetDir = scriptDir; // Navigate up directories based on number of dots for (let i = 1; i < dots.length; i++) { targetDir = path.dirname(targetDir); if (targetDir === path.dirname(targetDir)) break; // Reached filesystem root } if (modulePart) { const possiblePaths = [ path.join(targetDir, `${modulePart}.py`), path.join(targetDir, modulePart, "__init__.py"), ]; for (const possiblePath of possiblePaths) { try { if (possiblePath.startsWith(repoPath)) { const relativePath = path.relative(repoPath, possiblePath); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } } catch { // Continue } } } return null; } // Stub functions for internal dependency detection in other languages async function detectNodeInternalDependencies(entrypoint, pkg, repoPath) { // TODO: Implement Node.js internal dependency detection // Could parse require() and import statements for relative paths return []; } async function detectGoInternalDependencies(entrypoint, pkg, repoPath) { // TODO: Implement Go internal dependency detection // Could parse import statements for internal module references return []; } async function detectRustInternalDependencies(entrypoint, pkg, repoPath) { // TODO: Implement Rust internal dependency detection // Could parse mod and use statements for internal crate references return []; } async function detectJavaInternalDependencies(entrypoint, pkg, repoPath) { // TODO: Implement Java internal dependency detection // Could parse import statements for internal package references return []; } async function detectDependencyFiles(packagePath, packageType) { const files = []; const dependencyFileMap = { node: ["package.json"], python: ["requirements.txt", "setup.py", "pyproject.toml", "Pipfile"], go: ["go.mod"], rust: ["Cargo.toml"], "java-maven": ["pom.xml"], "java-gradle": ["build.gradle", "build.gradle.kts"], php: ["composer.json"], ruby: ["Gemfile"], }; const possibleFiles = dependencyFileMap[packageType] || []; for (const file of possibleFiles) { try { const filePath = path.join(packagePath, file); await fs.access(filePath); files.push(file); } catch { // File doesn't exist } } return files; } async function detectEnvFiles(packagePath) { const files = []; const envFileNames = [".env", ".env.local"]; for (const fileName of envFileNames) { try { const filePath = path.join(packagePath, fileName); await fs.access(filePath); files.push(fileName); } catch { // File doesn't exist } } return files; } async function getPackageVersion(pkg) { try { switch (pkg.type) { case "node": { const packageJsonPath = path.join(pkg.path, "package.json"); const content = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(content); return packageJson.version; } case "rust": { const cargoTomlPath = path.join(pkg.path, "Cargo.toml"); const content = await fs.readFile(cargoTomlPath, "utf-8"); const versionMatch = content.match(/version\s*=\s*"([^"]+)"/); return versionMatch?.[1]; } default: return undefined; } } catch { return undefined; } } async function parseEnvFiles(packagePath) { const envs = {}; const envFiles = [".env", ".env.local"]; for (const envFile of envFiles) { try { const envFilePath = path.join(packagePath, envFile); const content = await fs.readFile(envFilePath, "utf-8"); const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith("#")) continue; // Parse KEY=VALUE format const equalIndex = trimmed.indexOf("="); if (equalIndex > 0) { const key = trimmed.substring(0, equalIndex).trim(); let value = trimmed.substring(equalIndex + 1).trim(); // Remove quotes if present if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } envs[key] = value; } } } catch { // File doesn't exist or can't be read, continue } } return envs; } async function parsePackageDependencies(pkg) { try { switch (pkg.type) { case "node": { const packageJsonPath = path.join(pkg.path, "package.json"); const content = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(content); return { dependencies: packageJson.dependencies || {}, devDependencies: packageJson.devDependencies || {}, }; } case "python": { const dependencies = await parsePythonDependencies(pkg.path); return { dependencies, devDependencies: {}, }; } case "go": { const goModPath = path.join(pkg.path, "go.mod"); const content = await fs.readFile(goModPath, "utf-8"); const dependencies = await parseGoDependencies(content); return { dependencies, devDependencies: {}, }; } case "rust": { const cargoTomlPath = path.join(pkg.path, "Cargo.toml"); const content = await fs.readFile(cargoTomlPath, "utf-8"); const dependencies = await parseRustDependencies(content); return { dependencies, devDependencies: {}, }; } case "php": { const composerJsonPath = path.join(pkg.path, "composer.json"); const content = await fs.readFile(composerJsonPath, "utf-8"); const composer = JSON.parse(content); return { dependencies: composer.require || {}, devDependencies: composer["require-dev"] || {}, }; } case "java-maven": { // For Maven, we'd need to parse pom.xml - simplified for now return { dependencies: {}, devDependencies: {}, }; } case "java-gradle": { // For Gradle, we'd need to parse build.gradle - simplified for now return { dependencies: {}, devDependencies: {}, }; } case "ruby": { // For Ruby, we'd need to parse Gemfile - simplified for now return { dependencies: {}, devDependencies: {}, }; } default: return { dependencies: {}, devDependencies: {}, }; } } catch (error) { console.error(`Error parsing dependencies for ${pkg.path}: ${error}`); return { dependencies: {}, devDependencies: {}, }; } } async function addPackageActions(packageInfo, actions) { switch (packageInfo.type) { case "node": await addNodePackageActions(packageInfo, actions); break; case "python": await addPythonPackageActions(packageInfo, actions); break; case "go": await addGoPackageActions(packageInfo, actions); break; case "rust": await addRustPackageActions(packageInfo, actions); break; case "java-maven": await addMavenPackageActions(packageInfo, actions); break; case "java-gradle": await addGradlePackageActions(packageInfo, actions); break; case "php": await addPhpPackageActions(packageInfo, actions); break; case "ruby": await addRubyPackageActions(packageInfo, actions); break; } } async function addNodePackageActions(packageInfo, actions) { const packageManager = packageInfo.packageManager || "npm"; // Add install action actions.install.push({ command: getInstallCommand(packageManager), }); // Add package add action with flags support const addFlags = []; // Add dev dependency flags if (packageManager === "npm") { addFlags.push("--save-dev"); } else if (packageManager === "yarn") { addFlags.push("--dev"); } else if (packageManager === "pnpm") { addFlags.push("--save-dev", "-w"); // Include workspace flag for pnpm } else if (packageManager === "bun") { addFlags.push("--dev"); } actions.add.push({ command: getAddCommand(packageManager), flags: addFlags, }); try { const packageJsonPath = path.join(packageInfo.path, "package.json"); const content = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(content); const scripts = packageJson.scripts || {}; // Map scripts to actions const scriptMappings = { build: ["build", "compile", "dist", "bundle"], test: [ "test", "test:unit", "test:integration", "test:e2e", "spec", "jest", "mocha", ], deploy: ["deploy", "publish", "release", "ship"], dev: ["dev", "serve", "watch", "development"], lint: ["lint", "eslint", "tslint", "check"], format: ["format", "prettier", "fmt"], clean: ["clean", "clear", "reset"], }; for (const [actionType, patterns] of Object.entries(scriptMappings)) { for (const [scriptName] of Object.entries(scripts)) { if (patterns.some((pattern) => scriptName.toLowerCase().includes(pattern))) { actions[actionType].push({ command: `${getRunCommand(packageManager)} ${scriptName}`, }); } } } if (packageInfo.language === "typescript") { actions.check.push({ command: "tsc --noEmit", }); } } catch { // Error reading package.json } } async function addPythonPackageActions(packageInfo, actions) { // Check for different Python package files const hasRequirements = await fileExists(path.join(packageInfo.path, "requirements.txt")); const hasSetupPy = await fileExists(path.join(packageInfo.path, "setup.py")); const hasPyprojectToml = await fileExists(path.join(packageInfo.path, "pyproject.toml")); const hasPipfile = await fileExists(path.join(packageInfo.path, "Pipfile")); // Install actions if (hasRequirements) { actions.install.push({ command: "pip install -r requirements.txt", }); } if (hasPipfile) { actions.install.push({ command: "pipenv install", }); } // Add package actions if (hasPipfile) { actions.add.push({ command: "pipenv install", }); } else { actions.add.push({ command: "pip install", }); } // Test actions actions.test.push({ command: "python -m pytest", }); // Build actions if (hasSetupPy) { actions.build.push({ command: "python setup.py build", }); } if (hasPyprojectToml) { actions.build.push({ command: "python -m build", }); } } async function addGoPackageActions(packageInfo, actions) { actions.install.push({ command: "go mod download", }); actions.add.push({ command: "go get", }); actions.build.push({ command: "go build ./...", }); actions.test.push({ command: "go test ./...", }); } async function addRustPackageActions(packageInfo, actions) { actions.install.push({ command: "cargo fetch", }); actions.add.push({ command: "cargo add", }); actions.build.push({ command: "cargo build", }); actions.test.push({ command: "cargo test", }); } async function addMavenPackageActions(packageInfo, actions) { actions.install.push({ command: "mvn install", }); actions.add.push({ command: "mvn dependency:get -Dartifact=", }); actions.build.push({ command: "mvn compile", }); actions.test.push({ command: "mvn test", }); } async function addGradlePackageActions(packageInfo, actions) { actions.add.push({ command: "./gradlew dependencies --write-locks", }); actions.build.push({ command: "./gradlew build", }); actions.test.push({ command: "./gradlew test", }); } async function addPhpPackageActions(packageInfo, actions) { actions.install.push({ command: "composer install", }); actions.add.push({ command: "composer require", }); actions.test.push({ command: "composer test", }); } async function addRubyPackageActions(packageInfo, actions) { actions.install.push({ command: "bundle install", }); actions.add.push({ command: "bundle add", }); actions.test.push({ command: "bundle exec rspec", }); } async function fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } async function detectPackageManager(packagePath, repoPath) { const lockFiles = [ { file: "yarn.lock", manager: "yarn" }, { file: "pnpm-lock.yaml", manager: "pnpm" }, { file: "bun.lockb", manager: "bun" }, { file: "package-lock.json", manager: "npm" }, ]; const searchPaths = []; // If we have repoPath, check repo root first (for monorepos) if (repoPath && repoPath !== packagePath) { searchPaths.push(repoPath); } // Then check the package directory searchPaths.push(packagePath); // Also check parent directories up to repo root if (repoPath) { let currentPath = path.dirname(packagePath); while (currentPath !== repoPath && currentPath !== path.dirname(currentPath)) { searchPaths.push(currentPath); currentPath = path.dirname(currentPath);