Packages
fnord
0.8.71
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/settings.ex
defmodule Settings do
require Logger
# ----------------------------------------------------------------------------
# Settings type
# ----------------------------------------------------------------------------
defstruct [:path, :data]
@type t :: %__MODULE__{
path: binary,
data: map
}
@spec new() :: t
def new() do
Settings.Migrate.cleanup_default_project_dir()
path = settings_file()
Settings.Migrate.maybe_migrate_settings(path)
settings =
%Settings{path: path}
|> slurp()
settings
end
def get_running_version do
# Allow version override for testing
case Services.Globals.get_env(:fnord, :test_version_override) do
nil -> Application.spec(:fnord, :vsn) |> to_string()
version -> version
end
end
@doc """
Get the user's home directory, allowing override for testing.
"""
@spec get_user_home() :: binary | nil | no_return
def get_user_home do
Services.Globals.get_env(:fnord, :test_home_override, System.get_env("HOME"))
|> case do
nil -> raise "Could not determine user home directory. Is $HOME set?"
home -> home
end
end
@doc """
Get the path to the store root directory.
"""
@spec fnord_home() :: binary
def fnord_home() do
path = "#{get_user_home()}/.fnord"
File.mkdir_p!(path)
path
end
@doc """
Get the path to the settings file. If the file does not exist, it will be
created.
"""
@spec settings_file() :: binary
def settings_file() do
path = "#{fnord_home()}/settings.json"
if !File.exists?(path) do
write_atomic!(path, "{}")
end
path
end
@doc """
Get a value from the settings store.
"""
@spec get(t, binary, any) :: any
def get(settings, key, default \\ nil) do
key = make_key(key)
Map.get(settings.data, key, default)
end
@doc """
Set the project name for the --project option.
"""
@spec set_project(atom | binary) :: :ok
def set_project(project_name) when is_atom(project_name) do
project_name |> Atom.to_string() |> set_project()
end
def set_project(project_name) do
previous = Services.Globals.get_env(:fnord, :project)
Services.Globals.put_env(:fnord, :project, project_name)
if previous != project_name do
UI.debug("Project selected", project_name)
end
Services.Notes.load_notes()
:ok
end
@doc """
Set the quiet mode for UI output.
"""
@spec set_quiet(boolean) :: :ok
def set_quiet(quiet) do
# Force quiet mode to be a boolean
Services.Globals.put_env(:fnord, :quiet, !!quiet)
:ok
end
@doc """
Set the number of workers for concurrent operations.
"""
@spec set_workers(pos_integer) :: :ok
def set_workers(workers) when is_integer(workers) and workers > 0 do
Services.Globals.put_env(:fnord, :workers, workers)
:ok
end
@doc """
Set edit mode for the application.
"""
@spec set_edit_mode(boolean) :: :ok
def set_edit_mode(edit_mode) do
Services.Globals.put_env(:fnord, :edit_mode, !!edit_mode)
:ok
end
@doc """
Get current edit mode setting.
"""
@spec get_edit_mode() :: boolean
def get_edit_mode() do
Services.Globals.get_env(:fnord, :edit_mode, false)
end
@doc """
Set auto-approve mode for the application. In this mode, file edits
are automatically approved without user confirmation.
"""
@spec set_auto_approve(boolean) :: :ok
def set_auto_approve(auto_approve) do
Services.Globals.put_env(:fnord, :auto_approve, !!auto_approve)
:ok
end
@doc """
Get current auto-approve setting.
"""
@spec get_auto_approve() :: boolean
def get_auto_approve() do
Services.Globals.get_env(:fnord, :auto_approve, false)
end
@doc """
Set auto-approval policy for the application. This setting controls how
unattended approvals are handled.
The `policy` is a tuple consisting of an action and a timeout (or `nil` to
disable):
- `:approve` to automatically approve changes after a timeout.
- `:deny` to automatically deny changes after a timeout.
- `nil` to disable auto-approval.
The `timeout` is specified in milliseconds and determines how long to wait
before applying the auto-approval policy.
When an approval is required, the system will first send a notification to
the user after 60 seconds. If the user does not respond within the timeout
specified by the auto-approval policy, the specified action will be taken
automatically.
"""
@spec set_auto_policy({:approve | :deny, non_neg_integer} | nil) :: :ok
def set_auto_policy(policy) do
case policy do
{policy, timeout} -> Services.Globals.put_env(:fnord, :auto_policy, {policy, timeout})
nil -> Services.Globals.delete_env(:fnord, :auto_policy)
end
end
@doc """
Get current auto-approval policy setting.
"""
@spec get_auto_policy() :: {:approve, non_neg_integer} | {:deny, non_neg_integer} | nil
def get_auto_policy() do
Services.Globals.get_env(:fnord, :auto_policy, nil)
end
@doc """
Set or clear a temporary project root override path. Pass a directory path to
override or nil to clear the override.
"""
@spec set_project_root_override(binary | nil) :: :ok
def set_project_root_override(nil) do
Services.Globals.delete_env(:fnord, :project_root_override)
:ok
end
def set_project_root_override(path) when is_binary(path) do
Services.Globals.put_env(:fnord, :project_root_override, path)
:ok
end
@doc """
Gets the project root override path, or nil if not set.
"""
@spec get_project_root_override() :: binary | nil
def get_project_root_override() do
Services.Globals.get_env(:fnord, :project_root_override, nil)
end
@doc """
Check if hint docs feature is enabled. Defaults to `true` if unset.
"""
@spec get_hint_docs_enabled?() :: boolean
def get_hint_docs_enabled?() do
Services.Globals.get_env(:fnord, :hint_docs_enabled, true)
end
@doc """
Check if automatic injection of hint docs is enabled. Defaults to `true` if unset.
"""
@spec get_hint_docs_auto_inject?() :: boolean
def get_hint_docs_auto_inject?() do
Services.Globals.get_env(:fnord, :hint_docs_auto_inject, true)
end
@doc """
Check if the --project option is set.
"""
def project_is_set?() do
!is_nil(Services.Globals.get_env(:fnord, :project))
end
@doc """
Get the project specified with --project. If the project name is not set, an
error tuple is returned.
"""
@spec get_selected_project :: {:ok, binary} | {:error, :project_not_set}
def get_selected_project() do
Services.Globals.get_env(:fnord, :project)
|> case do
nil -> {:error, :project_not_set}
project -> {:ok, project}
end
end
@spec get_project(t) :: {:ok, map} | {:error, :project_not_found}
def get_project(settings) do
with {:ok, project} <- get_selected_project() do
case get_project_data(settings, project) do
nil -> {:error, :project_not_found}
data -> {:ok, data}
end
end
end
@doc """
Get project data from the settings, handling both old and new format.
"""
@spec get_project_data(t, binary) :: map | nil
def get_project_data(settings, project_name) do
projects_map = get(settings, "projects", %{})
case Map.get(projects_map, project_name) do
# Old style project data in the root of the settings object
nil -> get(settings, project_name, nil)
data -> data
end
|> case do
nil -> nil
data -> Map.put(data, "name", project_name)
end
end
@spec set_project(t, map) :: map
def set_project(settings, data) do
with {:ok, project} <- get_selected_project() do
# Validate that the project name is not a global config key
unless is_valid_project_name?(project) do
raise ArgumentError,
"Cannot use '#{project}' as project name - it conflicts with global configuration"
end
set_project_data(settings, project, data)
data
else
{:error, :project_not_set} -> raise "Project not set. Use --project to specify a project."
end
end
@doc """
Set project data in the settings using the new nested format.
"""
@spec set_project_data(t, binary, map) :: t
def set_project_data(settings, project_name, data) do
projects_map = get(settings, "projects", %{})
updated_projects_map = Map.put(projects_map, project_name, data)
update(settings, "projects", fn _ -> updated_projects_map end)
end
@doc """
Delete project data from the settings, handling both old and new format.
"""
@spec delete_project_data(t, binary) :: t
def delete_project_data(settings, project_name) do
# Delete from new nested format
projects_map = get(settings, "projects", %{})
updated_projects_map = Map.delete(projects_map, project_name)
settings = update(settings, "projects", fn _ -> updated_projects_map end)
# Also delete from old format for cleanup
update(settings, project_name, fn _ -> :delete end)
end
@spec list_projects(t) :: [binary]
def list_projects(settings) do
settings
|> get_projects()
|> Map.keys()
|> Enum.sort()
end
@spec get_root(t) :: {:ok, binary} | {:error, :not_found}
def get_root(settings) do
settings
|> get_project
|> case do
{:ok, %{"root" => root}} -> {:ok, Path.absname(root)}
{:error, _} -> {:error, :not_found}
end
end
@doc """
Get all project configurations.
Returns a map of project_name => project_config.
"""
@spec get_projects(t) :: %{binary => map}
def get_projects(settings) do
projects_map = get(settings, "projects", %{})
# Also check old format for backward compatibility
old_format_projects =
settings.data
|> Enum.filter(fn {key, value} ->
is_valid_project_name?(key) and is_map(value) and Map.has_key?(value, "root")
end)
|> Map.new()
Map.merge(old_format_projects, projects_map)
end
@doc """
Check if a given key represents a valid project name (not a global config key).
Returns false for global configuration keys like "approvals".
"""
@spec is_valid_project_name?(binary) :: boolean
def is_valid_project_name?(name) when is_binary(name) do
not Enum.member?(global_config_keys(), name)
end
def is_valid_project_name?(_), do: false
@doc """
Get the list of global configuration keys that should not be treated as project names.
"""
@spec global_config_keys() :: [binary]
def global_config_keys() do
["approvals", "projects", "version"]
end
@doc """
Check if model performance debugging is enabled via environment variable.
"""
@spec debug_models?() :: boolean
def debug_models?() do
case System.get_env("FNORD_DEBUG_MODELS") do
nil -> false
"" -> false
_ -> true
end
end
@doc """
Atomically update a top-level key in the settings file using a cross-process
lock and a read-merge-write cycle.
The updater receives the current value for `key` (or `default` when missing)
and must return the new value. If the updater returns `:delete`, the key will
be removed from the settings.
"""
@spec update(t, binary, (any -> any | :delete), any) :: t | no_return
def update(%Settings{path: path}, key, updater, default \\ %{}) do
key = make_key(key)
FileLock.with_lock(path, fn ->
data = fresh_read(path)
value = Map.get(data, key, default)
data =
value
|> updater.()
|> case do
:delete -> Map.delete(data, key)
value -> Map.put(data, key, value)
end
data
|> Jason.encode!(pretty: true)
|> then(&write_atomic!(path, &1))
%Settings{path: path, data: data}
end)
|> case do
{:ok, settings} -> settings
{:error, reason} -> raise "Failed to update settings: #{inspect(reason)}"
end
end
defp slurp(settings) do
data =
with {:ok, content} <- File.read(settings.path),
{:ok, parsed} <- Jason.decode(content) do
parsed
else
_ ->
raise """
Corrupted settings file: #{settings.path}
You may need to reset your fnord settings. Consider backing up the file
and running 'fnord init' to recreate it.
"""
end
%Settings{settings | data: data}
end
def write_atomic!(path, "") do
write_atomic!(path, "{}")
end
def write_atomic!(path, content) do
dir = Path.dirname(path)
base = Path.basename(path)
# Create temp file in the same directory to avoid cross-device rename
# issues and preserve atomicity.
tmp = Path.join(dir, ".#{base}.#{System.unique_integer([:positive])}.tmp")
File.write!(tmp, content)
File.rename!(tmp, path)
:ok
end
# Read the latest JSON from disk ignoring the cached struct data.
defp fresh_read(path) do
case File.read(path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, parsed} -> parsed
_ -> %{}
end
_ ->
%{}
end
end
defp make_key(key) when is_atom(key), do: Atom.to_string(key)
defp make_key(key), do: key
@doc """
Set yes count in settings.
"""
@spec set_yes_count(non_neg_integer) :: :ok
def set_yes_count(count) when is_integer(count) and count >= 0 do
Services.Globals.put_env(:fnord, :yes_count, count)
:ok
end
@doc """
Get yes count from settings.
"""
@spec get_yes_count() :: non_neg_integer
def get_yes_count() do
Services.Globals.get_env(:fnord, :yes_count, 0)
end
@doc """
Set fonz mode for application.
"""
@spec set_fonz_mode(boolean) :: :ok
def set_fonz_mode(fonz_mode) do
Services.Globals.put_env(:fnord, :fonz_mode, !!fonz_mode)
:ok
end
@doc """
Get fonz mode setting.
"""
@spec get_fonz_mode() :: boolean
def get_fonz_mode() do
Services.Globals.get_env(:fnord, :fonz_mode, false)
end
end