sourcewizard
Version:
SourceWizard - AI-powered setup wizard for dev tools and libraries with MCP integration
1,363 lines (1,362 loc) • 74 kB
JavaScript
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);