Skip to content

Add optimole delete: delete images by key, folder or website - #3

Merged
selul merged 5 commits into
mainfrom
claude/delete-command
Nov 10, 2026
Merged

selul merged 5 commits into
mainfrom
claude/delete-command

Conversation

@selul

@selul selul commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

The dashboard API can now delete images in bulk (optimole-service#1944), but the CLI had no way to delete anything. This PR adds optimole delete: delete images by asset key, every image of a folder, or every image of a website.

What changed

  • optimole delete --keys FILE|- — deletes the asset keys in a file or on stdin, one per line, 100 per request. The run stops at the first request that fails. It then says how many images are deleted, which may be deleted, and which keys to leave out to finish.

  • --folder PATH and --folder-id N — empty a folder and all its subfolders in the background; the folders stay. The CLI looks the folder up first. It refuses an unknown folder, or a path that two folders share, before anything is deleted. A folder named 2026 / Drafts counts as the same path as 2026/Drafts.

  • --site DOMAIN — empties one website of the account, in any folder, in the background. https://www.example.com/blog means example.com, as on the server. An internationalized domain must be given in its xn-- form.

  • Confirmation — on a terminal, the command says what it will delete. Then it asks you to type the folder path, the domain, or delete, then Enter. Any other answer, Ctrl-C or Ctrl-D deletes nothing. --yes skips the question. Without a terminal and without --yes, the command exits 2 and sends nothing.

  • Retries — a delete request repeats only when the server cannot have received it (a 429, or a connection that failed before sending). After a lost answer or a 5xx, the command says the images may be deleted.

  • Fake server and docs — internal/testserver answers DELETE /api/assets with the dashboard's rules: scope, the three body shapes, the 422 messages, websites and their folders. The README and optimole delete -h describe the command.

Note

Safety, by owner decision: there are no approval codes. Only full-access API keys can delete; the dashboard answers folder-scoped keys with 403, and the CLI says a full-access key is needed. The other guard is the typed confirmation, or an explicit --yes in scripts.

Note

A key that is deleted already counts as invalid when named again: the server refuses the whole request with 422. This is why a key request does not repeat after a lost answer. It is also why the hint after a failure says which keys to leave out.

Note

--folder takes a path and --folder-id takes an ID, as in optimole upload. One flag for both is ambiguous for a folder named 2024. A path longer than one terminal line (1000 bytes) is confirmed by the folder's name instead.

QA

Do these steps on staging, with a test account. Build the CLI from this branch first.

go build -o optimole ./cmd/optimole
export OPTIMOLE_API_URL=https://staging-dashboard.optimole.com/api
  1. Sign in to https://staging-dashboard.optimole.com with a test account. On the Dashboard page, copy the API Key. Run ./optimole login and paste it.

    Expect: Logged in.

  2. Upload test images: two files into the root, and a folder tree.

    ./optimole upload --json a.png b.png | jq -r 'select(.key).key' > keys.txt
    ./optimole upload -r ./tree --folder QA/Delete --create-folders

    Expect: keys.txt holds two keys, and ./optimole folders lists QA/Delete.

  3. Run ./optimole delete --keys - < keys.txt.

    Expect: exit code 2 and add --yes to delete without asking. The two images stay in the library.

  4. Run ./optimole delete --keys keys.txt and type Delete, then Enter.

    Expect: the answer did not match; nothing was deleted.

  5. Run ./optimole delete --keys keys.txt again and type delete, then Enter.

    Expect: Deleted 2 images. In the dashboard, the two images are gone from the media library.

  6. Run ./optimole delete --keys keys.txt --yes a third time.

    Expect: exit code 1, the 422 names both keys as invalid, and Nothing was deleted.

  7. Run ./optimole delete --folder qa/delete and type the path that the prompt shows (QA/Delete).

    Expect: Deleting N images in the background. Within a few minutes the images leave the library. ./optimole folders still lists QA/Delete and its subfolders.

  8. Go to Settings → API Keys and click Create a new key. This key is folder-scoped (it starts with optml-s). Run OPTIMOLE_API_KEY=<that key> ./optimole delete --folder QA/Delete --yes.

    Expect: exit code 1 and Deleting images needs a full-access API key.

  9. Run ./optimole delete --site https://www.<a website of the account>/ --yes, then ./optimole delete --site no-such-site.example --yes.

    Expect: first Deleting N images in the background. (or Nothing to delete: the website … has no images.), then The account has no website with this domain.

🤖 Generated with Claude Code

selul and others added 5 commits November 9, 2026 18:11
"optimole delete" takes exactly one of --keys FILE|- (asset keys, sent
100 per DELETE /api/assets and stopping at the first request that
fails), --folder PATH or --folder-id N (the folder's tree, emptied in
the background), and --site DOMAIN (a website's images, in any folder).

Safety is a full-access key plus a typed confirmation: on a terminal the
command says what it deletes and asks for the folder's path, the domain
or "delete"; anything else, Ctrl-C or Ctrl-D deletes nothing. --yes skips
the question, and without a terminal the command refuses to run without
it. A folder path is looked up in the folder list first, so an unknown
folder or a path two folders share is refused before anything goes.

Key deletes are retried like listing (a deleted key can be named again);
folder and website deletes only when the server cannot have received
them, since a repeat would start a second background deletion. After a
failed request the command says how many images were deleted, and which
may have been.

The fake server answers DELETE /api/assets with the dashboard's rules
(scope, the three body shapes, 422 messages), and knows websites, their
folders and the assets they offloaded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…eted

- --site is sent as typed: the server strips one "www." as the prompt
  does, where sending the stripped domain had it strip a second one
  ("www.www.example.com" would have emptied example.com).
- The answer must end with Enter: "delete" then Ctrl-D, or a line too long
  to read whole, deletes nothing. A folder path longer than a terminal
  line (1024 bytes on macOS) is confirmed by its name instead.
- A keys request repeated after a lost answer may have deleted, whatever
  the retries answered; a 202 whose body does not read, or a 5xx, may have
  started a background deletion; a request that never connected did not.
- Known API keys are hidden, and control characters quoted, in every
  message, the prompt and the JSON (folder paths, file names, server
  messages).
- A folder path is matched only against a whole folder list; a list cut
  short (page limit, a cursor missing or repeated) stops the command.
- Opening the --keys file gives way to Ctrl-C (a FIFO waits for a writer).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Staging answers a key that is deleted already with 422 "invalid or
unauthorized", and refuses its whole request. So a keys request repeated
after a lost answer would make a deletion that worked look failed, and
"running again with the same keys is safe" was wrong.

- Keys requests are now retried like folder and website requests: only
  after a 429 or a connection that failed before sending. One rule
  (untouched) tells what proves nothing was deleted.
- After a failure the command says how many keys to leave out to finish,
  and that keys a failed request deleted show up as invalid next time.
- The fake refuses deleted keys like unknown ones.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- --site sends the host in lower case, "www." kept: the server strips one,
  as the prompt does, and a long URL no longer passes its 255-character
  limit. A non-ASCII domain is refused (give its xn-- form): the server
  maps it with IDNA, which the standard library lacks, so "İ.com" would
  have shown "i.com" and deleted "xn--i-9bb.com".
- A folder whose name holds a slash ("2026 / Drafts") reads as the nested
  2026/Drafts: both now match, so the command asks for --folder-id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@selul
selul merged commit bbb5345 into main Nov 10, 2026
11 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