Skip to content
Draft
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
65 changes: 65 additions & 0 deletions src/cli/commands/create/__tests__/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@
expect(json.success).toBe(false);
expect(json.error.includes('conflicts')).toBeTruthy();
});

it('creates project-only scaffold with --project-name and no --name', async () => {
const projectName = `ProjOnly${Date.now()}`;
const result = await runCLI(['create', '--project-name', projectName, '--no-agent', '--json'], testDir);

expect(result.exitCode, `stderr: ${result.stderr}, stdout: ${result.stdout}`).toBe(0);

const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));

Check warning on line 50 in src/cli/commands/create/__tests__/create.test.ts

View workflow job for this annotation

GitHub Actions / lint

Found non-literal argument to RegExp Constructor
expect(await exists(join(json.projectPath, 'agentcore'))).toBeTruthy();
});
});

describe('with agent', () => {
Expand Down Expand Up @@ -144,6 +156,44 @@
expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']);
expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']);
});

it('uses --project-name for project and --name for agent resource', async () => {
const projectName = `AgentProj${Date.now().toString().slice(-6)}`;
const agentName = `AgentResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
const result = await runCLI(
[
'create',
'--project-name',
projectName,
'--name',
agentName,
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--skip-git',
'--skip-install',
'--json',
],
testDir
);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));

Check warning on line 189 in src/cli/commands/create/__tests__/create.test.ts

View workflow job for this annotation

GitHub Actions / lint

Found non-literal argument to RegExp Constructor
expect(json.agentName).toBe(agentName);
expect(await exists(join(json.projectPath, 'app', agentName))).toBeTruthy();

const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
expect(projectSpec.name).toBe(projectName);
expect(projectSpec.runtimes[0].name).toBe(agentName);
});
});

describe('--defaults', () => {
Expand All @@ -167,6 +217,21 @@
expect(result.stdout.includes('would create') || result.stdout.includes('Dry run')).toBeTruthy();
expect(await exists(join(testDir, name)), 'Should not create directory').toBe(false);
});

it('uses project-name for project paths and name for app paths', async () => {
const projectName = `DryProj${Date.now().toString().slice(-6)}`;
const agentName = `DryAgent${Date.now().toString().slice(-6)}`;
const result = await runCLI(
['create', '--project-name', projectName, '--name', agentName, '--defaults', '--dry-run', '--json'],
testDir
);

expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));

Check warning on line 231 in src/cli/commands/create/__tests__/create.test.ts

View workflow job for this annotation

GitHub Actions / lint

Found non-literal argument to RegExp Constructor
expect(json.wouldCreate).toContain(`${json.projectPath}/app/${agentName}/`);
expect(await exists(join(testDir, projectName)), 'Should not create directory').toBe(false);
});
});

describe('--skip-git', () => {
Expand Down
37 changes: 37 additions & 0 deletions src/cli/commands/create/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('validateCreateOptions', () => {
beforeAll(() => {
testDir = join(tmpdir(), `create-opts-${randomUUID()}`);
mkdirSync(testDir, { recursive: true });
mkdirSync(join(testDir, 'ExistingProject'), { recursive: true });
});

afterAll(() => {
Expand All @@ -59,6 +60,42 @@ describe('validateCreateOptions', () => {
expect(result.error).toContain('already exists');
});

it('validates projectName separately from agent name', () => {
const result = validateCreateOptions(
{
name: `Agent${'A'.repeat(30)}`,
projectName: 'ShortProject',
language: 'Python',
framework: 'Strands',
modelProvider: 'Bedrock',
memory: 'none',
},
testDir
);
expect(result.valid).toBe(true);
});

it('checks folder existence using projectName', () => {
const result = validateCreateOptions(
{
name: 'AgentName',
projectName: 'ExistingProject',
language: 'Python',
framework: 'Strands',
modelProvider: 'Bedrock',
memory: 'none',
},
testDir
);
expect(result.valid).toBe(false);
expect(result.error).toContain('ExistingProject');
});

it('allows project-only create with only projectName', () => {
const result = validateCreateOptions({ projectName: 'OnlyProject', agent: false }, testDir);
expect(result.valid).toBe(true);
});

it('returns valid with --no-agent flag', () => {
const result = validateCreateOptions({ name: 'NoAgentProject', agent: false }, testDir);
expect(result.valid).toBe(true);
Expand Down
26 changes: 21 additions & 5 deletions src/cli/commands/create/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm';

export interface CreateWithAgentOptions {
name: string;
projectName?: string;
cwd: string;
type?: 'create' | 'import';
buildType?: BuildType;
Expand Down Expand Up @@ -139,6 +140,7 @@ export interface CreateWithAgentOptions {
export async function createProjectWithAgent(options: CreateWithAgentOptions): Promise<CreateResult> {
const {
name,
projectName: explicitProjectName,
cwd,
buildType,
language,
Expand All @@ -159,7 +161,8 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
skipPythonSetup,
onProgress,
} = options;
const projectRoot = join(cwd, name);
const projectName = explicitProjectName ?? name;
const projectRoot = join(cwd, projectName);
const configBaseDir = join(projectRoot, CONFIG_DIR);

// Check CLI dependencies first (with language for conditional uv check)
Expand All @@ -172,7 +175,14 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
}

// First create the base project (skip dependency check since we already did it)
const projectResult = await createProject({ name, cwd, skipGit, skipInstall, skipDependencyCheck: true, onProgress });
const projectResult = await createProject({
name: projectName,
cwd,
skipGit,
skipInstall,
skipDependencyCheck: true,
onProgress,
});
if (!projectResult.success) {
// Merge warnings from both checks
const allWarnings = [...depWarnings, ...(projectResult.warnings ?? [])];
Expand Down Expand Up @@ -243,7 +253,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P

if (!isMcp && resolvedModelProvider !== 'Bedrock') {
strategy = await credentialPrimitive.resolveCredentialStrategy(
name,
projectName,
agentName,
resolvedModelProvider,
apiKey,
Expand Down Expand Up @@ -295,9 +305,15 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
}
}

export function getDryRunInfo(options: { name: string; cwd: string; language?: string }): CreateResult {
export function getDryRunInfo(options: {
name: string;
cwd: string;
language?: string;
projectName?: string;
}): CreateResult {
const { name, cwd, language } = options;
const projectRoot = join(cwd, name);
const projectName = options.projectName ?? name;
const projectRoot = join(cwd, projectName);

const wouldCreate = [
`${projectRoot}/`,
Expand Down
18 changes: 14 additions & 4 deletions src/cli/commands/create/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function printCreateSummary(
/** Handle CLI mode with progress output */
async function handleCreateCLI(options: CreateOptions): Promise<void> {
const cwd = options.outputDir ?? getWorkingDirectory();
const projectName = options.projectName ?? options.name!;

const validation = validateCreateOptions(options, cwd);
if (!validation.valid) {
Expand All @@ -89,7 +90,7 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {

// Handle dry-run mode
if (options.dryRun) {
const result = getDryRunInfo({ name: options.name!, cwd, language: options.language });
const result = getDryRunInfo({ name: options.name!, projectName, cwd, language: options.language });
if (options.json) {
console.log(JSON.stringify(result));
} else {
Expand Down Expand Up @@ -121,14 +122,15 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {

const result = skipAgent
? await createProject({
name: options.name!,
name: projectName,
cwd,
skipGit: options.skipGit,
skipInstall: options.skipInstall,
onProgress,
})
: await createProjectWithAgent({
name: options.name!,
projectName,
cwd,
type: options.type as 'create' | 'import' | undefined,
buildType: (options.build as BuildType) ?? 'CodeZip',
Expand Down Expand Up @@ -156,7 +158,7 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
if (options.json) {
console.log(JSON.stringify(result));
} else if (result.success) {
printCreateSummary(options.name!, result.agentName, options.language, options.framework);
printCreateSummary(projectName, result.agentName, options.language, options.framework);
if (options.skipInstall) {
console.log(
"\nDependency installation was skipped. Run 'npm install' in agentcore/cdk/ and 'uv sync' in your agent directory manually."
Expand All @@ -173,7 +175,11 @@ export const registerCreate = (program: Command) => {
program
.command('create')
.description(COMMAND_DESCRIPTIONS.create)
.option('--name <name>', 'Project name (start with letter, alphanumeric only, max 23 chars) [non-interactive]')
.option('--name <name>', 'Resource name [non-interactive]')
.option(
'--project-name <name>',
'Project name (start with letter, alphanumeric only, max 23 chars) [non-interactive]'
)
.option('--no-agent', 'Skip agent creation [non-interactive]')
.option('--defaults', 'Use defaults (Python, Strands, Bedrock, no memory) [non-interactive]')
.option('--build <type>', 'Build type: CodeZip or Container (default: CodeZip) [non-interactive]')
Expand Down Expand Up @@ -225,6 +231,7 @@ export const registerCreate = (program: Command) => {
// Any flag triggers non-interactive CLI mode
const hasAnyFlag = Boolean(
options.name ??
options.projectName ??
(options.agent === false ? true : null) ??
options.defaults ??
options.build ??
Expand All @@ -242,6 +249,9 @@ export const registerCreate = (program: Command) => {
);

if (hasAnyFlag) {
if (options.agent === false) {
options.name = options.name ?? options.projectName;
}
// Default language to Python (only supported option) for CLI mode
options.language = options.language ?? 'Python';
await handleCreateCLI(options as CreateOptions);
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/create/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { VpcOptions } from '../shared/vpc-utils';

export interface CreateOptions extends VpcOptions {
name?: string;
projectName?: string;
agent?: boolean;
defaults?: boolean;
type?: string;
Expand Down
20 changes: 14 additions & 6 deletions src/cli/commands/create/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AgentNameSchema,
BuildTypeSchema,
ModelProviderSchema,
ProjectNameSchema,
Expand Down Expand Up @@ -36,18 +37,20 @@ export function validateFolderNotExists(name: string, cwd: string): true | strin

export function validateCreateOptions(options: CreateOptions, cwd?: string): ValidationResult {
// Name is required for non-interactive mode
if (!options.name) {
if (!options.name && !(options.agent === false && options.projectName)) {
return { valid: false, error: '--name is required' };
}

// Validate name format
const nameResult = ProjectNameSchema.safeParse(options.name);
if (!nameResult.success) {
return { valid: false, error: nameResult.error.issues[0]?.message ?? 'Invalid project name' };
const projectName = options.projectName ?? options.name!;

// Validate project name format
const projectNameResult = ProjectNameSchema.safeParse(projectName);
if (!projectNameResult.success) {
return { valid: false, error: projectNameResult.error.issues[0]?.message ?? 'Invalid project name' };
}

// Check if directory already exists
const folderCheck = validateFolderNotExists(options.name, cwd ?? process.cwd());
const folderCheck = validateFolderNotExists(projectName, cwd ?? process.cwd());
if (folderCheck !== true) {
return { valid: false, error: folderCheck };
}
Expand All @@ -57,6 +60,11 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
return { valid: true };
}

const agentNameResult = AgentNameSchema.safeParse(options.name);
if (!agentNameResult.success) {
return { valid: false, error: agentNameResult.error.issues[0]?.message ?? 'Invalid agent name' };
}

// Import path: validate import-specific options
if (options.type === 'import') {
if (!options.agentId) return { valid: false, error: '--agent-id is required for import' };
Expand Down
Loading