commit-analyzer 1.1.4 → 1.1.6
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/README.md +164 -82
- package/dist/main.ts +0 -0
- package/package.json +2 -1
- package/.claude/settings.local.json +0 -23
- package/commits.csv +0 -2
- package/csv-to-report-prompt.md +0 -97
- package/eslint.config.mts +0 -45
- package/prompt.md +0 -69
- package/src/1.domain/analysis.ts +0 -93
- package/src/1.domain/analyzed-commit.ts +0 -97
- package/src/1.domain/application-error.ts +0 -32
- package/src/1.domain/category.ts +0 -52
- package/src/1.domain/commit-analysis-service.ts +0 -92
- package/src/1.domain/commit-hash.ts +0 -40
- package/src/1.domain/commit.ts +0 -99
- package/src/1.domain/date-formatting-service.ts +0 -81
- package/src/1.domain/date-range.ts +0 -76
- package/src/1.domain/report-generation-service.ts +0 -443
- package/src/2.application/analyze-commits.usecase.ts +0 -307
- package/src/2.application/generate-report.usecase.ts +0 -209
- package/src/2.application/llm-service.ts +0 -54
- package/src/2.application/resume-analysis.usecase.ts +0 -123
- package/src/3.presentation/analysis-repository.interface.ts +0 -27
- package/src/3.presentation/analyze-command.ts +0 -128
- package/src/3.presentation/cli-application.ts +0 -278
- package/src/3.presentation/command-handler.interface.ts +0 -4
- package/src/3.presentation/commit-analysis-controller.ts +0 -101
- package/src/3.presentation/commit-repository.interface.ts +0 -47
- package/src/3.presentation/console-formatter.ts +0 -129
- package/src/3.presentation/progress-repository.interface.ts +0 -49
- package/src/3.presentation/report-command.ts +0 -50
- package/src/3.presentation/resume-command.ts +0 -59
- package/src/3.presentation/storage-repository.interface.ts +0 -33
- package/src/3.presentation/storage-service.interface.ts +0 -32
- package/src/3.presentation/version-control-service.interface.ts +0 -46
- package/src/4.infrastructure/cache-service.ts +0 -271
- package/src/4.infrastructure/cached-analysis-repository.ts +0 -46
- package/src/4.infrastructure/claude-llm-adapter.ts +0 -124
- package/src/4.infrastructure/csv-service.ts +0 -252
- package/src/4.infrastructure/file-storage-repository.ts +0 -108
- package/src/4.infrastructure/file-system-storage-adapter.ts +0 -87
- package/src/4.infrastructure/gemini-llm-adapter.ts +0 -46
- package/src/4.infrastructure/git-adapter.ts +0 -143
- package/src/4.infrastructure/git-commit-repository.ts +0 -85
- package/src/4.infrastructure/json-progress-tracker.ts +0 -182
- package/src/4.infrastructure/llm-adapter-factory.ts +0 -26
- package/src/4.infrastructure/llm-adapter.ts +0 -485
- package/src/4.infrastructure/llm-analysis-repository.ts +0 -38
- package/src/4.infrastructure/openai-llm-adapter.ts +0 -57
- package/src/di.ts +0 -109
- package/src/main.ts +0 -63
- package/src/utils/app-paths.ts +0 -36
- package/src/utils/concurrency.ts +0 -81
- package/src/utils.ts +0 -77
- package/tsconfig.json +0 -25
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { AnalyzedCommit } from "./analyzed-commit"
|
|
2
|
-
import { Category, CategoryType } from "./category"
|
|
3
|
-
import { DateRange } from "./date-range"
|
|
4
|
-
|
|
5
|
-
export type TimePeriod =
|
|
6
|
-
| "hourly"
|
|
7
|
-
| "daily"
|
|
8
|
-
| "weekly"
|
|
9
|
-
| "monthly"
|
|
10
|
-
| "quarterly"
|
|
11
|
-
| "yearly"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Statistics for analyzed commits
|
|
15
|
-
*/
|
|
16
|
-
export interface CommitStatistics {
|
|
17
|
-
totalCommits: number
|
|
18
|
-
yearRange: {
|
|
19
|
-
min: number
|
|
20
|
-
max: number
|
|
21
|
-
}
|
|
22
|
-
categoryBreakdown: Record<CategoryType, number>
|
|
23
|
-
yearlyBreakdown: Record<number, number>
|
|
24
|
-
largeChanges: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Domain service for report generation operations
|
|
29
|
-
*/
|
|
30
|
-
export class ReportGenerationService {
|
|
31
|
-
/**
|
|
32
|
-
* Generates statistics from analyzed commits
|
|
33
|
-
*/
|
|
34
|
-
generateStatistics(commits: AnalyzedCommit[]): CommitStatistics {
|
|
35
|
-
if (commits.length === 0) {
|
|
36
|
-
throw new Error("Cannot generate statistics from empty commit list")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const years = commits.map((c) => c.getYear())
|
|
40
|
-
|
|
41
|
-
const categoryBreakdown: Record<CategoryType, number> = {
|
|
42
|
-
tweak: 0,
|
|
43
|
-
feature: 0,
|
|
44
|
-
process: 0,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const yearlyBreakdown: Record<number, number> = {}
|
|
48
|
-
let largeChanges = 0
|
|
49
|
-
|
|
50
|
-
for (const commit of commits) {
|
|
51
|
-
// Category breakdown
|
|
52
|
-
const category = commit.getAnalysis().getCategory().getValue()
|
|
53
|
-
categoryBreakdown[category]++
|
|
54
|
-
|
|
55
|
-
// Yearly breakdown
|
|
56
|
-
const year = commit.getYear()
|
|
57
|
-
yearlyBreakdown[year] = (yearlyBreakdown[year] || 0) + 1
|
|
58
|
-
|
|
59
|
-
// Large changes count
|
|
60
|
-
if (commit.isLargeChange()) {
|
|
61
|
-
largeChanges++
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
totalCommits: commits.length,
|
|
67
|
-
yearRange: {
|
|
68
|
-
min: Math.min(...years),
|
|
69
|
-
max: Math.max(...years),
|
|
70
|
-
},
|
|
71
|
-
categoryBreakdown,
|
|
72
|
-
yearlyBreakdown,
|
|
73
|
-
largeChanges,
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Filters commits by date range
|
|
79
|
-
*/
|
|
80
|
-
filterByDateRange(
|
|
81
|
-
commits: AnalyzedCommit[],
|
|
82
|
-
dateRange: DateRange,
|
|
83
|
-
): AnalyzedCommit[] {
|
|
84
|
-
return commits.filter((commit) => dateRange.contains(commit.getDate()))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Filters commits by category
|
|
89
|
-
*/
|
|
90
|
-
filterByCategory(
|
|
91
|
-
commits: AnalyzedCommit[],
|
|
92
|
-
category: Category,
|
|
93
|
-
): AnalyzedCommit[] {
|
|
94
|
-
return commits.filter((commit) =>
|
|
95
|
-
commit.getAnalysis().getCategory().equals(category),
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Groups commits by year
|
|
101
|
-
*/
|
|
102
|
-
groupByYear(commits: AnalyzedCommit[]): Map<number, AnalyzedCommit[]> {
|
|
103
|
-
const grouped = new Map<number, AnalyzedCommit[]>()
|
|
104
|
-
|
|
105
|
-
for (const commit of commits) {
|
|
106
|
-
const year = commit.getYear()
|
|
107
|
-
if (!grouped.has(year)) {
|
|
108
|
-
grouped.set(year, [])
|
|
109
|
-
}
|
|
110
|
-
grouped.get(year)!.push(commit)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return grouped
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Groups commits by category
|
|
118
|
-
*/
|
|
119
|
-
groupByCategory(
|
|
120
|
-
commits: AnalyzedCommit[],
|
|
121
|
-
): Map<CategoryType, AnalyzedCommit[]> {
|
|
122
|
-
const grouped = new Map<CategoryType, AnalyzedCommit[]>()
|
|
123
|
-
|
|
124
|
-
for (const commit of commits) {
|
|
125
|
-
const category = commit.getAnalysis().getCategory().getValue()
|
|
126
|
-
if (!grouped.has(category)) {
|
|
127
|
-
grouped.set(category, [])
|
|
128
|
-
}
|
|
129
|
-
grouped.get(category)!.push(commit)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return grouped
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Sorts commits by date (newest first by default)
|
|
137
|
-
*/
|
|
138
|
-
sortByDate(
|
|
139
|
-
commits: AnalyzedCommit[],
|
|
140
|
-
ascending: boolean = false,
|
|
141
|
-
): AnalyzedCommit[] {
|
|
142
|
-
return commits.slice().sort((a, b) => {
|
|
143
|
-
const aDate = a.getDate().getTime()
|
|
144
|
-
const bDate = b.getDate().getTime()
|
|
145
|
-
return ascending ? aDate - bDate : bDate - aDate
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Gets the most significant commits (features and large changes)
|
|
151
|
-
*/
|
|
152
|
-
getSignificantCommits(commits: AnalyzedCommit[]): AnalyzedCommit[] {
|
|
153
|
-
return commits.filter(
|
|
154
|
-
(commit) =>
|
|
155
|
-
commit.getAnalysis().isFeatureAnalysis() || commit.isLargeChange(),
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Determines the appropriate time period for summaries based on date range
|
|
161
|
-
*/
|
|
162
|
-
determineTimePeriod(commits: AnalyzedCommit[]): TimePeriod {
|
|
163
|
-
if (commits.length === 0) return "yearly"
|
|
164
|
-
|
|
165
|
-
const dates = commits.map((c) => c.getDate())
|
|
166
|
-
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())))
|
|
167
|
-
const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())))
|
|
168
|
-
|
|
169
|
-
const diffInMilliseconds = maxDate.getTime() - minDate.getTime()
|
|
170
|
-
const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24)
|
|
171
|
-
|
|
172
|
-
if (diffInDays <= 1) return "hourly"
|
|
173
|
-
if (diffInDays <= 7) return "daily"
|
|
174
|
-
if (diffInDays <= 31) return "weekly"
|
|
175
|
-
if (diffInDays <= 93) return "monthly" // ~3 months
|
|
176
|
-
if (diffInDays <= 365) return "quarterly" // ~3 months
|
|
177
|
-
return "yearly"
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Groups commits by the appropriate time period
|
|
182
|
-
*/
|
|
183
|
-
groupByTimePeriod(
|
|
184
|
-
commits: AnalyzedCommit[],
|
|
185
|
-
period: TimePeriod,
|
|
186
|
-
): Map<string, AnalyzedCommit[]> {
|
|
187
|
-
const grouped = new Map<string, AnalyzedCommit[]>()
|
|
188
|
-
|
|
189
|
-
for (const commit of commits) {
|
|
190
|
-
const date = commit.getDate()
|
|
191
|
-
let key: string
|
|
192
|
-
|
|
193
|
-
switch (period) {
|
|
194
|
-
case "hourly":
|
|
195
|
-
key = this.formatHourlyKey(date)
|
|
196
|
-
break
|
|
197
|
-
case "daily":
|
|
198
|
-
key = this.formatDailyKey(date)
|
|
199
|
-
break
|
|
200
|
-
case "weekly":
|
|
201
|
-
key = this.formatWeeklyKey(date)
|
|
202
|
-
break
|
|
203
|
-
case "monthly":
|
|
204
|
-
key = this.formatMonthlyKey(date)
|
|
205
|
-
break
|
|
206
|
-
case "quarterly":
|
|
207
|
-
key = this.formatQuarterlyKey(date)
|
|
208
|
-
break
|
|
209
|
-
case "yearly":
|
|
210
|
-
default:
|
|
211
|
-
key = date.getFullYear().toString()
|
|
212
|
-
break
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (!grouped.has(key)) {
|
|
216
|
-
grouped.set(key, [])
|
|
217
|
-
}
|
|
218
|
-
grouped.get(key)!.push(commit)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return grouped
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private formatHourlyKey(date: Date): string {
|
|
225
|
-
const hour = date.getHours()
|
|
226
|
-
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
|
227
|
-
const ampm = hour < 12 ? "AM" : "PM"
|
|
228
|
-
return `${displayHour}:00 ${ampm}`
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private formatDailyKey(date: Date): string {
|
|
232
|
-
const year = date.getFullYear()
|
|
233
|
-
const month = date.getMonth() + 1
|
|
234
|
-
const day = date.getDate()
|
|
235
|
-
const hour = date.getHours()
|
|
236
|
-
|
|
237
|
-
if (hour < 12)
|
|
238
|
-
return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Morning`
|
|
239
|
-
if (hour < 17)
|
|
240
|
-
return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Afternoon`
|
|
241
|
-
return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} Evening`
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private formatWeeklyKey(date: Date): string {
|
|
245
|
-
const startOfWeek = new Date(date)
|
|
246
|
-
startOfWeek.setDate(date.getDate() - date.getDay())
|
|
247
|
-
const endOfWeek = new Date(startOfWeek)
|
|
248
|
-
endOfWeek.setDate(startOfWeek.getDate() + 6)
|
|
249
|
-
|
|
250
|
-
const formatDate = (d: Date) =>
|
|
251
|
-
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`
|
|
252
|
-
|
|
253
|
-
return `Week of ${formatDate(startOfWeek)} to ${formatDate(endOfWeek)}`
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private formatMonthlyKey(date: Date): string {
|
|
257
|
-
const months = [
|
|
258
|
-
"January",
|
|
259
|
-
"February",
|
|
260
|
-
"March",
|
|
261
|
-
"April",
|
|
262
|
-
"May",
|
|
263
|
-
"June",
|
|
264
|
-
"July",
|
|
265
|
-
"August",
|
|
266
|
-
"September",
|
|
267
|
-
"October",
|
|
268
|
-
"November",
|
|
269
|
-
"December",
|
|
270
|
-
]
|
|
271
|
-
return `${months[date.getMonth()]} ${date.getFullYear()}`
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private formatQuarterlyKey(date: Date): string {
|
|
275
|
-
const quarter = Math.floor(date.getMonth() / 3) + 1
|
|
276
|
-
return `Q${quarter} ${date.getFullYear()}`
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Converts analyzed commits to CSV string format for LLM consumption with enhanced context
|
|
281
|
-
*/
|
|
282
|
-
convertToCSVString(commits: AnalyzedCommit[]): string {
|
|
283
|
-
const header = "year,category,summary,description,commit_count,date_range"
|
|
284
|
-
|
|
285
|
-
// Group commits by year and category for context
|
|
286
|
-
const contextMap = new Map<string, { count: number; dates: Date[] }>()
|
|
287
|
-
|
|
288
|
-
const rows = commits.map((commit) => {
|
|
289
|
-
const analysis = commit.getAnalysis()
|
|
290
|
-
const key = `${commit.getYear()}-${analysis.getCategory().getValue()}`
|
|
291
|
-
|
|
292
|
-
if (!contextMap.has(key)) {
|
|
293
|
-
contextMap.set(key, { count: 0, dates: [] })
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const context = contextMap.get(key)!
|
|
297
|
-
context.count++
|
|
298
|
-
context.dates.push(commit.getDate())
|
|
299
|
-
|
|
300
|
-
const dateRange =
|
|
301
|
-
context.dates.length > 1
|
|
302
|
-
? `${this.formatDate(Math.min(...context.dates.map((d) => d.getTime())))} to ${this.formatDate(Math.max(...context.dates.map((d) => d.getTime())))}`
|
|
303
|
-
: this.formatDate(commit.getDate())
|
|
304
|
-
|
|
305
|
-
return [
|
|
306
|
-
commit.getYear().toString(),
|
|
307
|
-
this.escapeCsvField(analysis.getCategory().getValue()),
|
|
308
|
-
this.escapeCsvField(analysis.getSummary()),
|
|
309
|
-
this.escapeCsvField(analysis.getDescription()),
|
|
310
|
-
context.count.toString(),
|
|
311
|
-
this.escapeCsvField(dateRange),
|
|
312
|
-
].join(",")
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
return [header, ...rows].join("\n")
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
private formatDate(date: Date | number): string {
|
|
319
|
-
const d = new Date(date)
|
|
320
|
-
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Converts grouped commits to CSV with time period information and enhanced context
|
|
325
|
-
*/
|
|
326
|
-
convertGroupedToCSV(
|
|
327
|
-
groupedCommits: Map<string, AnalyzedCommit[]>,
|
|
328
|
-
period: string,
|
|
329
|
-
): string {
|
|
330
|
-
const header = `${period},category,summary,description,commit_count,similar_commits`
|
|
331
|
-
const rows: string[] = []
|
|
332
|
-
|
|
333
|
-
for (const [timePeriod, commits] of groupedCommits) {
|
|
334
|
-
// Group commits by category within the time period for context
|
|
335
|
-
const categoryGroups = new Map<string, AnalyzedCommit[]>()
|
|
336
|
-
|
|
337
|
-
for (const commit of commits) {
|
|
338
|
-
const category = commit.getAnalysis().getCategory().getValue()
|
|
339
|
-
if (!categoryGroups.has(category)) {
|
|
340
|
-
categoryGroups.set(category, [])
|
|
341
|
-
}
|
|
342
|
-
categoryGroups.get(category)!.push(commit)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Add context about similar commits in the same period and category
|
|
346
|
-
for (const commit of commits) {
|
|
347
|
-
const analysis = commit.getAnalysis()
|
|
348
|
-
const category = analysis.getCategory().getValue()
|
|
349
|
-
const similarCommits = categoryGroups.get(category)!
|
|
350
|
-
|
|
351
|
-
// Find similar summaries in the same category
|
|
352
|
-
const similarSummaries = similarCommits
|
|
353
|
-
.filter((c) => c !== commit)
|
|
354
|
-
.map((c) => c.getAnalysis().getSummary())
|
|
355
|
-
.filter((summary) =>
|
|
356
|
-
this.isSimilarSummary(analysis.getSummary(), summary),
|
|
357
|
-
)
|
|
358
|
-
.slice(0, 3) // Limit to 3 similar items
|
|
359
|
-
|
|
360
|
-
rows.push(
|
|
361
|
-
[
|
|
362
|
-
this.escapeCsvField(timePeriod),
|
|
363
|
-
this.escapeCsvField(category),
|
|
364
|
-
this.escapeCsvField(analysis.getSummary()),
|
|
365
|
-
this.escapeCsvField(analysis.getDescription()),
|
|
366
|
-
similarCommits.length.toString(),
|
|
367
|
-
this.escapeCsvField(similarSummaries.join("; ")),
|
|
368
|
-
].join(","),
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return [header, ...rows].join("\n")
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Determines if two summaries are similar based on common keywords
|
|
378
|
-
*/
|
|
379
|
-
private isSimilarSummary(summary1: string, summary2: string): boolean {
|
|
380
|
-
const keywords1 = this.extractKeywords(summary1)
|
|
381
|
-
const keywords2 = this.extractKeywords(summary2)
|
|
382
|
-
|
|
383
|
-
// Check if they share significant keywords (at least 2 common words)
|
|
384
|
-
const commonKeywords = keywords1.filter((word) => keywords2.includes(word))
|
|
385
|
-
return commonKeywords.length >= 2
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Extracts meaningful keywords from a summary for similarity detection
|
|
390
|
-
*/
|
|
391
|
-
private extractKeywords(summary: string): string[] {
|
|
392
|
-
// Remove common stopwords and extract meaningful terms
|
|
393
|
-
const stopwords = new Set([
|
|
394
|
-
"the",
|
|
395
|
-
"a",
|
|
396
|
-
"an",
|
|
397
|
-
"and",
|
|
398
|
-
"or",
|
|
399
|
-
"but",
|
|
400
|
-
"in",
|
|
401
|
-
"on",
|
|
402
|
-
"at",
|
|
403
|
-
"to",
|
|
404
|
-
"for",
|
|
405
|
-
"of",
|
|
406
|
-
"with",
|
|
407
|
-
"by",
|
|
408
|
-
"is",
|
|
409
|
-
"are",
|
|
410
|
-
"was",
|
|
411
|
-
"were",
|
|
412
|
-
"be",
|
|
413
|
-
"been",
|
|
414
|
-
"have",
|
|
415
|
-
"has",
|
|
416
|
-
"had",
|
|
417
|
-
"do",
|
|
418
|
-
"does",
|
|
419
|
-
"did",
|
|
420
|
-
"will",
|
|
421
|
-
"would",
|
|
422
|
-
"could",
|
|
423
|
-
"should",
|
|
424
|
-
])
|
|
425
|
-
|
|
426
|
-
return summary
|
|
427
|
-
.toLowerCase()
|
|
428
|
-
.replace(/[^\w\s]/g, " ")
|
|
429
|
-
.split(/\s+/)
|
|
430
|
-
.filter((word) => word.length > 2 && !stopwords.has(word))
|
|
431
|
-
.slice(0, 5) // Take first 5 meaningful words
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Escape CSV fields that contain commas, quotes, or newlines
|
|
436
|
-
*/
|
|
437
|
-
private escapeCsvField(field: string): string {
|
|
438
|
-
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
|
439
|
-
return `"${field.replace(/"/g, '""')}"`
|
|
440
|
-
}
|
|
441
|
-
return field
|
|
442
|
-
}
|
|
443
|
-
}
|