mirror of
https://github.com/logseq/logseq.git
synced 2026-05-25 05:04:24 +00:00
enhance(cli): humanize human-mode output formatting
This commit is contained in:
1
deps.edn
1
deps.edn
@@ -23,6 +23,7 @@
|
||||
cljs-http/cljs-http {:mvn/version "0.1.49"}
|
||||
org.babashka/sci {:mvn/version "0.12.51"}
|
||||
org.clj-commons/hickory {:mvn/version "0.7.7"}
|
||||
org.clj-commons/humanize {:mvn/version "1.2"}
|
||||
org.babashka/cli {:mvn/version "0.8.67"}
|
||||
hiccups/hiccups {:mvn/version "0.3.0"}
|
||||
tongue/tongue {:mvn/version "0.4.4"}
|
||||
|
||||
209
docs/agent-guide/086-logseq-cli-humanize-output.md
Normal file
209
docs/agent-guide/086-logseq-cli-humanize-output.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 086 — Introduce `org.clj-commons/humanize` for logseq-cli human output
|
||||
|
||||
## Goal
|
||||
|
||||
Adopt `org.clj-commons/humanize` in `logseq-cli` to improve readability and consistency of **human-mode** output while keeping `json` and `edn` output stable.
|
||||
|
||||
This plan is based on the current implementation of:
|
||||
|
||||
- CLI output layer: `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`
|
||||
- CLI command message producers: `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/*.cljs`
|
||||
- db-worker-node sync progress producers used by CLI: `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/download.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/sync/upload.cljs`
|
||||
|
||||
---
|
||||
|
||||
## Scope and constraints
|
||||
|
||||
### In scope
|
||||
|
||||
1. Add `org.clj-commons/humanize` dependency for CLI/runtime code paths.
|
||||
2. Replace ad-hoc human text formatting in CLI human output with library-backed helpers where it improves readability.
|
||||
3. Audit all practical human-output points that can benefit from this library and prioritize them.
|
||||
4. Keep output changes focused on `--output human` behavior.
|
||||
|
||||
### Out of scope
|
||||
|
||||
1. Changing `json` or `edn` payload schemas.
|
||||
2. Any `db-worker-node` code change (including sync progress message producers and thread-api payload changes).
|
||||
3. Rewriting CLI table layout/alignment logic (`string-width`, truncation, padding).
|
||||
4. Internationalization (the library is English-first and current CLI output is also English).
|
||||
|
||||
### Locked decisions
|
||||
|
||||
1. Keep relative time in short style (compatible with current `10s ago` / `1m ago` style).
|
||||
2. `list asset` `SIZE` displays human-readable size only (no extra raw-byte suffix).
|
||||
3. Apply grouped-number formatting consistently for `Count:` values.
|
||||
4. Formatter-only scope: do not modify any `db-worker-node` code.
|
||||
|
||||
---
|
||||
|
||||
## Why `org.clj-commons/humanize`
|
||||
|
||||
Useful functions for current CLI needs:
|
||||
|
||||
- `clj-commons.humanize/intcomma` for grouped numbers (`12,345`).
|
||||
- `clj-commons.humanize/filesize` for byte formatting (`2.0KiB`, `3.0MB`).
|
||||
- `clj-commons.humanize/relative-datetime` for relative time strings.
|
||||
- `clj-commons.humanize.inflect/pluralize-noun` for singular/plural grammar.
|
||||
- `clj-commons.humanize/oxford` for readable list joining in error/help text.
|
||||
|
||||
---
|
||||
|
||||
## Current baseline and optimization inventory
|
||||
|
||||
## A) Central formatter (`format.cljs`) — highest impact
|
||||
|
||||
| Area | Current behavior | Candidate improvement with `humanize` | Priority |
|
||||
|---|---|---|---|
|
||||
| `human-ago` for `UPDATED-AT`/`CREATED-AT` columns and graph metadata | Custom `s/m/h/d/mo/y ago` calculation with fixed month/year heuristics | Replace with `relative-datetime` brief mode via a wrapper to keep compact style | P0 |
|
||||
| `format-counted-table` (`Count: N`) used by list/search/query/server/graph tables | Raw integer | Use `intcomma` consistently for all `Count:` values | P0 |
|
||||
| `list asset` `SIZE` column | Raw bytes integer | Show human-readable size only (`filesize`, e.g. `2.0KiB`) | P0 |
|
||||
| `format-sync-status` pending counters and tx values | Raw numbers | `intcomma` for counters/tx ids | P1 |
|
||||
| `format-server-cleanup` summary counters | Raw numbers | `intcomma` + optional noun pluralization cleanup | P1 |
|
||||
| `format-upsert-block` change counts and `format-remove-block` multi-id count | Raw counts | `intcomma` for counts | P1 |
|
||||
| Graph-list legacy warning line (`Warning: N legacy graph directories detected.`) | Raw count with hardcoded noun | `intcomma` + `pluralize-noun` | P1 |
|
||||
|
||||
## B) Command-level message producers (`command/*.cljs`)
|
||||
|
||||
| File | Current behavior | Candidate improvement | Priority |
|
||||
|---|---|---|---|
|
||||
| `command/doctor.cljs` (`check-running-servers`, `check-server-revision-mismatch`) | Manual pluralization (`server`/`servers`, `uses`/`use`) | Use `pluralize-noun` (and centralized verb helper if needed) | P0 |
|
||||
| `command/graph.cljs` (`format-validation-errors`) | Manual `entity/entities` + raw count | `intcomma` + `pluralize-noun` | P0 |
|
||||
| `command/example.cljs` (`Found N examples ...`) | Raw count | `intcomma` + `pluralize-noun` | P1 |
|
||||
| `command/show.cljs` (`Linked References (N)`, `Referenced Entities (N)`) | Raw count | `intcomma` for large refs count | P1 |
|
||||
| `command/sync.cljs` (`missing required sync config ...`) | Comma-joined key list | Optionally use `oxford` for clearer list text | P2 |
|
||||
|
||||
## C) Audited but excluded by current scope decision
|
||||
|
||||
| File | Current behavior | Candidate improvement | Decision |
|
||||
|---|---|---|---|
|
||||
| `frontend/worker/sync/download.cljs` (`Importing data X/Y`) | Raw counters in message string | Could use `intcomma` for `X` and `Y` | Excluded (no db-worker changes) |
|
||||
| `frontend/worker/sync/upload.cljs` (`Uploading X/Y`) | Raw counters in message string | Could use `intcomma` for `X` and `Y` | Excluded (no db-worker changes) |
|
||||
|
||||
## D) Candidate but likely keep as-is for now
|
||||
|
||||
| File | Current behavior | Decision |
|
||||
|---|---|---|
|
||||
| `src/main/logseq/cli/profile.cljs` | Technical `Nms` tree output for profiling | Keep as technical output; do not humanize in phase 1 |
|
||||
|
||||
---
|
||||
|
||||
## Proposed design
|
||||
|
||||
### 1) Add a CLI-local adapter namespace
|
||||
|
||||
Create a small wrapper namespace (e.g. `src/main/logseq/cli/humanize.cljs`) that centralizes:
|
||||
|
||||
- `format-count` (uses `intcomma`)
|
||||
- `pluralize` (uses `pluralize-noun`)
|
||||
- `format-filesize` (uses `filesize`, configurable binary/decimal)
|
||||
- `relative-ago` (uses `relative-datetime` with fixed CLI options)
|
||||
- Optional list helper (`oxford`)
|
||||
|
||||
This avoids scattering direct library calls and keeps style decisions local to CLI.
|
||||
|
||||
### 2) Preserve machine-output invariants
|
||||
|
||||
All changes must keep:
|
||||
|
||||
- `format-result` behavior for `:json` and `:edn`
|
||||
- existing keys in command `:data` and `:error`
|
||||
- transport payload compatibility
|
||||
|
||||
### 3) Strict boundary: CLI formatter only
|
||||
|
||||
- Do not modify any `db-worker-node` files.
|
||||
- Do not change sync event payload structure.
|
||||
- Limit all behavior changes to CLI human-output formatting paths.
|
||||
|
||||
---
|
||||
|
||||
## Implementation plan
|
||||
|
||||
### Phase 1 — dependency + adapter (foundation)
|
||||
|
||||
1. Add dependency in root project deps for CLI compile/runtime usage:
|
||||
- `/Users/rcmerci/gh-repos/logseq/deps.edn`
|
||||
2. Add wrapper namespace:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/humanize.cljs`
|
||||
3. Add unit tests for wrapper behavior (counts, pluralization, relative time, filesize).
|
||||
|
||||
### Phase 2 — migrate high-impact formatter paths
|
||||
|
||||
1. Replace `human-ago` implementation in `format.cljs` with wrapper call.
|
||||
2. Update `format-counted-table` count rendering with grouped numbers.
|
||||
3. Update list-asset size rendering to human-readable filesize.
|
||||
4. Update graph-list/server-cleanup/sync-status numeric display paths.
|
||||
|
||||
Primary file:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`
|
||||
|
||||
### Phase 3 — migrate command message producers
|
||||
|
||||
1. `command/doctor.cljs`: pluralization cleanup.
|
||||
2. `command/graph.cljs`: validation summary noun/count formatting.
|
||||
3. `command/example.cljs`: example count formatting.
|
||||
4. `command/show.cljs`: referenced count formatting.
|
||||
|
||||
Primary files:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/doctor.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/graph.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/example.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs`
|
||||
|
||||
### Phase 4 — tests and docs
|
||||
|
||||
1. Update/extend formatter tests for expected human-output changes:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`
|
||||
2. Update command tests where message text is asserted:
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/doctor_test.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/graph_test.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/show_test.cljs`
|
||||
- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/example_test.cljs`
|
||||
3. Update CLI documentation examples if human-mode samples are shown:
|
||||
- `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md`
|
||||
|
||||
---
|
||||
|
||||
## Verification plan
|
||||
|
||||
Run targeted tests first:
|
||||
|
||||
```bash
|
||||
bb dev:test -v logseq.cli.format-test
|
||||
bb dev:test -v logseq.cli.command.doctor-test
|
||||
bb dev:test -v logseq.cli.command.graph-test
|
||||
bb dev:test -v logseq.cli.command.show-test
|
||||
bb dev:test -v logseq.cli.command.example-test
|
||||
```
|
||||
|
||||
Then broader checks:
|
||||
|
||||
```bash
|
||||
bb dev:lint-and-test
|
||||
bb -f cli-e2e/bb.edn test --skip-build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `org.clj-commons/humanize` is integrated and used by CLI human-output formatting paths.
|
||||
2. High-impact areas (relative time, `Count:` rendering with grouped numbers, asset filesize, pluralization in doctor/graph) are migrated.
|
||||
3. Relative time remains in compact short style compatible with current CLI behavior.
|
||||
4. `json` and `edn` outputs remain schema-compatible.
|
||||
5. No `db-worker-node` code changes are introduced.
|
||||
6. Existing CLI tests are updated and passing.
|
||||
|
||||
---
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- **Risk: output churn breaks exact-string tests.**
|
||||
- Mitigation: migrate in phases and update tests in the same commit; avoid changing machine outputs.
|
||||
|
||||
- **Risk: bundle-size increase from adding library + transitive time utilities.**
|
||||
- Mitigation: route usage through wrapper, prefer only needed functions, and measure `static/logseq-cli.js` size delta.
|
||||
|
||||
- **Risk: semantic drift in relative-time wording.**
|
||||
- Mitigation: pin wrapper options to compact short style and keep compatibility snapshots in `format_test.cljs`.
|
||||
@@ -4,6 +4,7 @@
|
||||
[clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.data-dir :as data-dir]
|
||||
[logseq.cli.humanize :as cli-humanize]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.version :as version]
|
||||
[promesa.core :as p]))
|
||||
@@ -108,9 +109,7 @@
|
||||
:status :warning
|
||||
:code :doctor-server-not-ready
|
||||
:servers starting
|
||||
:message (str (count starting)
|
||||
" server"
|
||||
(when (> (count starting) 1) "s")
|
||||
:message (str (cli-humanize/format-count-with-noun (count starting) "server")
|
||||
" still starting: "
|
||||
(string/join ", " (map :repo starting)))}}
|
||||
{:ok? true
|
||||
@@ -147,9 +146,7 @@
|
||||
:code :doctor-server-revision-mismatch
|
||||
:cli-revision cli-revision
|
||||
:servers mismatch-servers
|
||||
:message (str mismatch-count
|
||||
" server"
|
||||
(when (> mismatch-count 1) "s")
|
||||
:message (str (cli-humanize/format-count-with-noun mismatch-count "server")
|
||||
" "
|
||||
(if (= 1 mismatch-count) "uses" "use")
|
||||
" a different revision than this CLI")}}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Example command generation and execution."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.humanize :as cli-humanize]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private phase1-groups
|
||||
@@ -122,8 +123,10 @@
|
||||
:matched-commands matched-commands
|
||||
:examples examples
|
||||
:message (str "Found "
|
||||
(count examples)
|
||||
" examples for selector "
|
||||
(cli-humanize/format-count (count examples))
|
||||
" "
|
||||
(cli-humanize/pluralize-noun (count examples) "example")
|
||||
" for selector "
|
||||
selector)}})))
|
||||
|
||||
(defn execute-example
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.common :as cli-common]
|
||||
[logseq.cli.config :as cli-config]
|
||||
[logseq.cli.humanize :as cli-humanize]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
[logseq.common.graph :as common-graph]
|
||||
@@ -421,10 +422,13 @@
|
||||
|
||||
(defn- format-validation-errors
|
||||
[errors]
|
||||
(str "Graph invalid. Found " (count errors)
|
||||
(if (= 1 (count errors)) " entity" " entities")
|
||||
" with errors:\n"
|
||||
(with-out-str (pprint/pprint errors))))
|
||||
(let [error-count (count errors)]
|
||||
(str "Graph invalid. Found "
|
||||
(cli-humanize/format-count error-count)
|
||||
" "
|
||||
(cli-humanize/pluralize-noun error-count "entity")
|
||||
" with errors:\n"
|
||||
(with-out-str (pprint/pprint errors)))))
|
||||
|
||||
(defn- graph-validate-result
|
||||
[result]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[clojure.walk :as walk]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.command.id :as id-command]
|
||||
[logseq.cli.humanize :as cli-humanize]
|
||||
[logseq.cli.output-mode :as output-mode]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.style :as style]
|
||||
@@ -855,7 +856,9 @@
|
||||
(if (seq refs)
|
||||
(str tree-text
|
||||
"\n\n"
|
||||
"Linked References (" count ")\n"
|
||||
"Linked References ("
|
||||
(cli-humanize/format-count count)
|
||||
")\n"
|
||||
(linked-refs->text refs uuid->label property-titles property-value-labels))
|
||||
tree-text)))
|
||||
|
||||
@@ -870,7 +873,9 @@
|
||||
[ordered-uuids uuid->entity]
|
||||
(let [ordered-uuids (vec (distinct (remove string/blank? ordered-uuids)))]
|
||||
(when (seq ordered-uuids)
|
||||
(str "Referenced Entities (" (count ordered-uuids) ")\n"
|
||||
(str "Referenced Entities ("
|
||||
(cli-humanize/format-count (count ordered-uuids))
|
||||
")\n"
|
||||
(string/join "\n" (map #(referenced-entity-row % uuid->entity) ordered-uuids))))))
|
||||
|
||||
(defn build-action
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[clojure.string :as string]
|
||||
[clojure.walk :as walk]
|
||||
[logseq.cli.command.core :as command-core]
|
||||
[logseq.cli.humanize :as cli-humanize]
|
||||
[logseq.cli.output-mode :as output-mode]
|
||||
[logseq.cli.style :as style]
|
||||
[logseq.common.util :as common-util]
|
||||
@@ -162,7 +163,7 @@
|
||||
(str (render-table headers rows) "\n")
|
||||
(str (string/join "\n" (map (comp string/trimr first) rows)) "\n"))
|
||||
"Count: "
|
||||
(count rows)))
|
||||
(cli-humanize/format-count (count rows))))
|
||||
|
||||
(defn- missing-search-query-hint
|
||||
[command]
|
||||
@@ -222,22 +223,20 @@
|
||||
(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")))
|
||||
(cli-humanize/relative-ago ts now-ms)
|
||||
"-"))
|
||||
|
||||
(defn- format-task-datetime
|
||||
[value now-ms]
|
||||
(if-let [ts (parse-ts value)]
|
||||
(cli-humanize/relative-datetime ts now-ms)
|
||||
(if (string? value)
|
||||
(let [text (string/trim value)]
|
||||
(if (seq text)
|
||||
text
|
||||
"-"))
|
||||
"-")))
|
||||
|
||||
(defn- items-have-key?
|
||||
[items & ks]
|
||||
(some (fn [item] (some #(contains? item %) ks)) items))
|
||||
@@ -349,9 +348,9 @@
|
||||
[:status :logseq.property/status]]
|
||||
["PRIORITY" (fn [item _] (format-task-choice (or (:priority item) (:logseq.property/priority item)) "priority."))
|
||||
[:priority :logseq.property/priority]]
|
||||
["SCHEDULED" (fn [item _] (or (:scheduled item) (:logseq.property/scheduled item) "-"))
|
||||
["SCHEDULED" (fn [item now-ms] (format-task-datetime (or (:scheduled item) (:logseq.property/scheduled item)) now-ms))
|
||||
[:scheduled :logseq.property/scheduled]]
|
||||
["DEADLINE" (fn [item _] (or (:deadline item) (:logseq.property/deadline item) "-"))
|
||||
["DEADLINE" (fn [item now-ms] (format-task-datetime (or (:deadline item) (:logseq.property/deadline item)) now-ms))
|
||||
[:deadline :logseq.property/deadline]]
|
||||
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
|
||||
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
|
||||
@@ -395,7 +394,7 @@
|
||||
[["ID" (fn [item _] (or (:db/id item) (:id item))) [:db/id :id]]
|
||||
["TITLE" (fn [item _] (or (:title item) (:block/title item) (:name item))) [:title :block/title :name]]
|
||||
["ASSET-TYPE" (fn [item _] (normalize-asset-type (:logseq.property.asset/type item))) [:logseq.property.asset/type]]
|
||||
["SIZE" (fn [item _] (or (:logseq.property.asset/size item) "-")) [:logseq.property.asset/size]]
|
||||
["SIZE" (fn [item _] (cli-humanize/format-filesize (:logseq.property.asset/size item))) [:logseq.property.asset/size]]
|
||||
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at]]
|
||||
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at]]])
|
||||
|
||||
@@ -496,7 +495,12 @@
|
||||
base-output (format-counted-table nil rows)
|
||||
legacy-items (filterv legacy-graph-item? graph-items)]
|
||||
(if (seq legacy-items)
|
||||
(let [warning-lines (vec (concat [(str "Warning: " (count legacy-items) " legacy graph directories detected.")]
|
||||
(let [legacy-count (count legacy-items)
|
||||
warning-lines (vec (concat [(str "Warning: "
|
||||
(cli-humanize/format-count legacy-count)
|
||||
" legacy graph "
|
||||
(cli-humanize/pluralize-noun legacy-count "directory")
|
||||
" detected.")]
|
||||
(mapcat #(format-legacy-warning-lines % data-dir) legacy-items)))]
|
||||
(str base-output "\n\n" (string/join "\n" warning-lines)))
|
||||
base-output)))
|
||||
@@ -631,12 +635,12 @@
|
||||
skipped-owner-targets (vec (or skipped-owner-targets []))
|
||||
header-lines [(str "Server cleanup summary")
|
||||
(str "CLI revision: " (normalize-cell cli-revision))
|
||||
(str "Checked: " (or checked 0))
|
||||
(str "Mismatched: " (or mismatched 0))
|
||||
(str "Eligible (:cli owner): " (or eligible 0))
|
||||
(str "Skipped owner mismatch: " (or skipped-owner 0))
|
||||
(str "Killed: " (count (or killed [])))
|
||||
(str "Failed: " (count failed))]
|
||||
(str "Checked: " (cli-humanize/format-count (or checked 0)))
|
||||
(str "Mismatched: " (cli-humanize/format-count (or mismatched 0)))
|
||||
(str "Eligible (:cli owner): " (cli-humanize/format-count (or eligible 0)))
|
||||
(str "Skipped owner mismatch: " (cli-humanize/format-count (or skipped-owner 0)))
|
||||
(str "Killed: " (cli-humanize/format-count (count (or killed []))))
|
||||
(str "Failed: " (cli-humanize/format-count (count failed)))]
|
||||
skipped-lines (when (seq skipped-owner-targets)
|
||||
(into ["" "Skipped owner targets:"]
|
||||
(mapv format-server-cleanup-target skipped-owner-targets)))
|
||||
@@ -721,11 +725,15 @@
|
||||
(str "repo: " (or repo "-"))
|
||||
(str "graph-id: " (or graph-id "-"))
|
||||
(str "ws-state: " (or ws-state :unknown))
|
||||
(str "pending-local: " (or pending-local 0))
|
||||
(str "pending-asset: " (or pending-asset 0))
|
||||
(str "pending-server: " (or pending-server 0))
|
||||
(str "local-tx: " (or local-tx "-"))
|
||||
(str "remote-tx: " (or remote-tx "-"))]
|
||||
(str "pending-local: " (cli-humanize/format-count (or pending-local 0)))
|
||||
(str "pending-asset: " (cli-humanize/format-count (or pending-asset 0)))
|
||||
(str "pending-server: " (cli-humanize/format-count (or pending-server 0)))
|
||||
(str "local-tx: " (if (number? local-tx)
|
||||
(cli-humanize/format-count local-tx)
|
||||
"-"))
|
||||
(str "remote-tx: " (if (number? remote-tx)
|
||||
(cli-humanize/format-count remote-tx)
|
||||
"-"))]
|
||||
last-error-line (conj last-error-line)))))
|
||||
|
||||
(defn- format-sync-remote-graphs
|
||||
@@ -794,10 +802,10 @@
|
||||
(if (vector? result)
|
||||
(str "Upserted blocks:\n" (pr-str (vec (or result []))))
|
||||
(let [change-parts (cond-> []
|
||||
(seq update-tags) (conj (str "tags:+" (count update-tags)))
|
||||
(seq update-properties) (conj (str "properties:+" (count update-properties)))
|
||||
(seq remove-tags) (conj (str "remove-tags:+" (count remove-tags)))
|
||||
(seq remove-properties) (conj (str "remove-properties:+" (count remove-properties))))
|
||||
(seq update-tags) (conj (str "tags:+" (cli-humanize/format-count (count update-tags))))
|
||||
(seq update-properties) (conj (str "properties:+" (cli-humanize/format-count (count update-properties))))
|
||||
(seq remove-tags) (conj (str "remove-tags:+" (cli-humanize/format-count (count remove-tags))))
|
||||
(seq remove-properties) (conj (str "remove-properties:+" (cli-humanize/format-count (count remove-properties)))))
|
||||
changes (when (seq change-parts)
|
||||
(str ", " (string/join ", " change-parts)))
|
||||
move-fragment (when (seq target)
|
||||
@@ -828,7 +836,7 @@
|
||||
[{:keys [repo uuid id ids]}]
|
||||
(cond
|
||||
(seq uuid) (str "Removed block: " uuid " (repo: " repo ")")
|
||||
(seq ids) (str "Removed blocks: " (count ids) " (repo: " repo ")")
|
||||
(seq ids) (str "Removed blocks: " (cli-humanize/format-count (count ids)) " (repo: " repo ")")
|
||||
(some? id) (str "Removed block: " id " (repo: " repo ")")
|
||||
:else (str "Removed block (repo: " repo ")")))
|
||||
|
||||
|
||||
82
src/main/logseq/cli/humanize.cljs
Normal file
82
src/main/logseq/cli/humanize.cljs
Normal file
@@ -0,0 +1,82 @@
|
||||
(ns logseq.cli.humanize
|
||||
"CLI-local wrappers around clj-commons humanize helpers."
|
||||
(:require [clj-commons.humanize :as humanize]
|
||||
[clj-commons.humanize.inflect :as humanize-inflect]
|
||||
[clojure.string :as string]))
|
||||
|
||||
(def ^:private relative-unit->abbr
|
||||
{"second" "s"
|
||||
"minute" "m"
|
||||
"hour" "h"
|
||||
"day" "d"
|
||||
"week" "w"
|
||||
"month" "mo"
|
||||
"year" "y"})
|
||||
|
||||
(defn format-count
|
||||
[value]
|
||||
(let [n (if (number? value) value 0)]
|
||||
(humanize/intcomma (js/Math.floor n))))
|
||||
|
||||
(defn pluralize-noun
|
||||
[count noun]
|
||||
(humanize-inflect/pluralize-noun (or count 0) noun))
|
||||
|
||||
(defn format-count-with-noun
|
||||
[count noun]
|
||||
(str (format-count count)
|
||||
" "
|
||||
(pluralize-noun count noun)))
|
||||
|
||||
(defn format-filesize
|
||||
[byte-count]
|
||||
(if (number? byte-count)
|
||||
(humanize/filesize byte-count :binary true :format "%.1f")
|
||||
"-"))
|
||||
|
||||
(defn- unit->abbr
|
||||
[unit]
|
||||
(let [unit* (-> unit string/lower-case (string/replace #"s$" ""))]
|
||||
(get relative-unit->abbr unit* unit*)))
|
||||
|
||||
(defn- abbreviate-relative-datetime
|
||||
[result]
|
||||
(let [result (string/trim (or result ""))]
|
||||
(or (when-let [[_ n unit] (re-matches #"^(\d+)\s+([a-zA-Z]+)\s+ago$" result)]
|
||||
(str n (unit->abbr unit) " ago"))
|
||||
(when-let [[_ n unit] (re-matches #"^in\s+(\d+)\s+([a-zA-Z]+)$" result)]
|
||||
(str "in " n (unit->abbr unit)))
|
||||
(when (= "in 0s" result)
|
||||
result)
|
||||
result)))
|
||||
|
||||
(defn relative-datetime
|
||||
[then-ms now-ms]
|
||||
(cond
|
||||
(not (number? then-ms)) "-"
|
||||
(not (number? now-ms)) "-"
|
||||
(<= then-ms 0) "-"
|
||||
:else
|
||||
(-> (humanize/relative-datetime (js/Date. then-ms)
|
||||
:now-dt (js/Date. now-ms)
|
||||
:max-terms 1
|
||||
:number-format str
|
||||
:short-text "0s"
|
||||
:prefix "in"
|
||||
:suffix "ago")
|
||||
abbreviate-relative-datetime)))
|
||||
|
||||
(defn relative-ago
|
||||
[then-ms now-ms]
|
||||
(cond
|
||||
(not (number? then-ms)) "-"
|
||||
(not (number? now-ms)) "-"
|
||||
(<= then-ms 0) "-"
|
||||
(>= then-ms now-ms) "0s ago"
|
||||
:else
|
||||
(let [result (relative-datetime then-ms now-ms)]
|
||||
(if (string/starts-with? result "in ")
|
||||
"0s ago"
|
||||
result))))
|
||||
|
||||
|
||||
@@ -174,4 +174,32 @@
|
||||
(is (string/ends-with? checked-path "/dist/db-worker-node.js"))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done))))
|
||||
(p/finally done))))
|
||||
|
||||
(deftest test-check-running-servers-formats-large-count
|
||||
(async done
|
||||
(let [servers (mapv (fn [idx]
|
||||
{:repo (str "logseq_db_graph-" idx)
|
||||
:status :starting})
|
||||
(range 1234))]
|
||||
(-> (p/with-redefs [cli-server/list-servers (fn [_] (p/resolved servers))]
|
||||
(p/let [result (#'doctor-command/check-running-servers {})
|
||||
message (get-in result [:check :message])]
|
||||
(is (= true (:warning? result)))
|
||||
(is (string/includes? message "1,234 servers still starting:"))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-check-server-revision-mismatch-formats-large-count
|
||||
(let [mismatch-servers (mapv (fn [idx]
|
||||
{:repo (str "logseq_db_graph-" idx)
|
||||
:revision "worker-rev"})
|
||||
(range 1234))
|
||||
result (with-redefs [cli-server/compute-revision-mismatches
|
||||
(fn [_ _]
|
||||
{:servers mismatch-servers})]
|
||||
(#'doctor-command/check-server-revision-mismatch "cli-rev" []))]
|
||||
(is (= true (:warning? result)))
|
||||
(is (string/includes? (get-in result [:check :message])
|
||||
"1,234 servers use a different revision than this CLI"))))
|
||||
@@ -80,3 +80,13 @@
|
||||
result (example-command/build-action mock-base-table ["example" "upsert"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-examples (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-build-action-formats-large-example-count
|
||||
(let [mock-base-table [{:cmds ["upsert" "page"]
|
||||
:examples (mapv (fn [idx]
|
||||
(str "logseq upsert page --graph demo --page Page-" idx))
|
||||
(range 1234))}]
|
||||
result (example-command/build-action mock-base-table ["example" "upsert" "page"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= "Found 1,234 examples for selector upsert page"
|
||||
(get-in result [:action :message])))))
|
||||
|
||||
@@ -22,6 +22,17 @@
|
||||
"Found 1 entity with errors:"))
|
||||
(is (= :ok (:status valid-result)))))
|
||||
|
||||
(deftest test-graph-validate-result-formats-large-error-count
|
||||
(let [graph-validate-result #'graph-command/graph-validate-result
|
||||
errors (mapv (fn [idx]
|
||||
{:entity {:db/id idx}
|
||||
:errors {:foo ["bad"]}})
|
||||
(range 1234))
|
||||
invalid-result (graph-validate-result {:errors errors})]
|
||||
(is (= :error (:status invalid-result)))
|
||||
(is (string/includes? (get-in invalid-result [:error :message])
|
||||
"Found 1,234 entities with errors:"))))
|
||||
|
||||
(deftest test-execute-graph-info-queries-kv-rows-with-thread-api-q
|
||||
(async done
|
||||
(let [invoke-calls* (atom [])
|
||||
|
||||
@@ -236,6 +236,16 @@
|
||||
{(string/lower-case u1) {:label "Broken ref"}
|
||||
(string/lower-case u2) {:id 88}}))))))
|
||||
|
||||
(deftest test-render-referenced-entities-footer-formats-large-count
|
||||
(let [render-footer (fn [ordered-uuids uuid->entity]
|
||||
(call-private 'render-referenced-entities-footer ordered-uuids uuid->entity))
|
||||
ordered-uuids (mapv (fn [idx]
|
||||
(str "uuid-" idx))
|
||||
(range 1234))
|
||||
uuid->entity {}
|
||||
output (render-footer ordered-uuids uuid->entity)]
|
||||
(is (string/includes? output "Referenced Entities (1,234)"))))
|
||||
|
||||
(deftest test-build-action-ref-id-footer
|
||||
(testing "ref-id-footer defaults to true"
|
||||
(let [result (show-command/build-action {:id "42"}
|
||||
|
||||
@@ -177,7 +177,30 @@
|
||||
:conflict? false}]}}
|
||||
{:output-format nil
|
||||
:data-dir "/tmp/graphs"})]
|
||||
(is (string/includes? result "mv '/tmp/graphs/weird'\"'\"'++name' '/tmp/graphs/weird~27~2Fname'")))))
|
||||
(is (string/includes? result "mv '/tmp/graphs/weird'\"'\"'++name' '/tmp/graphs/weird~27~2Fname'"))))
|
||||
|
||||
)
|
||||
|
||||
(deftest test-human-output-graph-list-count-grouping
|
||||
(let [graphs (mapv #(str "graph-" %) (range 1234))
|
||||
result (format/format-result {:status :ok
|
||||
:command :graph-list
|
||||
:data {:graphs graphs}}
|
||||
{:output-format nil})]
|
||||
(is (string/includes? result "Count: 1,234"))))
|
||||
|
||||
(deftest test-human-output-graph-list-legacy-warning-singular
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :graph-list
|
||||
:data {:graphs ["legacy/name"]
|
||||
:graph-items [{:kind :legacy
|
||||
:legacy-dir "legacy++name"
|
||||
:legacy-graph-name "legacy/name"
|
||||
:target-graph-dir "legacy~2Fname"
|
||||
:conflict? false}]}}
|
||||
{:output-format nil
|
||||
:data-dir "/tmp/graphs"})]
|
||||
(is (string/includes? result "Warning: 1 legacy graph directory detected."))))
|
||||
|
||||
(deftest test-human-output-list-page
|
||||
(testing "list page renders a table with count"
|
||||
@@ -265,7 +288,26 @@
|
||||
(is (string/includes? result "SCHEDULED"))
|
||||
(is (string/includes? result "DEADLINE"))
|
||||
(is (string/includes? result "Alpha task"))
|
||||
(is (string/includes? result "Count: 1"))))
|
||||
(is (string/includes? result "Count: 1")))
|
||||
|
||||
(testing "list task renders epoch-ms scheduled/deadline with humanized relative datetime"
|
||||
(let [now-ms 1000000000
|
||||
scheduled-ms (+ now-ms (* 3 60 60 1000))
|
||||
deadline-ms (- now-ms (* 2 24 60 60 1000))
|
||||
result (format/format-result {:status :ok
|
||||
:command :list-task
|
||||
:data {:items [{:db/id 226
|
||||
:block/title "q3"
|
||||
:logseq.property/status :logseq.property/status.todo
|
||||
:logseq.property/priority :logseq.property/priority.high
|
||||
:logseq.property/scheduled scheduled-ms
|
||||
:logseq.property/deadline deadline-ms}]}}
|
||||
{:output-format nil
|
||||
:now-ms now-ms})]
|
||||
(is (string/includes? result "in "))
|
||||
(is (string/includes? result "ago"))
|
||||
(is (not (string/includes? result (str scheduled-ms))))
|
||||
(is (not (string/includes? result (str deadline-ms)))))))
|
||||
|
||||
(deftest test-human-output-list-node
|
||||
(let [result (format/format-result {:status :ok
|
||||
@@ -306,11 +348,18 @@
|
||||
:block/updated-at 90000}]}}
|
||||
{:output-format nil
|
||||
:now-ms 100000})
|
||||
lines (string/split-lines result)]
|
||||
(is (= "ID TITLE ASSET-TYPE SIZE UPDATED-AT CREATED-AT" (first lines)))
|
||||
lines (string/split-lines result)
|
||||
header (first lines)]
|
||||
(is (string/includes? header "ID"))
|
||||
(is (string/includes? header "TITLE"))
|
||||
(is (string/includes? header "ASSET-TYPE"))
|
||||
(is (string/includes? header "SIZE"))
|
||||
(is (string/includes? header "UPDATED-AT"))
|
||||
(is (string/includes? header "CREATED-AT"))
|
||||
(is (string/includes? result "Asset Node"))
|
||||
(is (string/includes? result "md"))
|
||||
(is (string/includes? result "2552"))
|
||||
(is (not (string/includes? result "2552")))
|
||||
(is (some? (re-find #"\b2(\.\d+)?\s*[KM]i?B\b" result)))
|
||||
(is (not (string/includes? (first lines) " TYPE ")))
|
||||
(is (not (string/includes? result "PAGE-ID")))
|
||||
(is (not (string/includes? result "PAGE-TITLE")))
|
||||
@@ -530,6 +579,13 @@
|
||||
:ids [1 2 3]}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed blocks: 3 (repo: demo-repo)"]
|
||||
["remove block with large id list uses grouped count"
|
||||
{:status :ok
|
||||
:command :remove-block
|
||||
:context {:repo "demo-repo"
|
||||
:ids (vec (range 1234))}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed blocks: 1,234 (repo: demo-repo)"]
|
||||
["remove tag renders a succinct success line"
|
||||
{:status :ok
|
||||
:command :remove-tag
|
||||
@@ -559,6 +615,14 @@
|
||||
:remove-tags ["TagB"]
|
||||
:remove-properties [:logseq.property/deadline]}
|
||||
"Upserted block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)"]
|
||||
["upsert block update uses grouped counts for large changes"
|
||||
{:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
:update-tags (vec (range 1234))
|
||||
:update-properties (zipmap (map #(keyword (str "prop-" %)) (range 1234)) (repeat true))
|
||||
:remove-tags (vec (range 1234))
|
||||
:remove-properties (mapv #(keyword (str "remove-" %)) (range 1234))}
|
||||
"Upserted block: source-uuid (repo: demo-repo, tags:+1,234, properties:+1,234, remove-tags:+1,234, remove-properties:+1,234)"]
|
||||
["upsert block update without move target renders a succinct success line"
|
||||
{:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
@@ -657,18 +721,20 @@
|
||||
:data {:repo "demo-graph"
|
||||
:graph-id "graph-uuid"
|
||||
:ws-state :open
|
||||
:pending-local 2
|
||||
:pending-asset 1
|
||||
:pending-server 3
|
||||
:local-tx 10
|
||||
:remote-tx 13}}
|
||||
:pending-local 2345
|
||||
:pending-asset 1234
|
||||
:pending-server 9876
|
||||
:local-tx 12345
|
||||
:remote-tx 67890}}
|
||||
{:output-format nil})]
|
||||
(is (string/includes? result "Sync status"))
|
||||
(is (string/includes? result "demo-graph"))
|
||||
(is (string/includes? result "graph-uuid"))
|
||||
(is (string/includes? result "pending-local"))
|
||||
(is (string/includes? result "pending-asset"))
|
||||
(is (string/includes? result "pending-server"))))
|
||||
(is (string/includes? result "pending-local: 2,345"))
|
||||
(is (string/includes? result "pending-asset: 1,234"))
|
||||
(is (string/includes? result "pending-server: 9,876"))
|
||||
(is (string/includes? result "local-tx: 12,345"))
|
||||
(is (string/includes? result "remote-tx: 67,890"))))
|
||||
|
||||
(testing "sync status renders last error diagnostic when present"
|
||||
(let [result (format/format-result {:status :ok
|
||||
@@ -900,18 +966,20 @@
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :server-cleanup
|
||||
:data {:cli-revision "cli-rev"
|
||||
:checked 4
|
||||
:mismatched 3
|
||||
:eligible 2
|
||||
:skipped-owner 1
|
||||
:checked 4321
|
||||
:mismatched 3210
|
||||
:eligible 2100
|
||||
:skipped-owner 1111
|
||||
:skipped-owner-targets [{:repo "logseq_db_graph-b"
|
||||
:pid 22
|
||||
:owner-source :electron
|
||||
:revision "worker-rev-b"}]
|
||||
:killed [{:repo "logseq_db_graph-a"
|
||||
:pid 11
|
||||
:owner-source :cli
|
||||
:revision "worker-rev-a"}]
|
||||
:killed (mapv (fn [idx]
|
||||
{:repo (str "logseq_db_graph-killed-" idx)
|
||||
:pid (+ 1000 idx)
|
||||
:owner-source :cli
|
||||
:revision "worker-rev-a"})
|
||||
(range 1234))
|
||||
:failed [{:repo "logseq_db_graph-c"
|
||||
:pid 33
|
||||
:owner-source :cli
|
||||
@@ -921,11 +989,11 @@
|
||||
{:output-format nil})]
|
||||
(is (string/includes? result "Server cleanup summary"))
|
||||
(is (string/includes? result "CLI revision: cli-rev"))
|
||||
(is (string/includes? result "Checked: 4"))
|
||||
(is (string/includes? result "Mismatched: 3"))
|
||||
(is (string/includes? result "Eligible (:cli owner): 2"))
|
||||
(is (string/includes? result "Skipped owner mismatch: 1"))
|
||||
(is (string/includes? result "Killed: 1"))
|
||||
(is (string/includes? result "Checked: 4,321"))
|
||||
(is (string/includes? result "Mismatched: 3,210"))
|
||||
(is (string/includes? result "Eligible (:cli owner): 2,100"))
|
||||
(is (string/includes? result "Skipped owner mismatch: 1,111"))
|
||||
(is (string/includes? result "Killed: 1,234"))
|
||||
(is (string/includes? result "Failed: 1"))
|
||||
(is (string/includes? result "Skipped owner targets:"))
|
||||
(is (string/includes? result "graph-b"))
|
||||
|
||||
Reference in New Issue
Block a user