Packages

An Elixir SDK for the Amp CLI - programmatic access to Amp's AI coding agent.

Current section

Files

Jump to
amp_sdk lib amp_sdk examples_support.ex
Raw

lib/amp_sdk/examples_support.ex

defmodule AmpSdk.ExamplesSupport do
@moduledoc false
alias AmpSdk.CLIInvoke
alias CliSubprocessCore.ExecutionSurface
defmodule SSHContext do
@moduledoc false
@enforce_keys [:argv]
defstruct argv: [],
execution_surface: nil,
example_cwd: nil,
example_danger_full_access: false,
ssh_host: nil,
ssh_user: nil,
ssh_port: nil,
ssh_identity_file: nil
@type t :: %__MODULE__{
argv: [String.t()],
execution_surface: ExecutionSurface.t() | nil,
example_cwd: String.t() | nil,
example_danger_full_access: boolean(),
ssh_host: String.t() | nil,
ssh_user: String.t() | nil,
ssh_port: pos_integer() | nil,
ssh_identity_file: String.t() | nil
}
end
@context_key {__MODULE__, :ssh_context}
@ssh_switches [
cwd: :string,
danger_full_access: :boolean,
ssh_host: :string,
ssh_identity_file: :string,
ssh_port: :integer,
ssh_user: :string
]
@spec init!([String.t()]) :: SSHContext.t()
def init!(argv \\ System.argv()) when is_list(argv) do
case Process.get(@context_key) do
%SSHContext{} = context ->
context
nil ->
case parse_argv(argv) do
{:ok, %SSHContext{} = context} ->
System.argv(context.argv)
Process.put(@context_key, context)
context
{:error, message} ->
raise ArgumentError, message
end
end
end
@spec context() :: SSHContext.t()
def context do
case Process.get(@context_key) do
%SSHContext{} = context -> context
_ -> init!()
end
end
@spec parse_argv([String.t()]) :: {:ok, SSHContext.t()} | {:error, String.t()}
def parse_argv(argv) when is_list(argv) do
{parsed, remaining, invalid} =
argv
|> Enum.reject(&(&1 == "--"))
|> OptionParser.parse(strict: @ssh_switches)
if invalid != [] do
{:error, invalid_options_message(invalid)}
else
build_context(parsed, remaining)
end
end
@spec ssh_enabled?() :: boolean()
def ssh_enabled?, do: match?(%SSHContext{execution_surface: %ExecutionSurface{}}, context())
@spec danger_full_access?() :: boolean()
def danger_full_access?, do: context().example_danger_full_access == true
@spec execution_surface() :: ExecutionSurface.t() | nil
def execution_surface, do: context().execution_surface
@spec with_execution_surface(struct() | map()) :: struct() | map()
def with_execution_surface(options) when is_map(options) do
options
|> maybe_put_execution_surface()
|> maybe_put_example_cwd()
|> maybe_put_danger_full_access()
end
@spec command_opts(keyword()) :: keyword()
def command_opts(opts \\ []) when is_list(opts) do
opts
|> maybe_put_command_execution_surface()
|> maybe_put_command_cwd()
|> maybe_put_command_danger_full_access()
end
@spec invoke([String.t()], keyword()) :: {:ok, String.t()} | {:error, AmpSdk.Error.t()}
def invoke(args, opts \\ []) when is_list(args) and is_list(opts) do
CLIInvoke.invoke(args, command_opts(opts))
end
defp build_context(parsed, argv) do
example_cwd = Keyword.get(parsed, :cwd)
example_danger_full_access = Keyword.get(parsed, :danger_full_access, false)
ssh_host = Keyword.get(parsed, :ssh_host)
ssh_user = Keyword.get(parsed, :ssh_user)
ssh_port = Keyword.get(parsed, :ssh_port)
ssh_identity_file = Keyword.get(parsed, :ssh_identity_file)
cond do
is_nil(ssh_host) and Enum.any?([ssh_user, ssh_port, ssh_identity_file], &present?/1) ->
{:error, "SSH example flags require --ssh-host when any other --ssh-* flag is set."}
invalid_example_cwd?(example_cwd) ->
{:error, "--cwd must be a non-empty path"}
is_nil(ssh_host) ->
{:ok,
%SSHContext{
argv: argv,
example_cwd: normalize_example_cwd(example_cwd),
example_danger_full_access: example_danger_full_access
}}
true ->
with {:ok, {destination, parsed_user}} <- split_host(ssh_host),
{:ok, effective_user} <- coalesce_user(parsed_user, ssh_user),
{:ok, identity_file} <- normalize_identity_file(ssh_identity_file),
{:ok, %ExecutionSurface{} = execution_surface} <-
ExecutionSurface.new(
surface_kind: :ssh_exec,
transport_options:
[]
|> Keyword.put(:destination, destination)
|> maybe_put(:ssh_user, effective_user)
|> maybe_put(:port, ssh_port)
|> maybe_put(:identity_file, identity_file)
) do
{:ok,
%SSHContext{
argv: argv,
execution_surface: execution_surface,
example_cwd: normalize_example_cwd(example_cwd),
example_danger_full_access: example_danger_full_access,
ssh_host: destination,
ssh_user: effective_user,
ssh_port: ssh_port,
ssh_identity_file: identity_file
}}
else
{:error, reason} when is_binary(reason) -> {:error, reason}
{:error, reason} -> {:error, "invalid SSH example flags: #{inspect(reason)}"}
end
end
end
defp present?(value) when is_binary(value), do: String.trim(value) != ""
defp present?(nil), do: false
defp present?(_value), do: true
defp split_host(ssh_host) when is_binary(ssh_host) do
case String.trim(ssh_host) do
"" ->
{:error, "--ssh-host must be a non-empty host name"}
trimmed ->
case String.split(trimmed, "@", parts: 2) do
[destination] ->
{:ok, {destination, nil}}
[inline_user, destination] when inline_user != "" and destination != "" ->
{:ok, {destination, inline_user}}
_other ->
{:error, "--ssh-host must be either <host> or <user>@<host>"}
end
end
end
defp coalesce_user(nil, nil), do: {:ok, nil}
defp coalesce_user(inline_user, nil), do: {:ok, inline_user}
defp coalesce_user(nil, ssh_user) when is_binary(ssh_user) do
case String.trim(ssh_user) do
"" -> {:error, "--ssh-user must be a non-empty string"}
trimmed -> {:ok, trimmed}
end
end
defp coalesce_user(inline_user, ssh_user) when is_binary(ssh_user) do
normalized = String.trim(ssh_user)
cond do
normalized == "" ->
{:error, "--ssh-user must be a non-empty string"}
normalized == inline_user ->
{:ok, inline_user}
true ->
{:error,
"--ssh-host already contains #{inspect(inline_user)}; omit --ssh-user or make it match"}
end
end
defp normalize_identity_file(nil), do: {:ok, nil}
defp normalize_identity_file(path) when is_binary(path) do
case String.trim(path) do
"" -> {:error, "--ssh-identity-file must be a non-empty path"}
trimmed -> {:ok, Path.expand(trimmed)}
end
end
defp invalid_options_message(invalid) when is_list(invalid) do
rendered =
Enum.map_join(invalid, ", ", fn
{name, nil} -> "--#{name}"
{name, value} -> "--#{name}=#{value}"
end)
"invalid example flags: #{rendered}. Supported flags: --cwd, --danger-full-access, --ssh-host, --ssh-user, --ssh-port, --ssh-identity-file"
end
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
defp maybe_put_execution_surface(options) when is_map(options) do
case execution_surface() do
%ExecutionSurface{} = surface -> Map.put(options, :execution_surface, surface)
nil -> options
end
end
defp maybe_put_example_cwd(options) when is_map(options) do
case normalize_example_cwd(context().example_cwd) do
cwd when is_binary(cwd) -> Map.put(options, :cwd, cwd)
_ -> options
end
end
defp maybe_put_danger_full_access(options) when is_map(options) do
if danger_full_access?() do
Map.put(options, :dangerously_allow_all, true)
else
options
end
end
defp maybe_put_command_execution_surface(opts) when is_list(opts) do
case execution_surface() do
%ExecutionSurface{} = surface -> Keyword.put(opts, :execution_surface, surface)
nil -> opts
end
end
defp maybe_put_command_cwd(opts) when is_list(opts) do
case normalize_example_cwd(context().example_cwd) do
cwd when is_binary(cwd) -> Keyword.put(opts, :cwd, cwd)
_ -> opts
end
end
defp maybe_put_command_danger_full_access(opts) when is_list(opts) do
if danger_full_access?() do
Keyword.put(opts, :dangerously_allow_all, true)
else
opts
end
end
defp invalid_example_cwd?(nil), do: false
defp invalid_example_cwd?(path) when is_binary(path), do: String.trim(path) == ""
defp normalize_example_cwd(nil), do: nil
defp normalize_example_cwd(path) when is_binary(path), do: String.trim(path)
end