Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/cli/operations/agent/generate/schema-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function mapGenerateConfigToAgent(config: GenerateConfig): AgentEnvSpec {
return {
name: config.projectName,
build: config.buildType ?? 'CodeZip',
...(config.dockerfile && { dockerfile: config.dockerfile }),
entrypoint: DEFAULT_PYTHON_ENTRYPOINT as FilePath,
codeLocation: codeLocation as DirectoryPath,
runtimeVersion: DEFAULT_PYTHON_VERSION,
Expand Down Expand Up @@ -276,5 +277,6 @@ export async function mapGenerateConfigToRenderConfig(
gatewayProviders,
gatewayAuthTypes: [...new Set(gatewayProviders.map(g => g.authType))],
protocol: config.protocol,
dockerfile: config.dockerfile,
};
}
28 changes: 28 additions & 0 deletions src/cli/operations/deploy/__tests__/preflight-container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ vi.mock('node:fs', () => ({

vi.mock('../../../../lib', () => ({
DOCKERFILE_NAME: 'Dockerfile',
getDockerfilePath: (codeLocation: string, dockerfile?: string) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p = require('node:path') as typeof import('node:path');
return p.join(codeLocation, dockerfile ?? 'Dockerfile');
},
resolveCodeLocation: vi.fn((codeLocation: string, configBaseDir: string) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const p = require('node:path') as typeof import('node:path');
Expand Down Expand Up @@ -96,4 +101,27 @@ describe('validateContainerAgents', () => {

expect(() => validateContainerAgents(spec, CONFIG_ROOT)).toThrow(/agent-a.*agent-b/s);
});

it('checks for custom dockerfile name when specified', () => {
mockedExistsSync.mockReturnValue(true);

const spec = makeSpec([
{ name: 'gpu-agent', build: 'Container', codeLocation: dir('agents/gpu'), dockerfile: 'Dockerfile.gpu' },
]);

expect(() => validateContainerAgents(spec, CONFIG_ROOT)).not.toThrow();
// Should check for Dockerfile.gpu, not the default Dockerfile
const calledPath = mockedExistsSync.mock.calls[0]?.[0] as string;
expect(calledPath).toContain('Dockerfile.gpu');
});

it('throws with custom dockerfile name in error message when missing', () => {
mockedExistsSync.mockReturnValue(false);

const spec = makeSpec([
{ name: 'gpu-agent', build: 'Container', codeLocation: dir('agents/gpu'), dockerfile: 'Dockerfile.gpu' },
]);

expect(() => validateContainerAgents(spec, CONFIG_ROOT)).toThrow(/Dockerfile\.gpu not found/);
});
});
6 changes: 3 additions & 3 deletions src/cli/operations/deploy/preflight.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigIO, DOCKERFILE_NAME, requireConfigRoot, resolveCodeLocation } from '../../../lib';
import { ConfigIO, DOCKERFILE_NAME, getDockerfilePath, requireConfigRoot, resolveCodeLocation } from '../../../lib';
import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '../../../schema';
import { validateAwsCredentials } from '../../aws/account';
import { LocalCdkProject } from '../../cdk/local-cdk-project';
Expand Down Expand Up @@ -147,11 +147,11 @@ export function validateContainerAgents(projectSpec: AgentCoreProjectSpec, confi
for (const agent of projectSpec.runtimes || []) {
if (agent.build === 'Container') {
const codeLocation = resolveCodeLocation(agent.codeLocation, configRoot);
const dockerfilePath = path.join(codeLocation, DOCKERFILE_NAME);
const dockerfilePath = getDockerfilePath(codeLocation, agent.dockerfile);

if (!existsSync(dockerfilePath)) {
errors.push(
`Agent "${agent.name}": Dockerfile not found at ${dockerfilePath}. Container agents require a Dockerfile.`
`Agent "${agent.name}": ${agent.dockerfile ?? DOCKERFILE_NAME} not found at ${dockerfilePath}. Container agents require a Dockerfile.`
);
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/cli/operations/dev/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,35 @@ describe('getDevConfig', () => {
expect(config).not.toBeNull();
expect(config!.isPython).toBe(true);
});

it('threads dockerfile from Container agent spec to DevConfig', () => {
const project: AgentCoreProjectSpec = {
name: 'TestProject',
version: 1,
managedBy: 'CDK' as const,
runtimes: [
{
name: 'ContainerAgent',
build: 'Container',
runtimeVersion: 'PYTHON_3_12',
entrypoint: filePath('main.py'),
codeLocation: dirPath('./agents/container'),
protocol: 'HTTP',
dockerfile: 'Dockerfile.gpu',
},
],
memories: [],
credentials: [],
evaluators: [],
onlineEvalConfigs: [],
agentCoreGateways: [],
policyEngines: [],
};

const config = getDevConfig(workingDir, project, '/test/project/agentcore');
expect(config).not.toBeNull();
expect(config?.dockerfile).toBe('Dockerfile.gpu');
});
});

describe('getAgentPort', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/cli/operations/dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface DevConfig {
isPython: boolean;
buildType: BuildType;
protocol: ProtocolMode;
dockerfile?: string;
}

interface DevSupportResult {
Expand Down Expand Up @@ -140,6 +141,7 @@ export function getDevConfig(
isPython: isPythonAgent(targetAgent),
buildType: targetAgent.build,
protocol: targetAgent.protocol ?? 'HTTP',
dockerfile: targetAgent.dockerfile,
};
}

Expand Down
7 changes: 4 additions & 3 deletions src/cli/operations/dev/container-dev-server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CONTAINER_INTERNAL_PORT, DOCKERFILE_NAME } from '../../../lib';
import { CONTAINER_INTERNAL_PORT, DOCKERFILE_NAME, getDockerfilePath } from '../../../lib';
import { getUvBuildArgs } from '../../../lib/packaging/build-args';
import { detectContainerRuntime, getStartHint } from '../../external-requirements/detect';
import { DevServer, type LogLevel, type SpawnConfig } from './dev-server';
Expand Down Expand Up @@ -73,9 +73,10 @@ export class ContainerDevServer extends DevServer {
this.runtimeBinary = runtime.binary;

// 2. Verify Dockerfile exists
const dockerfilePath = join(this.config.directory, DOCKERFILE_NAME);
const dockerfileName = this.config.dockerfile ?? DOCKERFILE_NAME;
const dockerfilePath = getDockerfilePath(this.config.directory, this.config.dockerfile);
if (!existsSync(dockerfilePath)) {
onLog('error', `Dockerfile not found at ${dockerfilePath}. Container agents require a Dockerfile.`);
onLog('error', `${dockerfileName} not found at ${dockerfilePath}. Container agents require a Dockerfile.`);
return false;
}

Expand Down
3 changes: 2 additions & 1 deletion src/cli/templates/BaseRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export abstract class BaseRenderer {
const containerTemplateDir = path.join(this.baseTemplateDir, 'container', language);

if (existsSync(containerTemplateDir)) {
await copyAndRenderDir(containerTemplateDir, projectDir, { ...templateData, entrypoint: 'main' });
const exclude = this.config.dockerfile ? new Set(['Dockerfile']) : undefined;
await copyAndRenderDir(containerTemplateDir, projectDir, { ...templateData, entrypoint: 'main' }, { exclude });
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/cli/templates/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,19 @@ export async function copyDir(src: string, dest: string): Promise<void> {
/**
* Recursively copies a directory, rendering Handlebars templates.
*/
export async function copyAndRenderDir<T extends object>(src: string, dest: string, data: T): Promise<void> {
export async function copyAndRenderDir<T extends object>(
src: string,
dest: string,
data: T,
options?: { exclude?: Set<string> }
): Promise<void> {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destName = resolveTemplateName(entry.name);
if (options?.exclude?.has(destName)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, destName);

if (entry.isDirectory()) {
Expand Down
2 changes: 2 additions & 0 deletions src/cli/templates/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@ export interface AgentRenderConfig {
gatewayAuthTypes: string[];
/** Protocol (HTTP, MCP, A2A). Defaults to HTTP. */
protocol?: ProtocolMode;
/** Custom Dockerfile name — when set, the template Dockerfile is not scaffolded */
dockerfile?: string;
}
13 changes: 12 additions & 1 deletion src/cli/tui/components/PathInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ interface PathInputProps {
allowCreate?: boolean;
/** Show hidden files (dotfiles) in completions (default: false) */
showHidden?: boolean;
/** Allow empty input (user presses Enter without selecting a file) */
allowEmpty?: boolean;
/** Message shown when user submits empty input (only if allowEmpty is true) */
emptyHelpText?: string;
}

interface CompletionItem {
Expand Down Expand Up @@ -133,6 +137,8 @@ export function PathInput({
maxVisibleItems = 8,
allowCreate = false,
showHidden = false,
allowEmpty = false,
emptyHelpText,
}: PathInputProps) {
const [value, setValue] = useState(initialValue);
const [cursor, setCursor] = useState(initialValue.length);
Expand Down Expand Up @@ -207,6 +213,10 @@ export function PathInput({
if (key.return) {
const trimmed = value.trim();
if (!trimmed) {
if (allowEmpty) {
onSubmit('');
return;
}
setError('Please enter a path');
return;
}
Expand Down Expand Up @@ -322,8 +332,9 @@ export function PathInput({
)}

{/* Help text */}
<Box marginTop={1}>
<Box marginTop={1} flexDirection="column">
<Text dimColor>↑↓ move → open ← back Enter submit Esc cancel</Text>
{allowEmpty && emptyHelpText && !value && <Text dimColor>{emptyHelpText}</Text>}
</Box>
</Box>
);
Expand Down
Loading
Loading