@mcarvin/smart-diff 1.0.2 → 1.0.4

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.
@@ -0,0 +1,666 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('node:path'), require('simple-git')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'node:path', 'simple-git'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.jsTemplate = {}, global.node_path, global.simpleGit));
5
+ })(this, (function (exports, node_path, simpleGit) { 'use strict';
6
+
7
+ /******************************************************************************
8
+ Copyright (c) Microsoft Corporation.
9
+
10
+ Permission to use, copy, modify, and/or distribute this software for any
11
+ purpose with or without fee is hereby granted.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
14
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
15
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
16
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
17
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
18
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
19
+ PERFORMANCE OF THIS SOFTWARE.
20
+ ***************************************************************************** */
21
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
22
+
23
+
24
+ function __awaiter(thisArg, _arguments, P, generator) {
25
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
26
+ return new (P || (P = Promise))(function (resolve, reject) {
27
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
28
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
29
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
30
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
31
+ });
32
+ }
33
+
34
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
35
+ var e = new Error(message);
36
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
37
+ };
38
+
39
+ function resolveLlmBaseUrl() {
40
+ var _a, _b, _c;
41
+ return (_b = (_a = process.env.LLM_BASE_URL) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_BASE_URL) === null || _c === void 0 ? void 0 : _c.trim();
42
+ }
43
+ function parseHeaderJsonObject(raw) {
44
+ const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
45
+ if (!trimmed)
46
+ return {};
47
+ try {
48
+ const parsed = JSON.parse(trimmed);
49
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
50
+ return {};
51
+ }
52
+ const out = {};
53
+ for (const [key, value] of Object.entries(parsed)) {
54
+ if (typeof value === 'string' && value.length > 0) {
55
+ out[key] = value;
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ catch (_a) {
61
+ return {};
62
+ }
63
+ }
64
+ function parseLlmDefaultHeadersFromEnv() {
65
+ const base = parseHeaderJsonObject(process.env.OPENAI_DEFAULT_HEADERS);
66
+ const override = parseHeaderJsonObject(process.env.LLM_DEFAULT_HEADERS);
67
+ const merged = Object.assign(Object.assign({}, base), override);
68
+ return Object.keys(merged).length > 0 ? merged : undefined;
69
+ }
70
+ function findAuthorizationHeaderName(headers) {
71
+ return Object.keys(headers).find((k) => k.toLowerCase() === 'authorization');
72
+ }
73
+ function stripBearerPrefix(value) {
74
+ var _a;
75
+ const trimmed = value.trim();
76
+ const match = /^Bearer\s+(\S+)/i.exec(trimmed);
77
+ return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : trimmed;
78
+ }
79
+ function splitPromotableAuthorizationFromHeaders(headers) {
80
+ const authName = findAuthorizationHeaderName(headers);
81
+ if (!authName) {
82
+ return { defaultHeaders: headers };
83
+ }
84
+ const raw = headers[authName];
85
+ if (!raw) {
86
+ return { defaultHeaders: headers };
87
+ }
88
+ const token = stripBearerPrefix(raw);
89
+ const looksBearer = /^Bearer\s+\S+/i.test(raw.trim());
90
+ const looksOpenAiKey = /^sk-/i.test(token);
91
+ if (!looksBearer && !looksOpenAiKey) {
92
+ return { defaultHeaders: headers };
93
+ }
94
+ const next = Object.assign({}, headers);
95
+ delete next[authName];
96
+ return { defaultHeaders: next, apiKeyFromAuthHeader: token };
97
+ }
98
+ function shouldUseLlmGateway() {
99
+ var _a, _b, _c;
100
+ const apiKey = (_b = (_a = process.env.LLM_API_KEY) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_API_KEY) === null || _c === void 0 ? void 0 : _c.trim();
101
+ if (apiKey)
102
+ return true;
103
+ if (resolveLlmBaseUrl())
104
+ return true;
105
+ const jsonHeaders = parseLlmDefaultHeadersFromEnv();
106
+ if (jsonHeaders && Object.keys(jsonHeaders).length > 0)
107
+ return true;
108
+ return false;
109
+ }
110
+ function resolveOpenAiLikeClientInit() {
111
+ var _a, _b, _c, _d, _e;
112
+ const baseURL = resolveLlmBaseUrl();
113
+ const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
114
+ const envApiKey = (_e = (_c = (_b = process.env.LLM_API_KEY) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : (_d = process.env.OPENAI_API_KEY) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : '';
115
+ let defaultHeaders;
116
+ let apiKey = envApiKey;
117
+ if (apiKey.length === 0) {
118
+ const split = splitPromotableAuthorizationFromHeaders(mergedHeaders);
119
+ if (split.apiKeyFromAuthHeader) {
120
+ apiKey = split.apiKeyFromAuthHeader;
121
+ }
122
+ defaultHeaders = Object.keys(split.defaultHeaders).length > 0 ? split.defaultHeaders : undefined;
123
+ }
124
+ else {
125
+ defaultHeaders = Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
126
+ }
127
+ return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : 'unused' }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
128
+ }
129
+ function createOpenAiLikeClient() {
130
+ return __awaiter(this, void 0, void 0, function* () {
131
+ const { default: OpenAI } = yield import('openai');
132
+ return new OpenAI(resolveOpenAiLikeClientInit());
133
+ });
134
+ }
135
+
136
+ const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
137
+ function resolveLlmMaxDiffChars(cliOverride) {
138
+ var _a;
139
+ if (cliOverride !== undefined && Number.isFinite(cliOverride) && cliOverride > 0) {
140
+ return Math.trunc(cliOverride);
141
+ }
142
+ const raw = (_a = process.env.LLM_MAX_DIFF_CHARS) === null || _a === void 0 ? void 0 : _a.trim();
143
+ if (raw) {
144
+ const parsed = Number.parseInt(raw, 10);
145
+ if (Number.isFinite(parsed) && parsed > 0) {
146
+ return parsed;
147
+ }
148
+ }
149
+ return DEFAULT_LLM_MAX_DIFF_CHARS;
150
+ }
151
+ function truncateUnifiedDiffForLlm(diffText, maxChars) {
152
+ if (diffText.length <= maxChars) {
153
+ return diffText;
154
+ }
155
+ const marker = `\n\n--- TRUNCATED: unified diff was ${diffText.length} characters; only the first ${maxChars} were sent. Narrow the ref range, adjust commit/path filters, or raise maxDiffChars / LLM_MAX_DIFF_CHARS only if your model context allows. ---\n`;
156
+ return diffText.slice(0, maxChars) + marker;
157
+ }
158
+ const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
159
+ You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
160
+ Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
161
+ Produce a concise, developer-focused summary in Markdown.
162
+ Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
163
+ Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
164
+ If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
165
+ const LLM_GATEWAY_REQUIRED_MESSAGE = 'No LLM gateway configured. Set OPENAI_API_KEY or LLM_API_KEY, and/or LLM_BASE_URL or OPENAI_BASE_URL, ' +
166
+ 'and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. ' +
167
+ 'Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.';
168
+ function generateSummary(diffText, fileNames, commits, flags, openAiClientProvider, diffSummary) {
169
+ return __awaiter(this, void 0, void 0, function* () {
170
+ var _a, _b;
171
+ if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
172
+ throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
173
+ }
174
+ const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
175
+ const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
176
+ const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
177
+ return callOpenAi(userContent, (_a = flags.model) !== null && _a !== void 0 ? _a : 'gpt-4o-mini', (_b = flags.systemPrompt) !== null && _b !== void 0 ? _b : DEFAULT_GIT_DIFF_SYSTEM_PROMPT, openAiClientProvider !== null && openAiClientProvider !== void 0 ? openAiClientProvider : (() => __awaiter(this, void 0, void 0, function* () { return createOpenAiLikeClient(); })));
178
+ });
179
+ }
180
+ function formatRegexFilterLines(flags) {
181
+ var _a, _b;
182
+ const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : []).map((s) => s.trim()).filter(Boolean);
183
+ const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : []).map((s) => s.trim()).filter(Boolean);
184
+ const incLine = includes.length > 0
185
+ ? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(', ')}\n`
186
+ : '';
187
+ const excLine = excludes.length > 0
188
+ ? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(', ')}\n`
189
+ : '';
190
+ if (!incLine && !excLine) {
191
+ return 'Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n';
192
+ }
193
+ return (`${incLine}${excLine}` +
194
+ 'Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n');
195
+ }
196
+ function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
197
+ var _a, _b;
198
+ const from = flags.from;
199
+ const to = (_a = flags.to) !== null && _a !== void 0 ? _a : 'HEAD';
200
+ const team = (_b = flags.team) === null || _b === void 0 ? void 0 : _b.trim();
201
+ const ts = new Date().toISOString();
202
+ const teamLine = team ? `Team: ${team}\n` : '';
203
+ const filterBlock = formatRegexFilterLines(flags);
204
+ const commitBlock = commits.length > 0
205
+ ? commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, ' ')}`).join('\n')
206
+ : '- (no commits in range after filtering)';
207
+ const pathsBlock = fileNames.length > 0 ? fileNames.join('\n') : '(no paths in diff scope)';
208
+ const structuredDiffSection = diffSummary
209
+ ? `=== Structured git context (JSON summary) ===\n${JSON.stringify(diffSummary, null, 2)}\n\n`
210
+ : '';
211
+ return (`${teamLine}` +
212
+ `Date: ${ts}\n\n` +
213
+ `Git refs: ${from}..${to}\n` +
214
+ filterBlock +
215
+ '\n' +
216
+ '=== Included commits (subject lines) ===\n' +
217
+ `${commitBlock}\n\n` +
218
+ '=== Changed paths ===\n' +
219
+ `${pathsBlock}\n\n` +
220
+ structuredDiffSection +
221
+ '=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n' +
222
+ diffText);
223
+ }
224
+ function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
225
+ return __awaiter(this, void 0, void 0, function* () {
226
+ var _a, _b, _c, _d, _e, _f;
227
+ const client = yield openAiClientProvider();
228
+ const maxTokensRaw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
229
+ const parsed = maxTokensRaw !== undefined ? Number.parseInt(maxTokensRaw, 10) : 4000;
230
+ const maxTokens = Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
231
+ const response = yield client.chat.completions.create({
232
+ model,
233
+ messages: [
234
+ {
235
+ role: 'system',
236
+ content: systemPrompt,
237
+ },
238
+ {
239
+ role: 'user',
240
+ content: userContent,
241
+ },
242
+ ],
243
+ temperature: 0.2,
244
+ max_tokens: maxTokens,
245
+ });
246
+ const typedResponse = response;
247
+ const text = (_f = (_e = (_d = (_c = (_b = typedResponse.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '';
248
+ return text.length > 0 ? text : 'No summary generated by OpenAI.';
249
+ });
250
+ }
251
+
252
+ function createGitClient(cwd = process.cwd()) {
253
+ return simpleGit.simpleGit(cwd);
254
+ }
255
+ function getCommits(git, from, to) {
256
+ return __awaiter(this, void 0, void 0, function* () {
257
+ const logResult = yield git.log({ from, to });
258
+ return logResult.all;
259
+ });
260
+ }
261
+ function compileRegex(pattern, label) {
262
+ try {
263
+ return new RegExp(pattern, 'i');
264
+ }
265
+ catch (_a) {
266
+ throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
267
+ }
268
+ }
269
+ function commitMessagePassesFilters(message, includeRes, excludeRes) {
270
+ for (const ex of excludeRes) {
271
+ if (ex.test(message))
272
+ return false;
273
+ }
274
+ if (includeRes.length > 0 && !includeRes.some((r) => r.test(message)))
275
+ return false;
276
+ return true;
277
+ }
278
+ function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
279
+ const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
280
+ const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
281
+ const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
282
+ const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
283
+ return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
284
+ }
285
+ function getRepoRoot(git) {
286
+ return __awaiter(this, void 0, void 0, function* () {
287
+ const root = yield git.revparse(['--show-toplevel']);
288
+ return root.trim();
289
+ });
290
+ }
291
+ function normalizeRepoRelativePath(p) {
292
+ const trimmed = p.trim().replace(/\\/g, '/');
293
+ const noLeading = trimmed.replace(/^\/+/, '');
294
+ const noTrailingSlash = noLeading.replace(/\/+$/, '');
295
+ return noTrailingSlash.length > 0 ? noTrailingSlash : '.';
296
+ }
297
+ function assertPathUnderRepo(repoRoot, userPath) {
298
+ const abs = node_path.resolve(repoRoot, userPath);
299
+ const rel = node_path.relative(repoRoot, abs);
300
+ if (rel === '..') {
301
+ throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
302
+ }
303
+ const segments = rel.split(/[/\\]/);
304
+ if (segments.includes('..')) {
305
+ throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
306
+ }
307
+ }
308
+ function buildDiffPathspecs(repoRoot, pathFilter) {
309
+ var _a, _b, _c, _d;
310
+ const includeRaw = (_b = (_a = pathFilter === null || pathFilter === void 0 ? void 0 : pathFilter.includeFolders) === null || _a === void 0 ? void 0 : _a.filter((p) => p.trim().length > 0)) !== null && _b !== void 0 ? _b : [];
311
+ const excludeRaw = (_d = (_c = pathFilter === null || pathFilter === void 0 ? void 0 : pathFilter.excludeFolders) === null || _c === void 0 ? void 0 : _c.filter((p) => p.trim().length > 0)) !== null && _d !== void 0 ? _d : [];
312
+ const includes = includeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
313
+ const excludes = excludeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
314
+ const toValidate = includes.length > 0 ? includes : ['.'];
315
+ for (const inc of toValidate) {
316
+ assertPathUnderRepo(repoRoot, inc);
317
+ }
318
+ for (const exc of excludes) {
319
+ assertPathUnderRepo(repoRoot, exc);
320
+ }
321
+ const specs = [];
322
+ if (includes.length === 0) {
323
+ specs.push('.');
324
+ }
325
+ else {
326
+ for (const inc of includes) {
327
+ specs.push(inc);
328
+ }
329
+ }
330
+ for (const exc of excludes) {
331
+ specs.push(`:(exclude)${exc}`);
332
+ }
333
+ return specs;
334
+ }
335
+ function getDiffPathContext(git, pathFilter, repoRootOverride) {
336
+ return __awaiter(this, void 0, void 0, function* () {
337
+ const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
338
+ const specs = buildDiffPathspecs(repoRoot, pathFilter);
339
+ return { repoRoot, specs };
340
+ });
341
+ }
342
+ function getDiff(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
343
+ return __awaiter(this, void 0, void 0, function* () {
344
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
345
+ if (!filterByCommits) {
346
+ return git.diff([`${from}..${to}`, '--', ...specs]);
347
+ }
348
+ const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, '--', ...specs])));
349
+ return patches.filter(Boolean).join('\n');
350
+ });
351
+ }
352
+ function getDiffSummary(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
353
+ return __awaiter(this, void 0, void 0, function* () {
354
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
355
+ if (!filterByCommits) {
356
+ const [numOutput, nameOutput] = yield Promise.all([
357
+ git.diff(['--numstat', `${from}..${to}`, '--', ...specs]),
358
+ git.diff(['--name-status', `${from}..${to}`, '--', ...specs]),
359
+ ]);
360
+ return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
361
+ }
362
+ const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
363
+ const range = `${c.hash}^!`;
364
+ const [numOutput, nameOutput] = yield Promise.all([
365
+ git.diff(['--numstat', range, '--', ...specs]),
366
+ git.diff(['--name-status', range, '--', ...specs]),
367
+ ]);
368
+ return { numOutput, nameOutput };
369
+ })));
370
+ const nameJoined = pairs
371
+ .map((p) => p.nameOutput)
372
+ .filter(Boolean)
373
+ .join('\n');
374
+ const numJoined = pairs
375
+ .map((p) => p.numOutput)
376
+ .filter(Boolean)
377
+ .join('\n');
378
+ return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
379
+ });
380
+ }
381
+ function getChangedFiles(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
382
+ return __awaiter(this, void 0, void 0, function* () {
383
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
384
+ if (!filterByCommits) {
385
+ const output = yield git.diff(['--name-only', `${from}..${to}`, '--', ...specs]);
386
+ return output
387
+ .split(/\r?\n/)
388
+ .map((f) => f.trim())
389
+ .filter(Boolean);
390
+ }
391
+ const fileSet = new Set();
392
+ yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
393
+ const output = yield git.show(['--name-only', '--pretty=format:', c.hash, '--', ...specs]);
394
+ output
395
+ .split(/\r?\n/)
396
+ .map((f) => f.trim())
397
+ .filter(Boolean)
398
+ .forEach((f) => fileSet.add(f));
399
+ })));
400
+ return Array.from(fileSet);
401
+ });
402
+ }
403
+ const GIT_STATUS_BY_FIRST_CHAR = {
404
+ A: 'added',
405
+ D: 'deleted',
406
+ R: 'renamed',
407
+ C: 'copied',
408
+ T: 'type-changed',
409
+ M: 'modified',
410
+ };
411
+ function mapGitStatus(statusCode) {
412
+ var _a;
413
+ return (_a = GIT_STATUS_BY_FIRST_CHAR[statusCode.charAt(0)]) !== null && _a !== void 0 ? _a : 'unknown';
414
+ }
415
+ function mergeStatus(existing, next) {
416
+ if (existing === next)
417
+ return existing;
418
+ const precedence = ['deleted', 'added', 'renamed', 'copied', 'type-changed', 'modified', 'unknown'];
419
+ return precedence.indexOf(existing) <= precedence.indexOf(next) ? existing : next;
420
+ }
421
+ function parseNameStatusLine(line) {
422
+ var _a;
423
+ const parts = line.split('\t');
424
+ let entry = null;
425
+ if (parts.length >= 2) {
426
+ const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
427
+ const status = mapGitStatus(statusToken);
428
+ const isRenameOrCopy = statusToken.startsWith('R') || statusToken.startsWith('C');
429
+ if (isRenameOrCopy && parts.length >= 3) {
430
+ const oldPath = parts[1];
431
+ const newPath = parts[2];
432
+ if (oldPath !== undefined && newPath !== undefined) {
433
+ entry = { path: newPath, status, oldPath };
434
+ }
435
+ }
436
+ else if (!isRenameOrCopy) {
437
+ const pathOnly = parts[1];
438
+ if (pathOnly !== undefined) {
439
+ entry = { path: pathOnly, status };
440
+ }
441
+ }
442
+ }
443
+ return entry;
444
+ }
445
+ function parseNameStatusLines(nameStatusOutput) {
446
+ const entries = [];
447
+ for (const rawLine of nameStatusOutput.split(/\r?\n/)) {
448
+ const line = rawLine.trim();
449
+ if (!line)
450
+ continue;
451
+ const entry = parseNameStatusLine(line);
452
+ if (entry)
453
+ entries.push(entry);
454
+ }
455
+ return entries;
456
+ }
457
+ function mergeNameEntriesByPath(entries) {
458
+ var _a;
459
+ const byPath = new Map();
460
+ for (const e of entries) {
461
+ const existing = byPath.get(e.path);
462
+ if (!existing) {
463
+ byPath.set(e.path, Object.assign({}, e));
464
+ }
465
+ else {
466
+ existing.status = mergeStatus(existing.status, e.status);
467
+ if (e.oldPath) {
468
+ existing.oldPath = (_a = existing.oldPath) !== null && _a !== void 0 ? _a : e.oldPath;
469
+ }
470
+ }
471
+ }
472
+ return byPath;
473
+ }
474
+ function numStatPathToLookupKey(pathField) {
475
+ const brace = /^(.*)\{(.+) => (.+)\}$/.exec(pathField);
476
+ if (!brace) {
477
+ return pathField;
478
+ }
479
+ const dirRaw = brace[1];
480
+ const toSeg = brace[3].trim();
481
+ return `${dirRaw}${toSeg}`;
482
+ }
483
+ function parseNumStatLine(line) {
484
+ var _a, _b;
485
+ const parts = line.split('\t');
486
+ if (parts.length < 3)
487
+ return null;
488
+ const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
489
+ const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : '';
490
+ const pathField = parts.slice(2).join('\t');
491
+ const additions = addStr !== '-' ? Number.parseInt(addStr, 10) || 0 : 0;
492
+ const deletions = delStr !== '-' ? Number.parseInt(delStr, 10) || 0 : 0;
493
+ const key = numStatPathToLookupKey(pathField);
494
+ return { key, additions, deletions };
495
+ }
496
+ function accumulateNumStat(numStatOutput, into) {
497
+ var _a;
498
+ for (const rawLine of numStatOutput.split(/\r?\n/)) {
499
+ const line = rawLine.trim();
500
+ if (!line)
501
+ continue;
502
+ const parsed = parseNumStatLine(line);
503
+ if (!parsed)
504
+ continue;
505
+ const prev = (_a = into.get(parsed.key)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
506
+ into.set(parsed.key, { additions: prev.additions + parsed.additions, deletions: prev.deletions + parsed.deletions });
507
+ }
508
+ }
509
+ const STATUS_TO_SYNTHETIC_PREFIX = {
510
+ added: 'A',
511
+ deleted: 'D',
512
+ renamed: 'R100',
513
+ copied: 'C100',
514
+ 'type-changed': 'T',
515
+ modified: 'M',
516
+ unknown: 'X',
517
+ };
518
+ function diffStatusToSyntheticPrefix(status) {
519
+ return STATUS_TO_SYNTHETIC_PREFIX[status];
520
+ }
521
+ function buildSyntheticDiffLine(meta, counts) {
522
+ const prefix = diffStatusToSyntheticPrefix(meta.status);
523
+ if (meta.oldPath) {
524
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${meta.path}`;
525
+ }
526
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
527
+ }
528
+ function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
529
+ var _a;
530
+ const numMap = new Map();
531
+ accumulateNumStat(numStatOutput, numMap);
532
+ const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
533
+ const syntheticLines = [];
534
+ for (const [path, meta] of mergedName) {
535
+ const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
536
+ syntheticLines.push(buildSyntheticDiffLine(meta, counts));
537
+ }
538
+ return parseDiffSummary(syntheticLines.join('\n'));
539
+ }
540
+ function parseTabDiffSummaryLine(line) {
541
+ var _a;
542
+ const parts = line.split('\t');
543
+ if (parts.length < 3)
544
+ return null;
545
+ const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : '';
546
+ const status = mapGitStatus(statusToken);
547
+ const add0 = parts[0];
548
+ const del0 = parts[1];
549
+ const additions = add0 && add0 !== '-' ? Number.parseInt(add0, 10) || 0 : 0;
550
+ const deletions = del0 && del0 !== '-' ? Number.parseInt(del0, 10) || 0 : 0;
551
+ if (parts.length === 3) {
552
+ return { status, additions, deletions, newPath: parts[2] };
553
+ }
554
+ if (parts.length === 4) {
555
+ return { status, additions, deletions, oldPath: parts[2], newPath: parts[3] };
556
+ }
557
+ return null;
558
+ }
559
+ function mergeParsedDiffSummaryLine(fileMap, p) {
560
+ var _a, _b;
561
+ const { newPath, status, additions, deletions, oldPath } = p;
562
+ const existing = fileMap.get(newPath);
563
+ if (existing) {
564
+ existing.additions += additions;
565
+ existing.deletions += deletions;
566
+ existing.status = mergeStatus(existing.status, status);
567
+ if (oldPath)
568
+ existing.oldPath = (_a = existing.oldPath) !== null && _a !== void 0 ? _a : oldPath;
569
+ existing.newPath = (_b = existing.newPath) !== null && _b !== void 0 ? _b : newPath;
570
+ }
571
+ else {
572
+ fileMap.set(newPath, {
573
+ path: newPath,
574
+ status,
575
+ additions,
576
+ deletions,
577
+ oldPath,
578
+ newPath: oldPath ? newPath : undefined,
579
+ });
580
+ }
581
+ }
582
+ function parseDiffSummary(diffOutput) {
583
+ const fileMap = new Map();
584
+ for (const rawLine of diffOutput.split(/\r?\n/)) {
585
+ const line = rawLine.trim();
586
+ if (!line)
587
+ continue;
588
+ const parsed = parseTabDiffSummaryLine(line);
589
+ if (parsed)
590
+ mergeParsedDiffSummaryLine(fileMap, parsed);
591
+ }
592
+ const files = Array.from(fileMap.values());
593
+ return {
594
+ files,
595
+ totalFiles: files.length,
596
+ totalAdditions: files.reduce((sum, file) => sum + file.additions, 0),
597
+ totalDeletions: files.reduce((sum, file) => sum + file.deletions, 0),
598
+ };
599
+ }
600
+
601
+ function hasNonEmptyTrimmed(arr) {
602
+ return (arr !== null && arr !== void 0 ? arr : []).some((s) => s.trim().length > 0);
603
+ }
604
+ function shouldFilterByCommits(allCommits, filtered, opts) {
605
+ if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) || hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
606
+ return true;
607
+ }
608
+ return filtered.length !== allCommits.length;
609
+ }
610
+ function summarizeGitDiff(options) {
611
+ return __awaiter(this, void 0, void 0, function* () {
612
+ var _a, _b;
613
+ const git = (_a = options.git) !== null && _a !== void 0 ? _a : createGitClient(options.cwd);
614
+ const from = options.from;
615
+ const to = (_b = options.to) !== null && _b !== void 0 ? _b : 'HEAD';
616
+ const pathFilter = hasNonEmptyTrimmed(options.includeFolders) || hasNonEmptyTrimmed(options.excludeFolders)
617
+ ? {
618
+ includeFolders: options.includeFolders,
619
+ excludeFolders: options.excludeFolders,
620
+ }
621
+ : undefined;
622
+ const allCommits = yield getCommits(git, from, to);
623
+ const filteredCommits = filterCommitsByMessageRegexes(allCommits, options.commitMessageIncludeRegexes, options.commitMessageExcludeRegexes);
624
+ const filterByCommits = shouldFilterByCommits(allCommits, filteredCommits, options);
625
+ const [diffText, fileNames, diffSummary] = yield Promise.all([
626
+ getDiff(git, from, to, filteredCommits, filterByCommits, pathFilter),
627
+ getChangedFiles(git, from, to, filteredCommits, filterByCommits, pathFilter),
628
+ getDiffSummary(git, from, to, filteredCommits, filterByCommits, pathFilter),
629
+ ]);
630
+ const summarizeFlags = {
631
+ from,
632
+ to,
633
+ team: options.teamName,
634
+ model: options.model,
635
+ maxDiffChars: options.maxDiffChars,
636
+ systemPrompt: options.systemPrompt,
637
+ commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
638
+ commitMessageExcludeRegexes: options.commitMessageExcludeRegexes,
639
+ };
640
+ return generateSummary(diffText, fileNames, filteredCommits, summarizeFlags, options.openAiClientProvider, diffSummary);
641
+ });
642
+ }
643
+
644
+ exports.DEFAULT_GIT_DIFF_SYSTEM_PROMPT = DEFAULT_GIT_DIFF_SYSTEM_PROMPT;
645
+ exports.LLM_GATEWAY_REQUIRED_MESSAGE = LLM_GATEWAY_REQUIRED_MESSAGE;
646
+ exports.buildDiffPathspecs = buildDiffPathspecs;
647
+ exports.createGitClient = createGitClient;
648
+ exports.createOpenAiLikeClient = createOpenAiLikeClient;
649
+ exports.filterCommitsByMessageRegexes = filterCommitsByMessageRegexes;
650
+ exports.generateSummary = generateSummary;
651
+ exports.getChangedFiles = getChangedFiles;
652
+ exports.getCommits = getCommits;
653
+ exports.getDiff = getDiff;
654
+ exports.getDiffSummary = getDiffSummary;
655
+ exports.getRepoRoot = getRepoRoot;
656
+ exports.parseLlmDefaultHeadersFromEnv = parseLlmDefaultHeadersFromEnv;
657
+ exports.resolveLlmBaseUrl = resolveLlmBaseUrl;
658
+ exports.resolveLlmMaxDiffChars = resolveLlmMaxDiffChars;
659
+ exports.resolveOpenAiLikeClientInit = resolveOpenAiLikeClientInit;
660
+ exports.shouldUseLlmGateway = shouldUseLlmGateway;
661
+ exports.splitPromotableAuthorizationFromHeaders = splitPromotableAuthorizationFromHeaders;
662
+ exports.summarizeGitDiff = summarizeGitDiff;
663
+ exports.truncateUnifiedDiffForLlm = truncateUnifiedDiffForLlm;
664
+
665
+ }));
666
+ //# sourceMappingURL=index.umd.js.map