difit 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -18,59 +18,64 @@ A lightweight command-line tool that spins up a local web server to display Git
18
18
  - 🎨 **Syntax Highlighting**: Dynamic language loading for Bash, PHP, SQL, Ruby, Java, and more
19
19
  - ✨ **100% vibe coding**: Built with pure coding energy and good vibes
20
20
 
21
- ## 📦 Installation
21
+ ## Quick Start
22
22
 
23
23
  ```bash
24
- # use npx (no installation needed)
25
- npx reviewit <commit-ish>
26
-
27
- # or Global install
28
- npm install -g reviewit
24
+ npx reviewit # View HEAD commit changes in a beautiful diff viewer
29
25
  ```
30
26
 
31
27
  ## 🚀 Usage
32
28
 
29
+ ### Basic Usage
30
+
33
31
  ```bash
34
- # default: HEAD commit changes
35
- npx reviewit
32
+ npx reviewit <commit-ish> # View single commit diff
33
+ npx reviewit <commit-ish> [compare-with] # Compare two commits/branches
34
+ ```
36
35
 
37
- # Review a specific commit changes
38
- npx reviewit 6f4a9b7
39
- npx reviewit HEAD^
40
- npx reviewit HEAD~3
36
+ ### Single commit review
41
37
 
42
- # Special Arguments
43
- npx reviewit staging # staging area changes
44
- npx reviewit working # working directory changes
45
- npx reviewit . # uncommited all changes
38
+ ```bash
39
+ npx reviewit 6f4a9b7 # Specific commit
40
+ npx reviewit HEAD^ # Previous commit
41
+ npx reviewit feature # Latest commit on branch
42
+ ```
46
43
 
47
- # Custom port, don't auto-open browser
48
- npx reviewit 6f4a9b7 --port 4300 --no-open
44
+ ### Compare two commits
49
45
 
50
- # Terminal UI mode (no browser)
51
- npx reviewit --tui
52
- npx reviewit working --tui
46
+ ```bash
47
+ npx reviewit HEAD main # Compare HEAD with main branch
48
+ npx reviewit feature main # Compare branches
49
+ npx reviewit . origin/main # Compare working directory with remote main
53
50
  ```
54
51
 
55
- ### ⚙️ CLI Options
52
+ ### Special Arguments
56
53
 
57
- | Flag | Default | Description |
58
- | -------------- | ------------ | ------------------------------------------------------------------ |
59
- | `<commit-ish>` | (required) | Any Git reference: hash, tag, HEAD~n, branch, or Special Arguments |
60
- | `--port` | auto | Preferred port; falls back if occupied |
61
- | `--no-open` | false | Don't automatically open browser |
62
- | `--mode` | side-by-side | Diff mode: `inline` or `side-by-side` |
63
- | `--tui` | false | Use terminal UI mode instead of web interface |
54
+ ReviewIt supports special keywords for common diff scenarios:
64
55
 
65
- ### 🔑 Special Arguments
56
+ ```bash
57
+ npx reviewit # HEAD commit changes
58
+ npx reviewit . # All uncommitted changes (staged + unstaged)
59
+ npx reviewit staged # Staged changes ready for commit
60
+ npx reviewit working # Unstaged changes only (cannot use compare-with)
61
+ ```
66
62
 
67
- ReviewIt supports special arguments for common diff scenarios:
63
+ | Keyword | Description | Compare-with Support |
64
+ | --------- | ------------------------------------------------------ | -------------------- |
65
+ | `.` | Shows all uncommitted changes (both staged & unstaged) | ✅ Yes |
66
+ | `staged` | Shows staged changes ready to be committed | ✅ Yes |
67
+ | `working` | Shows unstaged changes in your working directory | ❌ No |
68
+
69
+ ### ⚙️ CLI Options
68
70
 
69
- | Keyword | Description |
70
- | --------- | ------------------------------------------------------ |
71
- | `working` | Shows unstaged changes in your working directory |
72
- | `staged` | Shows staged changes ready to be committed |
73
- | `.` | Shows all uncommitted changes (both staged & unstaged) |
71
+ | Flag | Default | Description |
72
+ | ---------------- | ------------ | ------------------------------------------------------------------- |
73
+ | `<commit-ish>` | HEAD | Any Git reference: hash, tag, HEAD~n, branch, or Special Arguments |
74
+ | `[compare-with]` | (optional) | Optional second commit to compare with (shows diff between the two) |
75
+ | `--port` | auto | Preferred port; falls back if occupied |
76
+ | `--no-open` | false | Don't automatically open browser |
77
+ | `--mode` | side-by-side | Diff mode: `inline` or `side-by-side` |
78
+ | `--tui` | false | Use terminal UI mode instead of web interface |
74
79
 
75
80
  ## 💬 Comment System
76
81
 
@@ -118,15 +123,10 @@ pnpm install
118
123
 
119
124
  # Start development server (with hot reload)
120
125
  # This runs both Vite dev server and CLI with NODE_ENV=development
121
- pnpm run dev # defaults to HEAD
122
- pnpm run dev HEAD~3 # review HEAD~3
123
- pnpm run dev main # review main branch
124
-
125
- # For development CLI only (connects to separate Vite server)
126
- pnpm run dev:cli <commit-ish>
126
+ pnpm run dev
127
127
 
128
128
  # Build and start production server
129
- pnpm run start HEAD
129
+ pnpm run start <commit-ish>
130
130
 
131
131
  # Build for production
132
132
  pnpm run build
@@ -143,20 +143,27 @@ pnpm run typecheck
143
143
  ### Development Workflow
144
144
 
145
145
  - **`pnpm run dev`**: Starts both Vite dev server (with hot reload) and CLI server simultaneously
146
- - **`pnpm run start <commit>`**: Builds everything and starts production server (for testing final build)
146
+ - **`pnpm run start <commit-ish>`**: Builds everything and starts production server (for testing final build)
147
147
  - **Development mode**: Uses Vite's dev server for hot reload and fast development
148
148
  - **Production mode**: Serves built static files (used by npx and production builds)
149
149
 
150
150
  ## 🏗️ Architecture
151
151
 
152
- - **CLI**: Commander.js for argument parsing
152
+ - **CLI**: Commander.js for argument parsing with comprehensive validation
153
153
  - **Backend**: Express server with simple-git for diff processing
154
154
  - **Frontend**: React 18 + TypeScript + Vite
155
155
  - **Styling**: Tailwind CSS v4 with GitHub-like dark theme
156
156
  - **Syntax Highlighting**: Prism.js with dynamic language loading
157
- - **Testing**: Vitest for unit tests
157
+ - **Testing**: Vitest for unit tests with co-located test files
158
158
  - **Quality**: ESLint, Prettier, lefthook pre-commit hooks
159
159
 
160
+ ### Key Components
161
+
162
+ - **Validation System**: Unified validation logic for CLI arguments with comprehensive error handling
163
+ - **Dual Parameter System**: Internal refactoring splits commitish into targetCommitish and baseCommitish for flexibility
164
+ - **Special Argument Support**: Working directory, staging area, and uncommitted changes detection
165
+ - **Hash Utilities**: Consistent short hash generation for commit display
166
+
160
167
  ## 📋 Requirements
161
168
 
162
169
  - Node.js ≥ 18.0.0
package/dist/cli/index.js CHANGED
@@ -3,30 +3,38 @@ import { Command } from 'commander';
3
3
  import React from 'react';
4
4
  import pkg from '../../package.json' with { type: 'json' };
5
5
  import { startServer } from '../server/server.js';
6
- import { validateCommitish } from './utils.js';
6
+ import { validateDiffArguments } from './utils.js';
7
+ function isSpecialArg(arg) {
8
+ return arg === 'working' || arg === 'staged' || arg === '.';
9
+ }
7
10
  const program = new Command();
8
11
  program
9
12
  .name('reviewit')
10
13
  .description('A lightweight Git diff viewer with GitHub-like interface')
11
14
  .version(pkg.version)
12
15
  .argument('[commit-ish]', 'Git commit, tag, branch, HEAD~n reference, or "working"/"staged"/"." (default: HEAD)', 'HEAD')
16
+ .argument('[compare-with]', 'Optional: Compare with this commit/branch (shows diff between commit-ish and compare-with)')
13
17
  .option('--port <port>', 'preferred port (auto-assigned if occupied)', parseInt)
14
18
  .option('--no-open', 'do not automatically open browser')
15
19
  .option('--mode <mode>', 'diff mode (inline only for now)', 'inline')
16
20
  .option('--tui', 'use terminal UI instead of web interface')
17
- .action(async (commitish, options) => {
21
+ .action(async (commitish, compareWith, options) => {
18
22
  try {
19
- // Determine what to show
23
+ // Determine target and base commitish
20
24
  let targetCommitish = commitish;
21
- // Handle special arguments
22
- if (commitish === 'working') {
23
- targetCommitish = 'working';
25
+ let baseCommitish;
26
+ if (compareWith) {
27
+ // If compareWith is provided, use it as base
28
+ baseCommitish = compareWith;
24
29
  }
25
- else if (commitish === 'staged') {
26
- targetCommitish = 'staged';
27
- }
28
- else if (commitish === '.') {
29
- targetCommitish = '.';
30
+ else {
31
+ // Handle special arguments
32
+ if (isSpecialArg(commitish)) {
33
+ baseCommitish = 'HEAD';
34
+ }
35
+ else {
36
+ baseCommitish = commitish + '^';
37
+ }
30
38
  }
31
39
  if (options.tui) {
32
40
  // Check if we're in a TTY environment
@@ -38,15 +46,17 @@ program
38
46
  // Dynamic import for TUI mode
39
47
  const { render } = await import('ink');
40
48
  const { default: TuiApp } = await import('../tui/App.js');
41
- render(React.createElement(TuiApp, { commitish: targetCommitish }));
49
+ render(React.createElement(TuiApp, { targetCommitish, baseCommitish }));
42
50
  return;
43
51
  }
44
- if (!validateCommitish(targetCommitish)) {
45
- console.error('Error: Invalid commit-ish format');
52
+ const validation = validateDiffArguments(targetCommitish, compareWith);
53
+ if (!validation.valid) {
54
+ console.error(`Error: ${validation.error}`);
46
55
  process.exit(1);
47
56
  }
48
- const { url } = await startServer({
49
- commitish: targetCommitish,
57
+ const { url, port } = await startServer({
58
+ targetCommitish,
59
+ baseCommitish,
50
60
  preferredPort: options.port,
51
61
  openBrowser: options.open,
52
62
  mode: options.mode,
@@ -59,8 +69,21 @@ program
59
69
  else {
60
70
  console.log('💡 Use --open to automatically open browser\n');
61
71
  }
62
- process.on('SIGINT', () => {
72
+ process.on('SIGINT', async () => {
63
73
  console.log('\n👋 Shutting down ReviewIt server...');
74
+ // Try to fetch comments before shutting down
75
+ try {
76
+ const response = await fetch(`http://localhost:${port}/api/comments-output`);
77
+ if (response.ok) {
78
+ const data = await response.text();
79
+ if (data.trim()) {
80
+ console.log(data);
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // Silently ignore fetch errors during shutdown
86
+ }
64
87
  process.exit(0);
65
88
  });
66
89
  }
@@ -1 +1,6 @@
1
1
  export declare function validateCommitish(commitish: string): boolean;
2
+ export declare function shortHash(hash: string): string;
3
+ export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
4
+ valid: boolean;
5
+ error?: string;
6
+ };
package/dist/cli/utils.js CHANGED
@@ -20,3 +20,36 @@ export function validateCommitish(commitish) {
20
20
  ];
21
21
  return validPatterns.some((pattern) => pattern.test(trimmed));
22
22
  }
23
+ export function shortHash(hash) {
24
+ return hash.substring(0, 7);
25
+ }
26
+ export function validateDiffArguments(targetCommitish, baseCommitish) {
27
+ // Validate target commitish format
28
+ if (!validateCommitish(targetCommitish)) {
29
+ return { valid: false, error: 'Invalid target commit-ish format' };
30
+ }
31
+ // Validate base commitish format if provided
32
+ if (baseCommitish !== undefined && !validateCommitish(baseCommitish)) {
33
+ return { valid: false, error: 'Invalid base commit-ish format' };
34
+ }
35
+ // Special arguments are only allowed in target, not base
36
+ const specialArgs = ['working', 'staged', '.'];
37
+ if (baseCommitish && specialArgs.includes(baseCommitish)) {
38
+ return {
39
+ valid: false,
40
+ error: `Special arguments (working, staged, .) are only allowed as target, not base. Got base: ${baseCommitish}`,
41
+ };
42
+ }
43
+ // Cannot compare same values
44
+ if (targetCommitish === baseCommitish) {
45
+ return { valid: false, error: `Cannot compare ${targetCommitish} with itself` };
46
+ }
47
+ // "working" shows unstaged changes and cannot be compared with another commit
48
+ if (targetCommitish === 'working' && baseCommitish) {
49
+ return {
50
+ valid: false,
51
+ error: '"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.',
52
+ };
53
+ }
54
+ return { valid: true };
55
+ }
@@ -1,23 +1,11 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { validateCommitish, validateDiffArguments, shortHash, parseGitHubPrUrl } from './utils';
2
+ import { validateCommitish, validateDiffArguments, shortHash } from './utils';
3
3
  describe('CLI Utils', () => {
4
4
  describe('validateCommitish', () => {
5
5
  it('should validate full SHA hashes', () => {
6
6
  expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd')).toBe(true);
7
7
  expect(validateCommitish('abc123')).toBe(true);
8
8
  });
9
- it('should validate SHA hashes with parent references', () => {
10
- expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd^')).toBe(true);
11
- expect(validateCommitish('abc123^')).toBe(true);
12
- expect(validateCommitish('abc123^^')).toBe(true);
13
- expect(validateCommitish('bd4b7513e075b5b245284c38fd23427b9bd0f42e^')).toBe(true);
14
- });
15
- it('should validate SHA hashes with ancestor references', () => {
16
- expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd~1')).toBe(true);
17
- expect(validateCommitish('abc123~5')).toBe(true);
18
- expect(validateCommitish('abc123~10')).toBe(true);
19
- expect(validateCommitish('bd4b7513e075b5b245284c38fd23427b9bd0f42e~2')).toBe(true);
20
- });
21
9
  it('should validate HEAD references', () => {
22
10
  expect(validateCommitish('HEAD')).toBe(true);
23
11
  expect(validateCommitish('HEAD~1')).toBe(true);
@@ -85,12 +73,6 @@ describe('CLI Utils', () => {
85
73
  expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
86
74
  expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
87
75
  });
88
- it('should allow staged as base only with working target', () => {
89
- expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
90
- const result = validateDiffArguments('HEAD', 'staged');
91
- expect(result.valid).toBe(false);
92
- expect(result.error).toBe('Special arguments (working, staged, .) are only allowed as target, not base. Got base: staged');
93
- });
94
76
  });
95
77
  describe('same value comparison', () => {
96
78
  it('should reject same target and base values', () => {
@@ -115,17 +97,6 @@ describe('CLI Utils', () => {
115
97
  it('should allow working without compareWith', () => {
116
98
  expect(validateDiffArguments('working')).toEqual({ valid: true });
117
99
  });
118
- it('should allow working with staged', () => {
119
- expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
120
- });
121
- it('should reject working with other commits', () => {
122
- const result1 = validateDiffArguments('working', 'main');
123
- expect(result1.valid).toBe(false);
124
- expect(result1.error).toBe('"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.');
125
- const result2 = validateDiffArguments('working', 'abc123');
126
- expect(result2.valid).toBe(false);
127
- expect(result2.error).toBe('"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.');
128
- });
129
100
  it('should allow other special args with compareWith', () => {
130
101
  expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
131
102
  expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
@@ -143,14 +114,6 @@ describe('CLI Utils', () => {
143
114
  valid: true,
144
115
  });
145
116
  });
146
- it('should handle SHA hashes with parent/ancestor references', () => {
147
- expect(validateDiffArguments('bd4b7513e075b5b245284c38fd23427b9bd0f42e^', 'abc123')).toEqual({ valid: true });
148
- expect(validateDiffArguments('abc123', 'def456^')).toEqual({ valid: true });
149
- expect(validateDiffArguments('abc123~1', 'def456~2')).toEqual({ valid: true });
150
- expect(validateDiffArguments('a1b2c3d4e5f6789012345678901234567890abcd^', 'HEAD')).toEqual({
151
- valid: true,
152
- });
153
- });
154
117
  });
155
118
  });
156
119
  describe('shortHash', () => {
@@ -164,51 +127,4 @@ describe('CLI Utils', () => {
164
127
  expect(shortHash('')).toBe('');
165
128
  });
166
129
  });
167
- describe('parseGitHubPrUrl', () => {
168
- it('should parse valid GitHub PR URLs', () => {
169
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/123');
170
- expect(result).toEqual({
171
- owner: 'owner',
172
- repo: 'repo',
173
- pullNumber: 123,
174
- });
175
- });
176
- it('should parse GitHub PR URLs with additional path segments', () => {
177
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/456/files');
178
- expect(result).toEqual({
179
- owner: 'owner',
180
- repo: 'repo',
181
- pullNumber: 456,
182
- });
183
- });
184
- it('should parse GitHub PR URLs with query parameters', () => {
185
- const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/789?tab=files');
186
- expect(result).toEqual({
187
- owner: 'owner',
188
- repo: 'repo',
189
- pullNumber: 789,
190
- });
191
- });
192
- it('should handle URLs with hyphens and underscores in owner/repo names', () => {
193
- const result = parseGitHubPrUrl('https://github.com/owner-name/repo_name/pull/123');
194
- expect(result).toEqual({
195
- owner: 'owner-name',
196
- repo: 'repo_name',
197
- pullNumber: 123,
198
- });
199
- });
200
- it('should return null for invalid URLs', () => {
201
- expect(parseGitHubPrUrl('not-a-url')).toBe(null);
202
- expect(parseGitHubPrUrl('https://example.com/owner/repo/pull/123')).toBe(null);
203
- expect(parseGitHubPrUrl('https://github.com/owner/repo/issues/123')).toBe(null);
204
- expect(parseGitHubPrUrl('https://github.com/owner/repo')).toBe(null);
205
- expect(parseGitHubPrUrl('https://github.com/owner/repo/pull/abc')).toBe(null);
206
- });
207
- it('should handle malformed URLs gracefully', () => {
208
- expect(parseGitHubPrUrl('')).toBe(null);
209
- expect(parseGitHubPrUrl('https://github.com')).toBe(null);
210
- expect(parseGitHubPrUrl('https://github.com/owner')).toBe(null);
211
- expect(parseGitHubPrUrl('https://github.com/owner/repo/pull')).toBe(null);
212
- });
213
- });
214
130
  });