reslop 0.2.4 → 0.2.6
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 +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +196 -404
- package/lib/deps.js +240 -399
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +138 -141
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +157 -132
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +16 -26
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased][unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.6][] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
- Add a live dashboard as the start screen
|
|
8
|
+
- Compact related failure contexts in test runnter logs
|
|
9
|
+
- Save the raw command output beside the reduced log
|
|
10
|
+
- Improve test results rendering
|
|
11
|
+
- Update the dashboard from file system events, without timers
|
|
12
|
+
- Record `reslop t` runs under `.log/.runs/` show progress on the dashboard
|
|
13
|
+
- Implement tasks screen: backlog, issues, bugs, features
|
|
14
|
+
|
|
15
|
+
## [0.2.5][] - 2026-09-30
|
|
16
|
+
|
|
17
|
+
- Remove UI and filesystem polling timers, refresh on events
|
|
18
|
+
- Refresh the commits list when HEAD changes outside reslop
|
|
19
|
+
- Update stage/unstage immediately, git/npm in the background
|
|
20
|
+
- Add editor hotkeys Ctrl+X, Ctrl+C, and Ctrl+V
|
|
21
|
+
- Improve diff calculation
|
|
22
|
+
- Improve npm command and test edit/run
|
|
23
|
+
- Refactor internals: simplify and add classes
|
|
24
|
+
- Hothey "i" add to ignore lists: .gitignore, .npmignore
|
|
25
|
+
- Rename TODO to tasks and improve TUI/TUX
|
|
26
|
+
|
|
5
27
|
## [0.2.4][] - 2026-09-28
|
|
6
28
|
|
|
7
29
|
- Add AI instructions to run `npm t` or `reslop t -- node --test`
|
|
@@ -141,7 +163,9 @@
|
|
|
141
163
|
- Feedback and todos that produce a repair plan for an agent
|
|
142
164
|
- Intra-line highlighting in unified, mixed, and side-by-side layouts
|
|
143
165
|
|
|
144
|
-
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.
|
|
166
|
+
[unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...HEAD
|
|
167
|
+
[0.2.6]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...v0.2.6
|
|
168
|
+
[0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
|
|
145
169
|
[0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
|
|
146
170
|
[0.2.3]: https://github.com/tshemsedinov/reslop/compare/v0.2.1...v0.2.3
|
|
147
171
|
[0.2.1]: https://github.com/tshemsedinov/reslop/compare/v0.2.0...v0.2.1
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Timur Shemsedinov
|
|
3
|
+
Copyright (c) 2026 Timur Shemsedinov and other contributors (see github)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -11,28 +11,25 @@
|
|
|
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
|
|
19
18
|
```
|
|
20
19
|
|
|
20
|
+
- Live dashboard: files, diffs, tasks, branches, commits, run, and npm, updated in real time
|
|
21
21
|
- Leave feedback, a project backlog, and in-place code proposals as a repair plan
|
|
22
22
|
- Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR
|
|
23
23
|
- File-scope review mode: not just diffs, with editor mode
|
|
24
24
|
- Stage, unstage, or revert each contiguous block of diff lines
|
|
25
25
|
- Auto-reload local diffs when files change, keeping the current screen
|
|
26
26
|
- Import GitHub PR and GitLab MR review comments into the local plan for AI
|
|
27
|
-
- Commits list: brief and full, view a commit's diff, commit, amend,
|
|
28
|
-
- Branches: checkout, create, rebase,
|
|
27
|
+
- Commits list: brief and full, view a commit's diff, commit, amend, apply, edit, fixup, and delete
|
|
28
|
+
- Branches: checkout, create, rebase, delete, pull, and push
|
|
29
29
|
- npm scripts and bins: run, edit, and reorder; double-click to run; clean logs older than 5 days
|
|
30
|
-
- Long lines wrap in the diff view; the editor scrolls horizontally
|
|
31
30
|
- Reduced test and lint output for agents: failures and a short summary, not passing noise
|
|
32
|
-
- Review
|
|
33
|
-
-
|
|
34
|
-
- Code highlighting and intra-line diffs: unified, mixed, and side-by-side
|
|
35
|
-
- Auto-update patch and minor releases; confirm a new major
|
|
31
|
+
- Review, renew, audit and update npm dependencies in `package.json` and the lockfile
|
|
32
|
+
- Auto-update reslop patch and minor releases; confirm a new major
|
|
36
33
|
- Work with git, npm and fetch in background
|
|
37
34
|
|
|
38
35
|
## Install
|
|
@@ -46,8 +43,8 @@ Requires Node.js `>=18.15.0`.
|
|
|
46
43
|
|
|
47
44
|
## Usage
|
|
48
45
|
|
|
49
|
-
- `reslop`
|
|
50
|
-
- `reslop path/file` show only that path or file
|
|
46
|
+
- `reslop` live dashboard of this repository; press a block key to open its screen
|
|
47
|
+
- `reslop path/file` show only that path or file (starts on the file list)
|
|
51
48
|
- `reslop 7ac260c` show that commit (read-only)
|
|
52
49
|
- `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
|
|
53
50
|
- `reslop -n` start a new review even if the latest is still editing
|
|
@@ -62,10 +59,12 @@ Prefix each program in an npm script with `reslop t --`. Keep `&&` between those
|
|
|
62
59
|
"test": "reslop t -- npm run -s lint && reslop t -- node --test"
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The
|
|
62
|
+
The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The reduced report and the raw output are both saved under `.log/`. The report names the raw file for an agent to read when a detail is missing. Set `RESLOP_OUTPUT=raw` to pass the command through unchanged. The review screen runs scripts in raw mode and applies its own filter once.
|
|
66
63
|
|
|
67
64
|
Run the full check with `npm t`. Run one or more test files with `reslop t -- node --test <files>`. New reviews include those two commands in the agent instructions.
|
|
68
65
|
|
|
66
|
+
`reslop t` records each run in `.log/.runs/` so that the dashboard can show commands started by an agent in another terminal.
|
|
67
|
+
|
|
69
68
|
Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
70
69
|
|
|
71
70
|
- `editing`: still writing the review in reslop
|
|
@@ -88,5 +87,5 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
|
|
|
88
87
|
|
|
89
88
|
## License
|
|
90
89
|
|
|
91
|
-
Copyright (c) 2026 Timur Shemsedinov.
|
|
90
|
+
Copyright (c) 2026 Timur Shemsedinov and other contributors (see github).
|
|
92
91
|
This is [MIT](./LICENSE) licensed software.
|
package/bin/reslop.js
CHANGED
|
@@ -16,12 +16,7 @@ const fail = (reason) => {
|
|
|
16
16
|
process.exit(1);
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
process.on('uncaughtException',
|
|
20
|
-
process.on('unhandledRejection',
|
|
19
|
+
process.on('uncaughtException', fail);
|
|
20
|
+
process.on('unhandledRejection', fail);
|
|
21
21
|
|
|
22
|
-
run(process).then(
|
|
23
|
-
(code) => {
|
|
24
|
-
process.exit(code);
|
|
25
|
-
},
|
|
26
|
-
(error) => fail(error),
|
|
27
|
-
);
|
|
22
|
+
run(process).then((code) => process.exit(code), fail);
|
package/lib/ansi.js
CHANGED
|
@@ -84,6 +84,14 @@ const DARK_UI = {
|
|
|
84
84
|
stagedAddLineBg: [0x26, 0x26, 0x2e],
|
|
85
85
|
stagedAddCharFg: [0xff, 0xff, 0xff],
|
|
86
86
|
stagedAddCharBg: [0x4e, 0x4e, 0x58],
|
|
87
|
+
dashBg: [0x00, 0x00, 0x00],
|
|
88
|
+
dashTileBg: [0x26, 0x26, 0x2a],
|
|
89
|
+
dashRowBg: [0x1c, 0x1c, 0x1f],
|
|
90
|
+
dashHeadBg: [0x70, 0x70, 0x7c],
|
|
91
|
+
dashHeadMuted: [0xf2, 0xf2, 0xfa],
|
|
92
|
+
dashDelFg: [0xff, 0xd2, 0xd2],
|
|
93
|
+
dashTrendFg: [0x4a, 0x62, 0x70],
|
|
94
|
+
dashKeyFg: [0xff, 0xff, 0xff],
|
|
87
95
|
};
|
|
88
96
|
|
|
89
97
|
const DARK_CODE = {
|
|
@@ -176,6 +184,14 @@ const LIGHT_UI = {
|
|
|
176
184
|
stagedAddLineBg: [0xea, 0xec, 0xef],
|
|
177
185
|
stagedAddCharFg: [0x0f, 0x14, 0x19],
|
|
178
186
|
stagedAddCharBg: [0xcc, 0xd1, 0xd7],
|
|
187
|
+
dashBg: [0xff, 0xff, 0xff],
|
|
188
|
+
dashTileBg: [0xe4, 0xe7, 0xeb],
|
|
189
|
+
dashRowBg: [0xd9, 0xdc, 0xe1],
|
|
190
|
+
dashHeadBg: [0xe8, 0xeb, 0xef],
|
|
191
|
+
dashHeadMuted: [0x3e, 0x46, 0x52],
|
|
192
|
+
dashDelFg: [0xd0, 0x3a, 0x44],
|
|
193
|
+
dashTrendFg: [0x8a, 0x98, 0xa6],
|
|
194
|
+
dashKeyFg: [0x00, 0x00, 0x00],
|
|
179
195
|
};
|
|
180
196
|
|
|
181
197
|
const LIGHT_CODE = {
|
|
@@ -233,26 +249,16 @@ const THEME_NAMES = Object.keys(PALETTES);
|
|
|
233
249
|
const applyColors = (into, from) => {
|
|
234
250
|
for (const key of Object.keys(from)) {
|
|
235
251
|
const src = from[key];
|
|
236
|
-
|
|
237
|
-
if (!dst) {
|
|
238
|
-
dst = [0, 0, 0];
|
|
239
|
-
into[key] = dst;
|
|
240
|
-
}
|
|
252
|
+
const dst = into[key];
|
|
241
253
|
dst[0] = src[0];
|
|
242
254
|
dst[1] = src[1];
|
|
243
255
|
dst[2] = src[2];
|
|
244
256
|
}
|
|
245
|
-
return into;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const cloneColors = (colors) => {
|
|
249
|
-
const into = Array.isArray(colors) ? [] : {};
|
|
250
|
-
return applyColors(into, colors);
|
|
251
257
|
};
|
|
252
258
|
|
|
253
|
-
const THEME =
|
|
254
|
-
const CODE_FG =
|
|
255
|
-
const LOG_COL_FG =
|
|
259
|
+
const THEME = structuredClone(DARK_UI);
|
|
260
|
+
const CODE_FG = structuredClone(DARK_CODE);
|
|
261
|
+
const LOG_COL_FG = structuredClone(DARK_LOG_COLS);
|
|
256
262
|
let currentTheme = 'dark';
|
|
257
263
|
|
|
258
264
|
const setTheme = (name) => {
|
|
@@ -339,22 +345,27 @@ const visibleWidth = (text) => {
|
|
|
339
345
|
return width;
|
|
340
346
|
};
|
|
341
347
|
|
|
342
|
-
const truncateVisible = (text, width) => {
|
|
348
|
+
const truncateVisible = (text, width, mark = '') => {
|
|
343
349
|
if (width <= 0) return '';
|
|
344
|
-
if (visibleWidth(text) <= width) return text;
|
|
345
350
|
const plain = stripAnsi(text);
|
|
351
|
+
if (visibleWidth(plain) <= width) return text;
|
|
352
|
+
const markW = mark ? visibleWidth(mark) : 0;
|
|
353
|
+
const fits = markW > 0 && markW <= width;
|
|
354
|
+
const limit = fits ? width - markW : width;
|
|
346
355
|
const parts = graphemes(plain);
|
|
347
356
|
let used = 0;
|
|
348
357
|
let out = '';
|
|
349
358
|
for (const ch of parts) {
|
|
350
359
|
const w = graphemeWidth(ch, used);
|
|
351
|
-
if (used + w >
|
|
360
|
+
if (used + w > limit) break;
|
|
352
361
|
out += ch;
|
|
353
362
|
used += w;
|
|
354
363
|
}
|
|
355
|
-
return out;
|
|
364
|
+
return fits ? `${out}${mark}` : out;
|
|
356
365
|
};
|
|
357
366
|
|
|
367
|
+
const trimVisible = (text, width) => truncateVisible(text, width, '…');
|
|
368
|
+
|
|
358
369
|
const csiLength = (text, index) => {
|
|
359
370
|
if (text[index] !== ESC || text[index + 1] !== '[') return 0;
|
|
360
371
|
let at = index + 2;
|
|
@@ -459,7 +470,7 @@ const foregroundOn = (text, bgRgb) => {
|
|
|
459
470
|
const kept = keepFgParams(sgrParams(seq));
|
|
460
471
|
const reset = kept.includes(0);
|
|
461
472
|
const rest = kept.filter((code) => code !== 0);
|
|
462
|
-
if (reset) out +=
|
|
473
|
+
if (reset) out += RESET;
|
|
463
474
|
out += ground;
|
|
464
475
|
if (rest.length) out += `${ESC}[${rest.join(';')}m`;
|
|
465
476
|
}
|
|
@@ -487,6 +498,8 @@ module.exports = {
|
|
|
487
498
|
graphemeWidth,
|
|
488
499
|
visibleWidth,
|
|
489
500
|
truncateVisible,
|
|
501
|
+
trimVisible,
|
|
502
|
+
firstGrapheme,
|
|
490
503
|
clipAnsi,
|
|
491
504
|
foregroundOn,
|
|
492
505
|
};
|
package/lib/capabilities.js
CHANGED
|
@@ -50,12 +50,14 @@ const BRANCH_METHODS = [
|
|
|
50
50
|
'pushAsync',
|
|
51
51
|
];
|
|
52
52
|
|
|
53
|
+
const VIEW_CAPS = { read: true, changes: false, branches: false, review: true };
|
|
54
|
+
|
|
53
55
|
const SOURCE_CAPS = {
|
|
54
56
|
local: { read: true, changes: true, branches: true, review: true },
|
|
55
|
-
commit:
|
|
56
|
-
pr:
|
|
57
|
-
mr:
|
|
58
|
-
readonly:
|
|
57
|
+
commit: VIEW_CAPS,
|
|
58
|
+
pr: VIEW_CAPS,
|
|
59
|
+
mr: VIEW_CAPS,
|
|
60
|
+
readonly: VIEW_CAPS,
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
const withReadOnly = (caps, readOnly) => {
|
|
@@ -88,21 +90,15 @@ const sessionCapabilities = (options) => {
|
|
|
88
90
|
if (options.capabilities) return withReadOnly(options.capabilities, readOnly);
|
|
89
91
|
if (options.rev) return capabilitiesFor('commit', { readOnly });
|
|
90
92
|
const source = options.change && options.change.source;
|
|
91
|
-
if (
|
|
92
|
-
return capabilitiesFor(source, { readOnly });
|
|
93
|
-
}
|
|
93
|
+
if (SOURCE_CAPS[source]) return capabilitiesFor(source, { readOnly });
|
|
94
94
|
const repoCaps = options.repo && options.repo.capabilities;
|
|
95
95
|
if (repoCaps) return withReadOnly(repoCaps, readOnly);
|
|
96
|
-
return capabilitiesFor(
|
|
96
|
+
return capabilitiesFor('local', { readOnly });
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
module.exports = {
|
|
100
|
-
READ_METHODS,
|
|
101
|
-
CHANGE_METHODS,
|
|
102
|
-
BRANCH_METHODS,
|
|
103
100
|
SOURCE_CAPS,
|
|
104
101
|
capabilitiesFor,
|
|
105
|
-
withReadOnly,
|
|
106
102
|
attachCapabilities,
|
|
107
103
|
sessionCapabilities,
|
|
108
104
|
};
|
package/lib/cli.js
CHANGED
|
@@ -44,12 +44,8 @@ const REMOTE_CHANGE = {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const errorMessage = (reason) =>
|
|
48
|
-
|
|
49
|
-
if (reason === null || reason === undefined) return '';
|
|
50
|
-
if (typeof reason === 'string') return reason;
|
|
51
|
-
return `${reason}`;
|
|
52
|
-
};
|
|
47
|
+
const errorMessage = (reason) =>
|
|
48
|
+
reason instanceof Error ? reason.message : `${reason ?? ''}`;
|
|
53
49
|
|
|
54
50
|
const fail = (stderr, reason) => {
|
|
55
51
|
stderr.write(`reslop: ${errorMessage(reason)}\n`);
|
|
@@ -62,81 +58,44 @@ const parseArgv = (argv) => {
|
|
|
62
58
|
let readOnly = false;
|
|
63
59
|
let theme = 'dark';
|
|
64
60
|
for (const arg of argv) {
|
|
65
|
-
if (arg === '-n')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
readOnly = true;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (arg === '-light') {
|
|
74
|
-
theme = 'light';
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (arg.startsWith('-')) {
|
|
78
|
-
throw new Error(`unknown option ${arg}`);
|
|
79
|
-
}
|
|
80
|
-
paths.push(arg);
|
|
61
|
+
if (arg === '-n') newReview = true;
|
|
62
|
+
else if (arg === '-r') readOnly = true;
|
|
63
|
+
else if (arg === '-light') theme = 'light';
|
|
64
|
+
else if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`);
|
|
65
|
+
else paths.push(arg);
|
|
81
66
|
}
|
|
82
67
|
return { paths, newReview, readOnly, theme };
|
|
83
68
|
};
|
|
84
69
|
|
|
85
|
-
const looksLikeRev = (spec) =>
|
|
86
|
-
|
|
87
|
-
if (HEX_REV.test(spec)) return true;
|
|
88
|
-
if (HEAD_REV.test(spec)) return true;
|
|
89
|
-
return RANGE_REV.test(spec);
|
|
90
|
-
};
|
|
70
|
+
const looksLikeRev = (spec) =>
|
|
71
|
+
HEX_REV.test(spec) || HEAD_REV.test(spec) || RANGE_REV.test(spec);
|
|
91
72
|
|
|
92
73
|
const resolveScope = async (cwd, paths, repo) => {
|
|
93
74
|
if (!paths.length) return { rev: null, paths };
|
|
94
75
|
const first = paths[0];
|
|
95
|
-
if (await exists(path.resolve(cwd, first))) {
|
|
96
|
-
return { rev: null, paths };
|
|
97
|
-
}
|
|
76
|
+
if (await exists(path.resolve(cwd, first))) return { rev: null, paths };
|
|
98
77
|
const resolved = repo.resolveRev(cwd, first);
|
|
99
78
|
if (resolved) {
|
|
100
|
-
if (paths.length > 1)
|
|
101
|
-
throw new Error('use a commit or a path, not both');
|
|
102
|
-
}
|
|
79
|
+
if (paths.length > 1) throw new Error('use a commit or a path, not both');
|
|
103
80
|
return { rev: resolved, paths: [] };
|
|
104
81
|
}
|
|
105
|
-
if (looksLikeRev(first)) {
|
|
106
|
-
throw new Error(`bad revision ${first}`);
|
|
107
|
-
}
|
|
82
|
+
if (looksLikeRev(first)) throw new Error(`bad revision ${first}`);
|
|
108
83
|
return { rev: null, paths };
|
|
109
84
|
};
|
|
110
85
|
|
|
111
|
-
const
|
|
112
|
-
cwd: deps.cwd ?? proc.cwd(),
|
|
113
|
-
stdin: proc.stdin,
|
|
114
|
-
stdout: proc.stdout,
|
|
115
|
-
color: proc.stdout.isTTY === true && !env.NO_COLOR,
|
|
116
|
-
startPane: 'files',
|
|
117
|
-
newReview: args.newReview,
|
|
118
|
-
readOnly: args.readOnly,
|
|
119
|
-
audit: true,
|
|
120
|
-
...extra,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const openLocalSession = async (proc, deps, args, selected, env) => {
|
|
124
|
-
const cwd = deps.cwd ?? proc.cwd();
|
|
86
|
+
const localOptions = async (deps, args, selected, cwd) => {
|
|
125
87
|
const rawRepo = deps.repo ?? createGitRepo();
|
|
126
88
|
const scope = await resolveScope(cwd, selected.paths, rawRepo);
|
|
127
89
|
const kind = scope.rev ? 'commit' : 'local';
|
|
128
90
|
const capabilities = capabilitiesFor(kind, { readOnly: args.readOnly });
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
repo,
|
|
91
|
+
const direct = scope.rev || scope.paths.length;
|
|
92
|
+
return {
|
|
93
|
+
repo: attachCapabilities(rawRepo, capabilities),
|
|
132
94
|
paths: scope.paths,
|
|
133
95
|
rev: scope.rev,
|
|
134
96
|
capabilities,
|
|
97
|
+
startPane: direct ? 'files' : 'dashboard',
|
|
135
98
|
};
|
|
136
|
-
const options = sessionOptions(proc, deps, args, env, extra);
|
|
137
|
-
const session = new Session(options);
|
|
138
|
-
session.ensureRepo();
|
|
139
|
-
return session;
|
|
140
99
|
};
|
|
141
100
|
|
|
142
101
|
const remoteRepo = (deps, selected, env, cwd) => {
|
|
@@ -160,28 +119,34 @@ const remoteRepo = (deps, selected, env, cwd) => {
|
|
|
160
119
|
});
|
|
161
120
|
};
|
|
162
121
|
|
|
163
|
-
const
|
|
122
|
+
const remoteOptions = (deps, args, selected, cwd, env) => ({
|
|
123
|
+
repo: remoteRepo(deps, selected, env, cwd),
|
|
124
|
+
paths: selected.paths,
|
|
125
|
+
capabilities: capabilitiesFor(selected.kind, { readOnly: args.readOnly }),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const loadSession = async (proc, deps, args) => {
|
|
129
|
+
const env = proc.env ?? {};
|
|
164
130
|
const cwd = deps.cwd ?? proc.cwd();
|
|
165
|
-
const
|
|
131
|
+
const selected = selectChangeSource(args.paths);
|
|
132
|
+
const extra = REMOTE_CHANGE[selected.kind]
|
|
133
|
+
? remoteOptions(deps, args, selected, cwd, env)
|
|
134
|
+
: await localOptions(deps, args, selected, cwd);
|
|
135
|
+
const session = new Session({
|
|
136
|
+
cwd,
|
|
137
|
+
stdin: proc.stdin,
|
|
138
|
+
stdout: proc.stdout,
|
|
139
|
+
color: proc.stdout.isTTY === true && !env.NO_COLOR,
|
|
140
|
+
startPane: 'files',
|
|
141
|
+
newReview: args.newReview,
|
|
166
142
|
readOnly: args.readOnly,
|
|
143
|
+
audit: true,
|
|
144
|
+
...extra,
|
|
167
145
|
});
|
|
168
|
-
const repo = remoteRepo(deps, selected, env, cwd);
|
|
169
|
-
const extra = { repo, paths: selected.paths, capabilities };
|
|
170
|
-
const options = sessionOptions(proc, deps, args, env, extra);
|
|
171
|
-
const session = new Session(options);
|
|
172
146
|
session.ensureRepo();
|
|
173
147
|
return session;
|
|
174
148
|
};
|
|
175
149
|
|
|
176
|
-
const loadSession = async (proc, deps, args) => {
|
|
177
|
-
const env = proc.env ?? {};
|
|
178
|
-
const selected = selectChangeSource(args.paths);
|
|
179
|
-
if (REMOTE_CHANGE[selected.kind]) {
|
|
180
|
-
return openRemoteSession(proc, deps, args, selected, env);
|
|
181
|
-
}
|
|
182
|
-
return openLocalSession(proc, deps, args, selected, env);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
150
|
const runUi = async (session, proc) => {
|
|
186
151
|
const code = await session.startUi();
|
|
187
152
|
if (session.loadError) return fail(proc.stderr, session.loadError);
|
|
@@ -196,7 +161,7 @@ const run = async (proc, deps = {}) => {
|
|
|
196
161
|
const stderr = proc.stderr;
|
|
197
162
|
let args;
|
|
198
163
|
try {
|
|
199
|
-
args = parseArgv(
|
|
164
|
+
args = parseArgv(argv);
|
|
200
165
|
} catch (error) {
|
|
201
166
|
fail(stderr, error);
|
|
202
167
|
stderr.write(`${USAGE}\n`);
|
|
@@ -228,5 +193,6 @@ module.exports = {
|
|
|
228
193
|
errorMessage,
|
|
229
194
|
parseArgv,
|
|
230
195
|
resolveScope,
|
|
196
|
+
loadSession,
|
|
231
197
|
run,
|
|
232
198
|
};
|
package/lib/clipboard.js
CHANGED
|
@@ -6,40 +6,70 @@ const { spawnSync } = childProcess;
|
|
|
6
6
|
const ansi = require('./ansi.js');
|
|
7
7
|
const { ESC } = ansi;
|
|
8
8
|
const utilities = require('./utilities.js');
|
|
9
|
-
const {
|
|
9
|
+
const { spawnBase } = utilities;
|
|
10
10
|
|
|
11
11
|
const CLIP_TIMEOUT_MS = 1000;
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const COPY_TOOLS = {
|
|
14
|
+
win32: [{ cmd: 'clip', args: [] }],
|
|
15
|
+
darwin: [{ cmd: 'pbcopy', args: [] }],
|
|
16
|
+
unix: [
|
|
17
|
+
{ cmd: 'wl-copy', args: [] },
|
|
18
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard'] },
|
|
19
|
+
{ cmd: 'xsel', args: ['--clipboard', '--input'] },
|
|
20
|
+
],
|
|
16
21
|
};
|
|
17
22
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
cmd,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
23
|
+
const PASTE_TOOLS = {
|
|
24
|
+
win32: [
|
|
25
|
+
{ cmd: 'powershell', args: ['-NoProfile', '-Command', 'Get-Clipboard'] },
|
|
26
|
+
],
|
|
27
|
+
darwin: [{ cmd: 'pbpaste', args: [] }],
|
|
28
|
+
unix: [
|
|
29
|
+
{ cmd: 'wl-paste', args: ['--no-newline'] },
|
|
30
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard', '-o'] },
|
|
31
|
+
{ cmd: 'xsel', args: ['--clipboard', '--output'] },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let copied = '';
|
|
36
|
+
|
|
37
|
+
const platformTools = (tools) => tools[process.platform] ?? tools.unix;
|
|
38
|
+
|
|
39
|
+
const runTool = (tool, input) =>
|
|
40
|
+
spawnSync(
|
|
41
|
+
tool.cmd,
|
|
42
|
+
tool.args,
|
|
43
|
+
spawnBase({ input, encoding: 'utf8', timeout: CLIP_TIMEOUT_MS }),
|
|
27
44
|
);
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
|
|
46
|
+
const osc52 = (text) => {
|
|
47
|
+
const b64 = Buffer.from(text, 'utf8').toString('base64');
|
|
48
|
+
return `${ESC}]52;c;${b64}\x07`;
|
|
30
49
|
};
|
|
31
50
|
|
|
32
51
|
const copyText = (text, stdout) => {
|
|
33
52
|
if (!text) return false;
|
|
53
|
+
copied = text;
|
|
34
54
|
let ok = false;
|
|
35
55
|
if (stdout && stdout.write) {
|
|
36
56
|
stdout.write(osc52(text));
|
|
37
57
|
ok = true;
|
|
38
58
|
}
|
|
39
|
-
for (const tool of
|
|
40
|
-
|
|
59
|
+
for (const tool of platformTools(COPY_TOOLS)) {
|
|
60
|
+
const result = runTool(tool, text);
|
|
61
|
+
if (!result.error && result.status === 0) return true;
|
|
41
62
|
}
|
|
42
63
|
return ok;
|
|
43
64
|
};
|
|
44
65
|
|
|
45
|
-
|
|
66
|
+
const pasteText = () => {
|
|
67
|
+
for (const tool of platformTools(PASTE_TOOLS)) {
|
|
68
|
+
const result = runTool(tool);
|
|
69
|
+
if (result.error || result.status !== 0) continue;
|
|
70
|
+
if (result.stdout) return result.stdout;
|
|
71
|
+
}
|
|
72
|
+
return copied;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = { osc52, copyText, pasteText };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const worktree = require('../git-worktree.js');
|
|
7
|
+
const { runGitAsync } = worktree;
|
|
8
|
+
|
|
9
|
+
const UNIT = '\x1f';
|
|
10
|
+
const RECENT_COUNT = 8;
|
|
11
|
+
const BRANCH_FORMAT = [
|
|
12
|
+
'%(HEAD)',
|
|
13
|
+
'%(refname:short)',
|
|
14
|
+
'%(objectname:short)',
|
|
15
|
+
'%(committerdate:unix)',
|
|
16
|
+
'%(upstream:short)',
|
|
17
|
+
'%(upstream:track)',
|
|
18
|
+
'%(subject)',
|
|
19
|
+
].join('%09');
|
|
20
|
+
const LAST_FORMAT = ['%H', '%h', '%ct', '%s'].join('%x1f');
|
|
21
|
+
const PROBES = {
|
|
22
|
+
total: ['rev-list', '--count', 'HEAD'],
|
|
23
|
+
recent: ['log', `--max-count=${RECENT_COUNT}`, `--format=${LAST_FORMAT}`],
|
|
24
|
+
refs: ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'],
|
|
25
|
+
dir: ['rev-parse', '--absolute-git-dir'],
|
|
26
|
+
};
|
|
27
|
+
const TRACK_AHEAD = /ahead (\d+)/;
|
|
28
|
+
const TRACK_BEHIND = /behind (\d+)/;
|
|
29
|
+
|
|
30
|
+
const gitText = async (top, args) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await runGitAsync(args, top);
|
|
33
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
34
|
+
} catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const countOf = (text) => {
|
|
40
|
+
const value = parseInt(text, 10);
|
|
41
|
+
return Number.isFinite(value) ? value : 0;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const msOf = (seconds) => (Number.isFinite(seconds) ? seconds * 1000 : 0);
|
|
45
|
+
|
|
46
|
+
const parseLines = (text, parse) => text.split('\n').map(parse).filter(Boolean);
|
|
47
|
+
|
|
48
|
+
const parseTrack = (text) => {
|
|
49
|
+
const raw = `${text ?? ''}`;
|
|
50
|
+
const ahead = TRACK_AHEAD.exec(raw);
|
|
51
|
+
const behind = TRACK_BEHIND.exec(raw);
|
|
52
|
+
return {
|
|
53
|
+
ahead: ahead ? Number(ahead[1]) : 0,
|
|
54
|
+
behind: behind ? Number(behind[1]) : 0,
|
|
55
|
+
gone: raw.includes('gone'),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const parseBranch = (line) => {
|
|
60
|
+
const parts = line.split('\t');
|
|
61
|
+
const name = (parts[1] ?? '').trim();
|
|
62
|
+
if (!name) return null;
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
current: parts[0].includes('*'),
|
|
66
|
+
sha: (parts[2] ?? '').trim(),
|
|
67
|
+
at: msOf(Number(parts[3])),
|
|
68
|
+
upstream: (parts[4] ?? '').trim(),
|
|
69
|
+
...parseTrack(parts[5]),
|
|
70
|
+
subject: parts.slice(6).join('\t').trim(),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const parseCommit = (line) => {
|
|
75
|
+
const parts = line.split(UNIT);
|
|
76
|
+
if (parts.length < 4 || !parts[0]) return null;
|
|
77
|
+
return {
|
|
78
|
+
sha: parts[0],
|
|
79
|
+
short: parts[1],
|
|
80
|
+
at: msOf(Number(parts[2])),
|
|
81
|
+
subject: parts.slice(3).join(UNIT).trim(),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const readNumber = (file) => {
|
|
86
|
+
try {
|
|
87
|
+
return countOf(fs.readFileSync(file, 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const readStep = (dir, step, total) => ({
|
|
94
|
+
step: readNumber(path.join(dir, step)),
|
|
95
|
+
total: readNumber(path.join(dir, total)),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const rebaseState = (gitDir) => {
|
|
99
|
+
if (!gitDir) return null;
|
|
100
|
+
const merge = path.join(gitDir, 'rebase-merge');
|
|
101
|
+
if (fs.existsSync(merge)) return readStep(merge, 'msgnum', 'end');
|
|
102
|
+
const apply = path.join(gitDir, 'rebase-apply');
|
|
103
|
+
if (fs.existsSync(apply)) return readStep(apply, 'next', 'last');
|
|
104
|
+
return null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const readGitSummary = async (top) => {
|
|
108
|
+
const names = Object.keys(PROBES);
|
|
109
|
+
const read = (name) => gitText(top, PROBES[name]);
|
|
110
|
+
const texts = await Promise.all(names.map(read));
|
|
111
|
+
const probes = {};
|
|
112
|
+
for (let i = 0; i < names.length; i++) probes[names[i]] = texts[i];
|
|
113
|
+
const branches = parseLines(probes.refs, parseBranch);
|
|
114
|
+
const current = branches.find((entry) => entry.current);
|
|
115
|
+
const recent = parseLines(probes.recent, parseCommit);
|
|
116
|
+
return {
|
|
117
|
+
branch: current ? current.name : '',
|
|
118
|
+
rebase: rebaseState(probes.dir),
|
|
119
|
+
commits: {
|
|
120
|
+
total: countOf(probes.total),
|
|
121
|
+
last: recent[0] ?? null,
|
|
122
|
+
recent,
|
|
123
|
+
},
|
|
124
|
+
branches,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
module.exports = { readGitSummary, parseTrack };
|