@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,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
+ }
@@ -0,0 +1,150 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { rootDir } from '../helper-cli/env.js';
6
+ import { colors, logGlobal } from '../utils/log.js';
7
+ import { run } from '../utils/run.js';
8
+ import { TraceReportError } from './report.js';
9
+ const PROJECT_CONFIG_CANDIDATES = [
10
+ 'tsconfig.typecheck.json',
11
+ 'tsconfig.base.json',
12
+ 'tsconfig.json',
13
+ ];
14
+ const TEMP_CONFIG_DIR = path.join(rootDir, '.manager-tmp', 'ts-trace');
15
+ const TRACE_FILE_NAME = 'trace.json';
16
+ const TYPES_FILE_NAME = 'types.json';
17
+ export async function resolveTraceProfileTargetFromPath(rawPath) {
18
+ const trimmed = rawPath.trim();
19
+ if (!trimmed) {
20
+ throw new TraceReportError('Target path cannot be empty.', 2);
21
+ }
22
+ const absolutePath = path.isAbsolute(trimmed)
23
+ ? path.resolve(trimmed)
24
+ : path.resolve(rootDir, trimmed);
25
+ const stats = await stat(absolutePath).catch(() => undefined);
26
+ if (!stats) {
27
+ throw new TraceReportError(`Target path not found: ${absolutePath}`, 2);
28
+ }
29
+ if (!stats.isFile() && !stats.isDirectory()) {
30
+ throw new TraceReportError(`Target must be a file or directory: ${absolutePath}`, 2);
31
+ }
32
+ if (!isPathInWorkspace(absolutePath)) {
33
+ throw new TraceReportError(`Target must be inside this workspace: ${absolutePath}`, 2);
34
+ }
35
+ const kind = stats.isDirectory() ? 'directory' : 'file';
36
+ return {
37
+ absolutePath,
38
+ kind,
39
+ label: formatWorkspaceRelativePath(absolutePath),
40
+ };
41
+ }
42
+ export async function generateTypeScriptTraceForTarget(options) {
43
+ const projectPath = await resolveProjectConfigPath(options.projectPathSetting);
44
+ const traceName = buildTraceName(options.target.label);
45
+ const traceDir = path.join(tmpdir(), `ts-trace-${traceName}-${Date.now()}`);
46
+ const tempConfigPath = await writeScopedTraceConfig(options.target, projectPath);
47
+ try {
48
+ logGlobal(`Generating trace for ${options.target.label} using ${formatWorkspaceRelativePath(projectPath)}`, colors.cyan);
49
+ await run('pnpm', [
50
+ 'tsc',
51
+ '-p',
52
+ tempConfigPath,
53
+ '--incremental',
54
+ 'false',
55
+ '--generateTrace',
56
+ traceDir,
57
+ '--pretty',
58
+ 'false',
59
+ ]);
60
+ }
61
+ catch (error) {
62
+ const hasArtifacts = await traceArtifactsExist(traceDir);
63
+ if (!hasArtifacts) {
64
+ throw new TraceReportError(`Failed to generate a trace for ${options.target.label}. ${error instanceof Error ? error.message : String(error)}`, 1);
65
+ }
66
+ logGlobal('TypeScript reported errors but emitted trace artifacts; continuing analysis.', colors.yellow);
67
+ }
68
+ finally {
69
+ await rm(tempConfigPath, { force: true }).catch(() => { });
70
+ }
71
+ const hasArtifacts = await traceArtifactsExist(traceDir);
72
+ if (!hasArtifacts) {
73
+ throw new TraceReportError(`Trace generation finished without ${TRACE_FILE_NAME} and ${TYPES_FILE_NAME}: ${traceDir}`, 1);
74
+ }
75
+ return { traceDir, projectPath };
76
+ }
77
+ export async function resolveProjectConfigPath(projectPathSetting) {
78
+ const configured = projectPathSetting?.trim();
79
+ if (configured && configured.toLowerCase() !== 'auto') {
80
+ const resolved = path.isAbsolute(configured)
81
+ ? path.resolve(configured)
82
+ : path.resolve(rootDir, configured);
83
+ const stats = await stat(resolved).catch(() => undefined);
84
+ if (!stats?.isFile()) {
85
+ throw new TraceReportError(`Configured TypeScript project not found: ${resolved}`, 2);
86
+ }
87
+ return resolved;
88
+ }
89
+ for (const candidate of PROJECT_CONFIG_CANDIDATES) {
90
+ const candidatePath = path.join(rootDir, candidate);
91
+ if (!existsSync(candidatePath))
92
+ continue;
93
+ return candidatePath;
94
+ }
95
+ throw new TraceReportError(`Could not find a TypeScript project config. Checked: ${PROJECT_CONFIG_CANDIDATES.join(', ')}`, 2);
96
+ }
97
+ async function writeScopedTraceConfig(target, projectPath) {
98
+ await mkdir(TEMP_CONFIG_DIR, { recursive: true });
99
+ const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
100
+ const tempConfigPath = path.join(TEMP_CONFIG_DIR, `tsconfig.${suffix}.json`);
101
+ const extendsPath = toPosixPath(path.relative(path.dirname(tempConfigPath), projectPath));
102
+ const relativeTargetPath = toPosixPath(path.relative(path.dirname(tempConfigPath), target.absolutePath));
103
+ const includePattern = buildIncludePattern(relativeTargetPath, target.kind);
104
+ const payload = {
105
+ extends: extendsPath || './tsconfig.json',
106
+ include: [includePattern],
107
+ compilerOptions: {
108
+ noEmit: true,
109
+ },
110
+ };
111
+ await writeFile(tempConfigPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
112
+ return tempConfigPath;
113
+ }
114
+ function buildIncludePattern(relativePath, kind) {
115
+ const normalized = relativePath.replace(/\\/g, '/');
116
+ if (!normalized || normalized === '.') {
117
+ return kind === 'directory' ? '**/*' : normalized;
118
+ }
119
+ return kind === 'directory' ? `${normalized}/**/*` : normalized;
120
+ }
121
+ function buildTraceName(label) {
122
+ const sanitized = label
123
+ .replace(/\\/g, '/')
124
+ .replace(/[^a-zA-Z0-9/_-]+/g, '-')
125
+ .replace(/\/+/g, '-')
126
+ .replace(/-+/g, '-')
127
+ .replace(/^-|-$/g, '')
128
+ .toLowerCase();
129
+ return sanitized.slice(0, 48) || 'scope';
130
+ }
131
+ async function traceArtifactsExist(traceDir) {
132
+ const tracePath = path.join(traceDir, TRACE_FILE_NAME);
133
+ const typesPath = path.join(traceDir, TYPES_FILE_NAME);
134
+ const [traceStats, typesStats] = await Promise.all([
135
+ stat(tracePath).catch(() => undefined),
136
+ stat(typesPath).catch(() => undefined),
137
+ ]);
138
+ return Boolean(traceStats?.isFile() && typesStats?.isFile());
139
+ }
140
+ function formatWorkspaceRelativePath(targetPath) {
141
+ const relative = path.relative(rootDir, targetPath).replace(/\\/g, '/');
142
+ return relative || '.';
143
+ }
144
+ function isPathInWorkspace(targetPath) {
145
+ const relative = path.relative(rootDir, targetPath);
146
+ return Boolean(relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)));
147
+ }
148
+ function toPosixPath(value) {
149
+ return value.replace(/\\/g, '/');
150
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Self-check:
3
+ * 1) pnpm manager-cli
4
+ * 2) utilities -> typescript trace profiler -> profile file/folder
5
+ * 3) pick target path, wait for trace generation, review offenders
6
+ */
7
+ import path from 'node:path';
8
+ import { runHelperCli } from '../helper-cli.js';
9
+ import { colors } from '../utils/log.js';
10
+ import { parseTraceReportCliArgs, printTraceReportUsage } from './cli/options.js';
11
+ import { promptTraceReportSettings } from './cli/prompts.js';
12
+ import { loadTraceReportSettings } from './config.js';
13
+ import { promptTraceDirectorySelection, promptTraceProfileTargetSelection, } from './picker.js';
14
+ import { generateTypeScriptTraceForTarget, resolveTraceProfileTargetFromPath, } from './generate.js';
15
+ import { TraceReportError, runTypeScriptTraceReport } from './report.js';
16
+ export async function runTypeScriptTraceProfiler() {
17
+ console.log(colors.cyan('Loading defaults from .vscode/settings.json (manager.tsTraceProfiler)'));
18
+ let settings = await loadTraceReportSettings();
19
+ while (true) {
20
+ let lastAction;
21
+ const ran = await runHelperCli({
22
+ title: 'TypeScript trace profiler',
23
+ scripts: [
24
+ {
25
+ name: 'Profile file/folder (auto trace)',
26
+ emoji: '⏱️',
27
+ description: traceSettingsSummary(settings),
28
+ handler: async () => {
29
+ try {
30
+ const target = await promptTraceProfileTargetSelection();
31
+ if (!target)
32
+ return;
33
+ const generated = await generateTypeScriptTraceForTarget({
34
+ target,
35
+ projectPathSetting: settings.projectPath,
36
+ });
37
+ console.log(colors.magenta(`Analyzing TypeScript trace for ${target.label}`));
38
+ console.log(colors.dim(`Trace output: ${generated.traceDir}`));
39
+ await runTypeScriptTraceReport(buildRunOptions(settings, generated.traceDir));
40
+ lastAction = 'run';
41
+ }
42
+ catch (error) {
43
+ logTraceError(error);
44
+ }
45
+ },
46
+ },
47
+ {
48
+ name: 'Analyze existing trace',
49
+ emoji: '🧾',
50
+ description: 'Pick an existing trace.json/types.json folder and report offenders',
51
+ handler: async () => {
52
+ try {
53
+ const selection = await promptTraceDirectorySelection();
54
+ if (!selection)
55
+ return;
56
+ console.log(colors.magenta(`Analyzing TypeScript trace from ${selection.label}`));
57
+ await runTypeScriptTraceReport(buildRunOptions(settings, selection.traceDir));
58
+ lastAction = 'run';
59
+ }
60
+ catch (error) {
61
+ logTraceError(error);
62
+ }
63
+ },
64
+ },
65
+ {
66
+ name: 'Adjust settings',
67
+ emoji: '⚙️',
68
+ description: traceSettingsSummary(settings),
69
+ handler: async () => {
70
+ settings = await promptTraceReportSettings(settings);
71
+ lastAction = 'configure';
72
+ },
73
+ },
74
+ ],
75
+ argv: [],
76
+ });
77
+ if (!ran)
78
+ return;
79
+ if (lastAction === 'run')
80
+ return;
81
+ }
82
+ }
83
+ export async function runTypeScriptTraceReportCli(argv) {
84
+ const defaults = await loadTraceReportSettings();
85
+ const parsed = parseTraceReportCliArgs(argv, defaults);
86
+ if (parsed.help) {
87
+ printTraceReportUsage();
88
+ return;
89
+ }
90
+ if (!parsed.command)
91
+ return;
92
+ if (parsed.command.mode === 'existing') {
93
+ await runTypeScriptTraceReport(parsed.command.report);
94
+ return;
95
+ }
96
+ const target = await resolveTraceProfileTargetFromPath(parsed.command.targetPath);
97
+ const generated = await generateTypeScriptTraceForTarget({
98
+ target,
99
+ projectPathSetting: parsed.command.projectPath,
100
+ });
101
+ await runTypeScriptTraceReport({
102
+ traceDir: generated.traceDir,
103
+ ...parsed.command.report,
104
+ });
105
+ }
106
+ function buildRunOptions(settings, traceDir) {
107
+ return {
108
+ traceDir,
109
+ top: settings.top,
110
+ minMs: settings.minMs,
111
+ json: settings.includeJson || Boolean(settings.outPath),
112
+ filterRegex: settings.filterRegex || undefined,
113
+ eventNames: settings.eventNames,
114
+ baseDir: path.resolve(settings.baseDir || process.cwd()),
115
+ outPath: settings.outPath || undefined,
116
+ };
117
+ }
118
+ function traceSettingsSummary(settings) {
119
+ const events = settings.eventNames.length
120
+ ? settings.eventNames.join(',')
121
+ : 'all';
122
+ const filter = settings.filterRegex ? settings.filterRegex : 'none';
123
+ const jsonLabel = settings.includeJson || settings.outPath ? 'on' : 'off';
124
+ const project = settings.projectPath || 'auto';
125
+ return `top=${settings.top} minMs=${settings.minMs} events=${events} filter=${filter} json=${jsonLabel} project=${project}`;
126
+ }
127
+ function logTraceError(error) {
128
+ if (error instanceof TraceReportError) {
129
+ console.log(colors.red(error.message));
130
+ return;
131
+ }
132
+ if (error instanceof Error) {
133
+ console.log(colors.red(error.message));
134
+ return;
135
+ }
136
+ console.log(colors.red(String(error)));
137
+ }