Skip to content

Commit a19fc8f

Browse files
authored
feat: add project-name option to preview create (#970)
* Add project-name option to create * fix: address review feedback — restore name description and move backfill logic
1 parent 57ee733 commit a19fc8f

10 files changed

Lines changed: 275 additions & 28 deletions

File tree

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

Lines changed: 118 additions & 1 deletion
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', () => {
@@ -145,6 +157,82 @@ describe('create command', () => {
145157
expect(episodic?.namespaces).toEqual(['/episodes/{actorId}/{sessionId}']);
146158
expect(episodic?.reflectionNamespaces).toEqual(['/episodes/{actorId}']);
147159
});
160+
161+
it('uses --project-name for project and --name for agent resource', async () => {
162+
const projectName = `AgentProj${Date.now().toString().slice(-6)}`;
163+
const agentName = `AgentResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
164+
const result = await runCLI(
165+
[
166+
'create',
167+
'--project-name',
168+
projectName,
169+
'--name',
170+
agentName,
171+
'--language',
172+
'Python',
173+
'--framework',
174+
'Strands',
175+
'--model-provider',
176+
'Bedrock',
177+
'--memory',
178+
'none',
179+
'--skip-git',
180+
'--skip-install',
181+
'--json',
182+
],
183+
testDir
184+
);
185+
186+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
187+
188+
const json = JSON.parse(result.stdout);
189+
expect(json.success).toBe(true);
190+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
191+
expect(json.agentName).toBe(agentName);
192+
expect(await exists(join(json.projectPath, 'app', agentName))).toBeTruthy();
193+
194+
const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
195+
expect(projectSpec.name).toBe(projectName);
196+
expect(projectSpec.runtimes[0].name).toBe(agentName);
197+
});
198+
});
199+
200+
describe('with harness', () => {
201+
it('uses --project-name for project and --name for harness resource', async () => {
202+
const projectName = `HarnessProj${Date.now().toString().slice(-6)}`;
203+
const harnessName = `HarnessResource${randomUUID().replace(/-/g, '').slice(0, 16)}`;
204+
const result = await runCLI(
205+
['create', '--project-name', projectName, '--name', harnessName, '--skip-git', '--skip-install', '--json'],
206+
testDir
207+
);
208+
209+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
210+
211+
const json = JSON.parse(result.stdout);
212+
expect(json.success).toBe(true);
213+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
214+
expect(await exists(join(json.projectPath, 'app', harnessName, 'harness.json'))).toBeTruthy();
215+
216+
const projectSpec = JSON.parse(await readFile(join(json.projectPath, 'agentcore/agentcore.json'), 'utf-8'));
217+
expect(projectSpec.name).toBe(projectName);
218+
expect(projectSpec.harnesses[0].name).toBe(harnessName);
219+
expect(projectSpec.harnesses[0].path).toBe(`app/${harnessName}`);
220+
});
221+
222+
it('rejects long harness name without --project-name but accepts it with --project-name', async () => {
223+
const harnessName = `Harness${'A'.repeat(30)}`;
224+
const rejected = await runCLI(['create', '--name', harnessName, '--skip-install', '--json'], testDir);
225+
expect(rejected.exitCode).toBe(1);
226+
expect(JSON.parse(rejected.stdout).success).toBe(false);
227+
228+
const projectName = `ShortProj${Date.now().toString().slice(-6)}`;
229+
const accepted = await runCLI(
230+
['create', '--project-name', projectName, '--name', harnessName, '--skip-git', '--skip-install', '--json'],
231+
testDir
232+
);
233+
expect(accepted.exitCode, `stdout: ${accepted.stdout}, stderr: ${accepted.stderr}`).toBe(0);
234+
expect(JSON.parse(accepted.stdout).success).toBe(true);
235+
});
148236
});
149237

150238
describe('--defaults', () => {
@@ -163,12 +251,41 @@ describe('create command', () => {
163251
it('shows files without creating', async () => {
164252
const name = `DryRun${Date.now()}`;
165253
// --framework triggers agent path where --dry-run is supported
166-
const result = await runCLI(['create', '--name', name, '--defaults', '--framework', 'Strands', '--dry-run'], testDir);
254+
const result = await runCLI(
255+
['create', '--name', name, '--defaults', '--framework', 'Strands', '--dry-run'],
256+
testDir
257+
);
167258

168259
expect(result.exitCode).toBe(0);
169260
expect(result.stdout.includes('would create') || result.stdout.includes('Dry run')).toBeTruthy();
170261
expect(await exists(join(testDir, name)), 'Should not create directory').toBe(false);
171262
});
263+
264+
it('uses project-name for project paths and name for app paths', async () => {
265+
const projectName = `DryProj${Date.now().toString().slice(-6)}`;
266+
const agentName = `DryAgent${Date.now().toString().slice(-6)}`;
267+
const result = await runCLI(
268+
[
269+
'create',
270+
'--project-name',
271+
projectName,
272+
'--name',
273+
agentName,
274+
'--defaults',
275+
'--framework',
276+
'Strands',
277+
'--dry-run',
278+
'--json',
279+
],
280+
testDir
281+
);
282+
283+
expect(result.exitCode).toBe(0);
284+
const json = JSON.parse(result.stdout);
285+
expect(json.projectPath).toMatch(new RegExp(`/${projectName}$`));
286+
expect(json.wouldCreate).toContain(`${json.projectPath}/app/${agentName}/`);
287+
expect(await exists(join(testDir, projectName)), 'Should not create directory').toBe(false);
288+
});
172289
});
173290

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

src/cli/commands/create/__tests__/harness-action.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ describe('createProjectWithHarness', () => {
4242
await expect(exists(join(harnessDir, 'system-prompt.md'))).resolves.toBe(true);
4343
});
4444

45+
it('uses projectName for project scaffold and name for harness resource', async () => {
46+
const projectName = `Proj${randomUUID().slice(0, 6)}`;
47+
const name = `HarnessName${randomUUID().replace(/-/g, '').slice(0, 12)}`;
48+
const result = await createProjectWithHarness({
49+
name,
50+
projectName,
51+
cwd: testDir,
52+
modelProvider: 'bedrock',
53+
modelId: 'global.anthropic.claude-sonnet-4-6',
54+
skipGit: true,
55+
skipInstall: true,
56+
});
57+
58+
expect(result.success, `Error: ${result.error}`).toBe(true);
59+
expect(result.projectPath).toBe(join(testDir, projectName));
60+
61+
await expect(exists(join(result.projectPath!, 'agentcore'))).resolves.toBe(true);
62+
await expect(exists(join(result.projectPath!, 'app', name, 'harness.json'))).resolves.toBe(true);
63+
});
64+
4565
it('creates harness with custom options', async () => {
4666
const name = `CustomH${randomUUID().slice(0, 6)}`;
4767
const result = await createProjectWithHarness({

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('validateCreateHarnessOptions', () => {
1212
testDir = join(tmpdir(), `harness-create-validate-${randomUUID()}`);
1313
mkdirSync(testDir, { recursive: true });
1414
mkdirSync(join(testDir, 'existingHarness'), { recursive: true });
15+
mkdirSync(join(testDir, 'existingProject'), { recursive: true });
1516
});
1617

1718
afterAll(() => {
@@ -140,13 +141,39 @@ describe('validateCreateHarnessOptions', () => {
140141
expect(options.modelId).toBe('global.anthropic.claude-sonnet-4-6');
141142
});
142143

143-
it('accepts valid harness name with underscores', () => {
144-
const result = validateCreateHarnessOptions({ name: 'my_valid_harness_123' }, testDir);
144+
it('accepts valid harness name with underscores when project-name is valid', () => {
145+
const result = validateCreateHarnessOptions(
146+
{ name: 'my_valid_harness_123', projectName: 'myValidHarness123' },
147+
testDir
148+
);
145149
expect(result.valid).toBe(true);
146150
});
147151

148152
it('rejects harness name longer than 48 characters', () => {
149153
const result = validateCreateHarnessOptions({ name: 'a'.repeat(49) }, testDir);
150154
expect(result.valid).toBe(false);
151155
});
156+
157+
it('allows long harness name when project-name is valid', () => {
158+
const result = validateCreateHarnessOptions(
159+
{ name: `Harness${'A'.repeat(30)}`, projectName: 'ShortProject' },
160+
testDir
161+
);
162+
expect(result.valid).toBe(true);
163+
});
164+
165+
it('validates project-name separately from harness name', () => {
166+
const result = validateCreateHarnessOptions(
167+
{ name: 'ValidHarness', projectName: 'ProjectNameTooLongForCli' },
168+
testDir
169+
);
170+
expect(result.valid).toBe(false);
171+
expect(result.error).toContain('Project name');
172+
});
173+
174+
it('checks folder existence using project-name', () => {
175+
const result = validateCreateHarnessOptions({ name: 'ValidHarness2', projectName: 'existingProject' }, testDir);
176+
expect(result.valid).toBe(false);
177+
expect(result.error).toContain('existingProject');
178+
});
152179
});

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}/`,

0 commit comments

Comments
 (0)