reslop 0.2.5 → 0.2.7

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
package/lib/git-deps.js CHANGED
@@ -4,162 +4,121 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
6
  const deps = require('./deps.js');
7
- const { foldDepItems, mergeDepFile, depFileMeta } = deps;
7
+ const { foldDepItems, mergeProposedItems } = deps;
8
8
  const { parseAuditReport, parseOutdatedReport } = deps;
9
- const { mergeProposedItems, MANIFEST, LOCKFILE } = deps;
10
9
  const { collectUsedNames, collectExportMeta } = deps;
10
+ const manifest = require('./manifest.js');
11
+ const { MANIFEST, LOCKFILE, depFileMeta, mergeDepFile, relOf } = manifest;
11
12
  const files = require('./files.js');
12
13
  const { itemPath, isPathInScope } = files;
13
- const proc = require('./utilities.js');
14
- const { abortError, npmBin, npmOpts, runProc, runProcSync } = proc;
15
- const git = require('./git-worktree.js');
16
- const { oneLine, requireOk, runGit } = git;
17
- const { gitText, worktreeText, readDepSides, revertOnePath } = git;
18
- const { writeFile, writeIndex } = git;
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;
18
+ const { revertOnePath, groupByPath, writeFile, writeIndex } = worktree;
19
19
 
20
20
  const AUDIT_MS = 15000;
21
21
  const INSTALL_MS = 120000;
22
22
 
23
- const npmProcOptions = (cwd, extra = {}) => ({
23
+ const NPM_REPORT = {
24
+ audit: {
25
+ file: LOCKFILE,
26
+ args: ['audit', '--json', '--package-lock-only'],
27
+ parse: parseAuditReport,
28
+ },
29
+ outdated: {
30
+ file: MANIFEST,
31
+ args: ['outdated', '--json', '--long'],
32
+ parse: parseOutdatedReport,
33
+ },
34
+ };
35
+
36
+ const npmOptions = (cwd, timeout, signal) => ({
24
37
  cwd,
25
- encoding: proc.DEFAULT_ENCODING,
26
- maxBuffer: proc.DEFAULT_MAX_BUFFER,
27
- timeout: extra.timeout,
28
- signal: extra.signal,
29
- shell: npmOpts().shell,
38
+ timeout,
39
+ signal,
40
+ shell: IS_WIN,
30
41
  });
31
42
 
32
43
  const runNpm = (cwd, args) =>
33
- runProcSync(npmBin(), args, npmProcOptions(cwd, { timeout: INSTALL_MS }));
44
+ runProcSync(npmBin(), args, npmOptions(cwd, INSTALL_MS));
34
45
 
35
- const runNpmAsync = (cwd, args, extra = {}) => {
36
- const bin = npmBin();
37
- const signal = extra.signal;
38
- const options = npmProcOptions(cwd, { timeout: INSTALL_MS, signal });
39
- return runProc(bin, args, options);
40
- };
46
+ const runNpmAsync = (cwd, args) =>
47
+ runProc(npmBin(), args, npmOptions(cwd, INSTALL_MS));
41
48
 
42
- const runNpmJson = (top, args, timeout) => {
43
- const result = runProcSync(npmBin(), args, npmProcOptions(top, { timeout }));
44
- if (result.error) return '';
45
- return result.stdout || '';
49
+ const npmOutput = (result) => (result.error ? '' : result.stdout);
50
+
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)));
46
56
  };
47
57
 
48
- const runNpmJsonAsync = async (top, args, timeout, signal) => {
49
- const result = await runProc(
50
- npmBin(),
51
- args,
52
- npmProcOptions(top, { timeout, signal }),
53
- );
54
- if (result.error) return '';
55
- return result.stdout || '';
58
+ const collectReportAsync = async (top, kind, signal) => {
59
+ const report = NPM_REPORT[kind];
60
+ if (!fs.existsSync(path.join(top, report.file))) return null;
61
+ const options = npmOptions(top, AUDIT_MS, signal);
62
+ const result = await runProc(npmBin(), report.args, options);
63
+ return report.parse(npmOutput(result));
56
64
  };
57
65
 
58
66
  const depPaths = (item) => item.dep?.files ?? [];
59
67
 
60
- const origDepItems = (item) => item.dep?.items ?? [];
61
-
62
68
  const depChange = (item) => item.dep?.change ?? null;
63
69
 
64
- const firstDepRel = (files, kind) => {
65
- for (const rel of files) {
66
- const meta = depFileMeta(rel);
67
- if (meta && meta.kind === kind) return rel;
68
- }
69
- return '';
70
- };
70
+ const hasProposed = (item) => !!depChange(item)?.propose;
71
+
72
+ const isStagedOrCommit = (item) =>
73
+ item.origin === 'staged' || item.origin === 'commit';
74
+
75
+ const firstDepRel = (rels, kind) =>
76
+ rels.find((rel) => depFileMeta(rel)?.kind === kind) ?? '';
71
77
 
72
78
  const proposedNpmPlan = (top, item) => {
73
79
  const change = depChange(item);
74
80
  if (!change) return null;
75
- const files = depPaths(item);
81
+ const rels = depPaths(item);
76
82
  const lockOnly = change.section === 'resolved';
77
83
  const removing = !!change.unused;
78
- const manifest = firstDepRel(files, 'manifest');
79
- const lockRel = firstDepRel(files, 'lockfile');
80
- const picked = lockOnly ? lockRel : manifest;
81
- const target = picked || files[0] || '';
82
- const meta = depFileMeta(target);
84
+ const manifestRel = firstDepRel(rels, 'manifest');
85
+ const lockRel = firstDepRel(rels, 'lockfile');
86
+ const meta = depFileMeta((lockOnly ? lockRel : manifestRel) || rels[0]);
83
87
  if (!meta) return null;
84
- const cwd = meta.dir === '.' ? top : path.join(top, meta.dir);
85
88
  let npmArgs = ['i'];
86
89
  if (removing) npmArgs = ['uninstall', change.name];
87
90
  else if (lockOnly) npmArgs = ['audit', 'fix'];
88
- const fail = removing ? 'npm uninstall failed' : 'npm i failed';
89
- const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
90
- const lockPath = lockRel || dirLock;
91
- const prepareManifest = !lockOnly && !removing && !!manifest;
92
91
  return {
93
- cwd,
92
+ cwd: meta.dir === '.' ? top : path.join(top, meta.dir),
94
93
  npmArgs,
95
- fail,
94
+ fail: removing ? 'npm uninstall failed' : 'npm i failed',
96
95
  lockOnly,
97
- lockPath,
98
- manifest,
99
- prepareManifest,
96
+ lockPath: lockRel || relOf(meta.dir, LOCKFILE),
97
+ manifest: manifestRel,
98
+ prepareManifest: !lockOnly && !removing && !!manifestRel,
100
99
  runner: item.dep.install,
101
100
  };
102
101
  };
103
102
 
104
- const hasProposed = (item) => {
105
- const change = depChange(item);
106
- return !!(change && change.propose);
107
- };
108
-
109
103
  const itemsForPath = (rel, items) => {
110
104
  const group = [];
111
105
  for (const item of items) {
112
- if (item.dep) {
113
- for (const orig of origDepItems(item)) {
114
- if (itemPath(orig) === rel) group.push(orig);
115
- }
116
- continue;
106
+ const listed = item.dep ? item.dep.items : [item];
107
+ for (const entry of listed ?? []) {
108
+ if (itemPath(entry) === rel) group.push(entry);
117
109
  }
118
- if (itemPath(item) === rel) group.push(item);
119
110
  }
120
111
  return group;
121
112
  };
122
113
 
123
- const hasDepFile = (items) => {
124
- for (const item of items) {
125
- if (depFileMeta(itemPath(item))) return true;
126
- }
127
- return false;
128
- };
129
-
130
- const NPM_REPORT = {
131
- audit: {
132
- file: LOCKFILE,
133
- args: ['audit', '--json', '--package-lock-only'],
134
- parse: parseAuditReport,
135
- },
136
- outdated: {
137
- file: MANIFEST,
138
- args: ['outdated', '--json', '--long'],
139
- parse: parseOutdatedReport,
140
- },
141
- };
142
-
143
- const collectReport = (top, kind) => {
144
- const { file, args, parse } = NPM_REPORT[kind];
145
- if (!fs.existsSync(path.join(top, file))) return null;
146
- return parse(runNpmJson(top, args, AUDIT_MS));
147
- };
148
-
149
- const collectReportAsync = async (top, kind, signal) => {
150
- const { file, args, parse } = NPM_REPORT[kind];
151
- if (!fs.existsSync(path.join(top, file))) return null;
152
- return parse(await runNpmJsonAsync(top, args, AUDIT_MS, signal));
153
- };
154
-
155
114
  const extrasNeeds = (items, rev, extra) => {
156
115
  const live = extra.audit === true && !rev;
157
- const hasDepDiff = hasDepFile(items);
116
+ const hasDepDiff = items.some((item) => depFileMeta(itemPath(item)));
158
117
  const audit = live || (Boolean(extra.audit) && hasDepDiff);
159
118
  return { live, scan: live || hasDepDiff, audit };
160
119
  };
161
120
 
162
- const foldLoaded = (items, top, rev, extra = {}) => {
121
+ const foldLoaded = (items, top, rev, extra) => {
163
122
  const readSides = (origin, rel) => readDepSides(top, rev, origin, rel);
164
123
  const needs = extrasNeeds(items, rev, extra);
165
124
  const collect = extra.collect !== false;
@@ -181,25 +140,18 @@ const foldLoaded = (items, top, rev, extra = {}) => {
181
140
  root: top,
182
141
  exportEntries,
183
142
  });
184
- if (!needs.live) return folded;
185
- if (!isPathInScope(MANIFEST, extra.paths ?? [])) return folded;
143
+ if (!needs.live || !isPathInScope(MANIFEST, extra.paths)) return folded;
186
144
  const pkgText = worktreeText(top, MANIFEST);
187
145
  const lockText = worktreeText(top, LOCKFILE);
188
146
  const options = { lockText, usedNames, root: top, exportEntries };
189
147
  return mergeProposedItems(folded, pkgText, outdated, audit, options);
190
148
  };
191
149
 
192
- const needsExtras = (parsed, rev, extra) =>
193
- extrasNeeds(parsed, rev, extra).audit;
194
-
195
150
  const finishLoad = (snapshot, extra) => {
196
- const options = { ...extra, paths: extra.paths ?? [] };
197
- if (extra.deferExtras) options.collect = false;
198
- const parsed = snapshot.parsed;
199
- const top = snapshot.top;
200
- const rev = snapshot.rev;
151
+ const { top, rev, parsed } = snapshot;
152
+ const options = { ...extra, collect: !extra.deferExtras };
201
153
  const items = foldLoaded(parsed, top, rev, options);
202
- const pending = extra.deferExtras && needsExtras(parsed, rev, extra);
154
+ const pending = extra.deferExtras && extrasNeeds(parsed, rev, extra).audit;
203
155
  return { ...snapshot, items, pending };
204
156
  };
205
157
 
@@ -228,123 +180,56 @@ const loadExtras = async (snapshot, extra = {}) => {
228
180
  : known,
229
181
  ]);
230
182
  if (signal && signal.aborted) throw abortError();
231
- const folded = foldLoaded(parsed, top, rev, {
183
+ const found = { usedNames, auditMap: audit, outdatedMap: outdated };
184
+ const items = foldLoaded(parsed, top, rev, {
232
185
  ...extra,
186
+ ...found,
233
187
  paths: extra.paths ?? [],
234
- usedNames,
235
- auditMap: audit,
236
- outdatedMap: outdated,
237
188
  exportEntries,
238
189
  collect: false,
239
190
  });
240
- return {
241
- ...snapshot,
242
- items: folded,
243
- pending: false,
244
- usedNames,
245
- auditMap: audit,
246
- outdatedMap: outdated,
247
- exportEntries,
248
- };
191
+ return { ...snapshot, items, pending: false, ...found, exportEntries };
249
192
  };
250
193
 
251
194
  const captureFile = (top, rel) => {
252
- const abs = path.join(top, rel);
253
195
  try {
254
- const text = fs.readFileSync(abs, 'utf8');
196
+ const text = fs.readFileSync(path.join(top, rel), 'utf8');
255
197
  return { rel, existed: true, text };
256
198
  } catch (error) {
257
- if (error.code === 'ENOENT') {
258
- return { rel, existed: false, text: '' };
259
- }
260
- throw error;
199
+ if (error.code !== 'ENOENT') throw error;
200
+ return { rel, existed: false, text: '' };
261
201
  }
262
202
  };
263
203
 
264
204
  const restoreFile = (top, saved) => {
265
205
  const abs = path.join(top, saved.rel);
266
- if (saved.existed) return void fs.writeFileSync(abs, saved.text);
267
- try {
268
- fs.unlinkSync(abs);
269
- } catch {
270
- // missing
271
- }
206
+ if (saved.existed) fs.writeFileSync(abs, saved.text);
207
+ else fs.rmSync(abs, { force: true });
272
208
  };
273
209
 
274
- const captureFiles = (top, rels) => {
275
- const { length } = rels;
276
- const owned = new Array(length);
277
- for (let i = 0; i < length; i++) owned[i] = captureFile(top, rels[i]);
278
- return owned;
279
- };
280
-
281
- const restoreFiles = (top, owned) => {
282
- for (const saved of owned) restoreFile(top, saved);
283
- };
284
-
285
- const mergeDepLive = (top, rel, change, origin, base, side, kind) => {
286
- const sides = readDepSides(top, null, origin, rel);
210
+ const mergeLive = (top, rel, item, base, side) => {
211
+ const sides = readDepSides(top, null, item.origin, rel);
212
+ const { kind } = depFileMeta(rel);
213
+ const change = depChange(item);
287
214
  return mergeDepFile(base, sides.oldText, sides.newText, change, side, kind);
288
215
  };
289
216
 
290
- const applyDepIndex = (top, rel, change, origin, side) => {
291
- const meta = depFileMeta(rel);
292
- if (!meta) return;
217
+ const applyDepIndex = (top, rel, item, side) => {
218
+ if (!depFileMeta(rel)) return;
293
219
  const indexed = gitText(top, `:${rel}`) || gitText(top, `HEAD:${rel}`);
294
- const text = mergeDepLive(top, rel, change, origin, indexed, side, meta.kind);
295
- writeIndex(top, rel, text);
220
+ writeIndex(top, rel, mergeLive(top, rel, item, indexed, side));
296
221
  };
297
222
 
298
- const applyDepWorktree = (top, rel, change, origin) => {
299
- const meta = depFileMeta(rel);
300
- if (!meta) return;
301
- const worktree = worktreeText(top, rel);
302
- const text = mergeDepLive(
303
- top,
304
- rel,
305
- change,
306
- origin,
307
- worktree,
308
- 'old',
309
- meta.kind,
310
- );
223
+ const applyDepWorktree = (top, rel, item, side) => {
224
+ if (!depFileMeta(rel)) return;
225
+ const text = mergeLive(top, rel, item, worktreeText(top, rel), side);
311
226
  writeFile(top, rel, text);
312
227
  };
313
228
 
314
- const ownedRelsOf = (plan) => {
315
- const rels = [];
316
- if (plan.manifest) rels.push(plan.manifest);
317
- if (plan.lockPath && plan.lockPath !== plan.manifest) {
318
- rels.push(plan.lockPath);
319
- }
320
- return rels;
321
- };
322
-
323
- const errorFromResult = (result, fail) => {
324
- if (!result) return new Error(fail);
325
- if (result.error) {
326
- const msg = result.error.message || fail;
327
- return new Error(oneLine(msg, fail));
328
- }
329
- const msg = result.stderr || result.stdout || fail;
330
- return new Error(oneLine(msg, fail));
331
- };
332
-
333
- const applyPreparation = (top, item, plan) => {
334
- if (!plan.prepareManifest || !plan.manifest) return;
335
- const change = depChange(item);
336
- const origin = item.origin;
337
- const worktree = worktreeText(top, plan.manifest);
338
- const text = mergeDepLive(
339
- top,
340
- plan.manifest,
341
- change,
342
- origin,
343
- worktree,
344
- 'new',
345
- 'manifest',
346
- );
347
- writeFile(top, plan.manifest, text);
229
+ const npmError = (result, fail) => {
230
+ const { error } = result;
231
+ const msg = error ? error.message : result.stderr || result.stdout;
232
+ return new Error(oneLine(msg || fail, fail));
348
233
  };
349
234
 
350
235
  const stagePlan = (top, plan) => {
@@ -355,131 +240,122 @@ const stagePlan = (top, plan) => {
355
240
  requireOk(runGit(['add', '--', ...toAdd], top));
356
241
  };
357
242
 
358
- const finishPlan = (top, plan, result, stage) => {
359
- if (result && (result.error || result.status !== 0)) {
360
- throw errorFromResult(result, plan.fail);
361
- }
362
- const stageFn = stage ?? stagePlan;
363
- stageFn(top, plan);
243
+ const startPlan = (top, item) => {
244
+ const plan = proposedNpmPlan(top, item);
245
+ if (!plan) return null;
246
+ const rels = [...new Set([plan.manifest, plan.lockPath].filter(Boolean))];
247
+ const owned = rels.map((rel) => captureFile(top, rel));
248
+ return { plan, owned };
249
+ };
250
+
251
+ const finishPlan = (top, plan, result, stage = stagePlan) => {
252
+ const failed = result && (result.error || result.status !== 0);
253
+ if (failed) throw npmError(result, plan.fail);
254
+ stage(top, plan);
255
+ };
256
+
257
+ const restoreOwned = (top, owned) => {
258
+ for (const saved of owned) restoreFile(top, saved);
364
259
  };
365
260
 
366
261
  const applyProposedUpdate = (top, item, extra = {}) => {
367
- const plan = proposedNpmPlan(top, item);
368
- if (!plan) return;
369
- const owned = captureFiles(top, ownedRelsOf(plan));
262
+ const started = startPlan(top, item);
263
+ if (!started) return;
264
+ const { plan, owned } = started;
370
265
  try {
371
- applyPreparation(top, item, plan);
266
+ if (plan.prepareManifest) applyDepWorktree(top, plan.manifest, item, 'new');
372
267
  const install = extra.run ?? plan.runner ?? runNpm;
373
- const result = install(plan.cwd, plan.npmArgs);
374
- finishPlan(top, plan, result, extra.stage);
268
+ finishPlan(top, plan, install(plan.cwd, plan.npmArgs), extra.stage);
375
269
  } catch (error) {
376
- restoreFiles(top, owned);
270
+ restoreOwned(top, owned);
377
271
  throw error;
378
272
  }
379
273
  };
380
274
 
381
- const applyProposedUpdateAsync = async (top, item, extra = {}) => {
382
- const plan = proposedNpmPlan(top, item);
383
- if (!plan) return;
384
- const owned = captureFiles(top, ownedRelsOf(plan));
275
+ const applyProposedUpdateAsync = async (top, item) => {
276
+ const started = startPlan(top, item);
277
+ if (!started) return;
278
+ const { plan, owned } = started;
385
279
  try {
386
- applyPreparation(top, item, plan);
387
- const run = extra.run ?? plan.runner ?? runNpmAsync;
388
- const result = await run(plan.cwd, plan.npmArgs);
389
- finishPlan(top, plan, result, extra.stage);
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));
390
283
  } catch (error) {
391
- restoreFiles(top, owned);
284
+ restoreOwned(top, owned);
392
285
  throw error;
393
286
  }
394
287
  };
395
288
 
396
289
  const applyDepChange = (top, item, mode) => {
397
290
  const change = depChange(item);
398
- if (change && change.propose) {
291
+ if (change.propose) {
399
292
  if (mode === 'add') return void applyProposedUpdate(top, item);
400
293
  if (!change.unused) return;
401
294
  }
402
- const origin = item.origin;
403
- const files = depPaths(item);
404
- for (const rel of files) {
405
- if (mode === 'add') {
406
- applyDepIndex(top, rel, change, origin, 'new');
295
+ for (const rel of depPaths(item)) {
296
+ if (mode !== 'revert') {
297
+ applyDepIndex(top, rel, item, mode === 'add' ? 'new' : 'old');
407
298
  continue;
408
299
  }
409
- if (mode === 'unstage') {
410
- applyDepIndex(top, rel, change, origin, 'old');
411
- continue;
412
- }
413
- if (origin === 'staged') applyDepIndex(top, rel, change, origin, 'old');
414
- applyDepWorktree(top, rel, change, origin);
300
+ if (item.origin === 'staged') applyDepIndex(top, rel, item, 'old');
301
+ applyDepWorktree(top, rel, item, 'old');
415
302
  }
416
303
  };
417
304
 
418
305
  const addItem = (top, item) => {
419
- if (item.origin === 'staged' || item.origin === 'commit') return;
306
+ if (!item.dep) return void worktree.addItem(top, item);
307
+ if (isStagedOrCommit(item)) return;
420
308
  if (depChange(item)) return void applyDepChange(top, item, 'add');
421
- const files = depPaths(item);
422
- if (files.length) {
423
- requireOk(runGit(['add', '--', ...files], top));
424
- }
309
+ const rels = depPaths(item);
310
+ if (rels.length) requireOk(runGit(['add', '--', ...rels], top));
425
311
  };
426
312
 
427
313
  const addItemAsync = async (top, item) => {
428
- if (item.origin === 'staged' || item.origin === 'commit') return;
429
- if (hasProposed(item)) {
430
- await applyProposedUpdateAsync(top, item);
431
- return;
314
+ if (isStagedOrCommit(item) || !hasProposed(item)) {
315
+ return void addItem(top, item);
432
316
  }
433
- addItem(top, item);
317
+ await applyProposedUpdateAsync(top, item);
434
318
  };
435
319
 
436
320
  const unstageItem = (top, item) => {
321
+ if (!item.dep) return void worktree.unstageItem(top, item);
437
322
  if (item.origin !== 'staged') return;
438
323
  if (depChange(item)) return void applyDepChange(top, item, 'unstage');
439
- const files = depPaths(item);
440
- if (files.length) {
441
- requireOk(runGit(['restore', '--staged', '--', ...files], top));
442
- }
324
+ const rels = depPaths(item);
325
+ if (!rels.length) return;
326
+ requireOk(runGit(['restore', '--staged', '--', ...rels], top));
443
327
  };
444
328
 
445
- const revertDepItem = (top, item) => {
329
+ const revertItem = (top, item) => {
330
+ if (item.origin === 'commit') return;
331
+ if (!item.dep) return void worktree.revertItem(top, item);
446
332
  if (depChange(item)) return void applyDepChange(top, item, 'revert');
447
- const byPath = new Map();
448
- for (const orig of origDepItems(item)) {
449
- const rel = itemPath(orig);
450
- const list = byPath.get(rel);
451
- if (list) list.push(orig);
452
- else byPath.set(rel, [orig]);
453
- }
454
- for (const rel of byPath.keys()) {
455
- revertOnePath(top, rel, byPath.get(rel));
456
- }
333
+ const byPath = groupByPath(item.dep.items ?? []);
334
+ for (const rel of byPath.keys()) revertOnePath(top, rel, byPath.get(rel));
457
335
  };
458
336
 
459
337
  const revertFile = (top, rel, items) => {
460
- const paths = new Set([rel]);
338
+ const rels = new Set([rel]);
461
339
  for (const item of items) {
462
- for (const file of depPaths(item)) paths.add(file);
340
+ for (const file of depPaths(item)) rels.add(file);
463
341
  }
464
- for (const file of paths) {
342
+ for (const file of rels) {
465
343
  const group = itemsForPath(file, items);
466
- const targets = group.length ? group : items;
467
- revertOnePath(top, file, targets);
344
+ revertOnePath(top, file, group.length ? group : items);
468
345
  }
469
346
  };
470
347
 
471
348
  module.exports = {
472
- itemsForPath,
349
+ collectReportAsync,
473
350
  finishLoad,
474
351
  loadExtras,
475
352
  captureFile,
476
353
  restoreFile,
354
+ proposedNpmPlan,
477
355
  applyProposedUpdate,
478
356
  addItem,
479
357
  addItemAsync,
480
358
  unstageItem,
481
- revertDepItem,
359
+ revertItem,
482
360
  revertFile,
483
- INSTALL_MS,
484
- proposedNpmPlan,
485
361
  };
@@ -5,16 +5,8 @@ const fs = require('node:fs');
5
5
  const file = process.argv[process.argv.length - 1];
6
6
  const msg = process.env.RESLOP_REWORD_FILE;
7
7
  if (!file || !msg) process.exit(1);
8
- const text = fs.readFileSync(file, 'utf8');
9
- const rows = text.split('\n');
10
- const out = [];
11
- let seen = false;
12
- for (const row of rows) {
13
- out.push(row);
14
- if (seen) continue;
15
- if (!/^(pick|p)\s+/.test(row)) continue;
16
- const spec = JSON.stringify(msg);
17
- out.push(`exec git commit --amend --only -F ${spec}`);
18
- seen = true;
19
- }
20
- fs.writeFileSync(file, out.join('\n'));
8
+ const rows = fs.readFileSync(file, 'utf8').split('\n');
9
+ const at = rows.findIndex((row) => /^(pick|p)\s+/.test(row));
10
+ const amend = `exec git commit --amend --only -F ${JSON.stringify(msg)}`;
11
+ if (at >= 0) rows.splice(at + 1, 0, amend);
12
+ fs.writeFileSync(file, rows.join('\n'));