create-aws-project 1.4.3 → 1.5.1
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 +5 -25
- package/dist/aws/iam.d.ts +16 -3
- package/dist/aws/iam.js +61 -5
- package/dist/cli.js +19 -11
- package/dist/commands/initialize-github.js +25 -5
- package/dist/commands/setup-aws-envs.js +46 -11
- package/dist/git/setup.d.ts +28 -0
- package/dist/git/setup.js +165 -0
- package/dist/utils/project-context.d.ts +1 -0
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js +6 -2
- package/package.json +1 -1
- package/templates/apps/api/cdk/static-stack.ts +0 -108
- package/templates/root/README.md +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Create a new AWS project from scratch including CloudFront, API Gateway, Lambdas
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/create-aws-project)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## Quick Start
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -67,6 +69,7 @@ The interactive wizard will ask you about:
|
|
|
67
69
|
- VS Code workspace configuration
|
|
68
70
|
6. **AWS region** - Where to deploy your infrastructure
|
|
69
71
|
7. **Brand color** - Theme color for your UI (blue, purple, teal, green, orange)
|
|
72
|
+
8. **GitHub repository** *(optional)* - Provide a repo URL to git init, commit, and push automatically. Press Enter to skip.
|
|
70
73
|
|
|
71
74
|
## Requirements
|
|
72
75
|
|
|
@@ -82,32 +85,9 @@ After creating your project, you'll set up AWS environments and GitHub deploymen
|
|
|
82
85
|
|
|
83
86
|
Before you begin:
|
|
84
87
|
- AWS CLI configured with credentials from your AWS management account
|
|
85
|
-
- GitHub repository created for your project
|
|
86
88
|
- GitHub Personal Access Token with "repo" scope ([create one here](https://github.com/settings/tokens/new))
|
|
87
89
|
|
|
88
|
-
### Step 1:
|
|
89
|
-
|
|
90
|
-
* Initialize the repository
|
|
91
|
-
```
|
|
92
|
-
git init
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
* Add the remote repository using the git remote add <name> <url> command. A common practice is to name it origin.
|
|
96
|
-
```bash
|
|
97
|
-
git remote add origin <REMOTE_URL>
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
* Verify the connection by listing your remotes. The -v flag shows the URLs.
|
|
101
|
-
```bash
|
|
102
|
-
git remote -v
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
* Push your local commits to the remote repository for the first time.
|
|
106
|
-
```bash
|
|
107
|
-
git push -u origin main
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Step 2: Set Up AWS Environments
|
|
90
|
+
### Step 1: Set Up AWS Environments
|
|
111
91
|
|
|
112
92
|
From your project directory, run:
|
|
113
93
|
|
|
@@ -134,7 +114,7 @@ AWS environment setup complete!
|
|
|
134
114
|
|
|
135
115
|
Account IDs are saved to `.aws-starter-config.json` for the next step.
|
|
136
116
|
|
|
137
|
-
### Step
|
|
117
|
+
### Step 2: Configure GitHub Environments
|
|
138
118
|
|
|
139
119
|
For each environment, run:
|
|
140
120
|
|
package/dist/aws/iam.d.ts
CHANGED
|
@@ -19,12 +19,17 @@ export declare function createIAMClient(region?: string): IAMClient;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function createCrossAccountIAMClient(region: string, targetAccountId: string): IAMClient;
|
|
21
21
|
/**
|
|
22
|
-
* Creates an IAM deployment user with path /deployment
|
|
22
|
+
* Creates an IAM deployment user with path /deployment/, or adopts existing tagged user
|
|
23
23
|
* @param client - IAMClient instance
|
|
24
24
|
* @param userName - User name (format: {project}-{environment}-deploy)
|
|
25
|
-
* @throws Error if user
|
|
25
|
+
* @throws Error if user exists but was not created by this tool
|
|
26
26
|
*/
|
|
27
|
-
export declare function
|
|
27
|
+
export declare function createOrAdoptDeploymentUser(client: IAMClient, userName: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Legacy function for backward compatibility
|
|
30
|
+
* @deprecated Use createOrAdoptDeploymentUser instead
|
|
31
|
+
*/
|
|
32
|
+
export declare const createDeploymentUser: typeof createOrAdoptDeploymentUser;
|
|
28
33
|
/**
|
|
29
34
|
* Creates a customer managed policy with minimal CDK deployment permissions
|
|
30
35
|
* @param client - IAMClient instance
|
|
@@ -48,11 +53,19 @@ export interface AccessKeyCredentials {
|
|
|
48
53
|
accessKeyId: string;
|
|
49
54
|
secretAccessKey: string;
|
|
50
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Gets the count of access keys for an IAM user
|
|
58
|
+
* @param client - IAMClient instance
|
|
59
|
+
* @param userName - IAM user name
|
|
60
|
+
* @returns Number of access keys (active + inactive)
|
|
61
|
+
*/
|
|
62
|
+
export declare function getAccessKeyCount(client: IAMClient, userName: string): Promise<number>;
|
|
51
63
|
/**
|
|
52
64
|
* Creates access key credentials for an IAM user
|
|
53
65
|
* @param client - IAMClient instance
|
|
54
66
|
* @param userName - IAM user name
|
|
55
67
|
* @returns Access key ID and secret access key
|
|
68
|
+
* @throws Error if user already has 2 access keys (AWS maximum)
|
|
56
69
|
* @note The secret access key is ONLY available at creation time
|
|
57
70
|
*/
|
|
58
71
|
export declare function createAccessKey(client: IAMClient, userName: string): Promise<AccessKeyCredentials>;
|
package/dist/aws/iam.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAMClient, CreateUserCommand, GetUserCommand, CreatePolicyCommand, GetPolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, NoSuchEntityException, } from '@aws-sdk/client-iam';
|
|
1
|
+
import { IAMClient, CreateUserCommand, GetUserCommand, CreatePolicyCommand, GetPolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, ListUserTagsCommand, ListAccessKeysCommand, NoSuchEntityException, } from '@aws-sdk/client-iam';
|
|
2
2
|
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
/**
|
|
@@ -73,16 +73,45 @@ async function policyExists(client, policyArn) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
76
|
+
* Checks if an IAM user has a specific tag
|
|
77
|
+
* @param client - IAMClient instance
|
|
78
|
+
* @param userName - User name to check
|
|
79
|
+
* @param tagKey - Tag key to check for
|
|
80
|
+
* @param tagValue - Expected tag value
|
|
81
|
+
* @returns true if user has the tag with the specified value
|
|
82
|
+
*/
|
|
83
|
+
async function userHasTag(client, userName, tagKey, tagValue) {
|
|
84
|
+
try {
|
|
85
|
+
const command = new ListUserTagsCommand({ UserName: userName });
|
|
86
|
+
const response = await client.send(command);
|
|
87
|
+
return response.Tags?.some((tag) => tag.Key === tagKey && tag.Value === tagValue) ?? false;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates an IAM deployment user with path /deployment/, or adopts existing tagged user
|
|
77
95
|
* @param client - IAMClient instance
|
|
78
96
|
* @param userName - User name (format: {project}-{environment}-deploy)
|
|
79
|
-
* @throws Error if user
|
|
97
|
+
* @throws Error if user exists but was not created by this tool
|
|
80
98
|
*/
|
|
81
|
-
export async function
|
|
99
|
+
export async function createOrAdoptDeploymentUser(client, userName) {
|
|
82
100
|
// Check if user already exists
|
|
83
101
|
if (await userExists(client, userName)) {
|
|
84
|
-
|
|
102
|
+
// Check if user was created by this tool (has ManagedBy tag)
|
|
103
|
+
const isManagedByUs = await userHasTag(client, userName, 'ManagedBy', 'create-aws-starter-kit');
|
|
104
|
+
if (isManagedByUs) {
|
|
105
|
+
// Adopt existing user - this is idempotent re-run
|
|
106
|
+
console.log(pc.yellow(` Adopting existing deployment user: ${userName}`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// User exists but not managed by us - error
|
|
111
|
+
throw new Error(`IAM user "${userName}" exists but was not created by this tool. Delete it or use a different project name.`);
|
|
112
|
+
}
|
|
85
113
|
}
|
|
114
|
+
// User doesn't exist - create it
|
|
86
115
|
const command = new CreateUserCommand({
|
|
87
116
|
UserName: userName,
|
|
88
117
|
Path: '/deployment/',
|
|
@@ -94,6 +123,11 @@ export async function createDeploymentUser(client, userName) {
|
|
|
94
123
|
await client.send(command);
|
|
95
124
|
console.log(pc.green(` Created IAM user: ${userName}`));
|
|
96
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Legacy function for backward compatibility
|
|
128
|
+
* @deprecated Use createOrAdoptDeploymentUser instead
|
|
129
|
+
*/
|
|
130
|
+
export const createDeploymentUser = createOrAdoptDeploymentUser;
|
|
97
131
|
/**
|
|
98
132
|
* CDK deployment policy document with minimal permissions
|
|
99
133
|
* @param accountId - AWS account ID for resource ARNs
|
|
@@ -226,14 +260,36 @@ export async function attachPolicyToUser(client, userName, policyArn) {
|
|
|
226
260
|
await client.send(command);
|
|
227
261
|
console.log(pc.green(` Attached policy to user ${userName}`));
|
|
228
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Gets the count of access keys for an IAM user
|
|
265
|
+
* @param client - IAMClient instance
|
|
266
|
+
* @param userName - IAM user name
|
|
267
|
+
* @returns Number of access keys (active + inactive)
|
|
268
|
+
*/
|
|
269
|
+
export async function getAccessKeyCount(client, userName) {
|
|
270
|
+
try {
|
|
271
|
+
const command = new ListAccessKeysCommand({ UserName: userName });
|
|
272
|
+
const response = await client.send(command);
|
|
273
|
+
return response.AccessKeyMetadata?.length ?? 0;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
229
279
|
/**
|
|
230
280
|
* Creates access key credentials for an IAM user
|
|
231
281
|
* @param client - IAMClient instance
|
|
232
282
|
* @param userName - IAM user name
|
|
233
283
|
* @returns Access key ID and secret access key
|
|
284
|
+
* @throws Error if user already has 2 access keys (AWS maximum)
|
|
234
285
|
* @note The secret access key is ONLY available at creation time
|
|
235
286
|
*/
|
|
236
287
|
export async function createAccessKey(client, userName) {
|
|
288
|
+
// Check access key limit before attempting creation
|
|
289
|
+
const keyCount = await getAccessKeyCount(client, userName);
|
|
290
|
+
if (keyCount >= 2) {
|
|
291
|
+
throw new Error(`IAM user ${userName} already has 2 access keys (AWS maximum). Delete an existing key in AWS Console > IAM > Users > ${userName} > Security credentials before retrying.`);
|
|
292
|
+
}
|
|
237
293
|
const command = new CreateAccessKeyCommand({
|
|
238
294
|
UserName: userName,
|
|
239
295
|
});
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { generateProject } from './generator/index.js';
|
|
|
7
7
|
import { showDeprecationNotice } from './commands/setup-github.js';
|
|
8
8
|
import { runSetupAwsEnvs } from './commands/setup-aws-envs.js';
|
|
9
9
|
import { runInitializeGitHub } from './commands/initialize-github.js';
|
|
10
|
+
import { promptGitSetup, setupGitRepository } from './git/setup.js';
|
|
10
11
|
/**
|
|
11
12
|
* Write project configuration file for downstream commands
|
|
12
13
|
*/
|
|
@@ -40,7 +41,7 @@ function getVersion() {
|
|
|
40
41
|
*/
|
|
41
42
|
function printHelp() {
|
|
42
43
|
console.log(`
|
|
43
|
-
create-aws-
|
|
44
|
+
create-aws-project [command] [options]
|
|
44
45
|
|
|
45
46
|
Scaffold a new AWS Starter Kit project with React, Lambda, and CDK infrastructure.
|
|
46
47
|
|
|
@@ -55,15 +56,15 @@ Options:
|
|
|
55
56
|
--version, -v Show version number
|
|
56
57
|
|
|
57
58
|
Usage:
|
|
58
|
-
create-aws-
|
|
59
|
-
create-aws-
|
|
60
|
-
create-aws-
|
|
59
|
+
create-aws-project Run interactive wizard
|
|
60
|
+
create-aws-project setup-aws-envs Create AWS accounts
|
|
61
|
+
create-aws-project initialize-github dev Configure dev environment
|
|
61
62
|
|
|
62
63
|
Examples:
|
|
63
|
-
create-aws-
|
|
64
|
-
create-aws-
|
|
65
|
-
create-aws-
|
|
66
|
-
create-aws-
|
|
64
|
+
create-aws-project my-app
|
|
65
|
+
create-aws-project setup-aws-envs
|
|
66
|
+
create-aws-project initialize-github dev
|
|
67
|
+
create-aws-project --help
|
|
67
68
|
`.trim());
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
@@ -73,7 +74,7 @@ function printWelcome() {
|
|
|
73
74
|
console.log(`
|
|
74
75
|
╔═══════════════════════════════════════════════════════╗
|
|
75
76
|
║ ║
|
|
76
|
-
║ create-aws-
|
|
77
|
+
║ create-aws-project ║
|
|
77
78
|
║ AWS Starter Kit Project Generator ║
|
|
78
79
|
║ ║
|
|
79
80
|
╚═══════════════════════════════════════════════════════╝
|
|
@@ -113,10 +114,12 @@ function printNextSteps(projectName, platforms) {
|
|
|
113
114
|
* Run the create project wizard flow
|
|
114
115
|
* This is the default command when no subcommand is specified
|
|
115
116
|
*/
|
|
116
|
-
async function runCreate(
|
|
117
|
+
async function runCreate(args) {
|
|
117
118
|
printWelcome();
|
|
118
119
|
console.log(''); // blank line after banner
|
|
119
|
-
|
|
120
|
+
// Extract project name from CLI args (first non-flag argument)
|
|
121
|
+
const nameArg = args.find(arg => !arg.startsWith('-'));
|
|
122
|
+
const config = await runWizard(nameArg ? { defaultName: nameArg } : undefined);
|
|
120
123
|
if (!config) {
|
|
121
124
|
console.log('\nProject creation cancelled.');
|
|
122
125
|
process.exit(1);
|
|
@@ -137,6 +140,11 @@ async function runCreate(_args) {
|
|
|
137
140
|
await generateProject(config, outputDir);
|
|
138
141
|
// Write config file for downstream commands
|
|
139
142
|
writeConfigFile(outputDir, config);
|
|
143
|
+
// Optional: GitHub repository setup
|
|
144
|
+
const gitResult = await promptGitSetup();
|
|
145
|
+
if (gitResult) {
|
|
146
|
+
await setupGitRepository(outputDir, gitResult.repoUrl, gitResult.pat);
|
|
147
|
+
}
|
|
140
148
|
// Success message and next steps
|
|
141
149
|
console.log('');
|
|
142
150
|
console.log(pc.green('✔') + ` Created ${pc.bold(config.projectName)} successfully!`);
|
|
@@ -10,7 +10,7 @@ import prompts from 'prompts';
|
|
|
10
10
|
import pc from 'picocolors';
|
|
11
11
|
import { execSync } from 'node:child_process';
|
|
12
12
|
import { requireProjectContext } from '../utils/project-context.js';
|
|
13
|
-
import { createCrossAccountIAMClient, createDeploymentUserWithCredentials, } from '../aws/iam.js';
|
|
13
|
+
import { createCrossAccountIAMClient, createDeploymentUserWithCredentials, createAccessKey, } from '../aws/iam.js';
|
|
14
14
|
import { createGitHubClient, setEnvironmentCredentials, parseGitHubUrl, } from '../github/secrets.js';
|
|
15
15
|
const VALID_ENVIRONMENTS = ['dev', 'stage', 'prod'];
|
|
16
16
|
/**
|
|
@@ -283,9 +283,24 @@ export async function runInitializeGitHub(args) {
|
|
|
283
283
|
// Step 1: Create cross-account IAM client
|
|
284
284
|
spinner.text = `Assuming role in ${env} account (${accountId})...`;
|
|
285
285
|
const iamClient = createCrossAccountIAMClient(awsRegion, accountId);
|
|
286
|
-
// Step 2:
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
// Step 2: Get deployment user credentials
|
|
287
|
+
// If setup-aws-envs already created the user, just create an access key.
|
|
288
|
+
// Otherwise fall back to full user creation (backward compat with older projects).
|
|
289
|
+
const existingUserName = config.deploymentUsers?.[env];
|
|
290
|
+
let userName;
|
|
291
|
+
let credentials;
|
|
292
|
+
if (existingUserName) {
|
|
293
|
+
spinner.text = `Using existing deployment user: ${existingUserName}`;
|
|
294
|
+
userName = existingUserName;
|
|
295
|
+
credentials = await createAccessKey(iamClient, userName);
|
|
296
|
+
spinner.succeed(`Created access key for ${userName}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
spinner.text = `Creating IAM deployment user...`;
|
|
300
|
+
const fullCredentials = await createDeploymentUserWithCredentials(iamClient, projectName, env, accountId);
|
|
301
|
+
userName = fullCredentials.userName;
|
|
302
|
+
credentials = fullCredentials;
|
|
303
|
+
}
|
|
289
304
|
// Step 3: Configure GitHub
|
|
290
305
|
const githubEnvName = GITHUB_ENV_NAMES[env];
|
|
291
306
|
spinner.text = `Configuring GitHub Environment "${githubEnvName}"...`;
|
|
@@ -297,8 +312,13 @@ export async function runInitializeGitHub(args) {
|
|
|
297
312
|
console.log(pc.green(`${env} environment setup complete!`));
|
|
298
313
|
console.log('');
|
|
299
314
|
console.log('Resources created:');
|
|
300
|
-
console.log(`
|
|
315
|
+
console.log(` Deployment User: ${pc.cyan(userName)}`);
|
|
301
316
|
console.log(` GitHub Environment: ${pc.cyan(githubEnvName)}`);
|
|
317
|
+
// Add note if using existing user from setup-aws-envs
|
|
318
|
+
if (existingUserName) {
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log(pc.dim('Note: Deployment user was created by setup-aws-envs. Access key created for GitHub.'));
|
|
321
|
+
}
|
|
302
322
|
console.log('');
|
|
303
323
|
console.log('View secrets at:');
|
|
304
324
|
console.log(` ${pc.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/environments`)}`);
|
|
@@ -10,6 +10,7 @@ import pc from 'picocolors';
|
|
|
10
10
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { requireProjectContext } from '../utils/project-context.js';
|
|
12
12
|
import { createOrganizationsClient, checkExistingOrganization, createOrganization, createAccount, waitForAccountCreation, } from '../aws/organizations.js';
|
|
13
|
+
import { createCrossAccountIAMClient, createOrAdoptDeploymentUser, createCDKDeploymentPolicy, attachPolicyToUser, } from '../aws/iam.js';
|
|
13
14
|
/**
|
|
14
15
|
* Environment names for account creation
|
|
15
16
|
*/
|
|
@@ -101,14 +102,18 @@ async function collectEmails(projectName) {
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
|
-
* Updates the config file with account IDs
|
|
105
|
+
* Updates the config file with account IDs and optionally deployment user names
|
|
105
106
|
* @param configPath Path to the config file
|
|
106
107
|
* @param accounts Record of environment to account ID
|
|
108
|
+
* @param deploymentUsers Optional record of environment to deployment user name
|
|
107
109
|
*/
|
|
108
|
-
function
|
|
110
|
+
function updateConfig(configPath, accounts, deploymentUsers) {
|
|
109
111
|
const content = readFileSync(configPath, 'utf-8');
|
|
110
112
|
const config = JSON.parse(content);
|
|
111
113
|
config.accounts = { ...config.accounts, ...accounts };
|
|
114
|
+
if (deploymentUsers) {
|
|
115
|
+
config.deploymentUsers = { ...config.deploymentUsers, ...deploymentUsers };
|
|
116
|
+
}
|
|
112
117
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
113
118
|
}
|
|
114
119
|
/**
|
|
@@ -174,14 +179,16 @@ export async function runSetupAwsEnvs(_args) {
|
|
|
174
179
|
const { config, configPath } = context;
|
|
175
180
|
// 2. Check if already configured (warn but don't abort - allows retry after partial failure)
|
|
176
181
|
const existingAccounts = config.accounts ?? {};
|
|
182
|
+
const existingUsers = config.deploymentUsers ?? {};
|
|
177
183
|
if (Object.keys(existingAccounts).length > 0) {
|
|
178
184
|
console.log('');
|
|
179
185
|
console.log(pc.yellow('Warning:') + ' AWS accounts already configured in this project:');
|
|
180
186
|
for (const [env, id] of Object.entries(existingAccounts)) {
|
|
181
|
-
|
|
187
|
+
const userInfo = existingUsers[env] ? ` (user: ${existingUsers[env]})` : '';
|
|
188
|
+
console.log(` ${env}: ${id}${userInfo}`);
|
|
182
189
|
}
|
|
183
190
|
console.log('');
|
|
184
|
-
console.log(pc.dim('Continuing will create
|
|
191
|
+
console.log(pc.dim('Continuing will skip existing accounts and create any missing ones...'));
|
|
185
192
|
}
|
|
186
193
|
// 3. Collect emails (all input before any AWS calls - per research pitfall #6)
|
|
187
194
|
const emails = await collectEmails(config.projectName);
|
|
@@ -202,8 +209,13 @@ export async function runSetupAwsEnvs(_args) {
|
|
|
202
209
|
spinner.succeed(`Using existing AWS Organization: ${orgId}`);
|
|
203
210
|
}
|
|
204
211
|
// Create accounts sequentially (per research - AWS rate limits require sequential)
|
|
205
|
-
const accounts = {};
|
|
212
|
+
const accounts = { ...existingAccounts };
|
|
206
213
|
for (const env of ENVIRONMENTS) {
|
|
214
|
+
// Skip if account already exists
|
|
215
|
+
if (existingAccounts[env]) {
|
|
216
|
+
spinner.succeed(`Using existing ${env} account: ${existingAccounts[env]}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
207
219
|
spinner.start(`Creating ${env} account (this may take several minutes)...`);
|
|
208
220
|
const accountName = `${config.projectName}-${env}`;
|
|
209
221
|
const { requestId } = await createAccount(client, emails[env], accountName);
|
|
@@ -211,19 +223,42 @@ export async function runSetupAwsEnvs(_args) {
|
|
|
211
223
|
const result = await waitForAccountCreation(client, requestId);
|
|
212
224
|
accounts[env] = result.accountId;
|
|
213
225
|
// Save after EACH successful account (per research pitfall #3 - handle partial success)
|
|
214
|
-
|
|
226
|
+
updateConfig(configPath, accounts);
|
|
215
227
|
spinner.succeed(`Created ${env} account: ${result.accountId}`);
|
|
216
228
|
}
|
|
217
|
-
//
|
|
229
|
+
// Create IAM deployment users in each account
|
|
230
|
+
const deploymentUsers = { ...existingUsers };
|
|
231
|
+
for (const env of ENVIRONMENTS) {
|
|
232
|
+
// Skip if user already exists in config
|
|
233
|
+
if (existingUsers[env]) {
|
|
234
|
+
spinner.succeed(`Using existing deployment user: ${existingUsers[env]}`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const accountId = accounts[env];
|
|
238
|
+
const userName = `${config.projectName}-${env}-deploy`;
|
|
239
|
+
const policyName = `${config.projectName}-${env}-cdk-deploy`;
|
|
240
|
+
spinner.start(`Creating deployment user in ${env} account...`);
|
|
241
|
+
const iamClient = createCrossAccountIAMClient(config.awsRegion, accountId);
|
|
242
|
+
await createOrAdoptDeploymentUser(iamClient, userName);
|
|
243
|
+
spinner.text = `Creating deployment policy for ${env}...`;
|
|
244
|
+
const policyArn = await createCDKDeploymentPolicy(iamClient, policyName, accountId);
|
|
245
|
+
await attachPolicyToUser(iamClient, userName, policyArn);
|
|
246
|
+
deploymentUsers[env] = userName;
|
|
247
|
+
// Save after EACH successful user creation
|
|
248
|
+
updateConfig(configPath, accounts, deploymentUsers);
|
|
249
|
+
spinner.succeed(`Created deployment user: ${userName}`);
|
|
250
|
+
}
|
|
251
|
+
// Final success with summary table
|
|
218
252
|
console.log('');
|
|
219
253
|
console.log(pc.green('AWS environment setup complete!'));
|
|
220
254
|
console.log('');
|
|
221
|
-
console.log('
|
|
222
|
-
|
|
223
|
-
|
|
255
|
+
console.log('Summary:');
|
|
256
|
+
console.log(` ${'Environment'.padEnd(14)}${'Account ID'.padEnd(16)}Deployment User`);
|
|
257
|
+
for (const env of ENVIRONMENTS) {
|
|
258
|
+
console.log(` ${env.padEnd(14)}${accounts[env].padEnd(16)}${deploymentUsers[env]}`);
|
|
224
259
|
}
|
|
225
260
|
console.log('');
|
|
226
|
-
console.log(
|
|
261
|
+
console.log('Deployment users created. Next: initialize-github to create access keys and push to GitHub.');
|
|
227
262
|
console.log(` ${pc.cyan('npx create-aws-project initialize-github dev')}`);
|
|
228
263
|
}
|
|
229
264
|
catch (error) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git setup module
|
|
3
|
+
*
|
|
4
|
+
* Provides optional GitHub repository setup after project generation.
|
|
5
|
+
* Users can push their generated project to GitHub in the same wizard flow.
|
|
6
|
+
* Fully optional - pressing Enter skips all git operations.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Checks if git is available on the system
|
|
10
|
+
* @returns true if git is installed and available, false otherwise
|
|
11
|
+
*/
|
|
12
|
+
export declare function isGitAvailable(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Prompts user for optional GitHub repository setup
|
|
15
|
+
* @returns Repository URL and PAT if user wants git setup, null if skipped
|
|
16
|
+
*/
|
|
17
|
+
export declare function promptGitSetup(): Promise<{
|
|
18
|
+
repoUrl: string;
|
|
19
|
+
pat: string;
|
|
20
|
+
} | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Sets up git repository with initial commit and pushes to GitHub
|
|
23
|
+
* Creates the remote repository if it doesn't exist
|
|
24
|
+
* @param projectDir - Path to the project directory
|
|
25
|
+
* @param repoUrl - GitHub repository URL
|
|
26
|
+
* @param pat - GitHub Personal Access Token
|
|
27
|
+
*/
|
|
28
|
+
export declare function setupGitRepository(projectDir: string, repoUrl: string, pat: string): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { parseGitHubUrl, createGitHubClient } from '../github/secrets.js';
|
|
6
|
+
/**
|
|
7
|
+
* Git setup module
|
|
8
|
+
*
|
|
9
|
+
* Provides optional GitHub repository setup after project generation.
|
|
10
|
+
* Users can push their generated project to GitHub in the same wizard flow.
|
|
11
|
+
* Fully optional - pressing Enter skips all git operations.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Checks if git is available on the system
|
|
15
|
+
* @returns true if git is installed and available, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export function isGitAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
execSync('git --version', { stdio: 'pipe' });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Prompts user for optional GitHub repository setup
|
|
28
|
+
* @returns Repository URL and PAT if user wants git setup, null if skipped
|
|
29
|
+
*/
|
|
30
|
+
export async function promptGitSetup() {
|
|
31
|
+
// Skip if git is not available
|
|
32
|
+
if (!isGitAvailable()) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(pc.bold('GitHub Repository (optional)'));
|
|
37
|
+
console.log(pc.dim('Push your project to a GitHub repository. Press Enter to skip.'));
|
|
38
|
+
console.log('');
|
|
39
|
+
// Prompt for repo URL
|
|
40
|
+
const repoResponse = await prompts({
|
|
41
|
+
type: 'text',
|
|
42
|
+
name: 'repoUrl',
|
|
43
|
+
message: 'GitHub repository URL:',
|
|
44
|
+
});
|
|
45
|
+
const repoUrl = repoResponse.repoUrl?.trim();
|
|
46
|
+
if (!repoUrl) {
|
|
47
|
+
return null; // User skipped
|
|
48
|
+
}
|
|
49
|
+
// Validate the URL
|
|
50
|
+
try {
|
|
51
|
+
parseGitHubUrl(repoUrl);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
console.log(pc.red('Error: ') + error.message);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Prompt for PAT
|
|
60
|
+
const patResponse = await prompts({
|
|
61
|
+
type: 'password',
|
|
62
|
+
name: 'pat',
|
|
63
|
+
message: 'GitHub Personal Access Token:',
|
|
64
|
+
validate: (value) => {
|
|
65
|
+
if (!value.trim()) {
|
|
66
|
+
return 'Token is required';
|
|
67
|
+
}
|
|
68
|
+
// GitHub tokens start with ghp_ (classic) or github_pat_ (fine-grained)
|
|
69
|
+
if (!value.startsWith('ghp_') && !value.startsWith('github_pat_')) {
|
|
70
|
+
return 'Invalid token format. Expected ghp_ or github_pat_ prefix.';
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
}, {
|
|
75
|
+
onCancel: () => {
|
|
76
|
+
// User cancelled PAT prompt
|
|
77
|
+
return;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!patResponse.pat) {
|
|
81
|
+
return null; // User cancelled
|
|
82
|
+
}
|
|
83
|
+
return { repoUrl, pat: patResponse.pat };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sets up git repository with initial commit and pushes to GitHub
|
|
87
|
+
* Creates the remote repository if it doesn't exist
|
|
88
|
+
* @param projectDir - Path to the project directory
|
|
89
|
+
* @param repoUrl - GitHub repository URL
|
|
90
|
+
* @param pat - GitHub Personal Access Token
|
|
91
|
+
*/
|
|
92
|
+
export async function setupGitRepository(projectDir, repoUrl, pat) {
|
|
93
|
+
const spinner = ora();
|
|
94
|
+
try {
|
|
95
|
+
// Parse the URL
|
|
96
|
+
const { owner, repo } = parseGitHubUrl(repoUrl);
|
|
97
|
+
// Create Octokit client
|
|
98
|
+
const octokit = createGitHubClient(pat);
|
|
99
|
+
// Git init
|
|
100
|
+
spinner.start('Initializing git repository...');
|
|
101
|
+
execSync('git init -b main', { cwd: projectDir, stdio: 'pipe' });
|
|
102
|
+
// Check for git user config, set if not configured
|
|
103
|
+
try {
|
|
104
|
+
execSync('git config user.name', { cwd: projectDir, stdio: 'pipe' });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// User config not set, use defaults
|
|
108
|
+
execSync('git config user.name "create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
|
|
109
|
+
execSync('git config user.email "noreply@create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
|
|
110
|
+
}
|
|
111
|
+
execSync('git add .', { cwd: projectDir, stdio: 'pipe' });
|
|
112
|
+
execSync('git commit -m "Initial commit from create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
|
|
113
|
+
spinner.succeed('Git repository initialized');
|
|
114
|
+
// Ensure remote repo exists
|
|
115
|
+
spinner.start('Checking GitHub repository...');
|
|
116
|
+
try {
|
|
117
|
+
await octokit.rest.repos.get({ owner, repo });
|
|
118
|
+
spinner.succeed(`Repository ${owner}/${repo} found`);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error.status === 404) {
|
|
122
|
+
// Repo doesn't exist, create it
|
|
123
|
+
const { data: user } = await octokit.rest.users.getAuthenticated();
|
|
124
|
+
if (owner === user.login) {
|
|
125
|
+
// Create in user's account
|
|
126
|
+
await octokit.rest.repos.createForAuthenticatedUser({
|
|
127
|
+
name: repo,
|
|
128
|
+
private: true,
|
|
129
|
+
auto_init: false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Create in organization
|
|
134
|
+
await octokit.rest.repos.createInOrg({
|
|
135
|
+
org: owner,
|
|
136
|
+
name: repo,
|
|
137
|
+
private: true,
|
|
138
|
+
auto_init: false,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
spinner.succeed(`Created repository ${owner}/${repo}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Push to remote
|
|
148
|
+
spinner.start('Pushing to GitHub...');
|
|
149
|
+
const authUrl = `https://${pat}@github.com/${owner}/${repo}.git`;
|
|
150
|
+
execSync(`git remote add origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
151
|
+
execSync('git push -u origin main', { cwd: projectDir, stdio: 'pipe' });
|
|
152
|
+
// CRITICAL: Remove PAT from .git/config
|
|
153
|
+
const cleanUrl = `https://github.com/${owner}/${repo}.git`;
|
|
154
|
+
execSync(`git remote set-url origin ${cleanUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
155
|
+
spinner.succeed(`Pushed to ${owner}/${repo}`);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
// Git setup failure should not prevent the user from using their project
|
|
159
|
+
if (spinner.isSpinning) {
|
|
160
|
+
spinner.fail();
|
|
161
|
+
}
|
|
162
|
+
console.log(pc.yellow('Warning:') + ' Git setup failed: ' + (error instanceof Error ? error.message : 'Unknown error'));
|
|
163
|
+
console.log(pc.dim('Your project was created successfully. You can set up git manually.'));
|
|
164
|
+
}
|
|
165
|
+
}
|
package/dist/wizard.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { ProjectConfig } from './types.js';
|
|
2
|
-
export
|
|
2
|
+
export interface WizardOptions {
|
|
3
|
+
defaultName?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function runWizard(options?: WizardOptions): Promise<ProjectConfig | null>;
|
package/dist/wizard.js
CHANGED
|
@@ -6,9 +6,13 @@ import { authProviderPrompt, authFeaturesPrompt } from './prompts/auth.js';
|
|
|
6
6
|
import { featuresPrompt } from './prompts/features.js';
|
|
7
7
|
import { awsRegionPrompt } from './prompts/aws-config.js';
|
|
8
8
|
import { themePrompt } from './prompts/theme.js';
|
|
9
|
-
export async function runWizard() {
|
|
9
|
+
export async function runWizard(options = {}) {
|
|
10
|
+
// Create name prompt with optional default override
|
|
11
|
+
const namePrompt = options.defaultName
|
|
12
|
+
? { ...projectNamePrompt, initial: options.defaultName }
|
|
13
|
+
: projectNamePrompt;
|
|
10
14
|
const response = await prompts([
|
|
11
|
-
|
|
15
|
+
namePrompt,
|
|
12
16
|
platformsPrompt,
|
|
13
17
|
authProviderPrompt,
|
|
14
18
|
authFeaturesPrompt,
|
package/package.json
CHANGED
|
@@ -3,7 +3,6 @@ import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
|
3
3
|
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
|
4
4
|
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
|
5
5
|
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
6
|
-
import * as opensearchserverless from 'aws-cdk-lib/aws-opensearchserverless';
|
|
7
6
|
import { Construct } from 'constructs';
|
|
8
7
|
|
|
9
8
|
export interface StaticStackProps extends cdk.StackProps {
|
|
@@ -24,7 +23,6 @@ export class StaticStack extends cdk.Stack {
|
|
|
24
23
|
public readonly bucket: s3.Bucket;
|
|
25
24
|
public readonly api: apigateway.RestApi;
|
|
26
25
|
public readonly distribution: cloudfront.Distribution;
|
|
27
|
-
public readonly openSearchCollection: opensearchserverless.CfnCollection;
|
|
28
26
|
|
|
29
27
|
constructor(scope: Construct, id: string, props: StaticStackProps) {
|
|
30
28
|
super(scope, id, props);
|
|
@@ -205,94 +203,6 @@ export class StaticStack extends cdk.Stack {
|
|
|
205
203
|
],
|
|
206
204
|
});
|
|
207
205
|
|
|
208
|
-
// OpenSearch Serverless Collection
|
|
209
|
-
const collectionName = `{{PROJECT_NAME}}-${environmentName}`;
|
|
210
|
-
|
|
211
|
-
// Encryption policy (required for all collections)
|
|
212
|
-
const encryptionPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OpenSearchEncryptionPolicy', {
|
|
213
|
-
name: `${collectionName}-encryption`,
|
|
214
|
-
type: 'encryption',
|
|
215
|
-
policy: JSON.stringify({
|
|
216
|
-
Rules: [
|
|
217
|
-
{
|
|
218
|
-
ResourceType: 'collection',
|
|
219
|
-
Resource: [`collection/${collectionName}`],
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
AWSOwnedKey: true,
|
|
223
|
-
}),
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// Network policy - allows public access (adjust for production)
|
|
227
|
-
const networkPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OpenSearchNetworkPolicy', {
|
|
228
|
-
name: `${collectionName}-network`,
|
|
229
|
-
type: 'network',
|
|
230
|
-
policy: JSON.stringify([
|
|
231
|
-
{
|
|
232
|
-
Rules: [
|
|
233
|
-
{
|
|
234
|
-
ResourceType: 'collection',
|
|
235
|
-
Resource: [`collection/${collectionName}`],
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
ResourceType: 'dashboard',
|
|
239
|
-
Resource: [`collection/${collectionName}`],
|
|
240
|
-
},
|
|
241
|
-
],
|
|
242
|
-
AllowFromPublic: true,
|
|
243
|
-
},
|
|
244
|
-
]),
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Data access policy - grants access to the collection
|
|
248
|
-
const dataAccessPolicy = new opensearchserverless.CfnAccessPolicy(this, 'OpenSearchDataAccessPolicy', {
|
|
249
|
-
name: `${collectionName}-access`,
|
|
250
|
-
type: 'data',
|
|
251
|
-
policy: JSON.stringify([
|
|
252
|
-
{
|
|
253
|
-
Rules: [
|
|
254
|
-
{
|
|
255
|
-
ResourceType: 'collection',
|
|
256
|
-
Resource: [`collection/${collectionName}`],
|
|
257
|
-
Permission: [
|
|
258
|
-
'aoss:CreateCollectionItems',
|
|
259
|
-
'aoss:DeleteCollectionItems',
|
|
260
|
-
'aoss:UpdateCollectionItems',
|
|
261
|
-
'aoss:DescribeCollectionItems',
|
|
262
|
-
],
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
ResourceType: 'index',
|
|
266
|
-
Resource: [`index/${collectionName}/*`],
|
|
267
|
-
Permission: [
|
|
268
|
-
'aoss:CreateIndex',
|
|
269
|
-
'aoss:DeleteIndex',
|
|
270
|
-
'aoss:UpdateIndex',
|
|
271
|
-
'aoss:DescribeIndex',
|
|
272
|
-
'aoss:ReadDocument',
|
|
273
|
-
'aoss:WriteDocument',
|
|
274
|
-
],
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
Principal: [
|
|
278
|
-
`arn:aws:iam::${this.account}:root`,
|
|
279
|
-
],
|
|
280
|
-
},
|
|
281
|
-
]),
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Create the OpenSearch Serverless collection
|
|
285
|
-
this.openSearchCollection = new opensearchserverless.CfnCollection(this, 'OpenSearchCollection', {
|
|
286
|
-
name: collectionName,
|
|
287
|
-
type: 'SEARCH', // SEARCH, TIMESERIES, or VECTORSEARCH
|
|
288
|
-
description: `OpenSearch Serverless collection for {{PROJECT_NAME_TITLE}} - ${environmentName}`,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// Ensure policies are created before the collection
|
|
292
|
-
this.openSearchCollection.addDependency(encryptionPolicy);
|
|
293
|
-
this.openSearchCollection.addDependency(networkPolicy);
|
|
294
|
-
this.openSearchCollection.addDependency(dataAccessPolicy);
|
|
295
|
-
|
|
296
206
|
// Output important values
|
|
297
207
|
new cdk.CfnOutput(this, 'BucketName', {
|
|
298
208
|
value: this.bucket.bucketName,
|
|
@@ -339,23 +249,5 @@ export class StaticStack extends cdk.Stack {
|
|
|
339
249
|
value: `https://${this.distribution.distributionDomainName}/api`,
|
|
340
250
|
description: 'API URL via CloudFront',
|
|
341
251
|
});
|
|
342
|
-
|
|
343
|
-
new cdk.CfnOutput(this, 'OpenSearchCollectionEndpoint', {
|
|
344
|
-
value: this.openSearchCollection.attrCollectionEndpoint,
|
|
345
|
-
description: 'OpenSearch Serverless collection endpoint',
|
|
346
|
-
exportName: `${environmentName}-opensearch-endpoint`,
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
new cdk.CfnOutput(this, 'OpenSearchDashboardEndpoint', {
|
|
350
|
-
value: this.openSearchCollection.attrDashboardEndpoint,
|
|
351
|
-
description: 'OpenSearch Serverless dashboard endpoint',
|
|
352
|
-
exportName: `${environmentName}-opensearch-dashboard`,
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
new cdk.CfnOutput(this, 'OpenSearchCollectionArn', {
|
|
356
|
-
value: this.openSearchCollection.attrArn,
|
|
357
|
-
description: 'OpenSearch Serverless collection ARN',
|
|
358
|
-
exportName: `${environmentName}-opensearch-arn`,
|
|
359
|
-
});
|
|
360
252
|
}
|
|
361
253
|
}
|
package/templates/root/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Push to main to trigger your first deployment:
|
|
|
109
109
|
git push origin main
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
For detailed setup instructions and troubleshooting, see the [create-aws-project documentation](https://www.npmjs.com/package/create-aws-
|
|
112
|
+
For detailed setup instructions and troubleshooting, see the [create-aws-project documentation](https://www.npmjs.com/package/create-aws-project).
|
|
113
113
|
|
|
114
114
|
## CI/CD Workflows
|
|
115
115
|
|