sourcesailor
Version:
A CLI tool for analyzing and documenting codebases
292 lines (291 loc) • 15.3 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { getDirStructure } from "../directoryProcessor.mjs";
import fs from 'fs';
import { addAnalysisInGitIgnore, readConfig, writeAnalysis, writeError } from "../utils.mjs";
import ora from 'ora';
import chalk from "chalk";
import { confirm } from '@inquirer/prompts';
import { handler as setExpertiseHandler } from './setExpertise.mjs';
import { markdownToTerminal } from "../terminalRenderrer.mjs";
import ModelUtils from "../modelUtils.mjs";
// Export these functions for testing
export { analyseDirectoryStructure, inferDependenciesAndWriteAnalysis, getDirectoryWithoutLockfile, analyzeCode, analyzeAndWriteCodeInference, analyseInterestingCode, analyzeCodebase };
export const command = 'analyse <path|p> [verbose|v] [streaming|s] [ignore|i]';
export const describe = 'Analyse the given directory structure to understand the project structure and dependencies';
export function builder(yargs) {
yargs.positional('path', {
alias: 'p',
describe: 'Path to the directory to analyse',
type: 'string',
});
yargs.option('verbose', {
alias: 'v',
describe: 'Run with verbose logging',
type: 'boolean',
default: false
});
yargs.option('streaming', {
alias: 's',
describe: 'Use OpenAI streaming to infer project structure',
type: 'boolean',
default: false
});
yargs.option('ignore', {
alias: 'i',
describe: 'Specify files or directories to ignore during analysis',
type: 'array',
default: []
});
yargs.option('model', {
alias: 'm',
describe: 'Specify the AI model to use for analysis',
type: 'string'
});
return yargs;
}
export async function handler(argv) {
const isVerbose = argv.verbose || argv.v || false;
const allowStreaming = argv.streaming || argv.s || false;
const ignore = argv.ignore || argv.i || [];
const modelName = argv.model || argv.m;
const config = readConfig();
if (isVerbose) {
console.log(`Analyse the given directory structure to understand the project structure and dependencies: ${argv.path}`);
console.log(`Using model: ${modelName || config.DEFAULT_OPENAI_MODEL}`);
}
const rootDir = config.ANALYSIS_DIR;
const userExpertise = JSON.stringify(config.userExpertise);
// Handle current directory case
const inputPath = argv.path;
const projectName = inputPath === '.' ? process.cwd().split('/').pop() ?? "" : inputPath;
// Log current directory name if analyzing current directory
if (inputPath === '.') {
console.log(`Analyzing current directory: ${projectName}`);
}
const isProjectRoot = rootDir === 'p';
if (isVerbose) {
console.log({ rootDir, isProjectRoot, projectName });
}
const modelUtils = ModelUtils.getInstance();
await modelUtils.initializeModels();
const llmInterface = modelUtils.getLlmInterface(modelName || config.DEFAULT_OPENAI_MODEL);
const selectedModelName = modelName || config.DEFAULT_OPENAI_MODEL;
if (isVerbose) {
console.log(`Using model: ${llmInterface.getName()}, modelName ${selectedModelName}`);
}
if (isProjectRoot) {
addAnalysisInGitIgnore(projectName);
}
console.log(`Analysing ${chalk.redBright(projectName)}'s file structure to getting started.`);
// const defaultSpinner = ora().start()
const sourceCodePath = inputPath;
const isRoot = true;
const dirToWriteAnalysis = isProjectRoot ? `${sourceCodePath}/.SourceSailor` : `${rootDir}/.SourceSailor/${projectName}`;
if (isVerbose) {
console.log({ sourceCodePath, isProjectRoot, isRoot, dirToWriteAnalysis });
}
const { directoryInferrence, directoryStructureWithContent } = await analyseDirectoryStructure(sourceCodePath, // Use sourceCodePath instead of undefined path
isVerbose, isRoot, dirToWriteAnalysis, isProjectRoot, ignore, llmInterface, userExpertise, selectedModelName);
if (isVerbose) {
console.log({ project: argv.path, directoryInferrence });
}
// console.log(`Analysing ${chalk.redBright(projectName)}'s file structure to getting started.`)
if (!directoryInferrence.isMonorepo) {
if (!directoryInferrence.programmingLanguage) {
const errorMessage = 'Programming language not defined for non-monorepo project';
console.error(chalk.red(errorMessage));
writeError(dirToWriteAnalysis, 'Analysis', errorMessage, 'Analysis stopped due to missing programming language');
throw new Error(errorMessage);
}
await inferDependenciesAndWriteAnalysis(sourceCodePath, directoryInferrence, allowStreaming, isVerbose, dirToWriteAnalysis, isProjectRoot, llmInterface, userExpertise, selectedModelName);
const directoryStructureWithoutLockFile = await getDirectoryWithoutLockfile(directoryInferrence, directoryStructureWithContent, isVerbose);
await analyzeCode(directoryStructureWithoutLockFile, allowStreaming, isVerbose, dirToWriteAnalysis, isProjectRoot, llmInterface, userExpertise, selectedModelName);
}
else {
if (isVerbose) {
console.log({ directories: directoryInferrence.directories });
}
for await (const directory of directoryInferrence.directories) {
console.log(`Analysing ${directory}'s file structure to getting started.`);
const sourceCodePath = `${argv.path}/${directory}`;
const analysisRootDir = isProjectRoot ? `${sourceCodePath}/.SourceSailor` : `${rootDir}/.SourceSailor/${projectName}/${directory}`;
try {
const { directoryInferrence, directoryStructureWithContent } = await analyseDirectoryStructure(sourceCodePath, isVerbose, false, analysisRootDir, isProjectRoot, ignore, llmInterface, userExpertise, selectedModelName);
await inferDependenciesAndWriteAnalysis(sourceCodePath, directoryInferrence, allowStreaming, isVerbose, analysisRootDir, isProjectRoot, llmInterface, userExpertise, selectedModelName);
const directoryStructureWithoutLockFile = await getDirectoryWithoutLockfile(directory, directoryStructureWithContent, isVerbose);
await analyzeCode(directoryStructureWithoutLockFile, allowStreaming, isVerbose, analysisRootDir, isProjectRoot, llmInterface, userExpertise, selectedModelName);
}
catch (error) {
const errorAnalysisSkipped = `Error analysing ${directory}: Moving on to next directory...`;
console.error(errorAnalysisSkipped);
writeError(analysisRootDir, 'ReadingDir', error.stack, errorAnalysisSkipped);
if (isVerbose) {
console.error(error);
}
}
}
}
if (!config.userExpertise) {
console.log(chalk.yellow("User expertise is not set. Setting your expertise level will help us provide more tailored analysis."));
const setExpertise = await confirm({ message: "Would you like to set your expertise now?", default: true });
if (setExpertise) {
await setExpertiseHandler();
}
}
}
async function analyseDirectoryStructure(path, isVerbose, isRoot, projectName, isProjectRoot, ignore, llm, userExpertise, modelName) {
const spinner = ora('Analyzing the directory structure...').start();
const directoryStructureWithContent = await getDirStructure(path, ignore, isVerbose);
const directoryStructure = JSON.parse(JSON.stringify(directoryStructureWithContent));
function deleteContent(file) {
delete file.content;
if (file.children) {
for (const child of file.children) {
deleteContent(child);
}
}
}
for (const file of directoryStructure.children ?? []) {
deleteContent(file);
}
spinner.text = 'Got the directory structure';
if (isRoot) {
writeAnalysis(projectName, "directoryStructure", directoryStructure, true, isProjectRoot);
writeAnalysis(projectName, "directoryStructureWithFileContent", directoryStructureWithContent, true, isProjectRoot);
}
spinner.text = "Analyzing the project directory for codebase shape...";
const directoryInferrenceResponse = await llm.inferProjectDirectory(JSON.stringify(directoryStructure), false, isVerbose, userExpertise, modelName);
const directoryInferrence = JSON.parse(directoryInferrenceResponse ?? "");
if (isVerbose) {
console.log({ isProjectRoot, projectName, directoryInferrence });
}
writeAnalysis(projectName, "directoryInferrence", directoryInferrence, true, isProjectRoot);
// const message =
spinner.text = "Analyzed the directory structure...";
spinner.stopAndPersist({ symbol: '✔️', text: `Inferred workflow: ${directoryInferrence.workflow}` });
return { directoryInferrence, directoryStructureWithContent };
}
async function analyzeCode(directoryStructureWithoutLockFile, allowStreaming, isVerbose, projectName, isProjectRoot, llmInterface, userExpertise, modelName) {
await analyzeAndWriteCodeInference(directoryStructureWithoutLockFile, allowStreaming, isVerbose, projectName, isProjectRoot, llmInterface, userExpertise, modelName);
}
async function analyzeAndWriteCodeInference(directoryStructureWithoutLockFile, allowStreaming, isVerbose, projectName, isProjectRoot, llmInterface, userExpertise, modelName) {
let codeInferrenceResponse = await analyzeCodebase(directoryStructureWithoutLockFile, allowStreaming, isVerbose, llmInterface, userExpertise, modelName);
const interestingCodeResponse = await analyseInterestingCode(directoryStructureWithoutLockFile, allowStreaming, isVerbose, llmInterface, userExpertise, modelName);
// Concatenate the code inferrence and interesting code
codeInferrenceResponse += interestingCodeResponse;
writeAnalysis(projectName, "codeInferrence", codeInferrenceResponse, false, isProjectRoot);
}
async function analyseInterestingCode(directoryStructureWithoutLockFile, allowStreaming, isVerbose, llmInterface, userExpertise, modelName) {
const spinner = ora('Analysing interesting code').start();
try {
const interestingCode = await llmInterface.inferInterestingCode(JSON.stringify(directoryStructureWithoutLockFile), allowStreaming, isVerbose, userExpertise, modelName);
let interestingCodeResponse = "";
if (allowStreaming) {
spinner.stop().clear();
for await (const chunk of interestingCode) {
process.stdout.write(chunk);
interestingCodeResponse += chunk;
}
process.stdout.write("\n");
}
else {
const interestingCodeAsString = interestingCode;
const markDownMessage = markdownToTerminal(interestingCodeAsString);
spinner.stopAndPersist({ symbol: '✔️', text: markDownMessage });
interestingCodeResponse = interestingCodeAsString;
}
return interestingCodeResponse;
}
catch (error) {
spinner.stopAndPersist({ symbol: '❌', text: 'Error inferring code' });
console.error(error);
return `Error inferring interesting code: ${error.message}`;
}
}
async function analyzeCodebase(directoryStructureWithoutLockFile, allowStreaming, isVerbose, llmInterface, userExpertise, modelName) {
const spinner = ora('Reading Codebase and inferring code...').start();
try {
const codeInferrence = await llmInterface.inferCode(JSON.stringify(directoryStructureWithoutLockFile), allowStreaming, isVerbose, userExpertise, modelName);
let codeInferrenceResponse = "";
if (allowStreaming) {
spinner.stop().clear();
for await (const chunk of codeInferrence) {
process.stdout.write(chunk);
codeInferrenceResponse += chunk;
}
process.stdout.write("\n");
}
else {
const codeInferrenceAsString = codeInferrence;
const markDownMessage = markdownToTerminal(codeInferrenceAsString);
spinner.stopAndPersist({ symbol: '✔️', text: markDownMessage });
codeInferrenceResponse = codeInferrenceAsString;
}
return codeInferrenceResponse;
}
catch (error) {
spinner.stopAndPersist({ symbol: '❌', text: 'Error inferring code' });
if (isVerbose) {
console.error(error);
}
return `Error inferring code: ${error.message}`;
}
}
async function inferDependenciesAndWriteAnalysis(sourceCodePath, directoryInferrence, allowStreaming, isVerbose, projectName, isProjectRoot, llmInterface, userExpertise, modelName) {
const spinner = ora('Inferring dependencies...').start();
if (isVerbose) {
console.log({ sourceCodePath, projectName, directoryInferrence });
}
if (!directoryInferrence.dependenciesFile || directoryInferrence.dependenciesFile.trim() === '') {
spinner.stopAndPersist({ symbol: '❌', text: 'No dependencies file' });
return;
}
const depenencyFile = fs.readFileSync(`${sourceCodePath}/${directoryInferrence.dependenciesFile}`, 'utf-8');
try {
const dependencyInferrence = await llmInterface.inferDependency(depenencyFile, directoryInferrence.workflow, allowStreaming, isVerbose, userExpertise, modelName);
let dependencyInferrenceResponse = "";
if (allowStreaming) {
spinner.stop().clear();
for await (const chunk of dependencyInferrence) {
process.stdout.write(chunk);
dependencyInferrenceResponse += chunk;
}
process.stdout.write("\n");
}
else {
const dependencyInferrenceAsString = dependencyInferrence;
const markDownMessage = markdownToTerminal(dependencyInferrenceAsString);
spinner.stopAndPersist({ symbol: '✔️', text: markDownMessage });
dependencyInferrenceResponse = dependencyInferrenceAsString;
}
writeAnalysis(projectName, "dependencyInferrence", dependencyInferrenceResponse, isProjectRoot);
}
catch (error) {
spinner.stopAndPersist({ symbol: '❌', text: 'Error inferring dependencies' });
console.error(error);
}
}
async function getDirectoryWithoutLockfile(directoryInferrence, directoryStructureWithContent, isVerbose) {
const lockfile = directoryInferrence.lockFile;
const directoryStructureWithoutLockFile = JSON.parse(JSON.stringify(directoryStructureWithContent));
removeLockFile(directoryStructureWithoutLockFile, lockfile);
if (isVerbose) {
console.log({ directoryStructureWithoutLockFile });
}
return directoryStructureWithoutLockFile;
}
// Find the lockfile recursively and remove it from entry
function removeLockFile(file, lockfile) {
if (file?.children) {
for (const child of file.children) {
if (child.name === lockfile) {
file.children = file.children.filter(child => child.name !== lockfile);
}
else {
removeLockFile(child, lockfile);
}
}
}
}
export const usage = '$0 <cmd> [args]';
export const aliases = ['h', 'help'];