enhance(cli): improve error messages for unspecified graphs

Introduced a breaking change for query and search commands where
graph(s) are specified as options instead of arguments. This
makes local graph usage consistent across all commands.
Also fixed in-app search not working and local query with multiple
graphs not querying from the right db
This commit is contained in:
Gabriel Horner
2025-11-17 17:40:23 -05:00
parent 62deed7e7f
commit 7b07116b41
7 changed files with 60 additions and 50 deletions

16
deps/cli/README.md vendored
View File

@@ -1,6 +1,6 @@
## Description
This library provides a `logseq` CLI for DB graphs. The CLI currently only applies to desktop DB graphs and requires the [database-version](/README.md#-database-version) desktop app to be installed. The CLI works offline by default which means it can also be used on CI/CD platforms like Github Actions. Some CLI commands can also interact with the current DB graph if the [HTTP Server](https://docs.logseq.com/#/page/local%20http%20server) is turned on in the Desktop app.
This library provides a `logseq` CLI for DB graphs. The CLI currently only applies to desktop DB graphs and requires the [database-version](/README.md#-database-version) desktop app to be installed. The CLI works offline by default which means it can also be used on CI/CD platforms like Github Actions. Most CLI commands can also interact with the current DB graph if the [HTTP Server](https://docs.logseq.com/#/page/local%20http%20server) is turned on in the Desktop app.
## Install
@@ -12,9 +12,9 @@ This section assumes you have installed the CLI from npm or via the [dev
setup](#setup). If you haven't, substitute `node cli.mjs` for `logseq` e.g.
`node.cli.mjs -h`.
All commands except for `append` can be used offline or on CI. The `search` command and any command that has an api-server-token option require the [HTTP API Server](https://docs.logseq.com/#/page/local%20http%20server) to be turned on.
All commands work with both local graphs and the current in-app graph except for `append` (in-app graph only) and `export` (local graph only). For a command to work with an in-app graph, the [HTTP API Server](https://docs.logseq.com/#/page/local%20http%20server) must be turned on.
Now let's use it!
Now let's use the CLI!
```
$ logseq -h
@@ -30,9 +30,9 @@ search [options] Search DB graph
query [options] Query DB graph(s)
export [options] Export DB graph as Markdown
export-edn [options] Export DB graph as EDN
import-edn [options] Import into DB graph with EDN
append [options] Appends text to current page
mcp-server [options] Run a MCP server
import-edn [options] Import into DB graph with EDN
help Print a command's help
$ logseq list
@@ -68,7 +68,7 @@ $ LOGSEQ_API_SERVER_TOKEN=my-token logseq search woot
...
# Search a local graph
$ logseq search woot page
$ logseq search page -g woot
Search found 23 results:
Node page
Annotation page
@@ -76,7 +76,7 @@ Annotation page
# Query a graph locally using `d/entity` id(s) like an integer or a :db/ident
# Can also specify a uuid string to fetch an entity
$ logseq query woot 10 :logseq.class/Tag
$ logseq query 10 :logseq.class/Tag -g woot
({:db/id 10,
:db/ident :logseq.kv/graph-git-sha,
:kv/value "f736895b1b-dirty"}
@@ -92,7 +92,7 @@ $ logseq query woot 10 :logseq.class/Tag
:block/name "tag"})
# Query a graph using a datalog query
$ logseq query woot '[:find (pull ?b [*]) :where [?b :kv/value]]'
$ logseq query '[:find (pull ?b [*]) :where [?b :kv/value]]' -g woot
[{:db/id 5, :db/ident :logseq.kv/db-type, :kv/value "db"}
{:db/id 6,
:db/ident :logseq.kv/schema-version,
@@ -117,7 +117,7 @@ $ logseq query '(task DOING)' -a my-token
...
# Export local graph as markdown
$ logseq export yep
$ logseq export -g yep
Exported 41 pages to yep_markdown_1756128259.zip
# Export current graph as EDN

View File

@@ -78,23 +78,27 @@
:fn (lazy-load-fn 'logseq.cli.commands.search/search)
:desc "Search DB graph"
:description "Search a local graph or the current in-app graph if --api-server-token is given. For a local graph it only searches the :block/title of blocks."
:args->opts [:graph :search-terms] :coerce {:search-terms []} :require [:graph]
:args->opts [:search-terms] :coerce {:search-terms []}
:spec cli-spec/search}
{:cmds ["query"] :desc "Query DB graph(s)"
:description "Query a local graph or the current in-app graph if --api-server-token is given. For a local graph, queries are a datalog query or an entity query. A datalog query can use built-in rules. An entity query consists of one or more integers, uuids or :db/ident keywords. For an in-app query, queries can be an advanced or simple query."
:fn (lazy-load-fn 'logseq.cli.commands.query/query)
:args->opts [:graph :args] :coerce {:args []} :no-keyword-opts true :require [:graph]
:args->opts [:args] :coerce {:args []} :no-keyword-opts true
:spec cli-spec/query}
{:cmds ["export"] :desc "Export DB graph as Markdown"
:description "Export a graph to Markdown like the in-app graph export."
:description "Export a local graph to Markdown like the in-app graph export."
:fn (lazy-load-fn 'logseq.cli.commands.export/export)
:args->opts [:graph] :require [:graph]
:spec cli-spec/export}
{:cmds ["export-edn"] :desc "Export DB graph as EDN"
:description "Export a local graph to EDN or the current in-app graph if --api-server-token is given. See https://github.com/logseq/docs/blob/master/db-version.md#edn-data-export for more about this export type."
:fn (lazy-load-fn 'logseq.cli.commands.export-edn/export)
:spec cli-spec/export-edn}
{:cmds ["import-edn"] :desc "Import into DB graph with EDN"
:description "Import with EDN into a local graph or the current in-app graph if --api-server-token is given. See https://github.com/logseq/docs/blob/master/db-version.md#edn-data-export for more about this import type."
:fn (lazy-load-fn 'logseq.cli.commands.import-edn/import-edn)
:spec cli-spec/import-edn}
{:cmds ["append"] :desc "Appends text to current page"
:description "Append text to current page of current in-app graph."
:fn (lazy-load-fn 'logseq.cli.commands.append/append)
:args->opts [:args] :require [:args] :coerce {:args []}
:spec cli-spec/append}
@@ -102,10 +106,6 @@
:description "Run a MCP server against a local graph if --graph is given or against the current in-app graph. By default the MCP server runs as a HTTP Streamable server. Use --stdio to run it as a stdio server."
:fn (lazy-load-fn 'logseq.cli.commands.mcp-server/start)
:spec cli-spec/mcp-server}
{:cmds ["import-edn"] :desc "Import into DB graph with EDN"
:description "Import with EDN into a local graph or the current in-app graph if --api-server-token is given. See https://github.com/logseq/docs/blob/master/db-version.md#edn-data-export for more about this import type."
:fn (lazy-load-fn 'logseq.cli.commands.import-edn/import-edn)
:spec cli-spec/import-edn}
{:cmds ["help"] :fn help-command :desc "Print a command's help"
:args->opts [:command] :require [:command]}
{:cmds []

View File

@@ -74,6 +74,8 @@
(println "Exported" (count exported-files) "pages to" file-name)))))
(defn export [{{:keys [graph] :as opts} :opts}]
(when-not graph
(cli-util/error "Command missing required option 'graph'"))
(if (fs/existsSync (cli-util/get-graph-path graph))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))]
(export-repo-as-markdown! (str common-config/db-version-prefix graph) @conn opts))

View File

@@ -20,6 +20,8 @@
(p/catch cli-util/command-catch-handler)))
(defn- local-import [{:keys [graph]} import-map]
(when-not graph
(cli-util/error "Command missing required option 'graph'"))
(if (fs/existsSync (cli-util/get-graph-path graph))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
{:keys [init-tx block-props-tx misc-tx]}

View File

@@ -71,36 +71,36 @@
(if (= 1 (count (first res))) (mapv first res) res)))
(defn- local-query
[{{:keys [graph args graphs properties-readable title-query]} :opts}]
(let [graphs' (into [graph] graphs)]
(doseq [graph' graphs']
(if (fs/existsSync (cli-util/get-graph-path graph'))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
query* (when (string? (first args)) (common-util/safe-read-string {:log-error? false} (first args)))
results (cond
;; Run datalog query if detected
(and (vector? query*) (= :find (first query*)))
(local-datalog-query @conn query*)
;; Runs predefined title query. Predefined queries could better off in a separate command
;; since they could be more powerful and have different args than query command
title-query
(let [query '[:find (pull ?b [*])
:in $ % ?search-term
:where (block-content ?b ?search-term)]
res (d/q query @conn (rules/extract-rules rules/db-query-dsl-rules)
(string/join " " args))]
;; Remove nesting for most queries which just have one :find binding
(if (= 1 (count (first res))) (mapv first res) res))
:else
(local-entities-query @conn properties-readable args))]
(when (> (count graphs') 1)
(println "Results for graph" (pr-str graph')))
(pprint/pprint results))
(cli-util/error "Graph" (pr-str graph') "does not exist")))))
[{{:keys [args graphs properties-readable title-query]} :opts}]
(when-not graphs
(cli-util/error "Command missing required option 'graphs'"))
(doseq [graph graphs]
(if (fs/existsSync (cli-util/get-graph-path graph))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
query* (when (string? (first args)) (common-util/safe-read-string {:log-error? false} (first args)))
results (cond
;; Run datalog query if detected
(and (vector? query*) (= :find (first query*)))
(local-datalog-query @conn query*)
;; Runs predefined title query. Predefined queries could better off in a separate command
;; since they could be more powerful and have different args than query command
title-query
(let [query '[:find (pull ?b [*])
:in $ % ?search-term
:where (block-content ?b ?search-term)]
res (d/q query @conn (rules/extract-rules rules/db-query-dsl-rules)
(string/join " " args))]
;; Remove nesting for most queries which just have one :find binding
(if (= 1 (count (first res))) (mapv first res) res))
:else
(local-entities-query @conn properties-readable args))]
(when (> (count graphs) 1)
(println "Results for graph" (pr-str graph)))
(pprint/pprint results))
(cli-util/error "Graph" (pr-str graph) "does not exist"))))
(defn query
[{{:keys [graph args api-server-token]} :opts :as m}]
[{{:keys [args api-server-token]} :opts :as m}]
(if api-server-token
;; graph can be query since it's not used for api-query
(api-query (or graph (first args)) api-server-token)
(api-query (first args) api-server-token)
(local-query m)))

View File

@@ -46,11 +46,13 @@
(if (= 200 (.-status resp))
(p/let [body (.json resp)]
(let [{:keys [blocks]} (js->clj body :keywordize-keys true)]
(format-results (map :block/title blocks) search-term {:raw raw :api? true})))
(format-results (map :title blocks) search-term {:raw raw :api? true})))
(cli-util/api-handle-error-response resp)))
(p/catch cli-util/command-catch-handler)))
(defn- local-search [search-term {{:keys [graph raw limit]} :opts}]
(when-not graph
(cli-util/error "Command missing required option 'graph'"))
(if (fs/existsSync (cli-util/get-graph-path graph))
(let [conn (apply sqlite-cli/open-db! (cli-util/->open-db-args graph))
nodes (->> (d/datoms @conn :aevt :block/title)
@@ -61,7 +63,7 @@
(format-results nodes search-term {:raw raw}))
(cli-util/error "Graph" (pr-str graph) "does not exist")))
(defn search [{{:keys [graph search-terms api-server-token]} :opts :as m}]
(defn search [{{:keys [search-terms api-server-token]} :opts :as m}]
(if api-server-token
(api-search (string/join " " (into [graph] search-terms)) m)
(api-search (string/join " " search-terms) m)
(local-search (string/join " " search-terms) m)))

View File

@@ -3,7 +3,9 @@
commands but are separate because command namespaces are lazy loaded")
(def export
{:file {:alias :f
{:graph {:alias :g
:desc "Local graph to export"}
:file {:alias :f
:desc "File to save export"}})
(def export-edn
@@ -43,7 +45,7 @@
:desc "API server token to query current graph"}
:graphs {:alias :g
:coerce []
:desc "Additional graphs to local query"}
:desc "Local graph(s) to query"}
:properties-readable {:alias :p
:coerce :boolean
:desc "Make properties on local, entity queries show property values instead of ids"}
@@ -53,6 +55,8 @@
(def search
{:api-server-token {:alias :a
:desc "API server token to search current graph"}
:graph {:alias :g
:desc "Local graph to search"}
:raw {:alias :r
:desc "Print raw response"}
:limit {:alias :l