@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.
@@ -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(/\/+$/, '');
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
- process.exit(1);
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
+ }