@emeryld/manager 1.5.2 → 1.5.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.
@@ -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(/\/+$/, '');
@@ -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.3",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",