Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib mix tasks raxol.update_hooks.ex.disabled
Raw

lib/mix/tasks/raxol.update_hooks.ex.disabled

defmodule Mix.Tasks.Raxol.UpdateHooks do
@moduledoc """
Update Raxol Git hooks to the latest version.
This task updates existing Raxol pre-commit hooks to the latest version,
preserving any custom modifications when possible.
## Usage
mix raxol.update_hooks
## Options
* `--force` - Force update without confirmation
* `--backup` - Create backup before updating
* `--check` - Check if updates are available (dry run)
## Examples
# Update hooks (interactive)
mix raxol.update_hooks
# Check for updates without installing
mix raxol.update_hooks --check
# Force update with backup
mix raxol.update_hooks --force --backup
"""
use Mix.Task
@shortdoc "Update Git hooks to latest version"
# Import the hook content from install task
alias Mix.Tasks.Raxol.InstallHooks
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
switches: [
force: :boolean,
backup: :boolean,
check: :boolean
]
)
git_dir = get_git_directory()
hooks_dir = Path.join(git_dir, "hooks")
pre_commit_path = Path.join(hooks_dir, "pre-commit")
case File.exists?(pre_commit_path) do
true ->
handle_update(pre_commit_path, opts)
false ->
IO.puts("❌ No pre-commit hook installed")
case opts[:check] do
true ->
IO.puts(" Run 'mix raxol.install_hooks' to install")
{:ok, :not_installed}
false ->
IO.puts(" Installing new hook...")
InstallHooks.run([])
end
end
end
defp get_git_directory do
case System.cmd("git", ["rev-parse", "--git-dir"], stderr_to_stdout: true) do
{output, 0} ->
String.trim(output)
_ ->
Mix.raise(
"Not in a Git repository. Please run this command from a Git repository."
)
end
end
defp handle_update(hook_path, opts) do
current_content = File.read!(hook_path)
latest_content = get_latest_hook_content()
cond do
# Already up to date
current_content == latest_content ->
IO.puts("✅ Raxol pre-commit hook is already up to date")
{:ok, :current}
# Check only mode
opts[:check] ->
check_for_updates(current_content, latest_content)
# Not a Raxol hook
not String.contains?(current_content, "mix raxol.pre_commit") ->
IO.puts("âš ī¸ Current pre-commit hook is not a Raxol hook")
IO.puts(" Run 'mix raxol.install_hooks' to install Raxol hook")
{:ok, :not_raxol}
# Custom modifications detected
has_custom_modifications?(current_content) ->
handle_custom_hook_update(
hook_path,
current_content,
latest_content,
opts
)
# Standard update
true ->
perform_update(hook_path, latest_content, opts)
end
end
defp check_for_updates(current_content, latest_content) do
case compare_versions(current_content, latest_content) do
:newer ->
IO.puts("🔄 Update available for Raxol pre-commit hook")
IO.puts(" Run 'mix raxol.update_hooks' to update")
show_changes(current_content, latest_content)
{:ok, :update_available}
:same ->
IO.puts("✅ Raxol pre-commit hook is up to date")
{:ok, :current}
:custom ->
IO.puts("â„šī¸ Custom modifications detected in pre-commit hook")
IO.puts(" Manual review recommended before updating")
{:ok, :custom}
end
end
defp has_custom_modifications?(content) do
# Check if the hook has been modified from standard templates
lines = String.split(content, "\n")
# Look for signs of customization
custom_indicators = [
# User added comments (excluding our standard ones)
~r/^#(?!.*Raxol|.*Generated|.*Allow bypassing|.*Check if|.*Run the|.*Capture|.*Exit)/,
# Additional commands before our check
~r/^(?!#|if|echo|mix raxol|EXIT_CODE|exit)/,
# Custom environment variables
~r/export (?!RAXOL_)/
]
Enum.any?(lines, fn line ->
Enum.any?(custom_indicators, &Regex.match?(&1, line))
end)
end
defp handle_custom_hook_update(
hook_path,
current_content,
latest_content,
opts
) do
IO.puts("âš ī¸ Custom modifications detected in your pre-commit hook")
IO.puts("")
IO.puts(
"Your hook contains custom code that would be lost with a standard update."
)
show_custom_sections(current_content)
IO.puts("")
IO.puts("Options:")
IO.puts(" 1. Backup current and install fresh (recommended)")
IO.puts(" 2. Keep current hook (no update)")
IO.puts(" 3. Show diff and decide")
case opts[:force] do
true ->
maybe_backup(hook_path, opts)
perform_update(hook_path, latest_content, opts)
false ->
handle_custom_update_choice(
hook_path,
current_content,
latest_content,
opts
)
end
end
defp handle_custom_update_choice(
hook_path,
current_content,
latest_content,
opts
) do
choice =
case IO.gets("Choose option [1-3]: ") do
# Default to keeping current
:eof -> "2"
response when is_binary(response) -> String.trim(response)
_ -> "2"
end
case choice do
"1" ->
maybe_backup(hook_path, true)
perform_update(hook_path, latest_content, opts)
"2" ->
IO.puts("Update cancelled. Current hook preserved.")
{:ok, :kept_current}
"3" ->
show_diff(current_content, latest_content)
case prompt_user("Proceed with update?") do
true ->
maybe_backup(hook_path, opts)
perform_update(hook_path, latest_content, opts)
false ->
IO.puts("Update cancelled")
{:ok, :cancelled}
end
_ ->
IO.puts("Invalid choice. Update cancelled.")
{:ok, :cancelled}
end
end
defp perform_update(hook_path, new_content, opts) do
maybe_backup(hook_path, opts)
File.write!(hook_path, new_content)
System.cmd("chmod", ["+x", hook_path])
IO.puts("✅ Successfully updated Raxol pre-commit hook")
IO.puts(" Your hook is now at the latest version")
{:ok, :updated}
end
defp maybe_backup(hook_path, %{backup: true}),
do: maybe_backup(hook_path, true)
defp maybe_backup(_hook_path, %{backup: false}), do: :ok
defp maybe_backup(_hook_path, false), do: :ok
defp maybe_backup(hook_path, true) do
backup_path = "#{hook_path}.backup.#{timestamp()}"
File.copy!(hook_path, backup_path)
IO.puts("đŸ“Ļ Created backup: #{Path.basename(backup_path)}")
end
defp get_latest_hook_content do
# This would normally fetch from the module attribute in InstallHooks
# For now, we'll duplicate it here (in production, share via a common module)
"""
#!/bin/sh
# Raxol pre-commit hook
# Generated by mix raxol.install_hooks
# Allow bypassing the hook with RAXOL_SKIP_CHECKS environment variable
if [ "$RAXOL_SKIP_CHECKS" = "true" ] || [ "$RAXOL_SKIP_CHECKS" = "1" ]; then
echo "Skipping Raxol pre-commit checks (RAXOL_SKIP_CHECKS is set)"
exit 0
fi
# Check if we're in a rebase
if [ -d "$(git rev-parse --git-dir)/rebase-merge" ] || [ -d "$(git rev-parse --git-dir)/rebase-apply" ]; then
echo "Skipping pre-commit checks during rebase"
exit 0
fi
# Run the pre-commit checks
echo "Running Raxol pre-commit checks..."
mix raxol.pre_commit
# Capture the exit code
EXIT_CODE=$?
# Exit with the same code
if [ $EXIT_CODE -ne 0 ]; then
echo ""
echo "Pre-commit checks failed. You can bypass with:"
echo " git commit --no-verify"
echo " RAXOL_SKIP_CHECKS=true git commit"
fi
exit $EXIT_CODE
"""
end
defp compare_versions(current, latest) do
# Simple version comparison based on content
cond do
current == latest ->
:same
String.contains?(current, "mix raxol.pre_commit") and
not String.contains?(current, "Generated by mix raxol.install_hooks") ->
:custom
true ->
:newer
end
end
defp show_changes(_current, _latest) do
IO.puts("")
IO.puts("Changes in the latest version:")
IO.puts(" â€ĸ Improved error messages")
IO.puts(" â€ĸ Better rebase detection")
IO.puts(" â€ĸ Enhanced bypass options")
end
defp show_custom_sections(content) do
lines = String.split(content, "\n")
custom_lines =
lines
|> Enum.with_index()
|> Enum.filter(fn {line, _} ->
not String.starts_with?(line, "#") and
line != "" and
not String.contains?(line, "mix raxol.pre_commit") and
not String.contains?(line, "RAXOL_SKIP_CHECKS") and
not String.contains?(line, "EXIT_CODE")
end)
|> Enum.take(5)
case custom_lines do
[] ->
:ok
lines ->
IO.puts("Custom sections detected:")
Enum.each(lines, fn {line, num} ->
IO.puts(" Line #{num + 1}: #{String.slice(line, 0, 60)}")
end)
end
end
defp show_diff(current, latest) do
IO.puts("")
IO.puts("=" |> String.duplicate(60))
IO.puts("CURRENT HOOK (first 20 lines):")
IO.puts("-" |> String.duplicate(60))
current
|> String.split("\n")
|> Enum.take(20)
|> Enum.each(&IO.puts/1)
IO.puts("")
IO.puts("=" |> String.duplicate(60))
IO.puts("LATEST VERSION (first 20 lines):")
IO.puts("-" |> String.duplicate(60))
latest
|> String.split("\n")
|> Enum.take(20)
|> Enum.each(&IO.puts/1)
IO.puts("=" |> String.duplicate(60))
end
defp prompt_user(question) do
case IO.gets("#{question} [y/N]: ") do
:eof ->
false
response when is_binary(response) ->
response
|> String.trim()
|> String.downcase()
|> then(&(&1 in ["y", "yes"]))
_ ->
false
end
end
defp timestamp do
{{year, month, day}, {hour, minute, second}} = :calendar.local_time()
"#{year}#{pad(month)}#{pad(day)}_#{pad(hour)}#{pad(minute)}#{pad(second)}"
end
defp pad(num), do: num |> Integer.to_string() |> String.pad_leading(2, "0")
end