From a6f43ed59c81e946c89dc054a813d1efc685ebf5 Mon Sep 17 00:00:00 2001 From: prosdev Date: Tue, 25 Nov 2025 01:26:02 -0800 Subject: [PATCH] feat(logger): create @lytics/kero zero-dependency logger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Core logger with trace/debug/info/warn/error/fatal levels - Pino-inspired API with structured logging - Child loggers with context inheritance - Pretty formatter (colored, human-readable) - JSON formatter (machine-readable, Pino-compatible) - Console transport (stdout/stderr routing) - Presets for development/production/test - Timer utility for measuring durations - success() method for success messages with checkmark icon - Configurable icons: unicode (» ○ ● ✔ ▲ ✖ ✘) or emoji - 62 tests with 100% line coverage Closes #46 --- packages/logger/README.md | 232 +++++++++++ packages/logger/package.json | 39 ++ .../logger/src/formatters/formatters.test.ts | 319 +++++++++++++++ packages/logger/src/formatters/index.ts | 2 + packages/logger/src/formatters/json.ts | 30 ++ packages/logger/src/formatters/pretty.ts | 174 ++++++++ packages/logger/src/index.ts | 47 +++ packages/logger/src/logger.test.ts | 274 +++++++++++++ packages/logger/src/logger.ts | 149 +++++++ packages/logger/src/presets.test.ts | 56 +++ packages/logger/src/presets.ts | 52 +++ packages/logger/src/transports/console.ts | 20 + packages/logger/src/transports/index.ts | 1 + .../logger/src/transports/transports.test.ts | 81 ++++ packages/logger/src/types.ts | 108 +++++ packages/logger/tsconfig.json | 12 + pnpm-lock.yaml | 384 ++++++++++++++++++ tsconfig.json | 4 +- vitest.config.ts | 1 + 19 files changed, 1984 insertions(+), 1 deletion(-) create mode 100644 packages/logger/README.md create mode 100644 packages/logger/package.json create mode 100644 packages/logger/src/formatters/formatters.test.ts create mode 100644 packages/logger/src/formatters/index.ts create mode 100644 packages/logger/src/formatters/json.ts create mode 100644 packages/logger/src/formatters/pretty.ts create mode 100644 packages/logger/src/index.ts create mode 100644 packages/logger/src/logger.test.ts create mode 100644 packages/logger/src/logger.ts create mode 100644 packages/logger/src/presets.test.ts create mode 100644 packages/logger/src/presets.ts create mode 100644 packages/logger/src/transports/console.ts create mode 100644 packages/logger/src/transports/index.ts create mode 100644 packages/logger/src/transports/transports.test.ts create mode 100644 packages/logger/src/types.ts create mode 100644 packages/logger/tsconfig.json diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 0000000..acef2ea --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,232 @@ +# @lytics/kero + +Zero-dependency TypeScript logger inspired by Pino. + +## Installation + +```bash +pnpm add @lytics/kero +``` + +## Quick Start + +```typescript +import { kero } from '@lytics/kero'; + +// Basic logging +kero.info('Server started'); +kero.success('Deployment complete'); +kero.debug({ port: 3000 }, 'Listening'); +kero.error(new Error('Failed'), 'Request failed'); +``` + +## Creating a Logger + +```typescript +import { createLogger } from '@lytics/kero'; + +const logger = createLogger({ + level: 'debug', // Minimum level: trace, debug, info, warn, error, fatal + format: 'pretty', // Output format: 'pretty' or 'json' +}); +``` + +## Presets + +```typescript +import { createLogger } from '@lytics/kero'; + +// Development: pretty output, debug level +const devLogger = createLogger({ preset: 'development' }); + +// Production: JSON output, info level +const prodLogger = createLogger({ preset: 'production' }); + +// Test: JSON output, warn level (minimal noise) +const testLogger = createLogger({ preset: 'test' }); +``` + +## Child Loggers + +Child loggers inherit parent configuration and add context: + +```typescript +const logger = createLogger({ level: 'info' }); +const reqLogger = logger.child({ requestId: 'abc-123' }); + +reqLogger.info('Processing'); // Includes requestId in output +``` + +## Structured Logging + +Pass an object as the first argument for structured data: + +```typescript +logger.info({ userId: 42, action: 'login' }, 'User authenticated'); +// JSON: {"level":30,"time":1732505525000,"userId":42,"action":"login","msg":"User authenticated"} +// Pretty: [14:32:05] INFO User authenticated {"userId":42,"action":"login"} +``` + +## Error Logging + +Pass an Error as the first argument: + +```typescript +try { + riskyOperation(); +} catch (err) { + logger.error(err, 'Operation failed'); +} +``` + +## Timing + +Measure operation duration: + +```typescript +const done = logger.startTimer('database-query'); +await db.query(sql); +done(); // Logs: "database-query completed (42ms)" +``` + +## Log Levels + +From lowest to highest priority: + +| Level | Value | Description | +|---------|-------|--------------------------| +| `trace` | 10 | Detailed tracing | +| `debug` | 20 | Debug information | +| `info` | 30 | Normal operations | +| `warn` | 40 | Warning conditions | +| `error` | 50 | Error conditions | +| `fatal` | 60 | System is unusable | + +## Output Formats + +### Pretty (Development) + +``` +[14:32:05] INFO Server started +[14:32:05] DEBUG Listening {"port":3000} +[14:32:06] INFO (abc-123) Processing +``` + +### Pretty with Icons + +Enable icons for visual scanning: + +```typescript +import { createLogger, PrettyFormatter } from '@lytics/kero'; + +// Unicode icons (default when icons=true) - works everywhere +const logger = createLogger({ + formatter: new PrettyFormatter({ icons: true }), +}); + +// Or explicitly: icons: 'unicode' or icons: 'emoji' +``` + +**Unicode icons** (terminal-safe): +``` +[14:32:05] » TRACE Detailed info +[14:32:05] ○ DEBUG Listening {"port":3000} +[14:32:05] ● INFO Server started +[14:32:05] ✔ INFO Deployment complete (success) +[14:32:06] ▲ WARN Memory usage high +[14:32:07] ✖ ERROR Request failed +``` + +| Level | Unicode | Emoji | +|-----------|---------|-------| +| `trace` | » | 🔍 | +| `debug` | ○ | 🐛 | +| `info` | ● | ℹ️ | +| `success` | ✔ | ✅ | +| `warn` | ▲ | ⚠️ | +| `error` | ✖ | ❌ | +| `fatal` | ✘ | 💀 | + +### JSON (Production) + +```json +{"level":30,"time":1732505525000,"msg":"Server started"} +{"level":20,"time":1732505525001,"port":3000,"msg":"Listening"} +{"level":30,"time":1732505526000,"requestId":"abc-123","msg":"Processing"} +``` + +## Custom Formatters + +Implement the `Formatter` interface: + +```typescript +import { createLogger, Formatter, LogEntry } from '@lytics/kero'; + +const myFormatter: Formatter = { + format(entry: LogEntry): string { + return `[${entry.level}] ${entry.msg}`; + } +}; + +const logger = createLogger({ formatter: myFormatter }); +``` + +## Custom Transports + +Implement the `Transport` interface: + +```typescript +import { createLogger, Transport, LogEntry } from '@lytics/kero'; + +const myTransport: Transport = { + write(entry: LogEntry, formatted: string): void { + // Send to your logging service + fetch('/logs', { method: 'POST', body: formatted }); + } +}; + +const logger = createLogger({ transports: [myTransport] }); +``` + +## API Reference + +### `createLogger(config?)` + +Creates a new logger instance. + +**Options:** +- `level`: Minimum log level (default: `'info'`) +- `format`: Output format `'pretty'` | `'json'` (default: `'pretty'`) +- `formatter`: Custom formatter instance +- `transports`: Array of transports (default: `[ConsoleTransport]`) +- `context`: Default context for all log entries +- `preset`: Preset name `'development'` | `'production'` | `'test'` + +### `kero` + +Default logger instance with `development` preset. + +### Logger Methods + +- `trace(msg)` / `trace(obj, msg)` - Log at trace level +- `debug(msg)` / `debug(obj, msg)` - Log at debug level +- `info(msg)` / `info(obj, msg)` - Log at info level +- `success(msg)` / `success(obj, msg)` - Log success at info level (with ✔ icon) +- `warn(msg)` / `warn(obj, msg)` - Log at warn level +- `error(msg)` / `error(err, msg)` / `error(obj, msg)` - Log at error level +- `fatal(msg)` / `fatal(err, msg)` / `fatal(obj, msg)` - Log at fatal level +- `child(context)` - Create child logger with added context +- `startTimer(label)` - Start a timer, returns function to stop and log +- `isLevelEnabled(level)` - Check if level would be logged +- `level` - Get current log level + +## Zero Dependencies + +This package has zero runtime dependencies. It uses only Node.js built-ins: +- `process.stdout` / `process.stderr` for output +- `process.env` for color detection + +## License + +MIT + diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..40d4fd6 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,39 @@ +{ + "name": "@lytics/kero", + "version": "0.1.0", + "description": "Zero-dependency TypeScript logger", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "typecheck": "tsc --noEmit", + "lint": "biome lint ./src", + "test": "vitest run", + "test:watch": "vitest" + }, + "keywords": [ + "logger", + "logging", + "typescript", + "zero-dependency" + ], + "author": "Lytics", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.4.5", + "vitest": "^1.4.0" + } +} diff --git a/packages/logger/src/formatters/formatters.test.ts b/packages/logger/src/formatters/formatters.test.ts new file mode 100644 index 0000000..760b3ff --- /dev/null +++ b/packages/logger/src/formatters/formatters.test.ts @@ -0,0 +1,319 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { LogEntry } from '../types'; +import { JsonFormatter } from './json'; +import { PrettyFormatter } from './pretty'; + +const createEntry = (overrides: Partial = {}): LogEntry => ({ + level: 'info', + levelValue: 30, + time: 1732505525000, + msg: 'Test message', + context: {}, + ...overrides, +}); + +describe('JsonFormatter', () => { + const formatter = new JsonFormatter(); + + it('formats a basic log entry', () => { + const entry = createEntry(); + const output = formatter.format(entry); + const parsed = JSON.parse(output); + + expect(parsed.level).toBe(30); + expect(parsed.time).toBe(1732505525000); + expect(parsed.msg).toBe('Test message'); + }); + + it('includes context in output', () => { + const entry = createEntry({ + context: { userId: 42, action: 'login' }, + }); + const output = formatter.format(entry); + const parsed = JSON.parse(output); + + expect(parsed.userId).toBe(42); + expect(parsed.action).toBe('login'); + }); + + it('formats errors correctly', () => { + const error = new Error('Something failed'); + const entry = createEntry({ + error, + msg: 'Operation failed', + }); + const output = formatter.format(entry); + const parsed = JSON.parse(output); + + expect(parsed.err.type).toBe('Error'); + expect(parsed.err.message).toBe('Something failed'); + expect(parsed.err.stack).toContain('Error: Something failed'); + }); + + it('puts msg at the end (Pino convention)', () => { + const entry = createEntry({ context: { a: 1 } }); + const output = formatter.format(entry); + + // msg should be the last key + expect(output.endsWith('"msg":"Test message"}')).toBe(true); + }); +}); + +describe('PrettyFormatter', () => { + // Disable colors for predictable testing + const formatter = new PrettyFormatter({ colors: false }); + + it('formats a basic log entry', () => { + const entry = createEntry(); + const output = formatter.format(entry); + + expect(output).toContain('INFO'); + expect(output).toContain('Test message'); + }); + + it('includes time in brackets', () => { + const entry = createEntry({ + time: new Date('2024-11-25T14:32:05.000Z').getTime(), + }); + const output = formatter.format(entry); + + // Time format depends on timezone, just check brackets exist + expect(output).toMatch(/\[\d{2}:\d{2}:\d{2}\]/); + }); + + it('shows requestId in parentheses', () => { + const entry = createEntry({ + context: { requestId: 'abc-123' }, + }); + const output = formatter.format(entry); + + expect(output).toContain('(abc-123)'); + }); + + it('shows component in brackets', () => { + const entry = createEntry({ + context: { component: 'database' }, + }); + const output = formatter.format(entry); + + expect(output).toContain('[database]'); + }); + + it('shows extra context as JSON', () => { + const entry = createEntry({ + context: { port: 3000, host: 'localhost' }, + }); + const output = formatter.format(entry); + + expect(output).toContain('{"port":3000,"host":"localhost"}'); + }); + + it('includes error stack', () => { + const error = new Error('Test error'); + const entry = createEntry({ + level: 'error', + levelValue: 50, + error, + }); + const output = formatter.format(entry); + + expect(output).toContain('Error: Test error'); + }); + + it('formats different log levels', () => { + const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const; + + for (const level of levels) { + const entry = createEntry({ level, levelValue: 10 }); + const output = formatter.format(entry); + expect(output.toUpperCase()).toContain(level.toUpperCase()); + } + }); + + describe('color detection', () => { + const originalEnv = process.env; + const originalIsTTY = process.stdout.isTTY; + + beforeEach(() => { + // Reset env for each test + process.env = { ...originalEnv }; + delete process.env.NO_COLOR; + delete process.env.FORCE_COLOR; + }); + + afterEach(() => { + process.env = originalEnv; + Object.defineProperty(process.stdout, 'isTTY', { + value: originalIsTTY, + writable: true, + }); + }); + + it('disables colors when NO_COLOR is set', () => { + process.env.NO_COLOR = '1'; + const formatter = new PrettyFormatter(); + const entry = createEntry(); + const output = formatter.format(entry); + + // Should not contain ANSI escape codes + expect(output).not.toContain('\x1b['); + }); + + it('enables colors when FORCE_COLOR is set', () => { + process.env.FORCE_COLOR = '1'; + const formatter = new PrettyFormatter(); + const entry = createEntry(); + const output = formatter.format(entry); + + // Should contain ANSI escape codes + expect(output).toContain('\x1b['); + }); + + it('enables colors when stdout is a TTY', () => { + Object.defineProperty(process.stdout, 'isTTY', { + value: true, + writable: true, + }); + const formatter = new PrettyFormatter(); + const entry = createEntry(); + const output = formatter.format(entry); + + // Should contain ANSI escape codes + expect(output).toContain('\x1b['); + }); + + it('disables colors when stdout is not a TTY', () => { + Object.defineProperty(process.stdout, 'isTTY', { + value: false, + writable: true, + }); + const formatter = new PrettyFormatter(); + const entry = createEntry(); + const output = formatter.format(entry); + + // Should not contain ANSI escape codes + expect(output).not.toContain('\x1b['); + }); + + it('explicit colors option overrides detection', () => { + process.env.NO_COLOR = '1'; // Would normally disable + const formatter = new PrettyFormatter({ colors: true }); + const entry = createEntry(); + const output = formatter.format(entry); + + // Should contain ANSI escape codes despite NO_COLOR + expect(output).toContain('\x1b['); + }); + }); + + describe('icons option', () => { + it('does not show icons by default', () => { + const formatter = new PrettyFormatter({ colors: false }); + const entry = createEntry({ level: 'info', levelValue: 30 }); + const output = formatter.format(entry); + + // Should not contain unicode icons or emoji + expect(output).not.toContain('●'); + expect(output).not.toContain('ℹ️'); + }); + + it('shows unicode icons when icons=true', () => { + const formatter = new PrettyFormatter({ colors: false, icons: true }); + const entry = createEntry({ level: 'info', levelValue: 30 }); + const output = formatter.format(entry); + + // true defaults to unicode + expect(output).toContain('●'); + }); + + it('shows unicode icons when icons="unicode"', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'unicode' }); + const entry = createEntry({ level: 'info', levelValue: 30 }); + const output = formatter.format(entry); + + expect(output).toContain('●'); + }); + + it('shows emoji icons when icons="emoji"', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'emoji' }); + const entry = createEntry({ level: 'info', levelValue: 30 }); + const output = formatter.format(entry); + + expect(output).toContain('ℹ️'); + }); + + it('shows correct unicode icon for each level', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'unicode' }); + const iconMap: Record = { + trace: '»', + debug: '○', + info: '●', + warn: '▲', + error: '✖', + fatal: '✘', + }; + + for (const [level, icon] of Object.entries(iconMap)) { + const entry = createEntry({ level: level as LogEntry['level'], levelValue: 10 }); + const output = formatter.format(entry); + expect(output).toContain(icon); + } + }); + + it('shows correct emoji icon for each level', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'emoji' }); + const emojiMap: Record = { + trace: '🔍', + debug: '🐛', + info: 'ℹ️', + warn: '⚠️', + error: '❌', + fatal: '💀', + }; + + for (const [level, emoji] of Object.entries(emojiMap)) { + const entry = createEntry({ level: level as LogEntry['level'], levelValue: 10 }); + const output = formatter.format(entry); + expect(output).toContain(emoji); + } + }); + + it('shows success icon when _success context is true (unicode)', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'unicode' }); + const entry = createEntry({ + level: 'info', + levelValue: 30, + context: { _success: true }, + }); + const output = formatter.format(entry); + + expect(output).toContain('✔'); + expect(output).not.toContain('●'); // Not regular info icon + }); + + it('shows success icon when _success context is true (emoji)', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'emoji' }); + const entry = createEntry({ + level: 'info', + levelValue: 30, + context: { _success: true }, + }); + const output = formatter.format(entry); + + expect(output).toContain('✅'); + }); + + it('does not show _success in extra context output', () => { + const formatter = new PrettyFormatter({ colors: false, icons: 'unicode' }); + const entry = createEntry({ + level: 'info', + levelValue: 30, + context: { _success: true, otherData: 'visible' }, + }); + const output = formatter.format(entry); + + expect(output).toContain('otherData'); + expect(output).not.toContain('_success'); + }); + }); +}); diff --git a/packages/logger/src/formatters/index.ts b/packages/logger/src/formatters/index.ts new file mode 100644 index 0000000..d2804ec --- /dev/null +++ b/packages/logger/src/formatters/index.ts @@ -0,0 +1,2 @@ +export { JsonFormatter } from './json'; +export { PrettyFormatter } from './pretty'; diff --git a/packages/logger/src/formatters/json.ts b/packages/logger/src/formatters/json.ts new file mode 100644 index 0000000..9b718f1 --- /dev/null +++ b/packages/logger/src/formatters/json.ts @@ -0,0 +1,30 @@ +import type { Formatter, LogEntry } from '../types'; + +/** + * JSON formatter - outputs machine-readable JSON lines + * + * Output format (Pino-compatible): + * {"level":30,"time":1732505525000,"msg":"Server started"} + * {"level":20,"time":1732505525001,"port":3000,"msg":"Listening"} + */ +export class JsonFormatter implements Formatter { + format(entry: LogEntry): string { + const output: Record = { + level: entry.levelValue, + time: entry.time, + ...entry.context, + }; + + if (entry.error) { + output.err = { + type: entry.error.name, + message: entry.error.message, + stack: entry.error.stack, + }; + } + + output.msg = entry.msg; + + return JSON.stringify(output); + } +} diff --git a/packages/logger/src/formatters/pretty.ts b/packages/logger/src/formatters/pretty.ts new file mode 100644 index 0000000..0653c04 --- /dev/null +++ b/packages/logger/src/formatters/pretty.ts @@ -0,0 +1,174 @@ +import type { Formatter, LogEntry, LogLevel } from '../types'; + +/** + * ANSI color codes + */ +const colors = { + reset: '\x1b[0m', + dim: '\x1b[2m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +const levelColors: Record = { + trace: colors.gray, + debug: colors.cyan, + info: colors.blue, + warn: colors.yellow, + error: colors.red, + fatal: colors.magenta, +}; + +const levelLabels: Record = { + trace: 'TRACE', + debug: 'DEBUG', + info: 'INFO ', + warn: 'WARN ', + error: 'ERROR', + fatal: 'FATAL', +}; + +/** + * Level icons using basic Unicode symbols + * These work reliably across all terminals + */ +const levelIcons: Record = { + trace: '»', // double angle - drilling down + debug: '○', // white circle - investigating + info: '●', // black circle - solid info + success: '✔', // checkmark - success + warn: '▲', // triangle - attention + error: '✖', // heavy X - problem + fatal: '✘', // heavy ballot X - critical +}; + +/** + * Level icons using emoji + * More expressive but may not render consistently + */ +const levelEmoji: Record = { + trace: '🔍', + debug: '🐛', + info: 'ℹ️ ', + success: '✅', + warn: '⚠️ ', + error: '❌', + fatal: '💀', +}; + +export interface PrettyFormatterOptions { + /** Enable/disable ANSI colors (auto-detected by default) */ + colors?: boolean; + /** Show icons before log level: 'unicode' for symbols, 'emoji' for emoji, false to disable (default: false) */ + icons?: boolean | 'unicode' | 'emoji'; +} + +/** + * Pretty formatter - outputs human-readable colored output + * + * Output format (default): + * [14:32:05] INFO Server started + * [14:32:05] DEBUG Listening {"port":3000} + * + * Output format (with unicode icons): + * [14:32:05] ● INFO Server started + * [14:32:05] ○ DEBUG Listening {"port":3000} + * + * Output format (with emoji icons): + * [14:32:05] ℹ️ INFO Server started + * [14:32:05] 🐛 DEBUG Listening {"port":3000} + */ +export class PrettyFormatter implements Formatter { + private useColors: boolean; + private iconStyle: false | 'unicode' | 'emoji'; + + constructor(options: PrettyFormatterOptions = {}) { + // Auto-detect color support if not specified + this.useColors = options.colors ?? this.detectColorSupport(); + // Normalize icons option: true defaults to 'unicode' + this.iconStyle = options.icons === true ? 'unicode' : options.icons || false; + } + + private detectColorSupport(): boolean { + // Check for NO_COLOR env var (https://no-color.org/) + if (process.env.NO_COLOR !== undefined) return false; + // Check for FORCE_COLOR env var + if (process.env.FORCE_COLOR !== undefined) return true; + // Check if stdout is a TTY + return process.stdout.isTTY ?? false; + } + + private colorize(text: string, color: string): string { + if (!this.useColors) return text; + return `${color}${text}${colors.reset}`; + } + + format(entry: LogEntry): string { + const time = this.formatTime(entry.time); + const isSuccess = entry.context._success === true; + const level = this.formatLevel(entry.level, isSuccess); + const context = this.formatContext(entry.context); + const message = entry.msg; + + let line = `${time} ${level} ${context}${message}`; + + // Add extra context data if present (filter out internal keys) + const extraKeys = Object.keys(entry.context).filter( + (k) => k !== 'requestId' && k !== 'component' && k !== '_success' + ); + if (extraKeys.length > 0) { + const extras = Object.fromEntries(extraKeys.map((k) => [k, entry.context[k]])); + line += ` ${this.colorize(JSON.stringify(extras), colors.dim)}`; + } + + // Add error stack if present + if (entry.error) { + line += `\n${this.colorize(entry.error.stack ?? entry.error.message, colors.red)}`; + } + + return line; + } + + private formatTime(timestamp: number): string { + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + const timeStr = `[${hours}:${minutes}:${seconds}]`; + return this.colorize(timeStr, colors.dim); + } + + private formatLevel(level: LogLevel, isSuccess?: boolean): string { + const iconKey = isSuccess ? 'success' : level; + let icon = ''; + if (this.iconStyle === 'unicode') { + icon = `${levelIcons[iconKey]} `; + } else if (this.iconStyle === 'emoji') { + icon = `${levelEmoji[iconKey]} `; + } + const label = levelLabels[level]; + const color = isSuccess ? colors.cyan : levelColors[level]; + return `${icon}${this.colorize(label, color)}`; + } + + private formatContext(context: Record): string { + const parts: string[] = []; + + // Show requestId in parentheses if present + if (context.requestId) { + parts.push(this.colorize(`(${context.requestId})`, colors.dim)); + } + + // Show component in brackets if present + if (context.component) { + parts.push(this.colorize(`[${context.component}]`, colors.cyan)); + } + + return parts.length > 0 ? `${parts.join(' ')} ` : ''; + } +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000..e3415fa --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,47 @@ +/** + * @lytics/kero - Zero-dependency TypeScript logger + * + * @example + * ```typescript + * import { createLogger, kero } from '@lytics/kero'; + * + * // Use the default logger + * kero.info('Hello, world!'); + * kero.debug({ user: 'alice' }, 'User logged in'); + * + * // Create a custom logger + * const logger = createLogger({ + * level: 'debug', + * format: 'json', + * }); + * + * // Child loggers inherit context + * const reqLogger = logger.child({ requestId: 'abc-123' }); + * reqLogger.info('Processing request'); + * + * // Timing + * const done = kero.startTimer('database-query'); + * await db.query(sql); + * done(); // logs: "database-query completed (42ms)" + * ``` + */ + +// Formatters +export { JsonFormatter } from './formatters/json'; +export { PrettyFormatter } from './formatters/pretty'; +// Main exports +export { createLogger, KeroLogger, kero } from './logger'; +// Presets +export { getPreset, presets } from './presets'; +// Transports +export { ConsoleTransport } from './transports/console'; +// Types +export type { + Formatter, + LogEntry, + Logger, + LoggerConfig, + LogLevel, + Transport, +} from './types'; +export { LOG_LEVELS } from './types'; diff --git a/packages/logger/src/logger.test.ts b/packages/logger/src/logger.test.ts new file mode 100644 index 0000000..d33b7a7 --- /dev/null +++ b/packages/logger/src/logger.test.ts @@ -0,0 +1,274 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createLogger, kero } from './logger'; +import type { LogEntry, Transport } from './types'; + +// Mock transport to capture log output +class MockTransport implements Transport { + entries: Array<{ entry: LogEntry; formatted: string }> = []; + + write(entry: LogEntry, formatted: string): void { + this.entries.push({ entry, formatted }); + } + + clear(): void { + this.entries = []; + } + + get lastEntry(): LogEntry | undefined { + return this.entries[this.entries.length - 1]?.entry; + } + + get lastFormatted(): string | undefined { + return this.entries[this.entries.length - 1]?.formatted; + } +} + +describe('KeroLogger', () => { + let transport: MockTransport; + + beforeEach(() => { + transport = new MockTransport(); + }); + + afterEach(() => { + transport.clear(); + }); + + describe('createLogger', () => { + it('creates a logger with default settings', () => { + const logger = createLogger({ transports: [transport] }); + expect(logger.level).toBe('info'); + }); + + it('creates a logger with custom level', () => { + const logger = createLogger({ level: 'debug', transports: [transport] }); + expect(logger.level).toBe('debug'); + }); + + it('creates a logger with preset', () => { + const logger = createLogger({ + preset: 'production', + transports: [transport], + }); + expect(logger.level).toBe('info'); + }); + + it('explicit config overrides preset', () => { + const logger = createLogger({ + preset: 'production', + level: 'debug', + transports: [transport], + }); + expect(logger.level).toBe('debug'); + }); + }); + + describe('log levels', () => { + it('logs at info level by default', () => { + const logger = createLogger({ transports: [transport] }); + + logger.trace('trace message'); + logger.debug('debug message'); + logger.info('info message'); + logger.warn('warn message'); + logger.error('error message'); + logger.fatal('fatal message'); + + // trace and debug should be filtered out + expect(transport.entries).toHaveLength(4); + expect(transport.entries.map((e) => e.entry.level)).toEqual([ + 'info', + 'warn', + 'error', + 'fatal', + ]); + }); + + it('logs all levels when set to trace', () => { + const logger = createLogger({ level: 'trace', transports: [transport] }); + + logger.trace('trace'); + logger.debug('debug'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.fatal('fatal'); + + expect(transport.entries).toHaveLength(6); + }); + + it('isLevelEnabled returns correct values', () => { + const logger = createLogger({ level: 'warn', transports: [transport] }); + + expect(logger.isLevelEnabled('trace')).toBe(false); + expect(logger.isLevelEnabled('debug')).toBe(false); + expect(logger.isLevelEnabled('info')).toBe(false); + expect(logger.isLevelEnabled('warn')).toBe(true); + expect(logger.isLevelEnabled('error')).toBe(true); + expect(logger.isLevelEnabled('fatal')).toBe(true); + }); + }); + + describe('log messages', () => { + it('logs a simple string message', () => { + const logger = createLogger({ transports: [transport] }); + logger.info('Hello, world!'); + + expect(transport.lastEntry?.msg).toBe('Hello, world!'); + expect(transport.lastEntry?.level).toBe('info'); + }); + + it('logs with context object', () => { + const logger = createLogger({ transports: [transport] }); + logger.info({ userId: 42, action: 'login' }, 'User action'); + + expect(transport.lastEntry?.msg).toBe('User action'); + expect(transport.lastEntry?.context).toEqual({ + userId: 42, + action: 'login', + }); + }); + + it('logs an error', () => { + const logger = createLogger({ transports: [transport] }); + const error = new Error('Something went wrong'); + logger.error(error, 'Operation failed'); + + expect(transport.lastEntry?.msg).toBe('Operation failed'); + expect(transport.lastEntry?.error).toBe(error); + }); + + it('uses error message if no message provided', () => { + const logger = createLogger({ transports: [transport] }); + const error = new Error('Something went wrong'); + logger.error(error, ''); + + // Should still work, just with empty message + expect(transport.lastEntry?.error).toBe(error); + }); + }); + + describe('success method', () => { + it('logs at info level with _success context', () => { + const logger = createLogger({ transports: [transport] }); + logger.success('Deployment complete'); + + expect(transport.lastEntry?.level).toBe('info'); + expect(transport.lastEntry?.msg).toBe('Deployment complete'); + expect(transport.lastEntry?.context._success).toBe(true); + }); + + it('logs with context object and _success', () => { + const logger = createLogger({ transports: [transport] }); + logger.success({ duration: 42 }, 'Build finished'); + + expect(transport.lastEntry?.level).toBe('info'); + expect(transport.lastEntry?.msg).toBe('Build finished'); + expect(transport.lastEntry?.context._success).toBe(true); + expect(transport.lastEntry?.context.duration).toBe(42); + }); + + it('respects log level filtering', () => { + const logger = createLogger({ level: 'warn', transports: [transport] }); + logger.success('Should not appear'); + + expect(transport.entries).toHaveLength(0); + }); + }); + + describe('child loggers', () => { + it('creates a child logger with inherited context', () => { + const logger = createLogger({ + transports: [transport], + context: { service: 'api' }, + }); + const child = logger.child({ requestId: 'abc-123' }); + + child.info('Processing'); + + expect(transport.lastEntry?.context).toEqual({ + service: 'api', + requestId: 'abc-123', + }); + }); + + it('child can override parent context', () => { + const logger = createLogger({ + transports: [transport], + context: { env: 'dev' }, + }); + const child = logger.child({ env: 'test' }); + + child.info('Test'); + + expect(transport.lastEntry?.context.env).toBe('test'); + }); + + it('child inherits parent level', () => { + const logger = createLogger({ + level: 'warn', + transports: [transport], + }); + const child = logger.child({ component: 'db' }); + + child.debug('Should not appear'); + child.warn('Should appear'); + + expect(transport.entries).toHaveLength(1); + expect(transport.lastEntry?.level).toBe('warn'); + }); + }); + + describe('timing', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('logs duration when timer is stopped', () => { + const logger = createLogger({ transports: [transport] }); + + const done = logger.startTimer('db-query'); + vi.advanceTimersByTime(150); + done(); + + expect(transport.lastEntry?.msg).toBe('db-query completed (150ms)'); + expect(transport.lastEntry?.context.duration).toBe(150); + expect(transport.lastEntry?.context.label).toBe('db-query'); + }); + + it('measures accurate time', () => { + const logger = createLogger({ transports: [transport] }); + + const done = logger.startTimer('operation'); + vi.advanceTimersByTime(42); + done(); + + expect(transport.lastEntry?.context.duration).toBe(42); + }); + }); + + describe('timestamp', () => { + it('includes timestamp in log entry', () => { + const logger = createLogger({ transports: [transport] }); + const before = Date.now(); + logger.info('Test'); + const after = Date.now(); + + expect(transport.lastEntry?.time).toBeGreaterThanOrEqual(before); + expect(transport.lastEntry?.time).toBeLessThanOrEqual(after); + }); + }); + + describe('default export', () => { + it('kero is a ready-to-use logger', () => { + // Just verify it exists and has the expected interface + expect(kero.level).toBe('debug'); // development preset + expect(typeof kero.info).toBe('function'); + expect(typeof kero.child).toBe('function'); + }); + }); +}); diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 0000000..7aa1e3f --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,149 @@ +import { JsonFormatter } from './formatters/json'; +import { PrettyFormatter } from './formatters/pretty'; +import { getPreset } from './presets'; +import { ConsoleTransport } from './transports/console'; +import type { Formatter, LogEntry, Logger, LoggerConfig, LogLevel, Transport } from './types'; +import { LOG_LEVELS } from './types'; + +/** + * Core logger implementation + */ +export class KeroLogger implements Logger { + private readonly _level: LogLevel; + private readonly levelValue: number; + private readonly formatter: Formatter; + private readonly transports: Transport[]; + private readonly context: Record; + + constructor(config: LoggerConfig = {}) { + // Apply preset first, then override with explicit config + const preset = config.preset ? getPreset(config.preset) : {}; + const merged = { ...preset, ...config }; + + this._level = merged.level ?? 'info'; + this.levelValue = LOG_LEVELS[this._level]; + this.context = merged.context ?? {}; + + // Set up formatter + if (merged.formatter) { + this.formatter = merged.formatter; + } else if (merged.format === 'json') { + this.formatter = new JsonFormatter(); + } else { + this.formatter = new PrettyFormatter(); + } + + // Set up transports + this.transports = merged.transports ?? [new ConsoleTransport()]; + } + + get level(): LogLevel { + return this._level; + } + + isLevelEnabled(level: LogLevel): boolean { + return LOG_LEVELS[level] >= this.levelValue; + } + + trace(msgOrObj: string | Record, msg?: string): void { + this.log('trace', msgOrObj, msg); + } + + debug(msgOrObj: string | Record, msg?: string): void { + this.log('debug', msgOrObj, msg); + } + + info(msgOrObj: string | Record, msg?: string): void { + this.log('info', msgOrObj, msg); + } + + success(msgOrObj: string | Record, msg?: string): void { + // Log at info level with success indicator + if (typeof msgOrObj === 'string') { + this.log('info', { _success: true }, msgOrObj); + } else { + this.log('info', { ...msgOrObj, _success: true }, msg); + } + } + + warn(msgOrObj: string | Record, msg?: string): void { + this.log('warn', msgOrObj, msg); + } + + error(msgOrObjOrErr: string | Record | Error, msg?: string): void { + this.log('error', msgOrObjOrErr, msg); + } + + fatal(msgOrObjOrErr: string | Record | Error, msg?: string): void { + this.log('fatal', msgOrObjOrErr, msg); + } + + child(childContext: Record): Logger { + return new KeroLogger({ + level: this._level, + formatter: this.formatter, + transports: this.transports, + context: { ...this.context, ...childContext }, + }); + } + + startTimer(label: string): () => void { + const start = Date.now(); + return () => { + const duration = Date.now() - start; + this.info({ duration, label }, `${label} completed (${duration}ms)`); + }; + } + + private log( + level: LogLevel, + msgOrObjOrErr: string | Record | Error, + msg?: string + ): void { + // Check if level is enabled + if (!this.isLevelEnabled(level)) return; + + // Parse arguments + let message: string; + let extraContext: Record = {}; + let error: Error | undefined; + + if (typeof msgOrObjOrErr === 'string') { + message = msgOrObjOrErr; + } else if (msgOrObjOrErr instanceof Error) { + error = msgOrObjOrErr; + message = msg ?? msgOrObjOrErr.message; + } else { + extraContext = msgOrObjOrErr; + message = msg ?? ''; + } + + // Build log entry + const entry: LogEntry = { + level, + levelValue: LOG_LEVELS[level], + time: Date.now(), + msg: message, + context: { ...this.context, ...extraContext }, + error, + }; + + // Format and write + const formatted = this.formatter.format(entry); + for (const transport of this.transports) { + transport.write(entry, formatted); + } + } +} + +/** + * Create a new logger instance + */ +export function createLogger(config: LoggerConfig = {}): Logger { + return new KeroLogger(config); +} + +/** + * Default logger instance (development preset) + */ +export const kero = createLogger({ preset: 'development' }); diff --git a/packages/logger/src/presets.test.ts b/packages/logger/src/presets.test.ts new file mode 100644 index 0000000..2369325 --- /dev/null +++ b/packages/logger/src/presets.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { getPreset, presets } from './presets'; + +describe('presets', () => { + it('has development preset with debug level and pretty format', () => { + expect(presets.development).toEqual({ + level: 'debug', + format: 'pretty', + }); + }); + + it('has production preset with info level and json format', () => { + expect(presets.production).toEqual({ + level: 'info', + format: 'json', + }); + }); + + it('has test preset with warn level and json format', () => { + expect(presets.test).toEqual({ + level: 'warn', + format: 'json', + }); + }); + + it('has silent preset with fatal level', () => { + expect(presets.silent).toEqual({ + level: 'fatal', + format: 'json', + }); + }); +}); + +describe('getPreset', () => { + it('returns development preset by name', () => { + expect(getPreset('development')).toBe(presets.development); + }); + + it('returns production preset by name', () => { + expect(getPreset('production')).toBe(presets.production); + }); + + it('returns test preset by name', () => { + expect(getPreset('test')).toBe(presets.test); + }); + + it('returns silent preset by name', () => { + expect(getPreset('silent')).toBe(presets.silent); + }); + + it('falls back to development preset for unknown names', () => { + expect(getPreset('unknown')).toBe(presets.development); + expect(getPreset('')).toBe(presets.development); + expect(getPreset('nonexistent')).toBe(presets.development); + }); +}); diff --git a/packages/logger/src/presets.ts b/packages/logger/src/presets.ts new file mode 100644 index 0000000..a247af9 --- /dev/null +++ b/packages/logger/src/presets.ts @@ -0,0 +1,52 @@ +import type { LoggerConfig } from './types'; + +/** + * Preset configurations for common use cases + */ +export const presets: Record> = { + /** + * Development preset + * - Pretty output with colors + * - Debug level for maximum visibility + */ + development: { + level: 'debug', + format: 'pretty', + }, + + /** + * Production preset + * - JSON output for log aggregation + * - Info level (no debug noise) + */ + production: { + level: 'info', + format: 'json', + }, + + /** + * Test preset + * - JSON output for parsing in tests + * - Warn level (minimal output) + */ + test: { + level: 'warn', + format: 'json', + }, + + /** + * Silent preset + * - Fatal only (essentially silent) + */ + silent: { + level: 'fatal', + format: 'json', + }, +}; + +/** + * Get preset configuration by name + */ +export function getPreset(name: string): Partial { + return presets[name] ?? presets.development; +} diff --git a/packages/logger/src/transports/console.ts b/packages/logger/src/transports/console.ts new file mode 100644 index 0000000..fd0aeb4 --- /dev/null +++ b/packages/logger/src/transports/console.ts @@ -0,0 +1,20 @@ +import type { LogEntry, Transport } from '../types'; +import { LOG_LEVELS } from '../types'; + +/** + * Console transport - writes to stdout/stderr + * + * - trace, debug, info -> stdout + * - warn, error, fatal -> stderr + */ +export class ConsoleTransport implements Transport { + write(entry: LogEntry, formatted: string): void { + const stream = entry.levelValue >= LOG_LEVELS.warn ? process.stderr : process.stdout; + + stream.write(`${formatted}\n`); + } + + flush(): void { + // No-op for console, writes are synchronous + } +} diff --git a/packages/logger/src/transports/index.ts b/packages/logger/src/transports/index.ts new file mode 100644 index 0000000..e926a5b --- /dev/null +++ b/packages/logger/src/transports/index.ts @@ -0,0 +1 @@ +export { ConsoleTransport } from './console'; diff --git a/packages/logger/src/transports/transports.test.ts b/packages/logger/src/transports/transports.test.ts new file mode 100644 index 0000000..816afe9 --- /dev/null +++ b/packages/logger/src/transports/transports.test.ts @@ -0,0 +1,81 @@ +import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; +import type { LogEntry } from '../types'; +import { ConsoleTransport } from './console'; + +describe('ConsoleTransport', () => { + let transport: ConsoleTransport; + let stdoutSpy: MockInstance; + let stderrSpy: MockInstance; + + beforeEach(() => { + transport = new ConsoleTransport(); + stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + }); + + afterEach(() => { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + }); + + const createEntry = (level: LogEntry['level'], levelValue: number): LogEntry => ({ + level, + levelValue, + time: Date.now(), + msg: 'Test message', + context: {}, + }); + + it('writes info level to stdout', () => { + const entry = createEntry('info', 30); + transport.write(entry, 'formatted output'); + + expect(stdoutSpy).toHaveBeenCalledWith('formatted output\n'); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('writes debug level to stdout', () => { + const entry = createEntry('debug', 20); + transport.write(entry, 'debug message'); + + expect(stdoutSpy).toHaveBeenCalledWith('debug message\n'); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('writes trace level to stdout', () => { + const entry = createEntry('trace', 10); + transport.write(entry, 'trace message'); + + expect(stdoutSpy).toHaveBeenCalledWith('trace message\n'); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('writes warn level to stderr', () => { + const entry = createEntry('warn', 40); + transport.write(entry, 'warning message'); + + expect(stderrSpy).toHaveBeenCalledWith('warning message\n'); + expect(stdoutSpy).not.toHaveBeenCalled(); + }); + + it('writes error level to stderr', () => { + const entry = createEntry('error', 50); + transport.write(entry, 'error message'); + + expect(stderrSpy).toHaveBeenCalledWith('error message\n'); + expect(stdoutSpy).not.toHaveBeenCalled(); + }); + + it('writes fatal level to stderr', () => { + const entry = createEntry('fatal', 60); + transport.write(entry, 'fatal message'); + + expect(stderrSpy).toHaveBeenCalledWith('fatal message\n'); + expect(stdoutSpy).not.toHaveBeenCalled(); + }); + + it('flush is a no-op', () => { + // Just verify it doesn't throw + expect(() => transport.flush()).not.toThrow(); + }); +}); diff --git a/packages/logger/src/types.ts b/packages/logger/src/types.ts new file mode 100644 index 0000000..cf687bc --- /dev/null +++ b/packages/logger/src/types.ts @@ -0,0 +1,108 @@ +/** + * Log levels in order of priority (lowest to highest) + */ +export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + +/** + * Numeric values for log levels (Pino-compatible) + */ +export const LOG_LEVELS: Record = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, +}; + +/** + * Log entry passed to formatters and transports + */ +export interface LogEntry { + level: LogLevel; + levelValue: number; + time: number; + msg: string; + context: Record; + error?: Error; +} + +/** + * Formatter transforms a log entry into a string + */ +export interface Formatter { + format(entry: LogEntry): string; +} + +/** + * Transport writes formatted log output somewhere + */ +export interface Transport { + write(entry: LogEntry, formatted: string): void; + flush?(): void; +} + +/** + * Logger configuration + */ +export interface LoggerConfig { + /** Minimum log level to output */ + level?: LogLevel; + /** Format preset: 'pretty' for development, 'json' for production */ + format?: 'pretty' | 'json'; + /** Custom formatter (overrides format) */ + formatter?: Formatter; + /** Custom transports (overrides default console transport) */ + transports?: Transport[]; + /** Context to include in all log entries */ + context?: Record; + /** Preset configuration */ + preset?: 'development' | 'production' | 'test'; +} + +/** + * Logger interface + */ +export interface Logger { + /** Log at trace level */ + trace(msg: string): void; + trace(obj: Record, msg: string): void; + + /** Log at debug level */ + debug(msg: string): void; + debug(obj: Record, msg: string): void; + + /** Log at info level */ + info(msg: string): void; + info(obj: Record, msg: string): void; + + /** Log success at info level (with success indicator) */ + success(msg: string): void; + success(obj: Record, msg: string): void; + + /** Log at warn level */ + warn(msg: string): void; + warn(obj: Record, msg: string): void; + + /** Log at error level */ + error(msg: string): void; + error(obj: Record, msg: string): void; + error(err: Error, msg: string): void; + + /** Log at fatal level */ + fatal(msg: string): void; + fatal(obj: Record, msg: string): void; + fatal(err: Error, msg: string): void; + + /** Create a child logger with additional context */ + child(context: Record): Logger; + + /** Start a timer, returns a function to stop and log */ + startTimer(label: string): () => void; + + /** Check if a level would be logged */ + isLevelEnabled(level: LogLevel): boolean; + + /** Get current log level */ + readonly level: LogLevel; +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..6a19d36 --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8ed709..056a14d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,18 @@ importers: specifier: ^5.3.3 version: 5.9.3 + packages/logger: + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + typescript: + specifier: ^5.4.5 + version: 5.9.3 + vitest: + specifier: ^1.4.0 + version: 1.6.1(@types/node@24.10.1) + packages/mcp-server: dependencies: '@lytics/dev-agent-core': @@ -1098,6 +1110,13 @@ packages: '@isaacs/balanced-match': 4.0.1 dev: false + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/resolve-uri@3.1.2: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1463,6 +1482,10 @@ packages: dev: true optional: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@sindresorhus/merge-streams@4.0.0: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -1587,6 +1610,14 @@ packages: - supports-color dev: true + /@vitest/expect@1.6.1: + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + dev: true + /@vitest/expect@2.1.9: resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} dependencies: @@ -1653,6 +1684,14 @@ packages: tinyrainbow: 3.0.3 dev: true + /@vitest/runner@1.6.1: + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + /@vitest/runner@2.1.9: resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} dependencies: @@ -1667,6 +1706,14 @@ packages: pathe: 2.0.3 dev: true + /@vitest/snapshot@1.6.1: + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + dependencies: + magic-string: 0.30.19 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + /@vitest/snapshot@2.1.9: resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} dependencies: @@ -1683,6 +1730,12 @@ packages: pathe: 2.0.3 dev: true + /@vitest/spy@1.6.1: + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + dependencies: + tinyspy: 2.2.1 + dev: true + /@vitest/spy@2.1.9: resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} dependencies: @@ -1693,6 +1746,15 @@ packages: resolution: {integrity: sha512-82vVL8Cqz7rbXaNUl35V2G7xeNMAjBdNOVaHbrzznT9BmiCiPOzhf0FhU3eP41nP1bLDm/5wWKZqkG4nyU95DQ==} dev: true + /@vitest/utils@1.6.1: + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@vitest/utils@2.1.9: resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} dependencies: @@ -1730,6 +1792,19 @@ packages: through: 2.3.8 dev: true + /acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.15.0 + dev: true + + /acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} dependencies: @@ -1760,6 +1835,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /apache-arrow@18.1.0: resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} hasBin: true @@ -1804,6 +1884,10 @@ packages: engines: {node: '>=8'} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1945,6 +2029,19 @@ packages: engines: {node: '>=6'} dev: true + /chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + dev: true + /chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1988,6 +2085,12 @@ packages: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} dev: true + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -2083,6 +2186,10 @@ packages: dot-prop: 5.3.0 dev: true + /confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + dev: true + /conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -2176,6 +2283,13 @@ packages: mimic-response: 3.1.0 dev: false + /deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.1.0 + dev: true + /deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2207,6 +2321,11 @@ packages: dequal: 2.0.3 dev: false + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2348,6 +2467,21 @@ packages: - bare-abort-controller dev: false + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2482,6 +2616,15 @@ packages: engines: {node: '>=18'} dev: false + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + /git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} @@ -2554,6 +2697,11 @@ packages: hasBin: true dev: true + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + /husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2653,6 +2801,11 @@ packages: engines: {node: '>=12'} dev: false + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -2775,6 +2928,14 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true + /local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -2840,6 +3001,12 @@ packages: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: false + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} dev: true @@ -2916,6 +3083,10 @@ packages: engines: {node: '>=16.10'} dev: true + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3102,6 +3273,11 @@ packages: braces: 3.0.3 picomatch: 2.3.1 + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + /mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3126,6 +3302,15 @@ packages: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false + /mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + dev: true + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3155,12 +3340,26 @@ packages: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} dev: false + /npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: false + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + /onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -3238,6 +3437,13 @@ packages: yocto-queue: 1.2.1 dev: true + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.2.1 + dev: true + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -3304,6 +3510,11 @@ packages: engines: {node: '>=8'} dev: true + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3317,6 +3528,10 @@ packages: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} dev: true + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} @@ -3339,6 +3554,14 @@ packages: engines: {node: '>=6'} dev: true + /pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + dev: true + /platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} dev: false @@ -3377,6 +3600,15 @@ packages: hasBin: true dev: true + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + dev: true + /protobufjs@6.11.4: resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} hasBin: true @@ -3421,6 +3653,10 @@ packages: strip-json-comments: 2.0.1 dev: false + /react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + dev: true + /read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -3711,11 +3947,22 @@ packages: engines: {node: '>=4'} dev: true + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} dev: false + /strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + dependencies: + js-tokens: 9.0.1 + dev: true + /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3816,6 +4063,11 @@ packages: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + dev: true + /tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3831,6 +4083,11 @@ packages: engines: {node: '>=14.0.0'} dev: true + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + /tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} @@ -3923,6 +4180,11 @@ packages: turbo-windows-arm64: 2.5.8 dev: true + /type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + dev: true + /typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3939,6 +4201,10 @@ packages: engines: {node: '>=12.17'} dev: false + /ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + dev: true + /undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -4017,6 +4283,28 @@ packages: vfile-message: 4.0.3 dev: false + /vite-node@1.6.1(@types/node@24.10.1): + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@24.10.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@2.1.9(@types/node@22.19.1): resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4078,6 +4366,45 @@ packages: fsevents: 2.3.3 dev: true + /vite@5.4.21(@types/node@24.10.1): + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 24.10.1 + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.4 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@7.1.12(@types/node@24.10.1): resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4129,6 +4456,63 @@ packages: fsevents: 2.3.3 dev: true + /vitest@1.6.1(@types/node@24.10.1): + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 24.10.1 + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.3 + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.19 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@24.10.1) + vite-node: 1.6.1(@types/node@24.10.1) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + dev: true + /vitest@2.1.9(@types/node@22.19.1): resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/tsconfig.json b/tsconfig.json index 10d28ae..d780fc4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ "@lytics/dev-agent-cli": ["packages/cli/src"], "@lytics/dev-agent-cli/*": ["packages/cli/src/*"], "@lytics/dev-agent-integrations": ["packages/integrations/src"], - "@lytics/dev-agent-integrations/*": ["packages/integrations/src/*"] + "@lytics/dev-agent-integrations/*": ["packages/integrations/src/*"], + "@lytics/kero": ["packages/logger/src"], + "@lytics/kero/*": ["packages/logger/src/*"] } }, "exclude": ["node_modules"], diff --git a/vitest.config.ts b/vitest.config.ts index 3fc69cc..e70b7fc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ '@lytics/dev-agent-subagents': resolve(__dirname, 'packages/subagents/src'), '@lytics/dev-agent-cli': resolve(__dirname, 'packages/cli/src'), '@lytics/dev-agent-integrations': resolve(__dirname, 'packages/integrations/src'), + '@lytics/kero': resolve(__dirname, 'packages/logger/src'), }, }, });