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.
Files changed (181) hide show
  1. package/README.md +118 -0
  2. package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
  3. package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
  4. package/dist/__tests__/generator.spec.d.ts +1 -0
  5. package/dist/__tests__/generator.spec.js +162 -0
  6. package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
  7. package/dist/__tests__/validation/project-name.spec.js +57 -0
  8. package/dist/__tests__/wizard.spec.d.ts +1 -0
  9. package/dist/__tests__/wizard.spec.js +232 -0
  10. package/dist/aws/iam.d.ts +75 -0
  11. package/dist/aws/iam.js +264 -0
  12. package/dist/aws/organizations.d.ts +79 -0
  13. package/dist/aws/organizations.js +168 -0
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.js +206 -0
  16. package/dist/commands/setup-github.d.ts +4 -0
  17. package/dist/commands/setup-github.js +185 -0
  18. package/dist/generator/copy-file.d.ts +15 -0
  19. package/dist/generator/copy-file.js +56 -0
  20. package/dist/generator/generate-project.d.ts +14 -0
  21. package/dist/generator/generate-project.js +81 -0
  22. package/dist/generator/index.d.ts +4 -0
  23. package/dist/generator/index.js +3 -0
  24. package/dist/generator/replace-tokens.d.ts +29 -0
  25. package/dist/generator/replace-tokens.js +68 -0
  26. package/dist/github/secrets.d.ts +109 -0
  27. package/dist/github/secrets.js +275 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +6 -0
  30. package/dist/prompts/auth.d.ts +3 -0
  31. package/dist/prompts/auth.js +23 -0
  32. package/dist/prompts/aws-config.d.ts +2 -0
  33. package/dist/prompts/aws-config.js +14 -0
  34. package/dist/prompts/features.d.ts +2 -0
  35. package/dist/prompts/features.js +10 -0
  36. package/dist/prompts/github-setup.d.ts +53 -0
  37. package/dist/prompts/github-setup.js +208 -0
  38. package/dist/prompts/org-structure.d.ts +9 -0
  39. package/dist/prompts/org-structure.js +93 -0
  40. package/dist/prompts/platforms.d.ts +2 -0
  41. package/dist/prompts/platforms.js +12 -0
  42. package/dist/prompts/project-name.d.ts +2 -0
  43. package/dist/prompts/project-name.js +8 -0
  44. package/dist/prompts/theme.d.ts +2 -0
  45. package/dist/prompts/theme.js +14 -0
  46. package/dist/templates/index.d.ts +4 -0
  47. package/dist/templates/index.js +2 -0
  48. package/dist/templates/manifest.d.ts +11 -0
  49. package/dist/templates/manifest.js +99 -0
  50. package/dist/templates/tokens.d.ts +39 -0
  51. package/dist/templates/tokens.js +37 -0
  52. package/dist/templates/types.d.ts +52 -0
  53. package/dist/templates/types.js +1 -0
  54. package/dist/types.d.ts +27 -0
  55. package/dist/types.js +1 -0
  56. package/dist/validation/project-name.d.ts +1 -0
  57. package/dist/validation/project-name.js +12 -0
  58. package/dist/wizard.d.ts +2 -0
  59. package/dist/wizard.js +81 -0
  60. package/package.json +68 -0
  61. package/templates/.github/actions/build-and-test/action.yml +24 -0
  62. package/templates/.github/actions/deploy-cdk/action.yml +46 -0
  63. package/templates/.github/actions/deploy-web/action.yml +72 -0
  64. package/templates/.github/actions/setup/action.yml +29 -0
  65. package/templates/.github/pull_request_template.md +15 -0
  66. package/templates/.github/workflows/deploy-dev.yml +80 -0
  67. package/templates/.github/workflows/deploy-prod.yml +67 -0
  68. package/templates/.github/workflows/deploy-stage.yml +77 -0
  69. package/templates/.github/workflows/pull-request.yml +72 -0
  70. package/templates/.vscode/extensions.json +7 -0
  71. package/templates/.vscode/settings.json +67 -0
  72. package/templates/apps/api/.eslintrc.json +18 -0
  73. package/templates/apps/api/cdk/app.ts +93 -0
  74. package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
  75. package/templates/apps/api/cdk/cdk.json +73 -0
  76. package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
  77. package/templates/apps/api/cdk/org-stack.ts +67 -0
  78. package/templates/apps/api/cdk/static-stack.ts +361 -0
  79. package/templates/apps/api/cdk/tsconfig.json +39 -0
  80. package/templates/apps/api/cdk/user-stack.ts +255 -0
  81. package/templates/apps/api/jest.config.ts +38 -0
  82. package/templates/apps/api/lambdas.yml +84 -0
  83. package/templates/apps/api/project.json.template +58 -0
  84. package/templates/apps/api/src/__tests__/setup.ts +10 -0
  85. package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
  86. package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
  87. package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
  88. package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
  89. package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
  90. package/templates/apps/api/src/handlers/users/index.ts +17 -0
  91. package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
  92. package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
  93. package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
  94. package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
  95. package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
  96. package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
  97. package/templates/apps/api/src/models/UserModel.ts +109 -0
  98. package/templates/apps/api/src/schemas/user.schema.ts +44 -0
  99. package/templates/apps/api/src/services/user-service.ts +108 -0
  100. package/templates/apps/api/src/utils/auth-context.ts +60 -0
  101. package/templates/apps/api/src/utils/common/helpers.ts +26 -0
  102. package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
  103. package/templates/apps/api/src/utils/response.ts +52 -0
  104. package/templates/apps/api/src/utils/validator.ts +75 -0
  105. package/templates/apps/api/tsconfig.app.json +15 -0
  106. package/templates/apps/api/tsconfig.json +19 -0
  107. package/templates/apps/api/tsconfig.spec.json +17 -0
  108. package/templates/apps/mobile/.env.example +5 -0
  109. package/templates/apps/mobile/.eslintrc.json +33 -0
  110. package/templates/apps/mobile/app.json +33 -0
  111. package/templates/apps/mobile/assets/.gitkeep +0 -0
  112. package/templates/apps/mobile/babel.config.js +19 -0
  113. package/templates/apps/mobile/index.js +7 -0
  114. package/templates/apps/mobile/jest.config.ts +22 -0
  115. package/templates/apps/mobile/metro.config.js +35 -0
  116. package/templates/apps/mobile/package.json +22 -0
  117. package/templates/apps/mobile/project.json.template +64 -0
  118. package/templates/apps/mobile/src/App.tsx +367 -0
  119. package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
  120. package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
  121. package/templates/apps/mobile/src/config/api.ts +16 -0
  122. package/templates/apps/mobile/src/store/user-store.ts +56 -0
  123. package/templates/apps/mobile/src/test-setup.ts +10 -0
  124. package/templates/apps/mobile/tsconfig.json +22 -0
  125. package/templates/apps/web/.env.example +13 -0
  126. package/templates/apps/web/.eslintrc.json +26 -0
  127. package/templates/apps/web/index.html +13 -0
  128. package/templates/apps/web/jest.config.ts +24 -0
  129. package/templates/apps/web/package.json +15 -0
  130. package/templates/apps/web/project.json.template +66 -0
  131. package/templates/apps/web/src/App.tsx +352 -0
  132. package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
  133. package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
  134. package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
  135. package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
  136. package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
  137. package/templates/apps/web/src/auth/index.ts +7 -0
  138. package/templates/apps/web/src/auth/use-auth.ts +16 -0
  139. package/templates/apps/web/src/config/amplify-config.ts +31 -0
  140. package/templates/apps/web/src/config/api.ts +38 -0
  141. package/templates/apps/web/src/config/auth0-config.ts +17 -0
  142. package/templates/apps/web/src/main.tsx +41 -0
  143. package/templates/apps/web/src/store/user-store.ts +56 -0
  144. package/templates/apps/web/src/styles.css +165 -0
  145. package/templates/apps/web/src/test-setup.ts +1 -0
  146. package/templates/apps/web/src/theme/index.ts +30 -0
  147. package/templates/apps/web/src/vite-env.d.ts +19 -0
  148. package/templates/apps/web/tsconfig.app.json +24 -0
  149. package/templates/apps/web/tsconfig.json +22 -0
  150. package/templates/apps/web/tsconfig.spec.json +28 -0
  151. package/templates/apps/web/vite.config.ts +87 -0
  152. package/templates/manifest.json +28 -0
  153. package/templates/packages/api-client/.eslintrc.json +18 -0
  154. package/templates/packages/api-client/jest.config.ts +13 -0
  155. package/templates/packages/api-client/package.json +8 -0
  156. package/templates/packages/api-client/project.json.template +34 -0
  157. package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
  158. package/templates/packages/api-client/src/api-client.ts +201 -0
  159. package/templates/packages/api-client/src/config.ts +193 -0
  160. package/templates/packages/api-client/src/index.ts +9 -0
  161. package/templates/packages/api-client/tsconfig.json +22 -0
  162. package/templates/packages/api-client/tsconfig.lib.json +11 -0
  163. package/templates/packages/api-client/tsconfig.spec.json +14 -0
  164. package/templates/packages/common-types/.eslintrc.json +18 -0
  165. package/templates/packages/common-types/package.json +6 -0
  166. package/templates/packages/common-types/project.json.template +26 -0
  167. package/templates/packages/common-types/src/api.types.ts +24 -0
  168. package/templates/packages/common-types/src/auth.types.ts +36 -0
  169. package/templates/packages/common-types/src/common.types.ts +46 -0
  170. package/templates/packages/common-types/src/index.ts +19 -0
  171. package/templates/packages/common-types/src/lambda.types.ts +39 -0
  172. package/templates/packages/common-types/src/user.types.ts +31 -0
  173. package/templates/packages/common-types/tsconfig.json +19 -0
  174. package/templates/packages/common-types/tsconfig.lib.json +11 -0
  175. package/templates/root/.editorconfig +23 -0
  176. package/templates/root/.nvmrc +1 -0
  177. package/templates/root/eslint.config.js +61 -0
  178. package/templates/root/jest.preset.js +16 -0
  179. package/templates/root/nx.json +29 -0
  180. package/templates/root/package.json +131 -0
  181. 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
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Parse command line arguments and run the CLI
3
+ */
4
+ export declare function run(): Promise<void>;
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,4 @@
1
+ /**
2
+ * Runs the setup-github command
3
+ */
4
+ export declare function runSetupGitHub(): Promise<void>;
@@ -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>;