let is_option token = String.length token > 0 && token.[0] = '-' let option_value key options = Vec.find_map (fun (candidate, value) -> if candidate = key then value else None) options let option_present key options = Vec.mem_assoc key options let normalize_key = function | "-g" -> Some "graph" | "-o" -> Some "output" | "-c" -> Some "content" | "-e" -> Some "edn-options" | "-f" -> Some "fields" | "-h" -> Some "help" | "-p" -> Some "pretty-print" | "-s" -> Some "sort" | "-t" -> Some "type" | "-v" -> Some "verbose" | token when String.length token > 2 && String.sub token 0 2 = "--" -> Some (String.sub token 2 (String.length token - 2)) | _ -> None let split_equals_option token = match String.index_opt token '=' with | None -> None | Some index -> let key = String.sub token 0 index in let value = String.sub token (index + 1) (String.length token - index - 1) in Option.map (fun key -> (key, value)) (normalize_key key) let boolean_option = function | "version" | "help" | "verbose" | "profile" | "enable-sync" | "expand" | "fix" | "include-built-in" | "include-journal" | "journal-only" | "include-hidden" | "with-properties" | "with-extends" | "with-classes" | "with-type" | "page-hierarchy" | "linked-references" | "ref-id-footer" | "progress" | "upload-keys" | "pretty-print" -> true | _ -> false let boolean_literal value = match String.lowercase_ascii (String.trim value) with | "true" | "false" -> true | _ -> false let parse_tokens argv = let rec loop options positional remaining = match Vec.pop_front remaining with | None -> (options, positional) | Some ("--", rest) -> (options, Vec.append positional rest) | Some (token, rest) -> ( match split_equals_option token with | Some (key, value) -> loop (Vec.push_back options (key, Some value)) positional rest | None -> ( match normalize_key token with | Some key when boolean_option key -> ( match Vec.pop_front rest with | Some (value, tail) when boolean_literal value -> loop (Vec.push_back options (key, Some value)) positional tail | _ -> loop (Vec.push_back options (key, Some "true")) positional rest) | Some key -> ( match Vec.pop_front rest with | Some (value, tail) when not (is_option value) -> loop (Vec.push_back options (key, Some value)) positional tail | _ -> loop (Vec.push_back options (key, None)) positional rest) | None -> loop options (Vec.push_back positional token) rest)) in loop Vec.empty Vec.empty argv let globals_of_options options = Global_opts.create ?graph: (Option.map Cli_primitive.create_graph (option_value "graph" options)) ?root_dir:(option_value "root-dir" options) ?config_path:(option_value "config" options) ?timeout_span: (Option.map Time.span_of_ms (Option.bind (option_value "timeout-ms" options) Int64.of_string_opt)) ?output_format: (Option.bind (option_value "output" options) Output.Mode.of_string) ~verbose:(option_present "verbose" options) ~profile:(option_present "profile" options) () let parse_csv value = Vec.split_on_char ',' value |> Vec.map String.trim |> Vec.filter (fun value -> value <> "") let int_option key options = Option.bind (option_value key options) int_of_string_opt let int64_option key options = Option.bind (option_value key options) Int64.of_string_opt let time_option key options = Option.bind (option_value key options) Time.parse_time let bool_option_value key options = match option_value key options with | Some value -> Some (String.lowercase_ascii (String.trim value) = "true") | None -> if option_present key options then Some true else None let common_list_opts options = { List_command.fields = Option.map parse_csv (option_value "fields" options); limit = int_option "limit" options; offset = int_option "offset" options; sort = option_value "sort" options; order = Option.bind (option_value "order" options) List_command.order_of_string; } let optional_flag key options = bool_option_value key options let graph_export_type options = Option.bind (Option.map String.lowercase_ascii (option_value "type" options)) Graph.normalize_export_type let graph_import_type options = Option.bind (Option.map String.lowercase_ascii (option_value "type" options)) Graph.normalize_import_type let global_options = Vec.of_array [| "graph"; "root-dir"; "config"; "timeout-ms"; "output"; "verbose"; "profile"; "help"; "version"; |] let common_list_options = Vec.of_array [| "fields"; "limit"; "offset"; "sort"; "order" |] let option_names values = Vec.of_array values let path1 path a = Vec.length path = 1 && Vec.nth path 0 = a let path2 path a b = Vec.length path = 2 && Vec.nth path 0 = a && Vec.nth path 1 = b let path3 path a b c = Vec.length path = 3 && Vec.nth path 0 = a && Vec.nth path 1 = b && Vec.nth path 2 = c let path2_any path a choices = Vec.length path = 2 && Vec.nth path 0 = a && Vec.mem (Vec.nth path 1) choices let path3_any path a b choices = Vec.length path = 3 && Vec.nth path 0 = a && Vec.nth path 1 = b && Vec.mem (Vec.nth path 2) choices let allowed_options_for_path path = if path2_any path "graph" (option_names [| "list"; "switch"; "remove"; "info" |]) || path3 path "graph" "backup" "list" then Vec.empty else if path2 path "graph" "create" then option_names [| "enable-sync"; "e2ee-password" |] else if path2 path "graph" "validate" then option_names [| "fix" |] else if path3 path "graph" "backup" "create" then option_names [| "name" |] else if path3 path "graph" "backup" "restore" then option_names [| "src"; "dst" |] else if path3 path "graph" "backup" "remove" then option_names [| "src" |] else if path2 path "graph" "export" then option_names [| "type"; "file"; "edn-options"; "pretty-print" |] else if path2 path "graph" "import" then option_names [| "type"; "input" |] else if path2 path "list" "page" then Vec.append common_list_options (option_names [| "expand"; "include-built-in"; "include-journal"; "journal-only"; "include-hidden"; "updated-after"; "created-after"; |]) else if path2 path "list" "tag" then Vec.append common_list_options (option_names [| "expand"; "include-built-in"; "with-properties"; "with-extends" |]) else if path2 path "list" "property" then Vec.append common_list_options (option_names [| "expand"; "include-built-in"; "with-classes"; "with-type" |]) else if path2 path "list" "task" then Vec.append common_list_options (option_names [| "status"; "priority"; "content" |]) else if path2 path "list" "node" then Vec.append common_list_options (option_names [| "tags"; "properties" |]) else if path2 path "list" "asset" then common_list_options else if path2 path "remove" "block" then option_names [| "id"; "uuid" |] else if path2 path "remove" "page" then option_names [| "id"; "page" |] else if path2_any path "remove" (option_names [| "tag"; "property" |]) then option_names [| "id"; "name" |] else if path2 path "upsert" "block" then option_names [| "id"; "uuid"; "target-id"; "target-uuid"; "target-page"; "pos"; "content"; "blocks"; "blocks-file"; "update-tags"; "update-properties"; "remove-tags"; "remove-properties"; |] else if path2 path "upsert" "page" then option_names [| "id"; "page"; "restore"; "update-tags"; "update-properties"; "remove-tags"; "remove-properties"; |] else if path2 path "upsert" "asset" then option_names [| "id"; "uuid"; "path"; "target-id"; "target-uuid"; "target-page"; "pos"; "content"; |] else if path2 path "upsert" "task" then option_names [| "id"; "uuid"; "page"; "content"; "target-id"; "target-uuid"; "target-page"; "pos"; "status"; "priority"; "scheduled"; "deadline"; "no-status"; "no-priority"; "no-scheduled"; "no-deadline"; |] else if path2 path "upsert" "tag" then option_names [| "id"; "name"; "add-properties"; "remove-properties" |] else if path2 path "upsert" "property" then option_names [| "id"; "name"; "type"; "cardinality"; "hide"; "public" |] else if path2_any path "search" (option_names [| "block"; "page"; "property"; "tag" |]) then option_names [| "content" |] else if path2 path "query" "list" then Vec.empty else if path1 path "query" then option_names [| "query"; "name"; "inputs" |] else if path1 path "show" then option_names [| "id"; "uuid"; "page"; "page-hierarchy"; "linked-references"; "ref-id-footer"; "level"; |] else if path2_any path "server" (option_names [| "list"; "cleanup"; "start"; "stop"; "restart" |]) then Vec.empty else if path2_any path "sync" (option_names [| "status"; "stop"; "remote-graphs" |]) || path3_any path "sync" "config" (option_names [| "get"; "unset" |]) then Vec.empty else if path2_any path "sync" (option_names [| "start"; "upload" |]) then option_names [| "e2ee-password" |] else if path2 path "sync" "download" then option_names [| "progress"; "e2ee-password" |] else if path3 path "sync" "asset" "download" then option_names [| "id"; "uuid" |] else if path2 path "sync" "ensure-keys" then option_names [| "e2ee-password"; "upload-keys" |] else if path2 path "sync" "grant-access" then option_names [| "graph-id"; "email" |] else if path3 path "sync" "config" "set" then Vec.empty else if path2 path "debug" "pull" then option_names [| "id"; "uuid"; "ident" |] else if path1 path "doctor" then option_names [| "dev-script" |] else if path1 path "login" || path1 path "logout" || path2 path "skill" "show" || path1 path "example" then Vec.empty else if path1 path "completion" then option_names [| "shell" |] else if path2 path "skill" "install" then option_names [| "global" |] else Vec.empty let validate_known_options path options = let allowed = Vec.sort_uniq String.compare (Vec.append global_options (allowed_options_for_path path)) in match Vec.find_opt (fun (key, _) -> not (Vec.mem key allowed)) options with | Some (key, _) -> Error (Error.invalid_options ("Unknown option: :" ^ key)) | None -> Ok () let invalid_value key value message = Error.invalid_options ("Invalid value for option :" ^ key ^ ": " ^ value ^ ". " ^ message) let validate_integer_option key options = match Vec.assoc_opt key options with | Some (Some value) when Int64.of_string_opt value = None -> Error (invalid_value key value "Expected integer") | _ -> Ok () let validate_integer_options keys options = Vec.fold_left (fun result key -> Error.bind result (fun () -> validate_integer_option key options)) (Ok ()) keys let validate_boolean_option_values options = match Vec.find_opt (fun (key, value) -> boolean_option key && match value with | Some value -> not (boolean_literal value) | None -> false) options with | Some (key, Some value) -> Error (invalid_value key value "Expected true or false") | _ -> Ok () let validate_member_option key values options = match option_value key options with | Some value when not (Vec.mem value values) -> Error (invalid_value key value ("Available values: " ^ Vec.string_concat ", " values)) | _ -> Ok () let validate_csv_member_option key values options = match option_value key options with | Some value -> ( match Vec.find_opt (fun field -> not (Vec.mem field values)) (parse_csv value) with | Some invalid -> Error (invalid_value key invalid ("Available values: " ^ Vec.string_concat ", " values)) | None -> Ok ()) | None -> Ok () let validate_time_option key options = match option_value key options with | Some value when Option.is_none (Time.parse_time value) -> Error (invalid_value key value "Expected RFC3339 datetime or epoch ms") | _ -> Ok () let list_page_sort_values = Vec.of_array [| "id"; "ident"; "title"; "uuid"; "created-at"; "updated-at" |] let list_tag_sort_values = Vec.append_array list_page_sort_values [| "properties"; "extends"; "description" |] let list_property_sort_values = Vec.append_array list_page_sort_values [| "classes"; "type"; "cardinality"; "description" |] let list_task_sort_values = Vec.of_array [| "id"; "title"; "status"; "priority"; "scheduled"; "deadline"; "updated-at"; "created-at"; |] let list_node_sort_values = Vec.of_array [| "id"; "title"; "type"; "page-id"; "page-title"; "created-at"; "updated-at"; |] let list_asset_sort_values = Vec.of_array [| "id"; "title"; "asset-type"; "size"; "updated-at"; "created-at" |] let validate_list_common_values sort_values options = Error.bind (validate_integer_option "limit" options) (fun () -> Error.bind (validate_integer_option "offset" options) (fun () -> Error.bind (validate_member_option "order" (Vec.of_array [| "asc"; "desc" |]) options) (fun () -> Error.bind (validate_member_option "sort" sort_values options) (fun () -> validate_csv_member_option "fields" sort_values options)))) let validate_global_values options = Error.bind (validate_integer_option "timeout-ms" options) (fun () -> validate_member_option "output" (Vec.of_array [| "human"; "json"; "edn" |]) options) let parse_graph_export_edn_options options = match option_value "edn-options" options with | None -> Ok None | Some text -> ( try let value = Melange_edn_melange.of_edn_string text in match Edn_util.as_map value with | Some _ -> Ok (Some value) | None -> Error (Error.invalid_options "graph export --edn-options must be an EDN map") with Melange_edn_melange.Parse_error _ -> Error (Error.invalid_options "graph export --edn-options must be an EDN map") ) let validate_non_empty_csv_option key message options = match Vec.assoc_opt key options with | Some (Some value) when Vec.is_empty (parse_csv value) -> Error (Error.invalid_options message) | Some None -> Error (Error.invalid_options message) | _ -> Ok () let validate_list_node_values options = Error.bind (validate_non_empty_csv_option "tags" "list node --tags must include at least one non-empty value" options) (fun () -> validate_non_empty_csv_option "properties" "list node --properties must include at least one non-empty value" options) let validate_pos_value options = validate_member_option "pos" (Vec.of_array [| "first-child"; "last-child"; "sibling" |]) options let validate_selector_integer_values path options = if path2_any path "remove" (option_names [| "page"; "tag"; "property" |]) || path2_any path "upsert" (option_names [| "page"; "tag"; "property" |]) || path3 path "sync" "asset" "download" || path2 path "debug" "pull" then validate_integer_option "id" options else if path2_any path "upsert" (option_names [| "block"; "asset"; "task" |]) then validate_integer_options (option_names [| "id"; "target-id" |]) options else Ok () let validate_option_values path options = Error.bind (validate_selector_integer_values path options) (fun () -> if path2 path "graph" "export" then Ok () else if path2 path "list" "page" then Error.bind (validate_list_common_values list_page_sort_values options) (fun () -> Error.bind (validate_time_option "updated-after" options) (fun () -> validate_time_option "created-after" options)) else if path2 path "list" "tag" then validate_list_common_values list_tag_sort_values options else if path2 path "list" "property" then validate_list_common_values list_property_sort_values options else if path2 path "list" "task" then validate_list_common_values list_task_sort_values options else if path2 path "list" "node" then Error.bind (validate_list_node_values options) (fun () -> validate_list_common_values list_node_sort_values options) else if path2 path "list" "asset" then validate_list_common_values list_asset_sort_values options else if path1 path "show" then validate_integer_option "level" options else if path1 path "completion" then validate_member_option "shell" (Vec.of_array [| "zsh"; "bash" |]) options else if path2 path "upsert" "task" then Error.bind (validate_pos_value options) (fun () -> Error.bind (validate_time_option "scheduled" options) (fun () -> validate_time_option "deadline" options)) else if path2 path "upsert" "block" || path2 path "upsert" "asset" then validate_pos_value options else Ok ()) let validate_options path options = Error.bind (validate_known_options path options) (fun () -> Error.bind (validate_boolean_option_values options) (fun () -> Error.bind (validate_global_values options) (fun () -> validate_option_values path options))) let sync_config_key_or_error = function | None -> Ok None | Some raw -> ( match Sync.config_key_of_string (String.lowercase_ascii (String.trim raw)) with | Some key -> Ok (Some key) | None -> Error (Error.invalid_options ("unknown config key: " ^ raw))) let parsed_remove_command options = function | "block" -> Some (Cli_request.Remove (Remove.Parsed_block { id_raw = option_value "id" options; uuid = option_value "uuid" options; })) | "page" -> Some (Remove (Parsed_page { id = int64_option "id" options; page = option_value "page" options; })) | "tag" -> Some (Remove (Parsed_tag { id = int64_option "id" options; name = option_value "name" options; })) | "property" -> Some (Remove (Parsed_property { id = int64_option "id" options; name = option_value "name" options; })) | _ -> None let content_option_or_args options args = match option_value "content" options with | Some _ as content -> content | None when not (Vec.is_empty args) -> Some (Vec.string_concat " " args) | None -> None let parsed_upsert_command ?(args = Vec.empty) options = function | "block" -> Some (Cli_request.Upsert (Upsert.Parsed_block { id = int64_option "id" options; uuid = option_value "uuid" options; target_id = int64_option "target-id" options; target_uuid = option_value "target-uuid" options; target_page = option_value "target-page" options; pos = Option.bind (option_value "pos" options) Block.position_of_string; content = content_option_or_args options args; blocks_edn = option_value "blocks" options; blocks_file = option_value "blocks-file" options; update_tags_edn = option_value "update-tags" options; update_properties_edn = option_value "update-properties" options; remove_tags_edn = option_value "remove-tags" options; remove_properties_edn = option_value "remove-properties" options; })) | "page" -> Some (Upsert (Parsed_page { id = int64_option "id" options; page = option_value "page" options; restore = option_present "restore" options; update_tags_edn = option_value "update-tags" options; update_properties_edn = option_value "update-properties" options; remove_tags_edn = option_value "remove-tags" options; remove_properties_edn = option_value "remove-properties" options; })) | "asset" -> Some (Upsert (Parsed_asset { id = int64_option "id" options; uuid = option_value "uuid" options; path = option_value "path" options; target_id = int64_option "target-id" options; target_uuid = option_value "target-uuid" options; target_page = option_value "target-page" options; pos = Option.bind (option_value "pos" options) Block.position_of_string; content = option_value "content" options; })) | "task" -> Some (Upsert (Parsed_task { id = int64_option "id" options; uuid = option_value "uuid" options; page = option_value "page" options; content = option_value "content" options; target_id = int64_option "target-id" options; target_uuid = option_value "target-uuid" options; target_page = option_value "target-page" options; pos = Option.bind (option_value "pos" options) Block.position_of_string; status = option_value "status" options; priority = option_value "priority" options; scheduled = time_option "scheduled" options; deadline = time_option "deadline" options; no_status = option_present "no-status" options; no_priority = option_present "no-priority" options; no_scheduled = option_present "no-scheduled" options; no_deadline = option_present "no-deadline" options; })) | "tag" -> Some (Upsert (Parsed_tag { id = int64_option "id" options; name = option_value "name" options; add_properties_edn = option_value "add-properties" options; remove_properties_edn = option_value "remove-properties" options; })) | "property" -> Some (Upsert (Parsed_property { id = int64_option "id" options; name = option_value "name" options; kind = Option.bind (option_value "type" options) Property.kind_of_string; cardinality = Option.bind (option_value "cardinality" options) Property.cardinality_of_string; hide = bool_option_value "hide" options; public = bool_option_value "public" options; })) | _ -> None let parsed_list_command options = function | "page" -> Some (Cli_request.List (List_command.Parsed_page { common = common_list_opts options; expand = option_present "expand" options; include_built_in = optional_flag "include-built-in" options; include_journal = optional_flag "include-journal" options; journal_only = option_present "journal-only" options; include_hidden = option_present "include-hidden" options; updated_after = time_option "updated-after" options; created_after = time_option "created-after" options; })) | "tag" -> Some (List (Parsed_tag { common = common_list_opts options; expand = option_present "expand" options; include_built_in = optional_flag "include-built-in" options; with_properties = option_present "with-properties" options; with_extends = option_present "with-extends" options; })) | "property" -> Some (List (Parsed_property { common = common_list_opts options; expand = option_present "expand" options; include_built_in = optional_flag "include-built-in" options; with_classes = option_present "with-classes" options; with_type = true; })) | "task" -> Some (List (Parsed_task { common = common_list_opts options; status = option_value "status" options; priority = option_value "priority" options; content = option_value "content" options; })) | "node" -> Some (List (Parsed_node { common = common_list_opts options; tags = Option.value (Option.map parse_csv (option_value "tags" options)) ~default:Vec.empty; properties = Option.value (Option.map parse_csv (option_value "properties" options)) ~default:Vec.empty; })) | "asset" -> Some (List (Parsed_asset { common = common_list_opts options })) | _ -> None (* normalize_key resolves short aliases globally (-f -> fields, -e -> edn-options), but a few legacy aliases mean something else under specific commands. Once the command path is known, rewrite those raw tokens and re-parse so the alias lands on the right option. *) let path_alias_overrides = function | [| "graph"; "validate" |] -> [ ("-f", "--fix") ] | [| "graph"; "export" |] -> [ ("-f", "--file") ] | [| "list"; ("page" | "tag" | "property") |] -> [ ("-e", "--expand") ] | _ -> [] (* rewrite a bare alias token, or its -x=value form *) let apply_alias_override overrides token = match List.assoc_opt token overrides with | Some replacement -> replacement | None -> ( match String.index_opt token '=' with | None -> token | Some index -> ( match List.assoc_opt (String.sub token 0 index) overrides with | Some replacement -> replacement ^ String.sub token index (String.length token - index) | None -> token)) let parse ?stdin argv = let options, positional = parse_tokens argv in let positional_array = Vec.to_array positional in let options = match path_alias_overrides positional_array with | [] -> options | overrides -> argv |> Vec.map (apply_alias_override overrides) |> parse_tokens |> fst in let positional_tail start = Vec.init (max 0 (Array.length positional_array - start)) (fun index -> positional_array.(index + start)) in let make_vec path command = Error.bind (validate_options path options) (fun () -> let globals = globals_of_options options in Ok (Cli_request.make ~globals ~path ~command ~raw_args:argv)) in let make path command = make_vec (Vec.of_array path) command in if option_present "version" options then make [||] Cli_request.Version else if Array.length positional_array >= 2 && positional_array.(0) = "upsert" && positional_array.(1) = "block" then match parsed_upsert_command ~args:(positional_tail 2) options "block" with | Some command -> make [| "upsert"; "block" |] command | None -> Error (Error.unknown_command "unknown command: upsert block") else if Array.length positional_array >= 1 && positional_array.(0) = "example" then let selector = positional_tail 1 in make_vec (Vec.push_front selector "example") (Example (Example.Parsed_example { selector })) else match positional_array with | [| "graph"; "list" |] -> make [| "graph"; "list" |] (Graph Graph.Parsed_list) | [| "graph"; "create" |] -> make [| "graph"; "create" |] (Graph (Parsed_create { enable_sync = option_present "enable-sync" options; e2ee_password = option_value "e2ee-password" options; })) | [| "graph"; "switch" |] -> make [| "graph"; "switch" |] (Graph Parsed_switch) | [| "graph"; "remove" |] -> make [| "graph"; "remove" |] (Graph Parsed_remove) | [| "graph"; "validate" |] -> make [| "graph"; "validate" |] (Graph (Parsed_validate { fix = option_present "fix" options })) | [| "graph"; "info" |] -> make [| "graph"; "info" |] (Graph Parsed_info) | [| "graph"; "backup"; "list" |] -> make [| "graph"; "backup"; "list" |] (Graph Parsed_backup_list) | [| "graph"; "backup"; "create" |] -> make [| "graph"; "backup"; "create" |] (Graph (Parsed_backup_create { name = option_value "name" options })) | [| "graph"; "backup"; "restore" |] -> ( match (option_value "src" options, option_value "dst" options) with | Some src, Some dst -> make [| "graph"; "backup"; "restore" |] (Graph (Parsed_backup_restore { src; dst = Cli_primitive.create_graph dst })) | None, _ -> Error (Error.invalid_options "graph backup restore --src is required") | _, None -> Error (Error.invalid_options "graph backup restore --dst is required")) | [| "graph"; "backup"; "remove" |] -> ( match option_value "src" options with | Some src -> make [| "graph"; "backup"; "remove" |] (Graph (Parsed_backup_remove { src })) | None -> Error (Error.invalid_options "graph backup remove --src is required")) | [| "graph"; "export" |] -> ( match graph_export_type options with | Some export_type -> Error.bind (parse_graph_export_edn_options options) (fun edn_options -> make [| "graph"; "export" |] (Graph (Parsed_export { export_type; file = option_value "file" options; edn_options; pretty_print = option_present "pretty-print" options; include_timestamps = false; exclude_built_in_pages = false; exclude_namespaces = Vec.empty; }))) | None -> Error (Error.invalid_options "graph export --type must be edn or sqlite") ) | [| "graph"; "import" |] -> ( match (graph_import_type options, option_value "input" options) with | Some import_type, Some input -> make [| "graph"; "import" |] (Graph (Parsed_import { import_type; input })) | None, _ -> Error (Error.invalid_options "graph import --type must be edn or sqlite") | _, None -> Error (Error.invalid_options "graph import --input is required")) | [| "list"; kind |] -> ( match parsed_list_command options kind with | Some command -> make [| "list"; kind |] command | None -> Error (Error.unknown_command ("unknown command: list " ^ kind))) | [| "remove"; kind |] -> ( match parsed_remove_command options kind with | Some command -> make [| "remove"; kind |] command | None -> Error (Error.unknown_command ("unknown command: remove " ^ kind))) | [| "upsert"; kind |] -> ( match parsed_upsert_command options kind with | Some command -> make [| "upsert"; kind |] command | None -> Error (Error.unknown_command ("unknown command: upsert " ^ kind))) | [| "search"; "block" |] -> make [| "search"; "block" |] (Search (Search.Parsed_block { content = Option.value (option_value "content" options) ~default:""; })) | [| "search"; "page" |] -> make [| "search"; "page" |] (Search (Search.Parsed_page { content = Option.value (option_value "content" options) ~default:""; })) | [| "search"; "property" |] -> make [| "search"; "property" |] (Search (Search.Parsed_property { content = Option.value (option_value "content" options) ~default:""; })) | [| "search"; "tag" |] -> make [| "search"; "tag" |] (Search (Search.Parsed_tag { content = Option.value (option_value "content" options) ~default:""; })) | [| "query"; "list" |] -> make [| "query"; "list" |] (Query Query.Parsed_list) | [| "query" |] -> make [| "query" |] (Query (Query.Parsed_run { query_edn = option_value "query" options; name = option_value "name" options; inputs_edn = option_value "inputs" options; })) | [| "show" |] -> let stdin_id = if option_present "id" options && Option.is_none (option_value "id" options) then stdin else None in make [| "show" |] (Show (Show.Parsed_show { id_raw = option_value "id" options; uuid = option_value "uuid" options; page = option_value "page" options; page_hierarchy = Option.value (bool_option_value "page-hierarchy" options) ~default:false; linked_references = optional_flag "linked-references" options; ref_id_footer = optional_flag "ref-id-footer" options; level = int_option "level" options; stdin_id; })) | [| "server"; "list" |] -> make [| "server"; "list" |] (Server Server_command.Parsed_list) | [| "server"; "cleanup" |] -> make [| "server"; "cleanup" |] (Server Parsed_cleanup) | [| "server"; "start" |] -> make [| "server"; "start" |] (Server Parsed_start) | [| "server"; "stop" |] -> make [| "server"; "stop" |] (Server Parsed_stop) | [| "server"; "restart" |] -> make [| "server"; "restart" |] (Server Parsed_restart) | [| "sync"; "status" |] -> make [| "sync"; "status" |] (Sync Sync.Parsed_status) | [| "sync"; "start" |] -> make [| "sync"; "start" |] (Sync (Sync.Parsed_start { e2ee_password = option_value "e2ee-password" options })) | [| "sync"; "stop" |] -> make [| "sync"; "stop" |] (Sync Sync.Parsed_stop) | [| "sync"; "upload" |] -> make [| "sync"; "upload" |] (Sync (Sync.Parsed_upload { e2ee_password = option_value "e2ee-password" options })) | [| "sync"; "download" |] -> make [| "sync"; "download" |] (Sync (Sync.Parsed_download { progress = (if option_present "progress" options then Some true else None); e2ee_password = option_value "e2ee-password" options; })) | [| "sync"; "asset"; "download" |] -> make [| "sync"; "asset"; "download" |] (Sync (Sync.Parsed_asset_download { id = int64_option "id" options; uuid = option_value "uuid" options; })) | [| "sync"; "remote-graphs" |] -> make [| "sync"; "remote-graphs" |] (Sync Sync.Parsed_remote_graphs) | [| "sync"; "ensure-keys" |] -> make [| "sync"; "ensure-keys" |] (Sync (Sync.Parsed_ensure_keys { e2ee_password = option_value "e2ee-password" options; upload_keys = option_present "upload-keys" options; })) | [| "sync"; "grant-access" |] -> make [| "sync"; "grant-access" |] (Sync (Sync.Parsed_grant_access { graph_id = option_value "graph-id" options; email = option_value "email" options; })) | [| "sync"; "config"; "get"; key |] -> ( match sync_config_key_or_error (Some key) with | Ok key -> make [| "sync"; "config"; "get" |] (Sync (Sync.Parsed_config_get { key })) | Error err -> Error err) | [| "sync"; "config"; "get" |] -> make [| "sync"; "config"; "get" |] (Sync (Sync.Parsed_config_get { key = None })) | [| "sync"; "config"; "set"; key; value |] -> ( match sync_config_key_or_error (Some key) with | Ok key -> make [| "sync"; "config"; "set" |] (Sync (Sync.Parsed_config_set { key; value = Some value })) | Error err -> Error err) | [| "sync"; "config"; "set"; key |] -> ( match sync_config_key_or_error (Some key) with | Ok key -> make [| "sync"; "config"; "set" |] (Sync (Sync.Parsed_config_set { key; value = None })) | Error err -> Error err) | [| "sync"; "config"; "set" |] -> make [| "sync"; "config"; "set" |] (Sync (Sync.Parsed_config_set { key = None; value = None })) | [| "sync"; "config"; "unset"; key |] -> ( match sync_config_key_or_error (Some key) with | Ok key -> make [| "sync"; "config"; "unset" |] (Sync (Sync.Parsed_config_unset { key })) | Error err -> Error err) | [| "sync"; "config"; "unset" |] -> make [| "sync"; "config"; "unset" |] (Sync (Sync.Parsed_config_unset { key = None })) | [| "debug"; "pull" |] -> ( match Option.map Debug.parse_ident_option (option_value "ident" options) with | Some (Error err) -> Error err | ident -> make [| "debug"; "pull" |] (Debug (Debug.Parsed_pull { id = int64_option "id" options; uuid = option_value "uuid" options; ident = Option.bind ident (function | Ok ident -> Some ident | Error _ -> None); }))) | [| "agent"; "bridge" |] -> make [| "agent"; "bridge" |] (Agent Agent.Parsed_bridge) | [| "doctor" |] -> make [| "doctor" |] (Doctor (Doctor.Parsed_doctor { dev_script = option_present "dev-script" options })) | [| "login" |] -> make [| "login" |] (Auth Auth_command.Parsed_login) | [| "logout" |] -> make [| "logout" |] (Auth Auth_command.Parsed_logout) | [| "skill"; "show" |] -> make [| "skill"; "show" |] (Skill Skill.Parsed_show) | [| "skill"; "install" |] -> make [| "skill"; "install" |] (Skill (Skill.Parsed_install { global = option_present "global" options })) | [| "completion"; shell |] -> ( match Cli_primitive.shell_of_string shell with | Some shell -> make [| "completion" |] (Completion (Completion.Parsed_completion { shell = Some shell })) | None -> Error (Error.invalid_options ("unsupported shell: " ^ shell ^ "; expected zsh or bash"))) | [| "completion" |] -> let shell = match option_value "shell" options with | None -> Error (Error.invalid_options "completion shell is required; expected zsh or bash") | Some shell -> ( match Cli_primitive.shell_of_string shell with | Some shell -> Ok (Some shell) | None -> Error (Error.invalid_options ("unsupported shell: " ^ shell ^ "; expected zsh or bash")) ) in Error.bind shell (fun shell -> make [| "completion" |] (Completion (Completion.Parsed_completion { shell }))) | [||] -> Error (Error.unknown_command "") | _ -> Error (Error.unknown_command ("unknown command: " ^ Vec.string_concat " " positional))