reslop 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -9
- package/README.md +25 -40
- package/bin/reslop.js +2 -2
- package/lib/ansi.js +0 -1
- package/lib/api.js +5 -0
- package/lib/cli.js +84 -79
- package/lib/clipboard.js +1 -1
- package/lib/deps.js +1440 -0
- package/lib/detect.js +4 -4
- package/lib/diff.js +40 -63
- package/lib/editor.js +1 -1
- package/lib/files.js +23 -4
- package/lib/git.js +367 -18
- package/lib/github.js +625 -0
- package/lib/highlight.js +149 -166
- package/lib/keys.js +34 -41
- package/lib/render.js +218 -143
- package/lib/review.js +52 -45
- package/lib/select.js +21 -31
- package/lib/session.js +298 -235
- package/lib/source.js +30 -0
- package/package.json +37 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,15 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.1.
|
|
5
|
+
## [0.1.2][] - 2026-09-11
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Review each dependency once across package.json and package-lock
|
|
8
|
+
- Mark unused npm dependencies that are not imported or used
|
|
9
|
+
- Propose unused dependency removals and run npm uninstall
|
|
10
|
+
- Mark dependency review items and show npm audit findings
|
|
11
|
+
- Show npm outdated and npm audit updates as proposed diffs
|
|
12
|
+
- Propose a lockfile version bump for transitive dependencies
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
- Stage, unstage, revert, and skip contiguous diff blocks
|
|
11
|
-
- Feedback and todos in `.review/` markdown for an agent to execute
|
|
12
|
-
- Unified, mixed, and side-by-side layouts
|
|
13
|
-
- Review of a worktree, a path, or a given commit (read-only)
|
|
14
|
+
## [0.1.1][] - 2026-09-10
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
- Open a GitHub pull request by URL and review its diff
|
|
17
|
+
- Import GitHub pull request review comments into the local review
|
|
18
|
+
- Improve TUI navigation and hotkeys
|
|
19
|
+
- CLI simplification and optimisation
|
|
20
|
+
- Fix diff highlighting for snake_case identifier changes
|
|
21
|
+
- Updated package metadata and README
|
|
22
|
+
|
|
23
|
+
## [0.1.0][] - 2026-09-07
|
|
24
|
+
|
|
25
|
+
- Interactive review of uncommitted git diffs with stage, unstage, revert
|
|
26
|
+
- Feedback and todos that produce a repair plan for an agent
|
|
27
|
+
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
28
|
+
|
|
29
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...HEAD
|
|
30
|
+
[0.1.2]: https://github.com/tshemsedinov/reslop/compare/v0.1.1...v0.1.2
|
|
31
|
+
[0.1.1]: https://github.com/tshemsedinov/reslop/compare/v0.1.0...v0.1.1
|
|
32
|
+
[0.1.0]: https://github.com/tshemsedinov/reslop/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
# reslop
|
|
1
|
+
# reslop — Review diff and plan changes
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Turn generated changes into owned changes
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Generate a plan the agent executes.
|
|
5
|
+
Open a change, leave findings and todos, and prepare a repair plan
|
|
6
|
+
for the agent.
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
```text
|
|
9
|
+
Review → Plan → Repair → Verify
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- Review uncommitted git diffs, a given commit, or a GitHub pull request.
|
|
13
|
+
- Stage, unstage, or revert each contiguous block of diff lines.
|
|
14
|
+
- Leave feedback and todos that become a repair plan for the agent.
|
|
15
|
+
- Import GitHub pull request review comments into the local plan for AI.
|
|
16
|
+
- Review each npm dependency once across `package.json` and the lockfile.
|
|
17
|
+
- Propose unused removals, npm audit fixes, and outdated updates as diffs.
|
|
18
|
+
|
|
19
|
+
Commit and pull request review is read-only. Applying a dependency
|
|
20
|
+
proposal stages the files and runs `npm i` or `npm uninstall`.
|
|
10
21
|
|
|
11
22
|
Intra-line highlighting paints the words that actually changed in a
|
|
12
23
|
stronger red/green. A close edit inside one word still marks only
|
|
@@ -14,52 +25,26 @@ those characters.
|
|
|
14
25
|
|
|
15
26
|
## Install
|
|
16
27
|
|
|
17
|
-
Global:
|
|
18
|
-
|
|
19
28
|
```bash
|
|
20
29
|
npm i -g reslop
|
|
21
30
|
```
|
|
22
31
|
|
|
23
|
-
Local:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npx reslop
|
|
27
|
-
```
|
|
28
|
-
|
|
29
32
|
## Usage
|
|
30
33
|
|
|
31
34
|
```bash
|
|
32
35
|
reslop
|
|
33
36
|
reslop -n
|
|
34
|
-
reslop path/
|
|
35
|
-
reslop path/to/file
|
|
37
|
+
reslop path/file
|
|
36
38
|
reslop 7ac260c
|
|
37
|
-
reslop
|
|
38
|
-
reslop --help
|
|
39
|
+
reslop https://github.com/metarhia/metacom/pull/555
|
|
39
40
|
```
|
|
40
41
|
|
|
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.
|
|
42
|
+
Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
60
43
|
|
|
61
|
-
|
|
62
|
-
|
|
44
|
+
- `editing`: still writing the review in reslop
|
|
45
|
+
- `ready`: ready for AI to work through the checkboxes
|
|
46
|
+
- `partial`: AI started; some items remain
|
|
47
|
+
- `done`: all items marked `[x]`
|
|
63
48
|
|
|
64
49
|
## Keys and buttons
|
|
65
50
|
|
|
@@ -72,7 +57,7 @@ checkbox with ` - path:old:new:block` on the same line.
|
|
|
72
57
|
| `←` | Previous remaining block |
|
|
73
58
|
| `→` | Next remaining block |
|
|
74
59
|
| `l` | File list (git status of this scope) |
|
|
75
|
-
| `Esc` |
|
|
60
|
+
| `Esc` | Quit file list or a tool |
|
|
76
61
|
| `m` | Cycle unified / mixed / side-by-side |
|
|
77
62
|
| `f` | Feedback on this diff block |
|
|
78
63
|
| `t` | New todo for this file |
|
package/bin/reslop.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { run,
|
|
4
|
+
const { run, failLine } = require('../lib/cli.js');
|
|
5
5
|
const { LEAVE_TERM } = require('../lib/session.js');
|
|
6
6
|
|
|
7
7
|
const fail = (reason) => {
|
|
8
8
|
try {
|
|
9
9
|
process.stdout.write(LEAVE_TERM);
|
|
10
10
|
} catch {}
|
|
11
|
-
process.stderr.write(
|
|
11
|
+
process.stderr.write(failLine(reason));
|
|
12
12
|
process.exit(1);
|
|
13
13
|
};
|
|
14
14
|
|
package/lib/ansi.js
CHANGED
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 '';
|
|
@@ -27,37 +21,25 @@ const errorMessage = (reason) => {
|
|
|
27
21
|
return `${reason}`;
|
|
28
22
|
};
|
|
29
23
|
|
|
24
|
+
const failLine = (reason) => `reslop: ${errorMessage(reason)}\n`;
|
|
25
|
+
|
|
30
26
|
const fail = (stderr, reason) => {
|
|
31
|
-
stderr.write(
|
|
27
|
+
stderr.write(failLine(reason));
|
|
32
28
|
return 1;
|
|
33
29
|
};
|
|
34
30
|
|
|
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`;
|
|
31
|
+
const failUsage = (stderr, reason) => {
|
|
32
|
+
fail(stderr, reason);
|
|
33
|
+
stderr.write(`${USAGE}\n`);
|
|
34
|
+
return 1;
|
|
52
35
|
};
|
|
53
36
|
|
|
54
37
|
const parseArgv = (argv) => {
|
|
55
38
|
const paths = [];
|
|
56
|
-
|
|
39
|
+
let newReview = false;
|
|
57
40
|
for (const arg of argv) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
flags[flag] = true;
|
|
41
|
+
if (arg === '-n') {
|
|
42
|
+
newReview = true;
|
|
61
43
|
continue;
|
|
62
44
|
}
|
|
63
45
|
if (arg.startsWith('-')) {
|
|
@@ -65,10 +47,7 @@ const parseArgv = (argv) => {
|
|
|
65
47
|
}
|
|
66
48
|
paths.push(arg);
|
|
67
49
|
}
|
|
68
|
-
|
|
69
|
-
const version = flags.version;
|
|
70
|
-
const newReview = flags.newReview;
|
|
71
|
-
return { paths, help, version, newReview };
|
|
50
|
+
return { paths, newReview };
|
|
72
51
|
};
|
|
73
52
|
|
|
74
53
|
const looksLikeRev = (spec) => {
|
|
@@ -93,54 +72,84 @@ const resolveScope = (cwd, paths, repo, statSync = fs.statSync) => {
|
|
|
93
72
|
if (pathExists(cwd, first, statSync)) {
|
|
94
73
|
return { rev: null, paths };
|
|
95
74
|
}
|
|
96
|
-
const resolved = repo.resolveRev
|
|
97
|
-
if (resolved)
|
|
75
|
+
const resolved = repo.resolveRev(cwd, first);
|
|
76
|
+
if (resolved) {
|
|
77
|
+
if (paths.length > 1) {
|
|
78
|
+
throw new Error('use a commit or a path, not both');
|
|
79
|
+
}
|
|
80
|
+
return { rev: resolved, paths: [] };
|
|
81
|
+
}
|
|
98
82
|
if (looksLikeRev(first)) {
|
|
99
83
|
throw new Error(`bad revision ${first}`);
|
|
100
84
|
}
|
|
101
85
|
return { rev: null, paths };
|
|
102
86
|
};
|
|
103
87
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
const isColorEnabled = (stdout, env) => stdout.isTTY === true && !env.NO_COLOR;
|
|
89
|
+
|
|
90
|
+
const sessionOptions = (proc, deps, args, env, extra) => ({
|
|
91
|
+
cwd: deps.cwd ?? proc.cwd(),
|
|
92
|
+
stdin: proc.stdin,
|
|
93
|
+
stdout: proc.stdout,
|
|
94
|
+
color: isColorEnabled(proc.stdout, env),
|
|
95
|
+
getSize: deps.getSize,
|
|
96
|
+
startPane: 'files',
|
|
97
|
+
newReview: args.newReview,
|
|
98
|
+
audit: true,
|
|
99
|
+
...extra,
|
|
100
|
+
});
|
|
108
101
|
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return 0;
|
|
102
|
+
const startSession = (options) => {
|
|
103
|
+
const session = new Session(options);
|
|
104
|
+
session.load();
|
|
105
|
+
return session;
|
|
114
106
|
};
|
|
115
107
|
|
|
116
|
-
const
|
|
117
|
-
const stdout = proc.stdout;
|
|
118
|
-
const env = proc.env ?? {};
|
|
119
|
-
const repo = deps.repo ?? createGitRepo();
|
|
108
|
+
const openLocalSession = (proc, deps, args, selected, env) => {
|
|
120
109
|
const cwd = deps.cwd ?? proc.cwd();
|
|
110
|
+
const repo = deps.repo ?? createGitRepo();
|
|
121
111
|
const statSync = deps.statSync ?? fs.statSync;
|
|
122
|
-
const scope = resolveScope(cwd,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
112
|
+
const scope = resolveScope(cwd, selected.paths, repo, statSync);
|
|
113
|
+
return startSession(
|
|
114
|
+
sessionOptions(proc, deps, args, env, {
|
|
115
|
+
repo,
|
|
116
|
+
paths: scope.paths,
|
|
117
|
+
rev: scope.rev,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const openPullRequestSession = async (proc, deps, args, selected, env) => {
|
|
123
|
+
const cwd = deps.cwd ?? proc.cwd();
|
|
124
|
+
const loader = deps.loadPullRequest ?? loadPullRequest;
|
|
125
|
+
const token = githubToken(env);
|
|
126
|
+
const loaded = await loader(selected.pr, {
|
|
132
127
|
cwd,
|
|
133
|
-
paths,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
stdout,
|
|
137
|
-
color,
|
|
138
|
-
getSize,
|
|
139
|
-
startPane,
|
|
140
|
-
newReview,
|
|
128
|
+
paths: selected.paths,
|
|
129
|
+
token,
|
|
130
|
+
fetch: deps.fetch,
|
|
141
131
|
});
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
const change = loaded.change ?? null;
|
|
133
|
+
const repository = change ? change.repository : '';
|
|
134
|
+
const repo = deps.repo ?? createLoadedSource(loaded);
|
|
135
|
+
return startSession(
|
|
136
|
+
sessionOptions(proc, deps, args, env, {
|
|
137
|
+
repo,
|
|
138
|
+
paths: selected.paths,
|
|
139
|
+
repoName: repository,
|
|
140
|
+
sourceLabel: loaded.sourceLabel,
|
|
141
|
+
change,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const loadSession = async (proc, deps, args) => {
|
|
147
|
+
const env = proc.env ?? {};
|
|
148
|
+
const selected = selectChangeSource(args.paths);
|
|
149
|
+
if (selected.kind === 'github-pr') {
|
|
150
|
+
return openPullRequestSession(proc, deps, args, selected, env);
|
|
151
|
+
}
|
|
152
|
+
return openLocalSession(proc, deps, args, selected, env);
|
|
144
153
|
};
|
|
145
154
|
|
|
146
155
|
const run = async (proc, deps = {}) => {
|
|
@@ -150,13 +159,11 @@ const run = async (proc, deps = {}) => {
|
|
|
150
159
|
try {
|
|
151
160
|
args = parseArgv(proc.argv.slice(2));
|
|
152
161
|
} catch (error) {
|
|
153
|
-
return
|
|
162
|
+
return failUsage(stderr, error);
|
|
154
163
|
}
|
|
155
|
-
if (args.help) return writeHelp(stdout);
|
|
156
|
-
if (args.version) return writeVersion(stdout, deps);
|
|
157
164
|
let session;
|
|
158
165
|
try {
|
|
159
|
-
session = loadSession(proc, deps, args);
|
|
166
|
+
session = await loadSession(proc, deps, args);
|
|
160
167
|
} catch (error) {
|
|
161
168
|
return fail(stderr, error);
|
|
162
169
|
}
|
|
@@ -172,10 +179,8 @@ const run = async (proc, deps = {}) => {
|
|
|
172
179
|
};
|
|
173
180
|
|
|
174
181
|
module.exports = {
|
|
175
|
-
|
|
182
|
+
failLine,
|
|
176
183
|
parseArgv,
|
|
177
|
-
helpText,
|
|
178
184
|
resolveScope,
|
|
179
|
-
|
|
180
|
-
errorMessage,
|
|
185
|
+
run,
|
|
181
186
|
};
|
package/lib/clipboard.js
CHANGED