@emeryld/manager 1.5.1 → 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.
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
+ }
@@ -0,0 +1,148 @@
1
+ import { stdin as input } from 'node:process';
2
+ import path from 'node:path';
3
+ import { colors } from '../../utils/log.js';
4
+ import { askLine, promptSingleKey } from '../../prompts.js';
5
+ import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
6
+ import { SETTING_DESCRIPTORS, formatValue, parseBooleanInput, parseInteractiveValue, parseNumericValue, parseStringList, validateTraceSettings, } from './settings.js';
7
+ import { rootDir } from '../../helper-cli/env.js';
8
+ const READY_PROMPT = colors.dim('Settings retained. Adjust values and confirm when ready.');
9
+ export async function promptTraceReportSettings(defaults) {
10
+ const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
11
+ let currentSettings = defaults;
12
+ while (true) {
13
+ const chosen = supportsInteractive
14
+ ? await promptSettingsInteractive(currentSettings)
15
+ : await promptSettingsSequential(currentSettings);
16
+ const confirmed = await confirmExecution();
17
+ if (confirmed)
18
+ return normalizePromptSettings(chosen);
19
+ currentSettings = normalizePromptSettings(chosen);
20
+ console.log(READY_PROMPT);
21
+ }
22
+ }
23
+ async function confirmExecution() {
24
+ const question = colors.cyan('Save these settings? (Y/n): ');
25
+ return promptSingleKey(question, (key) => {
26
+ const normalized = key.trim().toLowerCase();
27
+ if (!normalized || normalized === 'y' || normalized === 'yes')
28
+ return true;
29
+ if (normalized === 'n' || normalized === 'no')
30
+ return false;
31
+ return undefined;
32
+ });
33
+ }
34
+ async function promptSettingsInteractive(defaults) {
35
+ const descriptors = SETTING_DESCRIPTORS.map((descriptor) => ({
36
+ key: descriptor.key,
37
+ label: descriptor.label,
38
+ unit: descriptor.unit,
39
+ format: (value) => formatValue(value, descriptor),
40
+ parse: (buffer) => parseInteractiveValue(descriptor, buffer),
41
+ }));
42
+ return promptInteractiveSettings({
43
+ title: 'TypeScript trace profiler settings (type to edit values)',
44
+ descriptors,
45
+ initial: defaults,
46
+ instructions: [
47
+ 'Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.',
48
+ 'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
49
+ ],
50
+ validate: validateTraceSettings,
51
+ });
52
+ }
53
+ async function promptSettingsSequential(defaults) {
54
+ console.log(colors.dim('Enter values to override defaults or press Enter to keep them.'));
55
+ const top = await promptPositiveInteger('Top results', defaults.top);
56
+ const minMs = await promptNonNegativeNumber('Minimum span duration (ms)', defaults.minMs);
57
+ const filterRegex = await promptText('Path filter regex', defaults.filterRegex);
58
+ const eventNames = await promptEvents(defaults.eventNames);
59
+ const includeJson = await promptBoolean('Print JSON report in terminal', defaults.includeJson);
60
+ const outPath = await promptText('JSON output file path', defaults.outPath);
61
+ const baseDir = await promptPath('Display base directory', defaults.baseDir || rootDir);
62
+ const projectPath = await promptText('TypeScript project config path (blank = auto)', defaults.projectPath);
63
+ return {
64
+ top,
65
+ minMs,
66
+ filterRegex,
67
+ eventNames,
68
+ includeJson: includeJson || false,
69
+ outPath,
70
+ baseDir,
71
+ projectPath,
72
+ };
73
+ }
74
+ async function promptPositiveInteger(label, fallback) {
75
+ const question = colors.cyan(`${label} [default ${fallback}]: `);
76
+ while (true) {
77
+ const answer = await askLine(question);
78
+ if (!answer)
79
+ return fallback;
80
+ const parsed = parseNumericValue(answer);
81
+ if (parsed !== undefined && parsed > 0)
82
+ return Math.floor(parsed);
83
+ console.log(colors.yellow('Provide a positive number or leave blank.'));
84
+ }
85
+ }
86
+ async function promptNonNegativeNumber(label, fallback) {
87
+ const question = colors.cyan(`${label} [default ${fallback}]: `);
88
+ while (true) {
89
+ const answer = await askLine(question);
90
+ if (!answer)
91
+ return fallback;
92
+ const parsed = parseNumericValue(answer);
93
+ if (parsed !== undefined && parsed >= 0)
94
+ return parsed;
95
+ console.log(colors.yellow('Provide a non-negative number or leave blank.'));
96
+ }
97
+ }
98
+ async function promptBoolean(label, fallback) {
99
+ const question = colors.cyan(`${label} [default ${fallback ? 'yes' : 'no'}]: `);
100
+ while (true) {
101
+ const answer = (await askLine(question)).trim();
102
+ if (!answer)
103
+ return fallback;
104
+ const parsed = parseBooleanInput(answer);
105
+ if (parsed.error) {
106
+ console.log(colors.yellow(parsed.error));
107
+ continue;
108
+ }
109
+ return parsed.value;
110
+ }
111
+ }
112
+ async function promptText(label, fallback) {
113
+ const question = colors.cyan(`${label} [default ${fallback || '(none)'}]: `);
114
+ const answer = await askLine(question);
115
+ if (!answer)
116
+ return fallback;
117
+ return answer.trim();
118
+ }
119
+ async function promptEvents(fallback) {
120
+ const fallbackLabel = fallback.length ? fallback.join(', ') : '(all events)';
121
+ const question = colors.cyan(`Event names [default ${fallbackLabel}]: `);
122
+ const answer = await askLine(question);
123
+ if (!answer)
124
+ return fallback;
125
+ return parseStringList(answer);
126
+ }
127
+ async function promptPath(label, fallback) {
128
+ const question = colors.cyan(`${label} [default ${fallback}]: `);
129
+ const answer = await askLine(question);
130
+ if (!answer)
131
+ return fallback;
132
+ const trimmed = answer.trim();
133
+ if (!trimmed)
134
+ return fallback;
135
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(rootDir, trimmed);
136
+ }
137
+ function normalizePromptSettings(settings) {
138
+ return {
139
+ ...settings,
140
+ top: Math.max(1, Math.floor(settings.top)),
141
+ minMs: Math.max(0, settings.minMs),
142
+ filterRegex: settings.filterRegex.trim(),
143
+ eventNames: settings.eventNames.map((name) => name.trim()).filter(Boolean),
144
+ outPath: settings.outPath.trim(),
145
+ baseDir: settings.baseDir.trim() || rootDir,
146
+ projectPath: settings.projectPath.trim(),
147
+ };
148
+ }
@@ -0,0 +1,154 @@
1
+ export const SETTING_DESCRIPTORS = [
2
+ {
3
+ key: 'top',
4
+ label: 'Top results',
5
+ unit: 'entries',
6
+ type: 'number',
7
+ },
8
+ {
9
+ key: 'minMs',
10
+ label: 'Minimum span duration',
11
+ unit: 'milliseconds',
12
+ type: 'number',
13
+ allowZero: true,
14
+ },
15
+ {
16
+ key: 'filterRegex',
17
+ label: 'Path filter regex',
18
+ unit: 'blank = include all',
19
+ type: 'text',
20
+ },
21
+ {
22
+ key: 'eventNames',
23
+ label: 'Event names',
24
+ unit: 'comma-separated, blank = all',
25
+ type: 'list',
26
+ },
27
+ {
28
+ key: 'includeJson',
29
+ label: 'Print JSON in terminal',
30
+ type: 'boolean',
31
+ },
32
+ {
33
+ key: 'outPath',
34
+ label: 'JSON output path',
35
+ unit: 'blank = no file output',
36
+ type: 'text',
37
+ },
38
+ {
39
+ key: 'baseDir',
40
+ label: 'Display base directory',
41
+ type: 'text',
42
+ },
43
+ {
44
+ key: 'projectPath',
45
+ label: 'TypeScript project config',
46
+ unit: 'blank = auto-detect',
47
+ type: 'text',
48
+ },
49
+ ];
50
+ export function formatValue(value, descriptor) {
51
+ if (descriptor.type === 'boolean') {
52
+ return value ? 'true' : 'false';
53
+ }
54
+ if (descriptor.type === 'list') {
55
+ const list = value;
56
+ return list.length ? list.join(', ') : 'all events';
57
+ }
58
+ if (descriptor.type === 'text') {
59
+ const text = `${value}`.trim();
60
+ return text || '(none)';
61
+ }
62
+ return `${value}`;
63
+ }
64
+ export function parseInteractiveValue(descriptor, buffer) {
65
+ const trimmed = buffer.trim();
66
+ if (trimmed === '') {
67
+ if (descriptor.type === 'list')
68
+ return { value: [] };
69
+ if (descriptor.type === 'text')
70
+ return { value: '' };
71
+ return { value: undefined };
72
+ }
73
+ if (descriptor.type === 'number') {
74
+ const parsed = parseNumericValue(trimmed);
75
+ if (parsed === undefined) {
76
+ const rangeLabel = descriptor.allowZero ? 'non-negative' : 'positive';
77
+ return { error: `${descriptor.label} requires a ${rangeLabel} number.` };
78
+ }
79
+ if (!descriptor.allowZero && parsed <= 0) {
80
+ return { error: `${descriptor.label} requires a positive number.` };
81
+ }
82
+ if (descriptor.allowZero && parsed < 0) {
83
+ return { error: `${descriptor.label} must be non-negative.` };
84
+ }
85
+ if (descriptor.key === 'top') {
86
+ return { value: Math.floor(parsed) };
87
+ }
88
+ return { value: parsed };
89
+ }
90
+ if (descriptor.type === 'boolean') {
91
+ const parsed = parseBooleanInput(trimmed);
92
+ if (parsed.error)
93
+ return { error: parsed.error };
94
+ return { value: parsed.value };
95
+ }
96
+ if (descriptor.type === 'list') {
97
+ const events = parseStringList(trimmed);
98
+ return {
99
+ value: events,
100
+ };
101
+ }
102
+ if (descriptor.key === 'filterRegex') {
103
+ try {
104
+ // Validate early so mistakes are caught inside the settings page.
105
+ new RegExp(trimmed);
106
+ }
107
+ catch (error) {
108
+ return {
109
+ error: error instanceof Error ? error.message : 'Invalid regex pattern.',
110
+ };
111
+ }
112
+ }
113
+ return { value: trimmed };
114
+ }
115
+ export function validateTraceSettings(settings) {
116
+ if (!Number.isFinite(settings.top) || settings.top <= 0) {
117
+ return 'Top results must be a positive number.';
118
+ }
119
+ if (!Number.isFinite(settings.minMs) || settings.minMs < 0) {
120
+ return 'Minimum span duration must be non-negative.';
121
+ }
122
+ if (settings.filterRegex) {
123
+ try {
124
+ new RegExp(settings.filterRegex);
125
+ }
126
+ catch (error) {
127
+ return `Path filter regex is invalid: ${error instanceof Error ? error.message : String(error)}`;
128
+ }
129
+ }
130
+ if (!settings.baseDir.trim()) {
131
+ return 'Display base directory cannot be empty.';
132
+ }
133
+ return undefined;
134
+ }
135
+ export function parseNumericValue(raw) {
136
+ const parsed = Number(raw);
137
+ if (!Number.isFinite(parsed))
138
+ return undefined;
139
+ return parsed;
140
+ }
141
+ export function parseBooleanInput(raw) {
142
+ const normalized = raw.trim().toLowerCase();
143
+ if (['yes', 'y', 'true', '1'].includes(normalized))
144
+ return { value: true };
145
+ if (['no', 'n', 'false', '0'].includes(normalized))
146
+ return { value: false };
147
+ return { error: 'Provide "true" or "false".' };
148
+ }
149
+ export function parseStringList(raw) {
150
+ return raw
151
+ .split(',')
152
+ .map((item) => item.trim())
153
+ .filter(Boolean);
154
+ }
@@ -0,0 +1,100 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { rootDir } from '../helper-cli/env.js';
5
+ const SETTINGS_SECTION = 'manager.tsTraceProfiler';
6
+ const workspaceSettingsPath = path.join(rootDir, '.vscode', 'settings.json');
7
+ export const DEFAULT_TRACE_REPORT_SETTINGS = {
8
+ top: 20,
9
+ minMs: 1,
10
+ filterRegex: '',
11
+ eventNames: [],
12
+ includeJson: false,
13
+ outPath: '',
14
+ baseDir: rootDir,
15
+ projectPath: '',
16
+ };
17
+ export async function loadTraceReportSettings() {
18
+ if (!existsSync(workspaceSettingsPath)) {
19
+ return DEFAULT_TRACE_REPORT_SETTINGS;
20
+ }
21
+ try {
22
+ const raw = await readFile(workspaceSettingsPath, 'utf8');
23
+ const json = JSON.parse(raw);
24
+ const section = json[SETTINGS_SECTION];
25
+ if (!section || typeof section !== 'object') {
26
+ return DEFAULT_TRACE_REPORT_SETTINGS;
27
+ }
28
+ const record = section;
29
+ return {
30
+ top: coercePositiveInteger(record.top, DEFAULT_TRACE_REPORT_SETTINGS.top),
31
+ minMs: coerceNonNegativeNumber(record.minMs, DEFAULT_TRACE_REPORT_SETTINGS.minMs),
32
+ filterRegex: coerceString(record.filterRegex, DEFAULT_TRACE_REPORT_SETTINGS.filterRegex),
33
+ eventNames: coerceStringArray(record.eventNames, DEFAULT_TRACE_REPORT_SETTINGS.eventNames),
34
+ includeJson: coerceBoolean(record.includeJson, DEFAULT_TRACE_REPORT_SETTINGS.includeJson),
35
+ outPath: coerceString(record.outPath, DEFAULT_TRACE_REPORT_SETTINGS.outPath),
36
+ baseDir: coerceBaseDir(record.baseDir, DEFAULT_TRACE_REPORT_SETTINGS.baseDir),
37
+ projectPath: coerceString(record.projectPath, DEFAULT_TRACE_REPORT_SETTINGS.projectPath),
38
+ };
39
+ }
40
+ catch {
41
+ return DEFAULT_TRACE_REPORT_SETTINGS;
42
+ }
43
+ }
44
+ function coercePositiveInteger(value, fallback) {
45
+ const parsed = Number(value);
46
+ if (!Number.isFinite(parsed))
47
+ return fallback;
48
+ const floored = Math.floor(parsed);
49
+ if (floored <= 0)
50
+ return fallback;
51
+ return floored;
52
+ }
53
+ function coerceNonNegativeNumber(value, fallback) {
54
+ const parsed = Number(value);
55
+ if (!Number.isFinite(parsed))
56
+ return fallback;
57
+ if (parsed < 0)
58
+ return fallback;
59
+ return parsed;
60
+ }
61
+ function coerceString(value, fallback) {
62
+ if (typeof value !== 'string')
63
+ return fallback;
64
+ return value.trim();
65
+ }
66
+ function coerceStringArray(value, fallback) {
67
+ if (Array.isArray(value)) {
68
+ const parsed = value
69
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
70
+ .filter(Boolean);
71
+ return parsed;
72
+ }
73
+ if (typeof value === 'string') {
74
+ return value
75
+ .split(',')
76
+ .map((item) => item.trim())
77
+ .filter(Boolean);
78
+ }
79
+ return fallback;
80
+ }
81
+ function coerceBoolean(value, fallback) {
82
+ if (typeof value === 'boolean')
83
+ return value;
84
+ if (typeof value === 'string') {
85
+ const normalized = value.trim().toLowerCase();
86
+ if (normalized === 'true')
87
+ return true;
88
+ if (normalized === 'false')
89
+ return false;
90
+ }
91
+ return fallback;
92
+ }
93
+ function coerceBaseDir(value, fallback) {
94
+ if (typeof value !== 'string')
95
+ return fallback;
96
+ const trimmed = value.trim();
97
+ if (!trimmed)
98
+ return fallback;
99
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(rootDir, trimmed);
100
+ }