Repo refers to internal identifiers that start with 'logseq_db_' and used to start with 'logseq_local_'. The --repo option was not being used in that way and was just a reference to a graph name. While it's reasonable for internal CLIs like db-worker-node.js to use --repo, it is needlessly confusing to introduce repo to users. Almost all of our apps and docs label graphs as 'graph' and not 'repo'
10 KiB
Logseq CLI Upsert Tag Rename by ID Implementation Plan
Goal: Allow logseq upsert tag --id <id> --name <new-name> to rename an existing tag while preserving the current create-by-name and validate-by-id behavior.
Architecture: Keep the current logseq-cli -> transport/invoke -> :thread-api/apply-outliner-ops integration and implement rename-by-id entirely in the CLI command layer.
Architecture: Reuse the existing db-worker-node :rename-page outliner op instead of introducing a new thread API.
Architecture: Keep upsert tag --id <id> with no --name as an id-validation no-op for backward compatibility.
Tech Stack: ClojureScript, babashka.cli, Promesa, Datascript pull queries, db-worker-node outliner ops.
Related: Builds on /Users/rcmerci/gh-repos/logseq/docs/agent-guide/042-logseq-cli-add-tag-command.md, /Users/rcmerci/gh-repos/logseq/docs/agent-guide/043-logseq-cli-tag-property-management.md, and /Users/rcmerci/gh-repos/logseq/docs/agent-guide/045-logseq-cli-property-type-and-upsert-option-unification.md.
Problem statement
Current upsert tag validation in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs rejects --id and --name together with the error only one of --id or --name is allowed.
Current update mode in execute-upsert-tag only calls ensure-tag-by-id! and returns success without mutation when :mode is :update.
Current db-worker-node path already supports page rename through :thread-api/apply-outliner-ops with [:rename-page [page-uuid new-title]] in /Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs, so rename behavior can be reused without adding new APIs.
The user-visible gap is that a command like logseq upsert tag --graph <GRAPH> --id 180 --name "Project Renamed" should rename tag 180, but currently fails at option validation.
Testing Plan
I will use @test-driven-development for all implementation.
I will write all RED tests first in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs and /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljs before any production edits.
I will verify RED failures are behavior failures by asserting error codes or missing mutation ops rather than fixture or async setup problems.
I will use @clojure-debug only if async transport stubs or db-worker test harness behavior is unclear.
NOTE: I will write all tests before I add any implementation behavior.
Current implementation baseline
| Requirement | Current behavior | Gap |
|---|---|---|
upsert tag --id <id> --name <name> renames an existing tag. |
invalid-options? for :upsert-tag rejects mixed selectors in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs. |
Rename-by-id cannot be triggered. |
upsert tag --id <id> stays supported. |
execute-upsert-tag update mode validates id and returns [id] without mutation. |
Must remain unchanged for compatibility. |
| Rename execution uses existing db-worker-node contracts. | db-worker-node already handles :rename-page via :thread-api/apply-outliner-ops in /Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs. |
CLI does not currently call rename op in tag update path. |
Target contract
upsert tag --name <name> keeps create or idempotent-create semantics.
upsert tag --id <id> keeps id-validation no-op semantics and returns the same id.
upsert tag --id <id> --name <new-name> renames the target tag to <new-name> using the existing outliner rename op.
upsert tag --id <id> --name <new-name> must fail with :upsert-id-not-found or :upsert-id-type-mismatch when id is invalid or not a tag.
upsert tag --id <id> --name <new-name> must no-op when <new-name> normalizes to the same :block/name as the target.
upsert tag --id <id> --name <new-name> must fail if <new-name> belongs to another non-tag page with :tag-name-conflict.
upsert tag --id <id> --name <new-name> must fail if <new-name> belongs to another existing tag to avoid ambiguous cross-tag merges.
Architecture sketch
CLI
logseq upsert tag --id 180 --name "Project Renamed"
-> /src/main/logseq/cli/commands.cljs finalize-command
-> /src/main/logseq/cli/command/upsert.cljs build-tag-action
-> /src/main/logseq/cli/command/upsert.cljs execute-upsert-tag
1) ensure-tag-by-id!
2) conflict lookup by target name
3) transport/invoke :thread-api/apply-outliner-ops
[repo [[:rename-page [tag-uuid new-name]]] {}]
4) pull by id and return same id
DB Worker
/src/main/frontend/worker/db_core.cljs
:rename-page handler -> outliner-core/save-block! with new title
Detailed implementation plan
- Add a RED parse/build test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsassertingupsert tag --id 180 --name "Project Renamed"is accepted and routed to:upsert-tag. - Add a RED action test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsasserting update-mode tag action keeps:idand includes normalized:name. - Add a RED execute test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsasserting update-mode with both id and name emits exactly one:rename-pageop through:thread-api/apply-outliner-ops. - Add a RED execute test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsasserting update-mode with id-only remains no-op and does not call:thread-api/apply-outliner-ops. - Add a RED execute test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsasserting rename target conflict with non-tag page returns:tag-name-conflict. - Add a RED execute test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljsasserting rename target conflict with another tag returns a dedicated conflict error code. - Add a RED integration test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljsthat creates a tag, fetches its id, runsupsert tag --id <id> --name <new-name>, and verifies the new title appears inlist tag. - Add a RED integration test in
/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/integration_test.cljsasserting the old name no longer appears inlist tagafter rename. - Run focused RED commands and confirm failures are expected contract failures.
- Update
invalid-options?for:upsert-tagin/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljsto permit--idplus--nameand keep invalid checks for empty or malformed names. - Update
build-tag-actionin/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljsso update mode accepts optional:nameand keeps create mode semantics unchanged. - Add helper logic in
/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljsto detect whether normalized rename target equals current tag name and skip mutation in that case. - Add helper logic in
/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljsto pull by target name and detect conflicts with other entities before rename. - Update
execute-upsert-tagin/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljsupdate branch to call:rename-pagewhen id and name are both provided. - Keep error handling in
execute-upsert-tagaligned with existing:upsert-id-not-foundand:upsert-id-type-mismatchcontracts, and add one dedicated rename-conflict code for tag-to-tag collisions. - Update CLI reference docs in
/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.mdto documentupsert tag --id <id> --name <name>rename semantics and conflict behavior. - Run focused GREEN tests for
commands_testand targeted integration tests. - Run
bb dev:test -v logseq.cli.commands-testandbb dev:test -v logseq.cli.integration-testto confirm no regressions. - Run
bb dev:lint-and-testas final verification. - Refactor duplicated tag-name normalization or conflict checks only after GREEN, then rerun focused tests.
Edge cases
--namewith leading#in update mode should normalize exactly like create mode.--namethat trims to blank should return:invalid-optionsand not hit db-worker.- Rename to current name with different casing should follow normalized-name no-op behavior.
- Rename target that already exists as another tag should fail deterministically and not mutate either tag.
- Rename target that exists as a non-tag page should fail with
:tag-name-conflict. - Rename by id must preserve the original
:db/idin command output. - Id-mode not-found and type-mismatch errors must remain stable for scripts.
Verification commands and expected output
| Command | Expected output |
|---|---|
bb dev:test -v logseq.cli.commands-test |
Upsert tag parse, build, and execute tests pass including rename-by-id and no-op id-only paths. |
bb dev:test -v logseq.cli.integration-test/test-cli-upsert-tag-id-rename |
End-to-end rename-by-id test passes with renamed tag visible in list output. |
bb dev:test -v logseq.cli.integration-test/test-cli-upsert-tag-id-rename-conflict |
Conflict behavior test passes and returns expected error code/message. |
bb dev:lint-and-test |
Full suite passes with exit code 0. |
Testing Details
The new tests verify user-observable behavior at parser, action, executor, and CLI integration levels.
The tests assert command outputs, mutation calls, and list/query observable state instead of helper internals.
The tests keep existing id-only no-op behavior covered so rename support does not regress current automation scripts.
Implementation Details
- Modify tag option validation only in
/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs. - Keep db-worker-node thread API signatures unchanged in
/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs. - Reuse existing
:rename-pageoutliner op instead of introducing a new op. - Preserve create-by-name idempotency path for
upsert tag --name. - Preserve id-only validate path for
upsert tag --id. - Add deterministic rename conflict handling before invoking rename op.
- Keep error code stability for existing id lookup and type mismatch failures.
- Update CLI docs to reflect rename-by-id and no-op-by-id contracts.
- Follow
@test-driven-developmentsequence strictly and use@clojure-debugonly for harness issues.
Question
Resolved: choose option 1.
Rename-to-existing-tag returns a dedicated conflict error and must not be treated as success by returning the existing tag id.
This prevents implicit merges and accidental retargeting.