defmodule Mix.Tasks.Devcon4.Install do @shortdoc "Set up devcontainer in your project" @moduledoc """ Sets up devcontainer files in your project. mix devcon4.install ## What it does * Creates `.devcontainer/devcontainer.json` with container settings, volumes, and firewall initialization * Creates `.devcontainer/Dockerfile` with Elixir, Node.js, Claude Code, and development tools * Creates `.devcontainer/init-firewall.sh` with a restrictive outbound firewall (GitHub, Hex.pm, Anthropic API only) ## Options * `--elixir-version` - Elixir version for the base image (default: `1.18.3`) * `--erlang-version` - Erlang/OTP version for the base image (default: `27.3.3`) * `--ubuntu-version` - Ubuntu release codename (default: `noble`) * `--ubuntu-date-tag` - Ubuntu image date tag (default: `20260217`) * `--timezone` - Default timezone fallback (default: `America/Sao_Paulo`) * `--force` - Overwrite existing files without prompting """ use Mix.Task @switches [ elixir_version: :string, erlang_version: :string, ubuntu_version: :string, ubuntu_date_tag: :string, timezone: :string, force: :boolean ] @defaults [ elixir_version: "1.18.3", erlang_version: "27.3.3", ubuntu_version: "noble", ubuntu_date_tag: "20260217", timezone: "America/Sao_Paulo", force: false ] @impl Mix.Task def run(argv) do {opts, _args} = OptionParser.parse!(argv, strict: @switches) opts = Keyword.merge(@defaults, opts) app_name = detect_app_name() project_name = humanize(app_name) volume_prefix = String.replace(app_name, "_", "-") File.mkdir_p!(".devcontainer") create_from_template( "devcontainer.json.eex", ".devcontainer/devcontainer.json", [ project_name: project_name, volume_prefix: volume_prefix, timezone: opts[:timezone] ], opts[:force] ) create_from_template( "Dockerfile.eex", ".devcontainer/Dockerfile", [ elixir_version: opts[:elixir_version], erlang_version: opts[:erlang_version], ubuntu_version: opts[:ubuntu_version], ubuntu_date_tag: opts[:ubuntu_date_tag] ], opts[:force] ) create_file( "init-firewall.sh", ".devcontainer/init-firewall.sh", opts[:force] ) # Make firewall script executable File.chmod!(".devcontainer/init-firewall.sh", 0o755) Mix.shell().info(""" Devcontainer files installed in .devcontainer/ To start the container: # Via CLI devcontainer up --workspace-folder . devcontainer exec --workspace-folder . bash # Via VS Code Ctrl+Shift+P -> "Dev Containers: Reopen in Container" To customize, edit the generated files directly: - Add firewall domains in .devcontainer/init-firewall.sh - Add system packages in .devcontainer/Dockerfile - Change container settings in .devcontainer/devcontainer.json """) end defp create_from_template(template_name, target, assigns, force) do template = Devcon4.template_path(template_name) content = EEx.eval_file(template, assigns: assigns) write_file(target, content, force) end defp create_file(source_name, target, force) do source = Devcon4.template_path(source_name) content = File.read!(source) write_file(target, content, force) end defp write_file(target, content, force) do if File.exists?(target) and not force do Mix.shell().info("* skipping #{target} (already exists, use --force to overwrite)") else action = if File.exists?(target), do: "overwriting", else: "creating" Mix.shell().info("* #{action} #{target}") File.write!(target, content) end end defp detect_app_name do config = Mix.Project.config() config[:app] |> to_string() end defp humanize(name) do name |> String.replace("_", " ") |> String.split() |> Enum.map_join(" ", &String.capitalize/1) end end