reslop 0.2.1 → 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 (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { SQUOTE, emit, skipQuoted } = require('./core.js');
3
+ const core = require('./core.js');
4
+ const { SQUOTE, emit, skipQuoted } = core;
4
5
 
5
6
  const colorHtmlTag = (tag) => {
6
7
  const out = [];
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { emit, readString } = require('./core.js');
3
+ const core = require('./core.js');
4
+ const { emit, readString } = core;
4
5
 
5
6
  const JSON_LITERAL = {
6
7
  true: 'literal',
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { emit } = require('./core.js');
3
+ const core = require('./core.js');
4
+ const { emit } = core;
4
5
 
5
6
  const LOG_DATETIME_RE = new RegExp(
6
7
  '^(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}' +
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { emit } = require('./core.js');
3
+ const core = require('./core.js');
4
+ const { emit } = core;
4
5
 
5
6
  const highlightTxtLine = (line) => {
6
7
  if (!line) return [];
package/lib/highlight.js CHANGED
@@ -3,7 +3,8 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { tokensText } = require('./highlight/core.js');
6
+ const core = require('./highlight/core.js');
7
+ const { tokensText } = core;
7
8
 
8
9
  const PLUGIN_DIR = path.join(__dirname, 'highlight');
9
10
  const SKIP = ['core.js'];
package/lib/keys.js CHANGED
@@ -4,9 +4,9 @@ const actions = require('./session/actions.js');
4
4
 
5
5
  const { ACTIONS, ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS } = actions;
6
6
  const { DIFF_DISABLED, FILES_DISABLED, FILES_GIT_DISABLED } = actions;
7
- const { FILES_HIDDEN, FILES_TODO_DISABLED, TODO_DISABLED, BRANCHES_DISABLED } =
8
- actions;
9
- const { COMMITS_DISABLED, UNIT_DISABLED } = actions;
7
+ const { FILES_HIDDEN, FILES_TODO_DISABLED } = actions;
8
+ const { TODO_DISABLED, BRANCHES_DISABLED } = actions;
9
+ const { COMMITS_DISABLED, UNIT_DISABLED, NPM_DISABLED } = actions;
10
10
  const { disabledActions, diffGitDisabled, branchGitDisabled } = actions;
11
11
  const { commitGitDisabled } = actions;
12
12
  const { actionFromKey, actionHot, actionLetter, buttonWord } = actions;
@@ -65,15 +65,20 @@ const parseCsiSeq = (rest) => {
65
65
  return { params: body.slice(0, i), final: body[i], eat: 2 + i + 1 };
66
66
  };
67
67
 
68
+ const ctrlHeld = (modText) => {
69
+ const mod = parseInt(modText, 10);
70
+ return Number.isFinite(mod) && ((mod - 1) & 4) !== 0;
71
+ };
72
+
68
73
  const keyFromCsi = (paramsText, final) => {
69
74
  const params = paramsText ? paramsText.split(';') : [];
70
75
  if (final === '~') return CSI_TILDE[params[0]] ?? null;
71
76
  const base = CSI_KEYS[final];
72
77
  if (!base) return null;
73
78
  if (params.length < 2) return base;
74
- const mod = parseInt(params[1], 10);
75
- const ctrl = Number.isFinite(mod) && ((mod - 1) & 4) !== 0;
76
- if (ctrl && (base === 'left' || base === 'right')) return `ctrl-${base}`;
79
+ const arrow =
80
+ base === 'left' || base === 'right' || base === 'up' || base === 'down';
81
+ if (ctrlHeld(params[1]) && arrow) return `ctrl-${base}`;
77
82
  return base;
78
83
  };
79
84
 
@@ -193,6 +198,7 @@ module.exports = {
193
198
  TODO_DISABLED,
194
199
  BRANCHES_DISABLED,
195
200
  COMMITS_DISABLED,
201
+ NPM_DISABLED,
196
202
  UNIT_DISABLED,
197
203
  disabledActions,
198
204
  diffGitDisabled,
package/lib/manifest.js CHANGED
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
- const { isHashObject, jsonParse } = require('metautil');
5
- const { parseVersion, cmpVersion } = require('./utilities.js');
4
+ const metautil = require('metautil');
5
+ const { isHashObject, jsonParse } = metautil;
6
+ const utilities = require('./utilities.js');
7
+ const { parseVersion, cmpVersion } = utilities;
6
8
 
7
9
  const MANIFEST = 'package.json';
8
10
  const LOCKFILE = 'package-lock.json';
@@ -0,0 +1,592 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const childProcess = require('node:child_process');
6
+ const { spawn } = childProcess;
7
+ const metautil = require('metautil');
8
+ const { jsonParse } = metautil;
9
+ const ansi = require('./ansi.js');
10
+ const { stripAnsi, visibleWidth } = ansi;
11
+ const utilities = require('./utilities.js');
12
+ const { npmBin, npmOpts, spawnBase } = utilities;
13
+
14
+ const MANIFEST = 'package.json';
15
+ const SECTIONS = ['dependencies', 'devDependencies', 'optionalDependencies'];
16
+
17
+ const readText = (file) => {
18
+ try {
19
+ return fs.readFileSync(file, 'utf8');
20
+ } catch {
21
+ return '';
22
+ }
23
+ };
24
+
25
+ const readManifest = (root) => {
26
+ const text = readText(path.join(root, MANIFEST));
27
+ if (!text) return null;
28
+ const pkg = jsonParse(text);
29
+ if (!pkg || typeof pkg !== 'object') return null;
30
+ return pkg;
31
+ };
32
+
33
+ const writeManifest = (root, pkg) => {
34
+ const file = path.join(root, MANIFEST);
35
+ fs.writeFileSync(file, `${JSON.stringify(pkg, null, 2)}\n`);
36
+ };
37
+
38
+ const scriptEntries = (pkg) => {
39
+ const scripts = pkg && pkg.scripts;
40
+ if (!scripts || typeof scripts !== 'object') return [];
41
+ const entries = [];
42
+ for (const name of Object.keys(scripts)) {
43
+ const command = scripts[name];
44
+ if (typeof command !== 'string') continue;
45
+ entries.push({ name, command, kind: 'script' });
46
+ }
47
+ return entries;
48
+ };
49
+
50
+ const binNames = (pkg) => {
51
+ if (!pkg || typeof pkg !== 'object') return [];
52
+ const bin = pkg.bin;
53
+ if (typeof bin === 'string' && bin) {
54
+ const raw = `${pkg.name ?? ''}`;
55
+ const base = raw.startsWith('@') ? raw.split('/')[1] : raw;
56
+ return base ? [base] : [];
57
+ }
58
+ if (!bin || typeof bin !== 'object' || Array.isArray(bin)) return [];
59
+ const names = [];
60
+ for (const name of Object.keys(bin)) {
61
+ if (typeof bin[name] === 'string' && bin[name]) names.push(name);
62
+ }
63
+ return names;
64
+ };
65
+
66
+ const declaredNames = (pkg) => {
67
+ const names = [];
68
+ for (const section of SECTIONS) {
69
+ const map = pkg[section];
70
+ if (!map || typeof map !== 'object') continue;
71
+ for (const name of Object.keys(map)) names.push(name);
72
+ }
73
+ return names;
74
+ };
75
+
76
+ const depManifest = (root, name) => {
77
+ const parts = name.startsWith('@') ? name.split('/').slice(0, 2) : [name];
78
+ return path.join(root, 'node_modules', ...parts, MANIFEST);
79
+ };
80
+
81
+ const binEntries = (root, pkg, taken) => {
82
+ const entries = [];
83
+ const seen = new Set(taken);
84
+ for (const name of declaredNames(pkg)) {
85
+ const dep = jsonParse(readText(depManifest(root, name)));
86
+ for (const bin of binNames(dep)) {
87
+ if (seen.has(bin)) continue;
88
+ seen.add(bin);
89
+ entries.push({ name: bin, command: bin, kind: 'bin', package: name });
90
+ }
91
+ }
92
+ entries.sort((left, right) => left.name.localeCompare(right.name));
93
+ return entries;
94
+ };
95
+
96
+ const listCommands = (root) => {
97
+ const pkg = readManifest(root);
98
+ if (!pkg) return [];
99
+ const scripts = scriptEntries(pkg);
100
+ const taken = scripts.map((entry) => entry.name);
101
+ return [...scripts, ...binEntries(root, pkg, taken)];
102
+ };
103
+
104
+ const isPassingTest = (line) => {
105
+ const plain = stripAnsi(line);
106
+ const text = plain.trim();
107
+ if (!text) return false;
108
+ if (/^\s*[✔✓√] /.test(plain)) return true;
109
+ if (/^ok \d+ /.test(text)) return true;
110
+ if (/^\s*PASS\b/.test(plain)) return true;
111
+ return false;
112
+ };
113
+
114
+ const isNodeFrame = (line) => stripAnsi(line).includes('(node:');
115
+
116
+ const indentStack = (line) => {
117
+ const plain = stripAnsi(line);
118
+ if (!/^\s*at\s/.test(plain)) return line;
119
+ const lead = line.match(/^\s*/)?.[0] ?? '';
120
+ const body = lead ? line.slice(lead.length) : line;
121
+ return ` ${body}`;
122
+ };
123
+
124
+ const withoutNodeFrame = (line) => {
125
+ if (!isNodeFrame(line)) return line;
126
+ const plain = stripAnsi(line);
127
+ if (/\{\s*$/.test(plain)) return '{';
128
+ return null;
129
+ };
130
+
131
+ const unquote = (value) => {
132
+ const text = `${value ?? ''}`;
133
+ if (text.length < 2) return text;
134
+ const open = text[0];
135
+ const close = text[text.length - 1];
136
+ const code = open.charCodeAt(0);
137
+ const quoted = open === close && (code === 34 || code === 39);
138
+ if (!quoted) return text;
139
+ return text.slice(1, -1);
140
+ };
141
+
142
+ const objectEnd = (lines, start) => {
143
+ if (stripAnsi(lines[start]).trim() !== '{') return -1;
144
+ for (let i = start + 1; i < lines.length; i++) {
145
+ if (stripAnsi(lines[i]).trim() === '}') return i;
146
+ }
147
+ return -1;
148
+ };
149
+
150
+ const parseInspect = (lines) => {
151
+ const entries = [];
152
+ for (const line of lines) {
153
+ const plain = stripAnsi(line).trim().replace(/,$/, '');
154
+ const match = /^([A-Za-z_][\w]*)\s*:\s*([\s\S]*)$/.exec(plain);
155
+ if (!match) {
156
+ if (!entries.length) return null;
157
+ const last = entries[entries.length - 1];
158
+ last[1] = `${last[1]}\n${plain}`;
159
+ continue;
160
+ }
161
+ entries.push([match[1], unquote(match[2])]);
162
+ }
163
+ return entries.length ? entries : null;
164
+ };
165
+
166
+ const showValue = (value) => {
167
+ if (value === null) return 'null';
168
+ if (typeof value === 'string') return value;
169
+ if (typeof value === 'number' || typeof value === 'boolean') {
170
+ return `${value}`;
171
+ }
172
+ return JSON.stringify(value);
173
+ };
174
+
175
+ const parseObject = (lines) => {
176
+ const body = lines.slice(1, -1);
177
+ try {
178
+ const value = JSON.parse(lines.join('\n'));
179
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
180
+ return null;
181
+ }
182
+ const entries = [];
183
+ for (const key of Object.keys(value)) {
184
+ entries.push([key, showValue(value[key])]);
185
+ }
186
+ return entries.length ? entries : null;
187
+ } catch {
188
+ return parseInspect(body);
189
+ }
190
+ };
191
+
192
+ const TABLE_KEY = '\x1b[900m';
193
+ const TABLE_VAL = '\x1b[901m';
194
+
195
+ const formatTable = (entries) => {
196
+ let keyW = 0;
197
+ for (const entry of entries) {
198
+ keyW = Math.max(keyW, visibleWidth(entry[0]));
199
+ }
200
+ const rows = [];
201
+ for (const entry of entries) {
202
+ const parts = `${entry[1]}`.split('\n');
203
+ for (let i = 0; i < parts.length; i++) {
204
+ const name = i === 0 ? entry[0] : '';
205
+ const pad = ' '.repeat(Math.max(0, keyW - visibleWidth(name)));
206
+ const key = ` ${pad}${name} `;
207
+ const value = ` ${parts[i]} `;
208
+ rows.push(`${TABLE_KEY}${key}${TABLE_VAL} ${value}`);
209
+ }
210
+ }
211
+ return rows;
212
+ };
213
+
214
+ const foldObjects = (lines) => {
215
+ const out = [];
216
+ for (let i = 0; i < lines.length; i++) {
217
+ const end = objectEnd(lines, i);
218
+ if (end < 0) {
219
+ out.push(lines[i]);
220
+ continue;
221
+ }
222
+ const entries = parseObject(lines.slice(i, end + 1));
223
+ if (!entries) {
224
+ out.push(lines[i]);
225
+ continue;
226
+ }
227
+ if (out.length && out[out.length - 1] !== '') out.push('');
228
+ for (const row of formatTable(entries)) out.push(row);
229
+ i = end;
230
+ }
231
+ return out;
232
+ };
233
+
234
+ const withSep = (dir) => {
235
+ const sep = dir.includes('\\') ? '\\' : '/';
236
+ if (dir.endsWith(sep)) return dir;
237
+ return `${dir}${sep}`;
238
+ };
239
+
240
+ const stripOne = (text, dir) => {
241
+ if (!dir) return text;
242
+ let next = text.split(withSep(dir)).join('');
243
+ if (next.includes(dir)) next = next.split(dir).join('.');
244
+ return next;
245
+ };
246
+
247
+ const relativize = (text, root) => {
248
+ if (!root) return text;
249
+ const slash = root.replaceAll('\\', '/');
250
+ const back = slash.replaceAll('/', '\\');
251
+ const resolved = path.resolve(root).replaceAll('\\', '/');
252
+ const resolvedBack = resolved.replaceAll('/', '\\');
253
+ let next = text;
254
+ for (const dir of [slash, back, resolved, resolvedBack]) {
255
+ next = stripOne(next, dir);
256
+ }
257
+ return next;
258
+ };
259
+
260
+ const reduceOutput = (text, root, status) => {
261
+ const lines = `${text ?? ''}`.split(/\r?\n/);
262
+ const kept = [];
263
+ for (const line of lines) {
264
+ if (isPassingTest(line)) continue;
265
+ const next = withoutNodeFrame(line);
266
+ if (next === null) continue;
267
+ kept.push(relativize(indentStack(next), root));
268
+ }
269
+ const folded = foldObjects(kept);
270
+ while (folded.length && folded[folded.length - 1] === '') folded.pop();
271
+ if (status === '') {
272
+ if (!folded.length) return '';
273
+ return `${folded.join('\n')}\n`;
274
+ }
275
+ const code = status === undefined || status === null ? 'null' : status;
276
+ const exit = `exit ${code}`;
277
+ if (!folded.length) return `${exit}\n`;
278
+ return `${folded.join('\n')}\n${exit}\n`;
279
+ };
280
+
281
+ const logFileName = (command, date, index) => {
282
+ const safe = `${command}`.replace(/[^A-Za-z0-9._-]+/g, '-');
283
+ const n = `${index}`.padStart(2, '0');
284
+ return `${date}-${safe}-${n}.log`;
285
+ };
286
+
287
+ const dateStamp = (now) => {
288
+ const y = now.getFullYear();
289
+ const m = `${now.getMonth() + 1}`.padStart(2, '0');
290
+ const d = `${now.getDate()}`.padStart(2, '0');
291
+ return `${y}-${m}-${d}`;
292
+ };
293
+
294
+ const LOG_DIR = '.log';
295
+ const LOG_STALE_DAYS = 5;
296
+ const DAY_MS = 24 * 60 * 60 * 1000;
297
+ const LOG_DATE = /^(\d{4})-(\d{2})-(\d{2})-/;
298
+
299
+ const logDay = (name) => {
300
+ const match = LOG_DATE.exec(name);
301
+ if (!match) return null;
302
+ const year = Number(match[1]);
303
+ const month = Number(match[2]);
304
+ const day = Number(match[3]);
305
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
306
+ return Date.UTC(year, month - 1, day) / DAY_MS;
307
+ };
308
+
309
+ const todayIndex = (now) =>
310
+ Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) / DAY_MS;
311
+
312
+ const isStaleLog = (name, now) => {
313
+ const day = logDay(name);
314
+ if (day === null) return false;
315
+ return todayIndex(now) - day > LOG_STALE_DAYS;
316
+ };
317
+
318
+ const readLogNames = (dir) => {
319
+ try {
320
+ return fs.readdirSync(dir);
321
+ } catch (error) {
322
+ if (error.code === 'ENOENT') return [];
323
+ throw error;
324
+ }
325
+ };
326
+
327
+ const staleLogFiles = (root, now = new Date()) => {
328
+ const dir = path.join(root, LOG_DIR);
329
+ const names = readLogNames(dir);
330
+ const files = [];
331
+ let bytes = 0;
332
+ for (const name of names) {
333
+ if (!name.endsWith('.log') || !isStaleLog(name, now)) continue;
334
+ const full = path.join(dir, name);
335
+ let stat;
336
+ try {
337
+ stat = fs.statSync(full);
338
+ } catch (error) {
339
+ if (error.code === 'ENOENT') continue;
340
+ throw error;
341
+ }
342
+ if (!stat.isFile()) continue;
343
+ bytes += stat.size;
344
+ files.push(full);
345
+ }
346
+ return { bytes, files };
347
+ };
348
+
349
+ const removeStaleLogs = (root, now = new Date()) => {
350
+ const found = staleLogFiles(root, now);
351
+ for (const full of found.files) fs.rmSync(full, { force: true });
352
+ return found.bytes;
353
+ };
354
+
355
+ const scaled = (value) => {
356
+ if (value >= 10) return `${Math.round(value)}`;
357
+ return `${Math.round(value * 10) / 10}`;
358
+ };
359
+
360
+ const formatSize = (bytes) => {
361
+ const size = bytes > 0 ? bytes : 0;
362
+ if (size < 1024) return `${size}b`;
363
+ const kb = size / 1024;
364
+ if (kb < 1024) return `${scaled(kb)}k`;
365
+ return `${scaled(kb / 1024)}m`;
366
+ };
367
+
368
+ const nextLogFile = (root, command, now = new Date()) => {
369
+ const date = dateStamp(now);
370
+ const dir = path.join(root, LOG_DIR);
371
+ let index = 1;
372
+ let name = logFileName(command, date, index);
373
+ let full = path.join(dir, name);
374
+ while (index < 100 && fs.existsSync(full)) {
375
+ index += 1;
376
+ name = logFileName(command, date, index);
377
+ full = path.join(dir, name);
378
+ }
379
+ return { name, full, dir };
380
+ };
381
+
382
+ const logText = (text) =>
383
+ stripAnsi(text)
384
+ .split('\n')
385
+ .map((line) => line.trimEnd())
386
+ .join('\n');
387
+
388
+ const writeLog = (root, command, text, now) => {
389
+ const file = nextLogFile(root, command, now);
390
+ fs.mkdirSync(file.dir, { recursive: true });
391
+ fs.writeFileSync(file.full, logText(text));
392
+ return file.name;
393
+ };
394
+
395
+ const parseScriptLine = (text) => {
396
+ const raw = `${text ?? ''}`.trim();
397
+ const at = raw.indexOf(': ');
398
+ if (at <= 0) return null;
399
+ const name = raw.slice(0, at).trim();
400
+ const command = raw.slice(at + 2).trim();
401
+ if (!name || !command) return null;
402
+ if (!/^[\w:.-]+$/.test(name)) return null;
403
+ return { name, command };
404
+ };
405
+
406
+ const scriptLine = (entry) => `${entry.name}: ${entry.command}`;
407
+
408
+ const orderedScripts = (pkg) => {
409
+ const scripts = pkg.scripts;
410
+ if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
411
+ return {};
412
+ }
413
+ return scripts;
414
+ };
415
+
416
+ const upsertScript = (pkg, prev, next) => {
417
+ const scripts = orderedScripts(pkg);
418
+ const names = Object.keys(scripts);
419
+ const ordered = {};
420
+ if (prev && names.includes(prev)) {
421
+ for (const key of names) {
422
+ if (key === prev) ordered[next.name] = next.command;
423
+ else if (key !== next.name) ordered[key] = scripts[key];
424
+ }
425
+ } else {
426
+ for (const key of names) ordered[key] = scripts[key];
427
+ ordered[next.name] = next.command;
428
+ }
429
+ pkg.scripts = ordered;
430
+ };
431
+
432
+ const dropScript = (pkg, name) => {
433
+ const scripts = orderedScripts(pkg);
434
+ if (!Object.hasOwn(scripts, name)) return false;
435
+ const ordered = {};
436
+ for (const key of Object.keys(scripts)) {
437
+ if (key !== name) ordered[key] = scripts[key];
438
+ }
439
+ pkg.scripts = ordered;
440
+ return true;
441
+ };
442
+
443
+ const moveScript = (pkg, name, delta) => {
444
+ const scripts = orderedScripts(pkg);
445
+ const names = Object.keys(scripts);
446
+ const at = names.indexOf(name);
447
+ if (at < 0) return false;
448
+ const next = at + delta;
449
+ if (next < 0 || next >= names.length) return false;
450
+ const swap = names[at];
451
+ names[at] = names[next];
452
+ names[next] = swap;
453
+ const ordered = {};
454
+ for (const key of names) ordered[key] = scripts[key];
455
+ pkg.scripts = ordered;
456
+ return true;
457
+ };
458
+
459
+ const saveScript = (root, prev, next) => {
460
+ const pkg = readManifest(root) ?? {};
461
+ upsertScript(pkg, prev, next);
462
+ writeManifest(root, pkg);
463
+ };
464
+
465
+ const removeScript = (root, name) => {
466
+ const pkg = readManifest(root);
467
+ if (!pkg || !dropScript(pkg, name)) return false;
468
+ writeManifest(root, pkg);
469
+ return true;
470
+ };
471
+
472
+ const reorderScript = (root, name, delta) => {
473
+ const pkg = readManifest(root);
474
+ if (!pkg || !moveScript(pkg, name, delta)) return false;
475
+ writeManifest(root, pkg);
476
+ return true;
477
+ };
478
+
479
+ const npmArgs = (entry) => {
480
+ if (entry.kind === 'bin') return ['exec', '--', entry.name];
481
+ return ['run', entry.name];
482
+ };
483
+
484
+ const signalPid = (pid, signal) => {
485
+ try {
486
+ process.kill(pid, signal);
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ }
491
+ };
492
+
493
+ const killTree = (proc) => {
494
+ const pid = proc && proc.pid;
495
+ if (!pid) return;
496
+ if (process.platform === 'win32') {
497
+ const killer = spawn(
498
+ 'taskkill',
499
+ ['/pid', String(pid), '/t', '/f'],
500
+ spawnBase({ stdio: 'ignore' }),
501
+ );
502
+ killer.on('error', () => proc.kill());
503
+ return;
504
+ }
505
+ const group = -pid;
506
+ if (!signalPid(group, 'SIGTERM')) signalPid(pid, 'SIGTERM');
507
+ const timer = setTimeout(() => {
508
+ if (!signalPid(group, 'SIGKILL')) signalPid(pid, 'SIGKILL');
509
+ }, 200);
510
+ timer.unref();
511
+ proc.once('close', () => clearTimeout(timer));
512
+ };
513
+
514
+ const commandEnv = (base = process.env) => {
515
+ const env = { ...base };
516
+ if (!env.NO_COLOR && !env.FORCE_COLOR) env.FORCE_COLOR = '1';
517
+ if (!env.TERM) env.TERM = 'xterm-256color';
518
+ env.RESLOP_OUTPUT = 'raw';
519
+ return env;
520
+ };
521
+
522
+ const startNpm = (cwd, entry, onData, onClose) => {
523
+ let raw = '';
524
+ let settled = false;
525
+ let stopped = false;
526
+ const finish = (status) => {
527
+ if (settled) return;
528
+ settled = true;
529
+ onClose({ status, text: raw });
530
+ };
531
+ let child;
532
+ try {
533
+ const opts = npmOpts({
534
+ cwd,
535
+ env: commandEnv(),
536
+ detached: process.platform !== 'win32',
537
+ });
538
+ child = spawn(npmBin(), npmArgs(entry), opts);
539
+ } catch (error) {
540
+ raw = `${error.message}\n`;
541
+ finish(1);
542
+ return { kill() {} };
543
+ }
544
+ const push = (chunk) => {
545
+ if (stopped || settled) return;
546
+ raw += chunk;
547
+ onData(raw);
548
+ };
549
+ if (child.stdout) {
550
+ child.stdout.setEncoding('utf8');
551
+ child.stdout.on('data', push);
552
+ }
553
+ if (child.stderr) {
554
+ child.stderr.setEncoding('utf8');
555
+ child.stderr.on('data', push);
556
+ }
557
+ child.on('error', (error) => {
558
+ if (!stopped) raw += `${error.message}\n`;
559
+ finish(1);
560
+ });
561
+ child.on('close', (status) => finish(status));
562
+ return {
563
+ kill() {
564
+ if (stopped || settled) return;
565
+ stopped = true;
566
+ killTree(child);
567
+ },
568
+ };
569
+ };
570
+
571
+ module.exports = {
572
+ listCommands,
573
+ reduceOutput,
574
+ logFileName,
575
+ writeLog,
576
+ staleLogFiles,
577
+ removeStaleLogs,
578
+ formatSize,
579
+ parseScriptLine,
580
+ scriptLine,
581
+ saveScript,
582
+ removeScript,
583
+ reorderScript,
584
+ startNpm,
585
+ commandEnv,
586
+ isPassingTest,
587
+ withoutNodeFrame,
588
+ indentStack,
589
+ relativize,
590
+ TABLE_KEY,
591
+ TABLE_VAL,
592
+ };