reslop 0.1.0 → 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 +17 -8
- package/README.md +21 -38
- package/lib/api.js +5 -0
- package/lib/cli.js +72 -68
- package/lib/diff.js +9 -8
- package/lib/files.js +1 -1
- package/lib/git.js +25 -0
- package/lib/github.js +631 -0
- package/lib/highlight.js +23 -26
- package/lib/keys.js +9 -6
- package/lib/render.js +59 -27
- package/lib/review.js +32 -5
- package/lib/select.js +5 -13
- package/lib/session.js +212 -38
- package/lib/source.js +26 -0
- package/package.json +37 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,15 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.1.
|
|
5
|
+
## [0.1.1][] - 2026-09-10
|
|
6
|
+
|
|
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
|
|
6
15
|
|
|
7
16
|
### Added
|
|
8
17
|
|
|
9
|
-
- Interactive terminal git
|
|
10
|
-
|
|
11
|
-
- Feedback and todos
|
|
12
|
-
-
|
|
13
|
-
- Review of a worktree, a path, or a given commit (read-only)
|
|
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
|
|
14
22
|
|
|
15
|
-
[unreleased]: https://github.com/tshemsedinov/
|
|
16
|
-
[0.1.
|
|
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/README.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
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
|
+
```
|
|
8
|
+
|
|
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.
|
|
6
13
|
- Leave feedback, add todos.
|
|
7
14
|
- Generate a plan the agent executes.
|
|
8
15
|
|
|
9
|
-
Walk uncommitted git changes,
|
|
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.
|
|
10
19
|
|
|
11
20
|
Intra-line highlighting paints the words that actually changed in a
|
|
12
21
|
stronger red/green. A close edit inside one word still marks only
|
|
@@ -14,52 +23,26 @@ those characters.
|
|
|
14
23
|
|
|
15
24
|
## Install
|
|
16
25
|
|
|
17
|
-
Global:
|
|
18
|
-
|
|
19
26
|
```bash
|
|
20
27
|
npm i -g reslop
|
|
21
28
|
```
|
|
22
29
|
|
|
23
|
-
Local:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx reslop
|
|
27
|
-
```
|
|
28
|
-
|
|
29
30
|
## Usage
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
33
|
reslop
|
|
33
34
|
reslop -n
|
|
34
|
-
reslop path/
|
|
35
|
-
reslop path/to/file
|
|
35
|
+
reslop path/file
|
|
36
36
|
reslop 7ac260c
|
|
37
|
-
reslop
|
|
38
|
-
reslop --help
|
|
37
|
+
reslop https://github.com/metarhia/metacom/pull/555
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
A file argument opens that file's diff. A git revision that is not
|
|
43
|
-
also an existing path opens that commit's patch. Esc opens the file
|
|
44
|
-
list; Esc again quits.
|
|
45
|
-
|
|
46
|
-
Specs go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
47
|
-
|
|
48
|
-
| Status | Meaning |
|
|
49
|
-
| ------- | ------------------------------------------- |
|
|
50
|
-
| editing | Still writing the review in reslop |
|
|
51
|
-
| ready | Ready for AI to work through the checkboxes |
|
|
52
|
-
| partial | AI started; some items remain |
|
|
53
|
-
| done | All items marked `[x]` |
|
|
54
|
-
|
|
55
|
-
`reslop` resumes the latest file when its status is `editing`. Any other
|
|
56
|
-
status starts a new file. `-n` / `--new` always starts a new file. Quit asks
|
|
57
|
-
`f` (finish as `ready`, for the agent to execute) or `c` (keep `editing` and
|
|
58
|
-
continue next time). The agent should execute `ready` and `partial` reviews.
|
|
59
|
-
Files in `.review/` are omitted from the diff list.
|
|
40
|
+
Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
60
41
|
|
|
61
|
-
|
|
62
|
-
|
|
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]`
|
|
63
46
|
|
|
64
47
|
## Keys and buttons
|
|
65
48
|
|
|
@@ -72,7 +55,7 @@ checkbox with ` - path:old:new:block` on the same line.
|
|
|
72
55
|
| `←` | Previous remaining block |
|
|
73
56
|
| `→` | Next remaining block |
|
|
74
57
|
| `l` | File list (git status of this scope) |
|
|
75
|
-
| `Esc` |
|
|
58
|
+
| `Esc` | Quit file list or a tool |
|
|
76
59
|
| `m` | Cycle unified / mixed / side-by-side |
|
|
77
60
|
| `f` | Feedback on this diff block |
|
|
78
61
|
| `t` | New todo for this file |
|
package/lib/api.js
CHANGED
|
@@ -7,6 +7,8 @@ const keys = require('./keys.js');
|
|
|
7
7
|
const render = require('./render.js');
|
|
8
8
|
const highlight = require('./highlight.js');
|
|
9
9
|
const detect = require('./detect.js');
|
|
10
|
+
const github = require('./github.js');
|
|
11
|
+
const source = require('./source.js');
|
|
10
12
|
const { Session } = require('./session.js');
|
|
11
13
|
const cli = require('./cli.js');
|
|
12
14
|
|
|
@@ -21,6 +23,9 @@ module.exports = {
|
|
|
21
23
|
formatPatch: diff.formatPatch,
|
|
22
24
|
itemsFromFiles: diff.itemsFromFiles,
|
|
23
25
|
createGitRepo: git.createGitRepo,
|
|
26
|
+
parseGithubPrUrl: github.parseGithubPrUrl,
|
|
27
|
+
loadPullRequest: github.loadPullRequest,
|
|
28
|
+
selectChangeSource: source.selectChangeSource,
|
|
24
29
|
decodeChunk: keys.decodeChunk,
|
|
25
30
|
hitAction: keys.hitAction,
|
|
26
31
|
renderFrame: render.renderFrame,
|
package/lib/cli.js
CHANGED
|
@@ -4,22 +4,16 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
6
|
const { createGitRepo } = require('./git.js');
|
|
7
|
+
const { githubToken, loadPullRequest } = require('./github.js');
|
|
8
|
+
const { selectChangeSource, createLoadedSource } = require('./source.js');
|
|
7
9
|
const { Session } = require('./session.js');
|
|
8
|
-
const { ACTIONS, actionHot } = require('./keys.js');
|
|
9
|
-
|
|
10
|
-
const FLAGS = Object.assign(Object.create(null), {
|
|
11
|
-
'-h': 'help',
|
|
12
|
-
'--help': 'help',
|
|
13
|
-
'-v': 'version',
|
|
14
|
-
'--version': 'version',
|
|
15
|
-
'-n': 'newReview',
|
|
16
|
-
'--new': 'newReview',
|
|
17
|
-
});
|
|
18
10
|
|
|
19
11
|
const HEX_REV = /^[0-9a-f]{4,40}$/i;
|
|
20
12
|
const HEAD_REV = /^HEAD([~^].*)?$/i;
|
|
21
13
|
const RANGE_REV = /[~^]/;
|
|
22
14
|
|
|
15
|
+
const USAGE = 'Usage: reslop [-n] [path | commit | pr-url]';
|
|
16
|
+
|
|
23
17
|
const errorMessage = (reason) => {
|
|
24
18
|
if (reason instanceof Error) return reason.message;
|
|
25
19
|
if (reason === null || reason === undefined) return '';
|
|
@@ -32,32 +26,18 @@ const fail = (stderr, reason) => {
|
|
|
32
26
|
return 1;
|
|
33
27
|
};
|
|
34
28
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'Review uncommitted git changes, or a given commit (read only).',
|
|
40
|
-
'Resumes the latest .review file when its status is editing.',
|
|
41
|
-
'Finish marks the review ready for the agent to execute.',
|
|
42
|
-
'Use -n / --new to start a new review file.',
|
|
43
|
-
'',
|
|
44
|
-
'Keys:',
|
|
45
|
-
];
|
|
46
|
-
for (const action of ACTIONS) {
|
|
47
|
-
lines.push(` ${actionHot(action)} ${action.label}`);
|
|
48
|
-
}
|
|
49
|
-
lines.push(' ? Help');
|
|
50
|
-
lines.push('');
|
|
51
|
-
return `${lines.join('\n')}\n`;
|
|
29
|
+
const failUsage = (stderr, reason) => {
|
|
30
|
+
fail(stderr, reason);
|
|
31
|
+
stderr.write(`${USAGE}\n`);
|
|
32
|
+
return 1;
|
|
52
33
|
};
|
|
53
34
|
|
|
54
35
|
const parseArgv = (argv) => {
|
|
55
36
|
const paths = [];
|
|
56
|
-
|
|
37
|
+
let newReview = false;
|
|
57
38
|
for (const arg of argv) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
flags[flag] = true;
|
|
39
|
+
if (arg === '-n') {
|
|
40
|
+
newReview = true;
|
|
61
41
|
continue;
|
|
62
42
|
}
|
|
63
43
|
if (arg.startsWith('-')) {
|
|
@@ -65,10 +45,7 @@ const parseArgv = (argv) => {
|
|
|
65
45
|
}
|
|
66
46
|
paths.push(arg);
|
|
67
47
|
}
|
|
68
|
-
|
|
69
|
-
const version = flags.version;
|
|
70
|
-
const newReview = flags.newReview;
|
|
71
|
-
return { paths, help, version, newReview };
|
|
48
|
+
return { paths, newReview };
|
|
72
49
|
};
|
|
73
50
|
|
|
74
51
|
const looksLikeRev = (spec) => {
|
|
@@ -94,55 +71,84 @@ const resolveScope = (cwd, paths, repo, statSync = fs.statSync) => {
|
|
|
94
71
|
return { rev: null, paths };
|
|
95
72
|
}
|
|
96
73
|
const resolved = repo.resolveRev ? repo.resolveRev(cwd, first) : null;
|
|
97
|
-
if (resolved)
|
|
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
|
+
}
|
|
98
80
|
if (looksLikeRev(first)) {
|
|
99
81
|
throw new Error(`bad revision ${first}`);
|
|
100
82
|
}
|
|
101
83
|
return { rev: null, paths };
|
|
102
84
|
};
|
|
103
85
|
|
|
104
|
-
const
|
|
105
|
-
stdout.write(helpText());
|
|
106
|
-
return 0;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const writeVersion = (stdout, deps) => {
|
|
110
|
-
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
111
|
-
const pkg = deps.pkg ?? JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
112
|
-
stdout.write(`${pkg.version}\n`);
|
|
113
|
-
return 0;
|
|
114
|
-
};
|
|
86
|
+
const isColorEnabled = (stdout, env) => stdout.isTTY === true && !env.NO_COLOR;
|
|
115
87
|
|
|
116
|
-
const
|
|
88
|
+
const openLocalSession = (proc, deps, args, env) => {
|
|
117
89
|
const stdout = proc.stdout;
|
|
118
|
-
const env = proc.env ?? {};
|
|
119
90
|
const repo = deps.repo ?? createGitRepo();
|
|
120
91
|
const cwd = deps.cwd ?? proc.cwd();
|
|
121
92
|
const statSync = deps.statSync ?? fs.statSync;
|
|
122
93
|
const scope = resolveScope(cwd, args.paths, repo, statSync);
|
|
123
|
-
const paths = scope.paths;
|
|
124
|
-
const rev = scope.rev;
|
|
125
|
-
const stdin = proc.stdin;
|
|
126
|
-
const color = stdout.isTTY === true && !env.NO_COLOR;
|
|
127
|
-
const getSize = deps.getSize;
|
|
128
|
-
const startPane = 'diff';
|
|
129
|
-
const newReview = args.newReview;
|
|
130
94
|
const session = new Session({
|
|
131
95
|
repo,
|
|
132
96
|
cwd,
|
|
133
|
-
paths,
|
|
134
|
-
rev,
|
|
135
|
-
stdin,
|
|
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,
|
|
136
130
|
stdout,
|
|
137
|
-
color,
|
|
138
|
-
getSize,
|
|
139
|
-
startPane,
|
|
140
|
-
newReview,
|
|
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,
|
|
141
138
|
});
|
|
142
139
|
session.load();
|
|
143
140
|
return session;
|
|
144
141
|
};
|
|
145
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
|
+
|
|
146
152
|
const run = async (proc, deps = {}) => {
|
|
147
153
|
const stdout = proc.stdout;
|
|
148
154
|
const stderr = proc.stderr;
|
|
@@ -150,13 +156,11 @@ const run = async (proc, deps = {}) => {
|
|
|
150
156
|
try {
|
|
151
157
|
args = parseArgv(proc.argv.slice(2));
|
|
152
158
|
} catch (error) {
|
|
153
|
-
return
|
|
159
|
+
return failUsage(stderr, error);
|
|
154
160
|
}
|
|
155
|
-
if (args.help) return writeHelp(stdout);
|
|
156
|
-
if (args.version) return writeVersion(stdout, deps);
|
|
157
161
|
let session;
|
|
158
162
|
try {
|
|
159
|
-
session = loadSession(proc, deps, args);
|
|
163
|
+
session = await loadSession(proc, deps, args);
|
|
160
164
|
} catch (error) {
|
|
161
165
|
return fail(stderr, error);
|
|
162
166
|
}
|
|
@@ -174,8 +178,8 @@ const run = async (proc, deps = {}) => {
|
|
|
174
178
|
module.exports = {
|
|
175
179
|
run,
|
|
176
180
|
parseArgv,
|
|
177
|
-
helpText,
|
|
178
181
|
resolveScope,
|
|
179
182
|
looksLikeRev,
|
|
183
|
+
selectChangeSource,
|
|
180
184
|
errorMessage,
|
|
181
185
|
};
|
package/lib/diff.js
CHANGED
|
@@ -11,22 +11,23 @@ const PAIR_TOKEN_WEIGHT = 0.6;
|
|
|
11
11
|
const WORD_REFINE_MIN = 0.6;
|
|
12
12
|
const TOKEN_RE = /[A-Za-z_$][\w$]*|\d+|[^\s]/g;
|
|
13
13
|
const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
|
|
14
|
-
const LINE_MARK =
|
|
14
|
+
const LINE_MARK = {
|
|
15
15
|
add: '+',
|
|
16
16
|
del: '-',
|
|
17
17
|
ctx: ' ',
|
|
18
|
-
}
|
|
19
|
-
const LINE_TYPE =
|
|
18
|
+
};
|
|
19
|
+
const LINE_TYPE = {
|
|
20
20
|
'+': 'add',
|
|
21
21
|
'-': 'del',
|
|
22
22
|
' ': 'ctx',
|
|
23
|
-
}
|
|
24
|
-
const INDEX_SIDE =
|
|
23
|
+
};
|
|
24
|
+
const INDEX_SIDE = {
|
|
25
25
|
staged: 'new',
|
|
26
26
|
unstaged: 'old',
|
|
27
27
|
untracked: 'old',
|
|
28
28
|
commit: 'old',
|
|
29
|
-
|
|
29
|
+
pr: 'old',
|
|
30
|
+
};
|
|
30
31
|
|
|
31
32
|
const collapseSpans = (parts) => {
|
|
32
33
|
const spans = [];
|
|
@@ -774,10 +775,10 @@ const formatPatch = (file, hunk, blockId, contextSide) => {
|
|
|
774
775
|
return `${parts.join('\n')}\n`;
|
|
775
776
|
};
|
|
776
777
|
|
|
777
|
-
const LAYOUT_ALIGN =
|
|
778
|
+
const LAYOUT_ALIGN = {
|
|
778
779
|
mixed: alignPairs,
|
|
779
780
|
side: alignSplit,
|
|
780
|
-
}
|
|
781
|
+
};
|
|
781
782
|
|
|
782
783
|
const windowAroundBlock = (lines, blockId, radius) => {
|
|
783
784
|
let start = -1;
|
package/lib/files.js
CHANGED
package/lib/git.js
CHANGED
|
@@ -240,12 +240,36 @@ const revertItem = (top, item) => {
|
|
|
240
240
|
}
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
const revertFile = (top, rel, items) => {
|
|
244
|
+
const origins = new Set();
|
|
245
|
+
let isNew = false;
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
origins.add(item.origin);
|
|
248
|
+
if (item.file && item.file.isNew) isNew = true;
|
|
249
|
+
}
|
|
250
|
+
if (isNew) {
|
|
251
|
+
if (origins.has('staged')) {
|
|
252
|
+
requireOk(runGit(['rm', '-f', '--', rel], top));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
fs.unlinkSync(path.join(top, rel));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (origins.has('untracked') && origins.size === 1) {
|
|
259
|
+
fs.unlinkSync(path.join(top, rel));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const args = ['restore', '-s', 'HEAD', '--worktree', '--staged', '--', rel];
|
|
263
|
+
requireOk(runGit(args, top));
|
|
264
|
+
};
|
|
265
|
+
|
|
243
266
|
const createGitRepo = () => ({
|
|
244
267
|
load,
|
|
245
268
|
resolveRev,
|
|
246
269
|
add: addItem,
|
|
247
270
|
unstage: unstageItem,
|
|
248
271
|
revert: revertItem,
|
|
272
|
+
revertFile,
|
|
249
273
|
});
|
|
250
274
|
|
|
251
275
|
module.exports = {
|
|
@@ -256,6 +280,7 @@ module.exports = {
|
|
|
256
280
|
addItem,
|
|
257
281
|
unstageItem,
|
|
258
282
|
revertItem,
|
|
283
|
+
revertFile,
|
|
259
284
|
createGitRepo,
|
|
260
285
|
oneLine,
|
|
261
286
|
};
|