create-aws-project 1.2.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 +118 -0
- package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
- package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
- package/dist/__tests__/generator.spec.d.ts +1 -0
- package/dist/__tests__/generator.spec.js +162 -0
- package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
- package/dist/__tests__/validation/project-name.spec.js +57 -0
- package/dist/__tests__/wizard.spec.d.ts +1 -0
- package/dist/__tests__/wizard.spec.js +232 -0
- package/dist/aws/iam.d.ts +75 -0
- package/dist/aws/iam.js +264 -0
- package/dist/aws/organizations.d.ts +79 -0
- package/dist/aws/organizations.js +168 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +206 -0
- package/dist/commands/setup-github.d.ts +4 -0
- package/dist/commands/setup-github.js +185 -0
- package/dist/generator/copy-file.d.ts +15 -0
- package/dist/generator/copy-file.js +56 -0
- package/dist/generator/generate-project.d.ts +14 -0
- package/dist/generator/generate-project.js +81 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +3 -0
- package/dist/generator/replace-tokens.d.ts +29 -0
- package/dist/generator/replace-tokens.js +68 -0
- package/dist/github/secrets.d.ts +109 -0
- package/dist/github/secrets.js +275 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/prompts/auth.d.ts +3 -0
- package/dist/prompts/auth.js +23 -0
- package/dist/prompts/aws-config.d.ts +2 -0
- package/dist/prompts/aws-config.js +14 -0
- package/dist/prompts/features.d.ts +2 -0
- package/dist/prompts/features.js +10 -0
- package/dist/prompts/github-setup.d.ts +53 -0
- package/dist/prompts/github-setup.js +208 -0
- package/dist/prompts/org-structure.d.ts +9 -0
- package/dist/prompts/org-structure.js +93 -0
- package/dist/prompts/platforms.d.ts +2 -0
- package/dist/prompts/platforms.js +12 -0
- package/dist/prompts/project-name.d.ts +2 -0
- package/dist/prompts/project-name.js +8 -0
- package/dist/prompts/theme.d.ts +2 -0
- package/dist/prompts/theme.js +14 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.js +2 -0
- package/dist/templates/manifest.d.ts +11 -0
- package/dist/templates/manifest.js +99 -0
- package/dist/templates/tokens.d.ts +39 -0
- package/dist/templates/tokens.js +37 -0
- package/dist/templates/types.d.ts +52 -0
- package/dist/templates/types.js +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/dist/validation/project-name.d.ts +1 -0
- package/dist/validation/project-name.js +12 -0
- package/dist/wizard.d.ts +2 -0
- package/dist/wizard.js +81 -0
- package/package.json +68 -0
- package/templates/.github/actions/build-and-test/action.yml +24 -0
- package/templates/.github/actions/deploy-cdk/action.yml +46 -0
- package/templates/.github/actions/deploy-web/action.yml +72 -0
- package/templates/.github/actions/setup/action.yml +29 -0
- package/templates/.github/pull_request_template.md +15 -0
- package/templates/.github/workflows/deploy-dev.yml +80 -0
- package/templates/.github/workflows/deploy-prod.yml +67 -0
- package/templates/.github/workflows/deploy-stage.yml +77 -0
- package/templates/.github/workflows/pull-request.yml +72 -0
- package/templates/.vscode/extensions.json +7 -0
- package/templates/.vscode/settings.json +67 -0
- package/templates/apps/api/.eslintrc.json +18 -0
- package/templates/apps/api/cdk/app.ts +93 -0
- package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
- package/templates/apps/api/cdk/cdk.json +73 -0
- package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
- package/templates/apps/api/cdk/org-stack.ts +67 -0
- package/templates/apps/api/cdk/static-stack.ts +361 -0
- package/templates/apps/api/cdk/tsconfig.json +39 -0
- package/templates/apps/api/cdk/user-stack.ts +255 -0
- package/templates/apps/api/jest.config.ts +38 -0
- package/templates/apps/api/lambdas.yml +84 -0
- package/templates/apps/api/project.json.template +58 -0
- package/templates/apps/api/src/__tests__/setup.ts +10 -0
- package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
- package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
- package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
- package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
- package/templates/apps/api/src/handlers/users/index.ts +17 -0
- package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
- package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
- package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
- package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
- package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
- package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
- package/templates/apps/api/src/models/UserModel.ts +109 -0
- package/templates/apps/api/src/schemas/user.schema.ts +44 -0
- package/templates/apps/api/src/services/user-service.ts +108 -0
- package/templates/apps/api/src/utils/auth-context.ts +60 -0
- package/templates/apps/api/src/utils/common/helpers.ts +26 -0
- package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
- package/templates/apps/api/src/utils/response.ts +52 -0
- package/templates/apps/api/src/utils/validator.ts +75 -0
- package/templates/apps/api/tsconfig.app.json +15 -0
- package/templates/apps/api/tsconfig.json +19 -0
- package/templates/apps/api/tsconfig.spec.json +17 -0
- package/templates/apps/mobile/.env.example +5 -0
- package/templates/apps/mobile/.eslintrc.json +33 -0
- package/templates/apps/mobile/app.json +33 -0
- package/templates/apps/mobile/assets/.gitkeep +0 -0
- package/templates/apps/mobile/babel.config.js +19 -0
- package/templates/apps/mobile/index.js +7 -0
- package/templates/apps/mobile/jest.config.ts +22 -0
- package/templates/apps/mobile/metro.config.js +35 -0
- package/templates/apps/mobile/package.json +22 -0
- package/templates/apps/mobile/project.json.template +64 -0
- package/templates/apps/mobile/src/App.tsx +367 -0
- package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
- package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
- package/templates/apps/mobile/src/config/api.ts +16 -0
- package/templates/apps/mobile/src/store/user-store.ts +56 -0
- package/templates/apps/mobile/src/test-setup.ts +10 -0
- package/templates/apps/mobile/tsconfig.json +22 -0
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/.eslintrc.json +26 -0
- package/templates/apps/web/index.html +13 -0
- package/templates/apps/web/jest.config.ts +24 -0
- package/templates/apps/web/package.json +15 -0
- package/templates/apps/web/project.json.template +66 -0
- package/templates/apps/web/src/App.tsx +352 -0
- package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
- package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
- package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
- package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
- package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
- package/templates/apps/web/src/auth/index.ts +7 -0
- package/templates/apps/web/src/auth/use-auth.ts +16 -0
- package/templates/apps/web/src/config/amplify-config.ts +31 -0
- package/templates/apps/web/src/config/api.ts +38 -0
- package/templates/apps/web/src/config/auth0-config.ts +17 -0
- package/templates/apps/web/src/main.tsx +41 -0
- package/templates/apps/web/src/store/user-store.ts +56 -0
- package/templates/apps/web/src/styles.css +165 -0
- package/templates/apps/web/src/test-setup.ts +1 -0
- package/templates/apps/web/src/theme/index.ts +30 -0
- package/templates/apps/web/src/vite-env.d.ts +19 -0
- package/templates/apps/web/tsconfig.app.json +24 -0
- package/templates/apps/web/tsconfig.json +22 -0
- package/templates/apps/web/tsconfig.spec.json +28 -0
- package/templates/apps/web/vite.config.ts +87 -0
- package/templates/manifest.json +28 -0
- package/templates/packages/api-client/.eslintrc.json +18 -0
- package/templates/packages/api-client/jest.config.ts +13 -0
- package/templates/packages/api-client/package.json +8 -0
- package/templates/packages/api-client/project.json.template +34 -0
- package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
- package/templates/packages/api-client/src/api-client.ts +201 -0
- package/templates/packages/api-client/src/config.ts +193 -0
- package/templates/packages/api-client/src/index.ts +9 -0
- package/templates/packages/api-client/tsconfig.json +22 -0
- package/templates/packages/api-client/tsconfig.lib.json +11 -0
- package/templates/packages/api-client/tsconfig.spec.json +14 -0
- package/templates/packages/common-types/.eslintrc.json +18 -0
- package/templates/packages/common-types/package.json +6 -0
- package/templates/packages/common-types/project.json.template +26 -0
- package/templates/packages/common-types/src/api.types.ts +24 -0
- package/templates/packages/common-types/src/auth.types.ts +36 -0
- package/templates/packages/common-types/src/common.types.ts +46 -0
- package/templates/packages/common-types/src/index.ts +19 -0
- package/templates/packages/common-types/src/lambda.types.ts +39 -0
- package/templates/packages/common-types/src/user.types.ts +31 -0
- package/templates/packages/common-types/tsconfig.json +19 -0
- package/templates/packages/common-types/tsconfig.lib.json +11 -0
- package/templates/root/.editorconfig +23 -0
- package/templates/root/.nvmrc +1 -0
- package/templates/root/eslint.config.js +61 -0
- package/templates/root/jest.preset.js +16 -0
- package/templates/root/nx.json +29 -0
- package/templates/root/package.json +131 -0
- package/templates/root/tsconfig.base.json +29 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { OrganizationsClient, CreateOrganizationCommand, CreateAccountCommand, DescribeCreateAccountStatusCommand, DescribeOrganizationCommand, AlreadyInOrganizationException, } from '@aws-sdk/client-organizations';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
/**
|
|
4
|
+
* AWS Organizations service module
|
|
5
|
+
*
|
|
6
|
+
* Provides functions to create AWS Organizations and member accounts
|
|
7
|
+
* programmatically during project generation.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Creates a configured OrganizationsClient
|
|
11
|
+
* @param region - AWS region (defaults to us-east-1 as Organizations API is global)
|
|
12
|
+
* @returns Configured OrganizationsClient instance
|
|
13
|
+
*/
|
|
14
|
+
export function createOrganizationsClient(region = 'us-east-1') {
|
|
15
|
+
return new OrganizationsClient({ region });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the current AWS account is already part of an organization
|
|
19
|
+
* @param client - OrganizationsClient instance
|
|
20
|
+
* @returns Organization ID if exists, null if not in an organization
|
|
21
|
+
*/
|
|
22
|
+
export async function checkExistingOrganization(client) {
|
|
23
|
+
try {
|
|
24
|
+
const command = new DescribeOrganizationCommand({});
|
|
25
|
+
const response = await client.send(command);
|
|
26
|
+
return response.Organization?.Id ?? null;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// AWSOrganizationsNotInUseException means no organization exists
|
|
30
|
+
if (error instanceof Error && error.name === 'AWSOrganizationsNotInUseException') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new AWS Organization with all features enabled
|
|
38
|
+
* @param client - OrganizationsClient instance
|
|
39
|
+
* @returns Organization ID of the newly created organization
|
|
40
|
+
* @throws Error if organization creation fails (except for already existing)
|
|
41
|
+
*/
|
|
42
|
+
export async function createOrganization(client) {
|
|
43
|
+
try {
|
|
44
|
+
const command = new CreateOrganizationCommand({
|
|
45
|
+
FeatureSet: 'ALL',
|
|
46
|
+
});
|
|
47
|
+
const response = await client.send(command);
|
|
48
|
+
if (!response.Organization?.Id) {
|
|
49
|
+
throw new Error('Organization created but no ID returned');
|
|
50
|
+
}
|
|
51
|
+
return response.Organization.Id;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// If already in an organization, return the existing org ID
|
|
55
|
+
if (error instanceof AlreadyInOrganizationException) {
|
|
56
|
+
const existingOrgId = await checkExistingOrganization(client);
|
|
57
|
+
if (existingOrgId) {
|
|
58
|
+
return existingOrgId;
|
|
59
|
+
}
|
|
60
|
+
throw new Error('Already in organization but could not retrieve organization ID');
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new AWS account within the organization
|
|
67
|
+
* @param client - OrganizationsClient instance
|
|
68
|
+
* @param email - Email address for the new account's root user
|
|
69
|
+
* @param accountName - Display name for the account
|
|
70
|
+
* @returns CreateAccountStatus ID for polling
|
|
71
|
+
*/
|
|
72
|
+
export async function createAccount(client, email, accountName) {
|
|
73
|
+
const command = new CreateAccountCommand({
|
|
74
|
+
Email: email,
|
|
75
|
+
AccountName: accountName,
|
|
76
|
+
});
|
|
77
|
+
const response = await client.send(command);
|
|
78
|
+
if (!response.CreateAccountStatus?.Id) {
|
|
79
|
+
throw new Error('Account creation initiated but no request ID returned');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
requestId: response.CreateAccountStatus.Id,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Polls the account creation status until completion or timeout
|
|
87
|
+
* @param client - OrganizationsClient instance
|
|
88
|
+
* @param createAccountRequestId - The request ID from createAccount
|
|
89
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 5 minutes)
|
|
90
|
+
* @returns Account ID when creation succeeds
|
|
91
|
+
* @throws Error if creation fails or times out
|
|
92
|
+
*/
|
|
93
|
+
export async function waitForAccountCreation(client, createAccountRequestId, timeoutMs = 300000) {
|
|
94
|
+
const pollIntervalMs = 5000; // 5 seconds
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
97
|
+
const command = new DescribeCreateAccountStatusCommand({
|
|
98
|
+
CreateAccountRequestId: createAccountRequestId,
|
|
99
|
+
});
|
|
100
|
+
const response = await client.send(command);
|
|
101
|
+
const status = response.CreateAccountStatus;
|
|
102
|
+
if (!status) {
|
|
103
|
+
throw new Error('No status returned for account creation request');
|
|
104
|
+
}
|
|
105
|
+
// Use State field (not deprecated Status field)
|
|
106
|
+
const state = status.State;
|
|
107
|
+
if (state === 'SUCCEEDED') {
|
|
108
|
+
if (!status.AccountId) {
|
|
109
|
+
throw new Error('Account creation succeeded but no account ID returned');
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
accountId: status.AccountId,
|
|
113
|
+
accountName: status.AccountName ?? '',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (state === 'FAILED') {
|
|
117
|
+
const reason = status.FailureReason ?? 'Unknown failure reason';
|
|
118
|
+
throw new Error(`Account creation failed: ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
// State is IN_PROGRESS, continue polling
|
|
121
|
+
await sleep(pollIntervalMs);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Account creation timed out after ${timeoutMs / 1000} seconds`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Creates multiple environment accounts sequentially
|
|
127
|
+
* @param client - OrganizationsClient instance
|
|
128
|
+
* @param orgName - Organization name prefix for account names
|
|
129
|
+
* @param accounts - Array of environment and email configurations
|
|
130
|
+
* @returns Array of created account results
|
|
131
|
+
*/
|
|
132
|
+
export async function createEnvironmentAccounts(client, orgName, accounts) {
|
|
133
|
+
const results = [];
|
|
134
|
+
console.log(pc.cyan('\nCreating AWS environment accounts...'));
|
|
135
|
+
console.log(pc.dim('Note: AWS rate limits require sequential account creation\n'));
|
|
136
|
+
for (const account of accounts) {
|
|
137
|
+
const accountName = `${orgName}-${account.environment}`;
|
|
138
|
+
console.log(pc.yellow(`Creating account: ${accountName}`));
|
|
139
|
+
console.log(pc.dim(` Email: ${account.email}`));
|
|
140
|
+
try {
|
|
141
|
+
// Initiate account creation
|
|
142
|
+
const { requestId } = await createAccount(client, account.email, accountName);
|
|
143
|
+
console.log(pc.dim(` Request ID: ${requestId}`));
|
|
144
|
+
console.log(pc.dim(' Waiting for account creation (this may take a few minutes)...'));
|
|
145
|
+
// Wait for completion
|
|
146
|
+
const result = await waitForAccountCreation(client, requestId);
|
|
147
|
+
console.log(pc.green(` Account created: ${result.accountId}`));
|
|
148
|
+
results.push({
|
|
149
|
+
environment: account.environment,
|
|
150
|
+
email: account.email,
|
|
151
|
+
accountId: result.accountId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
156
|
+
console.log(pc.red(` Failed to create account: ${message}`));
|
|
157
|
+
throw new Error(`Failed to create ${account.environment} account: ${message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
console.log(pc.green(`\nSuccessfully created ${results.length} environment accounts`));
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Helper function to sleep for a specified duration
|
|
165
|
+
*/
|
|
166
|
+
function sleep(ms) {
|
|
167
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { runWizard } from './wizard.js';
|
|
6
|
+
import { generateProject } from './generator/index.js';
|
|
7
|
+
import { createOrganizationsClient, checkExistingOrganization, createOrganization, createEnvironmentAccounts, } from './aws/organizations.js';
|
|
8
|
+
import { runSetupGitHub } from './commands/setup-github.js';
|
|
9
|
+
/**
|
|
10
|
+
* Get the version from package.json
|
|
11
|
+
*/
|
|
12
|
+
function getVersion() {
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
17
|
+
return packageJson.version;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Print help message
|
|
21
|
+
*/
|
|
22
|
+
function printHelp() {
|
|
23
|
+
console.log(`
|
|
24
|
+
create-aws-starter-kit [command] [options]
|
|
25
|
+
|
|
26
|
+
Scaffold a new AWS Starter Kit project with React, Lambda, and CDK infrastructure.
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
(default) Create a new project (interactive wizard)
|
|
30
|
+
setup-github Configure GitHub Actions deployment credentials
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--help, -h Show this help message
|
|
34
|
+
--version, -v Show version number
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
create-aws-starter-kit [project-name] Create a new project
|
|
38
|
+
create-aws-starter-kit setup-github Configure GitHub deployment
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
create-aws-starter-kit my-app
|
|
42
|
+
create-aws-starter-kit setup-github
|
|
43
|
+
create-aws-starter-kit --help
|
|
44
|
+
`.trim());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Print welcome banner
|
|
48
|
+
*/
|
|
49
|
+
function printWelcome() {
|
|
50
|
+
console.log(`
|
|
51
|
+
╔═══════════════════════════════════════════════════════╗
|
|
52
|
+
║ ║
|
|
53
|
+
║ create-aws-starter-kit ║
|
|
54
|
+
║ AWS Starter Kit Project Generator ║
|
|
55
|
+
║ ║
|
|
56
|
+
╚═══════════════════════════════════════════════════════╝
|
|
57
|
+
`.trim());
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Print post-generation instructions
|
|
61
|
+
*/
|
|
62
|
+
function printNextSteps(projectName, platforms) {
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(pc.bold('Next steps:'));
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(` ${pc.cyan('cd')} ${projectName}`);
|
|
67
|
+
console.log(` ${pc.cyan('npm install')}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
if (platforms.includes('web')) {
|
|
70
|
+
console.log(` ${pc.gray('# Start web app')}`);
|
|
71
|
+
console.log(` ${pc.cyan('npm run web')}`);
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
if (platforms.includes('mobile')) {
|
|
75
|
+
console.log(` ${pc.gray('# Start mobile app')}`);
|
|
76
|
+
console.log(` ${pc.cyan('npm run mobile')}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|
|
79
|
+
if (platforms.includes('api')) {
|
|
80
|
+
console.log(` ${pc.gray('# Deploy API')}`);
|
|
81
|
+
console.log(` ${pc.cyan('npm run cdk:deploy')}`);
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
console.log(pc.gray('Happy coding!'));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse command line arguments and run the CLI
|
|
88
|
+
*/
|
|
89
|
+
export async function run() {
|
|
90
|
+
const args = process.argv.slice(2);
|
|
91
|
+
// Check for --help or -h
|
|
92
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
93
|
+
printHelp();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
// Check for --version or -v
|
|
97
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
98
|
+
console.log(getVersion());
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
// Check for subcommands (first non-flag argument)
|
|
102
|
+
const command = args.find((arg) => !arg.startsWith('-'));
|
|
103
|
+
// Handle setup-github command
|
|
104
|
+
if (command === 'setup-github') {
|
|
105
|
+
await runSetupGitHub();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
// Default: run interactive wizard
|
|
109
|
+
printWelcome();
|
|
110
|
+
console.log(''); // blank line after banner
|
|
111
|
+
const config = await runWizard();
|
|
112
|
+
if (!config) {
|
|
113
|
+
console.log('\nProject creation cancelled.');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
// Set up AWS Organizations if enabled
|
|
117
|
+
if (config.org?.enabled) {
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(pc.cyan('Setting up AWS Organizations...'));
|
|
120
|
+
try {
|
|
121
|
+
const orgClient = createOrganizationsClient(config.awsRegion);
|
|
122
|
+
// Check if already in an organization
|
|
123
|
+
const existingOrgId = await checkExistingOrganization(orgClient);
|
|
124
|
+
if (existingOrgId) {
|
|
125
|
+
console.log(pc.yellow(`Already in organization: ${existingOrgId}`));
|
|
126
|
+
console.log(pc.dim('Proceeding with existing organization...'));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Create new organization
|
|
130
|
+
console.log(pc.dim('Creating new AWS Organization...'));
|
|
131
|
+
const orgId = await createOrganization(orgClient);
|
|
132
|
+
console.log(pc.green('✔') + ` Organization created: ${orgId}`);
|
|
133
|
+
}
|
|
134
|
+
// Create environment accounts
|
|
135
|
+
const accountResults = await createEnvironmentAccounts(orgClient, config.org.organizationName, config.org.accounts);
|
|
136
|
+
// Update config.org.accounts with returned accountIds
|
|
137
|
+
for (const result of accountResults) {
|
|
138
|
+
const account = config.org.accounts.find((a) => a.environment === result.environment);
|
|
139
|
+
if (account) {
|
|
140
|
+
account.accountId = result.accountId;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(pc.green('✔') + ' AWS Organizations setup complete');
|
|
145
|
+
console.log(pc.dim('Account IDs:'));
|
|
146
|
+
for (const account of config.org.accounts) {
|
|
147
|
+
console.log(pc.dim(` ${account.environment}: ${account.accountId}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.log('');
|
|
152
|
+
// Handle specific AWS errors
|
|
153
|
+
if (error instanceof Error) {
|
|
154
|
+
if (error.name === 'CredentialsProviderError' ||
|
|
155
|
+
error.message.includes('Could not load credentials')) {
|
|
156
|
+
console.log(pc.red('Error:') + ' AWS credentials not configured.');
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log('Please configure AWS credentials using one of these methods:');
|
|
159
|
+
console.log(' 1. Run: ' + pc.cyan('aws configure'));
|
|
160
|
+
console.log(' 2. Set environment variables: ' + pc.cyan('AWS_ACCESS_KEY_ID') + ' and ' + pc.cyan('AWS_SECRET_ACCESS_KEY'));
|
|
161
|
+
console.log(' 3. Use an AWS profile: ' + pc.cyan('export AWS_PROFILE=your-profile'));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (error.name === 'AccessDeniedException' ||
|
|
165
|
+
error.message.includes('not authorized')) {
|
|
166
|
+
console.log(pc.red('Error:') + ' Insufficient AWS permissions.');
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log('Required IAM permissions for AWS Organizations:');
|
|
169
|
+
console.log(' - ' + pc.cyan('organizations:CreateOrganization'));
|
|
170
|
+
console.log(' - ' + pc.cyan('organizations:DescribeOrganization'));
|
|
171
|
+
console.log(' - ' + pc.cyan('organizations:CreateAccount'));
|
|
172
|
+
console.log(' - ' + pc.cyan('organizations:DescribeCreateAccountStatus'));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
// Log which account failed if it's an account creation error
|
|
176
|
+
if (error.message.includes('Failed to create')) {
|
|
177
|
+
console.log(pc.red('Error:') + ` ${error.message}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Generic error
|
|
182
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
183
|
+
console.log(pc.red('Error:') + ` AWS Organizations setup failed: ${message}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Determine output directory
|
|
188
|
+
const outputDir = resolve(process.cwd(), config.projectName);
|
|
189
|
+
// Check if directory already exists
|
|
190
|
+
if (existsSync(outputDir)) {
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(pc.red('Error:') + ` Directory ${pc.cyan(config.projectName)} already exists.`);
|
|
193
|
+
console.log('Please choose a different project name or remove the existing directory.');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Create project directory
|
|
197
|
+
mkdirSync(outputDir, { recursive: true });
|
|
198
|
+
// Generate project
|
|
199
|
+
console.log('');
|
|
200
|
+
await generateProject(config, outputDir);
|
|
201
|
+
// Success message and next steps
|
|
202
|
+
console.log('');
|
|
203
|
+
console.log(pc.green('✔') + ` Created ${pc.bold(config.projectName)} successfully!`);
|
|
204
|
+
printNextSteps(config.projectName, config.platforms);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { fromIni } from '@aws-sdk/credential-providers';
|
|
3
|
+
import { IAMClient } from '@aws-sdk/client-iam';
|
|
4
|
+
import { runGitHubSetupWizard } from '../prompts/github-setup.js';
|
|
5
|
+
import { createDeploymentUserWithCredentials } from '../aws/iam.js';
|
|
6
|
+
import { createGitHubClient, setEnvironmentCredentials } from '../github/secrets.js';
|
|
7
|
+
/**
|
|
8
|
+
* setup-github command module
|
|
9
|
+
*
|
|
10
|
+
* Orchestrates the complete flow of creating IAM deployment users
|
|
11
|
+
* and configuring GitHub repository secrets for each environment.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Prints the setup-github command banner
|
|
15
|
+
*/
|
|
16
|
+
function printBanner() {
|
|
17
|
+
console.log(`
|
|
18
|
+
${pc.cyan('=================================================')}
|
|
19
|
+
${pc.cyan(' GitHub Deployment Setup')}
|
|
20
|
+
${pc.cyan('=================================================')}
|
|
21
|
+
|
|
22
|
+
This command will:
|
|
23
|
+
1. Create IAM deployment users in each AWS account
|
|
24
|
+
2. Configure GitHub repository secrets for CI/CD
|
|
25
|
+
|
|
26
|
+
${pc.dim('Prerequisites:')}
|
|
27
|
+
${pc.dim('- AWS CLI profiles configured for each account')}
|
|
28
|
+
${pc.dim('- GitHub PAT with "repo" scope')}
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates an IAM client configured with credentials from a specific profile
|
|
33
|
+
* @param region - AWS region
|
|
34
|
+
* @param profile - AWS CLI profile name
|
|
35
|
+
* @returns Configured IAMClient
|
|
36
|
+
*/
|
|
37
|
+
function createIAMClientWithProfile(region, profile) {
|
|
38
|
+
return new IAMClient({
|
|
39
|
+
region,
|
|
40
|
+
credentials: fromIni({ profile }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sets up deployment for a single environment
|
|
45
|
+
* @param config - Complete setup configuration
|
|
46
|
+
* @param envConfig - Configuration for this specific environment
|
|
47
|
+
* @returns Setup result
|
|
48
|
+
*/
|
|
49
|
+
async function setupEnvironment(config, envConfig) {
|
|
50
|
+
const { environment, accountId, awsProfile } = envConfig;
|
|
51
|
+
const { projectName, awsRegion, github } = config;
|
|
52
|
+
console.log(pc.cyan(`\n${'='.repeat(50)}`));
|
|
53
|
+
console.log(pc.cyan(`Setting up ${pc.bold(environment)} environment`));
|
|
54
|
+
console.log(pc.cyan('='.repeat(50)));
|
|
55
|
+
console.log(pc.dim(`Account: ${accountId}`));
|
|
56
|
+
console.log(pc.dim(`Profile: ${awsProfile}`));
|
|
57
|
+
try {
|
|
58
|
+
// Step 1: Create IAM client with profile credentials
|
|
59
|
+
console.log(pc.dim(`\nConnecting to AWS using profile "${awsProfile}"...`));
|
|
60
|
+
const iamClient = createIAMClientWithProfile(awsRegion, awsProfile);
|
|
61
|
+
// Step 2: Create deployment user and get credentials
|
|
62
|
+
const credentials = await createDeploymentUserWithCredentials(iamClient, projectName, environment, accountId);
|
|
63
|
+
// Step 3: Create GitHub client
|
|
64
|
+
console.log(pc.dim('\nConnecting to GitHub...'));
|
|
65
|
+
const githubClient = createGitHubClient(github.token);
|
|
66
|
+
// Step 4: Set GitHub repository secrets
|
|
67
|
+
console.log(pc.dim(`Setting GitHub secrets for ${github.owner}/${github.repo}...`));
|
|
68
|
+
await setEnvironmentCredentials(githubClient, github.owner, github.repo, environment, credentials.accessKeyId, credentials.secretAccessKey);
|
|
69
|
+
return {
|
|
70
|
+
environment,
|
|
71
|
+
success: true,
|
|
72
|
+
secretsCreated: [
|
|
73
|
+
'AWS_ACCESS_KEY_ID',
|
|
74
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
80
|
+
// Provide helpful error messages for common issues
|
|
81
|
+
if (errorMessage.includes('Could not load credentials') ||
|
|
82
|
+
errorMessage.includes('SharedIniFileCredentials') ||
|
|
83
|
+
errorMessage.includes('Profile') && errorMessage.includes('not found')) {
|
|
84
|
+
return {
|
|
85
|
+
environment,
|
|
86
|
+
success: false,
|
|
87
|
+
error: `AWS profile "${awsProfile}" not found or invalid. Configure it with: aws configure --profile ${awsProfile}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (errorMessage.includes('not authorized') ||
|
|
91
|
+
errorMessage.includes('AccessDenied')) {
|
|
92
|
+
return {
|
|
93
|
+
environment,
|
|
94
|
+
success: false,
|
|
95
|
+
error: `Insufficient IAM permissions for profile "${awsProfile}". Ensure the profile has IAM admin access.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (errorMessage.includes('GitHub authentication failed')) {
|
|
99
|
+
return {
|
|
100
|
+
environment,
|
|
101
|
+
success: false,
|
|
102
|
+
error: 'GitHub authentication failed. Check that your PAT has the "repo" scope.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
environment,
|
|
107
|
+
success: false,
|
|
108
|
+
error: errorMessage,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Prints the final summary of setup results
|
|
114
|
+
*/
|
|
115
|
+
function printSummary(config, results) {
|
|
116
|
+
console.log(pc.cyan(`\n${'='.repeat(50)}`));
|
|
117
|
+
console.log(pc.cyan(' Summary'));
|
|
118
|
+
console.log(pc.cyan('='.repeat(50)));
|
|
119
|
+
const successful = results.filter((r) => r.success);
|
|
120
|
+
const failed = results.filter((r) => !r.success);
|
|
121
|
+
console.log(`\nGitHub Repository: ${pc.bold(`${config.github.owner}/${config.github.repo}`)}`);
|
|
122
|
+
console.log(`Project: ${pc.bold(config.projectName)}`);
|
|
123
|
+
console.log('');
|
|
124
|
+
if (successful.length > 0) {
|
|
125
|
+
console.log(pc.green('Successful environments:'));
|
|
126
|
+
for (const result of successful) {
|
|
127
|
+
console.log(pc.green(` ${pc.bold('+')} ${result.environment}`));
|
|
128
|
+
if (result.secretsCreated) {
|
|
129
|
+
for (const secret of result.secretsCreated) {
|
|
130
|
+
console.log(pc.dim(` ${secret}`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (failed.length > 0) {
|
|
136
|
+
console.log(pc.red('\nFailed environments:'));
|
|
137
|
+
for (const result of failed) {
|
|
138
|
+
console.log(pc.red(` ${pc.bold('x')} ${result.environment}`));
|
|
139
|
+
console.log(pc.dim(` ${result.error}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
if (failed.length === 0) {
|
|
144
|
+
console.log(pc.green(pc.bold('All environments configured successfully!')));
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(pc.dim('Your GitHub Actions workflows can now deploy to AWS using environment secrets.'));
|
|
147
|
+
console.log(pc.dim('Each environment has AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY configured:'));
|
|
148
|
+
for (const result of successful) {
|
|
149
|
+
console.log(pc.dim(` - ${result.environment}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (successful.length === 0) {
|
|
153
|
+
console.log(pc.red(pc.bold('Setup failed for all environments.')));
|
|
154
|
+
console.log(pc.dim('Please check the error messages above and try again.'));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log(pc.yellow(pc.bold(`Setup completed with errors (${successful.length}/${results.length} succeeded).`)));
|
|
158
|
+
console.log(pc.dim('You can re-run this command to retry failed environments.'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Runs the setup-github command
|
|
163
|
+
*/
|
|
164
|
+
export async function runSetupGitHub() {
|
|
165
|
+
printBanner();
|
|
166
|
+
// Run the wizard to collect configuration
|
|
167
|
+
const config = await runGitHubSetupWizard();
|
|
168
|
+
if (!config) {
|
|
169
|
+
console.log(pc.yellow('\nSetup cancelled.'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(pc.cyan('\nStarting deployment setup...'));
|
|
173
|
+
// Process each environment
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const envConfig of config.environments) {
|
|
176
|
+
const result = await setupEnvironment(config, envConfig);
|
|
177
|
+
results.push(result);
|
|
178
|
+
// If an environment fails, ask whether to continue
|
|
179
|
+
if (!result.success && config.environments.indexOf(envConfig) < config.environments.length - 1) {
|
|
180
|
+
console.log(pc.yellow(`\nEnvironment ${envConfig.environment} failed. Continuing with remaining environments...`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Print summary
|
|
184
|
+
printSummary(config, results);
|
|
185
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TokenValues } from '../templates/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Copy a single file with token replacement
|
|
4
|
+
* @param srcPath - Source file path
|
|
5
|
+
* @param destPath - Destination file path
|
|
6
|
+
* @param tokens - Token values for replacement
|
|
7
|
+
*/
|
|
8
|
+
export declare function copyFileWithTokens(srcPath: string, destPath: string, tokens: TokenValues): void;
|
|
9
|
+
/**
|
|
10
|
+
* Recursively copy a directory with token replacement
|
|
11
|
+
* @param srcDir - Source directory path
|
|
12
|
+
* @param destDir - Destination directory path
|
|
13
|
+
* @param tokens - Token values for replacement
|
|
14
|
+
*/
|
|
15
|
+
export declare function copyDirectoryWithTokens(srcDir: string, destDir: string, tokens: TokenValues): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { replaceTokens } from './replace-tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* Check if a file should have token replacement (text files only)
|
|
6
|
+
*/
|
|
7
|
+
function shouldReplaceTokens(filename) {
|
|
8
|
+
const textExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.yml', '.yaml', '.html', '.css'];
|
|
9
|
+
const ext = filename.substring(filename.lastIndexOf('.'));
|
|
10
|
+
return textExtensions.includes(ext);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Copy a single file with token replacement
|
|
14
|
+
* @param srcPath - Source file path
|
|
15
|
+
* @param destPath - Destination file path
|
|
16
|
+
* @param tokens - Token values for replacement
|
|
17
|
+
*/
|
|
18
|
+
export function copyFileWithTokens(srcPath, destPath, tokens) {
|
|
19
|
+
// Ensure destination directory exists
|
|
20
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
21
|
+
if (shouldReplaceTokens(srcPath)) {
|
|
22
|
+
// Text file - read, replace tokens, write
|
|
23
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
24
|
+
const replaced = replaceTokens(content, tokens);
|
|
25
|
+
writeFileSync(destPath, replaced, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Binary file - direct copy
|
|
29
|
+
copyFileSync(srcPath, destPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Recursively copy a directory with token replacement
|
|
34
|
+
* @param srcDir - Source directory path
|
|
35
|
+
* @param destDir - Destination directory path
|
|
36
|
+
* @param tokens - Token values for replacement
|
|
37
|
+
*/
|
|
38
|
+
export function copyDirectoryWithTokens(srcDir, destDir, tokens) {
|
|
39
|
+
mkdirSync(destDir, { recursive: true });
|
|
40
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const srcPath = join(srcDir, entry.name);
|
|
43
|
+
let destName = entry.name;
|
|
44
|
+
// Handle .template extension - rename back to original
|
|
45
|
+
if (destName.endsWith('.template')) {
|
|
46
|
+
destName = destName.replace('.template', '');
|
|
47
|
+
}
|
|
48
|
+
const destPath = join(destDir, destName);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
copyDirectoryWithTokens(srcPath, destPath, tokens);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
copyFileWithTokens(srcPath, destPath, tokens);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ProjectConfig } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for project generation
|
|
4
|
+
*/
|
|
5
|
+
export interface GenerateOptions {
|
|
6
|
+
onProgress?: (message: string) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generate a project from templates based on user configuration
|
|
10
|
+
* @param config - User's project configuration from wizard
|
|
11
|
+
* @param outputDir - Directory where project will be created
|
|
12
|
+
* @param options - Optional generation options including progress callback
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateProject(config: ProjectConfig, outputDir: string, options?: GenerateOptions): Promise<void>;
|