From 6ba7c868b46faf7fcf6b4aae8a95ea9d5efa517d Mon Sep 17 00:00:00 2001 From: rcmerci Date: Wed, 14 Jan 2026 23:12:48 +0800 Subject: [PATCH] fix lint, remove deprecated cmds --- .carve/ignore | 2 + docs/agent-guide/001-logseq-cli.md | 30 +- docs/cli/logseq-cli.md | 2 +- src/main/logseq/cli/commands.cljs | 372 +++++++++------------- src/main/logseq/cli/config.cljs | 7 +- src/main/logseq/cli/format.cljs | 4 +- src/main/logseq/cli/main.cljs | 7 +- src/main/logseq/cli/transport.cljs | 55 ++-- src/test/logseq/cli/commands_test.cljs | 42 +-- src/test/logseq/cli/config_test.cljs | 10 +- src/test/logseq/cli/integration_test.cljs | 24 +- src/test/logseq/cli/transport_test.cljs | 2 +- 12 files changed, 211 insertions(+), 346 deletions(-) diff --git a/.carve/ignore b/.carve/ignore index b239c5fcd6..0bf7dec926 100644 --- a/.carve/ignore +++ b/.carve/ignore @@ -62,6 +62,8 @@ frontend.worker.rtc.op-mem-layer/_sync-loop-canceler frontend.worker.db-worker/init ;; Used by shadow.cljs (node entrypoint) frontend.worker.db-worker-node/main +;; CLI entrypoint (shadow-cljs :node-script) +logseq.cli.main/main ;; Future use? frontend.worker.rtc.hash/hash-blocks ;; Repl fn diff --git a/docs/agent-guide/001-logseq-cli.md b/docs/agent-guide/001-logseq-cli.md index 69ed9847ea..bb4e10f165 100644 --- a/docs/agent-guide/001-logseq-cli.md +++ b/docs/agent-guide/001-logseq-cli.md @@ -17,7 +17,7 @@ The CLI should provide a stable interface for scripting and troubleshooting, and ## Testing Plan -I will add an integration test that starts db-worker-node on a test port and verifies the CLI can connect and run a simple request like ping or status. +I will add an integration test that starts db-worker-node on a test port and verifies the CLI can connect and run a simple graph/content request. I will add unit tests for command parsing, configuration precedence, and error formatting. I will add unit tests for the client transport layer to ensure timeouts and retries behave correctly. I will add unit tests for new graph/content commands (parsing, validation, and request mapping). @@ -45,7 +45,7 @@ The CLI will use JSON for request and response bodies for ease of scripting. ## Implementation plan -1. Use TodoWrite to track the full task list and include the @test-driven-development red-green-refactor steps. +1. Use tool(update_plan) to track the full task list and include the @test-driven-development red-green-refactor steps. 2. Read @test-driven-development guidelines and confirm the red phase will include all CLI tests first. 3. Identify existing db-worker-node request handlers and document their request and response shapes. 4. Define the initial CLI command surface as a table that includes command, input, output, and errors. @@ -63,14 +63,28 @@ The CLI will use JSON for request and response bodies for ease of scripting. 16. Refactor for naming and reuse while keeping tests green. 17. Document how to build and run the CLI in a short section in README.md. +## Current status (2026-01-14) + +Implemented: +- CLI build target, entrypoint, config resolution, transport, formatting, and command wiring. +- Graph commands: list/create/switch/remove/validate/info. +- Content commands: add/remove/search/tree. +- Unit tests for config/commands/format/transport and integration tests for graph/content commands. +- CLI docs moved to `docs/cli/logseq-cli.md` and linked from README. + +Not fully aligned with plan: +- Red-first TDD sequence was not strictly followed (some tests added after initial implementation). +- README section was replaced by a link to the dedicated doc. +- `search` currently queries `:block/title` only (no page name/content search). + +Open follow-ups (optional): +- Expand `search` to include page name/content and update tests. +- Add any additional graph metadata to `graph-info` beyond `:logseq.kv/graph-created-at` and `:logseq.kv/schema-version`. + ## Command surface definition | Command | Input | Output | Errors | | --- | --- | --- | --- | -| ping | none | ok message | server unavailable, timeout | -| status | none | server version, db state | server unavailable, timeout | -| query | query string or file | query result JSON | invalid query, parse error | -| export | target path and format | export result | unsupported format, write error | | graph-list | none | list of graphs | server unavailable, timeout | | graph-create | graph name | created graph + set current graph | invalid name, server unavailable | | graph-switch | graph name | switched graph + set current graph | missing graph, server unavailable | @@ -79,7 +93,7 @@ The CLI will use JSON for request and response bodies for ease of scripting. | graph-info | graph name or current graph | graph metadata/info | missing graph, server unavailable | | add | block/page payload | created block IDs | invalid input, server unavailable | | remove | block/page id or name | removal confirmation | invalid input, server unavailable | -| search | query string | matched blocks/pages | invalid input, server unavailable | +| search | text query | matched blocks/pages | invalid input, server unavailable | | tree | block/page id or name | hierarchical tree output | invalid input, server unavailable | ## Edge cases @@ -141,7 +155,7 @@ I will keep unit tests focused on pure functions like parsing, formatting, and c ## Question -Which exact db-worker-node endpoints and request schemas should the CLI use for ping, status, query, and export. +Which exact db-worker-node endpoints and request schemas should the CLI use for graph/content commands. - Answer: all thread-apis are available in http endpoint, check @src/main/frontend/worker/db_worker_node.cljs Do we want WebSocket or HTTP as the default transport for the CLI. diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index 240f9644fb..7e9cc8cb68 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -18,7 +18,7 @@ node ./static/db-worker-node.js ## Run the CLI ```bash -node ./static/logseq-cli.js ping --base-url http://127.0.0.1:9101 +node ./static/logseq-cli.js graph-list --base-url http://127.0.0.1:9101 ``` ## Configuration diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index ac7e545a78..bdf8a568d3 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -1,4 +1,5 @@ (ns logseq.cli.commands + "Command parsing and action building for the Logseq CLI." (:require ["fs" :as fs] [cljs-time.coerce :as tc] [cljs.reader :as reader] @@ -12,11 +13,7 @@ [promesa.core :as p])) (def ^:private command->keyword - {"ping" :ping - "status" :status - "query" :query - "export" :export - "graph-list" :graph-list + {"graph-list" :graph-list "graph-create" :graph-create "graph-switch" :graph-switch "graph-remove" :graph-remove @@ -45,7 +42,7 @@ [nil "--json" "Output JSON" :id :json? :default false] - [nil "--format FORMAT" "Output format (tree/export)"] + [nil "--format FORMAT" "Output format (tree)"] [nil "--limit N" "Limit results" :parse-fn #(js/parseInt % 10)] [nil "--page PAGE" "Page name"] @@ -54,10 +51,7 @@ [nil "--content TEXT" "Block content for add"] [nil "--blocks EDN" "EDN vector of blocks for add"] [nil "--blocks-file PATH" "EDN file of blocks for add"] - [nil "--text TEXT" "Search text"] - [nil "--query QUERY" "EDN query input"] - [nil "--file PATH" "Path to EDN query file"] - [nil "--out PATH" "Output path"]]) + [nil "--text TEXT" "Search text"]]) (defn parse-args [args] @@ -115,21 +109,6 @@ (first command-args) (:repo config))) -(defn- read-query - [{:keys [query file]}] - (cond - (seq query) - {:ok? true :value (reader/read-string query)} - - (seq file) - (let [contents (.toString (fs/readFileSync file) "utf8")] - {:ok? true :value (reader/read-string contents)}) - - :else - {:ok? false - :error {:code :missing-query - :message "query is required"}})) - (defn- read-blocks [options command-args] (cond @@ -151,14 +130,6 @@ :error {:code :missing-content :message "content is required"}})) -(defn- ensure-vector - [value] - (if (vector? value) - {:ok? true :value value} - {:ok? false - :error {:code :invalid-query - :message "query must be a vector"}})) - (defn- ensure-blocks [value] (if (vector? value) @@ -289,6 +260,139 @@ (when (seq graph) (graph->repo graph)))) +(defn- missing-graph-error + [] + {:ok? false + :error {:code :missing-graph + :message "graph name is required"}}) + +(defn- missing-repo-error + [message] + {:ok? false + :error {:code :missing-repo + :message message}}) + +(defn- build-graph-action + [command graph repo] + (case command + :graph-list + {:ok? true + :action {:type :invoke + :method "thread-api/list-db" + :direct-pass? false + :args []}} + + :graph-create + (if-not (seq graph) + (missing-graph-error) + {:ok? true + :action {:type :invoke + :method "thread-api/create-or-open-db" + :direct-pass? false + :args [repo {}] + :persist-repo (repo->graph repo)}}) + + :graph-switch + (if-not (seq graph) + (missing-graph-error) + {:ok? true + :action {:type :graph-switch + :repo repo + :graph (repo->graph repo)}}) + + :graph-remove + (if-not (seq graph) + (missing-graph-error) + {:ok? true + :action {:type :invoke + :method "thread-api/unsafe-unlink-db" + :direct-pass? false + :args [repo]}}) + + :graph-validate + (if-not (seq repo) + (missing-graph-error) + {:ok? true + :action {:type :invoke + :method "thread-api/validate-db" + :direct-pass? false + :args [repo]}}) + + :graph-info + (if-not (seq repo) + (missing-graph-error) + {:ok? true + :action {:type :graph-info + :repo repo + :graph (repo->graph repo)}}))) + +(defn- build-add-action + [options args repo] + (if-not (seq repo) + (missing-repo-error "repo is required for add") + (let [blocks-result (read-blocks options args)] + (if-not (:ok? blocks-result) + blocks-result + (let [vector-result (ensure-blocks (:value blocks-result))] + (if-not (:ok? vector-result) + vector-result + {:ok? true + :action {:type :add + :repo repo + :graph (repo->graph repo) + :page (:page options) + :parent (:parent options) + :blocks (:value vector-result)}})))))) + +(defn- build-remove-action + [options repo] + (if-not (seq repo) + (missing-repo-error "repo is required for remove") + (let [block (:block options) + page (:page options)] + (if (or (seq block) (seq page)) + {:ok? true + :action {:type :remove + :repo repo + :block block + :page page}} + {:ok? false + :error {:code :missing-target + :message "block or page is required"}})))) + +(defn- build-search-action + [options args repo] + (if-not (seq repo) + (missing-repo-error "repo is required for search") + (let [text (or (:text options) (string/join " " args))] + (if (seq text) + {:ok? true + :action {:type :search + :repo repo + :text text + :limit (:limit options)}} + {:ok? false + :error {:code :missing-search-text + :message "search text is required"}})))) + +(defn- build-tree-action + [options repo] + (if-not (seq repo) + (missing-repo-error "repo is required for tree") + (let [block (:block options) + page (:page options) + target (or block page)] + (if (seq target) + {:ok? true + :action {:type :tree + :repo repo + :block block + :page page + :format (some-> (:format options) string/lower-case)}} + {:ok? false + :error {:code :missing-target + :message "block or page is required"}})))) + (defn build-action [parsed config] (if-not (:ok? parsed) @@ -297,198 +401,20 @@ graph (pick-graph options args config) repo (resolve-repo graph)] (case command - :ping - {:ok? true :action {:type :ping}} - - :status - {:ok? true :action {:type :status}} - - :query - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for query"}} - (let [query-result (read-query options)] - (if-not (:ok? query-result) - query-result - (let [vector-result (ensure-vector (:value query-result))] - (if-not (:ok? vector-result) - vector-result - {:ok? true - :action {:type :invoke - :method "thread-api/q" - :direct-pass? false - :args [repo (:value vector-result)]}}))))) - - :export - (let [format (some-> (:format options) string/lower-case) - out (:out options) - repo repo] - (cond - (not (seq repo)) - {:ok? false - :error {:code :missing-repo - :message "repo is required for export"}} - - (not (seq out)) - {:ok? false - :error {:code :missing-output - :message "output path is required"}} - - (= format "edn") - {:ok? true - :action {:type :invoke - :method "thread-api/export-edn" - :direct-pass? false - :args [repo {}] - :write {:format :edn - :path out}}} - - (= format "db") - {:ok? true - :action {:type :invoke - :method "thread-api/export-db" - :direct-pass? true - :args [repo] - :write {:format :db - :path out}}} - - :else - {:ok? false - :error {:code :unsupported-format - :message (str "unsupported format: " format)}})) - - :graph-list - {:ok? true - :action {:type :invoke - :method "thread-api/list-db" - :direct-pass? false - :args []}} - - :graph-create - (if-not (seq graph) - {:ok? false - :error {:code :missing-graph - :message "graph name is required"}} - {:ok? true - :action {:type :invoke - :method "thread-api/create-or-open-db" - :direct-pass? false - :args [repo {}] - :persist-repo (repo->graph repo)}}) - - :graph-switch - (if-not (seq graph) - {:ok? false - :error {:code :missing-graph - :message "graph name is required"}} - {:ok? true - :action {:type :graph-switch - :repo repo - :graph (repo->graph repo)}}) - - :graph-remove - (if-not (seq graph) - {:ok? false - :error {:code :missing-graph - :message "graph name is required"}} - {:ok? true - :action {:type :invoke - :method "thread-api/unsafe-unlink-db" - :direct-pass? false - :args [repo]}}) - - :graph-validate - (if-not (seq repo) - {:ok? false - :error {:code :missing-graph - :message "graph name is required"}} - {:ok? true - :action {:type :invoke - :method "thread-api/validate-db" - :direct-pass? false - :args [repo]}}) - - :graph-info - (if-not (seq repo) - {:ok? false - :error {:code :missing-graph - :message "graph name is required"}} - {:ok? true - :action {:type :graph-info - :repo repo - :graph (repo->graph repo)}}) + (:graph-list :graph-create :graph-switch :graph-remove :graph-validate :graph-info) + (build-graph-action command graph repo) :add - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for add"}} - (let [blocks-result (read-blocks options args)] - (if-not (:ok? blocks-result) - blocks-result - (let [vector-result (ensure-blocks (:value blocks-result))] - (if-not (:ok? vector-result) - vector-result - {:ok? true - :action {:type :add - :repo repo - :graph (repo->graph repo) - :page (:page options) - :parent (:parent options) - :blocks (:value vector-result)}}))))) + (build-add-action options args repo) :remove - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for remove"}} - (let [block (:block options) - page (:page options)] - (if (or (seq block) (seq page)) - {:ok? true - :action {:type :remove - :repo repo - :block block - :page page}} - {:ok? false - :error {:code :missing-target - :message "block or page is required"}}))) + (build-remove-action options repo) :search - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for search"}} - (let [text (or (:text options) (string/join " " args))] - (if (seq text) - {:ok? true - :action {:type :search - :repo repo - :text text - :limit (:limit options)}} - {:ok? false - :error {:code :missing-search-text - :message "search text is required"}}))) + (build-search-action options args repo) :tree - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for tree"}} - (let [block (:block options) - page (:page options) - target (or block page)] - (if (seq target) - {:ok? true - :action {:type :tree - :repo repo - :block block - :page page - :format (some-> (:format options) string/lower-case)}} - {:ok? false - :error {:code :missing-target - :message "block or page is required"}}))) + (build-tree-action options repo) {:ok? false :error {:code :unknown-command @@ -497,18 +423,6 @@ (defn execute [action config] (case (:type action) - :ping - (-> (transport/ping config) - (p/then (fn [_] - {:status :ok :data {:message "ok"}}))) - - :status - (-> (p/let [ready? (transport/ready config) - dbs (transport/list-db config)] - {:status :ok - :data {:ready ready? - :dbs dbs}})) - :invoke (-> (p/let [result (transport/invoke config (:method action) diff --git a/src/main/logseq/cli/config.cljs b/src/main/logseq/cli/config.cljs index ff78753605..7aaf2239fa 100644 --- a/src/main/logseq/cli/config.cljs +++ b/src/main/logseq/cli/config.cljs @@ -1,10 +1,11 @@ (ns logseq.cli.config + "CLI configuration resolution and persistence." (:require [cljs.reader :as reader] [clojure.string :as string] [goog.object :as gobj] ["fs" :as fs] ["os" :as os] - ["path" :as path])) + ["path" :as node-path])) (defn- parse-int [value] @@ -13,7 +14,7 @@ (defn- default-config-path [] - (path/join (.homedir os) ".logseq" "cli.edn")) + (node-path/join (.homedir os) ".logseq" "cli.edn")) (defn- read-config-file [config-path] @@ -24,7 +25,7 @@ (defn- ensure-config-dir! [config-path] (when (seq config-path) - (let [dir (path/dirname config-path)] + (let [dir (node-path/dirname config-path)] (when (and (seq dir) (not (fs/existsSync dir))) (.mkdirSync fs dir #js {:recursive true}))))) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 3ed42cb8a8..e9631f734d 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -1,6 +1,6 @@ (ns logseq.cli.format - (:require [clojure.string :as string] - [clojure.walk :as walk])) + "Formatting helpers for CLI output." + (:require [clojure.walk :as walk])) (defn- normalize-json [value] diff --git a/src/main/logseq/cli/main.cljs b/src/main/logseq/cli/main.cljs index 11a32a2a4a..af5d4869fe 100644 --- a/src/main/logseq/cli/main.cljs +++ b/src/main/logseq/cli/main.cljs @@ -1,4 +1,5 @@ (ns logseq.cli.main + "CLI entrypoint for invoking db-worker-node." (:refer-clojure :exclude [run!]) (:require [clojure.string :as string] [logseq.cli.commands :as commands] @@ -11,14 +12,14 @@ (string/join "\n" ["logseq-cli [options]" "" - "Commands: ping, status, query, export, graph-list, graph-create, graph-switch, graph-remove, graph-validate, graph-info, add, remove, search, tree" + "Commands: graph-list, graph-create, graph-switch, graph-remove, graph-validate, graph-info, add, remove, search, tree" "" "Options:" summary])) (defn run! - ([args] (run! args {:exit? true})) - ([args {:keys [exit?] :or {exit? true}}] + ([args] (run! args {})) + ([args _opts] (let [parsed (commands/parse-args args)] (cond (:help? parsed) diff --git a/src/main/logseq/cli/transport.cljs b/src/main/logseq/cli/transport.cljs index 286034edbf..eb342bac65 100644 --- a/src/main/logseq/cli/transport.cljs +++ b/src/main/logseq/cli/transport.cljs @@ -1,4 +1,5 @@ (ns logseq.cli.transport + "HTTP transport for communicating with db-worker-node." (:require [clojure.string :as string] [logseq.db :as ldb] [promesa.core :as p] @@ -66,39 +67,23 @@ (defn request [{:keys [method url headers body timeout-ms retries] :or {retries 0}}] - (p/loop [attempt 0] - (-> (p/let [response ( (request {:method "GET" - :url (str (string/replace base-url #"/$" "") "/readyz") - :timeout-ms timeout-ms - :retries retries - :headers {}}) - (p/then (fn [_] true)))) + (letfn [(attempt-request [attempt] + (-> (p/let [response (clj (js/JSON.parse (:output result)) :keywordize-keys true)) -(deftest test-cli-ping - (async done - (let [data-dir (node-helper/create-tmp-dir "db-worker")] - (-> (p/let [daemon (db-worker-node/start-daemon! {:host "127.0.0.1" - :port 0 - :data-dir data-dir}) - url (str "http://127.0.0.1:" (:port daemon)) - result (cli-main/run! ["ping" "--base-url" url "--json"] {:exit? false})] - (is (= 0 (:exit-code result))) - (is (= "{\"status\":\"ok\",\"data\":{\"message\":\"ok\"}}" (:output result))) - (p/let [_ ((:stop! daemon))] - (done))) - (p/catch (fn [e] - (is false (str "unexpected error: " e)) - (done))))))) - (deftest test-cli-graph-list (async done (let [data-dir (node-helper/create-tmp-dir "db-worker")] @@ -39,7 +23,7 @@ :port 0 :data-dir data-dir}) url (str "http://127.0.0.1:" (:port daemon)) - cfg-path (path/join (node-helper/create-tmp-dir "cli") "cli.edn") + cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") result (run-cli ["graph-list"] url cfg-path) payload (parse-json-output result)] (is (= 0 (:exit-code result))) @@ -58,7 +42,7 @@ :port 0 :data-dir data-dir}) url (str "http://127.0.0.1:" (:port daemon)) - cfg-path (path/join (node-helper/create-tmp-dir "cli") "cli.edn") + cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{}") create-result (run-cli ["graph-create" "--graph" "demo-graph"] url cfg-path) create-payload (parse-json-output create-result) @@ -82,7 +66,7 @@ :port 0 :data-dir data-dir}) url (str "http://127.0.0.1:" (:port daemon)) - cfg-path (path/join (node-helper/create-tmp-dir "cli") "cli.edn") + cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn") _ (fs/writeFileSync cfg-path "{}") _ (run-cli ["graph-create" "--graph" "content-graph"] url cfg-path) add-result (run-cli ["add" "--page" "TestPage" "--content" "hello world"] url cfg-path) diff --git a/src/test/logseq/cli/transport_test.cljs b/src/test/logseq/cli/transport_test.cljs index fa95fd351a..f429287c08 100644 --- a/src/test/logseq/cli/transport_test.cljs +++ b/src/test/logseq/cli/transport_test.cljs @@ -1,5 +1,5 @@ (ns logseq.cli.transport-test - (:require [cljs.test :refer [deftest is async testing]] + (:require [cljs.test :refer [deftest is async]] [promesa.core :as p] [logseq.cli.transport :as transport]))