defmodule Mix.Tasks.LlmCore.Config.Set do @moduledoc """ Mutates `llm_core.toml` entries and reloads the runtime configuration. ## Examples mix llm_core.config.set --path memory.hindsight.default_bank_id --value dev-bank mix llm_core.config.set --path routing.tasks.coding.alias --value openai mix llm_core.config.set --path providers.auto.enabled --value false --type boolean Options: * `--path` (required) - dot-separated key path (e.g. `routing.tasks.coding.alias`) * `--value` - string value (interpreted via `--type`) * `--json` - raw JSON payload for the value (overrides `--value`) * `--type` - `string` (default), `integer`, `float`, `boolean` * `--file` - alternate path to `llm_core.toml` * `--reload` - reload config after writing (default true) """ use Mix.Task alias LlmCore.Config.{Editor, Loader} @shortdoc "Updates llm_core configuration" @impl true @spec run([String.t()]) :: :ok def run(args) do Mix.Task.run("app.start") {opts, _, _} = OptionParser.parse(args, switches: [ path: :string, value: :string, type: :string, json: :string, file: :string, reload: :boolean ], aliases: [p: :path] ) path = opts[:path] || Mix.raise("--path is required") value = decode_value(opts) file = opts[:file] || Editor.default_path() {:ok, new_config} = Editor.update( fn config -> segments = path |> String.split(".", trim: true) Editor.put_path(config, segments, value) end, file ) Mix.shell().info("Updated #{path} in #{file}") if Map.get(opts, :reload, true) do Loader.reload_providers(path: file) end Mix.shell().info("New value: #{inspect(get_in(new_config, path_segments(path)))}") end defp decode_value(opts) do cond do opts[:json] -> Jason.decode!(opts[:json]) true -> coerce_value(opts[:value], opts[:type]) end end defp coerce_value(nil, _type), do: Mix.raise("--value or --json is required") defp coerce_value(value, nil), do: value defp coerce_value(value, type) do case String.downcase(type) do "string" -> value "integer" -> String.to_integer(value) "int" -> String.to_integer(value) "float" -> String.to_float(value) "boolean" -> parse_boolean(value) "bool" -> parse_boolean(value) other -> Mix.raise("Unsupported --type #{other}") end end defp parse_boolean(value) do case String.downcase(value) do "true" -> true "false" -> false _ -> Mix.raise("Boolean value must be true/false") end end defp path_segments(path) do path |> String.split(".", trim: true) end end