forked from aws/agentcore-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCreateScreen.tsx
More file actions
367 lines (335 loc) · 12.6 KB
/
Copy pathCreateScreen.tsx
File metadata and controls
367 lines (335 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import { DEFAULT_MODEL_IDS, ProjectNameSchema } from '../../../../schema';
import { validateFolderNotExists } from '../../../commands/create/validate';
import { VPC_ENDPOINT_WARNING } from '../../../commands/shared/vpc-utils';
import { computeDefaultCredentialEnvVarName } from '../../../primitives/credential-utils';
import {
LogLink,
type NextStep,
NextSteps,
Screen,
SelectList,
type Step,
StepProgress,
TextInput,
} from '../../components';
import { HELP_TEXT } from '../../constants';
import { setExitMessage } from '../../exit-message';
import { useListNavigation } from '../../hooks';
import { STATUS_COLORS } from '../../theme';
import { AddAgentScreen } from '../agent/AddAgentScreen';
import type { AddAgentConfig } from '../agent/types';
import { FRAMEWORK_OPTIONS } from '../agent/types';
import { useCreateFlow } from './useCreateFlow';
import { Box, Text, useApp } from 'ink';
import { join } from 'path';
import { useCallback, useEffect } from 'react';
/** Build a text representation of the completion screen for terminal output */
function buildExitMessage(projectName: string, steps: Step[], agentConfig: AddAgentConfig | null): string {
const lines: string[] = [];
// Title
lines.push('\x1b[1mAgentCore Create\x1b[0m');
lines.push('');
// Project name
lines.push(`Project: \x1b[32m${projectName}\x1b[0m`);
lines.push('');
// Steps
for (const step of steps) {
const statusLabel = step.status === 'success' ? '\x1b[32m[done]\x1b[0m' : `[${step.status}]`;
lines.push(`${statusLabel} ${step.label}`);
}
lines.push('');
// Created summary
lines.push('\x1b[2mCreated:\x1b[0m');
lines.push(` ${projectName}/`);
if (agentConfig?.agentType === 'create' || agentConfig?.agentType === 'import') {
const frameworkOption = FRAMEWORK_OPTIONS.find(o => o.id === agentConfig.framework);
const frameworkLabel = frameworkOption?.title ?? agentConfig.framework;
const modelName = DEFAULT_MODEL_IDS[agentConfig.modelProvider];
const agentPath = `app/${agentConfig.name}/`;
const agentcorePath = 'agentcore/';
const maxPathLen = Math.max(agentPath.length, agentcorePath.length);
lines.push(` ${agentPath.padEnd(maxPathLen)} \x1b[2m${agentConfig.language} agent (${frameworkLabel})\x1b[0m`);
lines.push(` ${agentcorePath.padEnd(maxPathLen)} \x1b[2mConfig and CDK project\x1b[0m`);
lines.push('');
lines.push(`\x1b[2mModel:\x1b[0m ${modelName} \x1b[2mvia ${agentConfig.modelProvider}\x1b[0m`);
} else if (agentConfig?.agentType === 'byo') {
const agentPath = agentConfig.codeLocation;
const agentcorePath = 'agentcore/';
const maxPathLen = Math.max(agentPath.length, agentcorePath.length);
lines.push(` ${agentPath.padEnd(maxPathLen)} \x1b[2mAgent code location (empty)\x1b[0m`);
lines.push(` ${agentcorePath.padEnd(maxPathLen)} \x1b[2mConfig and CDK project\x1b[0m`);
} else {
lines.push(` agentcore/ \x1b[2mConfig and CDK project\x1b[0m`);
}
lines.push('');
// API key reminder if skipped
if (agentConfig && agentConfig.modelProvider !== 'Bedrock' && !agentConfig.apiKey) {
const credentialName = `${projectName}${agentConfig.modelProvider}`;
const envVarName = computeDefaultCredentialEnvVarName(credentialName);
lines.push('\x1b[33mNote:\x1b[0m API key not configured.');
lines.push(`Fill in \x1b[36m${envVarName}\x1b[0m in agentcore/.env.local before running.`);
lines.push('');
}
// BYO code location reminder
if (agentConfig?.agentType === 'byo') {
lines.push(`\x1b[33mCopy your agent code to \x1b[36m${agentConfig.codeLocation}\x1b[33m before deploying.\x1b[0m`);
lines.push(`\x1b[2mEnsure \x1b[36m${agentConfig.entrypoint}\x1b[2m is the entrypoint file in that folder.\x1b[0m`);
lines.push('');
}
// VPC endpoint warning
if (agentConfig?.networkMode === 'VPC') {
lines.push(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`);
lines.push('');
}
// Success message
lines.push('\x1b[32mProject created successfully!\x1b[0m');
lines.push('');
// Instructions
lines.push('To continue, navigate to your new project:');
lines.push('');
lines.push(` cd ${projectName}`);
lines.push(' agentcore');
lines.push('');
return lines.join('\n');
}
type NextCommand = 'dev' | 'deploy' | 'add';
interface NavigateParams {
command: NextCommand;
workingDir: string;
}
interface CreateScreenProps {
cwd: string;
/** Whether running in interactive TUI mode (from App.tsx) vs CLI mode */
isInteractive: boolean;
onExit: () => void;
onNavigate?: (params: NavigateParams) => void;
}
/** Next steps shown after successful project creation */
function getCreateNextSteps(hasAgent: boolean): NextStep[] {
if (hasAgent) {
return [
{ command: 'dev', label: 'Run agent locally' },
{ command: 'deploy', label: 'Deploy to AWS' },
];
}
return [{ command: 'add', label: 'Add an agent' }];
}
const CREATE_PROMPT_ITEMS = [
{ id: 'yes', title: 'Yes, add an agent' },
{ id: 'no', title: "No, I'll do it later" },
];
/** Tree-style display of created project structure */
function CreatedSummary({ projectName, agentConfig }: { projectName: string; agentConfig: AddAgentConfig | null }) {
const getFrameworkLabel = (framework: string) => {
const option = FRAMEWORK_OPTIONS.find(o => o.id === framework);
return option?.title ?? framework;
};
const isCreate = agentConfig?.agentType === 'create' || agentConfig?.agentType === 'import';
const isByo = agentConfig?.agentType === 'byo';
const agentPath = isCreate ? `app/${agentConfig.name}/` : isByo ? agentConfig.codeLocation : null;
const agentcorePath = 'agentcore/';
const maxPathLen = agentPath ? Math.max(agentPath.length, agentcorePath.length) : agentcorePath.length;
return (
<Box flexDirection="column" marginTop={1}>
<Text dimColor>Created:</Text>
<Box flexDirection="column" marginLeft={2}>
<Text>{projectName}/</Text>
{isCreate && agentPath && (
<Box marginLeft={2}>
<Text>
{agentPath.padEnd(maxPathLen)}
<Text dimColor>
{' '}
{agentConfig.language} agent ({getFrameworkLabel(agentConfig.framework)})
</Text>
</Text>
</Box>
)}
{isByo && agentPath && (
<Box marginLeft={2}>
<Text>
{agentPath.padEnd(maxPathLen)}
<Text dimColor>{' '}Agent code location</Text>
</Text>
</Box>
)}
<Box marginLeft={2}>
<Text>
{agentcorePath.padEnd(maxPathLen)}
<Text dimColor>{' '}Config and CDK project</Text>
</Text>
</Box>
</Box>
{isCreate && agentConfig && (
<Box marginTop={1}>
<Text dimColor>Model: </Text>
<Text>{DEFAULT_MODEL_IDS[agentConfig.modelProvider]}</Text>
<Text dimColor> via {agentConfig.modelProvider}</Text>
</Box>
)}
{isByo && agentConfig && (
<Box flexDirection="column" marginTop={1}>
<Text color="yellow">
Copy your agent code to <Text color="cyan">{agentConfig.codeLocation}</Text> before deploying.
</Text>
<Text dimColor>
Ensure <Text color="cyan">{agentConfig.entrypoint}</Text> is the entrypoint file in that folder.
</Text>
</Box>
)}
</Box>
);
}
export function CreateScreen({ cwd, isInteractive, onExit, onNavigate }: CreateScreenProps) {
const { exit } = useApp();
const flow = useCreateFlow(cwd);
// Project root is cwd/projectName (new project directory)
const projectRoot = join(cwd, flow.projectName);
// Completion state for next steps
const allSuccess = !flow.hasError && flow.isComplete;
// Handle exit - if successful, exit app completely and print completion screen
const handleExit = useCallback(() => {
if (allSuccess && isInteractive) {
// Set message to be printed after TUI exits (full completion screen)
setExitMessage(buildExitMessage(flow.projectName, flow.steps, flow.addAgentConfig));
exit();
} else {
onExit();
}
}, [allSuccess, isInteractive, flow.projectName, flow.steps, flow.addAgentConfig, exit, onExit]);
// Auto-exit when project creation completes successfully
useEffect(() => {
if (allSuccess) {
handleExit();
}
}, [allSuccess, handleExit]);
// Create prompt navigation
const { selectedIndex: createPromptIndex } = useListNavigation({
items: CREATE_PROMPT_ITEMS,
onSelect: item => {
flow.setWantsCreate(item.id === 'yes');
},
onExit: handleExit,
isActive: flow.phase === 'create-prompt',
});
// Checking phase: brief loading state
if (flow.phase === 'checking') {
return (
<Screen title="AgentCore Create" onExit={handleExit}>
<Text dimColor>Checking for existing project...</Text>
</Screen>
);
}
// Existing project error phase
if (flow.phase === 'existing-project-error') {
return (
<Screen title="AgentCore Create" onExit={handleExit} helpText="Press Esc to exit">
<Box marginBottom={1} flexDirection="column">
<Text color="red">A project already exists at this location.</Text>
{flow.existingProjectPath && <Text dimColor>Found: {flow.existingProjectPath}</Text>}
<Box marginTop={1}>
<Text>
Use <Text color="cyan">add agent</Text> to create a new agent in the existing project.
</Text>
</Box>
</Box>
</Screen>
);
}
// Input phase: ask for project name
if (flow.phase === 'input') {
return (
<Screen title="AgentCore Create" onExit={handleExit} helpText={HELP_TEXT.TEXT_INPUT}>
<Box marginBottom={1} flexDirection="column">
<Text>Create a new AgentCore project</Text>
<Text dimColor>This will create a directory with your project name.</Text>
</Box>
<TextInput
prompt="Project name"
initialValue=""
schema={ProjectNameSchema}
customValidation={name => validateFolderNotExists(name, cwd)}
onSubmit={name => {
flow.setProjectName(name);
flow.confirmProjectName();
}}
onCancel={handleExit}
/>
</Screen>
);
}
// Create prompt phase
if (flow.phase === 'create-prompt') {
return (
<Screen title="AgentCore Create" onExit={handleExit} helpText={HELP_TEXT.NAVIGATE_SELECT}>
<Box marginBottom={1}>
<Text>
Project: <Text color={STATUS_COLORS.success}>{flow.projectName}</Text>
</Text>
</Box>
<Box flexDirection="column">
<Text>Would you like to add an agent now?</Text>
<Box marginTop={1}>
<SelectList items={CREATE_PROMPT_ITEMS} selectedIndex={createPromptIndex} />
</Box>
</Box>
</Screen>
);
}
// Create wizard phase - use AddAgentScreen for consistent experience
if (flow.phase === 'create-wizard') {
return (
<AddAgentScreen
existingAgentNames={[]}
onComplete={flow.handleAddAgentComplete}
onExit={flow.goBackFromAddAgent}
/>
);
}
// Running/complete phase: show progress
const headerContent = (
<Box marginTop={1}>
<Text>
Project: <Text color={STATUS_COLORS.success}>{flow.projectName}</Text>
</Text>
</Box>
);
const helpText = flow.hasError || allSuccess ? HELP_TEXT.EXIT : undefined;
return (
<Screen title="AgentCore Create" onExit={handleExit} headerContent={headerContent} helpText={helpText}>
<StepProgress steps={flow.steps} />
{allSuccess && flow.outputDir && (
<Box marginTop={1} flexDirection="column">
<CreatedSummary projectName={flow.projectName} agentConfig={flow.addAgentConfig} />
{isInteractive ? (
<Box marginTop={1}>
<Text color="green">Project created successfully!</Text>
</Box>
) : (
<NextSteps
steps={getCreateNextSteps(flow.addAgentConfig !== null)}
isInteractive={isInteractive}
onSelect={step => {
if (onNavigate) {
onNavigate({ command: step.command as NextCommand, workingDir: projectRoot });
}
}}
onBack={handleExit}
isActive={allSuccess}
/>
)}
</Box>
)}
{flow.hasError && (
<Box marginTop={1} flexDirection="column">
<Text color={STATUS_COLORS.error}>Project creation failed.</Text>
{flow.logFilePath && (
<Box marginTop={1}>
<LogLink filePath={flow.logFilePath} />
</Box>
)}
</Box>
)}
</Screen>
);
}