@puntoycoma/bulki 3.0.1 → 3.0.3
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.
- package/README.md +175 -116
- package/dist/bin/bulki.js +144 -220
- package/dist/bin/bulki.js.map +1 -1
- package/dist/chunk-YUAKNSBH.js +851 -0
- package/dist/chunk-YUAKNSBH.js.map +1 -0
- package/dist/interactive-MTL4CMVQ.js +566 -0
- package/dist/interactive-MTL4CMVQ.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-OMZC5IWI.js +0 -501
- package/dist/chunk-OMZC5IWI.js.map +0 -1
- package/dist/interactive-LGL325GZ.js +0 -294
- package/dist/interactive-LGL325GZ.js.map +0 -1
package/dist/bin/bulki.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
DEFAULTS,
|
|
4
|
+
PlanError,
|
|
5
|
+
UNSUPPORTED,
|
|
4
6
|
buildJsonOutput,
|
|
5
|
-
|
|
7
|
+
defaultConcurrency,
|
|
6
8
|
detectFormat,
|
|
9
|
+
displayPath,
|
|
10
|
+
encodeImage,
|
|
7
11
|
formatFileResult,
|
|
8
12
|
formatSummary,
|
|
9
|
-
getFormatOptions,
|
|
10
13
|
hookCommand,
|
|
11
|
-
|
|
14
|
+
parseInteger,
|
|
15
|
+
parsePathInput,
|
|
12
16
|
parseSize,
|
|
13
|
-
|
|
17
|
+
planJobs,
|
|
18
|
+
resolveOutputDir,
|
|
19
|
+
resolveUserPath,
|
|
20
|
+
runBatch,
|
|
21
|
+
validateEncodeSettings,
|
|
14
22
|
validateOptions,
|
|
15
23
|
watchCommand
|
|
16
|
-
} from "../chunk-
|
|
24
|
+
} from "../chunk-YUAKNSBH.js";
|
|
17
25
|
|
|
18
26
|
// src/bin/bulki.ts
|
|
19
27
|
import { createRequire as createRequire2 } from "module";
|
|
20
28
|
import { Command as Command4 } from "commander";
|
|
21
29
|
|
|
22
30
|
// src/commands/compress.ts
|
|
23
|
-
import path from "path";
|
|
24
31
|
import { Command } from "commander";
|
|
25
32
|
|
|
26
33
|
// src/output/progress.ts
|
|
@@ -41,291 +48,208 @@ function createProgressBar(total) {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// src/commands/compress.ts
|
|
44
|
-
var compressCommand = new Command("compress").description(
|
|
51
|
+
var compressCommand = new Command("compress").description(
|
|
52
|
+
"Compress images. Originals are never modified; optimized copies go to the output folder."
|
|
53
|
+
).argument("<paths...>", "Images and/or folders to optimize (quote paths with spaces)").option("-o, --output <dir>", "Folder where optimized images are written", DEFAULTS.output).option("-q, --quality <n>", "Output quality (1-100)", parseInteger, DEFAULTS.quality).option("-f, --format <fmt>", "Convert to jpeg, png, webp, gif or avif (default: keep format)").option(
|
|
54
|
+
"-r, --resize <width>",
|
|
55
|
+
"Max width in px (smaller images are never enlarged)",
|
|
56
|
+
parseInteger
|
|
57
|
+
).option("--concurrency <n>", "Images processed in parallel (1-32)", parseInteger).option("--exclude <names...>", "Folder names to skip").option("--dry-run", "Show what would be processed without writing files").option("--verbose", "Show one line per file").option("--json", "Output results as JSON").option("--max-size <size>", "Fail if any output is bigger than this (e.g. 200kb, 1mb)").option("-d, --destination <dir>", "Deprecated: base folder; output goes to <dir>/bulki-output").option("--output-folder <name>", "Deprecated: subfolder name inside --destination").addHelpText(
|
|
58
|
+
"after",
|
|
59
|
+
`
|
|
60
|
+
Examples:
|
|
61
|
+
bulki compress ./photos \u2192 ./bulki-output
|
|
62
|
+
bulki compress ./photos -o ./optimized -f webp
|
|
63
|
+
bulki compress "My Photos" -o "Web Ready" -r 1920
|
|
64
|
+
bulki compress *.png -f avif`
|
|
65
|
+
).action(async (inputPaths, opts, cmd) => {
|
|
66
|
+
const outputFromCli = cmd.getOptionValueSource("output") === "cli";
|
|
45
67
|
let options;
|
|
46
68
|
try {
|
|
47
|
-
options = validateOptions(opts);
|
|
69
|
+
options = validateOptions({ ...opts, output: outputFromCli ? opts.output : void 0 });
|
|
48
70
|
} catch (err) {
|
|
49
|
-
|
|
50
|
-
process.exit(1);
|
|
71
|
+
fail(err.message);
|
|
51
72
|
}
|
|
52
|
-
const
|
|
53
|
-
let
|
|
54
|
-
let isDirectory;
|
|
73
|
+
const inputs = inputPaths.map((p) => resolveUserPath(p));
|
|
74
|
+
let plan;
|
|
55
75
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
plan = await planJobs({
|
|
77
|
+
inputs,
|
|
78
|
+
output: options.output,
|
|
79
|
+
format: options.format,
|
|
80
|
+
exclude: options.exclude
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof PlanError) fail(err.message);
|
|
84
|
+
throw err;
|
|
62
85
|
}
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
const log = options.json ? () => {
|
|
87
|
+
} : console.log;
|
|
88
|
+
for (const s of plan.skipped) console.error(`Skipped ${s.file}: ${s.reason}`);
|
|
89
|
+
if (plan.jobs.length === 0) {
|
|
90
|
+
if (options.json) console.log(JSON.stringify(buildJsonOutput([], [], 0, options.output)));
|
|
91
|
+
else log("No images found to process.");
|
|
65
92
|
return;
|
|
66
93
|
}
|
|
67
94
|
if (options.dryRun) {
|
|
68
|
-
|
|
69
|
-
for (const
|
|
95
|
+
log(`Would optimize ${plan.jobs.length} image(s) into ${displayPath(options.output)}`);
|
|
96
|
+
for (const job of plan.jobs) log(` ${displayPath(job.input)} \u2192 ${displayPath(job.output)}`);
|
|
70
97
|
return;
|
|
71
98
|
}
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
...options,
|
|
83
|
-
sourcePath,
|
|
84
|
-
detectedFormat: file.format
|
|
85
|
-
});
|
|
86
|
-
if (options.verbose && !options.json) {
|
|
87
|
-
console.log(formatFileResult(result));
|
|
88
|
-
}
|
|
89
|
-
progress?.tick();
|
|
90
|
-
return result;
|
|
91
|
-
} catch (err) {
|
|
92
|
-
errors.push(err);
|
|
93
|
-
jsonErrors.push({ file: file.path, message: err.message });
|
|
94
|
-
progress?.tick();
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
options.concurrency
|
|
99
|
-
);
|
|
100
|
-
const validResults = processedResults.filter((r) => r !== null);
|
|
101
|
-
const totalDuration = performance.now() - startTime;
|
|
99
|
+
const showBar = !options.verbose && !options.json && process.stdout.isTTY;
|
|
100
|
+
const progress = showBar ? createProgressBar(plan.jobs.length) : null;
|
|
101
|
+
const batch = await runBatch(plan.jobs, options, options.concurrency, (job, result, error) => {
|
|
102
|
+
progress?.tick();
|
|
103
|
+
if (!options.verbose || options.json) return;
|
|
104
|
+
if (result) console.log(formatFileResult(result));
|
|
105
|
+
else console.error(`\u2717 ${displayPath(job.input)}: ${error?.message}`);
|
|
106
|
+
});
|
|
107
|
+
const maxSize = options.maxSize;
|
|
108
|
+
const exceeded = maxSize ? batch.results.filter((r) => r.newSize > maxSize) : [];
|
|
102
109
|
if (options.json) {
|
|
103
|
-
const output = buildJsonOutput(
|
|
104
|
-
if (
|
|
105
|
-
for (const
|
|
106
|
-
if (file.newSize > options.maxSize) {
|
|
107
|
-
file.exceeded = true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
+
const output = buildJsonOutput(batch.results, batch.errors, batch.duration, options.output);
|
|
111
|
+
if (maxSize) {
|
|
112
|
+
for (const f of output.files) if (f.newSize > maxSize) f.exceeded = true;
|
|
110
113
|
}
|
|
111
114
|
console.log(JSON.stringify(output));
|
|
112
115
|
} else {
|
|
113
|
-
console.log(formatSummary(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
const maxSize = options.maxSize;
|
|
119
|
-
if (maxSize) {
|
|
120
|
-
const exceeded = validResults.filter((r) => r.newSize > maxSize);
|
|
121
|
-
if (exceeded.length > 0) {
|
|
122
|
-
if (!options.json) {
|
|
123
|
-
for (const r of exceeded) {
|
|
124
|
-
console.warn(`File exceeds size budget: ${r.outputPath}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
process.exit(1);
|
|
116
|
+
console.log(formatSummary(batch.results, batch.errors, batch.duration));
|
|
117
|
+
if (!options.verbose) {
|
|
118
|
+
for (const e of batch.errors) console.error(`\u2717 ${displayPath(e.file)}: ${e.message}`);
|
|
128
119
|
}
|
|
120
|
+
if (batch.results.length > 0) console.log(`Saved to ${displayPath(options.output)}`);
|
|
121
|
+
for (const r of exceeded) console.warn(`File exceeds size budget: ${r.outputPath}`);
|
|
129
122
|
}
|
|
123
|
+
if (batch.errors.length > 0 || exceeded.length > 0) process.exitCode = 1;
|
|
130
124
|
});
|
|
125
|
+
function fail(message) {
|
|
126
|
+
console.error(`error: ${message}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
131
129
|
|
|
132
130
|
// src/commands/mcp.ts
|
|
133
131
|
import { createRequire } from "module";
|
|
134
|
-
import
|
|
132
|
+
import path from "path";
|
|
135
133
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
136
134
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
137
135
|
import { Command as Command2 } from "commander";
|
|
138
136
|
import { z } from "zod";
|
|
139
137
|
var require2 = createRequire(import.meta.url);
|
|
140
138
|
var { version } = require2("../../package.json");
|
|
139
|
+
function text(payload) {
|
|
140
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
141
|
+
}
|
|
141
142
|
var mcpCommand = new Command2("mcp").description("Start MCP server for AI editor integration (Claude Code, Cursor, etc.)").action(async () => {
|
|
142
|
-
const server = new McpServer({
|
|
143
|
-
name: "bulki",
|
|
144
|
-
version
|
|
145
|
-
});
|
|
143
|
+
const server = new McpServer({ name: "bulki", version });
|
|
146
144
|
server.tool(
|
|
147
145
|
"compress",
|
|
148
|
-
"Compress and optimize images
|
|
146
|
+
"Compress and optimize images (JPEG, PNG, WebP, GIF, AVIF). Originals are never modified; optimized copies are written to `output`.",
|
|
149
147
|
{
|
|
150
148
|
path: z.string().describe("File or directory path to compress"),
|
|
151
|
-
quality: z.number().min(1).max(100).default(70).describe("Output quality (1-100)"),
|
|
149
|
+
quality: z.number().int().min(1).max(100).default(70).describe("Output quality (1-100)"),
|
|
152
150
|
format: z.enum(["jpeg", "png", "webp", "gif", "avif"]).optional().describe("Output format (omit to keep original)"),
|
|
153
|
-
resize: z.number().positive().optional().describe("Max width in pixels"),
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
resize: z.number().int().positive().optional().describe("Max width in pixels (never enlarges)"),
|
|
152
|
+
output: z.string().optional().describe("Folder where optimized images are written (default: ./bulki-output)"),
|
|
153
|
+
destination: z.string().optional().describe("Deprecated: base folder, use `output`"),
|
|
154
|
+
outputFolder: z.string().optional().describe("Deprecated: subfolder name, use `output`"),
|
|
156
155
|
maxSize: z.string().optional().describe("Max file size budget (e.g., 200kb, 1mb)")
|
|
157
156
|
},
|
|
158
157
|
async (params) => {
|
|
159
|
-
if (params.outputFolder.includes("/") || params.outputFolder.includes("\\") || params.outputFolder.includes("..")) {
|
|
160
|
-
return {
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
type: "text",
|
|
164
|
-
text: JSON.stringify({
|
|
165
|
-
error: "Invalid outputFolder: must be a simple directory name"
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
]
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const resolvedPath = path2.resolve(params.path);
|
|
172
|
-
let filePaths;
|
|
173
|
-
let isDirectory;
|
|
174
158
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (filePaths.length === 0) {
|
|
189
|
-
return {
|
|
190
|
-
content: [
|
|
191
|
-
{
|
|
192
|
-
type: "text",
|
|
193
|
-
text: JSON.stringify({ files: [], summary: { totalFiles: 0 } })
|
|
194
|
-
}
|
|
195
|
-
]
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
const sourcePath = isDirectory ? resolvedPath : path2.dirname(resolvedPath);
|
|
199
|
-
const startTime = performance.now();
|
|
200
|
-
const results = [];
|
|
201
|
-
const errors = [];
|
|
202
|
-
let maxSizeBytes;
|
|
203
|
-
if (params.maxSize) {
|
|
204
|
-
try {
|
|
205
|
-
maxSizeBytes = parseSize(params.maxSize);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
return {
|
|
208
|
-
content: [
|
|
209
|
-
{
|
|
210
|
-
type: "text",
|
|
211
|
-
text: JSON.stringify({ error: err.message })
|
|
212
|
-
}
|
|
213
|
-
]
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
for (const file of filePaths) {
|
|
218
|
-
try {
|
|
219
|
-
const result = await optimizeImage(file.path, {
|
|
220
|
-
quality: params.quality,
|
|
221
|
-
format: params.format,
|
|
222
|
-
resize: params.resize,
|
|
223
|
-
destination: params.destination,
|
|
224
|
-
outputFolder: params.outputFolder,
|
|
225
|
-
concurrency: 1,
|
|
226
|
-
exclude: [],
|
|
227
|
-
dryRun: false,
|
|
228
|
-
verbose: false,
|
|
229
|
-
json: false,
|
|
230
|
-
sourcePath,
|
|
231
|
-
detectedFormat: file.format
|
|
232
|
-
});
|
|
233
|
-
results.push(result);
|
|
234
|
-
} catch (err) {
|
|
235
|
-
errors.push({ file: file.path, message: err.message });
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const totalDuration = performance.now() - startTime;
|
|
239
|
-
const output = buildJsonOutput(results, errors, totalDuration);
|
|
240
|
-
if (maxSizeBytes !== void 0) {
|
|
241
|
-
for (const file of output.files) {
|
|
242
|
-
if (file.newSize > maxSizeBytes) {
|
|
243
|
-
file.exceeded = true;
|
|
244
|
-
}
|
|
159
|
+
const maxSizeBytes = params.maxSize ? parseSize(params.maxSize) : void 0;
|
|
160
|
+
const output = resolveOutputDir(params);
|
|
161
|
+
const plan = await planJobs({
|
|
162
|
+
inputs: [resolveUserPath(params.path)],
|
|
163
|
+
output,
|
|
164
|
+
format: params.format
|
|
165
|
+
});
|
|
166
|
+
const settings = { quality: params.quality, format: params.format, resize: params.resize };
|
|
167
|
+
const batch = await runBatch(plan.jobs, settings, defaultConcurrency());
|
|
168
|
+
const result = buildJsonOutput(batch.results, batch.errors, batch.duration, output);
|
|
169
|
+
if (maxSizeBytes !== void 0) {
|
|
170
|
+
for (const file of result.files) if (file.newSize > maxSizeBytes) file.exceeded = true;
|
|
245
171
|
}
|
|
172
|
+
return text(result);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return text({ error: err.message });
|
|
246
175
|
}
|
|
247
|
-
return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
|
|
248
176
|
}
|
|
249
177
|
);
|
|
250
178
|
server.tool(
|
|
251
179
|
"detect",
|
|
252
180
|
"Detect if a file is an image and identify its format",
|
|
253
|
-
{
|
|
254
|
-
path: z.string().describe("Path to the file")
|
|
255
|
-
},
|
|
181
|
+
{ path: z.string().describe("Path to the file") },
|
|
256
182
|
async (params) => {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
183
|
+
const format = await detectFormat(path.resolve(resolveUserPath(params.path)));
|
|
184
|
+
if (format === UNSUPPORTED) return text({ format: "heic", isImage: true, supported: false });
|
|
185
|
+
return text(format ? { format, isImage: true } : { format: null, isImage: false });
|
|
261
186
|
}
|
|
262
187
|
);
|
|
263
|
-
|
|
264
|
-
await server.connect(transport);
|
|
188
|
+
await server.connect(new StdioServerTransport());
|
|
265
189
|
});
|
|
266
190
|
|
|
267
191
|
// src/commands/pipe.ts
|
|
268
192
|
import { Command as Command3 } from "commander";
|
|
269
|
-
|
|
270
|
-
var pipeCommand = new Command3("pipe").description("Read image from stdin, write optimized image to stdout").requiredOption("-f, --format <fmt>", "Output format (jpeg, png, webp, gif, avif)").option("-q, --quality <n>", "Quality (1-100)",
|
|
271
|
-
|
|
193
|
+
var MAX_INPUT_SIZE = 100 * 1024 * 1024;
|
|
194
|
+
var pipeCommand = new Command3("pipe").description("Read image from stdin, write optimized image to stdout").requiredOption("-f, --format <fmt>", "Output format (jpeg, png, webp, gif, avif)").option("-q, --quality <n>", "Quality (1-100)", parseInteger, DEFAULTS.quality).option("-r, --resize <width>", "Max width in px (never enlarges)", parseInteger).action(async (opts) => {
|
|
195
|
+
let settings;
|
|
196
|
+
try {
|
|
197
|
+
settings = validateEncodeSettings(opts);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return fail2(err.message);
|
|
200
|
+
}
|
|
272
201
|
const chunks = [];
|
|
273
202
|
let totalSize = 0;
|
|
274
203
|
for await (const chunk of process.stdin) {
|
|
275
204
|
totalSize += chunk.length;
|
|
276
|
-
if (totalSize > MAX_INPUT_SIZE)
|
|
277
|
-
process.stderr.write("Error: input exceeds 100 MB limit\n");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
205
|
+
if (totalSize > MAX_INPUT_SIZE) return fail2("input exceeds 100 MB limit");
|
|
280
206
|
chunks.push(chunk);
|
|
281
207
|
}
|
|
282
208
|
const input = Buffer.concat(chunks);
|
|
283
|
-
if (input.length === 0)
|
|
284
|
-
process.stderr.write("Error: no input received on stdin\n");
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
const format = opts.format;
|
|
288
|
-
const quality = opts.quality;
|
|
289
|
-
const resize = opts.resize;
|
|
290
|
-
if (!VALID_FORMATS.includes(format)) {
|
|
291
|
-
process.stderr.write(`Error: invalid format "${format}"
|
|
292
|
-
`);
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
const outputFormat = format === "jpg" ? "jpeg" : format;
|
|
296
|
-
const formatOptions = getFormatOptions(quality);
|
|
209
|
+
if (input.length === 0) return fail2("no input received on stdin");
|
|
297
210
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
pipeline = pipeline.toFormat(
|
|
303
|
-
outputFormat,
|
|
304
|
-
formatOptions[outputFormat] ?? {}
|
|
305
|
-
);
|
|
306
|
-
const outputBuffer = await pipeline.toBuffer();
|
|
307
|
-
process.stdout.write(outputBuffer);
|
|
211
|
+
const detected = sniff(input);
|
|
212
|
+
const { data } = await encodeImage(input, detected, settings);
|
|
213
|
+
process.stdout.write(data);
|
|
308
214
|
} catch (err) {
|
|
309
|
-
|
|
310
|
-
`);
|
|
311
|
-
process.exit(1);
|
|
215
|
+
fail2(err.message);
|
|
312
216
|
}
|
|
313
217
|
});
|
|
218
|
+
function sniff(buf) {
|
|
219
|
+
if (buf[0] === 255 && buf[1] === 216) return "jpeg";
|
|
220
|
+
if (buf[0] === 137 && buf.toString("latin1", 1, 4) === "PNG") return "png";
|
|
221
|
+
if (buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP")
|
|
222
|
+
return "webp";
|
|
223
|
+
if (buf.toString("latin1", 0, 3) === "GIF") return "gif";
|
|
224
|
+
if (buf.toString("latin1", 4, 8) === "ftyp") {
|
|
225
|
+
const brand = buf.toString("latin1", 8, 12);
|
|
226
|
+
return brand === "avif" || brand === "avis" ? "avif" : UNSUPPORTED;
|
|
227
|
+
}
|
|
228
|
+
return "unknown";
|
|
229
|
+
}
|
|
230
|
+
function fail2(message) {
|
|
231
|
+
process.stderr.write(`Error: ${message}
|
|
232
|
+
`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
314
235
|
|
|
315
236
|
// src/bin/bulki.ts
|
|
316
237
|
var require3 = createRequire2(import.meta.url);
|
|
317
238
|
var { version: version2 } = require3("../../package.json");
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
239
|
+
var args = process.argv.slice(2);
|
|
240
|
+
var COMMANDS = /* @__PURE__ */ new Set(["compress", "hook", "mcp", "pipe", "watch", "help"]);
|
|
241
|
+
var droppedPaths = args.length > 0 && !args[0].startsWith("-") && !COMMANDS.has(args[0]) ? parsePathInput(args.join(" ")) : null;
|
|
242
|
+
if (args.length === 0 || droppedPaths?.ok) {
|
|
243
|
+
const { runInteractive } = await import("../interactive-MTL4CMVQ.js");
|
|
244
|
+
await runInteractive(droppedPaths?.ok ? droppedPaths.paths : void 0);
|
|
321
245
|
} else {
|
|
322
246
|
const program = new Command4();
|
|
323
|
-
program.name("bulki").description("
|
|
247
|
+
program.name("bulki").description("Fast image optimizer. Run `bulki` with no arguments for the guided mode.").version(version2).showSuggestionAfterError();
|
|
324
248
|
program.addCommand(compressCommand);
|
|
325
249
|
program.addCommand(hookCommand);
|
|
326
250
|
program.addCommand(mcpCommand);
|
|
327
251
|
program.addCommand(pipeCommand);
|
|
328
252
|
program.addCommand(watchCommand);
|
|
329
|
-
program.
|
|
253
|
+
await program.parseAsync();
|
|
330
254
|
}
|
|
331
255
|
//# sourceMappingURL=bulki.js.map
|
package/dist/bin/bulki.js.map
CHANGED
|
@@ -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":"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","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'\nimport { parsePathInput } from '../pipeline/paths.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../../package.json') as { version: string }\n\nconst args = process.argv.slice(2)\nconst COMMANDS = new Set(['compress', 'hook', 'mcp', 'pipe', 'watch', 'help'])\n\n// `bulki` alone, or `bulki <folder/images>` (e.g. dragged onto the terminal) → guided mode\nconst droppedPaths =\n\targs.length > 0 && !args[0].startsWith('-') && !COMMANDS.has(args[0])\n\t\t? parsePathInput(args.join(' '))\n\t\t: null\n\nif (args.length === 0 || droppedPaths?.ok) {\n\tconst { runInteractive } = await import('../interactive/index.js')\n\tawait runInteractive(droppedPaths?.ok ? droppedPaths.paths : undefined)\n} else {\n\tconst program = new Command()\n\tprogram\n\t\t.name('bulki')\n\t\t.description('Fast image optimizer. Run `bulki` with no arguments for the guided mode.')\n\t\t.version(version)\n\t\t.showSuggestionAfterError()\n\n\tprogram.addCommand(compressCommand)\n\tprogram.addCommand(hookCommand)\n\tprogram.addCommand(mcpCommand)\n\tprogram.addCommand(pipeCommand)\n\tprogram.addCommand(watchCommand)\n\tawait program.parseAsync()\n}\n","import { Command } from 'commander'\nimport { createProgressBar } from '../output/progress.js'\nimport { buildJsonOutput, formatFileResult, formatSummary } from '../output/results.js'\nimport { runBatch } from '../pipeline/optimize.js'\nimport { displayPath, resolveUserPath } from '../pipeline/paths.js'\nimport { PlanError, planJobs } from '../pipeline/plan.js'\nimport type { CompressOptions } from '../types/index.js'\nimport { DEFAULTS, parseInteger, type RawOptions, validateOptions } from '../validation/options.js'\n\nexport const compressCommand = new Command('compress')\n\t.description(\n\t\t'Compress images. Originals are never modified; optimized copies go to the output folder.',\n\t)\n\t.argument('<paths...>', 'Images and/or folders to optimize (quote paths with spaces)')\n\t.option('-o, --output <dir>', 'Folder where optimized images are written', DEFAULTS.output)\n\t.option('-q, --quality <n>', 'Output quality (1-100)', parseInteger, DEFAULTS.quality)\n\t.option('-f, --format <fmt>', 'Convert to jpeg, png, webp, gif or avif (default: keep format)')\n\t.option(\n\t\t'-r, --resize <width>',\n\t\t'Max width in px (smaller images are never enlarged)',\n\t\tparseInteger,\n\t)\n\t.option('--concurrency <n>', 'Images processed in parallel (1-32)', parseInteger)\n\t.option('--exclude <names...>', 'Folder names to skip')\n\t.option('--dry-run', 'Show what would be processed without writing files')\n\t.option('--verbose', 'Show one line per file')\n\t.option('--json', 'Output results as JSON')\n\t.option('--max-size <size>', 'Fail if any output is bigger than this (e.g. 200kb, 1mb)')\n\t// Legacy flags — kept so existing scripts and the GitHub Action keep working\n\t.option('-d, --destination <dir>', 'Deprecated: base folder; output goes to <dir>/bulki-output')\n\t.option('--output-folder <name>', 'Deprecated: subfolder name inside --destination')\n\t.addHelpText(\n\t\t'after',\n\t\t`\nExamples:\n bulki compress ./photos → ./bulki-output\n bulki compress ./photos -o ./optimized -f webp\n bulki compress \"My Photos\" -o \"Web Ready\" -r 1920\n bulki compress *.png -f avif`,\n\t)\n\t.action(async (inputPaths: string[], opts: Partial<RawOptions>, cmd: Command) => {\n\t\t// `-o` used to be the subfolder name; keep `-d x -o name` meaning `x/name`\n\t\tconst outputFromCli = cmd.getOptionValueSource('output') === 'cli'\n\t\tlet options: CompressOptions\n\t\ttry {\n\t\t\toptions = validateOptions({ ...opts, output: outputFromCli ? opts.output : undefined })\n\t\t} catch (err) {\n\t\t\tfail((err as Error).message)\n\t\t}\n\n\t\tconst inputs = inputPaths.map((p) => resolveUserPath(p))\n\t\tlet plan: Awaited<ReturnType<typeof planJobs>>\n\t\ttry {\n\t\t\tplan = await planJobs({\n\t\t\t\tinputs,\n\t\t\t\toutput: options.output,\n\t\t\t\tformat: options.format,\n\t\t\t\texclude: options.exclude,\n\t\t\t})\n\t\t} catch (err) {\n\t\t\tif (err instanceof PlanError) fail(err.message)\n\t\t\tthrow err\n\t\t}\n\n\t\tconst log = options.json ? () => {} : console.log\n\t\tfor (const s of plan.skipped) console.error(`Skipped ${s.file}: ${s.reason}`)\n\n\t\tif (plan.jobs.length === 0) {\n\t\t\tif (options.json) console.log(JSON.stringify(buildJsonOutput([], [], 0, options.output)))\n\t\t\telse log('No images found to process.')\n\t\t\treturn\n\t\t}\n\n\t\tif (options.dryRun) {\n\t\t\tlog(`Would optimize ${plan.jobs.length} image(s) into ${displayPath(options.output)}`)\n\t\t\tfor (const job of plan.jobs) log(` ${displayPath(job.input)} → ${displayPath(job.output)}`)\n\t\t\treturn\n\t\t}\n\n\t\tconst showBar = !options.verbose && !options.json && process.stdout.isTTY\n\t\tconst progress = showBar ? createProgressBar(plan.jobs.length) : null\n\n\t\tconst batch = await runBatch(plan.jobs, options, options.concurrency, (job, result, error) => {\n\t\t\tprogress?.tick()\n\t\t\tif (!options.verbose || options.json) return\n\t\t\tif (result) console.log(formatFileResult(result))\n\t\t\telse console.error(`✗ ${displayPath(job.input)}: ${error?.message}`)\n\t\t})\n\n\t\tconst maxSize = options.maxSize\n\t\tconst exceeded = maxSize ? batch.results.filter((r) => r.newSize > maxSize) : []\n\n\t\tif (options.json) {\n\t\t\tconst output = buildJsonOutput(batch.results, batch.errors, batch.duration, options.output)\n\t\t\tif (maxSize) for (const f of output.files) if (f.newSize > maxSize) f.exceeded = true\n\t\t\tconsole.log(JSON.stringify(output))\n\t\t} else {\n\t\t\tconsole.log(formatSummary(batch.results, batch.errors, batch.duration))\n\t\t\tif (!options.verbose) {\n\t\t\t\tfor (const e of batch.errors) console.error(`✗ ${displayPath(e.file)}: ${e.message}`)\n\t\t\t}\n\t\t\tif (batch.results.length > 0) console.log(`Saved to ${displayPath(options.output)}`)\n\t\t\tfor (const r of exceeded) console.warn(`File exceeds size budget: ${r.outputPath}`)\n\t\t}\n\n\t\tif (batch.errors.length > 0 || exceeded.length > 0) process.exitCode = 1\n\t})\n\nfunction fail(message: string): never {\n\tconsole.error(`error: ${message}`)\n\tprocess.exit(1)\n}\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 { detectFormat, UNSUPPORTED } from '../pipeline/filter.js'\nimport { runBatch } from '../pipeline/optimize.js'\nimport { resolveUserPath } from '../pipeline/paths.js'\nimport { planJobs } from '../pipeline/plan.js'\nimport { defaultConcurrency, parseSize, resolveOutputDir } from '../validation/options.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../../package.json') as { version: string }\n\nfunction text(payload: unknown) {\n\treturn { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] }\n}\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({ name: 'bulki', version })\n\n\t\tserver.tool(\n\t\t\t'compress',\n\t\t\t'Compress and optimize images (JPEG, PNG, WebP, GIF, AVIF). Originals are never modified; optimized copies are written to `output`.',\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().int().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\n\t\t\t\t\t.number()\n\t\t\t\t\t.int()\n\t\t\t\t\t.positive()\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Max width in pixels (never enlarges)'),\n\t\t\t\toutput: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.optional()\n\t\t\t\t\t.describe('Folder where optimized images are written (default: ./bulki-output)'),\n\t\t\t\tdestination: z.string().optional().describe('Deprecated: base folder, use `output`'),\n\t\t\t\toutputFolder: z.string().optional().describe('Deprecated: subfolder name, use `output`'),\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\ttry {\n\t\t\t\t\tconst maxSizeBytes = params.maxSize ? parseSize(params.maxSize) : undefined\n\t\t\t\t\tconst output = resolveOutputDir(params)\n\t\t\t\t\tconst plan = await planJobs({\n\t\t\t\t\t\tinputs: [resolveUserPath(params.path)],\n\t\t\t\t\t\toutput,\n\t\t\t\t\t\tformat: params.format,\n\t\t\t\t\t})\n\t\t\t\t\tconst settings = { quality: params.quality, format: params.format, resize: params.resize }\n\t\t\t\t\tconst batch = await runBatch(plan.jobs, settings, defaultConcurrency())\n\t\t\t\t\tconst result = buildJsonOutput(batch.results, batch.errors, batch.duration, output)\n\t\t\t\t\tif (maxSizeBytes !== undefined) {\n\t\t\t\t\t\tfor (const file of result.files) if (file.newSize > maxSizeBytes) file.exceeded = true\n\t\t\t\t\t}\n\t\t\t\t\treturn text(result)\n\t\t\t\t} catch (err) {\n\t\t\t\t\treturn text({ error: (err as Error).message })\n\t\t\t\t}\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{ path: z.string().describe('Path to the file') },\n\t\t\tasync (params) => {\n\t\t\t\tconst format = await detectFormat(path.resolve(resolveUserPath(params.path)))\n\t\t\t\tif (format === UNSUPPORTED) return text({ format: 'heic', isImage: true, supported: false })\n\t\t\t\treturn text(format ? { format, isImage: true } : { format: null, isImage: false })\n\t\t\t},\n\t\t)\n\n\t\tawait server.connect(new StdioServerTransport())\n\t})\n","import { Command } from 'commander'\nimport { encodeImage } from '../pipeline/encode.js'\nimport { UNSUPPORTED } from '../pipeline/filter.js'\nimport { DEFAULTS, parseInteger, validateEncodeSettings } from '../validation/options.js'\n\nconst MAX_INPUT_SIZE = 100 * 1024 * 1024 // 100 MB\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)', parseInteger, DEFAULTS.quality)\n\t.option('-r, --resize <width>', 'Max width in px (never enlarges)', parseInteger)\n\t.action(async (opts) => {\n\t\tlet settings: ReturnType<typeof validateEncodeSettings>\n\t\ttry {\n\t\t\tsettings = validateEncodeSettings(opts)\n\t\t} catch (err) {\n\t\t\treturn fail((err as Error).message)\n\t\t}\n\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) return fail('input exceeds 100 MB limit')\n\t\t\tchunks.push(chunk)\n\t\t}\n\t\tconst input = Buffer.concat(chunks)\n\t\tif (input.length === 0) return fail('no input received on stdin')\n\n\t\ttry {\n\t\t\tconst detected = sniff(input)\n\t\t\tconst { data } = await encodeImage(input, detected, settings)\n\t\t\tprocess.stdout.write(data)\n\t\t} catch (err) {\n\t\t\tfail((err as Error).message)\n\t\t}\n\t})\n\n/** Same magic-byte rules as detectFormat, for an in-memory buffer. */\nfunction sniff(buf: Buffer): string {\n\tif (buf[0] === 0xff && buf[1] === 0xd8) return 'jpeg'\n\tif (buf[0] === 0x89 && buf.toString('latin1', 1, 4) === 'PNG') return 'png'\n\tif (buf.toString('latin1', 0, 4) === 'RIFF' && buf.toString('latin1', 8, 12) === 'WEBP')\n\t\treturn 'webp'\n\tif (buf.toString('latin1', 0, 3) === 'GIF') return 'gif'\n\tif (buf.toString('latin1', 4, 8) === 'ftyp') {\n\t\tconst brand = buf.toString('latin1', 8, 12)\n\t\treturn brand === 'avif' || brand === 'avis' ? 'avif' : UNSUPPORTED\n\t}\n\treturn 'unknown'\n}\n\nfunction fail(message: string): never {\n\tprocess.stderr.write(`Error: ${message}\\n`)\n\tprocess.exit(1)\n}\n"],"mappings":"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","names":["createRequire","Command","Command","require","Command","Command","Command","fail","require","createRequire","version","Command"]}
|