@emeryld/manager 1.5.2 → 1.5.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.
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { IGNORED_DIRECTORIES } from './format-checker/scan/constants.js';
4
- import { normalizeScripts } from './helper-cli/scripts.js';
5
- import { promptForScript } from './helper-cli/prompts.js';
6
4
  import { rootDir } from './helper-cli/env.js';
5
+ import { promptForScript } from './helper-cli/prompts.js';
6
+ import { normalizeScripts } from './helper-cli/scripts.js';
7
7
  import { loadPackages } from './packages.js';
8
8
  const ROOT_ENTRY = {
9
9
  label: 'Workspace root',
@@ -11,21 +11,53 @@ const ROOT_ENTRY = {
11
11
  description: 'Root directory',
12
12
  color: 'brightBlue',
13
13
  };
14
+ const ACTION_SCRIPT_PREFIX = '__explorer_action__';
14
15
  export async function promptDirectorySelection(options = {}) {
15
- const candidates = await buildDirectoryCandidates(options.includeWorkspaceRoot ?? true);
16
- const entries = normalizeScripts(candidates.map((candidate) => ({
17
- name: candidate.label,
18
- emoji: '📁',
19
- color: candidate.color,
20
- description: candidate.description ?? candidate.relativePath,
21
- script: candidate.relativePath,
22
- })));
23
- const selection = await promptForScript(entries, options.title ?? 'Select a directory');
16
+ const packageMap = await buildPackageMap();
17
+ const selection = await promptWorkspaceExplorer({
18
+ title: options.title ?? 'Select a directory',
19
+ mode: 'directory',
20
+ rootPath: rootDir,
21
+ includeRootSelection: options.includeWorkspaceRoot ?? true,
22
+ includeFile: () => true,
23
+ describeDirectory: (entry) => describeDirectory(entry, packageMap),
24
+ describeFile: (entry) => ({
25
+ label: entry.name,
26
+ description: entry.relativePath,
27
+ }),
28
+ });
24
29
  if (!selection)
25
30
  return undefined;
26
- const relativePath = selection.script ?? '.';
27
- const absolutePath = path.resolve(rootDir, relativePath);
28
- return { label: selection.displayName, relativePath, absolutePath };
31
+ return {
32
+ label: selection.label,
33
+ relativePath: selection.relativePath,
34
+ absolutePath: selection.absolutePath,
35
+ };
36
+ }
37
+ export async function promptWorkspaceFileSelection(options = {}) {
38
+ const rootPath = resolveRootPath(options.rootPath);
39
+ const selection = await promptWorkspaceExplorer({
40
+ title: options.title ?? 'Select a file',
41
+ mode: 'file',
42
+ rootPath,
43
+ includeRootSelection: false,
44
+ includeFile: options.includeFile ?? (() => true),
45
+ describeDirectory: (entry) => ({
46
+ label: `${entry.name}/`,
47
+ description: entry.relativePath,
48
+ }),
49
+ describeFile: (entry) => ({
50
+ label: entry.name,
51
+ description: entry.relativePath,
52
+ }),
53
+ });
54
+ if (!selection || selection.kind !== 'file')
55
+ return undefined;
56
+ return {
57
+ label: selection.label,
58
+ relativePath: selection.relativePath,
59
+ absolutePath: selection.absolutePath,
60
+ };
29
61
  }
30
62
  export function describeDirectorySelection(selection) {
31
63
  const normalized = selection.relativePath
@@ -36,83 +68,243 @@ export function describeDirectorySelection(selection) {
36
68
  }
37
69
  return `${selection.label} (${normalized})`;
38
70
  }
39
- async function buildDirectoryCandidates(includeRoot) {
40
- const directories = await collectDirectories();
41
- const packages = await loadPackageMetadata();
42
- const packageMap = new Map(packages.map((pkg) => [pkg.relativeDir, pkg]));
43
- const candidates = [];
44
- const filtered = directories.filter((entry) => includeRoot || entry.relativePath !== '.');
45
- for (const entry of filtered) {
46
- const pkg = packageMap.get(entry.relativePath);
47
- if (pkg) {
48
- candidates.push({
49
- label: pkg.name ?? pkg.dirName,
50
- relativePath: entry.relativePath,
51
- description: pkg.relativeDir,
52
- color: pkg.color,
53
- });
71
+ async function promptWorkspaceExplorer(options) {
72
+ const ignorePatterns = await collectIgnorePatterns(options.rootPath);
73
+ let currentPath = options.rootPath;
74
+ // eslint-disable-next-line no-constant-condition
75
+ while (true) {
76
+ const entries = await readExplorerEntries(currentPath, options, ignorePatterns);
77
+ const { menuEntries, actionMap } = buildExplorerMenuEntries(options, currentPath, entries);
78
+ const selectedEntry = await promptForScript(menuEntries, `${options.title} (${formatCurrentLocation(options.rootPath, currentPath)})`);
79
+ if (!selectedEntry?.script) {
80
+ const parentPath = getParentPath(currentPath, options.rootPath);
81
+ if (parentPath) {
82
+ currentPath = parentPath;
83
+ continue;
84
+ }
85
+ return undefined;
86
+ }
87
+ const action = actionMap.get(selectedEntry.script);
88
+ if (!action) {
54
89
  continue;
55
90
  }
56
- if (entry.relativePath === '.') {
57
- candidates.push({
58
- label: ROOT_ENTRY.label,
59
- relativePath: ROOT_ENTRY.relativePath,
60
- description: ROOT_ENTRY.description,
61
- color: ROOT_ENTRY.color,
91
+ if (action.type === 'select-current-directory') {
92
+ return buildCurrentDirectorySelection(options, currentPath);
93
+ }
94
+ if (action.type === 'open-directory') {
95
+ currentPath = action.entry.absolutePath;
96
+ continue;
97
+ }
98
+ if (action.type === 'select-file') {
99
+ if (options.mode === 'file') {
100
+ return toExplorerSelection(action.entry);
101
+ }
102
+ continue;
103
+ }
104
+ // noop placeholder
105
+ }
106
+ }
107
+ function buildExplorerMenuEntries(options, currentPath, entries) {
108
+ const actionMap = new Map();
109
+ const scripts = [];
110
+ let actionIndex = 0;
111
+ const addAction = (action, entry) => {
112
+ const scriptId = `${ACTION_SCRIPT_PREFIX}${actionIndex++}`;
113
+ actionMap.set(scriptId, action);
114
+ scripts.push({
115
+ ...entry,
116
+ script: scriptId,
117
+ });
118
+ };
119
+ if (options.mode === 'directory' && canSelectCurrentDirectory(options, currentPath)) {
120
+ const currentSelection = buildCurrentDirectorySelection(options, currentPath);
121
+ addAction({ type: 'select-current-directory' }, {
122
+ name: currentSelection.relativePath === '.'
123
+ ? ROOT_ENTRY.label
124
+ : 'Select this directory',
125
+ emoji: '✅',
126
+ description: currentSelection.relativePath === '.'
127
+ ? ROOT_ENTRY.description
128
+ : currentSelection.relativePath,
129
+ color: currentSelection.relativePath === '.' ? ROOT_ENTRY.color : undefined,
130
+ });
131
+ }
132
+ for (const entry of entries) {
133
+ if (entry.kind === 'directory') {
134
+ addAction({ type: 'open-directory', entry }, {
135
+ name: entry.label,
136
+ emoji: '📁',
137
+ description: entry.description,
138
+ color: entry.color,
62
139
  });
63
140
  continue;
64
141
  }
65
- candidates.push({
66
- label: path.basename(entry.relativePath) || ROOT_ENTRY.label,
67
- relativePath: entry.relativePath,
68
- description: entry.relativePath,
142
+ addAction({ type: 'select-file', entry }, {
143
+ name: entry.label,
144
+ emoji: '📄',
145
+ description: entry.description,
146
+ color: entry.color,
69
147
  });
70
148
  }
71
- if (candidates.length === 0 && includeRoot) {
72
- candidates.push(ROOT_ENTRY);
149
+ if (scripts.length === 0) {
150
+ addAction({ type: 'noop' }, {
151
+ name: 'No entries at this level',
152
+ emoji: '🫥',
153
+ description: 'Go deeper into another folder or press 0 to go back.',
154
+ });
73
155
  }
74
- return candidates;
156
+ return {
157
+ menuEntries: normalizeScripts(scripts),
158
+ actionMap,
159
+ };
75
160
  }
76
- async function collectDirectories() {
77
- const patterns = await collectIgnorePatterns();
78
- const directories = [];
79
- async function walk(current) {
80
- const relativeRaw = path.relative(rootDir, current);
81
- const relativePath = normalizeRelativePath(relativeRaw);
82
- if (current !== rootDir && shouldIgnore(relativePath, patterns)) {
83
- return;
84
- }
85
- directories.push({ absolutePath: current, relativePath });
86
- let entries;
87
- try {
88
- entries = await readdir(current, { withFileTypes: true });
89
- }
90
- catch {
91
- return;
92
- }
93
- for (const entry of entries) {
94
- if (!entry.isDirectory() || entry.isSymbolicLink())
95
- continue;
96
- await walk(path.join(current, entry.name));
161
+ async function readExplorerEntries(currentPath, options, ignorePatterns) {
162
+ let filesystemEntries;
163
+ try {
164
+ filesystemEntries = await readdir(currentPath, { withFileTypes: true });
165
+ }
166
+ catch {
167
+ return [];
168
+ }
169
+ const entries = [];
170
+ for (const filesystemEntry of filesystemEntries) {
171
+ if (filesystemEntry.isSymbolicLink())
172
+ continue;
173
+ const absolutePath = path.join(currentPath, filesystemEntry.name);
174
+ const relativePath = normalizeRelativePath(path.relative(options.rootPath, absolutePath));
175
+ if (shouldIgnore(relativePath, ignorePatterns))
176
+ continue;
177
+ const candidate = {
178
+ name: filesystemEntry.name,
179
+ absolutePath,
180
+ relativePath,
181
+ };
182
+ if (filesystemEntry.isDirectory()) {
183
+ const presentation = options.describeDirectory(candidate);
184
+ entries.push({
185
+ ...candidate,
186
+ kind: 'directory',
187
+ label: presentation.label,
188
+ description: presentation.description ?? candidate.relativePath,
189
+ color: presentation.color,
190
+ });
191
+ continue;
97
192
  }
193
+ if (!filesystemEntry.isFile())
194
+ continue;
195
+ if (!options.includeFile(candidate))
196
+ continue;
197
+ const presentation = options.describeFile(candidate);
198
+ entries.push({
199
+ ...candidate,
200
+ kind: 'file',
201
+ label: presentation.label,
202
+ description: presentation.description ?? candidate.relativePath,
203
+ color: presentation.color,
204
+ });
98
205
  }
99
- await walk(rootDir);
100
- directories.sort((a, b) => {
101
- if (a.relativePath === '.')
102
- return -1;
103
- if (b.relativePath === '.')
104
- return 1;
105
- return a.relativePath.localeCompare(b.relativePath);
206
+ entries.sort((left, right) => {
207
+ if (left.kind !== right.kind)
208
+ return left.kind === 'directory' ? -1 : 1;
209
+ return left.label.localeCompare(right.label, undefined, {
210
+ sensitivity: 'base',
211
+ numeric: true,
212
+ });
106
213
  });
107
- return directories;
214
+ return entries;
215
+ }
216
+ function toExplorerSelection(entry) {
217
+ return {
218
+ kind: entry.kind,
219
+ label: entry.label,
220
+ relativePath: entry.relativePath,
221
+ absolutePath: entry.absolutePath,
222
+ };
108
223
  }
109
- async function collectIgnorePatterns() {
224
+ function formatCurrentLocation(rootPath, currentPath) {
225
+ const relativePath = normalizeRelativePath(path.relative(rootPath, currentPath));
226
+ return relativePath === '.'
227
+ ? `${ROOT_ENTRY.label} (${ROOT_ENTRY.relativePath})`
228
+ : relativePath;
229
+ }
230
+ function canSelectCurrentDirectory(options, currentPath) {
231
+ if (options.mode !== 'directory')
232
+ return false;
233
+ if (options.includeRootSelection)
234
+ return true;
235
+ const relativePath = normalizeRelativePath(path.relative(options.rootPath, currentPath));
236
+ return relativePath !== '.';
237
+ }
238
+ function buildCurrentDirectorySelection(options, currentPath) {
239
+ const relativePath = normalizeRelativePath(path.relative(options.rootPath, currentPath));
240
+ const candidate = {
241
+ name: path.basename(currentPath),
242
+ absolutePath: currentPath,
243
+ relativePath,
244
+ };
245
+ const presentation = relativePath === '.'
246
+ ? {
247
+ label: ROOT_ENTRY.label,
248
+ description: ROOT_ENTRY.description,
249
+ color: ROOT_ENTRY.color,
250
+ }
251
+ : options.describeDirectory(candidate);
252
+ return {
253
+ kind: 'directory',
254
+ label: presentation.label,
255
+ relativePath,
256
+ absolutePath: currentPath,
257
+ };
258
+ }
259
+ function getParentPath(currentPath, rootPath) {
260
+ const relativePath = normalizeRelativePath(path.relative(rootPath, currentPath));
261
+ if (relativePath === '.')
262
+ return undefined;
263
+ const parent = path.dirname(currentPath);
264
+ if (!isWithinRoot(parent, rootPath))
265
+ return undefined;
266
+ return parent;
267
+ }
268
+ function isWithinRoot(targetPath, rootPath) {
269
+ const relative = path.relative(rootPath, targetPath);
270
+ return (relative === '' ||
271
+ relative === '.' ||
272
+ (!relative.startsWith('..') && !path.isAbsolute(relative)));
273
+ }
274
+ function describeDirectory(entry, packageMap) {
275
+ if (entry.relativePath === '.') {
276
+ return {
277
+ label: ROOT_ENTRY.label,
278
+ description: ROOT_ENTRY.description,
279
+ color: ROOT_ENTRY.color,
280
+ };
281
+ }
282
+ const pkg = packageMap.get(entry.relativePath);
283
+ if (pkg) {
284
+ return {
285
+ label: pkg.name ?? pkg.dirName,
286
+ description: pkg.relativeDir,
287
+ color: pkg.color,
288
+ };
289
+ }
290
+ return {
291
+ label: entry.name,
292
+ description: entry.relativePath,
293
+ };
294
+ }
295
+ function resolveRootPath(rootPath) {
296
+ const basePath = rootPath ?? rootDir;
297
+ if (path.isAbsolute(basePath))
298
+ return path.resolve(basePath);
299
+ return path.resolve(rootDir, basePath);
300
+ }
301
+ async function collectIgnorePatterns(rootPath) {
110
302
  const patterns = new Set();
111
303
  for (const entry of IGNORED_DIRECTORIES) {
112
304
  patterns.add(normalizeRelativePath(entry));
113
305
  }
114
306
  try {
115
- const contents = await readFile(path.join(rootDir, '.gitignore'), 'utf-8');
307
+ const contents = await readFile(path.join(rootPath, '.gitignore'), 'utf-8');
116
308
  for (const rawLine of contents.split(/\r?\n/)) {
117
309
  const trimmed = rawLine.trim();
118
310
  if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!'))
@@ -125,6 +317,14 @@ async function collectIgnorePatterns() {
125
317
  }
126
318
  return Array.from(patterns).filter(Boolean);
127
319
  }
320
+ async function buildPackageMap() {
321
+ const packages = await loadPackageMetadata();
322
+ const packageMap = new Map();
323
+ for (const pkg of packages) {
324
+ packageMap.set(normalizeRelativePath(pkg.relativeDir), pkg);
325
+ }
326
+ return packageMap;
327
+ }
128
328
  function normalizeRelativePath(value) {
129
329
  const normalized = value.replace(/\\/g, '/').replace(/^\/+/, '');
130
330
  const trimmed = normalized.replace(/\/+$/, '');
@@ -4,6 +4,25 @@ import { colors, getEntryColor } from './colors.js';
4
4
  import { pageHeading } from './display.js';
5
5
  import { BACK_KEY, buildVisibleOptions, NEXT_KEY, PAGE_SIZE, PREVIOUS_KEY, } from './pagination.js';
6
6
  import { findScriptEntry } from './scripts.js';
7
+ const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
8
+ function visibleLength(text) {
9
+ const clean = text.replace(ANSI_REGEX, '');
10
+ let length = 0;
11
+ for (const ch of clean) {
12
+ const code = ch.codePointAt(0) ?? 0;
13
+ length += code > 0xffff ? 2 : 1;
14
+ }
15
+ return length;
16
+ }
17
+ function highlightHoveredLine(text) {
18
+ const columns = process.stdout.columns ?? 80;
19
+ const len = visibleLength(text);
20
+ // Avoid padding to the exact terminal width; that can trigger hard-wrap drift
21
+ // during repeated redraws in some terminals.
22
+ const targetWidth = Math.max(1, columns - 1);
23
+ const padded = len < targetWidth ? `${text}${' '.repeat(targetWidth - len)}` : text;
24
+ return `\x1b[7m${padded}\x1b[0m`;
25
+ }
7
26
  function formatInteractiveLines(state, selectedIndex, selectedEntries, title, searchState) {
8
27
  const heading = pageHeading(title, state.page, state.pageCount);
9
28
  const lines = [heading];
@@ -14,18 +33,28 @@ function formatInteractiveLines(state, selectedIndex, selectedEntries, title, se
14
33
  }
15
34
  state.options.forEach((option, index) => {
16
35
  const isSelectedRow = index === selectedIndex;
17
- const pointer = isSelectedRow ? `${colors.green('➤')} ` : '';
36
+ const pointer = isSelectedRow ? '➤ ' : '';
18
37
  const entryColorizer = option.type === 'entry' && option.entry.color
19
38
  ? getEntryColor(option.entry)
20
39
  : colors.cyan;
21
- const numberLabelColorizer = isSelectedRow ? colors.green : entryColorizer;
40
+ const numberLabelColorizer = isSelectedRow ? (text) => text : entryColorizer;
22
41
  const numberLabel = numberLabelColorizer(`${option.hotkey}`.padStart(2, ' '));
23
42
  if (option.type === 'entry') {
24
43
  const isChecked = selectedEntries.has(option.entry);
25
- const checkbox = isChecked ? colors.green('[x]') : colors.dim('[ ]');
26
- const labelColorizer = isSelectedRow ? colors.green : entryColorizer;
44
+ const checkbox = isChecked
45
+ ? isSelectedRow
46
+ ? '[x]'
47
+ : colors.green('[x]')
48
+ : isSelectedRow
49
+ ? '[ ]'
50
+ : colors.dim('[ ]');
51
+ const labelColorizer = isSelectedRow ? (text) => text : entryColorizer;
27
52
  const label = labelColorizer(option.entry.displayName);
28
- lines.push(`${pointer}${numberLabel}. ${checkbox} ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
53
+ const metaLabel = isSelectedRow
54
+ ? option.entry.metaLabel
55
+ : colors.dim(option.entry.metaLabel);
56
+ const row = `${pointer}${numberLabel}. ${checkbox} ${option.entry.emoji} ${label} ${metaLabel}`;
57
+ lines.push(isSelectedRow ? highlightHoveredLine(row) : row);
29
58
  return;
30
59
  }
31
60
  const icon = option.action === 'back'
@@ -38,12 +67,9 @@ function formatInteractiveLines(state, selectedIndex, selectedEntries, title, se
38
67
  : option.action === 'next'
39
68
  ? 'Next page'
40
69
  : 'Back';
41
- const navLabel = isSelectedRow && option.enabled
42
- ? colors.green(baseLabel)
43
- : option.enabled
44
- ? baseLabel
45
- : colors.dim(baseLabel);
46
- lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
70
+ const navLabel = option.enabled ? baseLabel : colors.dim(baseLabel);
71
+ const row = `${pointer}${numberLabel}. ${icon} ${navLabel}`;
72
+ lines.push(isSelectedRow ? highlightHoveredLine(row) : row);
47
73
  });
48
74
  if (searchState?.active && !searchState.hasResults) {
49
75
  lines.push(colors.yellow('No scripts match your search.'));
@@ -54,16 +80,6 @@ function formatInteractiveLines(state, selectedIndex, selectedEntries, title, se
54
80
  }
55
81
  function renderInteractiveList(lines, previousLineCount) {
56
82
  const columns = process.stdout.columns ?? 80;
57
- const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
58
- const visibleLength = (text) => {
59
- const clean = text.replace(ansiRegex, '');
60
- let length = 0;
61
- for (const ch of clean) {
62
- const code = ch.codePointAt(0) ?? 0;
63
- length += code > 0xffff ? 2 : 1;
64
- }
65
- return length;
66
- };
67
83
  const nextLineCount = lines.reduce((total, line) => {
68
84
  const len = visibleLength(line);
69
85
  const wrapped = Math.max(1, Math.ceil(len / columns));
@@ -53,6 +53,25 @@ async function promptWithReadline(entries, title) {
53
53
  rl.close();
54
54
  }
55
55
  }
56
+ const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
57
+ function visibleLength(text) {
58
+ const clean = text.replace(ANSI_REGEX, '');
59
+ let length = 0;
60
+ for (const ch of clean) {
61
+ const code = ch.codePointAt(0) ?? 0;
62
+ length += code > 0xffff ? 2 : 1;
63
+ }
64
+ return length;
65
+ }
66
+ function highlightHoveredLine(text) {
67
+ const columns = process.stdout.columns ?? 80;
68
+ const len = visibleLength(text);
69
+ // Avoid padding to the exact terminal width; that can trigger hard-wrap drift
70
+ // during repeated redraws in some terminals.
71
+ const targetWidth = Math.max(1, columns - 1);
72
+ const padded = len < targetWidth ? `${text}${' '.repeat(targetWidth - len)}` : text;
73
+ return `\x1b[7m${padded}\x1b[0m`;
74
+ }
56
75
  function formatInteractiveLines(state, selectedIndex, title, searchState) {
57
76
  const heading = pageHeading(title, state.page, state.pageCount);
58
77
  const lines = [heading];
@@ -62,21 +81,24 @@ function formatInteractiveLines(state, selectedIndex, title, searchState) {
62
81
  }
63
82
  state.options.forEach((option, index) => {
64
83
  const isSelected = index === selectedIndex;
65
- const pointer = isSelected ? `${colors.green('➤')} ` : '';
84
+ const pointer = isSelected ? '➤ ' : '';
66
85
  const entryColorizer = option.type === 'entry' && option.entry.color
67
86
  ? getEntryColor(option.entry)
68
87
  : colors.cyan;
69
- const numberLabelColorizer = isSelected ? colors.green : entryColorizer;
88
+ const numberLabelColorizer = isSelected ? (text) => text : entryColorizer;
70
89
  const numberLabel = numberLabelColorizer(`${option.hotkey}`.padStart(2, ' '));
71
90
  if (option.type === 'entry') {
72
- const labelColorizer = isSelected ? colors.green : entryColorizer;
91
+ const labelColorizer = isSelected ? (text) => text : entryColorizer;
73
92
  const label = labelColorizer(option.entry.displayName);
74
93
  const runHint = searchState?.active &&
75
94
  searchState.hasResults &&
76
95
  index === selectedIndex
77
- ? colors.dim(' (enter to run)')
96
+ ? isSelected
97
+ ? ' (enter to run)'
98
+ : colors.dim(' (enter to run)')
78
99
  : '';
79
- lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}${runHint}`);
100
+ const row = `${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${isSelected ? option.entry.metaLabel : colors.dim(option.entry.metaLabel)}${runHint}`;
101
+ lines.push(isSelected ? highlightHoveredLine(row) : row);
80
102
  return;
81
103
  }
82
104
  const icon = option.action === 'back'
@@ -89,12 +111,9 @@ function formatInteractiveLines(state, selectedIndex, title, searchState) {
89
111
  : option.action === 'next'
90
112
  ? 'Next page'
91
113
  : 'Back';
92
- const navLabel = isSelected && option.enabled
93
- ? colors.green(baseLabel)
94
- : option.enabled
95
- ? baseLabel
96
- : colors.dim(baseLabel);
97
- lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
114
+ const navLabel = option.enabled ? baseLabel : colors.dim(baseLabel);
115
+ const row = `${pointer}${numberLabel}. ${icon} ${navLabel}`;
116
+ lines.push(isSelected ? highlightHoveredLine(row) : row);
98
117
  });
99
118
  if (searchState?.active && !searchState.hasResults) {
100
119
  lines.push(colors.yellow('No scripts match your search.'));
@@ -111,16 +130,6 @@ function formatInteractiveLines(state, selectedIndex, title, searchState) {
111
130
  }
112
131
  function renderInteractiveList(lines, previousLineCount) {
113
132
  const columns = process.stdout.columns ?? 80;
114
- const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
115
- const visibleLength = (text) => {
116
- const clean = text.replace(ansiRegex, '');
117
- let length = 0;
118
- for (const ch of clean) {
119
- const code = ch.codePointAt(0) ?? 0;
120
- length += code > 0xffff ? 2 : 1;
121
- }
122
- return length;
123
- };
124
133
  const nextLineCount = lines.reduce((total, line) => {
125
134
  const len = visibleLength(line);
126
135
  const wrapped = Math.max(1, Math.ceil(len / columns));
@@ -1,7 +1,7 @@
1
1
  import { tmpdir } from 'node:os';
2
2
  import { readdir, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { describeDirectorySelection, promptDirectorySelection, } from '../directory-picker.js';
4
+ import { describeDirectorySelection, promptDirectorySelection, promptWorkspaceFileSelection, } from '../directory-picker.js';
5
5
  import { IGNORED_DIRECTORIES, SOURCE_EXTENSIONS, } from '../format-checker/scan/constants.js';
6
6
  import { rootDir } from '../helper-cli/env.js';
7
7
  import { promptForScript } from '../helper-cli/prompts.js';
@@ -13,8 +13,6 @@ const TRACE_FILE_NAME = 'trace.json';
13
13
  const TYPES_FILE_NAME = 'types.json';
14
14
  const TRACE_SCAN_MAX_DEPTH = 4;
15
15
  const TRACE_SCAN_MAX_RESULTS = 300;
16
- const PROFILE_SCAN_MAX_DEPTH = 8;
17
- const PROFILE_SCAN_MAX_RESULTS = 600;
18
16
  export async function promptTraceProfileTargetSelection() {
19
17
  // eslint-disable-next-line no-constant-condition
20
18
  while (true) {
@@ -25,7 +23,7 @@ export async function promptTraceProfileTargetSelection() {
25
23
  {
26
24
  name: 'Pick folder from workspace',
27
25
  emoji: '📁',
28
- description: 'Profile all TypeScript work under a selected folder',
26
+ description: 'Profile all TypeScript work under any workspace folder',
29
27
  handler: async () => {
30
28
  selected = await pickProfileDirectoryFromWorkspace();
31
29
  },
@@ -33,7 +31,7 @@ export async function promptTraceProfileTargetSelection() {
33
31
  {
34
32
  name: 'Pick file from workspace',
35
33
  emoji: '📄',
36
- description: 'Profile a single TypeScript file',
34
+ description: 'Profile a single file from anywhere in the workspace',
37
35
  handler: async () => {
38
36
  selected = await pickProfileFileFromWorkspace();
39
37
  },
@@ -116,22 +114,15 @@ async function pickProfileDirectoryFromWorkspace() {
116
114
  };
117
115
  }
118
116
  async function pickProfileFileFromWorkspace() {
119
- const baseSelection = await promptDirectorySelection({
120
- title: 'Select a directory to search for source files',
117
+ const chosen = await promptWorkspaceFileSelection({
118
+ title: 'Pick file to profile',
119
+ includeFile: (entry) => SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()),
121
120
  });
122
- if (!baseSelection)
123
- return undefined;
124
- const files = await collectProfileFiles(baseSelection.absolutePath, PROFILE_SCAN_MAX_DEPTH, PROFILE_SCAN_MAX_RESULTS);
125
- if (!files.length) {
126
- console.log(colors.yellow(`No TypeScript/JavaScript source files found under ${baseSelection.relativePath || '.'}.`));
127
- return undefined;
128
- }
129
- const chosen = await promptPathCandidate(files, 'Pick file to profile', baseSelection.absolutePath, '📄');
130
121
  if (!chosen)
131
122
  return undefined;
132
123
  return {
133
124
  absolutePath: chosen.absolutePath,
134
- label: relToRoot(chosen.absolutePath),
125
+ label: chosen.relativePath,
135
126
  kind: 'file',
136
127
  };
137
128
  }
@@ -207,17 +198,10 @@ async function pickTraceFolderFromTmp() {
207
198
  };
208
199
  }
209
200
  async function pickTraceFileFromWorkspace() {
210
- const baseSelection = await promptDirectorySelection({
211
- title: 'Select a directory to search for trace.json files',
201
+ const chosen = await promptWorkspaceFileSelection({
202
+ title: 'Pick trace.json file',
203
+ includeFile: (entry) => entry.name.toLowerCase() === TRACE_FILE_NAME,
212
204
  });
213
- if (!baseSelection)
214
- return undefined;
215
- const files = await collectNamedFiles(baseSelection.absolutePath, TRACE_FILE_NAME, TRACE_SCAN_MAX_DEPTH, TRACE_SCAN_MAX_RESULTS);
216
- if (!files.length) {
217
- console.log(colors.yellow('No trace.json files found in that selection.'));
218
- return undefined;
219
- }
220
- const chosen = await promptPathCandidate(files, 'Pick trace.json file', baseSelection.absolutePath, '📦');
221
205
  if (!chosen)
222
206
  return undefined;
223
207
  return resolveTraceDirectoryFromInput(chosen.absolutePath);
@@ -289,17 +273,6 @@ async function promptPathCandidate(candidates, title, basePath, emoji) {
289
273
  relativePath: path.relative(basePath, absolutePath).replace(/\\/g, '/') || '.',
290
274
  };
291
275
  }
292
- async function collectNamedFiles(rootPath, filename, maxDepth, maxResults) {
293
- const output = [];
294
- await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
295
- if (path.basename(absolutePath).toLowerCase() !== filename)
296
- return;
297
- if (output.length >= maxResults)
298
- return;
299
- output.push({ absolutePath, relativePath });
300
- }, maxDepth);
301
- return output;
302
- }
303
276
  async function collectTraceDirectories(rootPath, maxDepth, maxResults) {
304
277
  const output = [];
305
278
  await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
@@ -314,18 +287,6 @@ async function collectTraceDirectories(rootPath, maxDepth, maxResults) {
314
287
  }, maxDepth);
315
288
  return output;
316
289
  }
317
- async function collectProfileFiles(rootPath, maxDepth, maxResults) {
318
- const output = [];
319
- await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
320
- if (output.length >= maxResults)
321
- return;
322
- const extension = path.extname(absolutePath).toLowerCase();
323
- if (!SOURCE_EXTENSIONS.has(extension))
324
- return;
325
- output.push({ absolutePath, relativePath });
326
- }, maxDepth);
327
- return output;
328
- }
329
290
  async function walk(currentPath, basePath, depth, onEntry, maxDepth) {
330
291
  if (depth > maxDepth)
331
292
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "typescript": "^5.9.3"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/node": "^20.19.33",
32
+ "@types/node": "^20.19.37",
33
33
  "@types/semver": "^7.7.1",
34
34
  "cross-env": "^7.0.3"
35
35
  },
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "scripts": {
43
43
  "manager-cli": "node dist/manager-cli.js",
44
+ "update:agent-context": "node tools/update-agent-context.js",
44
45
  "build": "tsc -p tsconfig.base.json && node scripts/copy-manager-cli.mjs",
45
46
  "typecheck": "tsc -p tsconfig.base.json --noEmit",
46
47
  "test": "cross-env TS_NODE_PROJECT=tsconfig.test.json node --loader ts-node/esm --test test/*.test.ts"