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,81 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import pc from 'picocolors';
5
+ import { copyFileWithTokens, copyDirectoryWithTokens } from './copy-file.js';
6
+ import { deriveTokenValues, templateManifest } from '../templates/index.js';
7
+ /**
8
+ * Get the templates directory path
9
+ */
10
+ function getTemplatesDir() {
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ // Navigate from dist/src/generator to templates/
14
+ return join(__dirname, '..', '..', 'templates');
15
+ }
16
+ /**
17
+ * Copy a template entry (file or directory) to output
18
+ */
19
+ function copyTemplateEntry(entry, templatesDir, outputDir, tokens) {
20
+ const srcPath = join(templatesDir, entry.src);
21
+ const destPath = join(outputDir, entry.dest);
22
+ if (!existsSync(srcPath)) {
23
+ console.warn(`Warning: Template not found: ${entry.src}`);
24
+ return;
25
+ }
26
+ const stats = statSync(srcPath);
27
+ if (stats.isDirectory()) {
28
+ copyDirectoryWithTokens(srcPath, destPath, tokens);
29
+ }
30
+ else {
31
+ copyFileWithTokens(srcPath, destPath, tokens);
32
+ }
33
+ }
34
+ /**
35
+ * Generate a project from templates based on user configuration
36
+ * @param config - User's project configuration from wizard
37
+ * @param outputDir - Directory where project will be created
38
+ * @param options - Optional generation options including progress callback
39
+ */
40
+ export async function generateProject(config, outputDir, options = {}) {
41
+ const { onProgress = (msg) => console.log(msg) } = options;
42
+ const templatesDir = getTemplatesDir();
43
+ const tokens = deriveTokenValues(config);
44
+ onProgress(pc.cyan('Creating project structure...'));
45
+ // 1. Copy all shared files
46
+ onProgress(' Copying shared configuration...');
47
+ for (const entry of templateManifest.shared) {
48
+ copyTemplateEntry(entry, templatesDir, outputDir, tokens);
49
+ }
50
+ // 2. Copy platform-specific files based on user selection
51
+ for (const platform of config.platforms) {
52
+ onProgress(` Copying ${platform} app...`);
53
+ const platformEntries = templateManifest.byPlatform[platform];
54
+ if (platformEntries) {
55
+ for (const entry of platformEntries) {
56
+ copyTemplateEntry(entry, templatesDir, outputDir, tokens);
57
+ }
58
+ }
59
+ }
60
+ // 3. Copy feature-specific files based on user selection
61
+ for (const feature of config.features) {
62
+ onProgress(` Adding ${feature}...`);
63
+ const featureEntries = templateManifest.byFeature[feature];
64
+ if (featureEntries) {
65
+ for (const entry of featureEntries) {
66
+ copyTemplateEntry(entry, templatesDir, outputDir, tokens);
67
+ }
68
+ }
69
+ }
70
+ // 4. Copy auth provider-specific files based on user selection
71
+ if (config.auth.provider !== 'none') {
72
+ onProgress(` Adding ${config.auth.provider} authentication...`);
73
+ const authEntries = templateManifest.byAuthProvider[config.auth.provider];
74
+ if (authEntries) {
75
+ for (const entry of authEntries) {
76
+ copyTemplateEntry(entry, templatesDir, outputDir, tokens);
77
+ }
78
+ }
79
+ }
80
+ onProgress(pc.green('✔') + ' Project structure created');
81
+ }
@@ -0,0 +1,4 @@
1
+ export { replaceTokens } from './replace-tokens.js';
2
+ export { copyFileWithTokens, copyDirectoryWithTokens } from './copy-file.js';
3
+ export { generateProject } from './generate-project.js';
4
+ export type { GenerateOptions } from './generate-project.js';
@@ -0,0 +1,3 @@
1
+ export { replaceTokens } from './replace-tokens.js';
2
+ export { copyFileWithTokens, copyDirectoryWithTokens } from './copy-file.js';
3
+ export { generateProject } from './generate-project.js';
@@ -0,0 +1,29 @@
1
+ import type { TokenValues } from '../templates/types.js';
2
+ /**
3
+ * Process conditional blocks in content based on token values.
4
+ *
5
+ * Supports two formats:
6
+ * 1. Comment-wrapped (for TypeScript/JavaScript files):
7
+ * // {{#if TOKEN}}
8
+ * import { Something } from './something';
9
+ * // {{/if TOKEN}}
10
+ *
11
+ * 2. Plain (for other file types):
12
+ * {{#if TOKEN}}
13
+ * Some content
14
+ * {{/if TOKEN}}
15
+ *
16
+ * @param content - File content with conditional blocks
17
+ * @param tokens - Token values (value of 'true' keeps content, anything else removes it)
18
+ * @returns Content with conditional blocks processed
19
+ */
20
+ export declare function processConditionalBlocks(content: string, tokens: TokenValues): string;
21
+ /**
22
+ * Replace all {{TOKEN}} placeholders in content with actual values.
23
+ * Processes conditional blocks first, then replaces remaining tokens.
24
+ *
25
+ * @param content - File content with {{TOKEN}} placeholders and conditional blocks
26
+ * @param tokens - Token values to substitute
27
+ * @returns Content with conditionals processed and all tokens replaced
28
+ */
29
+ export declare function replaceTokens(content: string, tokens: TokenValues): string;
@@ -0,0 +1,68 @@
1
+ import { TOKEN_PATTERN } from '../templates/tokens.js';
2
+ /**
3
+ * Pattern to match comment-wrapped conditional blocks:
4
+ * // {{#if TOKEN}}
5
+ * ...content...
6
+ * // {{/if TOKEN}}
7
+ *
8
+ * Uses non-greedy matching and backreference to ensure opening/closing tokens match.
9
+ */
10
+ const COMMENT_CONDITIONAL_PATTERN = /\/\/\s*\{\{#if\s+(\w+)\}\}\r?\n([\s\S]*?)\/\/\s*\{\{\/if\s+\1\}\}\r?\n?/g;
11
+ /**
12
+ * Pattern to match plain conditional blocks (without comment prefix):
13
+ * {{#if TOKEN}}
14
+ * ...content...
15
+ * {{/if TOKEN}}
16
+ */
17
+ const PLAIN_CONDITIONAL_PATTERN = /\{\{#if\s+(\w+)\}\}\r?\n?([\s\S]*?)\{\{\/if\s+\1\}\}\r?\n?/g;
18
+ /**
19
+ * Process conditional blocks in content based on token values.
20
+ *
21
+ * Supports two formats:
22
+ * 1. Comment-wrapped (for TypeScript/JavaScript files):
23
+ * // {{#if TOKEN}}
24
+ * import { Something } from './something';
25
+ * // {{/if TOKEN}}
26
+ *
27
+ * 2. Plain (for other file types):
28
+ * {{#if TOKEN}}
29
+ * Some content
30
+ * {{/if TOKEN}}
31
+ *
32
+ * @param content - File content with conditional blocks
33
+ * @param tokens - Token values (value of 'true' keeps content, anything else removes it)
34
+ * @returns Content with conditional blocks processed
35
+ */
36
+ export function processConditionalBlocks(content, tokens) {
37
+ let result = content;
38
+ // Process comment-wrapped conditionals first (more specific pattern)
39
+ result = result.replace(COMMENT_CONDITIONAL_PATTERN, (_match, tokenName, blockContent) => {
40
+ const tokenValue = tokens[tokenName];
41
+ // Keep content if token value is exactly 'true', otherwise remove entire block
42
+ return tokenValue === 'true' ? blockContent : '';
43
+ });
44
+ // Process plain conditionals
45
+ result = result.replace(PLAIN_CONDITIONAL_PATTERN, (_match, tokenName, blockContent) => {
46
+ const tokenValue = tokens[tokenName];
47
+ // Keep content if token value is exactly 'true', otherwise remove entire block
48
+ return tokenValue === 'true' ? blockContent : '';
49
+ });
50
+ return result;
51
+ }
52
+ /**
53
+ * Replace all {{TOKEN}} placeholders in content with actual values.
54
+ * Processes conditional blocks first, then replaces remaining tokens.
55
+ *
56
+ * @param content - File content with {{TOKEN}} placeholders and conditional blocks
57
+ * @param tokens - Token values to substitute
58
+ * @returns Content with conditionals processed and all tokens replaced
59
+ */
60
+ export function replaceTokens(content, tokens) {
61
+ // Process conditional blocks BEFORE token replacement
62
+ const processedContent = processConditionalBlocks(content, tokens);
63
+ // Then replace individual tokens
64
+ return processedContent.replace(TOKEN_PATTERN, (match, tokenName) => {
65
+ const value = tokens[tokenName];
66
+ return value !== undefined ? value : match; // Keep original if unknown token
67
+ });
68
+ }
@@ -0,0 +1,109 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ /**
3
+ * GitHub secrets service module
4
+ *
5
+ * Provides functions to manage GitHub environment secrets
6
+ * for AWS deployment credentials via the GitHub REST API.
7
+ *
8
+ * Uses GitHub Environments to organize secrets by environment (dev, stage, prod),
9
+ * with the same secret names in each environment (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
10
+ */
11
+ /**
12
+ * Creates a configured Octokit client
13
+ * @param token - GitHub Personal Access Token (requires `repo` scope)
14
+ * @returns Configured Octokit instance
15
+ */
16
+ export declare function createGitHubClient(token: string): Octokit;
17
+ /**
18
+ * Public key for encrypting secrets
19
+ */
20
+ export interface PublicKey {
21
+ keyId: string;
22
+ key: string;
23
+ }
24
+ /**
25
+ * Fetches the repository's public key for encrypting secrets
26
+ * @param client - Octokit instance
27
+ * @param owner - Repository owner (username or organization)
28
+ * @param repo - Repository name
29
+ * @returns Public key ID and base64-encoded key
30
+ */
31
+ export declare function getRepositoryPublicKey(client: Octokit, owner: string, repo: string): Promise<PublicKey>;
32
+ /**
33
+ * Encrypts a secret value using the repository's public key
34
+ * @param publicKey - Base64-encoded repository public key
35
+ * @param secretValue - Plain text secret value to encrypt
36
+ * @returns Base64-encoded encrypted value
37
+ */
38
+ export declare function encryptSecret(publicKey: string, secretValue: string): Promise<string>;
39
+ /**
40
+ * Creates or ensures a GitHub environment exists
41
+ * @param client - Octokit instance
42
+ * @param owner - Repository owner
43
+ * @param repo - Repository name
44
+ * @param environmentName - Name of the environment (e.g., dev, stage, prod)
45
+ */
46
+ export declare function ensureEnvironmentExists(client: Octokit, owner: string, repo: string, environmentName: string): Promise<void>;
47
+ /**
48
+ * Fetches the environment's public key for encrypting secrets
49
+ * @param client - Octokit instance
50
+ * @param owner - Repository owner
51
+ * @param repo - Repository name
52
+ * @param environmentName - Name of the environment
53
+ * @returns Public key ID and base64-encoded key
54
+ */
55
+ export declare function getEnvironmentPublicKey(client: Octokit, owner: string, repo: string, environmentName: string): Promise<PublicKey>;
56
+ /**
57
+ * Sets (creates or updates) an environment secret
58
+ * @param client - Octokit instance
59
+ * @param owner - Repository owner
60
+ * @param repo - Repository name
61
+ * @param environmentName - Name of the environment
62
+ * @param secretName - Name of the secret (e.g., AWS_ACCESS_KEY_ID)
63
+ * @param secretValue - Plain text secret value
64
+ */
65
+ export declare function setEnvironmentSecret(client: Octokit, owner: string, repo: string, environmentName: string, secretName: string, secretValue: string): Promise<void>;
66
+ /**
67
+ * Sets (creates or updates) a repository secret
68
+ * @param client - Octokit instance
69
+ * @param owner - Repository owner
70
+ * @param repo - Repository name
71
+ * @param secretName - Name of the secret (e.g., AWS_ACCESS_KEY_ID_DEV)
72
+ * @param secretValue - Plain text secret value
73
+ */
74
+ export declare function setRepositorySecret(client: Octokit, owner: string, repo: string, secretName: string, secretValue: string): Promise<void>;
75
+ /**
76
+ * Sets AWS credentials as environment secrets for a specific GitHub environment
77
+ *
78
+ * Creates the GitHub environment if it doesn't exist, then sets AWS_ACCESS_KEY_ID
79
+ * and AWS_SECRET_ACCESS_KEY as secrets within that environment.
80
+ *
81
+ * @param client - Octokit instance
82
+ * @param owner - Repository owner
83
+ * @param repo - Repository name
84
+ * @param environment - Environment name (e.g., dev, stage, prod)
85
+ * @param accessKeyId - AWS Access Key ID
86
+ * @param secretAccessKey - AWS Secret Access Key
87
+ */
88
+ export declare function setEnvironmentCredentials(client: Octokit, owner: string, repo: string, environment: string, accessKeyId: string, secretAccessKey: string): Promise<void>;
89
+ /**
90
+ * Parsed GitHub repository URL
91
+ */
92
+ export interface GitHubRepoInfo {
93
+ owner: string;
94
+ repo: string;
95
+ }
96
+ /**
97
+ * Parses a GitHub repository URL to extract owner and repo
98
+ * @param url - GitHub repository URL in various formats
99
+ * @returns Parsed owner and repo
100
+ * @throws Error if URL format is not recognized
101
+ *
102
+ * Supported formats:
103
+ * - https://github.com/owner/repo
104
+ * - https://github.com/owner/repo.git
105
+ * - git@github.com:owner/repo.git
106
+ * - git@github.com:owner/repo
107
+ * - owner/repo
108
+ */
109
+ export declare function parseGitHubUrl(url: string): GitHubRepoInfo;
@@ -0,0 +1,275 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import nacl from 'tweetnacl';
3
+ import naclUtil from 'tweetnacl-util';
4
+ import pc from 'picocolors';
5
+ const { decodeUTF8, encodeBase64, decodeBase64 } = naclUtil;
6
+ /**
7
+ * GitHub secrets service module
8
+ *
9
+ * Provides functions to manage GitHub environment secrets
10
+ * for AWS deployment credentials via the GitHub REST API.
11
+ *
12
+ * Uses GitHub Environments to organize secrets by environment (dev, stage, prod),
13
+ * with the same secret names in each environment (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
14
+ */
15
+ /**
16
+ * Creates a configured Octokit client
17
+ * @param token - GitHub Personal Access Token (requires `repo` scope)
18
+ * @returns Configured Octokit instance
19
+ */
20
+ export function createGitHubClient(token) {
21
+ return new Octokit({
22
+ auth: token,
23
+ });
24
+ }
25
+ /**
26
+ * Fetches the repository's public key for encrypting secrets
27
+ * @param client - Octokit instance
28
+ * @param owner - Repository owner (username or organization)
29
+ * @param repo - Repository name
30
+ * @returns Public key ID and base64-encoded key
31
+ */
32
+ export async function getRepositoryPublicKey(client, owner, repo) {
33
+ try {
34
+ const response = await client.actions.getRepoPublicKey({
35
+ owner,
36
+ repo,
37
+ });
38
+ return {
39
+ keyId: response.data.key_id,
40
+ key: response.data.key,
41
+ };
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && 'status' in error && error.status === 401) {
45
+ throw new Error('GitHub authentication failed. Ensure your token has the "repo" scope for repository secrets access.');
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+ /**
51
+ * Encrypts a secret value using the repository's public key
52
+ * @param publicKey - Base64-encoded repository public key
53
+ * @param secretValue - Plain text secret value to encrypt
54
+ * @returns Base64-encoded encrypted value
55
+ */
56
+ export async function encryptSecret(publicKey, secretValue) {
57
+ // Decode the public key from base64
58
+ const publicKeyBytes = decodeBase64(publicKey);
59
+ // Convert the secret value to UTF-8 bytes
60
+ const secretBytes = decodeUTF8(secretValue);
61
+ // Encrypt using libsodium sealed box (via tweetnacl)
62
+ const encryptedBytes = naclSealedBox(secretBytes, publicKeyBytes);
63
+ // Return base64-encoded encrypted value
64
+ return encodeBase64(encryptedBytes);
65
+ }
66
+ /**
67
+ * Sealed box encryption compatible with GitHub's libsodium implementation
68
+ */
69
+ function naclSealedBox(message, publicKey) {
70
+ // Generate ephemeral key pair
71
+ const ephemeralKeyPair = nacl.box.keyPair();
72
+ // Compute nonce from ephemeral public key and recipient public key
73
+ const nonce = new Uint8Array(nacl.box.nonceLength);
74
+ const nonceInput = new Uint8Array(ephemeralKeyPair.publicKey.length + publicKey.length);
75
+ nonceInput.set(ephemeralKeyPair.publicKey);
76
+ nonceInput.set(publicKey, ephemeralKeyPair.publicKey.length);
77
+ // Use first 24 bytes of blake2b hash as nonce (simplified: use crypto hash)
78
+ const hashBytes = naclHash(nonceInput).slice(0, nacl.box.nonceLength);
79
+ nonce.set(hashBytes);
80
+ // Encrypt the message
81
+ const ciphertext = nacl.box(message, nonce, publicKey, ephemeralKeyPair.secretKey);
82
+ if (!ciphertext) {
83
+ throw new Error('Encryption failed');
84
+ }
85
+ // Concatenate ephemeral public key and ciphertext (sealed box format)
86
+ const sealedBox = new Uint8Array(ephemeralKeyPair.publicKey.length + ciphertext.length);
87
+ sealedBox.set(ephemeralKeyPair.publicKey);
88
+ sealedBox.set(ciphertext, ephemeralKeyPair.publicKey.length);
89
+ return sealedBox;
90
+ }
91
+ /**
92
+ * Simple hash function for nonce generation
93
+ */
94
+ function naclHash(input) {
95
+ return nacl.hash(input);
96
+ }
97
+ /**
98
+ * Creates or ensures a GitHub environment exists
99
+ * @param client - Octokit instance
100
+ * @param owner - Repository owner
101
+ * @param repo - Repository name
102
+ * @param environmentName - Name of the environment (e.g., dev, stage, prod)
103
+ */
104
+ export async function ensureEnvironmentExists(client, owner, repo, environmentName) {
105
+ try {
106
+ // PUT request creates or updates the environment
107
+ await client.request('PUT /repos/{owner}/{repo}/environments/{environment_name}', {
108
+ owner,
109
+ repo,
110
+ environment_name: environmentName,
111
+ });
112
+ }
113
+ catch (error) {
114
+ if (error instanceof Error && 'status' in error && error.status === 401) {
115
+ throw new Error('GitHub authentication failed. Ensure your token has the "repo" scope for environment access.');
116
+ }
117
+ throw error;
118
+ }
119
+ }
120
+ /**
121
+ * Fetches the environment's public key for encrypting secrets
122
+ * @param client - Octokit instance
123
+ * @param owner - Repository owner
124
+ * @param repo - Repository name
125
+ * @param environmentName - Name of the environment
126
+ * @returns Public key ID and base64-encoded key
127
+ */
128
+ export async function getEnvironmentPublicKey(client, owner, repo, environmentName) {
129
+ try {
130
+ const response = await client.request('GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key', {
131
+ owner,
132
+ repo,
133
+ environment_name: environmentName,
134
+ });
135
+ return {
136
+ keyId: response.data.key_id,
137
+ key: response.data.key,
138
+ };
139
+ }
140
+ catch (error) {
141
+ if (error instanceof Error && 'status' in error && error.status === 401) {
142
+ throw new Error('GitHub authentication failed. Ensure your token has the "repo" scope for environment secrets access.');
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ /**
148
+ * Sets (creates or updates) an environment secret
149
+ * @param client - Octokit instance
150
+ * @param owner - Repository owner
151
+ * @param repo - Repository name
152
+ * @param environmentName - Name of the environment
153
+ * @param secretName - Name of the secret (e.g., AWS_ACCESS_KEY_ID)
154
+ * @param secretValue - Plain text secret value
155
+ */
156
+ export async function setEnvironmentSecret(client, owner, repo, environmentName, secretName, secretValue) {
157
+ try {
158
+ // Get the environment's public key
159
+ const publicKey = await getEnvironmentPublicKey(client, owner, repo, environmentName);
160
+ // Encrypt the secret value
161
+ const encryptedValue = await encryptSecret(publicKey.key, secretValue);
162
+ // Set the environment secret
163
+ await client.request('PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}', {
164
+ owner,
165
+ repo,
166
+ environment_name: environmentName,
167
+ secret_name: secretName,
168
+ encrypted_value: encryptedValue,
169
+ key_id: publicKey.keyId,
170
+ });
171
+ }
172
+ catch (error) {
173
+ if (error instanceof Error && 'status' in error && error.status === 401) {
174
+ throw new Error('GitHub authentication failed. Ensure your token has the "repo" scope for environment secrets access.');
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+ /**
180
+ * Sets (creates or updates) a repository secret
181
+ * @param client - Octokit instance
182
+ * @param owner - Repository owner
183
+ * @param repo - Repository name
184
+ * @param secretName - Name of the secret (e.g., AWS_ACCESS_KEY_ID_DEV)
185
+ * @param secretValue - Plain text secret value
186
+ */
187
+ export async function setRepositorySecret(client, owner, repo, secretName, secretValue) {
188
+ try {
189
+ // Get the repository's public key
190
+ const publicKey = await getRepositoryPublicKey(client, owner, repo);
191
+ // Encrypt the secret value
192
+ const encryptedValue = await encryptSecret(publicKey.key, secretValue);
193
+ // Set the secret
194
+ await client.actions.createOrUpdateRepoSecret({
195
+ owner,
196
+ repo,
197
+ secret_name: secretName,
198
+ encrypted_value: encryptedValue,
199
+ key_id: publicKey.keyId,
200
+ });
201
+ }
202
+ catch (error) {
203
+ if (error instanceof Error && 'status' in error && error.status === 401) {
204
+ throw new Error('GitHub authentication failed. Ensure your token has the "repo" scope for repository secrets access.');
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+ /**
210
+ * Sets AWS credentials as environment secrets for a specific GitHub environment
211
+ *
212
+ * Creates the GitHub environment if it doesn't exist, then sets AWS_ACCESS_KEY_ID
213
+ * and AWS_SECRET_ACCESS_KEY as secrets within that environment.
214
+ *
215
+ * @param client - Octokit instance
216
+ * @param owner - Repository owner
217
+ * @param repo - Repository name
218
+ * @param environment - Environment name (e.g., dev, stage, prod)
219
+ * @param accessKeyId - AWS Access Key ID
220
+ * @param secretAccessKey - AWS Secret Access Key
221
+ */
222
+ export async function setEnvironmentCredentials(client, owner, repo, environment, accessKeyId, secretAccessKey) {
223
+ // Ensure the GitHub environment exists
224
+ console.log(pc.dim(` Creating/verifying "${environment}" environment...`));
225
+ await ensureEnvironmentExists(client, owner, repo, environment);
226
+ // Set AWS credentials as environment secrets (same names in each environment)
227
+ console.log(pc.dim(` Setting AWS_ACCESS_KEY_ID in "${environment}" environment...`));
228
+ await setEnvironmentSecret(client, owner, repo, environment, 'AWS_ACCESS_KEY_ID', accessKeyId);
229
+ console.log(pc.dim(` Setting AWS_SECRET_ACCESS_KEY in "${environment}" environment...`));
230
+ await setEnvironmentSecret(client, owner, repo, environment, 'AWS_SECRET_ACCESS_KEY', secretAccessKey);
231
+ console.log(pc.green(` Credentials set for "${environment}" environment`));
232
+ }
233
+ /**
234
+ * Parses a GitHub repository URL to extract owner and repo
235
+ * @param url - GitHub repository URL in various formats
236
+ * @returns Parsed owner and repo
237
+ * @throws Error if URL format is not recognized
238
+ *
239
+ * Supported formats:
240
+ * - https://github.com/owner/repo
241
+ * - https://github.com/owner/repo.git
242
+ * - git@github.com:owner/repo.git
243
+ * - git@github.com:owner/repo
244
+ * - owner/repo
245
+ */
246
+ export function parseGitHubUrl(url) {
247
+ // Remove trailing slashes and .git suffix
248
+ let cleanUrl = url.trim().replace(/\/$/, '').replace(/\.git$/, '');
249
+ // Format: https://github.com/owner/repo
250
+ const httpsMatch = cleanUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)$/);
251
+ if (httpsMatch) {
252
+ return {
253
+ owner: httpsMatch[1],
254
+ repo: httpsMatch[2],
255
+ };
256
+ }
257
+ // Format: git@github.com:owner/repo
258
+ const sshMatch = cleanUrl.match(/^git@github\.com:([^/]+)\/([^/]+)$/);
259
+ if (sshMatch) {
260
+ return {
261
+ owner: sshMatch[1],
262
+ repo: sshMatch[2],
263
+ };
264
+ }
265
+ // Format: owner/repo
266
+ const shortMatch = cleanUrl.match(/^([^/]+)\/([^/]+)$/);
267
+ if (shortMatch) {
268
+ return {
269
+ owner: shortMatch[1],
270
+ repo: shortMatch[2],
271
+ };
272
+ }
273
+ throw new Error(`Unable to parse GitHub URL: ${url}. ` +
274
+ 'Expected formats: https://github.com/owner/repo, git@github.com:owner/repo, or owner/repo');
275
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './cli.js';
3
+ run().catch((error) => {
4
+ console.error(error);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,3 @@
1
+ import type { PromptObject } from 'prompts';
2
+ export declare const authProviderPrompt: PromptObject;
3
+ export declare const authFeaturesPrompt: PromptObject;
@@ -0,0 +1,23 @@
1
+ import pc from 'picocolors';
2
+ export const authProviderPrompt = {
3
+ type: 'select',
4
+ name: 'authProvider',
5
+ message: 'Authentication provider:',
6
+ choices: [
7
+ { title: `${pc.yellow('○')} None (add later)`, value: 'none' },
8
+ { title: `${pc.cyan('◆')} AWS Cognito`, value: 'cognito' },
9
+ { title: `${pc.magenta('◆')} Auth0`, value: 'auth0' },
10
+ ],
11
+ initial: 0,
12
+ hint: '- Cognito for AWS-native, Auth0 for flexibility'
13
+ };
14
+ export const authFeaturesPrompt = {
15
+ type: (prev) => prev !== 'none' ? 'multiselect' : null,
16
+ name: 'authFeatures',
17
+ message: 'Auth features (space to toggle):',
18
+ choices: [
19
+ { title: 'Social Login (Google, GitHub)', value: 'social-login', selected: false },
20
+ { title: 'Multi-Factor Authentication (MFA)', value: 'mfa', selected: false },
21
+ ],
22
+ hint: '- Email/password always included'
23
+ };
@@ -0,0 +1,2 @@
1
+ import type { PromptObject } from 'prompts';
2
+ export declare const awsRegionPrompt: PromptObject;
@@ -0,0 +1,14 @@
1
+ export const awsRegionPrompt = {
2
+ type: 'select',
3
+ name: 'awsRegion',
4
+ message: 'AWS Region:',
5
+ choices: [
6
+ { title: 'US East (N. Virginia) - us-east-1', value: 'us-east-1' },
7
+ { title: 'US West (Oregon) - us-west-2', value: 'us-west-2' },
8
+ { title: 'EU (Ireland) - eu-west-1', value: 'eu-west-1' },
9
+ { title: 'EU (Frankfurt) - eu-central-1', value: 'eu-central-1' },
10
+ { title: 'Asia Pacific (Tokyo) - ap-northeast-1', value: 'ap-northeast-1' },
11
+ { title: 'Asia Pacific (Sydney) - ap-southeast-2', value: 'ap-southeast-2' }
12
+ ],
13
+ initial: 0
14
+ };
@@ -0,0 +1,2 @@
1
+ import type { PromptObject } from 'prompts';
2
+ export declare const featuresPrompt: PromptObject;
@@ -0,0 +1,10 @@
1
+ export const featuresPrompt = {
2
+ type: 'multiselect',
3
+ name: 'features',
4
+ message: 'Include optional features (space to toggle):',
5
+ choices: [
6
+ { title: 'GitHub Actions (CI/CD workflows)', value: 'github-actions', selected: true },
7
+ { title: 'VSCode Config (settings, extensions)', value: 'vscode-config', selected: true }
8
+ ],
9
+ hint: '- All optional'
10
+ };