diffwatch 1.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.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # diffwatch
2
+
3
+ A CLI-based Node.js application that provides a real-time Terminal User Interface (TUI) for monitoring Git repository changes. Watch file statuses and view diffs in a split-pane interface without leaving your terminal.
4
+
5
+ ![Screenshot](screenshot.jpg)
6
+
7
+ ## Features
8
+
9
+ - **Real-time Monitoring**: Automatically refreshes every 5 seconds to show the latest Git status
10
+ - **Split-pane TUI**: Left pane displays files with color-coded status, right pane shows unified diffs
11
+ - **Color-coded Status**: Green for added, Red for deleted, Blue for modified, White for untracked
12
+ - **Keyboard Navigation**: Use arrow keys to navigate files, Tab to switch panes
13
+ - **Git-only Operation**: Works exclusively in Git repositories
14
+ - **Cross-platform**: Built with Node.js and TypeScript
15
+
16
+ ## Installation
17
+
18
+ ### Global Installation (Recommended)
19
+
20
+ ```bash
21
+ npm install -g diffwatch
22
+ ```
23
+
24
+ ### Local Installation
25
+
26
+ ```bash
27
+ git clone <repository-url>
28
+ cd diffwatch
29
+ npm install
30
+ npm run build
31
+ npm link
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Navigate to any Git repository and run:
37
+
38
+ ```bash
39
+ diffwatch
40
+ ```
41
+
42
+ The application will:
43
+ 1. Check if the current directory is a Git repository
44
+ 2. Display a TUI with two panes
45
+ 3. Automatically refresh every 5 seconds
46
+
47
+ ### Controls
48
+
49
+ - **Arrow Keys / Tab**: Navigate through the file list and scroll in diff pane
50
+ - **Enter**: Open selected file in default editor
51
+ - **Escape/Q/Ctrl+C**: Exit the application
52
+
53
+ ## Prerequisites
54
+
55
+ - Node.js 16 or higher
56
+ - Git installed and accessible in PATH
57
+ - A terminal that supports TUI applications (most modern terminals)
58
+
59
+ ## Development
60
+
61
+ ### Setup
62
+
63
+ ```bash
64
+ npm install
65
+ npm run build
66
+ npm run dev # Run in development mode
67
+ npm test # Run tests
68
+ ```
69
+
70
+ ### Project Structure
71
+
72
+ ```
73
+ src/
74
+ ├── index.ts # Main application entry point
75
+ └── utils/
76
+ └── git.ts # Git operations and status handling
77
+ tests/
78
+ ├── git.test.ts # Unit tests for GitHandler
79
+ ```
80
+
81
+ ### Building
82
+
83
+ ```bash
84
+ npm run build # Compile TypeScript to JavaScript
85
+ ```
86
+
87
+ ### Testing
88
+
89
+ ```bash
90
+ npm test # Run Jest test suite
91
+ ```
92
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ const blessed = require('neo-neo-blessed');
17
+ const chalk_1 = __importDefault(require("chalk"));
18
+ const child_process_1 = require("child_process");
19
+ const git_1 = require("./utils/git");
20
+ const diff_formatter_1 = require("./utils/diff-formatter");
21
+ function main() {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ const gitHandler = new git_1.GitHandler();
24
+ if (!(yield gitHandler.isRepo())) {
25
+ console.log(chalk_1.default.red('Error: Current directory is not a git repository.'));
26
+ process.exit(1);
27
+ }
28
+ const screen = blessed.screen({
29
+ smartCSR: true,
30
+ title: 'diffwatch',
31
+ });
32
+ const fileList = blessed.list({
33
+ top: 0,
34
+ left: 0,
35
+ width: '30%',
36
+ height: '100%',
37
+ label: ' Files (0) ',
38
+ keys: true,
39
+ vi: true,
40
+ mouse: true,
41
+ tags: true,
42
+ scrollbar: {
43
+ ch: ' ',
44
+ track: { bg: 'white' },
45
+ style: { bg: 'blue' },
46
+ },
47
+ style: {
48
+ selected: { fg: 'black', bg: 'white' },
49
+ border: { fg: 'white' },
50
+ },
51
+ border: { type: 'line' },
52
+ });
53
+ const diffView = blessed.scrollabletext({
54
+ top: 0,
55
+ left: '30%',
56
+ width: '70%',
57
+ height: '100%',
58
+ label: ' Diff () ',
59
+ keys: true,
60
+ vi: true,
61
+ mouse: true,
62
+ scrollbar: {
63
+ ch: ' ',
64
+ track: { bg: 'white' },
65
+ style: { bg: 'blue' },
66
+ },
67
+ style: {
68
+ border: { fg: 'white' },
69
+ },
70
+ border: { type: 'line' },
71
+ tags: false,
72
+ });
73
+ screen.append(fileList);
74
+ screen.append(diffView);
75
+ const updateBorders = () => {
76
+ fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
77
+ diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
78
+ screen.render();
79
+ };
80
+ let currentFiles = [];
81
+ let lastSelectedPath = null;
82
+ let diffUpdateTimeout = null;
83
+ const scheduleDiffUpdate = () => {
84
+ if (diffUpdateTimeout)
85
+ clearTimeout(diffUpdateTimeout);
86
+ diffUpdateTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
87
+ yield updateDiff();
88
+ }), 150); // 150ms debounce
89
+ };
90
+ const openInEditor = (filePath) => {
91
+ try {
92
+ if (process.platform === 'win32') {
93
+ // On Windows, use 'start' to open with default program
94
+ (0, child_process_1.spawn)('cmd', ['/c', 'start', '', filePath], { stdio: 'ignore', detached: true }).unref();
95
+ }
96
+ else {
97
+ // On Unix-like systems, try EDITOR, fallback to xdg-open
98
+ const editor = process.env.EDITOR || process.env.VISUAL || 'xdg-open';
99
+ (0, child_process_1.spawn)(editor, [filePath], { stdio: 'ignore', detached: true }).unref();
100
+ }
101
+ }
102
+ catch (error) {
103
+ console.error(`Failed to open ${filePath}: ${error}`);
104
+ }
105
+ };
106
+ const updateDiff = () => __awaiter(this, void 0, void 0, function* () {
107
+ const selectedIndex = fileList.selected;
108
+ const selectedFile = currentFiles[selectedIndex];
109
+ if (selectedFile) {
110
+ const diff = yield gitHandler.getDiff(selectedFile.path);
111
+ const formattedDiff = (0, diff_formatter_1.formatDiffWithDiff2Html)(diff);
112
+ const newLabel = ` Diff (${selectedFile.path}) `;
113
+ const currentContent = diffView.content;
114
+ const currentLabel = diffView.label;
115
+ // Only update if content or label changed to reduce flickering
116
+ if (formattedDiff !== currentContent || newLabel !== currentLabel) {
117
+ const savedScroll = diffView.scrollTop;
118
+ const isNewFile = selectedFile.path !== lastSelectedPath;
119
+ diffView.setContent(formattedDiff);
120
+ diffView.setLabel(newLabel);
121
+ if (isNewFile) {
122
+ diffView.scrollTo(0);
123
+ }
124
+ else {
125
+ diffView.scrollTop = savedScroll;
126
+ }
127
+ }
128
+ lastSelectedPath = selectedFile.path;
129
+ }
130
+ else {
131
+ const newContent = 'Select a file to view diff.';
132
+ const newLabel = ' Diff () ';
133
+ if (diffView.content !== newContent || diffView.label !== newLabel) {
134
+ diffView.setContent(newContent);
135
+ diffView.setLabel(newLabel);
136
+ diffView.scrollTo(0);
137
+ }
138
+ lastSelectedPath = null;
139
+ }
140
+ screen.render();
141
+ });
142
+ const updateFileList = () => __awaiter(this, void 0, void 0, function* () {
143
+ var _a;
144
+ // Preserve selected file path and scroll positions
145
+ const selectedPath = (_a = currentFiles[fileList.selected]) === null || _a === void 0 ? void 0 : _a.path;
146
+ const fileListScroll = fileList.scroll;
147
+ const diffScroll = diffView.scrollTop;
148
+ const files = yield gitHandler.getStatus();
149
+ currentFiles = files;
150
+ const items = files.map(f => {
151
+ let color = '{white-fg}';
152
+ if (f.status === 'added')
153
+ color = '{green-fg}';
154
+ else if (f.status === 'deleted')
155
+ color = '{red-fg}';
156
+ else if (f.status === 'modified')
157
+ color = '{blue-fg}';
158
+ else if (f.status === 'unstaged')
159
+ color = '{white-fg}';
160
+ return `${color}${f.path}{/}`;
161
+ });
162
+ fileList.setItems(items);
163
+ fileList.setLabel(` Files (${files.length}) `);
164
+ if (items.length > 0) {
165
+ // Restore selection by path if possible
166
+ const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
167
+ fileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
168
+ // Cancel any pending diff update and update immediately
169
+ if (diffUpdateTimeout) {
170
+ clearTimeout(diffUpdateTimeout);
171
+ diffUpdateTimeout = null;
172
+ }
173
+ yield updateDiff();
174
+ }
175
+ else {
176
+ diffView.setContent('No changes detected.');
177
+ diffView.setLabel(' Diff () ');
178
+ }
179
+ // Restore scroll positions
180
+ fileList.scroll = fileListScroll;
181
+ diffView.scrollTop = diffScroll;
182
+ screen.render();
183
+ });
184
+ fileList.on('select item', () => {
185
+ scheduleDiffUpdate();
186
+ });
187
+ fileList.key(['up', 'down'], () => {
188
+ scheduleDiffUpdate();
189
+ });
190
+ screen.key(['escape', 'q', 'C-c'], () => {
191
+ screen.destroy();
192
+ process.exit(0);
193
+ });
194
+ screen.key(['tab'], () => {
195
+ if (screen.focused === fileList) {
196
+ diffView.focus();
197
+ }
198
+ else {
199
+ fileList.focus();
200
+ }
201
+ updateBorders();
202
+ });
203
+ screen.key(['left'], () => {
204
+ fileList.focus();
205
+ updateBorders();
206
+ });
207
+ screen.key(['right'], () => {
208
+ diffView.focus();
209
+ updateBorders();
210
+ });
211
+ screen.key(['enter'], () => {
212
+ const selectedIndex = fileList.selected;
213
+ const selectedFile = currentFiles[selectedIndex];
214
+ if (selectedFile) {
215
+ openInEditor(selectedFile.path);
216
+ }
217
+ });
218
+ setInterval(() => __awaiter(this, void 0, void 0, function* () {
219
+ yield updateFileList();
220
+ }), 5000);
221
+ yield updateFileList();
222
+ fileList.focus();
223
+ updateBorders();
224
+ });
225
+ }
226
+ main().catch(err => {
227
+ console.error(err);
228
+ process.exit(1);
229
+ });
230
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function formatDiffWithDiff2Html(diffString: string): string;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.formatDiffWithDiff2Html = formatDiffWithDiff2Html;
37
+ const cheerio = __importStar(require("cheerio"));
38
+ const Diff2Html = require('diff2html');
39
+ function formatDiffWithDiff2Html(diffString) {
40
+ if (!diffString || diffString.trim() === '') {
41
+ return 'No changes detected.';
42
+ }
43
+ try {
44
+ const html = Diff2Html.html(diffString, {
45
+ drawFileList: false,
46
+ matching: 'lines',
47
+ outputFormat: 'line-by-line',
48
+ colorScheme: 'dark',
49
+ });
50
+ const $ = cheerio.load(html);
51
+ let blessedText = '';
52
+ $('.d2h-diff-tbody tr').each((_, row) => {
53
+ const $row = $(row);
54
+ if ($row.find('.d2h-code-line').length > 0) {
55
+ const $codeCell = $row.find('td.d2h-code-linenumber');
56
+ const isAdded = $row.find('td.d2h-ins').length > 0;
57
+ const isDeleted = $row.find('td.d2h-del').length > 0;
58
+ const $lineContent = $row.find('.d2h-code-line-ctn');
59
+ const $linePrefix = $row.find('.d2h-code-line-prefix');
60
+ const $lineWrapper = $row.find('.d2h-code-line');
61
+ let prefix = '';
62
+ let content = '';
63
+ if ($linePrefix.length > 0) {
64
+ prefix = $linePrefix.text();
65
+ }
66
+ if ($lineContent.length > 0) {
67
+ content = $lineContent.text();
68
+ }
69
+ else {
70
+ content = $lineWrapper.text().trim();
71
+ }
72
+ const fullLine = prefix + content;
73
+ if (isAdded) {
74
+ blessedText += `\x1b[32m${fullLine}\x1b[0m\n`;
75
+ }
76
+ else if (isDeleted) {
77
+ blessedText += `\x1b[31m${fullLine}\x1b[0m\n`;
78
+ }
79
+ else {
80
+ blessedText += `\x1b[37m${fullLine}\x1b[0m\n`;
81
+ }
82
+ }
83
+ if ($row.find('.d2h-info').length > 0) {
84
+ const hunkText = $row.find('.d2h-info').text().trim();
85
+ blessedText += `\x1b[36m${hunkText}\x1b[0m\n`;
86
+ }
87
+ });
88
+ $('.d2h-file-header').each((_, header) => {
89
+ const fileText = $(header).text().trim();
90
+ blessedText += `\x1b[33m${fileText}\x1b[0m\n`;
91
+ });
92
+ return blessedText.trim() || 'No changes detected.';
93
+ }
94
+ catch (error) {
95
+ return `Error formatting diff: ${error}`;
96
+ }
97
+ }
98
+ //# sourceMappingURL=diff-formatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-formatter.js","sourceRoot":"","sources":["../../src/utils/diff-formatter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export interface FileStatus {
2
+ path: string;
3
+ status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown';
4
+ mtime?: Date;
5
+ }
6
+ export declare class GitHandler {
7
+ private git;
8
+ constructor(workingDir?: string);
9
+ isRepo(): Promise<boolean>;
10
+ getStatus(): Promise<FileStatus[]>;
11
+ getDiff(filePath: string): Promise<string>;
12
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;CACnE;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,GAAG,CAAY;gBAEX,UAAU,GAAE,MAAsB;IAIxC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAQ1B,SAAS,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IA8BlC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAiBjD"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.GitHandler = void 0;
16
+ const simple_git_1 = require("simple-git");
17
+ const promises_1 = __importDefault(require("fs/promises"));
18
+ class GitHandler {
19
+ constructor(workingDir = process.cwd()) {
20
+ this.git = (0, simple_git_1.simpleGit)(workingDir);
21
+ }
22
+ isRepo() {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ try {
25
+ return yield this.git.checkIsRepo();
26
+ }
27
+ catch (_a) {
28
+ return false;
29
+ }
30
+ });
31
+ }
32
+ getStatus() {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ const status = yield this.git.status();
35
+ const files = [];
36
+ status.modified.forEach(path => {
37
+ files.push({ path, status: 'modified' });
38
+ });
39
+ status.deleted.forEach(path => {
40
+ files.push({ path, status: 'deleted' });
41
+ });
42
+ status.created.forEach(path => {
43
+ files.push({ path, status: 'added' });
44
+ });
45
+ status.not_added.forEach(path => {
46
+ files.push({ path, status: 'unstaged' });
47
+ });
48
+ status.renamed.forEach(r => {
49
+ files.push({ path: r.to, status: 'added' });
50
+ });
51
+ const uniqueFiles = new Map();
52
+ files.forEach(f => uniqueFiles.set(f.path, f));
53
+ // Add last modified time for sorting
54
+ const fileArray = Array.from(uniqueFiles.values());
55
+ yield Promise.all(fileArray.map((f) => __awaiter(this, void 0, void 0, function* () {
56
+ try {
57
+ const stat = yield promises_1.default.stat(f.path);
58
+ f.mtime = stat.mtime;
59
+ }
60
+ catch (error) {
61
+ // For deleted or inaccessible files, use epoch time
62
+ f.mtime = new Date(0);
63
+ }
64
+ })));
65
+ // Sort by last modified descending, then by filename
66
+ return fileArray.sort((a, b) => {
67
+ const mtimeA = a.mtime || new Date(0);
68
+ const mtimeB = b.mtime || new Date(0);
69
+ const timeDiff = mtimeB.getTime() - mtimeA.getTime();
70
+ if (timeDiff !== 0) {
71
+ return timeDiff;
72
+ }
73
+ return a.path.localeCompare(b.path);
74
+ });
75
+ });
76
+ }
77
+ getDiff(filePath) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ try {
80
+ const isUntracked = (yield this.git.status()).not_added.includes(filePath);
81
+ if (isUntracked) {
82
+ return yield this.git.raw(['diff', '--no-index', '--', '/dev/null', filePath]).catch(() => {
83
+ return `New file: ${filePath}`;
84
+ });
85
+ }
86
+ const diff = yield this.git.diff(['HEAD', '--', filePath]);
87
+ return diff || 'No changes or file is new.';
88
+ }
89
+ catch (error) {
90
+ return `Error getting diff: ${error}`;
91
+ }
92
+ });
93
+ }
94
+ }
95
+ exports.GitHandler = GitHandler;
96
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,11 @@
1
+ const { createDefaultPreset } = require("ts-jest");
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ module.exports = {
7
+ testEnvironment: "node",
8
+ transform: {
9
+ ...tsJestTransformCfg,
10
+ },
11
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "diffwatch",
3
+ "version": "1.0.0",
4
+ "description": "An app for watching git repository file changes.",
5
+ "author": "Sarfraz Ahmed <sarfraznawaz2005@gmail.com>",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "diffwatch": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "ts-node src/index.ts",
14
+ "test": "jest"
15
+ },
16
+ "keywords": [
17
+ "diff",
18
+ "watch",
19
+ "file",
20
+ "comparison",
21
+ "utility"
22
+ ],
23
+ "license": "MIT",
24
+ "type": "commonjs",
25
+ "dependencies": {
26
+ "chalk": "^4.1.2",
27
+ "cheerio": "^1.1.2",
28
+ "diff2html": "^3.4.55",
29
+ "neo-neo-blessed": "^0.7.1",
30
+ "simple-git": "^3.30.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/jest": "^30.0.0",
34
+ "@types/node": "^25.0.8",
35
+ "jest": "^30.2.0",
36
+ "ts-jest": "^29.4.6",
37
+ "ts-node": "^10.9.2",
38
+ "typescript": "^5.9.3"
39
+ }
40
+ }
package/screenshot.jpg ADDED
Binary file
package/src/index.ts ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ const blessed = require('neo-neo-blessed');
3
+ import chalk from 'chalk';
4
+ import { spawn } from 'child_process';
5
+ import { GitHandler, FileStatus } from './utils/git';
6
+ import { formatDiffWithDiff2Html } from './utils/diff-formatter';
7
+
8
+ async function main() {
9
+ const gitHandler = new GitHandler();
10
+
11
+ if (!(await gitHandler.isRepo())) {
12
+ console.log(chalk.red('Error: Current directory is not a git repository.'));
13
+ process.exit(1);
14
+ }
15
+
16
+ const screen = blessed.screen({
17
+ smartCSR: true,
18
+ title: 'diffwatch',
19
+ });
20
+
21
+ const fileList = blessed.list({
22
+ top: 0,
23
+ left: 0,
24
+ width: '30%',
25
+ height: '100%',
26
+ label: ' Files (0) ',
27
+ keys: true,
28
+ vi: true,
29
+ mouse: true,
30
+ tags: true,
31
+ scrollbar: {
32
+ ch: ' ',
33
+ track: { bg: 'white' },
34
+ style: { bg: 'blue' },
35
+ },
36
+ style: {
37
+ selected: { fg: 'black', bg: 'white' },
38
+ border: { fg: 'white' },
39
+ },
40
+ border: { type: 'line' },
41
+ });
42
+
43
+ const diffView = blessed.scrollabletext({
44
+ top: 0,
45
+ left: '30%',
46
+ width: '70%',
47
+ height: '100%',
48
+ label: ' Diff () ',
49
+ keys: true,
50
+ vi: true,
51
+ mouse: true,
52
+ scrollbar: {
53
+ ch: ' ',
54
+ track: { bg: 'white' },
55
+ style: { bg: 'blue' },
56
+ },
57
+ style: {
58
+ border: { fg: 'white' },
59
+ },
60
+ border: { type: 'line' },
61
+ tags: false,
62
+ });
63
+
64
+ screen.append(fileList);
65
+ screen.append(diffView);
66
+
67
+ const updateBorders = () => {
68
+ fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
69
+ diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
70
+ screen.render();
71
+ };
72
+
73
+ let currentFiles: FileStatus[] = [];
74
+ let lastSelectedPath: string | null = null;
75
+ let diffUpdateTimeout: NodeJS.Timeout | null = null;
76
+
77
+ const scheduleDiffUpdate = () => {
78
+ if (diffUpdateTimeout) clearTimeout(diffUpdateTimeout);
79
+ diffUpdateTimeout = setTimeout(async () => {
80
+ await updateDiff();
81
+ }, 150); // 150ms debounce
82
+ };
83
+
84
+ const openInEditor = (filePath: string) => {
85
+ try {
86
+ if (process.platform === 'win32') {
87
+ // On Windows, use 'start' to open with default program
88
+ spawn('cmd', ['/c', 'start', '', filePath], { stdio: 'ignore', detached: true }).unref();
89
+ } else {
90
+ // On Unix-like systems, try EDITOR, fallback to xdg-open
91
+ const editor = process.env.EDITOR || process.env.VISUAL || 'xdg-open';
92
+ spawn(editor, [filePath], { stdio: 'ignore', detached: true }).unref();
93
+ }
94
+ } catch (error) {
95
+ console.error(`Failed to open ${filePath}: ${error}`);
96
+ }
97
+ };
98
+
99
+ const updateDiff = async () => {
100
+ const selectedIndex = fileList.selected;
101
+ const selectedFile = currentFiles[selectedIndex];
102
+ if (selectedFile) {
103
+ const diff = await gitHandler.getDiff(selectedFile.path);
104
+ const formattedDiff = formatDiffWithDiff2Html(diff);
105
+ const newLabel = ` Diff (${selectedFile.path}) `;
106
+ const currentContent = diffView.content;
107
+ const currentLabel = diffView.label;
108
+
109
+ // Only update if content or label changed to reduce flickering
110
+ if (formattedDiff !== currentContent || newLabel !== currentLabel) {
111
+ const savedScroll = diffView.scrollTop;
112
+ const isNewFile = selectedFile.path !== lastSelectedPath;
113
+
114
+ diffView.setContent(formattedDiff);
115
+ diffView.setLabel(newLabel);
116
+
117
+ if (isNewFile) {
118
+ diffView.scrollTo(0);
119
+ } else {
120
+ diffView.scrollTop = savedScroll;
121
+ }
122
+ }
123
+ lastSelectedPath = selectedFile.path;
124
+ } else {
125
+ const newContent = 'Select a file to view diff.';
126
+ const newLabel = ' Diff () ';
127
+ if (diffView.content !== newContent || diffView.label !== newLabel) {
128
+ diffView.setContent(newContent);
129
+ diffView.setLabel(newLabel);
130
+ diffView.scrollTo(0);
131
+ }
132
+ lastSelectedPath = null;
133
+ }
134
+ screen.render();
135
+ };
136
+
137
+ const updateFileList = async () => {
138
+ // Preserve selected file path and scroll positions
139
+ const selectedPath = currentFiles[fileList.selected]?.path;
140
+ const fileListScroll = fileList.scroll;
141
+ const diffScroll = diffView.scrollTop;
142
+
143
+ const files = await gitHandler.getStatus();
144
+ currentFiles = files;
145
+
146
+ const items = files.map(f => {
147
+ let color = '{white-fg}';
148
+ if (f.status === 'added') color = '{green-fg}';
149
+ else if (f.status === 'deleted') color = '{red-fg}';
150
+ else if (f.status === 'modified') color = '{blue-fg}';
151
+ else if (f.status === 'unstaged') color = '{white-fg}';
152
+
153
+ return `${color}${f.path}{/}`;
154
+ });
155
+
156
+ fileList.setItems(items);
157
+ fileList.setLabel(` Files (${files.length}) `);
158
+
159
+ if (items.length > 0) {
160
+ // Restore selection by path if possible
161
+ const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
162
+ fileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
163
+ // Cancel any pending diff update and update immediately
164
+ if (diffUpdateTimeout) {
165
+ clearTimeout(diffUpdateTimeout);
166
+ diffUpdateTimeout = null;
167
+ }
168
+ await updateDiff();
169
+ } else {
170
+ diffView.setContent('No changes detected.');
171
+ diffView.setLabel(' Diff () ');
172
+ }
173
+
174
+ // Restore scroll positions
175
+ fileList.scroll = fileListScroll;
176
+ diffView.scrollTop = diffScroll;
177
+
178
+ screen.render();
179
+ };
180
+
181
+ fileList.on('select item', () => {
182
+ scheduleDiffUpdate();
183
+ });
184
+
185
+ fileList.key(['up', 'down'], () => {
186
+ scheduleDiffUpdate();
187
+ });
188
+
189
+ screen.key(['escape', 'q', 'C-c'], () => {
190
+ screen.destroy();
191
+ process.exit(0);
192
+ });
193
+
194
+ screen.key(['tab'], () => {
195
+ if (screen.focused === fileList) {
196
+ diffView.focus();
197
+ } else {
198
+ fileList.focus();
199
+ }
200
+ updateBorders();
201
+ });
202
+
203
+ screen.key(['left'], () => {
204
+ fileList.focus();
205
+ updateBorders();
206
+ });
207
+
208
+ screen.key(['right'], () => {
209
+ diffView.focus();
210
+ updateBorders();
211
+ });
212
+
213
+ screen.key(['enter'], () => {
214
+ const selectedIndex = fileList.selected;
215
+ const selectedFile = currentFiles[selectedIndex];
216
+ if (selectedFile) {
217
+ openInEditor(selectedFile.path);
218
+ }
219
+ });
220
+
221
+ setInterval(async () => {
222
+ await updateFileList();
223
+ }, 5000);
224
+
225
+ await updateFileList();
226
+ fileList.focus();
227
+ updateBorders();
228
+ }
229
+
230
+ main().catch(err => {
231
+ console.error(err);
232
+ process.exit(1);
233
+ });
@@ -0,0 +1,70 @@
1
+ import * as cheerio from 'cheerio';
2
+ const Diff2Html = require('diff2html');
3
+
4
+ export function formatDiffWithDiff2Html(diffString: string): string {
5
+ if (!diffString || diffString.trim() === '') {
6
+ return 'No changes detected.';
7
+ }
8
+
9
+ try {
10
+ const html = Diff2Html.html(diffString, {
11
+ drawFileList: false,
12
+ matching: 'lines',
13
+ outputFormat: 'line-by-line',
14
+ colorScheme: 'dark',
15
+ });
16
+
17
+ const $ = cheerio.load(html);
18
+
19
+ let blessedText = '';
20
+
21
+ $('.d2h-diff-tbody tr').each((_, row) => {
22
+ const $row = $(row);
23
+
24
+ if ($row.find('.d2h-code-line').length > 0) {
25
+ const $codeCell = $row.find('td.d2h-code-linenumber');
26
+ const isAdded = $row.find('td.d2h-ins').length > 0;
27
+ const isDeleted = $row.find('td.d2h-del').length > 0;
28
+ const $lineContent = $row.find('.d2h-code-line-ctn');
29
+ const $linePrefix = $row.find('.d2h-code-line-prefix');
30
+ const $lineWrapper = $row.find('.d2h-code-line');
31
+
32
+ let prefix = '';
33
+ let content = '';
34
+
35
+ if ($linePrefix.length > 0) {
36
+ prefix = $linePrefix.text();
37
+ }
38
+ if ($lineContent.length > 0) {
39
+ content = $lineContent.text();
40
+ } else {
41
+ content = $lineWrapper.text().trim();
42
+ }
43
+
44
+ const fullLine = prefix + content;
45
+
46
+ if (isAdded) {
47
+ blessedText += `\x1b[32m${fullLine}\x1b[0m\n`;
48
+ } else if (isDeleted) {
49
+ blessedText += `\x1b[31m${fullLine}\x1b[0m\n`;
50
+ } else {
51
+ blessedText += `\x1b[37m${fullLine}\x1b[0m\n`;
52
+ }
53
+ }
54
+
55
+ if ($row.find('.d2h-info').length > 0) {
56
+ const hunkText = $row.find('.d2h-info').text().trim();
57
+ blessedText += `\x1b[36m${hunkText}\x1b[0m\n`;
58
+ }
59
+ });
60
+
61
+ $('.d2h-file-header').each((_, header) => {
62
+ const fileText = $(header).text().trim();
63
+ blessedText += `\x1b[33m${fileText}\x1b[0m\n`;
64
+ });
65
+
66
+ return blessedText.trim() || 'No changes detected.';
67
+ } catch (error) {
68
+ return `Error formatting diff: ${error}`;
69
+ }
70
+ }
@@ -0,0 +1,92 @@
1
+ import { simpleGit, SimpleGit, StatusResult } from 'simple-git';
2
+ import fs from 'fs/promises';
3
+
4
+ export interface FileStatus {
5
+ path: string;
6
+ status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown';
7
+ mtime?: Date;
8
+ }
9
+
10
+ export class GitHandler {
11
+ private git: SimpleGit;
12
+
13
+ constructor(workingDir: string = process.cwd()) {
14
+ this.git = simpleGit(workingDir);
15
+ }
16
+
17
+ async isRepo(): Promise<boolean> {
18
+ try {
19
+ return await this.git.checkIsRepo();
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ async getStatus(): Promise<FileStatus[]> {
26
+ const status: StatusResult = await this.git.status();
27
+ const files: FileStatus[] = [];
28
+
29
+ status.modified.forEach(path => {
30
+ files.push({ path, status: 'modified' });
31
+ });
32
+
33
+ status.deleted.forEach(path => {
34
+ files.push({ path, status: 'deleted' });
35
+ });
36
+
37
+ status.created.forEach(path => {
38
+ files.push({ path, status: 'added' });
39
+ });
40
+
41
+ status.not_added.forEach(path => {
42
+ files.push({ path, status: 'unstaged' });
43
+ });
44
+
45
+ status.renamed.forEach(r => {
46
+ files.push({ path: r.to, status: 'added' });
47
+ });
48
+
49
+ const uniqueFiles = new Map<string, FileStatus>();
50
+ files.forEach(f => uniqueFiles.set(f.path, f));
51
+
52
+ // Add last modified time for sorting
53
+ const fileArray = Array.from(uniqueFiles.values());
54
+ await Promise.all(fileArray.map(async (f) => {
55
+ try {
56
+ const stat = await fs.stat(f.path);
57
+ f.mtime = stat.mtime;
58
+ } catch (error) {
59
+ // For deleted or inaccessible files, use epoch time
60
+ f.mtime = new Date(0);
61
+ }
62
+ }));
63
+
64
+ // Sort by last modified descending, then by filename
65
+ return fileArray.sort((a, b) => {
66
+ const mtimeA = a.mtime || new Date(0);
67
+ const mtimeB = b.mtime || new Date(0);
68
+ const timeDiff = mtimeB.getTime() - mtimeA.getTime();
69
+ if (timeDiff !== 0) {
70
+ return timeDiff;
71
+ }
72
+ return a.path.localeCompare(b.path);
73
+ });
74
+ }
75
+
76
+ async getDiff(filePath: string): Promise<string> {
77
+ try {
78
+ const isUntracked = (await this.git.status()).not_added.includes(filePath);
79
+
80
+ if (isUntracked) {
81
+ return await this.git.raw(['diff', '--no-index', '--', '/dev/null', filePath]).catch(() => {
82
+ return `New file: ${filePath}`;
83
+ });
84
+ }
85
+
86
+ const diff = await this.git.diff(['HEAD', '--', filePath]);
87
+ return diff || 'No changes or file is new.';
88
+ } catch (error) {
89
+ return `Error getting diff: ${error}`;
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=git.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.test.js","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,0CAA8C;AAC9C,2CAAuC;AAEvC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAExB,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,IAAI,UAAsB,CAAC;IAC3B,IAAI,OAAY,CAAC;IAEjB,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,GAAG;YACR,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE;YACtB,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE;YACjB,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;YACf,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;SAChB,CAAC;QACD,sBAAuB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAClD,UAAU,GAAG,IAAI,gBAAU,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAS,EAAE;QACzD,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAS,EAAE;QAC9D,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAS,EAAE;QACnD,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YAC/B,QAAQ,EAAE,CAAC,UAAU,CAAC;YACtB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,SAAS,EAAE,CAAC,UAAU,CAAC;YACvB,OAAO,EAAE,EAAE;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAS,EAAE;QAC5C,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,8BAA8B,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IACpD,CAAC,CAAA,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,101 @@
1
+ jest.mock('simple-git', () => ({
2
+ simpleGit: jest.fn(),
3
+ }));
4
+ jest.mock('fs/promises', () => ({
5
+ stat: jest.fn(),
6
+ }));
7
+
8
+ import { simpleGit } from 'simple-git';
9
+ import fs from 'fs/promises';
10
+ import { GitHandler } from '../src/utils/git';
11
+
12
+ describe('GitHandler', () => {
13
+ let gitHandler: GitHandler;
14
+ let mockGit: any;
15
+
16
+ beforeEach(() => {
17
+ mockGit = {
18
+ checkIsRepo: jest.fn(),
19
+ status: jest.fn(),
20
+ diff: jest.fn(),
21
+ show: jest.fn(),
22
+ };
23
+ (simpleGit as jest.Mock).mockReturnValue(mockGit);
24
+ (fs.stat as jest.Mock).mockResolvedValue({ mtime: new Date() });
25
+ gitHandler = new GitHandler();
26
+ });
27
+
28
+ it('should return true if directory is a repo', async () => {
29
+ mockGit.checkIsRepo.mockResolvedValue(true);
30
+ const result = await gitHandler.isRepo();
31
+ expect(result).toBe(true);
32
+ });
33
+
34
+ it('should return false if directory is not a repo', async () => {
35
+ mockGit.checkIsRepo.mockRejectedValue(new Error('not a repo'));
36
+ const result = await gitHandler.isRepo();
37
+ expect(result).toBe(false);
38
+ });
39
+
40
+ it('should return file status correctly', async () => {
41
+ mockGit.status.mockResolvedValue({
42
+ modified: ['file1.ts'],
43
+ deleted: ['file2.ts'],
44
+ created: ['file3.ts'],
45
+ not_added: ['file4.ts'],
46
+ renamed: [],
47
+ });
48
+
49
+ const status = await gitHandler.getStatus();
50
+ expect(status).toEqual(expect.arrayContaining([
51
+ expect.objectContaining({ path: 'file1.ts', status: 'modified' }),
52
+ expect.objectContaining({ path: 'file2.ts', status: 'deleted' }),
53
+ expect.objectContaining({ path: 'file3.ts', status: 'added' }),
54
+ expect.objectContaining({ path: 'file4.ts', status: 'unstaged' }),
55
+ ]));
56
+ });
57
+
58
+ it('should return diff correctly', async () => {
59
+ mockGit.status.mockResolvedValue({ not_added: [] });
60
+ mockGit.diff.mockResolvedValue('+ added line\n- removed line');
61
+ const diff = await gitHandler.getDiff('file1.ts');
62
+ expect(diff).toBe('+ added line\n- removed line');
63
+ });
64
+
65
+ it('should sort files by last modified descending then filename', async () => {
66
+ // Mock fs.stat to return different mtimes
67
+ const mockStat = jest.fn();
68
+ (fs.stat as jest.Mock) = mockStat;
69
+ mockStat.mockImplementation((path: string) => {
70
+ const mtimes: Record<string, Date> = {
71
+ 'z-file.ts': new Date('2024-01-03'),
72
+ 'a-file.ts': new Date('2024-01-01'),
73
+ 'z-deleted.ts': new Date('2024-01-02'),
74
+ 'z-added.ts': new Date('2024-01-04'),
75
+ 'a-added.ts': new Date('2024-01-05'),
76
+ 'z-unstaged.ts': new Date('2024-01-06'),
77
+ 'a-unstaged.ts': new Date('2024-01-07'),
78
+ };
79
+ return Promise.resolve({ mtime: mtimes[path] || new Date(0) });
80
+ });
81
+
82
+ mockGit.status.mockResolvedValue({
83
+ modified: ['z-file.ts', 'a-file.ts'],
84
+ deleted: ['z-deleted.ts'],
85
+ created: ['z-added.ts', 'a-added.ts'],
86
+ not_added: ['z-unstaged.ts', 'a-unstaged.ts'],
87
+ renamed: [],
88
+ });
89
+
90
+ const status = await gitHandler.getStatus();
91
+ expect(status.map(f => f.path)).toEqual([
92
+ 'a-unstaged.ts', // newest
93
+ 'z-unstaged.ts',
94
+ 'a-added.ts',
95
+ 'z-added.ts',
96
+ 'z-file.ts',
97
+ 'z-deleted.ts',
98
+ 'a-file.ts', // oldest
99
+ ]);
100
+ });
101
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "commonjs",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "sourceMap": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "sample-tui", "tests"]
16
+ }