Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
518 lines
16 KiB
OCaml
518 lines
16 KiB
OCaml
type opts = { shell : Cli_primitive.shell option }
|
|
type parsed = Parsed_completion of opts
|
|
|
|
type action =
|
|
| Completion of {
|
|
shell : Cli_primitive.shell;
|
|
registry : Command_registry.t option;
|
|
}
|
|
|
|
let join_words words = Vec.string_concat " " words
|
|
|
|
let unique_preserve values =
|
|
let seen = Hashtbl.create 16 in
|
|
values
|
|
|> Vec.filter (fun value ->
|
|
if Hashtbl.mem seen value then false
|
|
else (
|
|
Hashtbl.add seen value ();
|
|
true))
|
|
|
|
let top_command_names registry =
|
|
registry.Command_registry.commands
|
|
|> Vec.filter_map (fun command -> Vec.nth_opt command.Command_registry.path 0)
|
|
|> Vec.sort_uniq String.compare
|
|
|
|
let is_prefix prefix path =
|
|
let rec loop index =
|
|
index = Vec.length prefix
|
|
|| (Vec.nth prefix index = Vec.nth path index && loop (index + 1))
|
|
in
|
|
Vec.length prefix <= Vec.length path && loop 0
|
|
|
|
let commands_under registry prefix =
|
|
registry.Command_registry.commands
|
|
|> Vec.filter (fun command ->
|
|
is_prefix prefix command.Command_registry.path
|
|
&& Vec.length command.Command_registry.path > Vec.length prefix)
|
|
|
|
let direct_children registry prefix =
|
|
let prefix_len = Vec.length prefix in
|
|
commands_under registry prefix
|
|
|> Vec.filter_map (fun command ->
|
|
Vec.nth_opt command.Command_registry.path prefix_len)
|
|
|> unique_preserve
|
|
|
|
let direct_leaf_children registry prefix =
|
|
let prefix_len = Vec.length prefix in
|
|
commands_under registry prefix
|
|
|> Vec.filter (fun command ->
|
|
Vec.length command.Command_registry.path = prefix_len + 1)
|
|
|
|
let nested_child_groups registry prefix =
|
|
let prefix_len = Vec.length prefix in
|
|
commands_under registry prefix
|
|
|> Vec.filter_map (fun command ->
|
|
if Vec.length command.Command_registry.path > prefix_len + 1 then
|
|
Vec.nth_opt command.Command_registry.path prefix_len
|
|
else None)
|
|
|> unique_preserve
|
|
|
|
let is_group registry path = not (Vec.is_empty (commands_under registry path))
|
|
|
|
let global_option_names =
|
|
Vec.of_array
|
|
[|
|
|
"--help";
|
|
"-h";
|
|
"--version";
|
|
"--config";
|
|
"--graph";
|
|
"-g";
|
|
"--root-dir";
|
|
"--timeout-ms";
|
|
"--output";
|
|
"-o";
|
|
"--verbose";
|
|
"-v";
|
|
"--profile";
|
|
|]
|
|
|
|
let global_value_option_names =
|
|
Vec.of_array
|
|
[|
|
|
"--config";
|
|
"--graph";
|
|
"-g";
|
|
"--root-dir";
|
|
"--timeout-ms";
|
|
"--output";
|
|
"-o";
|
|
|]
|
|
|
|
let command_desc_for_path registry path =
|
|
Command_registry.find_by_path path registry
|
|
|> Option.map (fun (command : Command_registry.command_meta) -> command.doc)
|
|
|> Option.value ~default:""
|
|
|
|
let zsh_quote_entry name desc = "'" ^ name ^ ":" ^ desc ^ "'"
|
|
|
|
let zsh_escape_desc doc =
|
|
let buffer = Buffer.create (String.length doc) in
|
|
String.iter
|
|
(fun c ->
|
|
match c with
|
|
| '\'' -> Buffer.add_string buffer "'\\''"
|
|
| '[' -> Buffer.add_string buffer "\\["
|
|
| ']' -> Buffer.add_string buffer "\\]"
|
|
| c -> Buffer.add_char buffer c)
|
|
doc;
|
|
Buffer.contents buffer
|
|
|
|
let zsh_option_action (option : Command_registry.option_meta) value_name =
|
|
if Vec.mem "--graph" option.names then "{_logseq_graphs}"
|
|
else if option.multi && not (Vec.is_empty option.choices) then
|
|
"{_values -s , " ^ value_name ^ " " ^ Vec.string_concat " " option.choices
|
|
^ "}"
|
|
else if not (Vec.is_empty option.choices) then
|
|
"(" ^ Vec.string_concat " " option.choices ^ ")"
|
|
else if Vec.mem "--root-dir" option.names then "_files -/"
|
|
else if value_name = "path" then "_files"
|
|
else if value_name = "bool" then "(true false)"
|
|
else ""
|
|
|
|
let zsh_option_spec (option : Command_registry.option_meta) =
|
|
let desc = "[" ^ zsh_escape_desc option.doc ^ "]" in
|
|
let value_spec =
|
|
match option.arity with
|
|
| Command_registry.Flag -> None
|
|
| Required_value name ->
|
|
Some (":" ^ name ^ ":" ^ zsh_option_action option name)
|
|
| Optional_value name ->
|
|
Some ("::" ^ name ^ ":" ^ zsh_option_action option name)
|
|
in
|
|
let name_suffix = if Option.is_some value_spec then "=" else "" in
|
|
let arg = Option.value value_spec ~default:"" in
|
|
let star = if option.repeatable then "*" else "" in
|
|
if Vec.length option.names = 1 then
|
|
"'" ^ star ^ Vec.nth option.names 0 ^ name_suffix ^ desc ^ arg ^ "'"
|
|
else
|
|
let exclusion =
|
|
if option.repeatable then "'" ^ star ^ "'"
|
|
else "'(" ^ Vec.string_concat " " option.names ^ ")'"
|
|
in
|
|
let alternatives =
|
|
option.names
|
|
|> Vec.map (fun name -> name ^ name_suffix)
|
|
|> Vec.string_concat ","
|
|
in
|
|
exclusion ^ "{" ^ alternatives ^ "}'" ^ desc ^ arg ^ "'"
|
|
|
|
let global_option_named name =
|
|
Command_registry.global_options
|
|
|> Vec.find_opt (fun (option : Command_registry.option_meta) ->
|
|
Vec.mem name option.names)
|
|
|
|
let zsh_global_specs names =
|
|
names |> Vec.filter_map global_option_named |> Vec.map zsh_option_spec
|
|
|
|
let zsh_spec_lines specs = " " ^ Vec.string_concat " \\\n " specs
|
|
|
|
let zsh_leaf_function command =
|
|
let func_name = "_logseq_" ^ join_words command.Command_registry.path in
|
|
let func_name = String.map (function ' ' -> '_' | c -> c) func_name in
|
|
let specs =
|
|
Vec.append
|
|
(zsh_global_specs (Vec.of_array [| "--help"; "--graph"; "--output" |]))
|
|
(Vec.map zsh_option_spec command.Command_registry.options)
|
|
in
|
|
String.concat "\n"
|
|
[ func_name ^ "() {"; " _arguments \\"; zsh_spec_lines specs; "}"; "" ]
|
|
|
|
let zsh_group_specs registry path =
|
|
let own_option_specs =
|
|
match Command_registry.find_by_path path registry with
|
|
| Some command -> Vec.map zsh_option_spec command.Command_registry.options
|
|
| None -> Vec.empty
|
|
in
|
|
Vec.append
|
|
(Vec.append
|
|
(zsh_global_specs (Vec.of_array [| "--help"; "--graph" |]))
|
|
own_option_specs)
|
|
(Vec.of_array [| "'(-)1:subcommand:->subcmd'"; "'(-)*::args:->args'" |])
|
|
|
|
let zsh_subgroup_function registry parent subgroup =
|
|
let prefix = Vec.push_back parent subgroup in
|
|
let child_entries =
|
|
direct_leaf_children registry prefix
|
|
|> Vec.map (fun command ->
|
|
" "
|
|
^ zsh_quote_entry
|
|
(Vec.nth command.Command_registry.path (Vec.length prefix))
|
|
command.Command_registry.doc)
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
let dispatches =
|
|
direct_leaf_children registry prefix
|
|
|> Vec.map (fun command ->
|
|
let leaf = Vec.nth command.Command_registry.path (Vec.length prefix) in
|
|
" " ^ leaf ^ ") _logseq_"
|
|
^ Vec.string_concat "_" command.Command_registry.path
|
|
^ " ;;")
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
String.concat "\n"
|
|
[
|
|
"_logseq_" ^ Vec.string_concat "_" prefix ^ "() {";
|
|
" local curcontext=\"$curcontext\" state line";
|
|
" typeset -A opt_args";
|
|
" _arguments -C \\";
|
|
zsh_spec_lines (zsh_group_specs registry prefix);
|
|
" case $state in";
|
|
" subcmd)";
|
|
" local -a subcmds";
|
|
" subcmds=(";
|
|
child_entries;
|
|
" )";
|
|
" _describe 'subcommand' subcmds";
|
|
" ;;";
|
|
" args)";
|
|
" case $line[1] in";
|
|
dispatches;
|
|
" esac";
|
|
" ;;";
|
|
" esac";
|
|
"}";
|
|
"";
|
|
]
|
|
|
|
let zsh_group_function registry group =
|
|
let prefix = Vec.singleton group in
|
|
let leaf_entries =
|
|
direct_leaf_children registry prefix
|
|
|> Vec.map (fun command ->
|
|
" "
|
|
^ zsh_quote_entry
|
|
(Vec.nth command.Command_registry.path 1)
|
|
command.Command_registry.doc)
|
|
in
|
|
let subgroup_entries =
|
|
nested_child_groups registry prefix
|
|
|> Vec.map (fun name ->
|
|
" " ^ zsh_quote_entry name (name ^ " commands"))
|
|
in
|
|
let dispatches =
|
|
direct_leaf_children registry prefix
|
|
|> Vec.map (fun command ->
|
|
let subcmd = Vec.nth command.Command_registry.path 1 in
|
|
" " ^ subcmd ^ ") _logseq_"
|
|
^ Vec.string_concat "_" command.Command_registry.path
|
|
^ " ;;")
|
|
|> fun leaf_dispatches ->
|
|
Vec.append leaf_dispatches
|
|
(nested_child_groups registry prefix
|
|
|> Vec.map (fun name ->
|
|
" " ^ name ^ ") _logseq_" ^ group ^ "_" ^ name ^ " ;;"))
|
|
in
|
|
let subgroup_functions =
|
|
nested_child_groups registry prefix
|
|
|> Vec.map (zsh_subgroup_function registry prefix)
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
subgroup_functions
|
|
^ String.concat "\n"
|
|
[
|
|
"_logseq_" ^ group ^ "() {";
|
|
" local curcontext=\"$curcontext\" state line";
|
|
" typeset -A opt_args";
|
|
" _arguments -C \\";
|
|
zsh_spec_lines (zsh_group_specs registry prefix);
|
|
" case $state in";
|
|
" subcmd)";
|
|
" local -a subcmds";
|
|
" subcmds=(";
|
|
Vec.string_concat "\n" (Vec.append leaf_entries subgroup_entries);
|
|
" )";
|
|
" _describe 'subcommand' subcmds";
|
|
" ;;";
|
|
" args)";
|
|
" case $line[1] in";
|
|
Vec.string_concat "\n" dispatches;
|
|
" esac";
|
|
" ;;";
|
|
" esac";
|
|
"}";
|
|
"";
|
|
]
|
|
|
|
let generate_zsh_completion registry =
|
|
let top_entries =
|
|
top_command_names registry
|
|
|> Vec.map (fun top ->
|
|
let desc =
|
|
let path = Vec.singleton top in
|
|
if is_group registry path then top ^ " commands"
|
|
else command_desc_for_path registry path
|
|
in
|
|
" " ^ zsh_quote_entry top desc)
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
let dispatches =
|
|
top_command_names registry
|
|
|> Vec.map (fun top -> " " ^ top ^ ") _logseq_" ^ top ^ " ;;")
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
let leaf_functions =
|
|
registry.Command_registry.commands
|
|
|> Vec.filter (fun command ->
|
|
(* commands that are also groups (e.g. `query`) are completed by
|
|
their group dispatcher, which includes their options *)
|
|
not (is_group registry command.Command_registry.path))
|
|
|> Vec.map zsh_leaf_function |> Vec.string_concat "\n"
|
|
in
|
|
let group_functions =
|
|
top_command_names registry
|
|
|> Vec.filter (fun top -> is_group registry (Vec.singleton top))
|
|
|> Vec.map (zsh_group_function registry)
|
|
|> Vec.string_concat "\n"
|
|
in
|
|
String.concat "\n"
|
|
[
|
|
"#compdef logseq";
|
|
"# Auto-generated by `logseq completion zsh` - do not edit manually.";
|
|
"";
|
|
"_logseq_graphs() {";
|
|
" local -a graphs";
|
|
" graphs=( ${(f)\"$(logseq graph list --output json 2>/dev/null | node \
|
|
-e 'const fs = require(\"fs\"); const input = fs.readFileSync(0, \
|
|
\"utf8\"); if (!input.trim()) process.exit(0); const data = \
|
|
JSON.parse(input); const graphs = data && data.data && \
|
|
data.data.graphs; if (Array.isArray(graphs)) \
|
|
console.log(graphs.join(\"\\n\"));')\"} )";
|
|
" compadd -a graphs";
|
|
"}";
|
|
"";
|
|
"# --- per-command functions ---";
|
|
"";
|
|
leaf_functions;
|
|
"# --- group dispatchers ---";
|
|
"";
|
|
group_functions;
|
|
"# --- top-level dispatcher ---";
|
|
"";
|
|
"_logseq() {";
|
|
" local curcontext=\"$curcontext\" state line";
|
|
" typeset -A opt_args";
|
|
" _arguments -C \\";
|
|
zsh_spec_lines
|
|
(Vec.append
|
|
(Vec.map zsh_option_spec Command_registry.global_options)
|
|
(Vec.of_array
|
|
[| "'(-)1:command:->cmds'"; "'(-)*::args:->args'" |]));
|
|
" case $state in";
|
|
" cmds)";
|
|
" local -a cmds";
|
|
" cmds=(";
|
|
top_entries;
|
|
" )";
|
|
" _describe 'command' cmds";
|
|
" ;;";
|
|
" args)";
|
|
" case $line[1] in";
|
|
dispatches;
|
|
" esac";
|
|
" ;;";
|
|
" esac";
|
|
"}";
|
|
"";
|
|
"compdef _logseq logseq";
|
|
]
|
|
|
|
let bash_case_pattern names = Vec.string_concat "|" names
|
|
|
|
let bash_subcommand_cases registry =
|
|
top_command_names registry
|
|
|> Vec.filter_map (fun top ->
|
|
let children = direct_children registry (Vec.singleton top) in
|
|
if Vec.is_empty children then None
|
|
else
|
|
Some
|
|
(" " ^ top ^ ") COMPREPLY=( $(compgen -W '"
|
|
^ Vec.string_concat " " children
|
|
^ "' -- \"$cur\") ) ;;"))
|
|
|> Vec.string_concat "\n"
|
|
|
|
let bash_sub_subcommand_cases registry =
|
|
top_command_names registry
|
|
|> Vec.concat_map (fun top ->
|
|
nested_child_groups registry (Vec.singleton top)
|
|
|> Vec.map (fun subgroup ->
|
|
let leaves =
|
|
direct_children registry (Vec.of_array [| top; subgroup |])
|
|
in
|
|
" " ^ top ^ ":" ^ subgroup ^ ") COMPREPLY=( $(compgen -W '"
|
|
^ Vec.string_concat " " leaves
|
|
^ "' -- \"$cur\") ) ;;"))
|
|
|> Vec.string_concat "\n"
|
|
|
|
let generate_bash_completion registry =
|
|
let top_commands = Vec.string_concat " " (top_command_names registry) in
|
|
String.concat "\n"
|
|
[
|
|
"# Auto-generated by `logseq completion bash` - do not edit manually.";
|
|
"";
|
|
"_logseq_is_value_opt() {";
|
|
" case \"$1\" in";
|
|
" " ^ bash_case_pattern global_value_option_names ^ ")";
|
|
" return 0 ;;";
|
|
" *) return 1 ;;";
|
|
" esac";
|
|
"}";
|
|
"";
|
|
"_logseq_cmd_and_subcmd() {";
|
|
" local i skip=0";
|
|
" __cmd='' __subcmd='' __subsubcmd=''";
|
|
" for (( i = 1; i < COMP_CWORD; i++ )); do";
|
|
" local w=\"${COMP_WORDS[i]}\"";
|
|
" if (( skip )); then skip=0; continue; fi";
|
|
" if [[ \"$w\" == -* ]]; then";
|
|
" _logseq_is_value_opt \"$w\" && skip=1";
|
|
" continue";
|
|
" fi";
|
|
" if [[ -z \"$__cmd\" ]]; then __cmd=\"$w\"";
|
|
" elif [[ -z \"$__subcmd\" ]]; then __subcmd=\"$w\"";
|
|
" elif [[ -z \"$__subsubcmd\" ]]; then __subsubcmd=\"$w\"";
|
|
" fi";
|
|
" done";
|
|
"}";
|
|
"";
|
|
"_logseq_json_names_bash() {";
|
|
" logseq graph list --output json 2>/dev/null | node -e 'const fs = \
|
|
require(\"fs\"); const input = fs.readFileSync(0, \"utf8\"); if \
|
|
(!input.trim()) process.exit(0); const data = JSON.parse(input); const \
|
|
graphs = data && data.data && data.data.graphs; if \
|
|
(Array.isArray(graphs)) console.log(graphs.join(\" \"));'";
|
|
"}";
|
|
"";
|
|
"_logseq_opts_for() {";
|
|
" printf '%s' \"" ^ Vec.string_concat " " global_option_names ^ "\"";
|
|
"}";
|
|
"";
|
|
"_logseq() {";
|
|
" local cur prev";
|
|
" cur=\"${COMP_WORDS[COMP_CWORD]}\"";
|
|
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"";
|
|
" COMPREPLY=()";
|
|
" local __cmd __subcmd __subsubcmd";
|
|
" _logseq_cmd_and_subcmd";
|
|
" if [[ \"$cur\" == -* ]]; then";
|
|
" COMPREPLY=( $(compgen -W \"$(_logseq_opts_for \"$__cmd\" \
|
|
\"$__subcmd\" \"$__subsubcmd\")\" -- \"$cur\") )";
|
|
" return";
|
|
" fi";
|
|
" if [[ -z \"$__cmd\" ]]; then";
|
|
" COMPREPLY=( $(compgen -W '" ^ top_commands ^ "' -- \"$cur\") )";
|
|
" return";
|
|
" fi";
|
|
" if [[ -z \"$__subcmd\" ]]; then";
|
|
" case \"$__cmd\" in";
|
|
bash_subcommand_cases registry;
|
|
" esac";
|
|
" return";
|
|
" fi";
|
|
" if [[ -z \"$__subsubcmd\" ]]; then";
|
|
" case \"$__cmd:$__subcmd\" in";
|
|
bash_sub_subcommand_cases registry;
|
|
" esac";
|
|
" return";
|
|
" fi";
|
|
"}";
|
|
"";
|
|
"complete -F _logseq logseq";
|
|
]
|
|
|
|
let generate shell registry =
|
|
match shell with
|
|
| Cli_primitive.Zsh -> generate_zsh_completion registry
|
|
| Bash -> generate_bash_completion registry
|
|
|
|
let command_id _ = Command_id.Completion
|
|
let validate_parsed _ = Ok ()
|
|
|
|
let build ?registry _config _ = function
|
|
| Parsed_completion { shell = Some shell } ->
|
|
Ok (Completion { shell; registry })
|
|
| Parsed_completion { shell = None } ->
|
|
Error
|
|
(Error.invalid_options
|
|
"completion shell is required; expected zsh or bash")
|
|
|
|
let execute_with_mode (Completion { shell; registry }) _config mode =
|
|
Cli_effect.pure
|
|
(Cli_result.ok ~command:Command_id.Completion mode
|
|
(Message
|
|
(generate shell
|
|
(Option.value registry ~default:Command_registry.empty))))
|
|
|
|
let metadata () =
|
|
Vec.of_array
|
|
[|
|
|
{
|
|
Command_registry.id = Command_id.Completion;
|
|
path = Command_id.to_path Command_id.Completion;
|
|
doc = "Generate shell completion script";
|
|
long_doc = None;
|
|
examples = Vec.empty;
|
|
options = Vec.empty;
|
|
category = Command_registry.Utilities;
|
|
requires_graph = Command_id.requires_graph Command_id.Completion;
|
|
requires_auth = Command_id.requires_auth Command_id.Completion;
|
|
write_command = Command_id.is_write Command_id.Completion;
|
|
human_table_headers_order = Vec.empty;
|
|
};
|
|
|]
|
|
|
|
let execute action config =
|
|
let (Output.Mode.Packed mode) = Output_mode.for_config config in
|
|
execute_with_mode action config mode
|