Implement ScriptAuditorService - Audit shell scripts for TypeScript conversion
Migrated from llm/agent-buildkit#3 on 2025-10-12T20:16:20.915Z Original author: @group_286_bot_4df652bbc58e62e505f1a777cd8f21b8 | Created: 2025-10-11T03:55:57.726Z
ScriptAuditorService Implementation
Parent Issue
Related to #1 (Project Health Auditor Epic)
Objective
Implement ScriptAuditorService that audits all shell scripts in a project, calculates their complexity, and identifies candidates for TypeScript conversion.
Contract (Port Interface)
Implements: `IScriptAuditorPort` from `/src/services/ports/project-health.port.ts`
Required Methods
```typescript interface IScriptAuditorPort { auditScripts(projectPath: string): Promise; calculateComplexity(scriptPath: string): Promise<'trivial' | 'simple' | 'moderate' | 'complex' | 'critical'>; findNpmScriptDependencies(projectPath: string): Promise<Map<string, string[]>>; } ```
Responsibilities (SINGLE RESPONSIBILITY)
ONLY audit shell scripts. Does NOT:
- Analyze project structure (StructureAnalyzerService)
- Calculate health scores (HealthScoringService)
- Execute cleanup (CleanupService)
Implementation Details
File Location
`/src/services/domain/project-health/ScriptAuditorService.ts`
Shell Script Types to Detect
- `.sh` files (bash scripts)
- `.bash` files (bash scripts)
- npm scripts in `package.json` that call shell commands
Complexity Calculation Algorithm
```typescript async calculateComplexity(scriptPath: string): Promise { const content = await this.fs.readFile(scriptPath, 'utf-8'); const lines = content.split('\n');
let score = 0;
// Line count scoring if (lines.length > 200) score += 5; else if (lines.length > 100) score += 3; else if (lines.length > 50) score += 2; else if (lines.length > 20) score += 1;
// Complexity patterns const patterns = { loops: /(for|while|until)\s+/g, // +2 per loop conditionals: /if\s+[/g, // +1 per conditional functions: /function\s+\w+|^\w+()\s*{/gm, // +2 per function pipes: /|/g, // +1 per pipe subshells: /$(/g, // +1 per subshell backgroundJobs: /&$/gm, // +3 per background job traps: /trap\s+/g, // +2 per trap heredocs: /<<[-]?\w+/g, // +1 per heredoc };
for (const [pattern, regex] of Object.entries(patterns)) { const matches = content.match(regex) || []; if (pattern === 'loops') score += matches.length * 2; else if (pattern === 'functions') score += matches.length * 2; else if (pattern === 'backgroundJobs') score += matches.length * 3; else if (pattern === 'traps') score += matches.length * 2; else score += matches.length; }
// Map score to complexity level if (score >= 15) return 'critical'; // Must convert to TS if (score >= 10) return 'complex'; // Should convert to TS if (score >= 5) return 'moderate'; // Consider converting if (score >= 2) return 'simple'; // Could be npm script return 'trivial'; // Can stay as shell script } ```
Algorithm: auditScripts()
```typescript async auditScripts(projectPath: string): Promise { const scripts: ScriptInfo[] = [];
// 1. Find all .sh and .bash files const shellFiles = await this.findShellScripts(projectPath);
for (const file of shellFiles) { const complexity = await this.calculateComplexity(file); const lines = await this.countLines(file);
scripts.push({
path: file,
type: file.endsWith('.bash') ? 'bash' : 'shell',
lines,
complexity,
recommendation: this.getRecommendation(complexity, lines)
});
}
// 2. Find npm scripts that call shell files const npmDeps = await this.findNpmScriptDependencies(projectPath); for (const [npmScript, shellScripts] of npmDeps) { for (const shellScript of shellScripts) { scripts.push({ path: shellScript, type: 'npm', lines: 0, complexity: 'simple', recommendation: `Called by npm script: ${npmScript}` }); } }
// 3. Calculate overall complexity const maxComplexity = scripts.reduce((max, s) => { const levels = ['trivial', 'simple', 'moderate', 'complex', 'critical']; const sIndex = levels.indexOf(s.complexity || 'trivial'); const maxIndex = levels.indexOf(max); return sIndex > maxIndex ? s.complexity! : max; }, 'trivial' as Complexity);
return { projectName: basename(projectPath), scripts, summary: { totalScripts: scripts.length, complexity: maxComplexity } }; } ```
Algorithm: findNpmScriptDependencies()
```typescript async findNpmScriptDependencies(projectPath: string): Promise<Map<string, string[]>> { const deps = new Map<string, string[]>();
// Read package.json const pkgPath = join(projectPath, 'package.json'); if (!await this.fs.exists(pkgPath)) { return deps; }
const pkg = JSON.parse(await this.fs.readFile(pkgPath, 'utf-8')); const scripts = pkg.scripts || {};
// Analyze each npm script for (const [name, command] of Object.entries(scripts)) { // Look for .sh file references const shellRefs = command.match(/([./\w-]+.sh)/g) || []; if (shellRefs.length > 0) { deps.set(name, shellRefs); } }
return deps; } ```
Recommendations Logic
```typescript
private getRecommendation(complexity: Complexity, lines: number): string {
switch (complexity) {
case 'critical':
return '
Dependencies
- IFileSystemPort: For file operations (inject via constructor)
- glob or fast-glob: For finding shell scripts recursively
- Zod schemas: ScriptAuditReportSchema, ScriptInfoSchema
Acceptance Criteria
-
Implements all 3 methods from IScriptAuditorPort -
Constructor accepts IFileSystemPort for testability -
Correctly calculates complexity for all script patterns -
Finds shell scripts recursively (respecting .gitignore) -
Parses package.json to find npm→shell dependencies -
Returns recommendations based on complexity -
Returns data matching Zod schemas -
Unit tests with mock filesystem (>90% coverage) -
Integration tests with real shell scripts
Testing Strategy
```typescript describe('ScriptAuditorService', () => { let service: ScriptAuditorService; let mockFs: MockFileSystem;
beforeEach(() => { mockFs = new MockFileSystem(); service = new ScriptAuditorService(mockFs); });
it('calculates trivial complexity for simple scripts', async () => { mockFs.addFile('/test.sh', '#!/bin/bash\necho "hello"'); const complexity = await service.calculateComplexity('/test.sh'); expect(complexity).toBe('trivial'); });
it('calculates critical complexity for complex scripts', async () => { mockFs.addFile('/complex.sh', ` #!/bin/bash for i in {1..100}; do if [ "$i" -gt 50 ]; then function process() { echo "processing $1" | grep pattern & wait } trap "exit 1" SIGINT process $i fi done `); const complexity = await service.calculateComplexity('/complex.sh'); expect(complexity).toBe('critical'); });
it('finds npm scripts that call shell scripts', async () => { mockFs.addFile('/package.json', JSON.stringify({ scripts: { "build": "./build.sh", "deploy": "bash ./deploy.sh --prod", "test": "jest" } }));
const deps = await service.findNpmScriptDependencies('/');
expect(deps.get('build')).toEqual(['./build.sh']);
expect(deps.get('deploy')).toEqual(['./deploy.sh']);
expect(deps.has('test')).toBe(false);
}); }); ```
Example Output
```json
{
"projectName": "agent-buildkit",
"scripts": [
{
"path": "/scripts/sync-roadmaps.sh",
"type": "bash",
"lines": 87,
"complexity": "complex",
"recommendation": "
Estimated Effort
10 hours
- Implementation: 5 hours
- Tests: 4 hours
- Documentation: 1 hour
References
- Port: `/src/services/ports/project-health.port.ts:IScriptAuditorPort`
- Reference: `/src/services/domain/project-health/ProjectDiscoveryService.ts`
- Schemas: `/src/types/dto/project-health.schemas.ts` (ScriptAuditReportSchema)
- Context: User goal is "Zero shell scripts in production" (from ECOSYSTEM_MODERNIZATION_SUMMARY.md)
Labels
`enhancement`, `service-implementation`, `script-analysis`, `health-monitoring`