commit 4d319b66be796954bd97faceadb2f03fd661cedb Author: p-sw Date: Sun May 31 22:40:20 2026 +0900 feat: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..42fbe03 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# brainbox + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..2d96623 --- /dev/null +++ b/bun.lock @@ -0,0 +1,68 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "brainbox", + "dependencies": { + "chalk": "^5.6.2", + "commander": "^15.0.0", + "ora": "^9.4.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.9.1", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="], + + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d6763d8 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "brainbox", + "module": "src/index.ts", + "type": "module", + "private": true, + "bin": { + "brainbox": "./src/index.ts" + }, + "scripts": { + "cli": "bun run src/index.ts" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.9.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "chalk": "^5.6.2", + "commander": "^15.0.0", + "ora": "^9.4.0" + } +} diff --git a/src/commands/greet.ts b/src/commands/greet.ts new file mode 100644 index 0000000..51a2b70 --- /dev/null +++ b/src/commands/greet.ts @@ -0,0 +1,16 @@ +import { logger } from "../utils/logger.js"; + +interface GreetOptions { + uppercase?: boolean; + count?: string; +} + +export function greet(name: string, options: GreetOptions) { + const message = `Hello, ${name}!`; + const output = options.uppercase ? message.toUpperCase() : message; + const count = Math.max(1, parseInt(options.count ?? "1", 10)); + + for (let i = 0; i < count; i++) { + logger.success(output); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..dd4f9c6 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { logger } from "./utils/logger.js"; +import { greet } from "./commands/greet.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function getVersion(): string { + try { + const pkgPath = join(__dirname, "..", "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); + return pkg.version ?? "0.0.0"; + } catch { + return "0.0.0"; + } +} + +function run(argv: string[] = process.argv) { + const program = new Command(); + + program + .name("brainbox") + .description("A CLI tool for brainbox") + .version(getVersion(), "-v, --version", "Display version number") + .helpOption("-h, --help", "Display help for command") + .configureOutput({ + outputError: (str) => logger.error(str.replace("error: ", "")), + }); + + program + .command("greet") + .description("Greet someone") + .argument("", "Name to greet") + .option("-u, --uppercase", "Convert greeting to uppercase") + .option("-c, --count ", "Repeat the greeting", "1") + .action(greet); + + program.on("command:*", () => { + logger.error(`Unknown command: ${program.args.join(" ")}`); + program.help(); + }); + + program.parse(argv); +} + +run(); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..022f83b --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,175 @@ +import chalk, { type ChalkInstance } from "chalk"; +import { appendFileSync, existsSync, mkdirSync, createWriteStream, type WriteStream } from "fs"; +import { dirname } from "path"; + +export type LogLevel = "debug" | "info" | "success" | "warn" | "error" | "fatal"; + +const LEVELS: Record = { + debug: { rank: 0, color: chalk.gray, stderr: false }, + info: { rank: 1, color: chalk.blue, stderr: false }, + success: { rank: 2, color: chalk.green, stderr: false }, + warn: { rank: 3, color: chalk.yellow, stderr: true }, + error: { rank: 4, color: chalk.red, stderr: true }, + fatal: { rank: 5, color: chalk.bgRed.white,stderr: true }, +}; + +const ICONS: Record = { + debug: "◆", + info: "ℹ", + success: "✔", + warn: "⚠", + error: "✖", + fatal: "▲", +}; + +export interface LoggerOptions { + /** Minimum log level to output. Default: "info" */ + level?: LogLevel; + /** Include timestamps. Default: true */ + timestamps?: boolean; + /** Enable colors. Default: auto-detected from TTY */ + colors?: boolean; + /** Tag prefix for all messages. Default: none */ + tag?: string; + /** File path to append logs to. Default: none */ + file?: string; + /** Write JSON lines to file instead of plain text. Default: false */ + json?: boolean; + /** Completely suppress console output. Default: false */ + silent?: boolean; +} + +class Logger { + private level: LogLevel; + private timestamps: boolean; + private colors: boolean; + private tag?: string; + private file?: string; + private json: boolean; + private silent: boolean; + private fileStream?: WriteStream; + + constructor(options: LoggerOptions = {}) { + this.level = options.level ?? "info"; + this.timestamps = options.timestamps ?? true; + this.colors = options.colors ?? chalk.level > 0; + this.tag = options.tag; + this.file = options.file; + this.json = options.json ?? false; + this.silent = options.silent ?? false; + + if (this.file) { + const dir = dirname(this.file); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + this.fileStream = createWriteStream(this.file, { flags: "a" }); + } + } + + private shouldLog(level: LogLevel): boolean { + return LEVELS[level].rank >= LEVELS[this.level].rank; + } + + private formatTimestamp(): string { + const now = new Date(); + const pad = (n: number) => n.toString().padStart(2, "0"); + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; + } + + private format(level: LogLevel, message: string): { console: string; file: string } { + const ts = this.timestamps ? `[${this.formatTimestamp()}]` : ""; + const tag = this.tag ? `[${this.tag}]` : ""; + const icon = ICONS[level]; + const levelStr = level.toUpperCase(); + + const consoleParts = [ts, tag, this.colors ? LEVELS[level].color(icon) : icon, message].filter(Boolean); + const consoleLine = consoleParts.join(" "); + + const fileParts = [ts, tag, `[${levelStr}]`, message].filter(Boolean); + const fileLine = fileParts.join(" ") + "\n"; + + return { console: consoleLine, file: fileLine }; + } + + private formatJson(level: LogLevel, message: string): string { + return JSON.stringify({ + timestamp: new Date().toISOString(), + level, + tag: this.tag, + message, + }) + "\n"; + } + + private write(level: LogLevel, message: string) { + if (!this.shouldLog(level)) return; + + const { console: consoleLine, file: fileLine } = this.format(level, message); + const jsonLine = this.formatJson(level, message); + + if (!this.silent) { + const out = LEVELS[level].stderr ? process.stderr : process.stdout; + out.write(consoleLine + "\n"); + } + + if (this.fileStream) { + this.fileStream.write(this.json ? jsonLine : fileLine); + } + } + + debug(message: string) { this.write("debug", message); } + info(message: string) { this.write("info", message); } + success(message: string) { this.write("success", message); } + warn(message: string) { this.write("warn", message); } + error(message: string) { this.write("error", message); } + fatal(message: string) { this.write("fatal", message); } + + /** Create a child logger with an additional tag */ + child(tag: string): Logger { + const combined = this.tag ? `${this.tag}:${tag}` : tag; + return new Logger({ + level: this.level, + timestamps: this.timestamps, + colors: this.colors, + tag: combined, + file: this.file, + json: this.json, + silent: this.silent, + }); + } + + /** Update options at runtime */ + configure(options: Partial) { + if (options.level !== undefined) this.level = options.level; + if (options.timestamps !== undefined) this.timestamps = options.timestamps; + if (options.colors !== undefined) this.colors = options.colors; + if (options.tag !== undefined) this.tag = options.tag; + if (options.silent !== undefined) this.silent = options.silent; + if (options.json !== undefined) this.json = options.json; + + if (options.file !== undefined && options.file !== this.file) { + this.fileStream?.end(); + this.file = options.file; + if (this.file) { + const dir = dirname(this.file); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + this.fileStream = createWriteStream(this.file, { flags: "a" }); + } else { + this.fileStream = undefined; + } + } + } + + /** Close file stream gracefully */ + close() { + this.fileStream?.end(); + } +} + +/** Default global logger instance */ +export const logger = new Logger(); + +/** Create a new logger instance with custom options */ +export function createLogger(options: LoggerOptions): Logger { + return new Logger(options); +} + +export type { Logger }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b2e7497 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}