mirror of
https://github.com/yamadashy/repomix.git
synced 2026-06-11 15:37:16 +02:00
feat(skill): Add tech-stack detection and file statistics to skill output
Add new features to improve skill output for understanding codebases: - Add tech-stack.md: Auto-detect languages, frameworks, and dependencies from package.json, requirements.txt, go.mod, Cargo.toml, etc. - Add file statistics section to SKILL.md with language breakdown and largest files list - Rename structure.md to project-structure.md for clarity - Add total lines count to SKILL.md header
This commit is contained in:
@@ -27,7 +27,9 @@ import {
|
||||
generateStructureSection,
|
||||
generateSummarySection,
|
||||
} from './skill/skillSectionGenerators.js';
|
||||
import { calculateStatistics, generateStatisticsSection } from './skill/skillStatistics.js';
|
||||
import { generateSkillMd } from './skill/skillStyle.js';
|
||||
import { detectTechStack, generateTechStackMd } from './skill/skillTechStack.js';
|
||||
import { generateProjectName, generateSkillDescription, validateSkillName } from './skill/skillUtils.js';
|
||||
|
||||
const calculateMarkdownDelimiter = (files: ReadonlyArray<ProcessedFile>): string => {
|
||||
@@ -374,6 +376,7 @@ export interface SkillReferences {
|
||||
summary: string;
|
||||
structure: string;
|
||||
files: string;
|
||||
techStack?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,6 +388,9 @@ export interface SkillReferencesResult {
|
||||
projectName: string;
|
||||
skillDescription: string;
|
||||
totalFiles: number;
|
||||
totalLines: number;
|
||||
statisticsSection: string;
|
||||
hasTechStack: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,7 +402,7 @@ export interface SkillOutputResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates skill reference files (summary, structure, files, git-diffs, git-logs).
|
||||
* Generates skill reference files (summary, structure, files, tech-stack).
|
||||
* This is the first step - call this, calculate metrics, then call generateSkillMdFromReferences.
|
||||
*/
|
||||
export const generateSkillReferences = async (
|
||||
@@ -443,11 +449,20 @@ export const generateSkillReferences = async (
|
||||
);
|
||||
const renderContext = createRenderContext(outputGeneratorContext);
|
||||
|
||||
// Calculate statistics
|
||||
const statistics = calculateStatistics(sortedProcessedFiles, renderContext.fileLineCounts);
|
||||
const statisticsSection = generateStatisticsSection(statistics);
|
||||
|
||||
// Detect tech stack
|
||||
const techStack = detectTechStack(sortedProcessedFiles);
|
||||
const techStackMd = techStack ? generateTechStackMd(techStack) : undefined;
|
||||
|
||||
// Generate each section separately
|
||||
const references: SkillReferences = {
|
||||
summary: generateSummarySection(renderContext),
|
||||
structure: generateStructureSection(renderContext),
|
||||
files: generateFilesSection(renderContext),
|
||||
techStack: techStackMd,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -456,6 +471,9 @@ export const generateSkillReferences = async (
|
||||
projectName,
|
||||
skillDescription,
|
||||
totalFiles: sortedProcessedFiles.length,
|
||||
totalLines: statistics.totalLines,
|
||||
statisticsSection,
|
||||
hasTechStack: techStack !== null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -472,7 +490,10 @@ export const generateSkillMdFromReferences = (
|
||||
skillDescription: referencesResult.skillDescription,
|
||||
projectName: referencesResult.projectName,
|
||||
totalFiles: referencesResult.totalFiles,
|
||||
totalLines: referencesResult.totalLines,
|
||||
totalTokens,
|
||||
statisticsSection: referencesResult.statisticsSection,
|
||||
hasTechStack: referencesResult.hasTechStack,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import path from 'node:path';
|
||||
import type { ProcessedFile } from '../../file/fileTypes.js';
|
||||
|
||||
interface FileTypeStats {
|
||||
extension: string;
|
||||
language: string;
|
||||
fileCount: number;
|
||||
lineCount: number;
|
||||
}
|
||||
|
||||
interface StatisticsInfo {
|
||||
totalFiles: number;
|
||||
totalLines: number;
|
||||
byFileType: FileTypeStats[];
|
||||
largestFiles: Array<{ path: string; lines: number }>;
|
||||
}
|
||||
|
||||
// Map extensions to language names
|
||||
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
||||
// JavaScript/TypeScript
|
||||
'.js': 'JavaScript',
|
||||
'.jsx': 'JavaScript (JSX)',
|
||||
'.ts': 'TypeScript',
|
||||
'.tsx': 'TypeScript (TSX)',
|
||||
'.mjs': 'JavaScript (ESM)',
|
||||
'.cjs': 'JavaScript (CJS)',
|
||||
|
||||
// Web
|
||||
'.html': 'HTML',
|
||||
'.htm': 'HTML',
|
||||
'.css': 'CSS',
|
||||
'.scss': 'SCSS',
|
||||
'.sass': 'Sass',
|
||||
'.less': 'Less',
|
||||
'.vue': 'Vue',
|
||||
'.svelte': 'Svelte',
|
||||
|
||||
// Data/Config
|
||||
'.json': 'JSON',
|
||||
'.yaml': 'YAML',
|
||||
'.yml': 'YAML',
|
||||
'.toml': 'TOML',
|
||||
'.xml': 'XML',
|
||||
'.ini': 'INI',
|
||||
'.env': 'Environment',
|
||||
|
||||
// Documentation
|
||||
'.md': 'Markdown',
|
||||
'.mdx': 'MDX',
|
||||
'.rst': 'reStructuredText',
|
||||
'.txt': 'Text',
|
||||
|
||||
// Backend
|
||||
'.py': 'Python',
|
||||
'.rb': 'Ruby',
|
||||
'.php': 'PHP',
|
||||
'.java': 'Java',
|
||||
'.kt': 'Kotlin',
|
||||
'.kts': 'Kotlin Script',
|
||||
'.scala': 'Scala',
|
||||
'.go': 'Go',
|
||||
'.rs': 'Rust',
|
||||
'.c': 'C',
|
||||
'.cpp': 'C++',
|
||||
'.cc': 'C++',
|
||||
'.h': 'C/C++ Header',
|
||||
'.hpp': 'C++ Header',
|
||||
'.cs': 'C#',
|
||||
'.swift': 'Swift',
|
||||
'.m': 'Objective-C',
|
||||
'.mm': 'Objective-C++',
|
||||
|
||||
// Shell/Scripts
|
||||
'.sh': 'Shell',
|
||||
'.bash': 'Bash',
|
||||
'.zsh': 'Zsh',
|
||||
'.fish': 'Fish',
|
||||
'.ps1': 'PowerShell',
|
||||
'.bat': 'Batch',
|
||||
'.cmd': 'Batch',
|
||||
|
||||
// Other
|
||||
'.sql': 'SQL',
|
||||
'.graphql': 'GraphQL',
|
||||
'.gql': 'GraphQL',
|
||||
'.proto': 'Protocol Buffers',
|
||||
'.dockerfile': 'Dockerfile',
|
||||
'.lua': 'Lua',
|
||||
'.r': 'R',
|
||||
'.ex': 'Elixir',
|
||||
'.exs': 'Elixir Script',
|
||||
'.erl': 'Erlang',
|
||||
'.clj': 'Clojure',
|
||||
'.hs': 'Haskell',
|
||||
'.ml': 'OCaml',
|
||||
'.nim': 'Nim',
|
||||
'.zig': 'Zig',
|
||||
'.dart': 'Dart',
|
||||
'.v': 'V',
|
||||
'.sol': 'Solidity',
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets language name from file extension.
|
||||
*/
|
||||
const getLanguageFromExtension = (ext: string): string => {
|
||||
return EXTENSION_TO_LANGUAGE[ext.toLowerCase()] || ext.slice(1).toUpperCase() || 'Unknown';
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates statistics from processed files.
|
||||
*/
|
||||
export const calculateStatistics = (
|
||||
processedFiles: ProcessedFile[],
|
||||
fileLineCounts: Record<string, number>,
|
||||
): StatisticsInfo => {
|
||||
const statsByExt: Record<string, { fileCount: number; lineCount: number }> = {};
|
||||
let totalLines = 0;
|
||||
|
||||
// Calculate stats by extension
|
||||
for (const file of processedFiles) {
|
||||
const ext = path.extname(file.path).toLowerCase() || '(no ext)';
|
||||
const lines = fileLineCounts[file.path] || file.content.split('\n').length;
|
||||
|
||||
if (!statsByExt[ext]) {
|
||||
statsByExt[ext] = { fileCount: 0, lineCount: 0 };
|
||||
}
|
||||
statsByExt[ext].fileCount++;
|
||||
statsByExt[ext].lineCount += lines;
|
||||
totalLines += lines;
|
||||
}
|
||||
|
||||
// Convert to array and sort by file count
|
||||
const byFileType: FileTypeStats[] = Object.entries(statsByExt)
|
||||
.map(([ext, stats]) => ({
|
||||
extension: ext,
|
||||
language: ext === '(no ext)' ? 'No Extension' : getLanguageFromExtension(ext),
|
||||
fileCount: stats.fileCount,
|
||||
lineCount: stats.lineCount,
|
||||
}))
|
||||
.sort((a, b) => b.fileCount - a.fileCount);
|
||||
|
||||
// Get largest files (top 5)
|
||||
const largestFiles = processedFiles
|
||||
.map((file) => ({
|
||||
path: file.path,
|
||||
lines: fileLineCounts[file.path] || file.content.split('\n').length,
|
||||
}))
|
||||
.sort((a, b) => b.lines - a.lines)
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
totalFiles: processedFiles.length,
|
||||
totalLines,
|
||||
byFileType,
|
||||
largestFiles,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates statistics markdown table for SKILL.md.
|
||||
*/
|
||||
export const generateStatisticsSection = (stats: StatisticsInfo): string => {
|
||||
const lines: string[] = ['## Statistics', ''];
|
||||
|
||||
// Summary line
|
||||
lines.push(`${stats.totalFiles} files | ${stats.totalLines.toLocaleString()} lines`);
|
||||
lines.push('');
|
||||
|
||||
// File type table (top 10)
|
||||
lines.push('| Language | Files | Lines |');
|
||||
lines.push('|----------|------:|------:|');
|
||||
|
||||
const topTypes = stats.byFileType.slice(0, 10);
|
||||
for (const type of topTypes) {
|
||||
lines.push(`| ${type.language} | ${type.fileCount} | ${type.lineCount.toLocaleString()} |`);
|
||||
}
|
||||
|
||||
if (stats.byFileType.length > 10) {
|
||||
const otherFiles = stats.byFileType.slice(10).reduce((sum, t) => sum + t.fileCount, 0);
|
||||
const otherLines = stats.byFileType.slice(10).reduce((sum, t) => sum + t.lineCount, 0);
|
||||
lines.push(`| Other | ${otherFiles} | ${otherLines.toLocaleString()} |`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
// Largest files
|
||||
if (stats.largestFiles.length > 0) {
|
||||
lines.push('**Largest files:**');
|
||||
for (const file of stats.largestFiles) {
|
||||
lines.push(`- \`${file.path}\` (${file.lines.toLocaleString()} lines)`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
@@ -5,7 +5,10 @@ export interface SkillRenderContext {
|
||||
skillDescription: string;
|
||||
projectName: string;
|
||||
totalFiles: number;
|
||||
totalLines: number;
|
||||
totalTokens: number;
|
||||
statisticsSection: string;
|
||||
hasTechStack: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,7 +23,7 @@ description: {{{skillDescription}}}
|
||||
|
||||
# {{{projectName}}} Codebase Reference
|
||||
|
||||
{{{totalFiles}}} files | {{{totalTokens}}} tokens
|
||||
{{{totalFiles}}} files | {{{totalLines}}} lines | {{{totalTokens}}} tokens
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -34,15 +37,20 @@ Use this skill when you need to:
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| \`references/structure.md\` | Directory tree with line counts per file |
|
||||
| \`references/project-structure.md\` | Directory tree with line counts per file |
|
||||
| \`references/files.md\` | All file contents (header: \`## File: <path>\`) |
|
||||
{{#if hasTechStack}}
|
||||
| \`references/tech-stack.md\` | Languages, frameworks, and dependencies |
|
||||
{{/if}}
|
||||
| \`references/summary.md\` | Purpose and format explanation |
|
||||
|
||||
{{{statisticsSection}}}
|
||||
|
||||
## How to Use
|
||||
|
||||
### 1. Find file locations
|
||||
|
||||
Check \`structure.md\` for the directory tree:
|
||||
Check \`project-structure.md\` for the directory tree:
|
||||
|
||||
\`\`\`
|
||||
src/
|
||||
@@ -70,22 +78,25 @@ function calculateTotal
|
||||
## Common Use Cases
|
||||
|
||||
**Understand a feature:**
|
||||
1. Search \`structure.md\` for related file names
|
||||
1. Search \`project-structure.md\` for related file names
|
||||
2. Read the main implementation file in \`files.md\`
|
||||
3. Search for imports/references to trace dependencies
|
||||
|
||||
**Debug an error:**
|
||||
1. Grep the error message or class name in \`files.md\`
|
||||
2. Check line counts in \`structure.md\` to find large files
|
||||
2. Check line counts in \`project-structure.md\` to find large files
|
||||
|
||||
**Find all usages:**
|
||||
1. Grep function or variable name in \`files.md\`
|
||||
|
||||
## Tips
|
||||
|
||||
- Use line counts in \`structure.md\` to estimate file complexity
|
||||
- Use line counts in \`project-structure.md\` to estimate file complexity
|
||||
- Search \`## File:\` pattern to jump between files
|
||||
- Check \`summary.md\` for excluded files and format details
|
||||
{{#if hasTechStack}}
|
||||
- Check \`tech-stack.md\` for languages, frameworks, and dependencies
|
||||
{{/if}}
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import type { ProcessedFile } from '../../file/fileTypes.js';
|
||||
|
||||
interface DependencyInfo {
|
||||
name: string;
|
||||
version?: string;
|
||||
isDev?: boolean;
|
||||
}
|
||||
|
||||
interface TechStackInfo {
|
||||
languages: string[];
|
||||
frameworks: string[];
|
||||
dependencies: DependencyInfo[];
|
||||
devDependencies: DependencyInfo[];
|
||||
packageManager?: string;
|
||||
}
|
||||
|
||||
// Dependency file patterns and their parsers
|
||||
const DEPENDENCY_FILES: Record<string, { language: string; parser: (content: string) => Partial<TechStackInfo> }> = {
|
||||
'package.json': { language: 'Node.js', parser: parsePackageJson },
|
||||
'requirements.txt': { language: 'Python', parser: parseRequirementsTxt },
|
||||
'pyproject.toml': { language: 'Python', parser: parsePyprojectToml },
|
||||
Pipfile: { language: 'Python', parser: parsePipfile },
|
||||
'go.mod': { language: 'Go', parser: parseGoMod },
|
||||
'Cargo.toml': { language: 'Rust', parser: parseCargoToml },
|
||||
'composer.json': { language: 'PHP', parser: parseComposerJson },
|
||||
Gemfile: { language: 'Ruby', parser: parseGemfile },
|
||||
'pom.xml': { language: 'Java', parser: parsePomXml },
|
||||
'build.gradle': { language: 'Java/Kotlin', parser: parseBuildGradle },
|
||||
'build.gradle.kts': { language: 'Kotlin', parser: parseBuildGradle },
|
||||
};
|
||||
|
||||
function parsePackageJson(content: string): Partial<TechStackInfo> {
|
||||
try {
|
||||
const pkg = JSON.parse(content);
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const devDependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Parse dependencies
|
||||
if (pkg.dependencies) {
|
||||
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
||||
dependencies.push({ name, version: String(version) });
|
||||
|
||||
// Detect frameworks
|
||||
if (name === 'react' || name === 'react-dom') frameworks.push('React');
|
||||
if (name === 'vue') frameworks.push('Vue');
|
||||
if (name === 'next') frameworks.push('Next.js');
|
||||
if (name === 'nuxt') frameworks.push('Nuxt');
|
||||
if (name === '@angular/core') frameworks.push('Angular');
|
||||
if (name === 'express') frameworks.push('Express');
|
||||
if (name === 'fastify') frameworks.push('Fastify');
|
||||
if (name === 'hono') frameworks.push('Hono');
|
||||
if (name === 'svelte') frameworks.push('Svelte');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse devDependencies
|
||||
if (pkg.devDependencies) {
|
||||
for (const [name, version] of Object.entries(pkg.devDependencies)) {
|
||||
devDependencies.push({ name, version: String(version), isDev: true });
|
||||
|
||||
// Detect TypeScript
|
||||
if (name === 'typescript') frameworks.push('TypeScript');
|
||||
}
|
||||
}
|
||||
|
||||
// Detect package manager
|
||||
let packageManager: string | undefined;
|
||||
if (pkg.packageManager) {
|
||||
const pm = String(pkg.packageManager);
|
||||
if (pm.startsWith('pnpm')) packageManager = 'pnpm';
|
||||
else if (pm.startsWith('yarn')) packageManager = 'yarn';
|
||||
else if (pm.startsWith('npm')) packageManager = 'npm';
|
||||
else if (pm.startsWith('bun')) packageManager = 'bun';
|
||||
}
|
||||
|
||||
return {
|
||||
dependencies,
|
||||
devDependencies,
|
||||
frameworks: [...new Set(frameworks)],
|
||||
packageManager,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function parseRequirementsTxt(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
|
||||
|
||||
// Parse package==version or package>=version format
|
||||
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([=<>!~]+)?(.+)?$/);
|
||||
if (match) {
|
||||
const name = match[1];
|
||||
const version = match[3];
|
||||
dependencies.push({ name, version });
|
||||
|
||||
// Detect frameworks
|
||||
if (name.toLowerCase() === 'django') frameworks.push('Django');
|
||||
if (name.toLowerCase() === 'flask') frameworks.push('Flask');
|
||||
if (name.toLowerCase() === 'fastapi') frameworks.push('FastAPI');
|
||||
if (name.toLowerCase() === 'pytorch' || name.toLowerCase() === 'torch') frameworks.push('PyTorch');
|
||||
if (name.toLowerCase() === 'tensorflow') frameworks.push('TensorFlow');
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parsePyprojectToml(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Simple TOML parsing for dependencies
|
||||
const depsMatch = content.match(/\[project\.dependencies\]([\s\S]*?)(?=\[|$)/);
|
||||
if (depsMatch) {
|
||||
const depsSection = depsMatch[1];
|
||||
const depLines = depsSection.match(/"([^"]+)"/g);
|
||||
if (depLines) {
|
||||
for (const dep of depLines) {
|
||||
const name = dep
|
||||
.replace(/"/g, '')
|
||||
.split(/[=<>!~]/)[0]
|
||||
.trim();
|
||||
if (name) {
|
||||
dependencies.push({ name });
|
||||
if (name.toLowerCase() === 'django') frameworks.push('Django');
|
||||
if (name.toLowerCase() === 'flask') frameworks.push('Flask');
|
||||
if (name.toLowerCase() === 'fastapi') frameworks.push('FastAPI');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parsePipfile(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
|
||||
// Simple parsing for [packages] section
|
||||
const packagesMatch = content.match(/\[packages\]([\s\S]*?)(?=\[|$)/);
|
||||
if (packagesMatch) {
|
||||
const lines = packagesMatch[1].split('\n');
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
||||
if (match) {
|
||||
dependencies.push({ name: match[1] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies };
|
||||
}
|
||||
|
||||
function parseGoMod(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Parse require block
|
||||
const requireMatch = content.match(/require\s*\(([\s\S]*?)\)/);
|
||||
if (requireMatch) {
|
||||
const lines = requireMatch[1].split('\n');
|
||||
for (const line of lines) {
|
||||
const match = line.trim().match(/^([^\s]+)\s+([^\s]+)/);
|
||||
if (match) {
|
||||
const name = match[1];
|
||||
const version = match[2];
|
||||
dependencies.push({ name, version });
|
||||
|
||||
if (name.includes('gin-gonic/gin')) frameworks.push('Gin');
|
||||
if (name.includes('labstack/echo')) frameworks.push('Echo');
|
||||
if (name.includes('gofiber/fiber')) frameworks.push('Fiber');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parseCargoToml(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Parse [dependencies] section
|
||||
const depsMatch = content.match(/\[dependencies\]([\s\S]*?)(?=\[|$)/);
|
||||
if (depsMatch) {
|
||||
const lines = depsMatch[1].split('\n');
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
||||
if (match) {
|
||||
const name = match[1];
|
||||
dependencies.push({ name });
|
||||
|
||||
if (name === 'actix-web') frameworks.push('Actix');
|
||||
if (name === 'axum') frameworks.push('Axum');
|
||||
if (name === 'rocket') frameworks.push('Rocket');
|
||||
if (name === 'tokio') frameworks.push('Tokio');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parseComposerJson(content: string): Partial<TechStackInfo> {
|
||||
try {
|
||||
const composer = JSON.parse(content);
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
if (composer.require) {
|
||||
for (const [name, version] of Object.entries(composer.require)) {
|
||||
if (name !== 'php') {
|
||||
dependencies.push({ name, version: String(version) });
|
||||
|
||||
if (name.startsWith('laravel/')) frameworks.push('Laravel');
|
||||
if (name.startsWith('symfony/')) frameworks.push('Symfony');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function parseGemfile(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
const gemMatches = content.matchAll(/gem\s+['"]([^'"]+)['"]/g);
|
||||
for (const match of gemMatches) {
|
||||
const name = match[1];
|
||||
dependencies.push({ name });
|
||||
|
||||
if (name === 'rails') frameworks.push('Rails');
|
||||
if (name === 'sinatra') frameworks.push('Sinatra');
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parsePomXml(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Simple XML parsing for dependencies
|
||||
const depMatches = content.matchAll(/<dependency>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<\/dependency>/g);
|
||||
for (const match of depMatches) {
|
||||
const name = match[1];
|
||||
dependencies.push({ name });
|
||||
|
||||
if (name.includes('spring')) frameworks.push('Spring');
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
function parseBuildGradle(content: string): Partial<TechStackInfo> {
|
||||
const dependencies: DependencyInfo[] = [];
|
||||
const frameworks: string[] = [];
|
||||
|
||||
// Parse implementation/compile dependencies
|
||||
const depMatches = content.matchAll(/(?:implementation|compile)\s*['"(]([^'"()]+)['"]/g);
|
||||
for (const match of depMatches) {
|
||||
const dep = match[1];
|
||||
const parts = dep.split(':');
|
||||
const name = parts.length >= 2 ? parts[1] : dep;
|
||||
dependencies.push({ name });
|
||||
|
||||
if (dep.includes('spring')) frameworks.push('Spring');
|
||||
if (dep.includes('ktor')) frameworks.push('Ktor');
|
||||
}
|
||||
|
||||
return { dependencies, frameworks: [...new Set(frameworks)] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects tech stack from processed files.
|
||||
* Only checks root-level dependency files.
|
||||
*/
|
||||
export const detectTechStack = (processedFiles: ProcessedFile[]): TechStackInfo | null => {
|
||||
const result: TechStackInfo = {
|
||||
languages: [],
|
||||
frameworks: [],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
};
|
||||
|
||||
let foundAny = false;
|
||||
|
||||
for (const file of processedFiles) {
|
||||
// Only check root-level files (no directory separator in path)
|
||||
const fileName = file.path.split('/').pop() || file.path;
|
||||
if (file.path !== fileName && !file.path.startsWith('./')) {
|
||||
// Skip files in subdirectories
|
||||
const dirDepth = file.path.split('/').length - 1;
|
||||
if (dirDepth > 0) continue;
|
||||
}
|
||||
|
||||
const config = DEPENDENCY_FILES[fileName];
|
||||
if (config) {
|
||||
foundAny = true;
|
||||
result.languages.push(config.language);
|
||||
|
||||
const parsed = config.parser(file.content);
|
||||
if (parsed.dependencies) {
|
||||
result.dependencies.push(...parsed.dependencies);
|
||||
}
|
||||
if (parsed.devDependencies) {
|
||||
result.devDependencies.push(...parsed.devDependencies);
|
||||
}
|
||||
if (parsed.frameworks) {
|
||||
result.frameworks.push(...parsed.frameworks);
|
||||
}
|
||||
if (parsed.packageManager) {
|
||||
result.packageManager = parsed.packageManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAny) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
result.languages = [...new Set(result.languages)];
|
||||
result.frameworks = [...new Set(result.frameworks)];
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates tech-stack.md content from detected tech stack.
|
||||
*/
|
||||
export const generateTechStackMd = (techStack: TechStackInfo): string => {
|
||||
const lines: string[] = ['# Tech Stack', ''];
|
||||
|
||||
// Languages
|
||||
if (techStack.languages.length > 0) {
|
||||
lines.push('## Languages');
|
||||
lines.push('');
|
||||
for (const lang of techStack.languages) {
|
||||
lines.push(`- ${lang}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Frameworks
|
||||
if (techStack.frameworks.length > 0) {
|
||||
lines.push('## Frameworks');
|
||||
lines.push('');
|
||||
for (const fw of techStack.frameworks) {
|
||||
lines.push(`- ${fw}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Package Manager
|
||||
if (techStack.packageManager) {
|
||||
lines.push('## Package Manager');
|
||||
lines.push('');
|
||||
lines.push(`- ${techStack.packageManager}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Dependencies (limit to top 20 for readability)
|
||||
if (techStack.dependencies.length > 0) {
|
||||
lines.push('## Dependencies');
|
||||
lines.push('');
|
||||
const deps = techStack.dependencies.slice(0, 20);
|
||||
for (const dep of deps) {
|
||||
const version = dep.version ? ` (${dep.version})` : '';
|
||||
lines.push(`- ${dep.name}${version}`);
|
||||
}
|
||||
if (techStack.dependencies.length > 20) {
|
||||
lines.push(`- ... and ${techStack.dependencies.length - 20} more`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Dev Dependencies (limit to top 10)
|
||||
if (techStack.devDependencies.length > 0) {
|
||||
lines.push('## Dev Dependencies');
|
||||
lines.push('');
|
||||
const devDeps = techStack.devDependencies.slice(0, 10);
|
||||
for (const dep of devDeps) {
|
||||
const version = dep.version ? ` (${dep.version})` : '';
|
||||
lines.push(`- ${dep.name}${version}`);
|
||||
}
|
||||
if (techStack.devDependencies.length > 10) {
|
||||
lines.push(`- ... and ${techStack.devDependencies.length - 10} more`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n').trim();
|
||||
};
|
||||
@@ -10,8 +10,9 @@ import type { SkillOutputResult } from '../output/outputGenerate.js';
|
||||
* ├── SKILL.md
|
||||
* └── references/
|
||||
* ├── summary.md
|
||||
* ├── structure.md
|
||||
* └── files.md
|
||||
* ├── project-structure.md
|
||||
* ├── files.md
|
||||
* └── tech-stack.md (if available)
|
||||
*/
|
||||
export const writeSkillOutput = async (
|
||||
output: SkillOutputResult,
|
||||
@@ -33,9 +34,14 @@ export const writeSkillOutput = async (
|
||||
|
||||
// Write reference files
|
||||
await deps.writeFile(path.join(referencesDir, 'summary.md'), output.references.summary, 'utf-8');
|
||||
await deps.writeFile(path.join(referencesDir, 'structure.md'), output.references.structure, 'utf-8');
|
||||
await deps.writeFile(path.join(referencesDir, 'project-structure.md'), output.references.structure, 'utf-8');
|
||||
await deps.writeFile(path.join(referencesDir, 'files.md'), output.references.files, 'utf-8');
|
||||
|
||||
// Write tech-stack.md if available
|
||||
if (output.references.techStack) {
|
||||
await deps.writeFile(path.join(referencesDir, 'tech-stack.md'), output.references.techStack, 'utf-8');
|
||||
}
|
||||
|
||||
return skillDir;
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { ProcessedFile } from '../../../../src/core/file/fileTypes.js';
|
||||
import { calculateStatistics, generateStatisticsSection } from '../../../../src/core/output/skill/skillStatistics.js';
|
||||
|
||||
describe('skillStatistics', () => {
|
||||
describe('calculateStatistics', () => {
|
||||
test('should calculate statistics by file type', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{ path: 'src/index.ts', content: 'line1\nline2\nline3' },
|
||||
{ path: 'src/utils.ts', content: 'line1\nline2' },
|
||||
{ path: 'src/styles.css', content: 'line1' },
|
||||
{ path: 'README.md', content: 'line1\nline2\nline3\nline4' },
|
||||
];
|
||||
|
||||
const lineCounts = {
|
||||
'src/index.ts': 3,
|
||||
'src/utils.ts': 2,
|
||||
'src/styles.css': 1,
|
||||
'README.md': 4,
|
||||
};
|
||||
|
||||
const result = calculateStatistics(files, lineCounts);
|
||||
|
||||
expect(result.totalFiles).toBe(4);
|
||||
expect(result.totalLines).toBe(10);
|
||||
expect(result.byFileType.length).toBe(3);
|
||||
|
||||
const tsStats = result.byFileType.find((s) => s.extension === '.ts');
|
||||
expect(tsStats?.fileCount).toBe(2);
|
||||
expect(tsStats?.lineCount).toBe(5);
|
||||
expect(tsStats?.language).toBe('TypeScript');
|
||||
|
||||
const cssStats = result.byFileType.find((s) => s.extension === '.css');
|
||||
expect(cssStats?.fileCount).toBe(1);
|
||||
expect(cssStats?.lineCount).toBe(1);
|
||||
expect(cssStats?.language).toBe('CSS');
|
||||
|
||||
const mdStats = result.byFileType.find((s) => s.extension === '.md');
|
||||
expect(mdStats?.fileCount).toBe(1);
|
||||
expect(mdStats?.lineCount).toBe(4);
|
||||
expect(mdStats?.language).toBe('Markdown');
|
||||
});
|
||||
|
||||
test('should return largest files sorted by line count', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{ path: 'small.ts', content: 'a' },
|
||||
{ path: 'large.ts', content: 'a\nb\nc\nd\ne' },
|
||||
{ path: 'medium.ts', content: 'a\nb\nc' },
|
||||
];
|
||||
|
||||
const lineCounts = {
|
||||
'small.ts': 1,
|
||||
'large.ts': 5,
|
||||
'medium.ts': 3,
|
||||
};
|
||||
|
||||
const result = calculateStatistics(files, lineCounts);
|
||||
|
||||
expect(result.largestFiles[0].path).toBe('large.ts');
|
||||
expect(result.largestFiles[0].lines).toBe(5);
|
||||
expect(result.largestFiles[1].path).toBe('medium.ts');
|
||||
expect(result.largestFiles[2].path).toBe('small.ts');
|
||||
});
|
||||
|
||||
test('should limit largest files to 5', () => {
|
||||
const files: ProcessedFile[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
path: `file${i}.ts`,
|
||||
content: 'a'.repeat(i + 1),
|
||||
}));
|
||||
|
||||
const lineCounts = Object.fromEntries(files.map((f, i) => [f.path, i + 1]));
|
||||
|
||||
const result = calculateStatistics(files, lineCounts);
|
||||
|
||||
expect(result.largestFiles.length).toBe(5);
|
||||
});
|
||||
|
||||
test('should sort file types by file count', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{ path: 'a.ts', content: 'a' },
|
||||
{ path: 'b.ts', content: 'a' },
|
||||
{ path: 'c.ts', content: 'a' },
|
||||
{ path: 'x.js', content: 'a' },
|
||||
{ path: 'y.css', content: 'a' },
|
||||
{ path: 'z.css', content: 'a' },
|
||||
];
|
||||
|
||||
const lineCounts = Object.fromEntries(files.map((f) => [f.path, 1]));
|
||||
|
||||
const result = calculateStatistics(files, lineCounts);
|
||||
|
||||
expect(result.byFileType[0].extension).toBe('.ts');
|
||||
expect(result.byFileType[0].fileCount).toBe(3);
|
||||
expect(result.byFileType[1].extension).toBe('.css');
|
||||
expect(result.byFileType[1].fileCount).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle files without extension', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{ path: 'Dockerfile', content: 'FROM node' },
|
||||
{ path: 'Makefile', content: 'all:' },
|
||||
];
|
||||
|
||||
const lineCounts = {
|
||||
Dockerfile: 1,
|
||||
Makefile: 1,
|
||||
};
|
||||
|
||||
const result = calculateStatistics(files, lineCounts);
|
||||
|
||||
const noExtStats = result.byFileType.find((s) => s.extension === '(no ext)');
|
||||
expect(noExtStats?.fileCount).toBe(2);
|
||||
expect(noExtStats?.language).toBe('No Extension');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateStatisticsSection', () => {
|
||||
test('should generate statistics markdown', () => {
|
||||
const stats = {
|
||||
totalFiles: 10,
|
||||
totalLines: 500,
|
||||
byFileType: [
|
||||
{ extension: '.ts', language: 'TypeScript', fileCount: 5, lineCount: 300 },
|
||||
{ extension: '.js', language: 'JavaScript', fileCount: 3, lineCount: 150 },
|
||||
{ extension: '.css', language: 'CSS', fileCount: 2, lineCount: 50 },
|
||||
],
|
||||
largestFiles: [
|
||||
{ path: 'src/main.ts', lines: 200 },
|
||||
{ path: 'src/utils.ts', lines: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
const result = generateStatisticsSection(stats);
|
||||
|
||||
expect(result).toContain('## Statistics');
|
||||
expect(result).toContain('10 files | 500 lines');
|
||||
expect(result).toContain('| Language | Files | Lines |');
|
||||
expect(result).toContain('| TypeScript | 5 | 300 |');
|
||||
expect(result).toContain('| JavaScript | 3 | 150 |');
|
||||
expect(result).toContain('| CSS | 2 | 50 |');
|
||||
expect(result).toContain('**Largest files:**');
|
||||
expect(result).toContain('`src/main.ts` (200 lines)');
|
||||
expect(result).toContain('`src/utils.ts` (100 lines)');
|
||||
});
|
||||
|
||||
test('should limit file types to 10 and show "Other" row', () => {
|
||||
const stats = {
|
||||
totalFiles: 50,
|
||||
totalLines: 1000,
|
||||
byFileType: Array.from({ length: 15 }, (_, i) => ({
|
||||
extension: `.ext${i}`,
|
||||
language: `Language${i}`,
|
||||
fileCount: 5 - Math.floor(i / 3),
|
||||
lineCount: 100 - i * 5,
|
||||
})),
|
||||
largestFiles: [],
|
||||
};
|
||||
|
||||
const result = generateStatisticsSection(stats);
|
||||
|
||||
expect(result).toContain('| Other |');
|
||||
expect(result).not.toContain('Language14');
|
||||
});
|
||||
|
||||
test('should format large numbers with locale string', () => {
|
||||
const stats = {
|
||||
totalFiles: 100,
|
||||
totalLines: 10000,
|
||||
byFileType: [{ extension: '.ts', language: 'TypeScript', fileCount: 100, lineCount: 10000 }],
|
||||
largestFiles: [{ path: 'big.ts', lines: 5000 }],
|
||||
};
|
||||
|
||||
const result = generateStatisticsSection(stats);
|
||||
|
||||
expect(result).toContain('10,000');
|
||||
expect(result).toContain('5,000');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,20 +36,34 @@ describe('skillStyle', () => {
|
||||
test('should reference multiple files', () => {
|
||||
const template = getSkillTemplate();
|
||||
expect(template).toContain('references/summary.md');
|
||||
expect(template).toContain('references/structure.md');
|
||||
expect(template).toContain('references/project-structure.md');
|
||||
expect(template).toContain('references/files.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSkillMd', () => {
|
||||
const createTestContext = (overrides = {}) => ({
|
||||
skillName: 'test-skill',
|
||||
skillDescription: 'Test description',
|
||||
projectName: 'Test Project',
|
||||
totalFiles: 1,
|
||||
totalLines: 100,
|
||||
totalTokens: 100,
|
||||
statisticsSection: '## Statistics\n\n1 files | 100 lines',
|
||||
hasTechStack: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('should generate SKILL.md with all fields', () => {
|
||||
const context = {
|
||||
const context = createTestContext({
|
||||
skillName: 'my-project-skill',
|
||||
skillDescription: 'Reference codebase for My Project.',
|
||||
projectName: 'My Project',
|
||||
totalFiles: 42,
|
||||
totalLines: 1000,
|
||||
totalTokens: 12345,
|
||||
};
|
||||
statisticsSection: '## Statistics\n\n42 files | 1,000 lines',
|
||||
});
|
||||
|
||||
const result = generateSkillMd(context);
|
||||
|
||||
@@ -65,45 +79,45 @@ describe('skillStyle', () => {
|
||||
});
|
||||
|
||||
test('should end with newline', () => {
|
||||
const context = {
|
||||
skillName: 'test-skill',
|
||||
skillDescription: 'Test description',
|
||||
projectName: 'Test Project',
|
||||
totalFiles: 1,
|
||||
totalTokens: 100,
|
||||
};
|
||||
|
||||
const result = generateSkillMd(context);
|
||||
const result = generateSkillMd(createTestContext());
|
||||
expect(result.endsWith('\n')).toBe(true);
|
||||
});
|
||||
|
||||
test('should include references to multiple files', () => {
|
||||
const context = {
|
||||
skillName: 'test-skill',
|
||||
skillDescription: 'Test description',
|
||||
projectName: 'Test Project',
|
||||
totalFiles: 1,
|
||||
totalTokens: 100,
|
||||
};
|
||||
|
||||
const result = generateSkillMd(context);
|
||||
const result = generateSkillMd(createTestContext());
|
||||
expect(result).toContain('`references/summary.md`');
|
||||
expect(result).toContain('`references/structure.md`');
|
||||
expect(result).toContain('`references/project-structure.md`');
|
||||
expect(result).toContain('`references/files.md`');
|
||||
});
|
||||
|
||||
test('should not include git sections (skill output is for reference codebases)', () => {
|
||||
const context = {
|
||||
skillName: 'test-skill',
|
||||
skillDescription: 'Test description',
|
||||
projectName: 'Test Project',
|
||||
totalFiles: 1,
|
||||
totalTokens: 100,
|
||||
};
|
||||
|
||||
const result = generateSkillMd(context);
|
||||
const result = generateSkillMd(createTestContext());
|
||||
expect(result).not.toContain('git-diffs.md');
|
||||
expect(result).not.toContain('git-logs.md');
|
||||
});
|
||||
|
||||
test('should include tech-stack reference when hasTechStack is true', () => {
|
||||
const result = generateSkillMd(createTestContext({ hasTechStack: true }));
|
||||
expect(result).toContain('`references/tech-stack.md`');
|
||||
});
|
||||
|
||||
test('should not include tech-stack reference when hasTechStack is false', () => {
|
||||
const result = generateSkillMd(createTestContext({ hasTechStack: false }));
|
||||
expect(result).not.toContain('tech-stack.md');
|
||||
});
|
||||
|
||||
test('should include statistics section', () => {
|
||||
const result = generateSkillMd(
|
||||
createTestContext({
|
||||
statisticsSection: '## Statistics\n\n10 files | 500 lines',
|
||||
}),
|
||||
);
|
||||
expect(result).toContain('## Statistics');
|
||||
});
|
||||
|
||||
test('should include total lines in header', () => {
|
||||
const result = generateSkillMd(createTestContext({ totalLines: 5000 }));
|
||||
expect(result).toContain('5000 lines');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { ProcessedFile } from '../../../../src/core/file/fileTypes.js';
|
||||
import { detectTechStack, generateTechStackMd } from '../../../../src/core/output/skill/skillTechStack.js';
|
||||
|
||||
describe('skillTechStack', () => {
|
||||
describe('detectTechStack', () => {
|
||||
test('should detect Node.js from package.json', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'package.json',
|
||||
content: JSON.stringify({
|
||||
dependencies: {
|
||||
react: '^18.2.0',
|
||||
express: '^4.18.0',
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: '^5.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.languages).toContain('Node.js');
|
||||
expect(result?.frameworks).toContain('React');
|
||||
expect(result?.frameworks).toContain('Express');
|
||||
expect(result?.frameworks).toContain('TypeScript');
|
||||
expect(result?.dependencies.length).toBeGreaterThan(0);
|
||||
expect(result?.devDependencies.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should detect Python from requirements.txt', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'requirements.txt',
|
||||
content: `django==4.2.0
|
||||
flask>=2.0.0
|
||||
fastapi
|
||||
# comment
|
||||
-r base.txt`,
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.languages).toContain('Python');
|
||||
expect(result?.frameworks).toContain('Django');
|
||||
expect(result?.frameworks).toContain('Flask');
|
||||
expect(result?.frameworks).toContain('FastAPI');
|
||||
});
|
||||
|
||||
test('should detect Go from go.mod', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'go.mod',
|
||||
content: `module example.com/myproject
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
)`,
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.languages).toContain('Go');
|
||||
expect(result?.frameworks).toContain('Gin');
|
||||
});
|
||||
|
||||
test('should detect Rust from Cargo.toml', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'Cargo.toml',
|
||||
content: `[package]
|
||||
name = "myproject"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0"
|
||||
tokio = { version = "1.0", features = ["full"] }`,
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.languages).toContain('Rust');
|
||||
expect(result?.frameworks).toContain('Actix');
|
||||
expect(result?.frameworks).toContain('Tokio');
|
||||
});
|
||||
|
||||
test('should return null when no dependency files found', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{ path: 'src/index.ts', content: 'console.log("hello")' },
|
||||
{ path: 'README.md', content: '# My Project' },
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should ignore dependency files in subdirectories', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'packages/sub/package.json',
|
||||
content: JSON.stringify({ dependencies: { lodash: '4.0.0' } }),
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should detect package manager from packageManager field', () => {
|
||||
const files: ProcessedFile[] = [
|
||||
{
|
||||
path: 'package.json',
|
||||
content: JSON.stringify({
|
||||
packageManager: 'pnpm@8.0.0',
|
||||
dependencies: {},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const result = detectTechStack(files);
|
||||
expect(result?.packageManager).toBe('pnpm');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateTechStackMd', () => {
|
||||
test('should generate markdown with all sections', () => {
|
||||
const techStack = {
|
||||
languages: ['Node.js'],
|
||||
frameworks: ['React', 'TypeScript'],
|
||||
dependencies: [
|
||||
{ name: 'react', version: '^18.2.0' },
|
||||
{ name: 'react-dom', version: '^18.2.0' },
|
||||
],
|
||||
devDependencies: [{ name: 'typescript', version: '^5.0.0' }],
|
||||
packageManager: 'npm',
|
||||
};
|
||||
|
||||
const result = generateTechStackMd(techStack);
|
||||
|
||||
expect(result).toContain('# Tech Stack');
|
||||
expect(result).toContain('## Languages');
|
||||
expect(result).toContain('- Node.js');
|
||||
expect(result).toContain('## Frameworks');
|
||||
expect(result).toContain('- React');
|
||||
expect(result).toContain('- TypeScript');
|
||||
expect(result).toContain('## Package Manager');
|
||||
expect(result).toContain('- npm');
|
||||
expect(result).toContain('## Dependencies');
|
||||
expect(result).toContain('- react (^18.2.0)');
|
||||
expect(result).toContain('## Dev Dependencies');
|
||||
expect(result).toContain('- typescript (^5.0.0)');
|
||||
});
|
||||
|
||||
test('should limit dependencies to 20', () => {
|
||||
const techStack = {
|
||||
languages: ['Node.js'],
|
||||
frameworks: [],
|
||||
dependencies: Array.from({ length: 25 }, (_, i) => ({ name: `dep-${i}`, version: '1.0.0' })),
|
||||
devDependencies: [],
|
||||
};
|
||||
|
||||
const result = generateTechStackMd(techStack);
|
||||
|
||||
expect(result).toContain('... and 5 more');
|
||||
expect(result).not.toContain('dep-24');
|
||||
});
|
||||
|
||||
test('should handle empty sections', () => {
|
||||
const techStack = {
|
||||
languages: ['Node.js'],
|
||||
frameworks: [],
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
};
|
||||
|
||||
const result = generateTechStackMd(techStack);
|
||||
|
||||
expect(result).toContain('# Tech Stack');
|
||||
expect(result).toContain('## Languages');
|
||||
expect(result).not.toContain('## Frameworks');
|
||||
expect(result).not.toContain('## Dependencies');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user