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 CHANGED
@@ -2,15 +2,31 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
- ## [0.1.0][] - 2026-09-09
5
+ ## [0.1.2][] - 2026-09-11
6
6
 
7
- ### Added
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
- - Interactive terminal git diff with intra-line highlighting
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
- [unreleased]: https://github.com/tshemsedinov/metadiff/compare/v0.1.0...HEAD
16
- [0.1.0]: https://github.com/tshemsedinov/metadiff/releases/tag/v0.1.0
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
- > Review diff and plan changes
3
+ > Turn generated changes into owned changes
4
4
 
5
- - Prepare specs for the agent.
6
- - Leave feedback, add todos.
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
- Walk uncommitted git changes, or a given commit. Each contiguous block of diff lines can be staged, unstaged, reverted; add review and todos. Commit review is read-only.
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/to/folder
35
- reslop path/to/file
37
+ reslop path/file
36
38
  reslop 7ac260c
37
- reslop HEAD~1 path/to/file
38
- reslop --help
39
+ reslop https://github.com/metarhia/metacom/pull/555
39
40
  ```
40
41
 
41
- Starts on the diff. No arguments, or a folder, reviews that scope.
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
- A `##` heading names the file. Todos are checkboxes under it. Feedback is a
62
- checkbox with ` - path:old:new:block` on the same line.
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` | File list, then quit |
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, errorMessage } = require('../lib/cli.js');
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(`reslop: ${errorMessage(reason)}\n`);
11
+ process.stderr.write(failLine(reason));
12
12
  process.exit(1);
13
13
  };
14
14
 
package/lib/ansi.js CHANGED
@@ -217,7 +217,6 @@ module.exports = {
217
217
  EL,
218
218
  THEME,
219
219
  CODE_FG,
220
- LOG_COL_FG,
221
220
  codeFg,
222
221
  fg,
223
222
  bg,
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(`reslop: ${errorMessage(reason)}\n`);
27
+ stderr.write(failLine(reason));
32
28
  return 1;
33
29
  };
34
30
 
35
- const helpText = () => {
36
- const lines = [
37
- 'Usage: reslop [-n] [commit] [path...]',
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
- const flags = { help: false, version: false, newReview: false };
39
+ let newReview = false;
57
40
  for (const arg of argv) {
58
- const flag = FLAGS[arg];
59
- if (flag) {
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
- const help = flags.help;
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 ? repo.resolveRev(cwd, first) : null;
97
- if (resolved) return { rev: resolved, paths: paths.slice(1) };
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 writeHelp = (stdout) => {
105
- stdout.write(helpText());
106
- return 0;
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 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;
102
+ const startSession = (options) => {
103
+ const session = new Session(options);
104
+ session.load();
105
+ return session;
114
106
  };
115
107
 
116
- const loadSession = (proc, deps, args) => {
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, 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
- const session = new Session({
131
- repo,
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
- rev,
135
- stdin,
136
- stdout,
137
- color,
138
- getSize,
139
- startPane,
140
- newReview,
128
+ paths: selected.paths,
129
+ token,
130
+ fetch: deps.fetch,
141
131
  });
142
- session.load();
143
- return session;
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 fail(stderr, error);
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
- run,
182
+ failLine,
176
183
  parseArgv,
177
- helpText,
178
184
  resolveScope,
179
- looksLikeRev,
180
- errorMessage,
185
+ run,
181
186
  };
package/lib/clipboard.js CHANGED
@@ -39,4 +39,4 @@ const copyText = (text, stdout) => {
39
39
  return ok;
40
40
  };
41
41
 
42
- module.exports = { osc52, copyText, runClip };
42
+ module.exports = { osc52, copyText };