Current section
Files
Jump to
Current section
Files
lib/mix/tasks/devcon4.install.ex
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