@lumy-pack/line-lore 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -20,6 +20,190 @@ var __copyProps = (to, from, except, desc) => {
20
20
  };
21
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
22
 
23
+ // src/cache/sharded-cache.ts
24
+ import {
25
+ mkdir,
26
+ readFile,
27
+ readdir,
28
+ rename,
29
+ rm,
30
+ writeFile
31
+ } from "fs/promises";
32
+ import { homedir } from "os";
33
+ import { join } from "path";
34
+ function getShardPrefix(key) {
35
+ return key.slice(0, 2).toLowerCase();
36
+ }
37
+ async function cleanupLegacyCache() {
38
+ const legacyDir = join(homedir(), ".line-lore", "cache");
39
+ try {
40
+ const entries = await readdir(legacyDir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (entry.isFile() && entry.name.endsWith(".json")) {
43
+ try {
44
+ await rm(join(legacyDir, entry.name), { force: true });
45
+ } catch {
46
+ }
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ var DEFAULT_CACHE_BASE, DEFAULT_MAX_ENTRIES_PER_SHARD, ShardedCache;
53
+ var init_sharded_cache = __esm({
54
+ "src/cache/sharded-cache.ts"() {
55
+ "use strict";
56
+ DEFAULT_CACHE_BASE = join(homedir(), ".line-lore", "cache");
57
+ DEFAULT_MAX_ENTRIES_PER_SHARD = 1e3;
58
+ ShardedCache = class {
59
+ baseDir;
60
+ maxEntriesPerShard;
61
+ enabled;
62
+ shards = /* @__PURE__ */ new Map();
63
+ constructor(namespace, options) {
64
+ const cacheBase = options?.cacheBase ?? DEFAULT_CACHE_BASE;
65
+ const repoId = options?.repoId ?? {
66
+ host: "_local",
67
+ owner: "_",
68
+ repo: "_default"
69
+ };
70
+ this.baseDir = join(
71
+ cacheBase,
72
+ repoId.host,
73
+ repoId.owner,
74
+ repoId.repo,
75
+ namespace
76
+ );
77
+ this.maxEntriesPerShard = options?.maxEntriesPerShard ?? DEFAULT_MAX_ENTRIES_PER_SHARD;
78
+ this.enabled = options?.enabled ?? true;
79
+ }
80
+ async get(key) {
81
+ if (!this.enabled) return null;
82
+ const data = await this.readShard(getShardPrefix(key));
83
+ const entry = data[key];
84
+ return entry?.value ?? null;
85
+ }
86
+ async has(key) {
87
+ if (!this.enabled) return false;
88
+ const data = await this.readShard(getShardPrefix(key));
89
+ return key in data;
90
+ }
91
+ set(key, value) {
92
+ if (!this.enabled) return Promise.resolve();
93
+ const prefix = getShardPrefix(key);
94
+ const state = this.getShardState(prefix);
95
+ state.writeQueue = state.writeQueue.then(() => this.doSet(prefix, key, value)).catch(() => {
96
+ });
97
+ return state.writeQueue;
98
+ }
99
+ delete(key) {
100
+ if (!this.enabled) return Promise.resolve(false);
101
+ const prefix = getShardPrefix(key);
102
+ const state = this.getShardState(prefix);
103
+ let deleted = false;
104
+ state.writeQueue = state.writeQueue.then(async () => {
105
+ const data = await this.readShard(prefix);
106
+ if (key in data) {
107
+ delete data[key];
108
+ state.store = data;
109
+ await this.writeShard(prefix, data);
110
+ deleted = true;
111
+ }
112
+ }).catch(() => {
113
+ });
114
+ return state.writeQueue.then(() => deleted);
115
+ }
116
+ async clear() {
117
+ this.shards.clear();
118
+ try {
119
+ await rm(this.baseDir, { recursive: true, force: true });
120
+ } catch {
121
+ }
122
+ }
123
+ async size() {
124
+ let total = 0;
125
+ try {
126
+ const files = await readdir(this.baseDir);
127
+ for (const file of files) {
128
+ if (!file.endsWith(".json")) continue;
129
+ const prefix = file.replace(".json", "");
130
+ const data = await this.readShard(prefix);
131
+ total += Object.keys(data).length;
132
+ }
133
+ } catch {
134
+ }
135
+ return total;
136
+ }
137
+ async destroy() {
138
+ this.shards.clear();
139
+ try {
140
+ await rm(this.baseDir, { recursive: true, force: true });
141
+ } catch {
142
+ }
143
+ }
144
+ getShardState(prefix) {
145
+ let state = this.shards.get(prefix);
146
+ if (!state) {
147
+ state = { store: null, writeQueue: Promise.resolve() };
148
+ this.shards.set(prefix, state);
149
+ }
150
+ return state;
151
+ }
152
+ async doSet(prefix, key, value) {
153
+ const state = this.getShardState(prefix);
154
+ const data = await this.readShard(prefix);
155
+ data[key] = { key, value, createdAt: Date.now() };
156
+ const keys = Object.keys(data);
157
+ if (keys.length > this.maxEntriesPerShard) {
158
+ const sorted = keys.sort((a, b) => data[a].createdAt - data[b].createdAt);
159
+ const toRemove = sorted.slice(0, keys.length - this.maxEntriesPerShard);
160
+ for (const k of toRemove) {
161
+ delete data[k];
162
+ }
163
+ }
164
+ state.store = data;
165
+ await this.writeShard(prefix, data);
166
+ }
167
+ async readShard(prefix) {
168
+ const state = this.getShardState(prefix);
169
+ if (state.store !== null) return state.store;
170
+ const filePath = join(this.baseDir, `${prefix}.json`);
171
+ try {
172
+ const content = await readFile(filePath, "utf-8");
173
+ state.store = JSON.parse(content);
174
+ return state.store;
175
+ } catch (error) {
176
+ if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ERR_INVALID_JSON") {
177
+ console.warn(
178
+ `[line-lore] Cache shard corrupted, resetting: ${filePath}`
179
+ );
180
+ state.store = {};
181
+ await this.writeShard(prefix, {});
182
+ return state.store;
183
+ }
184
+ state.store = {};
185
+ return state.store;
186
+ }
187
+ }
188
+ async writeShard(prefix, data) {
189
+ await mkdir(this.baseDir, { recursive: true });
190
+ const filePath = join(this.baseDir, `${prefix}.json`);
191
+ const tmpPath = `${filePath}.tmp`;
192
+ await writeFile(tmpPath, JSON.stringify(data), "utf-8");
193
+ await rename(tmpPath, filePath);
194
+ }
195
+ };
196
+ }
197
+ });
198
+
199
+ // src/cache/index.ts
200
+ var init_cache = __esm({
201
+ "src/cache/index.ts"() {
202
+ "use strict";
203
+ init_sharded_cache();
204
+ }
205
+ });
206
+
23
207
  // src/errors.ts
24
208
  var LineLoreErrorCode, LineLoreError;
25
209
  var init_errors = __esm({
@@ -84,9 +268,10 @@ async function execCommand(command, args, options, errorCode) {
84
268
  });
85
269
  const exitCode = result.exitCode ?? 0;
86
270
  if (exitCode !== 0 && !allowExitCodes.includes(exitCode)) {
271
+ const isNotGitRepo = exitCode === 128 && command === "git" && /not a git repository/i.test(result.stderr);
87
272
  throw new LineLoreError(
88
- failCode,
89
- `${command} ${args[0]} failed with exit code ${exitCode}: ${result.stderr}`,
273
+ isNotGitRepo ? LineLoreErrorCode.NOT_GIT_REPO : failCode,
274
+ isNotGitRepo ? `Not a git repository: ${result.stderr.trim()}` : `${command} ${args[0]} failed with exit code ${exitCode}: ${result.stderr}`,
90
275
  { command, args, exitCode, stderr: result.stderr, cwd }
91
276
  );
92
277
  }
@@ -135,115 +320,27 @@ var init_executor = __esm({
135
320
  }
136
321
  });
137
322
 
138
- // src/cache/file-cache.ts
139
- import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
140
- import { homedir } from "os";
141
- import { join } from "path";
142
- var DEFAULT_CACHE_DIR, DEFAULT_MAX_ENTRIES, FileCache;
143
- var init_file_cache = __esm({
144
- "src/cache/file-cache.ts"() {
145
- "use strict";
146
- DEFAULT_CACHE_DIR = join(homedir(), ".line-lore", "cache");
147
- DEFAULT_MAX_ENTRIES = 1e4;
148
- FileCache = class {
149
- filePath;
150
- maxEntries;
151
- writeQueue = Promise.resolve();
152
- constructor(fileName, options) {
153
- const cacheDir = options?.cacheDir ?? DEFAULT_CACHE_DIR;
154
- this.filePath = join(cacheDir, fileName);
155
- this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
156
- }
157
- async get(key) {
158
- const data = await this.readStore();
159
- const entry = data[key];
160
- return entry?.value ?? null;
161
- }
162
- async has(key) {
163
- const data = await this.readStore();
164
- return key in data;
165
- }
166
- set(key, value) {
167
- this.writeQueue = this.writeQueue.then(() => this.doSet(key, value)).catch(() => {
168
- });
169
- return this.writeQueue;
170
- }
171
- delete(key) {
172
- let deleted = false;
173
- this.writeQueue = this.writeQueue.then(async () => {
174
- const data = await this.readStore();
175
- if (key in data) {
176
- delete data[key];
177
- await this.writeStore(data);
178
- deleted = true;
179
- }
180
- }).catch(() => {
181
- });
182
- return this.writeQueue.then(() => deleted);
183
- }
184
- clear() {
185
- this.writeQueue = this.writeQueue.then(() => this.writeStore({})).catch(() => {
186
- });
187
- return this.writeQueue;
188
- }
189
- async size() {
190
- const data = await this.readStore();
191
- return Object.keys(data).length;
192
- }
193
- async doSet(key, value) {
194
- const data = await this.readStore();
195
- data[key] = { key, value, createdAt: Date.now() };
196
- const keys = Object.keys(data);
197
- if (keys.length > this.maxEntries) {
198
- const sorted = keys.sort((a, b) => data[a].createdAt - data[b].createdAt);
199
- const toRemove = sorted.slice(0, keys.length - this.maxEntries);
200
- for (const k of toRemove) {
201
- delete data[k];
202
- }
203
- }
204
- await this.writeStore(data);
205
- }
206
- async readStore() {
207
- try {
208
- const content = await readFile(this.filePath, "utf-8");
209
- return JSON.parse(content);
210
- } catch (error) {
211
- if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ERR_INVALID_JSON") {
212
- console.warn(
213
- `[line-lore] Cache file corrupted, resetting: ${this.filePath}`
214
- );
215
- await this.writeStore({});
216
- return {};
217
- }
218
- return {};
219
- }
220
- }
221
- async writeStore(data) {
222
- const dir = join(this.filePath, "..");
223
- await mkdir(dir, { recursive: true });
224
- const tmpPath = `${this.filePath}.tmp`;
225
- await writeFile(tmpPath, JSON.stringify(data), "utf-8");
226
- await rename(tmpPath, this.filePath);
227
- }
228
- async destroy() {
229
- try {
230
- await unlink(this.filePath);
231
- } catch {
232
- }
233
- }
234
- };
235
- }
236
- });
237
-
238
323
  // src/core/ancestry/ancestry.ts
324
+ import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
239
325
  async function findMergeCommit(commitSha, options) {
240
326
  const ref = options?.ref ?? "HEAD";
327
+ const firstParentResult = await findMergeCommitWithArgs(
328
+ commitSha,
329
+ ref,
330
+ ["--first-parent"],
331
+ options
332
+ );
333
+ if (firstParentResult) return firstParentResult;
334
+ return findMergeCommitWithArgs(commitSha, ref, [], options);
335
+ }
336
+ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
241
337
  try {
242
338
  const result = await gitExec(
243
339
  [
244
340
  "log",
245
341
  "--merges",
246
342
  "--ancestry-path",
343
+ ...extraArgs,
247
344
  `${commitSha}..${ref}`,
248
345
  "--topo-order",
249
346
  "--reverse",
@@ -251,28 +348,30 @@ async function findMergeCommit(commitSha, options) {
251
348
  ],
252
349
  { cwd: options?.cwd, timeout: options?.timeout }
253
350
  );
254
- const lines = result.stdout.trim().split("\n").filter(Boolean);
351
+ const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
255
352
  if (lines.length === 0) return null;
256
- const firstLine = lines[0];
257
- const parts = firstLine.split(" ");
258
- if (parts.length < 3) return null;
259
- const mergeCommitSha = parts[0];
260
- const parentShas = [];
261
- let subjectStart = 1;
262
- for (let i = 1; i < parts.length; i++) {
263
- if (/^[0-9a-f]{40}$/.test(parts[i])) {
264
- parentShas.push(parts[i]);
265
- subjectStart = i + 1;
266
- } else {
267
- break;
268
- }
269
- }
270
- const subject = parts.slice(subjectStart).join(" ");
271
- return { mergeCommitSha, parentShas, subject };
353
+ return parseMergeLogLine(lines[0]);
272
354
  } catch {
273
355
  return null;
274
356
  }
275
357
  }
358
+ function parseMergeLogLine(line) {
359
+ const parts = line.split(" ");
360
+ if (parts.length < 3) return null;
361
+ const mergeCommitSha = parts[0];
362
+ const parentShas = [];
363
+ let subjectStart = 1;
364
+ for (let i = 1; i < parts.length; i++) {
365
+ if (/^[0-9a-f]{40}$/.test(parts[i])) {
366
+ parentShas.push(parts[i]);
367
+ subjectStart = i + 1;
368
+ } else {
369
+ break;
370
+ }
371
+ }
372
+ const subject = parts.slice(subjectStart).join(" ");
373
+ return { mergeCommitSha, parentShas, subject };
374
+ }
276
375
  function extractPRFromMergeMessage(subject) {
277
376
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
278
377
  if (ghMatch) return parseInt(ghMatch[1], 10);
@@ -298,14 +397,26 @@ var init_ancestry2 = __esm({
298
397
  });
299
398
 
300
399
  // src/core/patch-id/patch-id.ts
301
- function getCache() {
302
- if (!patchIdCache) {
303
- patchIdCache = new FileCache("sha-to-patch-id.json");
400
+ import { filter as filter5, isTruthy as isTruthy5 } from "@winglet/common-utils";
401
+ function repoKey(repoId) {
402
+ return `${repoId.host}/${repoId.owner}/${repoId.repo}`;
403
+ }
404
+ function getCache(repoId, noCache) {
405
+ if (noCache) {
406
+ return new ShardedCache("patch-id", { repoId, enabled: false });
304
407
  }
305
- return patchIdCache;
408
+ const key = repoKey(
409
+ repoId ?? { host: "_local", owner: "_", repo: "_default" }
410
+ );
411
+ let cache = cacheRegistry.get(key);
412
+ if (!cache) {
413
+ cache = new ShardedCache("patch-id", { repoId });
414
+ cacheRegistry.set(key, cache);
415
+ }
416
+ return cache;
306
417
  }
307
418
  async function computePatchId(commitSha, options) {
308
- const cache = getCache();
419
+ const cache = getCache(options?.repoId, options?.noCache);
309
420
  const cached = await cache.get(commitSha);
310
421
  if (cached) return cached;
311
422
  try {
@@ -336,7 +447,7 @@ async function findPatchIdMatch(commitSha, options) {
336
447
  ["log", "--format=%H", `-${scanDepth}`, ref],
337
448
  { cwd: options?.cwd, timeout: options?.timeout }
338
449
  );
339
- const candidates = logResult.stdout.trim().split("\n").filter(Boolean);
450
+ const candidates = filter5(logResult.stdout.trim().split("\n"), isTruthy5);
340
451
  for (const candidateSha of candidates) {
341
452
  if (candidateSha === commitSha) continue;
342
453
  const candidatePatchId = await computePatchId(candidateSha, options);
@@ -349,16 +460,16 @@ async function findPatchIdMatch(commitSha, options) {
349
460
  return null;
350
461
  }
351
462
  function resetPatchIdCache() {
352
- patchIdCache = null;
463
+ cacheRegistry.clear();
353
464
  }
354
- var DEFAULT_SCAN_DEPTH, patchIdCache;
465
+ var DEFAULT_SCAN_DEPTH, cacheRegistry;
355
466
  var init_patch_id = __esm({
356
467
  "src/core/patch-id/patch-id.ts"() {
357
468
  "use strict";
358
- init_file_cache();
469
+ init_cache();
359
470
  init_executor();
360
471
  DEFAULT_SCAN_DEPTH = 500;
361
- patchIdCache = null;
472
+ cacheRegistry = /* @__PURE__ */ new Map();
362
473
  }
363
474
  });
364
475
 
@@ -377,40 +488,58 @@ var init_patch_id2 = __esm({
377
488
  });
378
489
 
379
490
  // src/core/pr-lookup/pr-lookup.ts
380
- function getCache2() {
381
- if (!prCache) {
382
- prCache = new FileCache("sha-to-pr.json");
491
+ function repoKey2(repoId) {
492
+ return `${repoId.host}/${repoId.owner}/${repoId.repo}`;
493
+ }
494
+ function getCache2(repoId, noCache) {
495
+ if (noCache) {
496
+ return new ShardedCache("pr", { repoId, enabled: false });
383
497
  }
384
- return prCache;
498
+ const key = repoKey2(
499
+ repoId ?? { host: "_local", owner: "_", repo: "_default" }
500
+ );
501
+ let cache = cacheRegistry2.get(key);
502
+ if (!cache) {
503
+ cache = new ShardedCache("pr", { repoId });
504
+ cacheRegistry2.set(key, cache);
505
+ }
506
+ return cache;
385
507
  }
386
508
  async function lookupPR(commitSha, adapter, options) {
387
- const cache = getCache2();
509
+ const cache = getCache2(options?.repoId, options?.noCache);
388
510
  const cached = await cache.get(commitSha);
389
511
  if (cached) return cached;
512
+ let mergeBasedPR = null;
390
513
  const mergeResult = await findMergeCommit(commitSha, options);
391
514
  if (mergeResult) {
392
515
  const prNumber = extractPRFromMergeMessage(mergeResult.subject);
393
516
  if (prNumber) {
394
517
  if (adapter) {
395
518
  const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
396
- if (prInfo) {
397
- await cache.set(commitSha, prInfo);
398
- return prInfo;
519
+ if (prInfo?.mergedAt) {
520
+ mergeBasedPR = prInfo;
399
521
  }
400
522
  }
401
- const minimalPR = {
402
- number: prNumber,
403
- title: mergeResult.subject,
404
- author: "",
405
- url: "",
406
- mergeCommit: mergeResult.mergeCommitSha,
407
- baseBranch: ""
408
- };
409
- await cache.set(commitSha, minimalPR);
410
- return minimalPR;
523
+ if (!mergeBasedPR) {
524
+ mergeBasedPR = {
525
+ number: prNumber,
526
+ title: mergeResult.subject,
527
+ author: "",
528
+ url: "",
529
+ mergeCommit: mergeResult.mergeCommitSha,
530
+ baseBranch: ""
531
+ };
532
+ }
533
+ if (!options?.deep || mergeBasedPR.mergedAt) {
534
+ await cache.set(commitSha, mergeBasedPR);
535
+ return mergeBasedPR;
536
+ }
411
537
  }
412
538
  }
413
- const patchIdMatch = await findPatchIdMatch(commitSha, options);
539
+ const patchIdMatch = await findPatchIdMatch(commitSha, {
540
+ ...options,
541
+ scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
542
+ });
414
543
  if (patchIdMatch) {
415
544
  const result = await lookupPR(patchIdMatch.matchedSha, adapter, options);
416
545
  if (result) {
@@ -418,9 +547,13 @@ async function lookupPR(commitSha, adapter, options) {
418
547
  return result;
419
548
  }
420
549
  }
550
+ if (mergeBasedPR) {
551
+ await cache.set(commitSha, mergeBasedPR);
552
+ return mergeBasedPR;
553
+ }
421
554
  if (adapter) {
422
555
  const prInfo = await adapter.getPRForCommit(commitSha);
423
- if (prInfo) {
556
+ if (prInfo?.mergedAt) {
424
557
  await cache.set(commitSha, prInfo);
425
558
  return prInfo;
426
559
  }
@@ -428,16 +561,17 @@ async function lookupPR(commitSha, adapter, options) {
428
561
  return null;
429
562
  }
430
563
  function resetPRCache() {
431
- prCache = null;
564
+ cacheRegistry2.clear();
432
565
  }
433
- var prCache;
566
+ var cacheRegistry2, DEEP_SCAN_DEPTH;
434
567
  var init_pr_lookup = __esm({
435
568
  "src/core/pr-lookup/pr-lookup.ts"() {
436
569
  "use strict";
437
- init_file_cache();
570
+ init_cache();
438
571
  init_ancestry2();
439
572
  init_patch_id2();
440
- prCache = null;
573
+ cacheRegistry2 = /* @__PURE__ */ new Map();
574
+ DEEP_SCAN_DEPTH = 2e3;
441
575
  }
442
576
  });
443
577
 
@@ -454,6 +588,15 @@ var init_pr_lookup2 = __esm({
454
588
  }
455
589
  });
456
590
 
591
+ // src/version.ts
592
+ var VERSION;
593
+ var init_version = __esm({
594
+ "src/version.ts"() {
595
+ "use strict";
596
+ VERSION = "0.0.4";
597
+ }
598
+ });
599
+
457
600
  // src/output/normalizer.ts
458
601
  var normalizer_exports = {};
459
602
  __export(normalizer_exports, {
@@ -465,7 +608,7 @@ function createSuccessResponse(command, data, operatingLevel, options) {
465
608
  return {
466
609
  tool: "line-lore",
467
610
  command,
468
- version: getVersion(),
611
+ version: VERSION,
469
612
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
470
613
  status: "success",
471
614
  operatingLevel,
@@ -478,7 +621,7 @@ function createPartialResponse(command, partialData, operatingLevel, warnings) {
478
621
  return {
479
622
  tool: "line-lore",
480
623
  command,
481
- version: getVersion(),
624
+ version: VERSION,
482
625
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
483
626
  status: "partial",
484
627
  operatingLevel,
@@ -490,7 +633,7 @@ function createErrorResponse(command, code, message, operatingLevel, options) {
490
633
  return {
491
634
  tool: "line-lore",
492
635
  command,
493
- version: getVersion(),
636
+ version: VERSION,
494
637
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
495
638
  status: "error",
496
639
  operatingLevel,
@@ -503,16 +646,10 @@ function createErrorResponse(command, code, message, operatingLevel, options) {
503
646
  }
504
647
  };
505
648
  }
506
- function getVersion() {
507
- try {
508
- return "0.0.1";
509
- } catch {
510
- return "unknown";
511
- }
512
- }
513
649
  var init_normalizer = __esm({
514
650
  "src/output/normalizer.ts"() {
515
651
  "use strict";
652
+ init_version();
516
653
  }
517
654
  });
518
655
 
@@ -551,7 +688,12 @@ function respondError(command, code, message, startTime, version2, details) {
551
688
  // src/cli.ts
552
689
  import { Command } from "commander";
553
690
 
691
+ // src/core/core.ts
692
+ import { createHash as createHash2 } from "crypto";
693
+ import { map as map8 } from "@winglet/common-utils";
694
+
554
695
  // src/ast/parser.ts
696
+ import { isNil } from "@winglet/common-utils";
555
697
  var astGrep = null;
556
698
  var loadAttempted = false;
557
699
  var available = false;
@@ -602,7 +744,7 @@ async function findSymbols(source, lang) {
602
744
  try {
603
745
  const { parse, Lang } = astGrep;
604
746
  const langEnum = Lang[lang] ?? Lang[lang.charAt(0).toUpperCase() + lang.slice(1)];
605
- if (langEnum == null) return [];
747
+ if (isNil(langEnum)) return [];
606
748
  const root = parse(langEnum, source).root();
607
749
  const symbols = [];
608
750
  const kindPatterns = [
@@ -754,8 +896,14 @@ function findPythonBlockEnd(lines, startIdx) {
754
896
  return lines.length - 1;
755
897
  }
756
898
 
899
+ // src/core/core.ts
900
+ init_cache();
901
+ init_errors();
902
+ init_executor();
903
+
757
904
  // src/git/health.ts
758
905
  init_executor();
906
+ import { map } from "@winglet/common-utils";
759
907
  var GIT_VERSION_PATTERN = /git version (\d+\.\d+\.\d+)/;
760
908
  var BLOOM_FILTER_MIN_VERSION = [2, 27, 0];
761
909
  function parseGitVersion(versionStr) {
@@ -763,7 +911,7 @@ function parseGitVersion(versionStr) {
763
911
  return match?.[1] ?? "0.0.0";
764
912
  }
765
913
  function isVersionAtLeast(version2, minVersion) {
766
- const parts = version2.split(".").map(Number);
914
+ const parts = map(version2.split("."), Number);
767
915
  for (let i = 0; i < 3; i++) {
768
916
  if ((parts[i] ?? 0) > minVersion[i]) return true;
769
917
  if ((parts[i] ?? 0) < minVersion[i]) return false;
@@ -835,13 +983,12 @@ async function getRemoteInfo(remoteName = "origin", options) {
835
983
 
836
984
  // src/platform/github/github-adapter.ts
837
985
  init_executor();
986
+ import { filter, isArray, isTruthy, map as map2 } from "@winglet/common-utils";
838
987
 
839
988
  // src/platform/scheduler/request-scheduler.ts
840
989
  var RequestScheduler = class {
841
990
  rateLimitInfo = null;
842
991
  threshold;
843
- etagCache = /* @__PURE__ */ new Map();
844
- responseCache = /* @__PURE__ */ new Map();
845
992
  constructor(options) {
846
993
  this.threshold = options?.rateLimitThreshold ?? 10;
847
994
  }
@@ -855,16 +1002,6 @@ var RequestScheduler = class {
855
1002
  getRateLimit() {
856
1003
  return this.rateLimitInfo;
857
1004
  }
858
- setEtag(url, etag, response) {
859
- this.etagCache.set(url, etag);
860
- this.responseCache.set(url, response);
861
- }
862
- getEtag(url) {
863
- return this.etagCache.get(url) ?? null;
864
- }
865
- getCachedResponse(url) {
866
- return this.responseCache.get(url) ?? null;
867
- }
868
1005
  };
869
1006
 
870
1007
  // src/platform/github/github-adapter.ts
@@ -872,9 +1009,14 @@ var GitHubAdapter = class {
872
1009
  platform = "github";
873
1010
  scheduler;
874
1011
  hostname;
1012
+ defaultBranchCache = null;
1013
+ remoteName;
1014
+ cwd;
875
1015
  constructor(options) {
876
1016
  this.hostname = options?.hostname ?? "github.com";
877
1017
  this.scheduler = options?.scheduler ?? new RequestScheduler();
1018
+ this.remoteName = options?.remoteName ?? "origin";
1019
+ this.cwd = options?.cwd;
878
1020
  }
879
1021
  async checkAuth() {
880
1022
  try {
@@ -882,6 +1024,7 @@ var GitHubAdapter = class {
882
1024
  "gh",
883
1025
  ["auth", "token", "--hostname", this.hostname],
884
1026
  {
1027
+ cwd: this.cwd,
885
1028
  allowExitCodes: [1]
886
1029
  }
887
1030
  );
@@ -897,64 +1040,98 @@ var GitHubAdapter = class {
897
1040
  async getPRForCommit(sha) {
898
1041
  if (this.scheduler.isRateLimited()) return null;
899
1042
  try {
900
- const result = await shellExec("gh", [
901
- "api",
902
- `repos/{owner}/{repo}/commits/${sha}/pulls`,
903
- "--hostname",
904
- this.hostname,
905
- "--jq",
906
- ".[0] | {number, title, user: .user.login, html_url, merge_commit_sha, base: .base.ref, merged_at}"
907
- ]);
908
- const data = JSON.parse(result.stdout);
909
- if (!data?.number) return null;
1043
+ const result = await shellExec(
1044
+ "gh",
1045
+ [
1046
+ "api",
1047
+ `repos/{owner}/{repo}/commits/${sha}/pulls`,
1048
+ "--hostname",
1049
+ this.hostname,
1050
+ "--jq",
1051
+ "[.[] | select(.merged_at != null) | {number, title, user: .user.login, html_url, merge_commit_sha, base: .base.ref, merged_at}] | sort_by(.merged_at)"
1052
+ ],
1053
+ { cwd: this.cwd }
1054
+ );
1055
+ const prs = JSON.parse(result.stdout);
1056
+ if (!isArray(prs) || prs.length === 0) return null;
1057
+ const defaultBranch = await this.detectDefaultBranch();
1058
+ const defaultBranchPR = prs.find(
1059
+ (pr) => pr.base === defaultBranch
1060
+ );
1061
+ const data = defaultBranchPR ?? prs[0];
910
1062
  return {
911
1063
  number: data.number,
912
1064
  title: data.title ?? "",
913
1065
  author: data.user ?? "",
914
1066
  url: data.html_url ?? "",
915
1067
  mergeCommit: data.merge_commit_sha ?? sha,
916
- baseBranch: data.base ?? "main",
1068
+ baseBranch: data.base ?? defaultBranch,
917
1069
  mergedAt: data.merged_at
918
1070
  };
919
1071
  } catch {
920
1072
  return null;
921
1073
  }
922
1074
  }
1075
+ async detectDefaultBranch() {
1076
+ if (this.defaultBranchCache) return this.defaultBranchCache;
1077
+ try {
1078
+ const result = await gitExec(
1079
+ ["symbolic-ref", `refs/remotes/${this.remoteName}/HEAD`],
1080
+ { cwd: this.cwd }
1081
+ );
1082
+ const ref = result.stdout.trim();
1083
+ this.defaultBranchCache = ref.replace(`refs/remotes/${this.remoteName}/`, "") || "main";
1084
+ return this.defaultBranchCache;
1085
+ } catch {
1086
+ return "main";
1087
+ }
1088
+ }
923
1089
  async getPRCommits(prNumber) {
924
1090
  try {
925
- const result = await shellExec("gh", [
926
- "api",
927
- `repos/{owner}/{repo}/pulls/${prNumber}/commits`,
928
- "--hostname",
929
- this.hostname,
930
- "--jq",
931
- ".[].sha"
932
- ]);
933
- return result.stdout.trim().split("\n").filter(Boolean);
1091
+ const result = await shellExec(
1092
+ "gh",
1093
+ [
1094
+ "api",
1095
+ `repos/{owner}/{repo}/pulls/${prNumber}/commits`,
1096
+ "--hostname",
1097
+ this.hostname,
1098
+ "--jq",
1099
+ ".[].sha"
1100
+ ],
1101
+ { cwd: this.cwd }
1102
+ );
1103
+ return filter(result.stdout.trim().split("\n"), isTruthy);
934
1104
  } catch {
935
1105
  return [];
936
1106
  }
937
1107
  }
938
1108
  async getLinkedIssues(prNumber) {
939
1109
  try {
940
- const result = await shellExec("gh", [
941
- "api",
942
- "graphql",
943
- "--hostname",
944
- this.hostname,
945
- "-f",
946
- `query=query { repository(owner: "{owner}", name: "{repo}") { pullRequest(number: ${prNumber}) { closingIssuesReferences(first: 10) { nodes { number title url state labels(first: 5) { nodes { name } } } } } } }`,
947
- "--jq",
948
- ".data.repository.pullRequest.closingIssuesReferences.nodes"
949
- ]);
1110
+ const result = await shellExec(
1111
+ "gh",
1112
+ [
1113
+ "api",
1114
+ "graphql",
1115
+ "--hostname",
1116
+ this.hostname,
1117
+ "-f",
1118
+ `query=query { repository(owner: "{owner}", name: "{repo}") { pullRequest(number: ${prNumber}) { closingIssuesReferences(first: 10) { nodes { number title url state labels(first: 5) { nodes { name } } } } } } }`,
1119
+ "--jq",
1120
+ ".data.repository.pullRequest.closingIssuesReferences.nodes"
1121
+ ],
1122
+ { cwd: this.cwd }
1123
+ );
950
1124
  const nodes = JSON.parse(result.stdout);
951
- if (!Array.isArray(nodes)) return [];
952
- return nodes.map((node) => ({
1125
+ if (!isArray(nodes)) return [];
1126
+ return map2(nodes, (node) => ({
953
1127
  number: node.number,
954
1128
  title: node.title ?? "",
955
1129
  url: node.url ?? "",
956
1130
  state: (node.state ?? "open").toLowerCase(),
957
- labels: (node.labels?.nodes ?? []).map((l) => l.name)
1131
+ labels: map2(
1132
+ node.labels?.nodes ?? [],
1133
+ (l) => l.name
1134
+ )
958
1135
  }));
959
1136
  } catch {
960
1137
  return [];
@@ -962,23 +1139,28 @@ var GitHubAdapter = class {
962
1139
  }
963
1140
  async getLinkedPRs(issueNumber) {
964
1141
  try {
965
- const result = await shellExec("gh", [
966
- "api",
967
- `repos/{owner}/{repo}/issues/${issueNumber}/timeline`,
968
- "--hostname",
969
- this.hostname,
970
- "--jq",
971
- '[.[] | select(.source.issue.pull_request) | .source.issue] | map({number, title, user: .user.login, html_url, merge_commit_sha: .pull_request.merge_commit_sha, base: "main", merged_at: .pull_request.merged_at})'
972
- ]);
1142
+ const result = await shellExec(
1143
+ "gh",
1144
+ [
1145
+ "api",
1146
+ `repos/{owner}/{repo}/issues/${issueNumber}/timeline`,
1147
+ "--hostname",
1148
+ this.hostname,
1149
+ "--jq",
1150
+ "[.[] | select(.source.issue.pull_request) | .source.issue] | map({number, title, user: .user.login, html_url, merge_commit_sha: .pull_request.merge_commit_sha, merged_at: .pull_request.merged_at})"
1151
+ ],
1152
+ { cwd: this.cwd }
1153
+ );
973
1154
  const prs = JSON.parse(result.stdout);
974
- if (!Array.isArray(prs)) return [];
975
- return prs.map((pr) => ({
1155
+ if (!isArray(prs)) return [];
1156
+ const defaultBranch = await this.detectDefaultBranch();
1157
+ return map2(prs, (pr) => ({
976
1158
  number: pr.number,
977
1159
  title: pr.title ?? "",
978
1160
  author: pr.user ?? "",
979
1161
  url: pr.html_url ?? "",
980
1162
  mergeCommit: pr.merge_commit_sha ?? "",
981
- baseBranch: pr.base ?? "main",
1163
+ baseBranch: defaultBranch,
982
1164
  mergedAt: pr.merged_at
983
1165
  }));
984
1166
  } catch {
@@ -987,14 +1169,18 @@ var GitHubAdapter = class {
987
1169
  }
988
1170
  async getRateLimit() {
989
1171
  try {
990
- const result = await shellExec("gh", [
991
- "api",
992
- "rate_limit",
993
- "--hostname",
994
- this.hostname,
995
- "--jq",
996
- ".rate | {limit, remaining, reset}"
997
- ]);
1172
+ const result = await shellExec(
1173
+ "gh",
1174
+ [
1175
+ "api",
1176
+ "rate_limit",
1177
+ "--hostname",
1178
+ this.hostname,
1179
+ "--jq",
1180
+ ".rate | {limit, remaining, reset}"
1181
+ ],
1182
+ { cwd: this.cwd }
1183
+ );
998
1184
  const data = JSON.parse(result.stdout);
999
1185
  const info = {
1000
1186
  limit: data.limit ?? 5e3,
@@ -1013,19 +1199,30 @@ var GitHubAdapter = class {
1013
1199
  var GitHubEnterpriseAdapter = class extends GitHubAdapter {
1014
1200
  platform = "github-enterprise";
1015
1201
  constructor(hostname, options) {
1016
- super({ hostname, scheduler: options?.scheduler });
1202
+ super({
1203
+ hostname,
1204
+ scheduler: options?.scheduler,
1205
+ remoteName: options?.remoteName,
1206
+ cwd: options?.cwd
1207
+ });
1017
1208
  }
1018
1209
  };
1019
1210
 
1020
1211
  // src/platform/gitlab/gitlab-adapter.ts
1021
1212
  init_executor();
1213
+ import { filter as filter2, isArray as isArray2, isNotNil, map as map3 } from "@winglet/common-utils";
1022
1214
  var GitLabAdapter = class {
1023
1215
  platform = "gitlab";
1024
1216
  scheduler;
1025
1217
  hostname;
1218
+ defaultBranchCache = null;
1219
+ remoteName;
1220
+ cwd;
1026
1221
  constructor(options) {
1027
1222
  this.hostname = options?.hostname ?? "gitlab.com";
1028
1223
  this.scheduler = options?.scheduler ?? new RequestScheduler();
1224
+ this.remoteName = options?.remoteName ?? "origin";
1225
+ this.cwd = options?.cwd;
1029
1226
  }
1030
1227
  async checkAuth() {
1031
1228
  if (process.env.GITLAB_TOKEN) {
@@ -1035,7 +1232,7 @@ var GitLabAdapter = class {
1035
1232
  const result = await shellExec(
1036
1233
  "glab",
1037
1234
  ["auth", "status", "--hostname", this.hostname],
1038
- { allowExitCodes: [1] }
1235
+ { cwd: this.cwd, allowExitCodes: [1] }
1039
1236
  );
1040
1237
  return {
1041
1238
  authenticated: result.exitCode === 0,
@@ -1048,54 +1245,93 @@ var GitLabAdapter = class {
1048
1245
  async getPRForCommit(sha) {
1049
1246
  if (this.scheduler.isRateLimited()) return null;
1050
1247
  try {
1051
- const result = await shellExec("glab", [
1052
- "api",
1053
- `projects/:id/repository/commits/${sha}/merge_requests`,
1054
- "--hostname",
1055
- this.hostname
1056
- ]);
1248
+ const result = await shellExec(
1249
+ "glab",
1250
+ [
1251
+ "api",
1252
+ `projects/:id/repository/commits/${sha}/merge_requests`,
1253
+ "--hostname",
1254
+ this.hostname
1255
+ ],
1256
+ { cwd: this.cwd }
1257
+ );
1057
1258
  const mrs = JSON.parse(result.stdout);
1058
- if (!Array.isArray(mrs) || mrs.length === 0) return null;
1059
- const mr = mrs[0];
1259
+ if (!isArray2(mrs) || mrs.length === 0) return null;
1260
+ const mergedMRs = filter2(
1261
+ mrs,
1262
+ (mr2) => mr2.state === "merged" && isNotNil(mr2.merged_at)
1263
+ ).sort((a, b) => {
1264
+ const aTime = new Date(a.merged_at).getTime();
1265
+ const bTime = new Date(b.merged_at).getTime();
1266
+ return aTime - bTime;
1267
+ });
1268
+ if (mergedMRs.length === 0) return null;
1269
+ const defaultBranch = await this.detectDefaultBranch();
1270
+ const defaultBranchMR = mergedMRs.find(
1271
+ (mr2) => mr2.target_branch === defaultBranch
1272
+ );
1273
+ const mr = defaultBranchMR ?? mergedMRs[0];
1060
1274
  return {
1061
1275
  number: mr.iid,
1062
1276
  title: mr.title ?? "",
1063
1277
  author: mr.author?.username ?? "",
1064
1278
  url: mr.web_url ?? "",
1065
1279
  mergeCommit: mr.merge_commit_sha ?? sha,
1066
- baseBranch: mr.target_branch ?? "main",
1280
+ baseBranch: mr.target_branch ?? defaultBranch,
1067
1281
  mergedAt: mr.merged_at
1068
1282
  };
1069
1283
  } catch {
1070
1284
  return null;
1071
1285
  }
1072
1286
  }
1287
+ async detectDefaultBranch() {
1288
+ if (this.defaultBranchCache) return this.defaultBranchCache;
1289
+ try {
1290
+ const result = await gitExec(
1291
+ ["symbolic-ref", `refs/remotes/${this.remoteName}/HEAD`],
1292
+ { cwd: this.cwd }
1293
+ );
1294
+ const ref = result.stdout.trim();
1295
+ this.defaultBranchCache = ref.replace(`refs/remotes/${this.remoteName}/`, "") || "main";
1296
+ return this.defaultBranchCache;
1297
+ } catch {
1298
+ return "main";
1299
+ }
1300
+ }
1073
1301
  async getPRCommits(prNumber) {
1074
1302
  try {
1075
- const result = await shellExec("glab", [
1076
- "api",
1077
- `projects/:id/merge_requests/${prNumber}/commits`,
1078
- "--hostname",
1079
- this.hostname
1080
- ]);
1303
+ const result = await shellExec(
1304
+ "glab",
1305
+ [
1306
+ "api",
1307
+ `projects/:id/merge_requests/${prNumber}/commits`,
1308
+ "--hostname",
1309
+ this.hostname
1310
+ ],
1311
+ { cwd: this.cwd }
1312
+ );
1081
1313
  const commits = JSON.parse(result.stdout);
1082
- if (!Array.isArray(commits)) return [];
1083
- return commits.map((c) => c.id);
1314
+ if (!isArray2(commits)) return [];
1315
+ return map3(commits, (c) => c.id);
1084
1316
  } catch {
1085
1317
  return [];
1086
1318
  }
1087
1319
  }
1088
1320
  async getLinkedIssues(prNumber) {
1089
1321
  try {
1090
- const result = await shellExec("glab", [
1091
- "api",
1092
- `projects/:id/merge_requests/${prNumber}/closes_issues`,
1093
- "--hostname",
1094
- this.hostname
1095
- ]);
1322
+ const result = await shellExec(
1323
+ "glab",
1324
+ [
1325
+ "api",
1326
+ `projects/:id/merge_requests/${prNumber}/closes_issues`,
1327
+ "--hostname",
1328
+ this.hostname
1329
+ ],
1330
+ { cwd: this.cwd }
1331
+ );
1096
1332
  const issues = JSON.parse(result.stdout);
1097
- if (!Array.isArray(issues)) return [];
1098
- return issues.map((issue) => ({
1333
+ if (!isArray2(issues)) return [];
1334
+ return map3(issues, (issue) => ({
1099
1335
  number: issue.iid,
1100
1336
  title: issue.title ?? "",
1101
1337
  url: issue.web_url ?? "",
@@ -1108,21 +1344,26 @@ var GitLabAdapter = class {
1108
1344
  }
1109
1345
  async getLinkedPRs(issueNumber) {
1110
1346
  try {
1111
- const result = await shellExec("glab", [
1112
- "api",
1113
- `projects/:id/issues/${issueNumber}/related_merge_requests`,
1114
- "--hostname",
1115
- this.hostname
1116
- ]);
1347
+ const result = await shellExec(
1348
+ "glab",
1349
+ [
1350
+ "api",
1351
+ `projects/:id/issues/${issueNumber}/related_merge_requests`,
1352
+ "--hostname",
1353
+ this.hostname
1354
+ ],
1355
+ { cwd: this.cwd }
1356
+ );
1117
1357
  const mrs = JSON.parse(result.stdout);
1118
- if (!Array.isArray(mrs)) return [];
1119
- return mrs.map((mr) => ({
1358
+ if (!isArray2(mrs)) return [];
1359
+ const defaultBranch = await this.detectDefaultBranch();
1360
+ return map3(mrs, (mr) => ({
1120
1361
  number: mr.iid,
1121
1362
  title: mr.title ?? "",
1122
1363
  author: mr.author?.username ?? "",
1123
1364
  url: mr.web_url ?? "",
1124
1365
  mergeCommit: mr.merge_commit_sha ?? "",
1125
- baseBranch: mr.target_branch ?? "main",
1366
+ baseBranch: mr.target_branch ?? defaultBranch,
1126
1367
  mergedAt: mr.merged_at
1127
1368
  }));
1128
1369
  } catch {
@@ -1138,7 +1379,12 @@ var GitLabAdapter = class {
1138
1379
  var GitLabSelfHostedAdapter = class extends GitLabAdapter {
1139
1380
  platform = "gitlab-self-hosted";
1140
1381
  constructor(hostname, options) {
1141
- super({ hostname, scheduler: options?.scheduler });
1382
+ super({
1383
+ hostname,
1384
+ scheduler: options?.scheduler,
1385
+ remoteName: options?.remoteName,
1386
+ cwd: options?.cwd
1387
+ });
1142
1388
  }
1143
1389
  };
1144
1390
 
@@ -1147,21 +1393,21 @@ async function detectPlatformAdapter(options) {
1147
1393
  const remote = await getRemoteInfo(options?.remoteName, {
1148
1394
  cwd: options?.cwd
1149
1395
  });
1150
- const adapter = createAdapter(remote);
1396
+ const adapter = createAdapter(remote, options?.remoteName, options?.cwd);
1151
1397
  return { adapter, remote };
1152
1398
  }
1153
- function createAdapter(remote) {
1399
+ function createAdapter(remote, remoteName, cwd) {
1154
1400
  switch (remote.platform) {
1155
1401
  case "github":
1156
- return new GitHubAdapter({ hostname: remote.host });
1402
+ return new GitHubAdapter({ hostname: remote.host, remoteName, cwd });
1157
1403
  case "github-enterprise":
1158
- return new GitHubEnterpriseAdapter(remote.host);
1404
+ return new GitHubEnterpriseAdapter(remote.host, { remoteName, cwd });
1159
1405
  case "gitlab":
1160
- return new GitLabAdapter({ hostname: remote.host });
1406
+ return new GitLabAdapter({ hostname: remote.host, remoteName, cwd });
1161
1407
  case "gitlab-self-hosted":
1162
- return new GitLabSelfHostedAdapter(remote.host);
1408
+ return new GitLabSelfHostedAdapter(remote.host, { remoteName, cwd });
1163
1409
  case "unknown":
1164
- return new GitHubEnterpriseAdapter(remote.host);
1410
+ return new GitHubEnterpriseAdapter(remote.host, { remoteName, cwd });
1165
1411
  }
1166
1412
  }
1167
1413
 
@@ -1208,6 +1454,7 @@ function parseLineNumber(value, originalInput) {
1208
1454
  }
1209
1455
 
1210
1456
  // src/core/ast-diff/ast-diff.ts
1457
+ import { isTruthy as isTruthy2, map as map4 } from "@winglet/common-utils";
1211
1458
  init_executor();
1212
1459
 
1213
1460
  // src/core/ast-diff/comparison/structure-comparator.ts
@@ -1249,13 +1496,13 @@ function compareSymbolMaps(current, parent) {
1249
1496
  }
1250
1497
  return results;
1251
1498
  }
1252
- function findHashMatch(target, map) {
1253
- for (const [name, hash] of map) {
1499
+ function findHashMatch(target, map9) {
1500
+ for (const [name, hash] of map9) {
1254
1501
  if (hash.exact === target.exact) {
1255
1502
  return { name, confidence: "exact" };
1256
1503
  }
1257
1504
  }
1258
- for (const [name, hash] of map) {
1505
+ for (const [name, hash] of map9) {
1259
1506
  if (hash.structural === target.structural) {
1260
1507
  return { name, confidence: "structural" };
1261
1508
  }
@@ -1417,10 +1664,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
1417
1664
  // src/core/ast-diff/ast-diff.ts
1418
1665
  var MAX_TRAVERSAL_DEPTH = 50;
1419
1666
  async function traceByAst(file, line, startCommitSha, options) {
1420
- if (!isAstAvailable()) {
1421
- const lang2 = detectLanguage(file);
1422
- if (!lang2) return null;
1423
- }
1424
1667
  const lang = detectLanguage(file);
1425
1668
  if (!lang) return null;
1426
1669
  const maxDepth = options?.maxDepth ?? MAX_TRAVERSAL_DEPTH;
@@ -1444,10 +1687,10 @@ async function traceByAst(file, line, startCommitSha, options) {
1444
1687
  const parentContent = await getFileAtCommit(parentSha, file, options);
1445
1688
  const parentSymbols = await extractSymbols(parentContent, lang);
1446
1689
  const currentMap = new Map(
1447
- [currentSymbol].filter(Boolean).map((s) => [s.name, computeContentHash(s.bodyText)])
1690
+ [currentSymbol].filter(isTruthy2).map((s) => [s.name, computeContentHash(s.bodyText)])
1448
1691
  );
1449
1692
  const parentMap = new Map(
1450
- parentSymbols.map((s) => [s.name, computeContentHash(s.bodyText)])
1693
+ map4(parentSymbols, (s) => [s.name, computeContentHash(s.bodyText)])
1451
1694
  );
1452
1695
  const comparison = compareSymbolMaps(currentMap, parentMap);
1453
1696
  if (comparison.length > 0) {
@@ -1509,9 +1752,11 @@ async function getParentCommit(sha, options) {
1509
1752
 
1510
1753
  // src/core/blame/blame.ts
1511
1754
  init_executor();
1755
+ import { forEach as forEach2, map as map6 } from "@winglet/common-utils";
1512
1756
 
1513
1757
  // src/core/blame/detection/cosmetic-detector.ts
1514
1758
  init_executor();
1759
+ import { filter as filter3, forEach, isTruthy as isTruthy3, map as map5 } from "@winglet/common-utils";
1515
1760
  function isCosmeticDiff(diff) {
1516
1761
  const hunks = extractHunks(diff);
1517
1762
  if (hunks.length === 0) return { isCosmetic: false };
@@ -1558,23 +1803,33 @@ function normalize(line) {
1558
1803
  }
1559
1804
  function isWhitespaceOnly(hunks) {
1560
1805
  return hunks.every((hunk) => {
1561
- const removedNorm = hunk.removed.map(normalize).filter(Boolean).sort();
1562
- const addedNorm = hunk.added.map(normalize).filter(Boolean).sort();
1806
+ const removedNorm = [];
1807
+ forEach(hunk.removed, (line) => {
1808
+ const n = normalize(line);
1809
+ if (isTruthy3(n)) removedNorm.push(n);
1810
+ });
1811
+ removedNorm.sort();
1812
+ const addedNorm = [];
1813
+ forEach(hunk.added, (line) => {
1814
+ const n = normalize(line);
1815
+ if (isTruthy3(n)) addedNorm.push(n);
1816
+ });
1817
+ addedNorm.sort();
1563
1818
  if (removedNorm.length !== addedNorm.length) return false;
1564
1819
  return removedNorm.every((line, idx) => line === addedNorm[idx]);
1565
1820
  });
1566
1821
  }
1567
1822
  function isImportReorder(hunks) {
1568
1823
  return hunks.every((hunk) => {
1569
- const removedImports = hunk.removed.filter(isImportLine);
1570
- const addedImports = hunk.added.filter(isImportLine);
1824
+ const removedImports = filter3(hunk.removed, isImportLine);
1825
+ const addedImports = filter3(hunk.added, isImportLine);
1571
1826
  if (removedImports.length === 0) return false;
1572
- if (removedImports.length !== hunk.removed.filter((l) => l.trim()).length)
1827
+ if (removedImports.length !== filter3(hunk.removed, (l) => isTruthy3(l.trim())).length)
1573
1828
  return false;
1574
- if (addedImports.length !== hunk.added.filter((l) => l.trim()).length)
1829
+ if (addedImports.length !== filter3(hunk.added, (l) => isTruthy3(l.trim())).length)
1575
1830
  return false;
1576
- const removedSorted = removedImports.map(normalize).sort();
1577
- const addedSorted = addedImports.map(normalize).sort();
1831
+ const removedSorted = map5(removedImports, normalize).sort();
1832
+ const addedSorted = map5(addedImports, normalize).sort();
1578
1833
  if (removedSorted.length !== addedSorted.length) return false;
1579
1834
  return removedSorted.every((line, idx) => line === addedSorted[idx]);
1580
1835
  });
@@ -1666,31 +1921,36 @@ function parsePorcelainOutput(output) {
1666
1921
 
1667
1922
  // src/core/blame/blame.ts
1668
1923
  async function executeBlame(file, lineRange, options) {
1669
- const lineSpec = lineRange.start === lineRange.end ? `${lineRange.start},${lineRange.end}` : `${lineRange.start},${lineRange.end}`;
1924
+ const lineSpec = `${lineRange.start},${lineRange.end}`;
1670
1925
  const result = await gitExec(
1671
1926
  ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file],
1672
1927
  options
1673
1928
  );
1674
1929
  return parsePorcelainOutput(result.stdout);
1675
1930
  }
1676
- async function analyzeBlameResults(results, options) {
1677
- const uniqueShas = [...new Set(results.map((r) => r.commitHash))];
1931
+ async function analyzeBlameResults(results, filePath, options) {
1932
+ const uniqueShas = [...new Set(map6(results, (r) => r.commitHash))];
1678
1933
  const cosmeticMap = /* @__PURE__ */ new Map();
1679
1934
  const zeroSha = "0".repeat(40);
1680
- await Promise.all(
1681
- uniqueShas.filter((sha) => sha !== zeroSha).map(async (sha) => {
1682
- try {
1683
- const blameResult = results.find((r) => r.commitHash === sha);
1684
- if (!blameResult) return;
1685
- const file = blameResult.originalFile ?? results.find((r) => r.commitHash === sha)?.lineContent;
1686
- const diff = await getCosmeticDiff(sha, file ?? "", options);
1687
- cosmeticMap.set(sha, isCosmeticDiff(diff));
1688
- } catch {
1689
- cosmeticMap.set(sha, { isCosmetic: false });
1690
- }
1691
- })
1692
- );
1693
- return results.map((blame) => {
1935
+ const tasks = [];
1936
+ forEach2(uniqueShas, (sha) => {
1937
+ if (sha === zeroSha) return;
1938
+ tasks.push(
1939
+ (async () => {
1940
+ try {
1941
+ const blameResult = results.find((r) => r.commitHash === sha);
1942
+ if (!blameResult) return;
1943
+ const file = blameResult.originalFile ?? filePath;
1944
+ const diff = await getCosmeticDiff(sha, file, options);
1945
+ cosmeticMap.set(sha, isCosmeticDiff(diff));
1946
+ } catch {
1947
+ cosmeticMap.set(sha, { isCosmetic: false });
1948
+ }
1949
+ })()
1950
+ );
1951
+ });
1952
+ await Promise.all(tasks);
1953
+ return map6(results, (blame) => {
1694
1954
  const cosmetic = cosmeticMap.get(blame.commitHash);
1695
1955
  return {
1696
1956
  blame,
@@ -1700,6 +1960,98 @@ async function analyzeBlameResults(results, options) {
1700
1960
  });
1701
1961
  }
1702
1962
 
1963
+ // src/core/issue-graph/issue-graph.ts
1964
+ import { map as map7 } from "@winglet/common-utils";
1965
+ var DEFAULT_MAX_DEPTH = 2;
1966
+ async function traverseIssueGraph(adapter, startType, startNumber, options) {
1967
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
1968
+ const nodes = [];
1969
+ const edges = [];
1970
+ const visited = /* @__PURE__ */ new Set();
1971
+ await traverse(
1972
+ adapter,
1973
+ startType,
1974
+ startNumber,
1975
+ 0,
1976
+ maxDepth,
1977
+ nodes,
1978
+ edges,
1979
+ visited
1980
+ );
1981
+ return { nodes, edges };
1982
+ }
1983
+ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, visited) {
1984
+ const key = `${type}:${number}`;
1985
+ if (visited.has(key)) return;
1986
+ if (depth > maxDepth) return;
1987
+ visited.add(key);
1988
+ if (type === "pr") {
1989
+ nodes.push({
1990
+ type: "pull_request",
1991
+ trackingMethod: "issue-link",
1992
+ confidence: "exact",
1993
+ prNumber: number
1994
+ });
1995
+ if (depth < maxDepth) {
1996
+ const linkedIssues = await adapter.getLinkedIssues(number);
1997
+ for (const issue of linkedIssues) {
1998
+ edges.push({
1999
+ from: `pr:${number}`,
2000
+ to: `issue:${issue.number}`,
2001
+ relation: "closes"
2002
+ });
2003
+ }
2004
+ await Promise.all(
2005
+ map7(
2006
+ linkedIssues,
2007
+ (issue) => traverse(
2008
+ adapter,
2009
+ "issue",
2010
+ issue.number,
2011
+ depth + 1,
2012
+ maxDepth,
2013
+ nodes,
2014
+ edges,
2015
+ visited
2016
+ )
2017
+ )
2018
+ );
2019
+ }
2020
+ } else {
2021
+ nodes.push({
2022
+ type: "issue",
2023
+ trackingMethod: "issue-link",
2024
+ confidence: "exact",
2025
+ issueNumber: number
2026
+ });
2027
+ if (depth < maxDepth) {
2028
+ const linkedPRs = await adapter.getLinkedPRs(number);
2029
+ for (const pr of linkedPRs) {
2030
+ edges.push({
2031
+ from: `issue:${number}`,
2032
+ to: `pr:${pr.number}`,
2033
+ relation: "referenced-by"
2034
+ });
2035
+ }
2036
+ await Promise.all(
2037
+ map7(
2038
+ linkedPRs,
2039
+ (pr) => traverse(
2040
+ adapter,
2041
+ "pr",
2042
+ pr.number,
2043
+ depth + 1,
2044
+ maxDepth,
2045
+ nodes,
2046
+ edges,
2047
+ visited
2048
+ )
2049
+ )
2050
+ );
2051
+ }
2052
+ }
2053
+ }
2054
+
1703
2055
  // src/core/core.ts
1704
2056
  init_pr_lookup2();
1705
2057
  function computeFeatureFlags(operatingLevel, options) {
@@ -1707,24 +2059,35 @@ function computeFeatureFlags(operatingLevel, options) {
1707
2059
  astDiff: isAstAvailable() && !options.noAst,
1708
2060
  deepTrace: operatingLevel === 2 && (options.deep ?? false),
1709
2061
  commitGraph: false,
1710
- issueGraph: operatingLevel === 2 && (options.graphDepth ?? 0) > 0,
1711
2062
  graphql: operatingLevel === 2
1712
2063
  };
1713
2064
  }
2065
+ async function resolveRepoIdentity(cwd) {
2066
+ try {
2067
+ const result = await gitExec(["rev-parse", "--show-toplevel"], { cwd });
2068
+ const hash = createHash2("sha256").update(result.stdout.trim()).digest("hex").slice(0, 16);
2069
+ return { host: "_local", owner: "_", repo: hash };
2070
+ } catch {
2071
+ return { host: "_local", owner: "_", repo: "_unknown" };
2072
+ }
2073
+ }
1714
2074
  async function detectPlatform2(options) {
1715
2075
  const warnings = [];
1716
2076
  let adapter = null;
2077
+ let remote = null;
1717
2078
  let operatingLevel = 0;
1718
2079
  try {
1719
- const { adapter: detectedAdapter } = await detectPlatformAdapter({
1720
- remoteName: options.remote
2080
+ const detected = await detectPlatformAdapter({
2081
+ remoteName: options.remote,
2082
+ cwd: options.cwd
1721
2083
  });
1722
- adapter = detectedAdapter;
2084
+ adapter = detected.adapter;
2085
+ remote = detected.remote;
1723
2086
  } catch {
1724
2087
  operatingLevel = 0;
1725
2088
  warnings.push("Could not detect platform. Running in Level 0 (git only).");
1726
2089
  }
1727
- return { adapter, operatingLevel, warnings };
2090
+ return { adapter, remote, operatingLevel, warnings };
1728
2091
  }
1729
2092
  async function runBlameAndAuth(adapter, options, execOptions) {
1730
2093
  const warnings = [];
@@ -1732,7 +2095,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
1732
2095
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
1733
2096
  );
1734
2097
  const blameChain = executeBlame(options.file, lineRange, execOptions).then(
1735
- (results) => analyzeBlameResults(results, execOptions)
2098
+ (results) => analyzeBlameResults(results, options.file, execOptions)
1736
2099
  );
1737
2100
  const [authResult, blameResult] = await Promise.allSettled([
1738
2101
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
@@ -1754,55 +2117,83 @@ async function runBlameAndAuth(adapter, options, execOptions) {
1754
2117
  }
1755
2118
  return { analyzed: blameResult.value, operatingLevel, warnings };
1756
2119
  }
1757
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions) {
2120
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
1758
2121
  const nodes = [];
1759
- for (const entry of analyzed) {
1760
- const commitNode = {
1761
- type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
1762
- sha: entry.blame.commitHash,
1763
- trackingMethod: "blame-CMw",
1764
- confidence: "exact",
1765
- note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
1766
- };
1767
- nodes.push(commitNode);
1768
- if (entry.isCosmetic && featureFlags.astDiff) {
1769
- const astResult = await traceByAst(
1770
- options.file,
1771
- options.line,
1772
- entry.blame.commitHash,
1773
- execOptions
1774
- );
1775
- if (astResult) {
1776
- nodes.push({
1777
- type: "original_commit",
1778
- sha: astResult.originSha,
1779
- trackingMethod: "ast-signature",
1780
- confidence: astResult.confidence
1781
- });
1782
- }
2122
+ const commitNode = {
2123
+ type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2124
+ sha: entry.blame.commitHash,
2125
+ trackingMethod: "blame-CMw",
2126
+ confidence: "exact",
2127
+ note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2128
+ };
2129
+ nodes.push(commitNode);
2130
+ if (entry.isCosmetic && featureFlags.astDiff) {
2131
+ const astResult = await traceByAst(
2132
+ options.file,
2133
+ options.line,
2134
+ entry.blame.commitHash,
2135
+ execOptions
2136
+ );
2137
+ if (astResult) {
2138
+ nodes.push({
2139
+ type: "original_commit",
2140
+ sha: astResult.originSha,
2141
+ trackingMethod: "ast-signature",
2142
+ confidence: astResult.confidence
2143
+ });
1783
2144
  }
1784
- const targetSha = nodes[nodes.length - 1].sha;
1785
- if (targetSha) {
1786
- const prInfo = await lookupPR(targetSha, adapter, execOptions);
1787
- if (prInfo) {
1788
- nodes.push({
1789
- type: "pull_request",
1790
- sha: prInfo.mergeCommit,
1791
- trackingMethod: prInfo.url ? "api" : "message-parse",
1792
- confidence: prInfo.url ? "exact" : "heuristic",
1793
- prNumber: prInfo.number,
1794
- prUrl: prInfo.url || void 0,
1795
- prTitle: prInfo.title || void 0,
1796
- mergedAt: prInfo.mergedAt
1797
- });
1798
- }
2145
+ }
2146
+ const targetSha = nodes[nodes.length - 1].sha;
2147
+ if (targetSha) {
2148
+ const prInfo = await lookupPR(targetSha, adapter, {
2149
+ ...execOptions,
2150
+ noCache: options.noCache,
2151
+ deep: featureFlags.deepTrace,
2152
+ repoId
2153
+ });
2154
+ if (prInfo) {
2155
+ nodes.push({
2156
+ type: "pull_request",
2157
+ sha: prInfo.mergeCommit,
2158
+ trackingMethod: prInfo.url ? "api" : "message-parse",
2159
+ confidence: prInfo.url ? "exact" : "heuristic",
2160
+ prNumber: prInfo.number,
2161
+ prUrl: prInfo.url || void 0,
2162
+ prTitle: prInfo.title || void 0,
2163
+ mergedAt: prInfo.mergedAt
2164
+ });
1799
2165
  }
1800
2166
  }
1801
2167
  return nodes;
1802
2168
  }
2169
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
2170
+ const results = await Promise.allSettled(
2171
+ map8(
2172
+ analyzed,
2173
+ (entry) => processEntry(entry, featureFlags, adapter, options, execOptions, repoId)
2174
+ )
2175
+ );
2176
+ return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
2177
+ }
2178
+ var legacyCacheCleaned = false;
1803
2179
  async function trace(options) {
1804
- const execOptions = { cwd: void 0 };
2180
+ const execOptions = { cwd: options.cwd };
2181
+ if (!legacyCacheCleaned) {
2182
+ legacyCacheCleaned = true;
2183
+ cleanupLegacyCache().catch(() => {
2184
+ });
2185
+ }
1805
2186
  const platform = await detectPlatform2(options);
2187
+ let repoId;
2188
+ if (platform.remote) {
2189
+ repoId = {
2190
+ host: platform.remote.host,
2191
+ owner: platform.remote.owner,
2192
+ repo: platform.remote.repo
2193
+ };
2194
+ } else {
2195
+ repoId = await resolveRepoIdentity(options.cwd);
2196
+ }
1806
2197
  const blameAuth = await runBlameAndAuth(
1807
2198
  platform.adapter,
1808
2199
  options,
@@ -1816,10 +2207,26 @@ async function trace(options) {
1816
2207
  featureFlags,
1817
2208
  platform.adapter,
1818
2209
  options,
1819
- execOptions
2210
+ execOptions,
2211
+ repoId
1820
2212
  );
1821
2213
  return { nodes, operatingLevel, featureFlags, warnings };
1822
2214
  }
2215
+ async function graph(options) {
2216
+ const { adapter } = await detectPlatformAdapter({
2217
+ remoteName: options.remote
2218
+ });
2219
+ const auth = await adapter.checkAuth();
2220
+ if (!auth.authenticated) {
2221
+ throw new LineLoreError(
2222
+ LineLoreErrorCode.CLI_NOT_AUTHENTICATED,
2223
+ 'Platform CLI is not authenticated. Run "gh auth login" or set the appropriate token.'
2224
+ );
2225
+ }
2226
+ return traverseIssueGraph(adapter, options.type, options.number, {
2227
+ maxDepth: options.depth
2228
+ });
2229
+ }
1823
2230
  async function health(options) {
1824
2231
  const healthReport = await checkGitHealth(options);
1825
2232
  let operatingLevel = 0;
@@ -1833,10 +2240,20 @@ async function health(options) {
1833
2240
  return { ...healthReport, operatingLevel };
1834
2241
  }
1835
2242
  async function clearCache() {
2243
+ const { rm: rm2 } = await import("fs/promises");
2244
+ const { homedir: homedir2 } = await import("os");
2245
+ const { join: join2 } = await import("path");
1836
2246
  const { resetPRCache: resetPRCache2 } = await Promise.resolve().then(() => (init_pr_lookup2(), pr_lookup_exports));
1837
2247
  const { resetPatchIdCache: resetPatchIdCache2 } = await Promise.resolve().then(() => (init_patch_id2(), patch_id_exports));
1838
2248
  resetPRCache2();
1839
2249
  resetPatchIdCache2();
2250
+ try {
2251
+ await rm2(join2(homedir2(), ".line-lore", "cache"), {
2252
+ recursive: true,
2253
+ force: true
2254
+ });
2255
+ } catch {
2256
+ }
1840
2257
  }
1841
2258
 
1842
2259
  // src/commands/cache.tsx
@@ -1852,98 +2269,15 @@ function registerCacheCommand(program2) {
1852
2269
  });
1853
2270
  }
1854
2271
 
1855
- // src/core/issue-graph/issue-graph.ts
1856
- var DEFAULT_MAX_DEPTH = 2;
1857
- async function traverseIssueGraph(adapter, startType, startNumber, options) {
1858
- const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
1859
- const nodes = [];
1860
- const edges = [];
1861
- const visited = /* @__PURE__ */ new Set();
1862
- await traverse(
1863
- adapter,
1864
- startType,
1865
- startNumber,
1866
- 0,
1867
- maxDepth,
1868
- nodes,
1869
- edges,
1870
- visited
1871
- );
1872
- return { nodes, edges };
1873
- }
1874
- async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, visited) {
1875
- const key = `${type}:${number}`;
1876
- if (visited.has(key)) return;
1877
- if (depth > maxDepth) return;
1878
- visited.add(key);
1879
- if (type === "pr") {
1880
- nodes.push({
1881
- type: "pull_request",
1882
- trackingMethod: "issue-link",
1883
- confidence: "exact",
1884
- prNumber: number
1885
- });
1886
- if (depth < maxDepth) {
1887
- const linkedIssues = await adapter.getLinkedIssues(number);
1888
- for (const issue of linkedIssues) {
1889
- edges.push({
1890
- from: `pr:${number}`,
1891
- to: `issue:${issue.number}`,
1892
- relation: "closes"
1893
- });
1894
- await traverse(
1895
- adapter,
1896
- "issue",
1897
- issue.number,
1898
- depth + 1,
1899
- maxDepth,
1900
- nodes,
1901
- edges,
1902
- visited
1903
- );
1904
- }
1905
- }
1906
- } else {
1907
- nodes.push({
1908
- type: "issue",
1909
- trackingMethod: "issue-link",
1910
- confidence: "exact",
1911
- issueNumber: number
1912
- });
1913
- if (depth < maxDepth) {
1914
- const linkedPRs = await adapter.getLinkedPRs(number);
1915
- for (const pr of linkedPRs) {
1916
- edges.push({
1917
- from: `issue:${number}`,
1918
- to: `pr:${pr.number}`,
1919
- relation: "referenced-by"
1920
- });
1921
- await traverse(
1922
- adapter,
1923
- "pr",
1924
- pr.number,
1925
- depth + 1,
1926
- maxDepth,
1927
- nodes,
1928
- edges,
1929
- visited
1930
- );
1931
- }
1932
- }
1933
- }
1934
- }
1935
-
1936
2272
  // src/commands/graph.tsx
2273
+ init_errors();
1937
2274
  function registerGraphCommand(program2) {
1938
2275
  const graphCmd = program2.command("graph").description("Explore the issue/PR graph");
1939
2276
  graphCmd.command("pr <number>").description("Show issues linked to a PR").option("--depth <n>", "Traversal depth", "1").option("--json", "Output in JSON format").action(async (number, opts) => {
1940
2277
  const prNumber = parseInt(number, 10);
1941
2278
  const depth = parseInt(opts.depth, 10) || 1;
1942
2279
  try {
1943
- const { adapter } = await detectPlatformAdapter();
1944
- const result = await traverseIssueGraph(adapter, "pr", prNumber, {
1945
- maxDepth: depth
1946
- });
2280
+ const result = await graph({ type: "pr", number: prNumber, depth });
1947
2281
  if (opts.json) {
1948
2282
  console.log(JSON.stringify(result, null, 2));
1949
2283
  } else {
@@ -1956,7 +2290,11 @@ function registerGraphCommand(program2) {
1956
2290
  }
1957
2291
  }
1958
2292
  } catch (error) {
1959
- console.error("Graph traversal failed:", error.message);
2293
+ if (error instanceof LineLoreError) {
2294
+ console.error(`Graph traversal failed: ${error.message}`);
2295
+ } else {
2296
+ console.error("Graph traversal failed:", error.message);
2297
+ }
1960
2298
  process.exit(1);
1961
2299
  }
1962
2300
  });
@@ -1964,10 +2302,7 @@ function registerGraphCommand(program2) {
1964
2302
  const issueNumber = parseInt(number, 10);
1965
2303
  const depth = parseInt(opts.depth, 10) || 1;
1966
2304
  try {
1967
- const { adapter } = await detectPlatformAdapter();
1968
- const result = await traverseIssueGraph(adapter, "issue", issueNumber, {
1969
- maxDepth: depth
1970
- });
2305
+ const result = await graph({ type: "issue", number: issueNumber, depth });
1971
2306
  if (opts.json) {
1972
2307
  console.log(JSON.stringify(result, null, 2));
1973
2308
  } else {
@@ -1980,7 +2315,11 @@ function registerGraphCommand(program2) {
1980
2315
  }
1981
2316
  }
1982
2317
  } catch (error) {
1983
- console.error("Graph traversal failed:", error.message);
2318
+ if (error instanceof LineLoreError) {
2319
+ console.error(`Graph traversal failed: ${error.message}`);
2320
+ } else {
2321
+ console.error("Graph traversal failed:", error.message);
2322
+ }
1984
2323
  process.exit(1);
1985
2324
  }
1986
2325
  });
@@ -2076,31 +2415,32 @@ function formatNodeHuman(node) {
2076
2415
  init_normalizer();
2077
2416
  init_errors();
2078
2417
  function registerTraceCommand(program2) {
2079
- program2.command("trace <file>").description("Trace a file line to its originating PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--deep", "Enable deep trace for squash PRs").option("--graph-depth <n>", "Issue graph traversal depth", "0").option("--no-ast", "Disable AST diff analysis").option("--no-cache", "Disable cache").option("--json", "Output in JSON format").option("-q, --quiet", "Output PR number only").option("--output <format>", "Output format: human, json, llm", "human").option("--no-color", "Disable colored output").action(async (file, opts) => {
2418
+ program2.command("trace <file>").description("Trace a file line to its originating PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--deep", "Enable deep trace for squash PRs").option("--no-ast", "Disable AST diff analysis").option("--no-cache", "Disable cache").option("--json", "Output in JSON format").option("-q, --quiet", "Output PR number only").option("--output <format>", "Output format: human, json, llm", "human").option("--no-color", "Disable colored output").action(async (file, opts) => {
2080
2419
  const lineStr = opts.line;
2081
2420
  const parts = lineStr.split(",");
2082
2421
  const line = parseInt(parts[0], 10);
2083
2422
  const endLine = parts.length > 1 ? parseInt(parts[1], 10) : void 0;
2084
- const options = {
2423
+ const traceOptions = {
2085
2424
  file,
2086
2425
  line,
2087
2426
  endLine,
2088
2427
  deep: opts.deep,
2089
- graphDepth: parseInt(opts.graphDepth, 10) || 0,
2090
2428
  noAst: opts.ast === false,
2091
- noCache: opts.cache === false,
2429
+ noCache: opts.cache === false
2430
+ };
2431
+ const cliOptions = {
2092
2432
  json: opts.json,
2093
2433
  quiet: opts.quiet,
2094
2434
  output: opts.output ?? "human"
2095
2435
  };
2096
2436
  try {
2097
- const result = await trace(options);
2437
+ const result = await trace(traceOptions);
2098
2438
  let output;
2099
- if (options.quiet) {
2439
+ if (cliOptions.quiet) {
2100
2440
  output = formatQuiet(result);
2101
- } else if (options.json || options.output === "json") {
2441
+ } else if (cliOptions.json || cliOptions.output === "json") {
2102
2442
  output = formatJson(result);
2103
- } else if (options.output === "llm") {
2443
+ } else if (cliOptions.output === "llm") {
2104
2444
  output = formatLlm(result);
2105
2445
  } else {
2106
2446
  output = formatHuman(result);