reslop 0.0.1 → 0.1.1
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/CHANGELOG.md +20 -3
- package/LICENSE +1 -1
- package/README.md +67 -4
- package/bin/reslop.js +23 -0
- package/lib/ansi.js +231 -0
- package/lib/api.js +37 -0
- package/lib/cli.js +185 -0
- package/lib/clipboard.js +42 -0
- package/lib/detect.js +84 -0
- package/lib/diff.js +907 -0
- package/lib/editor.js +148 -0
- package/lib/files.js +47 -0
- package/lib/git.js +286 -0
- package/lib/github.js +631 -0
- package/lib/highlight.js +1231 -0
- package/lib/keys.js +211 -0
- package/lib/render.js +996 -0
- package/lib/review.js +714 -0
- package/lib/select.js +210 -0
- package/lib/session.js +1590 -0
- package/lib/source.js +26 -0
- package/lib/wrap.js +118 -0
- package/package.json +51 -21
- package/.editorconfig +0 -12
- package/.gitattributes +0 -1
- package/.prettierignore +0 -2
- package/AUTHORS +0 -1
- package/CONTRIBUTING.md +0 -41
- package/SECURITY.md +0 -16
- package/eslint.config.js +0 -5
- package/main.js +0 -3
- package/prettier.config.js +0 -10
- package/tsconfig.json +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.1.1][] - 2026-09-10
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
- Open a GitHub pull request by URL and review its diff
|
|
8
|
+
- Import GitHub pull request review comments into the local review
|
|
9
|
+
- Improve TUI navigation and hotkeys
|
|
10
|
+
- CLI simplification and optimisation
|
|
11
|
+
- Dix diff highlighting for snake_case identifier changes
|
|
12
|
+
- Updated package metadata and README
|
|
13
|
+
|
|
14
|
+
## [0.1.0][] - 2026-09-07
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Interactive terminal review of uncommitted git diffs with stage, unstage,
|
|
19
|
+
revert, and skip
|
|
20
|
+
- Feedback and todos that produce a repair plan for an agent
|
|
21
|
+
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
22
|
+
|
|
23
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.1...HEAD
|
|
24
|
+
[0.1.1]: https://github.com/tshemsedinov/reslop/compare/v0.1.0...v0.1.1
|
|
25
|
+
[0.1.0]: https://github.com/tshemsedinov/reslop/releases/tag/v0.1.0
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,70 @@
|
|
|
1
|
-
# reslop
|
|
1
|
+
# reslop — Review diff and plan changes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Turn generated changes into owned changes
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```text
|
|
6
|
+
Review → Plan → Repair → Verify
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
`0.1.x` implements Review: open a change, leave findings and todos, and
|
|
10
|
+
prepare a repair plan for the agent. Plan, repair, and verify come later.
|
|
11
|
+
|
|
12
|
+
- Prepare review findings and a repair plan for the agent.
|
|
13
|
+
- Leave feedback, add todos.
|
|
14
|
+
- Generate a plan the agent executes.
|
|
15
|
+
|
|
16
|
+
Walk uncommitted git changes, a given commit, or a GitHub pull request.
|
|
17
|
+
Each contiguous block of diff lines can be staged, unstaged, reverted;
|
|
18
|
+
add review and todos. Commit and pull request review is read-only.
|
|
19
|
+
|
|
20
|
+
Intra-line highlighting paints the words that actually changed in a
|
|
21
|
+
stronger red/green. A close edit inside one word still marks only
|
|
22
|
+
those characters.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -g reslop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
reslop
|
|
34
|
+
reslop -n
|
|
35
|
+
reslop path/file
|
|
36
|
+
reslop 7ac260c
|
|
37
|
+
reslop https://github.com/metarhia/metacom/pull/555
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
41
|
+
|
|
42
|
+
- `editing`: still writing the review in reslop
|
|
43
|
+
- `ready`: ready for AI to work through the checkboxes
|
|
44
|
+
- `partial`: AI started; some items remain
|
|
45
|
+
- `done`: all items marked `[x]`
|
|
46
|
+
|
|
47
|
+
## Keys and buttons
|
|
48
|
+
|
|
49
|
+
| Key | Action |
|
|
50
|
+
| ----------- | --------------------------------------- |
|
|
51
|
+
| `a` | Stage this block (`git add`) |
|
|
52
|
+
| `u` | Unstage this block (keep worktree) |
|
|
53
|
+
| `r` | Restore this block to the last commit |
|
|
54
|
+
| `s` | Skip (leave unstaged, drop from review) |
|
|
55
|
+
| `←` | Previous remaining block |
|
|
56
|
+
| `→` | Next remaining block |
|
|
57
|
+
| `l` | File list (git status of this scope) |
|
|
58
|
+
| `Esc` | Quit file list or a tool |
|
|
59
|
+
| `m` | Cycle unified / mixed / side-by-side |
|
|
60
|
+
| `f` | Feedback on this diff block |
|
|
61
|
+
| `t` | New todo for this file |
|
|
62
|
+
| `⌫` / `Del` | Remove the selected todo |
|
|
63
|
+
| `⏎` | Open the selected file |
|
|
64
|
+
| `q` | Quit |
|
|
65
|
+
| `?` | Help |
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
Copyright (c) 2026 Timur Shemsedinov.
|
|
70
|
+
This is [MIT](./LICENSE) licensed software.
|
package/bin/reslop.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { run, errorMessage } = require('../lib/cli.js');
|
|
5
|
+
const { LEAVE_TERM } = require('../lib/session.js');
|
|
6
|
+
|
|
7
|
+
const fail = (reason) => {
|
|
8
|
+
try {
|
|
9
|
+
process.stdout.write(LEAVE_TERM);
|
|
10
|
+
} catch {}
|
|
11
|
+
process.stderr.write(`reslop: ${errorMessage(reason)}\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
process.on('uncaughtException', (error) => fail(error));
|
|
16
|
+
process.on('unhandledRejection', (reason) => fail(reason));
|
|
17
|
+
|
|
18
|
+
run(process).then(
|
|
19
|
+
(code) => {
|
|
20
|
+
process.exit(code);
|
|
21
|
+
},
|
|
22
|
+
(error) => fail(error),
|
|
23
|
+
);
|
package/lib/ansi.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
4
|
+
|
|
5
|
+
const ESC = String.fromCodePoint(0x1b);
|
|
6
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, 'g');
|
|
7
|
+
const RESET = `${ESC}[0m`;
|
|
8
|
+
const BOLD = `${ESC}[1m`;
|
|
9
|
+
const EL = `${ESC}[K`;
|
|
10
|
+
|
|
11
|
+
const WIDE_CP = [
|
|
12
|
+
0x1100, 0x115f, 0x231a, 0x231b, 0x2329, 0x232a, 0x23e9, 0x23ec, 0x23f0,
|
|
13
|
+
0x23f0, 0x23f3, 0x23f3, 0x25fd, 0x25fe, 0x2614, 0x2615, 0x2630, 0x2637,
|
|
14
|
+
0x2648, 0x2653, 0x267f, 0x267f, 0x268a, 0x268f, 0x2693, 0x2693, 0x26a1,
|
|
15
|
+
0x26a1, 0x26aa, 0x26ab, 0x26bd, 0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26ce,
|
|
16
|
+
0x26d4, 0x26d4, 0x26ea, 0x26ea, 0x26f2, 0x26f3, 0x26f5, 0x26f5, 0x26fa,
|
|
17
|
+
0x26fa, 0x26fd, 0x26fd, 0x2705, 0x2705, 0x270a, 0x270b, 0x2728, 0x2728,
|
|
18
|
+
0x274c, 0x274c, 0x274e, 0x274e, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795,
|
|
19
|
+
0x2797, 0x27b0, 0x27b0, 0x27bf, 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b50,
|
|
20
|
+
0x2b55, 0x2b55, 0x2e80, 0x2e99, 0x2e9b, 0x2ef3, 0x2f00, 0x2fd5, 0x2ff0,
|
|
21
|
+
0x303e, 0x3041, 0x3096, 0x3099, 0x30ff, 0x3105, 0x312f, 0x3131, 0x318e,
|
|
22
|
+
0x3190, 0x31e5, 0x31ef, 0x321e, 0x3220, 0x3247, 0x3250, 0xa48c, 0xa490,
|
|
23
|
+
0xa4c6, 0xa960, 0xa97c, 0xac00, 0xd7a3, 0xf900, 0xfaff, 0xfe10, 0xfe19,
|
|
24
|
+
0xfe30, 0xfe52, 0xfe54, 0xfe66, 0xfe68, 0xfe6b, 0xff01, 0xff60, 0xffe0,
|
|
25
|
+
0xffe6, 0x16fe0, 0x16fe4, 0x16ff0, 0x16ff1, 0x17000, 0x187f7, 0x18800,
|
|
26
|
+
0x18cd5, 0x18cff, 0x18d08, 0x1aff0, 0x1aff3, 0x1aff5, 0x1affb, 0x1affd,
|
|
27
|
+
0x1affe, 0x1b000, 0x1b122, 0x1b132, 0x1b132, 0x1b150, 0x1b152, 0x1b155,
|
|
28
|
+
0x1b155, 0x1b164, 0x1b167, 0x1b170, 0x1b2fb, 0x1d300, 0x1d356, 0x1d360,
|
|
29
|
+
0x1d376, 0x1f004, 0x1f004, 0x1f0cf, 0x1f0cf, 0x1f18e, 0x1f18e, 0x1f191,
|
|
30
|
+
0x1f19a, 0x1f200, 0x1f202, 0x1f210, 0x1f23b, 0x1f240, 0x1f248, 0x1f250,
|
|
31
|
+
0x1f251, 0x1f260, 0x1f265, 0x1f300, 0x1f320, 0x1f32d, 0x1f335, 0x1f337,
|
|
32
|
+
0x1f37c, 0x1f37e, 0x1f393, 0x1f3a0, 0x1f3ca, 0x1f3cf, 0x1f3d3, 0x1f3e0,
|
|
33
|
+
0x1f3f0, 0x1f3f4, 0x1f3f4, 0x1f3f8, 0x1f43e, 0x1f440, 0x1f440, 0x1f442,
|
|
34
|
+
0x1f4fc, 0x1f4ff, 0x1f53d, 0x1f54b, 0x1f54e, 0x1f550, 0x1f567, 0x1f57a,
|
|
35
|
+
0x1f57a, 0x1f595, 0x1f596, 0x1f5a4, 0x1f5a4, 0x1f5fb, 0x1f64f, 0x1f680,
|
|
36
|
+
0x1f6c5, 0x1f6cc, 0x1f6cc, 0x1f6d0, 0x1f6d2, 0x1f6d5, 0x1f6d7, 0x1f6dc,
|
|
37
|
+
0x1f6df, 0x1f6eb, 0x1f6ec, 0x1f6f4, 0x1f6fc, 0x1f7e0, 0x1f7eb, 0x1f7f0,
|
|
38
|
+
0x1f7f0, 0x1f90c, 0x1f93a, 0x1f93c, 0x1f945, 0x1f947, 0x1f9ff, 0x1fa70,
|
|
39
|
+
0x1fa7c, 0x1fa80, 0x1fa89, 0x1fa8f, 0x1fac6, 0x1face, 0x1fadc, 0x1fadf,
|
|
40
|
+
0x1fae9, 0x1faf0, 0x1faf8, 0x20000, 0x2fffd, 0x30000, 0x3fffd,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const THEME = {
|
|
44
|
+
delLineFg: [0xcc, 0x7a, 0x7a],
|
|
45
|
+
delLineBg: [0x3a, 0x1a, 0x1a],
|
|
46
|
+
delCharFg: [0xff, 0xff, 0xff],
|
|
47
|
+
delCharBg: [0x75, 0x22, 0x22],
|
|
48
|
+
addLineFg: [0x7a, 0xcc, 0x8a],
|
|
49
|
+
addLineBg: [0x14, 0x28, 0x14],
|
|
50
|
+
addCharFg: [0xff, 0xff, 0xff],
|
|
51
|
+
addCharBg: [0x1a, 0x4c, 0x26],
|
|
52
|
+
ctxFg: [0x8a, 0x8a, 0x8a],
|
|
53
|
+
ctxBg: [0x12, 0x12, 0x18],
|
|
54
|
+
splitGutterFg: [0x18, 0x18, 0x24],
|
|
55
|
+
splitGutterBg: [0x18, 0x18, 0x24],
|
|
56
|
+
headerFg: [0x3a, 0x3a, 0x3a],
|
|
57
|
+
headerBg: [0xe6, 0xe6, 0xee],
|
|
58
|
+
headerChromeFg: [0x5a, 0x5a, 0x5a],
|
|
59
|
+
headerRepoFg: [0x3a, 0x3a, 0x3a],
|
|
60
|
+
headerDirFg: [0x6e, 0x6e, 0x6e],
|
|
61
|
+
headerSlashFg: [0x8a, 0x8a, 0x8a],
|
|
62
|
+
headerFileFg: [0x4e, 0x4e, 0x4e],
|
|
63
|
+
headerMetaFg: [0x5a, 0x5a, 0x5a],
|
|
64
|
+
chromeFg: [0xd0, 0xd0, 0xe0],
|
|
65
|
+
chromeBg: [0x18, 0x18, 0x24],
|
|
66
|
+
noteBg: [0x24, 0x24, 0x36],
|
|
67
|
+
buttonFg: [0x6a, 0x6a, 0x7e],
|
|
68
|
+
buttonBg: [0x2a, 0x2a, 0x40],
|
|
69
|
+
buttonHotFg: [0xff, 0xff, 0xff],
|
|
70
|
+
checkFg: [0xf4, 0xf4, 0xfc],
|
|
71
|
+
checkBg: [0x52, 0x52, 0x74],
|
|
72
|
+
checkDoneFg: [0xf0, 0xff, 0xf4],
|
|
73
|
+
checkDoneBg: [0x22, 0x58, 0x34],
|
|
74
|
+
errorFg: [0xff, 0x80, 0x80],
|
|
75
|
+
warnFg: [0xe8, 0xc8, 0x4a],
|
|
76
|
+
mutedFg: [0x7a, 0x7a, 0x8a],
|
|
77
|
+
stagedDelLineFg: [0xa0, 0xa0, 0xa8],
|
|
78
|
+
stagedDelLineBg: [0x20, 0x20, 0x26],
|
|
79
|
+
stagedDelCharFg: [0xf0, 0xf0, 0xf4],
|
|
80
|
+
stagedDelCharBg: [0x44, 0x44, 0x4e],
|
|
81
|
+
stagedAddLineFg: [0xb0, 0xb0, 0xb8],
|
|
82
|
+
stagedAddLineBg: [0x26, 0x26, 0x2e],
|
|
83
|
+
stagedAddCharFg: [0xff, 0xff, 0xff],
|
|
84
|
+
stagedAddCharBg: [0x4e, 0x4e, 0x58],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const CODE_FG = {
|
|
88
|
+
keyword: [197, 134, 192],
|
|
89
|
+
control: [197, 134, 192],
|
|
90
|
+
storage: [86, 156, 214],
|
|
91
|
+
literal: [86, 156, 214],
|
|
92
|
+
string: [206, 145, 120],
|
|
93
|
+
template: [206, 145, 120],
|
|
94
|
+
escape: [209, 105, 105],
|
|
95
|
+
interpolation: [156, 220, 254],
|
|
96
|
+
number: [181, 206, 168],
|
|
97
|
+
comment: [106, 153, 85],
|
|
98
|
+
function: [220, 220, 170],
|
|
99
|
+
method: [220, 220, 170],
|
|
100
|
+
className: [78, 201, 176],
|
|
101
|
+
type: [78, 201, 176],
|
|
102
|
+
interface: [78, 201, 176],
|
|
103
|
+
property: [156, 220, 254],
|
|
104
|
+
variable: [156, 220, 254],
|
|
105
|
+
parameter: [156, 220, 254],
|
|
106
|
+
constant: [79, 193, 255],
|
|
107
|
+
operator: [212, 212, 212],
|
|
108
|
+
punct: [212, 212, 212],
|
|
109
|
+
regex: [209, 105, 105],
|
|
110
|
+
decorator: [220, 220, 170],
|
|
111
|
+
tag: [86, 156, 214],
|
|
112
|
+
attr: [156, 220, 254],
|
|
113
|
+
plain: [212, 212, 212],
|
|
114
|
+
punctDim: [128, 128, 128],
|
|
115
|
+
logDate: [210, 210, 220],
|
|
116
|
+
logError: [230, 90, 90],
|
|
117
|
+
logWarn: [230, 180, 80],
|
|
118
|
+
logInfo: [100, 170, 230],
|
|
119
|
+
logDebug: [140, 160, 180],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const LOG_COL_FG = [
|
|
123
|
+
[210, 210, 220],
|
|
124
|
+
[120, 180, 220],
|
|
125
|
+
[180, 160, 220],
|
|
126
|
+
[140, 200, 160],
|
|
127
|
+
[220, 180, 120],
|
|
128
|
+
[220, 140, 160],
|
|
129
|
+
[160, 200, 200],
|
|
130
|
+
[200, 170, 140],
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const codeFg = (style) => {
|
|
134
|
+
if (typeof style !== 'string') return CODE_FG.plain;
|
|
135
|
+
if (Object.hasOwn(CODE_FG, style)) return CODE_FG[style];
|
|
136
|
+
if (style.startsWith('logCol')) {
|
|
137
|
+
const i = parseInt(style.slice(6), 10);
|
|
138
|
+
return LOG_COL_FG[i % LOG_COL_FG.length];
|
|
139
|
+
}
|
|
140
|
+
return CODE_FG.plain;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const fg = (rgb) => `${ESC}[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
144
|
+
|
|
145
|
+
const bg = (rgb) => `${ESC}[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
146
|
+
|
|
147
|
+
const seq = (fgRgb, bgRgb) => `${fg(fgRgb)}${bg(bgRgb)}`;
|
|
148
|
+
|
|
149
|
+
const paint = (text, fgRgb, bgRgb, color, bold) => {
|
|
150
|
+
if (!color || text === '') return text;
|
|
151
|
+
const mark = bold ? BOLD : '';
|
|
152
|
+
return `${mark}${seq(fgRgb, bgRgb)}${text}${RESET}`;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const stripAnsi = (text) => text.replace(ANSI_RE, '');
|
|
156
|
+
|
|
157
|
+
const graphemes = (text) => {
|
|
158
|
+
const parts = [];
|
|
159
|
+
for (const { segment } of SEGMENTER.segment(text)) parts.push(segment);
|
|
160
|
+
return parts;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const isWide = (cp) => {
|
|
164
|
+
let lo = 0;
|
|
165
|
+
let hi = WIDE_CP.length >> 1;
|
|
166
|
+
while (lo < hi) {
|
|
167
|
+
const mid = (lo + hi) >> 1;
|
|
168
|
+
const start = WIDE_CP[mid << 1];
|
|
169
|
+
const end = WIDE_CP[(mid << 1) + 1];
|
|
170
|
+
if (cp < start) hi = mid;
|
|
171
|
+
else if (cp > end) lo = mid + 1;
|
|
172
|
+
else return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const graphemeWidth = (ch, col) => {
|
|
178
|
+
if (ch === '\t') return 8 - (col % 8);
|
|
179
|
+
if (ch === '\n' || ch === '\r') return 0;
|
|
180
|
+
const cp = ch.codePointAt(0);
|
|
181
|
+
if (cp <= 0x1f || cp === 0x7f) return 0;
|
|
182
|
+
if (cp === 0x200d) return 0;
|
|
183
|
+
if (cp >= 0xfe00 && cp <= 0xfe0f) return 0;
|
|
184
|
+
if (isWide(cp)) return 2;
|
|
185
|
+
return 1;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const visibleWidth = (text) => {
|
|
189
|
+
const plain = stripAnsi(text);
|
|
190
|
+
let width = 0;
|
|
191
|
+
for (const ch of graphemes(plain)) {
|
|
192
|
+
width += graphemeWidth(ch, width);
|
|
193
|
+
}
|
|
194
|
+
return width;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const truncateVisible = (text, width) => {
|
|
198
|
+
if (width <= 0) return '';
|
|
199
|
+
if (visibleWidth(text) <= width) return text;
|
|
200
|
+
const plain = stripAnsi(text);
|
|
201
|
+
const parts = graphemes(plain);
|
|
202
|
+
let used = 0;
|
|
203
|
+
let out = '';
|
|
204
|
+
for (const ch of parts) {
|
|
205
|
+
const w = graphemeWidth(ch, used);
|
|
206
|
+
if (used + w > width) break;
|
|
207
|
+
out += ch;
|
|
208
|
+
used += w;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
ESC,
|
|
215
|
+
RESET,
|
|
216
|
+
BOLD,
|
|
217
|
+
EL,
|
|
218
|
+
THEME,
|
|
219
|
+
CODE_FG,
|
|
220
|
+
LOG_COL_FG,
|
|
221
|
+
codeFg,
|
|
222
|
+
fg,
|
|
223
|
+
bg,
|
|
224
|
+
seq,
|
|
225
|
+
paint,
|
|
226
|
+
stripAnsi,
|
|
227
|
+
graphemes,
|
|
228
|
+
graphemeWidth,
|
|
229
|
+
visibleWidth,
|
|
230
|
+
truncateVisible,
|
|
231
|
+
};
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('./ansi.js');
|
|
4
|
+
const diff = require('./diff.js');
|
|
5
|
+
const git = require('./git.js');
|
|
6
|
+
const keys = require('./keys.js');
|
|
7
|
+
const render = require('./render.js');
|
|
8
|
+
const highlight = require('./highlight.js');
|
|
9
|
+
const detect = require('./detect.js');
|
|
10
|
+
const github = require('./github.js');
|
|
11
|
+
const source = require('./source.js');
|
|
12
|
+
const { Session } = require('./session.js');
|
|
13
|
+
const cli = require('./cli.js');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
THEME: ansi.THEME,
|
|
17
|
+
CODE_FG: ansi.CODE_FG,
|
|
18
|
+
ACTIONS: keys.ACTIONS,
|
|
19
|
+
diffChars: diff.diffChars,
|
|
20
|
+
parseDiff: diff.parseDiff,
|
|
21
|
+
splitHunk: diff.splitHunk,
|
|
22
|
+
displayLines: diff.displayLines,
|
|
23
|
+
formatPatch: diff.formatPatch,
|
|
24
|
+
itemsFromFiles: diff.itemsFromFiles,
|
|
25
|
+
createGitRepo: git.createGitRepo,
|
|
26
|
+
parseGithubPrUrl: github.parseGithubPrUrl,
|
|
27
|
+
loadPullRequest: github.loadPullRequest,
|
|
28
|
+
selectChangeSource: source.selectChangeSource,
|
|
29
|
+
decodeChunk: keys.decodeChunk,
|
|
30
|
+
hitAction: keys.hitAction,
|
|
31
|
+
renderFrame: render.renderFrame,
|
|
32
|
+
tokenize: highlight.tokenize,
|
|
33
|
+
overlayTokens: highlight.overlayTokens,
|
|
34
|
+
detectLang: detect.detectLang,
|
|
35
|
+
run: cli.run,
|
|
36
|
+
Session,
|
|
37
|
+
};
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { createGitRepo } = require('./git.js');
|
|
7
|
+
const { githubToken, loadPullRequest } = require('./github.js');
|
|
8
|
+
const { selectChangeSource, createLoadedSource } = require('./source.js');
|
|
9
|
+
const { Session } = require('./session.js');
|
|
10
|
+
|
|
11
|
+
const HEX_REV = /^[0-9a-f]{4,40}$/i;
|
|
12
|
+
const HEAD_REV = /^HEAD([~^].*)?$/i;
|
|
13
|
+
const RANGE_REV = /[~^]/;
|
|
14
|
+
|
|
15
|
+
const USAGE = 'Usage: reslop [-n] [path | commit | pr-url]';
|
|
16
|
+
|
|
17
|
+
const errorMessage = (reason) => {
|
|
18
|
+
if (reason instanceof Error) return reason.message;
|
|
19
|
+
if (reason === null || reason === undefined) return '';
|
|
20
|
+
if (typeof reason === 'string') return reason;
|
|
21
|
+
return `${reason}`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fail = (stderr, reason) => {
|
|
25
|
+
stderr.write(`reslop: ${errorMessage(reason)}\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const failUsage = (stderr, reason) => {
|
|
30
|
+
fail(stderr, reason);
|
|
31
|
+
stderr.write(`${USAGE}\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parseArgv = (argv) => {
|
|
36
|
+
const paths = [];
|
|
37
|
+
let newReview = false;
|
|
38
|
+
for (const arg of argv) {
|
|
39
|
+
if (arg === '-n') {
|
|
40
|
+
newReview = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg.startsWith('-')) {
|
|
44
|
+
throw new Error(`unknown option ${arg}`);
|
|
45
|
+
}
|
|
46
|
+
paths.push(arg);
|
|
47
|
+
}
|
|
48
|
+
return { paths, newReview };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const looksLikeRev = (spec) => {
|
|
52
|
+
if (!spec) return false;
|
|
53
|
+
if (HEX_REV.test(spec)) return true;
|
|
54
|
+
if (HEAD_REV.test(spec)) return true;
|
|
55
|
+
return RANGE_REV.test(spec);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pathExists = (cwd, rel, statSync) => {
|
|
59
|
+
try {
|
|
60
|
+
statSync(path.resolve(cwd, rel));
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const resolveScope = (cwd, paths, repo, statSync = fs.statSync) => {
|
|
68
|
+
if (!paths.length) return { rev: null, paths };
|
|
69
|
+
const first = paths[0];
|
|
70
|
+
if (pathExists(cwd, first, statSync)) {
|
|
71
|
+
return { rev: null, paths };
|
|
72
|
+
}
|
|
73
|
+
const resolved = repo.resolveRev ? repo.resolveRev(cwd, first) : null;
|
|
74
|
+
if (resolved) {
|
|
75
|
+
if (paths.length > 1) {
|
|
76
|
+
throw new Error('use a commit or a path, not both');
|
|
77
|
+
}
|
|
78
|
+
return { rev: resolved, paths: [] };
|
|
79
|
+
}
|
|
80
|
+
if (looksLikeRev(first)) {
|
|
81
|
+
throw new Error(`bad revision ${first}`);
|
|
82
|
+
}
|
|
83
|
+
return { rev: null, paths };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isColorEnabled = (stdout, env) => stdout.isTTY === true && !env.NO_COLOR;
|
|
87
|
+
|
|
88
|
+
const openLocalSession = (proc, deps, args, env) => {
|
|
89
|
+
const stdout = proc.stdout;
|
|
90
|
+
const repo = deps.repo ?? createGitRepo();
|
|
91
|
+
const cwd = deps.cwd ?? proc.cwd();
|
|
92
|
+
const statSync = deps.statSync ?? fs.statSync;
|
|
93
|
+
const scope = resolveScope(cwd, args.paths, repo, statSync);
|
|
94
|
+
const session = new Session({
|
|
95
|
+
repo,
|
|
96
|
+
cwd,
|
|
97
|
+
paths: scope.paths,
|
|
98
|
+
rev: scope.rev,
|
|
99
|
+
stdin: proc.stdin,
|
|
100
|
+
stdout,
|
|
101
|
+
color: isColorEnabled(stdout, env),
|
|
102
|
+
getSize: deps.getSize,
|
|
103
|
+
startPane: 'files',
|
|
104
|
+
newReview: args.newReview,
|
|
105
|
+
});
|
|
106
|
+
session.load();
|
|
107
|
+
return session;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const openPullRequestSession = async (proc, deps, args, selected, env) => {
|
|
111
|
+
const stdout = proc.stdout;
|
|
112
|
+
const cwd = deps.cwd ?? proc.cwd();
|
|
113
|
+
const loader = deps.loadPullRequest ?? loadPullRequest;
|
|
114
|
+
const token = githubToken(env);
|
|
115
|
+
const loaded = await loader(selected.pr, {
|
|
116
|
+
cwd,
|
|
117
|
+
paths: selected.paths,
|
|
118
|
+
token,
|
|
119
|
+
fetch: deps.fetch,
|
|
120
|
+
env,
|
|
121
|
+
});
|
|
122
|
+
const change = loaded.change ?? null;
|
|
123
|
+
const repository = change ? change.repository : '';
|
|
124
|
+
const repo = deps.repo ?? createLoadedSource(loaded);
|
|
125
|
+
const session = new Session({
|
|
126
|
+
repo,
|
|
127
|
+
cwd,
|
|
128
|
+
paths: selected.paths,
|
|
129
|
+
stdin: proc.stdin,
|
|
130
|
+
stdout,
|
|
131
|
+
color: isColorEnabled(stdout, env),
|
|
132
|
+
getSize: deps.getSize,
|
|
133
|
+
startPane: 'files',
|
|
134
|
+
newReview: args.newReview,
|
|
135
|
+
repoName: repository,
|
|
136
|
+
sourceLabel: loaded.sourceLabel,
|
|
137
|
+
change,
|
|
138
|
+
});
|
|
139
|
+
session.load();
|
|
140
|
+
return session;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const loadSession = async (proc, deps, args) => {
|
|
144
|
+
const env = proc.env ?? {};
|
|
145
|
+
const selected = selectChangeSource(args.paths);
|
|
146
|
+
if (selected.kind === 'github-pr') {
|
|
147
|
+
return openPullRequestSession(proc, deps, args, selected, env);
|
|
148
|
+
}
|
|
149
|
+
return openLocalSession(proc, deps, args, env);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const run = async (proc, deps = {}) => {
|
|
153
|
+
const stdout = proc.stdout;
|
|
154
|
+
const stderr = proc.stderr;
|
|
155
|
+
let args;
|
|
156
|
+
try {
|
|
157
|
+
args = parseArgv(proc.argv.slice(2));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return failUsage(stderr, error);
|
|
160
|
+
}
|
|
161
|
+
let session;
|
|
162
|
+
try {
|
|
163
|
+
session = await loadSession(proc, deps, args);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return fail(stderr, error);
|
|
166
|
+
}
|
|
167
|
+
if (!session.items.length) {
|
|
168
|
+
stdout.write('nothing to review\n');
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
if (!proc.stdin.isTTY || !stdout.isTTY) {
|
|
172
|
+
stderr.write('reslop: interactive terminal required\n');
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
return session.startUi();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
run,
|
|
180
|
+
parseArgv,
|
|
181
|
+
resolveScope,
|
|
182
|
+
looksLikeRev,
|
|
183
|
+
selectChangeSource,
|
|
184
|
+
errorMessage,
|
|
185
|
+
};
|
package/lib/clipboard.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const { ESC } = require('./ansi.js');
|
|
6
|
+
|
|
7
|
+
const CLIP_TIMEOUT_MS = 1000;
|
|
8
|
+
const CLIP_TOOLS = [
|
|
9
|
+
{ cmd: 'wl-copy', args: [] },
|
|
10
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard'] },
|
|
11
|
+
{ cmd: 'xsel', args: ['--clipboard', '--input'] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const osc52 = (text) => {
|
|
15
|
+
const b64 = Buffer.from(text, 'utf8').toString('base64');
|
|
16
|
+
return `${ESC}]52;c;${b64}\x07`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const runClip = (cmd, args, text) => {
|
|
20
|
+
const result = spawnSync(cmd, args, {
|
|
21
|
+
input: text,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
timeout: CLIP_TIMEOUT_MS,
|
|
24
|
+
});
|
|
25
|
+
if (result.error) return false;
|
|
26
|
+
return result.status === 0;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const copyText = (text, stdout) => {
|
|
30
|
+
if (!text) return false;
|
|
31
|
+
let ok = false;
|
|
32
|
+
if (stdout && stdout.write) {
|
|
33
|
+
stdout.write(osc52(text));
|
|
34
|
+
ok = true;
|
|
35
|
+
}
|
|
36
|
+
for (const tool of CLIP_TOOLS) {
|
|
37
|
+
if (runClip(tool.cmd, tool.args, text)) return true;
|
|
38
|
+
}
|
|
39
|
+
return ok;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = { osc52, copyText, runClip };
|