diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index 119fcf3b51..72dbeb1f02 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -556,6 +556,57 @@ :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] :tags [:show :pipe]} + {:id "debug-pull-id-json" + :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] + :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] + :expect {:exit 0 + :stdout-json-paths {[:status] "ok" + [:data :entity :block/title] "Home"}} + :covers {:commands ["debug pull"] + :options {:global ["--config" "--graph" "--data-dir" "--output"] + :debug ["--id"]}} + :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] + :tags [:debug]} + + {:id "debug-pull-uuid-json" + :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] + :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --uuid \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"Home\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] + :expect {:exit 0 + :stdout-json-paths {[:status] "ok" + [:data :entity :block/title] "Home"}} + :covers {:commands ["debug pull"] + :options {:global ["--config" "--graph" "--data-dir" "--output"] + :debug ["--uuid"]}} + :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] + :tags [:debug]} + + {:id "debug-pull-ident-json" + :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"] + :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --graph {{graph-arg}} --ident :logseq.class/Tag"] + :expect {:exit 0 + :stdout-json-paths {[:status] "ok" + [:data :entity :db/ident] "logseq.class/Tag"}} + :covers {:commands ["debug pull"] + :options {:global ["--config" "--graph" "--data-dir" "--output"] + :debug ["--ident"]}} + :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] + :tags [:debug]} + + {:id "debug-pull-current-graph-fallback-json" + :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" + "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] + :cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json debug pull --id \"$({{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?e . :where [?e :block/title \"Home\"]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')\""] + :expect {:exit 0 + :stdout-json-paths {[:status] "ok" + [:data :entity :block/title] "Home"}} + :covers {:commands ["debug pull"] + :options {:global ["--config" "--data-dir" "--output"] + :debug ["--id"]}} + :cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"] + :tags [:debug]} + {:id "remove-page-json" :setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null" "{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"] diff --git a/cli-e2e/spec/non_sync_inventory.edn b/cli-e2e/spec/non_sync_inventory.edn index 454991b011..5cbe543f97 100644 --- a/cli-e2e/spec/non_sync_inventory.edn +++ b/cli-e2e/spec/non_sync_inventory.edn @@ -103,6 +103,12 @@ "--level" "stdin:--id"]} + :debug + {:commands ["debug pull"] + :options ["--id" + "--uuid" + "--ident"]} + :example {:commands ["example list" "example list page" diff --git a/docs/agent-guide/073-logseq-cli-debug-command.md b/docs/agent-guide/073-logseq-cli-debug-command.md new file mode 100644 index 0000000000..c7e8fbdf6d --- /dev/null +++ b/docs/agent-guide/073-logseq-cli-debug-command.md @@ -0,0 +1,258 @@ +# Logseq CLI `debug` Command Implementation Plan + +Goal: Add a new `debug` command group with `debug pull` for low-level Datascript entity inspection via `db-worker-node`. + +Goal: `debug pull` must support `--graph ` or default to current graph resolution already used by existing commands. + +Goal: In global help (`logseq --help`), show `debug` under `Utilities` and do not show `debug` subcommands (same top-level-only behavior as `example`). + +Architecture: Keep command parsing, validation, and output behavior in `logseq-cli`, and reuse existing `db-worker-node` invoke API `:thread-api/pull` with selector `[*]`. + +Architecture: Avoid adding new worker thread APIs unless we discover a hard blocker; current worker already exposes `:thread-api/pull` and supports lookup refs. + +Tech Stack: ClojureScript, `babashka.cli` dispatch table, existing command pipeline in `logseq.cli.commands` (`parse-args -> build-action -> execute`), and existing CLI e2e spec harness. + +Related: +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/id.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/transport.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` +- `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/completion_generator_test.cljs` +- `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn` +- `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` + +## Problem statement + +Today we have user-facing inspection commands (`show`, `query`), but no direct debug command that exposes a raw `pull` for a single entity selector path. + +For debugging schema/data issues, we need a predictable command that can fetch one entity using either db id, uuid, or ident and return raw entity data. + +The requested command is: + +```text +logseq debug pull --id +logseq debug pull --uuid +logseq debug pull --ident +``` + +with selector fixed to `[*]` and data source being the current graph DB served by `db-worker-node`. + +## Requested behavior contract + +| Area | Requirement | +| --- | --- | +| Command shape | Add top-level `debug` group with subcommand `pull`. | +| Graph selection | Accept `--graph` globally; if omitted, use current graph resolution (same as existing commands using `pick-graph`). | +| Target selector | `debug pull` accepts exactly one of `--id`, `--uuid`, `--ident`. | +| Pull behavior | Execute `:thread-api/pull` with selector `[*]` and resolved lookup value. | +| Global help | `logseq --help` shows `debug` in `Utilities` and does not list `debug pull` there. | +| Group help | `logseq debug` and `logseq debug --help` show `debug pull` as subcommand. | + +## Current baseline + +- Top-level help grouping is implemented in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` with `Utilities` currently configured as top-level-only and already hiding `example` subcommands. +- Graph fallback is implemented centrally via `logseq.cli.command.core/pick-graph` and is already used in `build-action` for graph-scoped commands. +- `db-worker-node` already provides `:thread-api/pull` in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs`: + - accepts `(repo selector id)` + - supports integer ids and lookup refs (e.g. `[:db/ident kw]`, `[:block/uuid uuid]`) +- Existing command modules (`show`, `query`) already demonstrate ensure-server + invoke flow and can be reused as implementation patterns. + +Conclusion: this feature should be mostly CLI-side wiring; worker protocol changes are likely unnecessary. + +## CLI design proposal + +### 1) New command namespace + +Add `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/debug.cljs`. + +Planned entry: +- `debug pull` (`:debug-pull`) + +Planned command options: +- `--id` (db id) +- `--uuid` (UUID string) +- `--ident` (strict EDN keyword string, e.g. `:logseq.class/Tag`) + +Global options (`--graph`, `--output`, etc.) continue to come from `command-entry` merged global spec. + +### 2) Selector normalization + +`debug pull` resolves exactly one lookup target: +- `--id` -> numeric entity id +- `--uuid` -> lookup ref `[:block/uuid ]` +- `--ident` -> lookup ref `[:db/ident ]` + +Validation rules: +- exactly one of `--id`, `--uuid`, `--ident` is required +- reject multiple selectors with clear invalid-options message +- reject invalid uuid / invalid id / invalid ident formats +- `--ident` accepts only strict EDN keyword syntax with leading `:` + +### 3) Action model + +`build-action` output shape: + +```clojure +{:type :debug-pull + :repo + :lookup + :selector '[*]} +``` + +`repo` resolution should reuse existing command pipeline behavior: +- `--graph` first +- then config current graph +- then repo fallback from config + +### 4) Execute model + +Execution pattern: +1. ensure server for repo (`cli-server/ensure-server!`) +2. invoke worker `:thread-api/pull` with `[repo selector lookup]` +3. return structured data payload (include selector metadata for traceability) + +Suggested success payload: + +```clojure +{:entity + :lookup + :selector '[*]} +``` + +For missing entity, return a clear typed error (e.g. `:entity-not-found`) instead of silently returning `nil`. + +## Global help behavior plan + +Update `top-level-summary` groups in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs`: + +- add `debug` to `Utilities` command set +- keep `:top-level-only? true` for `Utilities` +- add `desc-overrides` entry for `debug` so top-level help renders stable one-line description (instead of inheriting from `debug pull` text) + +Expected top-level help effect: +- shows `debug` +- does not show `debug pull` +- remains consistent with current `example` visibility behavior + +## Testing Plan (TDD) + +I will follow `@test-driven-development` and write failing tests first. + +### Phase 1: Parser and help RED tests + +1. Extend `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs`: + - top-level help includes `debug` + - top-level help does **not** include `debug pull` + - `Utilities` section still lists `example` and `completion` +2. Add group-help tests: + - `parse-args ["debug"]` returns help summary containing `debug pull` +3. Add parse success tests: + - `debug pull --id 1` + - `debug pull --uuid ` + - `debug pull --ident :logseq.class/Tag` +4. Add parse failure tests: + - no selector + - multiple selectors + - malformed id / uuid / ident + +### Phase 2: Action and execute RED tests + +5. Add build-action tests in `commands_test.cljs`: + - uses `--graph` when provided + - falls back to current graph from config when `--graph` omitted + - returns missing-repo when neither explicit nor current graph exists +6. Add execute tests (new file suggested: `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/debug_test.cljs`): + - verifies invoke contract `:thread-api/pull` with selector `[*]` + - verifies id/uuid/ident normalization to lookup argument + - verifies entity-not-found behavior + +### Phase 3: GREEN implementation + +7. Implement new namespace `command/debug.cljs` with entries + build + execute. +8. Wire into `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs`: + - include `debug-command/entries` in base table + - finalize-command validation branch for selector constraints + - build-action case `:debug-pull` + - execute case `:debug-pull` +9. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` top-level group config for `Utilities` to include `debug` (hidden subcommands at global help). + +### Phase 4: Completion and e2e coverage + +10. Update completion tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/completion_generator_test.cljs`: + - includes `debug` group and `pull` subcommand completion. +11. Update inventory in `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn`: + - add `:debug` scope with command `debug pull` + - add options `--id`, `--uuid`, `--ident` +12. Add e2e cases in `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn`: + - id-based pull + - uuid-based pull + - ident-based pull + - one case proving current-graph fallback (no `--graph` on debug call after config/graph setup) + +### Phase 5: Formatter and docs + +13. Add explicit human formatting branch in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` for `:debug-pull`: + - pretty-print entity as EDN for readable debugging output + - include compact header context (selector and lookup) +14. Update `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` with `debug pull` usage and examples. + +## File-by-file change map + +| File | Planned change | +| --- | --- | +| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/debug.cljs` | New command namespace (`entries`, validation helpers, `build-action`, `execute-debug-pull`). | +| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs` | Register debug entries; add parse/finalize validation, build-action route, and execute dispatch. | +| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/core.cljs` | Add `debug` under `Utilities` top-level-only group and description override to hide subcommands in global help. | +| `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` | Add explicit human formatter for debug output with pretty-printed EDN entity payload. | +| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs` | Add parser/help/build coverage for debug command and utilities help visibility rules. | +| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/debug_test.cljs` | New execute/normalization tests for debug pull invoke contract. | +| `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/completion_generator_test.cljs` | Add completion coverage for `debug pull`. | +| `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn` | Add debug scope and option coverage metadata. | +| `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` | Add non-sync end-to-end cases for id/uuid/ident and graph fallback behavior. | +| `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` | Document `debug pull` usage and selector examples. | + +## db-worker-node impact + +No new worker API is required for the requested scope. + +Reason: +- `:thread-api/pull` already exists and already performs Datascript `d/pull`. +- selector `[*]` is legal and directly maps to requested debug behavior. +- lookup refs for `:db/ident` and `:block/uuid` are already supported by Datascript pull semantics. + +Potential worker-side follow-up is only needed if we later want: +- multi-entity pull +- custom selectors from CLI input +- stricter nil/not-found error signaling at worker layer + +## Edge cases to explicitly cover + +| Scenario | Expected behavior | +| --- | --- | +| `debug pull` with no selector flags | Error: exactly one selector required. | +| `debug pull --id 1 --uuid ...` | Error: only one of `--id`, `--uuid`, `--ident` allowed. | +| `debug pull --ident logseq.class/Tag` (without leading `:`) | Error with explicit invalid ident message (`--ident` requires strict EDN keyword). | +| `debug pull` without `--graph` and no current graph configured | Error: missing repo/graph. | +| entity not found | Return typed error (`:entity-not-found`) instead of ambiguous success with nil. | +| `logseq --help` | Shows `debug` under Utilities, not `debug pull`. | + +## Verification commands + +```bash +bb dev:test -v logseq.cli.commands-test +bb dev:test -v logseq.cli.command.debug-test +bb dev:test -v logseq.cli.completion-generator-test +bb -f cli-e2e/bb.edn test --skip-build +bb dev:lint-and-test +``` + +Expected outcome: parser/help behavior, invoke contract, completion, and e2e coverage all green. + +## Resolved decisions + +1. `--ident` accepts only strict EDN keyword input. +2. Not-found is a hard error (`:entity-not-found`). +3. Human output for `debug pull` uses pretty-printed EDN. \ No newline at end of file diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index f79906be9b..040a26ab6d 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -118,6 +118,14 @@ Auth commands: - `login` - authenticate this machine and create/update `~/logseq/auth.json` - `logout` - remove persisted CLI auth from `~/logseq/auth.json` +Debug commands: +- `debug pull --id [--graph ]` - pull a raw entity by db id with selector `[*]` +- `debug pull --uuid [--graph ]` - pull a raw entity by block UUID lookup +- `debug pull --ident [--graph ]` - pull a raw entity by `:db/ident` lookup + - `--ident` must be a strict EDN keyword (for example `:logseq.class/Tag`) + - exactly one of `--id`, `--uuid`, or `--ident` is required + - if `--graph` is omitted, CLI falls back to current graph config + Shell completion and examples: - `completion ` - generate shell completion script to stdout - `example ` - show runnable command examples for a command path or command prefix (phase 1 covers Graph Inspect and Edit commands) @@ -338,6 +346,7 @@ node ./dist/logseq.js upsert block --target-page TestPage --content "hello world node ./dist/logseq.js move --uuid --target-page TargetPage node ./dist/logseq.js search block --content "hello" node ./dist/logseq.js show --page TestPage --output json +node ./dist/logseq.js debug pull --graph demo --ident :logseq.class/Tag --output json node ./dist/logseq.js server list node ./dist/logseq.js doctor node ./dist/logseq.js doctor --dev-script diff --git a/src/main/logseq/cli/command/core.cljs b/src/main/logseq/cli/command/core.cljs index ed812de446..5c6b0fbd88 100644 --- a/src/main/logseq/cli/command/core.cljs +++ b/src/main/logseq/cli/command/core.cljs @@ -121,9 +121,10 @@ {:title "Authentication" :commands #{"login" "logout"}} {:title "Utilities" - :commands #{"completion" "example"} + :commands #{"completion" "debug" "example"} :top-level-only? true - :desc-overrides {"example" "Show command examples"}}] + :desc-overrides {"debug" "Pull raw entity data for debugging" + "example" "Show command examples"}}] to-top-level-entries (fn [entries commands desc-overrides] (->> commands sort diff --git a/src/main/logseq/cli/command/debug.cljs b/src/main/logseq/cli/command/debug.cljs new file mode 100644 index 0000000000..d6f3d47997 --- /dev/null +++ b/src/main/logseq/cli/command/debug.cljs @@ -0,0 +1,97 @@ +(ns logseq.cli.command.debug + "Debug-related CLI commands." + (:require [clojure.string :as string] + [logseq.cli.command.core :as core] + [logseq.cli.server :as cli-server] + [logseq.cli.transport :as transport] + [logseq.common.util :as common-util] + [promesa.core :as p])) + +(def ^:private debug-pull-spec + {:id {:desc "Entity db/id" + :coerce :long} + :uuid {:desc "Entity UUID" + :validate {:pred (comp parse-uuid str) + :ex-msg (constantly "Option uuid must be a valid UUID string")}} + :ident {:desc "Entity db/ident as strict EDN keyword"}}) + +(def entries + [(core/command-entry ["debug" "pull"] + :debug-pull + "Pull raw entity by id, uuid, or ident" + debug-pull-spec + {:examples ["logseq debug pull --graph my-graph --id 123" + "logseq debug pull --graph my-graph --uuid 11111111-1111-1111-1111-111111111111" + "logseq debug pull --graph my-graph --ident :logseq.class/Tag"]})]) + +(defn- parse-ident-option + [value] + (let [text (some-> value str string/trim) + parsed (when (seq text) + (common-util/safe-read-string {:log-error? false} text))] + (if (keyword? parsed) + {:ok? true :value parsed} + {:ok? false + :error {:code :invalid-options + :message "ident must be a strict EDN keyword (e.g. :logseq.class/Tag)"}}))) + +(defn invalid-options? + [opts] + (let [selectors (filter some? [(:id opts) + (some-> (:uuid opts) string/trim seq) + (some-> (:ident opts) str string/trim seq)]) + ident-text (some-> (:ident opts) str string/trim) + ident-result (when (seq ident-text) + (parse-ident-option ident-text))] + (cond + (empty? selectors) + "exactly one of --id, --uuid, or --ident is required" + + (> (count selectors) 1) + "only one of --id, --uuid, or --ident is allowed" + + (and ident-result (not (:ok? ident-result))) + (get-in ident-result [:error :message]) + + :else + nil))) + +(defn build-action + [options repo] + (if-not (seq repo) + {:ok? false + :error {:code :missing-repo + :message "repo is required for debug pull"}} + (let [uuid-text (some-> (:uuid options) string/trim) + ident-text (some-> (:ident options) str string/trim) + ident-result (when (seq ident-text) + (parse-ident-option ident-text))] + (cond + (and ident-result (not (:ok? ident-result))) + ident-result + + :else + {:ok? true + :action {:type :debug-pull + :repo repo + :graph (core/repo->graph repo) + :lookup (cond + (some? (:id options)) (:id options) + (seq uuid-text) [:block/uuid (uuid uuid-text)] + (seq ident-text) [:db/ident (:value ident-result)] + :else nil) + :selector '[*]}})))) + +(defn execute-debug-pull + [action config] + (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) + entity (transport/invoke cfg :thread-api/pull false + [(:repo action) (:selector action) (:lookup action)])] + (if (some? entity) + {:status :ok + :data {:entity entity + :lookup (:lookup action) + :selector (:selector action)}} + {:status :error + :error {:code :entity-not-found + :message "entity not found"}})))) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 3c25f7fcfe..47a1ab55aa 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -5,6 +5,7 @@ [logseq.cli.command.auth :as auth-command] [logseq.cli.command.completion :as completion-command] [logseq.cli.command.core :as command-core] + [logseq.cli.command.debug :as debug-command] [logseq.cli.command.doctor :as doctor-command] [logseq.cli.command.example :as example-command] [logseq.cli.command.graph :as graph-command] @@ -130,6 +131,7 @@ search-command/entries show-command/entries doctor-command/entries + debug-command/entries sync-command/entries auth-command/entries completion-command/entries))) @@ -293,6 +295,9 @@ (and (= command :show) (show-command/invalid-options? opts)) (command-core/invalid-options-result summary (show-command/invalid-options? opts)) + (and (= command :debug-pull) (debug-command/invalid-options? opts)) + (command-core/invalid-options-result summary (debug-command/invalid-options? opts)) + (and (= command :graph-export) (not (seq (graph-command/normalize-import-export-type (:type opts))))) (missing-type-result summary) @@ -538,6 +543,9 @@ :show (show-command/build-action options repo) + :debug-pull + (debug-command/build-action options repo) + :doctor (doctor-command/build-action options) @@ -605,6 +613,7 @@ :query (query-command/execute-query action config) :query-list (query-command/execute-query-list action config) :show (show-command/execute-show action config) + :debug-pull (debug-command/execute-debug-pull action config) :doctor (doctor-command/execute-doctor action config) :completion (p/resolved {:status :ok @@ -628,7 +637,7 @@ (assoc result :command (or (:command action) (:type action)) :context (select-keys action [:repo :graph :page :name :id :ids :uuid :block :blocks - :schema :query + :schema :query :lookup :selector :source :target :update-tags :update-properties :remove-tags :remove-properties :src :dst :backup-name diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index e44ebd9ebc..de2236f43d 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -1,6 +1,7 @@ (ns logseq.cli.format "Formatting helpers for CLI output." - (:require [clojure.string :as string] + (:require [cljs.pprint :as pprint] + [clojure.string :as string] [clojure.walk :as walk] [logseq.cli.command.core :as command-core] [logseq.cli.style :as style] @@ -417,6 +418,15 @@ (or doc "-")]) (or queries [])))) +(defn- format-debug-pull + [{:keys [entity lookup selector]}] + (let [header [(str "Selector: " (pr-str selector)) + (str "Lookup: " (pr-str lookup)) + "Entity:"] + entity* (-> (with-out-str (pprint/pprint entity)) + string/trimr)] + (string/join "\n" (conj header entity*)))) + (defn- format-example [{:keys [selector matched-commands examples message]}] (let [selector (or selector "-") @@ -782,6 +792,7 @@ :query-list (format-query-list (:queries data)) :example (format-example data) :show (or (:message data) (pr-str data)) + :debug-pull (format-debug-pull data) :doctor (format-doctor (:status data) (:checks data)) (if (and (map? data) (contains? data :message)) (:message data) diff --git a/src/test/logseq/cli/command/debug_test.cljs b/src/test/logseq/cli/command/debug_test.cljs new file mode 100644 index 0000000000..e19bedce32 --- /dev/null +++ b/src/test/logseq/cli/command/debug_test.cljs @@ -0,0 +1,83 @@ +(ns logseq.cli.command.debug-test + (:require [cljs.test :refer [async deftest is testing]] + [logseq.cli.command.debug :as debug-command] + [logseq.cli.server :as cli-server] + [logseq.cli.transport :as transport] + [promesa.core :as p])) + +(deftest test-build-action + (testing "builds action from --id selector" + (let [result (debug-command/build-action {:id 42} "logseq_db_demo")] + (is (true? (:ok? result))) + (is (= :debug-pull (get-in result [:action :type]))) + (is (= "logseq_db_demo" (get-in result [:action :repo]))) + (is (= 42 (get-in result [:action :lookup]))) + (is (= '[*] (get-in result [:action :selector]))))) + + (testing "builds action from --uuid selector" + (let [result (debug-command/build-action {:uuid "11111111-1111-1111-1111-111111111111"} + "logseq_db_demo") + lookup (get-in result [:action :lookup])] + (is (true? (:ok? result))) + (is (= :block/uuid (first lookup))) + (is (uuid? (second lookup))) + (is (= "11111111-1111-1111-1111-111111111111" (str (second lookup)))))) + + (testing "builds action from --ident selector" + (let [result (debug-command/build-action {:ident ":logseq.class/Tag"} + "logseq_db_demo")] + (is (true? (:ok? result))) + (is (= [:db/ident :logseq.class/Tag] + (get-in result [:action :lookup]))))) + + (testing "rejects invalid ident" + (let [result (debug-command/build-action {:ident "logseq.class/Tag"} + "logseq_db_demo")] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "requires repo" + (let [result (debug-command/build-action {:id 1} nil)] + (is (false? (:ok? result))) + (is (= :missing-repo (get-in result [:error :code])))))) + +(deftest test-execute-debug-pull + (testing "invokes :thread-api/pull and returns entity" + (async done + (let [invoke-calls* (atom []) + action {:type :debug-pull + :repo "logseq_db_demo" + :lookup 42 + :selector '[*]}] + (-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo] config) + transport/invoke (fn [_ method _ args] + (swap! invoke-calls* conj {:method method :args args}) + (p/resolved {:db/id 42 :block/title "Debug Home"}))] + (p/let [result (debug-command/execute-debug-pull action {:base-url "http://example"})] + (is (= :ok (:status result))) + (is (= {:db/id 42 :block/title "Debug Home"} + (get-in result [:data :entity]))) + (is (= 42 (get-in result [:data :lookup]))) + (is (= '[*] (get-in result [:data :selector]))) + (is (= [{:method :thread-api/pull + :args ["logseq_db_demo" '[*] 42]}] + @invoke-calls*)))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + + (testing "returns typed error when entity is missing" + (async done + (let [action {:type :debug-pull + :repo "logseq_db_demo" + :lookup [:db/ident :missing/ident] + :selector '[*]}] + (-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo] config) + transport/invoke (fn [_ _method _ _args] + (p/resolved nil))] + (p/let [result (debug-command/execute-debug-pull action {:base-url "http://example"})] + (is (= :error (:status result))) + (is (= :entity-not-found (get-in result [:error :code]))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done)))))) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index b2087de378..8aba952521 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -78,6 +78,9 @@ (is (string/includes? plain-summary "sync")) (is (string/includes? plain-summary "login")) (is (string/includes? plain-summary "logout")) + (is (string/includes? plain-summary "debug")) + (is (not (string/includes? plain-summary "debug pull"))) + (is (string/includes? plain-summary "completion")) (is (string/includes? plain-summary "example")) (is (not (string/includes? plain-summary "example upsert"))) (is (string/includes? plain-summary "Path to db-worker data dir (default ~/logseq/graphs)")) @@ -108,6 +111,9 @@ (is (contains-bold? summary "sync start")) (is (contains-bold? summary "login")) (is (contains-bold? summary "logout")) + (is (contains-bold? summary "debug")) + (is (not (contains-bold? summary "debug pull"))) + (is (contains-bold? summary "completion")) (is (contains-bold? summary "example")) (is (not (contains-bold? summary "example upsert"))) (is (contains-bold? summary "--help")) @@ -208,6 +214,15 @@ (is (contains-bold? summary "search property")) (is (contains-bold? summary "search tag")))) + (testing "debug group shows subcommands" + (let [result (binding [style/*color-enabled?* true] + (commands/parse-args ["debug"])) + summary (:summary result) + plain-summary (strip-ansi summary)] + (is (true? (:help? result))) + (is (string/includes? plain-summary "debug pull")) + (is (contains-bold? summary "debug pull")))) + (testing "example group shows selectors" (let [result (binding [style/*color-enabled?* true] (commands/parse-args ["example"])) @@ -314,7 +329,7 @@ (testing "all groups show group help with -h and --help" ;; query and example are excluded: they are both groups and exact commands, ;; so -h shows their command help with options instead of group subcommand listing - (doseq [group ["graph" "server" "list" "upsert" "remove" "search" "sync"] + (doseq [group ["graph" "server" "list" "upsert" "remove" "search" "sync" "debug"] help-flag ["-h" "--help"]] (let [result (binding [style/*color-enabled?* true] (commands/parse-args [group help-flag])) @@ -1558,6 +1573,50 @@ (is (string/includes? (strip-ansi summary) "--linked-references")) (is (string/includes? (strip-ansi summary) "--ref-id-footer"))))) +(deftest test-verb-subcommand-parse-debug + (testing "debug pull parses with id" + (let [result (commands/parse-args ["debug" "pull" "--id" "1"])] + (is (true? (:ok? result))) + (is (= :debug-pull (:command result))) + (is (= 1 (get-in result [:options :id]))))) + + (testing "debug pull parses with uuid" + (let [result (commands/parse-args ["debug" "pull" "--uuid" "11111111-1111-1111-1111-111111111111"])] + (is (true? (:ok? result))) + (is (= :debug-pull (:command result))) + (is (= "11111111-1111-1111-1111-111111111111" (get-in result [:options :uuid]))))) + + (testing "debug pull parses with ident" + (let [result (commands/parse-args ["debug" "pull" "--ident" ":logseq.class/Tag"])] + (is (true? (:ok? result))) + (is (= :debug-pull (:command result))) + (is (= :logseq.class/Tag (get-in result [:options :ident]))))) + + (testing "debug pull rejects missing selector" + (let [result (commands/parse-args ["debug" "pull"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "debug pull rejects multiple selectors" + (let [result (commands/parse-args ["debug" "pull" "--id" "1" "--uuid" "11111111-1111-1111-1111-111111111111"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "debug pull rejects malformed id" + (let [result (commands/parse-args ["debug" "pull" "--id" "abc"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "debug pull rejects malformed uuid" + (let [result (commands/parse-args ["debug" "pull" "--uuid" "not-a-uuid"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "debug pull rejects malformed ident" + (let [result (commands/parse-args ["debug" "pull" "--ident" "logseq.class/Tag"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code])))))) + (deftest test-verb-subcommand-parse-query (testing "query shows group help" (let [result (commands/parse-args ["query"])] @@ -1713,7 +1772,8 @@ ["upsert" "page" "--wat"] ["remove" "block" "--wat"] ["upsert" "tag" "--wat"] - ["show" "--wat"]]] + ["show" "--wat"] + ["debug" "pull" "--wat"]]] (let [result (commands/parse-args args)] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code])))))) @@ -2121,6 +2181,34 @@ (is (= :show (get-in result [:action :type]))) (is (= [1 2] (get-in result [:action :ids])))))) +(deftest test-build-action-debug-pull + (testing "debug pull uses --graph when provided" + (let [parsed {:ok? true :command :debug-pull :options {:graph "demo" :id 1}} + result (commands/build-action parsed {})] + (is (true? (:ok? result))) + (is (= :debug-pull (get-in result [:action :type]))) + (is (= "logseq_db_demo" (get-in result [:action :repo]))) + (is (= 1 (get-in result [:action :lookup]))) + (is (= '[*] (get-in result [:action :selector]))))) + + (testing "debug pull falls back to config graph" + (let [parsed {:ok? true :command :debug-pull :options {:id 1}} + result (commands/build-action parsed {:graph "demo"})] + (is (true? (:ok? result))) + (is (= "logseq_db_demo" (get-in result [:action :repo]))))) + + (testing "debug pull falls back to config repo" + (let [parsed {:ok? true :command :debug-pull :options {:id 1}} + result (commands/build-action parsed {:repo "logseq_db_demo"})] + (is (true? (:ok? result))) + (is (= "logseq_db_demo" (get-in result [:action :repo]))))) + + (testing "debug pull fails when repo cannot be resolved" + (let [parsed {:ok? true :command :debug-pull :options {:id 1}} + result (commands/build-action parsed {})] + (is (false? (:ok? result))) + (is (= :missing-repo (get-in result [:error :code])))))) + (deftest test-build-action-add-validates-properties (testing "add block accepts custom property key in update-properties" (let [parsed (commands/parse-args ["upsert" "block" diff --git a/src/test/logseq/cli/completion_generator_test.cljs b/src/test/logseq/cli/completion_generator_test.cljs index 36e636bc12..4d9ac57af3 100644 --- a/src/test/logseq/cli/completion_generator_test.cljs +++ b/src/test/logseq/cli/completion_generator_test.cljs @@ -3,6 +3,7 @@ [clojure.string :as string] [logseq.cli.command.completion :as completion-command] [logseq.cli.command.core :as core] + [logseq.cli.command.debug :as debug-command] [logseq.cli.command.doctor :as doctor-command] [logseq.cli.command.example :as example-command] [logseq.cli.command.graph :as graph-command] @@ -25,6 +26,7 @@ search-command/entries show-command/entries doctor-command/entries + debug-command/entries completion-command/entries))) (def ^:private full-table @@ -181,13 +183,14 @@ (testing "show and doctor are leaves" (is (contains? leaf-names "show")) (is (contains? leaf-names "doctor"))) - (testing "graph, server, list, upsert, remove, search, example are groups" + (testing "graph, server, list, upsert, remove, search, debug, example are groups" (is (contains? group-names "graph")) (is (contains? group-names "server")) (is (contains? group-names "list")) (is (contains? group-names "upsert")) (is (contains? group-names "remove")) (is (contains? group-names "search")) + (is (contains? group-names "debug")) (is (contains? group-names "example"))))) (deftest test-spec->token @@ -239,6 +242,7 @@ (is (string/includes? output "_logseq_graph_export()")) (is (string/includes? output "_logseq_graph_backup_restore()")) (is (string/includes? output "_logseq_graph_backup_remove()")) + (is (string/includes? output "_logseq_debug_pull()")) (is (string/includes? output "_logseq_show()")) (is (string/includes? output "_logseq_example_upsert_page()"))) (testing "output contains group dispatchers" @@ -246,6 +250,7 @@ (is (string/includes? output "_logseq_list()")) (is (string/includes? output "_logseq_search()")) (is (string/includes? output "_logseq_upsert()")) + (is (string/includes? output "_logseq_debug()")) (is (string/includes? output "_logseq_example()"))) (testing "output contains top-level dispatcher" (is (string/includes? output "_logseq()")))