Skip to content

Add optimole url: print assets' CDN URLs, resized or converted - #2

Merged
selul merged 11 commits into
mainfrom
claude/url-command
Nov 5, 2026
Merged

selul merged 11 commits into
mainfrom
claude/url-command

Conversation

@selul

@selul selul commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Scripts cannot get an asset's CDN URL in another size or format without building the URL by hand. The new optimole url command builds it from an asset key or an Optimole URL, and reads piped output from upload and url.

optimole url 42cf6621995747f69a30d32f6a6c4f82 --width 800 --format webp
optimole upload -r ./photos --json | optimole url --width 1200
optimole url KEY --width 800 | optimole url --resize fill --height 400 --gravity top

What changed

  • optimole url command — takes asset keys, Optimole URLs, or JSON lines from stdin. It prints one URL per input, in input order.

  • Asset keys — api.GetAsset calls GET /api/assets/{key}. The answer gives the URL on the account's delivery domain, custom domains included. Up to -c lookups run at a time.

  • URL rewriting (internal/imgurl) — reads a URL the way the CDN does. The command replaces an option that is already in the URL, and writes options in the CDN's order.

  • Value checks — the command refuses values that the CDN ignores or answers with an empty image. Examples: a bad w, h or q, a cb: without the abc.1 form, an unknown option name.

  • Unsafe input — the command refuses URLs with spaces or control characters, and URLs that the CDN misreads because of their origin path. Messages and flag errors do not show API keys.

  • Errors — a failed input does not stop the other inputs (exit 1). A missing or rejected API key stops the run at the first key. File URLs print without change, and image flags are an error for them.

Note

The rules come from the CDN's CloudFront viewer-request function, viewerRequest.js in optimole-lambda-functions. It uses only the first copy of a repeated option, so a second w: does nothing. It also removes f:avif and selects the format from the browser's Accept header. For this reason --format avif is refused, and --format auto gives AVIF to browsers that accept it. rt:crop (in the PHP SDK) answers a 302 to the original image, so --resize takes the dashboard's five types only. In a live check, f:best gave PNG to a browser that accepts AVIF, so best is not offered.

Note

The PR also has two small changes from #1. It removes internal/cli/optimole-export-1/, two files that a test run left and #1 committed by mistake. It also stops a timing flake in TestRunRestartsAStalledDownload, which failed once on macOS CI. Folder-scoped (optml-s) keys are tested only against the fake server, not the real dashboard.

Command flow

flowchart TB
    subgraph Input["1 · Input"]
        A["Argument, or a stdin line"]:::entry
        B{"Asset key, URL<br/>or JSON line?"}:::decision
        S["Skipped"]:::stopped
        A --> B
        B -- "Blank, or an<br/>upload summary" --> S
    end
    subgraph Resolve["2 · Resolve"]
        C["New: GET /api/assets/{key}<br/>up to -c at a time"]:::work
        D["New: Read the URL<br/>the way the CDN does"]:::work
        E["New: Replace options<br/>from the flags"]:::work
        F{"New: Image, and safe<br/>for the CDN?"}:::decision
        C -- "Found" --> D
        D --> E --> F
    end
    subgraph Output["3 · Output, in input order"]
        G["URL on stdout,<br/>or a --json line"]:::stored
        H["Error for this input;<br/>the next inputs continue"]:::stopped
        I["Run stops"]:::stopped
    end
    B -- "32 hex characters" --> C
    B -- "https:// URL, or JSON<br/>with a url" --> D
    B -- "Other" --> H
    C -- "403, 404 or<br/>other failure" --> H
    C -- "401, or no API key" --> I
    F -- "Yes" --> G
    F -- "No" --> H

    style Input fill:#eff6ff,stroke:#93c5fd,color:#172554
    style Resolve fill:#f0fdf4,stroke:#86efac,color:#14532d
    style Output fill:#faf5ff,stroke:#c4b5fd,color:#3b0764

    classDef entry fill:#dbeafe,color:#172554,stroke:#2563eb,stroke-width:2px
    classDef work fill:#1a7f37,color:#fff,stroke:#116329,stroke-width:2px
    classDef decision fill:#fef3c7,color:#451a03,stroke:#b45309,stroke-width:2px
    classDef stopped fill:#fee2e2,color:#450a0a,stroke:#dc2626,stroke-width:2px
    classDef stored fill:#ede9fe,color:#2e1065,stroke:#7c3aed,stroke-width:2px
Loading

Flags and URL options

Flag URL option
--width N|auto, --height N|auto w:, h:
--resize fit|fill|fill-down|force|auto rt:
--gravity center|top|…|bottom-right|smart, --focus X:Y g:, g:fp:X:Y
--quality auto|mauto|eco|1-100 q:
--format webp|jpg|png|gif|ico|jxl|heic f:
--format auto removes f:
--dpr 1-5, --download dpr:, att:1
--set NAME:VALUE, --unset NAME, --reset any option the CDN reads

--json output

One object per input. Other commands can read it back.

Field Present
input always
url when the input succeeds
key when the asset key is known
mime_type, original_width, original_height for key lookups, and when a piped line carries them
error, code when the input fails; code is invalid_input, not_found, no_url or not_an_image

QA

You need Go, an Optimole API key, and a JPEG that is at least 640 px wide. To get the key, go to https://dashboard.optimole.com/settings/api-keys/ (Settings → API Keys).

  1. Build the branch:

    git clone https://github.com/Codeinwp/optimole-cli && cd optimole-cli
    git checkout claude/url-command && go build -o optimole ./cmd/optimole
    ./optimole login

    Paste the API key when the command asks for it.

    Expect: ./optimole url -h shows the url flags.

  2. Upload the JPEG and pipe the result:

    ./optimole upload photo.jpg --json | ./optimole url --width 320 --format webp

    Expect: one URL that contains /w:320/ and /f:webp/.

  3. Open that URL in Chrome.

    Expect: the tab title shows a width of 320, for example (320×240).

  4. Pipe that URL into the command again:

    ./optimole url 'URL-FROM-STEP-2' --quality 40

    Expect: the new URL contains /q:40/ and only one /w: segment.

  5. Run ./optimole url 'URL-FROM-STEP-2' --format auto. Open the new URL in Chrome, with DevTools → Network open.

    Expect: the image row shows the type avif.

  6. Request the same URL without AVIF support:

    curl -s -o /dev/null -w '%{content_type}\n' 'URL-FROM-STEP-5'

    Expect: image/jpeg.

  7. Run ./optimole url 'URL-FROM-STEP-2' --download. Open the new URL in a browser.

    Expect: the browser downloads the image and does not show it.

🤖 Generated with Claude Code

selul and others added 11 commits November 5, 2026 11:16
internal/cli/optimole-export-1/ (an export.json and a lock file) was left
by a test run during the export work and committed with it. No test
writes there any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetAsset calls GET /api/assets/{key}, which answers with the asset's CDN
URL, MIME type and original size. The dashboard answers 403 both for an
unknown key and for an asset a folder-scoped key may not see. The fake
server serves the route the same way, and AddAsset puts a finished asset
in its library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Splits a delivery URL into its options and source (id:HASH/... or an
origin URL), following the CDN's viewer-request function: only the first
copy of an option counts, and options are applied in a fixed order, which
String writes them in. Check refuses values the CDN answers with an empty
image (w, h, q), ignores (a cache buster without a dot) or resets (an
unknown gravity). AVIF cannot be forced, because the CDN drops f:avif, and
the resize types are the dashboard's (rt:crop answers a redirect).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes asset keys, Optimole URLs, or the JSON lines of upload --json and
url --json (from stdin when there are no arguments), applies --width,
--height, --resize, --gravity, --focus, --quality, --format, --dpr,
--download, --set, --unset and --reset, and prints one URL per input in
input order. An option already in the URL is replaced, never repeated.
URLs need no API call and no key; keys are looked up in parallel. A
failed input is reported and the others are still printed; a rejected
key stops the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ctrl-C now stops a run waiting on an idle stdin pipe.
- A URL with spaces or control characters is refused, so a newline cannot
  turn one printed URL into two; messages quote control characters.
- Messages hide the API keys in use, and a line that looks like an API key
  is not echoed.
- A URL whose origin path holds a segment the CDN reads as an option
  (w:9 with no w: of its own) is refused: the CDN would misread it.
- Raw values match what the CDN reads: fp:X:Y with colons, cache busters
  without a colon, finite coordinates.
- A custom-domain URL whose path starts with the origin parses, so a URL
  left without options can be piped back.
- api.IsAssetKey replaces a copy of the key pattern; dead code and
  narrating comments removed. Tests cover the lookup limit, an idle pipe,
  output failures, newline injection and key redaction, and the fuzz test
  edits the URLs it reparses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Usage errors hide the API keys in use, and a long input line is
  shortened only after its keys are hidden, so no part of a key shows.
- Validate follows the CDN's literal rules: it deletes the first /ig:avif
  and /f:avif in the path, and reads no other ig value.
- Before an origin URL, the segments the CDN removes are dropped, so a
  custom-domain URL left without options pipes back.
- The dispatcher stops waiting for stdin when the run ends.
- gofmt on the test file, which CI caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Validate works on the written URL like the CDN: it refuses a URL whose
  first /ig:avif or /f:avif (deleted as text, not as a segment) falls in
  the origin path, or whose origin path alone holds ig:avif, which turns
  AVIF off. Checked against the viewer-request function on 7 URLs.
- Before an origin URL, a segment holding ig:avif is kept: dropping it
  would turn AVIF back on.
- The flag package's own errors (a bad -c or --json value) hide the keys
  known before the flags are read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flag package's errors were redacted with the environment and saved
keys only, so --api-key K -c K still printed K. The --api-key value is
now read from the arguments before they are parsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading --api-key before parsing missed keys in several cases (a value
taken by another flag, a repeated --api-key, a saved key next to a flag
key). The flag package only shows a value inside quotes in its "invalid
value" errors, so those messages now leave the value out: no key can
reach them, whatever its source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt test

- The flag package also repeats a whole argument (bad flag syntax:
  ---api-key=K) and an unknown flag's name. The first now shows no
  argument, and a name that looks like an API key is not shown.
- export tests: the fake records a download when it ends, and a stalled
  one can end after its retry (macOS CI did so), so the downloads helper
  now orders requests by when they started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pasted argument can hold spaces or newlines ("--api-key KEY" as one
argument, a key on a second line). Flag errors now drop the whole
argument of bad flag syntax, and name an unknown flag only when it looks
like one (letters, digits and dashes, under 20 characters).

export tests: the stalled-download test waits until the fake has recorded
both requests, since the stalled one is recorded only when its handler
sees the cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@selul
selul merged commit 012e83d into main Nov 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant