reslop 0.2.6 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/dashboard/blocks.js +490 -0
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +19 -61
  72. package/lib/render/tiles.js +203 -47
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +166 -59
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +72 -59
  126. package/lib/session/workspace.js +416 -0
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
package/lib/cli.js CHANGED
@@ -2,50 +2,27 @@
2
2
 
3
3
  const path = require('node:path');
4
4
 
5
- const metautil = require('metautil');
6
- const { exists } = metautil;
7
-
8
- const git = require('./git.js');
9
- const { createGitRepo } = git;
10
- const github = require('./github.js');
11
- const { githubToken, loadPullRequest } = github;
12
- const gitlab = require('./gitlab.js');
13
- const { gitlabToken, loadMergeRequest } = gitlab;
14
- const source = require('./source.js');
15
- const { selectChangeSource, createLoadedSource } = source;
16
- const session = require('./session.js');
17
- const { Session } = session;
18
- const capabilities = require('./capabilities.js');
19
- const { capabilitiesFor, attachCapabilities } = capabilities;
20
- const ansi = require('./ansi.js');
21
- const { setTheme } = ansi;
22
- const reportRun = require('./report-run.js');
23
- const { runCapture } = reportRun;
5
+ const { exists } = require('metautil');
6
+
7
+ const { errorMessage } = require('./common/utilities.js');
8
+ const { createGitRepo } = require('./git/git.js');
9
+ const source = require('./source/source.js');
10
+ const { remoteKind, selectChangeSource, createLoadedSource } = source;
11
+ const { Session } = require('./session/session.js');
12
+ const { hasNotes } = require('./review/review.js');
13
+ const {
14
+ capabilitiesFor,
15
+ attachCapabilities,
16
+ } = require('./source/capabilities.js');
17
+ const { runCapture } = require('./report/run.js');
18
+ const { workspaceFromArgs } = require('./dashboard/workspace.js');
24
19
 
25
20
  const HEX_REV = /^[0-9a-f]{4,40}$/i;
26
21
  const HEAD_REV = /^HEAD([~^].*)?$/i;
27
22
  const RANGE_REV = /[~^]/;
28
23
 
29
24
  const USAGE =
30
- 'Usage: reslop [-n] [-r] [-light] [path | commit | pr-url | mr-url]';
31
-
32
- const REMOTE_CHANGE = {
33
- pr: {
34
- load: 'loadPullRequest',
35
- fallback: loadPullRequest,
36
- token: githubToken,
37
- target: (selected) => selected.pr,
38
- },
39
- mr: {
40
- load: 'loadMergeRequest',
41
- fallback: loadMergeRequest,
42
- token: gitlabToken,
43
- target: (selected) => selected.mr,
44
- },
45
- };
46
-
47
- const errorMessage = (reason) =>
48
- reason instanceof Error ? reason.message : `${reason ?? ''}`;
25
+ 'Usage: reslop [-r] [path | commit | pr-url | mr-url | issue-url]';
49
26
 
50
27
  const fail = (stderr, reason) => {
51
28
  stderr.write(`reslop: ${errorMessage(reason)}\n`);
@@ -54,27 +31,27 @@ const fail = (stderr, reason) => {
54
31
 
55
32
  const parseArgv = (argv) => {
56
33
  const paths = [];
57
- let newReview = false;
58
34
  let readOnly = false;
59
- let theme = 'dark';
60
35
  for (const arg of argv) {
61
- if (arg === '-n') newReview = true;
62
- else if (arg === '-r') readOnly = true;
63
- else if (arg === '-light') theme = 'light';
36
+ if (arg === '-r') readOnly = true;
64
37
  else if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`);
65
38
  else paths.push(arg);
66
39
  }
67
- return { paths, newReview, readOnly, theme };
40
+ return { paths, readOnly };
68
41
  };
69
42
 
70
- const looksLikeRev = (spec) =>
71
- HEX_REV.test(spec) || HEAD_REV.test(spec) || RANGE_REV.test(spec);
43
+ const looksLikeRev = (spec) => {
44
+ const hex = HEX_REV.test(spec);
45
+ const head = HEAD_REV.test(spec);
46
+ const range = RANGE_REV.test(spec);
47
+ return hex || head || range;
48
+ };
72
49
 
73
50
  const resolveScope = async (cwd, paths, repo) => {
74
51
  if (!paths.length) return { rev: null, paths };
75
52
  const first = paths[0];
76
53
  if (await exists(path.resolve(cwd, first))) return { rev: null, paths };
77
- const resolved = repo.resolveRev(cwd, first);
54
+ const resolved = await repo.resolveRev(cwd, first);
78
55
  if (resolved) {
79
56
  if (paths.length > 1) throw new Error('use a commit or a path, not both');
80
57
  return { rev: resolved, paths: [] };
@@ -85,6 +62,20 @@ const resolveScope = async (cwd, paths, repo) => {
85
62
 
86
63
  const localOptions = async (deps, args, selected, cwd) => {
87
64
  const rawRepo = deps.repo ?? createGitRepo();
65
+ const found = workspaceFromArgs(cwd, selected.paths);
66
+ if (found) {
67
+ const capabilities = capabilitiesFor('local', { readOnly: args.readOnly });
68
+ return {
69
+ cwd: found.root,
70
+ repo: attachCapabilities(rawRepo, capabilities),
71
+ paths: [],
72
+ rev: null,
73
+ capabilities,
74
+ startPane: 'repos',
75
+ workspace: found,
76
+ repoName: path.basename(found.root),
77
+ };
78
+ }
88
79
  const scope = await resolveScope(cwd, selected.paths, rawRepo);
89
80
  const kind = scope.rev ? 'commit' : 'local';
90
81
  const capabilities = capabilitiesFor(kind, { readOnly: args.readOnly });
@@ -100,22 +91,21 @@ const localOptions = async (deps, args, selected, cwd) => {
100
91
 
101
92
  const remoteRepo = (deps, selected, env, cwd) => {
102
93
  if (deps.repo) return deps.repo;
103
- const remote = REMOTE_CHANGE[selected.kind];
104
- const loader = deps[remote.load] ?? remote.fallback;
94
+ const remote = remoteKind(selected);
95
+ const loader = deps[remote.option] ?? remote.load;
105
96
  const token = remote.token(env);
106
97
  const loadRemote = async (_cwd, _paths, options = {}) => {
107
- const loaded = await loader(remote.target(selected), {
108
- cwd,
109
- paths: selected.paths,
110
- token,
111
- fetch: deps.fetch,
112
- signal: options.signal,
113
- });
98
+ const target = remote.target(selected);
99
+ const paths = selected.paths;
100
+ const fetch = deps.fetch;
101
+ const signal = options.signal;
102
+ const request = { cwd, paths, token, fetch, signal };
103
+ const loaded = await loader(target, request);
114
104
  const items = loaded.items ?? [];
115
105
  return { ...loaded, items: [...items] };
116
106
  };
117
107
  return createLoadedSource({ items: [] }, selected.kind, {
118
- loadAsync: loadRemote,
108
+ load: loadRemote,
119
109
  });
120
110
  };
121
111
 
@@ -129,7 +119,8 @@ const loadSession = async (proc, deps, args) => {
129
119
  const env = proc.env ?? {};
130
120
  const cwd = deps.cwd ?? proc.cwd();
131
121
  const selected = selectChangeSource(args.paths);
132
- const extra = REMOTE_CHANGE[selected.kind]
122
+ const kind = remoteKind(selected);
123
+ const extra = kind
133
124
  ? remoteOptions(deps, args, selected, cwd, env)
134
125
  : await localOptions(deps, args, selected, cwd);
135
126
  const session = new Session({
@@ -138,12 +129,11 @@ const loadSession = async (proc, deps, args) => {
138
129
  stdout: proc.stdout,
139
130
  color: proc.stdout.isTTY === true && !env.NO_COLOR,
140
131
  startPane: 'files',
141
- newReview: args.newReview,
142
132
  readOnly: args.readOnly,
143
133
  audit: true,
144
134
  ...extra,
145
135
  });
146
- session.ensureRepo();
136
+ if (session.pane !== 'repos') await session.ensureRepo();
147
137
  return session;
148
138
  };
149
139
 
@@ -159,29 +149,34 @@ const run = async (proc, deps = {}) => {
159
149
  if (argv[0] === 't') return runCapture(proc, deps);
160
150
  const stdout = proc.stdout;
161
151
  const stderr = proc.stderr;
162
- let args;
152
+ let args = null;
163
153
  try {
164
154
  args = parseArgv(argv);
165
155
  } catch (error) {
166
156
  fail(stderr, error);
167
157
  stderr.write(`${USAGE}\n`);
168
- return 1;
169
158
  }
170
- setTheme(args.theme);
171
- let session;
159
+ if (!args) return 1;
160
+ let session = null;
172
161
  try {
173
162
  session = await loadSession(proc, deps, args);
174
163
  } catch (error) {
175
- return fail(stderr, error);
164
+ fail(stderr, error);
176
165
  }
166
+ if (!session) return 1;
177
167
  const tty = proc.stdin.isTTY === true && stdout.isTTY === true;
178
168
  if (tty) return runUi(session, proc);
169
+ if (session.pane === 'repos') {
170
+ stderr.write('reslop: interactive terminal required\n');
171
+ return 1;
172
+ }
179
173
  try {
180
174
  await session.loadReady();
181
175
  } catch (error) {
182
176
  return fail(stderr, error);
183
177
  }
184
- if (!session.items.length) {
178
+ const noted = session.notes ? hasNotes(session.notes) : false;
179
+ if (!session.items.length && !noted) {
185
180
  stdout.write('nothing to review\n');
186
181
  return 0;
187
182
  }
@@ -1,24 +1,21 @@
1
1
  'use strict';
2
2
 
3
- const manifest = require('./manifest.js');
4
- const { listFilePath } = manifest;
3
+ const path = require('node:path');
5
4
 
6
- const REVIEW_DIR = '.review';
5
+ const REVIEW_DIR = '.plan';
6
+ const LOG_DIR = '.log';
7
+ const ISSUE_FILE = 'issue';
7
8
  const ORIGIN_ORDER = ['staged', 'unstaged', 'untracked', 'commit', 'pr'];
8
9
  const TASKS_FILE = 'TODOs';
9
- const REPO_TASKS_LABEL =
10
- 'Project Tasks: Backlog, Issues, Bug Reports, Feature Requests';
11
- const TASK_KINDS = [
12
- { id: 'backlog', title: 'Backlog' },
13
- { id: 'issues', title: 'Issues' },
14
- { id: 'bugs', title: 'Bug Reports' },
15
- { id: 'features', title: 'Feature Requests' },
16
- ];
17
- const TASK_KIND_IDS = new Set(TASK_KINDS.map((kind) => kind.id));
10
+ const { label: REPO_TASKS_LABEL, kinds: TASK_KINDS } = require('./tasks.json');
11
+
12
+ const TASK_KIND_IDS = TASK_KINDS.map((kind) => kind.id);
13
+ const TASK_KIND_ALIAS = { backlog: 'debt', issues: 'debt' };
18
14
 
19
15
  const taskKind = (todo) => {
20
16
  const kind = todo && todo.kind;
21
- return TASK_KIND_IDS.has(kind) ? kind : 'backlog';
17
+ if (TASK_KIND_IDS.includes(kind)) return kind;
18
+ return TASK_KIND_ALIAS[kind] ?? 'debt';
22
19
  };
23
20
 
24
21
  const taskTitle = (id) => {
@@ -28,17 +25,9 @@ const taskTitle = (id) => {
28
25
  return TASK_KINDS[0].title;
29
26
  };
30
27
  const TOTAL_LABEL = 'total';
31
- const AGE_UNIT = {
32
- second: 's',
33
- minute: 'm',
34
- hour: 'h',
35
- day: 'd',
36
- week: 'w',
37
- month: 'mo',
38
- year: 'y',
39
- };
40
- const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
41
- const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
28
+ const MANIFEST = 'package.json';
29
+ const LOCKFILE = 'package-lock.json';
30
+ const DEP_KIND = { [MANIFEST]: 'manifest', [LOCKFILE]: 'lockfile' };
42
31
 
43
32
  const isPathInScope = (rel, paths) => {
44
33
  if (!paths.length) return true;
@@ -52,6 +41,34 @@ const isPathInScope = (rel, paths) => {
52
41
 
53
42
  const itemPath = (item) => item?.file?.newPath || item?.file?.oldPath || '';
54
43
 
44
+ const posixRel = (rel) => `${rel ?? ''}`.replaceAll('\\', '/');
45
+
46
+ const depFileMeta = (rel) => {
47
+ const normalized = posixRel(rel);
48
+ const kind = DEP_KIND[path.posix.basename(normalized)];
49
+ if (!normalized || !kind) return null;
50
+ return { kind, dir: path.posix.dirname(normalized) };
51
+ };
52
+
53
+ const relOf = (dir, name) => (dir === '.' ? name : `${dir}/${name}`);
54
+
55
+ const listFilePath = (rel) => {
56
+ const meta = depFileMeta(rel);
57
+ if (meta && meta.kind === 'lockfile') return relOf(meta.dir, MANIFEST);
58
+ return posixRel(rel);
59
+ };
60
+
61
+ const groupByPath = (items) => {
62
+ const byPath = new Map();
63
+ for (const item of items) {
64
+ const rel = itemPath(item);
65
+ const list = byPath.get(rel);
66
+ if (list) list.push(item);
67
+ else byPath.set(rel, [item]);
68
+ }
69
+ return byPath;
70
+ };
71
+
55
72
  const listPath = (item) => listFilePath(itemPath(item));
56
73
 
57
74
  const isTaskItem = (item) => item?.origin === 'task';
@@ -97,32 +114,6 @@ const lineDelta = ({ hunk, blockId }) => {
97
114
  return delta;
98
115
  };
99
116
 
100
- const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
101
-
102
- const shortAge = (text) => {
103
- const raw = `${text ?? ''}`.trim();
104
- if (/^yesterday$/i.test(raw)) return '1d ago';
105
- const match = AGE_AGO.exec(raw);
106
- if (match) return shortUnit(match[1], match[2].toLowerCase());
107
- const brief = AGE_SHORT.exec(raw);
108
- if (brief) return `${brief[1]}${brief[2].toLowerCase()} ago`;
109
- return raw;
110
- };
111
-
112
- const relativeAge = (thenMs, nowMs = Date.now()) => {
113
- const diff = Math.max(0, Math.round((nowMs - thenMs) / 1000));
114
- if (diff < 90) return shortUnit(diff, 'second');
115
- const minutes = Math.round(diff / 60);
116
- if (minutes < 90) return shortUnit(minutes, 'minute');
117
- const hours = Math.round(minutes / 60);
118
- if (hours < 36) return shortUnit(hours, 'hour');
119
- const days = Math.round(hours / 24);
120
- if (days < 14) return shortUnit(days, 'day');
121
- if (days < 70) return shortUnit(Math.round(days / 7), 'week');
122
- if (days < 365) return shortUnit(Math.round(days / 30), 'month');
123
- return shortUnit(Math.round(days / 365), 'year');
124
- };
125
-
126
117
  const emptyCounts = () => ({
127
118
  remaining: 0,
128
119
  added: 0,
@@ -221,12 +212,21 @@ const fileTotals = (files) => {
221
212
 
222
213
  module.exports = {
223
214
  REVIEW_DIR,
215
+ LOG_DIR,
216
+ ISSUE_FILE,
217
+ posixRel,
218
+ groupByPath,
224
219
  TASKS_FILE,
225
220
  REPO_TASKS_LABEL,
226
221
  TASK_KINDS,
227
222
  taskKind,
228
223
  taskTitle,
229
224
  TOTAL_LABEL,
225
+ MANIFEST,
226
+ LOCKFILE,
227
+ depFileMeta,
228
+ relOf,
229
+ listFilePath,
230
230
  isPathInScope,
231
231
  itemPath,
232
232
  listPath,
@@ -236,8 +236,7 @@ module.exports = {
236
236
  isTotalEntry,
237
237
  makeTaskItem,
238
238
  fileStatus,
239
+ lineDelta,
239
240
  fileEntries,
240
241
  fileTotals,
241
- relativeAge,
242
- shortAge,
243
242
  };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const matchesIn = (text, query) => {
4
+ const spans = [];
5
+ if (!query || !text) return spans;
6
+ const hay = text.toLowerCase();
7
+ const needle = query.toLowerCase();
8
+ const step = needle.length;
9
+ let from = 0;
10
+ while (from <= hay.length - step) {
11
+ const at = hay.indexOf(needle, from);
12
+ if (at < 0) break;
13
+ spans.push({ start: at, end: at + step });
14
+ from = at + step;
15
+ }
16
+ return spans;
17
+ };
18
+
19
+ const fileMatches = (entries, query) => {
20
+ const hits = [];
21
+ if (!query) return hits;
22
+ for (let index = 0; index < entries.length; index++) {
23
+ const entry = entries[index];
24
+ const path = entry && entry.path ? entry.path : '';
25
+ if (!path) continue;
26
+ for (const span of matchesIn(path, query)) {
27
+ hits.push({ index, path, start: span.start, end: span.end });
28
+ }
29
+ }
30
+ return hits;
31
+ };
32
+
33
+ const activeHit = (entries, find) => {
34
+ if (!find || !find.query) return null;
35
+ const hits = fileMatches(entries, find.query);
36
+ if (!hits.length) return null;
37
+ const count = hits.length;
38
+ let at = find.at % count;
39
+ if (at < 0) at += count;
40
+ return hits[at];
41
+ };
42
+
43
+ const keptPrefix = (shown, full) => {
44
+ const mark = shown.endsWith('…') && shown !== full ? 1 : 0;
45
+ const keep = shown.length - mark;
46
+ if (!full.startsWith(shown.slice(0, keep))) return -1;
47
+ return keep;
48
+ };
49
+
50
+ const spansFor = (shown, full, query, hit) => {
51
+ const spans = matchesIn(shown, query);
52
+ if (!hit || hit.path !== full) return spans;
53
+ const keep = keptPrefix(shown, full);
54
+ if (keep < 0 || hit.start >= keep) return spans;
55
+ return spans.map((span) => ({
56
+ ...span,
57
+ on: span.start === hit.start,
58
+ }));
59
+ };
60
+
61
+ module.exports = { fileMatches, activeHit, spansFor };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const { bytesToSize } = require('metautil');
4
+ const { trimText, pad2 } = require('./utilities.js');
5
+
6
+ const AGE_UNIT = {
7
+ second: 's',
8
+ minute: 'm',
9
+ hour: 'h',
10
+ day: 'd',
11
+ week: 'w',
12
+ month: 'mo',
13
+ year: 'y',
14
+ };
15
+ const AGE_AGO = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i;
16
+ const AGE_SHORT = /^(\d+)(mo|[smhdwy])(?: ago)?$/i;
17
+ const SECONDS_AGO = /^\d+s ago$/;
18
+
19
+ const shortUnit = (n, unit) => `${n}${AGE_UNIT[unit]} ago`;
20
+
21
+ const AGE_SEC = {
22
+ s: 1,
23
+ m: 60,
24
+ h: 3600,
25
+ d: 86400,
26
+ w: 604800,
27
+ mo: 2592000,
28
+ y: 31536000,
29
+ };
30
+
31
+ const ageParts = (text) => {
32
+ const raw = trimText(text);
33
+ if (/^yesterday$/i.test(raw)) return { count: 1, unit: 'd' };
34
+ const long = AGE_AGO.exec(raw);
35
+ if (long) return { count: long[1], unit: AGE_UNIT[long[2].toLowerCase()] };
36
+ const brief = AGE_SHORT.exec(raw);
37
+ if (brief) return { count: brief[1], unit: brief[2].toLowerCase() };
38
+ return null;
39
+ };
40
+
41
+ const shortAge = (text) => {
42
+ const age = ageParts(text);
43
+ return age ? `${age.count}${age.unit} ago` : trimText(text);
44
+ };
45
+
46
+ const ageSeconds = (text) => {
47
+ const age = ageParts(text);
48
+ if (!age) return Number.POSITIVE_INFINITY;
49
+ return Number(age.count) * AGE_SEC[age.unit];
50
+ };
51
+
52
+ const relativeAge = (thenMs, nowMs = Date.now()) => {
53
+ const diff = Math.max(0, Math.round((nowMs - thenMs) / 1000));
54
+ if (diff < 90) return shortUnit(diff, 'second');
55
+ const minutes = Math.round(diff / 60);
56
+ if (minutes < 90) return shortUnit(minutes, 'minute');
57
+ const hours = Math.round(minutes / 60);
58
+ if (hours < 36) return shortUnit(hours, 'hour');
59
+ const days = Math.round(hours / 24);
60
+ if (days < 14) return shortUnit(days, 'day');
61
+ if (days < 70) return shortUnit(Math.round(days / 7), 'week');
62
+ if (days < 365) return shortUnit(Math.round(days / 30), 'month');
63
+ return shortUnit(Math.round(days / 365), 'year');
64
+ };
65
+
66
+ const trimmed = (value) =>
67
+ value >= 10 ? `${Math.round(value)}` : `${Math.round(value * 10) / 10}`;
68
+
69
+ const num = (value) => {
70
+ const n = Math.max(0, Math.round(value));
71
+ if (n >= 1e6) return `${trimmed(n / 1e6)}m`;
72
+ if (n >= 1e4) return `${trimmed(n / 1e3)}k`;
73
+ return `${n}`;
74
+ };
75
+
76
+ const size = (bytes) => bytesToSize(bytes > 0 ? bytes : 0);
77
+
78
+ const ago = (at, now) => {
79
+ if (!at) return '';
80
+ const text = relativeAge(at, now);
81
+ return SECONDS_AGO.test(text) ? '<1m ago' : text;
82
+ };
83
+
84
+ const duration = (ms) => {
85
+ const value = Math.max(0, ms);
86
+ if (value < 1000) return `${Math.round(value)}ms`;
87
+ if (value < 60000) return `${(value / 1000).toFixed(1)}s`;
88
+ const minutes = Math.floor(value / 60000);
89
+ const seconds = Math.floor((value % 60000) / 1000);
90
+ return `${minutes}m${pad2(seconds)}s`;
91
+ };
92
+
93
+ module.exports = {
94
+ shortAge,
95
+ ageSeconds,
96
+ relativeAge,
97
+ num,
98
+ size,
99
+ ago,
100
+ duration,
101
+ };
@@ -1,48 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const childProcess = require('node:child_process');
4
- const { spawn, spawnSync } = childProcess;
5
- const metautil = require('metautil');
6
- const { split } = metautil;
3
+ const { spawn } = require('node:child_process');
7
4
 
8
5
  const IS_WIN = process.platform === 'win32';
6
+ const INSTALL_MS = 120000;
9
7
  const DEFAULT_ENCODING = 'utf8';
10
8
  const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;
11
9
 
12
- const parseVersion = (text) => {
13
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(`${text ?? ''}`);
14
- if (!match) return null;
15
- const major = Number(match[1]);
16
- const minor = Number(match[2]);
17
- const patch = Number(match[3]);
18
- return { major, minor, patch };
19
- };
20
-
21
- const cmpVersion = (left, right) =>
22
- left.major - right.major ||
23
- left.minor - right.minor ||
24
- left.patch - right.patch;
25
-
26
- const oneLine = (text, fallback = '') =>
27
- split(`${text ?? ''}`.trim(), '\n')[0] || fallback;
28
-
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
10
  const npmBin = () => (IS_WIN ? 'npm.cmd' : 'npm');
47
11
 
48
12
  const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
@@ -50,36 +14,6 @@ const spawnBase = (extra = {}) => ({ windowsHide: true, ...extra });
50
14
  const npmOpts = (extra = {}, platform = process.platform) =>
51
15
  spawnBase({ shell: platform === 'win32', ...extra });
52
16
 
53
- const listen = (emitter, event, handler) => {
54
- if (!emitter || typeof emitter.on !== 'function') return () => {};
55
- emitter.on(event, handler);
56
- return () => {
57
- if (typeof emitter.removeListener === 'function') {
58
- return void emitter.removeListener(event, handler);
59
- }
60
- if (typeof emitter.off === 'function') emitter.off(event, handler);
61
- };
62
- };
63
-
64
- const watchResize = (stdout, proc, onResize, signal) => {
65
- const stopStdout = listen(stdout, 'resize', onResize);
66
- let stopProc = () => {};
67
- try {
68
- stopProc = listen(proc, 'SIGWINCH', onResize);
69
- } catch {
70
- // SIGWINCH is not available on Windows
71
- }
72
- const stop = () => {
73
- stopStdout();
74
- stopProc();
75
- };
76
- if (signal) {
77
- if (signal.aborted) stop();
78
- else signal.addEventListener('abort', stop, { once: true });
79
- }
80
- return stop;
81
- };
82
-
83
17
  const codeError = (message, code) =>
84
18
  Object.assign(new Error(message), { code });
85
19
 
@@ -94,19 +28,6 @@ const procResult = (values = {}) => ({
94
28
  error: values.error ?? null,
95
29
  });
96
30
 
97
- const runProcSync = (cmd, args, options = {}) => {
98
- const opts = spawnBase({
99
- cwd: options.cwd,
100
- env: options.env,
101
- encoding: options.encoding ?? DEFAULT_ENCODING,
102
- input: options.input,
103
- maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER,
104
- timeout: options.timeout,
105
- });
106
- if (options.shell) opts.shell = true;
107
- return procResult(spawnSync(cmd, args, opts));
108
- };
109
-
110
31
  const listenChild = (child, options, resolve) => {
111
32
  const encoding = options.encoding ?? DEFAULT_ENCODING;
112
33
  const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
@@ -156,6 +77,8 @@ const listenChild = (child, options, resolve) => {
156
77
  finish(procResult({ status, stdout, stderr }));
157
78
  });
158
79
  if (!child.stdin) return;
80
+ // A child that exits before reading its input reports that by its status
81
+ child.stdin.on('error', () => {});
159
82
  try {
160
83
  if (options.input) child.stdin.end(options.input);
161
84
  else child.stdin.end();
@@ -173,34 +96,23 @@ const runProc = (cmd, args, options = {}) =>
173
96
  }
174
97
  const spawnOpts = { cwd: options.cwd, env: options.env };
175
98
  if (options.shell) spawnOpts.shell = true;
176
- let child;
99
+ let child = null;
177
100
  try {
178
101
  child = spawn(cmd, args, spawnBase(spawnOpts));
179
102
  } catch (error) {
180
103
  resolve(procResult({ error }));
181
- return;
182
104
  }
105
+ if (!child) return;
183
106
  listenChild(child, options, resolve);
184
107
  });
185
108
 
186
109
  module.exports = {
187
110
  IS_WIN,
188
- DEFAULT_ENCODING,
189
- DEFAULT_MAX_BUFFER,
190
- parseVersion,
191
- cmpVersion,
192
- oneLine,
193
- asText,
194
- unquote,
195
- pad2,
196
- dateStamp,
111
+ INSTALL_MS,
197
112
  npmBin,
198
113
  spawnBase,
199
114
  npmOpts,
200
- listen,
201
- watchResize,
202
115
  abortError,
203
116
  isAbort,
204
- runProcSync,
205
117
  runProc,
206
118
  };