@abelfubu/dv 0.1.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/dist/ansi-html.d.ts +42 -0
- package/dist/ansi-html.d.ts.map +1 -0
- package/dist/ansi-html.js +327 -0
- package/dist/ansi-output.d.ts +22 -0
- package/dist/ansi-output.d.ts.map +1 -0
- package/dist/ansi-output.js +154 -0
- package/dist/balance-delimiters.d.ts +25 -0
- package/dist/balance-delimiters.d.ts.map +1 -0
- package/dist/balance-delimiters.js +539 -0
- package/dist/balance-delimiters.test.d.ts +2 -0
- package/dist/balance-delimiters.test.d.ts.map +1 -0
- package/dist/balance-delimiters.test.js +1029 -0
- package/dist/cli-copy-notification.test.d.ts +2 -0
- package/dist/cli-copy-notification.test.d.ts.map +1 -0
- package/dist/cli-copy-notification.test.js +80 -0
- package/dist/cli-scroll.test.d.ts +2 -0
- package/dist/cli-scroll.test.d.ts.map +1 -0
- package/dist/cli-scroll.test.js +283 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +976 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +128 -0
- package/dist/components/diff-view.d.ts +32 -0
- package/dist/components/diff-view.d.ts.map +1 -0
- package/dist/components/diff-view.js +123 -0
- package/dist/components/diff-view.test.d.ts +5 -0
- package/dist/components/diff-view.test.d.ts.map +1 -0
- package/dist/components/diff-view.test.js +312 -0
- package/dist/components/directory-tree-view.d.ts +33 -0
- package/dist/components/directory-tree-view.d.ts.map +1 -0
- package/dist/components/directory-tree-view.js +262 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +5 -0
- package/dist/components/toast.d.ts +21 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/toast.js +47 -0
- package/dist/diff-cursor-utils.d.ts +20 -0
- package/dist/diff-cursor-utils.d.ts.map +1 -0
- package/dist/diff-cursor-utils.js +105 -0
- package/dist/diff-cursor-utils.test.d.ts +2 -0
- package/dist/diff-cursor-utils.test.d.ts.map +1 -0
- package/dist/diff-cursor-utils.test.js +40 -0
- package/dist/diff-surface-copy.d.ts +23 -0
- package/dist/diff-surface-copy.d.ts.map +1 -0
- package/dist/diff-surface-copy.js +64 -0
- package/dist/diff-surface-copy.test.d.ts +5 -0
- package/dist/diff-surface-copy.test.d.ts.map +1 -0
- package/dist/diff-surface-copy.test.js +142 -0
- package/dist/diff-utils.d.ts +196 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +682 -0
- package/dist/diff-utils.test.d.ts +2 -0
- package/dist/diff-utils.test.d.ts.map +1 -0
- package/dist/diff-utils.test.js +727 -0
- package/dist/directory-tree.d.ts +72 -0
- package/dist/directory-tree.d.ts.map +1 -0
- package/dist/directory-tree.js +161 -0
- package/dist/directory-tree.test.d.ts +2 -0
- package/dist/directory-tree.test.d.ts.map +1 -0
- package/dist/directory-tree.test.js +383 -0
- package/dist/dropdown.d.ts +26 -0
- package/dist/dropdown.d.ts.map +1 -0
- package/dist/dropdown.js +172 -0
- package/dist/dropdown.test.d.ts +2 -0
- package/dist/dropdown.test.d.ts.map +1 -0
- package/dist/dropdown.test.js +106 -0
- package/dist/filter-submodule.e2e.test.d.ts +2 -0
- package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
- package/dist/filter-submodule.e2e.test.js +109 -0
- package/dist/hooks/use-copy-selection.d.ts +29 -0
- package/dist/hooks/use-copy-selection.d.ts.map +1 -0
- package/dist/hooks/use-copy-selection.js +46 -0
- package/dist/kv-codec.d.ts +16 -0
- package/dist/kv-codec.d.ts.map +1 -0
- package/dist/kv-codec.js +36 -0
- package/dist/license.d.ts +14 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +63 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +78 -0
- package/dist/monochrome.d.ts +34 -0
- package/dist/monochrome.d.ts.map +1 -0
- package/dist/monochrome.js +613 -0
- package/dist/monotone.d.ts +22 -0
- package/dist/monotone.d.ts.map +1 -0
- package/dist/monotone.js +185 -0
- package/dist/parsers-config.d.ts +19 -0
- package/dist/parsers-config.d.ts.map +1 -0
- package/dist/parsers-config.js +271 -0
- package/dist/patch-terminal-dimensions.d.ts +2 -0
- package/dist/patch-terminal-dimensions.d.ts.map +1 -0
- package/dist/patch-terminal-dimensions.js +45 -0
- package/dist/stdin-pager.test.d.ts +2 -0
- package/dist/stdin-pager.test.d.ts.map +1 -0
- package/dist/stdin-pager.test.js +497 -0
- package/dist/store.d.ts +16 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +48 -0
- package/dist/themes/github.json +247 -0
- package/dist/themes.d.ts +59 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +248 -0
- package/dist/tree-icons.d.ts +4 -0
- package/dist/tree-icons.d.ts.map +1 -0
- package/dist/tree-icons.js +18 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +13 -0
- package/dist/web-utils.d.ts +56 -0
- package/dist/web-utils.d.ts.map +1 -0
- package/dist/web-utils.js +363 -0
- package/package.json +37 -0
- package/public/jetbrains-mono-nerd.ttf +0 -0
- package/public/jetbrains-mono-nerd.woff2 +0 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// Integration tests for --stdin pager mode (lazygit integration).
|
|
2
|
+
// Reproduces https://github.com/remorses/critique/issues/25
|
|
3
|
+
//
|
|
4
|
+
// Uses tuistory to launch critique in a PTY (exactly like lazygit does),
|
|
5
|
+
// pipes a real diff via stdin, and verifies the output is plain scrollback
|
|
6
|
+
// text — not interactive TUI escape sequences.
|
|
7
|
+
//
|
|
8
|
+
// tuistory spawns a PTY where isTTY=true, which is exactly how lazygit
|
|
9
|
+
// runs its pager (via github.com/creack/pty). This makes the test
|
|
10
|
+
// realistic: it catches the original bug where --stdin + TTY incorrectly
|
|
11
|
+
// entered interactive TUI mode instead of scrollback mode.
|
|
12
|
+
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
|
|
13
|
+
import { launchTerminal } from "tuistory";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
const TEMP_DIR = path.join(import.meta.dir, ".test-stdin-pager-tmp");
|
|
17
|
+
function tempFile(name, content) {
|
|
18
|
+
const p = path.join(TEMP_DIR, name);
|
|
19
|
+
fs.writeFileSync(p, content);
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
22
|
+
function launchCritique(diffPath, opts) {
|
|
23
|
+
return launchTerminal({
|
|
24
|
+
command: "bash",
|
|
25
|
+
args: ["-c", `cat "${diffPath}" | bun run src/cli.tsx --stdin`],
|
|
26
|
+
cols: opts?.cols ?? 100,
|
|
27
|
+
rows: opts?.rows ?? 30,
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
env: {
|
|
30
|
+
PATH: process.env.PATH,
|
|
31
|
+
HOME: process.env.HOME,
|
|
32
|
+
TERM: "xterm-256color",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Auto-detect piped stdin (no --stdin flag).
|
|
37
|
+
// Spawns critique with a real pipe (not a PTY) so process.stdin.isTTY is undefined.
|
|
38
|
+
async function runCritiqueAutoStdin(diffContent) {
|
|
39
|
+
const proc = Bun.spawn({
|
|
40
|
+
cmd: ["bun", "run", "src/cli.tsx"],
|
|
41
|
+
cwd: process.cwd(),
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
TERM: "xterm-256color",
|
|
45
|
+
},
|
|
46
|
+
stdin: "pipe",
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
});
|
|
50
|
+
const writer = proc.stdin;
|
|
51
|
+
writer.write(diffContent);
|
|
52
|
+
writer.end();
|
|
53
|
+
const output = new TextDecoder().decode(await Bun.readableStreamToArrayBuffer(proc.stdout));
|
|
54
|
+
const stderr = new TextDecoder().decode(await Bun.readableStreamToArrayBuffer(proc.stderr));
|
|
55
|
+
const code = await proc.exited;
|
|
56
|
+
if (code !== 0) {
|
|
57
|
+
throw new Error(`critique exited with ${code}: ${stderr}`);
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
// -- Sample diffs --
|
|
62
|
+
const SINGLE_FILE_DIFF = [
|
|
63
|
+
"diff --git a/src/hello.ts b/src/hello.ts",
|
|
64
|
+
"--- a/src/hello.ts",
|
|
65
|
+
"+++ b/src/hello.ts",
|
|
66
|
+
"@@ -1,3 +1,3 @@",
|
|
67
|
+
" const greeting = 'hello'",
|
|
68
|
+
"-console.log(greeting)",
|
|
69
|
+
"+console.log(greeting + ' world')",
|
|
70
|
+
" export default greeting",
|
|
71
|
+
].join("\n");
|
|
72
|
+
// Empty patch — lazygit sends this when there are no changes for a file
|
|
73
|
+
const EMPTY_DIFF = "";
|
|
74
|
+
// Diff with only context lines and no actual changes (can happen with -U999)
|
|
75
|
+
const CONTEXT_ONLY_DIFF = [
|
|
76
|
+
"diff --git a/readme.md b/readme.md",
|
|
77
|
+
"--- a/readme.md",
|
|
78
|
+
"+++ b/readme.md",
|
|
79
|
+
"@@ -1,3 +1,3 @@",
|
|
80
|
+
" # My Project",
|
|
81
|
+
" ",
|
|
82
|
+
" Some description",
|
|
83
|
+
].join("\n");
|
|
84
|
+
// Multiple files in a single diff
|
|
85
|
+
const MULTI_FILE_DIFF = [
|
|
86
|
+
"diff --git a/src/index.ts b/src/index.ts",
|
|
87
|
+
"--- a/src/index.ts",
|
|
88
|
+
"+++ b/src/index.ts",
|
|
89
|
+
"@@ -1,4 +1,6 @@",
|
|
90
|
+
" import { App } from './app'",
|
|
91
|
+
"+import { Logger } from './logger'",
|
|
92
|
+
" ",
|
|
93
|
+
" const app = new App()",
|
|
94
|
+
"+const logger = new Logger()",
|
|
95
|
+
" app.start()",
|
|
96
|
+
"diff --git a/src/logger.ts b/src/logger.ts",
|
|
97
|
+
"new file mode 100644",
|
|
98
|
+
"--- /dev/null",
|
|
99
|
+
"+++ b/src/logger.ts",
|
|
100
|
+
"@@ -0,0 +1,5 @@",
|
|
101
|
+
"+export class Logger {",
|
|
102
|
+
"+ log(msg: string) {",
|
|
103
|
+
"+ console.log(`[LOG] ${msg}`)",
|
|
104
|
+
"+ }",
|
|
105
|
+
"+}",
|
|
106
|
+
].join("\n");
|
|
107
|
+
// Deletion-only diff
|
|
108
|
+
const DELETE_ONLY_DIFF = [
|
|
109
|
+
"diff --git a/src/deprecated.ts b/src/deprecated.ts",
|
|
110
|
+
"deleted file mode 100644",
|
|
111
|
+
"--- a/src/deprecated.ts",
|
|
112
|
+
"+++ /dev/null",
|
|
113
|
+
"@@ -1,4 +0,0 @@",
|
|
114
|
+
"-// This module is no longer needed",
|
|
115
|
+
"-export function oldHelper() {",
|
|
116
|
+
"- return 'deprecated'",
|
|
117
|
+
"-}",
|
|
118
|
+
].join("\n");
|
|
119
|
+
// Addition-only diff (new file)
|
|
120
|
+
const NEW_FILE_DIFF = [
|
|
121
|
+
"diff --git a/src/utils.ts b/src/utils.ts",
|
|
122
|
+
"new file mode 100644",
|
|
123
|
+
"--- /dev/null",
|
|
124
|
+
"+++ b/src/utils.ts",
|
|
125
|
+
"@@ -0,0 +1,7 @@",
|
|
126
|
+
"+export function clamp(value: number, min: number, max: number): number {",
|
|
127
|
+
"+ return Math.min(Math.max(value, min), max)",
|
|
128
|
+
"+}",
|
|
129
|
+
"+",
|
|
130
|
+
"+export function identity<T>(x: T): T {",
|
|
131
|
+
"+ return x",
|
|
132
|
+
"+}",
|
|
133
|
+
].join("\n");
|
|
134
|
+
// Large hunk with many changes
|
|
135
|
+
const LARGE_HUNK_DIFF = [
|
|
136
|
+
"diff --git a/config.json b/config.json",
|
|
137
|
+
"--- a/config.json",
|
|
138
|
+
"+++ b/config.json",
|
|
139
|
+
"@@ -1,9 +1,11 @@",
|
|
140
|
+
" {",
|
|
141
|
+
'- "name": "my-app",',
|
|
142
|
+
'+ "name": "my-awesome-app",',
|
|
143
|
+
'- "version": "1.0.0",',
|
|
144
|
+
'+ "version": "2.0.0",',
|
|
145
|
+
' "description": "A sample app",',
|
|
146
|
+
'- "main": "index.js",',
|
|
147
|
+
'+ "main": "dist/index.js",',
|
|
148
|
+
'+ "types": "dist/index.d.ts",',
|
|
149
|
+
' "scripts": {',
|
|
150
|
+
'- "build": "tsc"',
|
|
151
|
+
'+ "build": "tsc --project tsconfig.build.json",',
|
|
152
|
+
'+ "test": "bun test"',
|
|
153
|
+
" }",
|
|
154
|
+
" }",
|
|
155
|
+
].join("\n");
|
|
156
|
+
// Rename diff (common in lazygit)
|
|
157
|
+
const RENAME_DIFF = [
|
|
158
|
+
"diff --git a/src/old-name.ts b/src/new-name.ts",
|
|
159
|
+
"similarity index 90%",
|
|
160
|
+
"rename from src/old-name.ts",
|
|
161
|
+
"rename to src/new-name.ts",
|
|
162
|
+
"--- a/src/old-name.ts",
|
|
163
|
+
"+++ b/src/new-name.ts",
|
|
164
|
+
"@@ -1,3 +1,3 @@",
|
|
165
|
+
"-export const name = 'old'",
|
|
166
|
+
"+export const name = 'new'",
|
|
167
|
+
" export const version = 1",
|
|
168
|
+
" export default name",
|
|
169
|
+
].join("\n");
|
|
170
|
+
// Colored diff (lazygit uses --color=always by default, issue #28)
|
|
171
|
+
const COLORED_DIFF = [
|
|
172
|
+
"\x1B[1mdiff --git a/src/hello.ts b/src/hello.ts\x1B[m",
|
|
173
|
+
"\x1B[1m--- a/src/hello.ts\x1B[m",
|
|
174
|
+
"\x1B[1m+++ b/src/hello.ts\x1B[m",
|
|
175
|
+
"\x1B[36m@@ -1,3 +1,3 @@\x1B[m",
|
|
176
|
+
" const greeting = 'hello'",
|
|
177
|
+
"\x1B[31m-console.log(greeting)\x1B[m",
|
|
178
|
+
"\x1B[32m+console.log(greeting + ' world')\x1B[m",
|
|
179
|
+
" export default greeting",
|
|
180
|
+
].join("\n");
|
|
181
|
+
// Binary file diff (lazygit shows these)
|
|
182
|
+
const BINARY_DIFF = [
|
|
183
|
+
"diff --git a/logo.png b/logo.png",
|
|
184
|
+
"new file mode 100644",
|
|
185
|
+
"Binary files /dev/null and b/logo.png differ",
|
|
186
|
+
].join("\n");
|
|
187
|
+
describe("--stdin pager mode (lazygit issue #25)", () => {
|
|
188
|
+
beforeAll(() => {
|
|
189
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
190
|
+
});
|
|
191
|
+
afterAll(() => {
|
|
192
|
+
try {
|
|
193
|
+
fs.rmSync(TEMP_DIR, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
});
|
|
197
|
+
test("single file change", async () => {
|
|
198
|
+
const diffPath = tempFile("single.diff", SINGLE_FILE_DIFF);
|
|
199
|
+
const session = await launchCritique(diffPath);
|
|
200
|
+
await session.waitForText("hello", { timeout: 15000 });
|
|
201
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
202
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
203
|
+
"
|
|
204
|
+
src
|
|
205
|
+
hello.ts (+1,-1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
src/hello.ts +1-1
|
|
209
|
+
|
|
210
|
+
1 const greeting = 'hello'
|
|
211
|
+
2 - console.log(greeting)
|
|
212
|
+
2 + console.log(greeting + ' world')
|
|
213
|
+
3 export default greeting"
|
|
214
|
+
`);
|
|
215
|
+
const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
|
|
216
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
217
|
+
expect(lines.length).toBeLessThan(25);
|
|
218
|
+
session.close();
|
|
219
|
+
}, 30000);
|
|
220
|
+
test("empty diff produces no crash", async () => {
|
|
221
|
+
const diffPath = tempFile("empty.diff", EMPTY_DIFF);
|
|
222
|
+
const session = await launchCritique(diffPath);
|
|
223
|
+
// Empty diff should cause critique to exit quickly.
|
|
224
|
+
// Wait a bit for it to process and exit.
|
|
225
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
226
|
+
const trimmed = await session.text({ trimEnd: true, immediate: true });
|
|
227
|
+
// Should either be empty or show an error message — not a TUI
|
|
228
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
229
|
+
"
|
|
230
|
+
No changes to display"
|
|
231
|
+
`);
|
|
232
|
+
session.close();
|
|
233
|
+
}, 15000);
|
|
234
|
+
test("context-only diff (no actual changes)", async () => {
|
|
235
|
+
const diffPath = tempFile("context-only.diff", CONTEXT_ONLY_DIFF);
|
|
236
|
+
const session = await launchCritique(diffPath);
|
|
237
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
238
|
+
const trimmed = await session.text({ trimEnd: true, immediate: true });
|
|
239
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
240
|
+
"
|
|
241
|
+
readme.md ()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
readme.md +0-0
|
|
245
|
+
|
|
246
|
+
1 # My Project
|
|
247
|
+
2
|
|
248
|
+
3 Some description"
|
|
249
|
+
`);
|
|
250
|
+
session.close();
|
|
251
|
+
}, 15000);
|
|
252
|
+
test("multiple files in one diff", async () => {
|
|
253
|
+
const diffPath = tempFile("multi.diff", MULTI_FILE_DIFF);
|
|
254
|
+
const session = await launchCritique(diffPath);
|
|
255
|
+
await session.waitForText("Logger", { timeout: 15000 });
|
|
256
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
257
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
258
|
+
"
|
|
259
|
+
src
|
|
260
|
+
index.ts (+2)
|
|
261
|
+
logger.ts (+5)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
src/index.ts +2-0
|
|
265
|
+
|
|
266
|
+
1 import { App } from './app'
|
|
267
|
+
2 + import { Logger } from './logger'
|
|
268
|
+
3
|
|
269
|
+
4 const app = new App()
|
|
270
|
+
5 + const logger = new Logger()
|
|
271
|
+
6 app.start()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
src/logger.ts +5-0
|
|
275
|
+
|
|
276
|
+
1 + export class Logger {
|
|
277
|
+
2 + log(msg: string) {
|
|
278
|
+
3 + console.log(\`[LOG] \${msg}\`)
|
|
279
|
+
4 + }
|
|
280
|
+
5 + }"
|
|
281
|
+
`);
|
|
282
|
+
// Should contain both filenames
|
|
283
|
+
expect(trimmed).toContain("index.ts");
|
|
284
|
+
expect(trimmed).toContain("logger.ts");
|
|
285
|
+
// Should not show the privacy notice
|
|
286
|
+
expect(trimmed).not.toContain("URL is private");
|
|
287
|
+
const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
|
|
288
|
+
expect(lines.length).toBeLessThan(40);
|
|
289
|
+
session.close();
|
|
290
|
+
}, 30000);
|
|
291
|
+
test("deleted file", async () => {
|
|
292
|
+
const diffPath = tempFile("delete.diff", DELETE_ONLY_DIFF);
|
|
293
|
+
const session = await launchCritique(diffPath);
|
|
294
|
+
await session.waitForText("deprecated", { timeout: 15000 });
|
|
295
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
296
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
297
|
+
"
|
|
298
|
+
src
|
|
299
|
+
deprecated.ts (-4)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
src/deprecated.ts +0-4
|
|
303
|
+
|
|
304
|
+
1 - // This module is no longer needed
|
|
305
|
+
2 - export function oldHelper() {
|
|
306
|
+
3 - return 'deprecated'
|
|
307
|
+
4 - }"
|
|
308
|
+
`);
|
|
309
|
+
expect(trimmed).toContain("deprecated");
|
|
310
|
+
expect(trimmed).not.toContain("URL is private");
|
|
311
|
+
session.close();
|
|
312
|
+
}, 30000);
|
|
313
|
+
test("new file", async () => {
|
|
314
|
+
const diffPath = tempFile("newfile.diff", NEW_FILE_DIFF);
|
|
315
|
+
const session = await launchCritique(diffPath);
|
|
316
|
+
await session.waitForText("clamp", { timeout: 15000 });
|
|
317
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
318
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
319
|
+
"
|
|
320
|
+
src
|
|
321
|
+
utils.ts (+7)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
src/utils.ts +7-0
|
|
325
|
+
|
|
326
|
+
1 + export function clamp(value: number, min: number, max: number): number {
|
|
327
|
+
2 + return Math.min(Math.max(value, min), max)
|
|
328
|
+
3 + }
|
|
329
|
+
4 +
|
|
330
|
+
5 + export function identity<T>(x: T): T {
|
|
331
|
+
6 + return x
|
|
332
|
+
7 + }"
|
|
333
|
+
`);
|
|
334
|
+
expect(trimmed).toContain("clamp");
|
|
335
|
+
expect(trimmed).toContain("identity");
|
|
336
|
+
expect(trimmed).not.toContain("URL is private");
|
|
337
|
+
session.close();
|
|
338
|
+
}, 30000);
|
|
339
|
+
test("large hunk with many changes", async () => {
|
|
340
|
+
const diffPath = tempFile("large.diff", LARGE_HUNK_DIFF);
|
|
341
|
+
const session = await launchCritique(diffPath);
|
|
342
|
+
await session.waitForText("config.json", { timeout: 15000 });
|
|
343
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
344
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
345
|
+
"
|
|
346
|
+
config.json (+6,-4)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
config.json +6-4
|
|
350
|
+
|
|
351
|
+
1 {
|
|
352
|
+
2 - "name": "my-app",
|
|
353
|
+
3 - "version": "1.0.0",
|
|
354
|
+
2 + "name": "my-awesome-app",
|
|
355
|
+
3 + "version": "2.0.0",
|
|
356
|
+
4 "description": "A sample app",
|
|
357
|
+
5 - "main": "index.js",
|
|
358
|
+
5 + "main": "dist/index.js",
|
|
359
|
+
6 + "types": "dist/index.d.ts",
|
|
360
|
+
7 "scripts": {
|
|
361
|
+
7 - "build": "tsc"
|
|
362
|
+
8 + "build": "tsc --project tsconfig.build.json",
|
|
363
|
+
9 + "test": "bun test"
|
|
364
|
+
10 }
|
|
365
|
+
11 }"
|
|
366
|
+
`);
|
|
367
|
+
expect(trimmed).toContain("config.json");
|
|
368
|
+
expect(trimmed).toContain("my-awesome-app");
|
|
369
|
+
expect(trimmed).not.toContain("URL is private");
|
|
370
|
+
session.close();
|
|
371
|
+
}, 30000);
|
|
372
|
+
test("file rename with changes", async () => {
|
|
373
|
+
const diffPath = tempFile("rename.diff", RENAME_DIFF);
|
|
374
|
+
const session = await launchCritique(diffPath);
|
|
375
|
+
await session.waitForText("new-name", { timeout: 15000 });
|
|
376
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
377
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
378
|
+
"
|
|
379
|
+
src
|
|
380
|
+
new-name.ts (+1,-1)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
src/old-name.ts → src/new-name.ts +1-1
|
|
384
|
+
|
|
385
|
+
1 - export const name = 'old'
|
|
386
|
+
1 + export const name = 'new'
|
|
387
|
+
2 export const version = 1
|
|
388
|
+
3 export default name"
|
|
389
|
+
`);
|
|
390
|
+
// Should show the rename
|
|
391
|
+
expect(trimmed).toContain("old-name");
|
|
392
|
+
expect(trimmed).toContain("new-name");
|
|
393
|
+
expect(trimmed).not.toContain("URL is private");
|
|
394
|
+
session.close();
|
|
395
|
+
}, 30000);
|
|
396
|
+
test("binary file diff", async () => {
|
|
397
|
+
const diffPath = tempFile("binary.diff", BINARY_DIFF);
|
|
398
|
+
const session = await launchCritique(diffPath);
|
|
399
|
+
// Binary diffs may not render content — wait for exit or timeout
|
|
400
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
401
|
+
const trimmed = await session.text({ trimEnd: true, immediate: true });
|
|
402
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
403
|
+
"
|
|
404
|
+
unknown ()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
unknown +0-0"
|
|
408
|
+
`);
|
|
409
|
+
expect(trimmed).not.toContain("URL is private");
|
|
410
|
+
session.close();
|
|
411
|
+
}, 15000);
|
|
412
|
+
test("colored diff from lazygit (issue #28)", async () => {
|
|
413
|
+
const diffPath = tempFile("colored.diff", COLORED_DIFF);
|
|
414
|
+
const session = await launchCritique(diffPath);
|
|
415
|
+
await session.waitForText("hello", { timeout: 15000 });
|
|
416
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
417
|
+
// Should strip ANSI codes and parse the diff correctly
|
|
418
|
+
// (not show "unknown +0-0")
|
|
419
|
+
expect(trimmed).toContain("hello.ts");
|
|
420
|
+
expect(trimmed).not.toContain("unknown");
|
|
421
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
422
|
+
"
|
|
423
|
+
src
|
|
424
|
+
hello.ts (+1,-1)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
src/hello.ts +1-1
|
|
428
|
+
|
|
429
|
+
1 const greeting = 'hello'
|
|
430
|
+
2 - console.log(greeting)
|
|
431
|
+
2 + console.log(greeting + ' world')
|
|
432
|
+
3 export default greeting"
|
|
433
|
+
`);
|
|
434
|
+
session.close();
|
|
435
|
+
}, 30000);
|
|
436
|
+
test("narrow terminal (40 cols) forces unified view", async () => {
|
|
437
|
+
const diffPath = tempFile("narrow.diff", SINGLE_FILE_DIFF);
|
|
438
|
+
const session = await launchCritique(diffPath, { cols: 40 });
|
|
439
|
+
await session.waitForText("hello", { timeout: 15000 });
|
|
440
|
+
const trimmed = await session.text({ trimEnd: true });
|
|
441
|
+
expect(trimmed).toMatchInlineSnapshot(`
|
|
442
|
+
"
|
|
443
|
+
src
|
|
444
|
+
hello.ts (+1,-1)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
src/hello.ts +1-1
|
|
448
|
+
|
|
449
|
+
1 const greeting = 'hello'
|
|
450
|
+
2 - console.log(greeting)
|
|
451
|
+
2 + console.log(greeting + ' world')
|
|
452
|
+
3 export default greeting"
|
|
453
|
+
`);
|
|
454
|
+
expect(trimmed).toContain("hello");
|
|
455
|
+
expect(trimmed).not.toContain("URL is private");
|
|
456
|
+
// Every non-empty line should fit within 40 cols
|
|
457
|
+
const lines = trimmed.split("\n").filter((l) => l.trim().length > 0);
|
|
458
|
+
for (const line of lines) {
|
|
459
|
+
expect(line.length).toBeLessThanOrEqual(40);
|
|
460
|
+
}
|
|
461
|
+
session.close();
|
|
462
|
+
}, 30000);
|
|
463
|
+
});
|
|
464
|
+
describe("auto-detect piped stdin (git diff | critique)", () => {
|
|
465
|
+
test("single file change without --stdin flag", async () => {
|
|
466
|
+
const output = await runCritiqueAutoStdin(SINGLE_FILE_DIFF);
|
|
467
|
+
const trimmed = output.replace(/\r/g, "").trim();
|
|
468
|
+
expect(trimmed).toContain("hello.ts");
|
|
469
|
+
expect(trimmed).toContain("console.log(greeting)");
|
|
470
|
+
expect(trimmed).toContain("console.log(greeting + ' world')");
|
|
471
|
+
expect(trimmed).not.toContain("unknown");
|
|
472
|
+
}, 30000);
|
|
473
|
+
test("empty diff falls back to git repo check (errors outside repo)", async () => {
|
|
474
|
+
// Empty piped stdin with no --stdin should fall back to git diff.
|
|
475
|
+
// Since we're running this inside the critique repo which may have no
|
|
476
|
+
// uncommitted changes, the output should be "No changes to display".
|
|
477
|
+
const output = await runCritiqueAutoStdin("");
|
|
478
|
+
const trimmed = output.replace(/\r/g, "").trim();
|
|
479
|
+
expect(trimmed).toContain("No changes to display");
|
|
480
|
+
}, 30000);
|
|
481
|
+
test("multiple files in one diff without --stdin", async () => {
|
|
482
|
+
const output = await runCritiqueAutoStdin(MULTI_FILE_DIFF);
|
|
483
|
+
const trimmed = output.replace(/\r/g, "").trim();
|
|
484
|
+
expect(trimmed).toContain("index.ts");
|
|
485
|
+
expect(trimmed).toContain("logger.ts");
|
|
486
|
+
expect(trimmed).toContain("Logger");
|
|
487
|
+
expect(trimmed).not.toContain("unknown");
|
|
488
|
+
}, 30000);
|
|
489
|
+
test("colored diff auto-detected and stripped", async () => {
|
|
490
|
+
const output = await runCritiqueAutoStdin(COLORED_DIFF);
|
|
491
|
+
const trimmed = output.replace(/\r/g, "").trim();
|
|
492
|
+
expect(trimmed).toContain("hello.ts");
|
|
493
|
+
expect(trimmed).not.toContain("unknown");
|
|
494
|
+
// ANSI codes should be stripped
|
|
495
|
+
expect(trimmed).not.toContain("\x1B[");
|
|
496
|
+
}, 30000);
|
|
497
|
+
});
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PersistedState {
|
|
2
|
+
themeName?: string;
|
|
3
|
+
italicsEnabled?: boolean;
|
|
4
|
+
transparentBackground?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadPersistedState(): PersistedState;
|
|
7
|
+
export declare function savePersistedState(state: PersistedState): void;
|
|
8
|
+
declare const persistedState: PersistedState;
|
|
9
|
+
export interface AppState {
|
|
10
|
+
themeName: string;
|
|
11
|
+
italicsEnabled: boolean;
|
|
12
|
+
transparentBackground: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const useAppStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AppState>>;
|
|
15
|
+
export { persistedState };
|
|
16
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC;AAED,wBAAgB,kBAAkB,IAAI,cAAc,CAOnD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAS9D;AAGD,QAAA,MAAM,cAAc,gBAAuB,CAAA;AAE3C,MAAM,WAAW,QAAQ;IAEvB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IACvB,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AAED,eAAO,MAAM,WAAW,uEAIrB,CAAA;AAYH,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Global Zustand store for persistent application state.
|
|
2
|
+
// Manages theme selection and italics toggle with automatic persistence to ~/.config/dv/state.json.
|
|
3
|
+
// Shared between main diff view and review view components.
|
|
4
|
+
import { create } from "zustand";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { defaultThemeName } from "./themes.js";
|
|
9
|
+
// State persistence
|
|
10
|
+
const STATE_DIR = join(homedir(), ".config", "dv");
|
|
11
|
+
const STATE_FILE = join(STATE_DIR, "state.json");
|
|
12
|
+
export function loadPersistedState() {
|
|
13
|
+
try {
|
|
14
|
+
const data = fs.readFileSync(STATE_FILE, "utf-8");
|
|
15
|
+
return JSON.parse(data);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function savePersistedState(state) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
24
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore write errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Load initial state
|
|
33
|
+
const persistedState = loadPersistedState();
|
|
34
|
+
export const useAppStore = create(() => ({
|
|
35
|
+
themeName: persistedState.themeName ?? defaultThemeName,
|
|
36
|
+
italicsEnabled: persistedState.italicsEnabled ?? true,
|
|
37
|
+
transparentBackground: persistedState.transparentBackground ?? false,
|
|
38
|
+
}));
|
|
39
|
+
// Subscribe to persist state changes
|
|
40
|
+
useAppStore.subscribe((state) => {
|
|
41
|
+
savePersistedState({
|
|
42
|
+
themeName: state.themeName,
|
|
43
|
+
italicsEnabled: state.italicsEnabled,
|
|
44
|
+
transparentBackground: state.transparentBackground,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
// Re-export persisted state for initial reads
|
|
48
|
+
export { persistedState };
|