reslop 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/session.js CHANGED
@@ -9,19 +9,19 @@ const review = require('./review.js');
9
9
  const wrap = require('./wrap.js');
10
10
  const { extractText, isRange } = require('./select.js');
11
11
  const { copyText } = require('./clipboard.js');
12
+ const { watchResize } = require('./sys.js');
12
13
  const { fileEntries, itemPath } = require('./files.js');
13
14
  const { Editor } = require('./editor.js');
14
- const { refreshIndexPatches } = require('./diff.js');
15
+ const { refreshIndexPatches, blockAddText } = require('./diff.js');
15
16
 
16
17
  const { renderFrame, presentRows, presentCursor } = render;
17
- const { noteInnerWidth, todoInnerWidth } = render;
18
- const { QUIT_FINISH, QUIT_CONTINUE } = render;
18
+ const { noteInnerWidth, todoInnerWidth, codeInnerWidth } = render;
19
19
  const { actionFromKey, hitAction, decodeChunk } = keys;
20
- const { FILES_DISABLED } = keys;
20
+ const { disabledActions } = keys;
21
21
  const { cursorInWrap, wrapDoc, wrapMove } = wrap;
22
22
  const { AUTOSAVE_MS, listReviewNames, loadTemplates } = review;
23
23
  const { resolveReviewPath, loadReview, createStore } = review;
24
- const { noteCounts, hasNotes, setFeedback, addTodo } = review;
24
+ const { noteCounts, hasNotes, setFeedback, setCode, addTodo } = review;
25
25
  const { applyImportedNotes } = review;
26
26
  const { setTodoText, removeTodo, flushReview, mergeTodos } = review;
27
27
  const { feedbackKey, checkLabel, rankedTemplates } = review;
@@ -29,6 +29,10 @@ const { prefixTemplates, TEMPLATE_SHOW, rememberTemplate } = review;
29
29
 
30
30
  const LAYOUTS = ['unified', 'mixed', 'side'];
31
31
  const READ_ONLY_ORIGINS = ['commit', 'pr'];
32
+ const SOURCE_KIND = {
33
+ 'github-pr': 'pr',
34
+ 'gitlab-mr': 'mr',
35
+ };
32
36
  const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
33
37
  const LEAVE_TERM =
34
38
  '\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
@@ -82,6 +86,13 @@ const itemFeedKey = (item) => {
82
86
  return feedbackKey({ file, oldStart, newStart, blockId });
83
87
  };
84
88
 
89
+ const canEditCode = (item) => {
90
+ if (!item || item.origin === 'todo') return false;
91
+ if (!item.hunk || !item.file) return false;
92
+ if (item.file.isBinary) return false;
93
+ return true;
94
+ };
95
+
85
96
  const isAbortError = (error) => !!(error && error.code === 'ABORT');
86
97
 
87
98
  const sameCursor = (a, b) => {
@@ -91,12 +102,26 @@ const sameCursor = (a, b) => {
91
102
  };
92
103
 
93
104
  const QUIT_CONFIRM = {
94
- [QUIT_FINISH]: (session) => session.finishQuit('ready'),
95
- [QUIT_CONTINUE]: (session) => session.finishQuit('editing'),
105
+ f: (session) => session.finishQuit('ready'),
106
+ c: (session) => session.finishQuit('editing'),
107
+ d: (session) => session.discardQuit(),
96
108
  'ctrl-c': (session) => session.finishQuit('ready'),
97
109
  escape: (session) => session.cancelQuit(),
98
110
  };
99
111
 
112
+ const COMMIT_CONFIRM = {
113
+ c: (session) => session.startGitCommit('commit'),
114
+ a: (session) => session.startGitCommit('amend'),
115
+ f: (session) => session.startGitCommit('fixup'),
116
+ escape: (session) => session.cancelCommit(),
117
+ };
118
+
119
+ const COMMIT_STATUS = {
120
+ commit: 'committed',
121
+ amend: 'amended',
122
+ fixup: 'fixup',
123
+ };
124
+
100
125
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
101
126
 
102
127
  const COMPOSE_SPECIAL = {
@@ -112,7 +137,7 @@ const COMPOSE_SPECIAL = {
112
137
  'ctrl-e': (session) => session.editor.end(),
113
138
  'ctrl-z': (session) => session.editor.undoEdit(),
114
139
  'ctrl-y': (session) => session.editor.redoEdit(),
115
- tab: (session) => session.applyTemplate(),
140
+ tab: (session) => session.composeTab(),
116
141
  };
117
142
 
118
143
  const HANDLERS = {
@@ -125,6 +150,9 @@ const HANDLERS = {
125
150
  files: (session) => session.onFiles(),
126
151
  feedback: (session) => session.onFeedback(),
127
152
  todo: (session) => session.onTodo(),
153
+ code: (session) => session.onCode(),
154
+ reload: (session) => session.onReload(),
155
+ commit: (session) => session.onCommit(),
128
156
  open: (session) => session.onOpenFile(),
129
157
  removeTodo: (session) => session.removeFocusedTodo(),
130
158
  quit: (session) => session.onQuit(),
@@ -138,9 +166,12 @@ const HANDLERS = {
138
166
 
139
167
  const KEY_MODE = {
140
168
  confirmQuit: (session, key) => session.onConfirmQuit(key),
169
+ confirmCommit: (session, key) => session.onConfirmCommit(key),
141
170
  compose: (session, key) => session.handleComposeKey(key),
142
171
  };
143
172
 
173
+ const COMPOSE_LEAVE = ['next', 'prev', 'todo'];
174
+
144
175
  const MOUSE_KIND = {
145
176
  wheelUp: (session) => session.onScroll(-1),
146
177
  wheelDown: (session) => session.onScroll(1),
@@ -195,8 +226,10 @@ class Session {
195
226
  this.readFileSync = options.readFileSync ?? fs.readFileSync;
196
227
  this.writeFileSync = options.writeFileSync ?? fs.writeFileSync;
197
228
  this.mkdirSync = options.mkdirSync ?? fs.mkdirSync;
229
+ this.unlinkSync = options.unlinkSync ?? fs.unlinkSync;
198
230
  this.now = options.now ?? systemNow;
199
231
  this.forceNewReview = options.newReview === true;
232
+ this.readOnly = options.readOnly === true;
200
233
  this.audit = options.audit === true;
201
234
  this.outdatedMap = options.outdatedMap;
202
235
  this.auditMap = options.auditMap;
@@ -230,6 +263,7 @@ class Session {
230
263
  this.mode = 'review';
231
264
  this.editor = null;
232
265
  this.composeKind = null;
266
+ this.commitKind = null;
233
267
  this.composeTodoId = null;
234
268
  this.composeBaseline = '';
235
269
  this.todoFocus = 0;
@@ -524,6 +558,7 @@ class Session {
524
558
  pr: 0,
525
559
  todo: 0,
526
560
  feedback: 0,
561
+ code: 0,
527
562
  };
528
563
  for (const item of this.items) {
529
564
  if (!Object.hasOwn(counts, item.origin)) continue;
@@ -532,13 +567,14 @@ class Session {
532
567
  const notes = noteCounts(this.notes);
533
568
  counts.feedback = notes.feedback;
534
569
  counts.todo = notes.todo;
570
+ counts.code = notes.code;
535
571
  return counts;
536
572
  }
537
573
 
538
574
  view() {
539
575
  const templates = this.shownTemplates();
540
576
  const change = this.change;
541
- const isGithubPr = change && change.source === 'github-pr';
577
+ const sourceKind = (change && SOURCE_KIND[change.source]) || '';
542
578
  const repoName = this.repoName || path.basename(this.top || this.cwd || '');
543
579
  return {
544
580
  item: this.current(),
@@ -556,13 +592,14 @@ class Session {
556
592
  repoName,
557
593
  revShort: this.revShort || '',
558
594
  sourceLabel: this.sourceLabel || '',
559
- sourceKind: isGithubPr ? 'pr' : '',
595
+ sourceKind,
560
596
  mode: this.mode,
561
597
  compose: this.composeView(),
562
598
  noteText: this.idleNoteText(),
563
599
  todos: this.todoTexts(),
564
600
  todoFocus: this.clampedTodoFocus(),
565
601
  todoEdit: this.todoEditView(),
602
+ codeOverlay: this.codeOverlayView(),
566
603
  templates,
567
604
  templateIndex: this.clampedTemplateIndex(templates),
568
605
  };
@@ -595,18 +632,18 @@ class Session {
595
632
 
596
633
  dispatch(action) {
597
634
  if (this.mode === 'confirmQuit') return;
635
+ if (this.mode === 'confirmCommit') return;
598
636
  if (this.mode === 'compose') {
599
- if (action !== 'next' && action !== 'prev' && action !== 'todo') {
600
- return;
601
- }
637
+ if (!COMPOSE_LEAVE.includes(action)) return;
602
638
  this.commitCompose();
603
639
  this.closeCompose();
604
640
  }
605
641
  if (this.pane === 'files') {
606
642
  if (action === 'next') return void this.onFileMove(1);
607
643
  if (action === 'prev') return void this.onFileMove(-1);
608
- if (FILES_DISABLED.includes(action)) return;
609
644
  }
645
+ const hidden = disabledActions(this.pane, this.current());
646
+ if (hidden.includes(action)) return;
610
647
  const handler = HANDLERS[action];
611
648
  if (!handler) return;
612
649
  handler(this);
@@ -616,8 +653,7 @@ class Session {
616
653
  return this.items[this.index] ?? null;
617
654
  }
618
655
 
619
- reloadAfterChange() {
620
- const here = this.index;
656
+ refreshFromRepo() {
621
657
  this.loadPromise = null;
622
658
  this.bumpLoad();
623
659
  const options = this.loadOptions({
@@ -625,10 +661,6 @@ class Session {
625
661
  });
626
662
  const loaded = this.repo.load(this.cwd, this.paths, options);
627
663
  this.applyLoaded(loaded);
628
- this.index = here;
629
- this.clampIndex();
630
- this.syncReviewPath();
631
- this.syncFileCursor();
632
664
  this.scroll = 0;
633
665
  this.clearSelection();
634
666
  if (loaded.pending) {
@@ -639,6 +671,22 @@ class Session {
639
671
  this.finishIfEmpty('reload');
640
672
  }
641
673
 
674
+ reloadAfterChange() {
675
+ const here = this.index;
676
+ this.refreshFromRepo();
677
+ this.index = here;
678
+ this.clampIndex();
679
+ this.syncReviewPath();
680
+ this.syncFileCursor();
681
+ }
682
+
683
+ onReload() {
684
+ this.dismissed.clear();
685
+ this.status = '';
686
+ this.refreshFromRepo();
687
+ if (!this.done && !this.pendingExtras) this.status = 'reloaded';
688
+ }
689
+
642
690
  clearSelection() {
643
691
  this.selection = null;
644
692
  this.mouseAnchor = null;
@@ -692,16 +740,20 @@ class Session {
692
740
  return items;
693
741
  }
694
742
 
695
- guardFileGit(items) {
696
- if (!items.length) return false;
697
- const first = items[0];
698
- if (isReadOnlyOrigin(first.origin)) {
743
+ guardWritable(item) {
744
+ if (!item) return false;
745
+ if (this.readOnly || isReadOnlyOrigin(item.origin)) {
699
746
  this.status = 'read only';
700
747
  return false;
701
748
  }
702
749
  return true;
703
750
  }
704
751
 
752
+ guardFileGit(items) {
753
+ if (!items.length) return false;
754
+ return this.guardWritable(items[0]);
755
+ }
756
+
705
757
  applyFileGit(items, label, write, afterItem) {
706
758
  try {
707
759
  for (const item of items) {
@@ -785,7 +837,7 @@ class Session {
785
837
  let ok = false;
786
838
  try {
787
839
  this.repo.revertFile(this.top, rel, items);
788
- this.status = 'reverted';
840
+ this.status = 'dropped';
789
841
  ok = true;
790
842
  } catch (error) {
791
843
  this.status = error.message;
@@ -797,11 +849,7 @@ class Session {
797
849
  onAdd() {
798
850
  if (this.pane === 'files') return void this.onFileAdd();
799
851
  const item = this.activeDiffItem();
800
- if (!item) return;
801
- if (isReadOnlyOrigin(item.origin)) {
802
- this.status = 'read only';
803
- return;
804
- }
852
+ if (!this.guardWritable(item)) return;
805
853
  if (item.origin === 'staged') {
806
854
  this.status = 'already staged';
807
855
  return;
@@ -823,11 +871,7 @@ class Session {
823
871
  onUnstage() {
824
872
  if (this.pane === 'files') return void this.onFileUnstage();
825
873
  const item = this.activeDiffItem();
826
- if (!item) return;
827
- if (isReadOnlyOrigin(item.origin)) {
828
- this.status = 'read only';
829
- return;
830
- }
874
+ if (!this.guardWritable(item)) return;
831
875
  if (item.origin !== 'staged') {
832
876
  this.status = 'not staged';
833
877
  return;
@@ -844,14 +888,10 @@ class Session {
844
888
  onRevert() {
845
889
  if (this.pane === 'files') return void this.onFileRevert();
846
890
  const item = this.activeDiffItem();
847
- if (!item) return;
848
- if (isReadOnlyOrigin(item.origin)) {
849
- this.status = 'read only';
850
- return;
851
- }
891
+ if (!this.guardWritable(item)) return;
852
892
  this.applyGit(
853
893
  item,
854
- 'reverted',
894
+ 'dropped',
855
895
  (top, current) => this.repo.revert(top, current),
856
896
  () => {
857
897
  if (needsReload(item)) this.dismissed.add(itemKey(item));
@@ -913,6 +953,23 @@ class Session {
913
953
  this.exitCode = 0;
914
954
  }
915
955
 
956
+ removeReviewFile() {
957
+ const file = this.notes && this.notes.reviewPath;
958
+ if (!file) return;
959
+ try {
960
+ this.unlinkSync(file);
961
+ } catch {
962
+ // ignore missing review file
963
+ }
964
+ }
965
+
966
+ discardQuit() {
967
+ if (this.notes) this.notes.dirty = false;
968
+ if (!this.didResumeReview) this.removeReviewFile();
969
+ this.done = true;
970
+ this.exitCode = 0;
971
+ }
972
+
916
973
  cancelQuit() {
917
974
  this.mode = 'review';
918
975
  this.status = '';
@@ -924,6 +981,69 @@ class Session {
924
981
  if (handler) handler(this);
925
982
  }
926
983
 
984
+ canCommit() {
985
+ if (this.readOnly || this.rev || this.change) {
986
+ this.status = 'read only';
987
+ return false;
988
+ }
989
+ for (const item of this.items) {
990
+ if (isReadOnlyOrigin(item.origin)) {
991
+ this.status = 'read only';
992
+ return false;
993
+ }
994
+ }
995
+ return true;
996
+ }
997
+
998
+ onCommit() {
999
+ if (!this.canCommit()) return;
1000
+ this.mode = 'confirmCommit';
1001
+ this.status = '';
1002
+ }
1003
+
1004
+ cancelCommit() {
1005
+ this.mode = 'review';
1006
+ this.commitKind = null;
1007
+ this.status = '';
1008
+ }
1009
+
1010
+ startGitCommit(kind) {
1011
+ this.commitKind = kind;
1012
+ let text = '';
1013
+ if (kind === 'fixup') text = 'HEAD';
1014
+ if (kind === 'amend' && this.repo.lastMessage) {
1015
+ text = this.repo.lastMessage(this.top) ?? '';
1016
+ }
1017
+ this.openCompose('commit', text);
1018
+ }
1019
+
1020
+ onConfirmCommit(key) {
1021
+ const commitKey = key.length === 1 ? key.toLowerCase() : key;
1022
+ const handler = COMMIT_CONFIRM[commitKey];
1023
+ if (handler) handler(this);
1024
+ }
1025
+
1026
+ finishGitCommit() {
1027
+ const kind = this.commitKind;
1028
+ const message = this.editor ? this.editor.text : '';
1029
+ if (kind !== 'fixup' && !message.trim()) {
1030
+ this.status = 'empty commit message';
1031
+ return;
1032
+ }
1033
+ try {
1034
+ this.repo.commit(this.top, kind, message);
1035
+ } catch (error) {
1036
+ this.status = error.message;
1037
+ return;
1038
+ }
1039
+ this.closeCompose();
1040
+ this.dismissed.clear();
1041
+ this.refreshFromRepo();
1042
+ if (!this.done && !this.pendingExtras) {
1043
+ this.status = COMMIT_STATUS[kind] ?? 'committed';
1044
+ }
1045
+ }
1046
+
927
1047
  onFeedback() {
928
1048
  if (this.pane === 'files') {
929
1049
  this.status = 'not a diff block';
@@ -954,6 +1074,22 @@ class Session {
954
1074
  this.createTodo(file);
955
1075
  }
956
1076
 
1077
+ onCode() {
1078
+ if (this.pane === 'files') {
1079
+ this.status = 'not a diff block';
1080
+ return;
1081
+ }
1082
+ const item = this.current();
1083
+ if (!canEditCode(item)) {
1084
+ this.status = 'not a diff block';
1085
+ return;
1086
+ }
1087
+ const prev = this.notes.code.get(itemFeedKey(item));
1088
+ const original = blockAddText(item.hunk, item.blockId);
1089
+ const text = prev ? prev.text : original;
1090
+ this.openCompose('code', text);
1091
+ }
1092
+
957
1093
  todoPageIndex(file) {
958
1094
  return this.items.findIndex(
959
1095
  (item) => item.origin === 'todo' && itemPath(item) === file,
@@ -1086,6 +1222,29 @@ class Session {
1086
1222
  return;
1087
1223
  }
1088
1224
  if (this.composeKind === 'todo') this.commitTodo(text);
1225
+ if (this.composeKind === 'code') this.commitCode(text);
1226
+ }
1227
+
1228
+ commitCode(text) {
1229
+ const item = this.current();
1230
+ if (!canEditCode(item)) return;
1231
+ const original = blockAddText(item.hunk, item.blockId);
1232
+ const key = itemFeedKey(item);
1233
+ const hunk = item.hunk;
1234
+ const note = {
1235
+ file: itemPath(item),
1236
+ oldStart: hunk.oldStart ?? 0,
1237
+ newStart: hunk.newStart ?? 0,
1238
+ blockId: item.blockId ?? 0,
1239
+ origin: item.origin,
1240
+ header: hunk.header ?? '',
1241
+ text,
1242
+ };
1243
+ if (text === original) {
1244
+ setCode(this.notes, key, { ...note, clear: true });
1245
+ return;
1246
+ }
1247
+ setCode(this.notes, key, note);
1089
1248
  }
1090
1249
 
1091
1250
  commitTodo(text) {
@@ -1116,6 +1275,7 @@ class Session {
1116
1275
  this.mode = 'review';
1117
1276
  this.editor = null;
1118
1277
  this.composeKind = null;
1278
+ this.commitKind = null;
1119
1279
  this.composeTodoId = null;
1120
1280
  this.composeBaseline = '';
1121
1281
  this.templateIndex = -1;
@@ -1176,7 +1336,9 @@ class Session {
1176
1336
 
1177
1337
  composeView() {
1178
1338
  if (this.mode !== 'compose' || !this.editor) return null;
1179
- if (this.composeKind !== 'feedback') return null;
1339
+ if (this.composeKind !== 'feedback' && this.composeKind !== 'commit') {
1340
+ return null;
1341
+ }
1180
1342
  const kind = this.composeKind;
1181
1343
  const text = this.editor.text;
1182
1344
  const cursor = this.editor.cursor;
@@ -1189,6 +1351,34 @@ class Session {
1189
1351
  return { cursor: this.editor.cursor };
1190
1352
  }
1191
1353
 
1354
+ liveCodeText() {
1355
+ const item = this.current();
1356
+ if (!canEditCode(item)) return null;
1357
+ if (this.mode === 'compose' && this.composeKind === 'code' && this.editor) {
1358
+ return this.editor.text;
1359
+ }
1360
+ if (!this.notes) return null;
1361
+ const note = this.notes.code.get(itemFeedKey(item));
1362
+ if (!note) return null;
1363
+ return note.text;
1364
+ }
1365
+
1366
+ codeOverlayView() {
1367
+ const text = this.liveCodeText();
1368
+ if (text === null) return null;
1369
+ const editing = this.mode === 'compose' && this.composeKind === 'code';
1370
+ const cursor = editing && this.editor ? this.editor.cursor : null;
1371
+ return { text, keepEmpty: editing, cursor };
1372
+ }
1373
+
1374
+ composeTab() {
1375
+ if (this.composeKind === 'code') {
1376
+ this.editor.insert('\t');
1377
+ return;
1378
+ }
1379
+ this.applyTemplate();
1380
+ }
1381
+
1192
1382
  fileTodos(rel) {
1193
1383
  if (!this.notes || !rel) return [];
1194
1384
  const todos = [];
@@ -1270,6 +1460,10 @@ class Session {
1270
1460
  }
1271
1461
 
1272
1462
  saveCompose() {
1463
+ if (this.composeKind === 'commit') {
1464
+ this.finishGitCommit();
1465
+ return;
1466
+ }
1273
1467
  this.commitCompose();
1274
1468
  if (this.composeKind === 'feedback') {
1275
1469
  const next = this.editor ? this.editor.text : '';
@@ -1282,7 +1476,10 @@ class Session {
1282
1476
 
1283
1477
  autosave() {
1284
1478
  const text = this.editor ? this.editor.text : '';
1285
- if (this.mode === 'compose' && text.trim()) this.commitCompose();
1479
+ const editingCode = this.mode === 'compose' && this.composeKind === 'code';
1480
+ if (this.mode === 'compose' && (editingCode || text.trim())) {
1481
+ this.commitCompose();
1482
+ }
1286
1483
  if (this.notes && this.notes.dirty) this.flushReview();
1287
1484
  }
1288
1485
 
@@ -1308,6 +1505,11 @@ class Session {
1308
1505
  this.moveTodoLine(delta, todoInnerWidth(size.width));
1309
1506
  return;
1310
1507
  }
1508
+ if (this.composeKind === 'code') {
1509
+ const inner = codeInnerWidth(size.width, this.layout);
1510
+ this.editor.moveLine(delta, inner);
1511
+ return;
1512
+ }
1311
1513
  const width = noteInnerWidth(size.width);
1312
1514
  const shown = this.shownTemplates();
1313
1515
  if (shown.length && this.composeKind === 'feedback') {
@@ -1357,10 +1559,20 @@ class Session {
1357
1559
  this.selectTemplate(this.templateIndex);
1358
1560
  return;
1359
1561
  }
1562
+ if (this.composeKind === 'code') {
1563
+ this.editor.insert('\n');
1564
+ this.resetBlink();
1565
+ return;
1566
+ }
1360
1567
  this.saveCompose();
1361
1568
  return;
1362
1569
  }
1363
1570
  if (key === 'escape' || key === 'ctrl-s') {
1571
+ if (this.composeKind === 'commit' && key === 'escape') {
1572
+ this.closeCompose();
1573
+ this.status = '';
1574
+ return;
1575
+ }
1364
1576
  this.saveCompose();
1365
1577
  return;
1366
1578
  }
@@ -1653,7 +1865,7 @@ class Session {
1653
1865
  CURSOR_BLINK_MS,
1654
1866
  );
1655
1867
  this.stdin.on('data', (chunk) => onData(chunk), { signal });
1656
- process.on('SIGWINCH', () => this.draw(), { signal });
1868
+ watchResize(this.stdout, process, () => this.draw(), signal);
1657
1869
  });
1658
1870
  }
1659
1871
  }
package/lib/source.js CHANGED
@@ -1,13 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const { parseGithubPrUrl } = require('./github.js');
4
+ const { parseGitlabMrUrl } = require('./gitlab.js');
4
5
 
5
6
  const selectChangeSource = (paths) => {
6
7
  if (!paths.length) return { kind: 'local', paths };
7
8
  const first = paths[0];
8
9
  const pr = parseGithubPrUrl(first);
9
- if (!pr) return { kind: 'local', paths };
10
- return { kind: 'github-pr', pr, paths: paths.slice(1) };
10
+ if (pr) return { kind: 'github-pr', pr, paths: paths.slice(1) };
11
+ const mr = parseGitlabMrUrl(first);
12
+ if (mr) return { kind: 'gitlab-mr', mr, paths: paths.slice(1) };
13
+ return { kind: 'local', paths };
11
14
  };
12
15
 
13
16
  const nop = () => {};
package/lib/sys.js ADDED
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const IS_WIN = process.platform === 'win32';
7
+
8
+ const UNIX_CLIP = [
9
+ { cmd: 'wl-copy', args: [] },
10
+ { cmd: 'xclip', args: ['-selection', 'clipboard'] },
11
+ { cmd: 'xsel', args: ['--clipboard', '--input'] },
12
+ ];
13
+
14
+ const npmBin = (platform = process.platform) =>
15
+ platform === 'win32' ? 'npm.cmd' : 'npm';
16
+
17
+ const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
18
+
19
+ const clipTools = (platform = process.platform) => {
20
+ if (platform === 'win32') return [{ cmd: 'clip', args: [] }];
21
+ if (platform === 'darwin') return [{ cmd: 'pbcopy', args: [] }];
22
+ return UNIX_CLIP;
23
+ };
24
+
25
+ const watchResize = (stdout, proc, onResize, signal) => {
26
+ if (stdout && typeof stdout.on === 'function') {
27
+ stdout.on('resize', onResize, { signal });
28
+ }
29
+ try {
30
+ proc.on('SIGWINCH', onResize, { signal });
31
+ } catch {
32
+ // SIGWINCH is not available on Windows
33
+ }
34
+ };
35
+
36
+ const canonPath = (value) => {
37
+ try {
38
+ return fs.realpathSync.native(value);
39
+ } catch {
40
+ return path.resolve(value);
41
+ }
42
+ };
43
+
44
+ const samePath = (left, right) => {
45
+ if (left === right) return true;
46
+ const a = canonPath(left);
47
+ const b = canonPath(right);
48
+ if (a === b) return true;
49
+ return IS_WIN && a.toLowerCase() === b.toLowerCase();
50
+ };
51
+
52
+ module.exports = {
53
+ IS_WIN,
54
+ npmBin,
55
+ spawnBase,
56
+ clipTools,
57
+ watchResize,
58
+ samePath,
59
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",
@@ -53,7 +53,7 @@
53
53
  "disable": "node scripts/disable.js",
54
54
  "lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\"",
55
55
  "fix": "eslint . --fix && prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\"",
56
- "test": "npm run -s lint && node --test test/*.test.js"
56
+ "test": "npm run -s lint && node --test"
57
57
  },
58
58
  "devDependencies": {
59
59
  "eslint": "^9.39.4",