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:
663
crates/domain/src/ports.rs
Normal file
663
crates/domain/src/ports.rs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user