create-nexu 1.0.0

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 (73) hide show
  1. package/README.md +149 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +603 -0
  4. package/package.json +41 -0
  5. package/templates/default/.changeset/config.json +11 -0
  6. package/templates/default/.eslintignore +13 -0
  7. package/templates/default/.eslintrc.js +66 -0
  8. package/templates/default/.github/actions/quality/action.yml +53 -0
  9. package/templates/default/.github/dependabot.yml +51 -0
  10. package/templates/default/.github/workflows/deploy-dev.yml +83 -0
  11. package/templates/default/.github/workflows/deploy-prod.yml +83 -0
  12. package/templates/default/.github/workflows/deploy-rec.yml +83 -0
  13. package/templates/default/.husky/commit-msg +1 -0
  14. package/templates/default/.husky/pre-commit +1 -0
  15. package/templates/default/.prettierignore +7 -0
  16. package/templates/default/.prettierrc +19 -0
  17. package/templates/default/.vscode/extensions.json +14 -0
  18. package/templates/default/.vscode/settings.json +36 -0
  19. package/templates/default/apps/.gitkeep +0 -0
  20. package/templates/default/commitlint.config.js +26 -0
  21. package/templates/default/docker/docker-compose.dev.yml +49 -0
  22. package/templates/default/docker/docker-compose.prod.yml +64 -0
  23. package/templates/default/docker/docker-compose.yml +5 -0
  24. package/templates/default/package.json +56 -0
  25. package/templates/default/packages/cache/package.json +26 -0
  26. package/templates/default/packages/cache/src/index.ts +137 -0
  27. package/templates/default/packages/cache/tsconfig.json +9 -0
  28. package/templates/default/packages/cache/tsup.config.ts +9 -0
  29. package/templates/default/packages/config/eslint/index.js +20 -0
  30. package/templates/default/packages/config/package.json +9 -0
  31. package/templates/default/packages/config/typescript/base.json +26 -0
  32. package/templates/default/packages/constants/package.json +26 -0
  33. package/templates/default/packages/constants/src/index.ts +121 -0
  34. package/templates/default/packages/constants/tsconfig.json +9 -0
  35. package/templates/default/packages/constants/tsup.config.ts +9 -0
  36. package/templates/default/packages/logger/package.json +27 -0
  37. package/templates/default/packages/logger/src/index.ts +197 -0
  38. package/templates/default/packages/logger/tsconfig.json +11 -0
  39. package/templates/default/packages/logger/tsup.config.ts +9 -0
  40. package/templates/default/packages/result/package.json +26 -0
  41. package/templates/default/packages/result/src/index.ts +142 -0
  42. package/templates/default/packages/result/tsconfig.json +9 -0
  43. package/templates/default/packages/result/tsup.config.ts +9 -0
  44. package/templates/default/packages/types/package.json +26 -0
  45. package/templates/default/packages/types/src/index.ts +78 -0
  46. package/templates/default/packages/types/tsconfig.json +9 -0
  47. package/templates/default/packages/types/tsup.config.ts +10 -0
  48. package/templates/default/packages/ui/package.json +38 -0
  49. package/templates/default/packages/ui/src/components/Button.tsx +65 -0
  50. package/templates/default/packages/ui/src/components/Card.tsx +90 -0
  51. package/templates/default/packages/ui/src/components/Input.tsx +51 -0
  52. package/templates/default/packages/ui/src/index.ts +15 -0
  53. package/templates/default/packages/ui/tsconfig.json +11 -0
  54. package/templates/default/packages/ui/tsup.config.ts +11 -0
  55. package/templates/default/packages/utils/package.json +30 -0
  56. package/templates/default/packages/utils/src/index.test.ts +130 -0
  57. package/templates/default/packages/utils/src/index.ts +154 -0
  58. package/templates/default/packages/utils/tsconfig.json +10 -0
  59. package/templates/default/packages/utils/tsup.config.ts +10 -0
  60. package/templates/default/pnpm-workspace.yaml +3 -0
  61. package/templates/default/scripts/deploy.sh +25 -0
  62. package/templates/default/scripts/generate-app.sh +166 -0
  63. package/templates/default/scripts/publish-cli.sh +54 -0
  64. package/templates/default/scripts/setup.sh +70 -0
  65. package/templates/default/services/.env.example +16 -0
  66. package/templates/default/services/docker-compose.yml +207 -0
  67. package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
  68. package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
  69. package/templates/default/services/postgres/init/.gitkeep +2 -0
  70. package/templates/default/services/prometheus/prometheus.yml +13 -0
  71. package/templates/default/tsconfig.json +27 -0
  72. package/templates/default/turbo.json +40 -0
  73. package/templates/default/vitest.config.ts +15 -0
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "1.0.0",
4
+ "packageManager": "pnpm@9.0.0",
5
+ "scripts": {
6
+ "build": "turbo build",
7
+ "dev": "turbo dev",
8
+ "lint": "turbo lint",
9
+ "lint:fix": "turbo lint:fix",
10
+ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
11
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
12
+ "test": "turbo test",
13
+ "test:coverage": "turbo test:coverage",
14
+ "typecheck": "turbo typecheck",
15
+ "clean": "turbo clean && rm -rf node_modules",
16
+ "prepare": "husky install",
17
+ "docker:dev": "docker-compose -f docker/docker-compose.dev.yml up",
18
+ "docker:build": "docker-compose -f docker/docker-compose.prod.yml build",
19
+ "docker:prod": "docker-compose -f docker/docker-compose.prod.yml up -d",
20
+ "generate:app": "./scripts/generate-app.sh",
21
+ "generate:template": "./scripts/generate-template.sh",
22
+ "publish:cli": "./scripts/publish-cli.sh",
23
+ "changeset": "changeset",
24
+ "version-packages": "changeset version",
25
+ "release": "changeset publish"
26
+ },
27
+ "devDependencies": {
28
+ "@changesets/cli": "^2.27.0",
29
+ "@commitlint/cli": "^19.0.0",
30
+ "@commitlint/config-conventional": "^19.0.0",
31
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
32
+ "@typescript-eslint/parser": "^7.0.0",
33
+ "@vitest/coverage-v8": "^2.0.0",
34
+ "@vitest/ui": "^2.0.0",
35
+ "eslint": "^8.57.0",
36
+ "eslint-config-prettier": "^9.1.0",
37
+ "eslint-plugin-import": "^2.29.0",
38
+ "eslint-plugin-unused-imports": "^3.1.0",
39
+ "husky": "^9.0.0",
40
+ "lint-staged": "^15.0.0",
41
+ "prettier": "^3.2.0",
42
+ "prettier-plugin-tailwindcss": "^0.5.0",
43
+ "turbo": "^2.0.0",
44
+ "typescript": "^5.4.0",
45
+ "vitest": "^2.0.0"
46
+ },
47
+ "lint-staged": {
48
+ "*.{ts,tsx,js,jsx}": [
49
+ "eslint --fix",
50
+ "prettier --write"
51
+ ],
52
+ "*.{json,md,yml,yaml}": [
53
+ "prettier --write"
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@repo/cache",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "lint": "eslint src/",
19
+ "lint:fix": "eslint src/ --fix",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.4.0"
25
+ }
26
+ }
@@ -0,0 +1,137 @@
1
+ export interface CacheOptions {
2
+ ttl?: number; // Time to live in milliseconds
3
+ maxSize?: number; // Maximum number of items
4
+ }
5
+
6
+ interface CacheEntry<T> {
7
+ value: T;
8
+ expiresAt?: number;
9
+ }
10
+
11
+ export class Cache<T = unknown> {
12
+ private store = new Map<string, CacheEntry<T>>();
13
+ private ttl?: number;
14
+ private maxSize?: number;
15
+
16
+ constructor(options: CacheOptions = {}) {
17
+ this.ttl = options.ttl;
18
+ this.maxSize = options.maxSize;
19
+ }
20
+
21
+ private isExpired(entry: CacheEntry<T>): boolean {
22
+ if (!entry.expiresAt) return false;
23
+ return Date.now() > entry.expiresAt;
24
+ }
25
+
26
+ private evictIfNeeded(): void {
27
+ if (!this.maxSize || this.store.size < this.maxSize) return;
28
+
29
+ // Remove expired entries first
30
+ for (const [key, entry] of this.store) {
31
+ if (this.isExpired(entry)) {
32
+ this.store.delete(key);
33
+ }
34
+ }
35
+
36
+ // If still over limit, remove oldest entries
37
+ if (this.store.size >= this.maxSize) {
38
+ const keysToDelete = this.store.size - this.maxSize + 1;
39
+ const keys = Array.from(this.store.keys()).slice(0, keysToDelete);
40
+ keys.forEach(key => this.store.delete(key));
41
+ }
42
+ }
43
+
44
+ get(key: string): T | undefined {
45
+ const entry = this.store.get(key);
46
+ if (!entry) return undefined;
47
+
48
+ if (this.isExpired(entry)) {
49
+ this.store.delete(key);
50
+ return undefined;
51
+ }
52
+
53
+ return entry.value;
54
+ }
55
+
56
+ set(key: string, value: T, ttl?: number): void {
57
+ this.evictIfNeeded();
58
+
59
+ const effectiveTtl = ttl ?? this.ttl;
60
+ const entry: CacheEntry<T> = {
61
+ value,
62
+ expiresAt: effectiveTtl ? Date.now() + effectiveTtl : undefined,
63
+ };
64
+
65
+ this.store.set(key, entry);
66
+ }
67
+
68
+ has(key: string): boolean {
69
+ return this.get(key) !== undefined;
70
+ }
71
+
72
+ delete(key: string): boolean {
73
+ return this.store.delete(key);
74
+ }
75
+
76
+ clear(): void {
77
+ this.store.clear();
78
+ }
79
+
80
+ size(): number {
81
+ // Clean expired entries first
82
+ for (const [key, entry] of this.store) {
83
+ if (this.isExpired(entry)) {
84
+ this.store.delete(key);
85
+ }
86
+ }
87
+ return this.store.size;
88
+ }
89
+
90
+ keys(): string[] {
91
+ return Array.from(this.store.keys()).filter(key => this.has(key));
92
+ }
93
+
94
+ values(): T[] {
95
+ return this.keys().map(key => this.get(key)!);
96
+ }
97
+
98
+ entries(): [string, T][] {
99
+ return this.keys().map(key => [key, this.get(key)!]);
100
+ }
101
+
102
+ async getOrSet(key: string, factory: () => T | Promise<T>, ttl?: number): Promise<T> {
103
+ const cached = this.get(key);
104
+ if (cached !== undefined) return cached;
105
+
106
+ const value = await factory();
107
+ this.set(key, value, ttl);
108
+ return value;
109
+ }
110
+ }
111
+
112
+ // Memoization helper
113
+ export function memoize<T extends (...args: unknown[]) => unknown>(
114
+ fn: T,
115
+ options: CacheOptions & { keyFn?: (...args: Parameters<T>) => string } = {}
116
+ ): T {
117
+ const cache = new Cache<ReturnType<T>>(options);
118
+ const keyFn = options.keyFn ?? ((...args) => JSON.stringify(args));
119
+
120
+ return ((...args: Parameters<T>) => {
121
+ const key = keyFn(...args);
122
+ const cached = cache.get(key);
123
+ if (cached !== undefined) return cached;
124
+
125
+ const result = fn(...args) as ReturnType<T>;
126
+ cache.set(key, result);
127
+ return result;
128
+ }) as T;
129
+ }
130
+
131
+ // Default cache instance
132
+ export const cache = new Cache();
133
+
134
+ // Factory function
135
+ export function createCache<T = unknown>(options: CacheOptions = {}): Cache<T> {
136
+ return new Cache<T>(options);
137
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../config/typescript/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ });
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
3
+ parser: '@typescript-eslint/parser',
4
+ plugins: ['@typescript-eslint', 'import'],
5
+ rules: {
6
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
7
+ '@typescript-eslint/no-explicit-any': 'warn',
8
+ 'import/order': [
9
+ 'error',
10
+ {
11
+ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
12
+ 'newlines-between': 'always',
13
+ alphabetize: { order: 'asc', caseInsensitive: true },
14
+ },
15
+ ],
16
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
17
+ 'prefer-const': 'error',
18
+ },
19
+ ignorePatterns: ['node_modules', 'dist', '.next', 'coverage'],
20
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@repo/config",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "exports": {
6
+ "./eslint": "./eslint/index.js",
7
+ "./typescript/base": "./typescript/base.json"
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "lib": ["ES2022"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "resolveJsonModule": true,
9
+ "allowJs": true,
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+ "strict": true,
14
+ "noImplicitAny": true,
15
+ "strictNullChecks": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "esModuleInterop": true,
20
+ "allowSyntheticDefaultImports": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+ "skipLibCheck": true,
23
+ "isolatedModules": true
24
+ },
25
+ "exclude": ["node_modules", "dist"]
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@repo/constants",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "lint": "eslint src/",
19
+ "lint:fix": "eslint src/ --fix",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.4.0"
25
+ }
26
+ }
@@ -0,0 +1,121 @@
1
+ // HTTP Status Codes
2
+ export const HTTP_STATUS = {
3
+ // Success
4
+ OK: 200,
5
+ CREATED: 201,
6
+ ACCEPTED: 202,
7
+ NO_CONTENT: 204,
8
+
9
+ // Redirection
10
+ MOVED_PERMANENTLY: 301,
11
+ FOUND: 302,
12
+ NOT_MODIFIED: 304,
13
+
14
+ // Client Errors
15
+ BAD_REQUEST: 400,
16
+ UNAUTHORIZED: 401,
17
+ FORBIDDEN: 403,
18
+ NOT_FOUND: 404,
19
+ METHOD_NOT_ALLOWED: 405,
20
+ CONFLICT: 409,
21
+ UNPROCESSABLE_ENTITY: 422,
22
+ TOO_MANY_REQUESTS: 429,
23
+
24
+ // Server Errors
25
+ INTERNAL_SERVER_ERROR: 500,
26
+ NOT_IMPLEMENTED: 501,
27
+ BAD_GATEWAY: 502,
28
+ SERVICE_UNAVAILABLE: 503,
29
+ GATEWAY_TIMEOUT: 504,
30
+ } as const;
31
+
32
+ export type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
33
+
34
+ // Error Codes
35
+ export const ERROR_CODE = {
36
+ // Authentication
37
+ AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
38
+ AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
39
+ AUTH_TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
40
+ AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
41
+ AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',
42
+
43
+ // Validation
44
+ VALIDATION_FAILED: 'VALIDATION_FAILED',
45
+ VALIDATION_REQUIRED: 'VALIDATION_REQUIRED',
46
+ VALIDATION_INVALID_FORMAT: 'VALIDATION_INVALID_FORMAT',
47
+
48
+ // Resource
49
+ RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
50
+ RESOURCE_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS',
51
+ RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',
52
+
53
+ // Rate Limiting
54
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
55
+
56
+ // Server
57
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
58
+ SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
59
+ DATABASE_ERROR: 'DATABASE_ERROR',
60
+ EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
61
+ } as const;
62
+
63
+ export type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];
64
+
65
+ // User Roles
66
+ export const USER_ROLE = {
67
+ ADMIN: 'admin',
68
+ USER: 'user',
69
+ GUEST: 'guest',
70
+ } as const;
71
+
72
+ export type UserRole = (typeof USER_ROLE)[keyof typeof USER_ROLE];
73
+
74
+ // Pagination
75
+ export const PAGINATION = {
76
+ DEFAULT_PAGE: 1,
77
+ DEFAULT_LIMIT: 20,
78
+ MAX_LIMIT: 100,
79
+ } as const;
80
+
81
+ // Date/Time
82
+ export const TIME = {
83
+ SECOND: 1000,
84
+ MINUTE: 60 * 1000,
85
+ HOUR: 60 * 60 * 1000,
86
+ DAY: 24 * 60 * 60 * 1000,
87
+ WEEK: 7 * 24 * 60 * 60 * 1000,
88
+ } as const;
89
+
90
+ // Regex Patterns
91
+ export const REGEX = {
92
+ EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
93
+ UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
94
+ SLUG: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
95
+ PHONE: /^\+?[1-9]\d{1,14}$/,
96
+ URL: /^https?:\/\/.+/,
97
+ } as const;
98
+
99
+ // Mime Types
100
+ export const MIME_TYPE = {
101
+ JSON: 'application/json',
102
+ HTML: 'text/html',
103
+ TEXT: 'text/plain',
104
+ XML: 'application/xml',
105
+ PDF: 'application/pdf',
106
+ PNG: 'image/png',
107
+ JPEG: 'image/jpeg',
108
+ GIF: 'image/gif',
109
+ SVG: 'image/svg+xml',
110
+ } as const;
111
+
112
+ export type MimeType = (typeof MIME_TYPE)[keyof typeof MIME_TYPE];
113
+
114
+ // Environment
115
+ export const ENV = {
116
+ DEVELOPMENT: 'development',
117
+ PRODUCTION: 'production',
118
+ TEST: 'test',
119
+ } as const;
120
+
121
+ export type Environment = (typeof ENV)[keyof typeof ENV];
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../config/typescript/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@repo/logger",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "lint": "eslint src/",
19
+ "lint:fix": "eslint src/ --fix",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "tsup": "^8.0.0",
25
+ "typescript": "^5.4.0"
26
+ }
27
+ }
@@ -0,0 +1,197 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
2
+
3
+ export interface LogContext {
4
+ [key: string]: unknown;
5
+ }
6
+
7
+ export interface LoggerOptions {
8
+ level?: LogLevel;
9
+ prefix?: string;
10
+ timestamp?: boolean;
11
+ colors?: boolean;
12
+ json?: boolean;
13
+ }
14
+
15
+ interface LogEntry {
16
+ timestamp: string;
17
+ level: LogLevel;
18
+ message: string;
19
+ prefix?: string;
20
+ context?: LogContext;
21
+ }
22
+
23
+ const LOG_LEVELS: Record<LogLevel, number> = {
24
+ debug: 0,
25
+ info: 1,
26
+ warn: 2,
27
+ error: 3,
28
+ silent: 4,
29
+ };
30
+
31
+ const LOG_COLORS: Record<Exclude<LogLevel, 'silent'>, string> = {
32
+ debug: '\x1b[36m', // cyan
33
+ info: '\x1b[32m', // green
34
+ warn: '\x1b[33m', // yellow
35
+ error: '\x1b[31m', // red
36
+ };
37
+
38
+ const RESET = '\x1b[0m';
39
+ const DIM = '\x1b[2m';
40
+ const BOLD = '\x1b[1m';
41
+
42
+ export class Logger {
43
+ private level: LogLevel;
44
+ private prefix: string;
45
+ private showTimestamp: boolean;
46
+ private useColors: boolean;
47
+ private jsonOutput: boolean;
48
+
49
+ constructor(options: LoggerOptions = {}) {
50
+ this.level = options.level ?? 'info';
51
+ this.prefix = options.prefix ?? '';
52
+ this.showTimestamp = options.timestamp ?? true;
53
+ this.useColors = options.colors ?? true;
54
+ this.jsonOutput = options.json ?? false;
55
+ }
56
+
57
+ private shouldLog(level: LogLevel): boolean {
58
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
59
+ }
60
+
61
+ private formatJson(entry: LogEntry): string {
62
+ return JSON.stringify(entry);
63
+ }
64
+
65
+ private formatPretty(level: Exclude<LogLevel, 'silent'>, message: string): string {
66
+ const parts: string[] = [];
67
+
68
+ if (this.showTimestamp) {
69
+ const time = new Date().toISOString();
70
+ parts.push(this.useColors ? `${DIM}[${time}]${RESET}` : `[${time}]`);
71
+ }
72
+
73
+ const levelTag = `[${level.toUpperCase()}]`;
74
+ if (this.useColors) {
75
+ parts.push(`${LOG_COLORS[level]}${BOLD}${levelTag}${RESET}`);
76
+ } else {
77
+ parts.push(levelTag);
78
+ }
79
+
80
+ if (this.prefix) {
81
+ parts.push(this.useColors ? `${DIM}[${this.prefix}]${RESET}` : `[${this.prefix}]`);
82
+ }
83
+
84
+ parts.push(message);
85
+
86
+ return parts.join(' ');
87
+ }
88
+
89
+ private log(level: Exclude<LogLevel, 'silent'>, message: string, context?: LogContext): void {
90
+ if (!this.shouldLog(level)) return;
91
+
92
+ const consoleMethod =
93
+ level === 'debug' ? 'debug' : level === 'info' ? 'info' : level === 'warn' ? 'warn' : 'error';
94
+
95
+ if (this.jsonOutput) {
96
+ const entry: LogEntry = {
97
+ timestamp: new Date().toISOString(),
98
+ level,
99
+ message,
100
+ ...(this.prefix && { prefix: this.prefix }),
101
+ ...(context && { context }),
102
+ };
103
+ // eslint-disable-next-line no-console
104
+ console[consoleMethod](this.formatJson(entry));
105
+ } else {
106
+ const formatted = this.formatPretty(level, message);
107
+ if (context) {
108
+ // eslint-disable-next-line no-console
109
+ console[consoleMethod](formatted, context);
110
+ } else {
111
+ // eslint-disable-next-line no-console
112
+ console[consoleMethod](formatted);
113
+ }
114
+ }
115
+ }
116
+
117
+ debug(message: string, context?: LogContext): void {
118
+ this.log('debug', message, context);
119
+ }
120
+
121
+ info(message: string, context?: LogContext): void {
122
+ this.log('info', message, context);
123
+ }
124
+
125
+ warn(message: string, context?: LogContext): void {
126
+ this.log('warn', message, context);
127
+ }
128
+
129
+ error(message: string, context?: LogContext): void {
130
+ this.log('error', message, context);
131
+ }
132
+
133
+ child(prefix: string): Logger {
134
+ return new Logger({
135
+ level: this.level,
136
+ prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
137
+ timestamp: this.showTimestamp,
138
+ colors: this.useColors,
139
+ json: this.jsonOutput,
140
+ });
141
+ }
142
+
143
+ setLevel(level: LogLevel): void {
144
+ this.level = level;
145
+ }
146
+
147
+ getLevel(): LogLevel {
148
+ return this.level;
149
+ }
150
+
151
+ isLevelEnabled(level: LogLevel): boolean {
152
+ return this.shouldLog(level);
153
+ }
154
+
155
+ time(label: string): () => void {
156
+ const start = performance.now();
157
+ return () => {
158
+ const duration = performance.now() - start;
159
+ this.debug(`${label} completed`, { durationMs: Math.round(duration * 100) / 100 });
160
+ };
161
+ }
162
+
163
+ async timeAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
164
+ const end = this.time(label);
165
+ try {
166
+ return await fn();
167
+ } finally {
168
+ end();
169
+ }
170
+ }
171
+
172
+ group(label: string): void {
173
+ if (!this.shouldLog('debug')) return;
174
+ // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
175
+ console.group(this.formatPretty('debug', label));
176
+ }
177
+
178
+ groupEnd(): void {
179
+ if (!this.shouldLog('debug')) return;
180
+ // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
181
+ console.groupEnd();
182
+ }
183
+
184
+ table(data: unknown): void {
185
+ if (!this.shouldLog('debug')) return;
186
+ // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
187
+ console.table(data);
188
+ }
189
+ }
190
+
191
+ // Default logger instance
192
+ export const logger = new Logger();
193
+
194
+ // Factory function
195
+ export function createLogger(options: LoggerOptions = {}): Logger {
196
+ return new Logger(options);
197
+ }