diff --git a/README.md b/README.md index edec6c4..b15691b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ pixelvault export -o my-images.tar The CLI is designed for AI coding agents. Output contract: -- `upload` prints **only the URL** to stdout +- `upload` prints **only the URL** to stdout (add `--optimized` for the smaller WebP/AVIF variant; it falls back to the original URL for SVG, GIF and private uploads) - `list` outputs one URL per line - `delete` outputs nothing on success - All human messages go to **stderr** diff --git a/src/commands/upload.ts b/src/commands/upload.ts index b0e0b4e..0985321 100644 --- a/src/commands/upload.ts +++ b/src/commands/upload.ts @@ -10,6 +10,7 @@ interface UploadResponse { data: { id: string; url: string; + optimized_url?: string; visibility?: "public" | "private"; mime_type: string; size: number; @@ -46,6 +47,12 @@ export default defineCommand({ description: "Lifetime of a --private signed link: e.g. 30m, 12h, 7d (default 7d; min 60s, max 30d)", }, + optimized: { + type: "boolean", + description: + "Print the optimized URL (WebP/AVIF, usually much smaller) instead of the original; falls back to the original URL when none is returned (SVG, GIF, private)", + default: false, + }, json: { type: "boolean", description: "Output full JSON response", @@ -120,7 +127,7 @@ export default defineCommand({ if (args.json) { results.push(res.data); } else { - stdout(res.data.url); + stdout(args.optimized ? (res.data.optimized_url ?? res.data.url) : res.data.url); } } catch (err) { hadError = true; diff --git a/tests/commands/upload.test.ts b/tests/commands/upload.test.ts index 97e12a5..bef5197 100644 --- a/tests/commands/upload.test.ts +++ b/tests/commands/upload.test.ts @@ -128,4 +128,40 @@ describe("upload command", () => { expect(printed).toEqual(["https://img.test/a.jpg", "https://img.test/c.jpg"]); expect(exitCodes).toEqual([1]); }); + + it("--optimized prints optimized_url, falling back to url when absent", async () => { + const names = ["a.png", "b.svg"]; + const paths = names.map((name) => { + const p = join(testDir, name); + writeFileSync(p, Buffer.from(name)); + return p; + }); + globalThis.fetch = vi.fn().mockImplementation(async (_url, init) => { + const file = (init.body as FormData).get("file") as File; + const base = `https://img.test/${file.name}`; + return { + ok: true, + json: () => + Promise.resolve({ + data: { + id: file.name, + url: base, + ...(file.name.endsWith(".png") ? { optimized_url: `${base}?fmt=auto&q=85` } : {}), + }, + }), + }; + }); + const out = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + + const { runCommand } = await import("citty"); + const { default: upload } = await import("../../src/commands/upload.js"); + await runCommand(upload, { rawArgs: [...paths, "--optimized"] }); + const printed = out.mock.calls.map((c) => String(c[0]).trim()); + out.mockRestore(); + + expect(printed).toEqual([ + "https://img.test/a.png?fmt=auto&q=85", + "https://img.test/b.svg", + ]); + }); });