feat: Add agent preview integration with auto-teardown and simplified commands
- Add agentId label to preview containers (cw.agent-id) for tracking - Add startForAgent/stopByAgentId methods to PreviewManager - Auto-teardown: previews torn down on agent:stopped event - Conditional preview prompt injection for execute/refine/discuss agents - Agent-simplified CLI: cw preview start/stop --agent <id> - cw preview setup command with --auto mode for guided config generation - hasPreviewConfig hint on cw project register output - New tRPC procedures: startPreviewForAgent, stopPreviewByAgent
This commit is contained in:
@@ -37,9 +37,10 @@ import type {
|
|||||||
ProcessCrashedEvent,
|
ProcessCrashedEvent,
|
||||||
} from '../events/index.js';
|
} from '../events/index.js';
|
||||||
import { writeInputFiles } from './file-io.js';
|
import { writeInputFiles } from './file-io.js';
|
||||||
import { buildWorkspaceLayout, buildInterAgentCommunication } from './prompts/index.js';
|
import { buildWorkspaceLayout, buildInterAgentCommunication, buildPreviewInstructions } from './prompts/index.js';
|
||||||
import { getProvider } from './providers/registry.js';
|
import { getProvider } from './providers/registry.js';
|
||||||
import { createModuleLogger } from '../logger/index.js';
|
import { createModuleLogger } from '../logger/index.js';
|
||||||
|
import { getProjectCloneDir } from '../git/project-clones.js';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { unlink, readFile, writeFile as writeFileAsync } from 'node:fs/promises';
|
import { unlink, readFile, writeFile as writeFileAsync } from 'node:fs/promises';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
@@ -282,7 +283,15 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
// 3a. Append inter-agent communication instructions with actual agent ID
|
// 3a. Append inter-agent communication instructions with actual agent ID
|
||||||
prompt = prompt + buildInterAgentCommunication(agentId, mode);
|
prompt = prompt + buildInterAgentCommunication(agentId, mode);
|
||||||
|
|
||||||
// 3b. Write input files (after agent creation so we can include agentId/agentName)
|
// 3b. Append preview deployment instructions if applicable
|
||||||
|
if (['execute', 'refine', 'discuss'].includes(mode) && initiativeId) {
|
||||||
|
const shouldInject = await this.shouldInjectPreviewInstructions(initiativeId);
|
||||||
|
if (shouldInject) {
|
||||||
|
prompt = prompt + buildPreviewInstructions(agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3c. Write input files (after agent creation so we can include agentId/agentName)
|
||||||
if (options.inputContext) {
|
if (options.inputContext) {
|
||||||
await writeInputFiles({ agentWorkdir: agentCwd, ...options.inputContext, agentId, agentName: alias });
|
await writeInputFiles({ agentWorkdir: agentCwd, ...options.inputContext, agentId, agentName: alias });
|
||||||
log.debug({ alias }, 'input files written');
|
log.debug({ alias }, 'input files written');
|
||||||
@@ -1038,6 +1047,23 @@ export class MultiProviderAgentManager implements AgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether preview instructions should be injected for this initiative.
|
||||||
|
* Returns true if exactly one project linked and it has .cw-preview.yml.
|
||||||
|
*/
|
||||||
|
private async shouldInjectPreviewInstructions(initiativeId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const projects = await this.projectRepository.findProjectsByInitiativeId(initiativeId);
|
||||||
|
if (projects.length !== 1) return false;
|
||||||
|
|
||||||
|
const project = projects[0];
|
||||||
|
const cloneDir = join(this.workspaceRoot, getProjectCloneDir(project.name, project.id));
|
||||||
|
return existsSync(join(cloneDir, '.cw-preview.yml'));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert database agent record to AgentInfo.
|
* Convert database agent record to AgentInfo.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export { buildRefinePrompt } from './refine.js';
|
|||||||
export { buildChatPrompt } from './chat.js';
|
export { buildChatPrompt } from './chat.js';
|
||||||
export type { ChatHistoryEntry } from './chat.js';
|
export type { ChatHistoryEntry } from './chat.js';
|
||||||
export { buildWorkspaceLayout } from './workspace.js';
|
export { buildWorkspaceLayout } from './workspace.js';
|
||||||
|
export { buildPreviewInstructions } from './preview.js';
|
||||||
|
|||||||
37
apps/server/agent/prompts/preview.ts
Normal file
37
apps/server/agent/prompts/preview.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Preview Deployment Prompt Instructions
|
||||||
|
*
|
||||||
|
* Conditional prompt section injected when the agent's initiative has
|
||||||
|
* a project with `.cw-preview.yml`. Provides prefilled commands using
|
||||||
|
* the agent's own ID so the server can resolve everything else.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function buildPreviewInstructions(agentId: string): string {
|
||||||
|
return `
|
||||||
|
|
||||||
|
<preview_deployments>
|
||||||
|
This project supports preview deployments via Docker. You can spin up a running
|
||||||
|
instance to verify your changes visually or explore the app.
|
||||||
|
|
||||||
|
## Start a Preview
|
||||||
|
cw preview start --agent ${agentId}
|
||||||
|
|
||||||
|
Automatically uses your worktree (dev mode with hot reload). The URL is printed
|
||||||
|
to stdout (e.g. http://abc123.localhost:9100).
|
||||||
|
|
||||||
|
## Check Preview Status
|
||||||
|
cw preview list
|
||||||
|
|
||||||
|
## Stop a Preview
|
||||||
|
cw preview stop --agent ${agentId}
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- After implementing a UI change, spin up a preview to verify rendering.
|
||||||
|
- After API changes, verify the frontend still works end-to-end.
|
||||||
|
- To explore the existing app state via Chrome DevTools or browser.
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
- Backend-only changes with no visual component — run tests instead.
|
||||||
|
- If Docker is not available (the command will fail gracefully).
|
||||||
|
</preview_deployments>`;
|
||||||
|
}
|
||||||
@@ -1038,6 +1038,11 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
|||||||
console.log(`Registered project: ${project.id}`);
|
console.log(`Registered project: ${project.id}`);
|
||||||
console.log(` Name: ${project.name}`);
|
console.log(` Name: ${project.name}`);
|
||||||
console.log(` URL: ${project.url}`);
|
console.log(` URL: ${project.url}`);
|
||||||
|
if (project.hasPreviewConfig) {
|
||||||
|
console.log(' Preview: .cw-preview.yml detected — preview deployments ready');
|
||||||
|
} else {
|
||||||
|
console.log(' Preview: No .cw-preview.yml found. Run `cw preview setup` for instructions.');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to register project:', (error as Error).message);
|
console.error('Failed to register project:', (error as Error).message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -1335,23 +1340,36 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
|||||||
.description('Manage Docker-based preview deployments');
|
.description('Manage Docker-based preview deployments');
|
||||||
|
|
||||||
// cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
|
// cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
|
||||||
|
// cw preview start --agent <id> (agent-simplified: server resolves everything)
|
||||||
previewCommand
|
previewCommand
|
||||||
.command('start')
|
.command('start')
|
||||||
.description('Start a preview deployment')
|
.description('Start a preview deployment')
|
||||||
.requiredOption('--initiative <id>', 'Initiative ID')
|
.option('--initiative <id>', 'Initiative ID')
|
||||||
.requiredOption('--project <id>', 'Project ID')
|
.option('--project <id>', 'Project ID')
|
||||||
.requiredOption('--branch <branch>', 'Branch to deploy')
|
.option('--branch <branch>', 'Branch to deploy')
|
||||||
.option('--phase <id>', 'Phase ID')
|
.option('--phase <id>', 'Phase ID')
|
||||||
.action(async (options: { initiative: string; project: string; branch: string; phase?: string }) => {
|
.option('--agent <id>', 'Agent ID (server resolves initiative/project/branch)')
|
||||||
|
.action(async (options: { initiative?: string; project?: string; branch?: string; phase?: string; agent?: string }) => {
|
||||||
try {
|
try {
|
||||||
const client = createDefaultTrpcClient();
|
const client = createDefaultTrpcClient();
|
||||||
console.log('Starting preview deployment...');
|
console.log('Starting preview deployment...');
|
||||||
const preview = await client.startPreview.mutate({
|
|
||||||
|
let preview;
|
||||||
|
if (options.agent) {
|
||||||
|
preview = await client.startPreviewForAgent.mutate({ agentId: options.agent });
|
||||||
|
} else {
|
||||||
|
if (!options.initiative || !options.project || !options.branch) {
|
||||||
|
console.error('Either --agent or all of --initiative, --project, --branch are required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
preview = await client.startPreview.mutate({
|
||||||
initiativeId: options.initiative,
|
initiativeId: options.initiative,
|
||||||
projectId: options.project,
|
projectId: options.project,
|
||||||
branch: options.branch,
|
branch: options.branch,
|
||||||
phaseId: options.phase,
|
phaseId: options.phase,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Preview started: ${preview.id}`);
|
console.log(`Preview started: ${preview.id}`);
|
||||||
console.log(` URL: ${preview.url}`);
|
console.log(` URL: ${preview.url}`);
|
||||||
console.log(` Branch: ${preview.branch}`);
|
console.log(` Branch: ${preview.branch}`);
|
||||||
@@ -1365,14 +1383,24 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
|||||||
});
|
});
|
||||||
|
|
||||||
// cw preview stop <previewId>
|
// cw preview stop <previewId>
|
||||||
|
// cw preview stop --agent <id>
|
||||||
previewCommand
|
previewCommand
|
||||||
.command('stop <previewId>')
|
.command('stop [previewId]')
|
||||||
.description('Stop a preview deployment')
|
.description('Stop a preview deployment')
|
||||||
.action(async (previewId: string) => {
|
.option('--agent <id>', 'Stop preview by agent ID')
|
||||||
|
.action(async (previewId: string | undefined, options: { agent?: string }) => {
|
||||||
try {
|
try {
|
||||||
const client = createDefaultTrpcClient();
|
const client = createDefaultTrpcClient();
|
||||||
|
if (options.agent) {
|
||||||
|
await client.stopPreviewByAgent.mutate({ agentId: options.agent });
|
||||||
|
console.log(`Previews for agent '${options.agent}' stopped`);
|
||||||
|
} else if (previewId) {
|
||||||
await client.stopPreview.mutate({ previewId });
|
await client.stopPreview.mutate({ previewId });
|
||||||
console.log(`Preview '${previewId}' stopped`);
|
console.log(`Preview '${previewId}' stopped`);
|
||||||
|
} else {
|
||||||
|
console.error('Either <previewId> or --agent <id> is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop preview:', (error as Error).message);
|
console.error('Failed to stop preview:', (error as Error).message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -1434,6 +1462,138 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cw preview setup [--auto --project <id>]
|
||||||
|
previewCommand
|
||||||
|
.command('setup')
|
||||||
|
.description('Show preview deployment setup instructions')
|
||||||
|
.option('--auto', 'Auto-create initiative and spawn agent to set up preview config')
|
||||||
|
.option('--project <id>', 'Project ID (required with --auto)')
|
||||||
|
.option('--provider <name>', 'Agent provider (optional, with --auto)')
|
||||||
|
.action(async (options: { auto?: boolean; project?: string; provider?: string }) => {
|
||||||
|
if (!options.auto) {
|
||||||
|
console.log(`Preview Deployment Setup
|
||||||
|
========================
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
- Docker installed and running
|
||||||
|
- Project registered in Codewalkers (cw project register)
|
||||||
|
|
||||||
|
Step 1: Add .cw-preview.yml to your project root
|
||||||
|
|
||||||
|
Minimal (single service with Dockerfile):
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
Multi-service (frontend + API + database):
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/web/Dockerfile
|
||||||
|
port: 3000
|
||||||
|
route: /
|
||||||
|
healthcheck:
|
||||||
|
path: /
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: packages/api/Dockerfile
|
||||||
|
port: 8080
|
||||||
|
route: /api
|
||||||
|
healthcheck:
|
||||||
|
path: /health
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
internal: true
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: preview
|
||||||
|
|
||||||
|
Step 2: Commit .cw-preview.yml to your repo
|
||||||
|
|
||||||
|
Step 3: Start a preview
|
||||||
|
cw preview start --initiative <id> --project <id> --branch <branch>
|
||||||
|
|
||||||
|
Without .cw-preview.yml, previews auto-discover:
|
||||||
|
1. docker-compose.yml / compose.yml
|
||||||
|
2. Dockerfile (single service, port 3000)
|
||||||
|
|
||||||
|
Full reference: docs/preview.md`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --auto mode
|
||||||
|
if (!options.project) {
|
||||||
|
console.error('--project <id> is required with --auto');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = createDefaultTrpcClient();
|
||||||
|
|
||||||
|
// Look up project
|
||||||
|
const projects = await client.listProjects.query();
|
||||||
|
const project = projects.find((p: { id: string }) => p.id === options.project);
|
||||||
|
if (!project) {
|
||||||
|
console.error(`Project '${options.project}' not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initiative
|
||||||
|
const initiative = await client.createInitiative.mutate({
|
||||||
|
name: `Preview setup: ${project.name}`,
|
||||||
|
projectIds: [project.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create root page with setup guide
|
||||||
|
const rootPage = await client.getRootPage.query({ initiativeId: initiative.id });
|
||||||
|
|
||||||
|
const { markdownToTiptapJson } = await import('../agent/markdown-to-tiptap.js');
|
||||||
|
const setupMarkdown = `# Preview Setup for ${project.name}
|
||||||
|
|
||||||
|
Analyze the project structure and create a \`.cw-preview.yml\` configuration file for Docker-based preview deployments.
|
||||||
|
|
||||||
|
## What to do
|
||||||
|
|
||||||
|
1. Examine the project's build system, frameworks, and service architecture
|
||||||
|
2. Identify all services (frontend, backend, database, etc.)
|
||||||
|
3. Create a \`.cw-preview.yml\` at the project root with appropriate service definitions
|
||||||
|
4. Include dev mode configuration for hot-reload support
|
||||||
|
5. Add healthcheck endpoints where applicable
|
||||||
|
6. Commit the file to the repository
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
See the Codewalkers documentation for .cw-preview.yml format and options.`;
|
||||||
|
|
||||||
|
await client.updatePage.mutate({
|
||||||
|
id: rootPage.id,
|
||||||
|
content: JSON.stringify(markdownToTiptapJson(setupMarkdown)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn refine agent
|
||||||
|
const spawnInput: { initiativeId: string; instruction: string; provider?: string } = {
|
||||||
|
initiativeId: initiative.id,
|
||||||
|
instruction: 'Analyze the project and create a .cw-preview.yml for preview deployments. Follow the setup guide in the initiative pages.',
|
||||||
|
};
|
||||||
|
if (options.provider) {
|
||||||
|
spawnInput.provider = options.provider;
|
||||||
|
}
|
||||||
|
const agent = await client.spawnArchitectRefine.mutate(spawnInput);
|
||||||
|
|
||||||
|
console.log(`Created initiative: ${initiative.id}`);
|
||||||
|
console.log(`Spawned agent: ${agent.id} (${agent.name})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set up preview:', (error as Error).message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Inter-agent conversation commands
|
// Inter-agent conversation commands
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export function generateLabels(opts: {
|
|||||||
gatewayPort: number;
|
gatewayPort: number;
|
||||||
previewId: string;
|
previewId: string;
|
||||||
mode: 'preview' | 'dev';
|
mode: 'preview' | 'dev';
|
||||||
|
agentId?: string;
|
||||||
}): Record<string, string> {
|
}): Record<string, string> {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
[PREVIEW_LABELS.preview]: 'true',
|
[PREVIEW_LABELS.preview]: 'true',
|
||||||
@@ -143,5 +144,9 @@ export function generateLabels(opts: {
|
|||||||
labels[PREVIEW_LABELS.phaseId] = opts.phaseId;
|
labels[PREVIEW_LABELS.phaseId] = opts.phaseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.agentId) {
|
||||||
|
labels[PREVIEW_LABELS.agentId] = opts.agentId;
|
||||||
|
}
|
||||||
|
|
||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ import type {
|
|||||||
PreviewStoppedEvent,
|
PreviewStoppedEvent,
|
||||||
PreviewFailedEvent,
|
PreviewFailedEvent,
|
||||||
PhasePendingReviewEvent,
|
PhasePendingReviewEvent,
|
||||||
|
AgentStoppedEvent,
|
||||||
} from '../events/types.js';
|
} from '../events/types.js';
|
||||||
|
import type { AgentManager } from '../agent/types.js';
|
||||||
|
|
||||||
const log = createModuleLogger('preview');
|
const log = createModuleLogger('preview');
|
||||||
|
|
||||||
@@ -156,6 +158,7 @@ export class PreviewManager {
|
|||||||
gatewayPort,
|
gatewayPort,
|
||||||
previewId: id,
|
previewId: id,
|
||||||
mode,
|
mode,
|
||||||
|
agentId: options.agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const composeYaml = generateComposeFile(config, {
|
const composeYaml = generateComposeFile(config, {
|
||||||
@@ -429,6 +432,60 @@ export class PreviewManager {
|
|||||||
await this.gatewayManager.stopGateway().catch(() => {});
|
await this.gatewayManager.stopGateway().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a preview for an agent, resolving initiative/project/branch automatically.
|
||||||
|
* The agent's worktree determines the source path and branch.
|
||||||
|
*/
|
||||||
|
async startForAgent(agentId: string, agentManager: AgentManager): Promise<PreviewStatus> {
|
||||||
|
const agent = await agentManager.get(agentId);
|
||||||
|
if (!agent) throw new Error(`Agent '${agentId}' not found`);
|
||||||
|
if (!agent.initiativeId) throw new Error('Agent has no initiative');
|
||||||
|
|
||||||
|
const projects = await this.projectRepository.findProjectsByInitiativeId(agent.initiativeId);
|
||||||
|
if (projects.length !== 1) {
|
||||||
|
throw new Error(`Expected 1 project for initiative, found ${projects.length}`);
|
||||||
|
}
|
||||||
|
const project = projects[0];
|
||||||
|
|
||||||
|
const agentWorkdir = join(this.workspaceRoot, 'agent-workdirs', agent.worktreeId);
|
||||||
|
const worktreePath = join(agentWorkdir, project.name);
|
||||||
|
|
||||||
|
const { simpleGit } = await import('simple-git');
|
||||||
|
const git = simpleGit(worktreePath);
|
||||||
|
const { current: branch } = await git.branchLocal();
|
||||||
|
|
||||||
|
return this.start({
|
||||||
|
initiativeId: agent.initiativeId,
|
||||||
|
projectId: project.id,
|
||||||
|
branch: branch || 'main',
|
||||||
|
mode: 'dev',
|
||||||
|
worktreePath,
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all previews associated with a specific agent ID (best-effort).
|
||||||
|
*/
|
||||||
|
async stopByAgentId(agentId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const projects = await listPreviewProjects();
|
||||||
|
for (const project of projects) {
|
||||||
|
if (project.Name === 'cw-preview-gateway') continue;
|
||||||
|
const labels = await getContainerLabels(project.Name);
|
||||||
|
if (labels[PREVIEW_LABELS.agentId] === agentId) {
|
||||||
|
const previewId = labels[PREVIEW_LABELS.previewId] ?? project.Name.replace(COMPOSE_PROJECT_PREFIX, '');
|
||||||
|
log.info({ previewId, agentId }, 'stopping preview for agent');
|
||||||
|
await this.stop(previewId).catch((err) => {
|
||||||
|
log.warn({ previewId, agentId, err }, 'failed to stop preview for agent');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn({ agentId, err }, 'stopByAgentId failed (best-effort)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register event listener for auto-starting previews on phase:pending_review.
|
* Register event listener for auto-starting previews on phase:pending_review.
|
||||||
*/
|
*/
|
||||||
@@ -479,6 +536,11 @@ export class PreviewManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-teardown previews when their owning agent stops
|
||||||
|
this.eventBus.on<AgentStoppedEvent>('agent:stopped', async (event) => {
|
||||||
|
await this.stopByAgentId(event.payload.agentId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const PREVIEW_LABELS = {
|
|||||||
port: `${PREVIEW_LABEL_PREFIX}.port`,
|
port: `${PREVIEW_LABEL_PREFIX}.port`,
|
||||||
previewId: `${PREVIEW_LABEL_PREFIX}.preview-id`,
|
previewId: `${PREVIEW_LABEL_PREFIX}.preview-id`,
|
||||||
mode: `${PREVIEW_LABEL_PREFIX}.mode`,
|
mode: `${PREVIEW_LABEL_PREFIX}.mode`,
|
||||||
|
agentId: `${PREVIEW_LABEL_PREFIX}.agent-id`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +98,7 @@ export interface StartPreviewOptions {
|
|||||||
branch: string;
|
branch: string;
|
||||||
mode?: 'preview' | 'dev';
|
mode?: 'preview' | 'dev';
|
||||||
worktreePath?: string;
|
worktreePath?: string;
|
||||||
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ProcedureBuilder } from '../trpc.js';
|
import type { ProcedureBuilder } from '../trpc.js';
|
||||||
import { requirePreviewManager } from './_helpers.js';
|
import { requirePreviewManager, requireAgentManager } from './_helpers.js';
|
||||||
|
|
||||||
export function previewProcedures(publicProcedure: ProcedureBuilder) {
|
export function previewProcedures(publicProcedure: ProcedureBuilder) {
|
||||||
return {
|
return {
|
||||||
@@ -16,12 +16,29 @@ export function previewProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
branch: z.string().min(1),
|
branch: z.string().min(1),
|
||||||
mode: z.enum(['preview', 'dev']).default('preview'),
|
mode: z.enum(['preview', 'dev']).default('preview'),
|
||||||
worktreePath: z.string().optional(),
|
worktreePath: z.string().optional(),
|
||||||
|
agentId: z.string().min(1).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const previewManager = requirePreviewManager(ctx);
|
const previewManager = requirePreviewManager(ctx);
|
||||||
return previewManager.start(input);
|
return previewManager.start(input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
startPreviewForAgent: publicProcedure
|
||||||
|
.input(z.object({ agentId: z.string().min(1) }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const previewManager = requirePreviewManager(ctx);
|
||||||
|
const agentManager = requireAgentManager(ctx);
|
||||||
|
return previewManager.startForAgent(input.agentId, agentManager);
|
||||||
|
}),
|
||||||
|
|
||||||
|
stopPreviewByAgent: publicProcedure
|
||||||
|
.input(z.object({ agentId: z.string().min(1) }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const previewManager = requirePreviewManager(ctx);
|
||||||
|
await previewManager.stopByAgentId(input.agentId);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
|
||||||
stopPreview: publicProcedure
|
stopPreview: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
previewId: z.string().min(1),
|
previewId: z.string().min(1),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { rm } from 'node:fs/promises';
|
import { rm, access } from 'node:fs/promises';
|
||||||
import type { ProcedureBuilder } from '../trpc.js';
|
import type { ProcedureBuilder } from '../trpc.js';
|
||||||
import { requireProjectRepository, requireProjectSyncManager } from './_helpers.js';
|
import { requireProjectRepository, requireProjectSyncManager } from './_helpers.js';
|
||||||
import { cloneProject } from '../../git/clone.js';
|
import { cloneProject } from '../../git/clone.js';
|
||||||
@@ -69,7 +69,17 @@ export function projectProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return project;
|
// Check for preview config
|
||||||
|
let hasPreviewConfig = false;
|
||||||
|
if (ctx.workspaceRoot) {
|
||||||
|
const clonePath = join(ctx.workspaceRoot, getProjectCloneDir(input.name, project.id));
|
||||||
|
try {
|
||||||
|
await access(join(clonePath, '.cw-preview.yml'));
|
||||||
|
hasPreviewConfig = true;
|
||||||
|
} catch { /* no config */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...project, hasPreviewConfig };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
listProjects: publicProcedure
|
listProjects: publicProcedure
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ All preview containers get `cw.*` labels for metadata retrieval:
|
|||||||
| `cw.port` | Gateway port |
|
| `cw.port` | Gateway port |
|
||||||
| `cw.preview-id` | Nanoid for this deployment |
|
| `cw.preview-id` | Nanoid for this deployment |
|
||||||
| `cw.mode` | `"preview"` or `"dev"` |
|
| `cw.mode` | `"preview"` or `"dev"` |
|
||||||
|
| `cw.agent-id` | Agent ID (optional, set when started via `--agent`) |
|
||||||
|
|
||||||
### Compose Project Naming
|
### Compose Project Naming
|
||||||
|
|
||||||
@@ -321,24 +322,62 @@ See [Setting Up Preview Deployments](#setting-up-preview-deployments-for-a-proje
|
|||||||
3. Branch is derived from `phaseBranchName(initiative.branch, phase.name)`
|
3. Branch is derived from `phaseBranchName(initiative.branch, phase.name)`
|
||||||
4. Errors are caught and logged (best-effort, never blocks the phase transition)
|
4. Errors are caught and logged (best-effort, never blocks the phase transition)
|
||||||
|
|
||||||
|
## Agent Integration
|
||||||
|
|
||||||
|
Agents can spin up and tear down preview deployments using simplified commands that only require their agent ID — the server resolves initiative, project, branch, and mode automatically.
|
||||||
|
|
||||||
|
### Agent-Simplified Commands
|
||||||
|
|
||||||
|
When an agent has a `<preview_deployments>` section in its prompt (injected automatically if the initiative's project has `.cw-preview.yml`), it can use:
|
||||||
|
|
||||||
|
```
|
||||||
|
cw preview start --agent <agentId> # Server resolves everything, starts dev mode
|
||||||
|
cw preview stop --agent <agentId> # Stops all previews for this agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prompt Injection
|
||||||
|
|
||||||
|
Preview instructions are automatically appended to agent prompts when all conditions are met:
|
||||||
|
1. Agent mode is `execute`, `refine`, or `discuss`
|
||||||
|
2. Agent has an `initiativeId`
|
||||||
|
3. Initiative has exactly one linked project
|
||||||
|
4. Project clone directory contains `.cw-preview.yml`
|
||||||
|
|
||||||
|
The injected `<preview_deployments>` block includes prefilled `cw preview start/stop --agent <agentId>` commands.
|
||||||
|
|
||||||
|
### Agent ID Label
|
||||||
|
|
||||||
|
Previews started with `--agent` receive a `cw.agent-id` Docker container label. This enables:
|
||||||
|
- **Auto-teardown**: When an agent stops (`agent:stopped` event), all previews labeled with its ID are automatically torn down (best-effort).
|
||||||
|
- **Agent-scoped stop**: `cw preview stop --agent <id>` finds and stops previews by label.
|
||||||
|
|
||||||
|
### Setup Command
|
||||||
|
|
||||||
|
`cw preview setup` prints inline setup instructions for `.cw-preview.yml`. With `--auto --project <id>`, it creates an initiative and spawns a refine agent to analyze the project and generate the config file.
|
||||||
|
|
||||||
## tRPC Procedures
|
## tRPC Procedures
|
||||||
|
|
||||||
| Procedure | Type | Input |
|
| Procedure | Type | Input |
|
||||||
|-----------|------|-------|
|
|-----------|------|-------|
|
||||||
| `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch, mode?, worktreePath?}` |
|
| `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch, mode?, worktreePath?, agentId?}` |
|
||||||
|
| `startPreviewForAgent` | mutation | `{agentId}` |
|
||||||
| `stopPreview` | mutation | `{previewId}` |
|
| `stopPreview` | mutation | `{previewId}` |
|
||||||
|
| `stopPreviewByAgent` | mutation | `{agentId}` |
|
||||||
| `listPreviews` | query | `{initiativeId?}` |
|
| `listPreviews` | query | `{initiativeId?}` |
|
||||||
| `getPreviewStatus` | query | `{previewId}` |
|
| `getPreviewStatus` | query | `{previewId}` |
|
||||||
|
|
||||||
`mode` defaults to `'preview'`. Set to `'dev'` with a `worktreePath` for dev mode.
|
`mode` defaults to `'preview'`. Set to `'dev'` with a `worktreePath` for dev mode. `startPreviewForAgent` always uses dev mode.
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>] [--mode preview|dev]
|
cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
|
||||||
|
cw preview start --agent <id>
|
||||||
cw preview stop <previewId>
|
cw preview stop <previewId>
|
||||||
|
cw preview stop --agent <id>
|
||||||
cw preview list [--initiative <id>]
|
cw preview list [--initiative <id>]
|
||||||
cw preview status <previewId>
|
cw preview status <previewId>
|
||||||
|
cw preview setup [--auto --project <id> [--provider <name>]]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|||||||
Reference in New Issue
Block a user