From de21c8a348075dcebe8463eaf21105979259f1ae Mon Sep 17 00:00:00 2001 From: Patrick Detlefsen Date: Sat, 28 Feb 2026 22:29:46 +0100 Subject: [PATCH] refactor(forge): rename SpriteSession/SpriteClient to InfraSession/InfraClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decouple the Forge subsystem from the Sprites-specific naming so it can work with any infrastructure provider (Sprites, Hetzner, local fake). - SpriteSession → InfraSession, SpriteClient → InfraClient, SpriteSupervisor → InfraSupervisor - InfraClient dispatch via resolve_infra_client/1 supports :default, :fake, :live, :sprite, :hetzner, and arbitrary modules - InfraSession.init merges hetzner_config/workspace_id into the infra spec so ForgeAdapter.create receives the full config - on_progress callback now returns :ok for emit_progress with-chain - Import colocated LiveView hooks in app.js (fixes terminal input) - DB columns kept as sprite_id/sprite_name to avoid migration - Legacy :sprite_client spec key still accepted for backward compat --- assets/js/app.js | 2 + lib/jido_code/application.ex | 4 +- lib/jido_code/forge.ex | 16 +-- lib/jido_code/forge/bootstrap.ex | 18 +-- .../{sprite_client.ex => infra_client.ex} | 20 +-- lib/jido_code/forge/infra_client/behaviour.ex | 78 +++++++++++ .../{sprite_client => infra_client}/fake.ex | 96 +++++++------- .../live.ex => infra_client/sprite.ex} | 10 +- .../{sprite_session.ex => infra_session.ex} | 123 ++++++++++++------ lib/jido_code/forge/manager.ex | 6 +- lib/jido_code/forge/operations.ex | 4 +- lib/jido_code/forge/persistence.ex | 6 +- lib/jido_code/forge/runner.ex | 12 +- lib/jido_code/forge/runners/claude_code.ex | 16 +-- lib/jido_code/forge/runners/shell.ex | 4 +- lib/jido_code/forge/runners/workflow.ex | 10 +- .../forge/sprite_client/behaviour.ex | 67 ---------- .../workers/streaming_exec_session_worker.ex | 14 +- lib/jido_code_web/live/forge/index_live.ex | 11 +- lib/jido_code_web/live/forge/show_live.ex | 16 ++- .../forge/sprite_integration_test.exs | 10 +- .../live/forge_show_live_redaction_test.exs | 2 +- ...forge_show_live_stream_continuity_test.exs | 2 +- 23 files changed, 306 insertions(+), 241 deletions(-) rename lib/jido_code/forge/{sprite_client.ex => infra_client.ex} (67%) create mode 100644 lib/jido_code/forge/infra_client/behaviour.ex rename lib/jido_code/forge/{sprite_client => infra_client}/fake.ex (66%) rename lib/jido_code/forge/{sprite_client/live.ex => infra_client/sprite.ex} (95%) rename lib/jido_code/forge/{sprite_session.ex => infra_session.ex} (74%) delete mode 100644 lib/jido_code/forge/sprite_client/behaviour.ex diff --git a/assets/js/app.js b/assets/js/app.js index 38e3a86..aab9caf 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -22,6 +22,7 @@ import { Socket } from "phoenix"; import { LiveSocket } from "phoenix_live_view"; import topbar from "../vendor/topbar"; import { createLiveToastHook } from "../../deps/live_toast/priv/static/live_toast.esm.js"; +import { hooks as colocatedHooks } from "../../_build/dev/phoenix-colocated/jido_code/index.js"; const csrfToken = document .querySelector("meta[name='csrf-token']") .getAttribute("content"); @@ -32,6 +33,7 @@ const liveSocket = new LiveSocket("/live", Socket, { }, hooks: { LiveToast: createLiveToastHook(), + ...colocatedHooks, }, }); // Show progress bar on live navigation and form submits diff --git a/lib/jido_code/application.ex b/lib/jido_code/application.ex index bb3cc1a..fbc1878 100644 --- a/lib/jido_code/application.ex +++ b/lib/jido_code/application.ex @@ -18,7 +18,7 @@ defmodule JidoCode.Application do {AshAuthentication.Supervisor, [otp_app: :jido_code]}, # Forge supervision tree {Registry, keys: :unique, name: JidoCode.Forge.SessionRegistry}, - {DynamicSupervisor, name: JidoCode.Forge.SpriteSupervisor, strategy: :one_for_one}, + {DynamicSupervisor, name: JidoCode.Forge.InfraSupervisor, strategy: :one_for_one}, {DynamicSupervisor, name: JidoCode.Forge.ExecSessionSupervisor, strategy: :one_for_one}, JidoCode.Forge.Manager ] ++ forge_dev_children() @@ -38,7 +38,7 @@ defmodule JidoCode.Application do end if Application.compile_env(:jido_code, :runtime_mode, :prod) in [:dev, :test] do - defp forge_dev_children, do: [{JidoCode.Forge.SpriteClient.Fake, []}] + defp forge_dev_children, do: [{JidoCode.Forge.InfraClient.Fake, []}] else defp forge_dev_children, do: [] end diff --git a/lib/jido_code/forge.ex b/lib/jido_code/forge.ex index 580bdd2..1af9918 100644 --- a/lib/jido_code/forge.ex +++ b/lib/jido_code/forge.ex @@ -2,10 +2,10 @@ defmodule JidoCode.Forge do @moduledoc """ Jido Forge - Generic parallel sandbox execution. - Forge manages sprite sessions with pluggable runners. + Forge manages infrastructure sessions with pluggable runners. """ - alias JidoCode.Forge.{Manager, Operations, SpriteSession} + alias JidoCode.Forge.{Manager, Operations, InfraSession} defmodule SessionHandle do @moduledoc """ @@ -69,7 +69,7 @@ defmodule JidoCode.Forge do """ @spec status(String.t()) :: {:ok, map()} | {:error, term()} def status(session_id) do - SpriteSession.status(session_id) + InfraSession.status(session_id) end # Execution @@ -79,20 +79,20 @@ defmodule JidoCode.Forge do """ @spec run_iteration(String.t(), keyword()) :: {:ok, map()} | {:error, term()} def run_iteration(session_id, opts \\ []) do - SpriteSession.run_iteration(session_id, opts) + InfraSession.run_iteration(session_id, opts) end @doc """ - Executes a command directly in the sprite. + Executes a command directly in the environment. """ @spec exec(String.t(), String.t(), keyword()) :: {String.t(), non_neg_integer()} | {:error, term()} def exec(session_id, command, opts \\ []) do - SpriteSession.exec(session_id, command, opts) + InfraSession.exec(session_id, command, opts) end @doc """ - Execute a command synchronously in the session's sprite (Sprites-style API). + Execute a command synchronously in the session's environment (Sprites-style API). Unlike `exec/3` which takes a raw command string, this takes command and args separately for proper escaping and consistency with the Sprites SDK. @@ -129,7 +129,7 @@ defmodule JidoCode.Forge do """ @spec apply_input(String.t(), term()) :: :ok | {:error, term()} def apply_input(session_id, input) do - SpriteSession.apply_input(session_id, input) + InfraSession.apply_input(session_id, input) end @doc """ diff --git a/lib/jido_code/forge/bootstrap.ex b/lib/jido_code/forge/bootstrap.ex index cb6c4c2..e9a6269 100644 --- a/lib/jido_code/forge/bootstrap.ex +++ b/lib/jido_code/forge/bootstrap.ex @@ -1,8 +1,8 @@ defmodule JidoCode.Forge.Bootstrap do @moduledoc """ - Execute bootstrap steps to set up a sprite environment. + Execute bootstrap steps to set up an infrastructure environment. - Bootstrap steps prepare the sprite for running iterations by executing + Bootstrap steps prepare the environment for running iterations by executing commands, writing files, and configuring the environment. """ @@ -21,8 +21,8 @@ defmodule JidoCode.Forge.Bootstrap do ## Options - * `:sprite_client` - The sprite client module to use (defaults to JidoCode.Forge.SpriteClient) - * `:sprite_id` - The sprite identifier for command execution + * `:infra_client` - The infra client module to use (defaults to JidoCode.Forge.InfraClient) + * `:infra_id` - The infrastructure identifier for command execution * `:on_step` - Optional callback `fn step, index -> :ok end` called before each step ## Examples @@ -32,7 +32,7 @@ defmodule JidoCode.Forge.Bootstrap do %{type: "file", path: "config.json", content: "{}"} ] - Bootstrap.execute(client, steps, sprite_id: "abc123") + Bootstrap.execute(client, steps, infra_id: "abc123") #=> :ok """ @@ -63,9 +63,9 @@ defmodule JidoCode.Forge.Bootstrap do def execute_step(client, %{type: "exec", command: command} = step, opts) do Logger.debug("Executing bootstrap command: #{command}") - sprite_client = Keyword.get(opts, :sprite_client, JidoCode.Forge.SpriteClient) + infra_client = Keyword.get(opts, :infra_client, JidoCode.Forge.InfraClient) - case sprite_client.exec(client, command, opts) do + case infra_client.exec(client, command, opts) do {_output, 0} -> :ok @@ -77,9 +77,9 @@ defmodule JidoCode.Forge.Bootstrap do def execute_step(client, %{type: "file", path: path, content: content}, opts) do Logger.debug("Writing bootstrap file: #{path}") - sprite_client = Keyword.get(opts, :sprite_client, JidoCode.Forge.SpriteClient) + infra_client = Keyword.get(opts, :infra_client, JidoCode.Forge.InfraClient) - case sprite_client.write_file(client, path, content) do + case infra_client.write_file(client, path, content) do :ok -> :ok {:error, reason} -> {:error, {:write_failed, path, reason}} end diff --git a/lib/jido_code/forge/sprite_client.ex b/lib/jido_code/forge/infra_client.ex similarity index 67% rename from lib/jido_code/forge/sprite_client.ex rename to lib/jido_code/forge/infra_client.ex index bd125fb..a430525 100644 --- a/lib/jido_code/forge/sprite_client.ex +++ b/lib/jido_code/forge/infra_client.ex @@ -1,23 +1,23 @@ -defmodule JidoCode.Forge.SpriteClient do +defmodule JidoCode.Forge.InfraClient do @moduledoc """ - Facade for sprite client operations. + Facade for infrastructure client operations. Delegates all calls to the appropriate implementation module based on the client struct type. For `create/1`, uses the configured implementation. Configure via: - config :jido_code, :forge_sprite_client, MyApp.SpriteClient.Impl + config :jido_code, :forge_infra_client, MyApp.InfraClient.Impl - Defaults to `JidoCode.Forge.SpriteClient.Fake` for development and testing. + Defaults to `JidoCode.Forge.InfraClient.Fake` for development and testing. """ - @behaviour JidoCode.Forge.SpriteClient.Behaviour + @behaviour JidoCode.Forge.InfraClient.Behaviour - alias JidoCode.Forge.SpriteClient.Fake + alias JidoCode.Forge.InfraClient.Fake defp impl do - Application.get_env(:jido_code, :forge_sprite_client, Fake) + Application.get_env(:jido_code, :forge_infra_client, Fake) end defp impl_for(%module{} = _client) when is_atom(module) do @@ -29,7 +29,7 @@ defmodule JidoCode.Forge.SpriteClient do end defp impl_for(client) do - raise ArgumentError, "Unknown sprite client struct: #{inspect(client)}" + raise ArgumentError, "Unknown infra client struct: #{inspect(client)}" end @impl true @@ -66,7 +66,7 @@ defmodule JidoCode.Forge.SpriteClient do end @impl true - def destroy(client, sprite_id) do - impl_for(client).destroy(client, sprite_id) + def destroy(client, infra_id) do + impl_for(client).destroy(client, infra_id) end end diff --git a/lib/jido_code/forge/infra_client/behaviour.ex b/lib/jido_code/forge/infra_client/behaviour.ex new file mode 100644 index 0000000..76acbe5 --- /dev/null +++ b/lib/jido_code/forge/infra_client/behaviour.ex @@ -0,0 +1,78 @@ +defmodule JidoCode.Forge.InfraClient.Behaviour do + @moduledoc """ + Behaviour defining the interface for infrastructure client implementations. + + An infra client provides an isolated execution environment (container, VM, or local sandbox) + where Forge runners execute commands and manage files. + + ## Progress Reporting + + The `create/1` callback receives a spec map that may include an `:on_progress` key — + a function `(stage, metadata) -> :ok` called during long-running provisioning to report + progress. Stages: `:ssh_key`, `:server_creating`, `:server_booting`, `:ssh_waiting`, + `:ssh_connected`, `:session_starting`. Fast-provisioning implementations (like local + sandboxes) may ignore this key. + """ + + @type client :: term() + @type infra_id :: String.t() + @type spec :: map() + @type command :: String.t() + @type path :: String.t() + @type content :: binary() + @type env_map :: %{String.t() => String.t()} + @type handle :: term() + @type opts :: keyword() + + @doc """ + Create a new infrastructure environment from the given specification. + + The spec map may include an `:on_progress` key with a function `(stage, metadata) -> :ok` + for reporting provisioning progress. + + Returns the client state and a unique infrastructure identifier. + """ + @callback create(spec()) :: {:ok, client(), infra_id()} | {:error, term()} + + @doc """ + Execute a command synchronously in the environment. + + Returns the output and exit code. + """ + @callback exec(client(), command(), opts()) :: {String.t(), non_neg_integer()} + + @doc """ + Spawn an asynchronous command in the environment. + + Returns a handle for monitoring or interacting with the process. + """ + @callback spawn(client(), command(), args :: [String.t()], opts()) :: + {:ok, handle()} | {:error, term()} + + @doc """ + Write content to a file in the environment. + """ + @callback write_file(client(), path(), content()) :: :ok | {:error, term()} + + @doc """ + Read content from a file in the environment. + """ + @callback read_file(client(), path()) :: {:ok, content()} | {:error, term()} + + @doc """ + Inject environment variables into the environment. + + These should be available to all subsequent commands. + """ + @callback inject_env(client(), env_map()) :: :ok | {:error, term()} + + @doc """ + Destroy the environment and clean up resources. + """ + @callback destroy(client(), infra_id()) :: :ok | {:error, term()} + + @doc """ + Returns the implementation module for this client type. + """ + @callback impl_module() :: module() +end diff --git a/lib/jido_code/forge/sprite_client/fake.ex b/lib/jido_code/forge/infra_client/fake.ex similarity index 66% rename from lib/jido_code/forge/sprite_client/fake.ex rename to lib/jido_code/forge/infra_client/fake.ex index 1f52e07..9c35c61 100644 --- a/lib/jido_code/forge/sprite_client/fake.ex +++ b/lib/jido_code/forge/infra_client/fake.ex @@ -1,12 +1,12 @@ -defmodule JidoCode.Forge.SpriteClient.Fake do +defmodule JidoCode.Forge.InfraClient.Fake do @moduledoc """ - Fake sprite client implementation for development and testing. + Fake infrastructure client implementation for development and testing. - Uses local temporary directories as isolated "sprites" and executes + Uses local temporary directories as isolated environments and executes commands via System.cmd. State is managed by an Agent process. """ - @behaviour JidoCode.Forge.SpriteClient.Behaviour + @behaviour JidoCode.Forge.InfraClient.Behaviour use Agent @@ -19,13 +19,13 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @type t :: %__MODULE__{agent_pid: pid()} - @type sprite_state :: %{ + @type infra_state :: %{ dir: String.t(), env: %{String.t() => String.t()} } @doc """ - Start the fake sprite client agent. + Start the fake infra client agent. """ @spec start_link(keyword()) :: Agent.on_start() def start_link(_opts) do @@ -49,21 +49,21 @@ defmodule JidoCode.Forge.SpriteClient.Fake do def create(spec) do {:ok, agent_pid} = Agent.start_link(fn -> %{} end) - sprite_id = generate_sprite_id() + infra_id = generate_infra_id() base_dir = Map.get(spec, :base_dir, System.tmp_dir!()) - sprite_dir = Path.join(base_dir, "forge_sprite_#{sprite_id}") + infra_dir = Path.join(base_dir, "forge_infra_#{infra_id}") - case File.mkdir_p(sprite_dir) do + case File.mkdir_p(infra_dir) do :ok -> - state = %{dir: sprite_dir, env: %{}} + state = %{dir: infra_dir, env: %{}} - Agent.update(agent_pid, fn sprites -> - Map.put(sprites, sprite_id, state) + Agent.update(agent_pid, fn envs -> + Map.put(envs, infra_id, state) end) - Logger.debug("Created fake sprite #{sprite_id} at #{sprite_dir}") + Logger.debug("Created fake infra #{infra_id} at #{infra_dir}") client = %__MODULE__{agent_pid: agent_pid} - {:ok, client, sprite_id} + {:ok, client, infra_id} {:error, reason} -> Agent.stop(agent_pid) @@ -74,17 +74,17 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @impl true def exec(%__MODULE__{agent_pid: agent_pid} = _client, command, opts) do ensure_agent_started(agent_pid) - sprite_id = Keyword.get(opts, :sprite_id) + infra_id = Keyword.get(opts, :infra_id) || Keyword.get(opts, :sprite_id) timeout = Keyword.get(opts, :timeout, 60_000) - sprite_state = get_sprite_state(agent_pid, sprite_id) + infra_state = get_infra_state(agent_pid, infra_id) env = - sprite_state.env + infra_state.env |> Enum.map(fn {k, v} -> {to_binary_string(k), to_binary_string(v)} end) cmd_opts = [ - cd: sprite_state.dir, + cd: infra_state.dir, env: env, stderr_to_stdout: true ] @@ -103,11 +103,11 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @impl true def spawn(%__MODULE__{agent_pid: agent_pid} = _client, command, args, opts) do ensure_agent_started(agent_pid) - sprite_id = Keyword.get(opts, :sprite_id) - sprite_state = get_sprite_state(agent_pid, sprite_id) + infra_id = Keyword.get(opts, :infra_id) || Keyword.get(opts, :sprite_id) + infra_state = get_infra_state(agent_pid, infra_id) env = - sprite_state.env + infra_state.env |> Enum.map(fn {k, v} -> {to_binary_string(k), to_binary_string(v)} end) port_opts = [ @@ -115,7 +115,7 @@ defmodule JidoCode.Forge.SpriteClient.Fake do :exit_status, :use_stdio, :stderr_to_stdout, - {:cd, sprite_state.dir}, + {:cd, infra_state.dir}, {:env, env}, {:args, args} ] @@ -131,14 +131,14 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @impl true def write_file(%__MODULE__{agent_pid: agent_pid} = _client, path, content) do ensure_agent_started(agent_pid) - sprites = Agent.get(agent_pid, & &1) + envs = Agent.get(agent_pid, & &1) - sprite_state = - sprites + infra_state = + envs |> Map.values() |> List.first() - full_path = resolve_path(sprite_state.dir, path) + full_path = resolve_path(infra_state.dir, path) with :ok <- File.mkdir_p(Path.dirname(full_path)), :ok <- File.write(full_path, content) do @@ -151,14 +151,14 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @impl true def read_file(%__MODULE__{agent_pid: agent_pid} = _client, path) do ensure_agent_started(agent_pid) - sprites = Agent.get(agent_pid, & &1) + envs = Agent.get(agent_pid, & &1) - sprite_state = - sprites + infra_state = + envs |> Map.values() |> List.first() - full_path = resolve_path(sprite_state.dir, path) + full_path = resolve_path(infra_state.dir, path) case File.read(full_path) do {:ok, content} -> {:ok, content} @@ -169,12 +169,12 @@ defmodule JidoCode.Forge.SpriteClient.Fake do @impl true def inject_env(%__MODULE__{agent_pid: agent_pid} = _client, env_map) do ensure_agent_started(agent_pid) - sprites = Agent.get(agent_pid, & &1) + envs = Agent.get(agent_pid, & &1) - case Map.keys(sprites) do - [sprite_id | _] -> - Agent.update(agent_pid, fn sprites -> - update_in(sprites, [sprite_id, :env], fn existing_env -> + case Map.keys(envs) do + [infra_id | _] -> + Agent.update(agent_pid, fn envs -> + update_in(envs, [infra_id, :env], fn existing_env -> # Normalize all env values to strings (binaries) normalized_map = env_map @@ -188,27 +188,27 @@ defmodule JidoCode.Forge.SpriteClient.Fake do :ok [] -> - {:error, :no_sprite} + {:error, :no_infra} end end @impl true - def destroy(%__MODULE__{agent_pid: agent_pid} = _client, sprite_id) do + def destroy(%__MODULE__{agent_pid: agent_pid} = _client, infra_id) do ensure_agent_started(agent_pid) - sprite_state = Agent.get(agent_pid, fn sprites -> Map.get(sprites, sprite_id) end) + infra_state = Agent.get(agent_pid, fn envs -> Map.get(envs, infra_id) end) - case sprite_state do + case infra_state do nil -> {:error, :not_found} %{dir: dir} -> File.rm_rf(dir) - Agent.update(agent_pid, fn sprites -> - Map.delete(sprites, sprite_id) + Agent.update(agent_pid, fn envs -> + Map.delete(envs, infra_id) end) - Logger.debug("Destroyed fake sprite #{sprite_id}") + Logger.debug("Destroyed fake infra #{infra_id}") :ok end end @@ -219,21 +219,21 @@ defmodule JidoCode.Forge.SpriteClient.Fake do end end - defp generate_sprite_id do + defp generate_infra_id do :crypto.strong_rand_bytes(8) |> Base.hex_encode32(case: :lower, padding: false) end - defp get_sprite_state(agent_pid, nil) do - sprites = Agent.get(agent_pid, & &1) + defp get_infra_state(agent_pid, nil) do + envs = Agent.get(agent_pid, & &1) - sprites + envs |> Map.values() |> List.first() end - defp get_sprite_state(agent_pid, sprite_id) do - Agent.get(agent_pid, fn sprites -> Map.get(sprites, sprite_id) end) + defp get_infra_state(agent_pid, infra_id) do + Agent.get(agent_pid, fn envs -> Map.get(envs, infra_id) end) end defp resolve_path(base_dir, path) do diff --git a/lib/jido_code/forge/sprite_client/live.ex b/lib/jido_code/forge/infra_client/sprite.ex similarity index 95% rename from lib/jido_code/forge/sprite_client/live.ex rename to lib/jido_code/forge/infra_client/sprite.ex index 01dcdbb..a0fd46f 100644 --- a/lib/jido_code/forge/sprite_client/live.ex +++ b/lib/jido_code/forge/infra_client/sprite.ex @@ -1,6 +1,6 @@ -defmodule JidoCode.Forge.SpriteClient.Live do +defmodule JidoCode.Forge.InfraClient.Sprite do @moduledoc """ - Live sprite client implementation using the real Sprites SDK. + Sprite infrastructure client implementation using the real Sprites SDK. Connects to the Sprites API to create and manage remote containers. @@ -10,11 +10,11 @@ defmodule JidoCode.Forge.SpriteClient.Live do Optional configuration via application env: - config :jido_code, JidoCode.Forge.SpriteClient.Live, + config :jido_code, JidoCode.Forge.InfraClient.Sprite, base_url: "https://api.sprites.dev" """ - @behaviour JidoCode.Forge.SpriteClient.Behaviour + @behaviour JidoCode.Forge.InfraClient.Behaviour require Logger @@ -157,7 +157,7 @@ defmodule JidoCode.Forge.SpriteClient.Live do end @impl true - def destroy(%__MODULE__{sprite: sprite, sprite_id: sprite_id} = _client, _sprite_id) do + def destroy(%__MODULE__{sprite: sprite, sprite_id: sprite_id} = _client, _infra_id) do Logger.debug("Destroying live sprite #{sprite_id}") case Sprites.destroy(sprite) do diff --git a/lib/jido_code/forge/sprite_session.ex b/lib/jido_code/forge/infra_session.ex similarity index 74% rename from lib/jido_code/forge/sprite_session.ex rename to lib/jido_code/forge/infra_session.ex index fce4297..28936d5 100644 --- a/lib/jido_code/forge/sprite_session.ex +++ b/lib/jido_code/forge/infra_session.ex @@ -1,9 +1,10 @@ -defmodule JidoCode.Forge.SpriteSession do +defmodule JidoCode.Forge.InfraSession do @moduledoc """ - Per-session GenServer managing a sprite lifecycle. + Per-session GenServer managing an infrastructure environment lifecycle. Handles provisioning, bootstrapping, runner initialization, and iteration - execution for a single forge session. + execution for a single forge session. Works with any infrastructure provider + (Sprites, Hetzner, local fake) via the `InfraClient.Behaviour` abstraction. """ use GenServer @@ -13,11 +14,12 @@ defmodule JidoCode.Forge.SpriteSession do alias JidoCode.Forge.Bootstrap alias JidoCode.Forge.Persistence alias JidoCode.Forge.PubSub, as: ForgePubSub - alias JidoCode.Forge.SpriteClient + alias JidoCode.Forge.InfraClient @type session_id :: String.t() @type state_name :: :starting + | :provisioning | :bootstrapping | :initializing | :ready @@ -28,7 +30,7 @@ defmodule JidoCode.Forge.SpriteSession do defstruct [ :session_id, :spec, - :sprite_id, + :infra_id, :client, :runner, :runner_state, @@ -38,13 +40,13 @@ defmodule JidoCode.Forge.SpriteSession do :started_at, :last_activity, :resume_checkpoint_id, - :sprite_client_module + :infra_client_module ] # Public API @doc """ - Start a new sprite session. + Start a new infrastructure session. """ @spec start_link({session_id(), map(), keyword()}) :: GenServer.on_start() def start_link({session_id, spec, opts}) do @@ -60,7 +62,7 @@ defmodule JidoCode.Forge.SpriteSession do end @doc """ - Execute a command directly in the sprite. + Execute a command directly in the environment. """ @spec exec(session_id(), String.t(), keyword()) :: {String.t(), non_neg_integer()} | {:error, term()} @@ -98,7 +100,7 @@ defmodule JidoCode.Forge.SpriteSession do def init({session_id, spec, opts}) do runner_type = Map.get(spec, :runner) || Map.get(spec, :runner_type, :shell) runner = resolve_runner(runner_type) - sprite_client = resolve_sprite_client(Map.get(spec, :sprite_client, :default)) + infra_client = resolve_infra_client(Map.get(spec, :infra_client) || Map.get(spec, :sprite_client, :default)) # Use runner_state from spec if resuming, otherwise use runner_config runner_state = @@ -112,7 +114,7 @@ defmodule JidoCode.Forge.SpriteSession do state = %__MODULE__{ session_id: session_id, spec: spec, - sprite_id: nil, + infra_id: nil, client: nil, runner: runner, runner_state: runner_state, @@ -122,7 +124,7 @@ defmodule JidoCode.Forge.SpriteSession do started_at: DateTime.utc_now(), last_activity: DateTime.utc_now(), resume_checkpoint_id: resume_checkpoint_id, - sprite_client_module: sprite_client + infra_client_module: infra_client } send(self(), :provision) @@ -131,36 +133,51 @@ defmodule JidoCode.Forge.SpriteSession do @impl true def handle_info(:provision, state) do - sprite_spec = Map.get(state.spec, :sprite, %{}) - sprite_client = state.sprite_client_module + infra_spec = + (Map.get(state.spec, :infra) || Map.get(state.spec, :sprite, %{})) + |> Map.merge(Map.take(state.spec, [:hetzner_config, :workspace_id])) + infra_client = state.infra_client_module - # If resuming from checkpoint, add checkpoint info to sprite spec - sprite_spec = + # Transition to :provisioning state + new_state = %{state | state: :provisioning, last_activity: DateTime.utc_now()} + notify_status(new_state) + + # If resuming from checkpoint, add checkpoint info to spec + infra_spec = if state.resume_checkpoint_id do - Map.put(sprite_spec, :restore_checkpoint, state.resume_checkpoint_id) + Map.put(infra_spec, :restore_checkpoint, state.resume_checkpoint_id) else - sprite_spec + infra_spec end - case sprite_client.create(sprite_spec) do - {:ok, client, sprite_id} -> + # Add on_progress callback for long-running provisioning + session_pid = self() + + infra_spec = + Map.put(infra_spec, :on_progress, fn stage, meta -> + send(session_pid, {:provision_progress, stage, meta}) + :ok + end) + + case infra_client.create(infra_spec) do + {:ok, client, infra_id} -> if state.resume_checkpoint_id do Logger.debug( - "Provisioned sprite #{sprite_id} from checkpoint #{state.resume_checkpoint_id} for session #{state.session_id}" + "Provisioned infra #{infra_id} from checkpoint #{state.resume_checkpoint_id} for session #{state.session_id}" ) else - Logger.debug("Provisioned sprite #{sprite_id} for session #{state.session_id}") + Logger.debug("Provisioned infra #{infra_id} for session #{state.session_id}") end new_state = %{ - state + new_state | client: client, - sprite_id: sprite_id, + infra_id: infra_id, state: :bootstrapping, last_activity: DateTime.utc_now() } - Persistence.record_provision_complete(state.session_id, sprite_id, nil) + Persistence.record_provision_complete(state.session_id, infra_id, nil) notify_status(new_state) # If resuming, skip bootstrap and go straight to runner init @@ -173,23 +190,39 @@ defmodule JidoCode.Forge.SpriteSession do {:noreply, new_state} {:error, reason} -> - Logger.error("Failed to provision sprite for session #{state.session_id}: #{inspect(reason)}") + Logger.error("Failed to provision infra for session #{state.session_id}: #{inspect(reason)}") {:stop, {:provision_failed, reason}, state} end end + def handle_info({:provision_progress, stage, meta}, state) do + status = %{ + session_id: state.session_id, + state: :provisioning, + provision_stage: stage, + provision_detail: meta, + infra_id: state.infra_id, + iteration: state.iteration, + started_at: state.started_at, + last_activity: DateTime.utc_now() + } + + ForgePubSub.broadcast_session(state.session_id, {:status, status}) + {:noreply, %{state | last_activity: DateTime.utc_now()}} + end + def handle_info(:bootstrap, state) do env = Map.get(state.spec, :env, %{}) - sprite_client = state.sprite_client_module + infra_client = state.infra_client_module - case sprite_client.inject_env(state.client, env) do + case infra_client.inject_env(state.client, env) do :ok -> bootstrap_steps = Map.get(state.spec, :bootstrap, []) case Bootstrap.execute(state.client, bootstrap_steps, - sprite_client: sprite_client, - sprite_id: state.sprite_id + infra_client: infra_client, + infra_id: state.infra_id ) do :ok -> Logger.debug("Bootstrap complete for session #{state.session_id}") @@ -269,8 +302,8 @@ defmodule JidoCode.Forge.SpriteSession do end def handle_call({:exec, command, opts}, _from, %{state: :ready} = state) do - sprite_client = state.sprite_client_module - result = sprite_client.exec(state.client, command, opts) + infra_client = state.infra_client_module + result = infra_client.exec(state.client, command, opts) new_state = %{state | last_activity: DateTime.utc_now()} {:reply, result, new_state} @@ -305,7 +338,7 @@ defmodule JidoCode.Forge.SpriteSession do status_map = %{ session_id: state.session_id, state: state.state, - sprite_id: state.sprite_id, + infra_id: state.infra_id, iteration: state.iteration, started_at: state.started_at, last_activity: state.last_activity @@ -353,15 +386,15 @@ defmodule JidoCode.Forge.SpriteSession do state.runner.terminate(state.client, reason) end - if state.client && state.sprite_id do - sprite_client = state.sprite_client_module + if state.client && state.infra_id do + infra_client = state.infra_client_module - case sprite_client.destroy(state.client, state.sprite_id) do + case infra_client.destroy(state.client, state.infra_id) do :ok -> - Logger.info("Destroyed sprite #{state.sprite_id}") + Logger.info("Destroyed infra #{state.infra_id}") {:error, err} -> - Logger.warning("Failed to destroy sprite #{state.sprite_id}: #{inspect(err)}") + Logger.warning("Failed to destroy infra #{state.infra_id}: #{inspect(err)}") end end @@ -380,16 +413,22 @@ defmodule JidoCode.Forge.SpriteSession do defp resolve_runner(:custom), do: JidoCode.Forge.Runners.Custom defp resolve_runner(module) when is_atom(module), do: module - defp resolve_sprite_client(:default), do: SpriteClient - defp resolve_sprite_client(:fake), do: JidoCode.Forge.SpriteClient.Fake - defp resolve_sprite_client(:live), do: JidoCode.Forge.SpriteClient.Live - defp resolve_sprite_client(module) when is_atom(module), do: module + defp resolve_infra_client(:default), do: InfraClient + defp resolve_infra_client(:fake), do: JidoCode.Forge.InfraClient.Fake + defp resolve_infra_client(:live), do: JidoCode.Forge.InfraClient.Sprite + defp resolve_infra_client(:sprite), do: JidoCode.Forge.InfraClient.Sprite + defp resolve_infra_client(module) when is_atom(module) do + case Application.get_env(:jido_code, :infra_clients, []) |> Keyword.get(module) do + nil -> module + resolved -> resolved + end + end defp notify_status(state) do status = %{ session_id: state.session_id, state: state.state, - sprite_id: state.sprite_id, + infra_id: state.infra_id, iteration: state.iteration, started_at: state.started_at, last_activity: state.last_activity diff --git a/lib/jido_code/forge/manager.ex b/lib/jido_code/forge/manager.ex index 10b26ce..b358a82 100644 --- a/lib/jido_code/forge/manager.ex +++ b/lib/jido_code/forge/manager.ex @@ -13,9 +13,9 @@ defmodule JidoCode.Forge.Manager do alias JidoCode.Forge.Persistence alias JidoCode.Forge.PubSub, as: ForgePubSub - alias JidoCode.Forge.SpriteSession + alias JidoCode.Forge.InfraSession - @supervisor JidoCode.Forge.SpriteSupervisor + @supervisor JidoCode.Forge.InfraSupervisor @registry JidoCode.Forge.SessionRegistry @default_max_sessions 50 @@ -106,7 +106,7 @@ defmodule JidoCode.Forge.Manager do [] -> Persistence.record_session_started(session_id, spec) - child_spec = {SpriteSession, {session_id, spec, []}} + child_spec = {InfraSession, {session_id, spec, []}} case DynamicSupervisor.start_child(@supervisor, child_spec) do {:ok, pid} -> diff --git a/lib/jido_code/forge/operations.ex b/lib/jido_code/forge/operations.ex index 10d2250..cf29ecc 100644 --- a/lib/jido_code/forge/operations.ex +++ b/lib/jido_code/forge/operations.ex @@ -18,7 +18,7 @@ defmodule JidoCode.Forge.Operations do 1. Loads session and validates it has a checkpoint 2. Updates session state to :resuming via Ash 3. Logs resume event - 4. Starts a new SpriteSession process with checkpoint restoration + 4. Starts a new InfraSession process with checkpoint restoration Returns `{:ok, pid}` on success or `{:error, reason}` on failure. """ @@ -40,7 +40,7 @@ defmodule JidoCode.Forge.Operations do 1. Updates session state to :cancelled via Ash 2. Logs cancellation event - 3. Stops the session process (which triggers sprite cleanup) + 3. Stops the session process (which triggers infra cleanup) 4. Broadcasts cancellation Returns `:ok` on success or `{:error, reason}` on failure. diff --git a/lib/jido_code/forge/persistence.ex b/lib/jido_code/forge/persistence.ex index 428899d..794f8a3 100644 --- a/lib/jido_code/forge/persistence.ex +++ b/lib/jido_code/forge/persistence.ex @@ -3,7 +3,7 @@ defmodule JidoCode.Forge.Persistence do Persistence layer for Forge sessions. Centralizes all Ash resource updates for session state transitions. - This module is called by SpriteSession to keep the database in sync + This module is called by InfraSession to keep the database in sync with runtime state. Design contract: Runtime (GenServer) is the source of truth for "what is @@ -35,7 +35,7 @@ defmodule JidoCode.Forge.Persistence do @doc """ Record that a session has started (provisioning phase). - Called from Manager.start_session or SpriteSession.init. + Called from Manager.start_session or InfraSession.init. """ @spec record_session_started(String.t(), map()) :: {:ok, Session.t()} | {:error, term()} | :noop def record_session_started(session_id, spec) do @@ -59,7 +59,7 @@ defmodule JidoCode.Forge.Persistence do end @doc """ - Record that provisioning is complete with sprite info. + Record that provisioning is complete with infra info. """ @spec record_provision_complete(String.t(), String.t(), String.t() | nil) :: {:ok, Session.t()} | {:error, term()} | :noop diff --git a/lib/jido_code/forge/runner.ex b/lib/jido_code/forge/runner.ex index 9fbebb6..f8ad2ae 100644 --- a/lib/jido_code/forge/runner.ex +++ b/lib/jido_code/forge/runner.ex @@ -1,6 +1,6 @@ defmodule JidoCode.Forge.Runner do @moduledoc """ - Behaviour for Forge runners that execute iterations in a sprite environment. + Behaviour for Forge runners that execute iterations in an infrastructure environment. Runners implement the core loop of the Forge system, handling initialization, iteration execution, input handling, and cleanup. @@ -18,7 +18,7 @@ defmodule JidoCode.Forge.Runner do } @type state :: term() - @type sprite_client :: term() + @type infra_client :: term() @type config :: map() @type opts :: keyword() @type chunk :: term() @@ -32,7 +32,7 @@ defmodule JidoCode.Forge.Runner do Called once before any iterations begin. Use this to set up initial state, inject environment variables, or run bootstrap commands. """ - @callback init(sprite_client(), config()) :: :ok | {:error, term()} + @callback init(infra_client(), config()) :: :ok | {:error, term()} @doc """ Execute a single iteration of the runner. @@ -40,7 +40,7 @@ defmodule JidoCode.Forge.Runner do Returns an iteration result indicating the status and any output. The runner should continue to be called while status is `:continue`. """ - @callback run_iteration(sprite_client(), state(), opts()) :: + @callback run_iteration(infra_client(), state(), opts()) :: {:ok, iteration_result()} | {:error, term()} @doc """ @@ -48,7 +48,7 @@ defmodule JidoCode.Forge.Runner do Called when the runner is in `:needs_input` status and input has been provided. """ - @callback apply_input(sprite_client(), input(), state()) :: :ok | {:error, term()} + @callback apply_input(infra_client(), input(), state()) :: :ok | {:error, term()} @doc """ Handle streaming output from the sprite. @@ -63,7 +63,7 @@ defmodule JidoCode.Forge.Runner do Optional callback for cleanup on normal or abnormal termination. """ - @callback terminate(sprite_client(), reason :: term()) :: :ok + @callback terminate(infra_client(), reason :: term()) :: :ok @optional_callbacks handle_output: 3, terminate: 2 diff --git a/lib/jido_code/forge/runners/claude_code.ex b/lib/jido_code/forge/runners/claude_code.ex index 39fba81..d0435ea 100644 --- a/lib/jido_code/forge/runners/claude_code.ex +++ b/lib/jido_code/forge/runners/claude_code.ex @@ -14,9 +14,9 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do * `:context_template` - Additional context to append * `:claude_settings` - Claude CLI settings JSON - ## Sprite Layout + ## Environment Layout - The runner sets up the following structure in the sprite: + The runner sets up the following structure in the environment: /var/local/forge/ +-- session/ # Session state files @@ -29,7 +29,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do require Logger alias JidoCode.Forge.PromptRedaction - alias JidoCode.Forge.SpriteClient + alias JidoCode.Forge.InfraClient @forge_home "/var/local/forge" @@ -52,7 +52,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do with {:ok, redacted_prompt} <- redact_prompt(prompt, :run_iteration_prompt) do cmd = build_claude_command(model, max_turns, max_budget, redacted_prompt) - case SpriteClient.exec(client, cmd, timeout: :infinity) do + case InfraClient.exec(client, cmd, timeout: :infinity) do {output, 0} -> result = parse_claude_output(output) {:ok, result} @@ -76,7 +76,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do @impl true def apply_input(client, input, _state) do response = Jason.encode!(%{answer: input}) - SpriteClient.write_file(client, "#{@forge_home}/session/response.json", response) + InfraClient.write_file(client, "#{@forge_home}/session/response.json", response) end @impl true @@ -102,7 +102,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do ] Enum.reduce_while(dirs, :ok, fn dir, :ok -> - case SpriteClient.exec(client, "mkdir -p #{dir}", []) do + case InfraClient.exec(client, "mkdir -p #{dir}", []) do {_, 0} -> {:cont, :ok} {output, code} -> {:halt, {:error, {:mkdir_failed, dir, output, code}}} end @@ -115,7 +115,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do :ok settings -> - SpriteClient.write_file( + InfraClient.write_file( client, "#{@forge_home}/.claude/settings.json", Jason.encode!(settings) @@ -137,7 +137,7 @@ defmodule JidoCode.Forge.Runners.ClaudeCode do content -> with {:ok, redacted_content} <- redact_prompt(content, template_operation(key)) do - SpriteClient.write_file(client, "#{@forge_home}/templates/#{filename}", redacted_content) + InfraClient.write_file(client, "#{@forge_home}/templates/#{filename}", redacted_content) else {:error, typed_error} = error -> emit_redaction_failure(template_operation(key), typed_error) diff --git a/lib/jido_code/forge/runners/shell.ex b/lib/jido_code/forge/runners/shell.ex index cacf469..33e099e 100644 --- a/lib/jido_code/forge/runners/shell.ex +++ b/lib/jido_code/forge/runners/shell.ex @@ -8,7 +8,7 @@ defmodule JidoCode.Forge.Runners.Shell do @behaviour JidoCode.Forge.Runner - alias JidoCode.Forge.SpriteClient + alias JidoCode.Forge.InfraClient @impl true def init(_client, _config) do @@ -19,7 +19,7 @@ defmodule JidoCode.Forge.Runners.Shell do def run_iteration(client, state, opts) do command = opts[:command] || Map.get(state, :command) || Map.get(state, "command") - case SpriteClient.exec(client, command, opts) do + case InfraClient.exec(client, command, opts) do {output, 0} -> {:ok, %{ diff --git a/lib/jido_code/forge/runners/workflow.ex b/lib/jido_code/forge/runners/workflow.ex index 3756703..7ccec36 100644 --- a/lib/jido_code/forge/runners/workflow.ex +++ b/lib/jido_code/forge/runners/workflow.ex @@ -3,7 +3,7 @@ defmodule JidoCode.Forge.Runners.Workflow do Data-driven workflow runner. Executes a series of steps defined in data, supporting: - - `:exec` - Run shell command with SpriteClient.exec + - `:exec` - Run shell command with InfraClient.exec - `:prompt` - Return :needs_input status with question - `:condition` - Evaluate check and set jump_to in metadata - `:call` - Call a custom StepHandler module @@ -24,14 +24,14 @@ defmodule JidoCode.Forge.Runners.Workflow do @behaviour JidoCode.Forge.Runner alias JidoCode.Forge.Runner - alias JidoCode.Forge.SpriteClient + alias JidoCode.Forge.InfraClient @impl true def init(client, %{workflow: workflow} = config) do - if config[:write_to_sprite] do + if config[:write_to_infra] do path = config[:workflow_path] || "/tmp/workflow.json" content = Jason.encode!(workflow) - SpriteClient.write_file(client, path, content) + InfraClient.write_file(client, path, content) end :ok @@ -94,7 +94,7 @@ defmodule JidoCode.Forge.Runners.Workflow do command = step[:command] || step["command"] interpolated = interpolate_variables(command, step_results) - case SpriteClient.exec(client, interpolated, opts) do + case InfraClient.exec(client, interpolated, opts) do {output, 0} -> new_results = Map.put(step_results, step_id, %{output: output, exit_code: 0}) diff --git a/lib/jido_code/forge/sprite_client/behaviour.ex b/lib/jido_code/forge/sprite_client/behaviour.ex deleted file mode 100644 index 3d538c5..0000000 --- a/lib/jido_code/forge/sprite_client/behaviour.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule JidoCode.Forge.SpriteClient.Behaviour do - @moduledoc """ - Behaviour defining the interface for sprite client implementations. - - A sprite is an isolated execution environment (container, VM, or local sandbox) - where Forge runners execute commands and manage files. - """ - - @type client :: term() - @type sprite_id :: String.t() - @type spec :: map() - @type command :: String.t() - @type path :: String.t() - @type content :: binary() - @type env_map :: %{String.t() => String.t()} - @type handle :: term() - @type opts :: keyword() - - @doc """ - Create a new sprite from the given specification. - - Returns the client state and a unique sprite identifier. - """ - @callback create(spec()) :: {:ok, client(), sprite_id()} | {:error, term()} - - @doc """ - Execute a command synchronously in the sprite. - - Returns the output and exit code. - """ - @callback exec(client(), command(), opts()) :: {String.t(), non_neg_integer()} - - @doc """ - Spawn an asynchronous command in the sprite. - - Returns a handle for monitoring or interacting with the process. - """ - @callback spawn(client(), command(), args :: [String.t()], opts()) :: - {:ok, handle()} | {:error, term()} - - @doc """ - Write content to a file in the sprite. - """ - @callback write_file(client(), path(), content()) :: :ok | {:error, term()} - - @doc """ - Read content from a file in the sprite. - """ - @callback read_file(client(), path()) :: {:ok, content()} | {:error, term()} - - @doc """ - Inject environment variables into the sprite. - - These should be available to all subsequent commands. - """ - @callback inject_env(client(), env_map()) :: :ok | {:error, term()} - - @doc """ - Destroy the sprite and clean up resources. - """ - @callback destroy(client(), sprite_id()) :: :ok | {:error, term()} - - @doc """ - Returns the implementation module for this client type. - """ - @callback impl_module() :: module() -end diff --git a/lib/jido_code/forge/workers/streaming_exec_session_worker.ex b/lib/jido_code/forge/workers/streaming_exec_session_worker.ex index 11615cb..1eb80f1 100644 --- a/lib/jido_code/forge/workers/streaming_exec_session_worker.ex +++ b/lib/jido_code/forge/workers/streaming_exec_session_worker.ex @@ -2,7 +2,7 @@ defmodule JidoCode.Forge.Workers.StreamingExecSessionWorker do @moduledoc """ Worker for streaming command execution with output coalescing and backpressure. - Handles real-time output streaming from sprite commands, coalescing chunks + Handles real-time output streaming from infra commands, coalescing chunks to avoid overwhelming subscribers while maintaining responsiveness. """ @@ -24,7 +24,7 @@ defmodule JidoCode.Forge.Workers.StreamingExecSessionWorker do :exec_session_id, :sequence, :command_ref, - :sprite_client, + :infra_client, :client, buffer: "", last_flush: 0, @@ -44,8 +44,8 @@ defmodule JidoCode.Forge.Workers.StreamingExecSessionWorker do * `:session_id` - The parent session ID (required) * `:sequence` - Execution sequence number (required) * `:command` - Command to execute (required) - * `:client` - Sprite client struct (required) - * `:sprite_client` - Sprite client module (required) + * `:client` - Infra client struct (required) + * `:infra_client` - Infra client module (required) * `:sprites_session_id` - Optional sprites API session ID * `:metadata` - Optional metadata map """ @@ -63,20 +63,20 @@ defmodule JidoCode.Forge.Workers.StreamingExecSessionWorker do sequence = Keyword.fetch!(args, :sequence) command = Keyword.fetch!(args, :command) client = Keyword.fetch!(args, :client) - sprite_client = Keyword.fetch!(args, :sprite_client) + infra_client = Keyword.fetch!(args, :infra_client) sprites_session_id = Keyword.get(args, :sprites_session_id) metadata = Keyword.get(args, :metadata, %{}) case create_exec_session_record(session_id, sequence, command, sprites_session_id, metadata) do {:ok, exec_session} -> - case sprite_client.spawn(client, "bash", ["-c", command], tty: true) do + case infra_client.spawn(client, "bash", ["-c", command], tty: true) do {:ok, cmd_ref} -> state = %__MODULE__{ session_id: session_id, exec_session_id: exec_session.id, sequence: sequence, command_ref: cmd_ref, - sprite_client: sprite_client, + infra_client: infra_client, client: client, buffer: "", last_flush: System.monotonic_time(:millisecond), diff --git a/lib/jido_code_web/live/forge/index_live.ex b/lib/jido_code_web/live/forge/index_live.ex index f8a6abf..a634745 100644 --- a/lib/jido_code_web/live/forge/index_live.ex +++ b/lib/jido_code_web/live/forge/index_live.ex @@ -3,7 +3,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do alias JidoCode.Forge alias JidoCode.Forge.PubSub, as: ForgePubSub - alias JidoCode.Forge.SpriteClient.Live, as: LiveClient + alias JidoCode.Forge.InfraClient.Sprite, as: SpriteClient @impl true def mount(_params, _session, socket) do @@ -56,7 +56,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do end def handle_event("destroy_sprite", %{"name" => name}, socket) do - case LiveClient.destroy_by_name(name) do + case SpriteClient.destroy_by_name(name) do :ok -> sprites = load_sprites() @@ -74,7 +74,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do session_id = "test-#{:erlang.unique_integer([:positive])}" spec = %{ - sprite_client: :live, + infra_client: :live, runner: :shell, runner_config: %{command: "cat /app/greeting.txt && echo 'TEST_VAR='$TEST_VAR"}, env: %{"TEST_VAR" => "hello_from_forge"}, @@ -105,7 +105,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do

Forge Sessions

<.link navigate={~p"/forge/new"} class="btn btn-primary"> New Session @@ -220,7 +220,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do end defp load_sprites do - case LiveClient.list_sprites() do + case SpriteClient.list_sprites() do {:ok, sprites} -> sprites {:error, _} -> [] end @@ -261,6 +261,7 @@ defmodule JidoCodeWeb.Forge.IndexLive do end defp state_colors(:starting), do: {"bg-info/20", "text-info"} + defp state_colors(:provisioning), do: {"bg-info/20", "text-info"} defp state_colors(:bootstrapping), do: {"bg-info/20", "text-info"} defp state_colors(:initializing), do: {"bg-info/20", "text-info"} defp state_colors(:ready), do: {"bg-success/20", "text-success"} diff --git a/lib/jido_code_web/live/forge/show_live.ex b/lib/jido_code_web/live/forge/show_live.ex index e7607b2..170f839 100644 --- a/lib/jido_code_web/live/forge/show_live.ex +++ b/lib/jido_code_web/live/forge/show_live.ex @@ -329,14 +329,17 @@ defmodule JidoCodeWeb.Forge.ShowLive do
State
<.state_badge state={@status.state} />
+ <%= if @status[:provision_stage] do %> +
<%= provision_stage_label(@status.provision_stage) %>
+ <% end %>
Iteration
{@status.iteration}
-
Sprite ID
-
{@status.sprite_id || "—"}
+
Infra ID
+
{@status[:infra_id] || @status[:sprite_id] || "—"}
Last Activity
@@ -703,6 +706,7 @@ defmodule JidoCodeWeb.Forge.ShowLive do end defp state_colors(:starting), do: {"bg-info/20", "text-info"} + defp state_colors(:provisioning), do: {"bg-info/20", "text-info"} defp state_colors(:bootstrapping), do: {"bg-info/20", "text-info"} defp state_colors(:initializing), do: {"bg-info/20", "text-info"} defp state_colors(:ready), do: {"bg-success/20", "text-success"} @@ -712,6 +716,14 @@ defmodule JidoCodeWeb.Forge.ShowLive do defp state_colors(:stopped), do: {"bg-base-300", "text-base-content/60"} defp state_colors(_), do: {"bg-base-300", "text-base-content"} + defp provision_stage_label(:ssh_key), do: "Setting up SSH key..." + defp provision_stage_label(:server_creating), do: "Creating server..." + defp provision_stage_label(:server_booting), do: "Booting server..." + defp provision_stage_label(:ssh_waiting), do: "Waiting for SSH..." + defp provision_stage_label(:ssh_connected), do: "SSH connected" + defp provision_stage_label(:session_starting), do: "Starting session..." + defp provision_stage_label(_), do: "Provisioning..." + defp maybe_raise_security_alert(socket, %{security_alert?: true, reason: reason}) do assign(socket, security_alert_message: UiRedaction.security_alert_message(reason), diff --git a/test/jido_code/forge/sprite_integration_test.exs b/test/jido_code/forge/sprite_integration_test.exs index bf36a66..56fb2d3 100644 --- a/test/jido_code/forge/sprite_integration_test.exs +++ b/test/jido_code/forge/sprite_integration_test.exs @@ -12,7 +12,7 @@ defmodule JidoCode.Forge.SpriteIntegrationTest do alias JidoCode.Forge alias JidoCode.Forge.PubSub, as: ForgePubSub - alias JidoCode.Forge.SpriteClient.Live + alias JidoCode.Forge.InfraClient.Sprite, as: Live @moduletag :sprite_integration @@ -22,17 +22,17 @@ defmodule JidoCode.Forge.SpriteIntegrationTest do if is_nil(token) or token == "" do {:ok, skip: true} else - original_client = Application.get_env(:jido_code, :forge_sprite_client) + original_client = Application.get_env(:jido_code, :forge_infra_client) original_persistence = Application.get_env(:jido_code, JidoCode.Forge.Persistence) - Application.put_env(:jido_code, :forge_sprite_client, Live) + Application.put_env(:jido_code, :forge_infra_client, Live) Application.put_env(:jido_code, JidoCode.Forge.Persistence, enabled: false) on_exit(fn -> if original_client do - Application.put_env(:jido_code, :forge_sprite_client, original_client) + Application.put_env(:jido_code, :forge_infra_client, original_client) else - Application.delete_env(:jido_code, :forge_sprite_client) + Application.delete_env(:jido_code, :forge_infra_client) end if original_persistence do diff --git a/test/jido_code_web/live/forge_show_live_redaction_test.exs b/test/jido_code_web/live/forge_show_live_redaction_test.exs index 4330b0f..0d0f7ff 100644 --- a/test/jido_code_web/live/forge_show_live_redaction_test.exs +++ b/test/jido_code_web/live/forge_show_live_redaction_test.exs @@ -39,7 +39,7 @@ defmodule JidoCodeWeb.ForgeShowLiveRedactionTest do session_id = "forge-redaction-#{System.unique_integer([:positive])}" spec = %{ - sprite_client: :fake, + infra_client: :fake, runner: :shell, runner_config: %{command: "echo redaction-ready"}, env: %{}, diff --git a/test/jido_code_web/live/forge_show_live_stream_continuity_test.exs b/test/jido_code_web/live/forge_show_live_stream_continuity_test.exs index bc606d9..6034925 100644 --- a/test/jido_code_web/live/forge_show_live_stream_continuity_test.exs +++ b/test/jido_code_web/live/forge_show_live_stream_continuity_test.exs @@ -104,7 +104,7 @@ defmodule JidoCodeWeb.ForgeShowLiveStreamContinuityTest do session_id = "forge-stream-#{System.unique_integer([:positive])}" spec = %{ - sprite_client: :fake, + infra_client: :fake, runner: :shell, runner_config: %{command: "echo stream-ready"}, env: %{},