Skip to content

feat(cli): deck edit — change a deck without deleting it - #224

Merged
jvsena42 merged 1 commit into
mainfrom
feat/cli-deck-edit
Sep 4, 2026
Merged

feat(cli): deck edit — change a deck without deleting it#224
jvsena42 merged 1 commit into
mainfrom
feat/cli-deck-edit

Conversation

@jvsena42

@jvsena42 jvsena42 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #222.

loopky could create a deck and edit every card in it, but nothing about the deck. The only workaround was deck delete + deck create --from-file, which mints a new deck id — every pubky://…/decks/<id>/ link breaks, every card goes back to new in the scheduler — and which is impossible for a deck built with card add, since there is no file to recreate it from.

deck edit <deckId> [--title T] [--description D] [--cover-url URL] [--cover-emoji E]
                   [--tag T]... [--clear-tags] [--clear-cover]
                   [--listen|--no-listen] [--speak|--no-speak]
                   [--type|--no-type] [--reverse|--no-reverse]
                   [--front-lang BCP47] [--back-lang BCP47]

One updateMetadata call — the manifest and the tag records, never the chunks — so it costs the same on a 20k-card deck as on an empty one.

The rules, and why each one is the surface's own

  • An absent flag leaves its field alone, card edit's rule; an explicitly empty value clears it (--description=, which is --back=). --clear-tags / --clear-cover are switches because those two are not single values: --tag "" would have to mean both "no tags" and "one blank tag", and a cover is an emoji and an image that layer.
  • --tag replaces rather than appends, as the issue proposed. Appending makes "the tags are exactly these" unsayable without a delete, and makes a retry after the hourly session expiry (Every homeserver write dies on /session with a raw transport error, and publish reports it as "check your connection" #165) grow its own list — the same property card add's dedupe exists for.
  • Nothing to change is not a write. A manifest write bumps updated_at, which is what every follower's "the author published changes" badge reads, so a re-run reports changed: false and leaves the record alone.
  • The derived language labels are reconciled only when the pair actually moves. Retyping a deck from Spanish to French drops spanish; --clear-tags on a language deck stays empty, because those are ordinary author-removable tags rather than a reserved family (Architecture §7.7).

--json gets {deck, changed, fields}fields names what actually moved, diffed against the stored deck rather than read off which flags were typed, so it is something a caller can verify against.

Two answers to the issue's open questions

The study-mode toggles are in, not split out. Turning one off costs no progress: review state is keyed by card_id alone and the modes decide only how a card is presented. Reverse's pairing is session-local and persisted nowhere, so even that one has nothing to lose.

--cover-url and --cover-emoji are not mutually exclusive, and they are not exclusive in deck create either. They are separate manifest fields, and DeckTile draws the image over the emoji — the emoji is the fallback when the image fails to load. Refusing the pair would have broken that. Both are settable in one call; --clear-cover removes both halves.

One thing this deliberately does not fix: deck create --front-lang es-ES still writes no spanish/language label, because it never did. That is a separate gap and changing an existing command's behaviour did not belong here.

Verification

./gradlew detektAll :cli:test green; 15 new tests in DeckEditTest covering the overlay, the two clear gestures, tag replacement, the retag, the no-op write and every refusal.

Then driven against a real staging homeserver on the live session, on a throwaway deck created and deleted in the same run, with two cards added partway through. Full table in journeys/RESULTS.md; the load-bearing rows:

Step Result
The same edit twice changed: false, updated_at unmoved — no second write
Edit a deck with 2 cards same deck id, card_count: 2, chunk table unchanged, card list intact
deck show in a fresh process every change is on the homeserver, not just in the cache
--back-lang fr-FR spanish dropped, french added, the author's own tags kept
--clear-tags on a language deck [] — the derived labels are not put back
--cover-url http://… exit 9, refused before the write
--clear-tags --tag x, --title=, --clear-cover --cover-emoji exit 2 each, refused rather than guessed at

🤖 Generated with Claude Code

https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye

`loopky` could create a deck and edit every card in it, but nothing about
the deck itself. The only route to a new cover or a fixed typo was
`deck delete` + `deck create --from-file`, which mints a **new deck id** —
so every `pubky://…/decks/<id>/` link breaks and every card goes back to
new in the scheduler — and which is impossible at all for a deck built
with `card add`, since there is no file to recreate it from (#222).

One `updateMetadata` call: the manifest and the tag records, never the
chunks, so it costs the same on a 20k-card deck as on an empty one.

Four rules, each already the surface's:

- An absent flag leaves its field alone (`card edit`'s rule), and an
  explicitly empty value clears it (`--description=`, which is `--back=`).
  `--clear-tags` and `--clear-cover` are switches because those two are
  not single values: `--tag ""` would have to mean both "no tags" and
  "one blank tag", and a cover is an emoji and an image that layer.
- `--tag` replaces rather than appends. Appending makes "the tags are
  exactly these" unsayable without a delete, and makes a retry after the
  hourly session expiry (#165) grow the list — the same property
  `card add`'s dedupe exists for.
- An edit whose fields all already hold that value writes nothing and
  reports `changed: false`. A manifest write bumps `updated_at`, which is
  what every follower's "the author published changes" badge reads.
- The derived language labels are reconciled only when the pair actually
  moves, so retyping a deck drops the old language's label — and
  `--clear-tags` on a language deck stays empty, because those are
  ordinary author-removable tags rather than a reserved family (§7.7).

The study opt-ins are in with the metadata rather than held back for
later: review state is keyed by `card_id` alone and the modes decide only
how a card is presented, so turning one off costs no progress. Reverse's
pairing is session-local and persisted nowhere.

`--cover-url` and `--cover-emoji` are *not* mutually exclusive, contrary
to the issue: they are separate manifest fields and `DeckTile` draws the
image over the emoji, which is its fallback. `--clear-cover` removes both.

Verified against a real staging homeserver end to end — see the #222
section of journeys/RESULTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116NZ92Pe7aGQpqtiRkP4Ye
@jvsena42
jvsena42 merged commit 8e7f34d into main Sep 4, 2026
4 checks passed
@jvsena42
jvsena42 deleted the feat/cli-deck-edit branch September 4, 2026 10:59
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.

No way to change a deck after creation: adding a cover means deleting and republishing the whole deck

1 participant