Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
9 changes: 8 additions & 1 deletion src/commands/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface UploadResponse {
data: {
id: string;
url: string;
optimized_url?: string;
visibility?: "public" | "private";
mime_type: string;
size: number;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions tests/commands/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]);
});
});
Loading