Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion apps/expert/lib/expert/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ defmodule Expert.Application do

@impl true
def start(_type, _args) do
argv = Burrito.Util.Args.argv()

# Handle engine subcommand first (before starting the LSP server)
with ["engine" | engine_args] <- argv do
engine_args
|> Expert.Engine.run()
|> System.halt()
end

{opts, _argv, _invalid} =
OptionParser.parse(Burrito.Util.Args.argv(),
OptionParser.parse(argv,
strict: [version: :boolean, help: :boolean, stdio: :boolean, port: :integer]
)

Expand All @@ -26,13 +35,18 @@ defmodule Expert.Application do
Source code: https://github.com/elixir-lang/expert

expert [flags]
expert engine <subcommand> [options]

#{IO.ANSI.bright()}FLAGS#{IO.ANSI.reset()}

--stdio Use stdio as the transport mechanism
--port <port> Use TCP as the transport mechanism, with the given port
--help Show this help message
--version Show Expert version

#{IO.ANSI.bright()}SUBCOMMANDS#{IO.ANSI.reset()}

engine Manage engine builds (use 'expert engine --help' for details)
"""

cond do
Expand Down
267 changes: 267 additions & 0 deletions apps/expert/lib/expert/engine.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
defmodule Expert.Engine do
@moduledoc """
Utilities for managing Expert engine builds.

When Expert builds the engine for a project using Mix.install, it caches
the build in the user data directory. If engine dependencies change (e.g.,
in nightly builds), Mix.install may not know to rebuild, causing errors.

This module provides functions to inspect and clean these cached builds.
"""

@doc """
Runs engine management commands based on parsed arguments.

Returns the exit code for the command. Clean operations will stop at the
first deletion error and return exit code 1.
"""

@success_code 0
@error_code 1

@spec run([String.t()]) :: non_neg_integer()
def run(args) do
{opts, subcommand, _invalid} =
OptionParser.parse_head(args,
strict: [help: :boolean],
aliases: [h: :help]
)

if opts[:help] do
print_help()
else
case subcommand do
["ls" | ls_opts] -> list_engines(ls_opts)
["clean" | clean_opts] -> clean_engines(clean_opts)
[unknown | _] -> print_unknown_subcommand(unknown)
[] -> print_help()
end
end
end

@spec list_engines([String.t()]) :: non_neg_integer()
defp list_engines(ls_options) do
{opts, _rest, _invalid} =
OptionParser.parse_head(ls_options,
strict: [help: :boolean],
aliases: [h: :help]
)

if opts[:help] do
print_ls_help()
else
print_engine_dirs()
end
end

@spec print_engine_dirs() :: non_neg_integer()
defp print_engine_dirs do
dirs = get_engine_dirs()

case dirs do
[] ->
print_no_engines_found()

dirs ->
Enum.each(dirs, &IO.puts/1)
end

@success_code
end

@spec clean_engines([String.t()]) :: non_neg_integer()
defp clean_engines(clean_options) do
{opts, _rest, _invalid} =
OptionParser.parse_head(clean_options,
strict: [force: :boolean, help: :boolean],
aliases: [f: :force, h: :help]
)

dirs = get_engine_dirs()

cond do
opts[:help] ->
print_clean_help()

dirs == [] ->
print_no_engines_found()

opts[:force] ->
clean_all_force(dirs)

true ->
clean_interactive(dirs)
end
end

@spec base_dir() :: String.t()
defp base_dir do
base = :filename.basedir(:user_data, ~c"Expert")
to_string(base)
end

@spec get_engine_dirs() :: [String.t()]
defp get_engine_dirs do
base = base_dir()

if File.exists?(base) do
base
|> File.ls!()
|> Enum.map(&Path.join(base, &1))
|> Enum.filter(&File.dir?/1)
|> Enum.sort()
else
[]
end
end

@spec clean_all_force([String.t()]) :: non_neg_integer()
# Deletes all directories without prompting. Stops on first error and returns 1.
defp clean_all_force(dirs) do
result =
Enum.reduce_while(dirs, :ok, fn dir, _acc ->
case File.rm_rf(dir) do
{:ok, _} ->
IO.puts("Deleted #{dir}")
{:cont, :ok}

{:error, reason, file} ->
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
{:halt, :error}
end
end)

case result do
:ok -> @success_code
:error -> @error_code
end
end

@spec clean_interactive([String.t()]) :: non_neg_integer()
# Prompts the user for each directory deletion. Stops on first error and returns 1.
defp clean_interactive(dirs) do
result =
Enum.reduce_while(dirs, :ok, fn dir, _acc ->
answer = prompt_delete(dir)

if answer do
case File.rm_rf(dir) do
{:ok, _} ->
{:cont, :ok}

{:error, reason, file} ->
IO.puts(:stderr, "Error deleting #{file}: #{inspect(reason)}")
{:halt, :error}
end
else
{:cont, :ok}
end
end)

case result do
:ok -> @success_code
:error -> @error_code
end
end

@spec prompt_delete(dir :: [String.t()]) :: boolean()
defp prompt_delete(dir) do
IO.puts(["Delete #{dir}", IO.ANSI.red(), "?", IO.ANSI.reset(), " [Yn] "])

input =
""
|> IO.gets()
|> String.trim()
|> String.downcase()

case input do
"" -> true
"y" -> true
"yes" -> true
_ -> false
end
end

@spec print_no_engines_found() :: non_neg_integer()
defp print_no_engines_found do
IO.puts("No engine builds found.")
IO.puts("\nEngine builds are stored in: #{base_dir()}")

@success_code
end

@spec print_unknown_subcommand(String.t()) :: non_neg_integer()
defp print_unknown_subcommand(subcommand) do
IO.puts(:stderr, """
Error: Unknown subcommand '#{subcommand}'

Run 'expert engine --help' for usage information.
""")

@error_code
end

@spec print_help() :: non_neg_integer()
defp print_help do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add help text to each subcommand, and the options can be presented for the relevant subcommand.

So in the existing help text here, we won't have any options, and the --force option will show up in the help text for the clean subcommand

IO.puts("""
Expert Engine Management

Manage cached engine builds created by Mix.install. Use these commands
to resolve dependency errors or free up disk space.

USAGE:
expert engine <subcommand>

SUBCOMMANDS:
ls List all engine build directories
clean Interactively delete engine build directories

Use 'expert engine <subcommand> --help' for more information on a specific command.

EXAMPLES:
expert engine ls
expert engine clean
""")

@success_code
end

@spec print_ls_help() :: non_neg_integer()
defp print_ls_help do
IO.puts("""
List Engine Builds

List all cached engine build directories.

USAGE:
expert engine ls

EXAMPLES:
expert engine ls
""")

@success_code
end

@spec print_clean_help() :: non_neg_integer()
defp print_clean_help do
IO.puts("""
Clean Engine Builds

Interactively delete cached engine build directories. By default, you will
be prompted to confirm deletion of each build. Use --force to skip prompts.

USAGE:
expert engine clean [options]

OPTIONS:
-f, --force Delete all builds without prompting

EXAMPLES:
expert engine clean
expert engine clean --force
""")

@success_code
end
end
Loading
Loading