reslop 0.2.1 → 0.2.3

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.
@@ -0,0 +1,513 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+ const { jsonParse } = require('metautil');
7
+ const { stripAnsi, visibleWidth } = require('./ansi.js');
8
+ const { npmBin, npmOpts, spawnBase } = require('./utilities.js');
9
+
10
+ const MANIFEST = 'package.json';
11
+ const SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies'];
12
+
13
+ const readText = (file) => {
14
+ try {
15
+ return fs.readFileSync(file, 'utf8');
16
+ } catch {
17
+ return '';
18
+ }
19
+ };
20
+
21
+ const readManifest = (root) => {
22
+ const text = readText(path.join(root, MANIFEST));
23
+ if (!text) return null;
24
+ const pkg = jsonParse(text);
25
+ if (!pkg || typeof pkg !== 'object') return null;
26
+ return pkg;
27
+ };
28
+
29
+ const writeManifest = (root, pkg) => {
30
+ const file = path.join(root, MANIFEST);
31
+ fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
32
+ };
33
+
34
+ const scriptEntries = (pkg) => {
35
+ const scripts = pkg && pkg.scripts;
36
+ if (!scripts || typeof scripts !== 'object') return [];
37
+ const entries = [];
38
+ for (const name of Object.keys(scripts)) {
39
+ const command = scripts[name];
40
+ if (typeof command !== 'string') continue;
41
+ entries.push({ name, command, kind: 'script' });
42
+ }
43
+ return entries;
44
+ };
45
+
46
+ const binNames = (pkg) => {
47
+ if (!pkg || typeof pkg !== 'object') return [];
48
+ const bin = pkg.bin;
49
+ if (typeof bin === 'string' && bin) {
50
+ const raw = `${pkg.name ?? ''}`;
51
+ const base = raw.startsWith('@') ? raw.split('/')[1] : raw;
52
+ return base ? [base] : [];
53
+ }
54
+ if (!bin || typeof bin !== 'object' || Array.isArray(bin)) return [];
55
+ const names = [];
56
+ for (const name of Object.keys(bin)) {
57
+ if (typeof bin[name] === 'string' && bin[name]) names.push(name);
58
+ }
59
+ return names;
60
+ };
61
+
62
+ const declaredNames = (pkg) => {
63
+ const names = [];
64
+ for (const section of SECTIONS) {
65
+ const map = pkg[section];
66
+ if (!map || typeof map !== 'object') continue;
67
+ for (const name of Object.keys(map)) names.push(name);
68
+ }
69
+ return names;
70
+ };
71
+
72
+ const depManifest = (root, name) => {
73
+ const parts = name.startsWith('@') ? name.split('/').slice(0, 2) : [name];
74
+ return path.join(root, 'node_modules', ...parts, MANIFEST);
75
+ };
76
+
77
+ const binEntries = (root, pkg, taken) => {
78
+ const entries = [];
79
+ const seen = new Set(taken);
80
+ for (const name of declaredNames(pkg)) {
81
+ const dep = jsonParse(readText(depManifest(root, name)));
82
+ for (const bin of binNames(dep)) {
83
+ if (seen.has(bin)) continue;
84
+ seen.add(bin);
85
+ entries.push({ name: bin, command: bin, kind: 'bin', package: name });
86
+ }
87
+ }
88
+ entries.sort((left, right) => left.name.localeCompare(right.name));
89
+ return entries;
90
+ };
91
+
92
+ const listCommands = (root) => {
93
+ const pkg = readManifest(root);
94
+ if (!pkg) return [];
95
+ const scripts = scriptEntries(pkg);
96
+ const taken = scripts.map((entry) => entry.name);
97
+ return [...scripts, ...binEntries(root, pkg, taken)];
98
+ };
99
+
100
+ const isPassingTest = (line) => {
101
+ const plain = stripAnsi(line);
102
+ const text = plain.trim();
103
+ if (!text) return false;
104
+ if (/^\s*[✔✓√] /.test(plain)) return true;
105
+ if (/^ok \d+ /.test(text)) return true;
106
+ if (/^\s*PASS\b/.test(plain)) return true;
107
+ return false;
108
+ };
109
+
110
+ const isNodeFrame = (line) => stripAnsi(line).includes('(node:');
111
+
112
+ const indentStack = (line) => {
113
+ const plain = stripAnsi(line);
114
+ if (!/^\s*at\s/.test(plain)) return line;
115
+ const lead = line.match(/^\s*/)?.[0] ?? '';
116
+ const body = lead ? line.slice(lead.length) : line;
117
+ return ` ${body}`;
118
+ };
119
+
120
+ const withoutNodeFrame = (line) => {
121
+ if (!isNodeFrame(line)) return line;
122
+ const plain = stripAnsi(line);
123
+ if (/\{\s*$/.test(plain)) return '{';
124
+ return null;
125
+ };
126
+
127
+ const unquote = (value) => {
128
+ const text = `${value ?? ''}`;
129
+ if (text.length < 2) return text;
130
+ const open = text[0];
131
+ const close = text[text.length - 1];
132
+ const code = open.charCodeAt(0);
133
+ const quoted = open === close && (code === 34 || code === 39);
134
+ if (!quoted) return text;
135
+ return text.slice(1, -1);
136
+ };
137
+
138
+ const objectEnd = (lines, start) => {
139
+ if (stripAnsi(lines[start]).trim() !== '{') return -1;
140
+ for (let i = start + 1; i < lines.length; i++) {
141
+ if (stripAnsi(lines[i]).trim() === '}') return i;
142
+ }
143
+ return -1;
144
+ };
145
+
146
+ const parseInspect = (lines) => {
147
+ const entries = [];
148
+ for (const line of lines) {
149
+ const plain = stripAnsi(line).trim().replace(/,$/, '');
150
+ const match = /^([A-Za-z_][\w]*)\s*:\s*([\s\S]*)$/.exec(plain);
151
+ if (!match) {
152
+ if (!entries.length) return null;
153
+ const last = entries[entries.length - 1];
154
+ last[1] = `${last[1]}\n${plain}`;
155
+ continue;
156
+ }
157
+ entries.push([match[1], unquote(match[2])]);
158
+ }
159
+ return entries.length ? entries : null;
160
+ };
161
+
162
+ const showValue = (value) => {
163
+ if (value === null) return 'null';
164
+ if (typeof value === 'string') return value;
165
+ if (typeof value === 'number' || typeof value === 'boolean') {
166
+ return `${value}`;
167
+ }
168
+ return JSON.stringify(value);
169
+ };
170
+
171
+ const parseObject = (lines) => {
172
+ const body = lines.slice(1, -1);
173
+ try {
174
+ const value = JSON.parse(lines.join('\n'));
175
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
176
+ return null;
177
+ }
178
+ const entries = [];
179
+ for (const key of Object.keys(value)) {
180
+ entries.push([key, showValue(value[key])]);
181
+ }
182
+ return entries.length ? entries : null;
183
+ } catch {
184
+ return parseInspect(body);
185
+ }
186
+ };
187
+
188
+ const TABLE_KEY = '\x1b[900m';
189
+ const TABLE_VAL = '\x1b[901m';
190
+
191
+ const formatTable = (entries) => {
192
+ let keyW = 0;
193
+ for (const entry of entries) {
194
+ keyW = Math.max(keyW, visibleWidth(entry[0]));
195
+ }
196
+ const rows = [];
197
+ for (const entry of entries) {
198
+ const parts = `${entry[1]}`.split('\n');
199
+ for (let i = 0; i < parts.length; i++) {
200
+ const name = i === 0 ? entry[0] : '';
201
+ const pad = ' '.repeat(Math.max(0, keyW - visibleWidth(name)));
202
+ const key = ` ${pad}${name} `;
203
+ const value = ` ${parts[i]} `;
204
+ rows.push(`${TABLE_KEY}${key}${TABLE_VAL} ${value}`);
205
+ }
206
+ }
207
+ return rows;
208
+ };
209
+
210
+ const foldObjects = (lines) => {
211
+ const out = [];
212
+ for (let i = 0; i < lines.length; i++) {
213
+ const end = objectEnd(lines, i);
214
+ if (end < 0) {
215
+ out.push(lines[i]);
216
+ continue;
217
+ }
218
+ const entries = parseObject(lines.slice(i, end + 1));
219
+ if (!entries) {
220
+ out.push(lines[i]);
221
+ continue;
222
+ }
223
+ if (out.length && out[out.length - 1] !== '') out.push('');
224
+ for (const row of formatTable(entries)) out.push(row);
225
+ i = end;
226
+ }
227
+ return out;
228
+ };
229
+
230
+ const withSep = (dir) => {
231
+ const sep = dir.includes('\\') ? '\\' : '/';
232
+ if (dir.endsWith(sep)) return dir;
233
+ return `${dir}${sep}`;
234
+ };
235
+
236
+ const stripOne = (text, dir) => {
237
+ if (!dir) return text;
238
+ let next = text.split(withSep(dir)).join('');
239
+ if (next.includes(dir)) next = next.split(dir).join('.');
240
+ return next;
241
+ };
242
+
243
+ const relativize = (text, root) => {
244
+ if (!root) return text;
245
+ const slash = root.replaceAll('\\', '/');
246
+ const back = slash.replaceAll('/', '\\');
247
+ const resolved = path.resolve(root).replaceAll('\\', '/');
248
+ const resolvedBack = resolved.replaceAll('/', '\\');
249
+ let next = text;
250
+ for (const dir of [slash, back, resolved, resolvedBack]) {
251
+ next = stripOne(next, dir);
252
+ }
253
+ return next;
254
+ };
255
+
256
+ const reduceOutput = (text, root, status) => {
257
+ const lines = `${text ?? ''}`.split(/\r?\n/);
258
+ const kept = [];
259
+ for (const line of lines) {
260
+ if (isPassingTest(line)) continue;
261
+ const next = withoutNodeFrame(line);
262
+ if (next === null) continue;
263
+ kept.push(relativize(indentStack(next), root));
264
+ }
265
+ const folded = foldObjects(kept);
266
+ while (folded.length && folded[folded.length - 1] === '') folded.pop();
267
+ if (status === '') {
268
+ if (!folded.length) return '';
269
+ return `${folded.join('\n')}\n`;
270
+ }
271
+ const code = status === undefined || status === null ? 'null' : status;
272
+ const exit = `exit ${code}`;
273
+ if (!folded.length) return `${exit}\n`;
274
+ return `${folded.join('\n')}\n${exit}\n`;
275
+ };
276
+
277
+ const logFileName = (command, date, index) => {
278
+ const safe = `${command}`.replace(/[^A-Za-z0-9._-]+/g, '-');
279
+ const n = `${index}`.padStart(2, '0');
280
+ return `${date}-${safe}-${n}.log`;
281
+ };
282
+
283
+ const dateStamp = (now) => {
284
+ const y = now.getFullYear();
285
+ const m = `${now.getMonth() + 1}`.padStart(2, '0');
286
+ const d = `${now.getDate()}`.padStart(2, '0');
287
+ return `${y}-${m}-${d}`;
288
+ };
289
+
290
+ const LOG_DIR = '.log';
291
+
292
+ const nextLogFile = (root, command, now = new Date()) => {
293
+ const date = dateStamp(now);
294
+ const dir = path.join(root, LOG_DIR);
295
+ let index = 1;
296
+ let name = logFileName(command, date, index);
297
+ let full = path.join(dir, name);
298
+ while (index < 100 && fs.existsSync(full)) {
299
+ index += 1;
300
+ name = logFileName(command, date, index);
301
+ full = path.join(dir, name);
302
+ }
303
+ return { name, full, dir };
304
+ };
305
+
306
+ const logText = (text) =>
307
+ stripAnsi(text)
308
+ .split('\n')
309
+ .map((line) => line.trimEnd())
310
+ .join('\n');
311
+
312
+ const writeLog = (root, command, text, now) => {
313
+ const file = nextLogFile(root, command, now);
314
+ fs.mkdirSync(file.dir, { recursive: true });
315
+ fs.writeFileSync(file.full, logText(text));
316
+ return file.name;
317
+ };
318
+
319
+ const parseScriptLine = (text) => {
320
+ const raw = `${text ?? ''}`.trim();
321
+ const at = raw.indexOf(': ');
322
+ if (at <= 0) return null;
323
+ const name = raw.slice(0, at).trim();
324
+ const command = raw.slice(at + 2).trim();
325
+ if (!name || !command) return null;
326
+ if (!/^[\w:.-]+$/.test(name)) return null;
327
+ return { name, command };
328
+ };
329
+
330
+ const scriptLine = (entry) => `${entry.name}: ${entry.command}`;
331
+
332
+ const orderedScripts = (pkg) => {
333
+ const scripts = pkg.scripts;
334
+ if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
335
+ return {};
336
+ }
337
+ return scripts;
338
+ };
339
+
340
+ const upsertScript = (pkg, prev, next) => {
341
+ const scripts = orderedScripts(pkg);
342
+ const names = Object.keys(scripts);
343
+ const ordered = {};
344
+ if (prev && names.includes(prev)) {
345
+ for (const key of names) {
346
+ if (key === prev) ordered[next.name] = next.command;
347
+ else if (key !== next.name) ordered[key] = scripts[key];
348
+ }
349
+ } else {
350
+ for (const key of names) ordered[key] = scripts[key];
351
+ ordered[next.name] = next.command;
352
+ }
353
+ pkg.scripts = ordered;
354
+ };
355
+
356
+ const dropScript = (pkg, name) => {
357
+ const scripts = orderedScripts(pkg);
358
+ if (!Object.hasOwn(scripts, name)) return false;
359
+ const ordered = {};
360
+ for (const key of Object.keys(scripts)) {
361
+ if (key !== name) ordered[key] = scripts[key];
362
+ }
363
+ pkg.scripts = ordered;
364
+ return true;
365
+ };
366
+
367
+ const moveScript = (pkg, name, delta) => {
368
+ const scripts = orderedScripts(pkg);
369
+ const names = Object.keys(scripts);
370
+ const at = names.indexOf(name);
371
+ if (at < 0) return false;
372
+ const next = at + delta;
373
+ if (next < 0 || next >= names.length) return false;
374
+ const swap = names[at];
375
+ names[at] = names[next];
376
+ names[next] = swap;
377
+ const ordered = {};
378
+ for (const key of names) ordered[key] = scripts[key];
379
+ pkg.scripts = ordered;
380
+ return true;
381
+ };
382
+
383
+ const saveScript = (root, prev, next) => {
384
+ const pkg = readManifest(root) ?? {};
385
+ upsertScript(pkg, prev, next);
386
+ writeManifest(root, pkg);
387
+ };
388
+
389
+ const removeScript = (root, name) => {
390
+ const pkg = readManifest(root);
391
+ if (!pkg || !dropScript(pkg, name)) return false;
392
+ writeManifest(root, pkg);
393
+ return true;
394
+ };
395
+
396
+ const reorderScript = (root, name, delta) => {
397
+ const pkg = readManifest(root);
398
+ if (!pkg || !moveScript(pkg, name, delta)) return false;
399
+ writeManifest(root, pkg);
400
+ return true;
401
+ };
402
+
403
+ const npmArgs = (entry) => {
404
+ if (entry.kind === 'bin') return ['exec', '--', entry.name];
405
+ return ['run', entry.name];
406
+ };
407
+
408
+ const signalPid = (pid, signal) => {
409
+ try {
410
+ process.kill(pid, signal);
411
+ return true;
412
+ } catch {
413
+ return false;
414
+ }
415
+ };
416
+
417
+ const killTree = (proc) => {
418
+ const pid = proc && proc.pid;
419
+ if (!pid) return;
420
+ if (process.platform === 'win32') {
421
+ const killer = spawn(
422
+ 'taskkill',
423
+ ['/pid', String(pid), '/t', '/f'],
424
+ spawnBase({ stdio: 'ignore' }),
425
+ );
426
+ killer.on('error', () => proc.kill());
427
+ return;
428
+ }
429
+ const group = -pid;
430
+ if (!signalPid(group, 'SIGTERM')) signalPid(pid, 'SIGTERM');
431
+ const timer = setTimeout(() => {
432
+ if (!signalPid(group, 'SIGKILL')) signalPid(pid, 'SIGKILL');
433
+ }, 200);
434
+ timer.unref();
435
+ proc.once('close', () => clearTimeout(timer));
436
+ };
437
+
438
+ const commandEnv = (base = process.env) => {
439
+ const env = { ...base };
440
+ if (!env.NO_COLOR && !env.FORCE_COLOR) env.FORCE_COLOR = '1';
441
+ if (!env.TERM) env.TERM = 'xterm-256color';
442
+ env.RESLOP_OUTPUT = 'raw';
443
+ return env;
444
+ };
445
+
446
+ const startNpm = (cwd, entry, onData, onClose) => {
447
+ let raw = '';
448
+ let settled = false;
449
+ let stopped = false;
450
+ const finish = (status) => {
451
+ if (settled) return;
452
+ settled = true;
453
+ onClose({ status, text: raw });
454
+ };
455
+ let child;
456
+ try {
457
+ const opts = npmOpts({
458
+ cwd,
459
+ env: commandEnv(),
460
+ detached: process.platform !== 'win32',
461
+ });
462
+ child = spawn(npmBin(), npmArgs(entry), opts);
463
+ } catch (error) {
464
+ raw = `${error.message}\n`;
465
+ finish(1);
466
+ return { kill() {} };
467
+ }
468
+ const push = (chunk) => {
469
+ if (stopped || settled) return;
470
+ raw += chunk;
471
+ onData(raw);
472
+ };
473
+ if (child.stdout) {
474
+ child.stdout.setEncoding('utf8');
475
+ child.stdout.on('data', push);
476
+ }
477
+ if (child.stderr) {
478
+ child.stderr.setEncoding('utf8');
479
+ child.stderr.on('data', push);
480
+ }
481
+ child.on('error', (error) => {
482
+ if (!stopped) raw += `${error.message}\n`;
483
+ finish(1);
484
+ });
485
+ child.on('close', (status) => finish(status));
486
+ return {
487
+ kill() {
488
+ if (stopped || settled) return;
489
+ stopped = true;
490
+ killTree(child);
491
+ },
492
+ };
493
+ };
494
+
495
+ module.exports = {
496
+ listCommands,
497
+ reduceOutput,
498
+ logFileName,
499
+ writeLog,
500
+ parseScriptLine,
501
+ scriptLine,
502
+ saveScript,
503
+ removeScript,
504
+ reorderScript,
505
+ startNpm,
506
+ commandEnv,
507
+ isPassingTest,
508
+ withoutNodeFrame,
509
+ indentStack,
510
+ relativize,
511
+ TABLE_KEY,
512
+ TABLE_VAL,
513
+ };
@@ -5,9 +5,9 @@ const primitives = require('./primitives.js');
5
5
  const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
6
6
 
7
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
8
- const { fill, FILE_MARK, paneResult } = primitives;
8
+ const { FILE_MARK, paneResult } = primitives;
9
9
  const { paintCursorList, paintListEdit } = primitives;
10
- const { branchLabel, paintBracketName } = primitives;
10
+ const { branchLabel } = primitives;
11
11
 
12
12
  const BRANCH_COL_ALIGN = {
13
13
  sha: 'end',
@@ -31,13 +31,11 @@ const BRANCH_CURRENT_COL_FG = {
31
31
  };
32
32
 
33
33
  const BRANCH_NAME_MAX = 24;
34
- const BRANCH_EDGE = 2;
35
34
  const BRANCH_TAIL = 1;
36
35
  const branchNameChip = (entry, rowBg) => {
37
36
  if (entry.current) {
38
37
  return {
39
38
  nameFg: THEME.mutedFg,
40
- wrapFg: THEME.mutedFg,
41
39
  nameBg: THEME.currentBg,
42
40
  nameBold: false,
43
41
  };
@@ -45,14 +43,12 @@ const branchNameChip = (entry, rowBg) => {
45
43
  if (entry.isDefault) {
46
44
  return {
47
45
  nameFg: THEME.warnFg,
48
- wrapFg: THEME.mutedFg,
49
46
  nameBg: rowBg,
50
47
  nameBold: true,
51
48
  };
52
49
  }
53
50
  return {
54
51
  nameFg: THEME.buttonHotFg,
55
- wrapFg: THEME.mutedFg,
56
52
  nameBg: rowBg,
57
53
  nameBold: false,
58
54
  };
@@ -72,8 +68,14 @@ const branchRowFields = (entry) => {
72
68
  return { sha, ahead, behind, gone, date };
73
69
  };
74
70
 
71
+ const branchRowBg = (entry, selected) => {
72
+ if (entry.current) return THEME.currentBg;
73
+ if (selected) return THEME.buttonBg;
74
+ return THEME.ctxBg;
75
+ };
76
+
75
77
  const paintBranchRow = (entry, width, color, selected, cols) => {
76
- const inner = Math.max(1, width - BRANCH_EDGE - BRANCH_TAIL);
78
+ const inner = Math.max(1, width - BRANCH_TAIL);
77
79
  const mark = selected ? FILE_MARK : ' ';
78
80
  const prefix = ` ${mark} `;
79
81
  const parts = padColumns(branchRowFields(entry), cols, BRANCH_COL_ALIGN);
@@ -91,34 +93,25 @@ const paintBranchRow = (entry, width, color, selected, cols) => {
91
93
  const after = `${' '.repeat(namePad)}${gap}${subject}${' '.repeat(pad)}`;
92
94
  const left = `${prefix}${label}${after}`;
93
95
  const fgRgb = branchRowFg(entry, selected);
94
- const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
95
- const chip = branchNameChip(entry, bgRgb);
96
- const markFg = entry.current ? THEME.headerFg : fgRgb;
97
- const markBg = entry.current ? THEME.currentBg : bgRgb;
96
+ const rowBg = branchRowBg(entry, selected);
97
+ const chip = branchNameChip(entry, rowBg);
98
+ const toneFg = entry.current ? THEME.headerFg : fgRgb;
98
99
  const tail = ' '.repeat(BRANCH_TAIL);
99
- const edge = fill(BRANCH_EDGE, fgRgb, THEME.ctxBg, color);
100
- if (!color) return `${left}${suffix}${tail}${edge}`;
101
- let out = `${seq(fgRgb, bgRgb)}${EL}`;
102
- out += paint(prefix, fgRgb, bgRgb, color);
103
- out += paintBracketName(
104
- label,
105
- chip.nameFg,
106
- chip.wrapFg,
107
- chip.nameBg,
108
- color,
109
- chip.nameBold,
110
- );
111
- out += paint(after, markFg, markBg, color);
100
+ if (!color) return `${left}${suffix}${tail}`;
101
+ let out = `${seq(toneFg, rowBg)}${EL}`;
102
+ out += paint(prefix, toneFg, rowBg, color);
103
+ out += paint(label, chip.nameFg, chip.nameBg, color, chip.nameBold);
104
+ out += paint(after, toneFg, rowBg, color);
112
105
  const tones = entry.current
113
106
  ? { ...BRANCH_COL_FG, ...BRANCH_CURRENT_COL_FG }
114
107
  : BRANCH_COL_FG;
115
- out += paintColumns(parts, cols, markFg, markBg, color, tones);
116
- out += paint(tail, markFg, markBg, color);
117
- return out + edge;
108
+ out += paintColumns(parts, cols, toneFg, rowBg, color, tones);
109
+ out += paint(tail, toneFg, rowBg, color);
110
+ return out;
118
111
  };
119
112
 
120
113
  const paintBranchEdit = (compose, width, color) =>
121
- paintListEdit(compose, width, color, BRANCH_EDGE);
114
+ paintListEdit(compose, width, color);
122
115
 
123
116
  const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
124
117
  const branches = view.branches ?? [];