defmodule Cmd.Config.Approvals do @moduledoc false alias Cmd.Config.Utils @spec run(map(), list(), list()) :: :ok def run(opts, [:approvals], _unknown) do cond do opts[:global] && opts[:project] -> build_list() |> SafeJson.encode!(pretty: true) |> UI.puts() opts[:global] -> build_list(:global) |> SafeJson.encode!(pretty: true) |> UI.puts() true -> case Settings.get_selected_project() do {:ok, _proj} -> build_list(:project) |> SafeJson.encode!(pretty: true) |> UI.puts() {:error, _} -> UI.error("Project not specified or not found") end end end # Unified approve entry def run(opts, [:approve], args) do cond do opts[:global] && opts[:project] -> UI.error("Cannot use both --global and --project.") is_nil(opts[:kind]) -> UI.error("Missing --kind option.") true -> case Utils.require_key(opts, args, :pattern, "Pattern") do {:error, msg} -> UI.error(msg) {:ok, pattern} -> case normalize_pattern(opts[:kind], pattern) do {:error, msg} -> UI.error(msg) {kind, pattern} -> # determine scope from --global flag or default to project scope = if opts[:global], do: :global, else: :project if scope == :project && opts[:project], do: Settings.set_project(opts[:project]) settings = Settings.new() case build_approve(settings, scope, kind, pattern) do {:ok, data} -> data |> SafeJson.encode!(pretty: true) |> UI.puts() {:error, err} -> UI.error(err) end end end end end # Helpers # ---------------------------------------------------------------------------- defp build_list(:global) do Settings.new() |> Settings.Approvals.get_approvals(:global) end defp build_list(:project) do Settings.new() |> Settings.Approvals.get_approvals(:project) end defp build_list() do global = build_list(:global) project = build_list(:project) Enum.concat([ Map.keys(global), Map.keys(project) ]) |> Enum.uniq() |> Enum.map(fn kind -> { kind, %{ global: Map.get(global, kind, []), project: Map.get(project, kind, []) } } end) |> Enum.into(%{}) end # Interpret /pattern/ under --kind shell as full-command regex defp normalize_pattern("shell", pat) when is_binary(pat) do if String.starts_with?(pat, "/") and String.ends_with?(pat, "/") do # Strip leading and trailing slash; use explicit step to avoid negative step warning inner = String.slice(pat, 1..-2//1) # Return error for empty inner regex if inner == "" do {:error, "Empty regex is not allowed"} else {"shell_full", inner} end else {"shell", pat} end end defp normalize_pattern(kind, pat), do: {kind, pat} defp build_approve(settings, scope, kind, pattern) do if kind == "shell_full" and pattern == "" do {:error, "Empty regex is not allowed"} else # validate that the pattern is a valid regex case Regex.compile(pattern, "u") do {:error, reason} -> # reason is a tuple {message, position} msg = elem(reason, 0) msg_str = to_string(msg) {:error, "Invalid regex: #{msg_str}"} {:ok, _regex} -> # add the approved prefix and output updated patterns new_settings = Settings.Approvals.approve(settings, scope, kind, pattern) patterns = Settings.Approvals.get_approvals(new_settings, scope, kind) {:ok, %{kind => patterns}} end end end end