reslop 0.2.3 → 0.2.4

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 (80) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +8 -3
  3. package/bin/reslop.js +4 -2
  4. package/lib/api.js +2 -1
  5. package/lib/cli.js +19 -10
  6. package/lib/clipboard.js +6 -3
  7. package/lib/deps-diff.js +61 -115
  8. package/lib/deps.js +24 -61
  9. package/lib/detect.js +4 -2
  10. package/lib/diff/align.js +2 -1
  11. package/lib/diff/diff.js +6 -3
  12. package/lib/diff/display.js +2 -1
  13. package/lib/diff/inline.js +2 -1
  14. package/lib/diff/patch.js +2 -1
  15. package/lib/diff/unit.js +4 -2
  16. package/lib/editor.js +22 -4
  17. package/lib/files.js +38 -12
  18. package/lib/git-branches.js +107 -154
  19. package/lib/git-deps.js +20 -60
  20. package/lib/git-worktree.js +508 -0
  21. package/lib/git.js +23 -580
  22. package/lib/github.js +2 -1
  23. package/lib/gitlab.js +2 -1
  24. package/lib/highlight/bash.js +2 -1
  25. package/lib/highlight/css.js +2 -1
  26. package/lib/highlight/csv.js +2 -1
  27. package/lib/highlight/dot.js +2 -1
  28. package/lib/highlight/html.js +2 -1
  29. package/lib/highlight/json.js +2 -1
  30. package/lib/highlight/log.js +2 -1
  31. package/lib/highlight/txt.js +2 -1
  32. package/lib/highlight.js +2 -1
  33. package/lib/keys.js +2 -2
  34. package/lib/manifest.js +4 -2
  35. package/lib/npm-commands.js +83 -4
  36. package/lib/remote.js +20 -13
  37. package/lib/render/branches.js +6 -15
  38. package/lib/render/chrome.js +27 -7
  39. package/lib/render/commits.js +158 -46
  40. package/lib/render/diff.js +130 -47
  41. package/lib/render/files.js +4 -12
  42. package/lib/render/frame.js +3 -9
  43. package/lib/render/notes.js +4 -11
  44. package/lib/render/npm.js +1 -13
  45. package/lib/render/present.js +2 -1
  46. package/lib/render/primitives.js +28 -15
  47. package/lib/render/render.js +2 -1
  48. package/lib/render/unit.js +14 -11
  49. package/lib/report-model.js +6 -4
  50. package/lib/report-parse.js +8 -13
  51. package/lib/report-render.js +38 -21
  52. package/lib/report-run.js +12 -10
  53. package/lib/report-test.js +5 -4
  54. package/lib/report-tools.js +44 -18
  55. package/lib/review-merge.js +2 -1
  56. package/lib/review.js +6 -2
  57. package/lib/session/actions.js +49 -65
  58. package/lib/session/branches.js +13 -49
  59. package/lib/session/change-actions.js +2 -1
  60. package/lib/session/commit.js +34 -49
  61. package/lib/session/compose.js +37 -11
  62. package/lib/session/dispatch.js +17 -25
  63. package/lib/session/files-pane.js +6 -27
  64. package/lib/session/items.js +6 -3
  65. package/lib/session/lifecycle.js +8 -4
  66. package/lib/session/load.js +4 -2
  67. package/lib/session/navigation.js +1 -0
  68. package/lib/session/npm.js +54 -1
  69. package/lib/session/ops.js +15 -1
  70. package/lib/session/pointer.js +22 -3
  71. package/lib/session/review.js +2 -1
  72. package/lib/session/terminal.js +4 -2
  73. package/lib/session/update.js +2 -1
  74. package/lib/session/view.js +29 -3
  75. package/lib/session.js +37 -23
  76. package/lib/source.js +6 -3
  77. package/lib/update.js +5 -9
  78. package/lib/utilities.js +4 -2
  79. package/lib/wrap.js +3 -1
  80. package/package.json +1 -1
@@ -3,14 +3,18 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { isAbortError } = require('./load.js');
7
- const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = require('./watch.js');
6
+ const sessionLoad = require('./load.js');
7
+ const { isAbortError } = sessionLoad;
8
+ const watch = require('./watch.js');
9
+ const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = watch;
8
10
  const files = require('../files.js');
9
11
  const { itemPath, relativeAge, TODO_FILE } = files;
10
- const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps.js');
12
+ const deps = require('../deps.js');
13
+ const { MANIFEST, LOCKFILE, depFileMeta } = deps;
11
14
  const review = require('../review.js');
12
15
  const { hasNotes, loadReview } = review;
13
- const { mergeReview } = require('../review-merge.js');
16
+ const reviewMerge = require('../review-merge.js');
17
+ const { mergeReview } = reviewMerge;
14
18
 
15
19
  const loadOptions = (api, extra = {}) => {
16
20
  const { loadConfig, ui } = api;
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
- const isAbortError = (error) => !!(error && error.code === 'ABORT');
6
+ const utilities = require('../utilities.js');
7
+ const { isAbort: isAbortError } = utilities;
6
8
 
7
9
  const createLoadCoordinator = () => {
8
10
  const state = {
@@ -18,6 +18,7 @@ const initialState = (options = {}) => ({
18
18
  selection: null,
19
19
  mouseAnchor: null,
20
20
  pendingClick: null,
21
+ lastClick: null,
21
22
  todoOpen: false,
22
23
  todoFocus: 0,
23
24
  });
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const commands = require('../npm-commands.js');
4
- const { logViewRows } = require('../render/npm.js');
4
+ const npm = require('../render/npm.js');
5
+ const { logViewRows } = npm;
5
6
 
6
7
  const { listCommands, reduceOutput, writeLog, parseScriptLine } = commands;
8
+ const { staleLogFiles, removeStaleLogs, formatSize } = commands;
7
9
  const { scriptLine, saveScript, removeScript, reorderScript } = commands;
8
10
  const { startNpm } = commands;
9
11
 
@@ -30,9 +32,20 @@ const canEdit = (ui) => {
30
32
  return true;
31
33
  };
32
34
 
35
+ const refreshLogs = (api) => {
36
+ try {
37
+ const found = staleLogFiles(api.ui.top);
38
+ api.state.logBytes = found.bytes;
39
+ } catch (error) {
40
+ api.state.logBytes = 0;
41
+ api.ui.status = error.message;
42
+ }
43
+ };
44
+
33
45
  const open = (api) => {
34
46
  if (api.ui.mode === 'compose') return;
35
47
  refresh(api);
48
+ refreshLogs(api);
36
49
  api.state.viewing = false;
37
50
  api.state.output = '';
38
51
  api.ui.nav.pane = 'npm';
@@ -214,15 +227,45 @@ const askDrop = (api) => {
214
227
  api.ui.status = 'not a script';
215
228
  return;
216
229
  }
230
+ api.state.dropKind = 'script';
217
231
  api.state.dropName = entry.name;
218
232
  api.ui.mode = 'confirmDrop';
219
233
  api.ui.status = '';
220
234
  };
221
235
 
236
+ const askLogs = (api) => {
237
+ if (api.state.viewing) return;
238
+ if (!canEdit(api.ui)) {
239
+ api.ui.status = 'read only';
240
+ return;
241
+ }
242
+ refreshLogs(api);
243
+ if (!api.state.logBytes) return;
244
+ api.state.dropKind = 'logs';
245
+ const size = formatSize(api.state.logBytes);
246
+ api.state.dropName = `logs older than 5 days (${size})`;
247
+ api.ui.mode = 'confirmDrop';
248
+ api.ui.status = '';
249
+ };
250
+
222
251
  const confirmDrop = (api) => {
252
+ const kind = api.state.dropKind;
223
253
  const name = api.state.dropName;
224
254
  api.state.dropName = '';
255
+ api.state.dropKind = '';
225
256
  api.ui.mode = 'review';
257
+ if (kind === 'logs') {
258
+ try {
259
+ removeStaleLogs(api.ui.top);
260
+ } catch (error) {
261
+ api.ui.status = error.message;
262
+ refreshLogs(api);
263
+ return;
264
+ }
265
+ refreshLogs(api);
266
+ api.ui.status = 'dropped logs';
267
+ return;
268
+ }
226
269
  if (!name) return;
227
270
  try {
228
271
  api.ui.ignoreWatch();
@@ -237,6 +280,7 @@ const confirmDrop = (api) => {
237
280
 
238
281
  const cancelDrop = (api) => {
239
282
  api.state.dropName = '';
283
+ api.state.dropKind = '';
240
284
  api.ui.mode = 'review';
241
285
  api.ui.status = '';
242
286
  };
@@ -274,6 +318,8 @@ const createNpmController = (ui) => {
274
318
  logScroll: 0,
275
319
  editName: '',
276
320
  dropName: '',
321
+ dropKind: '',
322
+ logBytes: 0,
277
323
  running: false,
278
324
  stopping: false,
279
325
  child: null,
@@ -293,6 +339,10 @@ const createNpmController = (ui) => {
293
339
  get dropName() {
294
340
  return state.dropName;
295
341
  },
342
+ get logLabel() {
343
+ if (!state.logBytes) return '';
344
+ return `old logs ${formatSize(state.logBytes)}`;
345
+ },
296
346
  get followEnd() {
297
347
  return state.followEnd;
298
348
  },
@@ -315,6 +365,8 @@ const createNpmController = (ui) => {
315
365
  state.logScroll = 0;
316
366
  state.editName = '';
317
367
  state.dropName = '';
368
+ state.dropKind = '';
369
+ state.logBytes = 0;
318
370
  state.stopping = false;
319
371
  },
320
372
  open: () => open(api),
@@ -326,6 +378,7 @@ const createNpmController = (ui) => {
326
378
  create: () => newCommand(api),
327
379
  finishEdit: () => finishEdit(api),
328
380
  askDrop: () => askDrop(api),
381
+ askLogs: () => askLogs(api),
329
382
  confirmDrop: () => confirmDrop(api),
330
383
  cancelDrop: () => cancelDrop(api),
331
384
  reorder: (delta) => reorder(api, delta),
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const ASYNC_WITHOUT_UI = ['pull', 'push'];
6
7
 
@@ -81,6 +82,18 @@ const createOpsRunner = (ui) => {
81
82
  finishBusy(done, after);
82
83
  };
83
84
 
85
+ const runWrite = (name, args, kind, done, after) => {
86
+ const picked = pickRepoWrite(ui.repo, name, ui.uiOpen);
87
+ const write = (top) => picked.write(top, ...args);
88
+ if (picked.async) {
89
+ const refresh = () => after({ doneStatus: done });
90
+ return void runBusy(kind, done, write, refresh);
91
+ }
92
+ if (!runRepo(done, write)) return;
93
+ after({});
94
+ if (!ui.status) ui.status = done;
95
+ };
96
+
84
97
  return stateView(
85
98
  state,
86
99
  {
@@ -88,6 +101,7 @@ const createOpsRunner = (ui) => {
88
101
  setBusy,
89
102
  clearBusy,
90
103
  runRepo,
104
+ runWrite,
91
105
  runBusy,
92
106
  },
93
107
  ['gitBusy', 'busy'],
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { extractText, isRange } = require('../select.js');
4
- const { hitAction } = require('../keys.js');
5
- const { copyText } = require('../clipboard.js');
3
+ const select = require('../select.js');
4
+ const { extractText, isRange } = select;
5
+ const keys = require('../keys.js');
6
+ const { hitAction } = keys;
7
+ const clipboard = require('../clipboard.js');
8
+ const { copyText } = clipboard;
6
9
 
7
10
  const mouseCell = (ui, event) => {
8
11
  const size = ui.lastSize ?? ui.getSize();
@@ -45,6 +48,20 @@ const clickButton = (ui, cell, clickId) => {
45
48
  return true;
46
49
  };
47
50
 
51
+ const DOUBLE_CLICK_MS = 500;
52
+
53
+ const armDoubleClick = (nav, pane, cursor) => {
54
+ const now = Date.now();
55
+ const prev = nav.lastClick;
56
+ const again =
57
+ prev !== null &&
58
+ prev.pane === pane &&
59
+ prev.cursor === cursor &&
60
+ now - prev.at <= DOUBLE_CLICK_MS;
61
+ nav.lastClick = { pane, cursor, at: now };
62
+ return again;
63
+ };
64
+
48
65
  const clickHit = (ui, hits, cell, onHit) => {
49
66
  const nav = ui.nav;
50
67
  if (!hits || isRange(nav.selection)) return false;
@@ -107,7 +124,9 @@ const RELEASE_TARGETS = [
107
124
  match: (ui) => ui.pane === 'npm',
108
125
  hits: 'fileHits',
109
126
  onHit: (ui, hit) => {
127
+ const again = armDoubleClick(ui.nav, 'npm', hit.cursor);
110
128
  ui.nav.npmCursor = hit.cursor;
129
+ if (again && ui.mode === 'review') ui.npm.run();
111
130
  },
112
131
  },
113
132
  {
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const review = require('../review.js');
6
7
  const { createStore, applyImportedNotes, setStatus } = review;
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { presentRows, presentCursor } = require('../render/render.js');
4
- const { listen, watchResize } = require('../utilities.js');
3
+ const render = require('../render/render.js');
4
+ const { presentRows, presentCursor } = render;
5
+ const utilities = require('../utilities.js');
6
+ const { listen, watchResize } = utilities;
5
7
 
6
8
  const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
7
9
  const LEAVE_TERM =
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const update = require('../update.js');
6
7
  const { checkUpdate, installUpdate, markSkipped } = update;
@@ -3,8 +3,9 @@
3
3
  const path = require('node:path');
4
4
 
5
5
  const files = require('../files.js');
6
- const { listedCommits } = require('./commit.js');
7
- const { fileEntries, REPO_TODOS_LABEL } = files;
6
+ const commit = require('./commit.js');
7
+ const { listedCommits } = commit;
8
+ const { fileEntries, fileTotals, REPO_TODOS_LABEL } = files;
8
9
  const review = require('../review.js');
9
10
  const { noteCounts } = review;
10
11
  const diff = require('../diff/diff.js');
@@ -143,6 +144,30 @@ const unitBodyLines = (api) => {
143
144
  return unitLines(unitFileText(api), group);
144
145
  };
145
146
 
147
+ const pendingLines = (items, rev, revShort) => {
148
+ if (rev || revShort) return null;
149
+ for (const item of items) {
150
+ if (item.origin === 'commit' || item.origin === 'pr') return null;
151
+ }
152
+ return fileTotals(fileEntries(items));
153
+ };
154
+
155
+ const commitRows = (api) => {
156
+ const rows = listedCommits(api.commits.commits);
157
+ const lines = pendingLines(api.collection.items, api.ui.rev, api.ui.revShort);
158
+ if (!lines || !rows[0]) return rows;
159
+ rows[0] = {
160
+ ...rows[0],
161
+ added: lines.added,
162
+ removed: lines.removed,
163
+ stagedAdded: lines.stagedAdded,
164
+ unstagedAdded: lines.unstagedAdded,
165
+ stagedRemoved: lines.stagedRemoved,
166
+ unstagedRemoved: lines.unstagedRemoved,
167
+ };
168
+ return rows;
169
+ };
170
+
146
171
  const view = (api) => {
147
172
  const { composer, nav, ui, gitBranches, commits, npm, updater } = api;
148
173
  const templates = composer.shownTemplates();
@@ -170,7 +195,7 @@ const view = (api) => {
170
195
  branch: ui.branch,
171
196
  branches: gitBranches.branches,
172
197
  branchCursor: nav.branchCursor,
173
- commits: listedCommits(commits.commits),
198
+ commits: commitRows(api),
174
199
  commitCursor: nav.commitCursor,
175
200
  commitView: commits.commitView,
176
201
  npmCommands: npm.commands,
@@ -181,6 +206,7 @@ const view = (api) => {
181
206
  npmScroll: npm.logScroll,
182
207
  npmEditName: npm.editName,
183
208
  npmRunning: npm.running,
209
+ npmLogs: nav.pane === 'npm' ? npm.logLabel : '',
184
210
  repoName: repoNameOf(api),
185
211
  rev: ui.rev || '',
186
212
  revShort: ui.revShort || '',
package/lib/session.js CHANGED
@@ -9,28 +9,47 @@ const { renderFrame } = render;
9
9
  const { decodeChunk } = keys;
10
10
  const { AUTOSAVE_MS, hasNotes } = review;
11
11
  const capabilities = require('./capabilities.js');
12
+ const actions = require('./session/actions.js');
13
+ const { LIST_PANES } = actions;
12
14
  const { sessionCapabilities } = capabilities;
13
- const { createTerminal, LEAVE_TERM } = require('./session/terminal.js');
14
- const { createLoadCoordinator } = require('./session/load.js');
15
+ const terminal = require('./session/terminal.js');
16
+ const { createTerminal, LEAVE_TERM } = terminal;
17
+ const load = require('./session/load.js');
18
+ const { createLoadCoordinator } = load;
15
19
  const items = require('./session/items.js');
16
20
  const { createItemCollection } = items;
17
- const { createNavigation } = require('./session/navigation.js');
18
- const { createReviewController } = require('./session/review.js');
19
- const { createUpdateCoordinator } = require('./session/update.js');
20
- const { createProgress } = require('./session/progress.js');
21
- const { createComposer } = require('./session/compose.js');
22
- const { createOpsRunner } = require('./session/ops.js');
23
- const { createChangeActions } = require('./session/change-actions.js');
24
- const { createCommitController } = require('./session/commit.js');
25
- const { createBranchController } = require('./session/branches.js');
26
- const { createNpmController } = require('./session/npm.js');
21
+ const navigation = require('./session/navigation.js');
22
+ const { createNavigation } = navigation;
23
+ const sessionReview = require('./session/review.js');
24
+ const { createReviewController } = sessionReview;
25
+ const update = require('./session/update.js');
26
+ const { createUpdateCoordinator } = update;
27
+ const progress = require('./session/progress.js');
28
+ const { createProgress } = progress;
29
+ const compose = require('./session/compose.js');
30
+ const { createComposer } = compose;
31
+ const ops = require('./session/ops.js');
32
+ const { createOpsRunner } = ops;
33
+ const changeActions = require('./session/change-actions.js');
34
+ const { createChangeActions } = changeActions;
35
+ const commit = require('./session/commit.js');
36
+ const { createCommitController } = commit;
37
+ const branches = require('./session/branches.js');
38
+ const { createBranchController } = branches;
39
+ const npm = require('./session/npm.js');
40
+ const { createNpmController } = npm;
27
41
  const accessors = require('./session/accessors.js');
28
42
  const { bindAccessors, bindGetters, bindAlias } = accessors;
29
- const { createView } = require('./session/view.js');
30
- const { createFilesPane } = require('./session/files-pane.js');
31
- const { createLifecycle } = require('./session/lifecycle.js');
32
- const { createPointer } = require('./session/pointer.js');
33
- const { createDispatcher, createInput } = require('./session/dispatch.js');
43
+ const view = require('./session/view.js');
44
+ const { createView } = view;
45
+ const filesPane = require('./session/files-pane.js');
46
+ const { createFilesPane } = filesPane;
47
+ const lifecycle = require('./session/lifecycle.js');
48
+ const { createLifecycle } = lifecycle;
49
+ const pointer = require('./session/pointer.js');
50
+ const { createPointer } = pointer;
51
+ const dispatch = require('./session/dispatch.js');
52
+ const { createDispatcher, createInput } = dispatch;
34
53
 
35
54
  const CURSOR_BLINK_MS = 500;
36
55
  const PROGRESS_MS = 160;
@@ -314,12 +333,7 @@ class Session {
314
333
  height: size.height,
315
334
  color: this.color,
316
335
  });
317
- if (
318
- this.pane === 'files' ||
319
- this.pane === 'branches' ||
320
- this.pane === 'commits' ||
321
- this.pane === 'npm'
322
- ) {
336
+ if (LIST_PANES.includes(this.pane)) {
323
337
  this.nav.listScroll = frame.listScroll;
324
338
  }
325
339
  if (this.nav.todoOpen) this.nav.scroll = frame.scroll;
package/lib/source.js CHANGED
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { parseGithubPrUrl } = require('./github.js');
4
- const { parseGitlabMrUrl } = require('./gitlab.js');
5
- const { capabilitiesFor, attachCapabilities } = require('./capabilities.js');
3
+ const github = require('./github.js');
4
+ const { parseGithubPrUrl } = github;
5
+ const gitlab = require('./gitlab.js');
6
+ const { parseGitlabMrUrl } = gitlab;
7
+ const capabilities = require('./capabilities.js');
8
+ const { capabilitiesFor, attachCapabilities } = capabilities;
6
9
 
7
10
  const selectChangeSource = (paths) => {
8
11
  if (!paths.length) return { kind: 'local', paths };
package/lib/update.js CHANGED
@@ -4,15 +4,11 @@ const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
6
 
7
- const { jsonParse, isHashObject } = require('metautil');
8
- const {
9
- parseVersion,
10
- cmpVersion,
11
- npmBin,
12
- npmOpts,
13
- runProc,
14
- oneLine,
15
- } = require('./utilities.js');
7
+ const metautil = require('metautil');
8
+ const { jsonParse, isHashObject } = metautil;
9
+ const utilities = require('./utilities.js');
10
+ const { parseVersion, cmpVersion, npmBin } = utilities;
11
+ const { npmOpts, runProc, oneLine } = utilities;
16
12
 
17
13
  const PKG = require('../package.json');
18
14
 
package/lib/utilities.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { spawn, spawnSync } = require('node:child_process');
4
- const { split } = require('metautil');
3
+ const childProcess = require('node:child_process');
4
+ const { spawn, spawnSync } = childProcess;
5
+ const metautil = require('metautil');
6
+ const { split } = metautil;
5
7
 
6
8
  const IS_WIN = process.platform === 'win32';
7
9
  const DEFAULT_ENCODING = 'utf8';
package/lib/wrap.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { graphemes, graphemeWidth, visibleWidth } = require('./ansi.js');
3
+ const ansi = require('./ansi.js');
4
+ const { graphemes, graphemeWidth, visibleWidth } = ansi;
4
5
 
5
6
  const wrapPlain = (text, width) => {
6
7
  if (width <= 0) return [''];
@@ -28,6 +29,7 @@ const wrapPlain = (text, width) => {
28
29
  if (col + w > width && col > 0) {
29
30
  if (lastBreak >= start) {
30
31
  flush(lastBreak + 1);
32
+ i = start;
31
33
  continue;
32
34
  }
33
35
  flush(i);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",