Kaynağa Gözat

impl 005-logseq-cli-output-and-db-worker-node-log.md (1)

rcmerci 1 gün önce
ebeveyn
işleme
23d7eb30bd

+ 74 - 69
deps/cli/src/logseq/cli/common/mcp/tools.cljs

@@ -21,60 +21,66 @@
   (when-not (ldb/db-based-graph? db)
     (throw (ex-info "This tool must be called on a DB graph" {}))))
 
+(defn- minimal-list-item
+  [e]
+  (cond-> {:db/id (:db/id e)
+           :block/title (:block/title e)
+           :block/created-at (:block/created-at e)
+           :block/updated-at (:block/updated-at e)}
+    (:db/ident e) (assoc :db/ident (:db/ident e))))
+
 (defn list-properties
   "Main fn for ListProperties tool"
   [db {:keys [expand include-built-in] :as options}]
   (ensure-db-graph db)
   (let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
-  (->> (d/datoms db :avet :block/tags :logseq.class/Property)
-       (map #(d/entity db (:e %)))
-       (remove (fn [e]
-                 (and (not include-built-in?)
-                      (ldb/built-in? e))))
-       #_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
-       (map (fn [e]
-              (if expand
-                (cond-> (into {} e)
-                  true
-                  (dissoc e :block/tags :block/order :block/refs :block/name :db/index
-                          :logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
-                  true
-                  (update :block/uuid str)
-                  (:logseq.property/classes e)
-                  (update :logseq.property/classes #(mapv :db/ident %))
-                  (:logseq.property/description e)
-                  (update :logseq.property/description db-property/property-value-content))
-                {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))}))))))
+    (->> (d/datoms db :avet :block/tags :logseq.class/Property)
+         (map #(d/entity db (:e %)))
+         (remove (fn [e]
+                   (and (not include-built-in?)
+                        (ldb/built-in? e))))
+         #_((fn [x] (prn :prop-keys (distinct (mapcat keys x))) x))
+         (map (fn [e]
+                (if expand
+                  (cond-> (into {} e)
+                    true
+                    (dissoc e :block/tags :block/order :block/refs :block/name :db/index
+                            :logseq.property.embedding/hnsw-label-updated-at :logseq.property/default-value)
+                    true
+                    (update :block/uuid str)
+                    (:logseq.property/classes e)
+                    (update :logseq.property/classes #(mapv :db/ident %))
+                    (:logseq.property/description e)
+                    (update :logseq.property/description db-property/property-value-content))
+                  (minimal-list-item e)))))))
 
 (defn list-tags
   "Main fn for ListTags tool"
   [db {:keys [expand include-built-in] :as options}]
   (ensure-db-graph db)
   (let [include-built-in? (if (contains? options :include-built-in) include-built-in true)]
-  (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
-       (map #(d/entity db (:e %)))
-       (remove (fn [e]
-                 (and (not include-built-in?)
-                      (ldb/built-in? e))))
-       (map (fn [e]
-              (if expand
-                (cond-> (into {} e)
-                  true
-                  (dissoc e :block/tags :block/order :block/refs :block/name
-                          :logseq.property.embedding/hnsw-label-updated-at)
-                  true
-                  (update :block/uuid str)
-                  (:logseq.property.class/extends e)
-                  (update :logseq.property.class/extends #(mapv :db/ident %))
-                  (:logseq.property.class/properties e)
-                  (update :logseq.property.class/properties #(mapv :db/ident %))
-                  (:logseq.property.view/type e)
-                  (assoc :logseq.property.view/type (:db/ident (:logseq.property.view/type e)))
-                  (:logseq.property/description e)
-                  (update :logseq.property/description db-property/property-value-content))
-                {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))}))))))
+    (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
+         (map #(d/entity db (:e %)))
+         (remove (fn [e]
+                   (and (not include-built-in?)
+                        (ldb/built-in? e))))
+         (map (fn [e]
+                (if expand
+                  (cond-> (into {} e)
+                    true
+                    (dissoc e :block/tags :block/order :block/refs :block/name
+                            :logseq.property.embedding/hnsw-label-updated-at)
+                    true
+                    (update :block/uuid str)
+                    (:logseq.property.class/extends e)
+                    (update :logseq.property.class/extends #(mapv :db/ident %))
+                    (:logseq.property.class/properties e)
+                    (update :logseq.property.class/properties #(mapv :db/ident %))
+                    (:logseq.property.view/type e)
+                    (assoc :logseq.property.view/type (:db/ident (:logseq.property.view/type e)))
+                    (:logseq.property/description e)
+                    (update :logseq.property/description db-property/property-value-content))
+                  (minimal-list-item e)))))))
 
 (defn- get-page-blocks
   [db page-id]
@@ -125,32 +131,31 @@
         journal-only? (boolean journal-only)
         created-after-ms (parse-time created-after)
         updated-after-ms (parse-time updated-after)]
-  (->> (d/datoms db :avet :block/name)
-       (map #(d/entity db (:e %)))
-       (remove (fn [e]
-                 (and (not include-hidden?)
-                      (entity-util/hidden? e))))
-       (remove (fn [e]
-                 (let [is-journal? (ldb/journal? e)]
-                   (cond
-                     journal-only? (not is-journal?)
-                     (false? include-journal?) is-journal?
-                     :else false))))
-       (remove (fn [e]
-                 (and created-after-ms
-                      (<= (:block/created-at e 0) created-after-ms))))
-       (remove (fn [e]
-                 (and updated-after-ms
-                      (<= (:block/updated-at e 0) updated-after-ms))))
-       (map (fn [e]
-              (if expand
-                (-> e
-                    ;; Until there are options to limit pages, return minimal info to avoid
-                    ;; exceeding max payload size
-                    (select-keys [:block/uuid :block/title :block/created-at :block/updated-at])
-                    (update :block/uuid str))
-                {:block/title (:block/title e)
-                 :block/uuid (str (:block/uuid e))}))))))
+    (->> (d/datoms db :avet :block/name)
+         (map #(d/entity db (:e %)))
+         (remove (fn [e]
+                   (and (not include-hidden?)
+                        (entity-util/hidden? e))))
+         (remove (fn [e]
+                   (let [is-journal? (ldb/journal? e)]
+                     (cond
+                       journal-only? (not is-journal?)
+                       (false? include-journal?) is-journal?
+                       :else false))))
+         (remove (fn [e]
+                   (and created-after-ms
+                        (<= (:block/created-at e 0) created-after-ms))))
+         (remove (fn [e]
+                   (and updated-after-ms
+                        (<= (:block/updated-at e 0) updated-after-ms))))
+         (map (fn [e]
+                (if expand
+                  (-> e
+                      ;; Until there are options to limit pages, return minimal info to avoid
+                      ;; exceeding max payload size
+                      (select-keys [:db/id :db/ident :block/uuid :block/title :block/created-at :block/updated-at])
+                      (update :block/uuid str))
+                  (minimal-list-item e)))))))
 
 ;; upsert-nodes tool
 ;; =================

+ 23 - 0
docs/agent-guide/005-logseq-cli-output-and-db-worker-node-log.md

@@ -9,6 +9,29 @@ Tech Stack: ClojureScript, babashka/cli, lambdaisland.glogi, Node.js fs/path.
 
 Related: Builds on docs/agent-guide/004-logseq-cli-verb-subcommands.md and docs/agent-guide/003-db-worker-node-cli-orchestration.md.
 
+## Human Output Specification
+
+Target: plain text, no ANSI colors. Each command has a stable layout and ordering.
+
+| Command | OK output (human) | Empty output | Notes |
+| --- | --- | --- | --- |
+| graph list | Table with header `GRAPH` and rows of graph names, followed by `Count: N` | Header + `Count: 0` | Data from `{:graphs [...]}` |
+| graph create | `Graph created: <graph>` | n/a | Use graph name from action/options |
+| graph switch | `Graph switched: <graph>` | n/a | Use graph name from action/options |
+| graph remove | `Graph removed: <graph>` | n/a | Use graph name from action/options |
+| graph validate | `Graph validated: <graph>` | n/a | Use graph name from action/options |
+| graph info | Lines: `Graph: <graph>`, `Created at: <ts>`, `Schema version: <v>` | n/a | Use `:logseq.kv/*` data; show `-` if missing |
+| 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 <status>: <repo>` + details line `Host: <host>  Port: <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 |
+| add block | `Added blocks: <count> (repo: <repo>)` | n/a | Count = number of blocks submitted |
+| add page | `Added page: <page> (repo: <repo>)` | n/a | |
+| remove block | `Removed block: <block-id> (repo: <repo>)` | n/a | Prefer UUID if available |
+| remove page | `Removed page: <page> (repo: <repo>)` | n/a | |
+| search | Table with header `TYPE TITLE/CONTENT UUID UPDATED-AT CREATED-AT`, rows in stable order, followed by `Count: N` | Header + `Count: 0` | For block rows use content snippet; for tag/property rows omit timestamps |
+| show (text) | Raw tree text (no table), trimmed | n/a | For `--format json|edn`, keep existing structured output |
+| errors | `Error (<code>): <message>` + optional `Hint: <hint>` line | n/a | Ensure error codes are stable and consistent |
+
 ## Problem statement
 
 The current logseq-cli human output is mostly raw pr-str output, which is hard to read and inconsistent across commands.

+ 1 - 0
docs/cli/logseq-cli.md

@@ -83,6 +83,7 @@ Subcommands:
 
 Output formats:
 - Global `--output <human|json|edn>` (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.
 
 Examples:
 

+ 76 - 9
src/main/frontend/worker/db_worker_node.cljs

@@ -1,6 +1,8 @@
 (ns frontend.worker.db-worker-node
   "Node.js daemon entrypoint for db-worker."
-  (:require ["http" :as http]
+  (:require ["fs" :as fs]
+            ["http" :as http]
+            ["path" :as node-path]
             [clojure.string :as string]
             [frontend.worker.db-core :as db-core]
             [frontend.worker.db-worker-node-lock :as db-lock]
@@ -8,13 +10,13 @@
             [frontend.worker.state :as worker-state]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [lambdaisland.glogi.console :as glogi-console]
             [logseq.db :as ldb]
             [promesa.core :as p]))
 
 (defonce ^:private *ready? (atom false))
 (defonce ^:private *sse-clients (atom #{}))
 (defonce ^:private *lock-info (atom nil))
+(defonce ^:private *file-handler (atom nil))
 
 (defn- send-json!
   [^js res status payload]
@@ -222,15 +224,82 @@
   (println "  --repo <name>        (required)")
   (println "  --rtc-ws-url <url>   (optional)")
   (println "  --log-level <level>  (default info)")
+  (println "  logs: <data-dir>/<graph-dir>/db-worker-node-YYYYMMDD.log (retains 7)")
   (println "  --auth-token <token> (optional)"))
 
+(defn- pad2
+  [value]
+  (if (< value 10)
+    (str "0" value)
+    (str value)))
+
+(defn- yyyymmdd
+  [^js date]
+  (str (.getFullYear date)
+       (pad2 (inc (.getMonth date)))
+       (pad2 (.getDate date))))
+
+(defn- log-path
+  [data-dir repo]
+  (let [data-dir (db-lock/resolve-data-dir data-dir)
+        repo-dir (db-lock/repo-dir data-dir repo)
+        date-str (yyyymmdd (js/Date.))]
+    (node-path/join repo-dir (str "db-worker-node-" date-str ".log"))))
+
+(defn- log-files
+  [repo-dir]
+  (->> (when (fs/existsSync repo-dir)
+         (fs/readdirSync repo-dir))
+       (filter (fn [^js name]
+                 (re-matches #"db-worker-node-\d{8}\.log" name)))
+       (sort)))
+
+(defn- enforce-log-retention!
+  [repo-dir]
+  (let [files (log-files repo-dir)
+        excess (max 0 (- (count files) 7))]
+    (doseq [name (take excess files)]
+      (fs/unlinkSync (node-path/join repo-dir name)))))
+
+(defn- format-log-line
+  [{:keys [time level message logger-name exception]}]
+  (let [ts (.toISOString (js/Date. time))
+        base (str ts
+                  " ["
+                  (name level)
+                  "] ["
+                  logger-name
+                  "] "
+                  (pr-str message))]
+    (str base (when exception (str " " (pr-str exception))) "\n")))
+
+(defn- install-file-logger!
+  [{:keys [data-dir repo log-level]}]
+  (let [data-dir (db-lock/resolve-data-dir data-dir)
+        repo-dir (db-lock/repo-dir data-dir repo)
+        file-path (log-path data-dir repo)]
+    (fs/mkdirSync repo-dir #js {:recursive true})
+    (fs/writeFileSync file-path "" #js {:flag "a"})
+    (enforce-log-retention! repo-dir)
+    (when-let [handler @*file-handler]
+      (log/remove-handler handler))
+    (let [handler (fn [record]
+                    (fs/appendFileSync file-path (format-log-line record)))]
+      (reset! *file-handler handler)
+      (log/add-handler handler))
+    (log/set-levels {:glogi/root log-level})
+    file-path))
+
 (defn start-daemon!
-  [{:keys [data-dir repo rtc-ws-url auth-token]}]
+  [{:keys [data-dir repo rtc-ws-url auth-token log-level]}]
   (let [host "127.0.0.1"
         port 0]
     (if-not (seq repo)
       (p/rejected (ex-info "repo is required" {:code :missing-repo}))
       (do
+        (install-file-logger! {:data-dir data-dir
+                               :repo repo
+                               :log-level (keyword (or log-level "info"))})
         (reset! *ready? false)
         (set-main-thread-stub!)
         (-> (p/let [platform (platform-node/node-platform {:data-dir data-dir
@@ -288,22 +357,20 @@
 
 (defn main
   []
-  (let [{:keys [data-dir repo rtc-ws-url log-level auth-token help?]}
-        (parse-args (.-argv js/process))
-        log-level (keyword (or log-level "info"))]
+  (let [{:keys [data-dir repo rtc-ws-url auth-token help?] :as opts}
+        (parse-args (.-argv js/process))]
     (when help?
       (show-help!)
       (.exit js/process 0))
     (when-not (seq repo)
       (show-help!)
       (.exit js/process 1))
-    (glogi-console/install!)
-    (log/set-levels {:glogi/root log-level})
     (p/let [{:keys [stop!] :as daemon}
             (start-daemon! {:data-dir data-dir
                             :repo repo
                             :rtc-ws-url rtc-ws-url
-                            :auth-token auth-token})]
+                            :auth-token auth-token
+                            :log-level (:log-level opts)})]
       (log/info :db-worker-node-ready {:host (:host daemon) :port (:port daemon)})
       (let [shutdown (fn []
                        (-> (stop!)

+ 41 - 29
src/main/logseq/cli/commands.cljs

@@ -778,17 +778,20 @@
   (case command
     :graph-list
     {:ok? true
-     :action {:type :graph-list}}
+     :action {:type :graph-list
+              :command :graph-list}}
 
     :graph-create
     (if-not (seq graph)
       (missing-graph-error)
       {:ok? true
        :action {:type :invoke
+                :command :graph-create
                 :method "thread-api/create-or-open-db"
                 :direct-pass? false
                 :args [repo {}]
                 :repo repo
+                :graph (repo->graph repo)
                 :allow-missing-graph true
                 :persist-repo (repo->graph repo)}})
 
@@ -797,6 +800,7 @@
       (missing-graph-error)
       {:ok? true
        :action {:type :graph-switch
+                :command :graph-switch
                 :repo repo
                 :graph (repo->graph repo)}})
 
@@ -805,26 +809,31 @@
       (missing-graph-error)
       {:ok? true
        :action {:type :invoke
+                :command :graph-remove
                 :method "thread-api/unsafe-unlink-db"
                 :direct-pass? false
                 :args [repo]
-                :repo repo}})
+                :repo repo
+                :graph (repo->graph repo)}})
 
     :graph-validate
     (if-not (seq repo)
       (missing-graph-error)
       {:ok? true
        :action {:type :invoke
+                :command :graph-validate
                 :method "thread-api/validate-db"
                 :direct-pass? false
                 :args [repo]
-                :repo repo}})
+                :repo repo
+                :graph (repo->graph repo)}})
 
     :graph-info
     (if-not (seq repo)
       (missing-graph-error)
       {:ok? true
        :action {:type :graph-info
+                :command :graph-info
                 :repo repo
                 :graph (repo->graph repo)}})))
 
@@ -1369,29 +1378,32 @@
 
 (defn execute
   [action config]
-  (-> (p/let [check (ensure-existing-graph action config)]
-        (if-not (:ok? check)
-          {:status :error
-           :error (:error check)}
-          (case (:type action)
-            :graph-list (execute-graph-list action config)
-            :invoke (execute-invoke action config)
-            :graph-switch (execute-graph-switch action config)
-            :graph-info (execute-graph-info action config)
-            :list-page (execute-list-page action config)
-            :list-tag (execute-list-tag action config)
-            :list-property (execute-list-property action config)
-            :add-block (execute-add-block action config)
-            :add-page (execute-add-page action config)
-            :remove-block (execute-remove action config)
-            :remove-page (execute-remove action config)
-            :search (execute-search action config)
-            :show (execute-show action config)
-            :server-list (execute-server-list action config)
-            :server-status (execute-server-status action config)
-            :server-start (execute-server-start action config)
-            :server-stop (execute-server-stop action config)
-            :server-restart (execute-server-restart action config)
-            {:status :error
-             :error {:code :unknown-action
-                     :message "unknown action"}})))))
+  (-> (p/let [check (ensure-existing-graph action config)
+              result (if-not (:ok? check)
+                       {:status :error
+                        :error (:error check)}
+                       (case (:type action)
+                         :graph-list (execute-graph-list action config)
+                         :invoke (execute-invoke action config)
+                         :graph-switch (execute-graph-switch action config)
+                         :graph-info (execute-graph-info action config)
+                         :list-page (execute-list-page action config)
+                         :list-tag (execute-list-tag action config)
+                         :list-property (execute-list-property action config)
+                         :add-block (execute-add-block action config)
+                         :add-page (execute-add-page action config)
+                         :remove-block (execute-remove action config)
+                         :remove-page (execute-remove action config)
+                         :search (execute-search action config)
+                         :show (execute-show action config)
+                         :server-list (execute-server-list action config)
+                         :server-status (execute-server-status action config)
+                         :server-start (execute-server-start action config)
+                         :server-stop (execute-server-stop action config)
+                         :server-restart (execute-server-restart action config)
+                         {:status :error
+                          :error {:code :unknown-action
+                                  :message "unknown action"}}))]
+        (assoc result
+               :command (or (:command action) (:type action))
+               :context (select-keys action [:repo :graph :page :block :blocks])))))

+ 234 - 12
src/main/logseq/cli/format.cljs

@@ -1,6 +1,7 @@
 (ns logseq.cli.format
   "Formatting helpers for CLI output."
-  (:require [clojure.walk :as walk]))
+  (:require [clojure.string :as string]
+            [clojure.walk :as walk]))
 
 (defn- normalize-json
   [value]
@@ -22,18 +23,239 @@
       (set! (.-error obj) (clj->js (normalize-json (update error :code name)))))
     (js/JSON.stringify obj)))
 
+(defn- pad-right
+  [value width]
+  (let [text (str value)
+        missing (- width (count text))]
+    (if (pos? missing)
+      (str text (apply str (repeat missing " ")))
+      text)))
+
+(defn- normalize-cell
+  [value]
+  (cond
+    (nil? value) "-"
+    (keyword? value) (str value)
+    :else (str value)))
+
+(defn- render-table
+  [headers rows]
+  (let [normalized-rows (mapv (fn [row]
+                                (mapv normalize-cell row))
+                              rows)
+        trim-right (fn [value]
+                     (string/replace value #"\s+$" ""))
+        widths (mapv (fn [idx header]
+                       (apply max (count header)
+                              (map #(count (nth % idx)) normalized-rows)))
+                     (range (count headers))
+                     headers)
+        render-row (fn [row]
+                     (->> (map pad-right row widths)
+                          (string/join "  ")
+                          (trim-right)))
+        lines (cons (render-row headers)
+                    (map render-row normalized-rows))]
+    (string/join "\n" lines)))
+
+(defn- format-counted-table
+  [headers rows]
+  (str (render-table headers rows)
+       "\n"
+       "Count: "
+       (count rows)))
+
+(defn- error-hint
+  [{:keys [code]}]
+  (case code
+    :missing-graph "Use --graph <name>"
+    :missing-repo "Use --repo <name>"
+    :missing-content "Use --content or pass content as args"
+    :missing-search-text "Provide search text or --text"
+    nil))
+
+(defn- format-error
+  [error]
+  (let [{:keys [code message]} error
+        hint (error-hint error)]
+    (cond-> (str "Error (" (name (or code :error)) "): " message)
+      hint (str "\nHint: " hint))))
+
+(defn- maybe-ident-header
+  [items]
+  (when (some :db/ident items)
+    ["IDENT"]))
+
+(defn- parse-ts
+  [value]
+  (cond
+    (number? value) value
+    (string? value) (let [ms (js/Date.parse value)]
+                      (when-not (js/isNaN ms) ms))
+    :else nil))
+
+(defn- human-ago
+  [value now-ms]
+  (if-let [ts (parse-ts value)]
+    (let [diff-ms (max 0 (- now-ms ts))
+          secs (js/Math.floor (/ diff-ms 1000))
+          mins (js/Math.floor (/ secs 60))
+          hours (js/Math.floor (/ mins 60))
+          days (js/Math.floor (/ hours 24))
+          months (js/Math.floor (/ days 30))
+          years (js/Math.floor (/ days 365))]
+      (cond
+        (< secs 60) (str secs "s ago")
+        (< mins 60) (str mins "m ago")
+        (< hours 24) (str hours "h ago")
+        (< days 30) (str days "d ago")
+        (< months 12) (str months "mo ago")
+        :else (str years "y ago")))
+    "-"))
+
+(defn- format-list-row
+  [item include-ident? now-ms]
+  (let [base [(or (:db/id item) (:id item))
+              (or (:title item) (:block/title item) (:name item))]
+        with-ident (cond-> base
+                     include-ident? (conj (:db/ident item)))
+        updated (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)
+        created (human-ago (or (:created-at item) (:block/created-at item)) now-ms)]
+    (conj with-ident updated created)))
+
+(defn- format-list-page
+  [items now-ms]
+  (let [items (or items [])
+        include-ident? (boolean (some :db/ident items))
+        headers (into ["ID" "TITLE"]
+                      (concat (or (maybe-ident-header items) [])
+                              ["UPDATED-AT" "CREATED-AT"]))]
+    (format-counted-table
+     headers
+     (mapv #(format-list-row % include-ident? now-ms) items))))
+
+(defn- format-list-tag-or-property
+  [items now-ms]
+  (let [items (or items [])
+        include-ident? (boolean (some :db/ident items))
+        headers (into ["ID" "TITLE"]
+                      (concat (or (maybe-ident-header items) [])
+                              ["UPDATED-AT" "CREATED-AT"]))]
+    (format-counted-table
+     headers
+     (mapv #(format-list-row % include-ident? now-ms) items))))
+
+(defn- format-graph-list
+  [graphs]
+  (format-counted-table
+   ["GRAPH"]
+   (mapv (fn [graph] [graph]) (or graphs []))))
+
+(defn- format-server-list
+  [servers]
+  (format-counted-table
+   ["REPO" "STATUS" "HOST" "PORT" "PID"]
+   (mapv (fn [server]
+           [(:repo server)
+            (:status server)
+            (:host server)
+            (:port server)
+            (:pid server)])
+         (or servers []))))
+
+(defn- format-search-results
+  [results]
+  (format-counted-table
+   ["TYPE" "TITLE/CONTENT" "UUID" "UPDATED-AT" "CREATED-AT"]
+   (mapv (fn [item]
+           [(:type item)
+            (or (:title item) (:content item))
+            (:uuid item)
+            (:updated-at item)
+            (:created-at item)])
+         (or results []))))
+
+(defn- format-graph-info
+  [{:keys [graph logseq.kv/graph-created-at logseq.kv/schema-version]}]
+  (string/join "\n"
+               [(str "Graph: " (or graph "-"))
+                (str "Created at: " (or graph-created-at "-"))
+                (str "Schema version: " (or schema-version "-"))]))
+
+(defn- format-server-status
+  [{:keys [repo status host port]}]
+  (string/join "\n"
+               (cond-> [(str "Server " (name (or status :unknown)) ": " repo)]
+                 (and host port) (conj (str "Host: " host "  Port: " port)))))
+
+(defn- format-server-action
+  [command {:keys [repo status host port]}]
+  (let [status (or status
+                   (case command
+                     :server-start :started
+                     :server-stop :stopped
+                     :server-restart :restarted
+                     :unknown))]
+    (string/join "\n"
+                 (cond-> [(str "Server " (name status) ": " repo)]
+                   (and host port) (conj (str "Host: " host "  Port: " port))))))
+
+(defn- format-add-block
+  [{:keys [repo blocks]}]
+  (str "Added blocks: " (count blocks) " (repo: " repo ")"))
+
+(defn- format-add-page
+  [{: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-graph-action
+  [command {:keys [graph]}]
+  (let [verb (case command
+               :graph-create "created"
+               :graph-switch "switched"
+               :graph-remove "removed"
+               :graph-validate "validated"
+               "updated")]
+    (str "Graph " verb ": " graph)))
+
 (defn- ->human
-  [{:keys [status data error]}]
-  (case status
-    :ok
-    (if (and (map? data) (contains? data :message))
-      (:message data)
-      (pr-str data))
+  [{:keys [status data error command context]} {:keys [now-ms]}]
+  (let [now-ms (or now-ms (js/Date.now))]
+    (case status
+      :ok
+      (case command
+        :graph-list (format-graph-list (:graphs data))
+        :graph-info (format-graph-info data)
+        (:graph-create :graph-switch :graph-remove :graph-validate)
+        (format-graph-action command context)
+        :server-list (format-server-list (:servers data))
+        :server-status (format-server-status data)
+        (:server-start :server-stop :server-restart)
+        (format-server-action command data)
+        :list-page (format-list-page (:items data) now-ms)
+        (: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)
+        :search (format-search-results (:results data))
+        :show (or (:message data) (pr-str data))
+        (if (and (map? data) (contains? data :message))
+          (:message data)
+          (pr-str data)))
 
-    :error
-    (str "error: " (:message error))
+      :error
+      (format-error error)
 
-    (pr-str {:status status :data data :error error})))
+      (pr-str {:status status :data data :error error}))))
 
 (defn- ->edn
   [{:keys [status data error]}]
@@ -42,7 +264,7 @@
             (= status :error) (assoc :error error))))
 
 (defn format-result
-  [result {:keys [output-format]}]
+  [result {:keys [output-format now-ms] :as opts}]
   (let [format (cond
                  (= output-format :edn) :edn
                  (= output-format :json) :json
@@ -50,4 +272,4 @@
     (case format
       :json (->json result)
       :edn (->edn result)
-      (->human result))))
+      (->human result opts))))

+ 6 - 2
src/main/logseq/cli/main.cljs

@@ -29,7 +29,8 @@
        (not (:ok? parsed))
        (p/resolved {:exit-code 1
                     :output (format/format-result {:status :error
-                                                   :error (:error parsed)}
+                                                   :error (:error parsed)
+                                                   :command (:command parsed)}
                                                   {})})
 
        :else
@@ -38,7 +39,10 @@
          (if-not (:ok? action-result)
            (p/resolved {:exit-code 1
                         :output (format/format-result {:status :error
-                                                       :error (:error action-result)}
+                                                       :error (:error action-result)
+                                                       :command (:command parsed)
+                                                       :context (select-keys (:options parsed)
+                                                                             [:repo :graph :page :block])}
                                                       cfg)})
            (-> (commands/execute (:action action-result) cfg)
                (p/then (fn [result]

+ 4 - 23
src/main/logseq/cli/server.cljs

@@ -6,17 +6,10 @@
             ["os" :as os]
             ["path" :as node-path]
             [clojure.string :as string]
-            [frontend.worker.db-worker-node :as db-worker-node]
             [frontend.worker-common.util :as worker-util]
             [lambdaisland.glogi :as log]
             [promesa.core :as p]))
 
-(defonce ^:private *inproc-servers (atom {}))
-
-(defn- inproc-enabled?
-  []
-  (boolean (.-DEBUG js/goog)))
-
 (defn- expand-home
   [path]
   (if (string/starts-with? path "~")
@@ -172,20 +165,13 @@
 
 (defn- spawn-server!
   [{:keys [repo data-dir]}]
-  (let [script (node-path/join (js/process.cwd) "static" "db-worker-node.js")
+  (let [script (node-path/join js/__dirname "db-worker-node.js")
         args #js [script "--repo" repo "--data-dir" data-dir]
         child (.spawn child-process "node" args #js {:detached true
                                                      :stdio "ignore"})]
     (.unref child)
     child))
 
-(defn- start-inproc-server!
-  [{:keys [repo data-dir]}]
-  (p/let [daemon (db-worker-node/start-daemon! {:data-dir data-dir
-                                                :repo repo})]
-    (swap! *inproc-servers assoc repo daemon)
-    daemon))
-
 (defn- ensure-server-started!
   [config repo]
   (let [data-dir (resolve-data-dir config)
@@ -193,9 +179,7 @@
     (p/let [existing (read-lock path)
             _ (cleanup-stale-lock! path existing)
             _ (when (not (fs/existsSync path))
-                (if (inproc-enabled?)
-                  (start-inproc-server! {:repo repo :data-dir data-dir})
-                  (spawn-server! {:repo repo :data-dir data-dir}))
+                (spawn-server! {:repo repo :data-dir data-dir})
                 (wait-for-lock path))
             lock (read-lock path)]
       (when-not lock
@@ -232,7 +216,6 @@
                         (p/resolved (not (fs/existsSync path))))
                       {:timeout-ms 5000
                        :interval-ms 200})
-            (swap! *inproc-servers dissoc repo)
             {:ok? true
              :data {:repo repo}})
           (p/catch (fn [_]
@@ -248,10 +231,8 @@
                        {:ok? false
                         :error {:code :server-stop-timeout
                                 :message "timed out stopping server"}}
-                       (do
-                         (swap! *inproc-servers dissoc repo)
-                         {:ok? true
-                          :data {:repo repo}}))))))))
+                       {:ok? true
+                        :data {:repo repo}})))))))
 
 (defn start-server!
   [config repo]

+ 61 - 0
src/test/frontend/worker/db_worker_node_test.cljs

@@ -79,6 +79,67 @@
         repo-dir (node-path/join data-dir (str "." pool-name))]
     (node-path/join repo-dir "db-worker.lock")))
 
+(defn- pad2
+  [value]
+  (if (< value 10)
+    (str "0" value)
+    (str value)))
+
+(defn- yyyymmdd
+  [^js date]
+  (str (.getFullYear date)
+       (pad2 (inc (.getMonth date)))
+       (pad2 (.getDate date))))
+
+(defn- log-path
+  [data-dir repo]
+  (let [pool-name (worker-util/get-pool-name repo)
+        repo-dir (node-path/join data-dir (str "." pool-name))
+        date-str (yyyymmdd (js/Date.))]
+    (node-path/join repo-dir (str "db-worker-node-" date-str ".log"))))
+
+(deftest db-worker-node-creates-log-file
+  (async done
+    (let [daemon (atom nil)
+          data-dir (node-helper/create-tmp-dir "db-worker-log")
+          repo (str "logseq_db_log_" (subs (str (random-uuid)) 0 8))
+          log-file (log-path data-dir repo)]
+      (-> (p/let [{:keys [stop!]}
+                  (db-worker-node/start-daemon! {:data-dir data-dir
+                                                 :repo repo})
+                  _ (reset! daemon {:stop! stop!})
+                  _ (p/delay 50)]
+            (is (fs/existsSync log-file)))
+          (p/catch (fn [e]
+                     (is false (str "unexpected error: " e))))
+          (p/finally (fn []
+                       (if-let [stop! (:stop! @daemon)]
+                         (-> (stop!) (p/finally (fn [] (done))))
+                         (done))))))))
+
+(deftest db-worker-node-log-file-has-entries
+  (async done
+    (let [daemon (atom nil)
+          data-dir (node-helper/create-tmp-dir "db-worker-log-entries")
+          repo (str "logseq_db_log_entries_" (subs (str (random-uuid)) 0 8))
+          log-file (log-path data-dir repo)]
+      (-> (p/let [{:keys [host port stop!]}
+                  (db-worker-node/start-daemon! {:data-dir data-dir
+                                                 :repo repo})
+                  _ (reset! daemon {:stop! stop!})
+                  _ (invoke host port "thread-api/create-or-open-db" [repo {}])
+                  _ (p/delay 50)
+                  contents (when (fs/existsSync log-file)
+                             (.toString (fs/readFileSync log-file) "utf8"))]
+            (is (fs/existsSync log-file))
+            (is (pos? (count contents))))
+          (p/catch (fn [e]
+                     (is false (str "unexpected error: " e))))
+          (p/finally (fn []
+                       (if-let [stop! (:stop! @daemon)]
+                         (-> (stop!) (p/finally (fn [] (done))))
+                         (done))))))))
+
 (deftest db-worker-node-parse-args-ignores-host-and-port
   (let [parse-args #'db-worker-node/parse-args
         result (parse-args #js ["node" "db-worker-node.js"

+ 130 - 1
src/test/logseq/cli/format_test.cljs

@@ -39,4 +39,133 @@
   (testing "human error (default)"
     (let [result (format/format-result {:status :error :error {:code :boom :message "nope"}}
                                        {:output-format nil})]
-      (is (= "error: nope" result)))))
+      (is (= "Error (boom): nope" result)))))
+
+(deftest test-human-output-list-page
+  (testing "list page renders a table with count"
+    (let [result (format/format-result {:status :ok
+                                        :command :list-page
+                                        :data {:items [{:db/id 1
+                                                        :title "Alpha"
+                                                        :updated-at 90000
+                                                        :created-at 40000}]}}
+                                       {:output-format nil
+                                        :now-ms 100000})]
+      (is (= (str "ID  TITLE  UPDATED-AT  CREATED-AT\n"
+                  "1   Alpha  10s ago     1m ago\n"
+                  "Count: 1")
+             result)))))
+
+(deftest test-human-output-list-tag-property
+  (testing "list tag uses ID column from :db/id"
+    (let [result (format/format-result {:status :ok
+                                        :command :list-tag
+                                        :data {:items [{:block/title "Tag"
+                                                        :db/id 42
+                                                        :block/created-at 40000
+                                                        :block/updated-at 90000
+                                                        :db/ident :logseq.class/Tag}]}}
+                                       {:output-format nil
+                                        :now-ms 100000})]
+      (is (= (str "ID  TITLE  IDENT              UPDATED-AT  CREATED-AT\n"
+                  "42  Tag    :logseq.class/Tag  10s ago     1m ago\n"
+                  "Count: 1")
+             result))))
+
+  (testing "list property uses ID column from :db/id"
+    (let [result (format/format-result {:status :ok
+                                        :command :list-property
+                                        :data {:items [{:block/title "Prop"
+                                                        :db/id 99
+                                                        :block/created-at 40000
+                                                        :block/updated-at 90000}]}}
+                                       {:output-format nil
+                                        :now-ms 100000})]
+      (is (= (str "ID  TITLE  UPDATED-AT  CREATED-AT\n"
+                  "99  Prop   10s ago     1m ago\n"
+                  "Count: 1")
+             result)))))
+
+(deftest test-human-output-add-remove
+  (testing "add block renders a succinct success line"
+    (let [result (format/format-result {:status :ok
+                                        :command :add-block
+                                        :context {:repo "demo-repo"
+                                                  :blocks ["a" "b"]}
+                                        :data {:result {:ok true}}}
+                                       {:output-format nil})]
+      (is (= "Added blocks: 2 (repo: demo-repo)" result))))
+
+  (testing "remove page renders a succinct success line"
+    (let [result (format/format-result {:status :ok
+                                        :command :remove-page
+                                        :context {:repo "demo-repo"
+                                                  :page "Home"}
+                                        :data {:result {:ok true}}}
+                                       {:output-format nil})]
+      (is (= "Removed page: Home (repo: demo-repo)" result)))))
+
+(deftest test-human-output-graph-info
+  (testing "graph info includes key metadata lines"
+    (let [result (format/format-result {:status :ok
+                                        :command :graph-info
+                                        :data {:graph "demo-graph"
+                                               :logseq.kv/graph-created-at 123
+                                               :logseq.kv/schema-version 2}}
+                                       {:output-format nil})]
+      (is (= (str "Graph: demo-graph\n"
+                  "Created at: 123\n"
+                  "Schema version: 2")
+             result)))))
+
+(deftest test-human-output-server-status
+  (testing "server status includes repo, status, host, port"
+    (let [result (format/format-result {:status :ok
+                                        :command :server-status
+                                        :data {:repo "demo-repo"
+                                               :status :ready
+                                               :host "127.0.0.1"
+                                               :port 1234}}
+                                       {:output-format nil})]
+      (is (= (str "Server ready: demo-repo\n"
+                  "Host: 127.0.0.1  Port: 1234")
+             result)))))
+
+(deftest test-human-output-search-and-show
+  (testing "search renders a table with count"
+    (let [result (format/format-result {:status :ok
+                                        :command :search
+                                        :data {:results [{:type "page"
+                                                         :title "Alpha"
+                                                         :uuid "u1"
+                                                         :updated-at 3
+                                                         :created-at 1}
+                                                        {:type "block"
+                                                         :content "Note"
+                                                         :uuid "u2"
+                                                         :updated-at 4
+                                                         :created-at 2}]}}
+                                       {:output-format nil})]
+      (is (= (str "TYPE   TITLE/CONTENT  UUID  UPDATED-AT  CREATED-AT\n"
+                  "page   Alpha          u1    3           1\n"
+                  "block  Note           u2    4           2\n"
+                  "Count: 2")
+             result))))
+
+  (testing "show renders text payloads directly"
+    (let [result (format/format-result {:status :ok
+                                        :command :show
+                                        :data {:message "Line 1\nLine 2"}}
+                                       {:output-format nil})]
+      (is (= "Line 1\nLine 2" result)))))
+
+(deftest test-human-output-error-formatting
+  (testing "errors include code and hint when available"
+    (let [result (format/format-result {:status :error
+                                        :command :graph-create
+                                        :error {:code :missing-graph
+                                                :message "graph name is required"}}
+                                       {:output-format nil})]
+      (is (= (str "Error (missing-graph): graph name is required\n"
+                  "Hint: Use --graph <name>")
+             result)))))

+ 45 - 0
src/test/logseq/cli/integration_test.cljs

@@ -128,3 +128,48 @@
           (p/catch (fn [e]
                      (is false (str "unexpected error: " e))
                      (done)))))))
+
+(deftest test-cli-list-outputs-include-id
+  (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" "list-id-graph"] data-dir cfg-path)
+                  _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path)
+                  list-page-result (run-cli ["list" "page"] data-dir cfg-path)
+                  list-page-payload (parse-json-output list-page-result)
+                  list-tag-result (run-cli ["list" "tag"] data-dir cfg-path)
+                  list-tag-payload (parse-json-output list-tag-result)
+                  list-property-result (run-cli ["list" "property"] data-dir cfg-path)
+                  list-property-payload (parse-json-output list-property-result)
+                  stop-result (run-cli ["server" "stop" "--repo" "list-id-graph"] data-dir cfg-path)
+                  stop-payload (parse-json-output stop-result)]
+            (is (= "ok" (:status list-page-payload)))
+            (is (every? #(contains? % :id) (get-in list-page-payload [:data :items])))
+            (is (= "ok" (:status list-tag-payload)))
+            (is (every? #(contains? % :id) (get-in list-tag-payload [:data :items])))
+            (is (= "ok" (:status list-property-payload)))
+            (is (every? #(contains? % :id) (get-in list-property-payload [:data :items])))
+            (is (= "ok" (:status stop-payload)))
+            (done))
+          (p/catch (fn [e]
+                     (is false (str "unexpected error: " e))
+                     (done)))))))
+
+(deftest test-cli-list-page-human-output
+  (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" "human-list-graph"] data-dir cfg-path)
+                  _ (run-cli ["add" "page" "--page" "TestPage"] data-dir cfg-path)
+                  list-page-result (run-cli ["list" "page" "--output" "human"] data-dir cfg-path)
+                  output (:output list-page-result)]
+            (is (= 0 (:exit-code list-page-result)))
+            (is (string/includes? output "TITLE"))
+            (is (string/includes? output "TestPage"))
+            (is (string/includes? output "Count:"))
+            (done))
+          (p/catch (fn [e]
+                     (is false (str "unexpected error: " e))
+                     (done)))))))

+ 7 - 1
src/test/logseq/cli/server_test.cljs

@@ -11,7 +11,8 @@
 (deftest spawn-server-omits-host-and-port-flags
   (let [spawn-server! #'cli-server/spawn-server!
         captured (atom nil)
-        original-spawn (.-spawn child-process)]
+        original-spawn (.-spawn child-process)
+        original-cwd (.cwd js/process)]
     (set! (.-spawn child-process)
           (fn [cmd args opts]
             (reset! captured {:cmd cmd
@@ -19,15 +20,20 @@
                               :opts (js->clj opts :keywordize-keys true)})
             (js-obj "unref" (fn [] nil))))
     (try
+      (.chdir js/process "/")
       (spawn-server! {:repo "logseq_db_spawn_test"
                       :data-dir "/tmp/logseq-db-worker"})
       (is (= "node" (:cmd @captured)))
+      (is (= (node-path/join js/__dirname "db-worker-node.js")
+             (first (:args @captured))))
       (is (some #{"--repo"} (:args @captured)))
       (is (some #{"--data-dir"} (:args @captured)))
       (is (not-any? #{"--host" "--port"} (:args @captured)))
       (finally
+        (.chdir js/process original-cwd)
         (set! (.-spawn child-process) original-spawn)))))
 
+
 (deftest ensure-server-repairs-stale-lock
   (async done
     (let [data-dir (node-helper/create-tmp-dir "cli-server")