Packages

fnord

0.9.29

AI code archaeology

Current section

Files

Jump to
fnord lib cmd config external_configs.ex
Raw

lib/cmd/config/external_configs.ex

defmodule Cmd.Config.ExternalConfigs do
@moduledoc """
CLI surface for toggling per-project support for Cursor rules, Cursor
skills, Claude Code skills, and Claude Code subagents. The underlying
persistence lives in `Settings.ExternalConfigs`; this module is the
user-facing entry point.
Four sources are supported: `cursor:rules`, `cursor:skills`,
`claude:skills`, `claude:agents`. All default to disabled; enabling any
source opts the selected project into discovery of the matching files
from both the user's home directory and the project source root.
"""
@doc """
Dispatches `fnord config external-configs` subcommands.
"""
@spec run(map(), list(), list()) :: :ok
def run(opts, command, args)
def run(opts, [:external_configs, :list], _args) do
with {:ok, project_name} <- resolve_project(opts),
:ok <- ensure_project_exists(project_name) do
project_name
|> Settings.ExternalConfigs.flags()
|> format_flags()
|> SafeJson.encode!(pretty: true)
|> UI.puts()
else
{:error, :project_not_set} ->
UI.error("Project not specified or not found")
{:error, :project_not_found} ->
UI.error("Project not found")
end
end
def run(opts, [:external_configs, :enable], args), do: toggle(opts, args, true)
def run(opts, [:external_configs, :disable], args), do: toggle(opts, args, false)
# Matches the precondition that Settings.ExternalConfigs.set/3 enforces
# for enable/disable. Without this, `list --project does-not-exist`
# silently returns the all-false defaults and the user has no way to
# tell they typed the wrong name.
defp ensure_project_exists(project_name) do
case Settings.get_project_data(Settings.new(), project_name) do
nil -> {:error, :project_not_found}
_ -> :ok
end
end
defp toggle(opts, args, value) do
with {:ok, project_name} <- resolve_project(opts),
{:ok, raw_source} <- Cmd.Config.Utils.require_key(opts, args, :source, "Source"),
{:ok, source} <- Settings.ExternalConfigs.source_from_string(raw_source),
{:ok, flags} <- Settings.ExternalConfigs.set(project_name, source, value) do
flags
|> format_flags()
|> SafeJson.encode!(pretty: true)
|> UI.puts()
else
{:error, :project_not_set} ->
UI.error("Project not specified or not found")
{:error, :project_not_found} ->
UI.error("Project not found")
{:error, {:invalid_source, raw}} ->
UI.error(
"Invalid source: #{inspect(raw)}. Valid sources: #{Enum.join(Settings.ExternalConfigs.source_strings(), ", ")}"
)
{:error, message} when is_binary(message) ->
UI.error(message)
end
end
@spec resolve_project(map()) :: {:ok, String.t()} | {:error, :project_not_set}
defp resolve_project(%{project: project_name}) when is_binary(project_name) do
Settings.set_project(project_name)
{:ok, project_name}
end
defp resolve_project(opts) when is_map(opts) do
Settings.get_selected_project()
end
@spec format_flags(Settings.ExternalConfigs.flags()) :: map()
defp format_flags(flags) do
Map.new(flags, fn {k, v} -> {Settings.ExternalConfigs.source_to_string(k), v} end)
end
end