difit 3.1.17 → 4.0.0

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.
Files changed (94) hide show
  1. package/README.ja.md +30 -3
  2. package/README.ko.md +30 -3
  3. package/README.md +30 -3
  4. package/README.zh.md +30 -3
  5. package/dist/cli/github.d.ts +65 -0
  6. package/dist/cli/github.js +296 -0
  7. package/dist/cli/github.test.d.ts +1 -0
  8. package/dist/cli/github.test.js +341 -0
  9. package/dist/cli/index.js +26 -1
  10. package/dist/cli/index.test.js +206 -4
  11. package/dist/cli/utils.d.ts +2 -8
  12. package/dist/cli/utils.js +43 -56
  13. package/dist/cli/utils.test.js +61 -67
  14. package/dist/client/assets/{_basePickBy-r8KiD0PT.js → _basePickBy-B9N-f0iT.js} +1 -1
  15. package/dist/client/assets/{_baseUniq-WYpg9s_f.js → _baseUniq-tbL7nVvN.js} +1 -1
  16. package/dist/client/assets/{arc-BZWd656X.js → arc-BOY-7mep.js} +1 -1
  17. package/dist/client/assets/{architectureDiagram-2XIMDMQ5-BiaoV1Oc.js → architectureDiagram-2XIMDMQ5-59AvHaSB.js} +1 -1
  18. package/dist/client/assets/{blockDiagram-WCTKOSBZ-T1RU4TI6.js → blockDiagram-WCTKOSBZ-DXIlumQk.js} +1 -1
  19. package/dist/client/assets/{c4Diagram-IC4MRINW-C1aQSMsj.js → c4Diagram-IC4MRINW-BbfZ0uRn.js} +1 -1
  20. package/dist/client/assets/channel-cZXsTJxA.js +1 -0
  21. package/dist/client/assets/{chunk-4BX2VUAB-DFcwtPlK.js → chunk-4BX2VUAB-l7rcB2IW.js} +1 -1
  22. package/dist/client/assets/{chunk-55IACEB6-Bl3vvNDx.js → chunk-55IACEB6-CrZL3qv9.js} +1 -1
  23. package/dist/client/assets/{chunk-FMBD7UC4-B_2obFwM.js → chunk-FMBD7UC4-CrKv7ndg.js} +1 -1
  24. package/dist/client/assets/{chunk-JSJVCQXG-BrSq4jyX.js → chunk-JSJVCQXG-DyBDhAEM.js} +1 -1
  25. package/dist/client/assets/{chunk-KX2RTZJC-18m3UONJ.js → chunk-KX2RTZJC-By5mkZmU.js} +1 -1
  26. package/dist/client/assets/{chunk-NQ4KR5QH-hFDbMzZU.js → chunk-NQ4KR5QH-C30p9xRx.js} +1 -1
  27. package/dist/client/assets/{chunk-QZHKN3VN-CyCFXX2j.js → chunk-QZHKN3VN-DVlhR2wU.js} +1 -1
  28. package/dist/client/assets/{chunk-WL4C6EOR-BDdHa7t1.js → chunk-WL4C6EOR-Cn7a6CO3.js} +1 -1
  29. package/dist/client/assets/classDiagram-VBA2DB6C-B_coIPEy.js +1 -0
  30. package/dist/client/assets/classDiagram-v2-RAHNMMFH-B_coIPEy.js +1 -0
  31. package/dist/client/assets/clone-BjaT2HOk.js +1 -0
  32. package/dist/client/assets/{cose-bilkent-S5V4N54A-D7t718Sq.js → cose-bilkent-S5V4N54A-LyauIk_9.js} +1 -1
  33. package/dist/client/assets/{dagre-KLK3FWXG-DJXcjsV8.js → dagre-KLK3FWXG-DRWb2KE3.js} +1 -1
  34. package/dist/client/assets/{diagram-E7M64L7V-DL8ck_Al.js → diagram-E7M64L7V-ChT6mNWK.js} +1 -1
  35. package/dist/client/assets/{diagram-IFDJBPK2-NTxUWyD3.js → diagram-IFDJBPK2-CqbTduoP.js} +1 -1
  36. package/dist/client/assets/{diagram-P4PSJMXO-CGkcnGxk.js → diagram-P4PSJMXO-Bzv5Z3ri.js} +1 -1
  37. package/dist/client/assets/{erDiagram-INFDFZHY-BqpbHQrZ.js → erDiagram-INFDFZHY-CvXfUZ4L.js} +1 -1
  38. package/dist/client/assets/{flowDiagram-PKNHOUZH-B-DK3_9I.js → flowDiagram-PKNHOUZH-CxmpNUKq.js} +1 -1
  39. package/dist/client/assets/{ganttDiagram-A5KZAMGK-BK1C57ll.js → ganttDiagram-A5KZAMGK-9LpZCsg6.js} +1 -1
  40. package/dist/client/assets/{gitGraphDiagram-K3NZZRJ6-Duxlcz8R.js → gitGraphDiagram-K3NZZRJ6-C6yZOrQJ.js} +1 -1
  41. package/dist/client/assets/{graph-C7r58m4O.js → graph-bUZ7uHLW.js} +1 -1
  42. package/dist/client/assets/index-BLNN1bfE.js +98 -0
  43. package/dist/client/assets/index-VxkpzDXr.css +1 -0
  44. package/dist/client/assets/{infoDiagram-LFFYTUFH-Bqt-4V9X.js → infoDiagram-LFFYTUFH-Djdy3W21.js} +1 -1
  45. package/dist/client/assets/{ishikawaDiagram-PHBUUO56-B1ZVSkls.js → ishikawaDiagram-PHBUUO56-oOdwCpeS.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-4ABVD52K-LSEcxqrO.js → journeyDiagram-4ABVD52K-DTb_nGAw.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-K7BYSVSG-CldPadPs.js → kanban-definition-K7BYSVSG-CMtP7pHA.js} +1 -1
  48. package/dist/client/assets/{layout-NpxIVVkp.js → layout-CXr5MatK.js} +1 -1
  49. package/dist/client/assets/{linear-JpKpxaS-.js → linear-pOMS9pjV.js} +1 -1
  50. package/dist/client/assets/{mermaid.core-gANNEmg0.js → mermaid.core-DV5JJ1Ie.js} +4 -4
  51. package/dist/client/assets/{mindmap-definition-YRQLILUH-ewFI1yc5.js → mindmap-definition-YRQLILUH-DN-sbonc.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-SKSYHLDU-CWlAr2t8.js → pieDiagram-SKSYHLDU-tAHCkgh1.js} +1 -1
  53. package/dist/client/assets/{prism-csharp-CxRfePTX.js → prism-csharp-5CQ0RcEE.js} +1 -1
  54. package/dist/client/assets/{prism-elixir-B0H1PC_E.js → prism-elixir-BSOTyVg2.js} +1 -1
  55. package/dist/client/assets/{prism-hcl-Csmcce-t.js → prism-hcl-BYvi1mtM.js} +1 -1
  56. package/dist/client/assets/{prism-java-BRzwomgj.js → prism-java-DMU2FM4X.js} +1 -1
  57. package/dist/client/assets/{prism-perl-DQMRA6u_.js → prism-perl-CpfvaEQk.js} +1 -1
  58. package/dist/client/assets/{prism-php-C6fR1C7-.js → prism-php-SC920LoD.js} +1 -1
  59. package/dist/client/assets/{prism-ruby-CWeh27h1.js → prism-ruby-DZph-YiO.js} +1 -1
  60. package/dist/client/assets/{prism-solidity-3wCU4ra_.js → prism-solidity-qTLbmiAT.js} +1 -1
  61. package/dist/client/assets/{quadrantDiagram-337W2JSQ-D76E3PCD.js → quadrantDiagram-337W2JSQ-B0wODmgR.js} +1 -1
  62. package/dist/client/assets/{requirementDiagram-Z7DCOOCP-C49LvKzR.js → requirementDiagram-Z7DCOOCP-A3aeHC06.js} +1 -1
  63. package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-DOvEhLMf.js → sankeyDiagram-WA2Y5GQK-BWa6kZhG.js} +1 -1
  64. package/dist/client/assets/{sequenceDiagram-2WXFIKYE-BR6dsfEq.js → sequenceDiagram-2WXFIKYE-Cx_COX9G.js} +1 -1
  65. package/dist/client/assets/{stateDiagram-RAJIS63D-CHII26YE.js → stateDiagram-RAJIS63D-BXGnN6rZ.js} +1 -1
  66. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-CMw3xNha.js +1 -0
  67. package/dist/client/assets/{timeline-definition-YZTLITO2-DhUTiAsW.js → timeline-definition-YZTLITO2-DbqaUm9k.js} +1 -1
  68. package/dist/client/assets/{treemap-KZPCXAKY-C0Rh3R0y.js → treemap-KZPCXAKY-CfEujPCR.js} +1 -1
  69. package/dist/client/assets/{vennDiagram-LZ73GAT5-CWt3wBDG.js → vennDiagram-LZ73GAT5-CqJE8CAD.js} +1 -1
  70. package/dist/client/assets/{xychartDiagram-JWTSCODW-DhwJwxGz.js → xychartDiagram-JWTSCODW-CfdDvzHC.js} +1 -1
  71. package/dist/client/index.html +2 -2
  72. package/dist/server/generated-file-check.js +113 -58
  73. package/dist/server/generated-file-check.test.js +2 -0
  74. package/dist/server/git-diff.d.ts +1 -0
  75. package/dist/server/git-diff.js +33 -6
  76. package/dist/server/git-diff.test.js +45 -0
  77. package/dist/server/server.d.ts +2 -0
  78. package/dist/server/server.js +107 -34
  79. package/dist/server/server.test.js +120 -0
  80. package/dist/types/diff.d.ts +73 -14
  81. package/dist/utils/commentFormatting.d.ts +4 -2
  82. package/dist/utils/commentFormatting.js +57 -19
  83. package/dist/utils/commentImports.d.ts +9 -0
  84. package/dist/utils/commentImports.js +264 -0
  85. package/dist/utils/commentImports.test.d.ts +1 -0
  86. package/dist/utils/commentImports.test.js +197 -0
  87. package/package.json +3 -3
  88. package/dist/client/assets/channel-C081SflL.js +0 -1
  89. package/dist/client/assets/classDiagram-VBA2DB6C-CD8hB8X7.js +0 -1
  90. package/dist/client/assets/classDiagram-v2-RAHNMMFH-CD8hB8X7.js +0 -1
  91. package/dist/client/assets/clone-DL1yO1kL.js +0 -1
  92. package/dist/client/assets/index-DcsVWNsS.css +0 -1
  93. package/dist/client/assets/index-Igyd6olF.js +0 -92
  94. package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DtCFGPiV.js +0 -1
@@ -14,12 +14,16 @@ vi.mock('./utils.js', async () => {
14
14
  promptUser: vi.fn(),
15
15
  findUntrackedFiles: vi.fn(),
16
16
  markFilesIntentToAdd: vi.fn(),
17
- getPrPatch: vi.fn(),
18
17
  };
19
18
  });
19
+ vi.mock('./github.js', () => ({
20
+ getPrPatch: vi.fn(),
21
+ getPrCommentImports: vi.fn(),
22
+ }));
20
23
  const { simpleGit } = await import('simple-git');
21
24
  const { startServer } = await import('../server/server.js');
22
- const { promptUser, findUntrackedFiles, markFilesIntentToAdd, getPrPatch } = await import('./utils.js');
25
+ const { promptUser, findUntrackedFiles, markFilesIntentToAdd, parseCommentOptions } = await import('./utils.js');
26
+ const { getPrPatch, getPrCommentImports } = await import('./github.js');
23
27
  describe('CLI index.ts', () => {
24
28
  let mockGit;
25
29
  let mockStartServer;
@@ -27,9 +31,12 @@ describe('CLI index.ts', () => {
27
31
  let mockFindUntrackedFiles;
28
32
  let mockMarkFilesIntentToAdd;
29
33
  let mockGetPrPatch;
34
+ let mockGetPrCommentImports;
35
+ let actualParseCommentOptions;
30
36
  // Store original console methods
31
37
  let originalConsoleLog;
32
38
  let originalConsoleError;
39
+ let originalConsoleWarn;
33
40
  let originalProcessExit;
34
41
  beforeEach(() => {
35
42
  // Setup mocks
@@ -48,12 +55,17 @@ describe('CLI index.ts', () => {
48
55
  mockFindUntrackedFiles = vi.mocked(findUntrackedFiles);
49
56
  mockMarkFilesIntentToAdd = vi.mocked(markFilesIntentToAdd);
50
57
  mockGetPrPatch = vi.mocked(getPrPatch);
58
+ mockGetPrCommentImports = vi.mocked(getPrCommentImports);
59
+ mockGetPrCommentImports.mockResolvedValue([]);
60
+ actualParseCommentOptions = parseCommentOptions;
51
61
  // Mock console and process.exit
52
62
  originalConsoleLog = console.log;
53
63
  originalConsoleError = console.error;
64
+ originalConsoleWarn = console.warn;
54
65
  originalProcessExit = process.exit;
55
66
  console.log = vi.fn();
56
67
  console.error = vi.fn();
68
+ console.warn = vi.fn();
57
69
  process.exit = vi.fn();
58
70
  // Reset all mocks
59
71
  vi.clearAllMocks();
@@ -62,6 +74,7 @@ describe('CLI index.ts', () => {
62
74
  // Restore original methods
63
75
  console.log = originalConsoleLog;
64
76
  console.error = originalConsoleError;
77
+ console.warn = originalConsoleWarn;
65
78
  process.exit = originalProcessExit;
66
79
  });
67
80
  describe('CLI argument processing', () => {
@@ -409,14 +422,38 @@ describe('CLI index.ts', () => {
409
422
  });
410
423
  });
411
424
  describe('GitHub PR integration', () => {
412
- it('loads PR patch with gh and starts server with stdin diff', async () => {
425
+ it('loads PR patch, appends manual comments after PR imports, and starts server with stdin diff', async () => {
413
426
  const prUrl = 'https://github.com/owner/repo/pull/123';
414
427
  const prPatch = 'diff --git a/file.ts b/file.ts\nindex 1111111..2222222 100644\n';
428
+ const prCommentImports = [
429
+ {
430
+ type: 'thread',
431
+ id: 'PR_COMMENT_1',
432
+ filePath: 'src/example.ts',
433
+ position: { side: 'new', line: 10 },
434
+ body: 'Imported PR thread',
435
+ author: 'octocat',
436
+ createdAt: '2026-03-25T09:00:00Z',
437
+ updatedAt: '2026-03-25T09:05:00Z',
438
+ },
439
+ {
440
+ type: 'reply',
441
+ id: 'PR_COMMENT_2',
442
+ filePath: 'src/example.ts',
443
+ position: { side: 'new', line: 10 },
444
+ body: 'Imported PR reply',
445
+ author: 'hubot',
446
+ createdAt: '2026-03-25T09:10:00Z',
447
+ updatedAt: '2026-03-25T09:12:00Z',
448
+ },
449
+ ];
415
450
  mockGetPrPatch.mockReturnValue(prPatch);
451
+ mockGetPrCommentImports.mockResolvedValue(prCommentImports);
416
452
  const program = new Command();
417
453
  program
418
454
  .argument('[commit-ish]', 'commit-ish', 'HEAD')
419
455
  .argument('[compare-with]', 'compare-with')
456
+ .option('--comment <json>', 'comment', (value, previous = []) => [...previous, value], [])
420
457
  .option('--port <port>', 'port', parseInt)
421
458
  .option('--host <host>', 'host', '')
422
459
  .option('--no-open', 'no-open')
@@ -424,11 +461,15 @@ describe('CLI index.ts', () => {
424
461
  .option('--tui', 'tui')
425
462
  .option('--pr <url>', 'pr')
426
463
  .action(async (commitish, _compareWith, options) => {
464
+ const manualCommentImports = actualParseCommentOptions(options.comment);
465
+ let commentImports = manualCommentImports;
427
466
  if (options.pr) {
428
467
  if (commitish !== 'HEAD' || _compareWith) {
429
468
  console.error('Error: --pr option cannot be used with positional arguments');
430
469
  process.exit(1);
431
470
  }
471
+ const importedPrComments = await getPrCommentImports(options.pr);
472
+ commentImports = [...importedPrComments, ...manualCommentImports];
432
473
  }
433
474
  await startServer({
434
475
  stdinDiff: getPrPatch(options.pr),
@@ -436,10 +477,82 @@ describe('CLI index.ts', () => {
436
477
  host: options.host,
437
478
  openBrowser: options.open,
438
479
  mode: options.mode,
480
+ commentImports,
439
481
  });
440
482
  });
441
- await program.parseAsync(['--pr', prUrl], { from: 'user' });
483
+ await program.parseAsync([
484
+ '--pr',
485
+ prUrl,
486
+ '--comment',
487
+ '{"type":"reply","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"Manual reply"}',
488
+ ], { from: 'user' });
442
489
  expect(mockGetPrPatch).toHaveBeenCalledWith(prUrl);
490
+ expect(mockGetPrCommentImports).toHaveBeenCalledWith(prUrl);
491
+ expect(mockStartServer).toHaveBeenCalledWith({
492
+ stdinDiff: prPatch,
493
+ preferredPort: undefined,
494
+ host: '',
495
+ openBrowser: true,
496
+ mode: 'split',
497
+ commentImports: [
498
+ ...prCommentImports,
499
+ {
500
+ type: 'reply',
501
+ id: undefined,
502
+ filePath: 'src/example.ts',
503
+ position: { side: 'new', line: 10 },
504
+ body: 'Manual reply',
505
+ author: undefined,
506
+ createdAt: undefined,
507
+ updatedAt: undefined,
508
+ codeSnapshot: undefined,
509
+ },
510
+ ],
511
+ });
512
+ });
513
+ it('continues with patch only when PR comment import fetch fails', async () => {
514
+ const prUrl = 'https://github.com/owner/repo/pull/123';
515
+ const prPatch = 'diff --git a/file.ts b/file.ts\nindex 1111111..2222222 100644\n';
516
+ mockGetPrPatch.mockReturnValue(prPatch);
517
+ mockGetPrCommentImports.mockRejectedValue(new Error('gh api graphql failed'));
518
+ const program = new Command();
519
+ program
520
+ .argument('[commit-ish]', 'commit-ish', 'HEAD')
521
+ .argument('[compare-with]', 'compare-with')
522
+ .option('--comment <json>', 'comment', (value, previous = []) => [...previous, value], [])
523
+ .option('--port <port>', 'port', parseInt)
524
+ .option('--host <host>', 'host', '')
525
+ .option('--no-open', 'no-open')
526
+ .option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
527
+ .option('--tui', 'tui')
528
+ .option('--pr <url>', 'pr')
529
+ .action(async (commitish, _compareWith, options) => {
530
+ const manualCommentImports = actualParseCommentOptions(options.comment);
531
+ let commentImports = manualCommentImports;
532
+ if (options.pr) {
533
+ if (commitish !== 'HEAD' || _compareWith) {
534
+ console.error('Error: --pr option cannot be used with positional arguments');
535
+ process.exit(1);
536
+ }
537
+ try {
538
+ const importedPrComments = await getPrCommentImports(options.pr);
539
+ commentImports = [...importedPrComments, ...manualCommentImports];
540
+ }
541
+ catch (error) {
542
+ console.warn(`Warning: Failed to load PR review comments: ${error instanceof Error ? error.message : 'Unknown error'}`);
543
+ }
544
+ }
545
+ await startServer({
546
+ stdinDiff: getPrPatch(options.pr),
547
+ preferredPort: options.port,
548
+ host: options.host,
549
+ openBrowser: options.open,
550
+ mode: options.mode,
551
+ ...(commentImports.length > 0 ? { commentImports } : {}),
552
+ });
553
+ });
554
+ await program.parseAsync(['--pr', prUrl], { from: 'user' });
555
+ expect(console.warn).toHaveBeenCalledWith('Warning: Failed to load PR review comments: gh api graphql failed');
443
556
  expect(mockStartServer).toHaveBeenCalledWith({
444
557
  stdinDiff: prPatch,
445
558
  preferredPort: undefined,
@@ -503,6 +616,95 @@ describe('CLI index.ts', () => {
503
616
  expect(mockStartServer).not.toHaveBeenCalled();
504
617
  });
505
618
  });
619
+ describe('--comment option', () => {
620
+ it('passes parsed comment imports to startServer', async () => {
621
+ const program = new Command();
622
+ program
623
+ .argument('[commit-ish]', 'commit-ish', 'HEAD')
624
+ .option('--comment <json>', 'comment', (value, previous = []) => [...previous, value], [])
625
+ .option('--port <port>', 'port', parseInt)
626
+ .option('--host <host>', 'host', '')
627
+ .option('--no-open', 'no-open')
628
+ .option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
629
+ .action(async (commitish, options) => {
630
+ const commentImports = actualParseCommentOptions(options.comment);
631
+ await startServer({
632
+ targetCommitish: commitish,
633
+ baseCommitish: `${commitish}^`,
634
+ preferredPort: options.port,
635
+ host: options.host,
636
+ openBrowser: options.open,
637
+ mode: options.mode,
638
+ commentImports,
639
+ });
640
+ });
641
+ await program.parseAsync([
642
+ '--comment',
643
+ '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"Imported comment"}',
644
+ ], { from: 'user' });
645
+ expect(mockStartServer).toHaveBeenCalledWith({
646
+ targetCommitish: 'HEAD',
647
+ baseCommitish: 'HEAD^',
648
+ preferredPort: undefined,
649
+ host: '',
650
+ openBrowser: true,
651
+ mode: 'split',
652
+ commentImports: [
653
+ {
654
+ type: 'thread',
655
+ id: undefined,
656
+ filePath: 'src/example.ts',
657
+ position: { side: 'new', line: 10 },
658
+ body: 'Imported comment',
659
+ author: undefined,
660
+ createdAt: undefined,
661
+ updatedAt: undefined,
662
+ codeSnapshot: undefined,
663
+ },
664
+ ],
665
+ });
666
+ });
667
+ it('rejects --comment with --tui', async () => {
668
+ const program = new Command();
669
+ program
670
+ .argument('[commit-ish]', 'commit-ish', 'HEAD')
671
+ .option('--comment <json>', 'comment', (value, previous = []) => [...previous, value], [])
672
+ .option('--tui', 'tui')
673
+ .action(async (_commitish, options) => {
674
+ const commentImports = actualParseCommentOptions(options.comment);
675
+ if (options.tui && commentImports.length > 0) {
676
+ console.error('Error: --comment option cannot be used with --tui');
677
+ process.exit(1);
678
+ }
679
+ });
680
+ await program.parseAsync([
681
+ '--tui',
682
+ '--comment',
683
+ '{"type":"thread","filePath":"src/example.ts","position":{"side":"new","line":10},"body":"Imported comment"}',
684
+ ], { from: 'user' });
685
+ expect(console.error).toHaveBeenCalledWith('Error: --comment option cannot be used with --tui');
686
+ expect(process.exit).toHaveBeenCalledWith(1);
687
+ });
688
+ it('reports invalid comment json before starting the server', async () => {
689
+ const program = new Command();
690
+ program
691
+ .argument('[commit-ish]', 'commit-ish', 'HEAD')
692
+ .option('--comment <json>', 'comment', (value, previous = []) => [...previous, value], [])
693
+ .action(async (_commitish, options) => {
694
+ try {
695
+ actualParseCommentOptions(options.comment);
696
+ }
697
+ catch (error) {
698
+ console.error(`Error: ${error instanceof Error ? error.message : 'Invalid --comment value'}`);
699
+ process.exit(1);
700
+ }
701
+ });
702
+ await program.parseAsync(['--comment', '{'], { from: 'user' });
703
+ expect(console.error).toHaveBeenCalledWith('Error: Invalid --comment JSON');
704
+ expect(process.exit).toHaveBeenCalledWith(1);
705
+ expect(mockStartServer).not.toHaveBeenCalled();
706
+ });
707
+ });
506
708
  describe('Clean flag functionality', () => {
507
709
  it('displays clean message when flag is used', async () => {
508
710
  mockFindUntrackedFiles.mockResolvedValue([]);
@@ -1,5 +1,6 @@
1
1
  import { type Stats } from 'node:fs';
2
2
  import type { SimpleGit } from 'simple-git';
3
+ import type { CommentImport } from '../types/diff.js';
3
4
  type StdinStat = Pick<Stats, 'isFIFO' | 'isFile' | 'isSocket'>;
4
5
  type StdinSource = 'pipe' | 'file' | 'socket' | 'tty';
5
6
  export declare function detectStdinSource(stdinStat?: StdinStat): StdinSource;
@@ -15,14 +16,7 @@ export declare function getGitRoot(): string;
15
16
  export declare function validateCommitish(commitish: string): boolean;
16
17
  export declare function shortHash(hash: string): string;
17
18
  export declare function createCommitRangeString(baseHash: string, targetHash: string): string;
18
- interface PullRequestInfo {
19
- owner: string;
20
- repo: string;
21
- pullNumber: number;
22
- hostname: string;
23
- }
24
- export declare function parseGitHubPrUrl(url: string): PullRequestInfo | null;
25
- export declare function getPrPatch(prArg: string): string;
19
+ export declare function parseCommentOptions(commentValues: string[]): CommentImport[];
26
20
  export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
27
21
  valid: boolean;
28
22
  error?: string;
package/dist/cli/utils.js CHANGED
@@ -1,6 +1,7 @@
1
- import { execFileSync, execSync } from 'child_process';
1
+ import { execSync } from 'child_process';
2
2
  import { fstatSync } from 'node:fs';
3
3
  import { createInterface } from 'readline/promises';
4
+ import { parseCommentImportValue } from '../utils/commentImports.js';
4
5
  export function detectStdinSource(stdinStat = fstatSync(0)) {
5
6
  if (stdinStat.isFIFO()) {
6
7
  return 'pipe';
@@ -43,26 +44,52 @@ export function validateCommitish(commitish) {
43
44
  if (trimmed.length === 0) {
44
45
  return false;
45
46
  }
46
- // Special cases
47
- if (trimmed === 'HEAD~') {
48
- return false;
49
- }
50
47
  if (trimmed === '.' || trimmed === 'working' || trimmed === 'staged') {
51
48
  return true; // Allow special keywords for working directory and staging area diff
52
49
  }
53
- const validPatterns = [
50
+ const baseCommitish = stripRevisionSuffix(trimmed);
51
+ if (baseCommitish.length === 0) {
52
+ return false;
53
+ }
54
+ return isValidCommitishBase(baseCommitish);
55
+ }
56
+ function isValidCommitishBase(baseCommitish) {
57
+ const validBasePatterns = [
54
58
  /^[a-f0-9]{4,40}$/i, // SHA hashes
55
- /^[a-f0-9]{4,40}\^+$/i, // SHA hashes with ^ suffix (parent references)
56
- /^[a-f0-9]{4,40}~\d+$/i, // SHA hashes with ~N suffix (ancestor references)
57
- /^HEAD(~\d+|\^\d*)*$/, // HEAD, HEAD~1, HEAD^, HEAD^2, etc.
58
- /^@(~\d+|\^\d*)*$/, // @, @~1, @^, @^2, etc. (@ is Git alias for HEAD)
59
+ /^HEAD$/, // HEAD
60
+ /^@$/, // @ is Git alias for HEAD
59
61
  ];
60
- // Check if it matches any specific patterns first
61
- if (validPatterns.some((pattern) => pattern.test(trimmed))) {
62
+ if (validBasePatterns.some((pattern) => pattern.test(baseCommitish))) {
62
63
  return true;
63
64
  }
64
- // For branch names, use git's rules
65
- return isValidBranchName(trimmed);
65
+ // For branch, tag, and remote refs, use git's ref naming rules.
66
+ return isValidBranchName(baseCommitish);
67
+ }
68
+ function stripRevisionSuffix(commitish) {
69
+ let suffixStart = commitish.length;
70
+ while (suffixStart > 0) {
71
+ const current = commitish[suffixStart - 1];
72
+ if (current === '^') {
73
+ suffixStart--;
74
+ continue;
75
+ }
76
+ if (!isAsciiDigit(current)) {
77
+ break;
78
+ }
79
+ let digitStart = suffixStart - 1;
80
+ while (digitStart > 0 && isAsciiDigit(commitish[digitStart - 1])) {
81
+ digitStart--;
82
+ }
83
+ const operator = commitish[digitStart - 1];
84
+ if (operator !== '^' && operator !== '~') {
85
+ break;
86
+ }
87
+ suffixStart = digitStart - 1;
88
+ }
89
+ return commitish.slice(0, suffixStart);
90
+ }
91
+ function isAsciiDigit(char) {
92
+ return char >= '0' && char <= '9';
66
93
  }
67
94
  function isValidBranchName(name) {
68
95
  // Git branch name rules
@@ -103,48 +130,8 @@ export function shortHash(hash) {
103
130
  export function createCommitRangeString(baseHash, targetHash) {
104
131
  return `${baseHash}...${targetHash}`;
105
132
  }
106
- export function parseGitHubPrUrl(url) {
107
- try {
108
- const urlObj = new URL(url);
109
- // Allow any hostname for GitHub Enterprise support
110
- // Just validate the path structure
111
- const pathParts = urlObj.pathname.split('/').filter(Boolean);
112
- if (pathParts.length < 4 || pathParts[2] !== 'pull') {
113
- return null;
114
- }
115
- const owner = pathParts[0];
116
- const repo = pathParts[1];
117
- const pullNumber = parseInt(pathParts[3], 10);
118
- if (isNaN(pullNumber)) {
119
- return null;
120
- }
121
- return { owner, repo, pullNumber, hostname: urlObj.hostname };
122
- }
123
- catch {
124
- return null;
125
- }
126
- }
127
- export function getPrPatch(prArg) {
128
- try {
129
- const patch = execFileSync('gh', ['pr', 'diff', prArg], {
130
- encoding: 'utf8',
131
- stdio: ['ignore', 'pipe', 'pipe'],
132
- });
133
- if (!patch.trim()) {
134
- throw new Error('No diff content returned from gh pr diff');
135
- }
136
- return patch;
137
- }
138
- catch (error) {
139
- const stderr = error.stderr;
140
- const stderrText = typeof stderr === 'string'
141
- ? stderr.trim()
142
- : Buffer.isBuffer(stderr)
143
- ? stderr.toString('utf8').trim()
144
- : '';
145
- const message = stderrText || (error instanceof Error ? error.message : 'Unknown error while running gh');
146
- throw new Error(`${message}\nTry: gh auth login`);
147
- }
133
+ export function parseCommentOptions(commentValues) {
134
+ return commentValues.flatMap((value) => parseCommentImportValue(value));
148
135
  }
149
136
  export function validateDiffArguments(targetCommitish, baseCommitish) {
150
137
  // Validate target commitish format
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { detectStdinSource, parseGitHubPrUrl, shortHash, shouldReadStdin, validateCommitish, validateDiffArguments, } from './utils';
2
+ import { detectStdinSource, parseCommentOptions, shortHash, shouldReadStdin, validateCommitish, validateDiffArguments, } from './utils';
3
3
  describe('CLI Utils', () => {
4
4
  describe('stdin detection', () => {
5
5
  it('detects pipe from stdin stat', () => {
@@ -99,6 +99,55 @@ describe('CLI Utils', () => {
99
99
  })).toBe(false);
100
100
  });
101
101
  });
102
+ describe('parseCommentOptions', () => {
103
+ it('parses a single comment import', () => {
104
+ const result = parseCommentOptions([
105
+ JSON.stringify({
106
+ type: 'thread',
107
+ filePath: 'src/example.ts',
108
+ position: { side: 'new', line: 10 },
109
+ body: 'Imported comment',
110
+ }),
111
+ ]);
112
+ expect(result).toEqual([
113
+ {
114
+ type: 'thread',
115
+ id: undefined,
116
+ filePath: 'src/example.ts',
117
+ position: { side: 'new', line: 10 },
118
+ body: 'Imported comment',
119
+ author: undefined,
120
+ createdAt: undefined,
121
+ updatedAt: undefined,
122
+ codeSnapshot: undefined,
123
+ },
124
+ ]);
125
+ });
126
+ it('flattens array values from repeated options', () => {
127
+ const result = parseCommentOptions([
128
+ JSON.stringify([
129
+ {
130
+ type: 'thread',
131
+ filePath: 'src/example.ts',
132
+ position: { side: 'new', line: 10 },
133
+ body: 'Imported comment',
134
+ },
135
+ ]),
136
+ JSON.stringify({
137
+ type: 'reply',
138
+ filePath: 'src/example.ts',
139
+ position: { side: 'new', line: 10 },
140
+ body: 'Imported reply',
141
+ }),
142
+ ]);
143
+ expect(result).toHaveLength(2);
144
+ expect(result[0]?.type).toBe('thread');
145
+ expect(result[1]?.type).toBe('reply');
146
+ });
147
+ it('throws for invalid json', () => {
148
+ expect(() => parseCommentOptions(['{'])).toThrow('Invalid --comment JSON');
149
+ });
150
+ });
102
151
  describe('validateCommitish', () => {
103
152
  it('should validate full SHA hashes', () => {
104
153
  expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd')).toBe(true);
@@ -152,6 +201,13 @@ describe('CLI Utils', () => {
152
201
  expect(validateCommitish('release/v2.3.1')).toBe(true); // version numbers
153
202
  expect(validateCommitish('bugfix/login-timeout')).toBe(true); // path with dash
154
203
  });
204
+ it('should validate branch and remote refs with revision suffixes', () => {
205
+ expect(validateCommitish('main^')).toBe(true);
206
+ expect(validateCommitish('origin/main~2')).toBe(true);
207
+ expect(validateCommitish('codex/comment-thread^')).toBe(true);
208
+ expect(validateCommitish('feature/new-feature^2')).toBe(true);
209
+ expect(validateCommitish('release/v2.3.1~3^1')).toBe(true);
210
+ });
155
211
  it('should validate special cases', () => {
156
212
  expect(validateCommitish('.')).toBe(true); // working directory diff
157
213
  });
@@ -282,6 +338,10 @@ describe('CLI Utils', () => {
282
338
  expect(validateDiffArguments('feature/branch-name', 'origin/main')).toEqual({
283
339
  valid: true,
284
340
  });
341
+ expect(validateDiffArguments('main', 'main^')).toEqual({ valid: true });
342
+ expect(validateDiffArguments('codex/comment-thread', 'codex/comment-thread^')).toEqual({
343
+ valid: true,
344
+ });
285
345
  });
286
346
  it('should handle SHA hashes with parent/ancestor references', () => {
287
347
  expect(validateDiffArguments('bd4b7513e075b5b245284c38fd23427b9bd0f42e^', 'abc123')).toEqual({ valid: true });
@@ -304,70 +364,4 @@ describe('CLI Utils', () => {
304
364
  expect(shortHash('')).toBe('');
305
365
  });
306
366
  });
307
- describe('parseGitHubPrUrl', () => {
308
- it('should parse valid GitHub PR URLs', () => {
309
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/123');
310
- expect(result).toEqual({
311
- owner: 'owner',
312
- repo: 'repo',
313
- pullNumber: 123,
314
- hostname: 'github.com',
315
- });
316
- });
317
- it('should parse GitHub PR URLs with additional path segments', () => {
318
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/456/files');
319
- expect(result).toEqual({
320
- owner: 'owner',
321
- repo: 'repo',
322
- pullNumber: 456,
323
- hostname: 'github.com',
324
- });
325
- });
326
- it('should parse GitHub PR URLs with query parameters', () => {
327
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/789?tab=files');
328
- expect(result).toEqual({
329
- owner: 'owner',
330
- repo: 'repo',
331
- pullNumber: 789,
332
- hostname: 'github.com',
333
- });
334
- });
335
- it('should handle URLs with hyphens and underscores in owner/repo names', () => {
336
- const result = parseGitHubPrUrl('https://github.com/owner-name/repo_name/pull/123');
337
- expect(result).toEqual({
338
- owner: 'owner-name',
339
- repo: 'repo_name',
340
- pullNumber: 123,
341
- hostname: 'github.com',
342
- });
343
- });
344
- it('should parse GitHub Enterprise PR URLs', () => {
345
- const result1 = parseGitHubPrUrl('https://github.enterprise.com/owner/repo/pull/123');
346
- expect(result1).toEqual({
347
- owner: 'owner',
348
- repo: 'repo',
349
- pullNumber: 123,
350
- hostname: 'github.enterprise.com',
351
- });
352
- const result2 = parseGitHubPrUrl('https://git.company.io/team/project/pull/456');
353
- expect(result2).toEqual({
354
- owner: 'team',
355
- repo: 'project',
356
- pullNumber: 456,
357
- hostname: 'git.company.io',
358
- });
359
- });
360
- it('should return null for invalid URLs', () => {
361
- expect(parseGitHubPrUrl('not-a-url')).toBe(null);
362
- expect(parseGitHubPrUrl('https://github.com/owner/repo/issues/123')).toBe(null);
363
- expect(parseGitHubPrUrl('https://github.com/owner/repo')).toBe(null);
364
- expect(parseGitHubPrUrl('https://github.com/owner/repo/pull/abc')).toBe(null);
365
- });
366
- it('should handle malformed URLs gracefully', () => {
367
- expect(parseGitHubPrUrl('')).toBe(null);
368
- expect(parseGitHubPrUrl('https://github.com')).toBe(null);
369
- expect(parseGitHubPrUrl('https://github.com/owner')).toBe(null);
370
- expect(parseGitHubPrUrl('https://github.com/owner/repo/pull')).toBe(null);
371
- });
372
- });
373
367
  });
@@ -1 +1 @@
1
- import{e as x,c as O,g as m,k as P,h as p,j as w,l as c,m as A,n as I,t as N,o as _}from"./_baseUniq-WYpg9s_f.js";import{aT as g,at as $,aU as E,aV as F,aW as M,aX as l,aY as T,aZ as B,a_ as y,a$ as S}from"./mermaid.core-gANNEmg0.js";var G=/\s/;function H(n){for(var r=n.length;r--&&G.test(n.charAt(r)););return r}var L=/^\s+/;function R(n){return n&&n.slice(0,H(n)+1).replace(L,"")}var o=NaN,W=/^[-+]0x[0-9a-f]+$/i,X=/^0b[01]+$/i,Y=/^0o[0-7]+$/i,q=parseInt;function z(n){if(typeof n=="number")return n;if(x(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=R(n);var t=X.test(n);return t||Y.test(n)?q(n.slice(2),t?2:8):W.test(n)?o:+n}var v=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=z(n),n===v||n===-v){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?O(n):[]}var b=Object.prototype,Z=b.hasOwnProperty,dn=$(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&E(r[0],r[1],a)&&(i=1);++t<i;)for(var f=r[t],e=F(f),s=-1,d=e.length;++s<d;){var u=e[s],h=n[u];(h===void 0||M(h,b[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,i){var a=Object(r);if(!l(r)){var f=m(t);r=P(r),t=function(s){return f(a[s],s,a)}}var e=n(r,t,i);return e>-1?a[f?r[e]:e]:void 0}}var J=Math.max;function Q(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:U(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,i=l(n)?Array(n.length):[];return w(n,function(a,f,e){i[++t]=r(a,f,e)}),i}function gn(n,r){var t=T(n)?c:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&A(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var i=-1,a=n.length;++i<a;){var f=n[i],e=r(f);if(e!=null&&(s===void 0?e===e&&!x(e):t(e,s)))var s=e,d=f}return d}function on(n){return n&&n.length?tn(n,B,rn):void 0}function an(n,r,t,i){if(!g(n))return n;r=I(r,n);for(var a=-1,f=r.length,e=f-1,s=n;s!=null&&++a<f;){var d=N(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=e){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:y(r[a+1])?[]:{})}S(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var i=-1,a=r.length,f={};++i<a;){var e=r[i],s=_(n,e);t(s,e)&&an(f,I(e,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};
1
+ import{e as x,c as O,g as m,k as P,h as p,j as w,l as c,m as A,n as I,t as N,o as _}from"./_baseUniq-tbL7nVvN.js";import{aT as g,at as $,aU as E,aV as F,aW as M,aX as l,aY as T,aZ as B,a_ as y,a$ as S}from"./mermaid.core-DV5JJ1Ie.js";var G=/\s/;function H(n){for(var r=n.length;r--&&G.test(n.charAt(r)););return r}var L=/^\s+/;function R(n){return n&&n.slice(0,H(n)+1).replace(L,"")}var o=NaN,W=/^[-+]0x[0-9a-f]+$/i,X=/^0b[01]+$/i,Y=/^0o[0-7]+$/i,q=parseInt;function z(n){if(typeof n=="number")return n;if(x(n))return o;if(g(n)){var r=typeof n.valueOf=="function"?n.valueOf():n;n=g(r)?r+"":r}if(typeof n!="string")return n===0?n:+n;n=R(n);var t=X.test(n);return t||Y.test(n)?q(n.slice(2),t?2:8):W.test(n)?o:+n}var v=1/0,C=17976931348623157e292;function K(n){if(!n)return n===0?n:0;if(n=z(n),n===v||n===-v){var r=n<0?-1:1;return r*C}return n===n?n:0}function U(n){var r=K(n),t=r%1;return r===r?t?r-t:r:0}function fn(n){var r=n==null?0:n.length;return r?O(n):[]}var b=Object.prototype,Z=b.hasOwnProperty,dn=$(function(n,r){n=Object(n);var t=-1,i=r.length,a=i>2?r[2]:void 0;for(a&&E(r[0],r[1],a)&&(i=1);++t<i;)for(var f=r[t],e=F(f),s=-1,d=e.length;++s<d;){var u=e[s],h=n[u];(h===void 0||M(h,b[u])&&!Z.call(n,u))&&(n[u]=f[u])}return n});function un(n){var r=n==null?0:n.length;return r?n[r-1]:void 0}function D(n){return function(r,t,i){var a=Object(r);if(!l(r)){var f=m(t);r=P(r),t=function(s){return f(a[s],s,a)}}var e=n(r,t,i);return e>-1?a[f?r[e]:e]:void 0}}var J=Math.max;function Q(n,r,t){var i=n==null?0:n.length;if(!i)return-1;var a=t==null?0:U(t);return a<0&&(a=J(i+a,0)),p(n,m(r),a)}var hn=D(Q);function V(n,r){var t=-1,i=l(n)?Array(n.length):[];return w(n,function(a,f,e){i[++t]=r(a,f,e)}),i}function gn(n,r){var t=T(n)?c:V;return t(n,m(r))}var j=Object.prototype,k=j.hasOwnProperty;function nn(n,r){return n!=null&&k.call(n,r)}function mn(n,r){return n!=null&&A(n,r,nn)}function rn(n,r){return n<r}function tn(n,r,t){for(var i=-1,a=n.length;++i<a;){var f=n[i],e=r(f);if(e!=null&&(s===void 0?e===e&&!x(e):t(e,s)))var s=e,d=f}return d}function on(n){return n&&n.length?tn(n,B,rn):void 0}function an(n,r,t,i){if(!g(n))return n;r=I(r,n);for(var a=-1,f=r.length,e=f-1,s=n;s!=null&&++a<f;){var d=N(r[a]),u=t;if(d==="__proto__"||d==="constructor"||d==="prototype")return n;if(a!=e){var h=s[d];u=void 0,u===void 0&&(u=g(h)?h:y(r[a+1])?[]:{})}S(s,d,u),s=s[d]}return n}function vn(n,r,t){for(var i=-1,a=r.length,f={};++i<a;){var e=r[i],s=_(n,e);t(s,e)&&an(f,I(e,n),s)}return f}export{rn as a,tn as b,V as c,vn as d,on as e,fn as f,hn as g,mn as h,dn as i,U as j,un as l,gn as m,K as t};