create-aws-project 1.5.1 → 1.7.0

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.
@@ -0,0 +1,27 @@
1
+ import * as z from 'zod';
2
+ /**
3
+ * Zod schema for the non-interactive JSON config file for setup-aws-envs.
4
+ * Only `email` is required — all other AWS setup config lives in .aws-starter-config.json.
5
+ */
6
+ export declare const SetupAwsEnvsConfigSchema: z.ZodObject<{
7
+ email: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export type SetupAwsEnvsConfig = z.infer<typeof SetupAwsEnvsConfigSchema>;
10
+ /**
11
+ * Load, validate, and return a SetupAwsEnvsConfig from a JSON config file path.
12
+ * Exits with code 1 and prints all errors if validation fails.
13
+ * Also exits with code 1 if the email does not contain an '@' sign.
14
+ */
15
+ export declare function loadSetupAwsEnvsConfig(configPath: string): SetupAwsEnvsConfig;
16
+ /**
17
+ * Derive per-environment email addresses from a root email.
18
+ * Inserts -{env} between the local part and the domain.
19
+ *
20
+ * Example:
21
+ * deriveEnvironmentEmails('owner@example.com', ['dev', 'stage', 'prod'])
22
+ * → { dev: 'owner-dev@example.com', stage: 'owner-stage@example.com', prod: 'owner-prod@example.com' }
23
+ *
24
+ * Handles plus aliases: 'user+tag@company.com' → 'user+tag-dev@company.com'
25
+ * Handles subdomains: 'admin@sub.example.com' → 'admin-dev@sub.example.com'
26
+ */
27
+ export declare function deriveEnvironmentEmails(rootEmail: string, environments: readonly string[]): Record<string, string>;
@@ -0,0 +1,80 @@
1
+ import * as z from 'zod';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import pc from 'picocolors';
5
+ /**
6
+ * Zod schema for the non-interactive JSON config file for setup-aws-envs.
7
+ * Only `email` is required — all other AWS setup config lives in .aws-starter-config.json.
8
+ */
9
+ export const SetupAwsEnvsConfigSchema = z.object({
10
+ email: z.string().min(1, { message: 'email is required' }),
11
+ });
12
+ /**
13
+ * Load, validate, and return a SetupAwsEnvsConfig from a JSON config file path.
14
+ * Exits with code 1 and prints all errors if validation fails.
15
+ * Also exits with code 1 if the email does not contain an '@' sign.
16
+ */
17
+ export function loadSetupAwsEnvsConfig(configPath) {
18
+ // 1. Resolve path relative to cwd
19
+ const absolutePath = resolve(process.cwd(), configPath);
20
+ // 2. Read file — fail fast if not found or unreadable
21
+ let rawContent;
22
+ try {
23
+ rawContent = readFileSync(absolutePath, 'utf-8');
24
+ }
25
+ catch {
26
+ console.error(pc.red('Error:') + ` Cannot read config file: ${absolutePath}`);
27
+ process.exit(1);
28
+ }
29
+ // 3. Parse JSON — fail fast if invalid
30
+ let rawData;
31
+ try {
32
+ rawData = JSON.parse(rawContent);
33
+ }
34
+ catch {
35
+ console.error(pc.red('Error:') + ` Config file is not valid JSON: ${absolutePath}`);
36
+ process.exit(1);
37
+ }
38
+ // 4. Validate with Zod — collect ALL errors in one pass
39
+ const result = SetupAwsEnvsConfigSchema.safeParse(rawData);
40
+ if (!result.success) {
41
+ console.error(pc.red('Error:') + ' Invalid config file:');
42
+ console.error('');
43
+ for (const issue of result.error.issues) {
44
+ const fieldPath = issue.path.length > 0 ? issue.path.join('.') : '(root)';
45
+ console.error(` ${pc.red('x')} ${fieldPath}: ${issue.message}`);
46
+ }
47
+ console.error('');
48
+ process.exit(1);
49
+ }
50
+ // 5. Additional email format check — must contain '@' for derivation to work correctly
51
+ if (!result.data.email.includes('@')) {
52
+ console.error(pc.red('Error:') + ' Invalid config file:');
53
+ console.error('');
54
+ console.error(` ${pc.red('x')} email: must be a valid email address`);
55
+ console.error('');
56
+ process.exit(1);
57
+ }
58
+ return result.data;
59
+ }
60
+ /**
61
+ * Derive per-environment email addresses from a root email.
62
+ * Inserts -{env} between the local part and the domain.
63
+ *
64
+ * Example:
65
+ * deriveEnvironmentEmails('owner@example.com', ['dev', 'stage', 'prod'])
66
+ * → { dev: 'owner-dev@example.com', stage: 'owner-stage@example.com', prod: 'owner-prod@example.com' }
67
+ *
68
+ * Handles plus aliases: 'user+tag@company.com' → 'user+tag-dev@company.com'
69
+ * Handles subdomains: 'admin@sub.example.com' → 'admin-dev@sub.example.com'
70
+ */
71
+ export function deriveEnvironmentEmails(rootEmail, environments) {
72
+ const atIndex = rootEmail.lastIndexOf('@');
73
+ const localPart = rootEmail.slice(0, atIndex);
74
+ const domain = rootEmail.slice(atIndex); // includes '@'
75
+ const derived = {};
76
+ for (const env of environments) {
77
+ derived[env] = `${localPart}-${env}${domain}`;
78
+ }
79
+ return derived;
80
+ }
@@ -0,0 +1,48 @@
1
+ import * as z from 'zod';
2
+ import type { ProjectConfig } from '../types.js';
3
+ /**
4
+ * Zod schema for the non-interactive JSON config file.
5
+ * Only `name` is required; all other fields have defaults matching NI-04 spec.
6
+ */
7
+ export declare const NonInteractiveConfigSchema: z.ZodObject<{
8
+ name: z.ZodString;
9
+ platforms: z.ZodDefault<z.ZodArray<z.ZodEnum<{
10
+ web: "web";
11
+ mobile: "mobile";
12
+ api: "api";
13
+ }>>>;
14
+ auth: z.ZodDefault<z.ZodEnum<{
15
+ cognito: "cognito";
16
+ auth0: "auth0";
17
+ none: "none";
18
+ }>>;
19
+ authFeatures: z.ZodDefault<z.ZodArray<z.ZodEnum<{
20
+ "social-login": "social-login";
21
+ mfa: "mfa";
22
+ }>>>;
23
+ features: z.ZodDefault<z.ZodArray<z.ZodEnum<{
24
+ "github-actions": "github-actions";
25
+ "vscode-config": "vscode-config";
26
+ }>>>;
27
+ region: z.ZodDefault<z.ZodEnum<{
28
+ "us-east-1": "us-east-1";
29
+ "us-west-2": "us-west-2";
30
+ "eu-west-1": "eu-west-1";
31
+ "eu-central-1": "eu-central-1";
32
+ "ap-northeast-1": "ap-northeast-1";
33
+ "ap-southeast-2": "ap-southeast-2";
34
+ }>>;
35
+ brandColor: z.ZodDefault<z.ZodEnum<{
36
+ blue: "blue";
37
+ purple: "purple";
38
+ teal: "teal";
39
+ green: "green";
40
+ orange: "orange";
41
+ }>>;
42
+ }, z.core.$strip>;
43
+ export type NonInteractiveConfig = z.infer<typeof NonInteractiveConfigSchema>;
44
+ /**
45
+ * Load, validate, and return a ProjectConfig from a JSON config file path.
46
+ * Exits with code 1 and prints all errors if validation fails.
47
+ */
48
+ export declare function loadNonInteractiveConfig(configPath: string): ProjectConfig;
@@ -0,0 +1,92 @@
1
+ import * as z from 'zod';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import pc from 'picocolors';
5
+ import { validateProjectName } from '../validation/project-name.js';
6
+ // Valid value constants (as const tuples for Zod enum compatibility)
7
+ const VALID_PLATFORMS = ['web', 'mobile', 'api'];
8
+ const VALID_AUTH_PROVIDERS = ['none', 'cognito', 'auth0'];
9
+ const VALID_AUTH_FEATURES = ['social-login', 'mfa'];
10
+ const VALID_FEATURES = ['github-actions', 'vscode-config'];
11
+ const VALID_REGIONS = [
12
+ 'us-east-1',
13
+ 'us-west-2',
14
+ 'eu-west-1',
15
+ 'eu-central-1',
16
+ 'ap-northeast-1',
17
+ 'ap-southeast-2',
18
+ ];
19
+ const VALID_BRAND_COLORS = ['blue', 'purple', 'teal', 'green', 'orange'];
20
+ /**
21
+ * Zod schema for the non-interactive JSON config file.
22
+ * Only `name` is required; all other fields have defaults matching NI-04 spec.
23
+ */
24
+ export const NonInteractiveConfigSchema = z.object({
25
+ name: z.string().min(1, { message: 'name is required' }),
26
+ platforms: z.array(z.enum(VALID_PLATFORMS)).min(1).default(['web', 'api']),
27
+ auth: z.enum(VALID_AUTH_PROVIDERS).default('none'),
28
+ authFeatures: z.array(z.enum(VALID_AUTH_FEATURES)).default([]),
29
+ features: z.array(z.enum(VALID_FEATURES)).default(['github-actions', 'vscode-config']),
30
+ region: z.enum(VALID_REGIONS).default('us-east-1'),
31
+ brandColor: z.enum(VALID_BRAND_COLORS).default('blue'),
32
+ });
33
+ /**
34
+ * Load, validate, and return a ProjectConfig from a JSON config file path.
35
+ * Exits with code 1 and prints all errors if validation fails.
36
+ */
37
+ export function loadNonInteractiveConfig(configPath) {
38
+ // 1. Resolve path relative to cwd
39
+ const absolutePath = resolve(process.cwd(), configPath);
40
+ // 2. Read file — fail fast if not found or unreadable
41
+ let rawContent;
42
+ try {
43
+ rawContent = readFileSync(absolutePath, 'utf-8');
44
+ }
45
+ catch {
46
+ console.error(pc.red('Error:') + ` Cannot read config file: ${absolutePath}`);
47
+ process.exit(1);
48
+ }
49
+ // 3. Parse JSON — fail fast if invalid
50
+ let rawData;
51
+ try {
52
+ rawData = JSON.parse(rawContent);
53
+ }
54
+ catch {
55
+ console.error(pc.red('Error:') + ` Config file is not valid JSON: ${absolutePath}`);
56
+ process.exit(1);
57
+ }
58
+ // 4. Validate with Zod — collect ALL errors in one pass
59
+ const result = NonInteractiveConfigSchema.safeParse(rawData);
60
+ if (!result.success) {
61
+ console.error(pc.red('Error:') + ' Invalid config file:');
62
+ console.error('');
63
+ for (const issue of result.error.issues) {
64
+ const fieldPath = issue.path.length > 0 ? issue.path.join('.') : '(root)';
65
+ console.error(` ${pc.red('x')} ${fieldPath}: ${issue.message}`);
66
+ }
67
+ console.error('');
68
+ process.exit(1);
69
+ }
70
+ const cfg = result.data;
71
+ // 5. Additional project name validation via existing npm package name validator
72
+ const nameValidation = validateProjectName(cfg.name);
73
+ if (nameValidation !== true) {
74
+ console.error(pc.red('Error:') + ' Invalid config file:');
75
+ console.error('');
76
+ console.error(` ${pc.red('x')} name: ${nameValidation}`);
77
+ console.error('');
78
+ process.exit(1);
79
+ }
80
+ // 6. Map to ProjectConfig — silently drop authFeatures when auth is 'none'
81
+ return {
82
+ projectName: cfg.name,
83
+ platforms: cfg.platforms,
84
+ awsRegion: cfg.region,
85
+ features: cfg.features,
86
+ brandColor: cfg.brandColor,
87
+ auth: {
88
+ provider: cfg.auth,
89
+ features: cfg.auth === 'none' ? [] : cfg.authFeatures,
90
+ },
91
+ };
92
+ }
@@ -19,10 +19,10 @@ export declare function promptGitSetup(): Promise<{
19
19
  pat: string;
20
20
  } | null>;
21
21
  /**
22
- * Sets up git repository with initial commit and pushes to GitHub
23
- * Creates the remote repository if it doesn't exist
22
+ * Sets up local git repository and ensures GitHub remote exists
23
+ * Does NOT commit or push code is pushed after AWS accounts are configured
24
24
  * @param projectDir - Path to the project directory
25
25
  * @param repoUrl - GitHub repository URL
26
- * @param pat - GitHub Personal Access Token
26
+ * @param pat - GitHub Personal Access Token (used to create repo if needed)
27
27
  */
28
28
  export declare function setupGitRepository(projectDir: string, repoUrl: string, pat: string): Promise<void>;
package/dist/git/setup.js CHANGED
@@ -83,11 +83,11 @@ export async function promptGitSetup() {
83
83
  return { repoUrl, pat: patResponse.pat };
84
84
  }
85
85
  /**
86
- * Sets up git repository with initial commit and pushes to GitHub
87
- * Creates the remote repository if it doesn't exist
86
+ * Sets up local git repository and ensures GitHub remote exists
87
+ * Does NOT commit or push code is pushed after AWS accounts are configured
88
88
  * @param projectDir - Path to the project directory
89
89
  * @param repoUrl - GitHub repository URL
90
- * @param pat - GitHub Personal Access Token
90
+ * @param pat - GitHub Personal Access Token (used to create repo if needed)
91
91
  */
92
92
  export async function setupGitRepository(projectDir, repoUrl, pat) {
93
93
  const spinner = ora();
@@ -99,19 +99,8 @@ export async function setupGitRepository(projectDir, repoUrl, pat) {
99
99
  // Git init
100
100
  spinner.start('Initializing git repository...');
101
101
  execSync('git init -b main', { cwd: projectDir, stdio: 'pipe' });
102
- // Check for git user config, set if not configured
103
- try {
104
- execSync('git config user.name', { cwd: projectDir, stdio: 'pipe' });
105
- }
106
- catch {
107
- // User config not set, use defaults
108
- execSync('git config user.name "create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
109
- execSync('git config user.email "noreply@create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
110
- }
111
- execSync('git add .', { cwd: projectDir, stdio: 'pipe' });
112
- execSync('git commit -m "Initial commit from create-aws-project"', { cwd: projectDir, stdio: 'pipe' });
113
102
  spinner.succeed('Git repository initialized');
114
- // Ensure remote repo exists
103
+ // Ensure remote repo exists on GitHub
115
104
  spinner.start('Checking GitHub repository...');
116
105
  try {
117
106
  await octokit.rest.repos.get({ owner, repo });
@@ -144,22 +133,37 @@ export async function setupGitRepository(projectDir, repoUrl, pat) {
144
133
  throw error;
145
134
  }
146
135
  }
147
- // Push to remote
148
- spinner.start('Pushing to GitHub...');
149
- const authUrl = `https://${pat}@github.com/${owner}/${repo}.git`;
150
- execSync(`git remote add origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
151
- execSync('git push -u origin main', { cwd: projectDir, stdio: 'pipe' });
152
- // CRITICAL: Remove PAT from .git/config
136
+ // Set origin remote (clean URL, no PAT embedded)
137
+ spinner.start('Setting remote origin...');
153
138
  const cleanUrl = `https://github.com/${owner}/${repo}.git`;
154
- execSync(`git remote set-url origin ${cleanUrl}`, { cwd: projectDir, stdio: 'pipe' });
155
- spinner.succeed(`Pushed to ${owner}/${repo}`);
139
+ execSync(`git remote add origin ${cleanUrl}`, { cwd: projectDir, stdio: 'pipe' });
140
+ spinner.succeed(`Origin set to ${owner}/${repo}`);
141
+ // Guide user on next steps
142
+ console.log('');
143
+ console.log(pc.dim('Code will be committed and pushed after AWS setup is complete.'));
144
+ console.log(pc.dim('Next: run setup-aws-envs to configure AWS accounts.'));
156
145
  }
157
146
  catch (error) {
158
147
  // Git setup failure should not prevent the user from using their project
159
148
  if (spinner.isSpinning) {
160
149
  spinner.fail();
161
150
  }
162
- console.log(pc.yellow('Warning:') + ' Git setup failed: ' + (error instanceof Error ? error.message : 'Unknown error'));
151
+ // Detect GitHub auth/permission errors and give actionable guidance
152
+ if (error?.status === 401 || error?.status === 403 || error?.message?.includes('Bad credentials')) {
153
+ console.log('');
154
+ console.log(pc.red('GitHub authentication failed.'));
155
+ console.log('');
156
+ console.log('Create a Personal Access Token at:');
157
+ console.log(` ${pc.cyan('https://github.com/settings/tokens/new')}`);
158
+ console.log('');
159
+ console.log('Required scopes:');
160
+ console.log(` ${pc.bold('repo')} — full repository access`);
161
+ console.log('');
162
+ console.log('Then re-run project creation and enter the new token.');
163
+ }
164
+ else {
165
+ console.log(pc.yellow('Warning:') + ' Git setup failed: ' + (error instanceof Error ? error.message : 'Unknown error'));
166
+ }
163
167
  console.log(pc.dim('Your project was created successfully. You can set up git manually.'));
164
168
  }
165
169
  }
@@ -9,6 +9,15 @@
9
9
  * This file is generated by the wizard and contains project settings
10
10
  */
11
11
  export declare const CONFIG_FILE = ".aws-starter-config.json";
12
+ /**
13
+ * Deployment credentials for a single environment
14
+ * Created by setup-aws-envs, consumed by initialize-github
15
+ */
16
+ export interface DeploymentCredentials {
17
+ userName: string;
18
+ accessKeyId: string;
19
+ secretAccessKey: string;
20
+ }
12
21
  /**
13
22
  * Minimal project config structure for context detection
14
23
  * Full config defined in types.ts - this is subset needed for context
@@ -20,6 +29,11 @@ export interface ProjectConfigMinimal {
20
29
  configVersion?: string;
21
30
  accounts?: Record<string, string>;
22
31
  deploymentUsers?: Record<string, string>;
32
+ deploymentCredentials?: Record<string, DeploymentCredentials>;
33
+ adminUser?: {
34
+ userName: string;
35
+ accessKeyId: string;
36
+ };
23
37
  }
24
38
  /**
25
39
  * Project context containing config path, project root, and parsed config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-aws-project",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "CLI tool to scaffold AWS projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,11 +52,12 @@
52
52
  "@aws-sdk/credential-providers": "^3.971.0",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "find-up": "^8.0.0",
55
+ "libsodium-wrappers": "^0.7.15",
55
56
  "ora": "^9.1.0",
56
57
  "picocolors": "^1.1.1",
57
58
  "prompts": "^2.4.2",
58
- "libsodium-wrappers": "^0.7.15",
59
- "validate-npm-package-name": "^7.0.2"
59
+ "validate-npm-package-name": "^7.0.2",
60
+ "zod": "^4.3.6"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/jest": "^30.0.0",
@@ -41,8 +41,10 @@ function App() {
41
41
 
42
42
  // Fetch users on mount
43
43
  useEffect(() => {
44
- handleFetchUsers();
45
- }, []);
44
+ if (isAuthenticated) {
45
+ handleFetchUsers();
46
+ }
47
+ }, [isAuthenticated]);
46
48
 
47
49
  const handleLoadDemoUser = () => {
48
50
  const demoUser: User = {
@@ -15,15 +15,9 @@ jest.mock('../config/api', () => ({
15
15
  }));
16
16
 
17
17
  // Mock useAuth hook to provide auth context for tests
18
+ const mockUseAuth = jest.fn();
18
19
  jest.mock('../auth/use-auth', () => ({
19
- useAuth: () => ({
20
- user: null,
21
- isAuthenticated: false,
22
- isLoading: false,
23
- login: jest.fn(),
24
- logout: jest.fn(),
25
- getAccessToken: jest.fn().mockResolvedValue(null),
26
- }),
20
+ useAuth: (...args: unknown[]) => mockUseAuth(...args),
27
21
  }));
28
22
 
29
23
  const mockApiClient = apiClient as jest.Mocked<typeof apiClient>;
@@ -51,6 +45,15 @@ describe('App', () => {
51
45
  });
52
46
  // Reset mocks
53
47
  jest.clearAllMocks();
48
+ // Default: not authenticated
49
+ mockUseAuth.mockReturnValue({
50
+ user: null,
51
+ isAuthenticated: false,
52
+ isLoading: false,
53
+ signIn: jest.fn(),
54
+ signOut: jest.fn(),
55
+ getAccessToken: jest.fn().mockResolvedValue(null),
56
+ });
54
57
  // Default mock implementations
55
58
  mockApiClient.getUsers.mockResolvedValue([]);
56
59
  });
@@ -217,7 +220,16 @@ describe('App', () => {
217
220
  });
218
221
  });
219
222
 
220
- it('should fetch users on mount and display count', async () => {
223
+ it('should fetch users on mount when authenticated', async () => {
224
+ mockUseAuth.mockReturnValue({
225
+ user: { email: 'test@example.com' },
226
+ isAuthenticated: true,
227
+ isLoading: false,
228
+ signIn: jest.fn(),
229
+ signOut: jest.fn(),
230
+ getAccessToken: jest.fn().mockResolvedValue('token'),
231
+ });
232
+
221
233
  const mockUsers = [
222
234
  { id: '1', email: 'user1@example.com', name: 'User 1', createdAt: '2024-01-01' },
223
235
  { id: '2', email: 'user2@example.com', name: 'User 2', createdAt: '2024-01-02' },
@@ -236,6 +248,12 @@ describe('App', () => {
236
248
  });
237
249
  });
238
250
 
251
+ it('should not fetch users on mount when not authenticated', async () => {
252
+ await renderWithChakra(<App />);
253
+
254
+ expect(mockApiClient.getUsers).not.toHaveBeenCalled();
255
+ });
256
+
239
257
  it('should display error message when error state is set', async () => {
240
258
  // Set error state before rendering
241
259
  useUserStore.setState({
@@ -15,6 +15,9 @@ export default defineConfig(({ mode }) => {
15
15
  strictPort: false, // Try next port if 3000 is taken
16
16
  open: false, // Don't auto-open browser
17
17
  cors: true,
18
+ fs: {
19
+ allow: [path.resolve(__dirname, '../..')],
20
+ },
18
21
  // Proxy API requests to backend during development
19
22
  // Note: Vite automatically loads VITE_ prefixed env vars
20
23
  proxy: mode === 'development' ? {