reslop 0.2.6 → 0.3.0

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.
Files changed (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/dashboard/blocks.js +490 -0
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +19 -61
  72. package/lib/render/tiles.js +203 -47
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +166 -59
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +72 -59
  126. package/lib/session/workspace.js +416 -0
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -4,12 +4,9 @@ const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
6
 
7
- const utilities = require('./utilities.js');
8
- const { oneLine } = utilities;
9
- const files = require('./files.js');
10
- const { shortAge } = files;
11
- const worktree = require('./git-worktree.js');
12
- const { requireOk, runGit, command, gitOp } = worktree;
7
+ const { oneLine, trimText } = require('../common/utilities.js');
8
+ const { shortAge } = require('../common/format.js');
9
+ const { requireOk, runGit, command, gitOp } = require('./worktree.js');
13
10
 
14
11
  const BRANCH_FORMAT = [
15
12
  '%(HEAD)',
@@ -43,9 +40,7 @@ const AUTOSQUASH_ARGS = [
43
40
  '--interactive',
44
41
  '--autosquash',
45
42
  ];
46
- const REWORD_TODO = path.join(__dirname, 'git-reword-todo.js');
47
-
48
- const trimText = (value) => `${value ?? ''}`.trim();
43
+ const REWORD_TODO = path.join(__dirname, 'reword-todo.js');
49
44
 
50
45
  const required = (value, message) => {
51
46
  const text = trimText(value);
@@ -60,9 +55,9 @@ const messageBody = (message) => {
60
55
  return text.endsWith('\n') ? text : `${text}\n`;
61
56
  };
62
57
 
63
- const commitMessage = (top, sha) => {
58
+ const commitMessage = async (top, sha) => {
64
59
  const spec = trimText(sha) || 'HEAD';
65
- const result = runGit(['log', '-1', '--format=%B', spec], top);
60
+ const result = await runGit(['log', '-1', '--format=%B', spec], top);
66
61
  return result.status === 0 ? result.stdout.trimEnd() : '';
67
62
  };
68
63
 
@@ -112,7 +107,7 @@ const statCount = (stat, word) => {
112
107
  return match ? parseInt(match[1], 10) : 0;
113
108
  };
114
109
 
115
- const listCommits = (top) => {
110
+ const listCommits = async (top) => {
116
111
  const args = [
117
112
  'log',
118
113
  '-z',
@@ -120,7 +115,7 @@ const listCommits = (top) => {
120
115
  `--format=${COMMIT_FORMAT}`,
121
116
  `--max-count=${COMMIT_LIMIT}`,
122
117
  ];
123
- const result = requireOk(runGit(args, top), 'log failed');
118
+ const result = requireOk(await runGit(args, top), 'log failed');
124
119
  const commits = [];
125
120
  for (const chunk of result.stdout.split('\0')) {
126
121
  const stat = STAT_LEAD.exec(chunk);
@@ -141,11 +136,10 @@ const parseTrack = (text) => {
141
136
  const raw = trimText(text).replace(/^\[|\]$/g, '');
142
137
  const ahead = /ahead (\d+)/.exec(raw);
143
138
  const behind = /behind (\d+)/.exec(raw);
144
- return {
145
- ahead: ahead ? Number(ahead[1]) : 0,
146
- behind: behind ? Number(behind[1]) : 0,
147
- gone: raw === 'gone',
148
- };
139
+ const aheadCount = ahead ? Number(ahead[1]) : 0;
140
+ const behindCount = behind ? Number(behind[1]) : 0;
141
+ const gone = raw === 'gone';
142
+ return { ahead: aheadCount, behind: behindCount, gone };
149
143
  };
150
144
 
151
145
  const parseBranchLine = (line) => {
@@ -163,31 +157,33 @@ const parseBranchLine = (line) => {
163
157
  };
164
158
  };
165
159
 
166
- const defaultBranch = (top, names) => {
167
- const result = runGit(['symbolic-ref', '--short', ORIGIN_HEAD], top);
160
+ const defaultBranch = async (top, names) => {
161
+ const args = ['symbolic-ref', '--short', ORIGIN_HEAD];
162
+ const result = await runGit(args, top);
168
163
  const ref = result.status === 0 ? result.stdout.trim() : '';
169
- const remote = ref.startsWith(ORIGIN_PREFIX)
170
- ? ref.slice(ORIGIN_PREFIX.length)
171
- : ref;
164
+ let remote = ref;
165
+ if (ref.startsWith(ORIGIN_PREFIX)) {
166
+ remote = ref.slice(ORIGIN_PREFIX.length);
167
+ }
172
168
  if (names.has(remote)) return remote;
173
169
  return DEFAULT_BRANCHES.find((name) => names.has(name)) ?? '';
174
170
  };
175
171
 
176
- const listBranches = (top) => {
172
+ const listBranches = async (top) => {
177
173
  const args = ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'];
178
- const result = requireOk(runGit(args, top), 'branch failed');
174
+ const result = requireOk(await runGit(args, top), 'branch failed');
179
175
  const lines = result.stdout.split('\n').filter(Boolean);
180
176
  const branches = lines.map((line) => parseBranchLine(line)).filter(Boolean);
181
177
  const names = new Set(branches.map((entry) => entry.name));
182
- const primary = defaultBranch(top, names);
178
+ const primary = await defaultBranch(top, names);
183
179
  return branches.map((entry) => ({
184
180
  ...entry,
185
181
  isDefault: entry.name === primary,
186
182
  }));
187
183
  };
188
184
 
189
- const hasStaged = (top) => {
190
- const result = runGit(['diff', '--cached', '--quiet'], top);
185
+ const hasStaged = async (top) => {
186
+ const result = await runGit(['diff', '--cached', '--quiet'], top);
191
187
  return result.status === 1;
192
188
  };
193
189
 
@@ -243,6 +239,24 @@ const commitOp = gitOp(function* (kind, message) {
243
239
  requireOk(result);
244
240
  });
245
241
 
242
+ const updateOp = gitOp(function* (sha) {
243
+ const commit = yield* resolveCommit(required(sha, 'empty commit'));
244
+ const head = yield* resolveCommit('HEAD');
245
+ if (commit === head) {
246
+ const result = yield command(['commit', '--amend', '--no-edit']);
247
+ return void requireOk(result);
248
+ }
249
+ const fixed = yield command(['commit', '--fixup', commit]);
250
+ requireOk(fixed);
251
+ const range = yield* rebaseRange(commit);
252
+ try {
253
+ yield* rebase([...AUTOSQUASH_ARGS, ...range]);
254
+ } catch (error) {
255
+ yield command(['reset', '--soft', 'HEAD~1']);
256
+ throw error;
257
+ }
258
+ });
259
+
246
260
  const dropCommitOp = gitOp(function* (sha) {
247
261
  const target = yield* resolveCommit(required(sha, 'empty commit'));
248
262
  const parent = yield command(['rev-parse', '--verify', `${target}^`]);
@@ -363,24 +377,15 @@ module.exports = {
363
377
  listCommits,
364
378
  listBranches,
365
379
  hasStaged,
366
- checkoutBranch: checkoutOp.sync,
367
- checkoutBranchAsync: checkoutOp.async,
368
- createBranch: createOp.sync,
369
- createBranchAsync: createOp.async,
370
- rebaseBranch: rebaseOp.sync,
371
- rebaseBranchAsync: rebaseOp.async,
372
- dropBranch: dropBranchOp.sync,
373
- dropBranchAsync: dropBranchOp.async,
374
- dropCommit: dropCommitOp.sync,
375
- dropCommitAsync: dropCommitOp.async,
376
- applyFixup: fixupOp.sync,
377
- applyFixupAsync: fixupOp.async,
378
- rewordCommit: rewordOp.sync,
379
- rewordCommitAsync: rewordOp.async,
380
- pullChanges: pullOp.sync,
381
- pullChangesAsync: pullOp.async,
382
- pushChanges: pushOp.sync,
383
- pushChangesAsync: pushOp.async,
384
- commitChanges: commitOp.sync,
385
- commitChangesAsync: commitOp.async,
380
+ checkoutBranch: checkoutOp,
381
+ createBranch: createOp,
382
+ rebaseBranch: rebaseOp,
383
+ dropBranch: dropBranchOp,
384
+ dropCommit: dropCommitOp,
385
+ applyFixup: fixupOp,
386
+ rewordCommit: rewordOp,
387
+ pullChanges: pullOp,
388
+ pushChanges: pushOp,
389
+ commitChanges: commitOp,
390
+ updateCommit: updateOp,
386
391
  };
@@ -3,22 +3,28 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const deps = require('./deps.js');
6
+ const deps = require('../deps/deps.js');
7
7
  const { foldDepItems, mergeProposedItems } = deps;
8
8
  const { parseAuditReport, parseOutdatedReport } = deps;
9
9
  const { collectUsedNames, collectExportMeta } = deps;
10
- const manifest = require('./manifest.js');
10
+ const { collectGroups } = require('../deps/diff.js');
11
+ const manifest = require('../deps/manifest.js');
11
12
  const { MANIFEST, LOCKFILE, depFileMeta, mergeDepFile, relOf } = manifest;
12
- const files = require('./files.js');
13
- const { itemPath, isPathInScope } = files;
14
- const utilities = require('./utilities.js');
15
- const { IS_WIN, abortError, npmBin, oneLine, runProc, runProcSync } = utilities;
16
- const worktree = require('./git-worktree.js');
17
- const { requireOk, runGit, gitText, worktreeText, readDepSides } = worktree;
13
+ const { itemPath, isPathInScope } = require('../common/files.js');
14
+ const utilities = require('../common/utilities.js');
15
+ const {
16
+ INSTALL_MS,
17
+ IS_WIN,
18
+ abortError,
19
+ npmBin,
20
+ runProc,
21
+ } = require('../common/process.js');
22
+ const { oneLine } = utilities;
23
+ const worktree = require('./worktree.js');
24
+ const { gitOk, indexedText, worktreeText, readDepSides } = worktree;
18
25
  const { revertOnePath, groupByPath, writeFile, writeIndex } = worktree;
19
26
 
20
27
  const AUDIT_MS = 15000;
21
- const INSTALL_MS = 120000;
22
28
 
23
29
  const NPM_REPORT = {
24
30
  audit: {
@@ -41,21 +47,11 @@ const npmOptions = (cwd, timeout, signal) => ({
41
47
  });
42
48
 
43
49
  const runNpm = (cwd, args) =>
44
- runProcSync(npmBin(), args, npmOptions(cwd, INSTALL_MS));
45
-
46
- const runNpmAsync = (cwd, args) =>
47
50
  runProc(npmBin(), args, npmOptions(cwd, INSTALL_MS));
48
51
 
49
52
  const npmOutput = (result) => (result.error ? '' : result.stdout);
50
53
 
51
- const collectReport = (top, kind) => {
52
- const report = NPM_REPORT[kind];
53
- if (!fs.existsSync(path.join(top, report.file))) return null;
54
- const options = npmOptions(top, AUDIT_MS);
55
- return report.parse(npmOutput(runProcSync(npmBin(), report.args, options)));
56
- };
57
-
58
- const collectReportAsync = async (top, kind, signal) => {
54
+ const collectReport = async (top, kind, signal) => {
59
55
  const report = NPM_REPORT[kind];
60
56
  if (!fs.existsSync(path.join(top, report.file))) return null;
61
57
  const options = npmOptions(top, AUDIT_MS, signal);
@@ -67,13 +63,16 @@ const depPaths = (item) => item.dep?.files ?? [];
67
63
 
68
64
  const depChange = (item) => item.dep?.change ?? null;
69
65
 
70
- const hasProposed = (item) => !!depChange(item)?.propose;
71
-
72
- const isStagedOrCommit = (item) =>
73
- item.origin === 'staged' || item.origin === 'commit';
66
+ const isStagedOrCommit = (item) => {
67
+ const staged = item.origin === 'staged';
68
+ const commit = item.origin === 'commit';
69
+ return staged || commit;
70
+ };
74
71
 
75
- const firstDepRel = (rels, kind) =>
76
- rels.find((rel) => depFileMeta(rel)?.kind === kind) ?? '';
72
+ const firstDepRel = (rels, kind) => {
73
+ const match = (rel) => depFileMeta(rel)?.kind === kind;
74
+ return rels.find(match) ?? '';
75
+ };
77
76
 
78
77
  const proposedNpmPlan = (top, item) => {
79
78
  const change = depChange(item);
@@ -118,8 +117,25 @@ const extrasNeeds = (items, rev, extra) => {
118
117
  return { live, scan: live || hasDepDiff, audit };
119
118
  };
120
119
 
121
- const foldLoaded = (items, top, rev, extra) => {
122
- const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
120
+ const NO_SIDES = { oldText: '', newText: '' };
121
+
122
+ const sidesReader = async (items, top, rev) => {
123
+ const sides = new Map();
124
+ const reads = [];
125
+ for (const group of collectGroups(items).values()) {
126
+ for (const name of [MANIFEST, LOCKFILE]) {
127
+ const rel = relOf(group.dir, name);
128
+ const key = `${group.origin}\0${rel}`;
129
+ const keep = (found) => sides.set(key, found);
130
+ reads.push(readDepSides(top, rev, group.origin, rel).then(keep));
131
+ }
132
+ }
133
+ await Promise.all(reads);
134
+ return (origin, rel) => sides.get(`${origin}\0${rel}`) ?? NO_SIDES;
135
+ };
136
+
137
+ const foldLoaded = async (items, top, rev, extra) => {
138
+ const readSides = await sidesReader(items, top, rev);
123
139
  const needs = extrasNeeds(items, rev, extra);
124
140
  const collect = extra.collect !== false;
125
141
  const scan = collect && needs.scan;
@@ -129,10 +145,10 @@ const foldLoaded = (items, top, rev, extra) => {
129
145
  exportEntries = collectExportMeta(top);
130
146
  }
131
147
  if (collect && needs.audit && audit === undefined) {
132
- audit = collectReport(top, 'audit');
148
+ audit = await collectReport(top, 'audit', extra.signal);
133
149
  }
134
150
  if (collect && needs.live && outdated === undefined) {
135
- outdated = collectReport(top, 'outdated');
151
+ outdated = await collectReport(top, 'outdated', extra.signal);
136
152
  }
137
153
  audit ??= null;
138
154
  outdated ??= null;
@@ -141,16 +157,16 @@ const foldLoaded = (items, top, rev, extra) => {
141
157
  exportEntries,
142
158
  });
143
159
  if (!needs.live || !isPathInScope(MANIFEST, extra.paths)) return folded;
144
- const pkgText = worktreeText(top, MANIFEST);
145
- const lockText = worktreeText(top, LOCKFILE);
160
+ const texts = [worktreeText(top, MANIFEST), worktreeText(top, LOCKFILE)];
161
+ const [pkgText, lockText] = await Promise.all(texts);
146
162
  const options = { lockText, usedNames, root: top, exportEntries };
147
163
  return mergeProposedItems(folded, pkgText, outdated, audit, options);
148
164
  };
149
165
 
150
- const finishLoad = (snapshot, extra) => {
166
+ const finishLoad = async (snapshot, extra) => {
151
167
  const { top, rev, parsed } = snapshot;
152
168
  const options = { ...extra, collect: !extra.deferExtras };
153
- const items = foldLoaded(parsed, top, rev, options);
169
+ const items = await foldLoaded(parsed, top, rev, options);
154
170
  const pending = extra.deferExtras && extrasNeeds(parsed, rev, extra).audit;
155
171
  return { ...snapshot, items, pending };
156
172
  };
@@ -167,21 +183,23 @@ const loadExtras = async (snapshot, extra = {}) => {
167
183
  const { signal } = extra;
168
184
  const needs = extrasNeeds(parsed, rev, extra);
169
185
  const known = extra.exportEntries;
186
+ let exportMeta = known;
187
+ if (known === undefined && needs.scan) {
188
+ exportMeta = later(() => collectExportMeta(top));
189
+ }
170
190
  const [usedNames, audit, outdated, exportEntries] = await Promise.all([
171
191
  needs.scan ? later(() => collectUsedNames(top)) : null,
172
192
  reportTask(needs.audit, extra.auditMap, () =>
173
- collectReportAsync(top, 'audit', signal),
193
+ collectReport(top, 'audit', signal),
174
194
  ),
175
195
  reportTask(needs.live, extra.outdatedMap, () =>
176
- collectReportAsync(top, 'outdated', signal),
196
+ collectReport(top, 'outdated', signal),
177
197
  ),
178
- known === undefined && needs.scan
179
- ? later(() => collectExportMeta(top))
180
- : known,
198
+ exportMeta,
181
199
  ]);
182
200
  if (signal && signal.aborted) throw abortError();
183
201
  const found = { usedNames, auditMap: audit, outdatedMap: outdated };
184
- const items = foldLoaded(parsed, top, rev, {
202
+ const items = await foldLoaded(parsed, top, rev, {
185
203
  ...extra,
186
204
  ...found,
187
205
  paths: extra.paths ?? [],
@@ -207,23 +225,23 @@ const restoreFile = (top, saved) => {
207
225
  else fs.rmSync(abs, { force: true });
208
226
  };
209
227
 
210
- const mergeLive = (top, rel, item, base, side) => {
211
- const sides = readDepSides(top, null, item.origin, rel);
228
+ const mergeLive = async (top, rel, item, base, side) => {
229
+ const sides = await readDepSides(top, null, item.origin, rel);
212
230
  const { kind } = depFileMeta(rel);
213
231
  const change = depChange(item);
214
232
  return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
215
233
  };
216
234
 
217
- const applyDepIndex = (top, rel, item, side) => {
235
+ const applyDepIndex = async (top, rel, item, side) => {
218
236
  if (!depFileMeta(rel)) return;
219
- const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
220
- writeIndex(top, rel, mergeLive(top, rel, item, indexed, side));
237
+ const indexed = await indexedText(top, rel);
238
+ await writeIndex(top, rel, await mergeLive(top, rel, item, indexed, side));
221
239
  };
222
240
 
223
- const applyDepWorktree = (top, rel, item, side) => {
241
+ const applyDepWorktree = async (top, rel, item, side) => {
224
242
  if (!depFileMeta(rel)) return;
225
- const text = mergeLive(top, rel, item, worktreeText(top, rel), side);
226
- writeFile(top, rel, text);
243
+ const live = await worktreeText(top, rel);
244
+ await writeFile(top, rel, await mergeLive(top, rel, item, live, side));
227
245
  };
228
246
 
229
247
  const npmError = (result, fail) => {
@@ -232,12 +250,12 @@ const npmError = (result, fail) => {
232
250
  return new Error(oneLine(msg || fail, fail));
233
251
  };
234
252
 
235
- const stagePlan = (top, plan) => {
253
+ const stagePlan = async (top, plan) => {
236
254
  const toAdd = [];
237
255
  if (!plan.lockOnly && plan.manifest) toAdd.push(plan.manifest);
238
256
  if (fs.existsSync(path.join(top, plan.lockPath))) toAdd.push(plan.lockPath);
239
257
  if (!toAdd.length) return;
240
- requireOk(runGit(['add', '--', ...toAdd], top));
258
+ await gitOk(['add', '--', ...toAdd], top);
241
259
  };
242
260
 
243
261
  const startPlan = (top, item) => {
@@ -248,104 +266,115 @@ const startPlan = (top, item) => {
248
266
  return { plan, owned };
249
267
  };
250
268
 
251
- const finishPlan = (top, plan, result, stage = stagePlan) => {
269
+ const finishPlan = async (top, plan, result, stage = stagePlan) => {
252
270
  const failed = result && (result.error || result.status !== 0);
253
271
  if (failed) throw npmError(result, plan.fail);
254
- stage(top, plan);
272
+ await stage(top, plan);
255
273
  };
256
274
 
257
275
  const restoreOwned = (top, owned) => {
258
276
  for (const saved of owned) restoreFile(top, saved);
259
277
  };
260
278
 
261
- const applyProposedUpdate = (top, item, extra = {}) => {
262
- const started = startPlan(top, item);
263
- if (!started) return;
264
- const { plan, owned } = started;
265
- try {
266
- if (plan.prepareManifest) applyDepWorktree(top, plan.manifest, item, 'new');
267
- const install = extra.run ?? plan.runner ?? runNpm;
268
- finishPlan(top, plan, install(plan.cwd, plan.npmArgs), extra.stage);
269
- } catch (error) {
270
- restoreOwned(top, owned);
271
- throw error;
272
- }
273
- };
279
+ const unlocked = (write) => write();
274
280
 
275
- const applyProposedUpdateAsync = async (top, item) => {
281
+ // The install can run for minutes, so only the file and index steps around
282
+ // it go through the caller's write lock
283
+ const applyProposedUpdate = async (top, item, extra = {}) => {
276
284
  const started = startPlan(top, item);
277
285
  if (!started) return;
278
286
  const { plan, owned } = started;
287
+ const locked = extra.locked ?? unlocked;
279
288
  try {
280
- if (plan.prepareManifest) applyDepWorktree(top, plan.manifest, item, 'new');
281
- const install = plan.runner ?? runNpmAsync;
282
- finishPlan(top, plan, await install(plan.cwd, plan.npmArgs));
289
+ if (plan.prepareManifest) {
290
+ await locked(() => applyDepWorktree(top, plan.manifest, item, 'new'));
291
+ }
292
+ const install = extra.run ?? plan.runner ?? runNpm;
293
+ const result = await install(plan.cwd, plan.npmArgs);
294
+ await locked(() => finishPlan(top, plan, result, extra.stage));
283
295
  } catch (error) {
284
296
  restoreOwned(top, owned);
285
297
  throw error;
286
298
  }
287
299
  };
288
300
 
289
- const applyDepChange = (top, item, mode) => {
301
+ const applyDepChange = async (top, item, mode, extra) => {
290
302
  const change = depChange(item);
291
303
  if (change.propose) {
292
- if (mode === 'add') return void applyProposedUpdate(top, item);
304
+ if (mode === 'add') {
305
+ await applyProposedUpdate(top, item, extra);
306
+ return;
307
+ }
293
308
  if (!change.unused) return;
294
309
  }
295
310
  for (const rel of depPaths(item)) {
296
311
  if (mode !== 'revert') {
297
- applyDepIndex(top, rel, item, mode === 'add' ? 'new' : 'old');
312
+ await applyDepIndex(top, rel, item, mode === 'add' ? 'new' : 'old');
298
313
  continue;
299
314
  }
300
- if (item.origin === 'staged') applyDepIndex(top, rel, item, 'old');
301
- applyDepWorktree(top, rel, item, 'old');
315
+ if (item.origin === 'staged') await applyDepIndex(top, rel, item, 'old');
316
+ await applyDepWorktree(top, rel, item, 'old');
302
317
  }
303
318
  };
304
319
 
305
- const addItem = (top, item) => {
306
- if (!item.dep) return void worktree.addItem(top, item);
320
+ const addItem = async (top, item, extra) => {
321
+ if (!item.dep) {
322
+ await worktree.addItem(top, item);
323
+ return;
324
+ }
307
325
  if (isStagedOrCommit(item)) return;
308
- if (depChange(item)) return void applyDepChange(top, item, 'add');
326
+ if (depChange(item)) {
327
+ await applyDepChange(top, item, 'add', extra);
328
+ return;
329
+ }
309
330
  const rels = depPaths(item);
310
- if (rels.length) requireOk(runGit(['add', '--', ...rels], top));
331
+ if (rels.length) await gitOk(['add', '--', ...rels], top);
311
332
  };
312
333
 
313
- const addItemAsync = async (top, item) => {
314
- if (isStagedOrCommit(item) || !hasProposed(item)) {
315
- return void addItem(top, item);
334
+ const unstageItem = async (top, item) => {
335
+ if (!item.dep) {
336
+ await worktree.unstageItem(top, item);
337
+ return;
316
338
  }
317
- await applyProposedUpdateAsync(top, item);
318
- };
319
-
320
- const unstageItem = (top, item) => {
321
- if (!item.dep) return void worktree.unstageItem(top, item);
322
339
  if (item.origin !== 'staged') return;
323
- if (depChange(item)) return void applyDepChange(top, item, 'unstage');
340
+ if (depChange(item)) {
341
+ await applyDepChange(top, item, 'unstage');
342
+ return;
343
+ }
324
344
  const rels = depPaths(item);
325
345
  if (!rels.length) return;
326
- requireOk(runGit(['restore', '--staged', '--', ...rels], top));
346
+ await gitOk(['restore', '--staged', '--', ...rels], top);
327
347
  };
328
348
 
329
- const revertItem = (top, item) => {
349
+ const revertItem = async (top, item) => {
330
350
  if (item.origin === 'commit') return;
331
- if (!item.dep) return void worktree.revertItem(top, item);
332
- if (depChange(item)) return void applyDepChange(top, item, 'revert');
351
+ if (!item.dep) {
352
+ await worktree.revertItem(top, item);
353
+ return;
354
+ }
355
+ if (depChange(item)) {
356
+ await applyDepChange(top, item, 'revert');
357
+ return;
358
+ }
333
359
  const byPath = groupByPath(item.dep.items ?? []);
334
- for (const rel of byPath.keys()) revertOnePath(top, rel, byPath.get(rel));
360
+ for (const rel of byPath.keys()) {
361
+ await revertOnePath(top, rel, byPath.get(rel));
362
+ }
335
363
  };
336
364
 
337
- const revertFile = (top, rel, items) => {
365
+ const revertFile = async (top, rel, items) => {
338
366
  const rels = new Set([rel]);
339
367
  for (const item of items) {
340
368
  for (const file of depPaths(item)) rels.add(file);
341
369
  }
342
370
  for (const file of rels) {
343
371
  const group = itemsForPath(file, items);
344
- revertOnePath(top, file, group.length ? group : items);
372
+ await revertOnePath(top, file, group.length ? group : items);
345
373
  }
346
374
  };
347
375
 
348
376
  module.exports = {
377
+ collectReport,
349
378
  finishLoad,
350
379
  loadExtras,
351
380
  captureFile,
@@ -353,7 +382,6 @@ module.exports = {
353
382
  proposedNpmPlan,
354
383
  applyProposedUpdate,
355
384
  addItem,
356
- addItemAsync,
357
385
  unstageItem,
358
386
  revertItem,
359
387
  revertFile,
package/lib/git/git.js ADDED
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ capabilitiesFor,
5
+ attachCapabilities,
6
+ } = require('../source/capabilities.js');
7
+ const worktree = require('./worktree.js');
8
+ const branches = require('./branches.js');
9
+ const gitDeps = require('./deps.js');
10
+
11
+ const load = async (cwd, paths = [], options = {}) => {
12
+ const snapshot = await worktree.readSnapshot(cwd, paths, options);
13
+ const deferExtras = options.deferExtras ?? true;
14
+ return gitDeps.finishLoad(snapshot, { ...options, paths, deferExtras });
15
+ };
16
+
17
+ const REPO_METHODS = {
18
+ load,
19
+ loadExtras: gitDeps.loadExtras,
20
+ toplevel: worktree.toplevel,
21
+ resolveRev: worktree.resolveRev,
22
+ listFiles: worktree.listScopeFiles,
23
+ fileText: worktree.fileText,
24
+ add: gitDeps.addItem,
25
+ unstage: gitDeps.unstageItem,
26
+ revert: gitDeps.revertItem,
27
+ revertFile: gitDeps.revertFile,
28
+ edit: worktree.editItem,
29
+ writeFile: worktree.writeFile,
30
+ stagePath: worktree.stagePath,
31
+ commit: branches.commitChanges,
32
+ updateCommit: branches.updateCommit,
33
+ hasStaged: branches.hasStaged,
34
+ lastMessage: branches.lastMessage,
35
+ commitMessage: branches.commitMessage,
36
+ listBranches: branches.listBranches,
37
+ listCommits: branches.listCommits,
38
+ currentBranch: worktree.currentBranch,
39
+ checkout: branches.checkoutBranch,
40
+ createBranch: branches.createBranch,
41
+ rebase: branches.rebaseBranch,
42
+ drop: branches.dropBranch,
43
+ dropCommit: branches.dropCommit,
44
+ applyFixup: branches.applyFixup,
45
+ reword: branches.rewordCommit,
46
+ pull: branches.pullChanges,
47
+ push: branches.pushChanges,
48
+ };
49
+
50
+ const createGitRepo = () =>
51
+ attachCapabilities(REPO_METHODS, capabilitiesFor('local'));
52
+
53
+ const listed = createGitRepo();
54
+ const unlisted = Object.keys(REPO_METHODS).filter(
55
+ (name) => !Object.hasOwn(listed, name),
56
+ );
57
+ if (unlisted.length) {
58
+ throw new Error(`git methods without a capability: ${unlisted.join(', ')}`);
59
+ }
60
+
61
+ module.exports = {
62
+ ...branches,
63
+ currentBranch: worktree.currentBranch,
64
+ editItem: worktree.editItem,
65
+ load,
66
+ loadExtras: gitDeps.loadExtras,
67
+ addItem: gitDeps.addItem,
68
+ unstageItem: gitDeps.unstageItem,
69
+ revertItem: gitDeps.revertItem,
70
+ createGitRepo,
71
+ };