reslop 0.1.9 → 0.2.1

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 (96) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +3 -1
  3. package/bin/reslop.js +2 -2
  4. package/lib/ansi.js +148 -5
  5. package/lib/api.js +4 -2
  6. package/lib/capabilities.js +13 -14
  7. package/lib/cli.js +38 -49
  8. package/lib/clipboard.js +1 -1
  9. package/lib/{deps/changes.js → deps-diff.js} +309 -19
  10. package/lib/deps.js +498 -29
  11. package/lib/detect.js +1 -7
  12. package/lib/diff/align.js +7 -10
  13. package/lib/diff/diff.js +53 -0
  14. package/lib/diff/display.js +5 -4
  15. package/lib/diff/inline.js +26 -21
  16. package/lib/diff/items.js +6 -4
  17. package/lib/diff/patch.js +1 -3
  18. package/lib/diff/unit.js +392 -0
  19. package/lib/files.js +5 -8
  20. package/lib/{git/branches.js → git-branches.js} +191 -11
  21. package/lib/{git/dependencies.js → git-deps.js} +111 -38
  22. package/lib/git-reword-todo.js +20 -0
  23. package/lib/git.js +606 -68
  24. package/lib/github.js +28 -197
  25. package/lib/gitlab.js +24 -195
  26. package/lib/highlight/bash.js +1 -3
  27. package/lib/highlight/css.js +1 -3
  28. package/lib/highlight/js.js +11 -14
  29. package/lib/highlight.js +6 -9
  30. package/lib/keys.js +14 -8
  31. package/lib/manifest.js +330 -0
  32. package/lib/remote.js +374 -0
  33. package/lib/render/branches.js +18 -100
  34. package/lib/render/chrome.js +71 -14
  35. package/lib/render/commits.js +69 -60
  36. package/lib/render/diff.js +20 -20
  37. package/lib/render/files.js +6 -34
  38. package/lib/render/frame.js +36 -13
  39. package/lib/render/notes.js +26 -8
  40. package/lib/render/primitives.js +99 -11
  41. package/lib/{render.js → render/render.js} +8 -8
  42. package/lib/render/unit.js +41 -0
  43. package/lib/review-merge.js +270 -0
  44. package/lib/review.js +782 -14
  45. package/lib/select.js +5 -10
  46. package/lib/session/accessors.js +9 -1
  47. package/lib/session/actions.js +61 -18
  48. package/lib/session/branches.js +116 -128
  49. package/lib/session/change-actions.js +202 -123
  50. package/lib/session/commit.js +201 -115
  51. package/lib/session/compose.js +181 -15
  52. package/lib/session/dispatch.js +110 -60
  53. package/lib/session/files-pane.js +156 -148
  54. package/lib/session/items.js +22 -30
  55. package/lib/session/lifecycle.js +151 -54
  56. package/lib/session/load.js +19 -33
  57. package/lib/session/navigation.js +12 -1
  58. package/lib/session/ops.js +28 -41
  59. package/lib/session/pointer.js +57 -57
  60. package/lib/session/review.js +23 -37
  61. package/lib/session/terminal.js +6 -12
  62. package/lib/session/todos.js +37 -13
  63. package/lib/session/unit.js +208 -0
  64. package/lib/session/update.js +33 -46
  65. package/lib/session/view.js +78 -9
  66. package/lib/session/watch.js +128 -24
  67. package/lib/session.js +22 -246
  68. package/lib/source.js +3 -3
  69. package/lib/update.js +40 -75
  70. package/lib/{git/proc.js → utilities.js} +75 -11
  71. package/lib/wrap.js +1 -3
  72. package/package.json +2 -2
  73. package/lib/deps/items.js +0 -134
  74. package/lib/deps/manifest.js +0 -152
  75. package/lib/deps/merge.js +0 -120
  76. package/lib/deps/proposals.js +0 -164
  77. package/lib/deps/reports.js +0 -180
  78. package/lib/deps/rows.js +0 -176
  79. package/lib/deps/scan.js +0 -138
  80. package/lib/deps/usage.js +0 -68
  81. package/lib/deps/versions.js +0 -119
  82. package/lib/diff.js +0 -33
  83. package/lib/git/changes.js +0 -164
  84. package/lib/git/read.js +0 -253
  85. package/lib/git/run.js +0 -100
  86. package/lib/npm/commands.js +0 -100
  87. package/lib/remote/changes.js +0 -37
  88. package/lib/remote/headers.js +0 -36
  89. package/lib/remote/http.js +0 -122
  90. package/lib/remote/notes.js +0 -94
  91. package/lib/review/format.js +0 -434
  92. package/lib/review/storage.js +0 -150
  93. package/lib/review/store.js +0 -178
  94. package/lib/review/templates.js +0 -49
  95. package/lib/session/origins.js +0 -7
  96. package/lib/sys.js +0 -84
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ const { stateView } = require('./accessors.js');
4
+
3
5
  const update = require('../update.js');
4
6
  const { checkUpdate, installUpdate, markSkipped } = update;
5
7
 
6
8
  const optsOf = (api) => {
7
- const value = api.options.updateOpts();
9
+ const value = api.ui.updateOpts;
8
10
  if (!value || value === true) return {};
9
11
  return value;
10
12
  };
@@ -19,15 +21,16 @@ const reset = (state) => {
19
21
 
20
22
  const maybePrompt = (api) => {
21
23
  if (!api.state.offer) return;
22
- if (!api.options.didLoad()) return;
23
- if (api.options.getMode() !== 'review') return;
24
- api.options.setMode('confirmUpdate');
24
+ if (!api.ui.loader.didLoad) return;
25
+ if (api.ui.mode !== 'review') return;
26
+ api.ui.mode = 'confirmUpdate';
25
27
  };
26
28
 
27
29
  const runInstall = async (api, version) => {
28
- api.options.setStatus('updating reslop');
29
- api.options.startInstallProgress();
30
- api.options.paint();
30
+ api.ui.status = 'updating reslop';
31
+ api.ui.progressFrame = 0;
32
+ api.ui.progress.start('install');
33
+ api.ui.paint();
31
34
  let ok = false;
32
35
  try {
33
36
  await installUpdate(version, optsOf(api));
@@ -35,23 +38,23 @@ const runInstall = async (api, version) => {
35
38
  } catch {
36
39
  // ignore install errors
37
40
  }
38
- api.options.stopInstallProgress();
39
- if (api.options.isDone() || !api.options.uiOpen()) return;
40
- api.options.setStatus(ok ? 'updated' : 'update failed');
41
- api.options.paint();
41
+ api.ui.progress.stop('install');
42
+ if (api.ui.done || !api.ui.uiOpen) return;
43
+ api.ui.status = ok ? 'updated' : 'update failed';
44
+ api.ui.paint();
42
45
  };
43
46
 
44
47
  const acceptUpdate = (api) => {
45
- api.options.setMode('review');
48
+ api.ui.mode = 'review';
46
49
  api.state.offer = false;
47
50
  const version = api.state.to;
48
51
  api.state.installPromise = runInstall(api, version);
49
52
  };
50
53
 
51
54
  const declineUpdate = (api) => {
52
- api.options.setMode('review');
55
+ api.ui.mode = 'review';
53
56
  api.state.offer = false;
54
- api.options.setStatus('');
57
+ api.ui.status = '';
55
58
  markSkipped(api.state.to, optsOf(api));
56
59
  };
57
60
 
@@ -62,7 +65,7 @@ const runOpenUpdate = async (api) => {
62
65
  } catch {
63
66
  return;
64
67
  }
65
- if (api.options.isDone()) return;
68
+ if (api.ui.done) return;
66
69
  if (plan.action === 'install') {
67
70
  api.state.installPromise = runInstall(api, plan.latest);
68
71
  await api.state.installPromise;
@@ -73,17 +76,17 @@ const runOpenUpdate = async (api) => {
73
76
  api.state.to = plan.latest;
74
77
  api.state.offer = true;
75
78
  maybePrompt(api);
76
- api.options.paint();
79
+ api.ui.paint();
77
80
  };
78
81
 
79
82
  const openUpdate = (api) => {
80
- if (api.options.updateOpts() === false) return Promise.resolve();
83
+ if (api.ui.updateOpts === false) return Promise.resolve();
81
84
  if (api.state.checkPromise) return api.state.checkPromise;
82
85
  api.state.checkPromise = runOpenUpdate(api);
83
86
  return api.state.checkPromise;
84
87
  };
85
88
 
86
- const createUpdateCoordinator = (options) => {
89
+ const createUpdateCoordinator = (ui) => {
87
90
  const state = {
88
91
  offer: false,
89
92
  from: '',
@@ -91,35 +94,19 @@ const createUpdateCoordinator = (options) => {
91
94
  checkPromise: null,
92
95
  installPromise: null,
93
96
  };
94
- const api = { options, state };
95
- return {
96
- get offer() {
97
- return state.offer;
98
- },
99
- set offer(value) {
100
- state.offer = value;
101
- },
102
- get from() {
103
- return state.from;
104
- },
105
- get to() {
106
- return state.to;
97
+ const api = { ui, state };
98
+ return stateView(
99
+ state,
100
+ {
101
+ reset: () => reset(state),
102
+ maybePrompt: () => maybePrompt(api),
103
+ accept: () => acceptUpdate(api),
104
+ decline: () => declineUpdate(api),
105
+ open: () => openUpdate(api),
107
106
  },
108
- get checkPromise() {
109
- return state.checkPromise;
110
- },
111
- get installPromise() {
112
- return state.installPromise;
113
- },
114
- set installPromise(value) {
115
- state.installPromise = value;
116
- },
117
- reset: () => reset(state),
118
- maybePrompt: () => maybePrompt(api),
119
- accept: () => acceptUpdate(api),
120
- decline: () => declineUpdate(api),
121
- open: () => openUpdate(api),
122
- };
107
+ ['offer', 'installPromise'],
108
+ ['from', 'to', 'checkPromise'],
109
+ );
123
110
  };
124
111
 
125
112
  module.exports = { createUpdateCoordinator };
@@ -6,11 +6,8 @@ const files = require('../files.js');
6
6
  const { fileEntries, REPO_TODOS_LABEL } = files;
7
7
  const review = require('../review.js');
8
8
  const { noteCounts } = review;
9
-
10
- const SOURCE_KIND = {
11
- 'github-pr': 'pr',
12
- 'gitlab-mr': 'mr',
13
- };
9
+ const diff = require('../diff/diff.js');
10
+ const { unitLines, itemsForPath, splitEditor, mergeEditRows } = diff;
14
11
 
15
12
  const relabelStatus = (entries, from, to) => {
16
13
  if (!to) return entries;
@@ -43,14 +40,60 @@ const todosEntry = (counts) => ({
43
40
  unstaged: 0,
44
41
  });
45
42
 
43
+ const blankEntry = (rel) => ({
44
+ path: rel,
45
+ origins: [],
46
+ firstIndex: 0,
47
+ openIndex: 0,
48
+ remaining: 0,
49
+ added: 0,
50
+ removed: 0,
51
+ stagedAdded: 0,
52
+ stagedRemoved: 0,
53
+ unstagedAdded: 0,
54
+ unstagedRemoved: 0,
55
+ staged: 0,
56
+ unstaged: 0,
57
+ date: '',
58
+ status: '',
59
+ });
60
+
61
+ const listedNames = (api) => {
62
+ const repo = api.ui.repo;
63
+ if (!repo || typeof repo.listFiles !== 'function') return [];
64
+ try {
65
+ return repo.listFiles(api.ui.top || api.ui.cwd, api.ui.paths ?? [], {
66
+ commit: api.ui.rev,
67
+ });
68
+ } catch {
69
+ return [];
70
+ }
71
+ };
72
+
73
+ const mergeUnitFiles = (entries, names) => {
74
+ const have = new Set();
75
+ for (const entry of entries) have.add(entry.path);
76
+ const extra = [];
77
+ for (const rel of names) {
78
+ if (!rel || have.has(rel)) continue;
79
+ extra.push(blankEntry(rel));
80
+ }
81
+ const merged = [...entries, ...extra];
82
+ merged.sort((left, right) => left.path.localeCompare(right.path, 'en'));
83
+ return merged;
84
+ };
85
+
46
86
  const fileList = (api, notes = noteCounts(api.review.store)) => {
47
87
  const entries = fileEntries(api.collection.items);
48
88
  const sourceLabel = api.ui.sourceLabel;
49
89
  const from = sourceLabel ? 'pr' : 'commit';
50
90
  const to = sourceLabel || api.ui.revShort;
51
91
  const labeled = relabelStatus(entries, from, to);
52
- labeled.unshift(todosEntry(notes));
53
- return labeled;
92
+ const names = listedNames(api);
93
+ const scoped =
94
+ api.nav.fileScope === 'file' ? mergeUnitFiles(labeled, names) : labeled;
95
+ scoped.unshift(todosEntry(notes));
96
+ return scoped;
54
97
  };
55
98
 
56
99
  const counts = (api, notes = noteCounts(api.review.store)) => {
@@ -76,11 +119,34 @@ const counts = (api, notes = noteCounts(api.review.store)) => {
76
119
 
77
120
  const viewStatus = (api) => api.ui.status || api.ui.busy;
78
121
 
122
+ const unitFileText = (api) => {
123
+ const rel = api.nav.reviewPath;
124
+ if (!rel) return '';
125
+ const repo = api.ui.repo;
126
+ if (repo && typeof repo.fileText === 'function') {
127
+ return repo.fileText(api.ui.top, rel, api.ui.rev);
128
+ }
129
+ return '';
130
+ };
131
+
132
+ const unitBodyLines = (api) => {
133
+ if (api.nav.pane !== 'unit') return [];
134
+ const rel = api.nav.reviewPath;
135
+ const group = itemsForPath(api.collection.items, rel);
136
+ const live = api.composer.liveFileText();
137
+ if (live !== null) {
138
+ const base = api.composer.fileEditRows();
139
+ if (base && base.length) return mergeEditRows(base, splitEditor(live));
140
+ return mergeEditRows(unitLines(live, group), splitEditor(live));
141
+ }
142
+ return unitLines(unitFileText(api), group);
143
+ };
144
+
79
145
  const view = (api) => {
80
146
  const { composer, nav, ui, gitBranches, commits, updater } = api;
81
147
  const templates = composer.shownTemplates();
82
148
  const change = ui.change;
83
- const sourceKind = (change && SOURCE_KIND[change.source]) || '';
149
+ const sourceKind = (change && change.source) || '';
84
150
  const notes = noteCounts(api.review.store);
85
151
  return {
86
152
  item: ui.current(),
@@ -94,9 +160,12 @@ const view = (api) => {
94
160
  counts: counts(api, notes),
95
161
  selection: nav.selection,
96
162
  pane: nav.pane,
163
+ fileScope: nav.fileScope,
97
164
  files: fileList(api, notes),
98
165
  fileCursor: nav.fileCursor,
99
166
  reviewPath: nav.reviewPath,
167
+ unitLine: nav.unitLine,
168
+ unitLines: unitBodyLines(api),
100
169
  branch: ui.branch,
101
170
  branches: gitBranches.branches,
102
171
  branchCursor: nav.branchCursor,
@@ -128,4 +197,4 @@ const createView = (api) => ({
128
197
  view: () => view(api),
129
198
  });
130
199
 
131
- module.exports = { SOURCE_KIND, relabelStatus, createView };
200
+ module.exports = { relabelStatus, createView };
@@ -72,25 +72,127 @@ const closeWatcher = (watcher) => {
72
72
  }
73
73
  };
74
74
 
75
- const schedule = (state) => {
76
- if (state.closed) return;
77
- if (state.timer !== null) state.clearTimeoutFn(state.timer);
78
- state.timer = state.setTimeoutFn(() => {
79
- state.timer = null;
75
+ const reviewMarkdown = (rel) => {
76
+ const norm = `${rel}`.replaceAll('\\', '/');
77
+ if (!norm.startsWith('.review/')) return false;
78
+ const rest = norm.slice('.review/'.length);
79
+ if (!rest || rest.includes('/')) return false;
80
+ return rest.endsWith('.md');
81
+ };
82
+
83
+ const isReviewDir = (root, dir) => {
84
+ const rel = path.relative(root, dir).replaceAll('\\', '/');
85
+ return rel === '.review' || rel.startsWith('.review/');
86
+ };
87
+
88
+ const hasFile = (full) => {
89
+ try {
90
+ fs.accessSync(full);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ };
96
+
97
+ const isBareReviewName = (root, dir, rel) => {
98
+ if (!rel || rel.includes('/') || !rel.endsWith('.md')) return false;
99
+ const base = path.basename(rel);
100
+ if (hasFile(path.join(dir, base))) return false;
101
+ return hasFile(path.join(root, '.review', base));
102
+ };
103
+
104
+ const isReviewEvent = (root, dir, name) => {
105
+ if (isReviewDir(root, dir)) return true;
106
+ if (!name) return false;
107
+ const rel = path.relative(root, path.join(dir, name));
108
+ const norm = rel.replaceAll('\\', '/');
109
+ if (reviewMarkdown(norm)) return true;
110
+ return isBareReviewName(root, dir, norm);
111
+ };
112
+
113
+ const isOwnDirEvent = (dir, name) => {
114
+ if (!name) return false;
115
+ const base = path.basename(dir);
116
+ const slash = `${name}`.replaceAll('\\', '/');
117
+ const bare = slash.startsWith('./') ? slash.slice(2) : slash;
118
+ const trimmed = bare.endsWith('/') ? bare.slice(0, -1) : bare;
119
+ if (trimmed === base) {
120
+ try {
121
+ return !fs.statSync(path.join(dir, trimmed)).isFile();
122
+ } catch {
123
+ return true;
124
+ }
125
+ }
126
+ const full = path.resolve(dir, name);
127
+ if (full === path.resolve(dir)) return true;
128
+ if (path.basename(full) !== base) return false;
129
+ try {
130
+ return fs.realpathSync(full) === fs.realpathSync(dir);
131
+ } catch {
132
+ return false;
133
+ }
134
+ };
135
+
136
+ const schedule = (state, timerKey, fire) => {
137
+ if (state.closed || !fire) return;
138
+ if (state[timerKey] !== null) clearTimeout(state[timerKey]);
139
+ state[timerKey] = setTimeout(() => {
140
+ state[timerKey] = null;
80
141
  if (state.closed) return;
81
- state.onChange();
142
+ fire();
82
143
  }, state.debounceMs);
144
+ const timer = state[timerKey];
145
+ if (timer && typeof timer.unref === 'function') timer.unref();
146
+ };
147
+
148
+ const REVIEW_HOLD_MS = 200;
149
+
150
+ const reviewBurst = (state) => {
151
+ const seen = state.reviewSeenAt;
152
+ if (!seen) return false;
153
+ const hold = Math.max(state.debounceMs * 2, REVIEW_HOLD_MS);
154
+ return Date.now() - seen < hold;
155
+ };
156
+
157
+ const scheduleChange = (state) => {
158
+ schedule(state, 'timer', () => {
159
+ if (reviewBurst(state)) return;
160
+ if (state.onChange) state.onChange();
161
+ });
162
+ };
163
+
164
+ const scheduleUnnamed = (state) => {
165
+ if (reviewBurst(state)) return;
166
+ schedule(state, 'looseTimer', () => {
167
+ if (reviewBurst(state)) return;
168
+ scheduleChange(state);
169
+ });
170
+ };
171
+
172
+ const dropChange = (state) => {
173
+ if (state.timer !== null) clearTimeout(state.timer);
174
+ if (state.looseTimer !== null) clearTimeout(state.looseTimer);
175
+ state.timer = null;
176
+ state.looseTimer = null;
83
177
  };
84
178
 
85
179
  const handleWatchEvent = (state, dir, eventType, filename) => {
86
180
  if (state.closed) return;
87
181
  const name = filename ? `${filename}` : '';
88
182
  const rel = name ? path.relative(state.root, path.join(dir, name)) : '';
183
+ if (isReviewEvent(state.root, dir, name)) {
184
+ state.reviewSeenAt = Date.now();
185
+ dropChange(state);
186
+ return void schedule(state, 'reviewTimer', state.onReview);
187
+ }
89
188
  if (rel && ignoredRel(rel)) return;
189
+ if (reviewBurst(state)) return;
190
+ if (!name) return void scheduleUnnamed(state);
191
+ if (isOwnDirEvent(dir, name)) return;
90
192
  if (eventType === 'rename' && name) {
91
193
  state.watchNewDir(path.join(dir, name));
92
194
  }
93
- schedule(state);
195
+ scheduleChange(state);
94
196
  };
95
197
 
96
198
  const watchOne = (state, target, extra = {}) => {
@@ -100,9 +202,9 @@ const watchOne = (state, target, extra = {}) => {
100
202
  const onEvent = (eventType, filename) => {
101
203
  handleWatchEvent(state, target, eventType, filename);
102
204
  };
103
- const watcher = state.watchFn(
205
+ const watcher = fs.watch(
104
206
  target,
105
- { persistent: true, encoding: 'utf8', ...extra },
207
+ { persistent: false, encoding: 'utf8', ...extra },
106
208
  onEvent,
107
209
  );
108
210
  if (typeof watcher.on === 'function') {
@@ -120,7 +222,7 @@ const watchTree = (state, dir) => {
120
222
  if (state.recursive) return;
121
223
  let entries;
122
224
  try {
123
- entries = state.readdirSync(dir, { withFileTypes: true });
225
+ entries = fs.readdirSync(dir, { withFileTypes: true });
124
226
  } catch {
125
227
  return;
126
228
  }
@@ -135,11 +237,12 @@ const maybeWatchNewDir = (state, full) => {
135
237
  if (state.recursive) return;
136
238
  let stat;
137
239
  try {
138
- stat = state.statSync(full);
240
+ stat = fs.statSync(full);
139
241
  } catch {
140
242
  return;
141
243
  }
142
244
  if (!stat.isDirectory()) return;
245
+ if (path.basename(full) === '.review') return void watchOne(state, full);
143
246
  if (skipDirName(path.basename(full))) return;
144
247
  watchTree(state, full);
145
248
  };
@@ -148,15 +251,11 @@ const watchScope = (state, spec) => {
148
251
  const target = path.resolve(state.root, spec);
149
252
  let stat;
150
253
  try {
151
- stat = state.statSync(target);
254
+ stat = fs.statSync(target);
152
255
  } catch {
153
- watchOne(state, path.dirname(target));
154
- return;
155
- }
156
- if (stat.isDirectory()) {
157
- watchTree(state, target);
158
- return;
256
+ return void watchOne(state, path.dirname(target));
159
257
  }
258
+ if (stat.isDirectory()) return void watchTree(state, target);
160
259
  watchOne(state, path.dirname(target));
161
260
  };
162
261
 
@@ -168,8 +267,12 @@ const watchGit = (state) => {
168
267
 
169
268
  const stopState = (state) => {
170
269
  state.closed = true;
171
- if (state.timer !== null) state.clearTimeoutFn(state.timer);
270
+ if (state.timer !== null) clearTimeout(state.timer);
271
+ if (state.reviewTimer !== null) clearTimeout(state.reviewTimer);
272
+ if (state.looseTimer !== null) clearTimeout(state.looseTimer);
172
273
  state.timer = null;
274
+ state.reviewTimer = null;
275
+ state.looseTimer = null;
173
276
  while (state.watchers.length) closeWatcher(state.watchers.pop());
174
277
  state.watched.clear();
175
278
  };
@@ -186,24 +289,24 @@ const createDiskWatcher = (options) => {
186
289
  const recursive = options.recursive ?? canWatchRecursive();
187
290
  const state = {
188
291
  root: path.resolve(options.root),
189
- watchFn: options.watch ?? fs.watch,
190
- readdirSync: options.readdirSync ?? fs.readdirSync,
191
- statSync: options.statSync ?? fs.statSync,
192
- setTimeoutFn: options.setTimeout ?? setTimeout,
193
- clearTimeoutFn: options.clearTimeout ?? clearTimeout,
194
292
  debounceMs: options.debounceMs ?? DEBOUNCE_MS,
195
293
  onChange: options.onChange,
196
294
  recursive,
197
295
  dirWatchOpts: recursive ? { recursive: true } : {},
198
296
  watchers: [],
199
297
  watched: new Set(),
298
+ onReview: options.onReview,
200
299
  timer: null,
300
+ reviewTimer: null,
301
+ looseTimer: null,
302
+ reviewSeenAt: 0,
201
303
  closed: false,
202
304
  watchNewDir: (full) => maybeWatchNewDir(state, full),
203
305
  };
204
306
  const scopes = options.paths && options.paths.length ? options.paths : ['.'];
205
307
  for (const spec of scopes) watchScope(state, spec);
206
308
  watchGit(state);
309
+ watchOne(state, path.resolve(state.root, '.review'));
207
310
  return {
208
311
  close: () => stopState(state),
209
312
  watchFiles: (rels) => watchFiles(state, rels),
@@ -215,5 +318,6 @@ module.exports = {
215
318
  POLL_MS,
216
319
  canWatchRecursive,
217
320
  ignoredRel,
321
+ isOwnDirEvent,
218
322
  createDiskWatcher,
219
323
  };