@puntoycoma/bulki 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/bulki.ts","../../src/commands/compress.ts","../../src/output/progress.ts","../../src/commands/mcp.ts","../../src/commands/pipe.ts"],"sourcesContent":["import { createRequire } from 'node:module'\nimport { Command } from 'commander'\nimport { compressCommand } from '../commands/compress.js'\nimport { hookCommand } from '../commands/hook.js'\nimport { mcpCommand } from '../commands/mcp.js'\nimport { pipeCommand } from '../commands/pipe.js'\nimport { watchCommand } from '../commands/watch.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../../package.json') as { version: string }\n\nif (process.argv.length <= 2) {\n\tconst { runInteractive } = await import('../interactive/index.js')\n\tawait runInteractive()\n} else {\n\tconst program = new Command()\n\tprogram.name('bulki').description('Ultra-fast CLI image optimizer').version(version)\n\n\tprogram.addCommand(compressCommand)\n\tprogram.addCommand(hookCommand)\n\tprogram.addCommand(mcpCommand)\n\tprogram.addCommand(pipeCommand)\n\tprogram.addCommand(watchCommand)\n\tprogram.parse()\n}\n","import path from 'node:path'\nimport { Command } from 'commander'\nimport { processPool } from '../concurrency/pool.js'\nimport { createProgressBar } from '../output/progress.js'\nimport { buildJsonOutput, formatFileResult, formatSummary } from '../output/results.js'\nimport { collectImageFiles } from '../pipeline/discover.js'\nimport { optimizeImage } from '../pipeline/optimize.js'\nimport type { CompressOptions, FileResult } from '../types/index.js'\nimport { type RawOptions, validateOptions } from '../validation/options.js'\n\nexport const compressCommand = new Command('compress')\n\t.description('Compress images in a file or directory')\n\t.argument('<path>', 'File or directory to process')\n\t.option('-q, --quality <n>', 'Output quality (1-100)', (v) => parseInt(v, 10), 70)\n\t.option('-f, --format <fmt>', 'Output format (jpeg, png, webp, gif, avif)')\n\t.option('-r, --resize <width>', 'Resize to width (px)', (v) => parseInt(v, 10))\n\t.option('-d, --destination <path>', 'Destination directory', '.')\n\t.option('-o, --output-folder <name>', 'Output subfolder name', 'bulki-output')\n\t.option('--concurrency <n>', 'Number of parallel operations', (v) => parseInt(v, 10))\n\t.option('--exclude <patterns...>', 'Directory names to exclude')\n\t.option('--dry-run', 'Show what would be processed without writing files')\n\t.option('--verbose', 'Show per-file output')\n\t.option('--json', 'Output results as JSON')\n\t.option('--max-size <size>', 'Maximum file size budget (e.g., 200kb, 1mb)')\n\t.action(async (inputPath: string, opts: Partial<RawOptions>) => {\n\t\tlet options: CompressOptions\n\t\ttry {\n\t\t\toptions = validateOptions(opts)\n\t\t} catch (err) {\n\t\t\tconsole.error((err as Error).message)\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tconst resolvedPath = path.resolve(inputPath)\n\t\tlet filePaths: { path: string; format: string }[]\n\t\tlet isDirectory: boolean\n\t\ttry {\n\t\t\tconst collected = await collectImageFiles(resolvedPath, options.exclude)\n\t\t\tfilePaths = collected.files\n\t\t\tisDirectory = collected.isDirectory\n\t\t} catch {\n\t\t\tconsole.error(`Path not found: ${resolvedPath}`)\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tif (filePaths.length === 0) {\n\t\t\tconsole.log('No images found to process.')\n\t\t\treturn\n\t\t}\n\n\t\tif (options.dryRun) {\n\t\t\tconsole.log(`Would process ${filePaths.length} image(s)`)\n\t\t\tfor (const f of filePaths) console.log(` ${path.basename(f.path)}`)\n\t\t\treturn\n\t\t}\n\n\t\tconst sourcePath = isDirectory ? resolvedPath : path.dirname(resolvedPath)\n\t\tconst startTime = performance.now()\n\n\t\tconst progress = !options.verbose && !options.json ? createProgressBar(filePaths.length) : null\n\n\t\tconst errors: Error[] = []\n\t\tconst jsonErrors: { file: string; message: string }[] = []\n\n\t\tconst processedResults = await processPool(\n\t\t\tfilePaths,\n\t\t\tasync (file) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await optimizeImage(file.path, {\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\tsourcePath,\n\t\t\t\t\t\tdetectedFormat: file.format,\n\t\t\t\t\t})\n\t\t\t\t\tif (options.verbose && !options.json) {\n\t\t\t\t\t\tconsole.log(formatFileResult(result))\n\t\t\t\t\t}\n\t\t\t\t\tprogress?.tick()\n\t\t\t\t\treturn result\n\t\t\t\t} catch (err) {\n\t\t\t\t\terrors.push(err as Error)\n\t\t\t\t\tjsonErrors.push({ file: file.path, message: (err as Error).message })\n\t\t\t\t\tprogress?.tick()\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t},\n\t\t\toptions.concurrency,\n\t\t)\n\n\t\tconst validResults = processedResults.filter((r): r is FileResult => r !== null)\n\t\tconst totalDuration = performance.now() - startTime\n\n\t\tif (options.json) {\n\t\t\tconst output = buildJsonOutput(validResults, jsonErrors, totalDuration)\n\t\t\tif (options.maxSize) {\n\t\t\t\tfor (const file of output.files) {\n\t\t\t\t\tif (file.newSize > options.maxSize) {\n\t\t\t\t\t\tfile.exceeded = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsole.log(JSON.stringify(output))\n\t\t} else {\n\t\t\tconsole.log(formatSummary(validResults, errors, totalDuration))\n\t\t}\n\n\t\tif (validResults.length === 0 && errors.length > 0) {\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tconst maxSize = options.maxSize\n\t\tif (maxSize) {\n\t\t\tconst exceeded = validResults.filter((r) => r.newSize > maxSize)\n\t\t\tif (exceeded.length > 0) {\n\t\t\t\tif (!options.json) {\n\t\t\t\t\tfor (const r of exceeded) {\n\t\t\t\t\t\tconsole.warn(`File exceeds size budget: ${r.outputPath}`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tprocess.exit(1)\n\t\t\t}\n\t\t}\n\t})\n","export function createProgressBar(total: number) {\n\tlet current = 0\n\n\treturn {\n\t\ttick() {\n\t\t\tcurrent++\n\t\t\tconst percent = Math.round((current / total) * 100)\n\t\t\tconst barWidth = Math.max(0, Math.min(process.stdout.columns || 40, 40) - 20)\n\t\t\tconst filled = Math.round((current / total) * barWidth)\n\t\t\tconst empty = barWidth - filled\n\t\t\tconst bar = `[${'='.repeat(filled)}${' '.repeat(empty)}] ${percent}% ${current}/${total}`\n\t\t\tprocess.stdout.write(`\\r${bar}`)\n\t\t\tif (current === total) process.stdout.write('\\n')\n\t\t},\n\t}\n}\n","import { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { Command } from 'commander'\nimport { z } from 'zod'\nimport { buildJsonOutput } from '../output/results.js'\nimport { collectImageFiles } from '../pipeline/discover.js'\nimport { detectFormat } from '../pipeline/filter.js'\nimport { optimizeImage } from '../pipeline/optimize.js'\nimport type { FileResult } from '../types/index.js'\nimport { parseSize } from '../validation/options.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../../package.json') as { version: string }\n\nexport const mcpCommand = new Command('mcp')\n\t.description('Start MCP server for AI editor integration (Claude Code, Cursor, etc.)')\n\t.action(async () => {\n\t\tconst server = new McpServer({\n\t\t\tname: 'bulki',\n\t\t\tversion,\n\t\t})\n\n\t\tserver.tool(\n\t\t\t'compress',\n\t\t\t'Compress and optimize images. Supports JPEG, PNG, WebP, GIF, AVIF.',\n\t\t\t{\n\t\t\t\tpath: z.string().describe('File or directory path to compress'),\n\t\t\t\tquality: z.number().min(1).max(100).default(70).describe('Output quality (1-100)'),\n\t\t\t\tformat: z\n\t\t\t\t\t.enum(['jpeg', 'png', 'webp', 'gif', 'avif'])\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Output format (omit to keep original)'),\n\t\t\t\tresize: z.number().positive().optional().describe('Max width in pixels'),\n\t\t\t\tdestination: z.string().default('.').describe('Output directory'),\n\t\t\t\toutputFolder: z.string().default('bulki-output').describe('Output folder name'),\n\t\t\t\tmaxSize: z.string().optional().describe('Max file size budget (e.g., 200kb, 1mb)'),\n\t\t\t},\n\t\t\tasync (params) => {\n\t\t\t\tif (\n\t\t\t\t\tparams.outputFolder.includes('/') ||\n\t\t\t\t\tparams.outputFolder.includes('\\\\') ||\n\t\t\t\t\tparams.outputFolder.includes('..')\n\t\t\t\t) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'text' as const,\n\t\t\t\t\t\t\t\ttext: JSON.stringify({\n\t\t\t\t\t\t\t\t\terror: 'Invalid outputFolder: must be a simple directory name',\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resolvedPath = path.resolve(params.path)\n\n\t\t\t\tlet filePaths: { path: string; format: string }[]\n\t\t\t\tlet isDirectory: boolean\n\t\t\t\ttry {\n\t\t\t\t\tconst collected = await collectImageFiles(resolvedPath, [])\n\t\t\t\t\tfilePaths = collected.files\n\t\t\t\t\tisDirectory = collected.isDirectory\n\t\t\t\t} catch {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'text' as const,\n\t\t\t\t\t\t\t\ttext: JSON.stringify({ error: `Path not found: ${resolvedPath}` }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (filePaths.length === 0) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: 'text' as const,\n\t\t\t\t\t\t\t\ttext: JSON.stringify({ files: [], summary: { totalFiles: 0 } }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst sourcePath = isDirectory ? resolvedPath : path.dirname(resolvedPath)\n\t\t\t\tconst startTime = performance.now()\n\t\t\t\tconst results: FileResult[] = []\n\t\t\t\tconst errors: { file: string; message: string }[] = []\n\n\t\t\t\tlet maxSizeBytes: number | undefined\n\t\t\t\tif (params.maxSize) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tmaxSizeBytes = parseSize(params.maxSize)\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: 'text' as const,\n\t\t\t\t\t\t\t\t\ttext: JSON.stringify({ error: (err as Error).message }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor (const file of filePaths) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst result = await optimizeImage(file.path, {\n\t\t\t\t\t\t\tquality: params.quality,\n\t\t\t\t\t\t\tformat: params.format,\n\t\t\t\t\t\t\tresize: params.resize,\n\t\t\t\t\t\t\tdestination: params.destination,\n\t\t\t\t\t\t\toutputFolder: params.outputFolder,\n\t\t\t\t\t\t\tconcurrency: 1,\n\t\t\t\t\t\t\texclude: [],\n\t\t\t\t\t\t\tdryRun: false,\n\t\t\t\t\t\t\tverbose: false,\n\t\t\t\t\t\t\tjson: false,\n\t\t\t\t\t\t\tsourcePath,\n\t\t\t\t\t\t\tdetectedFormat: file.format,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tresults.push(result)\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\terrors.push({ file: file.path, message: (err as Error).message })\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst totalDuration = performance.now() - startTime\n\t\t\t\tconst output = buildJsonOutput(results, errors, totalDuration)\n\t\t\t\tif (maxSizeBytes !== undefined) {\n\t\t\t\t\tfor (const file of output.files) {\n\t\t\t\t\t\tif (file.newSize > maxSizeBytes) {\n\t\t\t\t\t\t\tfile.exceeded = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn { content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }] }\n\t\t\t},\n\t\t)\n\n\t\tserver.tool(\n\t\t\t'detect',\n\t\t\t'Detect if a file is an image and identify its format',\n\t\t\t{\n\t\t\t\tpath: z.string().describe('Path to the file'),\n\t\t\t},\n\t\t\tasync (params) => {\n\t\t\t\tconst resolvedPath = path.resolve(params.path)\n\t\t\t\tconst format = await detectFormat(resolvedPath)\n\t\t\t\tconst result = format ? { format, isImage: true } : { format: null, isImage: false }\n\t\t\t\treturn { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }\n\t\t\t},\n\t\t)\n\n\t\tconst transport = new StdioServerTransport()\n\t\tawait server.connect(transport)\n\t})\n","import { Command } from 'commander'\nimport type { FormatEnum } from 'sharp'\nimport sharp from 'sharp'\nimport { getFormatOptions } from '../pipeline/optimize.js'\nimport { VALID_FORMATS } from '../validation/options.js'\n\nexport const pipeCommand = new Command('pipe')\n\t.description('Read image from stdin, write optimized image to stdout')\n\t.requiredOption('-f, --format <fmt>', 'Output format (jpeg, png, webp, gif, avif)')\n\t.option('-q, --quality <n>', 'Quality (1-100)', (v) => parseInt(v, 10), 70)\n\t.option('-r, --resize <width>', 'Resize to width (px)', (v) => parseInt(v, 10))\n\t.action(async (opts) => {\n\t\tconst MAX_INPUT_SIZE = 100 * 1024 * 1024 // 100 MB\n\t\tconst chunks: Buffer[] = []\n\t\tlet totalSize = 0\n\t\tfor await (const chunk of process.stdin) {\n\t\t\ttotalSize += chunk.length\n\t\t\tif (totalSize > MAX_INPUT_SIZE) {\n\t\t\t\tprocess.stderr.write('Error: input exceeds 100 MB limit\\n')\n\t\t\t\tprocess.exit(1)\n\t\t\t}\n\t\t\tchunks.push(chunk)\n\t\t}\n\t\tconst input = Buffer.concat(chunks)\n\n\t\tif (input.length === 0) {\n\t\t\tprocess.stderr.write('Error: no input received on stdin\\n')\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tconst format = opts.format\n\t\tconst quality = opts.quality\n\t\tconst resize = opts.resize\n\n\t\tif (!VALID_FORMATS.includes(format)) {\n\t\t\tprocess.stderr.write(`Error: invalid format \"${format}\"\\n`)\n\t\t\tprocess.exit(1)\n\t\t}\n\n\t\tconst outputFormat = format === 'jpg' ? 'jpeg' : format\n\n\t\tconst formatOptions = getFormatOptions(quality)\n\n\t\ttry {\n\t\t\tlet pipeline = sharp(input)\n\n\t\t\tif (resize) {\n\t\t\t\tpipeline = pipeline.resize(resize)\n\t\t\t}\n\n\t\t\tpipeline = pipeline.toFormat(\n\t\t\t\toutputFormat as keyof FormatEnum,\n\t\t\t\tformatOptions[outputFormat] ?? {},\n\t\t\t)\n\n\t\t\tconst outputBuffer = await pipeline.toBuffer()\n\t\t\tprocess.stdout.write(outputBuffer)\n\t\t} catch (err) {\n\t\t\tprocess.stderr.write(`Error: ${(err as Error).message}\\n`)\n\t\t\tprocess.exit(1)\n\t\t}\n\t})\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,iBAAAA,sBAAqB;AAC9B,SAAS,WAAAC,gBAAe;;;ACDxB,OAAO,UAAU;AACjB,SAAS,eAAe;;;ACDjB,SAAS,kBAAkB,OAAe;AAChD,MAAI,UAAU;AAEd,SAAO;AAAA,IACN,OAAO;AACN;AACA,YAAM,UAAU,KAAK,MAAO,UAAU,QAAS,GAAG;AAClD,YAAM,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,OAAO,WAAW,IAAI,EAAE,IAAI,EAAE;AAC5E,YAAM,SAAS,KAAK,MAAO,UAAU,QAAS,QAAQ;AACtD,YAAM,QAAQ,WAAW;AACzB,YAAM,MAAM,IAAI,IAAI,OAAO,MAAM,CAAC,GAAG,IAAI,OAAO,KAAK,CAAC,KAAK,OAAO,KAAK,OAAO,IAAI,KAAK;AACvF,cAAQ,OAAO,MAAM,KAAK,GAAG,EAAE;AAC/B,UAAI,YAAY,MAAO,SAAQ,OAAO,MAAM,IAAI;AAAA,IACjD;AAAA,EACD;AACD;;;ADLO,IAAM,kBAAkB,IAAI,QAAQ,UAAU,EACnD,YAAY,wCAAwC,EACpD,SAAS,UAAU,8BAA8B,EACjD,OAAO,qBAAqB,0BAA0B,CAAC,MAAM,SAAS,GAAG,EAAE,GAAG,EAAE,EAChF,OAAO,sBAAsB,4CAA4C,EACzE,OAAO,wBAAwB,wBAAwB,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,EAC7E,OAAO,4BAA4B,yBAAyB,GAAG,EAC/D,OAAO,8BAA8B,yBAAyB,cAAc,EAC5E,OAAO,qBAAqB,iCAAiC,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,EACnF,OAAO,2BAA2B,4BAA4B,EAC9D,OAAO,aAAa,oDAAoD,EACxE,OAAO,aAAa,sBAAsB,EAC1C,OAAO,UAAU,wBAAwB,EACzC,OAAO,qBAAqB,6CAA6C,EACzE,OAAO,OAAO,WAAmB,SAA8B;AAC/D,MAAI;AACJ,MAAI;AACH,cAAU,gBAAgB,IAAI;AAAA,EAC/B,SAAS,KAAK;AACb,YAAQ,MAAO,IAAc,OAAO;AACpC,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,eAAe,KAAK,QAAQ,SAAS;AAC3C,MAAI;AACJ,MAAI;AACJ,MAAI;AACH,UAAM,YAAY,MAAM,kBAAkB,cAAc,QAAQ,OAAO;AACvE,gBAAY,UAAU;AACtB,kBAAc,UAAU;AAAA,EACzB,QAAQ;AACP,YAAQ,MAAM,mBAAmB,YAAY,EAAE;AAC/C,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,MAAI,UAAU,WAAW,GAAG;AAC3B,YAAQ,IAAI,6BAA6B;AACzC;AAAA,EACD;AAEA,MAAI,QAAQ,QAAQ;AACnB,YAAQ,IAAI,iBAAiB,UAAU,MAAM,WAAW;AACxD,eAAW,KAAK,UAAW,SAAQ,IAAI,KAAK,KAAK,SAAS,EAAE,IAAI,CAAC,EAAE;AACnE;AAAA,EACD;AAEA,QAAM,aAAa,cAAc,eAAe,KAAK,QAAQ,YAAY;AACzE,QAAM,YAAY,YAAY,IAAI;AAElC,QAAM,WAAW,CAAC,QAAQ,WAAW,CAAC,QAAQ,OAAO,kBAAkB,UAAU,MAAM,IAAI;AAE3F,QAAM,SAAkB,CAAC;AACzB,QAAM,aAAkD,CAAC;AAEzD,QAAM,mBAAmB,MAAM;AAAA,IAC9B;AAAA,IACA,OAAO,SAAS;AACf,UAAI;AACH,cAAM,SAAS,MAAM,cAAc,KAAK,MAAM;AAAA,UAC7C,GAAG;AAAA,UACH;AAAA,UACA,gBAAgB,KAAK;AAAA,QACtB,CAAC;AACD,YAAI,QAAQ,WAAW,CAAC,QAAQ,MAAM;AACrC,kBAAQ,IAAI,iBAAiB,MAAM,CAAC;AAAA,QACrC;AACA,kBAAU,KAAK;AACf,eAAO;AAAA,MACR,SAAS,KAAK;AACb,eAAO,KAAK,GAAY;AACxB,mBAAW,KAAK,EAAE,MAAM,KAAK,MAAM,SAAU,IAAc,QAAQ,CAAC;AACpE,kBAAU,KAAK;AACf,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IACA,QAAQ;AAAA,EACT;AAEA,QAAM,eAAe,iBAAiB,OAAO,CAAC,MAAuB,MAAM,IAAI;AAC/E,QAAM,gBAAgB,YAAY,IAAI,IAAI;AAE1C,MAAI,QAAQ,MAAM;AACjB,UAAM,SAAS,gBAAgB,cAAc,YAAY,aAAa;AACtE,QAAI,QAAQ,SAAS;AACpB,iBAAW,QAAQ,OAAO,OAAO;AAChC,YAAI,KAAK,UAAU,QAAQ,SAAS;AACnC,eAAK,WAAW;AAAA,QACjB;AAAA,MACD;AAAA,IACD;AACA,YAAQ,IAAI,KAAK,UAAU,MAAM,CAAC;AAAA,EACnC,OAAO;AACN,YAAQ,IAAI,cAAc,cAAc,QAAQ,aAAa,CAAC;AAAA,EAC/D;AAEA,MAAI,aAAa,WAAW,KAAK,OAAO,SAAS,GAAG;AACnD,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,UAAU,QAAQ;AACxB,MAAI,SAAS;AACZ,UAAM,WAAW,aAAa,OAAO,CAAC,MAAM,EAAE,UAAU,OAAO;AAC/D,QAAI,SAAS,SAAS,GAAG;AACxB,UAAI,CAAC,QAAQ,MAAM;AAClB,mBAAW,KAAK,UAAU;AACzB,kBAAQ,KAAK,6BAA6B,EAAE,UAAU,EAAE;AAAA,QACzD;AAAA,MACD;AACA,cAAQ,KAAK,CAAC;AAAA,IACf;AAAA,EACD;AACD,CAAC;;;AEzHF,SAAS,qBAAqB;AAC9B,OAAOC,WAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,WAAAC,gBAAe;AACxB,SAAS,SAAS;AAQlB,IAAMC,WAAU,cAAc,YAAY,GAAG;AAC7C,IAAM,EAAE,QAAQ,IAAIA,SAAQ,oBAAoB;AAEzC,IAAM,aAAa,IAAIC,SAAQ,KAAK,EACzC,YAAY,wEAAwE,EACpF,OAAO,YAAY;AACnB,QAAM,SAAS,IAAI,UAAU;AAAA,IAC5B,MAAM;AAAA,IACN;AAAA,EACD,CAAC;AAED,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,MACC,MAAM,EAAE,OAAO,EAAE,SAAS,oCAAoC;AAAA,MAC9D,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE,EAAE,SAAS,wBAAwB;AAAA,MACjF,QAAQ,EACN,KAAK,CAAC,QAAQ,OAAO,QAAQ,OAAO,MAAM,CAAC,EAC3C,SAAS,EACT,SAAS,uCAAuC;AAAA,MAClD,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,qBAAqB;AAAA,MACvE,aAAa,EAAE,OAAO,EAAE,QAAQ,GAAG,EAAE,SAAS,kBAAkB;AAAA,MAChE,cAAc,EAAE,OAAO,EAAE,QAAQ,cAAc,EAAE,SAAS,oBAAoB;AAAA,MAC9E,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,yCAAyC;AAAA,IAClF;AAAA,IACA,OAAO,WAAW;AACjB,UACC,OAAO,aAAa,SAAS,GAAG,KAChC,OAAO,aAAa,SAAS,IAAI,KACjC,OAAO,aAAa,SAAS,IAAI,GAChC;AACD,eAAO;AAAA,UACN,SAAS;AAAA,YACR;AAAA,cACC,MAAM;AAAA,cACN,MAAM,KAAK,UAAU;AAAA,gBACpB,OAAO;AAAA,cACR,CAAC;AAAA,YACF;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAEA,YAAM,eAAeC,MAAK,QAAQ,OAAO,IAAI;AAE7C,UAAI;AACJ,UAAI;AACJ,UAAI;AACH,cAAM,YAAY,MAAM,kBAAkB,cAAc,CAAC,CAAC;AAC1D,oBAAY,UAAU;AACtB,sBAAc,UAAU;AAAA,MACzB,QAAQ;AACP,eAAO;AAAA,UACN,SAAS;AAAA,YACR;AAAA,cACC,MAAM;AAAA,cACN,MAAM,KAAK,UAAU,EAAE,OAAO,mBAAmB,YAAY,GAAG,CAAC;AAAA,YAClE;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAEA,UAAI,UAAU,WAAW,GAAG;AAC3B,eAAO;AAAA,UACN,SAAS;AAAA,YACR;AAAA,cACC,MAAM;AAAA,cACN,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,YAAY,EAAE,EAAE,CAAC;AAAA,YAC/D;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAEA,YAAM,aAAa,cAAc,eAAeA,MAAK,QAAQ,YAAY;AACzE,YAAM,YAAY,YAAY,IAAI;AAClC,YAAM,UAAwB,CAAC;AAC/B,YAAM,SAA8C,CAAC;AAErD,UAAI;AACJ,UAAI,OAAO,SAAS;AACnB,YAAI;AACH,yBAAe,UAAU,OAAO,OAAO;AAAA,QACxC,SAAS,KAAK;AACb,iBAAO;AAAA,YACN,SAAS;AAAA,cACR;AAAA,gBACC,MAAM;AAAA,gBACN,MAAM,KAAK,UAAU,EAAE,OAAQ,IAAc,QAAQ,CAAC;AAAA,cACvD;AAAA,YACD;AAAA,UACD;AAAA,QACD;AAAA,MACD;AAEA,iBAAW,QAAQ,WAAW;AAC7B,YAAI;AACH,gBAAM,SAAS,MAAM,cAAc,KAAK,MAAM;AAAA,YAC7C,SAAS,OAAO;AAAA,YAChB,QAAQ,OAAO;AAAA,YACf,QAAQ,OAAO;AAAA,YACf,aAAa,OAAO;AAAA,YACpB,cAAc,OAAO;AAAA,YACrB,aAAa;AAAA,YACb,SAAS,CAAC;AAAA,YACV,QAAQ;AAAA,YACR,SAAS;AAAA,YACT,MAAM;AAAA,YACN;AAAA,YACA,gBAAgB,KAAK;AAAA,UACtB,CAAC;AACD,kBAAQ,KAAK,MAAM;AAAA,QACpB,SAAS,KAAK;AACb,iBAAO,KAAK,EAAE,MAAM,KAAK,MAAM,SAAU,IAAc,QAAQ,CAAC;AAAA,QACjE;AAAA,MACD;AAEA,YAAM,gBAAgB,YAAY,IAAI,IAAI;AAC1C,YAAM,SAAS,gBAAgB,SAAS,QAAQ,aAAa;AAC7D,UAAI,iBAAiB,QAAW;AAC/B,mBAAW,QAAQ,OAAO,OAAO;AAChC,cAAI,KAAK,UAAU,cAAc;AAChC,iBAAK,WAAW;AAAA,UACjB;AAAA,QACD;AAAA,MACD;AAEA,aAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAE,CAAC,EAAE;AAAA,IACtF;AAAA,EACD;AAEA,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,MACC,MAAM,EAAE,OAAO,EAAE,SAAS,kBAAkB;AAAA,IAC7C;AAAA,IACA,OAAO,WAAW;AACjB,YAAM,eAAeA,MAAK,QAAQ,OAAO,IAAI;AAC7C,YAAM,SAAS,MAAM,aAAa,YAAY;AAC9C,YAAM,SAAS,SAAS,EAAE,QAAQ,SAAS,KAAK,IAAI,EAAE,QAAQ,MAAM,SAAS,MAAM;AACnF,aAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,KAAK,UAAU,MAAM,EAAE,CAAC,EAAE;AAAA,IAC7E;AAAA,EACD;AAEA,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAC/B,CAAC;;;AChKF,SAAS,WAAAC,gBAAe;AAExB,OAAO,WAAW;AAIX,IAAM,cAAc,IAAIC,SAAQ,MAAM,EAC3C,YAAY,wDAAwD,EACpE,eAAe,sBAAsB,4CAA4C,EACjF,OAAO,qBAAqB,mBAAmB,CAAC,MAAM,SAAS,GAAG,EAAE,GAAG,EAAE,EACzE,OAAO,wBAAwB,wBAAwB,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,EAC7E,OAAO,OAAO,SAAS;AACvB,QAAM,iBAAiB,MAAM,OAAO;AACpC,QAAM,SAAmB,CAAC;AAC1B,MAAI,YAAY;AAChB,mBAAiB,SAAS,QAAQ,OAAO;AACxC,iBAAa,MAAM;AACnB,QAAI,YAAY,gBAAgB;AAC/B,cAAQ,OAAO,MAAM,qCAAqC;AAC1D,cAAQ,KAAK,CAAC;AAAA,IACf;AACA,WAAO,KAAK,KAAK;AAAA,EAClB;AACA,QAAM,QAAQ,OAAO,OAAO,MAAM;AAElC,MAAI,MAAM,WAAW,GAAG;AACvB,YAAQ,OAAO,MAAM,qCAAqC;AAC1D,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,SAAS,KAAK;AACpB,QAAM,UAAU,KAAK;AACrB,QAAM,SAAS,KAAK;AAEpB,MAAI,CAAC,cAAc,SAAS,MAAM,GAAG;AACpC,YAAQ,OAAO,MAAM,0BAA0B,MAAM;AAAA,CAAK;AAC1D,YAAQ,KAAK,CAAC;AAAA,EACf;AAEA,QAAM,eAAe,WAAW,QAAQ,SAAS;AAEjD,QAAM,gBAAgB,iBAAiB,OAAO;AAE9C,MAAI;AACH,QAAI,WAAW,MAAM,KAAK;AAE1B,QAAI,QAAQ;AACX,iBAAW,SAAS,OAAO,MAAM;AAAA,IAClC;AAEA,eAAW,SAAS;AAAA,MACnB;AAAA,MACA,cAAc,YAAY,KAAK,CAAC;AAAA,IACjC;AAEA,UAAM,eAAe,MAAM,SAAS,SAAS;AAC7C,YAAQ,OAAO,MAAM,YAAY;AAAA,EAClC,SAAS,KAAK;AACb,YAAQ,OAAO,MAAM,UAAW,IAAc,OAAO;AAAA,CAAI;AACzD,YAAQ,KAAK,CAAC;AAAA,EACf;AACD,CAAC;;;AJrDF,IAAMC,WAAUC,eAAc,YAAY,GAAG;AAC7C,IAAM,EAAE,SAAAC,SAAQ,IAAIF,SAAQ,oBAAoB;AAEhD,IAAI,QAAQ,KAAK,UAAU,GAAG;AAC7B,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,4BAAyB;AACjE,QAAM,eAAe;AACtB,OAAO;AACN,QAAM,UAAU,IAAIG,SAAQ;AAC5B,UAAQ,KAAK,OAAO,EAAE,YAAY,gCAAgC,EAAE,QAAQD,QAAO;AAEnF,UAAQ,WAAW,eAAe;AAClC,UAAQ,WAAW,WAAW;AAC9B,UAAQ,WAAW,UAAU;AAC7B,UAAQ,WAAW,WAAW;AAC9B,UAAQ,WAAW,YAAY;AAC/B,UAAQ,MAAM;AACf;","names":["createRequire","Command","path","Command","require","Command","path","Command","Command","require","createRequire","version","Command"]}
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/concurrency/pool.ts
4
+ async function processPool(items, processor, concurrency) {
5
+ const results = new Array(items.length);
6
+ const executing = /* @__PURE__ */ new Set();
7
+ let firstError;
8
+ let hasError = false;
9
+ for (let i = 0; i < items.length; i++) {
10
+ const index = i;
11
+ const p = processor(items[index]).then((result) => {
12
+ results[index] = result;
13
+ }).catch((err) => {
14
+ if (!hasError) {
15
+ hasError = true;
16
+ firstError = err;
17
+ }
18
+ }).finally(() => {
19
+ executing.delete(p);
20
+ });
21
+ executing.add(p);
22
+ if (executing.size >= concurrency) {
23
+ await Promise.race(executing);
24
+ }
25
+ }
26
+ await Promise.all(executing);
27
+ if (hasError) {
28
+ throw firstError;
29
+ }
30
+ return results;
31
+ }
32
+
33
+ // src/output/results.ts
34
+ import path from "path";
35
+
36
+ // src/output/format-bytes.ts
37
+ function formatBytes(bytes) {
38
+ if (bytes <= 0) return "0 B";
39
+ const GB = 1024 * 1024 * 1024;
40
+ const MB = 1024 * 1024;
41
+ const KB = 1024;
42
+ if (bytes >= GB) return `${(bytes / GB).toFixed(2)} GB`;
43
+ if (bytes >= MB) return `${(bytes / MB).toFixed(2)} MB`;
44
+ if (bytes >= KB) return `${(bytes / KB).toFixed(2)} KB`;
45
+ return `${bytes} B`;
46
+ }
47
+
48
+ // src/output/results.ts
49
+ function calcReduction(originalSize, newSize) {
50
+ return originalSize > 0 ? Math.round((1 - newSize / originalSize) * 100) : 0;
51
+ }
52
+ function formatFileResult(result) {
53
+ const filename = path.basename(result.inputPath);
54
+ const original = formatBytes(result.originalSize);
55
+ const newSize = formatBytes(result.newSize);
56
+ const reduction = calcReduction(result.originalSize, result.newSize);
57
+ const pct = reduction < 0 ? `+${Math.abs(reduction)}%` : `${reduction}%`;
58
+ return `${filename} ${original} -> ${newSize} (${pct})`;
59
+ }
60
+ function buildJsonOutput(results, errors, totalDuration) {
61
+ const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
62
+ const totalNew = results.reduce((sum, r) => sum + r.newSize, 0);
63
+ const totalReduction = calcReduction(totalOriginal, totalNew);
64
+ return {
65
+ files: results.map((r) => ({
66
+ input: r.inputPath,
67
+ output: r.outputPath,
68
+ originalSize: r.originalSize,
69
+ newSize: r.newSize,
70
+ reduction: calcReduction(r.originalSize, r.newSize),
71
+ format: r.format,
72
+ duration: r.duration
73
+ })),
74
+ summary: {
75
+ totalFiles: results.length,
76
+ totalOriginalSize: totalOriginal,
77
+ totalNewSize: totalNew,
78
+ totalReduction,
79
+ duration: totalDuration,
80
+ errors: errors.length
81
+ },
82
+ errors
83
+ };
84
+ }
85
+ function formatSummary(results, errors, totalDuration) {
86
+ const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
87
+ const totalNew = results.reduce((sum, r) => sum + r.newSize, 0);
88
+ const reduction = totalOriginal > 0 ? Math.round((1 - totalNew / totalOriginal) * 100) : 0;
89
+ const duration = `${(totalDuration / 1e3).toFixed(1)}s`;
90
+ const fileCount = results.length;
91
+ let summary = `${fileCount} files | ${formatBytes(totalOriginal)} -> ${formatBytes(totalNew)} (${reduction}%) | ${duration}`;
92
+ if (errors.length > 0) {
93
+ summary += ` | ${errors.length} errors`;
94
+ }
95
+ return summary;
96
+ }
97
+
98
+ // src/pipeline/discover.ts
99
+ import fs2 from "fs/promises";
100
+ import path2 from "path";
101
+
102
+ // src/pipeline/filter.ts
103
+ import fs from "fs/promises";
104
+ import sharp from "sharp";
105
+ async function detectFormat(filePath) {
106
+ try {
107
+ const handle = await fs.open(filePath, "r");
108
+ const buffer = Buffer.alloc(12);
109
+ try {
110
+ await handle.read(buffer, 0, 12, 0);
111
+ } finally {
112
+ await handle.close();
113
+ }
114
+ if (buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) return "jpeg";
115
+ if (buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10)
116
+ return "png";
117
+ if (buffer[0] === 82 && buffer[1] === 73 && buffer[2] === 70 && buffer[3] === 70 && buffer[8] === 87 && buffer[9] === 69 && buffer[10] === 66 && buffer[11] === 80)
118
+ return "webp";
119
+ if (buffer[0] === 71 && buffer[1] === 73 && buffer[2] === 70) return "gif";
120
+ const ext = filePath.split(".").pop()?.toLowerCase();
121
+ if (ext === "avif" || ext === "heic" || ext === "heif") {
122
+ try {
123
+ const metadata = await sharp(filePath).metadata();
124
+ if (metadata.format === "heif") return "avif";
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ return null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // src/pipeline/discover.ts
136
+ async function collectImageFiles(inputPath, exclude = []) {
137
+ const stats = await fs2.stat(inputPath);
138
+ const isDirectory = stats.isDirectory();
139
+ const files = [];
140
+ if (isDirectory) {
141
+ for await (const file of discoverFiles(inputPath, exclude)) {
142
+ const fmt = await detectFormat(file);
143
+ if (fmt) files.push({ path: file, format: fmt });
144
+ }
145
+ } else {
146
+ const fmt = await detectFormat(inputPath);
147
+ if (fmt) files.push({ path: inputPath, format: fmt });
148
+ }
149
+ return { files, isDirectory };
150
+ }
151
+ async function* discoverFiles(dirPath, exclude) {
152
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
153
+ for (const entry of entries) {
154
+ if (entry.name.startsWith(".")) continue;
155
+ if (entry.isSymbolicLink()) continue;
156
+ const fullPath = path2.resolve(dirPath, entry.name);
157
+ if (entry.isDirectory()) {
158
+ if (exclude?.includes(entry.name)) continue;
159
+ yield* discoverFiles(fullPath, exclude);
160
+ } else if (entry.isFile()) {
161
+ yield fullPath;
162
+ }
163
+ }
164
+ }
165
+
166
+ // src/pipeline/optimize.ts
167
+ import fs4 from "fs/promises";
168
+ import sharp2 from "sharp";
169
+
170
+ // src/pipeline/destination.ts
171
+ import fs3 from "fs/promises";
172
+ import path3 from "path";
173
+ async function createDestinationPath(filePath, options) {
174
+ const relativePath = path3.relative(options.sourcePath, filePath);
175
+ if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
176
+ throw new Error(`Path traversal detected: ${filePath}`);
177
+ }
178
+ let outputPath = path3.join(path3.resolve(options.destination), options.outputFolder, relativePath);
179
+ if (options.toFormat) {
180
+ const ext = path3.extname(outputPath);
181
+ outputPath = `${outputPath.slice(0, outputPath.length - ext.length)}.${options.toFormat}`;
182
+ }
183
+ await fs3.mkdir(path3.dirname(outputPath), { recursive: true });
184
+ return outputPath;
185
+ }
186
+
187
+ // src/pipeline/optimize.ts
188
+ function getFormatOptions(quality) {
189
+ return {
190
+ jpeg: { mozjpeg: true, quality },
191
+ png: { quality },
192
+ webp: { quality },
193
+ avif: { quality },
194
+ gif: {}
195
+ };
196
+ }
197
+ async function optimizeImage(filePath, options) {
198
+ const start = performance.now();
199
+ const format = options.detectedFormat;
200
+ const outputFormat = options.format ?? format;
201
+ const destinationPath = await createDestinationPath(filePath, {
202
+ destination: options.destination,
203
+ outputFolder: options.outputFolder,
204
+ sourcePath: options.sourcePath,
205
+ toFormat: options.format
206
+ });
207
+ const formatOptions = getFormatOptions(options.quality);
208
+ let pipeline = sharp2(filePath, {
209
+ animated: format === "gif" || format === "webp" || format === "avif"
210
+ });
211
+ if (options.resize !== void 0) {
212
+ pipeline = pipeline.resize(options.resize);
213
+ }
214
+ pipeline = pipeline.toFormat(
215
+ outputFormat,
216
+ formatOptions[outputFormat] ?? {}
217
+ );
218
+ await pipeline.toFile(destinationPath);
219
+ const [originalStat, newStat] = await Promise.all([fs4.stat(filePath), fs4.stat(destinationPath)]);
220
+ const duration = performance.now() - start;
221
+ return {
222
+ inputPath: filePath,
223
+ outputPath: destinationPath,
224
+ originalSize: originalStat.size,
225
+ newSize: newStat.size,
226
+ format: outputFormat,
227
+ duration
228
+ };
229
+ }
230
+
231
+ // src/commands/hook.ts
232
+ import fs5 from "fs/promises";
233
+ import path4 from "path";
234
+ import { Command } from "commander";
235
+ var HOOK_MARKER = "# bulki-pre-commit-hook";
236
+ var HOOK_END_MARKER = "# bulki-pre-commit-hook-end";
237
+ var HOOK_SCRIPT = `#!/bin/sh
238
+ ${HOOK_MARKER}
239
+ # Auto-optimize staged images before commit
240
+ # Installed by: bulki hook install
241
+ # Remove with: bulki hook uninstall
242
+
243
+ git diff --cached --name-only -z --diff-filter=ACM | grep -zE '\\.(jpe?g|png|webp|gif|avif)$' | while IFS= read -r -d '' img; do
244
+ if [ -f "$img" ]; then
245
+ dir=$(mktemp -d)
246
+ npx @puntoycoma/bulki compress "$img" -d "$dir" -o "." -q 80 2>/dev/null
247
+ optimized="$dir/$(basename "$img")"
248
+ if [ -f "$optimized" ]; then
249
+ mv "$optimized" "$img"
250
+ fi
251
+ rm -rf "$dir"
252
+ git add "$img"
253
+ fi
254
+ done
255
+
256
+ echo "bulki: done"
257
+ ${HOOK_END_MARKER}
258
+ `;
259
+ async function installHook() {
260
+ const gitDir = await findGitDir(process.cwd());
261
+ if (!gitDir) {
262
+ console.error("Not a git repository. Run this from inside a git project.");
263
+ process.exit(1);
264
+ }
265
+ const hooksDir = path4.join(gitDir, "hooks");
266
+ const hookPath = path4.join(hooksDir, "pre-commit");
267
+ await fs5.mkdir(hooksDir, { recursive: true });
268
+ try {
269
+ const existing = await fs5.readFile(hookPath, "utf-8");
270
+ if (existing.includes(HOOK_MARKER)) {
271
+ console.log("bulki hook is already installed.");
272
+ return;
273
+ }
274
+ await fs5.appendFile(hookPath, `
275
+ ${HOOK_SCRIPT}`);
276
+ console.log("bulki hook appended to existing pre-commit hook.");
277
+ } catch {
278
+ await fs5.writeFile(hookPath, HOOK_SCRIPT, { mode: 493 });
279
+ console.log("bulki pre-commit hook installed.");
280
+ }
281
+ console.log("Staged images will be auto-optimized before each commit.");
282
+ }
283
+ async function uninstallHook() {
284
+ const gitDir = await findGitDir(process.cwd());
285
+ if (!gitDir) {
286
+ console.error("Not a git repository.");
287
+ process.exit(1);
288
+ }
289
+ const hookPath = path4.join(gitDir, "hooks", "pre-commit");
290
+ try {
291
+ const content = await fs5.readFile(hookPath, "utf-8");
292
+ if (!content.includes(HOOK_MARKER)) {
293
+ console.log("No bulki hook found in pre-commit.");
294
+ return;
295
+ }
296
+ const lines = content.split("\n");
297
+ const markerIndex = lines.findIndex((l) => l.includes(HOOK_MARKER));
298
+ let endIndex = lines.findIndex((l) => l.includes(HOOK_END_MARKER));
299
+ if (endIndex === -1) endIndex = lines.length - 1;
300
+ const before = lines.slice(0, markerIndex).join("\n");
301
+ const after = lines.slice(endIndex + 1).join("\n");
302
+ const trimmed = `${before}
303
+ ${after}`.trim();
304
+ if (trimmed === "" || trimmed === "#!/bin/sh") {
305
+ await fs5.unlink(hookPath);
306
+ console.log("bulki pre-commit hook removed.");
307
+ } else {
308
+ await fs5.writeFile(hookPath, `${trimmed}
309
+ `, { mode: 493 });
310
+ console.log("bulki section removed from pre-commit hook.");
311
+ }
312
+ } catch {
313
+ console.log("No pre-commit hook found.");
314
+ }
315
+ }
316
+ var hookCommand = new Command("hook").description(
317
+ "Manage git pre-commit hook for auto image optimization"
318
+ );
319
+ hookCommand.command("install").description("Install pre-commit hook to optimize images on commit").action(async () => {
320
+ await installHook();
321
+ });
322
+ hookCommand.command("uninstall").description("Remove the bulki pre-commit hook").action(async () => {
323
+ await uninstallHook();
324
+ });
325
+ async function findGitDir(startPath) {
326
+ let current = path4.resolve(startPath);
327
+ while (true) {
328
+ const gitPath = path4.join(current, ".git");
329
+ try {
330
+ await fs5.stat(gitPath);
331
+ return gitPath;
332
+ } catch {
333
+ const parent = path4.dirname(current);
334
+ if (parent === current) return null;
335
+ current = parent;
336
+ }
337
+ }
338
+ }
339
+
340
+ // src/commands/watch.ts
341
+ import fs6 from "fs";
342
+ import path5 from "path";
343
+ import { Command as Command2 } from "commander";
344
+
345
+ // src/validation/options.ts
346
+ import { availableParallelism } from "os";
347
+ function parseSize(input) {
348
+ const match = /^(\d+(?:\.\d+)?)(b|kb|mb)$/i.exec(input);
349
+ if (!match) {
350
+ throw new Error(`Invalid size format: "${input}". Expected format like 200kb, 1mb, 500b`);
351
+ }
352
+ const value = Number(match[1]);
353
+ const unit = match[2].toLowerCase();
354
+ if (unit === "b") return value;
355
+ if (unit === "kb") return value * 1024;
356
+ return value * 1024 * 1024;
357
+ }
358
+ var VALID_FORMATS = ["jpeg", "jpg", "png", "webp", "gif", "avif"];
359
+ var DEFAULTS = {
360
+ quality: 70,
361
+ destination: ".",
362
+ outputFolder: "bulki-output"
363
+ };
364
+ function validateOptions(raw) {
365
+ const quality = raw.quality ?? DEFAULTS.quality;
366
+ if (typeof quality !== "number" || !Number.isInteger(quality) || quality < 1 || quality > 100) {
367
+ throw new Error(`Invalid quality: must be an integer between 1 and 100`);
368
+ }
369
+ let format = raw.format;
370
+ if (format !== void 0 && !VALID_FORMATS.includes(format)) {
371
+ throw new Error(`Invalid format: "${format}" is not a supported format`);
372
+ }
373
+ if (format === "jpg") format = "jpeg";
374
+ const resize = raw.resize;
375
+ if (resize !== void 0) {
376
+ if (typeof resize !== "number" || !Number.isInteger(resize) || resize <= 0) {
377
+ throw new Error(`Invalid resize: must be a positive integer`);
378
+ }
379
+ }
380
+ const concurrency = raw.concurrency ?? availableParallelism();
381
+ if (typeof concurrency !== "number" || concurrency < 1 || concurrency > 32) {
382
+ throw new Error(`Invalid concurrency: must be between 1 and 32`);
383
+ }
384
+ let maxSize;
385
+ if (raw.maxSize !== void 0) {
386
+ maxSize = parseSize(raw.maxSize);
387
+ }
388
+ const outputFolder = raw.outputFolder ?? DEFAULTS.outputFolder;
389
+ if (outputFolder.includes("/") || outputFolder.includes("\\") || outputFolder.includes("..")) {
390
+ throw new Error("Invalid outputFolder: must be a simple directory name without path separators");
391
+ }
392
+ return {
393
+ quality,
394
+ format,
395
+ resize,
396
+ destination: raw.destination ?? DEFAULTS.destination,
397
+ outputFolder,
398
+ concurrency,
399
+ exclude: raw.exclude ?? [],
400
+ dryRun: raw.dryRun ?? false,
401
+ verbose: raw.verbose ?? false,
402
+ json: raw.json ?? false,
403
+ maxSize
404
+ };
405
+ }
406
+
407
+ // src/commands/watch.ts
408
+ async function startWatch(dirPath, opts = {}) {
409
+ const resolvedPath = path5.resolve(dirPath);
410
+ try {
411
+ const stats = await fs6.promises.stat(resolvedPath);
412
+ if (!stats.isDirectory()) {
413
+ console.error("Watch path must be a directory");
414
+ process.exit(1);
415
+ }
416
+ } catch {
417
+ console.error(`Path not found: ${resolvedPath}`);
418
+ process.exit(1);
419
+ }
420
+ const quality = opts.quality ?? DEFAULTS.quality;
421
+ const format = opts.format === "jpg" ? "jpeg" : opts.format;
422
+ const resize = opts.resize;
423
+ const destination = opts.destination ?? DEFAULTS.destination;
424
+ const outputFolder = opts.outputFolder ?? DEFAULTS.outputFolder;
425
+ console.log(`Watching ${resolvedPath} for new images...`);
426
+ console.log("Press Ctrl+C to stop.\n");
427
+ const processing = /* @__PURE__ */ new Set();
428
+ const watcher = fs6.watch(resolvedPath, { recursive: true }, async (_eventType, filename) => {
429
+ if (!filename) return;
430
+ const fullPath = path5.resolve(resolvedPath, filename);
431
+ if (processing.has(fullPath)) return;
432
+ processing.add(fullPath);
433
+ const resolvedDest = destination === "." ? resolvedPath : path5.resolve(destination);
434
+ const outputDir = path5.join(resolvedDest, outputFolder);
435
+ if (fullPath === outputDir || fullPath.startsWith(`${outputDir}${path5.sep}`)) {
436
+ processing.delete(fullPath);
437
+ return;
438
+ }
439
+ await new Promise((r) => setTimeout(r, 100));
440
+ try {
441
+ await fs6.promises.stat(fullPath);
442
+ } catch {
443
+ processing.delete(fullPath);
444
+ return;
445
+ }
446
+ const detectedFormat = await detectFormat(fullPath);
447
+ if (!detectedFormat) {
448
+ processing.delete(fullPath);
449
+ return;
450
+ }
451
+ try {
452
+ const result = await optimizeImage(fullPath, {
453
+ quality,
454
+ format,
455
+ resize,
456
+ destination,
457
+ outputFolder,
458
+ concurrency: 1,
459
+ exclude: [],
460
+ dryRun: false,
461
+ verbose: false,
462
+ json: false,
463
+ sourcePath: resolvedPath,
464
+ detectedFormat
465
+ });
466
+ console.log(formatFileResult(result));
467
+ } catch (err) {
468
+ console.error(`Error processing ${filename}: ${err.message}`);
469
+ } finally {
470
+ setTimeout(() => processing.delete(fullPath), 1e3);
471
+ }
472
+ });
473
+ process.on("SIGINT", () => {
474
+ watcher.close();
475
+ console.log("\nWatch stopped.");
476
+ process.exit(0);
477
+ });
478
+ }
479
+ var watchCommand = new Command2("watch").description("Watch a directory and auto-optimize new images").argument("<path>", "Directory to watch").option("-q, --quality <n>", "Output quality (1-100)", (v) => parseInt(v, 10), 70).option("-f, --format <fmt>", "Output format (jpeg, png, webp, gif, avif)").option("-r, --resize <width>", "Resize to width (px)", (v) => parseInt(v, 10)).option("-d, --destination <path>", "Destination directory", ".").option("-o, --output-folder <name>", "Output subfolder name", "bulki-output").action(async (dirPath, opts) => {
480
+ await startWatch(dirPath, opts);
481
+ });
482
+
483
+ export {
484
+ processPool,
485
+ formatFileResult,
486
+ buildJsonOutput,
487
+ formatSummary,
488
+ detectFormat,
489
+ collectImageFiles,
490
+ getFormatOptions,
491
+ optimizeImage,
492
+ parseSize,
493
+ VALID_FORMATS,
494
+ validateOptions,
495
+ installHook,
496
+ uninstallHook,
497
+ hookCommand,
498
+ startWatch,
499
+ watchCommand
500
+ };
501
+ //# sourceMappingURL=chunk-OMZC5IWI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/concurrency/pool.ts","../src/output/results.ts","../src/output/format-bytes.ts","../src/pipeline/discover.ts","../src/pipeline/filter.ts","../src/pipeline/optimize.ts","../src/pipeline/destination.ts","../src/commands/hook.ts","../src/commands/watch.ts","../src/validation/options.ts"],"sourcesContent":["export async function processPool<T, R>(\n\titems: T[],\n\tprocessor: (item: T) => Promise<R>,\n\tconcurrency: number,\n): Promise<R[]> {\n\tconst results: R[] = new Array(items.length)\n\tconst executing = new Set<Promise<void>>()\n\tlet firstError: unknown\n\tlet hasError = false\n\n\tfor (let i = 0; i < items.length; i++) {\n\t\tconst index = i\n\t\tconst p: Promise<void> = processor(items[index])\n\t\t\t.then((result) => {\n\t\t\t\tresults[index] = result\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tif (!hasError) {\n\t\t\t\t\thasError = true\n\t\t\t\t\tfirstError = err\n\t\t\t\t}\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\texecuting.delete(p)\n\t\t\t})\n\t\texecuting.add(p)\n\n\t\tif (executing.size >= concurrency) {\n\t\t\tawait Promise.race(executing)\n\t\t}\n\t}\n\n\tawait Promise.all(executing)\n\n\tif (hasError) {\n\t\tthrow firstError\n\t}\n\n\treturn results\n}\n","import path from 'node:path'\nimport type { FileResult, JsonError, JsonOutput } from '../types/index.js'\nimport { formatBytes } from './format-bytes.js'\n\nfunction calcReduction(originalSize: number, newSize: number): number {\n\treturn originalSize > 0 ? Math.round((1 - newSize / originalSize) * 100) : 0\n}\n\nexport type { FileResult }\n\nexport function formatFileResult(result: FileResult): string {\n\tconst filename = path.basename(result.inputPath)\n\tconst original = formatBytes(result.originalSize)\n\tconst newSize = formatBytes(result.newSize)\n\tconst reduction = calcReduction(result.originalSize, result.newSize)\n\tconst pct = reduction < 0 ? `+${Math.abs(reduction)}%` : `${reduction}%`\n\treturn `${filename} ${original} -> ${newSize} (${pct})`\n}\n\nexport function buildJsonOutput(\n\tresults: FileResult[],\n\terrors: JsonError[],\n\ttotalDuration: number,\n): JsonOutput {\n\tconst totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0)\n\tconst totalNew = results.reduce((sum, r) => sum + r.newSize, 0)\n\tconst totalReduction = calcReduction(totalOriginal, totalNew)\n\n\treturn {\n\t\tfiles: results.map((r) => ({\n\t\t\tinput: r.inputPath,\n\t\t\toutput: r.outputPath,\n\t\t\toriginalSize: r.originalSize,\n\t\t\tnewSize: r.newSize,\n\t\t\treduction: calcReduction(r.originalSize, r.newSize),\n\t\t\tformat: r.format,\n\t\t\tduration: r.duration,\n\t\t})),\n\t\tsummary: {\n\t\t\ttotalFiles: results.length,\n\t\t\ttotalOriginalSize: totalOriginal,\n\t\t\ttotalNewSize: totalNew,\n\t\t\ttotalReduction,\n\t\t\tduration: totalDuration,\n\t\t\terrors: errors.length,\n\t\t},\n\t\terrors,\n\t}\n}\n\nexport function formatSummary(\n\tresults: FileResult[],\n\terrors: Error[],\n\ttotalDuration: number,\n): string {\n\tconst totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0)\n\tconst totalNew = results.reduce((sum, r) => sum + r.newSize, 0)\n\tconst reduction = totalOriginal > 0 ? Math.round((1 - totalNew / totalOriginal) * 100) : 0\n\tconst duration = `${(totalDuration / 1000).toFixed(1)}s`\n\tconst fileCount = results.length\n\tlet summary = `${fileCount} files | ${formatBytes(totalOriginal)} -> ${formatBytes(totalNew)} (${reduction}%) | ${duration}`\n\tif (errors.length > 0) {\n\t\tsummary += ` | ${errors.length} errors`\n\t}\n\treturn summary\n}\n","export function formatBytes(bytes: number): string {\n\tif (bytes <= 0) return '0 B'\n\tconst GB = 1024 * 1024 * 1024\n\tconst MB = 1024 * 1024\n\tconst KB = 1024\n\tif (bytes >= GB) return `${(bytes / GB).toFixed(2)} GB`\n\tif (bytes >= MB) return `${(bytes / MB).toFixed(2)} MB`\n\tif (bytes >= KB) return `${(bytes / KB).toFixed(2)} KB`\n\treturn `${bytes} B`\n}\n","import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { detectFormat } from './filter.js'\n\nexport async function collectImageFiles(\n\tinputPath: string,\n\texclude: string[] = [],\n): Promise<{ files: { path: string; format: string }[]; isDirectory: boolean }> {\n\tconst stats = await fs.stat(inputPath)\n\tconst isDirectory = stats.isDirectory()\n\tconst files: { path: string; format: string }[] = []\n\n\tif (isDirectory) {\n\t\tfor await (const file of discoverFiles(inputPath, exclude)) {\n\t\t\tconst fmt = await detectFormat(file)\n\t\t\tif (fmt) files.push({ path: file, format: fmt })\n\t\t}\n\t} else {\n\t\tconst fmt = await detectFormat(inputPath)\n\t\tif (fmt) files.push({ path: inputPath, format: fmt })\n\t}\n\n\treturn { files, isDirectory }\n}\n\nexport async function* discoverFiles(dirPath: string, exclude?: string[]): AsyncGenerator<string> {\n\tconst entries = await fs.readdir(dirPath, { withFileTypes: true })\n\tfor (const entry of entries) {\n\t\tif (entry.name.startsWith('.')) continue\n\t\tif (entry.isSymbolicLink()) continue\n\t\tconst fullPath = path.resolve(dirPath, entry.name)\n\t\tif (entry.isDirectory()) {\n\t\t\tif (exclude?.includes(entry.name)) continue\n\t\t\tyield* discoverFiles(fullPath, exclude)\n\t\t} else if (entry.isFile()) {\n\t\t\tyield fullPath\n\t\t}\n\t}\n}\n","import fs from 'node:fs/promises'\nimport sharp from 'sharp'\n\nexport async function detectFormat(filePath: string): Promise<string | null> {\n\ttry {\n\t\tconst handle = await fs.open(filePath, 'r')\n\t\tconst buffer = Buffer.alloc(12)\n\t\ttry {\n\t\t\tawait handle.read(buffer, 0, 12, 0)\n\t\t} finally {\n\t\t\tawait handle.close()\n\t\t}\n\n\t\t// JPEG\n\t\tif (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'jpeg'\n\t\t// PNG\n\t\tif (\n\t\t\tbuffer[0] === 0x89 &&\n\t\t\tbuffer[1] === 0x50 &&\n\t\t\tbuffer[2] === 0x4e &&\n\t\t\tbuffer[3] === 0x47 &&\n\t\t\tbuffer[4] === 0x0d &&\n\t\t\tbuffer[5] === 0x0a &&\n\t\t\tbuffer[6] === 0x1a &&\n\t\t\tbuffer[7] === 0x0a\n\t\t)\n\t\t\treturn 'png'\n\t\t// WebP (RIFF....WEBP)\n\t\tif (\n\t\t\tbuffer[0] === 0x52 &&\n\t\t\tbuffer[1] === 0x49 &&\n\t\t\tbuffer[2] === 0x46 &&\n\t\t\tbuffer[3] === 0x46 &&\n\t\t\tbuffer[8] === 0x57 &&\n\t\t\tbuffer[9] === 0x45 &&\n\t\t\tbuffer[10] === 0x42 &&\n\t\t\tbuffer[11] === 0x50\n\t\t)\n\t\t\treturn 'webp'\n\t\t// GIF\n\t\tif (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) return 'gif'\n\n\t\t// AVIF fallback via Sharp (ISOBMFF container makes magic bytes unreliable)\n\t\t// Only try Sharp for potential AVIF files (extension hint)\n\t\tconst ext = filePath.split('.').pop()?.toLowerCase()\n\t\tif (ext === 'avif' || ext === 'heic' || ext === 'heif') {\n\t\t\ttry {\n\t\t\t\tconst metadata = await sharp(filePath).metadata()\n\t\t\t\tif (metadata.format === 'heif') return 'avif'\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t}\n\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nexport async function isImage(filePath: string): Promise<boolean> {\n\treturn (await detectFormat(filePath)) !== null\n}\n","import fs from 'node:fs/promises'\nimport type { FormatEnum } from 'sharp'\nimport sharp from 'sharp'\nimport type { CompressOptions, FileResult, OptimizeParams } from '../types/index.js'\nimport { createDestinationPath } from './destination.js'\n\nexport function getFormatOptions(quality: number): Record<string, object> {\n\treturn {\n\t\tjpeg: { mozjpeg: true, quality },\n\t\tpng: { quality },\n\t\twebp: { quality },\n\t\tavif: { quality },\n\t\tgif: {},\n\t}\n}\n\nexport async function optimizeImage(\n\tfilePath: string,\n\toptions: OptimizeParams | (CompressOptions & { sourcePath: string; detectedFormat: string }),\n): Promise<FileResult> {\n\tconst start = performance.now()\n\n\tconst format = options.detectedFormat\n\n\tconst outputFormat = options.format ?? format\n\n\tconst destinationPath = await createDestinationPath(filePath, {\n\t\tdestination: options.destination,\n\t\toutputFolder: options.outputFolder,\n\t\tsourcePath: options.sourcePath,\n\t\ttoFormat: options.format,\n\t})\n\n\tconst formatOptions = getFormatOptions(options.quality)\n\n\tlet pipeline = sharp(filePath, {\n\t\tanimated: format === 'gif' || format === 'webp' || format === 'avif',\n\t})\n\n\tif (options.resize !== undefined) {\n\t\tpipeline = pipeline.resize(options.resize)\n\t}\n\n\tpipeline = pipeline.toFormat(\n\t\toutputFormat as keyof FormatEnum,\n\t\tformatOptions[outputFormat as keyof FormatEnum] ?? {},\n\t)\n\n\tawait pipeline.toFile(destinationPath)\n\n\tconst [originalStat, newStat] = await Promise.all([fs.stat(filePath), fs.stat(destinationPath)])\n\n\tconst duration = performance.now() - start\n\n\treturn {\n\t\tinputPath: filePath,\n\t\toutputPath: destinationPath,\n\t\toriginalSize: originalStat.size,\n\t\tnewSize: newStat.size,\n\t\tformat: outputFormat,\n\t\tduration,\n\t}\n}\n","import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nexport async function createDestinationPath(\n\tfilePath: string,\n\toptions: {\n\t\tdestination: string\n\t\toutputFolder: string\n\t\tsourcePath: string\n\t\ttoFormat?: string\n\t},\n): Promise<string> {\n\tconst relativePath = path.relative(options.sourcePath, filePath)\n\n\t// Prevent path traversal — relative path must not escape the source\n\tif (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n\t\tthrow new Error(`Path traversal detected: ${filePath}`)\n\t}\n\n\tlet outputPath = path.join(path.resolve(options.destination), options.outputFolder, relativePath)\n\n\tif (options.toFormat) {\n\t\tconst ext = path.extname(outputPath)\n\t\toutputPath = `${outputPath.slice(0, outputPath.length - ext.length)}.${options.toFormat}`\n\t}\n\n\tawait fs.mkdir(path.dirname(outputPath), { recursive: true })\n\treturn outputPath\n}\n","import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { Command } from 'commander'\n\nconst HOOK_MARKER = '# bulki-pre-commit-hook'\nconst HOOK_END_MARKER = '# bulki-pre-commit-hook-end'\n\nconst HOOK_SCRIPT = `#!/bin/sh\n${HOOK_MARKER}\n# Auto-optimize staged images before commit\n# Installed by: bulki hook install\n# Remove with: bulki hook uninstall\n\ngit diff --cached --name-only -z --diff-filter=ACM | grep -zE '\\\\.(jpe?g|png|webp|gif|avif)$' | while IFS= read -r -d '' img; do\n if [ -f \"$img\" ]; then\n dir=$(mktemp -d)\n npx @puntoycoma/bulki compress \"$img\" -d \"$dir\" -o \".\" -q 80 2>/dev/null\n optimized=\"$dir/$(basename \"$img\")\"\n if [ -f \"$optimized\" ]; then\n mv \"$optimized\" \"$img\"\n fi\n rm -rf \"$dir\"\n git add \"$img\"\n fi\ndone\n\necho \"bulki: done\"\n${HOOK_END_MARKER}\n`\n\nexport async function installHook(): Promise<void> {\n\tconst gitDir = await findGitDir(process.cwd())\n\tif (!gitDir) {\n\t\tconsole.error('Not a git repository. Run this from inside a git project.')\n\t\tprocess.exit(1)\n\t}\n\n\tconst hooksDir = path.join(gitDir, 'hooks')\n\tconst hookPath = path.join(hooksDir, 'pre-commit')\n\n\tawait fs.mkdir(hooksDir, { recursive: true })\n\n\ttry {\n\t\tconst existing = await fs.readFile(hookPath, 'utf-8')\n\t\tif (existing.includes(HOOK_MARKER)) {\n\t\t\tconsole.log('bulki hook is already installed.')\n\t\t\treturn\n\t\t}\n\t\tawait fs.appendFile(hookPath, `\\n${HOOK_SCRIPT}`)\n\t\tconsole.log('bulki hook appended to existing pre-commit hook.')\n\t} catch {\n\t\tawait fs.writeFile(hookPath, HOOK_SCRIPT, { mode: 0o755 })\n\t\tconsole.log('bulki pre-commit hook installed.')\n\t}\n\n\tconsole.log('Staged images will be auto-optimized before each commit.')\n}\n\nexport async function uninstallHook(): Promise<void> {\n\tconst gitDir = await findGitDir(process.cwd())\n\tif (!gitDir) {\n\t\tconsole.error('Not a git repository.')\n\t\tprocess.exit(1)\n\t}\n\n\tconst hookPath = path.join(gitDir, 'hooks', 'pre-commit')\n\n\ttry {\n\t\tconst content = await fs.readFile(hookPath, 'utf-8')\n\t\tif (!content.includes(HOOK_MARKER)) {\n\t\t\tconsole.log('No bulki hook found in pre-commit.')\n\t\t\treturn\n\t\t}\n\n\t\tconst lines = content.split('\\n')\n\t\tconst markerIndex = lines.findIndex((l) => l.includes(HOOK_MARKER))\n\n\t\tlet endIndex = lines.findIndex((l) => l.includes(HOOK_END_MARKER))\n\t\tif (endIndex === -1) endIndex = lines.length - 1\n\n\t\tconst before = lines.slice(0, markerIndex).join('\\n')\n\t\tconst after = lines.slice(endIndex + 1).join('\\n')\n\t\tconst trimmed = `${before}\\n${after}`.trim()\n\n\t\tif (trimmed === '' || trimmed === '#!/bin/sh') {\n\t\t\tawait fs.unlink(hookPath)\n\t\t\tconsole.log('bulki pre-commit hook removed.')\n\t\t} else {\n\t\t\tawait fs.writeFile(hookPath, `${trimmed}\\n`, { mode: 0o755 })\n\t\t\tconsole.log('bulki section removed from pre-commit hook.')\n\t\t}\n\t} catch {\n\t\tconsole.log('No pre-commit hook found.')\n\t}\n}\n\nexport const hookCommand = new Command('hook').description(\n\t'Manage git pre-commit hook for auto image optimization',\n)\n\nhookCommand\n\t.command('install')\n\t.description('Install pre-commit hook to optimize images on commit')\n\t.action(async () => {\n\t\tawait installHook()\n\t})\n\nhookCommand\n\t.command('uninstall')\n\t.description('Remove the bulki pre-commit hook')\n\t.action(async () => {\n\t\tawait uninstallHook()\n\t})\n\nasync function findGitDir(startPath: string): Promise<string | null> {\n\tlet current = path.resolve(startPath)\n\twhile (true) {\n\t\tconst gitPath = path.join(current, '.git')\n\t\ttry {\n\t\t\tawait fs.stat(gitPath)\n\t\t\treturn gitPath\n\t\t} catch {\n\t\t\tconst parent = path.dirname(current)\n\t\t\tif (parent === current) return null\n\t\t\tcurrent = parent\n\t\t}\n\t}\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { Command } from 'commander'\nimport { formatFileResult } from '../output/results.js'\nimport { detectFormat } from '../pipeline/filter.js'\nimport { optimizeImage } from '../pipeline/optimize.js'\nimport { DEFAULTS } from '../validation/options.js'\n\nexport interface WatchOptions {\n\tquality?: number\n\tformat?: string\n\tresize?: number\n\tdestination?: string\n\toutputFolder?: string\n}\n\nexport async function startWatch(dirPath: string, opts: WatchOptions = {}): Promise<void> {\n\tconst resolvedPath = path.resolve(dirPath)\n\n\t// Verify path exists and is a directory\n\ttry {\n\t\tconst stats = await fs.promises.stat(resolvedPath)\n\t\tif (!stats.isDirectory()) {\n\t\t\tconsole.error('Watch path must be a directory')\n\t\t\tprocess.exit(1)\n\t\t}\n\t} catch {\n\t\tconsole.error(`Path not found: ${resolvedPath}`)\n\t\tprocess.exit(1)\n\t}\n\n\tconst quality = opts.quality ?? DEFAULTS.quality\n\tconst format = opts.format === 'jpg' ? 'jpeg' : opts.format\n\tconst resize = opts.resize\n\tconst destination = opts.destination ?? DEFAULTS.destination\n\tconst outputFolder = opts.outputFolder ?? DEFAULTS.outputFolder\n\n\tconsole.log(`Watching ${resolvedPath} for new images...`)\n\tconsole.log('Press Ctrl+C to stop.\\n')\n\n\t// Track processed files to avoid duplicates (fs.watch can fire multiple events)\n\tconst processing = new Set<string>()\n\n\t// Use fs.watch with recursive option\n\tconst watcher = fs.watch(resolvedPath, { recursive: true }, async (_eventType, filename) => {\n\t\tif (!filename) return\n\n\t\tconst fullPath = path.resolve(resolvedPath, filename)\n\n\t\t// Skip if already processing this file\n\t\tif (processing.has(fullPath)) return\n\t\tprocessing.add(fullPath)\n\n\t\t// Skip files in the output folder to avoid infinite loops\n\t\tconst resolvedDest = destination === '.' ? resolvedPath : path.resolve(destination)\n\t\tconst outputDir = path.join(resolvedDest, outputFolder)\n\t\tif (fullPath === outputDir || fullPath.startsWith(`${outputDir}${path.sep}`)) {\n\t\t\tprocessing.delete(fullPath)\n\t\t\treturn\n\t\t}\n\n\t\t// Small delay to let file writes complete\n\t\tawait new Promise((r) => setTimeout(r, 100))\n\n\t\t// Check if file exists (could be a delete event)\n\t\ttry {\n\t\t\tawait fs.promises.stat(fullPath)\n\t\t} catch {\n\t\t\tprocessing.delete(fullPath)\n\t\t\treturn\n\t\t}\n\n\t\t// Check if it's an image\n\t\tconst detectedFormat = await detectFormat(fullPath)\n\t\tif (!detectedFormat) {\n\t\t\tprocessing.delete(fullPath)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await optimizeImage(fullPath, {\n\t\t\t\tquality,\n\t\t\t\tformat,\n\t\t\t\tresize,\n\t\t\t\tdestination,\n\t\t\t\toutputFolder,\n\t\t\t\tconcurrency: 1,\n\t\t\t\texclude: [],\n\t\t\t\tdryRun: false,\n\t\t\t\tverbose: false,\n\t\t\t\tjson: false,\n\t\t\t\tsourcePath: resolvedPath,\n\t\t\t\tdetectedFormat,\n\t\t\t})\n\t\t\tconsole.log(formatFileResult(result))\n\t\t} catch (err) {\n\t\t\tconsole.error(`Error processing ${filename}: ${(err as Error).message}`)\n\t\t} finally {\n\t\t\t// Remove from processing after a delay to debounce\n\t\t\tsetTimeout(() => processing.delete(fullPath), 1000)\n\t\t}\n\t})\n\n\t// Keep process alive and handle Ctrl+C\n\tprocess.on('SIGINT', () => {\n\t\twatcher.close()\n\t\tconsole.log('\\nWatch stopped.')\n\t\tprocess.exit(0)\n\t})\n}\n\nexport const watchCommand = new Command('watch')\n\t.description('Watch a directory and auto-optimize new images')\n\t.argument('<path>', 'Directory to watch')\n\t.option('-q, --quality <n>', 'Output quality (1-100)', (v) => parseInt(v, 10), 70)\n\t.option('-f, --format <fmt>', 'Output format (jpeg, png, webp, gif, avif)')\n\t.option('-r, --resize <width>', 'Resize to width (px)', (v) => parseInt(v, 10))\n\t.option('-d, --destination <path>', 'Destination directory', '.')\n\t.option('-o, --output-folder <name>', 'Output subfolder name', 'bulki-output')\n\t.action(async (dirPath: string, opts) => {\n\t\tawait startWatch(dirPath, opts)\n\t})\n","import { availableParallelism } from 'node:os'\nimport type { CompressOptions } from '../types/index.js'\n\nexport interface RawOptions {\n\tquality: number\n\tformat: string\n\tresize: number\n\tdestination: string\n\toutputFolder: string\n\tconcurrency: number\n\texclude: string[]\n\tdryRun: boolean\n\tverbose: boolean\n\tjson: boolean\n\tmaxSize: string\n}\n\nexport function parseSize(input: string): number {\n\tconst match = /^(\\d+(?:\\.\\d+)?)(b|kb|mb)$/i.exec(input)\n\tif (!match) {\n\t\tthrow new Error(`Invalid size format: \"${input}\". Expected format like 200kb, 1mb, 500b`)\n\t}\n\tconst value = Number(match[1])\n\tconst unit = match[2].toLowerCase()\n\tif (unit === 'b') return value\n\tif (unit === 'kb') return value * 1024\n\treturn value * 1024 * 1024\n}\n\nexport const VALID_FORMATS = ['jpeg', 'jpg', 'png', 'webp', 'gif', 'avif']\n\nexport const DEFAULTS = {\n\tquality: 70,\n\tdestination: '.',\n\toutputFolder: 'bulki-output',\n} as const\n\nexport function validateOptions(raw: Partial<RawOptions>): CompressOptions {\n\tconst quality = raw.quality ?? DEFAULTS.quality\n\tif (typeof quality !== 'number' || !Number.isInteger(quality) || quality < 1 || quality > 100) {\n\t\tthrow new Error(`Invalid quality: must be an integer between 1 and 100`)\n\t}\n\n\tlet format = raw.format\n\tif (format !== undefined && !VALID_FORMATS.includes(format)) {\n\t\tthrow new Error(`Invalid format: \"${format}\" is not a supported format`)\n\t}\n\tif (format === 'jpg') format = 'jpeg'\n\n\tconst resize = raw.resize\n\tif (resize !== undefined) {\n\t\tif (typeof resize !== 'number' || !Number.isInteger(resize) || resize <= 0) {\n\t\t\tthrow new Error(`Invalid resize: must be a positive integer`)\n\t\t}\n\t}\n\n\tconst concurrency = raw.concurrency ?? availableParallelism()\n\tif (typeof concurrency !== 'number' || concurrency < 1 || concurrency > 32) {\n\t\tthrow new Error(`Invalid concurrency: must be between 1 and 32`)\n\t}\n\n\tlet maxSize: number | undefined\n\tif (raw.maxSize !== undefined) {\n\t\tmaxSize = parseSize(raw.maxSize)\n\t}\n\n\tconst outputFolder = raw.outputFolder ?? DEFAULTS.outputFolder\n\tif (outputFolder.includes('/') || outputFolder.includes('\\\\') || outputFolder.includes('..')) {\n\t\tthrow new Error('Invalid outputFolder: must be a simple directory name without path separators')\n\t}\n\n\treturn {\n\t\tquality,\n\t\tformat,\n\t\tresize,\n\t\tdestination: raw.destination ?? DEFAULTS.destination,\n\t\toutputFolder,\n\t\tconcurrency,\n\t\texclude: raw.exclude ?? [],\n\t\tdryRun: raw.dryRun ?? false,\n\t\tverbose: raw.verbose ?? false,\n\t\tjson: raw.json ?? false,\n\t\tmaxSize,\n\t}\n}\n"],"mappings":"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","names":["fs","path","fs","path","fs","sharp","fs","path","sharp","fs","fs","path","fs","path","Command","path","fs","Command"]}