@emeryld/manager 1.5.1 → 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.
- package/dist/directory-picker.js +273 -73
- package/dist/publish.js +26 -2
- package/dist/ts-trace/cli/options.js +196 -0
- package/dist/ts-trace/cli/prompts.js +148 -0
- package/dist/ts-trace/cli/settings.js +154 -0
- package/dist/ts-trace/config.js +100 -0
- package/dist/ts-trace/generate.js +150 -0
- package/dist/ts-trace/index.js +137 -0
- package/dist/ts-trace/picker.js +334 -0
- package/dist/ts-trace/report.js +718 -0
- package/dist/ts-trace/types.js +1 -0
- package/package.json +1 -1
package/dist/directory-picker.js
CHANGED
|
@@ -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
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
description: entry.
|
|
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 (
|
|
72
|
-
|
|
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
|
|
156
|
+
return {
|
|
157
|
+
menuEntries: normalizeScripts(scripts),
|
|
158
|
+
actionMap,
|
|
159
|
+
};
|
|
75
160
|
}
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(/\/+$/, '');
|
package/dist/publish.js
CHANGED
|
@@ -13,6 +13,7 @@ import { publishCliState } from './prompts.js';
|
|
|
13
13
|
import { createRrrPackage, runCreatePackageCli, } from './create-package/index.js';
|
|
14
14
|
import { runRobot } from './robot/index.js';
|
|
15
15
|
import { runDocsFeature } from './docs/index.js';
|
|
16
|
+
import { runTypeScriptTraceProfiler, runTypeScriptTraceReportCli, } from './ts-trace/index.js';
|
|
16
17
|
import { colors, logGlobal } from './utils/log.js';
|
|
17
18
|
import { run } from './utils/run.js';
|
|
18
19
|
const MANAGER_PACKAGE = '@emeryld/manager';
|
|
@@ -187,7 +188,7 @@ async function runPackageSelectionLoop(packages, helperArgs) {
|
|
|
187
188
|
{
|
|
188
189
|
name: 'utils',
|
|
189
190
|
emoji: '🛠️',
|
|
190
|
-
description: 'Docs, format checker, robot metadata, and package scaffolding',
|
|
191
|
+
description: 'Docs, format checker, TypeScript trace profiler, robot metadata, and package scaffolding',
|
|
191
192
|
handler: async () => {
|
|
192
193
|
// eslint-disable-next-line no-constant-condition
|
|
193
194
|
while (true) {
|
|
@@ -210,6 +211,14 @@ async function runPackageSelectionLoop(packages, helperArgs) {
|
|
|
210
211
|
await runFormatChecker();
|
|
211
212
|
},
|
|
212
213
|
},
|
|
214
|
+
{
|
|
215
|
+
name: 'typescript trace profiler',
|
|
216
|
+
emoji: '⏱️',
|
|
217
|
+
description: 'Analyze TypeScript generateTrace output and rank slow files, spans, and type relations',
|
|
218
|
+
handler: async () => {
|
|
219
|
+
await runTypeScriptTraceProfiler();
|
|
220
|
+
},
|
|
221
|
+
},
|
|
213
222
|
{
|
|
214
223
|
name: 'robot metadata',
|
|
215
224
|
emoji: '🤖',
|
|
@@ -258,6 +267,15 @@ async function main() {
|
|
|
258
267
|
await runFormatCheckerScanCli(cliArgs.slice(1));
|
|
259
268
|
return;
|
|
260
269
|
}
|
|
270
|
+
if (cliArgs[0] === 'ts-trace') {
|
|
271
|
+
if (cliArgs.length === 1) {
|
|
272
|
+
await runTypeScriptTraceProfiler();
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
await runTypeScriptTraceReportCli(cliArgs.slice(1));
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
261
279
|
const parsed = parseCliArgs(cliArgs);
|
|
262
280
|
const packages = await loadPackages();
|
|
263
281
|
// If user provided non-interactive flags, run headless path
|
|
@@ -294,5 +312,11 @@ async function main() {
|
|
|
294
312
|
}
|
|
295
313
|
main().catch((error) => {
|
|
296
314
|
console.error(error);
|
|
297
|
-
|
|
315
|
+
const exitCode = typeof error === 'object' &&
|
|
316
|
+
error !== null &&
|
|
317
|
+
'exitCode' in error &&
|
|
318
|
+
typeof error.exitCode === 'number'
|
|
319
|
+
? error.exitCode
|
|
320
|
+
: 1;
|
|
321
|
+
process.exit(exitCode);
|
|
298
322
|
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { colors } from '../../utils/log.js';
|
|
3
|
+
export function parseTraceReportCliArgs(argv, defaults) {
|
|
4
|
+
let traceDir;
|
|
5
|
+
let targetPath;
|
|
6
|
+
let projectPath = defaults.projectPath;
|
|
7
|
+
let top = defaults.top;
|
|
8
|
+
let minMs = defaults.minMs;
|
|
9
|
+
let json = defaults.includeJson;
|
|
10
|
+
let filterRegex = defaults.filterRegex;
|
|
11
|
+
let eventNames = [...defaults.eventNames];
|
|
12
|
+
let didProvideEventFlag = false;
|
|
13
|
+
let baseDir = defaults.baseDir;
|
|
14
|
+
let outPath = defaults.outPath;
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const token = argv[i];
|
|
17
|
+
if (token === '--help' || token === '-h') {
|
|
18
|
+
return { help: true };
|
|
19
|
+
}
|
|
20
|
+
if (token === '--json') {
|
|
21
|
+
json = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (token === '--traceDir' || token === '--trace-dir') {
|
|
25
|
+
const raw = argv[++i];
|
|
26
|
+
if (!raw)
|
|
27
|
+
throw new Error('Flag "--traceDir" expects a directory path.');
|
|
28
|
+
traceDir = raw;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (token === '--target') {
|
|
32
|
+
const raw = argv[++i];
|
|
33
|
+
if (!raw)
|
|
34
|
+
throw new Error('Flag "--target" expects a file or folder path.');
|
|
35
|
+
targetPath = raw;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (token === '--project') {
|
|
39
|
+
const raw = argv[++i];
|
|
40
|
+
if (!raw)
|
|
41
|
+
throw new Error('Flag "--project" expects a tsconfig path.');
|
|
42
|
+
projectPath = raw;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token === '--top') {
|
|
46
|
+
const raw = argv[++i];
|
|
47
|
+
const parsed = parsePositiveInteger(raw);
|
|
48
|
+
if (parsed === undefined) {
|
|
49
|
+
throw new Error('Flag "--top" expects a positive integer.');
|
|
50
|
+
}
|
|
51
|
+
top = parsed;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (token === '--minMs' || token === '--min-ms') {
|
|
55
|
+
const raw = argv[++i];
|
|
56
|
+
const parsed = parseNonNegativeNumber(raw);
|
|
57
|
+
if (parsed === undefined) {
|
|
58
|
+
throw new Error('Flag "--minMs" expects a non-negative number.');
|
|
59
|
+
}
|
|
60
|
+
minMs = parsed;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (token === '--filter') {
|
|
64
|
+
const raw = argv[++i];
|
|
65
|
+
if (raw === undefined) {
|
|
66
|
+
throw new Error('Flag "--filter" expects a regex string.');
|
|
67
|
+
}
|
|
68
|
+
ensureRegex(raw);
|
|
69
|
+
filterRegex = raw;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === '--event') {
|
|
73
|
+
const raw = argv[++i];
|
|
74
|
+
if (!raw)
|
|
75
|
+
throw new Error('Flag "--event" expects an event name.');
|
|
76
|
+
if (!didProvideEventFlag) {
|
|
77
|
+
eventNames = [];
|
|
78
|
+
}
|
|
79
|
+
didProvideEventFlag = true;
|
|
80
|
+
eventNames.push(raw);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (token === '--baseDir' || token === '--base-dir') {
|
|
84
|
+
const raw = argv[++i];
|
|
85
|
+
if (!raw)
|
|
86
|
+
throw new Error('Flag "--baseDir" expects a path.');
|
|
87
|
+
baseDir = raw;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (token === '--out') {
|
|
91
|
+
const raw = argv[++i];
|
|
92
|
+
if (!raw)
|
|
93
|
+
throw new Error('Flag "--out" expects a file path.');
|
|
94
|
+
outPath = raw;
|
|
95
|
+
json = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`Unknown option "${token}". Use "pnpm manager-cli ts-trace --help".`);
|
|
99
|
+
}
|
|
100
|
+
if (traceDir && targetPath) {
|
|
101
|
+
throw new Error('Use either --traceDir (existing trace) or --target (generate trace), not both.');
|
|
102
|
+
}
|
|
103
|
+
const reportBase = {
|
|
104
|
+
top,
|
|
105
|
+
minMs,
|
|
106
|
+
json,
|
|
107
|
+
filterRegex,
|
|
108
|
+
eventNames: normalizeEventNames(eventNames),
|
|
109
|
+
baseDir: path.resolve(baseDir || process.cwd()),
|
|
110
|
+
outPath: outPath ? path.resolve(outPath) : undefined,
|
|
111
|
+
};
|
|
112
|
+
if (traceDir) {
|
|
113
|
+
return {
|
|
114
|
+
help: false,
|
|
115
|
+
command: {
|
|
116
|
+
mode: 'existing',
|
|
117
|
+
report: {
|
|
118
|
+
traceDir: path.resolve(traceDir),
|
|
119
|
+
...reportBase,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (targetPath) {
|
|
125
|
+
return {
|
|
126
|
+
help: false,
|
|
127
|
+
command: {
|
|
128
|
+
mode: 'generate',
|
|
129
|
+
targetPath: path.resolve(targetPath),
|
|
130
|
+
projectPath: projectPath ? path.resolve(projectPath) : undefined,
|
|
131
|
+
report: reportBase,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
throw new Error('Provide one of: --target <file|folder> (generate trace) or --traceDir <path> (analyze existing).');
|
|
136
|
+
}
|
|
137
|
+
export function printTraceReportUsage() {
|
|
138
|
+
console.log(colors.bold('Usage: pnpm manager-cli ts-trace [flags]'));
|
|
139
|
+
console.log(colors.dim('Profile TypeScript checker speed by target path, or analyze an existing --generateTrace directory.'));
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(`${colors.cyan('--target <path>')}\tProfile a file/folder (auto-generates trace then reports offenders).`);
|
|
142
|
+
console.log(`${colors.cyan('--project <tsconfig>')}\tOptional project config for generation (default: auto-detect).`);
|
|
143
|
+
console.log(`${colors.cyan('--traceDir <path>')}\tAnalyze an already-generated trace directory with trace.json/types.json.`);
|
|
144
|
+
console.log(`${colors.cyan('--top <n>')}\tShow top results per table. Defaults to workspace settings or 20.`);
|
|
145
|
+
console.log(`${colors.cyan('--json')}\tPrint machine-readable JSON in addition to tables.`);
|
|
146
|
+
console.log(`${colors.cyan('--filter <regex>')}\tFilter events by matching file path.`);
|
|
147
|
+
console.log(`${colors.cyan('--event <name>')}\tInclude only specified event names (repeatable).`);
|
|
148
|
+
console.log(`${colors.cyan('--minMs <n>')}\tIgnore events shorter than n ms (default 1).`);
|
|
149
|
+
console.log(`${colors.cyan('--baseDir <path>')}\tBase directory for relative display paths.`);
|
|
150
|
+
console.log(`${colors.cyan('--out <path>')}\tWrite JSON report to file (implies --json).`);
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(colors.dim('Example (auto profile): pnpm manager-cli ts-trace --target src/format-checker --top 30'));
|
|
153
|
+
console.log(colors.dim('Example (existing trace): pnpm manager-cli ts-trace --traceDir /tmp/ts-trace-foo --json --out /tmp/ts-report.json'));
|
|
154
|
+
console.log(colors.dim('Large traces: set NODE_OPTIONS="--max-old-space-size=4096" before running if needed.'));
|
|
155
|
+
}
|
|
156
|
+
function parsePositiveInteger(raw) {
|
|
157
|
+
if (!raw)
|
|
158
|
+
return undefined;
|
|
159
|
+
const parsed = Number(raw);
|
|
160
|
+
if (!Number.isFinite(parsed))
|
|
161
|
+
return undefined;
|
|
162
|
+
const floored = Math.floor(parsed);
|
|
163
|
+
if (floored <= 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
return floored;
|
|
166
|
+
}
|
|
167
|
+
function parseNonNegativeNumber(raw) {
|
|
168
|
+
if (!raw)
|
|
169
|
+
return undefined;
|
|
170
|
+
const parsed = Number(raw);
|
|
171
|
+
if (!Number.isFinite(parsed))
|
|
172
|
+
return undefined;
|
|
173
|
+
if (parsed < 0)
|
|
174
|
+
return undefined;
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
function normalizeEventNames(eventNames) {
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const normalized = [];
|
|
180
|
+
for (const value of eventNames) {
|
|
181
|
+
const trimmed = value.trim();
|
|
182
|
+
if (!trimmed || seen.has(trimmed))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(trimmed);
|
|
185
|
+
normalized.push(trimmed);
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
function ensureRegex(raw) {
|
|
190
|
+
try {
|
|
191
|
+
new RegExp(raw);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`Flag "--filter" received an invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|