reslop 0.2.4 → 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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  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 +196 -404
  14. package/lib/deps.js +240 -399
  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 +138 -141
  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 +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  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 +157 -132
  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 +16 -26
  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 +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  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 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  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 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
package/lib/update.js CHANGED
@@ -8,7 +8,7 @@ const metautil = require('metautil');
8
8
  const { jsonParse, isHashObject } = metautil;
9
9
  const utilities = require('./utilities.js');
10
10
  const { parseVersion, cmpVersion, npmBin } = utilities;
11
- const { npmOpts, runProc, oneLine } = utilities;
11
+ const { npmOpts, runProc, oneLine, asText } = utilities;
12
12
 
13
13
  const PKG = require('../package.json');
14
14
 
@@ -21,21 +21,10 @@ const INSTALL_MS = 120000;
21
21
  const REGISTRY = 'https://registry.npmjs.org';
22
22
 
23
23
  const RELEASE = /^\d+\.\d+\.\d+$/;
24
- const emptyCache = () => ({
25
- checkedAt: 0,
26
- compatible: '',
27
- major: '',
28
- skipped: '',
29
- });
24
+ const NO_TARGETS = { compatible: '', major: '' };
30
25
 
31
- const asText = (value) => (typeof value === 'string' ? value : '');
32
-
33
- const toInt = (text) => parseInt(text, 10);
34
-
35
- const isNpmInstall = (pkgDir) => {
36
- const parent = path.basename(path.dirname(pkgDir));
37
- return parent === 'node_modules';
38
- };
26
+ const isNpmInstall = (pkgDir) =>
27
+ path.basename(path.dirname(pkgDir)) === 'node_modules';
39
28
 
40
29
  const cacheFile = (env = process.env) => {
41
30
  if (env.XDG_CACHE_HOME) {
@@ -51,29 +40,26 @@ const cacheFile = (env = process.env) => {
51
40
 
52
41
  const readCache = (file) => {
53
42
  try {
54
- const text = fs.readFileSync(file, 'utf8');
55
- const data = jsonParse(text);
56
- if (!isHashObject(data)) {
57
- return emptyCache();
43
+ const data = jsonParse(fs.readFileSync(file, 'utf8'));
44
+ if (isHashObject(data)) {
45
+ return {
46
+ checkedAt: parseInt(data.checkedAt, 10) || 0,
47
+ compatible: asText(data.compatible),
48
+ major: asText(data.major) || asText(data.latest),
49
+ skipped: asText(data.skipped) || asText(data.handled),
50
+ };
58
51
  }
59
- const checkedAt = toInt(data.checkedAt) || 0;
60
- const compatible = asText(data.compatible);
61
- const major = asText(data.major) || asText(data.latest);
62
- const skipped = asText(data.skipped) || asText(data.handled);
63
- return { checkedAt, compatible, major, skipped };
64
52
  } catch {
65
- return emptyCache();
53
+ // missing or unreadable cache
66
54
  }
55
+ return { checkedAt: 0, compatible: '', major: '', skipped: '' };
67
56
  };
68
57
 
69
58
  const writeCache = (file, data) => {
59
+ const { checkedAt, compatible, major, skipped } = data;
60
+ const body = JSON.stringify({ checkedAt, compatible, major, skipped });
70
61
  try {
71
62
  fs.mkdirSync(path.dirname(file), { recursive: true });
72
- const checkedAt = data.checkedAt ?? 0;
73
- const compatible = data.compatible ?? '';
74
- const major = data.major ?? '';
75
- const skipped = data.skipped ?? '';
76
- const body = JSON.stringify({ checkedAt, compatible, major, skipped });
77
63
  fs.writeFileSync(file, `${body}\n`);
78
64
  } catch {
79
65
  // ignore cache write errors
@@ -81,76 +67,43 @@ const writeCache = (file, data) => {
81
67
  };
82
68
 
83
69
  const isFresh = (cache, now, interval = CHECK_INTERVAL_MS) => {
84
- if (!cache || !cache.checkedAt) return false;
85
- if (now < cache.checkedAt) return false;
70
+ if (!cache.checkedAt || now < cache.checkedAt) return false;
86
71
  return now - cache.checkedAt < interval;
87
72
  };
88
73
 
89
74
  const listVersions = (data) => {
90
75
  if (!data || typeof data !== 'object') return [];
91
76
  const names = [];
92
- const map = data.versions;
93
- if (isHashObject(map)) {
94
- for (const name of Object.keys(map)) names.push(name);
95
- }
77
+ if (isHashObject(data.versions)) names.push(...Object.keys(data.versions));
96
78
  const tags = data['dist-tags'];
97
79
  if (tags && typeof tags.latest === 'string') names.push(tags.latest);
98
80
  if (typeof data.version === 'string') names.push(data.version);
99
- const listed = [];
100
- for (const name of names) {
101
- if (!RELEASE.test(name)) continue;
102
- if (listed.includes(name)) continue;
103
- listed.push(name);
104
- }
105
- return listed;
81
+ return [...new Set(names)].filter((name) => RELEASE.test(name));
106
82
  };
107
83
 
108
- const pickNewerName = (best, name, ver) => {
84
+ const newer = (best, name) => {
109
85
  if (!best) return name;
110
- const prev = parseVersion(best);
111
- if (!prev || cmpVersion(ver, prev) > 0) return name;
112
- return best;
86
+ return cmpVersion(parseVersion(name), parseVersion(best)) > 0 ? name : best;
113
87
  };
114
88
 
115
89
  const pickTargets = (current, names) => {
116
90
  const from = parseVersion(current);
117
- if (!from) return { compatible: '', major: '' };
91
+ if (!from) return NO_TARGETS;
118
92
  let compatible = '';
119
93
  let major = '';
120
94
  for (const name of names) {
121
95
  const ver = parseVersion(name);
122
- if (!ver) continue;
123
96
  if (cmpVersion(ver, from) <= 0) continue;
124
- if (ver.major === from.major) {
125
- compatible = pickNewerName(compatible, name, ver);
126
- continue;
127
- }
128
- if (ver.major > from.major) major = pickNewerName(major, name, ver);
97
+ if (ver.major === from.major) compatible = newer(compatible, name);
98
+ else if (ver.major > from.major) major = newer(major, name);
129
99
  }
130
100
  return { compatible, major };
131
101
  };
132
102
 
133
- const classifyLatest = (current, latest) => {
134
- const from = parseVersion(current);
135
- const to = parseVersion(latest);
136
- if (!from || !to || cmpVersion(to, from) <= 0) {
137
- return { compatible: '', major: '' };
138
- }
139
- if (to.major > from.major) return { compatible: '', major: latest };
140
- return { compatible: latest, major: '' };
141
- };
142
-
143
- const planUpdate = (current, extra = {}) => {
103
+ const planUpdate = (current, targets) => {
144
104
  const from = parseVersion(current);
145
105
  if (!from) return { action: 'none' };
146
- let compatible = extra.compatible ?? '';
147
- let major = extra.major ?? '';
148
- const skipped = extra.skipped ?? extra.handled ?? '';
149
- if (!compatible && !major && extra.latest) {
150
- const classified = classifyLatest(current, extra.latest);
151
- compatible = classified.compatible;
152
- major = classified.major;
153
- }
106
+ const { compatible, major, skipped } = targets;
154
107
  const compatVer = parseVersion(compatible);
155
108
  if (compatVer && compatVer.major === from.major) {
156
109
  if (cmpVersion(compatVer, from) > 0) {
@@ -164,53 +117,40 @@ const planUpdate = (current, extra = {}) => {
164
117
  return { action: 'confirm', current, latest: major };
165
118
  };
166
119
 
167
- const isEnabled = (options = {}) => {
168
- if (options.enabled === false) return false;
169
- if (options.enabled === true) return true;
170
- const pkgDir = options.pkgDir ?? PKG_DIR;
171
- return isNpmInstall(pkgDir);
120
+ const isEnabled = (options) => {
121
+ if (typeof options.enabled === 'boolean') return options.enabled;
122
+ return isNpmInstall(options.pkgDir ?? PKG_DIR);
172
123
  };
173
124
 
174
- const fetchTargets = async (current, options = {}) => {
175
- const empty = { compatible: '', major: '' };
125
+ const fetchTargets = async (current, options) => {
176
126
  const fetchImpl = options.fetch ?? globalThis.fetch;
177
- if (typeof fetchImpl !== 'function') return empty;
178
- const encoded = encodeURIComponent(PKG_NAME);
179
- const url = `${REGISTRY}/${encoded}`;
127
+ if (typeof fetchImpl !== 'function') return NO_TARGETS;
128
+ const url = `${REGISTRY}/${encodeURIComponent(PKG_NAME)}`;
180
129
  const headers = { accept: 'application/json' };
181
130
  const signal = options.signal ?? AbortSignal.timeout(FETCH_MS);
182
131
  const response = await fetchImpl(url, { headers, signal });
183
- if (!response || !response.ok) return empty;
132
+ if (!response || !response.ok) return NO_TARGETS;
184
133
  const data = await response.json();
185
134
  return pickTargets(current, listVersions(data));
186
135
  };
187
136
 
188
- const npmInstall = async (spec, extra = {}) => {
189
- const timeout = extra.timeout ?? INSTALL_MS;
190
- const result = await runProc(
191
- npmBin(),
192
- ['install', '-g', spec],
193
- npmOpts({ timeout }),
194
- );
195
- if (result.error) throw result.error;
196
- if (result.status !== 0) {
197
- throw new Error(oneLine(result.stderr, 'npm install failed'));
198
- }
199
- };
200
-
201
137
  const installUpdate = async (version, options = {}) => {
202
138
  if (typeof options.install === 'function') {
203
139
  await options.install(version);
204
140
  return;
205
141
  }
206
- const spec = `${PKG_NAME}@${version}`;
207
- await npmInstall(spec, options);
142
+ const timeout = options.timeout ?? INSTALL_MS;
143
+ const args = ['install', '-g', `${PKG_NAME}@${version}`];
144
+ const result = await runProc(npmBin(), args, npmOpts({ timeout }));
145
+ if (result.error) throw result.error;
146
+ if (result.status !== 0) {
147
+ throw new Error(oneLine(result.stderr, 'npm install failed'));
148
+ }
208
149
  };
209
150
 
210
151
  const markSkipped = (version, options = {}) => {
211
152
  const file = options.cacheFile ?? cacheFile(options.env);
212
- const cache = readCache(file);
213
- writeCache(file, { ...cache, skipped: version });
153
+ writeCache(file, { ...readCache(file), skipped: version });
214
154
  };
215
155
 
216
156
  const checkUpdate = async (options = {}) => {
@@ -218,39 +158,28 @@ const checkUpdate = async (options = {}) => {
218
158
  const now = Date.now();
219
159
  const current = options.current ?? PKG_VERSION;
220
160
  const file = options.cacheFile ?? cacheFile(options.env);
221
- const interval = options.interval ?? CHECK_INTERVAL_MS;
222
161
  const cache = readCache(file);
223
- if (isFresh(cache, now, interval)) {
162
+ if (isFresh(cache, now, options.interval)) {
224
163
  const plan = planUpdate(current, cache);
225
- if (plan.action === 'install') return { action: 'none' };
226
- return plan;
164
+ return plan.action === 'install' ? { action: 'none' } : plan;
227
165
  }
228
- let targets = { compatible: '', major: '' };
166
+ let targets = NO_TARGETS;
229
167
  try {
230
168
  targets = await fetchTargets(current, options);
231
169
  } catch {
232
170
  // ignore registry errors
233
171
  }
234
- const next = {
235
- ...cache,
236
- checkedAt: now,
237
- compatible: targets.compatible,
238
- major: targets.major,
239
- };
240
172
  if (!targets.compatible && !targets.major) {
241
173
  writeCache(file, { ...cache, checkedAt: now });
242
174
  return { action: 'none' };
243
175
  }
176
+ const next = { ...cache, checkedAt: now, ...targets };
244
177
  writeCache(file, next);
245
178
  return planUpdate(current, next);
246
179
  };
247
180
 
248
181
  module.exports = {
249
182
  CHECK_INTERVAL_MS,
250
- PKG_NAME,
251
- PKG_VERSION,
252
- parseVersion,
253
- cmpVersion,
254
183
  isNpmInstall,
255
184
  cacheFile,
256
185
  readCache,
package/lib/utilities.js CHANGED
@@ -9,16 +9,12 @@ const IS_WIN = process.platform === 'win32';
9
9
  const DEFAULT_ENCODING = 'utf8';
10
10
  const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;
11
11
 
12
- const UNIX_CLIP = [
13
- { cmd: 'wl-copy', args: [] },
14
- { cmd: 'xclip', args: ['-selection', 'clipboard'] },
15
- { cmd: 'xsel', args: ['--clipboard', '--input'] },
16
- ];
17
-
18
12
  const parseVersion = (text) => {
19
13
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
20
14
  if (!match) return null;
21
- const [, major, minor, patch] = match.map(Number);
15
+ const major = Number(match[1]);
16
+ const minor = Number(match[2]);
17
+ const patch = Number(match[3]);
22
18
  return { major, minor, patch };
23
19
  };
24
20
 
@@ -30,20 +26,30 @@ const cmpVersion = (left, right) =>
30
26
  const oneLine = (text, fallback = '') =>
31
27
  split(`${text ?? ''}`.trim(), '\n')[0] || fallback;
32
28
 
33
- const npmBin = (platform = process.platform) =>
34
- platform === 'win32' ? 'npm.cmd' : 'npm';
29
+ const asText = (value) => (typeof value === 'string' ? value : '');
30
+
31
+ const unquote = (value) => {
32
+ const text = `${value ?? ''}`;
33
+ if (text.length < 2) return text;
34
+ const open = text[0];
35
+ const quoted = open === text.at(-1) && (open === '"' || open === '\x27');
36
+ return quoted ? text.slice(1, -1) : text;
37
+ };
38
+
39
+ const pad2 = (n) => `${n}`.padStart(2, '0');
40
+
41
+ const dateStamp = (date) => {
42
+ const month = pad2(date.getMonth() + 1);
43
+ return `${date.getFullYear()}-${month}-${pad2(date.getDate())}`;
44
+ };
45
+
46
+ const npmBin = () => (IS_WIN ? 'npm.cmd' : 'npm');
35
47
 
36
48
  const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
37
49
 
38
50
  const npmOpts = (extra = {}, platform = process.platform) =>
39
51
  spawnBase({ shell: platform === 'win32', ...extra });
40
52
 
41
- const clipTools = (platform = process.platform) => {
42
- if (platform === 'win32') return [{ cmd: 'clip', args: [] }];
43
- if (platform === 'darwin') return [{ cmd: 'pbcopy', args: [] }];
44
- return UNIX_CLIP;
45
- };
46
-
47
53
  const listen = (emitter, event, handler) => {
48
54
  if (!emitter || typeof emitter.on !== 'function') return () => {};
49
55
  emitter.on(event, handler);
@@ -74,69 +80,36 @@ const watchResize = (stdout, proc, onResize, signal) => {
74
80
  return stop;
75
81
  };
76
82
 
77
- const abortError = () => {
78
- const error = new Error('aborted');
79
- error.code = 'ABORT';
80
- return error;
81
- };
83
+ const codeError = (message, code) =>
84
+ Object.assign(new Error(message), { code });
82
85
 
83
- const isAbort = (error) => !!(error && error.code === 'ABORT');
86
+ const abortError = () => codeError('aborted', 'ABORT');
84
87
 
85
- const asOutput = (value) => {
86
- if (value === undefined || value === null) return '';
87
- if (typeof value === 'string') return value;
88
- return `${value}`;
89
- };
88
+ const isAbort = (error) => !!(error && error.code === 'ABORT');
90
89
 
91
- const procResult = (values = {}) => {
92
- const status = values.status ?? null;
93
- const stdout = asOutput(values.stdout);
94
- const stderr = asOutput(values.stderr);
95
- const error = values.error ?? null;
96
- return { status, stdout, stderr, error };
97
- };
90
+ const procResult = (values = {}) => ({
91
+ status: values.status ?? null,
92
+ stdout: `${values.stdout ?? ''}`,
93
+ stderr: `${values.stderr ?? ''}`,
94
+ error: values.error ?? null,
95
+ });
98
96
 
99
- const spawnOptions = (options = {}) => {
100
- const encoding = options.encoding ?? DEFAULT_ENCODING;
101
- const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
97
+ const runProcSync = (cmd, args, options = {}) => {
102
98
  const opts = spawnBase({
103
99
  cwd: options.cwd,
104
100
  env: options.env,
105
- encoding,
101
+ encoding: options.encoding ?? DEFAULT_ENCODING,
106
102
  input: options.input,
107
- maxBuffer,
103
+ maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER,
108
104
  timeout: options.timeout,
109
105
  });
110
106
  if (options.shell) opts.shell = true;
111
- return opts;
112
- };
113
-
114
- const runProcSync = (cmd, args, options = {}) => {
115
- const result = spawnSync(cmd, args, spawnOptions(options));
116
- return procResult(result);
117
- };
118
-
119
- const attachOutput = (stream, encoding, onChunk) => {
120
- if (!stream) return;
121
- stream.setEncoding(encoding);
122
- stream.on('data', onChunk);
123
- };
124
-
125
- const writeInput = (child, input) => {
126
- if (!child.stdin) return;
127
- try {
128
- if (input) child.stdin.end(input);
129
- else child.stdin.end();
130
- } catch {
131
- // closed
132
- }
107
+ return procResult(spawnSync(cmd, args, opts));
133
108
  };
134
109
 
135
110
  const listenChild = (child, options, resolve) => {
136
111
  const encoding = options.encoding ?? DEFAULT_ENCODING;
137
112
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
138
- const timeout = options.timeout;
139
- const input = options.input;
140
113
  const signal = options.signal;
141
114
  let stdout = '';
142
115
  let stderr = '';
@@ -146,46 +119,49 @@ const listenChild = (child, options, resolve) => {
146
119
  const finish = (result) => {
147
120
  if (settled) return;
148
121
  settled = true;
149
- if (timer) clearTimeout(timer);
150
- timer = null;
151
- if (signal && onAbort) {
152
- signal.removeEventListener('abort', onAbort);
153
- }
154
- onAbort = null;
122
+ clearTimeout(timer);
123
+ if (signal) signal.removeEventListener('abort', onAbort);
155
124
  resolve(result);
156
125
  };
157
- onAbort = () => {
126
+ const fail = (error) => {
158
127
  child.kill();
159
- finish(procResult({ stdout, stderr, error: abortError() }));
128
+ finish(procResult({ stdout, stderr, error }));
160
129
  };
161
- const append = (kind, chunk) => {
162
- if (kind === 'out') stdout += chunk;
130
+ onAbort = () => fail(abortError());
131
+ const append = (chunk, isOut) => {
132
+ if (isOut) stdout += chunk;
163
133
  else stderr += chunk;
164
- if (stdout.length + stderr.length <= maxBuffer) return;
165
- child.kill();
166
- const error = new Error('maxBuffer exceeded');
167
- error.code = 'ENOBUFS';
168
- finish(procResult({ stdout, stderr, error }));
134
+ if (stdout.length + stderr.length > maxBuffer) {
135
+ fail(codeError('maxBuffer exceeded', 'ENOBUFS'));
136
+ }
169
137
  };
170
138
  if (signal) signal.addEventListener('abort', onAbort);
171
- if (timeout) {
172
- timer = setTimeout(() => {
173
- child.kill();
174
- const error = new Error('timed out');
175
- error.code = 'ETIMEDOUT';
176
- finish(procResult({ stdout, stderr, error }));
177
- }, timeout);
139
+ if (options.timeout) {
140
+ const onTimeout = () => fail(codeError('timed out', 'ETIMEDOUT'));
141
+ timer = setTimeout(onTimeout, options.timeout);
178
142
  }
179
143
  if (signal && signal.aborted) onAbort();
180
- attachOutput(child.stdout, encoding, (chunk) => append('out', chunk));
181
- attachOutput(child.stderr, encoding, (chunk) => append('err', chunk));
144
+ if (child.stdout) {
145
+ child.stdout.setEncoding(encoding);
146
+ child.stdout.on('data', (chunk) => append(chunk, true));
147
+ }
148
+ if (child.stderr) {
149
+ child.stderr.setEncoding(encoding);
150
+ child.stderr.on('data', (chunk) => append(chunk, false));
151
+ }
182
152
  child.on('error', (error) => {
183
153
  finish(procResult({ stdout, stderr, error }));
184
154
  });
185
155
  child.on('close', (status) => {
186
156
  finish(procResult({ status, stdout, stderr }));
187
157
  });
188
- writeInput(child, input);
158
+ if (!child.stdin) return;
159
+ try {
160
+ if (options.input) child.stdin.end(options.input);
161
+ else child.stdin.end();
162
+ } catch {
163
+ // closed
164
+ }
189
165
  };
190
166
 
191
167
  const runProc = (cmd, args, options = {}) =>
@@ -214,15 +190,17 @@ module.exports = {
214
190
  parseVersion,
215
191
  cmpVersion,
216
192
  oneLine,
193
+ asText,
194
+ unquote,
195
+ pad2,
196
+ dateStamp,
217
197
  npmBin,
218
198
  spawnBase,
219
199
  npmOpts,
220
- clipTools,
221
200
  listen,
222
201
  watchResize,
223
202
  abortError,
224
203
  isAbort,
225
- procResult,
226
204
  runProcSync,
227
205
  runProc,
228
206
  };
package/lib/wrap.js CHANGED
@@ -12,53 +12,37 @@ const wrapPlain = (text, width) => {
12
12
  let col = 0;
13
13
  let lastBreak = -1;
14
14
  let i = 0;
15
- const slice = (from, to) => {
16
- let out = '';
17
- for (let k = from; k < to; k++) out += parts[k];
18
- return out;
19
- };
20
- const flush = (end) => {
21
- rows.push(slice(start, end));
22
- start = end;
23
- col = 0;
24
- lastBreak = -1;
25
- };
26
15
  while (i < parts.length) {
27
16
  const ch = parts[i];
28
17
  const w = graphemeWidth(ch, col);
29
18
  if (col + w > width && col > 0) {
30
- if (lastBreak >= start) {
31
- flush(lastBreak + 1);
32
- i = start;
33
- continue;
34
- }
35
- flush(i);
19
+ const end = lastBreak >= start ? lastBreak + 1 : i;
20
+ rows.push(parts.slice(start, end).join(''));
21
+ start = end;
22
+ i = end;
23
+ col = 0;
24
+ lastBreak = -1;
36
25
  continue;
37
26
  }
38
27
  col += w;
39
28
  if (ch === ' ' || ch === '\t') lastBreak = i;
40
29
  i += 1;
41
30
  }
42
- rows.push(slice(start, parts.length));
31
+ rows.push(parts.slice(start).join(''));
43
32
  return rows;
44
33
  };
45
34
 
46
35
  const wrapDoc = (text, width) => {
47
36
  const rows = [];
48
- const parts = text.split('\n');
49
37
  let offset = 0;
50
- for (let p = 0; p < parts.length; p++) {
51
- const part = parts[p];
52
- const wrapped = wrapPlain(part, width);
53
- let used = 0;
54
- for (const row of wrapped) {
55
- rows.push({ start: offset + used, text: row });
56
- used += row.length;
38
+ for (const part of text.split('\n')) {
39
+ let start = offset;
40
+ for (const row of wrapPlain(part, width)) {
41
+ rows.push({ start, text: row });
42
+ start += row.length;
57
43
  }
58
- offset += part.length;
59
- if (p < parts.length - 1) offset += 1;
44
+ offset += part.length + 1;
60
45
  }
61
- if (!rows.length) rows.push({ start: 0, text: '' });
62
46
  return rows;
63
47
  };
64
48
 
@@ -69,9 +53,8 @@ const cursorInWrap = (text, cursor, width) => {
69
53
  const rows = wrapDoc(text, width);
70
54
  const clamped = Math.max(0, Math.min(cursor, text.length));
71
55
  let row = rows.length - 1;
72
- for (let i = 0; i < rows.length; i++) {
73
- const next = i + 1 < rows.length ? rows[i + 1].start : text.length + 1;
74
- if (clamped < next) {
56
+ for (let i = 0; i < rows.length - 1; i++) {
57
+ if (clamped < rows[i + 1].start) {
75
58
  row = i;
76
59
  break;
77
60
  }
@@ -81,16 +64,25 @@ const cursorInWrap = (text, cursor, width) => {
81
64
  return { row, col: visibleWidth(line.text.slice(0, take)) };
82
65
  };
83
66
 
84
- const indexAtWrapCol = (row, col, limit) => {
67
+ const colIndex = (text, col) => {
85
68
  let used = 0;
86
- let idx = 0;
87
- for (const ch of graphemes(row.text)) {
69
+ let index = 0;
70
+ for (const ch of graphemes(text)) {
88
71
  const w = graphemeWidth(ch, used);
89
72
  if (used + w > col) break;
90
73
  used += w;
91
- idx += ch.length;
74
+ index += ch.length;
92
75
  }
93
- const pos = row.start + idx;
76
+ return index;
77
+ };
78
+
79
+ const indexAtRow = (start, text, col) => {
80
+ if (col <= 0) return start;
81
+ return start + colIndex(`${text ?? ''}`, col);
82
+ };
83
+
84
+ const indexAtWrapCol = (row, col, limit) => {
85
+ const pos = row.start + colIndex(row.text, col);
94
86
  if (pos < limit) return pos;
95
87
  if (pos <= row.start) return row.start;
96
88
  const parts = graphemes(row.text);
@@ -100,15 +92,11 @@ const indexAtWrapCol = (row, col, limit) => {
100
92
  const wrapMove = (text, cursor, width, delta, wantCol) => {
101
93
  const rows = wrapDoc(text, width);
102
94
  const pos = cursorInWrap(text, cursor, width);
103
- let next = pos.row + delta;
104
- if (next < 0) next = 0;
105
- if (next >= rows.length) next = rows.length - 1;
95
+ const next = Math.min(Math.max(pos.row + delta, 0), rows.length - 1);
106
96
  const col = wantCol ?? pos.col;
107
97
  if (next === pos.row) return { cursor, col };
108
- const row = rows[next];
109
- let limit = text.length + 1;
110
- if (next + 1 < rows.length) limit = rows[next + 1].start;
111
- return { cursor: indexAtWrapCol(row, col, limit), col };
98
+ const limit = next + 1 < rows.length ? rows[next + 1].start : text.length + 1;
99
+ return { cursor: indexAtWrapCol(rows[next], col, limit), col };
112
100
  };
113
101
 
114
102
  module.exports = {
@@ -116,5 +104,6 @@ module.exports = {
116
104
  wrapDoc,
117
105
  wrapMultiline,
118
106
  cursorInWrap,
107
+ indexAtRow,
119
108
  wrapMove,
120
109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",