diff --git a/docs/agent-guide/005-logseq-cli-output-and-db-worker-node-log.md b/docs/agent-guide/005-logseq-cli-output-and-db-worker-node-log.md index f2527db35e..b05ebf25b9 100644 --- a/docs/agent-guide/005-logseq-cli-output-and-db-worker-node-log.md +++ b/docs/agent-guide/005-logseq-cli-output-and-db-worker-node-log.md @@ -20,7 +20,7 @@ Target: plain text, no ANSI colors. Each command has a stable layout and orderin | graph switch | `Graph switched: ` | n/a | Use graph name from action/options | | graph remove | `Graph removed: ` | n/a | Use graph name from action/options | | graph validate | `Graph validated: ` | n/a | Use graph name from action/options | -| graph info | Lines: `Graph: `, `Created at: `, `Schema version: ` | n/a | Use `:logseq.kv/*` data; show `-` if missing | +| graph info | Lines: `Graph: `, `Created at: `, `Schema version: ` | n/a | Use `:logseq.kv/*` data; show `-` if missing; `Created at` should use the same human-friendly relative format as list outputs | | server list | Table with header `REPO STATUS HOST PORT PID`, rows for servers, followed by `Count: N` | Header + `Count: 0` | Data from `{:servers [...]}` | | server status/start/stop/restart | `Server : ` + details line `Host: Port: ` when available | n/a | Use `:status` keyword where present | | list page/tag/property | Table with header (fields vary by command) and rows, followed by `Count: N` | Header + `Count: 0` | Defaults: page/tag/property `ID TITLE UPDATED-AT CREATED-AT` (ID uses `:db/id`); if `:db/ident` present, include `IDENT` column | diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index 4f09c628b4..0e17b7bd82 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -1,6 +1,6 @@ # Logseq CLI (Node) -The Logseq CLI is a Node.js program compiled from ClojureScript that connects to a db-worker-node server managed by the CLI. +The Logseq CLI is a Node.js program compiled from ClojureScript that connects to a db-worker-node server managed by the CLI. When installed, the CLI binary name is `logseq`. ## Build the CLI @@ -10,12 +10,18 @@ clojure -M:cljs compile logseq-cli ## db-worker-node lifecycle -`logseq-cli` manages `db-worker-node` automatically. You should not start the server manually. The server binds to localhost on a random port and records that port in the repo lock file. +`logseq` manages `db-worker-node` automatically. You should not start the server manually. The server binds to localhost on a random port and records that port in the repo lock file. ## Run the CLI ```bash node ./static/logseq-cli.js graph list + +If installed globally, run: + +```bash +logseq graph list +``` ``` ## Configuration @@ -81,9 +87,12 @@ Subcommands: show [options] Show tree ``` +Options grouping: +- Help output separates **Global options** (apply to all commands) and **Command options** (command-specific flags). + Output formats: - Global `--output ` (also accepted per subcommand) -- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. Errors include error codes and may include a `Hint:` line. Use `--output json|edn` for structured output. +- Human output is plain text. List/search commands render tables with a final `Count: N` line. For list subcommands, the ID column uses `:db/id` (not UUID). If `:db/ident` exists, an `IDENT` column is included. 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. Examples: diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 9232c87b1c..3bebb76c2e 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -131,13 +131,16 @@ [group table] (let [group-table (filter #(= group (first (:cmds %))) table)] (string/join "\n" - [(str "Usage: logseq-cli " group " [options]") + [(str "Usage: logseq " group " [options]") "" "Subcommands:" (format-commands group-table) "" - "Options:" - (cli/format-opts {:spec global-spec})]))) + "Global options:" + (cli/format-opts {:spec global-spec}) + "" + "Command options:" + (str " See `logseq " group " --help`")]))) (defn- top-level-summary [table] @@ -149,21 +152,28 @@ (let [entries (filter #(contains? commands (first (:cmds %))) table)] (string/join "\n" [title (format-commands entries)])))] (string/join "\n" - ["Usage: logseq-cli [options]" + ["Usage: logseq [options]" "" "Commands:" (string/join "\n\n" (map render-group groups)) "" - "Options:" - (cli/format-opts {:spec global-spec})]))) + "Global options:" + (cli/format-opts {:spec global-spec}) + "" + "Command options:" + " See `logseq --help`"]))) (defn- command-summary [{:keys [cmds spec]}] - (string/join "\n" - [(str "Usage: logseq-cli " (string/join " " cmds) " [options]") - "" - "Options:" - (cli/format-opts {:spec spec})])) + (let [command-spec (apply dissoc spec (keys global-spec))] + (string/join "\n" + [(str "Usage: logseq " (string/join " " cmds) " [options]") + "" + "Global options:" + (cli/format-opts {:spec global-spec}) + "" + "Command options:" + (cli/format-opts {:spec command-spec})]))) (defn- merge-spec [spec] @@ -631,29 +641,43 @@ (build root-id 1))) (defn- fetch-tree - [config {:keys [repo id uuid page-name level]}] - (let [max-depth (or level 10)] + [config {:keys [repo id page-name level] :as opts}] + (let [max-depth (or level 10) + uuid-str (:uuid opts)] (cond (some? id) (p/let [entity (transport/invoke config "thread-api/pull" false - [repo [:db/id :block/uuid :block/title {:block/page [:db/id :block/title]}] id])] + [repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}] id])] (if-let [page-id (get-in entity [:block/page :db/id])] (p/let [blocks (fetch-blocks-for-page config repo page-id) children (build-tree blocks (:db/id entity) max-depth)] {:root (assoc entity :block/children children)}) - (throw (ex-info "block not found" {:code :block-not-found})))) + (if (:db/id entity) + (p/let [blocks (fetch-blocks-for-page config repo (:db/id entity)) + children (build-tree blocks (:db/id entity) max-depth)] + {:root (assoc entity :block/children children)}) + (throw (ex-info "block not found" {:code :block-not-found}))))) - (seq uuid) - (if-not (common-util/uuid-string? uuid) + (seq uuid-str) + (if-not (common-util/uuid-string? uuid-str) (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/title {:block/page [:db/id :block/title]}] - [:block/uuid (uuid uuid)]])] + [repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}] + [:block/uuid (uuid uuid-str)]]) + entity (if (:db/id entity) + entity + (transport/invoke config "thread-api/pull" false + [repo [:db/id :block/name :block/uuid :block/title {:block/page [:db/id :block/title]}] + [:block/uuid uuid-str]]))] (if-let [page-id (get-in entity [:block/page :db/id])] (p/let [blocks (fetch-blocks-for-page config repo page-id) children (build-tree blocks (:db/id entity) max-depth)] {:root (assoc entity :block/children children)}) - (throw (ex-info "block not found" {:code :block-not-found}))))) + (if (:db/id entity) + (p/let [blocks (fetch-blocks-for-page config repo (:db/id entity)) + children (build-tree blocks (:db/id entity) max-depth)] + {:root (assoc entity :block/children children)}) + (throw (ex-info "block not found" {:code :block-not-found})))))) (seq page-name) (p/let [page-entity (transport/invoke config "thread-api/pull" false diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index d6fac1bc2a..3a620f7704 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -176,10 +176,12 @@ (or results [])))) (defn- format-graph-info - [{:keys [graph logseq.kv/graph-created-at logseq.kv/schema-version]}] + [{:keys [graph logseq.kv/graph-created-at logseq.kv/schema-version]} now-ms] (string/join "\n" [(str "Graph: " (or graph "-")) - (str "Created at: " (or graph-created-at "-")) + (str "Created at: " (if (some? graph-created-at) + (human-ago graph-created-at now-ms) + "-")) (str "Schema version: " (or schema-version "-"))])) (defn- format-server-status @@ -233,7 +235,7 @@ :ok (case command :graph-list (format-graph-list (:graphs data)) - :graph-info (format-graph-info data) + :graph-info (format-graph-info data now-ms) (:graph-create :graph-switch :graph-remove :graph-validate) (format-graph-action command context) :server-list (format-server-list (:servers data)) @@ -264,7 +266,7 @@ (= status :error) (assoc :error error)))) (defn format-result - [result {:keys [output-format now-ms] :as opts}] + [result {:keys [output-format] :as opts}] (let [format (cond (= output-format :edn) :edn (= output-format :json) :json diff --git a/src/main/logseq/cli/main.cljs b/src/main/logseq/cli/main.cljs index 8d72d1bca5..59ff135756 100644 --- a/src/main/logseq/cli/main.cljs +++ b/src/main/logseq/cli/main.cljs @@ -10,7 +10,7 @@ (defn- usage [summary] (string/join "\n" - ["logseq-cli [options]" + ["logseq [options]" "" "Commands: list page, list tag, list property, add block, add page, remove block, remove page, search, show, graph list, graph create, graph switch, graph remove, graph validate, graph info, server list, server status, server start, server stop, server restart" "" diff --git a/src/main/logseq/cli/server.cljs b/src/main/logseq/cli/server.cljs index b28a3123f4..f835fcbc60 100644 --- a/src/main/logseq/cli/server.cljs +++ b/src/main/logseq/cli/server.cljs @@ -1,5 +1,5 @@ (ns logseq.cli.server - "db-worker-node lifecycle orchestration for logseq-cli." + "db-worker-node lifecycle orchestration for logseq." (:require ["child_process" :as child-process] ["fs" :as fs] ["http" :as http] diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 225939a66f..599c8c51a7 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -18,9 +18,14 @@ (is (string/includes? summary "search")) (is (string/includes? summary "show")) (is (string/includes? summary "graph")) - (is (string/includes? summary "server"))))) + (is (string/includes? summary "server")))) -(deftest test-parse-args + (testing "top-level help separates global and command options" + (let [summary (:summary (commands/parse-args ["--help"]))] + (is (string/includes? summary "Global options:")) + (is (string/includes? summary "Command options:"))))) + +(deftest test-parse-args-help (testing "graph group shows subcommands" (let [result (commands/parse-args ["graph"]) summary (:summary result)] @@ -34,7 +39,9 @@ (is (true? (:help? result))) (is (string/includes? summary "list page")) (is (string/includes? summary "list tag")) - (is (string/includes? summary "list property")))) + (is (string/includes? summary "list property")) + (is (string/includes? summary "Global options:")) + (is (string/includes? summary "Command options:")))) (testing "add group shows subcommands" (let [result (commands/parse-args ["add"]) @@ -55,8 +62,9 @@ summary (:summary result)] (is (true? (:help? result))) (is (string/includes? summary "server list")) - (is (string/includes? summary "server start")))) + (is (string/includes? summary "server start"))))) +(deftest test-parse-args-help-alignment (testing "graph group aligns subcommand columns" (let [result (commands/parse-args ["graph"]) summary (:summary result) @@ -85,8 +93,9 @@ (when-let [[_ desc] (re-matches #"^\s+.*?\s{2,}(.+)$" line)] (.indexOf line desc)))))] (is (seq subcommand-lines)) - (is (apply = desc-starts)))) + (is (apply = desc-starts))))) +(deftest test-parse-args-errors (testing "rejects legacy commands" (doseq [command ["graph-list" "graph-create" "graph-switch" "graph-remove" "graph-validate" "graph-info" "block" "tree" @@ -113,8 +122,9 @@ (testing "errors on unknown command" (let [result (commands/parse-args ["wat"])] (is (false? (:ok? result))) - (is (= :unknown-command (get-in result [:error :code]))))) + (is (= :unknown-command (get-in result [:error :code])))))) +(deftest test-parse-args-global-options (testing "global output option is accepted" (let [result (commands/parse-args ["--output" "json" "graph" "list"])] (is (true? (:ok? result))) diff --git a/src/test/logseq/cli/format_test.cljs b/src/test/logseq/cli/format_test.cljs index 069d84f5bb..8b79d2e628 100644 --- a/src/test/logseq/cli/format_test.cljs +++ b/src/test/logseq/cli/format_test.cljs @@ -110,11 +110,12 @@ (let [result (format/format-result {:status :ok :command :graph-info :data {:graph "demo-graph" - :logseq.kv/graph-created-at 123 + :logseq.kv/graph-created-at 40000 :logseq.kv/schema-version 2}} - {:output-format nil})] + {:output-format nil + :now-ms 100000})] (is (= (str "Graph: demo-graph\n" - "Created at: 123\n" + "Created at: 1m ago\n" "Schema version: 2") result))))) diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index 4f51bf2ae2..0df87c97dd 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -173,3 +173,40 @@ (p/catch (fn [e] (is false (str "unexpected error: " e)) (done))))))) + +(deftest test-cli-show-page-block-by-id-and-uuid + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker")] + (-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") + _ (fs/writeFileSync cfg-path "{:output-format :json}") + _ (run-cli ["graph" "create" "--repo" "show-page-block-graph"] data-dir cfg-path) + _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path) + list-page-result (run-cli ["list" "page" "--expand"] data-dir cfg-path) + list-page-payload (parse-json-output list-page-result) + page-item (some (fn [item] + (when (= "TestPage" (or (:block/title item) (:title item))) + item)) + (get-in list-page-payload [:data :items])) + page-id (or (:db/id page-item) (:id page-item)) + page-uuid (or (:block/uuid page-item) (:uuid page-item)) + show-by-id-result (run-cli ["show" "--id" (str page-id) "--format" "json"] data-dir cfg-path) + show-by-id-payload (parse-json-output show-by-id-result) + show-by-uuid-result (run-cli ["show" "--uuid" (str page-uuid) "--format" "json"] data-dir cfg-path) + show-by-uuid-payload (parse-json-output show-by-uuid-result) + stop-result (run-cli ["server" "stop" "--repo" "show-page-block-graph"] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (= "ok" (:status list-page-payload))) + (is (some? page-item)) + (is (some? page-id)) + (is (some? page-uuid)) + (is (= "ok" (:status show-by-id-payload))) + (is (= (str page-uuid) (str (or (get-in show-by-id-payload [:data :root :uuid]) + (get-in show-by-id-payload [:data :root :block/uuid]))))) + (is (= "ok" (:status show-by-uuid-payload))) + (is (= (str page-uuid) (str (or (get-in show-by-uuid-payload [:data :root :uuid]) + (get-in show-by-uuid-payload [:data :root :block/uuid]))))) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done)))))))