reslop 0.1.2 → 0.1.3

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,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.1.3][] - 2026-09-12
6
+
7
+ - Showed added/removed and staged/total counts in the file list
8
+ - Highlighted JSX and TSX syntax, including
9
+ - Added vim-style j/k navigation and Ctrl scrolling
10
+ - Improved TUI/UX with background git and npm checks to avoid a freeze
11
+
5
12
  ## [0.1.2][] - 2026-09-11
6
13
 
7
14
  - Review each dependency once across package.json and package-lock
@@ -26,7 +33,8 @@
26
33
  - Feedback and todos that produce a repair plan for an agent
27
34
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
28
35
 
29
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...HEAD
36
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.1.3...HEAD
37
+ [0.1.3]: https://github.com/tshemsedinov/reslop/compare/v0.1.2...v0.1.3
30
38
  [0.1.2]: https://github.com/tshemsedinov/reslop/compare/v0.1.1...v0.1.2
31
39
  [0.1.1]: https://github.com/tshemsedinov/reslop/compare/v0.1.0...v0.1.1
32
40
  [0.1.0]: https://github.com/tshemsedinov/reslop/releases/tag/v0.1.0
package/README.md CHANGED
@@ -48,23 +48,24 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
48
48
 
49
49
  ## Keys and buttons
50
50
 
51
- | Key | Action |
52
- | ----------- | --------------------------------------- |
53
- | `a` | Stage this block (`git add`) |
54
- | `u` | Unstage this block (keep worktree) |
55
- | `r` | Restore this block to the last commit |
56
- | `s` | Skip (leave unstaged, drop from review) |
57
- | `←` | Previous remaining block |
58
- | `→` | Next remaining block |
59
- | `l` | File list (git status of this scope) |
60
- | `Esc` | Quit file list or a tool |
61
- | `m` | Cycle unified / mixed / side-by-side |
62
- | `f` | Feedback on this diff block |
63
- | `t` | New todo for this file |
64
- | `⌫` / `Del` | Remove the selected todo |
65
- | `⏎` | Open the selected file |
66
- | `q` | Quit |
67
- | `?` | Help |
51
+ | Key | Action |
52
+ | ------------------- | ---------------------------------------- |
53
+ | `a` | Stage this block (`git add`) |
54
+ | `u` | Unstage this block (keep worktree) |
55
+ | `r` | Restore this block to the last commit |
56
+ | `f` | Feedback on this diff block |
57
+ | `t` | New todo for this file |
58
+ | `l` | File list (git status of this scope) |
59
+ | `m` | Cycle unified / mixed / side-by-side |
60
+ | `⏎` | Open the selected file |
61
+ | `Esc` | Quit file list or a tool |
62
+ | `→` / `j` | Next remaining block / next file |
63
+ | `←` / `k` | Previous remaining block / previous file |
64
+ | `Ctrl-e` / `Ctrl-y` | Scroll one line down / up |
65
+ | `Ctrl-f` / `Ctrl-b` | Scroll page down / up |
66
+ | `Ctrl-d` / `Ctrl-u` | Scroll half-page down / up |
67
+ | `⌫` / `Del` | Remove the selected todo |
68
+ | `q` | Quit |
68
69
 
69
70
  ## License
70
71
 
package/lib/cli.js CHANGED
@@ -99,9 +99,9 @@ const sessionOptions = (proc, deps, args, env, extra) => ({
99
99
  ...extra,
100
100
  });
101
101
 
102
- const startSession = (options) => {
102
+ const startSession = (options, extra = {}) => {
103
103
  const session = new Session(options);
104
- session.load();
104
+ if (extra.load) session.load();
105
105
  return session;
106
106
  };
107
107
 
@@ -110,13 +110,15 @@ const openLocalSession = (proc, deps, args, selected, env) => {
110
110
  const repo = deps.repo ?? createGitRepo();
111
111
  const statSync = deps.statSync ?? fs.statSync;
112
112
  const scope = resolveScope(cwd, selected.paths, repo, statSync);
113
- return startSession(
113
+ const session = startSession(
114
114
  sessionOptions(proc, deps, args, env, {
115
115
  repo,
116
116
  paths: scope.paths,
117
117
  rev: scope.rev,
118
118
  }),
119
119
  );
120
+ session.ensureRepo();
121
+ return session;
120
122
  };
121
123
 
122
124
  const openPullRequestSession = async (proc, deps, args, selected, env) => {
@@ -140,6 +142,7 @@ const openPullRequestSession = async (proc, deps, args, selected, env) => {
140
142
  sourceLabel: loaded.sourceLabel,
141
143
  change,
142
144
  }),
145
+ { load: true },
143
146
  );
144
147
  };
145
148
 
@@ -152,6 +155,13 @@ const loadSession = async (proc, deps, args) => {
152
155
  return openLocalSession(proc, deps, args, selected, env);
153
156
  };
154
157
 
158
+ const runUi = async (session, proc) => {
159
+ const code = await session.startUi();
160
+ if (session.loadError) return fail(proc.stderr, session.loadError);
161
+ if (session.emptyReview) proc.stdout.write('nothing to review\n');
162
+ return code;
163
+ };
164
+
155
165
  const run = async (proc, deps = {}) => {
156
166
  const stdout = proc.stdout;
157
167
  const stderr = proc.stderr;
@@ -167,15 +177,19 @@ const run = async (proc, deps = {}) => {
167
177
  } catch (error) {
168
178
  return fail(stderr, error);
169
179
  }
180
+ const tty = proc.stdin.isTTY === true && stdout.isTTY === true;
181
+ if (tty) return runUi(session, proc);
182
+ try {
183
+ await session.loadReady();
184
+ } catch (error) {
185
+ return fail(stderr, error);
186
+ }
170
187
  if (!session.items.length) {
171
188
  stdout.write('nothing to review\n');
172
189
  return 0;
173
190
  }
174
- if (!proc.stdin.isTTY || !stdout.isTTY) {
175
- stderr.write('reslop: interactive terminal required\n');
176
- return 1;
177
- }
178
- return session.startUi();
191
+ stderr.write('reslop: interactive terminal required\n');
192
+ return 1;
179
193
  };
180
194
 
181
195
  module.exports = {
package/lib/detect.js CHANGED
@@ -11,6 +11,8 @@ const LANG = {
11
11
  cjs: 'js',
12
12
  ts: 'ts',
13
13
  typescript: 'ts',
14
+ jsx: 'jsx',
15
+ tsx: 'tsx',
14
16
  json: 'json',
15
17
  csv: 'csv',
16
18
  html: 'html',
package/lib/files.js CHANGED
@@ -33,26 +33,50 @@ const fileStatus = (origins) => {
33
33
  return key;
34
34
  };
35
35
 
36
+ const lineDelta = ({ hunk, blockId }) => {
37
+ const delta = { added: 0, removed: 0 };
38
+ if (!hunk) return delta;
39
+ const { lines } = hunk;
40
+ if (!lines) return delta;
41
+ const scoped = typeof blockId === 'number';
42
+ for (const { type, blockId: lineBlock } of lines) {
43
+ if (scoped && lineBlock !== blockId) continue;
44
+ if (type === 'add') delta.added += 1;
45
+ else if (type === 'del') delta.removed += 1;
46
+ }
47
+ return delta;
48
+ };
49
+
36
50
  const fileEntries = (items) => {
37
51
  const groups = new Map();
38
52
  for (let i = 0; i < items.length; i++) {
39
53
  const item = items[i];
40
54
  const rel = itemPath(item);
41
- let group = groups.get(rel);
42
- if (!group) {
43
- group = { path: rel, origins: [], firstIndex: i, remaining: 0 };
44
- groups.set(rel, group);
55
+ if (!groups.has(rel)) {
56
+ groups.set(rel, {
57
+ path: rel,
58
+ origins: [],
59
+ firstIndex: i,
60
+ remaining: 0,
61
+ added: 0,
62
+ removed: 0,
63
+ staged: 0,
64
+ unstaged: 0,
65
+ });
45
66
  }
46
- group.origins.push(item.origin);
67
+ const group = groups.get(rel);
68
+ const { origin } = item;
69
+ group.origins.push(origin);
47
70
  group.remaining += 1;
71
+ const delta = lineDelta(item);
72
+ group.added += delta.added;
73
+ group.removed += delta.removed;
74
+ if (origin === 'staged') group.staged += 1;
75
+ if (origin === 'unstaged') group.unstaged += 1;
48
76
  }
49
- const entries = [];
50
- for (const group of groups.values()) {
51
- const path = group.path;
52
- const status = fileStatus(group.origins);
53
- const firstIndex = group.firstIndex;
54
- const remaining = group.remaining;
55
- entries.push({ path, status, firstIndex, remaining });
77
+ const entries = [...groups.values()];
78
+ for (const group of entries) {
79
+ group.status = fileStatus(group.origins);
56
80
  }
57
81
  return entries;
58
82
  };
package/lib/git.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { spawnSync } = require('node:child_process');
3
+ const { spawn, spawnSync } = require('node:child_process');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
 
@@ -46,17 +46,114 @@ const dropReviewItems = (items) => {
46
46
  return kept;
47
47
  };
48
48
 
49
+ const abortError = () => {
50
+ const error = new Error('aborted');
51
+ error.code = 'ABORT';
52
+ return error;
53
+ };
54
+
55
+ const isAbort = (error) => !!(error && error.code === 'ABORT');
56
+
57
+ const runProc = (cmd, args, options = {}) =>
58
+ new Promise((resolve) => {
59
+ const cwd = options.cwd;
60
+ const env = options.env;
61
+ const encoding = options.encoding ?? GIT_ENCODING;
62
+ const maxBuffer = options.maxBuffer ?? GIT_MAX_BUFFER;
63
+ const timeout = options.timeout;
64
+ const input = options.input;
65
+ const signal = options.signal;
66
+ if (signal && signal.aborted) {
67
+ resolve({ status: null, error: abortError(), stdout: '', stderr: '' });
68
+ return;
69
+ }
70
+ let stdout = '';
71
+ let stderr = '';
72
+ let settled = false;
73
+ let timer = null;
74
+ const child = spawn(cmd, args, { cwd, env });
75
+ const finish = (result) => {
76
+ if (settled) return;
77
+ settled = true;
78
+ if (timer) clearTimeout(timer);
79
+ resolve(result);
80
+ };
81
+ const onAbort = () => {
82
+ child.kill();
83
+ finish({ status: null, error: abortError(), stdout, stderr });
84
+ };
85
+ const append = (kind, chunk) => {
86
+ if (kind === 'out') stdout += chunk;
87
+ else stderr += chunk;
88
+ if (stdout.length + stderr.length <= maxBuffer) return;
89
+ child.kill();
90
+ const error = new Error('maxBuffer exceeded');
91
+ error.code = 'ENOBUFS';
92
+ finish({ status: null, error, stdout, stderr });
93
+ };
94
+ if (signal) signal.addEventListener('abort', onAbort);
95
+ if (child.stdout) {
96
+ child.stdout.setEncoding(encoding);
97
+ child.stdout.on('data', (chunk) => append('out', chunk));
98
+ }
99
+ if (child.stderr) {
100
+ child.stderr.setEncoding(encoding);
101
+ child.stderr.on('data', (chunk) => append('err', chunk));
102
+ }
103
+ if (timeout) {
104
+ timer = setTimeout(() => {
105
+ child.kill();
106
+ const error = new Error('timed out');
107
+ error.code = 'ETIMEDOUT';
108
+ finish({ status: null, error, stdout, stderr });
109
+ }, timeout);
110
+ }
111
+ child.on('error', (error) => {
112
+ finish({ status: null, error, stdout, stderr });
113
+ });
114
+ child.on('close', (status) => {
115
+ finish({ status, stdout, stderr });
116
+ });
117
+ if (child.stdin) {
118
+ try {
119
+ if (input) child.stdin.end(input);
120
+ else child.stdin.end();
121
+ } catch {
122
+ // closed
123
+ }
124
+ }
125
+ });
126
+
127
+ const gitResult = (result) => {
128
+ if (!result.error) return result;
129
+ if (isAbort(result.error)) throw result.error;
130
+ if (result.error.code === 'ENOENT') throw new Error('git not found');
131
+ throw result.error;
132
+ };
133
+
49
134
  const runGit = (args, cwd, input) => {
50
135
  const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
51
136
  const encoding = GIT_ENCODING;
52
137
  const maxBuffer = GIT_MAX_BUFFER;
53
138
  const options = { cwd, encoding, input, maxBuffer, env };
54
139
  const result = spawnSync('git', args, options);
55
- if (result.error) {
56
- if (result.error.code === 'ENOENT') throw new Error('git not found');
57
- throw result.error;
58
- }
59
- return result;
140
+ return gitResult(result);
141
+ };
142
+
143
+ const runGitAsync = (args, cwd, extra = {}) => {
144
+ const env = { ...process.env, GIT_OPTIONAL_LOCKS: '0' };
145
+ const encoding = GIT_ENCODING;
146
+ const maxBuffer = GIT_MAX_BUFFER;
147
+ const input = extra.input;
148
+ const signal = extra.signal;
149
+ return runProc('git', args, {
150
+ cwd,
151
+ env,
152
+ encoding,
153
+ maxBuffer,
154
+ input,
155
+ signal,
156
+ }).then(gitResult);
60
157
  };
61
158
 
62
159
  const toplevel = (cwd) => {
@@ -76,12 +173,16 @@ const isBinaryBuffer = (buf) => {
76
173
  return buf.includes(0);
77
174
  };
78
175
 
176
+ const namesFromLs = (stdout) => {
177
+ if (!stdout) return [];
178
+ return stdout.split('\0').filter(Boolean);
179
+ };
180
+
79
181
  const listUntracked = (top, paths) => {
80
182
  const args = ['ls-files', '-o', '--exclude-standard', '-z'];
81
183
  if (paths.length) args.push('--', ...paths);
82
184
  const result = requireOk(runGit(args, top));
83
- if (!result.stdout) return [];
84
- return result.stdout.split('\0').filter(Boolean);
185
+ return namesFromLs(result.stdout);
85
186
  };
86
187
 
87
188
  const binaryNewFile = (rel) => ({
@@ -94,9 +195,9 @@ const binaryNewFile = (rel) => ({
94
195
  hunks: [],
95
196
  });
96
197
 
97
- const untrackedItems = (top, paths) => {
198
+ const untrackedItemsFrom = (top, names) => {
98
199
  const files = [];
99
- for (const rel of listUntracked(top, paths)) {
200
+ for (const rel of names) {
100
201
  if (isReviewFile(rel)) continue;
101
202
  const full = path.join(top, rel);
102
203
  let buf;
@@ -184,6 +285,18 @@ const runNpmJson = (top, args, timeout) => {
184
285
  return result.stdout || '';
185
286
  };
186
287
 
288
+ const runNpmJsonAsync = async (top, args, timeout, signal) => {
289
+ const result = await runProc('npm', args, {
290
+ cwd: top,
291
+ encoding: GIT_ENCODING,
292
+ maxBuffer: GIT_MAX_BUFFER,
293
+ timeout,
294
+ signal,
295
+ });
296
+ if (result.error) return '';
297
+ return result.stdout || '';
298
+ };
299
+
187
300
  const collectAudit = (top) => {
188
301
  const lock = path.join(top, LOCKFILE);
189
302
  if (!fs.existsSync(lock)) return null;
@@ -200,30 +313,55 @@ const collectOutdated = (top) => {
200
313
  return parseOutdatedReport(text);
201
314
  };
202
315
 
316
+ const collectAuditAsync = async (top, extra, signal) => {
317
+ if (extra.auditMap !== undefined) return extra.auditMap;
318
+ const lock = path.join(top, LOCKFILE);
319
+ if (!fs.existsSync(lock)) return null;
320
+ const args = ['audit', '--json', '--package-lock-only'];
321
+ const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
322
+ return parseAuditReport(text);
323
+ };
324
+
325
+ const collectOutdatedAsync = async (top, extra, signal) => {
326
+ if (extra.outdatedMap !== undefined) return extra.outdatedMap;
327
+ const manifest = path.join(top, MANIFEST);
328
+ if (!fs.existsSync(manifest)) return null;
329
+ const args = ['outdated', '--json', '--long'];
330
+ const text = await runNpmJsonAsync(top, args, AUDIT_MS, signal);
331
+ return parseOutdatedReport(text);
332
+ };
333
+
203
334
  const pickNpmMap = (override, collect, top) => {
204
335
  if (override !== undefined) return override;
205
336
  return collect(top);
206
337
  };
207
338
 
339
+ const hasDepFile = (items) => {
340
+ for (const item of items) {
341
+ if (depFileMeta(itemPath(item))) return true;
342
+ }
343
+ return false;
344
+ };
345
+
208
346
  const foldLoaded = (items, top, rev, extra = {}) => {
209
347
  const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
210
- let usedNames = null;
211
- let audit = null;
212
- let outdated = null;
213
348
  const live = extra.audit === true && !rev;
214
- let hasDepDiff = false;
215
- for (const item of items) {
216
- if (!depFileMeta(itemPath(item))) continue;
217
- hasDepDiff = true;
218
- break;
219
- }
220
- if (hasDepDiff || live) usedNames = collectUsedNames(top);
221
- if (live) {
349
+ const hasDepDiff = hasDepFile(items);
350
+ const collect = extra.collect !== false;
351
+ let usedNames = extra.usedNames ?? null;
352
+ let audit = extra.auditMap;
353
+ let outdated = extra.outdatedMap;
354
+ if (collect && usedNames === null && (hasDepDiff || live)) {
355
+ usedNames = collectUsedNames(top);
356
+ }
357
+ if (collect && live) {
222
358
  audit = pickNpmMap(extra.auditMap, collectAudit, top);
223
359
  outdated = pickNpmMap(extra.outdatedMap, collectOutdated, top);
224
- } else if (extra.audit && hasDepDiff) {
360
+ } else if (collect && extra.audit && hasDepDiff) {
225
361
  audit = pickNpmMap(extra.auditMap, collectAudit, top);
226
362
  }
363
+ if (audit === undefined) audit = null;
364
+ if (outdated === undefined) outdated = null;
227
365
  const folded = foldDepItems(items, readSides, usedNames, audit, outdated, {
228
366
  root: top,
229
367
  });
@@ -408,40 +546,73 @@ const shortRev = (top, commit) => {
408
546
  return result.stdout.trim();
409
547
  };
410
548
 
411
- const loadCommit = (top, commit, extra) => {
412
- const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
413
- let args;
549
+ const commitDiffArgs = (commit, parent, pathArgs) => {
414
550
  if (parent.status === 0) {
415
551
  const from = parent.stdout.trim();
416
- args = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...extra];
417
- } else {
418
- args = [
419
- ...GIT_CONFIG,
420
- 'diff-tree',
421
- '--no-commit-id',
422
- '--root',
423
- '-p',
424
- ...DIFF_OPTS,
425
- commit,
426
- ...extra,
427
- ];
552
+ return [...GIT_CONFIG, 'diff', ...DIFF_OPTS, from, commit, ...pathArgs];
428
553
  }
554
+ return [
555
+ ...GIT_CONFIG,
556
+ 'diff-tree',
557
+ '--no-commit-id',
558
+ '--root',
559
+ '-p',
560
+ ...DIFF_OPTS,
561
+ commit,
562
+ ...pathArgs,
563
+ ];
564
+ };
565
+
566
+ const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
567
+
568
+ const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
569
+ const stagedDiff = parseDiff(stagedOut);
570
+ const unstagedDiff = parseDiff(unstagedOut);
571
+ const staged = itemsFromFiles(stagedDiff, 'staged');
572
+ const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
573
+ const untracked = untrackedItemsFrom(top, names);
574
+ const combined = mergeItems([staged, unstaged, untracked]);
575
+ const parsed = dropReviewItems(combined);
576
+ return { top, parsed };
577
+ };
578
+
579
+ const snapshotCommit = (top, commit, files) => {
580
+ const commitItems = itemsFromFiles(files, 'commit');
581
+ const parsed = dropReviewItems(commitItems);
582
+ const revShort = shortRev(top, commit);
583
+ return { top, parsed, rev: commit, revShort };
584
+ };
585
+
586
+ const loadCommit = (top, commit, extra) => {
587
+ const parent = runGit(['rev-parse', '--verify', `${commit}^`], top);
588
+ const args = commitDiffArgs(commit, parent, extra);
429
589
  const result = requireOk(runGit(args, top));
430
590
  return parseDiff(result.stdout);
431
591
  };
432
592
 
433
- const load = (cwd, paths = [], options = {}) => {
593
+ const needsExtras = (parsed, rev, extra) => {
594
+ const live = extra.audit === true && !rev;
595
+ if (live) return true;
596
+ if (!extra.audit) return false;
597
+ return hasDepFile(parsed);
598
+ };
599
+
600
+ const finishLoad = (snapshot, extra) => {
601
+ const options = { ...extra, paths: extra.paths ?? [] };
602
+ if (extra.deferExtras) options.collect = false;
603
+ const parsed = snapshot.parsed;
604
+ const top = snapshot.top;
605
+ const rev = snapshot.rev;
606
+ const items = foldLoaded(parsed, top, rev, options);
607
+ const pending = extra.deferExtras && needsExtras(parsed, rev, extra);
608
+ return { ...snapshot, items, pending };
609
+ };
610
+
611
+ const readSnapshotSync = (cwd, paths, options) => {
434
612
  const top = toplevel(cwd);
435
- const extra = paths.length ? ['--', ...paths] : [];
613
+ const extra = pathArgsOf(paths);
436
614
  const rev = options.commit;
437
- if (rev) {
438
- const files = loadCommit(top, rev, extra);
439
- const commitItems = itemsFromFiles(files, 'commit');
440
- const parsed = dropReviewItems(commitItems);
441
- const items = foldLoaded(parsed, top, rev, { ...options, paths });
442
- const revShort = shortRev(top, rev);
443
- return { top, items, rev, revShort };
444
- }
615
+ if (rev) return snapshotCommit(top, rev, loadCommit(top, rev, extra));
445
616
  const stagedArgs = [
446
617
  ...GIT_CONFIG,
447
618
  'diff',
@@ -452,15 +623,95 @@ const load = (cwd, paths = [], options = {}) => {
452
623
  const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
453
624
  const stagedRes = requireOk(runGit(stagedArgs, top));
454
625
  const unstagedRes = requireOk(runGit(unstagedArgs, top));
455
- const stagedDiff = parseDiff(stagedRes.stdout);
456
- const unstagedDiff = parseDiff(unstagedRes.stdout);
457
- const staged = itemsFromFiles(stagedDiff, 'staged');
458
- const unstaged = itemsFromFiles(unstagedDiff, 'unstaged');
459
- const untracked = untrackedItems(top, paths);
460
- const combined = mergeItems([staged, unstaged, untracked]);
461
- const merged = dropReviewItems(combined);
462
- const items = foldLoaded(merged, top, undefined, { ...options, paths });
463
- return { top, items };
626
+ const names = listUntracked(top, paths);
627
+ return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
628
+ };
629
+
630
+ const loadCommitAsync = async (top, commit, extra, signal) => {
631
+ const parent = await runGitAsync(
632
+ ['rev-parse', '--verify', `${commit}^`],
633
+ top,
634
+ { signal },
635
+ );
636
+ const args = commitDiffArgs(commit, parent, extra);
637
+ const result = requireOk(await runGitAsync(args, top, { signal }));
638
+ return parseDiff(result.stdout);
639
+ };
640
+
641
+ const readSnapshotAsync = async (cwd, paths, options) => {
642
+ const top = toplevel(cwd);
643
+ const extra = pathArgsOf(paths);
644
+ const rev = options.commit;
645
+ const signal = options.signal;
646
+ if (rev) {
647
+ const files = await loadCommitAsync(top, rev, extra, signal);
648
+ return snapshotCommit(top, rev, files);
649
+ }
650
+ const stagedArgs = [
651
+ ...GIT_CONFIG,
652
+ 'diff',
653
+ '--cached',
654
+ ...DIFF_OPTS,
655
+ ...extra,
656
+ ];
657
+ const unstagedArgs = [...GIT_CONFIG, 'diff', ...DIFF_OPTS, ...extra];
658
+ const lsArgs = ['ls-files', '-o', '--exclude-standard', '-z'];
659
+ if (paths.length) lsArgs.push('--', ...paths);
660
+ const stagedP = runGitAsync(stagedArgs, top, { signal });
661
+ const unstagedP = runGitAsync(unstagedArgs, top, { signal });
662
+ const lsP = runGitAsync(lsArgs, top, { signal });
663
+ const stagedRes = requireOk(await stagedP);
664
+ const unstagedRes = requireOk(await unstagedP);
665
+ const names = namesFromLs(requireOk(await lsP).stdout);
666
+ return snapshotWorktree(top, stagedRes.stdout, unstagedRes.stdout, names);
667
+ };
668
+
669
+ const load = (cwd, paths = [], options = {}) => {
670
+ const snapshot = readSnapshotSync(cwd, paths, options);
671
+ return finishLoad(snapshot, { ...options, paths });
672
+ };
673
+
674
+ const loadAsync = async (cwd, paths = [], options = {}) => {
675
+ const snapshot = await readSnapshotAsync(cwd, paths, options);
676
+ return finishLoad(snapshot, { ...options, paths, deferExtras: true });
677
+ };
678
+
679
+ const usedNamesPromise = (parsed, rev, extra, top) => {
680
+ const live = extra.audit === true && !rev;
681
+ if (!live && !hasDepFile(parsed)) return Promise.resolve(null);
682
+ return Promise.resolve().then(() => collectUsedNames(top));
683
+ };
684
+
685
+ const loadExtras = async (snapshot, extra = {}) => {
686
+ const top = snapshot.top;
687
+ const rev = snapshot.rev;
688
+ const parsed = snapshot.parsed;
689
+ const signal = extra.signal;
690
+ const live = extra.audit === true && !rev;
691
+ const hasDepDiff = hasDepFile(parsed);
692
+ const usedP = usedNamesPromise(parsed, rev, extra, top);
693
+ let auditP = Promise.resolve(null);
694
+ let outdatedP = Promise.resolve(null);
695
+ if (live) {
696
+ auditP = collectAuditAsync(top, extra, signal);
697
+ outdatedP = collectOutdatedAsync(top, extra, signal);
698
+ } else if (extra.audit && hasDepDiff) {
699
+ auditP = collectAuditAsync(top, extra, signal);
700
+ }
701
+ const extras = await Promise.all([usedP, auditP, outdatedP]);
702
+ const usedNames = extras[0];
703
+ const audit = extras[1];
704
+ const outdated = extras[2];
705
+ if (signal && signal.aborted) throw abortError();
706
+ const folded = foldLoaded(parsed, top, rev, {
707
+ ...extra,
708
+ paths: extra.paths ?? [],
709
+ usedNames,
710
+ auditMap: audit,
711
+ outdatedMap: outdated,
712
+ collect: false,
713
+ });
714
+ return { ...snapshot, items: folded, pending: false };
464
715
  };
465
716
 
466
717
  const addItem = (top, item) => {
@@ -594,6 +845,9 @@ const revertFile = (top, rel, items) => {
594
845
 
595
846
  const createGitRepo = () => ({
596
847
  load,
848
+ loadAsync,
849
+ loadExtras,
850
+ toplevel,
597
851
  resolveRev,
598
852
  add: addItem,
599
853
  unstage: unstageItem,
@@ -603,6 +857,8 @@ const createGitRepo = () => ({
603
857
 
604
858
  module.exports = {
605
859
  load,
860
+ loadAsync,
861
+ loadExtras,
606
862
  addItem,
607
863
  unstageItem,
608
864
  revertItem,