reslop 0.2.5 → 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
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
+
5
15
  ## [0.2.5][] - 2026-09-30
6
16
 
7
17
  - Remove UI and filesystem polling timers, refresh on events
@@ -153,7 +163,8 @@
153
163
  - Feedback and todos that produce a repair plan for an agent
154
164
  - Intra-line highlighting in unified, mixed, and side-by-side layouts
155
165
 
156
- [unreleased]: https://github.com/tshemsedinov/reslop/compare/v0.2.5...HEAD
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
157
168
  [0.2.5]: https://github.com/tshemsedinov/reslop/compare/v0.2.4...v0.2.5
158
169
  [0.2.4]: https://github.com/tshemsedinov/reslop/compare/v0.2.3...v0.2.4
159
170
  [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
@@ -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
- let dst = into[key];
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 = cloneColors(DARK_UI);
254
- const CODE_FG = cloneColors(DARK_CODE);
255
- const LOG_COL_FG = cloneColors(DARK_LOG_COLS);
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 > width) break;
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 += `${ESC}[0m`;
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
  };
@@ -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
@@ -44,12 +44,8 @@ const REMOTE_CHANGE = {
44
44
  },
45
45
  };
46
46
 
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
- };
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
- 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);
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
- 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
- };
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 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();
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 repo = attachCapabilities(rawRepo, capabilities);
130
- const extra = {
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 openRemoteSession = (proc, deps, args, selected, env) => {
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 capabilities = capabilitiesFor(selected.kind, {
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(proc.argv.slice(2));
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,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 };
@@ -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 };