Skip to content

Commit 9b46fbb

Browse files
authored
feat: add project-name option to create (#969)
* Add project-name option to create * fix: address review feedback — restore name description and move backfill logic
1 parent 17b5727 commit 9b46fbb

6 files changed

Lines changed: 151 additions & 16 deletions

File tree

src/cli/commands/create/__tests__/create.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ describe('create command', () => {
3838
expect(json.success).toBe(false);
3939
expect(json.error.includes('conflicts')).toBeTruthy();
4040
});
41+
42+
it('creates project-only scaffold with --project-name and no --name', async () => {
43+
const projectName = `ProjOnly${Date.now()}`;
44+
const result = await runCLI(['create', '--project-name', projectName, '--no-agent', '--json'], testDir);
45+
46+
expect(result.exitCode, `stderr: ${result.stderr}, stdout: ${result.stdout}`).toBe(0);
47+
48+
const json = JSON.parse(result.stdout);
49+
expect(json.success).toBe(true);
50+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
51+
expect(await exists(join(json.projectPath, 'agentcore'))).toBeTruthy();
52+
});
4153
});
4254

4355
describe('with agent', () => {
@@ -144,6 +156,44 @@ describe('create command', () => {
144156
expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']);
145157
expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']);
146158
});
159+
160+
it('uses --project-name for project and --name for agent resource', async () => {
161+
const projectName = `AgentProj${Date.now().toString().slice(-6)}`;
162+
const agentName = `AgentResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
163+
const result = await runCLI(
164+
[
165+
'create',
166+
'--project-name',
167+
projectName,
168+
'--name',
169+
agentName,
170+
'--language',
171+
'Python',
172+
'--framework',
173+
'Strands',
174+
'--model-provider',
175+
'Bedrock',
176+
'--memory',
177+
'none',
178+
'--skip-git',
179+
'--skip-install',
180+
'--json',
181+
],
182+
testDir
183+
);
184+
185+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
186+
187+
const json = JSON.parse(result.stdout);
188+
expect(json.success).toBe(true);
189+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
190+
expect(json.agentName).toBe(agentName);
191+
expect(await exists(join(json.projectPath, 'app', agentName))).toBeTruthy();
192+
193+
const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
194+
expect(projectSpec.name).toBe(projectName);
195+
expect(projectSpec.runtimes[0].name).toBe(agentName);
196+
});
147197
});
148198

149199
describe('--defaults', () => {
@@ -167,6 +217,21 @@ describe('create command', () => {
167217
expect(result.stdout.includes('would create') || result.stdout.includes('Dry run')).toBeTruthy();
168218
expect(await exists(join(testDir, name)), 'Should not create directory').toBe(false);
169219
});
220+
221+
it('uses project-name for project paths and name for app paths', async () => {
222+
const projectName = `DryProj${Date.now().toString().slice(-6)}`;
223+
const agentName = `DryAgent${Date.now().toString().slice(-6)}`;
224+
const result = await runCLI(
225+
['create', '--project-name', projectName, '--name', agentName, '--defaults', '--dry-run', '--json'],
226+
testDir
227+
);
228+
229+
expect(result.exitCode).toBe(0);
230+
const json = JSON.parse(result.stdout);
231+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
232+
expect(json.wouldCreate).toContain(`${json.projectPath}/app/${agentName}/`);
233+
expect(await exists(join(testDir, projectName)), 'Should not create directory').toBe(false);
234+
});
170235
});
171236

172237
describe('--skip-git', () => {

src/cli/commands/create/__tests__/validate.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('validateCreateOptions', () => {
3535
beforeAll(() => {
3636
testDir = join(tmpdir(), `create-opts-${randomUUID()}`);
3737
mkdirSync(testDir, { recursive: true });
38+
mkdirSync(join(testDir, 'ExistingProject'), { recursive: true });
3839
});
3940

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

63+
it('validates projectName separately from agent name', () => {
64+
const result = validateCreateOptions(
65+
{
66+
name: `Agent${'A'.repeat(30)}`,
67+
projectName: 'ShortProject',
68+
language: 'Python',
69+
framework: 'Strands',
70+
modelProvider: 'Bedrock',
71+
memory: 'none',
72+
},
73+
testDir
74+
);
75+
expect(result.valid).toBe(true);
76+
});
77+
78+
it('checks folder existence using projectName', () => {
79+
const result = validateCreateOptions(
80+
{
81+
name: 'AgentName',
82+
projectName: 'ExistingProject',
83+
language: 'Python',
84+
framework: 'Strands',
85+
modelProvider: 'Bedrock',
86+
memory: 'none',
87+
},
88+
testDir
89+
);
90+
expect(result.valid).toBe(false);
91+
expect(result.error).toContain('ExistingProject');
92+
});
93+
94+
it('allows project-only create with only projectName', () => {
95+
const result = validateCreateOptions({ projectName: 'OnlyProject', agent: false }, testDir);
96+
expect(result.valid).toBe(true);
97+
});
98+
6299
it('returns valid with --no-agent flag', () => {
63100
const result = validateCreateOptions({ name: 'NoAgentProject', agent: false }, testDir);
64101
expect(result.valid).toBe(true);

src/cli/commands/create/action.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm';
111111

112112
export interface CreateWithAgentOptions {
113113
name: string;
114+
projectName?: string;
114115
cwd: string;
115116
type?: 'create' | 'import';
116117
buildType?: BuildType;
@@ -139,6 +140,7 @@ export interface CreateWithAgentOptions {
139140
export async function createProjectWithAgent(options: CreateWithAgentOptions): Promise<CreateResult> {
140141
const {
141142
name,
143+
projectName: explicitProjectName,
142144
cwd,
143145
buildType,
144146
language,
@@ -159,7 +161,8 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
159161
skipPythonSetup,
160162
onProgress,
161163
} = options;
162-
const projectRoot = join(cwd, name);
164+
const projectName = explicitProjectName ?? name;
165+
const projectRoot = join(cwd, projectName);
163166
const configBaseDir = join(projectRoot, CONFIG_DIR);
164167

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

174177
// First create the base project (skip dependency check since we already did it)
175-
const projectResult = await createProject({ name, cwd, skipGit, skipInstall, skipDependencyCheck: true, onProgress });
178+
const projectResult = await createProject({
179+
name: projectName,
180+
cwd,
181+
skipGit,
182+
skipInstall,
183+
skipDependencyCheck: true,
184+
onProgress,
185+
});
176186
if (!projectResult.success) {
177187
// Merge warnings from both checks
178188
const allWarnings = [...depWarnings, ...(projectResult.warnings ?? [])];
@@ -243,7 +253,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
243253

244254
if (!isMcp && resolvedModelProvider !== 'Bedrock') {
245255
strategy = await credentialPrimitive.resolveCredentialStrategy(
246-
name,
256+
projectName,
247257
agentName,
248258
resolvedModelProvider,
249259
apiKey,
@@ -295,9 +305,15 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
295305
}
296306
}
297307

298-
export function getDryRunInfo(options: { name: string; cwd: string; language?: string }): CreateResult {
308+
export function getDryRunInfo(options: {
309+
name: string;
310+
cwd: string;
311+
language?: string;
312+
projectName?: string;
313+
}): CreateResult {
299314
const { name, cwd, language } = options;
300-
const projectRoot = join(cwd, name);
315+
const projectName = options.projectName ?? name;
316+
const projectRoot = join(cwd, projectName);
301317

302318
const wouldCreate = [
303319
`${projectRoot}/`,

src/cli/commands/create/command.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ function printCreateSummary(
7676
/** Handle CLI mode with progress output */
7777
async function handleCreateCLI(options: CreateOptions): Promise<void> {
7878
const cwd = options.outputDir ?? getWorkingDirectory();
79+
const name = options.name ?? options.projectName;
80+
const projectName = options.projectName ?? name;
7981

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

9092
// Handle dry-run mode
9193
if (options.dryRun) {
92-
const result = getDryRunInfo({ name: options.name!, cwd, language: options.language });
94+
const result = getDryRunInfo({ name: name!, projectName, cwd, language: options.language });
9395
if (options.json) {
9496
console.log(JSON.stringify(result));
9597
} else {
@@ -121,14 +123,15 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
121123

122124
const result = skipAgent
123125
? await createProject({
124-
name: options.name!,
126+
name: projectName!,
125127
cwd,
126128
skipGit: options.skipGit,
127129
skipInstall: options.skipInstall,
128130
onProgress,
129131
})
130132
: await createProjectWithAgent({
131-
name: options.name!,
133+
name: name!,
134+
projectName,
132135
cwd,
133136
type: options.type as 'create' | 'import' | undefined,
134137
buildType: (options.build as BuildType) ?? 'CodeZip',
@@ -156,7 +159,7 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
156159
if (options.json) {
157160
console.log(JSON.stringify(result));
158161
} else if (result.success) {
159-
printCreateSummary(options.name!, result.agentName, options.language, options.framework);
162+
printCreateSummary(projectName!, result.agentName, options.language, options.framework);
160163
if (options.skipInstall) {
161164
console.log(
162165
"\nDependency installation was skipped. Run 'npm install' in agentcore/cdk/ and 'uv sync' in your agent directory manually."
@@ -173,7 +176,11 @@ export const registerCreate = (program: Command) => {
173176
program
174177
.command('create')
175178
.description(COMMAND_DESCRIPTIONS.create)
176-
.option('--name <name>', 'Project name (start with letter, alphanumeric only, max 23 chars) [non-interactive]')
179+
.option('--name <name>', 'Resource name (agent or harness) [non-interactive]')
180+
.option(
181+
'--project-name <name>',
182+
'Project name (start with letter, alphanumeric only, max 23 chars) [non-interactive]'
183+
)
177184
.option('--no-agent', 'Skip agent creation [non-interactive]')
178185
.option('--defaults', 'Use defaults (Python, Strands, Bedrock, no memory) [non-interactive]')
179186
.option('--build <type>', 'Build type: CodeZip or Container (default: CodeZip) [non-interactive]')
@@ -225,6 +232,7 @@ export const registerCreate = (program: Command) => {
225232
// Any flag triggers non-interactive CLI mode
226233
const hasAnyFlag = Boolean(
227234
options.name ??
235+
options.projectName ??
228236
(options.agent === false ? true : null) ??
229237
options.defaults ??
230238
options.build ??

src/cli/commands/create/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { VpcOptions } from '../shared/vpc-utils';
22

33
export interface CreateOptions extends VpcOptions {
44
name?: string;
5+
projectName?: string;
56
agent?: boolean;
67
defaults?: boolean;
78
type?: string;

src/cli/commands/create/validate.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AgentNameSchema,
23
BuildTypeSchema,
34
ModelProviderSchema,
45
ProjectNameSchema,
@@ -36,18 +37,20 @@ export function validateFolderNotExists(name: string, cwd: string): true | strin
3637

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

43-
// Validate name format
44-
const nameResult = ProjectNameSchema.safeParse(options.name);
45-
if (!nameResult.success) {
46-
return { valid: false, error: nameResult.error.issues[0]?.message ?? 'Invalid project name' };
44+
const projectName = options.projectName ?? options.name!;
45+
46+
// Validate project name format
47+
const projectNameResult = ProjectNameSchema.safeParse(projectName);
48+
if (!projectNameResult.success) {
49+
return { valid: false, error: projectNameResult.error.issues[0]?.message ?? 'Invalid project name' };
4750
}
4851

4952
// Check if directory already exists
50-
const folderCheck = validateFolderNotExists(options.name, cwd ?? process.cwd());
53+
const folderCheck = validateFolderNotExists(projectName, cwd ?? process.cwd());
5154
if (folderCheck !== true) {
5255
return { valid: false, error: folderCheck };
5356
}
@@ -57,6 +60,11 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
5760
return { valid: true };
5861
}
5962

63+
const agentNameResult = AgentNameSchema.safeParse(options.name);
64+
if (!agentNameResult.success) {
65+
return { valid: false, error: agentNameResult.error.issues[0]?.message ?? 'Invalid agent name' };
66+
}
67+
6068
// Import path: validate import-specific options
6169
if (options.type === 'import') {
6270
if (!options.agentId) return { valid: false, error: '--agent-id is required for import' };

0 commit comments

Comments
 (0)