@genui/a3-create 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/dist/index.js +684 -0
- package/package.json +52 -0
- package/template/.cursor/rules/example-app.mdc +9 -0
- package/template/CLAUDE.md +121 -0
- package/template/README.md +20 -0
- package/template/_gitignore +36 -0
- package/template/app/ThemeProvider.tsx +17 -0
- package/template/app/agents/age.ts +25 -0
- package/template/app/agents/greeting.ts +30 -0
- package/template/app/agents/index.ts +57 -0
- package/template/app/agents/onboarding/index.ts +15 -0
- package/template/app/agents/onboarding/prompt.ts +59 -0
- package/template/app/agents/registry.ts +17 -0
- package/template/app/agents/state.ts +10 -0
- package/template/app/api/agui/route.ts +56 -0
- package/template/app/api/chat/route.ts +35 -0
- package/template/app/api/stream/route.ts +57 -0
- package/template/app/apple-icon-dark.png +0 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/components/atoms/AgentNode.tsx +56 -0
- package/template/app/components/atoms/AppLogo.tsx +44 -0
- package/template/app/components/atoms/ChatContainer.tsx +13 -0
- package/template/app/components/atoms/ChatHeader.tsx +49 -0
- package/template/app/components/atoms/MarkdownRenderer.tsx +134 -0
- package/template/app/components/atoms/MessageBubble.tsx +21 -0
- package/template/app/components/atoms/TransitionEdge.tsx +49 -0
- package/template/app/components/atoms/index.ts +7 -0
- package/template/app/components/molecules/ChatInput.tsx +94 -0
- package/template/app/components/molecules/ChatMessage.tsx +45 -0
- package/template/app/components/molecules/index.ts +2 -0
- package/template/app/components/organisms/AgentGraph.tsx +75 -0
- package/template/app/components/organisms/AguiChat.tsx +133 -0
- package/template/app/components/organisms/Chat.tsx +88 -0
- package/template/app/components/organisms/ChatMessageList.tsx +35 -0
- package/template/app/components/organisms/ExamplePageLayout.tsx +118 -0
- package/template/app/components/organisms/OnboardingChat.tsx +24 -0
- package/template/app/components/organisms/Sidebar.tsx +147 -0
- package/template/app/components/organisms/SidebarLayout.tsx +58 -0
- package/template/app/components/organisms/StateViewer.tsx +126 -0
- package/template/app/components/organisms/StreamChat.tsx +173 -0
- package/template/app/components/organisms/index.ts +10 -0
- package/template/app/constants/chat.ts +52 -0
- package/template/app/constants/paths.ts +1 -0
- package/template/app/constants/ui.ts +61 -0
- package/template/app/examples/agui/page.tsx +26 -0
- package/template/app/examples/chat/page.tsx +26 -0
- package/template/app/examples/page.tsx +106 -0
- package/template/app/examples/stream/page.tsx +26 -0
- package/template/app/favicon-dark.ico +0 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/icon.svg +13 -0
- package/template/app/layout.tsx +36 -0
- package/template/app/lib/actions/restartSession.ts +10 -0
- package/template/app/lib/getAgentGraphData.ts +43 -0
- package/template/app/lib/getGraphLayout.ts +99 -0
- package/template/app/lib/hooks/useRestart.ts +33 -0
- package/template/app/lib/parseTransitionTargets.ts +140 -0
- package/template/app/lib/providers/anthropic.ts +12 -0
- package/template/app/lib/providers/bedrock.ts +12 -0
- package/template/app/lib/providers/openai.ts +10 -0
- package/template/app/onboarding/page.tsx +21 -0
- package/template/app/page.tsx +16 -0
- package/template/app/styled.d.ts +6 -0
- package/template/app/theme.ts +22 -0
- package/template/docs/A3-README.md +121 -0
- package/template/docs/API-REFERENCE.md +85 -0
- package/template/docs/ARCHITECTURE.md +84 -0
- package/template/docs/CORE-CONCEPTS.md +347 -0
- package/template/docs/CUSTOM_LOGGING.md +36 -0
- package/template/docs/CUSTOM_PROVIDERS.md +642 -0
- package/template/docs/CUSTOM_STORES.md +228 -0
- package/template/docs/PROVIDER-ANTHROPIC.md +45 -0
- package/template/docs/PROVIDER-BEDROCK.md +45 -0
- package/template/docs/PROVIDER-OPENAI.md +47 -0
- package/template/docs/PROVIDERS.md +124 -0
- package/template/docs/QUICK-START-EXAMPLES.md +197 -0
- package/template/docs/RESILIENCE.md +226 -0
- package/template/docs/TRANSITIONS.md +245 -0
- package/template/docs/WIDGETS.md +331 -0
- package/template/docs/contributing/LOGGING.md +104 -0
- package/template/docs/designs/a3-gtm-strategy.md +280 -0
- package/template/docs/designs/a3-platform-vision.md +276 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +41 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/site.webmanifest +11 -0
- package/template/scripts/dev.mjs +29 -0
- package/template/tsconfig.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import path3 from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import * as p2 from "@clack/prompts";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs3 from "fs-extra";
|
|
10
|
+
|
|
11
|
+
// src/utils/generators.ts
|
|
12
|
+
import path from "path";
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
|
|
15
|
+
// src/utils/providers/providersMeta.json
|
|
16
|
+
var providersMeta_default = {
|
|
17
|
+
openai: {
|
|
18
|
+
label: "OpenAI",
|
|
19
|
+
exportName: "getOpenAIProvider",
|
|
20
|
+
file: "openai.ts",
|
|
21
|
+
npmPackage: "@genui/a3-openai",
|
|
22
|
+
urls: {
|
|
23
|
+
keys: "https://platform.openai.com/account/api-keys"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
bedrock: {
|
|
27
|
+
label: "AWS Bedrock",
|
|
28
|
+
exportName: "getBedrockProvider",
|
|
29
|
+
file: "bedrock.ts",
|
|
30
|
+
npmPackage: "@genui/a3-bedrock",
|
|
31
|
+
urls: {
|
|
32
|
+
keys: "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html",
|
|
33
|
+
configure: "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html",
|
|
34
|
+
credentials: "https://console.aws.amazon.com/iam/home#/security_credentials",
|
|
35
|
+
docs: "https://docs.aws.amazon.com/bedrock/latest/userguide/"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
anthropic: {
|
|
39
|
+
label: "Anthropic",
|
|
40
|
+
exportName: "getAnthropicProvider",
|
|
41
|
+
file: "anthropic.ts",
|
|
42
|
+
npmPackage: "@genui/a3-anthropic",
|
|
43
|
+
urls: {
|
|
44
|
+
keys: "https://console.anthropic.com/settings/keys"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/utils/providers/index.ts
|
|
50
|
+
var PROVIDER_META = providersMeta_default;
|
|
51
|
+
function providerDocName(key) {
|
|
52
|
+
return `PROVIDER-${key.toUpperCase()}.md`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/utils/generators.ts
|
|
56
|
+
function generateProviderFiles(targetDir, config) {
|
|
57
|
+
const providersDir = path.join(targetDir, "app", "lib", "providers");
|
|
58
|
+
const docsDir = path.join(targetDir, "docs");
|
|
59
|
+
for (const [key, meta] of Object.entries(PROVIDER_META)) {
|
|
60
|
+
if (!config.providers.includes(key)) {
|
|
61
|
+
const filePath = path.join(providersDir, meta.file);
|
|
62
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
63
|
+
const docPath = path.join(docsDir, providerDocName(key));
|
|
64
|
+
if (fs.existsSync(docPath)) fs.unlinkSync(docPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const primaryMeta = PROVIDER_META[config.primaryProvider];
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const provKey of config.providers) {
|
|
70
|
+
const meta = PROVIDER_META[provKey];
|
|
71
|
+
const baseName = meta.file.replace(".ts", "");
|
|
72
|
+
lines.push(`export { ${meta.exportName} } from './${baseName}'`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(`export { ${primaryMeta.exportName} as getProvider } from './${primaryMeta.file.replace(".ts", "")}'`);
|
|
75
|
+
lines.push("");
|
|
76
|
+
fs.outputFileSync(path.join(providersDir, "index.ts"), lines.join("\n"));
|
|
77
|
+
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
78
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
79
|
+
const pkg = fs.readJsonSync(pkgJsonPath);
|
|
80
|
+
for (const [key, meta] of Object.entries(PROVIDER_META)) {
|
|
81
|
+
if (!config.providers.includes(key) && pkg.dependencies?.[meta.npmPackage]) {
|
|
82
|
+
delete pkg.dependencies[meta.npmPackage];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
fs.writeJsonSync(pkgJsonPath, pkg, { spaces: 2 });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function generateEnvFile(targetDir, config) {
|
|
89
|
+
const lines = [
|
|
90
|
+
"# Generated by create-genui-a3",
|
|
91
|
+
"# This file is git-ignored. Do not commit credentials.",
|
|
92
|
+
""
|
|
93
|
+
];
|
|
94
|
+
if (config.providers.includes("openai")) {
|
|
95
|
+
lines.push("# OpenAI", `OPENAI_API_KEY=${config.openaiApiKey || ""}`, "");
|
|
96
|
+
}
|
|
97
|
+
if (config.providers.includes("anthropic")) {
|
|
98
|
+
lines.push("# Anthropic", `ANTHROPIC_API_KEY=${config.anthropicApiKey || ""}`, "");
|
|
99
|
+
}
|
|
100
|
+
if (config.providers.includes("bedrock")) {
|
|
101
|
+
if (config.bedrockAuthMode === "profile") {
|
|
102
|
+
lines.push("# AWS Bedrock (profile mode)", `AWS_PROFILE=${config.awsProfile || "default"}`);
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(
|
|
105
|
+
"# AWS Bedrock (access keys)",
|
|
106
|
+
`AWS_ACCESS_KEY_ID=${config.awsAccessKeyId || ""}`,
|
|
107
|
+
`AWS_SECRET_ACCESS_KEY=${config.awsSecretAccessKey || ""}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
lines.push(`AWS_REGION=${config.awsRegion || "us-east-1"}`, "");
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(path.join(targetDir, ".env"), lines.join("\n"));
|
|
113
|
+
}
|
|
114
|
+
function scaffoldProject(templateDir, targetDir, projectName) {
|
|
115
|
+
fs.copySync(templateDir, targetDir);
|
|
116
|
+
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
117
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
118
|
+
const pkg = fs.readJsonSync(pkgJsonPath);
|
|
119
|
+
pkg.name = projectName;
|
|
120
|
+
delete pkg.private;
|
|
121
|
+
fs.writeJsonSync(pkgJsonPath, pkg, { spaces: 2 });
|
|
122
|
+
}
|
|
123
|
+
const gitignoreSrc = path.join(targetDir, "_gitignore");
|
|
124
|
+
if (fs.existsSync(gitignoreSrc)) {
|
|
125
|
+
fs.renameSync(gitignoreSrc, path.join(targetDir, ".gitignore"));
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const cursorrulesPath = path.join(targetDir, ".cursorrules");
|
|
129
|
+
const claudeMdPath = path.join(targetDir, "CLAUDE.md");
|
|
130
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
131
|
+
fs.symlinkSync("CLAUDE.md", cursorrulesPath, "file");
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
try {
|
|
135
|
+
fs.copyFileSync(path.join(targetDir, "CLAUDE.md"), path.join(targetDir, ".cursorrules"));
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/utils/prompts.ts
|
|
142
|
+
import * as p from "@clack/prompts";
|
|
143
|
+
|
|
144
|
+
// src/utils/aws.ts
|
|
145
|
+
import fs2 from "fs";
|
|
146
|
+
import os from "os";
|
|
147
|
+
import path2 from "path";
|
|
148
|
+
function detectAwsProfiles() {
|
|
149
|
+
try {
|
|
150
|
+
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || path2.join(os.homedir(), ".aws", "credentials");
|
|
151
|
+
const content = fs2.readFileSync(credentialsPath, "utf-8");
|
|
152
|
+
const profiles = [];
|
|
153
|
+
const regex = /^\[([^\]]+)\]$/gm;
|
|
154
|
+
let match;
|
|
155
|
+
while ((match = regex.exec(content)) !== null) {
|
|
156
|
+
profiles.push(match[1]);
|
|
157
|
+
}
|
|
158
|
+
return profiles;
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function detectAwsProfileRegion(profileName) {
|
|
164
|
+
try {
|
|
165
|
+
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || path2.join(os.homedir(), ".aws", "credentials");
|
|
166
|
+
const content = fs2.readFileSync(credentialsPath, "utf-8");
|
|
167
|
+
const region = extractRegionFromIni(content, profileName);
|
|
168
|
+
if (region) return region;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const configPath = process.env.AWS_CONFIG_FILE || path2.join(os.homedir(), ".aws", "config");
|
|
173
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
174
|
+
const sectionName = profileName === "default" ? profileName : `profile ${profileName}`;
|
|
175
|
+
const region = extractRegionFromIni(content, sectionName);
|
|
176
|
+
if (region) return region;
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
function extractRegionFromIni(content, sectionName) {
|
|
182
|
+
const lines = content.split("\n");
|
|
183
|
+
let inSection = false;
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
if (trimmed.startsWith("[")) {
|
|
187
|
+
const name = trimmed.slice(1, -1);
|
|
188
|
+
inSection = name === sectionName;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (inSection && trimmed.startsWith("region")) {
|
|
192
|
+
const match = /^region\s*=\s*(.+)$/.exec(trimmed);
|
|
193
|
+
if (match) return match[1].trim();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/utils/validation.ts
|
|
200
|
+
function normalizeKey(key) {
|
|
201
|
+
return key.replace(/\s/g, "");
|
|
202
|
+
}
|
|
203
|
+
function buildErrorMessage({
|
|
204
|
+
detail,
|
|
205
|
+
provider,
|
|
206
|
+
helpUrl
|
|
207
|
+
}) {
|
|
208
|
+
let message = `${provider} API key error: ${detail}
|
|
209
|
+
|
|
210
|
+
Make sure:
|
|
211
|
+
\u2022 Your credentials are active and not revoked
|
|
212
|
+
\u2022 Your account has appropriate permissions
|
|
213
|
+
\u2022 The credentials haven't exceeded rate limits`;
|
|
214
|
+
if (helpUrl) {
|
|
215
|
+
message += `
|
|
216
|
+
|
|
217
|
+
Get help at: ${helpUrl}`;
|
|
218
|
+
}
|
|
219
|
+
return message;
|
|
220
|
+
}
|
|
221
|
+
async function validateOpenAIKey(apiKey) {
|
|
222
|
+
if (!apiKey.startsWith("sk-proj-")) {
|
|
223
|
+
return {
|
|
224
|
+
valid: false,
|
|
225
|
+
message: `Invalid format: OpenAI API key must start with sk-proj-
|
|
226
|
+
|
|
227
|
+
Get a new key at: ${providersMeta_default.openai.urls.keys}`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
Authorization: `Bearer ${apiKey}`,
|
|
235
|
+
"Content-Type": "application/json"
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
model: "gpt-4o-mini",
|
|
239
|
+
max_tokens: 1,
|
|
240
|
+
messages: [{ role: "user", content: "hi" }]
|
|
241
|
+
}),
|
|
242
|
+
signal: AbortSignal.timeout(1e4)
|
|
243
|
+
});
|
|
244
|
+
if (response.ok) {
|
|
245
|
+
return { valid: true, message: "OpenAI API key verified \u2014 completions working." };
|
|
246
|
+
}
|
|
247
|
+
const body = await response.json().catch(() => null);
|
|
248
|
+
const detail = body?.error?.message ?? `HTTP ${response.status}`;
|
|
249
|
+
return { valid: false, message: buildErrorMessage({ detail, provider: "OpenAI" }) };
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
252
|
+
return { valid: false, message: `Network error: ${msg}` };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function validateAnthropicKey(apiKey) {
|
|
256
|
+
if (!apiKey.startsWith("sk-ant-")) {
|
|
257
|
+
return {
|
|
258
|
+
valid: false,
|
|
259
|
+
message: `Invalid format: Anthropic API key must start with sk-ant-
|
|
260
|
+
|
|
261
|
+
Get a new key at: ${providersMeta_default.anthropic.urls.keys}`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: {
|
|
268
|
+
"x-api-key": apiKey,
|
|
269
|
+
"anthropic-version": "2023-06-01",
|
|
270
|
+
"content-type": "application/json"
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
model: "claude-haiku-4-5-20251001",
|
|
274
|
+
max_tokens: 1,
|
|
275
|
+
messages: [{ role: "user", content: "hi" }]
|
|
276
|
+
}),
|
|
277
|
+
signal: AbortSignal.timeout(1e4)
|
|
278
|
+
});
|
|
279
|
+
if (response.ok) {
|
|
280
|
+
return { valid: true, message: "Anthropic API key verified \u2014 completions working." };
|
|
281
|
+
}
|
|
282
|
+
const body = await response.json().catch(() => null);
|
|
283
|
+
const detail = body?.error?.message ?? `HTTP ${response.status}`;
|
|
284
|
+
return {
|
|
285
|
+
valid: false,
|
|
286
|
+
message: buildErrorMessage({ detail, provider: "Anthropic", helpUrl: providersMeta_default.anthropic.urls.keys })
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
290
|
+
return { valid: false, message: `Network error: ${msg}` };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function validateAwsCredentials(credentials) {
|
|
294
|
+
if (credentials.mode === "keys") {
|
|
295
|
+
if (!credentials.accessKeyId.match(/^AKIA[0-9A-Z]{16}$/)) {
|
|
296
|
+
return {
|
|
297
|
+
valid: false,
|
|
298
|
+
message: `Invalid format: AWS Access Key ID must start with AKIA and be 20 characters total
|
|
299
|
+
|
|
300
|
+
Manage keys at: ${providersMeta_default.bedrock.urls.keys}`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (credentials.secretAccessKey.length < 40) {
|
|
304
|
+
return {
|
|
305
|
+
valid: false,
|
|
306
|
+
message: `Invalid format: AWS Secret Access Key appears too short (should be ~40 characters)
|
|
307
|
+
|
|
308
|
+
Manage keys at: ${providersMeta_default.bedrock.urls.keys}`
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const { BedrockClient, ListFoundationModelsCommand } = await import("@aws-sdk/client-bedrock");
|
|
314
|
+
let clientConfig;
|
|
315
|
+
if (credentials.mode === "profile") {
|
|
316
|
+
const { fromIni } = await import("@aws-sdk/credential-provider-ini");
|
|
317
|
+
clientConfig = {
|
|
318
|
+
region: credentials.region,
|
|
319
|
+
credentials: fromIni({ profile: credentials.profile })
|
|
320
|
+
};
|
|
321
|
+
} else {
|
|
322
|
+
clientConfig = {
|
|
323
|
+
region: credentials.region,
|
|
324
|
+
credentials: {
|
|
325
|
+
accessKeyId: credentials.accessKeyId,
|
|
326
|
+
secretAccessKey: credentials.secretAccessKey
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const client = new BedrockClient(clientConfig);
|
|
331
|
+
await client.send(new ListFoundationModelsCommand({ byOutputModality: "TEXT" }));
|
|
332
|
+
return { valid: true, message: "AWS credentials verified with Bedrock access." };
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
335
|
+
let message = `AWS validation failed: ${msg}`;
|
|
336
|
+
if (msg.includes("Could not resolve credentials")) {
|
|
337
|
+
const profile = credentials.mode === "profile" ? credentials.profile : "unknown";
|
|
338
|
+
message = `Could not find credentials for profile "${profile}" in ~/.aws/credentials
|
|
339
|
+
|
|
340
|
+
Set up a profile with: aws configure
|
|
341
|
+
Guide: ${providersMeta_default.bedrock.urls.configure}`;
|
|
342
|
+
} else if (msg.includes("InvalidSignatureException") || msg.includes("InvalidSignature")) {
|
|
343
|
+
message = `Invalid AWS credentials (signature mismatch)
|
|
344
|
+
|
|
345
|
+
Make sure:
|
|
346
|
+
\u2022 Your Access Key ID and Secret Access Key are correct
|
|
347
|
+
\u2022 They haven't been revoked or rotated
|
|
348
|
+
|
|
349
|
+
Manage credentials at: ${providersMeta_default.bedrock.urls.credentials}`;
|
|
350
|
+
} else if (msg.includes("UnauthorizedOperation") || msg.includes("NotAuthorizedForSourceException")) {
|
|
351
|
+
message = `AWS credentials don't have permission to access Bedrock
|
|
352
|
+
|
|
353
|
+
Make sure:
|
|
354
|
+
\u2022 Your IAM user/role has Bedrock access
|
|
355
|
+
\u2022 Your region has Bedrock enabled
|
|
356
|
+
|
|
357
|
+
Learn more: ${providersMeta_default.bedrock.urls.docs}`;
|
|
358
|
+
}
|
|
359
|
+
return { valid: false, message };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/utils/prompts.ts
|
|
364
|
+
function handleCancel(value) {
|
|
365
|
+
if (p.isCancel(value)) {
|
|
366
|
+
p.cancel("Setup cancelled.");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function promptProjectName() {
|
|
371
|
+
let projectName = process.argv[2];
|
|
372
|
+
if (!projectName) {
|
|
373
|
+
const value = await p.text({
|
|
374
|
+
message: "What is your project named?",
|
|
375
|
+
placeholder: "my-a3-quickstart",
|
|
376
|
+
defaultValue: "my-a3-quickstart"
|
|
377
|
+
});
|
|
378
|
+
handleCancel(value);
|
|
379
|
+
projectName = value.trim();
|
|
380
|
+
}
|
|
381
|
+
return projectName;
|
|
382
|
+
}
|
|
383
|
+
async function promptAccessKeys(config) {
|
|
384
|
+
p.log.info(
|
|
385
|
+
"You'll need AWS access keys to authenticate with Bedrock.\n Create or manage keys at: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"
|
|
386
|
+
);
|
|
387
|
+
const awsAccessKeyId = await p.text({
|
|
388
|
+
message: "AWS_ACCESS_KEY_ID:",
|
|
389
|
+
validate(input) {
|
|
390
|
+
if (!input) return "Access key ID is required.";
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
handleCancel(awsAccessKeyId);
|
|
394
|
+
config.awsAccessKeyId = normalizeKey(awsAccessKeyId);
|
|
395
|
+
const awsSecretAccessKey = await p.password({
|
|
396
|
+
message: "AWS_SECRET_ACCESS_KEY:",
|
|
397
|
+
validate(input) {
|
|
398
|
+
if (!input) return "Secret access key is required.";
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
handleCancel(awsSecretAccessKey);
|
|
402
|
+
config.awsSecretAccessKey = normalizeKey(awsSecretAccessKey);
|
|
403
|
+
}
|
|
404
|
+
async function promptOpenAIConfig(config) {
|
|
405
|
+
p.log.step(PROVIDER_META.openai.label);
|
|
406
|
+
let validated = false;
|
|
407
|
+
while (!validated) {
|
|
408
|
+
const openaiApiKey = await p.password({
|
|
409
|
+
message: "OpenAI API key:",
|
|
410
|
+
validate(input) {
|
|
411
|
+
if (!input) return "API key is required (starts with sk-...). Get one at https://platform.openai.com/api-keys";
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
handleCancel(openaiApiKey);
|
|
415
|
+
config.openaiApiKey = normalizeKey(openaiApiKey);
|
|
416
|
+
const spin = p.spinner();
|
|
417
|
+
spin.start("Validating OpenAI credentials...");
|
|
418
|
+
const result = await validateOpenAIKey(config.openaiApiKey);
|
|
419
|
+
spin.stop(result.valid ? "OpenAI credentials verified" : "OpenAI validation failed");
|
|
420
|
+
if (result.valid) {
|
|
421
|
+
validated = true;
|
|
422
|
+
} else {
|
|
423
|
+
p.log.warn(result.message);
|
|
424
|
+
p.log.info("Please try again.");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function promptAnthropicConfig(config) {
|
|
429
|
+
p.log.step(PROVIDER_META.anthropic.label);
|
|
430
|
+
let validated = false;
|
|
431
|
+
while (!validated) {
|
|
432
|
+
const anthropicApiKey = await p.password({
|
|
433
|
+
message: "Anthropic API key:",
|
|
434
|
+
validate(input) {
|
|
435
|
+
if (!input)
|
|
436
|
+
return "API key is required (starts with sk-ant-...). Get one at https://console.anthropic.com/settings/keys";
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
handleCancel(anthropicApiKey);
|
|
440
|
+
config.anthropicApiKey = normalizeKey(anthropicApiKey);
|
|
441
|
+
const spin = p.spinner();
|
|
442
|
+
spin.start("Validating Anthropic credentials...");
|
|
443
|
+
const result = await validateAnthropicKey(config.anthropicApiKey);
|
|
444
|
+
spin.stop(result.valid ? "Anthropic credentials verified" : "Anthropic validation failed");
|
|
445
|
+
if (result.valid) {
|
|
446
|
+
validated = true;
|
|
447
|
+
} else {
|
|
448
|
+
p.log.warn(result.message);
|
|
449
|
+
p.log.info("Please try again.");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function promptProfileSelection(profiles) {
|
|
454
|
+
const MANUAL_ENTRY = "__manual__";
|
|
455
|
+
const awsProfile = await p.select({
|
|
456
|
+
message: "AWS profile",
|
|
457
|
+
options: [
|
|
458
|
+
...profiles.map((prof) => ({ label: prof, value: prof })),
|
|
459
|
+
{ label: "Enter manually", value: MANUAL_ENTRY }
|
|
460
|
+
]
|
|
461
|
+
});
|
|
462
|
+
handleCancel(awsProfile);
|
|
463
|
+
if (awsProfile === MANUAL_ENTRY) {
|
|
464
|
+
const manualProfile = await p.text({
|
|
465
|
+
message: "AWS profile name:",
|
|
466
|
+
placeholder: "default",
|
|
467
|
+
defaultValue: "default",
|
|
468
|
+
validate(input) {
|
|
469
|
+
if (!input) return "Profile name is required.";
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
handleCancel(manualProfile);
|
|
473
|
+
return manualProfile.trim();
|
|
474
|
+
}
|
|
475
|
+
return awsProfile;
|
|
476
|
+
}
|
|
477
|
+
async function promptDetectProfile(config) {
|
|
478
|
+
const profiles = detectAwsProfiles();
|
|
479
|
+
if (profiles.length > 0) {
|
|
480
|
+
return promptProfileSelection(profiles);
|
|
481
|
+
}
|
|
482
|
+
while (true) {
|
|
483
|
+
p.log.warn("No AWS profiles found in ~/.aws/credentials");
|
|
484
|
+
p.log.info(
|
|
485
|
+
"Set up a profile with: aws configure\n Guide: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html"
|
|
486
|
+
);
|
|
487
|
+
const action = await p.select({
|
|
488
|
+
message: "How would you like to proceed?",
|
|
489
|
+
options: [
|
|
490
|
+
{ label: "Try again", value: "retry", hint: "Re-check ~/.aws/credentials" },
|
|
491
|
+
{ label: "Use access keys instead", value: "keys", hint: "Provide key ID + secret directly" }
|
|
492
|
+
]
|
|
493
|
+
});
|
|
494
|
+
handleCancel(action);
|
|
495
|
+
if (action === "keys") {
|
|
496
|
+
config.bedrockAuthMode = "keys";
|
|
497
|
+
await promptAccessKeys(config);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const retryProfiles = detectAwsProfiles();
|
|
501
|
+
if (retryProfiles.length > 0) {
|
|
502
|
+
return promptProfileSelection(retryProfiles);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function promptBedrockConfig(config) {
|
|
507
|
+
p.log.step(PROVIDER_META.bedrock.label);
|
|
508
|
+
const authMode = await p.select({
|
|
509
|
+
message: "How do you want to authenticate with AWS Bedrock?",
|
|
510
|
+
options: [
|
|
511
|
+
{ label: "AWS Profile (recommended)", value: "profile", hint: "Uses ~/.aws/credentials" },
|
|
512
|
+
{ label: "Access Keys", value: "keys", hint: "Provide key ID + secret directly" }
|
|
513
|
+
]
|
|
514
|
+
});
|
|
515
|
+
handleCancel(authMode);
|
|
516
|
+
config.bedrockAuthMode = authMode;
|
|
517
|
+
if (authMode === "profile") {
|
|
518
|
+
const profile = await promptDetectProfile(config);
|
|
519
|
+
if (profile) config.awsProfile = profile;
|
|
520
|
+
} else {
|
|
521
|
+
await promptAccessKeys(config);
|
|
522
|
+
}
|
|
523
|
+
let validated = false;
|
|
524
|
+
let firstAttempt = true;
|
|
525
|
+
while (!validated) {
|
|
526
|
+
if (!firstAttempt) {
|
|
527
|
+
if (config.bedrockAuthMode === "keys") {
|
|
528
|
+
await promptAccessKeys(config);
|
|
529
|
+
} else {
|
|
530
|
+
const retryAction = await p.select({
|
|
531
|
+
message: "How would you like to retry?",
|
|
532
|
+
options: [
|
|
533
|
+
{ label: "Try a different profile", value: "profile" },
|
|
534
|
+
{ label: "Switch to access keys", value: "keys" }
|
|
535
|
+
]
|
|
536
|
+
});
|
|
537
|
+
handleCancel(retryAction);
|
|
538
|
+
if (retryAction === "keys") {
|
|
539
|
+
config.bedrockAuthMode = "keys";
|
|
540
|
+
await promptAccessKeys(config);
|
|
541
|
+
} else {
|
|
542
|
+
const profile = await promptDetectProfile(config);
|
|
543
|
+
if (profile) config.awsProfile = profile;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
firstAttempt = false;
|
|
548
|
+
const currentDetectedRegion = config.bedrockAuthMode === "profile" ? detectAwsProfileRegion(config.awsProfile) : void 0;
|
|
549
|
+
const awsRegion = await p.text({
|
|
550
|
+
message: "AWS region:",
|
|
551
|
+
...currentDetectedRegion ? { initialValue: currentDetectedRegion } : { placeholder: "us-east-1" },
|
|
552
|
+
validate(input) {
|
|
553
|
+
if (!input) return "AWS region is required.";
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
handleCancel(awsRegion);
|
|
557
|
+
config.awsRegion = awsRegion.trim();
|
|
558
|
+
const spin = p.spinner();
|
|
559
|
+
spin.start("Validating AWS Bedrock credentials...");
|
|
560
|
+
const credentialInput = config.bedrockAuthMode === "profile" ? { mode: "profile", profile: config.awsProfile, region: config.awsRegion } : {
|
|
561
|
+
mode: "keys",
|
|
562
|
+
accessKeyId: config.awsAccessKeyId,
|
|
563
|
+
secretAccessKey: config.awsSecretAccessKey,
|
|
564
|
+
region: config.awsRegion
|
|
565
|
+
};
|
|
566
|
+
const result = await validateAwsCredentials(credentialInput);
|
|
567
|
+
spin.stop(result.valid ? "AWS Bedrock credentials verified" : "AWS Bedrock validation failed");
|
|
568
|
+
if (result.valid) {
|
|
569
|
+
validated = true;
|
|
570
|
+
} else {
|
|
571
|
+
p.log.warn(result.message);
|
|
572
|
+
p.log.info("Re-entering credentials...");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function promptPrimaryProvider(providers, config) {
|
|
577
|
+
const primaryProvider = await p.select({
|
|
578
|
+
message: "Which provider should the app use by default?",
|
|
579
|
+
options: providers.map((prov) => ({ label: PROVIDER_META[prov].label, value: prov }))
|
|
580
|
+
});
|
|
581
|
+
handleCancel(primaryProvider);
|
|
582
|
+
config.primaryProvider = primaryProvider;
|
|
583
|
+
}
|
|
584
|
+
async function promptProviders() {
|
|
585
|
+
const providers = await p.multiselect({
|
|
586
|
+
message: "Which LLM provider(s) do you want to configure?",
|
|
587
|
+
options: [
|
|
588
|
+
{ label: "OpenAI", value: "openai" },
|
|
589
|
+
{ label: "AWS Bedrock", value: "bedrock" },
|
|
590
|
+
{ label: "Anthropic", value: "anthropic" }
|
|
591
|
+
],
|
|
592
|
+
required: true
|
|
593
|
+
});
|
|
594
|
+
handleCancel(providers);
|
|
595
|
+
const config = {
|
|
596
|
+
providers,
|
|
597
|
+
primaryProvider: providers[0]
|
|
598
|
+
};
|
|
599
|
+
if (providers.includes("openai")) {
|
|
600
|
+
await promptOpenAIConfig(config);
|
|
601
|
+
}
|
|
602
|
+
if (providers.includes("bedrock")) {
|
|
603
|
+
await promptBedrockConfig(config);
|
|
604
|
+
}
|
|
605
|
+
if (providers.includes("anthropic")) {
|
|
606
|
+
await promptAnthropicConfig(config);
|
|
607
|
+
}
|
|
608
|
+
if (providers.length > 1) {
|
|
609
|
+
await promptPrimaryProvider(providers, config);
|
|
610
|
+
}
|
|
611
|
+
return config;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/index.ts
|
|
615
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
616
|
+
var BANNER = `
|
|
617
|
+
${chalk.cyan("\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ")}
|
|
618
|
+
${chalk.cyan("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
619
|
+
${chalk.cyan("\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
620
|
+
${chalk.cyan("\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
621
|
+
${chalk.cyan("\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
622
|
+
${chalk.cyan("\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D ")}
|
|
623
|
+
|
|
624
|
+
${chalk.white.bold("A3")} ${chalk.dim("\u2014 Agentic App Architecture")}
|
|
625
|
+
`;
|
|
626
|
+
function installDependencies(targetDir, projectName) {
|
|
627
|
+
try {
|
|
628
|
+
execSync("npm install --legacy-peer-deps", { cwd: targetDir, stdio: "inherit" });
|
|
629
|
+
} catch {
|
|
630
|
+
p2.log.error("Failed to install dependencies. You can try manually:");
|
|
631
|
+
p2.log.info(` cd ${projectName}
|
|
632
|
+
npm install`);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function printSuccess(projectName, targetDir, config) {
|
|
637
|
+
const { label } = PROVIDER_META[config.primaryProvider];
|
|
638
|
+
p2.note(
|
|
639
|
+
[
|
|
640
|
+
`Primary provider: ${label}`,
|
|
641
|
+
"Check .env for credentials configuration",
|
|
642
|
+
"",
|
|
643
|
+
"Get started:",
|
|
644
|
+
"",
|
|
645
|
+
` cd ${projectName}`,
|
|
646
|
+
" npm run dev"
|
|
647
|
+
].join("\n"),
|
|
648
|
+
`Created ${projectName} at ${targetDir}`
|
|
649
|
+
);
|
|
650
|
+
p2.outro("Happy building!");
|
|
651
|
+
}
|
|
652
|
+
async function main() {
|
|
653
|
+
console.log(BANNER);
|
|
654
|
+
p2.intro("Create a new A3 app");
|
|
655
|
+
const projectName = await promptProjectName();
|
|
656
|
+
const targetDir = path3.resolve(process.cwd(), projectName);
|
|
657
|
+
if (fs3.existsSync(targetDir) && fs3.readdirSync(targetDir).length > 0) {
|
|
658
|
+
p2.cancel(`Directory "${projectName}" already exists and is not empty.`);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
const templateDir = path3.resolve(__dirname, "..", "template");
|
|
662
|
+
if (!fs3.existsSync(templateDir)) {
|
|
663
|
+
p2.cancel("Template directory not found. The package may not have been built correctly.");
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
const providerConfig = await promptProviders();
|
|
667
|
+
p2.log.info(`Creating a new A3 app in ${targetDir}`);
|
|
668
|
+
const spin = p2.spinner();
|
|
669
|
+
spin.start("Scaffolding project files");
|
|
670
|
+
scaffoldProject(templateDir, targetDir, projectName);
|
|
671
|
+
spin.message("Configuring providers");
|
|
672
|
+
generateProviderFiles(targetDir, providerConfig);
|
|
673
|
+
spin.message("Generating .env file");
|
|
674
|
+
generateEnvFile(targetDir, providerConfig);
|
|
675
|
+
spin.stop("Project scaffolded");
|
|
676
|
+
p2.log.step("Installing dependencies...");
|
|
677
|
+
installDependencies(targetDir, projectName);
|
|
678
|
+
printSuccess(projectName, targetDir, providerConfig);
|
|
679
|
+
}
|
|
680
|
+
main().catch((err) => {
|
|
681
|
+
p2.cancel("Unexpected error");
|
|
682
|
+
console.error(err);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
});
|