017-logseq-cli-db-worker-node-housekeeping-2.md

This commit is contained in:
rcmerci
2026-01-25 16:12:28 +08:00
parent 9edbe1be3f
commit 83bb0b2da6
10 changed files with 393 additions and 176 deletions

View File

@@ -0,0 +1,97 @@
# Logseq CLI Housekeeping 2 Implementation Plan
Goal: Simplify CLI options for show, move, and remove while keeping db-worker-node behavior unchanged.
Architecture: The changes are limited to CLI option parsing, action building, and output formatting in the CLI layer.
The db-worker-node API calls remain the same, but we will verify expected input shapes for delete and move operations.
We will centralize shared id parsing so show and remove stay consistent.
Tech Stack: ClojureScript, babashka.cli, promesa, Logseq CLI, db-worker-node thread-api.
Related: Builds on docs/agent-guide/015-logseq-cli-db-worker-node-housekeeping.md and relates to docs/agent-guide/014-logseq-cli-show-multi-id.md.
## Testing Plan
I will follow @test-driven-development by writing failing tests for each new CLI behavior before changing implementation.
I will add unit tests in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs for parsing and validation of the new remove options and renamed flags.
I will add unit tests for show option parsing to accept --page and reject --page-name and --format, while preserving global --output handling.
I will add unit tests for move option parsing to accept --target-page and reject --target-page-name.
I will add unit tests for remove parsing to accept --id, --uuid, and --page, and to reject multiple selectors or missing target.
I will add unit tests for remove parsing to accept multi-id vectors and to reject invalid or empty vectors.
I will run the CLI test namespace and confirm the new tests fail before any implementation changes.
I will rerun the CLI tests after each behavioral change to confirm they pass.
Command to run tests is shown below.
```bash
bb dev:test -v logseq.cli.commands-test
```
Expected test output is described below.
The output should include zero failures and zero errors for logseq.cli.commands-test.
NOTE: I will write *all* tests before I add any implementation behavior.
## Problem statement
The CLI currently exposes overlapping option names and per-command format flags that conflict with the global output option.
The remove command is split into remove block and remove page, which makes scripting awkward and inconsistent with show and move selectors.
The move and show commands use page-name flags that should be renamed for clarity and consistency across commands.
We need a small, coordinated change that updates CLI parsing, action building, and documentation without changing db-worker-node APIs.
## Plan
1. Review current CLI option specs and validation in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs, /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs, and /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/move.cljs to confirm existing behavior and data shapes.
2. Review db-worker-node call sites for delete and move operations by searching for :delete-blocks and :delete-page usages to confirm expected argument shapes in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs and related call sites.
3. Add failing parsing and validation tests for the unified remove command and renamed flags in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs.
4. Add failing tests that assert legacy flags and subcommands are rejected in /Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs.
5. Run the CLI test namespace and record the failing cases using the test command in the testing plan.
6. Extract the show id parsing logic into a shared helper in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/id.cljs or a similar shared namespace, and update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs to use it.
7. Update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/remove.cljs to define a single remove spec with --id, --uuid, and --page, and to build actions based on which selector is present.
8. Update remove execution to support single and multiple id deletion while preserving page deletion behavior, and ensure returned data matches existing format expectations.
9. Update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs to rename --page-name to --page and remove the --format option and related validation.
10. Update show execution to use the resolved output format from config instead of a command-specific flag, while preserving human-readable output for the default format.
11. Update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/move.cljs to rename --target-page-name to --target-page and adjust validation and target resolution accordingly.
12. Update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs to reflect the new remove command, show selector, and move target flag in validation, action building, and help routing.
13. Update /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs if the new remove action type changes the command name used for human formatting.
14. Update CLI usage text in /Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/main.cljs to remove the remove block and remove page references.
15. Update CLI documentation and examples in /Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md to use --page, --target-page, and the unified remove command without --format.
16. Rerun the CLI test namespace and confirm all tests pass.
17. Run bb dev:lint-and-test if time permits and confirm there are no regressions in other CLI tests.
18. Review the changes against @prompts/review.md and ensure the updated flags are reflected everywhere in docs and tests.
## Edge cases
Removing blocks with a vector of ids that contains non-integers should produce a clear invalid options error.
Removing blocks with an empty id vector should produce a clear invalid options error.
Removing with both --id and --uuid should fail validation with a single-selector error.
Removing with both --page and a block selector should fail validation with a single-selector error.
Showing with --page should still reject invalid level values and missing targets.
Showing with --output json or edn should return structured data rather than human text for both single and multi-id cases.
Moving with --target-page should still reject --pos sibling.
Legacy flags like --page-name, --target-page-name, and show --format should be rejected by the parser with invalid-options errors.
## Testing Details
I will focus tests on CLI behavior by asserting parse-args results, invalid option errors, and build-action normalization for ids and selectors.
I will avoid mock-only tests and instead assert actual validation behavior and action shapes that drive CLI execution.
## Implementation Details
- Consolidate remove command parsing around a single spec and selector validation.
- Share id parsing between show and remove to keep behavior identical.
- Keep db-worker-node API calls unchanged and only adjust CLI argument shapes.
- Use config output-format in show execution to decide between human text and structured data.
- Remove show-specific format validation and option from the CLI help output.
- Rename show and move page flags and update all associated validation logic.
- Update CLI documentation and examples to match the new flags and remove subcommands.
- Update CLI help routing so logseq remove behaves like a command, not a group.
## Decisions
- Remove `--page-name` and `--target-page-name` entirely (no aliases or warnings).
- For remove with multiple ids, continue with best-effort deletion (do not fail on the first missing id).
- For remove ids, allow only the show-style vector and single value formats (no repeated `--id` flags).
---

View File

@@ -69,14 +69,13 @@ Inspect and edit commands:
- `add block --blocks <edn> [--target-page-name <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector
- `add block --blocks-file <path> [--target-page-name <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file
- `add page --page <name>` - create a page
- `move --id <id>|--uuid <uuid> --target-id <id>|--target-uuid <uuid>|--target-page-name <name> [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child)
- `remove block --block <uuid>` - remove a block and its children
- `remove page --page <name>` - remove a page and its children
- `move --id <id>|--uuid <uuid> --target-id <id>|--target-uuid <uuid>|--target-page <name> [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child)
- `remove --id <id>|--uuid <uuid>|--page <name>` - remove blocks (by db/id or UUID) or pages
- `search <query> [--type page|block|tag|property|all] [--tag <name>] [--case-sensitive] [--sort updated-at|created-at] [--order asc|desc]` - search across pages, blocks, tags, and properties (query is positional)
- `query --query <edn> [--inputs <edn-vector>]` - run a Datascript query against the graph
- `show --page-name <name> [--format text|json|edn] [--level <n>]` - show page tree
- `show --uuid <uuid> [--format text|json|edn] [--level <n>]` - show block tree
- `show --id <id> [--format text|json|edn] [--level <n>]` - show block tree by db/id
- `show --page <name> [--level <n>]` - show page tree
- `show --uuid <uuid> [--level <n>]` - show block tree
- `show --id <id> [--level <n>]` - show block tree by db/id
Help output:
@@ -88,8 +87,7 @@ Subcommands:
add block [options] Add blocks
add page [options] Create page
move [options] Move block
remove block [options] Remove block
remove page [options] Remove page
remove [options] Remove block or page
search <query> [options] Search graph
show [options] Show tree
```
@@ -106,7 +104,7 @@ Revision: <commit>
```
Output formats:
- Global `--output <human|json|edn>` (also accepted per subcommand)
- Global `--output <human|json|edn>` applies to all commands
- For `graph export`, `--output` refers to the destination file path. Output formatting is controlled via `:output-format` in config or `LOGSEQ_CLI_OUTPUT`.
- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list and search subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. Search table columns are `ID` and `TITLE`. Block titles can include multiple lines; multi-line rows align additional lines under the `TITLE` column. Times such as list `UPDATED-AT`/`CREATED-AT` and `graph info` `Created at` are shown in human-friendly relative form. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output.
- `query` human output returns a plain string (the query result rendered via `pr-str`), which is convenient for pipelines like `logseq query ... | xargs logseq show --id`.
@@ -131,8 +129,8 @@ node ./dist/logseq.js graph create --repo demo
node ./dist/logseq.js graph export --type edn --output /tmp/demo.edn --repo demo
node ./dist/logseq.js graph import --type edn --input /tmp/demo.edn --repo demo-import
node ./dist/logseq.js add block --target-page-name TestPage --content "hello world"
node ./dist/logseq.js move --uuid <uuid> --target-page-name TargetPage
node ./dist/logseq.js move --uuid <uuid> --target-page TargetPage
node ./dist/logseq.js search "hello"
node ./dist/logseq.js show --page-name TestPage --format json --output json
node ./dist/logseq.js show --page TestPage --output json
node ./dist/logseq.js server list
```

View File

@@ -0,0 +1,49 @@
(ns logseq.cli.command.id
"Shared id parsing helpers for CLI commands."
(:require [clojure.string :as string]
[logseq.common.util :as common-util]))
(defn valid-id?
[value]
(and (number? value) (integer? value)))
(defn parse-id-option
[value]
(let [invalid (fn [message]
{:ok? false :message message})]
(cond
(nil? value)
{:ok? true :value nil :multi? false}
(vector? value)
(cond
(empty? value) (invalid "id vector must contain at least one id")
(every? valid-id? value) {:ok? true :value (vec value) :multi? true}
:else (invalid "id vector must contain only integers"))
(valid-id? value)
{:ok? true :value [value] :multi? false}
(string? value)
(let [text (string/trim value)]
(cond
(string/blank? text)
(invalid "id is required")
(string/starts-with? text "[")
(let [parsed (common-util/safe-read-string {:log-error? false} text)]
(cond
(nil? parsed) (invalid "invalid id edn")
(not (vector? parsed)) (invalid "id must be a vector")
(empty? parsed) (invalid "id vector must contain at least one id")
(every? valid-id? parsed) {:ok? true :value (vec parsed) :multi? true}
:else (invalid "id vector must contain only integers")))
(re-matches #"-?\\d+" text)
{:ok? true :value [(js/parseInt text 10)] :multi? false}
:else
(invalid "id must be a number or vector of numbers")))
:else
(invalid "id must be a number or vector of numbers"))))

View File

@@ -14,7 +14,7 @@
:target-id {:desc "Target block db/id"
:coerce :long}
:target-uuid {:desc "Target block UUID"}
:target-page-name {:desc "Target page name"}
:target-page {:desc "Target page name"}
:pos {:desc "Position (first-child, last-child, sibling)"}})
(def entries
@@ -29,7 +29,7 @@
source-selectors (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])
target-selectors (filter some? [(:target-id opts)
(:target-uuid opts)
(some-> (:target-page-name opts) string/trim)])]
(some-> (:target-page opts) string/trim)])]
(cond
(and (seq pos) (not (contains? move-positions pos)))
(str "invalid pos: " (:pos opts))
@@ -38,9 +38,9 @@
"only one of --id or --uuid is allowed"
(> (count target-selectors) 1)
"only one of --target-id, --target-uuid, or --target-page-name is allowed"
"only one of --target-id, --target-uuid, or --target-page is allowed"
(and (= pos "sibling") (seq (some-> (:target-page-name opts) string/trim)))
(and (= pos "sibling") (seq (some-> (:target-page opts) string/trim)))
"--pos sibling is only valid for block targets"
:else
@@ -86,7 +86,7 @@
(p/rejected (ex-info "source is required" {:code :missing-source}))))
(defn- resolve-target
[config repo {:keys [target-id target-uuid target-page-name]}]
[config repo {:keys [target-id target-uuid target-page]}]
(cond
(some? target-id)
(p/let [entity (transport/invoke config :thread-api/pull false
@@ -103,10 +103,10 @@
(ensure-non-page entity "target must be a block" :invalid-target)
(throw (ex-info "target block not found" {:code :target-not-found})))))
(seq target-page-name)
(seq target-page)
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title]
[:block/name target-page-name]])]
[:block/name target-page]])]
(if (:db/id entity)
entity
(throw (ex-info "page not found" {:code :page-not-found}))))
@@ -132,7 +132,7 @@
uuid (some-> (:uuid options) string/trim)
target-id (:target-id options)
target-uuid (some-> (:target-uuid options) string/trim)
page-name (some-> (:target-page-name options) string/trim)
page-name (some-> (:target-page options) string/trim)
pos (some-> (:pos options) string/trim string/lower-case)
source-label (cond
(seq uuid) uuid
@@ -163,7 +163,7 @@
:uuid uuid
:target-id target-id
:target-uuid target-uuid
:target-page-name page-name
:target-page page-name
:pos (or pos "first-child")
:source source-label
:target target-label}}))))

View File

@@ -2,32 +2,93 @@
"Remove-related CLI commands."
(:require [clojure.string :as string]
[logseq.cli.command.core :as core]
[logseq.cli.command.id :as id-command]
[logseq.cli.server :as cli-server]
[logseq.cli.transport :as transport]
[logseq.common.util :as common-util]
[promesa.core :as p]))
(def ^:private remove-block-spec
{:block {:desc "Block UUID"}})
(def ^:private remove-page-spec
{:page {:desc "Page name"}})
(def ^:private remove-spec
{:id {:desc "Block db/id or EDN vector of ids"}
:uuid {:desc "Block UUID"}
:page {:desc "Page name"}})
(def entries
[(core/command-entry ["remove" "block"] :remove-block "Remove block" remove-block-spec)
(core/command-entry ["remove" "page"] :remove-page "Remove page" remove-page-spec)])
[(core/command-entry ["remove"] :remove "Remove blocks or pages" remove-spec)])
(defn invalid-options?
[opts]
(let [id-result (id-command/parse-id-option (:id opts))]
(cond
(and (some? (:id opts)) (not (:ok? id-result)))
(:message id-result)
:else
nil)))
(def ^:private block-id-selector
[:db/id :block/uuid])
(defn- fetch-block-by-id
[config repo id]
(transport/invoke config :thread-api/pull false
[repo block-id-selector id]))
(defn- fetch-block-by-uuid
[config repo uuid-str]
(p/let [entity (transport/invoke config :thread-api/pull false
[repo block-id-selector [:block/uuid (uuid uuid-str)]])]
(if (:db/id entity)
entity
(transport/invoke config :thread-api/pull false
[repo block-id-selector [:block/uuid uuid-str]]))))
(defn- delete-block-ids
[config repo ids]
(transport/invoke config :thread-api/apply-outliner-ops false
[repo [[:delete-blocks [ids {}]]] {}]))
(defn- remove-block-id
[config repo id]
(p/let [entity (fetch-block-by-id config repo id)]
(if (:db/id entity)
(delete-block-ids config repo [id])
(throw (ex-info "block not found" {:code :block-not-found})))))
(defn- remove-block-ids-best-effort
[config repo ids]
(p/let [entities (p/all (map (fn [id]
(fetch-block-by-id config repo id))
ids))
id-entities (map vector ids entities)
existing-ids (vec (keep (fn [[id entity]]
(when (:db/id entity) id))
id-entities))
missing-ids (vec (keep (fn [[id entity]]
(when-not (:db/id entity) id))
id-entities))
result (if (seq existing-ids)
(delete-block-ids config repo existing-ids)
nil)]
{:deleted-ids existing-ids
:missing-ids missing-ids
:result result}))
(defn- perform-remove
[config {:keys [repo block page]}]
[config {:keys [repo ids multi-id? uuid page]}]
(cond
(seq block)
(if-not (common-util/uuid-string? block)
(and (seq ids) multi-id?)
(remove-block-ids-best-effort config repo ids)
(seq ids)
(remove-block-id config repo (first ids))
(seq uuid)
(if-not (common-util/uuid-string? uuid)
(p/rejected (ex-info "block must be a uuid" {:code :invalid-block}))
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid] [:block/uuid (uuid block)]])]
(p/let [entity (fetch-block-by-uuid config repo uuid)]
(if-let [id (:db/id entity)]
(transport/invoke config :thread-api/apply-outliner-ops false
[repo [[:delete-blocks [[id] {}]]] {}])
(delete-block-ids config repo [id])
(throw (ex-info "block not found" {:code :block-not-found})))))
(seq page)
@@ -41,41 +102,48 @@
:else
(p/rejected (ex-info "block or page required" {:code :missing-target}))))
(defn build-remove-block-action
(defn build-action
[options repo]
(if-not (seq repo)
{:ok? false
:error {:code :missing-repo
:message "repo is required for remove"}}
(let [block (some-> (:block options) string/trim)]
(if (seq block)
{:ok? true
:action {:type :remove-block
:repo repo
:block block}}
(let [id-result (id-command/parse-id-option (:id options))
ids (:value id-result)
multi-id? (:multi? id-result)
uuid (some-> (:uuid options) string/trim)
page (some-> (:page options) string/trim)
selectors (filter some? [(:id options) uuid page])]
(cond
(empty? selectors)
{:ok? false
:error {:code :missing-target
:message "block is required"}}))))
:message "block or page is required"}}
(defn build-remove-page-action
[options repo]
(if-not (seq repo)
{:ok? false
:error {:code :missing-repo
:message "repo is required for remove"}}
(let [page (some-> (:page options) string/trim)]
(if (seq page)
{:ok? true
:action {:type :remove-page
:repo repo
:page page}}
(> (count selectors) 1)
{:ok? false
:error {:code :missing-target
:message "page is required"}}))))
:error {:code :invalid-options
:message "only one of --id, --uuid, or --page is allowed"}}
(and (some? (:id options)) (not (:ok? id-result)))
{:ok? false
:error {:code :invalid-options
:message (:message id-result)}}
:else
{:ok? true
:action {:type :remove
:repo repo
:id (when (and (seq ids) (not multi-id?)) (first ids))
:ids ids
:multi-id? multi-id?
:uuid uuid
:page page}}))))
(defn execute-remove
[action config]
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
result (perform-remove cfg action)]
{:status :ok
:data {:result result}})))
:data (cond-> {:result result}
(map? result) (merge (dissoc result :result)))})))

View File

@@ -1,6 +1,7 @@
(ns logseq.cli.command.show
"Show-related CLI commands."
(:require [clojure.string :as string]
[logseq.cli.command.id :as id-command]
[logseq.cli.command.core :as core]
[logseq.cli.server :as cli-server]
[logseq.cli.transport :as transport]
@@ -10,73 +11,20 @@
(def ^:private show-spec
{:id {:desc "Block db/id or EDN vector of ids"}
:uuid {:desc "Block UUID"}
:page-name {:desc "Page name"}
:page {:desc "Page name"}
:level {:desc "Limit tree depth"
:coerce :long}
:format {:desc "Output format (text, json, edn)"}})
:coerce :long}})
(def entries
[(core/command-entry ["show"] :show "Show tree" show-spec)])
(def ^:private show-formats
#{"text" "json" "edn"})
(def ^:private multi-id-delimiter "\n================================================================\n")
(defn- valid-id?
[value]
(and (number? value) (integer? value)))
(defn- parse-id-option
[value]
(let [invalid (fn [message]
{:ok? false :message message})]
(cond
(nil? value)
{:ok? true :value nil :multi? false}
(vector? value)
(cond
(empty? value) (invalid "id vector must contain at least one id")
(every? valid-id? value) {:ok? true :value (vec value) :multi? true}
:else (invalid "id vector must contain only integers"))
(valid-id? value)
{:ok? true :value [value] :multi? false}
(string? value)
(let [text (string/trim value)]
(cond
(string/blank? text)
(invalid "id is required")
(string/starts-with? text "[")
(let [parsed (common-util/safe-read-string {:log-error? false} text)]
(cond
(nil? parsed) (invalid "invalid id edn")
(not (vector? parsed)) (invalid "id must be a vector")
(empty? parsed) (invalid "id vector must contain at least one id")
(every? valid-id? parsed) {:ok? true :value (vec parsed) :multi? true}
:else (invalid "id vector must contain only integers")))
(re-matches #"-?\\d+" text)
{:ok? true :value [(js/parseInt text 10)] :multi? false}
:else
(invalid "id must be a number or vector of numbers")))
:else
(invalid "id must be a number or vector of numbers"))))
(defn invalid-options?
[opts]
(let [format (:format opts)
level (:level opts)
id-result (parse-id-option (:id opts))]
(let [level (:level opts)
id-result (id-command/parse-id-option (:id opts))]
(cond
(and (seq format) (not (contains? show-formats (string/lower-case format))))
(str "invalid format: " format)
(and (some? level) (< level 1))
"level must be >= 1"
@@ -367,7 +315,7 @@
(build root-id 1)))
(defn- fetch-tree
[config {:keys [repo id page-name level] :as opts}]
[config {:keys [repo id page level] :as opts}]
(let [max-depth (or level 10)
uuid-str (:uuid opts)]
(cond
@@ -414,12 +362,12 @@
{:root (assoc entity :block/children children)})
(throw (ex-info "block not found" {:code :block-not-found}))))))
(seq page-name)
(seq page)
(p/let [page-entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/title
{:logseq.property/status [:db/ident :block/name :block/title]}
{:block/tags [:db/id :block/name :block/title :block/uuid]}]
[:block/name page-name]])]
[:block/name page]])]
(if-let [page-id (:db/id page-entity)]
(p/let [blocks (fetch-blocks-for-page config repo page-id)
children (build-tree blocks page-id max-depth)]
@@ -491,11 +439,10 @@
{:ok? false
:error {:code :missing-repo
:message "repo is required for show"}}
(let [format (some-> (:format options) string/lower-case)
id-result (parse-id-option (:id options))
(let [id-result (id-command/parse-id-option (:id options))
ids (:value id-result)
multi-id? (:multi? id-result)
targets (filter some? [(:id options) (:uuid options) (:page-name options)])]
targets (filter some? [(:id options) (:uuid options) (:page options)])]
(if (empty? targets)
{:ok? false
:error {:code :missing-target
@@ -511,9 +458,8 @@
:ids ids
:multi-id? multi-id?
:uuid (:uuid options)
:page-name (:page-name options)
:level (:level options)
:format format}})))))
:page (:page options)
:level (:level options)}})))))
(defn- build-tree-data
[config action]
@@ -565,7 +511,7 @@
(defn execute-show
[action config]
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
format (:format action)
format (:output-format config)
ids (:ids action)
multi-id? (:multi-id? action)]
(if (and (seq ids) multi-id?)
@@ -594,7 +540,7 @@
(and ok? (contained? id)))
results))
payload (case format
"edn"
:edn
{:status :ok
:data (mapv (fn [{:keys [ok? tree id error]}]
(if ok?
@@ -603,7 +549,7 @@
results)
:output-format :edn}
"json"
:json
{:status :ok
:data (mapv (fn [{:keys [ok? tree id error]}]
(if ok?
@@ -622,12 +568,12 @@
payload)
(p/let [tree-data (build-tree-data cfg action)]
(case format
"edn"
:edn
{:status :ok
:data tree-data
:output-format :edn}
"json"
:json
{:status :ok
:data tree-data
:output-format :json}

View File

@@ -121,11 +121,14 @@
(seq (:blocks opts))
(seq (:blocks-file opts))
has-args?)
show-targets (filter some? [(:id opts) (:uuid opts) (:page-name opts)])
show-targets (filter some? [(:id opts) (:uuid opts) (:page opts)])
remove-targets (filter some? [(:id opts)
(some-> (:uuid opts) string/trim)
(some-> (:page opts) string/trim)])
move-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)])
move-targets (filter some? [(:target-id opts)
(some-> (:target-uuid opts) string/trim)
(some-> (:target-page-name opts) string/trim)])]
(some-> (:target-page opts) string/trim)])]
(cond
(:help opts)
(command-core/help-result cmd-summary)
@@ -143,11 +146,14 @@
(and (= command :add-page) (not (seq (:page opts))))
(missing-page-name-result summary)
(and (= command :remove-block) (not (seq (:block opts))))
(and (= command :remove) (seq args))
(command-core/invalid-options-result summary "remove does not accept subcommands")
(and (= command :remove) (empty? remove-targets))
(missing-target-result summary)
(and (= command :remove-page) (not (seq (:page opts))))
(missing-target-result summary)
(and (= command :remove) (> (count remove-targets) 1))
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page is allowed")
(and (= command :move-block) (move-command/invalid-options? opts))
(command-core/invalid-options-result summary (move-command/invalid-options? opts))
@@ -162,7 +168,7 @@
(missing-target-result summary)
(and (= command :show) (> (count show-targets) 1))
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page-name is allowed")
(command-core/invalid-options-result summary "only one of --id, --uuid, or --page is allowed")
(and (= command :query)
(not (seq (some-> (:query opts) string/trim)))
@@ -173,6 +179,9 @@
(list-command/invalid-options? command opts))
(command-core/invalid-options-result summary (list-command/invalid-options? command opts))
(and (= command :remove) (remove-command/invalid-options? opts))
(command-core/invalid-options-result summary (remove-command/invalid-options? opts))
(and (= command :show) (show-command/invalid-options? opts))
(command-core/invalid-options-result summary (show-command/invalid-options? opts))
@@ -226,7 +235,7 @@
:error {:code :missing-command
:message "missing command"}
:summary summary})
(if (and (= 1 (count args)) (#{"graph" "server" "list" "add" "remove" "query"} (first args)))
(if (and (= 1 (count args)) (#{"graph" "server" "list" "add" "query"} (first args)))
(command-core/help-result (command-core/group-summary (first args) table))
(try
(let [result (cli/dispatch table args {:spec global-spec})]
@@ -331,11 +340,8 @@
:move-block
(move-command/build-action options repo)
:remove-block
(remove-command/build-remove-block-action options repo)
:remove-page
(remove-command/build-remove-page-action options repo)
:remove
(remove-command/build-action options repo)
:query
(query-command/build-action options repo config)
@@ -377,8 +383,7 @@
:add-block (add-command/execute-add-block action config)
:add-page (add-command/execute-add-page action config)
:move-block (move-command/execute-move action config)
:remove-block (remove-command/execute-remove action config)
:remove-page (remove-command/execute-remove action config)
:remove (remove-command/execute-remove action config)
:query (query-command/execute-query action config)
:query-list (query-command/execute-query-list action config)
:show (show-command/execute-show action config)
@@ -392,4 +397,4 @@
:message "unknown action"}}))]
(assoc result
:command (or (:command action) (:type action))
:context (select-keys action [:repo :graph :page :block :blocks :source :target])))))
:context (select-keys action [:repo :graph :page :id :ids :uuid :block :blocks :source :target])))))

View File

@@ -234,13 +234,14 @@
[{:keys [repo page]}]
(str "Added page: " page " (repo: " repo ")"))
(defn- format-remove-page
[{:keys [repo page]}]
(str "Removed page: " page " (repo: " repo ")"))
(defn- format-remove-block
[{:keys [repo block]}]
(str "Removed block: " block " (repo: " repo ")"))
(defn- format-remove
[{:keys [repo page uuid id ids]}]
(cond
(seq page) (str "Removed page: " page " (repo: " repo ")")
(seq uuid) (str "Removed block: " uuid " (repo: " repo ")")
(seq ids) (str "Removed blocks: " (count ids) " (repo: " repo ")")
(some? id) (str "Removed block: " id " (repo: " repo ")")
:else (str "Removed item (repo: " repo ")")))
(defn- format-move-block
[{:keys [repo source target]}]
@@ -283,8 +284,7 @@
(:list-tag :list-property) (format-list-tag-or-property (:items data) now-ms)
:add-block (format-add-block context)
:add-page (format-add-page context)
:remove-page (format-remove-page context)
:remove-block (format-remove-block context)
:remove (format-remove context)
:move-block (format-move-block context)
:graph-export (format-graph-export context)
:graph-import (format-graph-import context)

View File

@@ -13,7 +13,7 @@
(string/join "\n"
["logseq <command> [options]"
""
"Commands: list page, list tag, list property, add block, add page, move, remove block, remove page, query, query list, show, graph list, graph create, graph switch, graph remove, graph validate, graph info, graph export, graph import, server list, server status, server start, server stop, server restart"
"Commands: list page, list tag, list property, add block, add page, move, remove, query, query list, show, graph list, graph create, graph switch, graph remove, graph validate, graph info, graph export, graph import, server list, server status, server start, server stop, server restart"
""
"Options:"
summary]))

View File

@@ -57,12 +57,12 @@
(is (string/includes? summary "add block"))
(is (string/includes? summary "add page"))))
(testing "remove group shows subcommands"
(let [result (commands/parse-args ["remove"])
(testing "remove command shows help"
(let [result (commands/parse-args ["remove" "--help"])
summary (:summary result)]
(is (true? (:help? result)))
(is (string/includes? summary "remove block"))
(is (string/includes? summary "remove page"))))
(is (string/includes? summary "Usage: logseq remove"))
(is (string/includes? summary "Command options:"))))
(testing "move command shows help"
(let [result (commands/parse-args ["move" "--help"])
@@ -145,6 +145,14 @@
(is (false? (:ok? result)))
(is (= :unknown-command (get-in result [:error :code]))))))
(deftest test-parse-args-rejects-legacy-remove-subcommands
(testing "rejects legacy remove subcommands"
(doseq [args [["remove" "block"]
["remove" "page"]]]
(let [result (commands/parse-args args)]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))))
(deftest test-parse-args-rejects-graph-option
(testing "rejects legacy --graph option"
(let [result (commands/parse-args ["--graph" "demo" "graph" "list"])]
@@ -433,23 +441,44 @@
(is (= :add-page (:command result)))
(is (= "Home" (get-in result [:options :page])))))
(testing "remove block requires target"
(let [result (commands/parse-args ["remove" "block"])]
(testing "remove requires target"
(let [result (commands/parse-args ["remove"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "remove block parses with block"
(let [result (commands/parse-args ["remove" "block" "--block" "demo"])]
(testing "remove parses with id"
(let [result (commands/parse-args ["remove" "--id" "10"])]
(is (true? (:ok? result)))
(is (= :remove-block (:command result)))
(is (= "demo" (get-in result [:options :block])))))
(is (= :remove (:command result)))
(is (= 10 (get-in result [:options :id])))))
(testing "remove page parses with page"
(let [result (commands/parse-args ["remove" "page" "--page" "Home"])]
(testing "remove parses with uuid"
(let [result (commands/parse-args ["remove" "--uuid" "abc"])]
(is (true? (:ok? result)))
(is (= :remove-page (:command result)))
(is (= :remove (:command result)))
(is (= "abc" (get-in result [:options :uuid])))))
(testing "remove parses with page"
(let [result (commands/parse-args ["remove" "--page" "Home"])]
(is (true? (:ok? result)))
(is (= :remove (:command result)))
(is (= "Home" (get-in result [:options :page])))))
(testing "remove rejects multiple selectors"
(let [result (commands/parse-args ["remove" "--id" "1" "--page" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects empty id vector"
(let [result (commands/parse-args ["remove" "--id" "[]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "remove rejects invalid id vector"
(let [result (commands/parse-args ["remove" "--id" "[1 \"no\"]"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move requires source selector"
(let [result (commands/parse-args ["move" "--target-id" "10"])]
(is (false? (:ok? result)))
@@ -468,17 +497,25 @@
(is (= "def" (get-in result [:options :target-uuid])))
(is (= "last-child" (get-in result [:options :pos]))))))
(deftest test-verb-subcommand-parse-move-target-page
(testing "move parses with target page"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page" "Home"])]
(is (true? (:ok? result)))
(is (= :move-block (:command result)))
(is (= 1 (get-in result [:options :id])))
(is (= "Home" (get-in result [:options :target-page]))))))
(deftest test-verb-subcommand-parse-show
(testing "show requires target"
(let [result (commands/parse-args ["show"])]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "show parses with page name"
(let [result (commands/parse-args ["show" "--page-name" "Home"])]
(testing "show parses with page"
(let [result (commands/parse-args ["show" "--page" "Home"])]
(is (true? (:ok? result)))
(is (= :show (:command result)))
(is (= "Home" (get-in result [:options :page-name])))))
(is (= "Home" (get-in result [:options :page])))))
(testing "show parses with id vector"
(let [result (commands/parse-args ["show" "--id" "[1 2]"])]
@@ -491,6 +528,16 @@
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code]))))))
(testing "show rejects legacy page-name option"
(let [result (commands/parse-args ["show" "--page-name" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "show rejects format option"
(let [result (commands/parse-args ["show" "--format" "json" "--page" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(deftest test-verb-subcommand-parse-query
(testing "query shows group help"
(let [result (commands/parse-args ["query"])]
@@ -562,7 +609,7 @@
(testing "verb subcommands reject unknown flags"
(doseq [args [["list" "page" "--wat"]
["add" "block" "--wat"]
["remove" "block" "--wat"]
["remove" "--wat"]
["move" "--wat"]
["show" "--wat"]]]
(let [result (commands/parse-args args)]
@@ -570,7 +617,7 @@
(is (= :invalid-options (get-in result [:error :code]))))))
(testing "verb subcommands accept output option"
(let [result (commands/parse-args ["show" "--output" "json" "--page-name" "Home"])]
(let [result (commands/parse-args ["show" "--output" "json" "--page" "Home"])]
(is (true? (:ok? result)))
(is (= "json" (get-in result [:options :output]))))))
@@ -659,12 +706,19 @@
(is (false? (:ok? result)))
(is (= :missing-page-name (get-in result [:error :code])))))
(testing "remove block requires target"
(let [parsed {:ok? true :command :remove-block :options {}}
(testing "remove requires target"
(let [parsed {:ok? true :command :remove :options {}}
result (commands/build-action parsed {:repo "demo"})]
(is (false? (:ok? result)))
(is (= :missing-target (get-in result [:error :code])))))
(testing "remove normalizes id vector in build action"
(let [parsed {:ok? true :command :remove :options {:id "[1 2]"}}
result (commands/build-action parsed {:repo "demo"})]
(is (true? (:ok? result)))
(is (= :remove (get-in result [:action :type])))
(is (= [1 2] (get-in result [:action :ids])))))
(testing "show requires target"
(let [parsed {:ok? true :command :show :options {}}
result (commands/build-action parsed {:repo "demo"})]
@@ -708,12 +762,12 @@
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move rejects sibling pos for page target"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page-name" "Home" "--pos" "sibling"])]
(let [result (commands/parse-args ["move" "--id" "1" "--target-page" "Home" "--pos" "sibling"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code])))))
(testing "move rejects legacy page-name option"
(let [result (commands/parse-args ["move" "--id" "1" "--page-name" "Home"])]
(testing "move rejects legacy target-page-name option"
(let [result (commands/parse-args ["move" "--id" "1" "--target-page-name" "Home"])]
(is (false? (:ok? result)))
(is (= :invalid-options (get-in result [:error :code]))))))