@emeryld/manager 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,373 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { readdir, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { describeDirectorySelection, promptDirectorySelection, } from '../directory-picker.js';
5
+ import { IGNORED_DIRECTORIES, SOURCE_EXTENSIONS, } from '../format-checker/scan/constants.js';
6
+ import { rootDir } from '../helper-cli/env.js';
7
+ import { promptForScript } from '../helper-cli/prompts.js';
8
+ import { normalizeScripts } from '../helper-cli/scripts.js';
9
+ import { runHelperCli } from '../helper-cli.js';
10
+ import { askLine } from '../prompts.js';
11
+ import { colors } from '../utils/log.js';
12
+ const TRACE_FILE_NAME = 'trace.json';
13
+ const TYPES_FILE_NAME = 'types.json';
14
+ const TRACE_SCAN_MAX_DEPTH = 4;
15
+ const TRACE_SCAN_MAX_RESULTS = 300;
16
+ const PROFILE_SCAN_MAX_DEPTH = 8;
17
+ const PROFILE_SCAN_MAX_RESULTS = 600;
18
+ export async function promptTraceProfileTargetSelection() {
19
+ // eslint-disable-next-line no-constant-condition
20
+ while (true) {
21
+ let selected;
22
+ const ran = await runHelperCli({
23
+ title: 'TypeScript trace profiler target',
24
+ scripts: [
25
+ {
26
+ name: 'Pick folder from workspace',
27
+ emoji: '📁',
28
+ description: 'Profile all TypeScript work under a selected folder',
29
+ handler: async () => {
30
+ selected = await pickProfileDirectoryFromWorkspace();
31
+ },
32
+ },
33
+ {
34
+ name: 'Pick file from workspace',
35
+ emoji: '📄',
36
+ description: 'Profile a single TypeScript file',
37
+ handler: async () => {
38
+ selected = await pickProfileFileFromWorkspace();
39
+ },
40
+ },
41
+ {
42
+ name: 'Enter file/folder path',
43
+ emoji: '⌨️',
44
+ description: 'Manual path entry relative to the workspace',
45
+ handler: async () => {
46
+ selected = await pickProfilePathManually();
47
+ },
48
+ },
49
+ ],
50
+ argv: [],
51
+ });
52
+ if (!ran)
53
+ return undefined;
54
+ if (selected)
55
+ return selected;
56
+ }
57
+ }
58
+ export async function promptTraceDirectorySelection() {
59
+ // eslint-disable-next-line no-constant-condition
60
+ while (true) {
61
+ let selected;
62
+ const ran = await runHelperCli({
63
+ title: 'TypeScript trace input (pick a folder or file)',
64
+ scripts: [
65
+ {
66
+ name: 'Pick folder from workspace',
67
+ emoji: '📁',
68
+ description: 'Choose a directory and auto-detect trace.json/types.json',
69
+ handler: async () => {
70
+ selected = await pickTraceFolderFromWorkspace();
71
+ },
72
+ },
73
+ {
74
+ name: `Pick folder from ${tmpdir()}`,
75
+ emoji: '🧊',
76
+ description: 'Quick pick for /tmp/ts-trace-* directories',
77
+ handler: async () => {
78
+ selected = await pickTraceFolderFromTmp();
79
+ },
80
+ },
81
+ {
82
+ name: 'Pick trace.json file from workspace',
83
+ emoji: '📄',
84
+ description: 'Choose a file; its parent directory is used as traceDir',
85
+ handler: async () => {
86
+ selected = await pickTraceFileFromWorkspace();
87
+ },
88
+ },
89
+ {
90
+ name: 'Enter file or folder path',
91
+ emoji: '⌨️',
92
+ description: 'Manual path entry for any location',
93
+ handler: async () => {
94
+ selected = await pickTracePathManually();
95
+ },
96
+ },
97
+ ],
98
+ argv: [],
99
+ });
100
+ if (!ran)
101
+ return undefined;
102
+ if (selected)
103
+ return selected;
104
+ }
105
+ }
106
+ async function pickProfileDirectoryFromWorkspace() {
107
+ const selection = await promptDirectorySelection({
108
+ title: 'Select a directory to profile',
109
+ });
110
+ if (!selection)
111
+ return undefined;
112
+ return {
113
+ absolutePath: selection.absolutePath,
114
+ label: describeDirectorySelection(selection),
115
+ kind: 'directory',
116
+ };
117
+ }
118
+ async function pickProfileFileFromWorkspace() {
119
+ const baseSelection = await promptDirectorySelection({
120
+ title: 'Select a directory to search for source files',
121
+ });
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
+ if (!chosen)
131
+ return undefined;
132
+ return {
133
+ absolutePath: chosen.absolutePath,
134
+ label: relToRoot(chosen.absolutePath),
135
+ kind: 'file',
136
+ };
137
+ }
138
+ async function pickProfilePathManually() {
139
+ const rawPath = await askLine(colors.cyan('Enter workspace-relative file/folder path to profile: '));
140
+ const trimmed = rawPath.trim();
141
+ if (!trimmed)
142
+ return undefined;
143
+ const absolutePath = path.isAbsolute(trimmed)
144
+ ? path.resolve(trimmed)
145
+ : path.resolve(rootDir, trimmed);
146
+ let stats;
147
+ try {
148
+ stats = await stat(absolutePath);
149
+ }
150
+ catch {
151
+ console.log(colors.yellow(`Path not found: ${absolutePath}`));
152
+ return undefined;
153
+ }
154
+ if (!stats.isFile() && !stats.isDirectory()) {
155
+ console.log(colors.yellow('Target must be a file or a folder.'));
156
+ return undefined;
157
+ }
158
+ return {
159
+ absolutePath,
160
+ label: relToRoot(absolutePath),
161
+ kind: stats.isDirectory() ? 'directory' : 'file',
162
+ };
163
+ }
164
+ async function pickTraceFolderFromWorkspace() {
165
+ const selection = await promptDirectorySelection({
166
+ title: 'Select a directory containing a TypeScript trace',
167
+ });
168
+ if (!selection)
169
+ return undefined;
170
+ return resolveTraceDirectoryFromInput(selection.absolutePath);
171
+ }
172
+ async function pickTraceFolderFromTmp() {
173
+ const tmp = tmpdir();
174
+ let entries;
175
+ try {
176
+ entries = await readdir(tmp, { withFileTypes: true });
177
+ }
178
+ catch {
179
+ console.log(colors.yellow(`Unable to read temporary directory: ${tmp}`));
180
+ return undefined;
181
+ }
182
+ const candidates = [];
183
+ for (const entry of entries) {
184
+ if (!entry.isDirectory())
185
+ continue;
186
+ if (!entry.name.startsWith('ts-trace-'))
187
+ continue;
188
+ const absolutePath = path.join(tmp, entry.name);
189
+ const hasArtifacts = await directoryHasTraceArtifacts(absolutePath);
190
+ if (!hasArtifacts)
191
+ continue;
192
+ candidates.push({
193
+ absolutePath,
194
+ relativePath: absolutePath.replace(/\\/g, '/'),
195
+ });
196
+ }
197
+ if (!candidates.length) {
198
+ console.log(colors.yellow(`No ts-trace-* folders with trace artifacts found in ${tmp}.`));
199
+ return undefined;
200
+ }
201
+ const chosen = await promptPathCandidate(candidates, `Pick a trace folder from ${tmp}`, tmp, '📦');
202
+ if (!chosen)
203
+ return undefined;
204
+ return {
205
+ traceDir: chosen.absolutePath,
206
+ label: chosen.relativePath,
207
+ };
208
+ }
209
+ async function pickTraceFileFromWorkspace() {
210
+ const baseSelection = await promptDirectorySelection({
211
+ title: 'Select a directory to search for trace.json files',
212
+ });
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
+ if (!chosen)
222
+ return undefined;
223
+ return resolveTraceDirectoryFromInput(chosen.absolutePath);
224
+ }
225
+ async function pickTracePathManually() {
226
+ const rawPath = await askLine(colors.cyan('Enter path to trace directory or trace.json/types.json file: '));
227
+ const trimmed = rawPath.trim();
228
+ if (!trimmed)
229
+ return undefined;
230
+ const absolutePath = path.isAbsolute(trimmed)
231
+ ? trimmed
232
+ : path.resolve(rootDir, trimmed);
233
+ return resolveTraceDirectoryFromInput(absolutePath);
234
+ }
235
+ async function resolveTraceDirectoryFromInput(inputPath) {
236
+ let targetPath = inputPath;
237
+ let stats;
238
+ try {
239
+ stats = await stat(targetPath);
240
+ }
241
+ catch {
242
+ console.log(colors.yellow(`Path not found: ${targetPath}`));
243
+ return undefined;
244
+ }
245
+ if (stats.isFile()) {
246
+ const fileName = path.basename(targetPath).toLowerCase();
247
+ if (fileName !== TRACE_FILE_NAME && fileName !== TYPES_FILE_NAME) {
248
+ console.log(colors.yellow('Selected file must be trace.json or types.json. Pick one of those files or a folder.'));
249
+ return undefined;
250
+ }
251
+ targetPath = path.dirname(targetPath);
252
+ }
253
+ if (!(await isDirectory(targetPath))) {
254
+ console.log(colors.yellow(`Not a directory: ${targetPath}`));
255
+ return undefined;
256
+ }
257
+ if (await directoryHasTraceArtifacts(targetPath)) {
258
+ return {
259
+ traceDir: targetPath,
260
+ label: relToRoot(targetPath),
261
+ };
262
+ }
263
+ const nestedCandidates = await collectTraceDirectories(targetPath, TRACE_SCAN_MAX_DEPTH, TRACE_SCAN_MAX_RESULTS);
264
+ if (!nestedCandidates.length) {
265
+ console.log(colors.yellow(`No folders containing ${TRACE_FILE_NAME} and ${TYPES_FILE_NAME} were found.`));
266
+ return undefined;
267
+ }
268
+ const chosen = await promptPathCandidate(nestedCandidates, 'Select a trace directory', targetPath, '📦');
269
+ if (!chosen)
270
+ return undefined;
271
+ return {
272
+ traceDir: chosen.absolutePath,
273
+ label: chosen.relativePath,
274
+ };
275
+ }
276
+ async function promptPathCandidate(candidates, title, basePath, emoji) {
277
+ const entries = normalizeScripts(candidates.map((candidate) => ({
278
+ name: candidate.relativePath,
279
+ emoji,
280
+ description: candidate.absolutePath,
281
+ script: candidate.absolutePath,
282
+ })));
283
+ const chosen = await promptForScript(entries, title);
284
+ if (!chosen || !chosen.script)
285
+ return undefined;
286
+ const absolutePath = chosen.script;
287
+ return {
288
+ absolutePath,
289
+ relativePath: path.relative(basePath, absolutePath).replace(/\\/g, '/') || '.',
290
+ };
291
+ }
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
+ async function collectTraceDirectories(rootPath, maxDepth, maxResults) {
304
+ const output = [];
305
+ await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
306
+ if (output.length >= maxResults)
307
+ return;
308
+ if (!(await isDirectory(absolutePath)))
309
+ return;
310
+ const hasArtifacts = await directoryHasTraceArtifacts(absolutePath);
311
+ if (!hasArtifacts)
312
+ return;
313
+ output.push({ absolutePath, relativePath });
314
+ }, maxDepth);
315
+ return output;
316
+ }
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
+ async function walk(currentPath, basePath, depth, onEntry, maxDepth) {
330
+ if (depth > maxDepth)
331
+ return;
332
+ let entries;
333
+ try {
334
+ entries = await readdir(currentPath, { withFileTypes: true });
335
+ }
336
+ catch {
337
+ return;
338
+ }
339
+ for (const entry of entries) {
340
+ if (entry.isSymbolicLink())
341
+ continue;
342
+ if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name))
343
+ continue;
344
+ const absolutePath = path.join(currentPath, entry.name);
345
+ const relativePath = path.relative(basePath, absolutePath).replace(/\\/g, '/') || '.';
346
+ await onEntry(absolutePath, relativePath);
347
+ if (entry.isDirectory()) {
348
+ await walk(absolutePath, basePath, depth + 1, onEntry, maxDepth);
349
+ }
350
+ }
351
+ }
352
+ async function directoryHasTraceArtifacts(directory) {
353
+ const tracePath = path.join(directory, TRACE_FILE_NAME);
354
+ const typesPath = path.join(directory, TYPES_FILE_NAME);
355
+ const [traceStats, typesStats] = await Promise.all([
356
+ stat(tracePath).catch(() => undefined),
357
+ stat(typesPath).catch(() => undefined),
358
+ ]);
359
+ return Boolean(traceStats?.isFile() && typesStats?.isFile());
360
+ }
361
+ async function isDirectory(targetPath) {
362
+ try {
363
+ const stats = await stat(targetPath);
364
+ return stats.isDirectory();
365
+ }
366
+ catch {
367
+ return false;
368
+ }
369
+ }
370
+ function relToRoot(targetPath) {
371
+ const relative = path.relative(rootDir, targetPath).replace(/\\/g, '/');
372
+ return relative || '.';
373
+ }