Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Agent Deployment Service Interface
*
* Higher-level deployment service that uses the DevEnvironmentAgent
* to analyze repos before starting deployments. Wraps the low-level
* IDeploymentService with AI-driven analysis.
*
* Flow:
* 1. Analyze repo via DevEnvironmentAgent (cached per-repo)
* 2. If not deployable → return "not deployable" result
* 3. If deployable → run setup commands → start dev server
*/

import type { DeploymentState } from '@/domain/generated/output.js';
import type { DevEnvironmentAnalysis } from './dev-environment-agent.interface.js';

/** Result of an agent-driven deployment attempt. */
export interface AgentDeployResult {
/** Whether the deployment was started successfully. */
success: boolean;

/** Error message if the deployment failed. */
error?: string;

/** Current deployment state (Booting on success). */
state?: DeploymentState;

/** The analysis result from the dev environment agent. */
analysis?: DevEnvironmentAnalysis;
}

/**
* Port interface for agent-driven deployments.
*
* Uses DevEnvironmentAgent to analyze the repo, then starts the
* deployment using the detected command. Handles repos that have
* nothing to deploy by returning a descriptive result.
*/
export interface IAgentDeploymentService {
/**
* Analyze a repository and start its dev environment if possible.
*
* @param targetId - Unique identifier for the deployment (featureId or repositoryPath)
* @param targetPath - Absolute filesystem path to the directory to deploy from
* @param options - Optional: force re-analysis by skipping cache
* @returns Result with success/error and the analysis
*/
deploy(
targetId: string,
targetPath: string,
options?: { skipCache?: boolean }
): Promise<AgentDeployResult>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface DeploymentStatus {
url: string | null;
}

/** Options for overriding the default dev script detection. */
export interface DeploymentStartOptions {
/** Shell command to run instead of auto-detecting from package.json. */
command?: string;
/** Working directory relative to targetPath (defaults to "."). */
cwd?: string;
}

/**
* Port interface for managing local dev server deployments.
*
Expand All @@ -49,10 +57,11 @@ export interface IDeploymentService {
*
* @param targetId - Unique identifier for the deployment target (featureId or repositoryId)
* @param targetPath - Absolute filesystem path to the directory to run the dev server in
* @param options - Optional: override the auto-detected command
* @returns The initial deployment state (always Booting on success)
* @throws Error if no dev script is found in package.json or the process fails to spawn
*/
start(targetId: string, targetPath: string): void;
start(targetId: string, targetPath: string, options?: DeploymentStartOptions): void;

/**
* Stop a running deployment gracefully.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Dev Environment Agent Interface
*
* Output port for agent-based dev environment analysis.
* Uses an AI agent to analyze any repository (language-agnostic) and determine
* how to start a local development server. Supports per-repo caching for fast
* repeated invocations.
*
* Key behaviors:
* - Analyzes repo structure to detect dev server commands across all languages
* - Returns a "not deployable" result for repos with no server/UI to start
* - Caches analysis results per-repo for fast subsequent calls
*/

/** Result of analyzing a repository for dev environment setup. */
export interface DevEnvironmentAnalysis {
/** Whether this repo has a startable dev environment. */
deployable: boolean;

/** Human-readable explanation of what was detected or why it's not deployable. */
reason: string;

/** The shell command to start the dev server (null if not deployable). */
command: string | null;

/** Working directory relative to repo root (defaults to "."). */
cwd: string;

/** Expected port the dev server will listen on (null if unknown). */
expectedPort: number | null;

/** Detected language/framework (e.g., "node", "python", "go", "rust"). */
language: string | null;

/** Detected framework if any (e.g., "next.js", "django", "flask", "gin"). */
framework: string | null;

/** Setup commands to run before the dev command (e.g., "npm install"). */
setupCommands: string[];
}

/** Options for the analyze method. */
export interface DevEnvironmentAnalyzeOptions {
/** Skip the cache and force a fresh analysis. */
skipCache?: boolean;
}

/**
* Port interface for AI-driven dev environment analysis.
*
* Implementations use a structured agent caller to analyze repositories
* and determine how to start a local dev server. Results are cached
* per-repo (keyed by absolute path) for fast repeated calls.
*/
export interface IDevEnvironmentAgent {
/**
* Analyze a repository to determine how to start its dev environment.
*
* @param repositoryPath - Absolute filesystem path to the repository
* @param options - Optional configuration (e.g., skip cache)
* @returns Analysis result with command, language, and deployability info
*/
analyze(
repositoryPath: string,
options?: DevEnvironmentAnalyzeOptions
): Promise<DevEnvironmentAnalysis>;

/**
* Clear the cached analysis for a specific repository.
*
* @param repositoryPath - Absolute filesystem path to the repository
*/
clearCache(repositoryPath: string): void;

/**
* Clear all cached analyses.
*/
clearAllCaches(): void;
}
23 changes: 23 additions & 0 deletions packages/core/src/infrastructure/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ import type { IDaemonService } from '../../application/ports/output/services/dae
import { DaemonPidService } from '../services/daemon/daemon-pid.service.js';
import type { IDeploymentService } from '../../application/ports/output/services/deployment-service.interface.js';
import { DeploymentService } from '../services/deployment/deployment.service.js';
import type { IDevEnvironmentAgent } from '../../application/ports/output/services/dev-environment-agent.interface.js';
import { DevEnvironmentAgentService } from '../services/deployment/dev-environment-agent.service.js';
import type { IAgentDeploymentService } from '../../application/ports/output/services/agent-deployment-service.interface.js';
import { AgentDeploymentService } from '../services/deployment/agent-deployment.service.js';
import { AttachmentStorageService } from '../services/attachment-storage.service.js';

// Agent infrastructure interfaces and implementations
Expand Down Expand Up @@ -298,6 +302,25 @@ export async function initializeContainer(): Promise<typeof container> {
},
});

// Register agent-based deployment services
container.register<IDevEnvironmentAgent>('IDevEnvironmentAgent', {
useFactory: (c) => {
const caller = c.resolve<IStructuredAgentCaller>('IStructuredAgentCaller');
return new DevEnvironmentAgentService({ structuredAgentCaller: caller });
},
});

container.register<IAgentDeploymentService>('IAgentDeploymentService', {
useFactory: (c) => {
const devEnvAgent = c.resolve<IDevEnvironmentAgent>('IDevEnvironmentAgent');
const deploySvc = c.resolve<IDeploymentService>('IDeploymentService');
return new AgentDeploymentService({
devEnvironmentAgent: devEnvAgent,
deploymentService: deploySvc,
});
},
});

container.register<ISpecInitializerService>('ISpecInitializerService', {
useFactory: () => new SpecInitializerService(),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* Agent Deployment Service
*
* Higher-level deployment orchestrator that uses the DevEnvironmentAgent
* to analyze repos before starting deployments. Replaces the simple
* "detect package.json scripts" approach with AI-driven analysis that
* supports any language/framework.
*
* Flow:
* 1. Analyze repo via DevEnvironmentAgent (cached per-repo for speed)
* 2. If not deployable → return descriptive reason
* 3. If deployable → run setup commands → start dev server via DeploymentService
*/

import { exec } from 'node:child_process';
import { join } from 'node:path';
import type {
IAgentDeploymentService,
AgentDeployResult,
} from '@/application/ports/output/services/agent-deployment-service.interface.js';
import type { IDevEnvironmentAgent } from '@/application/ports/output/services/dev-environment-agent.interface.js';
import type { IDeploymentService } from '@/application/ports/output/services/deployment-service.interface.js';
import { DeploymentState } from '@/domain/generated/output.js';
import { createDeploymentLogger } from './deployment-logger.js';

const log = createDeploymentLogger('[AgentDeploymentService]');

/** Dependencies injectable for testing. */
export interface AgentDeploymentServiceDeps {
devEnvironmentAgent: IDevEnvironmentAgent;
deploymentService: IDeploymentService;
execCommand?: (command: string, cwd: string) => Promise<void>;
}

function defaultExecCommand(command: string, cwd: string): Promise<void> {
return new Promise((resolve, reject) => {
exec(command, { cwd }, (error) => {
if (error) reject(error);
else resolve();
});
});
}

type ResolvedDeps = Required<AgentDeploymentServiceDeps>;

export class AgentDeploymentService implements IAgentDeploymentService {
private readonly deps: ResolvedDeps;

constructor(deps: AgentDeploymentServiceDeps) {
this.deps = {
execCommand: defaultExecCommand,
...deps,
};
}

async deploy(
targetId: string,
targetPath: string,
options?: { skipCache?: boolean }
): Promise<AgentDeployResult> {
log.info(`deploy() called — targetId="${targetId}", targetPath="${targetPath}"`);

// Step 1: Analyze via agent
let analysis;
try {
analysis = await this.deps.devEnvironmentAgent.analyze(targetPath, {
skipCache: options?.skipCache,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Analysis failed';
log.error(`agent analysis failed: ${message}`);
return { success: false, error: message };
}

log.info(`analysis result — deployable=${analysis.deployable}, command=${analysis.command}`);

// Step 2: Handle not-deployable repos
if (!analysis.deployable) {
log.info(`repo is not deployable: ${analysis.reason}`);
return {
success: false,
error: analysis.reason,
analysis,
};
}

// Validate: deployable but no command is an error
if (!analysis.command) {
log.error('analysis marked repo as deployable but provided no command');
return {
success: false,
error: 'Analysis marked repo as deployable but provided no command',
analysis,
};
}

// Step 3: Run setup commands
const resolvedCwd =
analysis.cwd && analysis.cwd !== '.' ? join(targetPath, analysis.cwd) : targetPath;

for (const setupCmd of analysis.setupCommands) {
log.info(`running setup command: "${setupCmd}" in "${resolvedCwd}"`);
try {
await this.deps.execCommand(setupCmd, resolvedCwd);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Setup failed';
log.error(`setup command failed: ${message}`);
return {
success: false,
error: `Setup command failed: ${message}`,
analysis,
};
}
}

// Step 4: Start the dev server
try {
this.deps.deploymentService.start(targetId, targetPath, {
command: analysis.command,
cwd: analysis.cwd,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to start deployment';
log.error(`deployment start failed: ${message}`);
return {
success: false,
error: message,
analysis,
};
}

log.info('deployment started successfully');
return {
success: true,
state: DeploymentState.Booting,
analysis,
};
}
}
Loading
Loading