feat: add main features

Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

663
crates/domain/src/ports.rs Normal file
View File

@ -0,0 +1,663 @@
//! Ports (traits) — the boundary the domain defines and the infrastructure
//! implements (the **D** of SOLID, materialised).
//!
//! # Async decision
//!
//! Ports that touch the outside world (PTY, filesystem, process, stores, git,
//! remote connect) are inherently asynchronous in every realistic adapter
//! (tokio fs/process, russh, sftp). We therefore make those traits `async` via
//! [`async_trait`]. The rationale for `#[async_trait]` over native
//! `async fn` in traits / `-> impl Future`:
//!
//! - These ports are consumed as **trait objects** (`Arc<dyn FileSystem>`,
//! injected at the composition root). Native `async fn` in traits is not yet
//! dyn-compatible without boxing, so `async_trait` (which boxes the future)
//! is the pragmatic, stable choice and keeps the call sites object-safe.
//! - It keeps signatures readable and uniform across all adapters.
//!
//! Purely synchronous, non-blocking ports ([`Clock`], [`IdGenerator`],
//! [`EventBus`], the CPU-bound part of [`AgentRuntime`]) stay plain `fn` — no
//! need to pay the boxing cost.
//!
//! Each port is **fine-grained** (Interface Segregation): `FileSystem`,
//! `PtyPort`, `ProcessSpawner` are separate, never a `System` god-trait.
use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error;
use crate::agent::AgentManifest;
use crate::events::DomainEvent;
use crate::ids::{AgentId, SessionId};
use crate::markdown::MarkdownDoc;
use crate::profile::AgentProfile;
use crate::project::{Project, ProjectPath};
use crate::remote::RemoteKind;
use crate::template::AgentTemplate;
use crate::terminal::PtySize;
// ---------------------------------------------------------------------------
// Support value types shared across ports
// ---------------------------------------------------------------------------
/// How the `.md` context should be delivered to a spawned process, resolved
/// from a profile's [`crate::profile::ContextInjection`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContextInjectionPlan {
/// Materialise the context at a (relative) file path before launch.
File {
/// Relative target path inside the cwd.
target: String,
},
/// Append the given already-rendered arguments to the command line.
Args {
/// Extra arguments carrying the context path.
args: Vec<String>,
},
/// Pipe the content on stdin.
Stdin,
/// Provide the content (or its path) via an environment variable.
Env {
/// Variable name.
var: String,
},
}
/// A fully-resolved process invocation: command, args, cwd, environment, and the
/// plan for delivering the agent context.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpawnSpec {
/// Executable to run.
pub command: String,
/// Arguments.
pub args: Vec<String>,
/// Working directory.
pub cwd: ProjectPath,
/// Extra environment variables.
pub env: Vec<(String, String)>,
/// How the context is injected, if any.
pub context_plan: Option<ContextInjectionPlan>,
}
/// The agent context prepared for injection (content + the on-disk path it maps
/// to within the project).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreparedContext {
/// Rendered Markdown content.
pub content: MarkdownDoc,
/// Relative path of the `.md` inside the project.
pub relative_path: String,
}
/// An opaque handle to a live PTY, owned by the adapter.
///
/// The domain only needs an identity to address the PTY in subsequent calls;
/// the actual OS handle lives in infrastructure.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PtyHandle {
/// The session this PTY backs.
pub session_id: SessionId,
}
/// Exit status of a process.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExitStatus {
/// Exit code (`None` if terminated by a signal).
pub code: Option<i32>,
}
/// Captured output of a non-interactive process run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output {
/// Exit status.
pub status: ExitStatus,
/// Captured stdout.
pub stdout: Vec<u8>,
/// Captured stderr.
pub stderr: Vec<u8>,
}
/// A location-neutral path used by [`FileSystem`] (local, SFTP, or WSL).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RemotePath(pub String);
impl RemotePath {
/// Wraps a raw path.
#[must_use]
pub fn new(p: impl Into<String>) -> Self {
Self(p.into())
}
/// Returns the path as a string slice.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// A single directory entry returned by [`FileSystem::list`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
/// Entry name (basename).
pub name: String,
/// Whether the entry is a directory.
pub is_dir: bool,
}
/// An owned, boxed stream of PTY output chunks.
///
/// Concrete adapters decide the underlying transport; the domain only sees a
/// dynamically-dispatched iterator of byte chunks.
pub type OutputStream = Box<dyn Iterator<Item = Vec<u8>> + Send>;
/// A boxed stream of domain events, returned by [`EventBus::subscribe`].
pub type EventStream = Box<dyn Iterator<Item = DomainEvent> + Send>;
// ---------------------------------------------------------------------------
// Per-port error types
// ---------------------------------------------------------------------------
/// Errors from [`AgentRuntime`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum RuntimeError {
/// The configured command could not be resolved/detected.
#[error("agent runtime: {0}")]
Detection(String),
/// The invocation could not be prepared (bad profile, etc.).
#[error("agent runtime: invalid invocation: {0}")]
Invocation(String),
}
/// Errors from [`PtyPort`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PtyError {
/// Failed to spawn the PTY.
#[error("pty spawn failed: {0}")]
Spawn(String),
/// Read/write failure.
#[error("pty io failed: {0}")]
Io(String),
/// The handle referred to no live PTY.
#[error("pty handle not found")]
NotFound,
}
/// Errors from [`ProcessSpawner`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ProcessError {
/// The process could not be started.
#[error("process spawn failed: {0}")]
Spawn(String),
/// I/O failure while running.
#[error("process io failed: {0}")]
Io(String),
}
/// Errors from [`FileSystem`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum FsError {
/// Path not found.
#[error("path not found: {0}")]
NotFound(String),
/// Permission denied.
#[error("permission denied: {0}")]
PermissionDenied(String),
/// Other I/O error.
#[error("filesystem io failed: {0}")]
Io(String),
}
/// Errors from persistence stores ([`TemplateStore`], [`ProjectStore`],
/// [`AgentContextStore`]).
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum StoreError {
/// The requested item was not found.
#[error("not found")]
NotFound,
/// (De)serialisation failed.
#[error("serialization failed: {0}")]
Serialization(String),
/// Underlying I/O error.
#[error("store io failed: {0}")]
Io(String),
}
/// Errors from [`RemoteHost`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum RemoteError {
/// Connection failed.
#[error("remote connection failed: {0}")]
Connection(String),
/// Authentication failed.
#[error("remote authentication failed: {0}")]
Auth(String),
}
/// Errors from the git [`GitPort`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum GitError {
/// The repository was not found / not initialised.
#[error("git repository not found")]
NotFound,
/// A git operation failed.
#[error("git operation failed: {0}")]
Operation(String),
}
// ---------------------------------------------------------------------------
// Git port support types
// ---------------------------------------------------------------------------
/// Status of a single path in the working tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitFileStatus {
/// Path relative to the repo root.
pub path: String,
/// Whether the change is staged.
pub staged: bool,
}
/// A commit summary.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitCommitInfo {
/// Commit hash.
pub hash: String,
/// Commit message summary.
pub summary: String,
}
/// A commit enriched for graph display (DAG edges + refs + author + time).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraphCommit {
/// Full commit hash (hex string).
pub hash: String,
/// First line of the commit message.
pub summary: String,
/// Parent commit hashes (0 for root, ≥2 for merges).
pub parents: Vec<String>,
/// Human-readable ref labels pointing at this commit
/// (e.g. `"main"`, `"tag: v1.0"`).
pub refs: Vec<String>,
/// Author name (empty string when unavailable).
pub author: String,
/// Author timestamp in Unix seconds.
pub timestamp: i64,
}
// ---------------------------------------------------------------------------
// Ports
// ---------------------------------------------------------------------------
/// Launch/drive an AI CLI according to an [`AgentProfile`], handling context
/// injection. CPU-bound and synchronous — kept plain `fn`.
pub trait AgentRuntime: Send + Sync {
/// Detects whether the profile's command is available.
///
/// # Errors
/// [`RuntimeError`] on detection failure.
fn detect(&self, profile: &AgentProfile) -> Result<bool, RuntimeError>;
/// Builds a [`SpawnSpec`] (command + args + injection plan) for launching
/// the agent in `cwd` with the prepared context.
///
/// # Errors
/// [`RuntimeError`] if the invocation cannot be prepared.
fn prepare_invocation(
&self,
profile: &AgentProfile,
ctx: &PreparedContext,
cwd: &ProjectPath,
) -> Result<SpawnSpec, RuntimeError>;
}
/// Open and drive pseudo-terminals.
#[async_trait]
pub trait PtyPort: Send + Sync {
/// Spawns a PTY running `spec` at the given `size`.
///
/// # Errors
/// [`PtyError`] on failure.
async fn spawn(&self, spec: SpawnSpec, size: PtySize) -> Result<PtyHandle, PtyError>;
/// Writes bytes to the PTY.
///
/// # Errors
/// [`PtyError`] on failure.
fn write(&self, handle: &PtyHandle, data: &[u8]) -> Result<(), PtyError>;
/// Resizes the PTY.
///
/// # Errors
/// [`PtyError`] on failure.
fn resize(&self, handle: &PtyHandle, size: PtySize) -> Result<(), PtyError>;
/// Subscribes to the PTY's byte output stream.
///
/// # Errors
/// [`PtyError`] if the handle is unknown.
fn subscribe_output(&self, handle: &PtyHandle) -> Result<OutputStream, PtyError>;
/// Kills the PTY's process, returning its exit status.
///
/// # Errors
/// [`PtyError`] on failure.
async fn kill(&self, handle: &PtyHandle) -> Result<ExitStatus, PtyError>;
}
/// Run a non-interactive process and capture its output.
#[async_trait]
pub trait ProcessSpawner: Send + Sync {
/// Runs `spec` to completion.
///
/// # Errors
/// [`ProcessError`] on failure.
async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError>;
}
/// Location-neutral filesystem access.
#[async_trait]
pub trait FileSystem: Send + Sync {
/// Reads a file.
///
/// # Errors
/// [`FsError`] on failure.
async fn read(&self, path: &RemotePath) -> Result<Vec<u8>, FsError>;
/// Writes a file (creating or truncating).
///
/// # Errors
/// [`FsError`] on failure.
async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError>;
/// Returns whether the path exists.
///
/// # Errors
/// [`FsError`] on failure.
async fn exists(&self, path: &RemotePath) -> Result<bool, FsError>;
/// Creates a directory and all missing parents.
///
/// # Errors
/// [`FsError`] on failure.
async fn create_dir_all(&self, path: &RemotePath) -> Result<(), FsError>;
/// Lists the entries of a directory.
///
/// # Errors
/// [`FsError`] on failure.
async fn list(&self, path: &RemotePath) -> Result<Vec<DirEntry>, FsError>;
/// Creates a symbolic link `dst` pointing at `src`.
///
/// # Errors
/// [`FsError`] on failure.
async fn symlink(&self, src: &RemotePath, dst: &RemotePath) -> Result<(), FsError>;
}
/// Strategy abstracting *where* execution happens (local / SSH / WSL). Acts as a
/// factory for the location-appropriate fine-grained ports.
#[async_trait]
pub trait RemoteHost: Send + Sync {
/// The kind of this host.
fn kind(&self) -> RemoteKind;
/// Establishes the connection (no-op for local).
///
/// # Errors
/// [`RemoteError`] on failure.
async fn connect(&self) -> Result<(), RemoteError>;
/// Returns the filesystem port for this host.
fn file_system(&self) -> Arc<dyn FileSystem>;
/// Returns the process spawner for this host.
fn process_spawner(&self) -> Arc<dyn ProcessSpawner>;
/// Returns the PTY port for this host.
fn pty(&self) -> Arc<dyn PtyPort>;
}
/// CRUD + versioning for agent templates in the global IDE store.
#[async_trait]
pub trait TemplateStore: Send + Sync {
/// Lists all templates.
///
/// # Errors
/// [`StoreError`] on failure.
async fn list(&self) -> Result<Vec<AgentTemplate>, StoreError>;
/// Gets a template by id.
///
/// # Errors
/// [`StoreError::NotFound`] if absent.
async fn get(&self, id: crate::ids::TemplateId) -> Result<AgentTemplate, StoreError>;
/// Saves (creates or replaces) a template.
///
/// # Errors
/// [`StoreError`] on failure.
async fn save(&self, template: &AgentTemplate) -> Result<(), StoreError>;
/// Deletes a template.
///
/// # Errors
/// [`StoreError`] on failure.
async fn delete(&self, id: crate::ids::TemplateId) -> Result<(), StoreError>;
}
/// Persistence of the known-projects registry and the workspace.
#[async_trait]
pub trait ProjectStore: Send + Sync {
/// Lists all known projects.
///
/// # Errors
/// [`StoreError`] on failure.
async fn list_projects(&self) -> Result<Vec<Project>, StoreError>;
/// Loads a single project.
///
/// # Errors
/// [`StoreError::NotFound`] if absent.
async fn load_project(&self, id: crate::ids::ProjectId) -> Result<Project, StoreError>;
/// Saves (creates or replaces) a project.
///
/// # Errors
/// [`StoreError`] on failure.
async fn save_project(&self, project: &Project) -> Result<(), StoreError>;
/// Saves the whole workspace (windows/tabs/layouts).
///
/// # Errors
/// [`StoreError`] on failure.
async fn save_workspace(&self, workspace: &crate::layout::Workspace) -> Result<(), StoreError>;
/// Loads the persisted workspace.
///
/// # Errors
/// [`StoreError`] on failure.
async fn load_workspace(&self) -> Result<crate::layout::Workspace, StoreError>;
}
/// CRUD for the configured [`AgentProfile`]s in the global IDE store
/// (`profiles.json`, ARCHITECTURE §9.2). Profiles are the *data* that drives the
/// single generic [`AgentRuntime`] adapter (Open/Closed).
#[async_trait]
pub trait ProfileStore: Send + Sync {
/// Lists all configured profiles.
///
/// # Errors
/// [`StoreError`] on failure.
async fn list(&self) -> Result<Vec<AgentProfile>, StoreError>;
/// Saves (creates or replaces by id) a profile.
///
/// # Errors
/// [`StoreError`] on failure.
async fn save(&self, profile: &AgentProfile) -> Result<(), StoreError>;
/// Deletes a profile by id.
///
/// # Errors
/// [`StoreError`] on failure.
async fn delete(&self, id: crate::ids::ProfileId) -> Result<(), StoreError>;
/// Whether the profiles store has been initialised yet — used to detect the
/// first run of the IDE (no `profiles.json` ⇒ first run).
///
/// # Errors
/// [`StoreError`] on failure.
async fn is_configured(&self) -> Result<bool, StoreError>;
/// Persists an (possibly empty) profiles store, recording that the first run
/// is complete even when the user kept no profile.
///
/// # Errors
/// [`StoreError`] on failure.
async fn mark_configured(&self) -> Result<(), StoreError>;
}
/// Reads/writes agent `.md` contexts and the project manifest, within a project.
#[async_trait]
pub trait AgentContextStore: Send + Sync {
/// Reads an agent's context.
///
/// # Errors
/// [`StoreError`] on failure.
async fn read_context(
&self,
project: &Project,
agent: &AgentId,
) -> Result<MarkdownDoc, StoreError>;
/// Writes an agent's context.
///
/// # Errors
/// [`StoreError`] on failure.
async fn write_context(
&self,
project: &Project,
agent: &AgentId,
md: &MarkdownDoc,
) -> Result<(), StoreError>;
/// Loads the project manifest.
///
/// # Errors
/// [`StoreError`] on failure.
async fn load_manifest(&self, project: &Project) -> Result<AgentManifest, StoreError>;
/// Saves the project manifest.
///
/// # Errors
/// [`StoreError`] on failure.
async fn save_manifest(
&self,
project: &Project,
manifest: &AgentManifest,
) -> Result<(), StoreError>;
}
/// Git operations for a project. Named `GitPort` to avoid clashing with the
/// [`crate::git::GitRepository`] *entity* (state image).
#[async_trait]
pub trait GitPort: Send + Sync {
/// Initialises a repository at the root.
///
/// # Errors
/// [`GitError`] on failure.
async fn init(&self, root: &ProjectPath) -> Result<(), GitError>;
/// Returns the status of changed paths.
///
/// # Errors
/// [`GitError`] on failure.
async fn status(&self, root: &ProjectPath) -> Result<Vec<GitFileStatus>, GitError>;
/// Stages a path.
///
/// # Errors
/// [`GitError`] on failure.
async fn stage(&self, root: &ProjectPath, path: &str) -> Result<(), GitError>;
/// Unstages a path.
///
/// # Errors
/// [`GitError`] on failure.
async fn unstage(&self, root: &ProjectPath, path: &str) -> Result<(), GitError>;
/// Creates a commit with the given message.
///
/// # Errors
/// [`GitError`] on failure.
async fn commit(&self, root: &ProjectPath, message: &str) -> Result<GitCommitInfo, GitError>;
/// Lists branches.
///
/// # Errors
/// [`GitError`] on failure.
async fn branches(&self, root: &ProjectPath) -> Result<Vec<String>, GitError>;
/// Returns the current branch.
///
/// # Errors
/// [`GitError`] on failure.
async fn current_branch(&self, root: &ProjectPath) -> Result<Option<String>, GitError>;
/// Checks out a branch.
///
/// # Errors
/// [`GitError`] on failure.
async fn checkout(&self, root: &ProjectPath, branch: &str) -> Result<(), GitError>;
/// Returns the recent commit log.
///
/// # Errors
/// [`GitError`] on failure.
async fn log(&self, root: &ProjectPath, limit: usize)
-> Result<Vec<GitCommitInfo>, GitError>;
/// Returns the commit graph (all local branches, topological + time sort).
///
/// # Errors
/// [`GitError`] on failure.
async fn log_graph(
&self,
root: &ProjectPath,
limit: usize,
) -> Result<Vec<GraphCommit>, GitError>;
/// Pulls from the default remote.
///
/// # Errors
/// [`GitError`] on failure.
async fn pull(&self, root: &ProjectPath) -> Result<(), GitError>;
/// Pushes to the default remote.
///
/// # Errors
/// [`GitError`] on failure.
async fn push(&self, root: &ProjectPath) -> Result<(), GitError>;
}
/// Publish/subscribe domain events. Synchronous, in-process.
pub trait EventBus: Send + Sync {
/// Publishes an event.
fn publish(&self, event: DomainEvent);
/// Subscribes to the event stream.
fn subscribe(&self) -> EventStream;
}
/// Provides the current time (epoch milliseconds), abstracted for determinism.
pub trait Clock: Send + Sync {
/// Returns "now" as epoch milliseconds.
fn now_millis(&self) -> i64;
}
/// Generates fresh UUIDs, abstracted for determinism in tests.
pub trait IdGenerator: Send + Sync {
/// Returns a fresh UUID.
fn new_uuid(&self) -> uuid::Uuid;
}