Merge remote-tracking branch 'origin/feat/cliable' into feat/cliable

This commit is contained in:
rcmerci
2026-04-19 12:06:00 +08:00
6 changed files with 287 additions and 72 deletions

View File

@@ -19,13 +19,13 @@ Use `logseq` to inspect and edit graph entities, run Datascript queries, and con
## Command groups (from `logseq --help`)
- Graph inspect/edit:
- `list page`, `list tag`, `list property`
- `upsert block`, `upsert page`, `upsert tag`, `upsert property`
- `list node`, `list page`, `list tag`, `list property`, `list task`, `list asset`
- `upsert block`, `upsert page`, `upsert tag`, `upsert property`, `upsert task`, `upsert assert`
- `remove block`, `remove page`, `remove tag`, `remove property`
- `query`, `query list`, `show`
- Graph management: `graph list|create|switch|remove|validate|info|export|import`
- Server management: `server list|status|start|stop|restart`
- Diagnostics: `doctor`
- `query`, `query list`, `show`, `search`
- Graph management: `graph list|create|switch|remove|validate|info|export|import|backup`
- Server management: `server list|cleanup|start|stop|restart`
- Diagnostics: `doctor`, `debug`
## Global options
@@ -84,8 +84,4 @@ Use `logseq` to inspect and edit graph entities, run Datascript queries, and con
- `upsert block` enters update mode when `--id` or `--uuid` is provided.
- Always verify command flags with `logseq --help` and `logseq <...> --help` before execution.
- If `logseq` reports that it doesnt have read/write permission for data-dir, then add read/write permission for data-dir in the agents config.
- In sandboxed environments, `graph create` may print a process-scan warning to stderr; if command status is `ok`, the graph is still created.
## References
- Built-in tags and properties: See `references/logseq-builtins.md` when you need canonical built-ins for `list ... --include-built-in` or for tag/property upsert fields.
- In sandboxed environments, `graph create` may print a process-scan warning to stderr; if command status is `ok`, the graph is still created.

View File

@@ -406,9 +406,8 @@
:close-db (fn [db] (.close db))
:exec (fn [db sql-or-opts] (.exec db sql-or-opts))
:transaction (fn [db f] (.transaction db f))
:backup-db (fn [db path]
(let [backup-fn (gobj/get db "backup")]
(backup-fn path)))}
:backup-db (fn [^js db path]
(.backup db path))}
:crypto {:save-secret-text! (fn [key text]
((:set! kv) (secret-key key) text))
:read-secret-text (fn [key]

View File

@@ -69,22 +69,6 @@
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]]))))))
;; TODO: Replace uses of this fn with ensure-page! when users are able to specify ids for contexts this
;; is used in
(defn- ensure-first-page!
"Unlike ensure-page!, chooses the first random page and doesn't ensure the page is unique. Only use this
when the user unable to specificy the specific page e.g. page refs in a block/title"
[config repo page-name]
(let [page-name-lc (common-util/page-name-sanity-lc page-name)]
(p/let [page (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]])]
(if (:db/id page)
page
(p/let [_ (transport/invoke config :thread-api/apply-outliner-ops false
[repo [[:create-page [page-name {}]]] {}])]
(transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name-lc]]))))))
(defn pull-tag-by-name
"Look up a tag by name, constrained to entities tagged with :logseq.class/Tag."
[config repo tag-name selector]
@@ -266,6 +250,10 @@
(remove string/blank?)
vec))
(defn- integer-string?
[s]
(boolean (re-matches #"-?\d+" s)))
(defn- partition-ref-values
[refs]
(reduce
@@ -278,9 +266,12 @@
(common-util/uuid-string? value)
(update acc :uuid-refs conj value)
(integer-string? value)
(update acc :id-refs conj value)
:else
(update acc :page-refs conj value))))
{:uuid-refs [] :page-refs []}
{:uuid-refs [] :page-refs [] :id-refs []}
refs))
(defn- resolve-page-ref-entities
@@ -295,7 +286,7 @@
page-refs)]
(p/let [resolved (p/all
(map (fn [[_ page-name]]
(p/let [page (ensure-first-page! config repo page-name)
(p/let [page (ensure-page! config repo page-name)
page-uuid (:block/uuid page)]
(when-not page-uuid
(throw (ex-info "page not found"
@@ -320,6 +311,26 @@
:uuid uuid-ref})))))
(distinct uuid-refs)))))
(defn- resolve-id-ref-entities
"Resolve integer id refs (db/id values) to entity maps with :block/uuid and
:block/title so they can be normalized like page-name refs."
[config repo id-refs]
(if (seq id-refs)
(p/let [entities (p/all
(map (fn [id-str]
(let [id (parse-long id-str)]
(p/let [entity (transport/invoke config :thread-api/pull false
[repo [:db/id :block/uuid :block/title] id])]
(when-not (:db/id entity)
(throw (ex-info (str "id ref not found: " id-str)
{:code :id-ref-not-found
:id id-str})))
{:block/uuid (:block/uuid entity)
:block/title id-str})))
(distinct id-refs)))]
(vec entities))
(p/resolved nil)))
(defn- normalize-block-title-refs
[blocks refs]
(mapv (fn update-block [block]
@@ -754,7 +765,7 @@
(and (string? value) (common-util/uuid-string? (string/trim value)))
(resolve-entity-id config repo [:block/uuid (uuid (string/trim value))])
(string? value)
(p/let [page (ensure-first-page! config repo value)]
(p/let [page (ensure-page! config repo value)]
(or (:db/id page)
(throw (ex-info "page not found" {:code :page-not-found :value value}))))
:else
@@ -1138,9 +1149,11 @@
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
target-block-uuid (resolve-add-target cfg action)
ref-values (collect-page-refs (:blocks action))
{:keys [uuid-refs page-refs]} (partition-ref-values ref-values)
{:keys [uuid-refs page-refs id-refs]} (partition-ref-values ref-values)
_ (ensure-block-refs-exist! cfg (:repo action) uuid-refs)
refs (or (resolve-page-ref-entities cfg (:repo action) page-refs) [])
page-refs' (or (resolve-page-ref-entities cfg (:repo action) page-refs) [])
id-refs' (or (resolve-id-ref-entities cfg (:repo action) id-refs) [])
refs (into page-refs' id-refs')
blocks (if (seq refs)
(normalize-block-title-refs (:blocks action) refs)
(:blocks action))

View File

@@ -358,41 +358,30 @@ _logseq_multi_values() {
[cmds]
(str "_logseq_" (string/join "_" cmds)))
(defn- zsh-group-function
"Generate a group dispatcher function.
Handles groups that have both a root command (e.g. [\"query\"]) and
subcommands (e.g. [\"query\" \"list\"])."
[group-name subentries global-spec]
(let [func-name (str "_logseq_" group-name)
;; Separate root entry from subcommand entries
root-entry (first (filter #(= 1 (count (:cmds %))) subentries))
sub-entries (filter #(> (count (:cmds %)) 1) subentries)
;; Root-level options (global + root command's own spec if present)
root-spec (if root-entry
(:spec root-entry)
global-spec)
(defn- zsh-subgroup-function
"Generate a dispatcher for a 3-level subgroup (e.g. graph backup)."
[parent-name subgroup-name sub-entries global-spec]
(let [func-name (str "_logseq_" parent-name "_" subgroup-name)
global-keys (set (keys global-spec))
root-tokens (zsh-arguments-tokens root-spec global-keys)
root-lines (string/join " \\\n " root-tokens)
tokens (zsh-arguments-tokens global-spec global-keys)
options-lines (string/join " \\\n " tokens)
subcmds (->> sub-entries
(mapv (fn [entry]
(let [subcmd (second (:cmds entry))
(let [sub-subcmd (nth (:cmds entry) 2)
desc (or (:desc entry) "")]
(str " '" subcmd ":" desc "'")))))
(str " '" sub-subcmd ":" desc "'")))))
subcmd-lines (string/join "\n" subcmds)
dispatches (->> sub-entries
(mapv (fn [entry]
(let [subcmd (second (:cmds entry))
sub-func (cmd->func-name (:cmds entry))]
(str " " subcmd ") " sub-func " ;;"))))
)
dispatch-lines (string/join "\n" dispatches)]
(let [sub-subcmd (nth (:cmds entry) 2)
leaf-func (cmd->func-name (:cmds entry))]
(str " " sub-subcmd ") " leaf-func " ;;")))))]
(str func-name "() {\n"
" local curcontext=\"$curcontext\" state line\n"
" typeset -A opt_args\n"
"\n"
" _arguments -C -s \\\n"
" " root-lines " \\\n"
" " options-lines " \\\n"
" '1:subcommand:->subcmd' \\\n"
" '*::args:->args'\n"
"\n"
@@ -406,12 +395,90 @@ _logseq_multi_values() {
" ;;\n"
" args)\n"
" case $line[1] in\n"
dispatch-lines "\n"
(string/join "\n" dispatches) "\n"
" esac\n"
" ;;\n"
" esac\n"
"}\n")))
(defn- zsh-group-function
"Generate a group dispatcher function.
Handles groups that have both a root command (e.g. [\"query\"]) and
subcommands (e.g. [\"query\" \"list\"]).
Also handles nested subgroups (e.g. [\"graph\" \"backup\" \"list\"])."
[group-name subentries global-spec]
(let [func-name (str "_logseq_" group-name)
;; Separate root entry from subcommand entries
root-entry (first (filter #(= 1 (count (:cmds %))) subentries))
sub-entries (filter #(> (count (:cmds %)) 1) subentries)
;; Partition into leaf subcmds (2 elements) and nested (3+ elements)
leaf-entries (filter #(= 2 (count (:cmds %))) sub-entries)
nested-entries (filter #(> (count (:cmds %)) 2) sub-entries)
;; Group nested entries by their 2nd element to find subgroups
nested-groups (group-by #(second (:cmds %)) nested-entries)
;; Generate subgroup dispatcher functions
subgroup-fns (when (seq nested-groups)
(->> nested-groups
(mapv (fn [[sg-name sg-entries]]
(zsh-subgroup-function group-name sg-name sg-entries global-spec)))
(string/join "\n")))
;; Root-level options (global + root command's own spec if present)
root-spec (if root-entry
(:spec root-entry)
global-spec)
global-keys (set (keys global-spec))
root-tokens (zsh-arguments-tokens root-spec global-keys)
root-lines (string/join " \\\n " root-tokens)
;; Build subcmd descriptions: leaf entries + subgroup names
leaf-subcmds (->> leaf-entries
(mapv (fn [entry]
(let [subcmd (second (:cmds entry))
desc (or (:desc entry) "")]
(str " '" subcmd ":" desc "'")))))
subgroup-subcmds (->> (keys nested-groups)
sort
(mapv (fn [sg-name]
(str " '" sg-name ":" sg-name " commands'"))))
subcmd-lines (string/join "\n" (concat leaf-subcmds subgroup-subcmds))
;; Build dispatch: leaf entries dispatch to leaf fns, subgroups to subgroup fns
leaf-dispatches (->> leaf-entries
(mapv (fn [entry]
(let [subcmd (second (:cmds entry))
sub-func (cmd->func-name (:cmds entry))]
(str " " subcmd ") " sub-func " ;;")))))
subgroup-dispatches (->> (keys nested-groups)
sort
(mapv (fn [sg-name]
(str " " sg-name ") _logseq_" group-name "_" sg-name " ;;"))))
dispatch-lines (string/join "\n" (concat leaf-dispatches subgroup-dispatches))
group-fn (str func-name "() {\n"
" local curcontext=\"$curcontext\" state line\n"
" typeset -A opt_args\n"
"\n"
" _arguments -C -s \\\n"
" " root-lines " \\\n"
" '1:subcommand:->subcmd' \\\n"
" '*::args:->args'\n"
"\n"
" case $state in\n"
" subcmd)\n"
" local -a subcmds\n"
" subcmds=(\n"
subcmd-lines "\n"
" )\n"
" _describe 'subcommand' subcmds\n"
" ;;\n"
" args)\n"
" case $line[1] in\n"
dispatch-lines "\n"
" esac\n"
" ;;\n"
" esac\n"
"}\n")]
(if subgroup-fns
(str subgroup-fns "\n" group-fn)
group-fn)))
(defn- zsh-toplevel-function
"Generate the _logseq() root dispatcher."
[table global-spec]
@@ -434,8 +501,7 @@ _logseq_multi_values() {
(= 1 (count (:cmds (first entries)))))
(cmd->func-name (:cmds (first entries)))
(str "_logseq_" g))]
(str " " g ") " func " ;;"))))
)
(str " " g ") " func " ;;")))))
dispatch-lines (string/join "\n" dispatches)
global-keys (set (keys global-spec))
global-tokens (zsh-arguments-tokens global-spec global-keys)
@@ -686,7 +752,7 @@ _logseq_multi_values_bash() {
[]
"_logseq_cmd_and_subcmd() {
local i skip=0
__cmd='' __subcmd=''
__cmd='' __subcmd='' __subsubcmd=''
for (( i = 1; i < COMP_CWORD; i++ )); do
local w=\"${COMP_WORDS[i]}\"
if (( skip )); then skip=0; continue; fi
@@ -698,6 +764,8 @@ _logseq_multi_values_bash() {
__cmd=\"$w\"
elif [[ -z \"$__subcmd\" ]]; then
__subcmd=\"$w\"
elif [[ -z \"$__subsubcmd\" ]]; then
__subsubcmd=\"$w\"
fi
done
}\n")
@@ -739,31 +807,52 @@ _logseq_multi_values_bash() {
(let [;; Root entry opts go at group level, sub-entries get case branches
root-entry (first (filter #(= 1 (count (:cmds %))) entries))
sub-entries (filter #(> (count (:cmds %)) 1) entries)
leaf-subs (filter #(= 2 (count (:cmds %))) sub-entries)
nested-subs (filter #(> (count (:cmds %)) 2) sub-entries)
nested-groups (group-by #(second (:cmds %)) nested-subs)
root-opts (when root-entry
(let [cmd-spec (apply dissoc (:spec root-entry) (keys global-spec))]
(->> (keys cmd-spec)
(mapcat (fn [k] (bash-option-names k (get cmd-spec k))))
(string/join " "))))
sub-branches
(->> sub-entries
leaf-branches
(->> leaf-subs
(mapv (fn [entry]
(let [subcmd (second (:cmds entry))
cmd-spec (apply dissoc (:spec entry) (keys global-spec))
cmd-opts (->> (keys cmd-spec)
(mapcat (fn [k] (bash-option-names k (get cmd-spec k))))
(string/join " "))]
(str " " subcmd ") opts+=' " cmd-opts "' ;;"))))
(string/join "\n"))]
(str " " subcmd ") opts+=' " cmd-opts "' ;;")))))
;; For nested subgroups, dispatch on subsubcmd
nested-branches
(->> (sort-by first nested-groups)
(mapv (fn [[sg-name sg-entries]]
(let [inner (->> sg-entries
(mapv (fn [entry]
(let [sub-subcmd (nth (:cmds entry) 2)
cmd-spec (apply dissoc (:spec entry) (keys global-spec))
cmd-opts (->> (keys cmd-spec)
(mapcat (fn [k] (bash-option-names k (get cmd-spec k))))
(string/join " "))]
(str " " sub-subcmd ") opts+=' " cmd-opts "' ;;"))))
(string/join "\n"))]
(str " " sg-name ")\n"
" case \"$subsubcmd\" in\n"
inner "\n"
" esac\n"
" ;;")))))
all-branches (concat leaf-branches nested-branches)
sub-branches (string/join "\n" all-branches)]
(str " " group-name ")\n"
(when (seq root-opts)
(str " opts+=' " root-opts "'\n"))
" case \"$subcmd\" in\n"
sub-branches "\n"
" esac\n"
" ;;"))))
))]
" ;;"))))))]
(str "_logseq_opts_for() {\n"
" local cmd=\"$1\" subcmd=\"$2\"\n"
" local cmd=\"$1\" subcmd=\"$2\" subsubcmd=\"$3\"\n"
" local opts=\"" global-str "\"\n"
"\n"
" case \"$cmd\" in\n"
@@ -841,7 +930,9 @@ _logseq_multi_values_bash() {
nil)))
(defn- bash-subcommand-cases
"Generate subcommand completion for each group."
"Generate subcommand completion for each group.
Deduplicates subcmd names so that 3-level commands like
[\"graph\" \"backup\" \"list\"] produce a single \"backup\" entry."
[table]
(let [groups (extract-groups table)
cases (->> (sort-by first groups)
@@ -850,12 +941,35 @@ _logseq_multi_values_bash() {
(> (count (:cmds (first entries))) 1))
(let [subcmds (->> entries
(keep #(second (:cmds %)))
distinct
(string/join " "))]
(when (seq subcmds)
(str " " group-name ") COMPREPLY=( $(compgen -W '"
subcmds "' -- \"$cur\") ) ;;")))))))]
(string/join "\n" cases)))
(defn- bash-sub-subcommand-cases
"Generate sub-subcommand completion for 3-level command groups.
Produces cases like: graph:backup) COMPREPLY=( $(compgen -W 'list create ...' ...) ) ;;"
[table]
(let [groups (extract-groups table)
cases (->> (sort-by first groups)
(mapcat (fn [[group-name entries]]
(let [nested (filter #(> (count (:cmds %)) 2) entries)
nested-groups (group-by #(second (:cmds %)) nested)]
(->> (sort-by first nested-groups)
(mapv (fn [[sg-name sg-entries]]
(let [sub-subcmds (->> sg-entries
(map #(nth (:cmds %) 2))
distinct
(string/join " "))]
(str " " group-name ":" sg-name
") COMPREPLY=( $(compgen -W '"
sub-subcmds "' -- \"$cur\") ) ;;")))))))))
cases (remove nil? cases)]
(when (seq cases)
(string/join "\n" cases))))
(defn- bash-toplevel-commands
"Get all top-level command names."
[table]
@@ -1003,6 +1117,8 @@ _logseq_multi_values_bash() {
varied-cases (bash-varied-prev-cases table varied-keys global-keys)
;; Subcommand completion
subcmd-cases (bash-subcommand-cases table)
;; Sub-subcommand completion for 3-level commands
sub-subcmd-cases (bash-sub-subcommand-cases table)
;; Top-level commands
top-cmds (bash-toplevel-commands table)]
(str "_logseq() {\n"
@@ -1011,7 +1127,7 @@ _logseq_multi_values_bash() {
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n"
" COMPREPLY=()\n"
"\n"
" local __cmd __subcmd\n"
" local __cmd __subcmd __subsubcmd\n"
" _logseq_cmd_and_subcmd\n"
"\n"
" # --- Option value completion ---\n"
@@ -1024,7 +1140,7 @@ _logseq_multi_values_bash() {
" # --- Flag / positional completion ---\n"
" if [[ \"$cur\" == -* ]]; then\n"
" # shellcheck disable=SC2046\n"
" COMPREPLY=( $(compgen -W \"$(_logseq_opts_for \"$__cmd\" \"$__subcmd\")\" -- \"$cur\") )\n"
" COMPREPLY=( $(compgen -W \"$(_logseq_opts_for \"$__cmd\" \"$__subcmd\" \"$__subsubcmd\")\" -- \"$cur\") )\n"
" return\n"
" fi\n"
"\n"
@@ -1039,6 +1155,14 @@ _logseq_multi_values_bash() {
" esac\n"
" return\n"
" fi\n"
"\n"
(when (seq sub-subcmd-cases)
(str " if [[ -z \"$__subsubcmd\" ]]; then\n"
" case \"$__cmd:$__subcmd\" in\n"
sub-subcmd-cases "\n"
" esac\n"
" return\n"
" fi\n"))
"}\n")))
(defn generate-bash

View File

@@ -47,6 +47,30 @@
(is (= :add-id-resolution-failed (-> error ex-data :code)))
(is (= [uuid-b] (-> error ex-data :missing-uuids))))))
(deftest test-partition-ref-values
(testing "partitions uuid, integer id, and page-name refs"
(let [result (#'add-command/partition-ref-values
["some page"
"550e8400-e29b-41d4-a716-446655440000"
"101"
" 42 "
"another page"
""
" "])]
(is (= ["550e8400-e29b-41d4-a716-446655440000"] (:uuid-refs result)))
(is (= ["101" "42"] (:id-refs result)))
(is (= ["some page" "another page"] (:page-refs result)))))
(testing "negative integers are recognized as id refs"
(let [result (#'add-command/partition-ref-values ["-5"])]
(is (= ["-5"] (:id-refs result)))
(is (empty? (:page-refs result)))))
(testing "non-integer numbers stay as page refs"
(let [result (#'add-command/partition-ref-values ["3.14" "1e5"])]
(is (empty? (:id-refs result)))
(is (= ["3.14" "1e5"] (:page-refs result))))))
(def ^:private mock-transport-invoke
(fn [_ method _ args]
(case method
@@ -77,6 +101,26 @@
(p/rejected (ex-info "unexpected method" {:method method :args args})))))
(deftest test-resolve-id-ref-entities
(testing "resolves integer id refs to uuid+title maps"
(async done
(let [page-uuid (random-uuid)
mock-invoke (fn [_ _ _ args]
(let [[_ _ lookup] args]
(p/resolved
(cond
(= lookup 101)
{:db/id 101 :block/uuid page-uuid :block/title "My Page"}
:else {}))))]
(-> (p/with-redefs [transport/invoke mock-invoke]
(p/let [result (#'add-command/resolve-id-ref-entities {} "demo" ["101"])]
(is (= 1 (count result)))
(is (= page-uuid (:block/uuid (first result))))
(is (= "101" (:block/title (first result)))
"title is the original id string for title-ref->id-ref replacement")))
(p/catch (fn [e] (is false (str "unexpected error: " e))))
(p/finally done))))))
(deftest test-resolve-tags-accepts-valid-tag
(async done
(-> (p/with-redefs [transport/invoke mock-transport-invoke]

View File

@@ -563,6 +563,45 @@
(testing "uniform options like --cardinality are not varied"
(is (not (contains? varied :cardinality))))))
(deftest test-zsh-nested-subcommand-completion
(let [output (gen/generate-completions "zsh" full-table)]
(testing "zsh generates subgroup dispatcher for graph backup"
(is (string/includes? output "_logseq_graph_backup()"))
(is (re-find #"(?s)_logseq_graph_backup\(\).*?'list:" output)
"graph backup dispatcher lists 'list' subcommand")
(is (re-find #"(?s)_logseq_graph_backup\(\).*?'create:" output)
"graph backup dispatcher lists 'create' subcommand")
(is (re-find #"(?s)_logseq_graph_backup\(\).*?'restore:" output)
"graph backup dispatcher lists 'restore' subcommand")
(is (re-find #"(?s)_logseq_graph_backup\(\).*?'remove:" output)
"graph backup dispatcher lists 'remove' subcommand"))
(testing "graph dispatcher dispatches backup to subgroup function"
(is (re-find #"(?s)_logseq_graph\(\).*?backup\) _logseq_graph_backup" output)))
(testing "graph backup remove leaf has its own function with --src option"
(is (string/includes? output "_logseq_graph_backup_remove()"))
(is (re-find #"(?s)_logseq_graph_backup\(\).*?remove\) _logseq_graph_backup_remove" output)))))
(deftest test-bash-nested-subcommand-completion
(let [output (gen/generate-completions "bash" full-table)]
(testing "bash subcmd completion for graph includes backup (deduplicated)"
(is (re-find #"graph\) COMPREPLY=.*backup" output))
;; backup should appear only once, not repeated per sub-subcommand
(let [graph-case (re-find #"graph\) COMPREPLY=\( \$\(compgen -W '([^']*)'" output)
subcmds (when graph-case (string/split (second graph-case) #" "))]
(is (= (count (filter #(= "backup" %) subcmds)) 1)
"backup appears exactly once in graph subcmd list")))
(testing "bash sub-subcommand dispatch for graph:backup"
(is (string/includes? output "graph:backup)")))
(testing "bash sub-subcommand completions for graph backup include list, create, restore, remove"
(let [case-match (re-find #"graph:backup\) COMPREPLY=\( \$\(compgen -W '([^']*)'" output)]
(is (some? case-match) "graph:backup case exists")
(when case-match
(let [sub-subcmds (set (string/split (second case-match) #" "))]
(is (contains? sub-subcmds "list"))
(is (contains? sub-subcmds "create"))
(is (contains? sub-subcmds "restore"))
(is (contains? sub-subcmds "remove"))))))))
(deftest test-e2e-generated-header
(testing "zsh output includes do-not-edit header"
(let [output (gen/generate-completions "zsh" full-table)]