@mcarvin/smart-diff 1.0.4 → 1.0.5

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/dist/index.cjs CHANGED
@@ -37,7 +37,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
37
37
 
38
38
  function resolveLlmBaseUrl() {
39
39
  var _a, _b, _c;
40
- 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();
40
+ 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());
41
41
  }
42
42
  function parseHeaderJsonObject(raw) {
43
43
  const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
@@ -45,12 +45,14 @@ function parseHeaderJsonObject(raw) {
45
45
  return {};
46
46
  try {
47
47
  const parsed = JSON.parse(trimmed);
48
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
48
+ if (typeof parsed !== "object" ||
49
+ parsed === null ||
50
+ Array.isArray(parsed)) {
49
51
  return {};
50
52
  }
51
53
  const out = {};
52
54
  for (const [key, value] of Object.entries(parsed)) {
53
- if (typeof value === 'string' && value.length > 0) {
55
+ if (typeof value === "string" && value.length > 0) {
54
56
  out[key] = value;
55
57
  }
56
58
  }
@@ -67,7 +69,7 @@ function parseLlmDefaultHeadersFromEnv() {
67
69
  return Object.keys(merged).length > 0 ? merged : undefined;
68
70
  }
69
71
  function findAuthorizationHeaderName(headers) {
70
- return Object.keys(headers).find((k) => k.toLowerCase() === 'authorization');
72
+ return Object.keys(headers).find((k) => k.toLowerCase() === "authorization");
71
73
  }
72
74
  function stripBearerPrefix(value) {
73
75
  var _a;
@@ -110,7 +112,7 @@ function resolveOpenAiLikeClientInit() {
110
112
  var _a, _b, _c, _d, _e;
111
113
  const baseURL = resolveLlmBaseUrl();
112
114
  const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
113
- 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
+ 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 : "";
114
116
  let defaultHeaders;
115
117
  let apiKey = envApiKey;
116
118
  if (apiKey.length === 0) {
@@ -118,12 +120,16 @@ function resolveOpenAiLikeClientInit() {
118
120
  if (split.apiKeyFromAuthHeader) {
119
121
  apiKey = split.apiKeyFromAuthHeader;
120
122
  }
121
- defaultHeaders = Object.keys(split.defaultHeaders).length > 0 ? split.defaultHeaders : undefined;
123
+ defaultHeaders =
124
+ Object.keys(split.defaultHeaders).length > 0
125
+ ? split.defaultHeaders
126
+ : undefined;
122
127
  }
123
128
  else {
124
- defaultHeaders = Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
129
+ defaultHeaders =
130
+ Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
125
131
  }
126
- return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : 'unused' }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
132
+ return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : "unused" }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
127
133
  }
128
134
  function createOpenAiLikeClient() {
129
135
  return __awaiter(this, void 0, void 0, function* () {
@@ -133,9 +139,22 @@ function createOpenAiLikeClient() {
133
139
  }
134
140
 
135
141
  const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
142
+ 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.
143
+ 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.
144
+ Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
145
+ Produce a concise, developer-focused summary in Markdown.
146
+ 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.
147
+ Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
148
+ If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
149
+ 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, " +
150
+ "and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. " +
151
+ "Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.";
152
+
136
153
  function resolveLlmMaxDiffChars(cliOverride) {
137
154
  var _a;
138
- if (cliOverride !== undefined && Number.isFinite(cliOverride) && cliOverride > 0) {
155
+ if (cliOverride !== undefined &&
156
+ Number.isFinite(cliOverride) &&
157
+ cliOverride > 0) {
139
158
  return Math.trunc(cliOverride);
140
159
  }
141
160
  const raw = (_a = process.env.LLM_MAX_DIFF_CHARS) === null || _a === void 0 ? void 0 : _a.trim();
@@ -154,70 +173,67 @@ function truncateUnifiedDiffForLlm(diffText, maxChars) {
154
173
  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`;
155
174
  return diffText.slice(0, maxChars) + marker;
156
175
  }
157
- 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.
158
- 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.
159
- Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
160
- Produce a concise, developer-focused summary in Markdown.
161
- 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.
162
- Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
163
- If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
164
- 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, ' +
165
- 'and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. ' +
166
- 'Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.';
167
- function generateSummary(diffText, fileNames, commits, flags, openAiClientProvider, diffSummary) {
176
+ function generateSummary(input) {
168
177
  return __awaiter(this, void 0, void 0, function* () {
169
178
  var _a, _b;
179
+ const { diffText, fileNames, commits, flags, openAiClientProvider, diffSummary, } = input;
170
180
  if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
171
181
  throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
172
182
  }
173
183
  const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
174
184
  const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
175
185
  const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
176
- 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(); })));
186
+ 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(); })));
177
187
  });
178
188
  }
179
189
  function formatRegexFilterLines(flags) {
180
190
  var _a, _b;
181
- const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : []).map((s) => s.trim()).filter(Boolean);
182
- const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : []).map((s) => s.trim()).filter(Boolean);
191
+ const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : [])
192
+ .map((s) => s.trim())
193
+ .filter(Boolean);
194
+ const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : [])
195
+ .map((s) => s.trim())
196
+ .filter(Boolean);
183
197
  const incLine = includes.length > 0
184
- ? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(', ')}\n`
185
- : '';
198
+ ? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(", ")}\n`
199
+ : "";
186
200
  const excLine = excludes.length > 0
187
- ? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(', ')}\n`
188
- : '';
201
+ ? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(", ")}\n`
202
+ : "";
189
203
  if (!incLine && !excLine) {
190
- return 'Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n';
204
+ return "Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n";
191
205
  }
192
206
  return (`${incLine}${excLine}` +
193
- 'Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n');
207
+ "Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n");
194
208
  }
195
209
  function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
196
210
  var _a, _b;
197
211
  const from = flags.from;
198
- const to = (_a = flags.to) !== null && _a !== void 0 ? _a : 'HEAD';
212
+ const to = (_a = flags.to) !== null && _a !== void 0 ? _a : "HEAD";
199
213
  const team = (_b = flags.team) === null || _b === void 0 ? void 0 : _b.trim();
200
214
  const ts = new Date().toISOString();
201
- const teamLine = team ? `Team: ${team}\n` : '';
215
+ const teamLine = team ? `Team: ${team}\n` : "";
202
216
  const filterBlock = formatRegexFilterLines(flags);
203
217
  const commitBlock = commits.length > 0
204
- ? commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, ' ')}`).join('\n')
205
- : '- (no commits in range after filtering)';
206
- const pathsBlock = fileNames.length > 0 ? fileNames.join('\n') : '(no paths in diff scope)';
218
+ ? commits
219
+ .map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, " ")}`)
220
+ .join("\n")
221
+ : "- (no commits in range after filtering)";
222
+ const pathsBlock = fileNames.length > 0 ? fileNames.join("\n") : "(no paths in diff scope)";
207
223
  const structuredDiffSection = diffSummary
208
224
  ? `=== Structured git context (JSON summary) ===\n${JSON.stringify(diffSummary, null, 2)}\n\n`
209
- : '';
225
+ : "";
210
226
  return (`${teamLine}` +
211
227
  `Date: ${ts}\n\n` +
212
228
  `Git refs: ${from}..${to}\n` +
213
229
  filterBlock +
214
- '\n' +
215
- '=== Included commits (subject lines) ===\n' +
230
+ "\n" +
231
+ "=== Included commits (subject lines) ===\n" +
216
232
  `${commitBlock}\n\n` +
217
- '=== Changed paths ===\n' +
233
+ "=== Changed paths ===\n" +
218
234
  `${pathsBlock}\n\n` +
219
235
  structuredDiffSection +
220
- '=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n' +
236
+ "=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n" +
221
237
  diffText);
222
238
  }
223
239
  function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
@@ -231,11 +247,11 @@ function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
231
247
  model,
232
248
  messages: [
233
249
  {
234
- role: 'system',
250
+ role: "system",
235
251
  content: systemPrompt,
236
252
  },
237
253
  {
238
- role: 'user',
254
+ role: "user",
239
255
  content: userContent,
240
256
  },
241
257
  ],
@@ -243,64 +259,25 @@ function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
243
259
  max_tokens: maxTokens,
244
260
  });
245
261
  const typedResponse = response;
246
- 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 : '';
247
- return text.length > 0 ? text : 'No summary generated by OpenAI.';
262
+ 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 : "";
263
+ return text.length > 0 ? text : "No summary generated by OpenAI.";
248
264
  });
249
265
  }
250
266
 
251
- function createGitClient(cwd = process.cwd()) {
252
- return simpleGit.simpleGit(cwd);
253
- }
254
- function getCommits(git, from, to) {
255
- return __awaiter(this, void 0, void 0, function* () {
256
- const logResult = yield git.log({ from, to });
257
- return logResult.all;
258
- });
259
- }
260
- function compileRegex(pattern, label) {
261
- try {
262
- return new RegExp(pattern, 'i');
263
- }
264
- catch (_a) {
265
- throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
266
- }
267
- }
268
- function commitMessagePassesFilters(message, includeRes, excludeRes) {
269
- for (const ex of excludeRes) {
270
- if (ex.test(message))
271
- return false;
272
- }
273
- if (includeRes.length > 0 && !includeRes.some((r) => r.test(message)))
274
- return false;
275
- return true;
276
- }
277
- function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
278
- const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
279
- const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
280
- const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
281
- const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
282
- return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
283
- }
284
- function getRepoRoot(git) {
285
- return __awaiter(this, void 0, void 0, function* () {
286
- const root = yield git.revparse(['--show-toplevel']);
287
- return root.trim();
288
- });
289
- }
290
267
  function normalizeRepoRelativePath(p) {
291
- const trimmed = p.trim().replace(/\\/g, '/');
292
- const noLeading = trimmed.replace(/^\/+/, '');
293
- const noTrailingSlash = noLeading.replace(/\/+$/, '');
294
- return noTrailingSlash.length > 0 ? noTrailingSlash : '.';
268
+ const trimmed = p.trim().replace(/\\/g, "/");
269
+ const noLeading = trimmed.replace(/^\/+/, "");
270
+ const noTrailingSlash = noLeading.replace(/\/+$/, "");
271
+ return noTrailingSlash.length > 0 ? noTrailingSlash : ".";
295
272
  }
296
273
  function assertPathUnderRepo(repoRoot, userPath) {
297
274
  const abs = node_path.resolve(repoRoot, userPath);
298
275
  const rel = node_path.relative(repoRoot, abs);
299
- if (rel === '..') {
276
+ if (rel === "..") {
300
277
  throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
301
278
  }
302
279
  const segments = rel.split(/[/\\]/);
303
- if (segments.includes('..')) {
280
+ if (segments.includes("..")) {
304
281
  throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
305
282
  }
306
283
  }
@@ -308,9 +285,13 @@ function buildDiffPathspecs(repoRoot, pathFilter) {
308
285
  var _a, _b, _c, _d;
309
286
  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 : [];
310
287
  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 : [];
311
- const includes = includeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
312
- const excludes = excludeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
313
- const toValidate = includes.length > 0 ? includes : ['.'];
288
+ const includes = includeRaw
289
+ .map(normalizeRepoRelativePath)
290
+ .filter((p) => p !== "." && p !== "");
291
+ const excludes = excludeRaw
292
+ .map(normalizeRepoRelativePath)
293
+ .filter((p) => p !== "." && p !== "");
294
+ const toValidate = includes.length > 0 ? includes : ["."];
314
295
  for (const inc of toValidate) {
315
296
  assertPathUnderRepo(repoRoot, inc);
316
297
  }
@@ -319,7 +300,7 @@ function buildDiffPathspecs(repoRoot, pathFilter) {
319
300
  }
320
301
  const specs = [];
321
302
  if (includes.length === 0) {
322
- specs.push('.');
303
+ specs.push(".");
323
304
  }
324
305
  else {
325
306
  for (const inc of includes) {
@@ -331,100 +312,73 @@ function buildDiffPathspecs(repoRoot, pathFilter) {
331
312
  }
332
313
  return specs;
333
314
  }
334
- function getDiffPathContext(git, pathFilter, repoRootOverride) {
335
- return __awaiter(this, void 0, void 0, function* () {
336
- const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
337
- const specs = buildDiffPathspecs(repoRoot, pathFilter);
338
- return { repoRoot, specs };
339
- });
340
- }
341
- function getDiff(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
342
- return __awaiter(this, void 0, void 0, function* () {
343
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
344
- if (!filterByCommits) {
345
- return git.diff([`${from}..${to}`, '--', ...specs]);
346
- }
347
- const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, '--', ...specs])));
348
- return patches.filter(Boolean).join('\n');
349
- });
315
+
316
+ function compileRegex(pattern, label) {
317
+ try {
318
+ return new RegExp(pattern, "i");
319
+ }
320
+ catch (_a) {
321
+ throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
322
+ }
350
323
  }
351
- function getDiffSummary(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
352
- return __awaiter(this, void 0, void 0, function* () {
353
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
354
- if (!filterByCommits) {
355
- const [numOutput, nameOutput] = yield Promise.all([
356
- git.diff(['--numstat', `${from}..${to}`, '--', ...specs]),
357
- git.diff(['--name-status', `${from}..${to}`, '--', ...specs]),
358
- ]);
359
- return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
360
- }
361
- const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
362
- const range = `${c.hash}^!`;
363
- const [numOutput, nameOutput] = yield Promise.all([
364
- git.diff(['--numstat', range, '--', ...specs]),
365
- git.diff(['--name-status', range, '--', ...specs]),
366
- ]);
367
- return { numOutput, nameOutput };
368
- })));
369
- const nameJoined = pairs
370
- .map((p) => p.nameOutput)
371
- .filter(Boolean)
372
- .join('\n');
373
- const numJoined = pairs
374
- .map((p) => p.numOutput)
375
- .filter(Boolean)
376
- .join('\n');
377
- return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
378
- });
324
+ function commitMessagePassesFilters(message, includeRes, excludeRes) {
325
+ for (const ex of excludeRes) {
326
+ if (ex.test(message))
327
+ return false;
328
+ }
329
+ if (includeRes.length > 0 && !includeRes.some((r) => r.test(message)))
330
+ return false;
331
+ return true;
379
332
  }
380
- function getChangedFiles(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
381
- return __awaiter(this, void 0, void 0, function* () {
382
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
383
- if (!filterByCommits) {
384
- const output = yield git.diff(['--name-only', `${from}..${to}`, '--', ...specs]);
385
- return output
386
- .split(/\r?\n/)
387
- .map((f) => f.trim())
388
- .filter(Boolean);
389
- }
390
- const fileSet = new Set();
391
- yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
392
- const output = yield git.show(['--name-only', '--pretty=format:', c.hash, '--', ...specs]);
393
- output
394
- .split(/\r?\n/)
395
- .map((f) => f.trim())
396
- .filter(Boolean)
397
- .forEach((f) => fileSet.add(f));
398
- })));
399
- return Array.from(fileSet);
400
- });
333
+ function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
334
+ const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : [])
335
+ .map((p) => p.trim())
336
+ .filter((p) => p.length > 0);
337
+ const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : [])
338
+ .map((p) => p.trim())
339
+ .filter((p) => p.length > 0);
340
+ const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
341
+ const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
342
+ return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
401
343
  }
344
+
402
345
  const GIT_STATUS_BY_FIRST_CHAR = {
403
- A: 'added',
404
- D: 'deleted',
405
- R: 'renamed',
406
- C: 'copied',
407
- T: 'type-changed',
408
- M: 'modified',
346
+ A: "added",
347
+ D: "deleted",
348
+ R: "renamed",
349
+ C: "copied",
350
+ T: "type-changed",
351
+ M: "modified",
409
352
  };
410
353
  function mapGitStatus(statusCode) {
411
354
  var _a;
412
- return (_a = GIT_STATUS_BY_FIRST_CHAR[statusCode.charAt(0)]) !== null && _a !== void 0 ? _a : 'unknown';
355
+ return (_a = GIT_STATUS_BY_FIRST_CHAR[statusCode.charAt(0)]) !== null && _a !== void 0 ? _a : "unknown";
413
356
  }
414
357
  function mergeStatus(existing, next) {
415
358
  if (existing === next)
416
359
  return existing;
417
- const precedence = ['deleted', 'added', 'renamed', 'copied', 'type-changed', 'modified', 'unknown'];
418
- return precedence.indexOf(existing) <= precedence.indexOf(next) ? existing : next;
360
+ const precedence = [
361
+ "deleted",
362
+ "added",
363
+ "renamed",
364
+ "copied",
365
+ "type-changed",
366
+ "modified",
367
+ "unknown",
368
+ ];
369
+ return precedence.indexOf(existing) <= precedence.indexOf(next)
370
+ ? existing
371
+ : next;
419
372
  }
373
+
420
374
  function parseNameStatusLine(line) {
421
375
  var _a;
422
- const parts = line.split('\t');
376
+ const parts = line.split("\t");
423
377
  let entry = null;
424
378
  if (parts.length >= 2) {
425
- const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
379
+ const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
426
380
  const status = mapGitStatus(statusToken);
427
- const isRenameOrCopy = statusToken.startsWith('R') || statusToken.startsWith('C');
381
+ const isRenameOrCopy = statusToken.startsWith("R") || statusToken.startsWith("C");
428
382
  if (isRenameOrCopy && parts.length >= 3) {
429
383
  const oldPath = parts[1];
430
384
  const newPath = parts[2];
@@ -470,6 +424,7 @@ function mergeNameEntriesByPath(entries) {
470
424
  }
471
425
  return byPath;
472
426
  }
427
+
473
428
  function numStatPathToLookupKey(pathField) {
474
429
  const brace = /^(.*)\{(.+) => (.+)\}$/.exec(pathField);
475
430
  if (!brace) {
@@ -481,14 +436,14 @@ function numStatPathToLookupKey(pathField) {
481
436
  }
482
437
  function parseNumStatLine(line) {
483
438
  var _a, _b;
484
- const parts = line.split('\t');
439
+ const parts = line.split("\t");
485
440
  if (parts.length < 3)
486
441
  return null;
487
- const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
488
- const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : '';
489
- const pathField = parts.slice(2).join('\t');
490
- const additions = addStr !== '-' ? Number.parseInt(addStr, 10) || 0 : 0;
491
- const deletions = delStr !== '-' ? Number.parseInt(delStr, 10) || 0 : 0;
442
+ const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
443
+ const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : "";
444
+ const pathField = parts.slice(2).join("\t");
445
+ const additions = addStr !== "-" ? Number.parseInt(addStr, 10) || 0 : 0;
446
+ const deletions = delStr !== "-" ? Number.parseInt(delStr, 10) || 0 : 0;
492
447
  const key = numStatPathToLookupKey(pathField);
493
448
  return { key, additions, deletions };
494
449
  }
@@ -502,56 +457,35 @@ function accumulateNumStat(numStatOutput, into) {
502
457
  if (!parsed)
503
458
  continue;
504
459
  const prev = (_a = into.get(parsed.key)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
505
- into.set(parsed.key, { additions: prev.additions + parsed.additions, deletions: prev.deletions + parsed.deletions });
506
- }
507
- }
508
- const STATUS_TO_SYNTHETIC_PREFIX = {
509
- added: 'A',
510
- deleted: 'D',
511
- renamed: 'R100',
512
- copied: 'C100',
513
- 'type-changed': 'T',
514
- modified: 'M',
515
- unknown: 'X',
516
- };
517
- function diffStatusToSyntheticPrefix(status) {
518
- return STATUS_TO_SYNTHETIC_PREFIX[status];
519
- }
520
- function buildSyntheticDiffLine(meta, counts) {
521
- const prefix = diffStatusToSyntheticPrefix(meta.status);
522
- if (meta.oldPath) {
523
- return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${meta.path}`;
524
- }
525
- return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
526
- }
527
- function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
528
- var _a;
529
- const numMap = new Map();
530
- accumulateNumStat(numStatOutput, numMap);
531
- const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
532
- const syntheticLines = [];
533
- for (const [path, meta] of mergedName) {
534
- const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
535
- syntheticLines.push(buildSyntheticDiffLine(meta, counts));
460
+ into.set(parsed.key, {
461
+ additions: prev.additions + parsed.additions,
462
+ deletions: prev.deletions + parsed.deletions,
463
+ });
536
464
  }
537
- return parseDiffSummary(syntheticLines.join('\n'));
538
465
  }
466
+
539
467
  function parseTabDiffSummaryLine(line) {
540
468
  var _a;
541
- const parts = line.split('\t');
469
+ const parts = line.split("\t");
542
470
  if (parts.length < 3)
543
471
  return null;
544
- const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : '';
472
+ const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : "";
545
473
  const status = mapGitStatus(statusToken);
546
474
  const add0 = parts[0];
547
475
  const del0 = parts[1];
548
- const additions = add0 && add0 !== '-' ? Number.parseInt(add0, 10) || 0 : 0;
549
- const deletions = del0 && del0 !== '-' ? Number.parseInt(del0, 10) || 0 : 0;
476
+ const additions = add0 && add0 !== "-" ? Number.parseInt(add0, 10) || 0 : 0;
477
+ const deletions = del0 && del0 !== "-" ? Number.parseInt(del0, 10) || 0 : 0;
550
478
  if (parts.length === 3) {
551
479
  return { status, additions, deletions, newPath: parts[2] };
552
480
  }
553
481
  if (parts.length === 4) {
554
- return { status, additions, deletions, oldPath: parts[2], newPath: parts[3] };
482
+ return {
483
+ status,
484
+ additions,
485
+ deletions,
486
+ oldPath: parts[2],
487
+ newPath: parts[3],
488
+ };
555
489
  }
556
490
  return null;
557
491
  }
@@ -597,11 +531,142 @@ function parseDiffSummary(diffOutput) {
597
531
  };
598
532
  }
599
533
 
534
+ const STATUS_TO_SYNTHETIC_PREFIX = {
535
+ added: "A",
536
+ deleted: "D",
537
+ renamed: "R100",
538
+ copied: "C100",
539
+ "type-changed": "T",
540
+ modified: "M",
541
+ unknown: "X",
542
+ };
543
+ function diffStatusToSyntheticPrefix(status) {
544
+ return STATUS_TO_SYNTHETIC_PREFIX[status];
545
+ }
546
+ function buildSyntheticDiffLine(meta, counts) {
547
+ const prefix = diffStatusToSyntheticPrefix(meta.status);
548
+ if (meta.oldPath) {
549
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${meta.path}`;
550
+ }
551
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
552
+ }
553
+ function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
554
+ var _a;
555
+ const numMap = new Map();
556
+ accumulateNumStat(numStatOutput, numMap);
557
+ const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
558
+ const syntheticLines = [];
559
+ for (const [path, meta] of mergedName) {
560
+ const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
561
+ syntheticLines.push(buildSyntheticDiffLine(meta, counts));
562
+ }
563
+ return parseDiffSummary(syntheticLines.join("\n"));
564
+ }
565
+
566
+ function createGitClient(cwd = process.cwd()) {
567
+ return simpleGit.simpleGit(cwd);
568
+ }
569
+ function getCommits(git, from, to) {
570
+ return __awaiter(this, void 0, void 0, function* () {
571
+ const logResult = yield git.log({ from, to });
572
+ return logResult.all;
573
+ });
574
+ }
575
+ function getRepoRoot(git) {
576
+ return __awaiter(this, void 0, void 0, function* () {
577
+ const root = yield git.revparse(["--show-toplevel"]);
578
+ return root.trim();
579
+ });
580
+ }
581
+ function getDiffPathContext(git, pathFilter, repoRootOverride) {
582
+ return __awaiter(this, void 0, void 0, function* () {
583
+ const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
584
+ const specs = buildDiffPathspecs(repoRoot, pathFilter);
585
+ return { repoRoot, specs };
586
+ });
587
+ }
588
+ function getDiff(git, query) {
589
+ return __awaiter(this, void 0, void 0, function* () {
590
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
591
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
592
+ if (!filterByCommits) {
593
+ return git.diff([`${from}..${to}`, "--", ...specs]);
594
+ }
595
+ const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, "--", ...specs])));
596
+ return patches.filter(Boolean).join("\n");
597
+ });
598
+ }
599
+ function getDiffSummary(git, query) {
600
+ return __awaiter(this, void 0, void 0, function* () {
601
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
602
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
603
+ if (!filterByCommits) {
604
+ const [numOutput, nameOutput] = yield Promise.all([
605
+ git.diff(["--numstat", `${from}..${to}`, "--", ...specs]),
606
+ git.diff(["--name-status", `${from}..${to}`, "--", ...specs]),
607
+ ]);
608
+ return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
609
+ }
610
+ const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
611
+ const range = `${c.hash}^!`;
612
+ const [numOutput, nameOutput] = yield Promise.all([
613
+ git.diff(["--numstat", range, "--", ...specs]),
614
+ git.diff(["--name-status", range, "--", ...specs]),
615
+ ]);
616
+ return { numOutput, nameOutput };
617
+ })));
618
+ const nameJoined = pairs
619
+ .map((p) => p.nameOutput)
620
+ .filter(Boolean)
621
+ .join("\n");
622
+ const numJoined = pairs
623
+ .map((p) => p.numOutput)
624
+ .filter(Boolean)
625
+ .join("\n");
626
+ return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
627
+ });
628
+ }
629
+ function getChangedFiles(git, query) {
630
+ return __awaiter(this, void 0, void 0, function* () {
631
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
632
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
633
+ if (!filterByCommits) {
634
+ const output = yield git.diff([
635
+ "--name-only",
636
+ `${from}..${to}`,
637
+ "--",
638
+ ...specs,
639
+ ]);
640
+ return output
641
+ .split(/\r?\n/)
642
+ .map((f) => f.trim())
643
+ .filter(Boolean);
644
+ }
645
+ const fileSet = new Set();
646
+ yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
647
+ const output = yield git.show([
648
+ "--name-only",
649
+ "--pretty=format:",
650
+ c.hash,
651
+ "--",
652
+ ...specs,
653
+ ]);
654
+ output
655
+ .split(/\r?\n/)
656
+ .map((f) => f.trim())
657
+ .filter(Boolean)
658
+ .forEach((f) => fileSet.add(f));
659
+ })));
660
+ return Array.from(fileSet);
661
+ });
662
+ }
663
+
600
664
  function hasNonEmptyTrimmed(arr) {
601
665
  return (arr !== null && arr !== void 0 ? arr : []).some((s) => s.trim().length > 0);
602
666
  }
603
667
  function shouldFilterByCommits(allCommits, filtered, opts) {
604
- if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) || hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
668
+ if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) ||
669
+ hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
605
670
  return true;
606
671
  }
607
672
  return filtered.length !== allCommits.length;
@@ -611,8 +676,9 @@ function summarizeGitDiff(options) {
611
676
  var _a, _b;
612
677
  const git = (_a = options.git) !== null && _a !== void 0 ? _a : createGitClient(options.cwd);
613
678
  const from = options.from;
614
- const to = (_b = options.to) !== null && _b !== void 0 ? _b : 'HEAD';
615
- const pathFilter = hasNonEmptyTrimmed(options.includeFolders) || hasNonEmptyTrimmed(options.excludeFolders)
679
+ const to = (_b = options.to) !== null && _b !== void 0 ? _b : "HEAD";
680
+ const pathFilter = hasNonEmptyTrimmed(options.includeFolders) ||
681
+ hasNonEmptyTrimmed(options.excludeFolders)
616
682
  ? {
617
683
  includeFolders: options.includeFolders,
618
684
  excludeFolders: options.excludeFolders,
@@ -621,10 +687,17 @@ function summarizeGitDiff(options) {
621
687
  const allCommits = yield getCommits(git, from, to);
622
688
  const filteredCommits = filterCommitsByMessageRegexes(allCommits, options.commitMessageIncludeRegexes, options.commitMessageExcludeRegexes);
623
689
  const filterByCommits = shouldFilterByCommits(allCommits, filteredCommits, options);
690
+ const rangeQuery = {
691
+ from,
692
+ to,
693
+ commits: filteredCommits,
694
+ filterByCommits,
695
+ pathFilter,
696
+ };
624
697
  const [diffText, fileNames, diffSummary] = yield Promise.all([
625
- getDiff(git, from, to, filteredCommits, filterByCommits, pathFilter),
626
- getChangedFiles(git, from, to, filteredCommits, filterByCommits, pathFilter),
627
- getDiffSummary(git, from, to, filteredCommits, filterByCommits, pathFilter),
698
+ getDiff(git, rangeQuery),
699
+ getChangedFiles(git, rangeQuery),
700
+ getDiffSummary(git, rangeQuery),
628
701
  ]);
629
702
  const summarizeFlags = {
630
703
  from,
@@ -636,7 +709,14 @@ function summarizeGitDiff(options) {
636
709
  commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
637
710
  commitMessageExcludeRegexes: options.commitMessageExcludeRegexes,
638
711
  };
639
- return generateSummary(diffText, fileNames, filteredCommits, summarizeFlags, options.openAiClientProvider, diffSummary);
712
+ return generateSummary({
713
+ diffText,
714
+ fileNames,
715
+ commits: filteredCommits,
716
+ flags: summarizeFlags,
717
+ openAiClientProvider: options.openAiClientProvider,
718
+ diffSummary,
719
+ });
640
720
  });
641
721
  }
642
722