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/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased][unreleased]
4
4
 
5
+ ## [0.2.7][] - 2026-10-03
6
+
7
+ - Add tiling view for multi-repository environment
8
+ - Improve navigation and hotkeys
9
+ - Add npm and dependencies screen
10
+ - Rework tasks: regroup into feature, bug, debt, research, and security
11
+ - Show tasks progress
12
+ - Search file paths in the files and diff screens
13
+
14
+ ## [0.2.6][] - 2026-10-02
15
+
16
+ - Add a live dashboard as the start screen
17
+ - Compact related failure contexts in test runnter logs
18
+ - Save the raw command output beside the reduced log
19
+ - Improve test results rendering
20
+ - Update the dashboard from file system events, without timers
21
+ - Record `reslop t` runs under `.log/.runs/` show progress on the dashboard
22
+ - Implement tasks screen: backlog, issues, bugs, features
23
+
5
24
  ## [0.2.5][] - 2026-09-30
6
25
 
7
26
  - Remove UI and filesystem polling timers, refresh on events
@@ -153,7 +172,9 @@
153
172
  - Feedback and todos that produce a repair plan for an agent
154
173
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
155
174
 
156
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...HEAD
175
+ [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.7...HEAD
176
+ [0.2.7]: https://github.com/tshemsedinov/reslop/compare/v0.2.6...v0.2.7
177
+ [0.2.6]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...v0.2.6
157
178
  [0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
158
179
  [0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
159
180
  [0.2.3]: https://github.com/tshemsedinov/reslop/compare/v0.2.1...v0.2.3
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
@@ -17,21 +17,19 @@ Open a change, leave findings and a backlog, and prepare a repair plan for the a
17
17
  Review → Plan → Repair → Verify
18
18
  ```
19
19
 
20
+ - Live dashboard: files, diffs, tasks, branches, commits, run, and npm, updated in real time
20
21
  - Leave feedback, a project backlog, and in-place code proposals as a repair plan
21
22
  - Review uncommitted git diffs, a given commit, a GitHub PR, or a GitLab MR
22
23
  - File-scope review mode: not just diffs, with editor mode
23
24
  - Stage, unstage, or revert each contiguous block of diff lines
24
25
  - Auto-reload local diffs when files change, keeping the current screen
25
26
  - Import GitHub PR and GitLab MR review comments into the local plan for AI
26
- - Commits list: brief and full, view a commit's diff, commit, amend, fixup, reword, drop, pull, and push
27
- - Branches: checkout, create, rebase, drop, pull, and push
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
28
29
  - npm scripts and bins: run, edit, and reorder; double-click to run; clean logs older than 5 days
29
- - Long lines wrap in the diff view; the editor scrolls horizontally
30
30
  - Reduced test and lint output for agents: failures and a short summary, not passing noise
31
- - Review each npm dependency once across `package.json` and the lockfile
32
- - Propose unused removals, npm audit fixes, and outdated updates as diffs
33
- - Code highlighting and intra-line diffs: unified, mixed, and side-by-side
34
- - 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
35
33
  - Work with git, npm and fetch in background
36
34
 
37
35
  ## Install
@@ -45,8 +43,8 @@ Requires Node.js `>=18.15.0`.
45
43
 
46
44
  ## Usage
47
45
 
48
- - `reslop` uncommitted diffs in this repository
49
- - `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)
50
48
  - `reslop 7ac260c` show that commit (read-only)
51
49
  - `reslop https://github.com/metarhia/metacom/pull/555` GitHub/GitLab PR/MR
52
50
  - `reslop -n` start a new review even if the latest is still editing
@@ -61,10 +59,12 @@ Prefix each program in an npm script with `reslop t --`. Keep `&&` between those
61
59
  "test": "reslop t -- npm run -s lint && reslop t -- node --test"
62
60
  ```
63
61
 
64
- The report is Markdown: failures, diagnostics, and a short summary. Passing results are omitted and similar problems are grouped. The same text is saved under `.log/`. Set `RESLOP_OUTPUT=raw` to pass the command through unchanged. The review screen runs scripts in raw mode and applies its own filter once.
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.
65
63
 
66
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.
67
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
+
68
68
  Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
69
69
 
70
70
  - `editing`: still writing the review in reslop
@@ -87,5 +87,5 @@ Reviews go in `.review/YYYY-MM-DD-NN.md` with frontmatter `status`:
87
87
 
88
88
  ## License
89
89
 
90
- Copyright (c) 2026 Timur Shemsedinov.
90
+ Copyright (c) 2026 Timur Shemsedinov and other contributors (see github).
91
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', (error) => fail(error));
20
- process.on('unhandledRejection', (reason) => fail(reason));
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
@@ -75,6 +75,10 @@ const DARK_UI = {
75
75
  warnFg: [0xe8, 0xc8, 0x4a],
76
76
  mutedFg: [0x7a, 0x7a, 0x8a],
77
77
  shaFg: [0x56, 0x9c, 0xd6],
78
+ pkgDepFg: [0xaf, 0xe0, 0xb9],
79
+ pkgDevFg: [0x9a, 0xc4, 0xe6],
80
+ pkgFg: [0xc8, 0xc8, 0xd2],
81
+ pkgChainFg: [0x55, 0x55, 0x62],
78
82
  shaDarkFg: [0x15, 0x4a, 0x8c],
79
83
  stagedDelLineFg: [0xa0, 0xa0, 0xa8],
80
84
  stagedDelLineBg: [0x20, 0x20, 0x26],
@@ -84,6 +88,30 @@ const DARK_UI = {
84
88
  stagedAddLineBg: [0x26, 0x26, 0x2e],
85
89
  stagedAddCharFg: [0xff, 0xff, 0xff],
86
90
  stagedAddCharBg: [0x4e, 0x4e, 0x58],
91
+ dashBg: [0x00, 0x00, 0x00],
92
+ dashTileBg: [0x26, 0x26, 0x2a],
93
+ dashRowBg: [0x1c, 0x1c, 0x1f],
94
+ dashHeadBg: [0x70, 0x70, 0x7c],
95
+ dashDimHeadBg: [0x30, 0x30, 0x38],
96
+ dashDimTileBg: [0x12, 0x12, 0x14],
97
+ dashDimRowBg: [0x0c, 0x0c, 0x0e],
98
+ dashHeadMuted: [0xf2, 0xf2, 0xfa],
99
+ dashDelFg: [0xff, 0xd2, 0xd2],
100
+ heatTrack: [0x3a, 0x3c, 0x48],
101
+ heat1: [0x1c, 0x3a, 0x32],
102
+ heat2: [0x2a, 0x5a, 0x38],
103
+ heat3: [0x8a, 0x6a, 0x18],
104
+ heat4: [0xa0, 0x3a, 0x28],
105
+ heatFg: [0xf4, 0xf4, 0xf8],
106
+ heatBlink: [0xf2, 0xe2, 0x8a],
107
+ heatBlinkFg: [0x1a, 0x16, 0x08],
108
+ dashKeyFg: [0xff, 0xff, 0xff],
109
+ taskHeadFg: [0xff, 0xff, 0xff],
110
+ taskHeadBg: [0x3c, 0x3c, 0x48],
111
+ searchFg: [0x1a, 0x16, 0x08],
112
+ searchBg: [0x8a, 0x6a, 0x18],
113
+ searchOnFg: [0x1a, 0x16, 0x08],
114
+ searchOnBg: [0xf2, 0xe2, 0x8a],
87
115
  };
88
116
 
89
117
  const DARK_CODE = {
@@ -167,6 +195,10 @@ const LIGHT_UI = {
167
195
  warnFg: [0x9a, 0x67, 0x00],
168
196
  mutedFg: [0x6e, 0x77, 0x81],
169
197
  shaFg: [0x05, 0x50, 0xae],
198
+ pkgDepFg: [0x3c, 0xa8, 0x5a],
199
+ pkgDevFg: [0x3d, 0x7e, 0xc8],
200
+ pkgFg: [0x9a, 0xa2, 0xac],
201
+ pkgChainFg: [0x3a, 0x42, 0x4c],
170
202
  shaDarkFg: [0x0b, 0x3d, 0x70],
171
203
  stagedDelLineFg: [0x57, 0x60, 0x6a],
172
204
  stagedDelLineBg: [0xf3, 0xf4, 0xf6],
@@ -176,6 +208,30 @@ const LIGHT_UI = {
176
208
  stagedAddLineBg: [0xea, 0xec, 0xef],
177
209
  stagedAddCharFg: [0x0f, 0x14, 0x19],
178
210
  stagedAddCharBg: [0xcc, 0xd1, 0xd7],
211
+ dashBg: [0xff, 0xff, 0xff],
212
+ dashTileBg: [0xe4, 0xe7, 0xeb],
213
+ dashRowBg: [0xd9, 0xdc, 0xe1],
214
+ dashHeadBg: [0xe8, 0xeb, 0xef],
215
+ dashDimHeadBg: [0xb0, 0xb6, 0xbe],
216
+ dashDimTileBg: [0xc4, 0xc8, 0xce],
217
+ dashDimRowBg: [0xb0, 0xb4, 0xba],
218
+ dashHeadMuted: [0x3e, 0x46, 0x52],
219
+ dashDelFg: [0xd0, 0x3a, 0x44],
220
+ heatTrack: [0xc5, 0xca, 0xd3],
221
+ heat1: [0xc8, 0xe6, 0xd4],
222
+ heat2: [0x7d, 0xc8, 0x96],
223
+ heat3: [0xe6, 0xb4, 0x3c],
224
+ heat4: [0xe0, 0x78, 0x68],
225
+ heatFg: [0x1f, 0x23, 0x28],
226
+ heatBlink: [0xff, 0xf3, 0xb0],
227
+ heatBlinkFg: [0x1a, 0x16, 0x08],
228
+ dashKeyFg: [0x00, 0x00, 0x00],
229
+ taskHeadFg: [0xff, 0xff, 0xff],
230
+ taskHeadBg: [0x6e, 0x77, 0x81],
231
+ searchFg: [0x1a, 0x16, 0x08],
232
+ searchBg: [0xf0, 0xc8, 0x6a],
233
+ searchOnFg: [0x1a, 0x16, 0x08],
234
+ searchOnBg: [0xff, 0xe1, 0x4a],
179
235
  };
180
236
 
181
237
  const LIGHT_CODE = {
@@ -233,26 +289,16 @@ const THEME_NAMES = Object.keys(PALETTES);
233
289
  const applyColors = (into, from) => {
234
290
  for (const key of Object.keys(from)) {
235
291
  const src = from[key];
236
- let dst = into[key];
237
- if (!dst) {
238
- dst = [0, 0, 0];
239
- into[key] = dst;
240
- }
292
+ const dst = into[key];
241
293
  dst[0] = src[0];
242
294
  dst[1] = src[1];
243
295
  dst[2] = src[2];
244
296
  }
245
- return into;
246
- };
247
-
248
- const cloneColors = (colors) => {
249
- const into = Array.isArray(colors) ? [] : {};
250
- return applyColors(into, colors);
251
297
  };
252
298
 
253
- const THEME = cloneColors(DARK_UI);
254
- const CODE_FG = cloneColors(DARK_CODE);
255
- const LOG_COL_FG = cloneColors(DARK_LOG_COLS);
299
+ const THEME = structuredClone(DARK_UI);
300
+ const CODE_FG = structuredClone(DARK_CODE);
301
+ const LOG_COL_FG = structuredClone(DARK_LOG_COLS);
256
302
  let currentTheme = 'dark';
257
303
 
258
304
  const setTheme = (name) => {
@@ -339,22 +385,27 @@ const visibleWidth = (text) => {
339
385
  return width;
340
386
  };
341
387
 
342
- const truncateVisible = (text, width) => {
388
+ const truncateVisible = (text, width, mark = '') => {
343
389
  if (width <= 0) return '';
344
- if (visibleWidth(text) <= width) return text;
345
390
  const plain = stripAnsi(text);
391
+ if (visibleWidth(plain) <= width) return text;
392
+ const markW = mark ? visibleWidth(mark) : 0;
393
+ const fits = markW > 0 && markW <= width;
394
+ const limit = fits ? width - markW : width;
346
395
  const parts = graphemes(plain);
347
396
  let used = 0;
348
397
  let out = '';
349
398
  for (const ch of parts) {
350
399
  const w = graphemeWidth(ch, used);
351
- if (used + w > width) break;
400
+ if (used + w > limit) break;
352
401
  out += ch;
353
402
  used += w;
354
403
  }
355
- return out;
404
+ return fits ? `${out}${mark}` : out;
356
405
  };
357
406
 
407
+ const trimVisible = (text, width) => truncateVisible(text, width, '…');
408
+
358
409
  const csiLength = (text, index) => {
359
410
  if (text[index] !== ESC || text[index + 1] !== '[') return 0;
360
411
  let at = index + 2;
@@ -459,7 +510,7 @@ const foregroundOn = (text, bgRgb) => {
459
510
  const kept = keepFgParams(sgrParams(seq));
460
511
  const reset = kept.includes(0);
461
512
  const rest = kept.filter((code) => code !== 0);
462
- if (reset) out += `${ESC}[0m`;
513
+ if (reset) out += RESET;
463
514
  out += ground;
464
515
  if (rest.length) out += `${ESC}[${rest.join(';')}m`;
465
516
  }
@@ -487,6 +538,8 @@ module.exports = {
487
538
  graphemeWidth,
488
539
  visibleWidth,
489
540
  truncateVisible,
541
+ trimVisible,
542
+ firstGrapheme,
490
543
  clipAnsi,
491
544
  foregroundOn,
492
545
  };
package/lib/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
-
2
+ // a
3
3
  const ansi = require('./ansi.js');
4
4
  const diff = require('./diff/diff.js');
5
5
  const git = require('./git.js');
@@ -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: { read: true, changes: false, branches: false, review: true },
56
- pr: { read: true, changes: false, branches: false, review: true },
57
- mr: { read: true, changes: false, branches: false, review: true },
58
- readonly: { read: true, changes: false, branches: false, review: true },
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,12 +90,10 @@ 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 (source && SOURCE_CAPS[source]) {
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(readOnly ? 'readonly' : 'local');
96
+ return capabilitiesFor('local', { readOnly });
97
97
  };
98
98
 
99
99
  module.exports = {
package/lib/cli.js CHANGED
@@ -21,6 +21,8 @@ const ansi = require('./ansi.js');
21
21
  const { setTheme } = ansi;
22
22
  const reportRun = require('./report-run.js');
23
23
  const { runCapture } = reportRun;
24
+ const workspace = require('./workspace.js');
25
+ const { workspaceFromArgs } = workspace;
24
26
 
25
27
  const HEX_REV = /^[0-9a-f]{4,40}$/i;
26
28
  const HEAD_REV = /^HEAD([~^].*)?$/i;
@@ -44,12 +46,8 @@ const REMOTE_CHANGE = {
44
46
  },
45
47
  };
46
48
 
47
- const errorMessage = (reason) => {
48
- if (reason instanceof Error) return reason.message;
49
- if (reason === null || reason === undefined) return '';
50
- if (typeof reason === 'string') return reason;
51
- return `${reason}`;
52
- };
49
+ const errorMessage = (reason) =>
50
+ reason instanceof Error ? reason.message : `${reason ?? ''}`;
53
51
 
54
52
  const fail = (stderr, reason) => {
55
53
  stderr.write(`reslop: ${errorMessage(reason)}\n`);
@@ -62,81 +60,58 @@ const parseArgv = (argv) => {
62
60
  let readOnly = false;
63
61
  let theme = 'dark';
64
62
  for (const arg of argv) {
65
- if (arg === '-n') {
66
- newReview = true;
67
- continue;
68
- }
69
- if (arg === '-r') {
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);
63
+ if (arg === '-n') newReview = true;
64
+ else if (arg === '-r') readOnly = true;
65
+ else if (arg === '-light') theme = 'light';
66
+ else if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`);
67
+ else paths.push(arg);
81
68
  }
82
69
  return { paths, newReview, readOnly, theme };
83
70
  };
84
71
 
85
- const looksLikeRev = (spec) => {
86
- if (!spec) return false;
87
- if (HEX_REV.test(spec)) return true;
88
- if (HEAD_REV.test(spec)) return true;
89
- return RANGE_REV.test(spec);
90
- };
72
+ const looksLikeRev = (spec) =>
73
+ HEX_REV.test(spec) || HEAD_REV.test(spec) || RANGE_REV.test(spec);
91
74
 
92
75
  const resolveScope = async (cwd, paths, repo) => {
93
76
  if (!paths.length) return { rev: null, paths };
94
77
  const first = paths[0];
95
- if (await exists(path.resolve(cwd, first))) {
96
- return { rev: null, paths };
97
- }
78
+ if (await exists(path.resolve(cwd, first))) return { rev: null, paths };
98
79
  const resolved = repo.resolveRev(cwd, first);
99
80
  if (resolved) {
100
- if (paths.length > 1) {
101
- throw new Error('use a commit or a path, not both');
102
- }
81
+ if (paths.length > 1) throw new Error('use a commit or a path, not both');
103
82
  return { rev: resolved, paths: [] };
104
83
  }
105
- if (looksLikeRev(first)) {
106
- throw new Error(`bad revision ${first}`);
107
- }
84
+ if (looksLikeRev(first)) throw new Error(`bad revision ${first}`);
108
85
  return { rev: null, paths };
109
86
  };
110
87
 
111
- const sessionOptions = (proc, deps, args, env, extra) => ({
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();
88
+ const localOptions = async (deps, args, selected, cwd) => {
125
89
  const rawRepo = deps.repo ?? createGitRepo();
90
+ const found = workspaceFromArgs(cwd, selected.paths);
91
+ if (found) {
92
+ const capabilities = capabilitiesFor('local', { readOnly: args.readOnly });
93
+ return {
94
+ cwd: found.root,
95
+ repo: attachCapabilities(rawRepo, capabilities),
96
+ paths: [],
97
+ rev: null,
98
+ capabilities,
99
+ startPane: 'repos',
100
+ workspace: found,
101
+ repoName: path.basename(found.root),
102
+ };
103
+ }
126
104
  const scope = await resolveScope(cwd, selected.paths, rawRepo);
127
105
  const kind = scope.rev ? 'commit' : 'local';
128
106
  const capabilities = capabilitiesFor(kind, { readOnly: args.readOnly });
129
- const repo = attachCapabilities(rawRepo, capabilities);
130
- const extra = {
131
- repo,
107
+ const direct = scope.rev || scope.paths.length;
108
+ return {
109
+ repo: attachCapabilities(rawRepo, capabilities),
132
110
  paths: scope.paths,
133
111
  rev: scope.rev,
134
112
  capabilities,
113
+ startPane: direct ? 'files' : 'dashboard',
135
114
  };
136
- const options = sessionOptions(proc, deps, args, env, extra);
137
- const session = new Session(options);
138
- session.ensureRepo();
139
- return session;
140
115
  };
141
116
 
142
117
  const remoteRepo = (deps, selected, env, cwd) => {
@@ -160,26 +135,32 @@ const remoteRepo = (deps, selected, env, cwd) => {
160
135
  });
161
136
  };
162
137
 
163
- const openRemoteSession = (proc, deps, args, selected, env) => {
164
- const cwd = deps.cwd ?? proc.cwd();
165
- const capabilities = capabilitiesFor(selected.kind, {
166
- readOnly: args.readOnly,
167
- });
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
- session.ensureRepo();
173
- return session;
174
- };
138
+ const remoteOptions = (deps, args, selected, cwd, env) => ({
139
+ repo: remoteRepo(deps, selected, env, cwd),
140
+ paths: selected.paths,
141
+ capabilities: capabilitiesFor(selected.kind, { readOnly: args.readOnly }),
142
+ });
175
143
 
176
144
  const loadSession = async (proc, deps, args) => {
177
145
  const env = proc.env ?? {};
146
+ const cwd = deps.cwd ?? proc.cwd();
178
147
  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);
148
+ const extra = REMOTE_CHANGE[selected.kind]
149
+ ? remoteOptions(deps, args, selected, cwd, env)
150
+ : await localOptions(deps, args, selected, cwd);
151
+ const session = new Session({
152
+ cwd,
153
+ stdin: proc.stdin,
154
+ stdout: proc.stdout,
155
+ color: proc.stdout.isTTY === true && !env.NO_COLOR,
156
+ startPane: 'files',
157
+ newReview: args.newReview,
158
+ readOnly: args.readOnly,
159
+ audit: true,
160
+ ...extra,
161
+ });
162
+ if (session.pane !== 'repos') session.ensureRepo();
163
+ return session;
183
164
  };
184
165
 
185
166
  const runUi = async (session, proc) => {
@@ -196,7 +177,7 @@ const run = async (proc, deps = {}) => {
196
177
  const stderr = proc.stderr;
197
178
  let args;
198
179
  try {
199
- args = parseArgv(proc.argv.slice(2));
180
+ args = parseArgv(argv);
200
181
  } catch (error) {
201
182
  fail(stderr, error);
202
183
  stderr.write(`${USAGE}\n`);
@@ -211,6 +192,10 @@ const run = async (proc, deps = {}) => {
211
192
  }
212
193
  const tty = proc.stdin.isTTY === true && stdout.isTTY === true;
213
194
  if (tty) return runUi(session, proc);
195
+ if (session.pane === 'repos') {
196
+ stderr.write('reslop: interactive terminal required\n');
197
+ return 1;
198
+ }
214
199
  try {
215
200
  await session.loadReady();
216
201
  } catch (error) {
@@ -228,5 +213,6 @@ module.exports = {
228
213
  errorMessage,
229
214
  parseArgv,
230
215
  resolveScope,
216
+ loadSession,
231
217
  run,
232
218
  };
package/lib/clipboard.js CHANGED
@@ -6,62 +6,46 @@ const { spawnSync } = childProcess;
6
6
  const ansi = require('./ansi.js');
7
7
  const { ESC } = ansi;
8
8
  const utilities = require('./utilities.js');
9
- const { clipTools, spawnBase } = utilities;
9
+ const { spawnBase } = utilities;
10
10
 
11
11
  const CLIP_TIMEOUT_MS = 1000;
12
12
 
13
- const osc52 = (text) => {
14
- const b64 = Buffer.from(text, 'utf8').toString('base64');
15
- return `${ESC}]52;c;${b64}\x07`;
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 runClip = (cmd, args, text) => {
19
- const result = spawnSync(
20
- cmd,
21
- args,
22
- spawnBase({
23
- input: text,
24
- encoding: 'utf8',
25
- timeout: CLIP_TIMEOUT_MS,
26
- }),
27
- );
28
- if (result.error) return false;
29
- return result.status === 0;
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
+ ],
30
33
  };
31
34
 
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
35
  let copied = '';
39
36
 
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
- };
37
+ const platformTools = (tools) => tools[process.platform] ?? tools.unix;
48
38
 
49
- const readPaste = (cmd, args) => {
50
- const result = spawnSync(
51
- cmd,
52
- args,
53
- spawnBase({ encoding: 'utf8', timeout: CLIP_TIMEOUT_MS }),
39
+ const runTool = (tool, input) =>
40
+ spawnSync(
41
+ tool.cmd,
42
+ tool.args,
43
+ spawnBase({ input, encoding: 'utf8', timeout: CLIP_TIMEOUT_MS }),
54
44
  );
55
- if (result.error || result.status !== 0) return '';
56
- return result.stdout || '';
57
- };
58
45
 
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 '';
46
+ const osc52 = (text) => {
47
+ const b64 = Buffer.from(text, 'utf8').toString('base64');
48
+ return `${ESC}]52;c;${b64}\x07`;
65
49
  };
66
50
 
67
51
  const copyText = (text, stdout) => {
@@ -72,12 +56,20 @@ const copyText = (text, stdout) => {
72
56
  stdout.write(osc52(text));
73
57
  ok = true;
74
58
  }
75
- for (const tool of clipTools()) {
76
- if (runClip(tool.cmd, tool.args, text)) return true;
59
+ for (const tool of platformTools(COPY_TOOLS)) {
60
+ const result = runTool(tool, text);
61
+ if (!result.error && result.status === 0) return true;
77
62
  }
78
63
  return ok;
79
64
  };
80
65
 
81
- const pasteText = () => systemPaste() || copied;
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
+ };
82
74
 
83
75
  module.exports = { osc52, copyText, pasteText };