reslop 0.2.4 → 0.2.5
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 +14 -1
- package/README.md +1 -2
- package/lib/capabilities.js +0 -4
- package/lib/clipboard.js +39 -1
- package/lib/deps-diff.js +0 -3
- package/lib/deps.js +144 -176
- package/lib/diff/inline.js +66 -1
- package/lib/diff/unit.js +11 -28
- package/lib/editor.js +69 -9
- package/lib/files.js +17 -16
- package/lib/git-deps.js +58 -95
- package/lib/github.js +7 -7
- package/lib/gitlab.js +3 -3
- package/lib/highlight/core.js +11 -0
- package/lib/highlight/dot.js +2 -12
- package/lib/highlight/log.js +4 -11
- package/lib/highlight/txt.js +2 -12
- package/lib/keys.js +22 -12
- package/lib/manifest.js +0 -4
- package/lib/npm-commands.js +1 -22
- package/lib/remote.js +1 -7
- package/lib/render/branches.js +4 -13
- package/lib/render/chrome.js +85 -31
- package/lib/render/commits.js +95 -207
- package/lib/render/diff.js +31 -1
- package/lib/render/files.js +7 -16
- package/lib/render/frame.js +95 -110
- package/lib/render/notes.js +71 -16
- package/lib/render/npm.js +104 -18
- package/lib/render/primitives.js +108 -45
- package/lib/render/unit.js +4 -1
- package/lib/report-parse.js +0 -1
- package/lib/report-run.js +0 -1
- package/lib/report-test.js +40 -47
- package/lib/report-tools.js +0 -7
- package/lib/review-merge.js +101 -131
- package/lib/review.js +64 -77
- package/lib/select.js +52 -0
- package/lib/session/actions.js +23 -22
- package/lib/session/branches.js +5 -3
- package/lib/session/change-actions.js +173 -146
- package/lib/session/commit.js +32 -13
- package/lib/session/compose.js +120 -77
- package/lib/session/dispatch.js +36 -21
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +24 -24
- package/lib/session/items.js +69 -89
- package/lib/session/lifecycle.js +31 -96
- package/lib/session/load.js +51 -58
- package/lib/session/navigation.js +105 -96
- package/lib/session/npm.js +158 -77
- package/lib/session/ops.js +192 -51
- package/lib/session/pointer.js +11 -5
- package/lib/session/progress.js +9 -6
- package/lib/session/review.js +35 -54
- package/lib/session/tasks.js +292 -0
- package/lib/session/terminal.js +13 -17
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +17 -14
- package/lib/session/watch.js +47 -27
- package/lib/session.js +34 -79
- package/lib/update.js +1 -5
- package/lib/utilities.js +21 -1
- package/lib/wrap.js +15 -0
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.5][] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
- Remove UI and filesystem polling timers, refresh on events
|
|
8
|
+
- Refresh the commits list when HEAD changes outside reslop
|
|
9
|
+
- Update stage/unstage immediately, git/npm in the background
|
|
10
|
+
- Add editor hotkeys Ctrl+X, Ctrl+C, and Ctrl+V
|
|
11
|
+
- Improve diff calculation
|
|
12
|
+
- Improve npm command and test edit/run
|
|
13
|
+
- Refactor internals: simplify and add classes
|
|
14
|
+
- Hothey "i" add to ignore lists: .gitignore, .npmignore
|
|
15
|
+
- Rename TODO to tasks and improve TUI/TUX
|
|
16
|
+
|
|
5
17
|
## [0.2.4][] - 2026-09-28
|
|
6
18
|
|
|
7
19
|
- Add AI instructions to run `npm t` or `reslop t -- node --test`
|
|
@@ -141,7 +153,8 @@
|
|
|
141
153
|
- Feedback and todos that produce a repair plan for an agent
|
|
142
154
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
143
155
|
|
|
144
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.
|
|
156
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...HEAD
|
|
157
|
+
[0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
|
|
145
158
|
[0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
|
|
146
159
|
[0.2.3]: https://github.com/tshemsedinov/reslop/compare/v0.2.1...v0.2.3
|
|
147
160
|
[0.2.1]: https://github.com/tshemsedinov/reslop/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
> Turn generated changes into owned changes
|
|
13
13
|
|
|
14
|
-
Open a change, leave findings and a backlog, and prepare a repair plan
|
|
15
|
-
for the agent.
|
|
14
|
+
Open a change, leave findings and a backlog, and prepare a repair plan for the agent.
|
|
16
15
|
|
|
17
16
|
```text
|
|
18
17
|
Review → Plan → Repair → Verify
|
package/lib/capabilities.js
CHANGED
package/lib/clipboard.js
CHANGED
|
@@ -29,8 +29,44 @@ const runClip = (cmd, args, text) => {
|
|
|
29
29
|
return result.status === 0;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const UNIX_PASTE = [
|
|
33
|
+
{ cmd: 'wl-paste', args: ['--no-newline'] },
|
|
34
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard', '-o'] },
|
|
35
|
+
{ cmd: 'xsel', args: ['--clipboard', '--output'] },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
let copied = '';
|
|
39
|
+
|
|
40
|
+
const pasteTools = (platform = process.platform) => {
|
|
41
|
+
if (platform === 'win32') {
|
|
42
|
+
const args = ['-NoProfile', '-Command', 'Get-Clipboard'];
|
|
43
|
+
return [{ cmd: 'powershell', args }];
|
|
44
|
+
}
|
|
45
|
+
if (platform === 'darwin') return [{ cmd: 'pbpaste', args: [] }];
|
|
46
|
+
return UNIX_PASTE;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const readPaste = (cmd, args) => {
|
|
50
|
+
const result = spawnSync(
|
|
51
|
+
cmd,
|
|
52
|
+
args,
|
|
53
|
+
spawnBase({ encoding: 'utf8', timeout: CLIP_TIMEOUT_MS }),
|
|
54
|
+
);
|
|
55
|
+
if (result.error || result.status !== 0) return '';
|
|
56
|
+
return result.stdout || '';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const systemPaste = () => {
|
|
60
|
+
for (const tool of pasteTools()) {
|
|
61
|
+
const text = readPaste(tool.cmd, tool.args);
|
|
62
|
+
if (text) return text;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
};
|
|
66
|
+
|
|
32
67
|
const copyText = (text, stdout) => {
|
|
33
68
|
if (!text) return false;
|
|
69
|
+
copied = text;
|
|
34
70
|
let ok = false;
|
|
35
71
|
if (stdout && stdout.write) {
|
|
36
72
|
stdout.write(osc52(text));
|
|
@@ -42,4 +78,6 @@ const copyText = (text, stdout) => {
|
|
|
42
78
|
return ok;
|
|
43
79
|
};
|
|
44
80
|
|
|
45
|
-
|
|
81
|
+
const pasteText = () => systemPaste() || copied;
|
|
82
|
+
|
|
83
|
+
module.exports = { osc52, copyText, pasteText };
|
package/lib/deps-diff.js
CHANGED
|
@@ -645,14 +645,11 @@ module.exports = {
|
|
|
645
645
|
DEP_CAPTION,
|
|
646
646
|
diffSections,
|
|
647
647
|
mergeResolved,
|
|
648
|
-
orderedChanges,
|
|
649
648
|
summarizeGroup,
|
|
650
649
|
collectGroups,
|
|
651
650
|
takeManifestItems,
|
|
652
651
|
foldedDepNames,
|
|
653
652
|
stripFoldedDepItem,
|
|
654
|
-
buildRows,
|
|
655
|
-
makeHunk,
|
|
656
653
|
makeDepItems,
|
|
657
654
|
stubFileItem,
|
|
658
655
|
};
|
package/lib/deps.js
CHANGED
|
@@ -6,6 +6,8 @@ const metautil = require('metautil');
|
|
|
6
6
|
const { jsonParse, isHashObject } = metautil;
|
|
7
7
|
const files = require('./files.js');
|
|
8
8
|
const { itemPath } = files;
|
|
9
|
+
const utilities = require('./utilities.js');
|
|
10
|
+
const { asText } = utilities;
|
|
9
11
|
const manifest = require('./manifest.js');
|
|
10
12
|
const { MANIFEST, LOCKFILE, SECTION_KEYS, depFileMeta, relOf } = manifest;
|
|
11
13
|
const { listFilePath, readSections, lockEntries, lockPackageCount } = manifest;
|
|
@@ -240,7 +242,7 @@ const viaNotes = (via, pkgName) => {
|
|
|
240
242
|
const notes = [];
|
|
241
243
|
for (const item of via) {
|
|
242
244
|
if (!item || typeof item !== 'object') continue;
|
|
243
|
-
const title =
|
|
245
|
+
const title = asText(item.title);
|
|
244
246
|
if (!title) continue;
|
|
245
247
|
const hasName = typeof item.name === 'string' && item.name;
|
|
246
248
|
const named = hasName ? item.name : pkgName;
|
|
@@ -271,32 +273,52 @@ const auditEntryNames = (key, entry) => {
|
|
|
271
273
|
return names;
|
|
272
274
|
};
|
|
273
275
|
|
|
274
|
-
const
|
|
276
|
+
const uniqueTexts = (list) => [...new Set(list.filter(Boolean))];
|
|
277
|
+
|
|
278
|
+
const keepHighestAudit = (audit, found) => {
|
|
279
|
+
const { name, severity } = found;
|
|
275
280
|
if (!name || !severity || severity === 'info') return;
|
|
276
|
-
const titles =
|
|
277
|
-
for (const note of notes) {
|
|
278
|
-
if (note && !titles.includes(note)) titles.push(note);
|
|
279
|
-
}
|
|
281
|
+
const titles = uniqueTexts(found.titles);
|
|
280
282
|
const prev = audit.get(name);
|
|
281
283
|
if (prev && severityRank(prev.severity) >= severityRank(severity)) {
|
|
282
|
-
|
|
283
|
-
for (const note of titles) {
|
|
284
|
-
if (!merged.includes(note)) merged.push(note);
|
|
285
|
-
}
|
|
286
|
-
const next = {
|
|
284
|
+
audit.set(name, {
|
|
287
285
|
...prev,
|
|
288
|
-
titles:
|
|
289
|
-
fix: prev.fix || fix,
|
|
290
|
-
range: prev.range || range,
|
|
291
|
-
};
|
|
292
|
-
audit.set(name, next);
|
|
286
|
+
titles: uniqueTexts([...prev.titles, ...titles]),
|
|
287
|
+
fix: prev.fix || found.fix,
|
|
288
|
+
range: prev.range || found.range,
|
|
289
|
+
});
|
|
293
290
|
return;
|
|
294
291
|
}
|
|
295
292
|
const title = titles[0] || '';
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
const fix = found.fix || '';
|
|
294
|
+
const range = found.range || '';
|
|
295
|
+
audit.set(name, { name, severity, title, titles, fix, range });
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const isEntry = (value) => Boolean(value) && typeof value === 'object';
|
|
299
|
+
|
|
300
|
+
const vulnFindings = (vulns) => {
|
|
301
|
+
const found = [];
|
|
302
|
+
for (const [key, entry] of Object.entries(vulns)) {
|
|
303
|
+
if (!isEntry(entry)) continue;
|
|
304
|
+
const { severity } = entry;
|
|
305
|
+
const titles = viaNotes(entry.via, entry.name || key);
|
|
306
|
+
const fix = auditFix(entry);
|
|
307
|
+
const range = asText(entry.range);
|
|
308
|
+
for (const name of auditEntryNames(key, entry)) {
|
|
309
|
+
found.push({ name, severity, fix, range, titles });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return found;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const advisoryFinding = (entry) => {
|
|
316
|
+
const name = entry.module_name || entry.name;
|
|
317
|
+
const title = namedTitle(name, asText(entry.title));
|
|
318
|
+
const titles = title ? [title] : [];
|
|
319
|
+
const fix = auditFix(entry);
|
|
320
|
+
const range = asText(entry.range);
|
|
321
|
+
return { name, severity: entry.severity, fix, range, titles };
|
|
300
322
|
};
|
|
301
323
|
|
|
302
324
|
const parseAuditReport = (text) => {
|
|
@@ -304,36 +326,13 @@ const parseAuditReport = (text) => {
|
|
|
304
326
|
if (!text || !`${text}`.trim()) return audit;
|
|
305
327
|
const data = jsonParse(text);
|
|
306
328
|
if (!isHashObject(data)) return audit;
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const severity = entry.severity;
|
|
313
|
-
const names = auditEntryNames(key, entry);
|
|
314
|
-
const pkgName = entry.name || key;
|
|
315
|
-
const titles = viaNotes(entry.via, pkgName);
|
|
316
|
-
const fix = auditFix(entry);
|
|
317
|
-
const range = typeof entry.range === 'string' ? entry.range : '';
|
|
318
|
-
for (const name of names) {
|
|
319
|
-
keepHighestAudit(audit, name, severity, fix, range, titles);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const advisories = data.advisories;
|
|
324
|
-
if (advisories && typeof advisories === 'object') {
|
|
325
|
-
for (const entry of Object.values(advisories)) {
|
|
326
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
327
|
-
const name = entry.module_name || entry.name;
|
|
328
|
-
const severity = entry.severity;
|
|
329
|
-
const raw = typeof entry.title === 'string' ? entry.title : '';
|
|
330
|
-
const title = namedTitle(name, raw);
|
|
331
|
-
const titles = title ? [title] : [];
|
|
332
|
-
const fix = auditFix(entry);
|
|
333
|
-
const range = typeof entry.range === 'string' ? entry.range : '';
|
|
334
|
-
keepHighestAudit(audit, name, severity, fix, range, titles);
|
|
335
|
-
}
|
|
329
|
+
const { vulnerabilities, advisories } = data;
|
|
330
|
+
const found = isEntry(vulnerabilities) ? vulnFindings(vulnerabilities) : [];
|
|
331
|
+
if (isEntry(advisories)) {
|
|
332
|
+
const listed = Object.values(advisories).filter(isEntry);
|
|
333
|
+
found.push(...listed.map(advisoryFinding));
|
|
336
334
|
}
|
|
335
|
+
for (const entry of found) keepHighestAudit(audit, entry);
|
|
337
336
|
return audit;
|
|
338
337
|
};
|
|
339
338
|
|
|
@@ -345,11 +344,11 @@ const parseOutdatedReport = (text) => {
|
|
|
345
344
|
for (const name of Object.keys(data)) {
|
|
346
345
|
const entry = data[name];
|
|
347
346
|
if (!entry || typeof entry !== 'object') continue;
|
|
348
|
-
const
|
|
349
|
-
const wanted = typeof entry.wanted === 'string' ? entry.wanted : '';
|
|
350
|
-
const latest = typeof entry.latest === 'string' ? entry.latest : '';
|
|
351
|
-
const type = typeof entry.type === 'string' ? entry.type : '';
|
|
347
|
+
const wanted = asText(entry.wanted);
|
|
352
348
|
if (!wanted) continue;
|
|
349
|
+
const current = asText(entry.current);
|
|
350
|
+
const latest = asText(entry.latest);
|
|
351
|
+
const type = asText(entry.type);
|
|
353
352
|
outdated.set(name, { current, wanted, latest, type });
|
|
354
353
|
}
|
|
355
354
|
return outdated;
|
|
@@ -390,21 +389,18 @@ const unusedChangeNames = (changes, used) => {
|
|
|
390
389
|
return names;
|
|
391
390
|
};
|
|
392
391
|
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
if (!pkgHasExportEntry(pkg)) return
|
|
396
|
-
if (!used || !used.size) return names;
|
|
392
|
+
const unusedDeclared = (pkg, used) => {
|
|
393
|
+
const found = [];
|
|
394
|
+
if (!pkgHasExportEntry(pkg) || !used?.size) return found;
|
|
397
395
|
const maps = readSections(pkg);
|
|
398
396
|
for (const section of SECTION_KEYS) {
|
|
399
397
|
if (section === 'peerDependencies') continue;
|
|
400
|
-
const
|
|
401
|
-
keys.sort()
|
|
402
|
-
|
|
403
|
-
if (used.has(name)) continue;
|
|
404
|
-
names.push(name);
|
|
398
|
+
const map = maps[section];
|
|
399
|
+
for (const name of [...map.keys()].sort()) {
|
|
400
|
+
if (!used.has(name)) found.push({ name, section, from: map.get(name) });
|
|
405
401
|
}
|
|
406
402
|
}
|
|
407
|
-
return
|
|
403
|
+
return found;
|
|
408
404
|
};
|
|
409
405
|
|
|
410
406
|
const markUnusedChanges = (changes, used, pkg, exportEntries) => {
|
|
@@ -418,92 +414,69 @@ const markUnusedChanges = (changes, used, pkg, exportEntries) => {
|
|
|
418
414
|
|
|
419
415
|
const unusedDeclaredChanges = (pkg, used, exportEntries) => {
|
|
420
416
|
const changes = [];
|
|
421
|
-
if (!pkgHasExportEntry(pkg)) return changes;
|
|
422
|
-
if (!used || !used.size) return changes;
|
|
423
|
-
const maps = readSections(pkg);
|
|
424
417
|
const taken = new Set();
|
|
425
|
-
for (const section of
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
action: 'removed',
|
|
440
|
-
from: version,
|
|
441
|
-
to: '',
|
|
442
|
-
propose: true,
|
|
443
|
-
unused: true,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
418
|
+
for (const { name, section, from } of unusedDeclared(pkg, used)) {
|
|
419
|
+
if (taken.has(name) || !depExports(exportEntries, name)) continue;
|
|
420
|
+
taken.add(name);
|
|
421
|
+
const action = 'removed';
|
|
422
|
+
const to = '';
|
|
423
|
+
changes.push({
|
|
424
|
+
name,
|
|
425
|
+
section,
|
|
426
|
+
action,
|
|
427
|
+
from,
|
|
428
|
+
to,
|
|
429
|
+
propose: true,
|
|
430
|
+
unused: true,
|
|
431
|
+
});
|
|
446
432
|
}
|
|
447
433
|
return changes;
|
|
448
434
|
};
|
|
449
435
|
|
|
436
|
+
const resolvedChange = (name, vul, resolved) => {
|
|
437
|
+
const from = resolved.get(name) || '';
|
|
438
|
+
if (!vul || !from) return null;
|
|
439
|
+
const to = proposedLockVersion(from, vul);
|
|
440
|
+
const section = 'resolved';
|
|
441
|
+
const action = 'vulnerable';
|
|
442
|
+
return { name, section, action, from, to, propose: true, audit: vul };
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const declaredChange = (name, declared, out, vul) => {
|
|
446
|
+
const wanted = proposedWanted(out, vul);
|
|
447
|
+
if (!wanted && !vul) return null;
|
|
448
|
+
const from = declared.version;
|
|
449
|
+
const to = wanted ? applyWantedRange(from, wanted) : from;
|
|
450
|
+
if (to === from && !vul) return null;
|
|
451
|
+
const action = to === from ? 'vulnerable' : 'changed';
|
|
452
|
+
const { section } = declared;
|
|
453
|
+
const change = { name, section, action, from, to, propose: true };
|
|
454
|
+
if (out) change.outdated = out;
|
|
455
|
+
if (vul) change.audit = vul;
|
|
456
|
+
return change;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const mapKeys = (map) => (map ? [...map.keys()] : []);
|
|
460
|
+
|
|
450
461
|
const buildProposedChanges = (pkg, outdated, audit, lock, options = {}) => {
|
|
451
|
-
const
|
|
452
|
-
const exportEntries = options.exportEntries;
|
|
453
|
-
const names = new Set();
|
|
454
|
-
if (outdated) {
|
|
455
|
-
for (const name of outdated.keys()) names.add(name);
|
|
456
|
-
}
|
|
457
|
-
if (audit) {
|
|
458
|
-
for (const name of audit.keys()) names.add(name);
|
|
459
|
-
}
|
|
460
|
-
const listed = [...names];
|
|
462
|
+
const listed = [...new Set([...mapKeys(outdated), ...mapKeys(audit)])];
|
|
461
463
|
listed.sort();
|
|
462
464
|
const resolved = lockEntries(lock);
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
465
|
+
const changes = unusedDeclaredChanges(
|
|
466
|
+
pkg,
|
|
467
|
+
options.used,
|
|
468
|
+
options.exportEntries,
|
|
469
|
+
);
|
|
470
|
+
const taken = new Set(changes.map((change) => change.name));
|
|
467
471
|
for (const name of listed) {
|
|
468
472
|
if (taken.has(name)) continue;
|
|
469
473
|
const out = outdated ? outdated.get(name) : null;
|
|
470
474
|
const vul = audit ? audit.get(name) : null;
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (!version) continue;
|
|
477
|
-
const to = proposedLockVersion(version, vul);
|
|
478
|
-
changes.push({
|
|
479
|
-
name,
|
|
480
|
-
section: 'resolved',
|
|
481
|
-
action: 'vulnerable',
|
|
482
|
-
from: version,
|
|
483
|
-
to,
|
|
484
|
-
propose: true,
|
|
485
|
-
audit: vul,
|
|
486
|
-
});
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
const wanted = proposedWanted(out, vul);
|
|
490
|
-
if (!wanted && !vul) continue;
|
|
491
|
-
let to = declared.version;
|
|
492
|
-
if (wanted) to = applyWantedRange(declared.version, wanted);
|
|
493
|
-
if (to === declared.version && !vul) continue;
|
|
494
|
-
let action = 'changed';
|
|
495
|
-
if (to === declared.version) action = 'vulnerable';
|
|
496
|
-
const change = {
|
|
497
|
-
name,
|
|
498
|
-
section: declared.section,
|
|
499
|
-
action,
|
|
500
|
-
from: declared.version,
|
|
501
|
-
to,
|
|
502
|
-
propose: true,
|
|
503
|
-
};
|
|
504
|
-
if (out) change.outdated = out;
|
|
505
|
-
if (vul) change.audit = vul;
|
|
506
|
-
changes.push(change);
|
|
475
|
+
const declared = findDeclared(pkg, name, out ? out.type : '');
|
|
476
|
+
const change = declared
|
|
477
|
+
? declaredChange(name, declared, out, vul)
|
|
478
|
+
: resolvedChange(name, vul, resolved);
|
|
479
|
+
if (change) changes.push(change);
|
|
507
480
|
}
|
|
508
481
|
return changes;
|
|
509
482
|
};
|
|
@@ -561,6 +534,30 @@ const insertAfterPath = (items, added, rel) => {
|
|
|
561
534
|
return [...items.slice(0, at + 1), ...added, ...items.slice(at + 1)];
|
|
562
535
|
};
|
|
563
536
|
|
|
537
|
+
const placeFolded = (items, { folded, synthetic, leftoverNames }) => {
|
|
538
|
+
const out = [];
|
|
539
|
+
const slot = new Map();
|
|
540
|
+
for (const item of items) {
|
|
541
|
+
const meta = depFileMeta(itemPath(item));
|
|
542
|
+
const pkgRel = meta ? relOf(meta.dir, MANIFEST) : '';
|
|
543
|
+
const key = meta ? `${item.origin}\0${meta.dir}` : '';
|
|
544
|
+
if (meta && synthetic.has(key) && !slot.has(pkgRel)) {
|
|
545
|
+
slot.set(pkgRel, out.length);
|
|
546
|
+
}
|
|
547
|
+
if (folded.has(item)) continue;
|
|
548
|
+
if (!meta || meta.kind !== 'manifest') {
|
|
549
|
+
out.push(item);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const names = leftoverNames.get(key);
|
|
553
|
+
const kept = names ? stripFoldedDepItem(item, names) : item;
|
|
554
|
+
if (!kept) continue;
|
|
555
|
+
out.push(kept);
|
|
556
|
+
slot.set(pkgRel, out.length);
|
|
557
|
+
}
|
|
558
|
+
return spliceAtSlots(out, synthetic, slot);
|
|
559
|
+
};
|
|
560
|
+
|
|
564
561
|
const foldDepItems = (
|
|
565
562
|
items,
|
|
566
563
|
readSides = () => ({ oldText: '', newText: '' }),
|
|
@@ -578,51 +575,22 @@ const foldDepItems = (
|
|
|
578
575
|
const summary = summarizeGroup(group, readSides);
|
|
579
576
|
const unusedNames = unusedChangeNames(summary.changes, usedNames);
|
|
580
577
|
const exportEntries = loadExportEntries(unusedNames, extra, group.dir);
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
);
|
|
587
|
-
const audited = markChanges(unused, audit, 'audit');
|
|
588
|
-
const changes = markChanges(audited, outdated, 'outdated');
|
|
578
|
+
const { pkg } = summary;
|
|
579
|
+
let changes = summary.changes;
|
|
580
|
+
changes = markUnusedChanges(changes, usedNames, pkg, exportEntries);
|
|
581
|
+
changes = markChanges(changes, audit, 'audit');
|
|
582
|
+
changes = markChanges(changes, outdated, 'outdated');
|
|
589
583
|
const next = { ...summary, changes };
|
|
590
|
-
leftoverNames.set(key, foldedDepNames(
|
|
584
|
+
leftoverNames.set(key, foldedDepNames(changes));
|
|
591
585
|
const taken = takeManifestItems(next);
|
|
592
|
-
if (next.foldLock)
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
if (!taken.length && !next.changes.length) continue;
|
|
586
|
+
if (next.foldLock) taken.push(...next.lockItems);
|
|
587
|
+
if (!taken.length && !changes.length) continue;
|
|
596
588
|
const made = makeDepItems(group.origin, group.items, taken, next);
|
|
597
589
|
if (made.length) synthetic.set(key, made);
|
|
598
590
|
for (const item of taken) folded.add(item);
|
|
599
591
|
}
|
|
600
592
|
if (!synthetic.size && !folded.size) return items;
|
|
601
|
-
|
|
602
|
-
const slot = new Map();
|
|
603
|
-
for (const item of items) {
|
|
604
|
-
const rel = itemPath(item);
|
|
605
|
-
const meta = depFileMeta(rel);
|
|
606
|
-
if (meta) {
|
|
607
|
-
const pkgRel = relOf(meta.dir, MANIFEST);
|
|
608
|
-
const key = `${item.origin}\0${meta.dir}`;
|
|
609
|
-
if (synthetic.has(key) && !slot.has(pkgRel)) {
|
|
610
|
-
slot.set(pkgRel, out.length);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (folded.has(item)) continue;
|
|
614
|
-
if (meta && meta.kind === 'manifest') {
|
|
615
|
-
const names = leftoverNames.get(`${item.origin}\0${meta.dir}`);
|
|
616
|
-
const kept = names ? stripFoldedDepItem(item, names) : item;
|
|
617
|
-
if (kept) {
|
|
618
|
-
out.push(kept);
|
|
619
|
-
slot.set(relOf(meta.dir, MANIFEST), out.length);
|
|
620
|
-
}
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
out.push(item);
|
|
624
|
-
}
|
|
625
|
-
return spliceAtSlots(out, synthetic, slot);
|
|
593
|
+
return placeFolded(items, { folded, synthetic, leftoverNames });
|
|
626
594
|
};
|
|
627
595
|
|
|
628
596
|
const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
|
|
@@ -630,7 +598,7 @@ const proposeDepItems = (pkgText, outdated, audit, extra = {}) => {
|
|
|
630
598
|
if (!pkg) return [];
|
|
631
599
|
const lock = jsonParse(extra.lockText ?? '');
|
|
632
600
|
const used = extra.usedNames;
|
|
633
|
-
const names =
|
|
601
|
+
const names = unusedDeclared(pkg, used).map((dep) => dep.name);
|
|
634
602
|
const exportEntries = loadExportEntries(names, extra, extra.dir);
|
|
635
603
|
const options = { used, exportEntries };
|
|
636
604
|
const changes = buildProposedChanges(pkg, outdated, audit, lock, options);
|
package/lib/diff/inline.js
CHANGED
|
@@ -6,6 +6,7 @@ const { graphemes } = ansi;
|
|
|
6
6
|
const LCS_CELL_LIMIT = 250000;
|
|
7
7
|
const NOISY_TINY_SPANS = 6;
|
|
8
8
|
const WORD_REFINE_MIN = 0.6;
|
|
9
|
+
const WHOLE_MIN_SHARED = 0.25;
|
|
9
10
|
const PIECE_RE = /[A-Za-z_$][\w$]*|\d+|\s+|./g;
|
|
10
11
|
const CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+|./g;
|
|
11
12
|
|
|
@@ -280,6 +281,65 @@ const spansFromWordOps = (ops) => {
|
|
|
280
281
|
};
|
|
281
282
|
};
|
|
282
283
|
|
|
284
|
+
const opRuns = (ops) => {
|
|
285
|
+
const runs = [];
|
|
286
|
+
for (const op of ops) {
|
|
287
|
+
const eq = op.type === 'eq';
|
|
288
|
+
let run = runs[runs.length - 1];
|
|
289
|
+
if (!run || run.eq !== eq) {
|
|
290
|
+
run = { eq, del: [], add: [] };
|
|
291
|
+
runs.push(run);
|
|
292
|
+
}
|
|
293
|
+
if (op.type !== 'add') run.del.push(op.text);
|
|
294
|
+
if (op.type !== 'del') run.add.push(op.text);
|
|
295
|
+
}
|
|
296
|
+
return runs;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const textSize = (parts) => parts.join('').length;
|
|
300
|
+
|
|
301
|
+
const editSize = (run) => Math.max(textSize(run.del), textSize(run.add));
|
|
302
|
+
|
|
303
|
+
const absorbTinyEquals = (runs) => {
|
|
304
|
+
for (let k = 1; k < runs.length - 1; k++) {
|
|
305
|
+
const [before, eq, after] = runs.slice(k - 1, k + 2);
|
|
306
|
+
if (!eq.eq) continue;
|
|
307
|
+
const size = textSize(eq.del);
|
|
308
|
+
if (size > editSize(before) || size > editSize(after)) continue;
|
|
309
|
+
const del = [...before.del, ...eq.del, ...after.del];
|
|
310
|
+
const add = [...before.add, ...eq.add, ...after.add];
|
|
311
|
+
runs.splice(k - 1, 3, { eq: false, del, add });
|
|
312
|
+
k = 0;
|
|
313
|
+
}
|
|
314
|
+
return runs;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const runOps = (runs) => {
|
|
318
|
+
const ops = [];
|
|
319
|
+
for (const run of runs) {
|
|
320
|
+
if (run.eq) {
|
|
321
|
+
for (const text of run.del) ops.push({ type: 'eq', text });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
for (const text of run.del) ops.push({ type: 'del', text });
|
|
325
|
+
for (const text of run.add) ops.push({ type: 'add', text });
|
|
326
|
+
}
|
|
327
|
+
return ops;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const cleanOps = (ops) => runOps(absorbTinyEquals(opRuns(ops)));
|
|
331
|
+
|
|
332
|
+
const sharedShare = (spans) => {
|
|
333
|
+
let shared = 0;
|
|
334
|
+
let total = 0;
|
|
335
|
+
for (const span of spans) {
|
|
336
|
+
const size = span.text.replace(/\s/g, '').length;
|
|
337
|
+
total += size;
|
|
338
|
+
if (!span.changed) shared += size;
|
|
339
|
+
}
|
|
340
|
+
return total === 0 ? 1 : shared / total;
|
|
341
|
+
};
|
|
342
|
+
|
|
283
343
|
const diffChars = (oldText, newText) => {
|
|
284
344
|
const a = splitPieces(oldText);
|
|
285
345
|
const b = splitPieces(newText);
|
|
@@ -296,10 +356,15 @@ const diffChars = (oldText, newText) => {
|
|
|
296
356
|
} else if (midA.length * midB.length > LCS_CELL_LIMIT) {
|
|
297
357
|
mid = wholeChanged(midA, midB);
|
|
298
358
|
} else {
|
|
299
|
-
mid = spansFromWordOps(lcsOps(midA, midB));
|
|
359
|
+
mid = spansFromWordOps(cleanOps(lcsOps(midA, midB)));
|
|
300
360
|
}
|
|
301
361
|
const oldSpans = joinAffix(prefix, mid.oldSpans, suffix);
|
|
302
362
|
const newSpans = joinAffix(prefix, mid.newSpans, suffix);
|
|
363
|
+
const oldShare = sharedShare(oldSpans);
|
|
364
|
+
const newShare = sharedShare(newSpans);
|
|
365
|
+
if (oldShare < WHOLE_MIN_SHARED && newShare < WHOLE_MIN_SHARED) {
|
|
366
|
+
return wholeChanged(a, b);
|
|
367
|
+
}
|
|
303
368
|
return { oldSpans, newSpans };
|
|
304
369
|
};
|
|
305
370
|
|
package/lib/diff/unit.js
CHANGED
|
@@ -128,43 +128,26 @@ const sameBlock = (line, item) => {
|
|
|
128
128
|
return line.blockId === item.blockId;
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
-
const
|
|
132
|
-
let start =
|
|
131
|
+
const changedLineRange = (lines, item, belongs) => {
|
|
132
|
+
let start = -1;
|
|
133
133
|
let end = 0;
|
|
134
|
-
|
|
135
|
-
if (!item) return { start, end };
|
|
134
|
+
if (!item) return { start: 0, end };
|
|
136
135
|
for (let i = 0; i < lines.length; i++) {
|
|
137
136
|
const line = lines[i];
|
|
138
|
-
if (!
|
|
139
|
-
if (
|
|
140
|
-
if (!found) {
|
|
141
|
-
start = i;
|
|
142
|
-
found = true;
|
|
143
|
-
}
|
|
137
|
+
if (line.type === 'ctx' || !belongs(line, item)) continue;
|
|
138
|
+
if (start < 0) start = i;
|
|
144
139
|
end = i;
|
|
145
140
|
}
|
|
146
|
-
return { start, end };
|
|
141
|
+
return { start: Math.max(start, 0), end };
|
|
147
142
|
};
|
|
148
143
|
|
|
144
|
+
const blockLineRange = (lines, item) =>
|
|
145
|
+
changedLineRange(lines, item, sameBlock);
|
|
146
|
+
|
|
149
147
|
const blockLineIndex = (lines, item) => blockLineRange(lines, item).start;
|
|
150
148
|
|
|
151
|
-
const hunkLineRange = (lines, item) =>
|
|
152
|
-
|
|
153
|
-
let end = 0;
|
|
154
|
-
let found = false;
|
|
155
|
-
if (!item) return { start, end };
|
|
156
|
-
for (let i = 0; i < lines.length; i++) {
|
|
157
|
-
const line = lines[i];
|
|
158
|
-
if (!sameHunk(line.item, item)) continue;
|
|
159
|
-
if (line.type === 'ctx') continue;
|
|
160
|
-
if (!found) {
|
|
161
|
-
start = i;
|
|
162
|
-
found = true;
|
|
163
|
-
}
|
|
164
|
-
end = i;
|
|
165
|
-
}
|
|
166
|
-
return { start, end };
|
|
167
|
-
};
|
|
149
|
+
const hunkLineRange = (lines, item) =>
|
|
150
|
+
changedLineRange(lines, item, (line) => sameHunk(line.item, item));
|
|
168
151
|
|
|
169
152
|
const fileOffset = (lines, at) => {
|
|
170
153
|
let offset = 0;
|