Preview deployments let reviewers spin up the app at a specific branch in local Docker containers, accessible through a single Caddy reverse proxy port. Docker is the source of truth — no database table needed. New module: src/preview/ with config discovery (.cw-preview.yml → compose → Dockerfile fallback), compose generation, Docker CLI wrapper, health checking, and port allocation (9100-9200 range).
619 lines
12 KiB
TypeScript
619 lines
12 KiB
TypeScript
/**
|
|
* Event Bus Types
|
|
*
|
|
* Port interface for the event bus - the backbone of the hexagonal architecture.
|
|
* EventBus is the PORT. Implementations (EventEmitterBus, RabbitMQ, etc.) are ADAPTERS.
|
|
*/
|
|
|
|
/**
|
|
* Base interface for all domain events.
|
|
* Every event in the system extends this.
|
|
*/
|
|
export interface DomainEvent {
|
|
/** Event type identifier (e.g., 'process:spawned', 'server:started') */
|
|
type: string;
|
|
/** When the event occurred */
|
|
timestamp: Date;
|
|
/** Event-specific data */
|
|
payload: unknown;
|
|
}
|
|
|
|
/**
|
|
* Event Bus Port Interface
|
|
*
|
|
* All modules communicate through this interface.
|
|
* Can be swapped for external systems (RabbitMQ, WebSocket forwarding) later.
|
|
*/
|
|
// =============================================================================
|
|
// Domain Event Types - Typed payloads for each event
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Process Events
|
|
*/
|
|
|
|
export interface ProcessSpawnedEvent extends DomainEvent {
|
|
type: 'process:spawned';
|
|
payload: {
|
|
processId: string;
|
|
pid: number;
|
|
command: string;
|
|
};
|
|
}
|
|
|
|
export interface ProcessStoppedEvent extends DomainEvent {
|
|
type: 'process:stopped';
|
|
payload: {
|
|
processId: string;
|
|
pid: number;
|
|
exitCode: number | null;
|
|
};
|
|
}
|
|
|
|
export interface ProcessCrashedEvent extends DomainEvent {
|
|
type: 'process:crashed';
|
|
payload: {
|
|
processId: string;
|
|
pid: number;
|
|
exitCode: number | null;
|
|
signal: string | null;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Server Events
|
|
*/
|
|
|
|
export interface ServerStartedEvent extends DomainEvent {
|
|
type: 'server:started';
|
|
payload: {
|
|
port: number;
|
|
host: string;
|
|
pid: number;
|
|
};
|
|
}
|
|
|
|
export interface ServerStoppedEvent extends DomainEvent {
|
|
type: 'server:stopped';
|
|
payload: {
|
|
uptime: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Log Events
|
|
*/
|
|
|
|
export interface LogEntryEvent extends DomainEvent {
|
|
type: 'log:entry';
|
|
payload: {
|
|
processId: string;
|
|
stream: 'stdout' | 'stderr';
|
|
data: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Git Worktree Events
|
|
*/
|
|
|
|
export interface WorktreeCreatedEvent extends DomainEvent {
|
|
type: 'worktree:created';
|
|
payload: {
|
|
worktreeId: string;
|
|
branch: string;
|
|
path: string;
|
|
};
|
|
}
|
|
|
|
export interface WorktreeRemovedEvent extends DomainEvent {
|
|
type: 'worktree:removed';
|
|
payload: {
|
|
worktreeId: string;
|
|
branch: string;
|
|
};
|
|
}
|
|
|
|
export interface WorktreeMergedEvent extends DomainEvent {
|
|
type: 'worktree:merged';
|
|
payload: {
|
|
worktreeId: string;
|
|
sourceBranch: string;
|
|
targetBranch: string;
|
|
};
|
|
}
|
|
|
|
export interface WorktreeConflictEvent extends DomainEvent {
|
|
type: 'worktree:conflict';
|
|
payload: {
|
|
worktreeId: string;
|
|
sourceBranch: string;
|
|
targetBranch: string;
|
|
conflictingFiles: string[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Agent Events
|
|
*/
|
|
|
|
export interface AgentSpawnedEvent extends DomainEvent {
|
|
type: 'agent:spawned';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
taskId: string | null;
|
|
worktreeId: string;
|
|
provider: string;
|
|
};
|
|
}
|
|
|
|
export interface AgentStoppedEvent extends DomainEvent {
|
|
type: 'agent:stopped';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
taskId: string | null;
|
|
reason:
|
|
| 'user_requested'
|
|
| 'task_complete'
|
|
| 'error'
|
|
| 'waiting_for_input'
|
|
| 'context_complete'
|
|
| 'plan_complete'
|
|
| 'detail_complete'
|
|
| 'refine_complete';
|
|
};
|
|
}
|
|
|
|
export interface AgentCrashedEvent extends DomainEvent {
|
|
type: 'agent:crashed';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
taskId: string | null;
|
|
error: string;
|
|
};
|
|
}
|
|
|
|
export interface AgentResumedEvent extends DomainEvent {
|
|
type: 'agent:resumed';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
taskId: string | null;
|
|
sessionId: string;
|
|
};
|
|
}
|
|
|
|
export interface AgentAccountSwitchedEvent extends DomainEvent {
|
|
type: 'agent:account_switched';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
previousAccountId: string;
|
|
newAccountId: string;
|
|
reason: 'account_exhausted';
|
|
};
|
|
}
|
|
|
|
export interface AgentWaitingEvent extends DomainEvent {
|
|
type: 'agent:waiting';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
taskId: string | null;
|
|
sessionId: string;
|
|
questions: Array<{
|
|
id: string;
|
|
question: string;
|
|
options?: Array<{ label: string; description?: string }>;
|
|
multiSelect?: boolean;
|
|
}>;
|
|
};
|
|
}
|
|
|
|
export interface AgentDeletedEvent extends DomainEvent {
|
|
type: 'agent:deleted';
|
|
payload: {
|
|
agentId: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
export interface AgentOutputEvent extends DomainEvent {
|
|
type: 'agent:output';
|
|
payload: {
|
|
agentId: string;
|
|
stream: 'stdout' | 'stderr';
|
|
data: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Task Dispatch Events
|
|
*/
|
|
|
|
export interface TaskQueuedEvent extends DomainEvent {
|
|
type: 'task:queued';
|
|
payload: {
|
|
taskId: string;
|
|
priority: string;
|
|
dependsOn: string[];
|
|
};
|
|
}
|
|
|
|
export interface TaskDispatchedEvent extends DomainEvent {
|
|
type: 'task:dispatched';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
agentName: string;
|
|
};
|
|
}
|
|
|
|
export interface TaskCompletedEvent extends DomainEvent {
|
|
type: 'task:completed';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
success: boolean;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
export interface TaskBlockedEvent extends DomainEvent {
|
|
type: 'task:blocked';
|
|
payload: {
|
|
taskId: string;
|
|
reason: string;
|
|
blockedBy?: string[];
|
|
};
|
|
}
|
|
|
|
export interface TaskPendingApprovalEvent extends DomainEvent {
|
|
type: 'task:pending_approval';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
category: string;
|
|
name: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Phase Events
|
|
*/
|
|
|
|
export interface PhaseQueuedEvent extends DomainEvent {
|
|
type: 'phase:queued';
|
|
payload: {
|
|
phaseId: string;
|
|
initiativeId: string;
|
|
dependsOn: string[];
|
|
};
|
|
}
|
|
|
|
export interface PhaseStartedEvent extends DomainEvent {
|
|
type: 'phase:started';
|
|
payload: {
|
|
phaseId: string;
|
|
initiativeId: string;
|
|
};
|
|
}
|
|
|
|
export interface PhaseCompletedEvent extends DomainEvent {
|
|
type: 'phase:completed';
|
|
payload: {
|
|
phaseId: string;
|
|
initiativeId: string;
|
|
success: boolean;
|
|
message?: string;
|
|
};
|
|
}
|
|
|
|
export interface PhaseBlockedEvent extends DomainEvent {
|
|
type: 'phase:blocked';
|
|
payload: {
|
|
phaseId: string;
|
|
reason: string;
|
|
};
|
|
}
|
|
|
|
export interface PhasePendingReviewEvent extends DomainEvent {
|
|
type: 'phase:pending_review';
|
|
payload: {
|
|
phaseId: string;
|
|
initiativeId: string;
|
|
};
|
|
}
|
|
|
|
export interface PhaseMergedEvent extends DomainEvent {
|
|
type: 'phase:merged';
|
|
payload: {
|
|
phaseId: string;
|
|
initiativeId: string;
|
|
sourceBranch: string;
|
|
targetBranch: string;
|
|
};
|
|
}
|
|
|
|
export interface TaskMergedEvent extends DomainEvent {
|
|
type: 'task:merged';
|
|
payload: {
|
|
taskId: string;
|
|
phaseId: string;
|
|
sourceBranch: string;
|
|
targetBranch: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Merge Coordination Events
|
|
*/
|
|
|
|
export interface MergeQueuedEvent extends DomainEvent {
|
|
type: 'merge:queued';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
worktreeId: string;
|
|
priority: 'low' | 'medium' | 'high';
|
|
};
|
|
}
|
|
|
|
export interface MergeStartedEvent extends DomainEvent {
|
|
type: 'merge:started';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
worktreeId: string;
|
|
targetBranch: string;
|
|
};
|
|
}
|
|
|
|
export interface MergeCompletedEvent extends DomainEvent {
|
|
type: 'merge:completed';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
worktreeId: string;
|
|
targetBranch: string;
|
|
};
|
|
}
|
|
|
|
export interface MergeConflictedEvent extends DomainEvent {
|
|
type: 'merge:conflicted';
|
|
payload: {
|
|
taskId: string;
|
|
agentId: string;
|
|
worktreeId: string;
|
|
targetBranch: string;
|
|
conflictingFiles: string[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Page Events
|
|
*/
|
|
|
|
export interface PageCreatedEvent extends DomainEvent {
|
|
type: 'page:created';
|
|
payload: {
|
|
pageId: string;
|
|
initiativeId: string;
|
|
title: string;
|
|
};
|
|
}
|
|
|
|
export interface PageUpdatedEvent extends DomainEvent {
|
|
type: 'page:updated';
|
|
payload: {
|
|
pageId: string;
|
|
initiativeId: string;
|
|
title?: string;
|
|
};
|
|
}
|
|
|
|
export interface PageDeletedEvent extends DomainEvent {
|
|
type: 'page:deleted';
|
|
payload: {
|
|
pageId: string;
|
|
initiativeId: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Change Set Events
|
|
*/
|
|
|
|
export interface ChangeSetCreatedEvent extends DomainEvent {
|
|
type: 'changeset:created';
|
|
payload: {
|
|
changeSetId: string;
|
|
initiativeId: string;
|
|
agentId: string;
|
|
mode: string;
|
|
entryCount: number;
|
|
};
|
|
}
|
|
|
|
export interface ChangeSetRevertedEvent extends DomainEvent {
|
|
type: 'changeset:reverted';
|
|
payload: {
|
|
changeSetId: string;
|
|
initiativeId: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Preview Events
|
|
*/
|
|
|
|
export interface PreviewBuildingEvent extends DomainEvent {
|
|
type: 'preview:building';
|
|
payload: {
|
|
previewId: string;
|
|
initiativeId: string;
|
|
branch: string;
|
|
port: number;
|
|
};
|
|
}
|
|
|
|
export interface PreviewReadyEvent extends DomainEvent {
|
|
type: 'preview:ready';
|
|
payload: {
|
|
previewId: string;
|
|
initiativeId: string;
|
|
branch: string;
|
|
port: number;
|
|
url: string;
|
|
};
|
|
}
|
|
|
|
export interface PreviewStoppedEvent extends DomainEvent {
|
|
type: 'preview:stopped';
|
|
payload: {
|
|
previewId: string;
|
|
initiativeId: string;
|
|
};
|
|
}
|
|
|
|
export interface PreviewFailedEvent extends DomainEvent {
|
|
type: 'preview:failed';
|
|
payload: {
|
|
previewId: string;
|
|
initiativeId: string;
|
|
error: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Account Credential Events
|
|
*/
|
|
|
|
export interface AccountCredentialsRefreshedEvent extends DomainEvent {
|
|
type: 'account:credentials_refreshed';
|
|
payload: {
|
|
accountId: string | null;
|
|
expiresAt: number;
|
|
previousExpiresAt: number | null;
|
|
};
|
|
}
|
|
|
|
export interface AccountCredentialsExpiredEvent extends DomainEvent {
|
|
type: 'account:credentials_expired';
|
|
payload: {
|
|
accountId: string | null;
|
|
reason: 'token_expired' | 'refresh_failed' | 'credentials_missing';
|
|
error: string | null;
|
|
};
|
|
}
|
|
|
|
export interface AccountCredentialsValidatedEvent extends DomainEvent {
|
|
type: 'account:credentials_validated';
|
|
payload: {
|
|
accountId: string | null;
|
|
valid: boolean;
|
|
expiresAt: number | null;
|
|
wasRefreshed: boolean;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Union of all domain events - enables type-safe event handling
|
|
*/
|
|
export type DomainEventMap =
|
|
| ProcessSpawnedEvent
|
|
| ProcessStoppedEvent
|
|
| ProcessCrashedEvent
|
|
| ServerStartedEvent
|
|
| ServerStoppedEvent
|
|
| LogEntryEvent
|
|
| WorktreeCreatedEvent
|
|
| WorktreeRemovedEvent
|
|
| WorktreeMergedEvent
|
|
| WorktreeConflictEvent
|
|
| AgentSpawnedEvent
|
|
| AgentStoppedEvent
|
|
| AgentCrashedEvent
|
|
| AgentResumedEvent
|
|
| AgentAccountSwitchedEvent
|
|
| AgentDeletedEvent
|
|
| AgentWaitingEvent
|
|
| AgentOutputEvent
|
|
| TaskQueuedEvent
|
|
| TaskDispatchedEvent
|
|
| TaskCompletedEvent
|
|
| TaskBlockedEvent
|
|
| TaskPendingApprovalEvent
|
|
| PhaseQueuedEvent
|
|
| PhaseStartedEvent
|
|
| PhaseCompletedEvent
|
|
| PhaseBlockedEvent
|
|
| PhasePendingReviewEvent
|
|
| PhaseMergedEvent
|
|
| TaskMergedEvent
|
|
| MergeQueuedEvent
|
|
| MergeStartedEvent
|
|
| MergeCompletedEvent
|
|
| MergeConflictedEvent
|
|
| PageCreatedEvent
|
|
| PageUpdatedEvent
|
|
| PageDeletedEvent
|
|
| ChangeSetCreatedEvent
|
|
| ChangeSetRevertedEvent
|
|
| AccountCredentialsRefreshedEvent
|
|
| AccountCredentialsExpiredEvent
|
|
| AccountCredentialsValidatedEvent
|
|
| PreviewBuildingEvent
|
|
| PreviewReadyEvent
|
|
| PreviewStoppedEvent
|
|
| PreviewFailedEvent;
|
|
|
|
/**
|
|
* Event type literal union for type checking
|
|
*/
|
|
export type DomainEventType = DomainEventMap['type'];
|
|
|
|
// =============================================================================
|
|
// Event Bus Port Interface
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Event Bus Port Interface
|
|
*
|
|
* All modules communicate through this interface.
|
|
* Can be swapped for external systems (RabbitMQ, WebSocket forwarding) later.
|
|
*/
|
|
export interface EventBus {
|
|
/**
|
|
* Emit an event to all subscribed handlers
|
|
*/
|
|
emit<T extends DomainEvent>(event: T): void;
|
|
|
|
/**
|
|
* Subscribe to events of a specific type
|
|
*/
|
|
on<T extends DomainEvent>(
|
|
eventType: T['type'],
|
|
handler: (event: T) => void
|
|
): void;
|
|
|
|
/**
|
|
* Unsubscribe from events of a specific type
|
|
*/
|
|
off<T extends DomainEvent>(
|
|
eventType: T['type'],
|
|
handler: (event: T) => void
|
|
): void;
|
|
|
|
/**
|
|
* Subscribe to a single occurrence of an event type
|
|
*/
|
|
once<T extends DomainEvent>(
|
|
eventType: T['type'],
|
|
handler: (event: T) => void
|
|
): void;
|
|
}
|