commit d55459976859d364871374c71322660e066587e4
Author: creations <creations@creations.works>
Date:   Sat May 24 09:42:42 2025 -0400

    first commit

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..980ef21
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.forgejo/workflows/biomejs.yml b/.forgejo/workflows/biomejs.yml
new file mode 100644
index 0000000..15c990c
--- /dev/null
+++ b/.forgejo/workflows/biomejs.yml
@@ -0,0 +1,24 @@
+name: Code quality checks
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  biome:
+    runs-on: docker
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Install Bun
+        run: |
+          curl -fsSL https://bun.sh/install | bash
+          export BUN_INSTALL="$HOME/.bun"
+          echo "$BUN_INSTALL/bin" >> $GITHUB_PATH
+
+      - name: Install Dependencies
+        run: bun install
+
+      - name: Run Biome with verbose output
+        run: bunx biome ci . --verbose
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d93a942
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2025, creations.works
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..66455d1
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,50 @@
+{
+	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+	"vcs": {
+		"enabled": true,
+		"clientKind": "git",
+		"useIgnoreFile": false
+	},
+	"files": {
+		"ignoreUnknown": true,
+		"ignore": ["dist", "types"]
+	},
+	"formatter": {
+		"enabled": true,
+		"indentStyle": "tab",
+		"lineEnding": "lf"
+	},
+	"organizeImports": {
+		"enabled": true
+	},
+	"css": {
+		"formatter": {
+			"indentStyle": "tab",
+			"lineEnding": "lf"
+		}
+	},
+	"linter": {
+		"enabled": true,
+		"rules": {
+			"recommended": true,
+			"correctness": {
+				"noUnusedImports": "error",
+				"useJsxKeyInIterable": "off",
+				"noUnusedVariables": "error"
+			},
+			"style": {
+				"useConst": "error",
+				"noVar": "error"
+			}
+		}
+	},
+	"javascript": {
+		"formatter": {
+			"quoteStyle": "double",
+			"indentStyle": "tab",
+			"lineEnding": "lf",
+			"jsxQuoteStyle": "double",
+			"semicolons": "always"
+		}
+	}
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ec7703b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+	"name": "@atums/echo",
+	"version": "1.0.0",
+	"description": "",
+	"private": false,
+	"type": "module",
+	"main": "./dist/index.js",
+	"module": "./dist/index.mjs",
+	"types": "./dist/index.d.ts",
+	"exports": {
+		".": {
+			"import": "./dist/index.js",
+			"require": "./dist/index.js",
+			"types": "./dist/index.d.ts"
+		}
+	},
+	"scripts": {
+		"lint": "bunx biome check",
+		"lint:fix": "bunx biome check --fix",
+		"cleanup": "rm -rf logs node_modules bun.lock"
+	},
+	"license": "BSD-3-Clause",
+	"devDependencies": {
+		"@biomejs/biome": "^1.9.4",
+		"@types/bun": "^1.2.13"
+	},
+	"files": ["dist", "README.md", "LICENSE"],
+	"repository": {
+		"type": "git",
+		"url": "https://git.creations.works/creations/logger.git"
+	}
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..1ba12c1
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,61 @@
+import {
+	constants,
+	type Stats,
+	accessSync,
+	existsSync,
+	mkdirSync,
+	statSync,
+} from "node:fs";
+import { resolve } from "node:path";
+import { defaultConfig, loadEnvConfig, loadLoggerConfig } from "@lib/config";
+
+export class Echo {
+	private readonly directory: string;
+	private readonly config: Required<LoggerConfig>;
+
+	constructor(configOrPath?: string | LoggerConfig) {
+		const fileConfig: LoggerConfig =
+			typeof configOrPath === "string"
+				? loadLoggerConfig(configOrPath)
+				: loadLoggerConfig();
+
+		const overrideConfig: LoggerConfig =
+			typeof configOrPath === "object" ? configOrPath : {};
+
+		const envConfig: LoggerConfig = loadEnvConfig();
+
+		this.config = {
+			...defaultConfig,
+			...fileConfig,
+			...envConfig,
+			...overrideConfig,
+		};
+
+		this.directory = resolve(this.config.directory);
+
+		if (!this.config.disableFile) {
+			Echo.validateDirectory(this.directory);
+		}
+	}
+
+	private static validateDirectory(dir: string): void {
+		if (!existsSync(dir)) {
+			mkdirSync(dir, { recursive: true });
+		}
+
+		const stat: Stats = statSync(dir);
+		if (!stat.isDirectory()) {
+			throw new Error(`[@atums/echo] ${dir} is not a directory`);
+		}
+
+		accessSync(dir, constants.W_OK);
+	}
+
+	public getDirectory(): string {
+		return this.directory;
+	}
+
+	public getConfig(): Required<LoggerConfig> {
+		return this.config;
+	}
+}
diff --git a/src/lib/char.ts b/src/lib/char.ts
new file mode 100644
index 0000000..f6062ba
--- /dev/null
+++ b/src/lib/char.ts
@@ -0,0 +1,8 @@
+function timestampToReadable(timestamp?: number): string {
+	const date: Date =
+		timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date();
+	if (Number.isNaN(date.getTime())) return "Invalid Date";
+	return date.toISOString().replace("T", " ").replace("Z", "");
+}
+
+export { timestampToReadable };
diff --git a/src/lib/config.ts b/src/lib/config.ts
new file mode 100644
index 0000000..ebe28c3
--- /dev/null
+++ b/src/lib/config.ts
@@ -0,0 +1,59 @@
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const defaultConfig: Required<LoggerConfig> = {
+	directory: "logs",
+	level: "info",
+	disableFile: false,
+
+	rotate: true,
+	maxSizeMB: 5,
+	maxFiles: 3,
+
+	console: true,
+	consoleColor: true,
+
+	dateFormat: "YYYY-MM-DD HH:mm:ss",
+	timezone: "UTC",
+
+	silent: false,
+
+	pattern: "{timestamp} [{level-name}] ({file-name}:{line}){message}",
+};
+
+function loadLoggerConfig(configPath = "logger.json"): LoggerConfig {
+	try {
+		const fullPath: string = resolve(process.cwd(), configPath);
+		const raw: string = readFileSync(fullPath, "utf-8");
+		return JSON.parse(raw);
+	} catch {
+		return {};
+	}
+}
+
+function loadEnvConfig(): LoggerConfig {
+	const config: LoggerConfig = {};
+
+	if (process.env.LOG_LEVEL) config.level = process.env.LOG_LEVEL as LogLevel;
+	if (process.env.LOG_DIRECTORY) config.directory = process.env.LOG_DIRECTORY;
+	if (process.env.LOG_DISABLE_FILE)
+		config.disableFile = process.env.LOG_DISABLE_FILE === "true";
+	if (process.env.LOG_ROTATE) config.rotate = process.env.LOG_ROTATE === "true";
+	if (process.env.LOG_MAX_SIZE_MB)
+		config.maxSizeMB = Number.parseInt(process.env.LOG_MAX_SIZE_MB, 10);
+	if (process.env.LOG_MAX_FILES)
+		config.maxFiles = Number.parseInt(process.env.LOG_MAX_FILES, 10);
+	if (process.env.LOG_CONSOLE)
+		config.console = process.env.LOG_CONSOLE === "true";
+	if (process.env.LOG_CONSOLE_COLOR)
+		config.consoleColor = process.env.LOG_CONSOLE_COLOR === "true";
+	if (process.env.LOG_DATE_FORMAT)
+		config.dateFormat = process.env.LOG_DATE_FORMAT;
+	if (process.env.LOG_TIMEZONE) config.timezone = process.env.LOG_TIMEZONE;
+	if (process.env.LOG_SILENT) config.silent = process.env.LOG_SILENT === "true";
+	if (process.env.LOG_PATTERN) config.pattern = process.env.LOG_PATTERN;
+
+	return config;
+}
+
+export { defaultConfig, loadLoggerConfig, loadEnvConfig };
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0ae7837
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+	"compilerOptions": {
+		"baseUrl": "./",
+		"paths": {
+			"@/*": ["src/*"],
+			"@types/*": ["types/*"],
+			"@lib/*": ["src/lib/*"]
+		},
+		"typeRoots": ["./types", "./node_modules/@types"],
+		"lib": ["ESNext", "DOM"],
+		"target": "ESNext",
+		"module": "ESNext",
+		"moduleDetection": "force",
+		"allowJs": true,
+		"moduleResolution": "bundler",
+		"allowImportingTsExtensions": true,
+		"verbatimModuleSyntax": true,
+		"noEmit": true,
+		"strict": true,
+		"skipLibCheck": true,
+		"noFallthroughCasesInSwitch": true,
+		"noUnusedLocals": false,
+		"noUnusedParameters": false,
+		"noPropertyAccessFromIndexSignature": false
+	},
+	"include": ["src", "types", "config"]
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..dcd32e4
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,32 @@
+const LogLevelValue = {
+	trace: 10,
+	debug: 20,
+	info: 30,
+	warn: 40,
+	error: 50,
+	fatal: 60,
+	silent: 70,
+} as const;
+
+type LogLevelValue = typeof LogLevelValue[keyof typeof LogLevelValue];
+type LogLevel = keyof typeof LogLevelValue;
+
+type LoggerConfig = {
+	directory?: string;
+	level?: LogLevel;
+	disableFile?: boolean;
+
+	rotate?: boolean;
+	maxSizeMB?: number;
+	maxFiles?: number;
+
+	console?: boolean;
+	consoleColor?: boolean;
+
+	dateFormat?: string;
+	timezone?: string;
+
+	silent?: boolean;
+
+	pattern?: string;
+};