@densetsuuu/docteur 0.1.1-beta

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.
Files changed (47) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +155 -0
  3. package/build/index.d.ts +23 -0
  4. package/build/index.js +23 -0
  5. package/build/src/cli/commands/diagnose.d.ts +27 -0
  6. package/build/src/cli/commands/diagnose.js +67 -0
  7. package/build/src/cli/commands/xray.d.ts +7 -0
  8. package/build/src/cli/commands/xray.js +49 -0
  9. package/build/src/cli/main.d.ts +2 -0
  10. package/build/src/cli/main.js +30 -0
  11. package/build/src/profiler/collector.d.ts +16 -0
  12. package/build/src/profiler/collector.js +142 -0
  13. package/build/src/profiler/hooks.d.ts +27 -0
  14. package/build/src/profiler/hooks.js +45 -0
  15. package/build/src/profiler/loader.d.ts +1 -0
  16. package/build/src/profiler/loader.js +111 -0
  17. package/build/src/profiler/profiler.d.ts +9 -0
  18. package/build/src/profiler/profiler.js +117 -0
  19. package/build/src/profiler/registries/categories.d.ts +7 -0
  20. package/build/src/profiler/registries/categories.js +87 -0
  21. package/build/src/profiler/registries/index.d.ts +2 -0
  22. package/build/src/profiler/registries/index.js +2 -0
  23. package/build/src/profiler/registries/symbols.d.ts +42 -0
  24. package/build/src/profiler/registries/symbols.js +71 -0
  25. package/build/src/profiler/reporters/base_reporter.d.ts +19 -0
  26. package/build/src/profiler/reporters/base_reporter.js +9 -0
  27. package/build/src/profiler/reporters/console_reporter.d.ts +8 -0
  28. package/build/src/profiler/reporters/console_reporter.js +237 -0
  29. package/build/src/profiler/reporters/format.d.ts +62 -0
  30. package/build/src/profiler/reporters/format.js +84 -0
  31. package/build/src/profiler/reporters/tui_reporter.d.ts +8 -0
  32. package/build/src/profiler/reporters/tui_reporter.js +32 -0
  33. package/build/src/types.d.ts +167 -0
  34. package/build/src/types.js +9 -0
  35. package/build/src/xray/components/ListView.d.ts +9 -0
  36. package/build/src/xray/components/ListView.js +69 -0
  37. package/build/src/xray/components/ModuleView.d.ts +9 -0
  38. package/build/src/xray/components/ModuleView.js +144 -0
  39. package/build/src/xray/components/ProviderListView.d.ts +8 -0
  40. package/build/src/xray/components/ProviderListView.js +57 -0
  41. package/build/src/xray/components/ProviderView.d.ts +7 -0
  42. package/build/src/xray/components/ProviderView.js +35 -0
  43. package/build/src/xray/components/XRayApp.d.ts +7 -0
  44. package/build/src/xray/components/XRayApp.js +78 -0
  45. package/build/src/xray/tree.d.ts +22 -0
  46. package/build/src/xray/tree.js +85 -0
  47. package/package.json +110 -0
@@ -0,0 +1,111 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Profiler Loader
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Registers ESM hooks for module timing and subscribes to AdonisJS
7
+ | tracing channels for provider lifecycle timing.
8
+ |
9
+ */
10
+ import { tracingChannels } from '@adonisjs/application';
11
+ import { register } from 'node:module';
12
+ import { performance } from 'node:perf_hooks';
13
+ import { MessageChannel } from 'node:worker_threads';
14
+ // Module timing data from hooks
15
+ const parents = new Map();
16
+ const loadTimes = new Map();
17
+ // Provider timing data
18
+ const providerPhases = new Map();
19
+ const providerStarts = new Map();
20
+ const asyncCalls = new Set();
21
+ // Set up message channel for hooks
22
+ const { port1, port2 } = new MessageChannel();
23
+ port1.unref?.();
24
+ port1.on('message', (msg) => {
25
+ if (msg.type !== 'batch' || !msg.messages)
26
+ return;
27
+ for (const m of msg.messages) {
28
+ if (m.type === 'parent')
29
+ parents.set(m.child, m.parent);
30
+ else if (m.type === 'timing')
31
+ loadTimes.set(m.url, m.loadTime);
32
+ }
33
+ });
34
+ register('./hooks.js', {
35
+ parentURL: import.meta.url,
36
+ data: { port: port2 },
37
+ transferList: [port2],
38
+ });
39
+ // Subscribe to provider lifecycle phases
40
+ // For async methods: start -> end -> asyncStart -> asyncEnd (we wait for asyncEnd)
41
+ // For sync methods: start -> end (we record on end, but defer to check if async fires)
42
+ function subscribePhase(channel, phase) {
43
+ const getName = (msg) => msg.provider.constructor.name;
44
+ channel.subscribe({
45
+ start(msg) {
46
+ providerStarts.set(`${getName(msg)}:${phase}`, performance.now());
47
+ },
48
+ end(msg) {
49
+ const name = getName(msg);
50
+ const key = `${name}:${phase}`;
51
+ const endTime = performance.now();
52
+ // Defer to check if this becomes async (asyncStart fires before our setTimeout)
53
+ setTimeout(() => {
54
+ if (asyncCalls.has(key))
55
+ return;
56
+ const start = providerStarts.get(key);
57
+ if (start !== undefined) {
58
+ const phases = providerPhases.get(name) || {};
59
+ phases[phase] = endTime - start;
60
+ providerPhases.set(name, phases);
61
+ providerStarts.delete(key);
62
+ }
63
+ }, 0);
64
+ },
65
+ asyncStart(msg) {
66
+ asyncCalls.add(`${getName(msg)}:${phase}`);
67
+ },
68
+ asyncEnd(msg) {
69
+ const name = getName(msg);
70
+ const key = `${name}:${phase}`;
71
+ const start = providerStarts.get(key);
72
+ if (start !== undefined) {
73
+ const phases = providerPhases.get(name) || {};
74
+ phases[phase] = performance.now() - start;
75
+ providerPhases.set(name, phases);
76
+ providerStarts.delete(key);
77
+ }
78
+ asyncCalls.delete(key);
79
+ },
80
+ error() { },
81
+ });
82
+ }
83
+ subscribePhase(tracingChannels.providerRegister, 'register');
84
+ subscribePhase(tracingChannels.providerBoot, 'boot');
85
+ subscribePhase(tracingChannels.providerStart, 'start');
86
+ subscribePhase(tracingChannels.providerReady, 'ready');
87
+ subscribePhase(tracingChannels.providerShutdown, 'shutdown');
88
+ // Send results to parent process when requested
89
+ if (process.send) {
90
+ process.on('message', (msg) => {
91
+ if (msg.type !== 'getResults')
92
+ return;
93
+ const providers = [...providerPhases.entries()].map(([name, t]) => ({
94
+ name,
95
+ registerTime: t.register || 0,
96
+ bootTime: t.boot || 0,
97
+ startTime: t.start || 0,
98
+ readyTime: t.ready || 0,
99
+ shutdownTime: t.shutdown || 0,
100
+ totalTime: (t.register || 0) + (t.boot || 0) + (t.start || 0) + (t.ready || 0),
101
+ }));
102
+ process.send({
103
+ type: 'results',
104
+ data: {
105
+ loadTimes: Object.fromEntries(loadTimes),
106
+ parents: Object.fromEntries(parents),
107
+ providers,
108
+ },
109
+ });
110
+ });
111
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProfileResult } from '../types.js';
2
+ export interface ProfileOptions {
3
+ entryPoint?: string;
4
+ suppressOutput?: boolean;
5
+ }
6
+ export declare function findLoaderPath(): string;
7
+ export declare function findEntryPoint(cwd: string, entry?: string): string;
8
+ export declare function isAdonisProject(cwd: string): boolean;
9
+ export declare function profile(cwd: string, options?: ProfileOptions): Promise<ProfileResult>;
@@ -0,0 +1,117 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Core Profiler
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Standalone profiler that doesn't depend on AdonisJS Ace framework.
7
+ | Used by both the CLI and the ace commands.
8
+ |
9
+ */
10
+ import { fork } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { join } from 'node:path';
13
+ import { existsSync } from 'node:fs';
14
+ import { ProfileCollector } from './collector.js';
15
+ import { simplifyUrl } from './reporters/format.js';
16
+ const TIMEOUT_MS = 30_000;
17
+ export function findLoaderPath() {
18
+ return fileURLToPath(import.meta.resolve('@densetsuuu/docteur/profiler/loader'));
19
+ }
20
+ export function findEntryPoint(cwd, entry) {
21
+ const entryPath = join(cwd, entry || 'bin/server.ts');
22
+ if (!existsSync(entryPath)) {
23
+ throw new Error(`Entry point not found: ${entryPath}`);
24
+ }
25
+ return entryPath;
26
+ }
27
+ export function isAdonisProject(cwd) {
28
+ return existsSync(join(cwd, 'adonisrc.ts')) || existsSync(join(cwd, '.adonisrc.ts'));
29
+ }
30
+ export async function profile(cwd, options = {}) {
31
+ const loaderPath = findLoaderPath();
32
+ const entryPoint = findEntryPoint(cwd, options.entryPoint);
33
+ const state = await runProfiledProcess(loaderPath, entryPoint, cwd, options);
34
+ return buildResults(state, cwd);
35
+ }
36
+ function runProfiledProcess(loaderPath, entryPoint, cwd, options) {
37
+ const suppressOutput = options.suppressOutput ?? false;
38
+ return new Promise((resolve, reject) => {
39
+ const state = {
40
+ providers: [],
41
+ loadTimes: new Map(),
42
+ parents: new Map(),
43
+ done: false,
44
+ };
45
+ const child = fork(entryPoint, [], {
46
+ cwd,
47
+ stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
48
+ execArgv: ['--import', loaderPath, '--import', '@poppinss/ts-exec', '--no-warnings'],
49
+ env: { ...process.env, DOCTEUR_PROFILING: 'true' },
50
+ });
51
+ const timeout = setTimeout(() => {
52
+ child.kill('SIGKILL');
53
+ reject(new Error(`Profiling timed out after ${TIMEOUT_MS / 1000} seconds`));
54
+ }, TIMEOUT_MS);
55
+ const complete = () => {
56
+ if (state.done)
57
+ return;
58
+ state.done = true;
59
+ clearTimeout(timeout);
60
+ child.kill('SIGTERM');
61
+ const forceKill = setTimeout(() => child.kill('SIGKILL'), 500);
62
+ child.once('exit', () => {
63
+ clearTimeout(forceKill);
64
+ resolve(state);
65
+ });
66
+ };
67
+ child.stdout?.on('data', (data) => {
68
+ const output = data.toString();
69
+ if (!suppressOutput)
70
+ process.stdout.write(output);
71
+ if (output.includes('started HTTP server')) {
72
+ child.send({ type: 'getResults' });
73
+ }
74
+ });
75
+ child.stderr?.on('data', (data) => {
76
+ if (!suppressOutput)
77
+ process.stderr.write(data);
78
+ });
79
+ child.on('message', (message) => {
80
+ const msg = message;
81
+ if (msg.isAdonisJS === true && msg.environment === 'web' && msg.duration) {
82
+ state.bootDuration = msg.duration;
83
+ return;
84
+ }
85
+ if (msg.type === 'results') {
86
+ const data = msg.data;
87
+ state.loadTimes = new Map(Object.entries(data.loadTimes));
88
+ state.parents = new Map(Object.entries(data.parents || {}));
89
+ state.providers = data.providers || [];
90
+ complete();
91
+ }
92
+ });
93
+ child.on('error', (err) => {
94
+ clearTimeout(timeout);
95
+ reject(err);
96
+ });
97
+ child.on('exit', (code) => {
98
+ if (code !== null && code !== 0 && code !== 137 && code !== 143 && !state.done) {
99
+ clearTimeout(timeout);
100
+ reject(new Error(`Process exited with code ${code}`));
101
+ }
102
+ });
103
+ });
104
+ }
105
+ function buildResults(state, cwd) {
106
+ const modules = [...state.loadTimes].map(([url, loadTime]) => ({
107
+ specifier: simplifyUrl(url, cwd),
108
+ resolvedUrl: url,
109
+ loadTime,
110
+ parentUrl: state.parents.get(url),
111
+ }));
112
+ const collector = new ProfileCollector(modules, state.providers);
113
+ const bootTimeMs = state.bootDuration
114
+ ? state.bootDuration[0] * 1000 + state.bootDuration[1] / 1_000_000
115
+ : 0;
116
+ return collector.collectResults(bootTimeMs);
117
+ }
@@ -0,0 +1,7 @@
1
+ import type { AppFileCategory } from '../../types.js';
2
+ export interface CategoryDefinition {
3
+ displayName: string;
4
+ icon: string;
5
+ patterns: string[];
6
+ }
7
+ export declare const categories: Record<AppFileCategory, CategoryDefinition>;
@@ -0,0 +1,87 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | App File Categories Registry
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Defines how app files are categorized based on path patterns,
7
+ | along with display names and icons for each category.
8
+ |
9
+ */
10
+ import { symbols } from './symbols.js';
11
+ export const categories = {
12
+ controller: {
13
+ displayName: 'Controllers',
14
+ icon: symbols.controller,
15
+ patterns: ['/controllers/', '_controller.'],
16
+ },
17
+ service: {
18
+ displayName: 'Services',
19
+ icon: symbols.service,
20
+ patterns: ['/services/', '_service.'],
21
+ },
22
+ model: {
23
+ displayName: 'Models',
24
+ icon: symbols.model,
25
+ patterns: ['/models/', '/model/'],
26
+ },
27
+ middleware: {
28
+ displayName: 'Middleware',
29
+ icon: symbols.middleware,
30
+ patterns: ['/middleware/', '_middleware.'],
31
+ },
32
+ validator: {
33
+ displayName: 'Validators',
34
+ icon: symbols.validator,
35
+ patterns: ['/validators/', '_validator.'],
36
+ },
37
+ exception: {
38
+ displayName: 'Exceptions',
39
+ icon: symbols.exception,
40
+ patterns: ['/exceptions/', '_exception.'],
41
+ },
42
+ event: {
43
+ displayName: 'Events',
44
+ icon: symbols.event,
45
+ patterns: ['/events/', '_event.'],
46
+ },
47
+ listener: {
48
+ displayName: 'Listeners',
49
+ icon: symbols.listener,
50
+ patterns: ['/listeners/', '_listener.'],
51
+ },
52
+ mailer: {
53
+ displayName: 'Mailers',
54
+ icon: symbols.mailer,
55
+ patterns: ['/mailers/', '_mailer.'],
56
+ },
57
+ policy: {
58
+ displayName: 'Policies',
59
+ icon: symbols.policy,
60
+ patterns: ['/policies/', '_policy.'],
61
+ },
62
+ command: {
63
+ displayName: 'Commands',
64
+ icon: symbols.command,
65
+ patterns: ['/commands/', '_command.'],
66
+ },
67
+ provider: {
68
+ displayName: 'Providers',
69
+ icon: symbols.provider,
70
+ patterns: ['/providers/', '_provider.'],
71
+ },
72
+ config: {
73
+ displayName: 'Config',
74
+ icon: symbols.config,
75
+ patterns: ['/config/'],
76
+ },
77
+ start: {
78
+ displayName: 'Start Files',
79
+ icon: symbols.start,
80
+ patterns: ['/start/'],
81
+ },
82
+ other: {
83
+ displayName: 'Other',
84
+ icon: symbols.file,
85
+ patterns: [],
86
+ },
87
+ };
@@ -0,0 +1,2 @@
1
+ export { categories, type CategoryDefinition } from './categories.js';
2
+ export { symbols, fileIcons, type SymbolKey } from './symbols.js';
@@ -0,0 +1,2 @@
1
+ export { categories } from './categories.js';
2
+ export { symbols, fileIcons } from './symbols.js';
@@ -0,0 +1,42 @@
1
+ export declare const symbols: {
2
+ readonly controller: "🎮";
3
+ readonly service: "âš™ī¸";
4
+ readonly model: "đŸ“Ļ";
5
+ readonly middleware: "🔗";
6
+ readonly validator: "✅";
7
+ readonly exception: "đŸ’Ĩ";
8
+ readonly event: "📡";
9
+ readonly listener: "👂";
10
+ readonly mailer: "📧";
11
+ readonly policy: "🔐";
12
+ readonly command: "âŒ¨ī¸";
13
+ readonly provider: "🔌";
14
+ readonly config: "âš™ī¸";
15
+ readonly start: "🚀";
16
+ readonly file: "📄";
17
+ readonly folder: "📁";
18
+ readonly checkmark: "✅";
19
+ readonly cross: "❌";
20
+ readonly warning: "âš ī¸";
21
+ readonly info: "â„šī¸";
22
+ readonly lightning: "⚡";
23
+ readonly turtle: "đŸĸ";
24
+ readonly package: "đŸ“Ļ";
25
+ readonly chart: "📊";
26
+ readonly lightbulb: "💡";
27
+ readonly stethoscope: "đŸŠē";
28
+ readonly barFull: "█";
29
+ readonly barEmpty: "░";
30
+ readonly barMedium: "▒";
31
+ readonly barLight: "▓";
32
+ readonly dash: "─";
33
+ readonly bullet: "â€ĸ";
34
+ readonly arrow: "→";
35
+ readonly arrowLeft: "←";
36
+ readonly arrowUp: "↑";
37
+ readonly arrowDown: "↓";
38
+ readonly sourcePackage: "ī’‡";
39
+ readonly sourceHome: "";
40
+ };
41
+ export declare const fileIcons: Record<string, string>;
42
+ export type SymbolKey = keyof typeof symbols;
@@ -0,0 +1,71 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Unicode Symbols Registry
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Centralized registry of Unicode symbols used throughout the UI.
7
+ | Access via: symbols.controller, symbols.checkmark, etc.
8
+ |
9
+ */
10
+ export const symbols = {
11
+ // App file categories
12
+ controller: '\uD83C\uDFAE', // 🎮
13
+ service: '\u2699\uFE0F', // âš™ī¸
14
+ model: '\uD83D\uDCE6', // đŸ“Ļ
15
+ middleware: '\uD83D\uDD17', // 🔗
16
+ validator: '\u2705', // ✅
17
+ exception: '\uD83D\uDCA5', // đŸ’Ĩ
18
+ event: '\uD83D\uDCE1', // 📡
19
+ listener: '\uD83D\uDC42', // 👂
20
+ mailer: '\uD83D\uDCE7', // 📧
21
+ policy: '\uD83D\uDD10', // 🔐
22
+ command: '\u2328\uFE0F', // âŒ¨ī¸
23
+ provider: '\uD83D\uDD0C', // 🔌
24
+ config: '\u2699\uFE0F', // âš™ī¸
25
+ start: '\uD83D\uDE80', // 🚀
26
+ file: '\uD83D\uDCC4', // 📄
27
+ folder: '\uD83D\uDCC1', // 📁
28
+ // Status & UI
29
+ checkmark: '\u2705', // ✅
30
+ cross: '\u274C', // ❌
31
+ warning: '\u26A0\uFE0F', // âš ī¸
32
+ info: '\u2139\uFE0F', // â„šī¸
33
+ lightning: '\u26A1', // ⚡
34
+ turtle: '\uD83D\uDC22', // đŸĸ
35
+ package: '\uD83D\uDCE6', // đŸ“Ļ
36
+ chart: '\uD83D\uDCCA', // 📊
37
+ lightbulb: '\uD83D\uDCA1', // 💡
38
+ stethoscope: '\uD83E\uDE7A', // đŸŠē
39
+ // Bars (for progress indicators)
40
+ barFull: '\u2588', // █
41
+ barEmpty: '\u2591', // ░
42
+ barMedium: '\u2592', // ▒
43
+ barLight: '\u2593', // ▓
44
+ // Misc
45
+ dash: '\u2500', // ─
46
+ bullet: '\u2022', // â€ĸ
47
+ arrow: '\u2192', // →
48
+ arrowLeft: '\u2190', // ←
49
+ arrowUp: '\u2191', // ↑
50
+ arrowDown: '\u2193', // ↓
51
+ // Nerd Font source icons (for TUI)
52
+ sourcePackage: '\uf487',
53
+ sourceHome: '\uf015',
54
+ };
55
+ // Nerd Font file icons by extension
56
+ export const fileIcons = {
57
+ ts: '\ue628',
58
+ tsx: '\ue628',
59
+ js: '\ue781',
60
+ jsx: '\ue781',
61
+ mjs: '\ue718',
62
+ cjs: '\ue718',
63
+ json: '\ue60b',
64
+ vue: '\ue6a0',
65
+ css: '\ue749',
66
+ scss: '\ue749',
67
+ sass: '\ue749',
68
+ html: '\ue736',
69
+ md: '\ue73e',
70
+ default: '\uf15b',
71
+ };
@@ -0,0 +1,19 @@
1
+ import type { ProfileResult, ResolvedConfig } from '../../types.js';
2
+ /**
3
+ * Context passed to reporters containing all data needed for rendering.
4
+ */
5
+ export interface ReportContext {
6
+ result: ProfileResult;
7
+ config: ResolvedConfig;
8
+ cwd: string;
9
+ }
10
+ /**
11
+ * Strategy interface for rendering profile reports.
12
+ * Implementations can render to console, TUI, file, etc.
13
+ */
14
+ export interface Reporter {
15
+ /**
16
+ * Renders the complete report using the provided context.
17
+ */
18
+ render(context: ReportContext): void | Promise<void>;
19
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Reporter Types
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Type definitions for the reporter strategy pattern.
7
+ |
8
+ */
9
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { ReportContext, Reporter } from './base_reporter.js';
2
+ export declare class ConsoleReporter implements Reporter {
3
+ #private;
4
+ /**
5
+ * Renders the complete report to console.
6
+ */
7
+ render(context: ReportContext): void;
8
+ }