commit-analyzer 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -1
- package/README.md +33 -2
- package/commits.csv +2 -0
- package/eslint.config.mts +45 -0
- package/package.json +17 -9
- package/src/1.domain/analysis.ts +93 -0
- package/src/1.domain/analyzed-commit.ts +97 -0
- package/src/1.domain/application-error.ts +32 -0
- package/src/1.domain/category.ts +52 -0
- package/src/1.domain/commit-analysis-service.ts +92 -0
- package/src/1.domain/commit-hash.ts +40 -0
- package/src/1.domain/commit.ts +99 -0
- package/src/1.domain/date-formatting-service.ts +81 -0
- package/src/1.domain/date-range.ts +76 -0
- package/src/1.domain/report-generation-service.ts +292 -0
- package/src/2.application/analyze-commits.usecase.ts +307 -0
- package/src/2.application/generate-report.usecase.ts +204 -0
- package/src/2.application/llm-service.ts +54 -0
- package/src/2.application/resume-analysis.usecase.ts +123 -0
- package/src/3.presentation/analysis-repository.interface.ts +27 -0
- package/src/3.presentation/analyze-command.ts +128 -0
- package/src/3.presentation/cli-application.ts +255 -0
- package/src/3.presentation/command-handler.interface.ts +4 -0
- package/src/3.presentation/commit-analysis-controller.ts +101 -0
- package/src/3.presentation/commit-repository.interface.ts +47 -0
- package/src/3.presentation/console-formatter.ts +129 -0
- package/src/3.presentation/progress-repository.interface.ts +49 -0
- package/src/3.presentation/report-command.ts +50 -0
- package/src/3.presentation/resume-command.ts +59 -0
- package/src/3.presentation/storage-repository.interface.ts +33 -0
- package/src/3.presentation/storage-service.interface.ts +32 -0
- package/src/3.presentation/version-control-service.interface.ts +41 -0
- package/src/4.infrastructure/cache-service.ts +271 -0
- package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
- package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
- package/src/4.infrastructure/csv-service.ts +206 -0
- package/src/4.infrastructure/file-storage-repository.ts +108 -0
- package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
- package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
- package/src/4.infrastructure/git-adapter.ts +116 -0
- package/src/4.infrastructure/git-commit-repository.ts +85 -0
- package/src/4.infrastructure/json-progress-tracker.ts +182 -0
- package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
- package/src/4.infrastructure/llm-adapter.ts +455 -0
- package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
- package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
- package/src/di.ts +108 -0
- package/src/main.ts +63 -0
- package/src/utils/app-paths.ts +36 -0
- package/src/utils/concurrency.ts +81 -0
- package/src/utils.ts +77 -0
- package/tsconfig.json +7 -1
- package/src/cli.ts +0 -170
- package/src/csv-reader.ts +0 -180
- package/src/csv.ts +0 -40
- package/src/errors.ts +0 -49
- package/src/git.ts +0 -112
- package/src/index.ts +0 -395
- package/src/llm.ts +0 -396
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execSync } from "child_process"
|
|
2
|
+
|
|
3
|
+
import { LLMAdapter } from "./llm-adapter"
|
|
4
|
+
|
|
5
|
+
export class OpenAILLMAdapter extends LLMAdapter {
|
|
6
|
+
private static readonly MAX_PROMPT_LENGTH = parseInt(
|
|
7
|
+
process.env.OPENAI_MAX_PROMPT_LENGTH || "100000",
|
|
8
|
+
10,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
protected getMaxPromptLength(): number {
|
|
12
|
+
return OpenAILLMAdapter.MAX_PROMPT_LENGTH
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async detectAvailableModels(): Promise<string[]> {
|
|
16
|
+
try {
|
|
17
|
+
execSync("command -v codex", { stdio: "ignore" })
|
|
18
|
+
return ["codex -q", "openai"]
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async isAvailable(): Promise<boolean> {
|
|
25
|
+
const available = await this.detectAvailableModels()
|
|
26
|
+
return available.length > 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async executeModelCommand(prompt: string): Promise<string> {
|
|
30
|
+
const truncatedPrompt = this.truncatePrompt(prompt)
|
|
31
|
+
|
|
32
|
+
if (this.verbose) {
|
|
33
|
+
console.log(` - Prompt length: ${truncatedPrompt.length} characters`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const modelCommand = this.model || "codex -q"
|
|
37
|
+
|
|
38
|
+
// Handle codex -q quirk: requires prompt as command line argument
|
|
39
|
+
if (modelCommand.includes("-q")) {
|
|
40
|
+
const escapedPrompt = truncatedPrompt.replace(/"/g, '\\"')
|
|
41
|
+
return execSync(`${modelCommand} "${escapedPrompt}"`, {
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
+
timeout: LLMAdapter.DEFAULT_TIMEOUT,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback for non-quiet mode commands
|
|
49
|
+
return execSync(modelCommand, {
|
|
50
|
+
input: truncatedPrompt,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
timeout: LLMAdapter.DEFAULT_TIMEOUT,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
package/src/di.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CommitAnalysisService } from "@domain/commit-analysis-service"
|
|
2
|
+
import { DateFormattingService } from "@domain/date-formatting-service"
|
|
3
|
+
import { ReportGenerationService } from "@domain/report-generation-service"
|
|
4
|
+
|
|
5
|
+
import { AnalyzeCommitsUseCase } from "@app/analyze-commits.usecase"
|
|
6
|
+
import { GenerateReportUseCase } from "@app/generate-report.usecase"
|
|
7
|
+
import { ResumeAnalysisUseCase } from "@app/resume-analysis.usecase"
|
|
8
|
+
|
|
9
|
+
import { AnalyzeCommand } from "@presentation/analyze-command"
|
|
10
|
+
import { CLIApplication } from "@presentation/cli-application"
|
|
11
|
+
import { CommitAnalysisController } from "@presentation/commit-analysis-controller"
|
|
12
|
+
import { ReportCommand } from "@presentation/report-command"
|
|
13
|
+
import { ResumeCommand } from "@presentation/resume-command"
|
|
14
|
+
|
|
15
|
+
import { CacheService } from "@infra/cache-service"
|
|
16
|
+
import { CachedAnalysisRepository } from "@infra/cached-analysis-repository"
|
|
17
|
+
import { FileStorageRepository } from "@infra/file-storage-repository"
|
|
18
|
+
import { FileSystemStorageAdapter } from "@infra/file-system-storage-adapter"
|
|
19
|
+
import { GitAdapter } from "@infra/git-adapter"
|
|
20
|
+
import { GitCommitRepository } from "@infra/git-commit-repository"
|
|
21
|
+
import { JSONProgressTracker } from "@infra/json-progress-tracker"
|
|
22
|
+
import { LLMAdapterFactory } from "@infra/llm-adapter-factory"
|
|
23
|
+
import { LLMAnalysisRepository } from "@infra/llm-analysis-repository"
|
|
24
|
+
|
|
25
|
+
export interface DIContainerOptions {
|
|
26
|
+
llm?: string
|
|
27
|
+
noCache?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class DIContainer {
|
|
31
|
+
private readonly gitAdapter = new GitAdapter()
|
|
32
|
+
private readonly llmAdapter = LLMAdapterFactory.create(this.options?.llm)
|
|
33
|
+
private readonly storageAdapter = new FileSystemStorageAdapter()
|
|
34
|
+
private readonly cacheService = (() => {
|
|
35
|
+
const service = new CacheService()
|
|
36
|
+
if (this.options?.noCache) {
|
|
37
|
+
service.setCacheEnabled(false)
|
|
38
|
+
}
|
|
39
|
+
return service
|
|
40
|
+
})()
|
|
41
|
+
private readonly progressTracker = new JSONProgressTracker(
|
|
42
|
+
this.storageAdapter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
private readonly commitRepository = new GitCommitRepository(this.gitAdapter)
|
|
46
|
+
private readonly llmAnalysisRepository = new LLMAnalysisRepository(
|
|
47
|
+
this.llmAdapter,
|
|
48
|
+
)
|
|
49
|
+
private readonly analysisRepository = this.options?.noCache
|
|
50
|
+
? this.llmAnalysisRepository
|
|
51
|
+
: new CachedAnalysisRepository(
|
|
52
|
+
this.llmAnalysisRepository,
|
|
53
|
+
this.cacheService,
|
|
54
|
+
)
|
|
55
|
+
private readonly storageRepository = new FileStorageRepository(
|
|
56
|
+
this.storageAdapter,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
private readonly commitAnalysisService = new CommitAnalysisService(
|
|
60
|
+
this.commitRepository,
|
|
61
|
+
this.analysisRepository,
|
|
62
|
+
)
|
|
63
|
+
private readonly reportGenerationService = new ReportGenerationService()
|
|
64
|
+
private readonly dateFormattingService = new DateFormattingService()
|
|
65
|
+
|
|
66
|
+
private readonly analyzeCommitsUseCase = new AnalyzeCommitsUseCase(
|
|
67
|
+
this.commitAnalysisService,
|
|
68
|
+
this.progressTracker,
|
|
69
|
+
this.storageRepository,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
private readonly generateReportUseCase = new GenerateReportUseCase(
|
|
73
|
+
this.reportGenerationService,
|
|
74
|
+
this.storageRepository,
|
|
75
|
+
this.llmAdapter,
|
|
76
|
+
this.dateFormattingService,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
private readonly resumeAnalysisUseCase = new ResumeAnalysisUseCase(
|
|
80
|
+
this.progressTracker,
|
|
81
|
+
this.analyzeCommitsUseCase,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
private readonly analyzeCommand = new AnalyzeCommand(
|
|
85
|
+
this.analyzeCommitsUseCase,
|
|
86
|
+
this.commitAnalysisService,
|
|
87
|
+
this.commitRepository,
|
|
88
|
+
)
|
|
89
|
+
private readonly reportCommand = new ReportCommand(this.generateReportUseCase)
|
|
90
|
+
private readonly resumeCommand = new ResumeCommand(this.resumeAnalysisUseCase)
|
|
91
|
+
|
|
92
|
+
private readonly controller = new CommitAnalysisController(
|
|
93
|
+
this.analyzeCommand,
|
|
94
|
+
this.reportCommand,
|
|
95
|
+
this.resumeCommand,
|
|
96
|
+
this.progressTracker,
|
|
97
|
+
this.cacheService,
|
|
98
|
+
this.commitRepository,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
private readonly application = new CLIApplication(this.controller)
|
|
102
|
+
|
|
103
|
+
constructor(private readonly options?: DIContainerOptions) {}
|
|
104
|
+
|
|
105
|
+
getApplication(): CLIApplication {
|
|
106
|
+
return this.application
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { ApplicationError } from "@domain/application-error"
|
|
4
|
+
|
|
5
|
+
import { ConsoleFormatter } from "@presentation/console-formatter"
|
|
6
|
+
|
|
7
|
+
import { DIContainer } from "./di"
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
// Extract options from command line args before creating container
|
|
12
|
+
const llmOption = extractLLMOption(process.argv)
|
|
13
|
+
const noCacheOption = extractNoCacheOption(process.argv)
|
|
14
|
+
const container = new DIContainer({
|
|
15
|
+
llm: llmOption,
|
|
16
|
+
noCache: noCacheOption
|
|
17
|
+
})
|
|
18
|
+
const app = container.getApplication()
|
|
19
|
+
|
|
20
|
+
await app.run(process.argv)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof ApplicationError) {
|
|
23
|
+
ConsoleFormatter.logError(`[${error.code}]: ${error.message}`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
ConsoleFormatter.logError(`Unexpected error: ${error.message}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ConsoleFormatter.logError("Unknown error occurred")
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract the --llm option from command line arguments
|
|
39
|
+
* This is needed before creating the DI container
|
|
40
|
+
*/
|
|
41
|
+
function extractLLMOption(args: string[]): string | undefined {
|
|
42
|
+
const llmIndex = args.findIndex(arg => arg === '--llm')
|
|
43
|
+
if (llmIndex !== -1 && llmIndex + 1 < args.length) {
|
|
44
|
+
return args[llmIndex + 1]
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract the --no-cache option from command line arguments
|
|
51
|
+
* This is needed before creating the DI container
|
|
52
|
+
*/
|
|
53
|
+
function extractNoCacheOption(args: string[]): boolean {
|
|
54
|
+
return args.includes('--no-cache')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run the application if this file is executed directly
|
|
58
|
+
if (require.main === module) {
|
|
59
|
+
main().catch((error) => {
|
|
60
|
+
ConsoleFormatter.logError(`Failed to bootstrap application: ${error}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility for managing application data directory paths
|
|
5
|
+
*/
|
|
6
|
+
export class AppPaths {
|
|
7
|
+
private static readonly APP_DATA_DIR = ".commit-analyzer"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the application data directory path
|
|
11
|
+
*/
|
|
12
|
+
static getAppDataDir(baseDir: string = process.cwd()): string {
|
|
13
|
+
return path.join(baseDir, AppPaths.APP_DATA_DIR)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the cache directory path
|
|
18
|
+
*/
|
|
19
|
+
static getCacheDir(baseDir: string = process.cwd()): string {
|
|
20
|
+
return path.join(AppPaths.getAppDataDir(baseDir), "cache")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the progress file path
|
|
25
|
+
*/
|
|
26
|
+
static getProgressFilePath(baseDir: string = process.cwd()): string {
|
|
27
|
+
return path.join(AppPaths.getAppDataDir(baseDir), "progress.json")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get any file path within the app data directory
|
|
32
|
+
*/
|
|
33
|
+
static getAppDataFilePath(fileName: string, baseDir: string = process.cwd()): string {
|
|
34
|
+
return path.join(AppPaths.getAppDataDir(baseDir), fileName)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for managing concurrent operations with semaphore-like behavior
|
|
3
|
+
*/
|
|
4
|
+
export class ConcurrencyManager {
|
|
5
|
+
private running = 0
|
|
6
|
+
private queue: (() => void)[] = []
|
|
7
|
+
|
|
8
|
+
constructor(private readonly maxConcurrency: number) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute a function with concurrency control
|
|
12
|
+
*/
|
|
13
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const runTask = async () => {
|
|
16
|
+
this.running++
|
|
17
|
+
try {
|
|
18
|
+
const result = await fn()
|
|
19
|
+
resolve(result)
|
|
20
|
+
} catch (error) {
|
|
21
|
+
reject(error)
|
|
22
|
+
} finally {
|
|
23
|
+
this.running--
|
|
24
|
+
this.processQueue()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.running < this.maxConcurrency) {
|
|
29
|
+
runTask()
|
|
30
|
+
} else {
|
|
31
|
+
this.queue.push(runTask)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private processQueue(): void {
|
|
37
|
+
if (this.queue.length > 0 && this.running < this.maxConcurrency) {
|
|
38
|
+
const nextTask = this.queue.shift()!
|
|
39
|
+
nextTask()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Process items in parallel with controlled concurrency
|
|
46
|
+
*/
|
|
47
|
+
export async function processInParallel<T, R>(
|
|
48
|
+
items: T[],
|
|
49
|
+
processor: (item: T, index: number) => Promise<R>,
|
|
50
|
+
maxConcurrency: number = 5
|
|
51
|
+
): Promise<R[]> {
|
|
52
|
+
const manager = new ConcurrencyManager(maxConcurrency)
|
|
53
|
+
const promises = items.map((item, index) =>
|
|
54
|
+
manager.execute(() => processor(item, index))
|
|
55
|
+
)
|
|
56
|
+
return Promise.all(promises)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process items in batches with parallel processing within each batch
|
|
61
|
+
*/
|
|
62
|
+
export async function processInBatches<T, R>(
|
|
63
|
+
items: T[],
|
|
64
|
+
processor: (item: T, index: number) => Promise<R>,
|
|
65
|
+
batchSize: number = 10,
|
|
66
|
+
maxConcurrencyPerBatch: number = 5
|
|
67
|
+
): Promise<R[]> {
|
|
68
|
+
const results: R[] = []
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
71
|
+
const batch = items.slice(i, i + batchSize)
|
|
72
|
+
const batchResults = await processInParallel(
|
|
73
|
+
batch,
|
|
74
|
+
(item, batchIndex) => processor(item, i + batchIndex),
|
|
75
|
+
maxConcurrencyPerBatch
|
|
76
|
+
)
|
|
77
|
+
results.push(...batchResults)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results
|
|
81
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import readline from "readline"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common utility functions used across the application
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts error message from unknown error type
|
|
9
|
+
*/
|
|
10
|
+
export function getErrorMessage(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : "Unknown error"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a promise-based readline interface for user input
|
|
16
|
+
*/
|
|
17
|
+
export function createPromiseReadline(): {
|
|
18
|
+
question: (prompt: string) => Promise<string>
|
|
19
|
+
close: () => void
|
|
20
|
+
} {
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
question: (prompt: string): Promise<string> => {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
rl.question(prompt, (answer: string) => {
|
|
30
|
+
resolve(answer.trim())
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
close: () => rl.close(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sleep utility for async delays
|
|
40
|
+
*/
|
|
41
|
+
export function sleep(ms: number): Promise<void> {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats file size in human readable format
|
|
47
|
+
*/
|
|
48
|
+
export function formatFileSize(bytes: number): string {
|
|
49
|
+
const units = ["B", "KB", "MB", "GB"]
|
|
50
|
+
let size = bytes
|
|
51
|
+
let unitIndex = 0
|
|
52
|
+
|
|
53
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
54
|
+
size /= 1024
|
|
55
|
+
unitIndex++
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Truncates text to specified length with ellipsis
|
|
63
|
+
*/
|
|
64
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
65
|
+
if (text.length <= maxLength) {
|
|
66
|
+
return text
|
|
67
|
+
}
|
|
68
|
+
return text.substring(0, maxLength - 3) + "..."
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculates percentage with proper rounding
|
|
73
|
+
*/
|
|
74
|
+
export function calculatePercentage(part: number, total: number): number {
|
|
75
|
+
if (total === 0) return 0
|
|
76
|
+
return Math.round((part / total) * 100)
|
|
77
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"resolveJsonModule": true,
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"declarationMap": true,
|
|
15
|
-
"sourceMap": true
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"paths": {
|
|
17
|
+
"@domain/*": ["./src/1.domain/*"],
|
|
18
|
+
"@app/*": ["./src/2.application/*"],
|
|
19
|
+
"@presentation/*": ["./src/3.presentation/*"],
|
|
20
|
+
"@infra/*": ["./src/4.infrastructure/*"]
|
|
21
|
+
}
|
|
16
22
|
},
|
|
17
23
|
"include": ["src/**/*"],
|
|
18
24
|
"exclude": ["node_modules", "dist"]
|
package/src/cli.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { readFileSync, mkdirSync } from "fs"
|
|
2
|
-
import { Command } from "commander"
|
|
3
|
-
import { join, dirname } from "path"
|
|
4
|
-
|
|
5
|
-
export interface CLIOptions {
|
|
6
|
-
output?: string
|
|
7
|
-
outputDir?: string
|
|
8
|
-
file?: string
|
|
9
|
-
commits: string[]
|
|
10
|
-
author?: string
|
|
11
|
-
limit?: number
|
|
12
|
-
useDefaults: boolean
|
|
13
|
-
resume?: boolean
|
|
14
|
-
clear?: boolean
|
|
15
|
-
model?: string
|
|
16
|
-
report?: boolean
|
|
17
|
-
inputCsv?: string
|
|
18
|
-
verbose?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class CLIService {
|
|
22
|
-
static parseArguments(): CLIOptions {
|
|
23
|
-
const program = new Command()
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.name("commit-analyzer")
|
|
27
|
-
.description(
|
|
28
|
-
"Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
|
|
29
|
-
)
|
|
30
|
-
.version("1.0.2")
|
|
31
|
-
.option("-o, --output <file>", "Output CSV file (default: commits.csv)")
|
|
32
|
-
.option(
|
|
33
|
-
"--output-dir <dir>",
|
|
34
|
-
"Output directory for CSV and report files (default: current directory)",
|
|
35
|
-
)
|
|
36
|
-
.option(
|
|
37
|
-
"-f, --file <file>",
|
|
38
|
-
"Read commit hashes from file (one per line)",
|
|
39
|
-
)
|
|
40
|
-
.option(
|
|
41
|
-
"-a, --author <email>",
|
|
42
|
-
"Filter commits by author email (defaults to current user)",
|
|
43
|
-
)
|
|
44
|
-
.option(
|
|
45
|
-
"-l, --limit <number>",
|
|
46
|
-
"Limit number of commits to analyze",
|
|
47
|
-
parseInt,
|
|
48
|
-
)
|
|
49
|
-
.option("-r, --resume", "Resume from last checkpoint if available")
|
|
50
|
-
.option("-c, --clear", "Clear any existing progress checkpoint")
|
|
51
|
-
.option("-m, --model <model>", "LLM model to use (claude, gemini, codex)")
|
|
52
|
-
.option(
|
|
53
|
-
"--report",
|
|
54
|
-
"Generate condensed markdown report from existing CSV",
|
|
55
|
-
)
|
|
56
|
-
.option(
|
|
57
|
-
"--input-csv <file>",
|
|
58
|
-
"Input CSV file to read for report generation",
|
|
59
|
-
)
|
|
60
|
-
.option(
|
|
61
|
-
"-v, --verbose",
|
|
62
|
-
"Enable verbose logging (shows detailed error information)",
|
|
63
|
-
)
|
|
64
|
-
.argument(
|
|
65
|
-
"[commits...]",
|
|
66
|
-
"Commit hashes to analyze (if none provided, uses current user's commits)",
|
|
67
|
-
)
|
|
68
|
-
.parse()
|
|
69
|
-
|
|
70
|
-
const options = program.opts()
|
|
71
|
-
const args = program.args
|
|
72
|
-
|
|
73
|
-
let commits: string[] = []
|
|
74
|
-
let useDefaults = false
|
|
75
|
-
|
|
76
|
-
if (options.file) {
|
|
77
|
-
commits = this.readCommitsFromFile(options.file)
|
|
78
|
-
} else if (args.length > 0) {
|
|
79
|
-
commits = args
|
|
80
|
-
} else {
|
|
81
|
-
useDefaults = true
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
output:
|
|
86
|
-
options.output ||
|
|
87
|
-
CLIService.resolveOutputPath("commits.csv", options.outputDir),
|
|
88
|
-
outputDir: options.outputDir,
|
|
89
|
-
file: options.file,
|
|
90
|
-
commits,
|
|
91
|
-
author: options.author,
|
|
92
|
-
limit: options.limit,
|
|
93
|
-
useDefaults,
|
|
94
|
-
resume: options.resume,
|
|
95
|
-
clear: options.clear,
|
|
96
|
-
model: options.model,
|
|
97
|
-
report: options.report,
|
|
98
|
-
inputCsv: options.inputCsv,
|
|
99
|
-
verbose: options.verbose,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private static readCommitsFromFile(filename: string): string[] {
|
|
104
|
-
try {
|
|
105
|
-
const content = readFileSync(filename, "utf8")
|
|
106
|
-
return content
|
|
107
|
-
.split("\n")
|
|
108
|
-
.map((line) => line.trim())
|
|
109
|
-
.filter((line) => line.length > 0)
|
|
110
|
-
} catch (error) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`Failed to read commits from file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Resolve the full file path with optional output directory.
|
|
119
|
-
*/
|
|
120
|
-
static resolveOutputPath(filename: string, outputDir?: string): string {
|
|
121
|
-
if (outputDir) {
|
|
122
|
-
// Ensure output directory exists
|
|
123
|
-
try {
|
|
124
|
-
mkdirSync(outputDir, { recursive: true })
|
|
125
|
-
} catch (error) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Failed to create output directory ${outputDir}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
return join(outputDir, filename)
|
|
131
|
-
}
|
|
132
|
-
return filename
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
static showHelp(): void {
|
|
136
|
-
console.log(`
|
|
137
|
-
Usage: commit-analyzer [options] [commits...]
|
|
138
|
-
|
|
139
|
-
Analyze git commits and generate categorized summaries using LLM.
|
|
140
|
-
If no commits are specified, analyzes all commits authored by the current user.
|
|
141
|
-
|
|
142
|
-
Options:
|
|
143
|
-
-o, --output <file> Output file (default: commits.csv for analysis, report.md for reports)
|
|
144
|
-
--output-dir <dir> Output directory for CSV and report files (default: current directory)
|
|
145
|
-
-f, --file <file> Read commit hashes from file (one per line)
|
|
146
|
-
-a, --author <email> Filter commits by author email (defaults to current user)
|
|
147
|
-
-l, --limit <number> Limit number of commits to analyze
|
|
148
|
-
-r, --resume Resume from last checkpoint if available
|
|
149
|
-
-c, --clear Clear any existing progress checkpoint
|
|
150
|
-
--report Generate condensed markdown report from existing CSV
|
|
151
|
-
--input-csv <file> Input CSV file to read for report generation
|
|
152
|
-
-v, --verbose Enable verbose logging (shows detailed error information)
|
|
153
|
-
-h, --help Display help for command
|
|
154
|
-
-V, --version Display version number
|
|
155
|
-
|
|
156
|
-
Examples:
|
|
157
|
-
commit-analyzer # Analyze your authored commits
|
|
158
|
-
commit-analyzer --limit 10 # Analyze your last 10 commits
|
|
159
|
-
commit-analyzer --author user@example.com # Analyze specific user's commits
|
|
160
|
-
commit-analyzer abc123 def456 ghi789 # Analyze specific commits
|
|
161
|
-
commit-analyzer --file commits.txt # Read commits from file
|
|
162
|
-
commit-analyzer --output analysis.csv --limit 20 # Analyze last 20 commits to custom file
|
|
163
|
-
commit-analyzer --resume # Resume from last checkpoint
|
|
164
|
-
commit-analyzer --clear # Clear checkpoint and start fresh
|
|
165
|
-
commit-analyzer --report # Analyze commits, generate CSV, then generate report
|
|
166
|
-
commit-analyzer --input-csv data.csv --report # Skip analysis, generate report from existing CSV
|
|
167
|
-
commit-analyzer --report -o custom-report.md # Analyze commits, generate CSV, then generate custom report
|
|
168
|
-
`)
|
|
169
|
-
}
|
|
170
|
-
}
|