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>
250 lines
8.3 KiB
OCaml
250 lines
8.3 KiB
OCaml
type install_opts = { global : bool }
|
|
type parsed = Parsed_show | Parsed_install of install_opts
|
|
|
|
type action =
|
|
| Skill_show of { source_path : Cli_primitive.path option }
|
|
| Skill_install of {
|
|
global : bool;
|
|
source_path : Cli_primitive.path option;
|
|
destination_dir : Cli_primitive.path option;
|
|
destination_file : Cli_primitive.path option;
|
|
}
|
|
|
|
type install_target = {
|
|
scope : Cli_primitive.keyword;
|
|
path : Cli_primitive.path;
|
|
update_command : string;
|
|
}
|
|
|
|
type update_status = {
|
|
installed : bool;
|
|
outdated : bool;
|
|
outdated_targets : install_target Rrbvec.t;
|
|
error : Error.t option;
|
|
}
|
|
|
|
let skill_dir_name = "logseq-cli"
|
|
let skill_file_name = "SKILL.md"
|
|
|
|
let skill_file_under base =
|
|
Filename.concat
|
|
(Filename.concat
|
|
(Filename.concat (Filename.concat base ".agents") "skills")
|
|
skill_dir_name)
|
|
skill_file_name
|
|
|
|
external process_argv : string array = "argv" [@@mel.module "process"]
|
|
|
|
let executable_dir_candidates () =
|
|
let candidates_for_script script =
|
|
let script_dir = Filename.dirname script in
|
|
Vec.of_array
|
|
[|
|
|
skill_file_under script_dir;
|
|
skill_file_under (Filename.dirname script_dir);
|
|
|]
|
|
in
|
|
if Array.length process_argv >= 2 then candidates_for_script process_argv.(1)
|
|
else if Array.length process_argv = 1 then
|
|
candidates_for_script process_argv.(0)
|
|
else Vec.empty
|
|
|
|
let non_empty value =
|
|
match Option.map String.trim value with
|
|
| Some "" | None -> None
|
|
| Some value -> Some value
|
|
|
|
let resolve_install_target ~global ~cwd ~home_dir =
|
|
match (global, non_empty home_dir) with
|
|
| true, None ->
|
|
Error
|
|
(Error.make Error.Skill_home_dir_unavailable
|
|
"home directory is unavailable; cannot resolve --global install \
|
|
target")
|
|
| _ ->
|
|
let base =
|
|
if global then Option.value (non_empty home_dir) ~default:cwd else cwd
|
|
in
|
|
Ok
|
|
{
|
|
scope = Edn_util.keyword_t (if global then "global" else "local");
|
|
path = skill_file_under base;
|
|
update_command =
|
|
(if global then "logseq skill install --global"
|
|
else "logseq skill install");
|
|
}
|
|
|
|
let installed_skill_targets ~cwd ~home_dir =
|
|
let local = resolve_install_target ~global:false ~cwd ~home_dir in
|
|
let global = resolve_install_target ~global:true ~cwd ~home_dir in
|
|
Vec.of_array [| local; global |]
|
|
|> Vec.filter_map (function Ok target -> Some target | Error _ -> None)
|
|
|
|
let read_file = Cli_unix.read_text_file
|
|
|
|
let rec mkdir_p path =
|
|
if path = "" || path = Filename.dirname path || Cli_unix.file_exists path then
|
|
()
|
|
else (
|
|
mkdir_p (Filename.dirname path);
|
|
Cli_unix.mkdir path 0o755)
|
|
|
|
let write_file path content =
|
|
mkdir_p (Filename.dirname path);
|
|
Cli_unix.write_text_file path content
|
|
|
|
let default_source_candidates config =
|
|
let executable = executable_dir_candidates () in
|
|
let project =
|
|
match config.Cli_config.project_dir with
|
|
| Some project_dir -> Vec.singleton (skill_file_under project_dir)
|
|
| None -> Vec.empty
|
|
in
|
|
let home =
|
|
match Sys.getenv_opt "HOME" with
|
|
| Some home -> Vec.singleton (skill_file_under home)
|
|
| None -> Vec.empty
|
|
in
|
|
Vec.append executable project |> fun candidates ->
|
|
Vec.append candidates home |> fun candidates ->
|
|
Vec.push_back candidates (skill_file_under (Sys.getcwd ()))
|
|
|
|
let resolve_source_path ?source_path config =
|
|
match non_empty source_path with
|
|
| Some path when Cli_unix.file_exists path -> Ok path
|
|
| Some path ->
|
|
Error
|
|
(Error.make
|
|
~context:
|
|
(Edn_util.vector_vec (Vec.of_array [| Edn_util.string path |]))
|
|
Error.Skill_source_not_found
|
|
("skill source file not found: " ^ path))
|
|
| None -> (
|
|
let candidates = default_source_candidates config in
|
|
match Vec.find_opt Cli_unix.file_exists candidates with
|
|
| Some path -> Ok path
|
|
| None ->
|
|
Error
|
|
(Error.make
|
|
~context:
|
|
(Edn_util.vector_vec
|
|
(candidates |> Vec.map (fun path -> Edn_util.string path)))
|
|
Error.Skill_source_not_found
|
|
("skill source file not found. Checked paths: "
|
|
^ Vec.string_concat ", " candidates)))
|
|
|
|
let command_id = function
|
|
| Parsed_show -> Command_id.Skill_show
|
|
| Parsed_install _ -> Skill_install
|
|
|
|
let validate_parsed _ = Ok ()
|
|
|
|
let build ?registry:_ config _globals = function
|
|
| Parsed_show -> Ok (Skill_show { source_path = None })
|
|
| Parsed_install { global } ->
|
|
let cwd =
|
|
Option.value config.Cli_config.project_dir ~default:(Sys.getcwd ())
|
|
in
|
|
let home_dir = Sys.getenv_opt "HOME" in
|
|
Error.bind (resolve_install_target ~global ~cwd ~home_dir) (fun target ->
|
|
Ok
|
|
(Skill_install
|
|
{
|
|
global;
|
|
source_path = None;
|
|
destination_dir = Some (Filename.dirname target.path);
|
|
destination_file = Some target.path;
|
|
}))
|
|
|
|
let target_from_action config global destination_dir destination_file =
|
|
match (destination_dir, destination_file) with
|
|
| Some dir, Some file -> Ok (dir, file)
|
|
| _ ->
|
|
let cwd =
|
|
Option.value config.Cli_config.project_dir ~default:(Sys.getcwd ())
|
|
in
|
|
Error.bind
|
|
(resolve_install_target ~global ~cwd ~home_dir:(Sys.getenv_opt "HOME"))
|
|
(fun target -> Ok (Filename.dirname target.path, target.path))
|
|
|
|
let execute_show config mode source_path =
|
|
let command = Command_id.Skill_show in
|
|
match resolve_source_path ?source_path config with
|
|
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
|
| Ok path -> (
|
|
match read_file path with
|
|
| content ->
|
|
Cli_effect.pure (Cli_result.ok ~command mode (Message content))
|
|
| exception exn ->
|
|
Cli_effect.pure
|
|
(Output_mode.error ~command mode
|
|
(Error.make Error.Skill_show_failed
|
|
("failed to read skill source: " ^ Printexc.to_string exn))))
|
|
|
|
let install_value ~source ~file =
|
|
Edn_util.map_vec
|
|
(Vec.of_array
|
|
[|
|
|
(Edn_util.keyword "source-path", Edn_util.string source);
|
|
(Edn_util.keyword "installed-path", Edn_util.string file);
|
|
( Edn_util.keyword "message",
|
|
Edn_util.string ("Installed skill to " ^ file) );
|
|
|])
|
|
|
|
let execute_install config mode ~global ~source_path ~destination_dir
|
|
~destination_file =
|
|
let command = Command_id.Skill_install in
|
|
match resolve_source_path ?source_path config with
|
|
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
|
| Ok source -> (
|
|
match
|
|
target_from_action config global destination_dir destination_file
|
|
with
|
|
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
|
| Ok (dir, file) -> (
|
|
try
|
|
if Cli_unix.file_exists dir && not (Cli_unix.is_directory dir) then
|
|
raise (Invalid_argument ("destination path is a file: " ^ dir));
|
|
let content = read_file source in
|
|
write_file file content;
|
|
Cli_effect.pure
|
|
(Cli_result.ok ~command mode (Raw (install_value ~source ~file)))
|
|
with exn ->
|
|
Cli_effect.pure
|
|
(Output_mode.error ~command mode
|
|
(Error.make Error.Skill_install_failed
|
|
("failed to install skill: " ^ Printexc.to_string exn)))))
|
|
|
|
let execute_with_mode action config mode =
|
|
match action with
|
|
| Skill_show { source_path } -> execute_show config mode source_path
|
|
| Skill_install { global; source_path; destination_dir; destination_file } ->
|
|
execute_install config mode ~global ~source_path ~destination_dir
|
|
~destination_file
|
|
|
|
let meta id doc =
|
|
{
|
|
Command_registry.id;
|
|
path = Command_id.to_path id;
|
|
doc;
|
|
long_doc = None;
|
|
examples = Vec.empty;
|
|
options = Vec.empty;
|
|
category = Command_registry.Utilities;
|
|
requires_graph = Command_id.requires_graph id;
|
|
requires_auth = Command_id.requires_auth id;
|
|
write_command = Command_id.is_write id;
|
|
human_table_headers_order = Vec.empty;
|
|
}
|
|
|
|
let metadata () =
|
|
Vec.of_array
|
|
[|
|
|
meta Command_id.Skill_show "Show built-in logseq-cli skill";
|
|
meta Skill_install "Install built-in logseq-cli skill";
|
|
|]
|
|
|
|
let execute action config =
|
|
let (Output.Mode.Packed mode) = Output_mode.for_config config in
|
|
execute_with_mode action config mode
|