@wispbit/local 1.0.25 → 1.0.26

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/cli.js CHANGED
@@ -168,6 +168,7 @@ var Config = class _Config {
168
168
 
169
169
  // src/utils/git.ts
170
170
  import { exec, execSync } from "child_process";
171
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
171
172
  import { promisify } from "util";
172
173
 
173
174
  // src/utils/hashString.ts
@@ -182,189 +183,498 @@ function findGitRoot() {
182
183
  const stdout = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" });
183
184
  return stdout.trim();
184
185
  }
186
+ async function getGitIgnoredFiles(repoRoot) {
187
+ const { stdout } = await execPromise("git ls-files --ignored --exclude-standard --others", {
188
+ cwd: repoRoot,
189
+ maxBuffer: 50 * 1024 * 1024
190
+ });
191
+ return stdout.split("\n").filter(Boolean).map((file) => file.trim());
192
+ }
185
193
  async function getRepositoryUrl(repoRoot, remoteName = "origin") {
186
- var _a;
187
- const { stdout } = await execPromise(`git remote show ${remoteName}`, {
194
+ const { stdout } = await execPromise(`git config --get remote.${remoteName}.url`, {
188
195
  cwd: repoRoot
189
196
  });
190
- const fetchUrlLine = stdout.split("\n").find((line) => line.includes("Fetch URL:"));
191
- if (fetchUrlLine) {
192
- return ((_a = fetchUrlLine.split("Fetch URL:").pop()) == null ? void 0 : _a.trim()) || null;
193
- }
194
- return null;
197
+ return stdout.trim() || null;
195
198
  }
196
199
  async function getDefaultBranch(repoRoot, remoteName = "origin") {
197
- var _a;
198
- const { stdout } = await execPromise(`git remote show ${remoteName}`, {
200
+ try {
201
+ const { stdout } = await execPromise(`git rev-parse --abbrev-ref ${remoteName}/HEAD`, {
202
+ cwd: repoRoot
203
+ });
204
+ const fullRef = stdout.trim();
205
+ const branchName = fullRef.split("/").pop();
206
+ return branchName || null;
207
+ } catch (error) {
208
+ const commonBranches = ["main", "master"];
209
+ for (const branch of commonBranches) {
210
+ try {
211
+ await execPromise(`git rev-parse --verify ${branch}`, { cwd: repoRoot });
212
+ return branch;
213
+ } catch {
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+ async function tryGetUpstream(repoRoot) {
220
+ const { stdout } = await execPromise(`git rev-parse --abbrev-ref --symbolic-full-name @{u}`, {
221
+ cwd: repoRoot
222
+ });
223
+ return stdout.trim() || void 0;
224
+ }
225
+ function tryGetGraphiteParent(repoRoot, branch) {
226
+ const metadataPath = `.git/refs/branch-metadata/${branch}`;
227
+ const fullPath = `${repoRoot}/${metadataPath}`;
228
+ if (!existsSync(fullPath)) {
229
+ return void 0;
230
+ }
231
+ const content = readFileSync2(fullPath, "utf-8");
232
+ const match = content.match(/"parent"\s*:\s*"([^"]+)"/);
233
+ return match == null ? void 0 : match[1];
234
+ }
235
+ function shellQuote(path11) {
236
+ return `'${path11.replace(/'/g, "'\\''")}'`;
237
+ }
238
+ function joinAsShellArgs(paths) {
239
+ return paths.map(shellQuote).join(" ");
240
+ }
241
+ function resolveIncludes(raw) {
242
+ const all = {
243
+ committed: true,
244
+ staged: true,
245
+ unstaged: true,
246
+ untracked: true
247
+ };
248
+ if (!raw || raw.length === 0) return all;
249
+ const s = new Set(raw);
250
+ return {
251
+ committed: s.has("committed"),
252
+ staged: s.has("staged"),
253
+ unstaged: s.has("unstaged"),
254
+ untracked: s.has("untracked")
255
+ };
256
+ }
257
+ function parseNameStatusZ(buffer, source) {
258
+ if (!buffer) return [];
259
+ const entries = [];
260
+ const parts = buffer.split("\0").filter(Boolean);
261
+ for (let i = 0; i < parts.length; ) {
262
+ const status = parts[i];
263
+ if (!status) break;
264
+ if (status.startsWith("R")) {
265
+ const oldPath = parts[i + 1];
266
+ const newPath = parts[i + 2];
267
+ if (oldPath && newPath) {
268
+ entries.push({ status, path: newPath, oldPath, source });
269
+ i += 3;
270
+ } else {
271
+ i++;
272
+ }
273
+ } else {
274
+ const path11 = parts[i + 1];
275
+ if (path11) {
276
+ entries.push({ status, path: path11, source });
277
+ i += 2;
278
+ } else {
279
+ i++;
280
+ }
281
+ }
282
+ }
283
+ return entries;
284
+ }
285
+ function parseLsFilesZ(buffer) {
286
+ if (!buffer) return [];
287
+ return buffer.split("\0").filter(Boolean).map((path11) => ({ status: "U", path: path11, source: "untracked" }));
288
+ }
289
+ function dedupeEntries(entries) {
290
+ const byPath = /* @__PURE__ */ new Map();
291
+ for (const entry of entries) {
292
+ const existing = byPath.get(entry.path);
293
+ if (!existing) {
294
+ byPath.set(entry.path, entry);
295
+ } else {
296
+ const existingPriority = getPriority(existing.source);
297
+ const newPriority = getPriority(entry.source);
298
+ if (newPriority > existingPriority) {
299
+ byPath.set(entry.path, entry);
300
+ } else if (newPriority === existingPriority && entry.oldPath) {
301
+ existing.oldPath = existing.oldPath || entry.oldPath;
302
+ }
303
+ }
304
+ }
305
+ return Array.from(byPath.values());
306
+ }
307
+ function getPriority(source) {
308
+ switch (source) {
309
+ case "untracked":
310
+ return 4;
311
+ case "unstaged":
312
+ return 3;
313
+ case "staged":
314
+ return 2;
315
+ case "committed":
316
+ return 1;
317
+ }
318
+ }
319
+ function splitGitPatchPerFile(patchOutput) {
320
+ const patches = /* @__PURE__ */ new Map();
321
+ if (!patchOutput) return patches;
322
+ const sections = patchOutput.split(/^diff --git /m).filter(Boolean);
323
+ for (const section of sections) {
324
+ const lines = section.split("\n");
325
+ const firstLine = lines[0];
326
+ const match = firstLine.match(/a\/(.+?) b\//);
327
+ if (match) {
328
+ const filename = match[1];
329
+ patches.set(filename, "diff --git " + section);
330
+ }
331
+ }
332
+ return patches;
333
+ }
334
+ function toChangeStatus(entry) {
335
+ if (entry.status === "U" || entry.status === "A") return "added";
336
+ if (entry.status === "D") return "removed";
337
+ return "modified";
338
+ }
339
+ function stripDiffHeaders(patch) {
340
+ if (!patch) return "";
341
+ const lines = patch.split("\n");
342
+ const output = [];
343
+ let inHunk = false;
344
+ for (const line of lines) {
345
+ if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("new file mode") || line.startsWith("deleted file mode") || line.startsWith("old mode") || line.startsWith("new mode") || line.startsWith("similarity index") || line.startsWith("rename from") || line.startsWith("rename to") || line.startsWith("copy from") || line.startsWith("copy to") || line === "\") {
346
+ continue;
347
+ }
348
+ if (line.startsWith("@@")) {
349
+ inHunk = true;
350
+ }
351
+ if (inHunk) {
352
+ output.push(line);
353
+ }
354
+ }
355
+ return output.join("\n").trimEnd();
356
+ }
357
+ function parseRange(selector) {
358
+ const threeDotsMatch = selector.match(/^(.+)\.\.\.(.+)$/);
359
+ if (threeDotsMatch) {
360
+ return [threeDotsMatch[1], threeDotsMatch[2], true];
361
+ }
362
+ const twoDotsMatch = selector.match(/^(.+)\.\.([^.].*)$/);
363
+ if (twoDotsMatch) {
364
+ return [twoDotsMatch[1], twoDotsMatch[2], false];
365
+ }
366
+ return [selector, selector, false];
367
+ }
368
+ function parseRangeRight(selector) {
369
+ const [, right] = parseRange(selector);
370
+ return right;
371
+ }
372
+ async function getCurrentBranch(repoRoot) {
373
+ const { stdout } = await execPromise("git rev-parse --abbrev-ref HEAD", {
199
374
  cwd: repoRoot
200
375
  });
201
- const headBranchLine = stdout.split("\n").find((line) => line.includes("HEAD branch"));
202
- if (headBranchLine) {
203
- return ((_a = headBranchLine.split(":").pop()) == null ? void 0 : _a.trim()) || null;
376
+ return stdout.trim();
377
+ }
378
+ function validateWorktreeIncludes(commitSelector, includes, currentBranch) {
379
+ const isRange = commitSelector.includes("..");
380
+ if (!isRange) {
381
+ return null;
382
+ }
383
+ const hasWorktreeIncludes = includes.includes("staged") || includes.includes("unstaged") || includes.includes("untracked");
384
+ if (!hasWorktreeIncludes) {
385
+ return null;
386
+ }
387
+ const rangeRight = parseRangeRight(commitSelector);
388
+ const endsAtCurrent = rangeRight === "HEAD" || rangeRight === currentBranch;
389
+ if (!endsAtCurrent) {
390
+ return `Worktree includes (staged, unstaged, untracked) require range to end at HEAD or current branch (${currentBranch}). Got: ${commitSelector}`;
204
391
  }
205
392
  return null;
206
393
  }
207
- async function getChangedFiles(repoRoot, base) {
208
- var _a, _b;
394
+ async function getDefaultCommitSelector(repoRoot) {
209
395
  const { stdout: currentBranchOutput } = await execPromise("git rev-parse --abbrev-ref HEAD", {
210
396
  cwd: repoRoot
211
397
  });
212
398
  const currentBranch = currentBranchOutput.trim();
399
+ const defaultInclude = ["committed", "staged", "unstaged", "untracked"];
400
+ const graphiteParent = tryGetGraphiteParent(repoRoot, currentBranch);
401
+ if (graphiteParent) {
402
+ return {
403
+ commitSelector: `${graphiteParent}...${currentBranch}`,
404
+ include: defaultInclude
405
+ };
406
+ }
213
407
  const defaultBranch = await getDefaultBranch(repoRoot);
214
- const compareTo = base ?? (defaultBranch ? `origin/${defaultBranch}` : "HEAD^");
215
- let currentCommit;
216
- try {
217
- const { stdout: remoteCommitOutput } = await execPromise(
218
- `git rev-parse origin/${currentBranch}`,
219
- { cwd: repoRoot }
220
- );
221
- currentCommit = remoteCommitOutput.trim();
222
- } catch (error) {
223
- const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
408
+ if (defaultBranch) {
409
+ if (currentBranch === defaultBranch) {
410
+ const upstream = await tryGetUpstream(repoRoot).catch(() => void 0);
411
+ if (upstream) {
412
+ return {
413
+ commitSelector: `${upstream}...HEAD`,
414
+ include: defaultInclude
415
+ };
416
+ }
417
+ return {
418
+ commitSelector: `HEAD~1...HEAD`,
419
+ include: defaultInclude
420
+ };
421
+ }
422
+ const originBranch = `origin/${defaultBranch}`;
423
+ const { stdout } = await execPromise(`git rev-parse --verify ${originBranch}`, {
224
424
  cwd: repoRoot
225
425
  });
226
- currentCommit = currentCommitOutput.trim();
227
- }
228
- let mergeBase;
229
- try {
230
- const { stdout: mergeBaseOutput } = await execPromise(
231
- `git merge-base ${currentBranch} ${compareTo}`,
232
- { cwd: repoRoot }
233
- );
234
- mergeBase = mergeBaseOutput.trim();
235
- } catch (error) {
236
- mergeBase = "HEAD^";
426
+ const base = stdout.trim() ? originBranch : defaultBranch;
427
+ return {
428
+ commitSelector: `${base}...${currentBranch}`,
429
+ include: defaultInclude
430
+ };
237
431
  }
238
- const { stdout: statusOutput } = await execPromise("git status --porcelain", {
239
- cwd: repoRoot
240
- });
241
- const statusLines = statusOutput.split("\n").filter(Boolean);
242
- const fileStatuses = /* @__PURE__ */ new Map();
243
- statusLines.forEach((line) => {
244
- const statusCode = line.substring(0, 2).trim();
245
- const filename = line.substring(3);
246
- fileStatuses.set(filename, statusCode);
247
- });
248
- const { stdout: diffOutput } = await execPromise(`git diff ${mergeBase} --name-only`, {
432
+ return {
433
+ commitSelector: `HEAD...HEAD`,
434
+ include: defaultInclude
435
+ };
436
+ }
437
+ async function getChangedFiles(repoRoot, options) {
438
+ const selector = options.commitSelector.trim();
439
+ const isRange = selector.includes("..");
440
+ const { stdout: currentBranchOutput } = await execPromise("git rev-parse --abbrev-ref HEAD", {
249
441
  cwd: repoRoot
250
442
  });
251
- const allFiles = diffOutput.split("\n").filter(Boolean);
252
- const { stdout: deletedFilesOutput } = await execPromise("git ls-files --deleted", {
443
+ const currentBranch = currentBranchOutput.trim();
444
+ const { stdout: currentCommitOutput } = await execPromise("git rev-parse HEAD", {
253
445
  cwd: repoRoot
254
446
  });
255
- const deletedFiles = deletedFilesOutput.split("\n").filter(Boolean);
256
- allFiles.push(...deletedFiles.filter((file) => !allFiles.includes(file)));
257
- const { stdout: untrackedOutput } = await execPromise(
258
- "git ls-files --others --exclude-standard",
259
- {
260
- cwd: repoRoot
447
+ const currentCommit = currentCommitOutput.trim();
448
+ let compareTo = "";
449
+ let parentRef = "";
450
+ if (isRange) {
451
+ compareTo = selector;
452
+ parentRef = selector;
453
+ } else {
454
+ const graphiteParent = tryGetGraphiteParent(repoRoot, currentBranch);
455
+ const defaultBranch = await getDefaultBranch(repoRoot);
456
+ const target = selector || graphiteParent || `origin/${defaultBranch || "main"}`;
457
+ parentRef = target;
458
+ const mergeBaseCmd = `git merge-base --fork-point ${shellQuote(target)} HEAD || git merge-base ${shellQuote(target)} HEAD`;
459
+ const { stdout: mergeBaseOutput } = await execPromise(mergeBaseCmd, { cwd: repoRoot });
460
+ compareTo = mergeBaseOutput.trim();
461
+ if (!selector && currentBranch === (defaultBranch || "main")) {
462
+ const upstream = await tryGetUpstream(repoRoot).catch(() => void 0);
463
+ if (upstream) {
464
+ parentRef = upstream;
465
+ const { stdout: upstreamMergeBase } = await execPromise(
466
+ `git merge-base --fork-point ${shellQuote(upstream)} HEAD || git merge-base ${shellQuote(upstream)} HEAD`,
467
+ { cwd: repoRoot }
468
+ );
469
+ compareTo = upstreamMergeBase.trim();
470
+ } else {
471
+ parentRef = "HEAD~1";
472
+ compareTo = "HEAD~1";
473
+ }
261
474
  }
262
- );
263
- const untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
264
- allFiles.push(...untrackedFiles.filter((file) => !allFiles.includes(file)));
265
- const nonDeletedFiles = [];
266
- const deletedFilesSet = new Set(deletedFiles);
267
- const fileIsDeleted = /* @__PURE__ */ new Map();
268
- for (const file of allFiles) {
269
- const isDeleted = deletedFilesSet.has(file) || ((_a = fileStatuses.get(file)) == null ? void 0 : _a.includes("D")) || ((_b = fileStatuses.get(file)) == null ? void 0 : _b.includes("R"));
270
- fileIsDeleted.set(file, Boolean(isDeleted));
271
- if (!isDeleted) {
272
- nonDeletedFiles.push(file);
273
- }
274
- }
275
- const fileStats = /* @__PURE__ */ new Map();
276
- if (nonDeletedFiles.length > 0) {
277
- const { stdout: batchNumstatOutput } = await execPromise(
278
- `git diff ${mergeBase} --numstat -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
279
- { cwd: repoRoot }
280
- );
281
- const numstatLines = batchNumstatOutput.split("\n").filter(Boolean);
282
- numstatLines.forEach((line) => {
475
+ }
476
+ const includes = resolveIncludes(options.include);
477
+ const validationError = validateWorktreeIncludes(selector, options.include, currentBranch);
478
+ if (validationError) {
479
+ throw new Error(validationError);
480
+ }
481
+ const allEntries = [];
482
+ if (includes.committed) {
483
+ if (isRange) {
484
+ const [rangeA, rangeB] = parseRange(selector);
485
+ const { stdout } = await execPromise(
486
+ `git diff --name-status -M -z ${shellQuote(rangeA)}..${shellQuote(rangeB)}`,
487
+ { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
488
+ );
489
+ allEntries.push(...parseNameStatusZ(stdout, "committed"));
490
+ } else {
491
+ const { stdout } = await execPromise(`git diff --name-status -M -z ${compareTo}..HEAD`, {
492
+ cwd: repoRoot,
493
+ maxBuffer: 50 * 1024 * 1024
494
+ });
495
+ allEntries.push(...parseNameStatusZ(stdout, "committed"));
496
+ }
497
+ }
498
+ if (includes.staged) {
499
+ const { stdout } = await execPromise(`git diff --name-status -M -z --cached`, {
500
+ cwd: repoRoot,
501
+ maxBuffer: 50 * 1024 * 1024
502
+ });
503
+ allEntries.push(...parseNameStatusZ(stdout, "staged"));
504
+ }
505
+ if (includes.unstaged) {
506
+ const { stdout } = await execPromise(`git diff --name-status -M -z`, {
507
+ cwd: repoRoot,
508
+ maxBuffer: 50 * 1024 * 1024
509
+ });
510
+ allEntries.push(...parseNameStatusZ(stdout, "unstaged"));
511
+ }
512
+ if (includes.untracked) {
513
+ const { stdout } = await execPromise(`git ls-files --others --exclude-standard -z`, {
514
+ cwd: repoRoot,
515
+ maxBuffer: 50 * 1024 * 1024
516
+ });
517
+ allEntries.push(...parseLsFilesZ(stdout));
518
+ }
519
+ const entries = dedupeEntries(allEntries);
520
+ const committedEntries = entries.filter((e) => e.source === "committed");
521
+ const stagedEntries = entries.filter((e) => e.source === "staged");
522
+ const unstagedEntries = entries.filter((e) => e.source === "unstaged");
523
+ const untrackedEntries = entries.filter((e) => e.source === "untracked");
524
+ const patchByFile = /* @__PURE__ */ new Map();
525
+ if (committedEntries.length > 0) {
526
+ const paths = committedEntries.map((e) => e.path);
527
+ let diffCmd = "";
528
+ if (isRange) {
529
+ const [rangeA, rangeB] = parseRange(selector);
530
+ diffCmd = `git diff -U0 -M ${shellQuote(rangeA)}..${shellQuote(rangeB)} -- ${joinAsShellArgs(paths)}`;
531
+ } else {
532
+ diffCmd = `git diff -U0 -M ${compareTo}..HEAD -- ${joinAsShellArgs(paths)}`;
533
+ }
534
+ const { stdout: patchOutput } = await execPromise(diffCmd, {
535
+ cwd: repoRoot,
536
+ maxBuffer: 50 * 1024 * 1024
537
+ });
538
+ const patches = splitGitPatchPerFile(patchOutput);
539
+ patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
540
+ }
541
+ if (stagedEntries.length > 0) {
542
+ const paths = stagedEntries.map((e) => e.path);
543
+ const diffCmd = `git diff -U0 -M --cached -- ${joinAsShellArgs(paths)}`;
544
+ const { stdout: patchOutput } = await execPromise(diffCmd, {
545
+ cwd: repoRoot,
546
+ maxBuffer: 50 * 1024 * 1024
547
+ });
548
+ const patches = splitGitPatchPerFile(patchOutput);
549
+ patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
550
+ }
551
+ if (unstagedEntries.length > 0) {
552
+ const paths = unstagedEntries.map((e) => e.path);
553
+ const diffCmd = `git diff -U0 -M -- ${joinAsShellArgs(paths)}`;
554
+ const { stdout: patchOutput } = await execPromise(diffCmd, {
555
+ cwd: repoRoot,
556
+ maxBuffer: 50 * 1024 * 1024
557
+ });
558
+ const patches = splitGitPatchPerFile(patchOutput);
559
+ patches.forEach((patch, filename) => patchByFile.set(filename, stripDiffHeaders(patch)));
560
+ }
561
+ for (const entry of untrackedEntries) {
562
+ try {
563
+ const { stdout } = await execPromise(
564
+ `git diff -U0 --no-index /dev/null ${shellQuote(entry.path)}`,
565
+ { cwd: repoRoot, maxBuffer: 50 * 1024 * 1024 }
566
+ );
567
+ patchByFile.set(entry.path, stripDiffHeaders(stdout));
568
+ } catch (error) {
569
+ if (error.stdout) {
570
+ patchByFile.set(entry.path, stripDiffHeaders(error.stdout));
571
+ } else {
572
+ throw error;
573
+ }
574
+ }
575
+ }
576
+ const statsByFile = /* @__PURE__ */ new Map();
577
+ if (committedEntries.length > 0) {
578
+ const paths = committedEntries.map((e) => e.path);
579
+ let numstatCmd = "";
580
+ if (isRange) {
581
+ const [rangeA, rangeB] = parseRange(selector);
582
+ numstatCmd = `git diff --numstat ${shellQuote(rangeA)}..${shellQuote(rangeB)} -- ${joinAsShellArgs(paths)}`;
583
+ } else {
584
+ numstatCmd = `git diff --numstat ${compareTo}..HEAD -- ${joinAsShellArgs(paths)}`;
585
+ }
586
+ const { stdout: numstatOutput } = await execPromise(numstatCmd, {
587
+ cwd: repoRoot,
588
+ maxBuffer: 50 * 1024 * 1024
589
+ });
590
+ const lines = numstatOutput.split("\n").filter(Boolean);
591
+ for (const line of lines) {
283
592
  const parts = line.split(" ");
284
593
  if (parts.length >= 3) {
285
- const [additionsStr, deletionsStr, filename] = parts;
286
- fileStats.set(filename, {
287
- additions: parseInt(additionsStr) || 0,
288
- deletions: parseInt(deletionsStr) || 0
594
+ const [addStr, delStr, filename] = parts;
595
+ statsByFile.set(filename, {
596
+ additions: parseInt(addStr) || 0,
597
+ deletions: parseInt(delStr) || 0
289
598
  });
290
599
  }
291
- });
600
+ }
292
601
  }
293
- const fileDiffs = /* @__PURE__ */ new Map();
294
- if (nonDeletedFiles.length > 0) {
295
- const { stdout: batchDiffOutput } = await execPromise(
296
- `git diff ${mergeBase} -- ${nonDeletedFiles.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ")}`,
297
- { cwd: repoRoot }
298
- );
299
- const diffSections = batchDiffOutput.split(/^diff --git /m).filter(Boolean);
300
- diffSections.forEach((section) => {
301
- const lines = section.split("\n");
302
- const firstLine = lines[0];
303
- const match = firstLine.match(/a\/(.+?) b\//);
304
- if (match) {
305
- const filename = match[1];
306
- const diffContent = lines.slice(1).filter((line) => {
307
- return !(line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ "));
308
- }).join("\n");
309
- fileDiffs.set(filename, diffContent);
310
- }
602
+ if (stagedEntries.length > 0) {
603
+ const paths = stagedEntries.map((e) => e.path);
604
+ const numstatCmd = `git diff --numstat --cached -- ${joinAsShellArgs(paths)}`;
605
+ const { stdout: numstatOutput } = await execPromise(numstatCmd, {
606
+ cwd: repoRoot,
607
+ maxBuffer: 50 * 1024 * 1024
311
608
  });
609
+ const lines = numstatOutput.split("\n").filter(Boolean);
610
+ for (const line of lines) {
611
+ const parts = line.split(" ");
612
+ if (parts.length >= 3) {
613
+ const [addStr, delStr, filename] = parts;
614
+ statsByFile.set(filename, {
615
+ additions: parseInt(addStr) || 0,
616
+ deletions: parseInt(delStr) || 0
617
+ });
618
+ }
619
+ }
312
620
  }
313
- const deletedFileContents = /* @__PURE__ */ new Map();
314
- const actualDeletedFiles = allFiles.filter((file) => fileIsDeleted.get(file));
315
- if (actualDeletedFiles.length > 0) {
316
- const deletedFilePromises = actualDeletedFiles.map(async (file) => {
317
- const { stdout: lastContent } = await execPromise(
318
- `git show '${mergeBase}:${file.replace(/'/g, "'\\''")}'`,
319
- {
320
- cwd: repoRoot
321
- }
322
- );
323
- return { file, content: lastContent };
621
+ if (unstagedEntries.length > 0) {
622
+ const paths = unstagedEntries.map((e) => e.path);
623
+ const numstatCmd = `git diff --numstat -- ${joinAsShellArgs(paths)}`;
624
+ const { stdout: numstatOutput } = await execPromise(numstatCmd, {
625
+ cwd: repoRoot,
626
+ maxBuffer: 50 * 1024 * 1024
324
627
  });
325
- const results = await Promise.allSettled(deletedFilePromises);
326
- results.forEach((result, index) => {
327
- if (result.status === "fulfilled") {
328
- deletedFileContents.set(actualDeletedFiles[index], result.value.content);
628
+ const lines = numstatOutput.split("\n").filter(Boolean);
629
+ for (const line of lines) {
630
+ const parts = line.split(" ");
631
+ if (parts.length >= 3) {
632
+ const [addStr, delStr, filename] = parts;
633
+ statsByFile.set(filename, {
634
+ additions: parseInt(addStr) || 0,
635
+ deletions: parseInt(delStr) || 0
636
+ });
329
637
  }
330
- });
638
+ }
331
639
  }
332
- const fileChanges = [];
333
- for (const file of allFiles) {
334
- const isDeleted = fileIsDeleted.get(file);
640
+ for (const entry of untrackedEntries) {
641
+ const patch = patchByFile.get(entry.path) || "";
642
+ const lines = patch.split("\n");
335
643
  let additions = 0;
336
644
  let deletions = 0;
337
- let diffOutput2 = "";
338
- if (isDeleted) {
339
- const lastContent = deletedFileContents.get(file);
340
- if (lastContent) {
341
- deletions = lastContent.split("\n").length;
342
- diffOutput2 = lastContent.split("\n").map((line) => `-${line}`).join("\n");
343
- }
344
- } else {
345
- const stats = fileStats.get(file);
346
- if (stats) {
347
- additions = stats.additions;
348
- deletions = stats.deletions;
645
+ for (const line of lines) {
646
+ if (line.startsWith("+") && !line.startsWith("+++")) {
647
+ additions++;
648
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
649
+ deletions++;
349
650
  }
350
- diffOutput2 = fileDiffs.get(file) || "";
351
651
  }
352
- const status = isDeleted ? "removed" : additions > 0 && deletions === 0 ? "added" : "modified";
353
- fileChanges.push({
354
- filename: file,
652
+ statsByFile.set(entry.path, { additions, deletions });
653
+ }
654
+ const fileChanges = [];
655
+ for (const entry of entries) {
656
+ const patch = patchByFile.get(entry.path) || "";
657
+ const stats = statsByFile.get(entry.path) || { additions: 0, deletions: 0 };
658
+ const status = toChangeStatus(entry);
659
+ const fileChange = {
660
+ filename: entry.path,
355
661
  status,
356
- patch: diffOutput2,
357
- additions,
358
- deletions,
359
- sha: hashString(diffOutput2)
360
- });
662
+ patch,
663
+ additions: stats.additions,
664
+ deletions: stats.deletions,
665
+ sha: hashString(patch)
666
+ };
667
+ if (entry.oldPath) {
668
+ fileChange.oldFilename = entry.oldPath;
669
+ }
670
+ fileChanges.push(fileChange);
361
671
  }
362
672
  return {
363
673
  files: fileChanges,
364
674
  currentBranch,
365
675
  currentCommit,
366
- diffCommit: mergeBase,
367
- diffBranch: compareTo
676
+ diffBranch: parentRef,
677
+ diffCommit: compareTo
368
678
  };
369
679
  }
370
680
 
@@ -808,7 +1118,7 @@ var ExecutionEventEmitter = class extends EventEmitter {
808
1118
  import * as fs2 from "fs";
809
1119
  import * as path2 from "path";
810
1120
  import { glob } from "glob";
811
- import { minimatch } from "minimatch";
1121
+ import ignore from "ignore";
812
1122
  var FileExecutionContext = class _FileExecutionContext {
813
1123
  environment;
814
1124
  _filePaths = [];
@@ -825,9 +1135,33 @@ var FileExecutionContext = class _FileExecutionContext {
825
1135
  /**
826
1136
  * Create and initialize an ExecutionContext
827
1137
  */
828
- static async initialize(config, environment, mode, eventEmitter, filePath, baseSha) {
1138
+ static async initialize(config, environment, mode, eventEmitter, filePath, diffOptions) {
829
1139
  const context = new _FileExecutionContext(config, environment, mode, eventEmitter);
830
- const initialFiles = await context.discoverFiles(filePath, baseSha);
1140
+ if (mode === "diff" && !filePath) {
1141
+ const workspaceRoot = context.environment.getWorkspaceRoot();
1142
+ const include = diffOptions == null ? void 0 : diffOptions.include;
1143
+ const commitSelector = diffOptions == null ? void 0 : diffOptions.commitSelector;
1144
+ if (!commitSelector || !include)
1145
+ throw new Error("Commit selector and include are required in diff mode");
1146
+ const gitChanges = await getChangedFiles(workspaceRoot, {
1147
+ include,
1148
+ commitSelector
1149
+ }).catch(() => {
1150
+ throw new Error(
1151
+ "Diff mode requires a git repository. Please run this command from within a git repository."
1152
+ );
1153
+ });
1154
+ context.diffMode = {
1155
+ gitChanges,
1156
+ changedFiles: gitChanges.files.map((f) => f.filename),
1157
+ fileChangeMap: new Map(gitChanges.files.map((file) => [file.filename, file]))
1158
+ };
1159
+ context.eventEmitter.fileDiscoveryProgress(
1160
+ `Found ${context.diffMode.changedFiles.length} changed files`
1161
+ );
1162
+ }
1163
+ const gitIgnoredFiles = mode === "check" ? await context.loadGitIgnoredFiles() : /* @__PURE__ */ new Set();
1164
+ const initialFiles = await context.discoverFiles(filePath, gitIgnoredFiles);
831
1165
  context._filePaths = initialFiles;
832
1166
  return context;
833
1167
  }
@@ -843,8 +1177,6 @@ var FileExecutionContext = class _FileExecutionContext {
843
1177
  let filteredFiles;
844
1178
  if (this.mode === "check") {
845
1179
  filteredFiles = filePaths;
846
- } else if (!this.diffMode) {
847
- filteredFiles = filePaths;
848
1180
  } else {
849
1181
  filteredFiles = filePaths.filter((filePath) => this.isFileValid({ filePath }));
850
1182
  }
@@ -881,6 +1213,27 @@ var FileExecutionContext = class _FileExecutionContext {
881
1213
  get executionMode() {
882
1214
  return this.mode;
883
1215
  }
1216
+ // ===== Git Ignored Files =====
1217
+ /**
1218
+ * Load git-ignored files from the repository
1219
+ * Uses: git ls-files --ignored --exclude-standard --others
1220
+ *
1221
+ * Only called in "check" mode - in "diff" mode, git automatically excludes ignored files.
1222
+ * Returns a Set for efficient lookup, which can be discarded after file discovery.
1223
+ * If not in a git repository, returns an empty Set.
1224
+ */
1225
+ async loadGitIgnoredFiles() {
1226
+ const workspaceRoot = this.environment.getWorkspaceRoot();
1227
+ const ignoredFiles = await getGitIgnoredFiles(workspaceRoot).catch(() => {
1228
+ this.eventEmitter.fileDiscoveryProgress("Not in a git repository, skipping git-ignored files");
1229
+ return [];
1230
+ });
1231
+ const ignoredFilesSet = new Set(ignoredFiles);
1232
+ if (ignoredFiles.length > 0) {
1233
+ this.eventEmitter.fileDiscoveryProgress(`Found ${ignoredFiles.length} git-ignored files`);
1234
+ }
1235
+ return ignoredFilesSet;
1236
+ }
884
1237
  // ===== File Discovery =====
885
1238
  /**
886
1239
  * Discover files based on the execution mode
@@ -892,38 +1245,23 @@ var FileExecutionContext = class _FileExecutionContext {
892
1245
  * - A directory path (e.g., "src/components/")
893
1246
  * - A glob pattern (e.g., ".ts")
894
1247
  */
895
- async discoverFiles(filePath, baseSha) {
1248
+ async discoverFiles(filePath, gitIgnoredFiles) {
896
1249
  this.eventEmitter.startFileDiscovery(this.mode);
897
1250
  let discoveredFiles;
898
1251
  if (filePath) {
899
- discoveredFiles = await this.discoverFilesFromPath(filePath);
1252
+ discoveredFiles = await this.discoverFilesFromPath(filePath, gitIgnoredFiles);
900
1253
  } else if (this.mode === "diff") {
901
1254
  const workspaceRoot = this.environment.getWorkspaceRoot();
902
- if (baseSha) {
903
- this.eventEmitter.fileDiscoveryProgress(`Getting changed files from ${baseSha}...`);
904
- } else {
905
- this.eventEmitter.fileDiscoveryProgress("Getting changed files from git...");
906
- }
907
- const gitChanges = await getChangedFiles(workspaceRoot, baseSha);
908
- this.diffMode = {
909
- gitChanges,
910
- changedFiles: gitChanges.files.map((f) => f.filename),
911
- fileChangeMap: new Map(gitChanges.files.map((file) => [file.filename, file]))
912
- };
913
- this.eventEmitter.fileDiscoveryProgress(
914
- `Found ${this.diffMode.changedFiles.length} changed files`
915
- );
916
- discoveredFiles = this.diffMode.changedFiles.filter((file) => fs2.existsSync(path2.resolve(workspaceRoot, file))).map((file) => file);
917
- this.eventEmitter.fileDiscoveryProgress("Applying ignore patterns...");
1255
+ discoveredFiles = this.diffMode.changedFiles.filter(
1256
+ (file) => fs2.existsSync(path2.resolve(workspaceRoot, file))
1257
+ ).map((file) => file);
918
1258
  const allIgnorePatterns = this.config.getIgnoredGlobs();
919
1259
  if (allIgnorePatterns.length > 0) {
1260
+ this.eventEmitter.fileDiscoveryProgress("Applying ignore patterns...");
920
1261
  const beforeIgnore = discoveredFiles.length;
921
- discoveredFiles = discoveredFiles.filter((filePath2) => {
922
- const matchesIgnore = allIgnorePatterns.some(
923
- (pattern) => this.matchesPattern(filePath2, pattern)
924
- );
925
- return !matchesIgnore;
926
- });
1262
+ discoveredFiles = discoveredFiles.filter(
1263
+ (filePath2) => !this.matchesAnyPattern(filePath2, allIgnorePatterns)
1264
+ );
927
1265
  if (beforeIgnore !== discoveredFiles.length) {
928
1266
  this.eventEmitter.fileDiscoveryProgress(
929
1267
  `Filtered out ${beforeIgnore - discoveredFiles.length} ignored files`
@@ -934,7 +1272,11 @@ var FileExecutionContext = class _FileExecutionContext {
934
1272
  this.eventEmitter.fileDiscoveryProgress("Scanning workspace for files...");
935
1273
  const workspaceRoot = this.environment.getWorkspaceRoot();
936
1274
  const allIgnorePatterns = this.config.getIgnoredGlobs();
937
- discoveredFiles = await this.discoverAllFiles([workspaceRoot], allIgnorePatterns);
1275
+ discoveredFiles = await this.discoverAllFiles(
1276
+ [workspaceRoot],
1277
+ allIgnorePatterns,
1278
+ gitIgnoredFiles
1279
+ );
938
1280
  }
939
1281
  this.eventEmitter.completeFileDiscovery(discoveredFiles.length, this.mode);
940
1282
  return discoveredFiles;
@@ -942,7 +1284,7 @@ var FileExecutionContext = class _FileExecutionContext {
942
1284
  /**
943
1285
  * Discover files from a given path which can be a file, directory, or glob pattern
944
1286
  */
945
- async discoverFilesFromPath(filePath) {
1287
+ async discoverFilesFromPath(filePath, gitIgnoredFiles) {
946
1288
  const workspaceRoot = this.environment.getWorkspaceRoot();
947
1289
  const fullPath = path2.resolve(workspaceRoot, filePath);
948
1290
  const allIgnorePatterns = this.config.getIgnoredGlobs();
@@ -950,7 +1292,7 @@ var FileExecutionContext = class _FileExecutionContext {
950
1292
  this.eventEmitter.fileDiscoveryProgress(
951
1293
  `Discovering files matching glob pattern: ${filePath}`
952
1294
  );
953
- return await this.discoverFilesFromGlob(filePath, allIgnorePatterns);
1295
+ return await this.discoverFilesFromGlob(filePath, allIgnorePatterns, gitIgnoredFiles);
954
1296
  }
955
1297
  if (!fs2.existsSync(fullPath)) {
956
1298
  throw new Error(`Path not found: ${filePath}`);
@@ -958,10 +1300,11 @@ var FileExecutionContext = class _FileExecutionContext {
958
1300
  const stats = fs2.statSync(fullPath);
959
1301
  if (stats.isFile()) {
960
1302
  this.eventEmitter.fileDiscoveryProgress(`Checking specific file: ${filePath}`);
961
- const matchesIgnore = allIgnorePatterns.some(
962
- (pattern) => this.matchesPattern(filePath, pattern)
963
- );
964
- if (matchesIgnore) {
1303
+ if (gitIgnoredFiles.has(filePath)) {
1304
+ this.eventEmitter.fileDiscoveryProgress(`File ${filePath} is ignored by git`);
1305
+ return [];
1306
+ }
1307
+ if (this.matchesAnyPattern(filePath, allIgnorePatterns)) {
965
1308
  this.eventEmitter.fileDiscoveryProgress(`File ${filePath} is ignored by patterns`);
966
1309
  return [];
967
1310
  } else {
@@ -969,7 +1312,7 @@ var FileExecutionContext = class _FileExecutionContext {
969
1312
  }
970
1313
  } else if (stats.isDirectory()) {
971
1314
  this.eventEmitter.fileDiscoveryProgress(`Discovering files in directory: ${filePath}`);
972
- return await this.discoverFilesFromDirectory(filePath, allIgnorePatterns);
1315
+ return await this.discoverFilesFromDirectory(filePath, allIgnorePatterns, gitIgnoredFiles);
973
1316
  } else {
974
1317
  throw new Error(`Path is neither a file nor directory: ${filePath}`);
975
1318
  }
@@ -983,7 +1326,7 @@ var FileExecutionContext = class _FileExecutionContext {
983
1326
  /**
984
1327
  * Discover files matching a glob pattern
985
1328
  */
986
- async discoverFilesFromGlob(globPattern, ignorePatterns) {
1329
+ async discoverFilesFromGlob(globPattern, ignorePatterns, gitIgnoredFiles) {
987
1330
  const workspaceRoot = this.environment.getWorkspaceRoot();
988
1331
  const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
989
1332
  const matches = await glob(globPattern, {
@@ -992,13 +1335,16 @@ var FileExecutionContext = class _FileExecutionContext {
992
1335
  absolute: false,
993
1336
  ignore: allIgnorePatterns
994
1337
  });
995
- this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files matching pattern`);
996
- return matches;
1338
+ const filteredMatches = matches.filter((match) => !gitIgnoredFiles.has(match));
1339
+ this.eventEmitter.fileDiscoveryProgress(
1340
+ `Found ${filteredMatches.length} files matching pattern`
1341
+ );
1342
+ return filteredMatches;
997
1343
  }
998
1344
  /**
999
1345
  * Discover all files in a directory
1000
1346
  */
1001
- async discoverFilesFromDirectory(dirPath, ignorePatterns) {
1347
+ async discoverFilesFromDirectory(dirPath, ignorePatterns, gitIgnoredFiles) {
1002
1348
  const workspaceRoot = this.environment.getWorkspaceRoot();
1003
1349
  const globPattern = path2.join(dirPath, "**/*").replace(/\\/g, "/");
1004
1350
  const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
@@ -1008,22 +1354,25 @@ var FileExecutionContext = class _FileExecutionContext {
1008
1354
  absolute: false,
1009
1355
  ignore: allIgnorePatterns
1010
1356
  });
1011
- this.eventEmitter.fileDiscoveryProgress(`Found ${matches.length} files in directory`);
1012
- return matches;
1357
+ const filteredMatches = matches.filter((match) => !gitIgnoredFiles.has(match));
1358
+ this.eventEmitter.fileDiscoveryProgress(`Found ${filteredMatches.length} files in directory`);
1359
+ return filteredMatches;
1013
1360
  }
1014
1361
  /**
1015
1362
  * Discover all files from directories using glob patterns (scan mode)
1016
1363
  */
1017
- async discoverAllFiles(directories, ignorePatterns) {
1364
+ async discoverAllFiles(directories, ignorePatterns, gitIgnoredFiles) {
1018
1365
  const allFiles = [];
1019
1366
  const workspaceRoot = this.environment.getWorkspaceRoot();
1020
1367
  const allIgnorePatterns = ["**/node_modules/**", "**/.git/**", ...ignorePatterns];
1021
1368
  for (const dir of directories) {
1022
1369
  const stats = fs2.statSync(dir);
1023
1370
  if (!stats.isDirectory()) {
1024
- const shouldIgnore = ignorePatterns.some((pattern) => this.matchesPattern(dir, pattern));
1025
- if (!shouldIgnore) {
1026
- allFiles.push(path2.relative(workspaceRoot, dir));
1371
+ const relativePath = path2.relative(workspaceRoot, dir);
1372
+ const isGitIgnored = gitIgnoredFiles.has(relativePath);
1373
+ const shouldIgnore = this.matchesAnyPattern(relativePath, ignorePatterns);
1374
+ if (!isGitIgnored && !shouldIgnore) {
1375
+ allFiles.push(relativePath);
1027
1376
  }
1028
1377
  continue;
1029
1378
  }
@@ -1037,16 +1386,18 @@ var FileExecutionContext = class _FileExecutionContext {
1037
1386
  const relativePaths = matches.map((match) => {
1038
1387
  const absolutePath = path2.resolve(dir, match);
1039
1388
  return path2.relative(workspaceRoot, absolutePath);
1040
- });
1389
+ }).filter((relativePath) => !gitIgnoredFiles.has(relativePath));
1041
1390
  allFiles.push(...relativePaths);
1042
1391
  }
1043
1392
  return [...new Set(allFiles)];
1044
1393
  }
1045
1394
  /**
1046
- * Pattern matching function that uses consistent options
1395
+ * Check if a file matches any of the given patterns using the ignore package
1047
1396
  */
1048
- matchesPattern(filePath, pattern) {
1049
- return minimatch(filePath, pattern, { dot: true });
1397
+ matchesAnyPattern(filePath, patterns) {
1398
+ if (patterns.length === 0) return false;
1399
+ const ig = ignore().add(patterns);
1400
+ return ig.ignores(filePath);
1050
1401
  }
1051
1402
  /**
1052
1403
  * Check if a file should be processed
@@ -1055,9 +1406,6 @@ var FileExecutionContext = class _FileExecutionContext {
1055
1406
  if (this.mode === "check") {
1056
1407
  return true;
1057
1408
  }
1058
- if (!this.diffMode) {
1059
- return true;
1060
- }
1061
1409
  const { filePath } = options;
1062
1410
  return this.diffMode.changedFiles.some((changedFile) => {
1063
1411
  return filePath === changedFile || filePath.endsWith(changedFile) || changedFile.endsWith(filePath);
@@ -1070,9 +1418,6 @@ var FileExecutionContext = class _FileExecutionContext {
1070
1418
  if (this.mode === "check") {
1071
1419
  return true;
1072
1420
  }
1073
- if (!this.diffMode) {
1074
- return true;
1075
- }
1076
1421
  const { filePath, startLine, endLine } = options;
1077
1422
  const fileChange = this.diffMode.fileChangeMap.get(filePath);
1078
1423
  if (!fileChange) {
@@ -1093,9 +1438,6 @@ var FileExecutionContext = class _FileExecutionContext {
1093
1438
  if (this.mode === "check") {
1094
1439
  return true;
1095
1440
  }
1096
- if (!this.diffMode) {
1097
- return true;
1098
- }
1099
1441
  const { match } = options;
1100
1442
  return this.isFileValid({ filePath: match.filePath }) && this.isLineRangeValid({
1101
1443
  filePath: match.filePath,
@@ -1142,20 +1484,19 @@ var FileExecutionContext = class _FileExecutionContext {
1142
1484
  // src/steps/FileFilterStep.ts
1143
1485
  import * as fs3 from "fs";
1144
1486
  import * as path3 from "path";
1145
- import { minimatch as minimatch2 } from "minimatch";
1487
+ import ignore2 from "ignore";
1146
1488
  var FileFilterStep = class {
1147
1489
  environment;
1148
1490
  constructor(environment) {
1149
1491
  this.environment = environment;
1150
1492
  }
1151
1493
  /**
1152
- * Centralized pattern matching function that uses consistent options
1153
- * @param path - The path to test
1154
- * @param pattern - The glob pattern to match against
1155
- * @returns true if the path matches the pattern
1494
+ * Check if a file matches any of the given patterns using the ignore package
1156
1495
  */
1157
- matchesPattern(path11, pattern) {
1158
- return minimatch2(path11, pattern, { dot: true });
1496
+ matchesAnyPattern(filePath, patterns) {
1497
+ if (patterns.length === 0) return false;
1498
+ const ig = ignore2().add(patterns);
1499
+ return ig.ignores(filePath);
1159
1500
  }
1160
1501
  /**
1161
1502
  * Evaluate a single file filter condition for a given file path
@@ -1169,10 +1510,6 @@ var FileFilterStep = class {
1169
1510
  const siblingPath = path3.join(dir, filename);
1170
1511
  return fs3.existsSync(siblingPath);
1171
1512
  }
1172
- if ("fs.pathMatches" in condition) {
1173
- const { pattern } = condition["fs.pathMatches"];
1174
- return this.matchesPattern(filePath, pattern);
1175
- }
1176
1513
  if ("fs.ancestorHas" in condition) {
1177
1514
  const { filename } = condition["fs.ancestorHas"];
1178
1515
  let currentDir = path3.dirname(absoluteFilePath);
@@ -1195,7 +1532,7 @@ var FileFilterStep = class {
1195
1532
  return false;
1196
1533
  }
1197
1534
  const siblings = fs3.readdirSync(dir);
1198
- return siblings.some((sibling) => this.matchesPattern(sibling, pattern));
1535
+ return siblings.some((sibling) => this.matchesAnyPattern(sibling, [pattern]));
1199
1536
  }
1200
1537
  return false;
1201
1538
  }
@@ -1204,13 +1541,13 @@ var FileFilterStep = class {
1204
1541
  */
1205
1542
  evaluateConditions(conditions, filePath) {
1206
1543
  const results = [];
1207
- if (conditions.all) {
1544
+ if (conditions.all && conditions.all.length > 0) {
1208
1545
  results.push(conditions.all.every((condition) => this.evaluateCondition(condition, filePath)));
1209
1546
  }
1210
- if (conditions.any) {
1547
+ if (conditions.any && conditions.any.length > 0) {
1211
1548
  results.push(conditions.any.some((condition) => this.evaluateCondition(condition, filePath)));
1212
1549
  }
1213
- if (conditions.not) {
1550
+ if (conditions.not && conditions.not.length > 0) {
1214
1551
  results.push(!conditions.not.some((condition) => this.evaluateCondition(condition, filePath)));
1215
1552
  }
1216
1553
  return results.length === 0 ? true : results.every((result) => result === true);
@@ -1224,18 +1561,15 @@ var FileFilterStep = class {
1224
1561
  let filteredPaths = filePaths;
1225
1562
  if ((_a = options.include) == null ? void 0 : _a.length) {
1226
1563
  filteredPaths = filteredPaths.filter(
1227
- (filePath) => options.include.some((pattern) => this.matchesPattern(filePath, pattern))
1564
+ (filePath) => this.matchesAnyPattern(filePath, options.include)
1228
1565
  );
1229
1566
  }
1230
1567
  if ((_b = options.ignore) == null ? void 0 : _b.length) {
1231
- filteredPaths = filteredPaths.filter((filePath) => {
1232
- const matchesIgnore = options.ignore.some(
1233
- (pattern) => this.matchesPattern(filePath, pattern)
1234
- );
1235
- return !matchesIgnore;
1236
- });
1568
+ filteredPaths = filteredPaths.filter(
1569
+ (filePath) => !this.matchesAnyPattern(filePath, options.ignore)
1570
+ );
1237
1571
  }
1238
- if (options.conditions) {
1572
+ if (Object.keys(options.conditions || {}).length > 0 && filteredPaths.length > 0) {
1239
1573
  filteredPaths = filteredPaths.filter(
1240
1574
  (filePath) => this.evaluateConditions(options.conditions, filePath)
1241
1575
  );
@@ -1252,7 +1586,7 @@ var FileFilterStep = class {
1252
1586
  import path8 from "path";
1253
1587
 
1254
1588
  // src/languages.ts
1255
- import { existsSync as existsSync3 } from "fs";
1589
+ import { existsSync as existsSync4 } from "fs";
1256
1590
  import { createRequire } from "module";
1257
1591
  import path4 from "path";
1258
1592
  import angular from "@ast-grep/lang-angular";
@@ -1290,15 +1624,15 @@ var require2 = createRequire(import.meta.url ? import.meta.url : __filename);
1290
1624
  function getGraphQLLibPath() {
1291
1625
  const graphqlDir = path4.dirname(require2.resolve("tree-sitter-graphql"));
1292
1626
  const releaseNode = path4.join(graphqlDir, "../../build/Release/tree_sitter_graphql_binding.node");
1293
- if (existsSync3(releaseNode)) {
1627
+ if (existsSync4(releaseNode)) {
1294
1628
  return releaseNode;
1295
1629
  }
1296
1630
  const debugNode = path4.join(graphqlDir, "../../build/Debug/tree_sitter_graphql_binding.node");
1297
- if (existsSync3(debugNode)) {
1631
+ if (existsSync4(debugNode)) {
1298
1632
  return debugNode;
1299
1633
  }
1300
1634
  const soFile = path4.join(graphqlDir, "parser.so");
1301
- if (existsSync3(soFile)) {
1635
+ if (existsSync4(soFile)) {
1302
1636
  return soFile;
1303
1637
  }
1304
1638
  return null;
@@ -2130,7 +2464,7 @@ var FindMatchesStep = class {
2130
2464
 
2131
2465
  // src/steps/GotoDefinitionStep.ts
2132
2466
  import path9 from "path";
2133
- import { minimatch as minimatch3 } from "minimatch";
2467
+ import ignore3 from "ignore";
2134
2468
  var GotoDefinitionStep = class {
2135
2469
  maxDepth = 1;
2136
2470
  environment;
@@ -2297,7 +2631,8 @@ var GotoDefinitionStep = class {
2297
2631
  return relativePath === spec.path;
2298
2632
  }
2299
2633
  if (spec.glob) {
2300
- return minimatch3(relativePath, spec.glob);
2634
+ const ig = ignore3().add(spec.glob);
2635
+ return ig.ignores(relativePath);
2301
2636
  }
2302
2637
  if (spec.regex) {
2303
2638
  const regex = new RegExp(spec.regex);
@@ -2643,7 +2978,7 @@ var RuleExecutor = class {
2643
2978
  options.mode,
2644
2979
  this.eventEmitter,
2645
2980
  options.filePath,
2646
- options.baseSha
2981
+ options.diffOptions
2647
2982
  );
2648
2983
  this.currentMode = options.mode;
2649
2984
  }
@@ -2732,9 +3067,9 @@ var SKIPPED_COLOR = "#9b59b6";
2732
3067
  var BRAND_COLOR = "#fbbf24";
2733
3068
  function formatClickableRuleId(ruleId, internalId) {
2734
3069
  if (internalId) {
2735
- return chalk.underline.dim(
2736
- `\x1B]8;;https://app.wispbit.com/rules/${internalId}\x1B\\${ruleId}\x1B]8;;\x1B\\`
2737
- );
3070
+ const url = `https://app.wispbit.com/rules/${internalId}`;
3071
+ const link = `\x1B]8;;${url}\x07${chalk.dim.underline(ruleId)}\x1B]8;;\x07`;
3072
+ return link;
2738
3073
  }
2739
3074
  return chalk.dim(ruleId);
2740
3075
  }
@@ -2855,12 +3190,12 @@ function printSummary(results, summary) {
2855
3190
  `;
2856
3191
  } else {
2857
3192
  const sortedMatches = ruleData.matches.sort((a, b) => a.filePath.localeCompare(b.filePath));
2858
- const shouldShowCodeSnippets = sortedMatches.length < 10 && sortedMatches.some((match) => match.text && match.text.split("\n").length <= 10);
3193
+ const shouldShowCodeSnippets = sortedMatches.length < 10 && sortedMatches.some((match) => match.text && match.text.split("\n").length <= 20);
2859
3194
  if (shouldShowCodeSnippets) {
2860
3195
  sortedMatches.forEach((match, index) => {
2861
3196
  output += ` ${match.filePath}
2862
3197
  `;
2863
- if (match.text && match.text.split("\n").length <= 10) {
3198
+ if (match.text && match.text.split("\n").length <= 20) {
2864
3199
  const lines = match.text.split("\n");
2865
3200
  lines.forEach((line, lineIndex) => {
2866
3201
  const lineNumber = match.line + lineIndex;
@@ -2888,24 +3223,22 @@ function printSummary(results, summary) {
2888
3223
  `;
2889
3224
  }
2890
3225
  });
3226
+ console.log(output);
2891
3227
  const total = violationCount + suggestionCount;
2892
- if (total === 0) {
2893
- console.log(`
2894
- no results`);
2895
- }
2896
3228
  const violationText = violationCount > 0 ? `${chalk.dim("violations".padStart(12))} ${chalk.hex(VIOLATION_COLOR)("\u25A0")} ${chalk.hex(VIOLATION_COLOR).bold(violationCount)}` : `${chalk.dim("violations".padStart(12))} ${chalk.dim(violationCount)}`;
2897
3229
  const suggestionText = suggestionCount > 0 ? `${chalk.dim("suggestions".padStart(12))} ${chalk.hex(SUGGESTION_COLOR)("\u25CF")} ${chalk.hex(SUGGESTION_COLOR).bold(suggestionCount)}` : `${chalk.dim("suggestions".padStart(12))} ${chalk.dim(suggestionCount)}`;
2898
3230
  const filesText = summary.totalFiles ? `${summary.totalFiles} ${pluralize("file", summary.totalFiles)}` : "0 files";
2899
3231
  const rulesText = `${summary.totalRules} ${pluralize("rule", summary.totalRules)}`;
2900
3232
  const timeText = summary.executionTime ? `${Math.round(summary.executionTime)}ms` : "";
2901
3233
  const detailsText = [filesText, rulesText, timeText].filter(Boolean).join(", ");
2902
- output += `${violationText}
3234
+ let summaryOutput = "";
3235
+ summaryOutput += `${violationText}
2903
3236
  `;
2904
- output += `${suggestionText}
3237
+ summaryOutput += `${suggestionText}
2905
3238
  `;
2906
- output += `${chalk.dim("summary".padStart(12))} ${detailsText}
3239
+ summaryOutput += `${chalk.dim("summary".padStart(12))} ${detailsText}
2907
3240
  `;
2908
- console.log(total > 0 ? chalk.reset(output) : output);
3241
+ console.log(total > 0 ? chalk.reset(summaryOutput) : summaryOutput);
2909
3242
  if (violationCount > 0) {
2910
3243
  process.exit(1);
2911
3244
  }
@@ -3406,13 +3739,13 @@ https://wispbit.com/
3406
3739
  Usage:
3407
3740
  $ wispbit [diff-options] (run diff by default)
3408
3741
  $ wispbit check [file-path/directory] [check-options]
3409
- $ wispbit diff [diff-options]
3742
+ $ wispbit diff [commit] [diff-options]
3410
3743
  $ wispbit list
3411
3744
  $ wispbit cache purge
3412
3745
 
3413
3746
  Commands:
3414
- check [file-path/directory] Run linting rules against a specific file/folder or entire codebase
3415
- diff Run linting rules only on changed files in the current PR
3747
+ check [file-path/directory] Run linting rules against a specific file/folder or entire codebase
3748
+ diff [commit] Run linting rules on changed files
3416
3749
  list List all available rules with their ID, message, and severity
3417
3750
  cache purge Purge the cache directory (indexes, caching, etc.)
3418
3751
 
@@ -3424,10 +3757,21 @@ Options for check:
3424
3757
  Options for diff:
3425
3758
  --rule <ruleId> Optional rule ID to run specific rule
3426
3759
  --json [format] Output in JSON format (pretty, stream, compact)
3427
- --base <commit> Base commit/branch/SHA to compare against (defaults to origin/main)
3428
- --head <commit> Head commit/branch/SHA to compare against (defaults to current branch)
3760
+ --include <types> Include change types (comma-separated): committed, staged, unstaged, untracked
3761
+ \u2022 committed: All committed changes (modified, deleted, renamed)
3762
+ \u2022 staged: Only staged changes ready to commit
3763
+ \u2022 unstaged: Only unstaged tracked file modifications
3764
+ \u2022 untracked: Only new untracked files
3765
+ (default: all four types)
3429
3766
  -d, --debug Enable debug output
3430
3767
 
3768
+ Diff examples:
3769
+ $ wispbit diff Show all changes (default: committed,staged,unstaged,untracked)
3770
+ $ wispbit diff --include staged Show only staged changes
3771
+ $ wispbit diff --include staged,untracked Show staged and untracked changes
3772
+ $ wispbit diff HEAD~3 Compare working directory against 3 commits ago
3773
+ $ wispbit diff main..HEAD Compare against main branch including worktree changes
3774
+ $ wispbit abc123 --include committed Compare against specific commit SHA, show only committed
3431
3775
 
3432
3776
  Global options:
3433
3777
  -v, --version Show version number
@@ -3443,10 +3787,8 @@ Global options:
3443
3787
  json: {
3444
3788
  type: "string"
3445
3789
  },
3446
- base: {
3447
- type: "string"
3448
- },
3449
- head: {
3790
+ // Diff include
3791
+ include: {
3450
3792
  type: "string"
3451
3793
  },
3452
3794
  // Debug option
@@ -3472,14 +3814,43 @@ async function executeCommand(options) {
3472
3814
  const environment = new Environment();
3473
3815
  const config = await ensureConfigured();
3474
3816
  const { ruleId, json: json2, mode, filePath } = options;
3817
+ let { commitSelector, diffInclude } = options;
3818
+ if (mode === "diff") {
3819
+ const repoRoot = environment.getWorkspaceRoot();
3820
+ if (!commitSelector) {
3821
+ const defaults = await getDefaultCommitSelector(repoRoot);
3822
+ commitSelector = defaults.commitSelector;
3823
+ if (!diffInclude) {
3824
+ diffInclude = defaults.include;
3825
+ }
3826
+ } else {
3827
+ if (!diffInclude) {
3828
+ const defaults = await getDefaultCommitSelector(repoRoot);
3829
+ diffInclude = defaults.include;
3830
+ }
3831
+ }
3832
+ }
3833
+ if (mode === "diff" && commitSelector && diffInclude) {
3834
+ const repoRoot = environment.getWorkspaceRoot();
3835
+ const currentBranch = await getCurrentBranch(repoRoot);
3836
+ const validationError = validateWorktreeIncludes(commitSelector, diffInclude, currentBranch);
3837
+ if (validationError) {
3838
+ console.error(chalk4.red(validationError));
3839
+ process.exit(1);
3840
+ }
3841
+ }
3475
3842
  let jsonOutput = false;
3476
3843
  let jsonFormat = "pretty";
3477
- if (json2) {
3844
+ if (json2 !== void 0) {
3478
3845
  jsonOutput = true;
3479
- if (json2 === "stream") {
3480
- jsonFormat = "stream";
3481
- } else if (json2 === "compact") {
3482
- jsonFormat = "compact";
3846
+ if (typeof json2 === "string") {
3847
+ if (json2 === "stream") {
3848
+ jsonFormat = "stream";
3849
+ } else if (json2 === "compact") {
3850
+ jsonFormat = "compact";
3851
+ } else {
3852
+ jsonFormat = "pretty";
3853
+ }
3483
3854
  } else {
3484
3855
  jsonFormat = "pretty";
3485
3856
  }
@@ -3508,26 +3879,30 @@ async function executeCommand(options) {
3508
3879
  }
3509
3880
  throw error;
3510
3881
  }
3511
- if (!json2) {
3882
+ if (!jsonOutput) {
3512
3883
  if (mode === "check") {
3513
3884
  const modeBox = chalk4.bgHex("#eab308").black(" CHECK ");
3514
3885
  const targetInfo = chalk4.dim(` (${filePath || "all files"})`);
3515
3886
  console.log(`${modeBox}${targetInfo}`);
3516
3887
  } else {
3517
- const baseCommit = cli.flags.base || "origin/main";
3518
- const headCommit = cli.flags.head || "HEAD";
3519
3888
  const modeBox = chalk4.bgHex("#4a90e2").black(" DIFF ");
3520
- const branchInfo = chalk4.dim(` (${headCommit}..${baseCommit})`);
3521
- console.log(`${modeBox}${branchInfo}`);
3889
+ const truncatedRange = (commitSelector || "").replace(
3890
+ /\b[0-9a-f]{30,}\b/gi,
3891
+ (match) => match.substring(0, 7)
3892
+ );
3893
+ const includeInfo = diffInclude ? ` ${truncatedRange} ${chalk4.dim(`[${diffInclude.join("/")}]`)}` : ` ${truncatedRange}`;
3894
+ console.log(`${modeBox}${includeInfo}`);
3522
3895
  }
3523
3896
  }
3524
3897
  const eventEmitter = new ExecutionEventEmitter();
3525
3898
  if (mode === "check") {
3526
3899
  eventEmitter.setExecutionMode("check", { filePath });
3527
3900
  } else {
3528
- const baseCommit = cli.flags.base || "origin/main";
3529
- const headCommit = cli.flags.head || "current branch";
3530
- eventEmitter.setExecutionMode("diff", { baseCommit, headCommit });
3901
+ const includeDisplay = diffInclude ? diffInclude.join(", ") : "all";
3902
+ eventEmitter.setExecutionMode("diff", {
3903
+ baseCommit: commitSelector || "",
3904
+ headCommit: includeDisplay
3905
+ });
3531
3906
  }
3532
3907
  const cleanupTerminalReporter = !options.json ? setupTerminalReporter(eventEmitter, options.debug || false) : void 0;
3533
3908
  const executionStartTime = Date.now();
@@ -3539,7 +3914,10 @@ async function executeCommand(options) {
3539
3914
  const ruleResults = await ruleExecutor.execute(rules, {
3540
3915
  mode,
3541
3916
  filePath,
3542
- baseSha: cli.flags.base
3917
+ diffOptions: mode === "diff" ? {
3918
+ include: diffInclude,
3919
+ commitSelector
3920
+ } : void 0
3543
3921
  });
3544
3922
  const results = [];
3545
3923
  for (const ruleResult of ruleResults) {
@@ -3574,9 +3952,16 @@ async function executeCommand(options) {
3574
3952
  if (mode === "check") {
3575
3953
  executionModeInfo = { mode: "check", filePath };
3576
3954
  } else {
3577
- const baseCommit = cli.flags.base || "origin/main";
3578
- const headCommit = cli.flags.head || "current branch";
3579
- executionModeInfo = { mode: "diff", baseCommit, headCommit };
3955
+ const truncatedRange = (commitSelector || "").replace(
3956
+ /\b[0-9a-f]{30,}\b/gi,
3957
+ (match) => match.substring(0, 7)
3958
+ );
3959
+ const includeDisplay = diffInclude ? diffInclude.join(", ") : "all";
3960
+ executionModeInfo = {
3961
+ mode: "diff",
3962
+ baseCommit: truncatedRange,
3963
+ headCommit: includeDisplay
3964
+ };
3580
3965
  }
3581
3966
  const executionTime = Date.now() - executionStartTime;
3582
3967
  printSummary(results, {
@@ -3616,11 +4001,30 @@ async function main() {
3616
4001
  break;
3617
4002
  }
3618
4003
  case "diff": {
4004
+ let diffInclude;
4005
+ if (cli.flags.include) {
4006
+ const includeString = cli.flags.include;
4007
+ const includes = includeString.split(",").map((f) => f.trim());
4008
+ const validIncludes = ["committed", "staged", "unstaged", "untracked"];
4009
+ const invalidIncludes = includes.filter((f) => !validIncludes.includes(f));
4010
+ if (invalidIncludes.length > 0) {
4011
+ console.error(
4012
+ chalk4.red(
4013
+ `Invalid include(s): ${invalidIncludes.join(", ")}. Valid includes: ${validIncludes.join(", ")}`
4014
+ )
4015
+ );
4016
+ process.exit(1);
4017
+ }
4018
+ diffInclude = includes;
4019
+ }
4020
+ const commitSelector = cli.input[1];
3619
4021
  await executeCommand({
3620
4022
  ruleId: cli.flags.rule,
3621
4023
  json: cli.flags.json,
3622
4024
  debug: cli.flags.debug,
3623
- mode: "diff"
4025
+ mode: "diff",
4026
+ diffInclude,
4027
+ commitSelector
3624
4028
  });
3625
4029
  break;
3626
4030
  }
@@ -3645,18 +4049,31 @@ ${chalk4.green("Cache purged successfully.")} Removed ${result.deletedCount} ite
3645
4049
  break;
3646
4050
  }
3647
4051
  default: {
3648
- if (!command) {
3649
- await executeCommand({
3650
- ruleId: cli.flags.rule,
3651
- json: cli.flags.json,
3652
- debug: cli.flags.debug,
3653
- mode: "diff"
3654
- });
3655
- } else {
3656
- console.error(chalk4.red("Unknown command:"), command);
3657
- cli.showHelp();
3658
- process.exit(1);
4052
+ let diffInclude;
4053
+ if (cli.flags.include) {
4054
+ const includeString = cli.flags.include;
4055
+ const includes = includeString.split(",").map((f) => f.trim());
4056
+ const validIncludes = ["committed", "staged", "unstaged", "untracked"];
4057
+ const invalidIncludes = includes.filter((f) => !validIncludes.includes(f));
4058
+ if (invalidIncludes.length > 0) {
4059
+ console.error(
4060
+ chalk4.red(
4061
+ `Invalid include(s): ${invalidIncludes.join(", ")}. Valid includes: ${validIncludes.join(", ")}`
4062
+ )
4063
+ );
4064
+ process.exit(1);
4065
+ }
4066
+ diffInclude = includes;
3659
4067
  }
4068
+ const commitSelector = cli.input[0];
4069
+ await executeCommand({
4070
+ ruleId: cli.flags.rule,
4071
+ json: cli.flags.json,
4072
+ debug: cli.flags.debug,
4073
+ mode: "diff",
4074
+ diffInclude,
4075
+ commitSelector
4076
+ });
3660
4077
  break;
3661
4078
  }
3662
4079
  }