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

This commit is contained in:
rcmerci
2026-01-18 18:09:23 +08:00
parent bd61765803
commit 3b79fd88da
12 changed files with 707 additions and 146 deletions

View File

@@ -16,60 +16,71 @@
[malli.core :as m]
[malli.error :as me]))
(defn- ensure-db-graph
[db]
(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]
@@ -118,32 +129,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
;; =================

View File

@@ -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.

View File

@@ -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:

View File

@@ -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!)

View File

@@ -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])))))

View File

@@ -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))))

View File

@@ -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]

View File

@@ -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]

View File

@@ -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"

View File

@@ -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)))))

View File

@@ -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)))))))

View File

@@ -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")