@puntoycoma/bulki 2.0.0 → 3.0.2

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.
@@ -1 +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"]}
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 { availableParallelism } from 'node:os'\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 { processPool } from '../concurrency/pool.js'\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 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\tconst processedResults = await processPool(\n\t\t\t\t\tfilePaths,\n\t\t\t\t\tasync (file) => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await optimizeImage(file.path, {\n\t\t\t\t\t\t\t\tquality: params.quality,\n\t\t\t\t\t\t\t\tformat: params.format,\n\t\t\t\t\t\t\t\tresize: params.resize,\n\t\t\t\t\t\t\t\tdestination: params.destination,\n\t\t\t\t\t\t\t\toutputFolder: params.outputFolder,\n\t\t\t\t\t\t\t\tconcurrency: 1,\n\t\t\t\t\t\t\t\texclude: [],\n\t\t\t\t\t\t\t\tdryRun: false,\n\t\t\t\t\t\t\t\tverbose: false,\n\t\t\t\t\t\t\t\tjson: false,\n\t\t\t\t\t\t\t\tsourcePath,\n\t\t\t\t\t\t\t\tdetectedFormat: file.format,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn result\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\terrors.push({ file: file.path, message: (err as Error).message })\n\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMath.min(availableParallelism(), 8),\n\t\t\t\t)\n\n\t\t\t\tconst results = processedResults.filter((r): r is FileResult => r !== null)\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":"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","names":["createRequire","Command","path","Command","require","Command","path","Command","Command","require","createRequire","version","Command"]}
@@ -228,6 +228,68 @@ async function optimizeImage(filePath, options) {
228
228
  };
229
229
  }
230
230
 
231
+ // src/validation/options.ts
232
+ import { availableParallelism } from "os";
233
+ function parseSize(input) {
234
+ const match = /^(\d+(?:\.\d+)?)(b|kb|mb)$/i.exec(input);
235
+ if (!match) {
236
+ throw new Error(`Invalid size format: "${input}". Expected format like 200kb, 1mb, 500b`);
237
+ }
238
+ const value = Number(match[1]);
239
+ const unit = match[2].toLowerCase();
240
+ if (unit === "b") return value;
241
+ if (unit === "kb") return value * 1024;
242
+ return value * 1024 * 1024;
243
+ }
244
+ var VALID_FORMATS = ["jpeg", "jpg", "png", "webp", "gif", "avif"];
245
+ var DEFAULTS = {
246
+ quality: 70,
247
+ destination: ".",
248
+ outputFolder: "bulki-output"
249
+ };
250
+ function validateOptions(raw) {
251
+ const quality = raw.quality ?? DEFAULTS.quality;
252
+ if (typeof quality !== "number" || !Number.isInteger(quality) || quality < 1 || quality > 100) {
253
+ throw new Error(`Invalid quality: must be an integer between 1 and 100`);
254
+ }
255
+ let format = raw.format;
256
+ if (format !== void 0 && !VALID_FORMATS.includes(format)) {
257
+ throw new Error(`Invalid format: "${format}" is not a supported format`);
258
+ }
259
+ if (format === "jpg") format = "jpeg";
260
+ const resize = raw.resize;
261
+ if (resize !== void 0) {
262
+ if (typeof resize !== "number" || !Number.isInteger(resize) || resize <= 0) {
263
+ throw new Error(`Invalid resize: must be a positive integer`);
264
+ }
265
+ }
266
+ const concurrency = raw.concurrency ?? availableParallelism();
267
+ if (typeof concurrency !== "number" || concurrency < 1 || concurrency > 32) {
268
+ throw new Error(`Invalid concurrency: must be between 1 and 32`);
269
+ }
270
+ let maxSize;
271
+ if (raw.maxSize !== void 0) {
272
+ maxSize = parseSize(raw.maxSize);
273
+ }
274
+ const outputFolder = raw.outputFolder ?? DEFAULTS.outputFolder;
275
+ if (outputFolder.includes("/") || outputFolder.includes("\\") || outputFolder.includes("..")) {
276
+ throw new Error("Invalid outputFolder: must be a simple directory name without path separators");
277
+ }
278
+ return {
279
+ quality,
280
+ format,
281
+ resize,
282
+ destination: raw.destination ?? DEFAULTS.destination,
283
+ outputFolder,
284
+ concurrency,
285
+ exclude: raw.exclude ?? [],
286
+ dryRun: raw.dryRun ?? false,
287
+ verbose: raw.verbose ?? false,
288
+ json: raw.json ?? false,
289
+ maxSize
290
+ };
291
+ }
292
+
231
293
  // src/commands/hook.ts
232
294
  import fs5 from "fs/promises";
233
295
  import path4 from "path";
@@ -240,16 +302,35 @@ ${HOOK_MARKER}
240
302
  # Installed by: bulki hook install
241
303
  # Remove with: bulki hook uninstall
242
304
 
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
305
+ STAGED=$(git diff --cached --name-only -z --diff-filter=ACM | tr '\\0' '\\n' | grep -iE '\\.(jpe?g|png|webp|gif|avif)$')
306
+
307
+ if [ -z "$STAGED" ]; then
308
+ exit 0
309
+ fi
310
+
311
+ echo "bulki: optimizing staged images..."
312
+
313
+ tmpdir=$(mktemp -d)
314
+ trap 'rm -rf "$tmpdir"' EXIT
315
+
316
+ echo "$STAGED" | while IFS= read -r img; do
244
317
  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"
318
+ ext="\${img##*.}"
319
+ case "$(echo "$ext" | tr '[:upper:]' '[:lower:]')" in
320
+ jpg|jpeg) fmt="jpeg" ;;
321
+ png) fmt="png" ;;
322
+ webp) fmt="webp" ;;
323
+ gif) fmt="gif" ;;
324
+ avif) fmt="avif" ;;
325
+ *) continue ;;
326
+ esac
327
+ tmpfile="$tmpdir/$(basename "$img")"
328
+ if npx --yes @puntoycoma/bulki pipe -f "$fmt" -q 80 < "$img" > "$tmpfile" 2>/dev/null; then
329
+ if [ -s "$tmpfile" ]; then
330
+ mv "$tmpfile" "$img"
331
+ git add "$img"
332
+ fi
250
333
  fi
251
- rm -rf "$dir"
252
- git add "$img"
253
334
  fi
254
335
  done
255
336
 
@@ -273,6 +354,7 @@ async function installHook() {
273
354
  }
274
355
  await fs5.appendFile(hookPath, `
275
356
  ${HOOK_SCRIPT}`);
357
+ await fs5.chmod(hookPath, 493);
276
358
  console.log("bulki hook appended to existing pre-commit hook.");
277
359
  } catch {
278
360
  await fs5.writeFile(hookPath, HOOK_SCRIPT, { mode: 493 });
@@ -341,70 +423,6 @@ async function findGitDir(startPath) {
341
423
  import fs6 from "fs";
342
424
  import path5 from "path";
343
425
  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
426
  async function startWatch(dirPath, opts = {}) {
409
427
  const resolvedPath = path5.resolve(dirPath);
410
428
  try {
@@ -424,12 +442,23 @@ async function startWatch(dirPath, opts = {}) {
424
442
  const outputFolder = opts.outputFolder ?? DEFAULTS.outputFolder;
425
443
  console.log(`Watching ${resolvedPath} for new images...`);
426
444
  console.log("Press Ctrl+C to stop.\n");
445
+ if (process.platform === "linux") {
446
+ console.log(
447
+ "Note: recursive watching may not work on all Linux systems. Use macOS or Windows for reliable subdirectory monitoring."
448
+ );
449
+ }
427
450
  const processing = /* @__PURE__ */ new Set();
451
+ let activeJobs = 0;
452
+ const maxJobs = 4;
428
453
  const watcher = fs6.watch(resolvedPath, { recursive: true }, async (_eventType, filename) => {
429
454
  if (!filename) return;
430
455
  const fullPath = path5.resolve(resolvedPath, filename);
431
456
  if (processing.has(fullPath)) return;
432
457
  processing.add(fullPath);
458
+ while (activeJobs >= maxJobs) {
459
+ await new Promise((r) => setTimeout(r, 100));
460
+ }
461
+ activeJobs++;
433
462
  const resolvedDest = destination === "." ? resolvedPath : path5.resolve(destination);
434
463
  const outputDir = path5.join(resolvedDest, outputFolder);
435
464
  if (fullPath === outputDir || fullPath.startsWith(`${outputDir}${path5.sep}`)) {
@@ -467,6 +496,7 @@ async function startWatch(dirPath, opts = {}) {
467
496
  } catch (err) {
468
497
  console.error(`Error processing ${filename}: ${err.message}`);
469
498
  } finally {
499
+ activeJobs--;
470
500
  setTimeout(() => processing.delete(fullPath), 1e3);
471
501
  }
472
502
  });
@@ -491,6 +521,7 @@ export {
491
521
  optimizeImage,
492
522
  parseSize,
493
523
  VALID_FORMATS,
524
+ DEFAULTS,
494
525
  validateOptions,
495
526
  installHook,
496
527
  uninstallHook,
@@ -498,4 +529,4 @@ export {
498
529
  startWatch,
499
530
  watchCommand
500
531
  };
501
- //# sourceMappingURL=chunk-OMZC5IWI.js.map
532
+ //# sourceMappingURL=chunk-7TFBGWBB.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/validation/options.ts","../src/commands/hook.ts","../src/commands/watch.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 { 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","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\nSTAGED=$(git diff --cached --name-only -z --diff-filter=ACM | tr '\\\\0' '\\\\n' | grep -iE '\\\\.(jpe?g|png|webp|gif|avif)$')\n\nif [ -z \"$STAGED\" ]; then\n exit 0\nfi\n\necho \"bulki: optimizing staged images...\"\n\ntmpdir=$(mktemp -d)\ntrap 'rm -rf \"$tmpdir\"' EXIT\n\necho \"$STAGED\" | while IFS= read -r img; do\n if [ -f \"$img\" ]; then\n ext=\"\\${img##*.}\"\n case \"$(echo \"$ext\" | tr '[:upper:]' '[:lower:]')\" in\n jpg|jpeg) fmt=\"jpeg\" ;;\n png) fmt=\"png\" ;;\n webp) fmt=\"webp\" ;;\n gif) fmt=\"gif\" ;;\n avif) fmt=\"avif\" ;;\n *) continue ;;\n esac\n tmpfile=\"$tmpdir/$(basename \"$img\")\"\n if npx --yes @puntoycoma/bulki pipe -f \"$fmt\" -q 80 < \"$img\" > \"$tmpfile\" 2>/dev/null; then\n if [ -s \"$tmpfile\" ]; then\n mv \"$tmpfile\" \"$img\"\n git add \"$img\"\n fi\n fi\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\tawait fs.chmod(hookPath, 0o755)\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\tif (process.platform === 'linux') {\n\t\tconsole.log(\n\t\t\t'Note: recursive watching may not work on all Linux systems. Use macOS or Windows for reliable subdirectory monitoring.',\n\t\t)\n\t}\n\n\t// Track processed files to avoid duplicates (fs.watch can fire multiple events)\n\tconst processing = new Set<string>()\n\tlet activeJobs = 0\n\tconst maxJobs = 4\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// Concurrency limit\n\t\twhile (activeJobs >= maxJobs) {\n\t\t\tawait new Promise((r) => setTimeout(r, 100))\n\t\t}\n\t\tactiveJobs++\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\tactiveJobs--\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"],"mappings":"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","names":["fs","path","fs","path","fs","sharp","fs","path","sharp","fs","fs","path","fs","path","Command","path","fs","Command"]}
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULTS,
3
4
  collectImageFiles,
4
5
  formatSummary,
5
6
  installHook,
@@ -7,7 +8,7 @@ import {
7
8
  processPool,
8
9
  startWatch,
9
10
  uninstallHook
10
- } from "./chunk-OMZC5IWI.js";
11
+ } from "./chunk-7TFBGWBB.js";
11
12
 
12
13
  // src/interactive/index.ts
13
14
  import { availableParallelism } from "os";
@@ -45,7 +46,7 @@ async function collectPresetOptions() {
45
46
  process.exit(0);
46
47
  }
47
48
  let format;
48
- let quality = 70;
49
+ let quality = DEFAULTS.quality;
49
50
  let resize;
50
51
  if (presetKey !== "custom") {
51
52
  const preset = PRESETS[presetKey];
@@ -68,8 +69,8 @@ async function collectPresetOptions() {
68
69
  }),
69
70
  quality: () => p.text({
70
71
  message: "Quality (1-100)",
71
- placeholder: "70",
72
- initialValue: "70",
72
+ placeholder: String(DEFAULTS.quality),
73
+ initialValue: String(DEFAULTS.quality),
73
74
  validate(value) {
74
75
  const n = parseInt(value ?? "", 10);
75
76
  if (Number.isNaN(n) || n < 1 || n > 100) return "Must be between 1 and 100";
@@ -147,14 +148,14 @@ async function runCompress() {
147
148
  }
148
149
  const destInput = await p.text({
149
150
  message: "Destination directory",
150
- placeholder: ".",
151
- initialValue: "."
151
+ placeholder: DEFAULTS.destination,
152
+ initialValue: DEFAULTS.destination
152
153
  });
153
154
  if (isCancel(destInput)) {
154
155
  p.cancel("Operation cancelled.");
155
156
  process.exit(0);
156
157
  }
157
- const destination = destInput || ".";
158
+ const destination = destInput || DEFAULTS.destination;
158
159
  const resizeLabel = resize ? `${resize}px` : "No resize";
159
160
  const formatLabel = format ?? "Keep original";
160
161
  p.note(
@@ -163,7 +164,7 @@ async function runCompress() {
163
164
  `Format: ${formatLabel}`,
164
165
  `Quality: ${quality}`,
165
166
  `Resize: ${resizeLabel}`,
166
- `Output: ${path.join(destination, "bulki-output")}`
167
+ `Output: ${path.join(destination, DEFAULTS.outputFolder)}`
167
168
  ].join("\n"),
168
169
  "Summary"
169
170
  );
@@ -177,7 +178,7 @@ async function runCompress() {
177
178
  format,
178
179
  resize,
179
180
  destination,
180
- outputFolder: "bulki-output",
181
+ outputFolder: DEFAULTS.outputFolder,
181
182
  concurrency: availableParallelism(),
182
183
  exclude: [],
183
184
  dryRun: false,
@@ -230,14 +231,14 @@ async function runWatch() {
230
231
  const { format, quality, resize } = await collectPresetOptions();
231
232
  const destInput = await p.text({
232
233
  message: "Destination directory",
233
- placeholder: ".",
234
- initialValue: "."
234
+ placeholder: DEFAULTS.destination,
235
+ initialValue: DEFAULTS.destination
235
236
  });
236
237
  if (isCancel(destInput)) {
237
238
  p.cancel("Operation cancelled.");
238
239
  process.exit(0);
239
240
  }
240
- const destination = destInput || ".";
241
+ const destination = destInput || DEFAULTS.destination;
241
242
  p.log.info(`Watching ${watchDir}... Press Ctrl+C to stop.`);
242
243
  await startWatch(watchDir, {
243
244
  quality,
@@ -291,4 +292,4 @@ async function runInteractive() {
291
292
  export {
292
293
  runInteractive
293
294
  };
294
- //# sourceMappingURL=interactive-LGL325GZ.js.map
295
+ //# sourceMappingURL=interactive-HBGX4FD6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/interactive/index.ts"],"sourcesContent":["import { availableParallelism } from 'node:os'\nimport path from 'node:path'\nimport * as p from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport { installHook, uninstallHook } from '../commands/hook.js'\nimport { startWatch } from '../commands/watch.js'\nimport { processPool } from '../concurrency/pool.js'\nimport { 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 { DEFAULTS } from '../validation/options.js'\n\nconst PRESETS = {\n\tweb: {\n\t\tlabel: 'Web optimized',\n\t\thint: 'WebP, quality 80, max 1920px',\n\t\toptions: { format: 'webp', quality: 80, resize: 1920 },\n\t},\n\tsocial: {\n\t\tlabel: 'Social media',\n\t\thint: 'PNG, quality 85, max 1080px — lossless colors',\n\t\toptions: { format: 'png', quality: 85, resize: 1080 },\n\t},\n\tcustom: {\n\t\tlabel: 'Custom settings',\n\t\thint: 'Choose each option manually',\n\t\toptions: null,\n\t},\n}\n\nasync function collectPresetOptions(): Promise<{\n\tformat: string | undefined\n\tquality: number\n\tresize: number | undefined\n}> {\n\tconst presetKey = await p.select({\n\t\tmessage: 'Choose a preset or configure manually',\n\t\toptions: [\n\t\t\t{ value: 'web', label: PRESETS.web.label, hint: PRESETS.web.hint },\n\t\t\t{ value: 'social', label: PRESETS.social.label, hint: PRESETS.social.hint },\n\t\t\t{ value: 'custom', label: PRESETS.custom.label, hint: PRESETS.custom.hint },\n\t\t],\n\t})\n\n\tif (isCancel(presetKey)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tlet format: string | undefined\n\tlet quality = DEFAULTS.quality\n\tlet resize: number | undefined\n\n\tif (presetKey !== 'custom') {\n\t\tconst preset = PRESETS[presetKey as keyof typeof PRESETS]\n\t\tconst opts = preset.options as { format: string; quality: number; resize: number }\n\t\tformat = opts.format\n\t\tquality = opts.quality\n\t\tresize = opts.resize\n\t} else {\n\t\t// Custom mode\n\t\tconst settings = await p.group(\n\t\t\t{\n\t\t\t\tformat: () =>\n\t\t\t\t\tp.select({\n\t\t\t\t\t\tmessage: 'Output format',\n\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t{ value: 'original', label: 'Keep original', hint: 'no conversion' },\n\t\t\t\t\t\t\t{ value: 'webp', label: 'WebP', hint: 'best compression/quality ratio' },\n\t\t\t\t\t\t\t{ value: 'avif', label: 'AVIF', hint: 'smallest files, slower encoding' },\n\t\t\t\t\t\t\t{ value: 'jpeg', label: 'JPEG', hint: 'universal compatibility' },\n\t\t\t\t\t\t\t{ value: 'png', label: 'PNG', hint: 'lossless' },\n\t\t\t\t\t\t],\n\t\t\t\t\t}),\n\t\t\t\tquality: () =>\n\t\t\t\t\tp.text({\n\t\t\t\t\t\tmessage: 'Quality (1-100)',\n\t\t\t\t\t\tplaceholder: String(DEFAULTS.quality),\n\t\t\t\t\t\tinitialValue: String(DEFAULTS.quality),\n\t\t\t\t\t\tvalidate(value) {\n\t\t\t\t\t\t\tconst n = parseInt(value ?? '', 10)\n\t\t\t\t\t\t\tif (Number.isNaN(n) || n < 1 || n > 100) return 'Must be between 1 and 100'\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\tresize: () =>\n\t\t\t\t\tp.select({\n\t\t\t\t\t\tmessage: 'Resize images?',\n\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t{ value: 'none', label: 'No resize', hint: 'keep original dimensions' },\n\t\t\t\t\t\t\t{ value: '1920', label: '1920px', hint: 'Full HD width' },\n\t\t\t\t\t\t\t{ value: '1080', label: '1080px', hint: 'HD width' },\n\t\t\t\t\t\t\t{ value: '800', label: '800px', hint: 'web thumbnails' },\n\t\t\t\t\t\t\t{ value: 'custom', label: 'Custom width...' },\n\t\t\t\t\t\t],\n\t\t\t\t\t}),\n\t\t\t},\n\t\t\t{\n\t\t\t\tonCancel: () => {\n\t\t\t\t\tp.cancel('Operation cancelled.')\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tlet resizeValue: number | undefined\n\t\tif (settings.resize === 'custom') {\n\t\t\tconst customWidth = await p.text({\n\t\t\t\tmessage: 'Enter custom width (px)',\n\t\t\t\tplaceholder: '1200',\n\t\t\t\tvalidate(value) {\n\t\t\t\t\tconst n = parseInt(value ?? '', 10)\n\t\t\t\t\tif (Number.isNaN(n) || n <= 0) return 'Must be a positive number'\n\t\t\t\t},\n\t\t\t})\n\t\t\tif (isCancel(customWidth)) {\n\t\t\t\tp.cancel('Operation cancelled.')\n\t\t\t\tprocess.exit(0)\n\t\t\t}\n\t\t\tresizeValue = parseInt(customWidth as string, 10)\n\t\t} else if (settings.resize !== 'none') {\n\t\t\tresizeValue = parseInt(settings.resize as string, 10)\n\t\t}\n\n\t\tformat = settings.format === 'original' ? undefined : (settings.format as string)\n\t\tquality = parseInt(settings.quality as string, 10)\n\t\tresize = resizeValue\n\t}\n\n\treturn { format, quality, resize }\n}\n\nasync function runCompress(): Promise<void> {\n\tconst { format, quality, resize } = await collectPresetOptions()\n\n\t// Path input\n\tconst inputPath = await p.text({\n\t\tmessage: 'Path to images (file or directory)',\n\t\tplaceholder: './photos',\n\t\tvalidate(value) {\n\t\t\tif (!value || value.trim() === '') return 'Path is required'\n\t\t},\n\t})\n\n\tif (isCancel(inputPath)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tconst resolvedPath = path.resolve(inputPath as string)\n\n\tlet filePaths: { path: string; format: string }[]\n\tlet isDirectory: boolean\n\ttry {\n\t\tconst collected = await collectImageFiles(resolvedPath, [])\n\t\tfilePaths = collected.files\n\t\tisDirectory = collected.isDirectory\n\t} catch {\n\t\tp.cancel(`Path not found: ${resolvedPath}`)\n\t\tprocess.exit(1)\n\t}\n\n\tif (filePaths.length === 0) {\n\t\tp.outro('No images found to process.')\n\t\treturn\n\t}\n\n\t// Destination input\n\tconst destInput = await p.text({\n\t\tmessage: 'Destination directory',\n\t\tplaceholder: DEFAULTS.destination,\n\t\tinitialValue: DEFAULTS.destination,\n\t})\n\n\tif (isCancel(destInput)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tconst destination = (destInput as string) || DEFAULTS.destination\n\n\t// Show summary\n\tconst resizeLabel = resize ? `${resize}px` : 'No resize'\n\tconst formatLabel = format ?? 'Keep original'\n\tp.note(\n\t\t[\n\t\t\t`Source: ${resolvedPath} (${filePaths.length} images found)`,\n\t\t\t`Format: ${formatLabel}`,\n\t\t\t`Quality: ${quality}`,\n\t\t\t`Resize: ${resizeLabel}`,\n\t\t\t`Output: ${path.join(destination, DEFAULTS.outputFolder)}`,\n\t\t].join('\\n'),\n\t\t'Summary',\n\t)\n\n\tconst confirmed = await p.confirm({ message: 'Start processing?' })\n\n\tif (isCancel(confirmed) || !confirmed) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Process images\n\tconst options: CompressOptions = {\n\t\tquality: quality,\n\t\tformat,\n\t\tresize,\n\t\tdestination,\n\t\toutputFolder: DEFAULTS.outputFolder,\n\t\tconcurrency: availableParallelism(),\n\t\texclude: [],\n\t\tdryRun: false,\n\t\tverbose: false,\n\t\tjson: false,\n\t}\n\n\tconst sourcePath = isDirectory ? resolvedPath : path.dirname(resolvedPath)\n\tconst startTime = performance.now()\n\tconst errors: Error[] = []\n\n\tconst bar = p.progress({ max: filePaths.length })\n\tbar.start('Processing images')\n\n\tconst processedResults = await processPool(\n\t\tfilePaths,\n\t\tasync (file) => {\n\t\t\ttry {\n\t\t\t\tconst result = await optimizeImage(file.path, {\n\t\t\t\t\t...options,\n\t\t\t\t\tsourcePath,\n\t\t\t\t\tdetectedFormat: file.format,\n\t\t\t\t})\n\t\t\t\tbar.advance(1, `Processed ${path.basename(file.path)}`)\n\t\t\t\treturn result\n\t\t\t} catch (err) {\n\t\t\t\terrors.push(err as Error)\n\t\t\t\tbar.advance(1, `Failed ${path.basename(file.path)}`)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\toptions.concurrency,\n\t)\n\n\tbar.stop('All images processed')\n\n\tconst validResults = processedResults.filter((r): r is FileResult => r !== null)\n\tconst totalDuration = performance.now() - startTime\n\n\tconst summary = formatSummary(validResults, errors, totalDuration)\n\tp.note(summary, 'Results')\n\n\tp.outro('Done! Your optimized images are ready.')\n}\n\nasync function runWatch(): Promise<void> {\n\tconst watchDir = await p.text({\n\t\tmessage: 'Directory to watch',\n\t\tplaceholder: './photos',\n\t\tvalidate(value) {\n\t\t\tif (!value?.trim()) return 'Path is required'\n\t\t},\n\t})\n\n\tif (isCancel(watchDir)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tconst { format, quality, resize } = await collectPresetOptions()\n\n\tconst destInput = await p.text({\n\t\tmessage: 'Destination directory',\n\t\tplaceholder: DEFAULTS.destination,\n\t\tinitialValue: DEFAULTS.destination,\n\t})\n\n\tif (isCancel(destInput)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tconst destination = (destInput as string) || DEFAULTS.destination\n\n\tp.log.info(`Watching ${watchDir}... Press Ctrl+C to stop.`)\n\n\tawait startWatch(watchDir as string, {\n\t\tquality,\n\t\tformat,\n\t\tresize,\n\t\tdestination,\n\t})\n}\n\nasync function runHook(): Promise<void> {\n\tconst hookAction = await p.select({\n\t\tmessage: 'Git hook action',\n\t\toptions: [\n\t\t\t{ value: 'install', label: 'Install', hint: 'auto-optimize images on commit' },\n\t\t\t{ value: 'uninstall', label: 'Uninstall', hint: 'remove the hook' },\n\t\t],\n\t})\n\n\tif (isCancel(hookAction)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tif (hookAction === 'install') {\n\t\tawait installHook()\n\t\tp.outro('Git hook installed! Images will be auto-optimized on commit.')\n\t} else {\n\t\tawait uninstallHook()\n\t\tp.outro('Git hook removed.')\n\t}\n}\n\nexport async function runInteractive(): Promise<void> {\n\tp.intro('Punto y Coma — bulki')\n\n\tconst action = await p.select({\n\t\tmessage: 'What do you want to do?',\n\t\toptions: [\n\t\t\t{ value: 'compress', label: 'Compress images', hint: 'optimize files or directories' },\n\t\t\t{ value: 'watch', label: 'Watch directory', hint: 'auto-optimize new images' },\n\t\t\t{ value: 'hook', label: 'Git hook', hint: 'auto-optimize on commit' },\n\t\t],\n\t})\n\n\tif (isCancel(action)) {\n\t\tp.cancel('Operation cancelled.')\n\t\tprocess.exit(0)\n\t}\n\n\tif (action === 'compress') {\n\t\tawait runCompress()\n\t} else if (action === 'watch') {\n\t\tawait runWatch()\n\t} else {\n\t\tawait runHook()\n\t}\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@puntoycoma/bulki",
3
- "version": "2.0.0",
4
- "description": "Ultra-fast CLI image optimizer with interactive TUI, MCP server for AI editors, git hooks, watch mode, and piping. Compress, resize, and convert JPEG, PNG, WebP, GIF, AVIF in bulk. Free and open source.",
3
+ "version": "3.0.2",
4
+ "description": "Bulk image optimizer CLI — compress, resize, convert JPEG/PNG/WebP/AVIF. Interactive mode, MCP server for AI editors, watch mode, git hooks. Free.",
5
5
  "keywords": [
6
6
  "image",
7
7
  "optimizer",
@@ -34,14 +34,26 @@
34
34
  "open-source",
35
35
  "image-compression",
36
36
  "image-optimization",
37
- "image-processing"
37
+ "image-processing",
38
+ "tinypng-alternative",
39
+ "imagemin-alternative",
40
+ "squoosh-alternative",
41
+ "sharp-cli",
42
+ "optimize-images",
43
+ "compress-images",
44
+ "webp-converter",
45
+ "avif-converter",
46
+ "image-resizer",
47
+ "performance",
48
+ "web-performance",
49
+ "lossless",
50
+ "lossy"
38
51
  ],
39
- "author": "Gabriel Rubio",
40
- "license": "MIT",
52
+ "author": "PuntoyComaTech",
53
+ "license": "AGPL-3.0-only",
41
54
  "type": "module",
42
55
  "bin": {
43
- "bulki": "./dist/bin/bulki.js",
44
- "gio": "./dist/bin/bulki.js"
56
+ "bulki": "./dist/bin/bulki.js"
45
57
  },
46
58
  "files": [
47
59
  "dist"
@@ -71,9 +83,13 @@
71
83
  "typescript": "^5.0.0",
72
84
  "vitest": "^3.0.0"
73
85
  },
86
+ "publishConfig": {
87
+ "access": "public",
88
+ "registry": "https://registry.npmjs.org/"
89
+ },
74
90
  "repository": {
75
91
  "type": "git",
76
- "url": "git+https://github.com/PuntoyComaTech/bulki.git"
92
+ "url": "https://github.com/PuntoyComaTech/bulki.git"
77
93
  },
78
94
  "bugs": {
79
95
  "url": "https://github.com/PuntoyComaTech/bulki/issues"