@sembix/cli 0.1.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.
Files changed (61) hide show
  1. package/.env.example +6 -0
  2. package/LICENSE +15 -0
  3. package/README.md +698 -0
  4. package/config.example.yaml +118 -0
  5. package/dist/commands/hub-integration.d.ts +3 -0
  6. package/dist/commands/hub-integration.d.ts.map +1 -0
  7. package/dist/commands/hub-integration.js +107 -0
  8. package/dist/commands/hub-integration.js.map +1 -0
  9. package/dist/commands/setup.d.ts +3 -0
  10. package/dist/commands/setup.d.ts.map +1 -0
  11. package/dist/commands/setup.js +282 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/commands/update.d.ts +3 -0
  14. package/dist/commands/update.d.ts.map +1 -0
  15. package/dist/commands/update.js +498 -0
  16. package/dist/commands/update.js.map +1 -0
  17. package/dist/config-schema.d.ts +417 -0
  18. package/dist/config-schema.d.ts.map +1 -0
  19. package/dist/config-schema.js +152 -0
  20. package/dist/config-schema.js.map +1 -0
  21. package/dist/config.d.ts +6 -0
  22. package/dist/config.d.ts.map +1 -0
  23. package/dist/config.js +17 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +260 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/prompts/environment-setup.d.ts +4 -0
  30. package/dist/prompts/environment-setup.d.ts.map +1 -0
  31. package/dist/prompts/environment-setup.js +689 -0
  32. package/dist/prompts/environment-setup.js.map +1 -0
  33. package/dist/prompts/hub-integration-step.d.ts +21 -0
  34. package/dist/prompts/hub-integration-step.d.ts.map +1 -0
  35. package/dist/prompts/hub-integration-step.js +104 -0
  36. package/dist/prompts/hub-integration-step.js.map +1 -0
  37. package/dist/prompts/hub-integration.d.ts +4 -0
  38. package/dist/prompts/hub-integration.d.ts.map +1 -0
  39. package/dist/prompts/hub-integration.js +141 -0
  40. package/dist/prompts/hub-integration.js.map +1 -0
  41. package/dist/prompts/prompt-helpers.d.ts +18 -0
  42. package/dist/prompts/prompt-helpers.d.ts.map +1 -0
  43. package/dist/prompts/prompt-helpers.js +50 -0
  44. package/dist/prompts/prompt-helpers.js.map +1 -0
  45. package/dist/types.d.ts +144 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +23 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/utils/config-loader.d.ts +34 -0
  50. package/dist/utils/config-loader.d.ts.map +1 -0
  51. package/dist/utils/config-loader.js +377 -0
  52. package/dist/utils/config-loader.js.map +1 -0
  53. package/dist/utils/github.d.ts +38 -0
  54. package/dist/utils/github.d.ts.map +1 -0
  55. package/dist/utils/github.js +188 -0
  56. package/dist/utils/github.js.map +1 -0
  57. package/dist/utils/ui.d.ts +18 -0
  58. package/dist/utils/ui.d.ts.map +1 -0
  59. package/dist/utils/ui.js +62 -0
  60. package/dist/utils/ui.js.map +1 -0
  61. package/package.json +80 -0
@@ -0,0 +1,689 @@
1
+ import { input, select, confirm } from '@inquirer/prompts';
2
+ import { awsRegions, awsAccountIdSchema, iamRoleArnSchema, acmCertArnSchema, route53ZoneIdSchema } from '../types.js';
3
+ import * as ui from '../utils/ui.js';
4
+ import chalk from 'chalk';
5
+ import { promptIfMissing } from './prompt-helpers.js';
6
+ /**
7
+ * Helper function to display information before a prompt
8
+ */
9
+ function explainField(title, description, example) {
10
+ console.log();
11
+ console.log(chalk.cyan('❓ ' + chalk.bold(title)));
12
+ console.log(chalk.dim(' ' + description));
13
+ if (example) {
14
+ console.log(chalk.yellow(' Example: ') + chalk.white(example));
15
+ }
16
+ console.log();
17
+ }
18
+ export async function promptEnvironmentSetup(githubClient, providedEnvironmentName, providedRepo, partialConfig, isUpdate = false) {
19
+ ui.section('Step 1: Repository Selection');
20
+ console.log(chalk.dim('📦 Select the GitHub repository where your Sembix Studio deployment will be configured.'));
21
+ console.log(chalk.dim(' This repository contains your deployment workflows and configuration files.'));
22
+ console.log();
23
+ // Step 1: Repository Selection
24
+ let repositoryChoice;
25
+ let owner;
26
+ let repo;
27
+ if (providedRepo) {
28
+ // Repository provided via CLI argument - validate it exists
29
+ const parts = providedRepo.split('/');
30
+ if (parts.length !== 2) {
31
+ ui.error('Invalid repository format. Use: owner/repo');
32
+ process.exit(1);
33
+ }
34
+ [owner, repo] = parts;
35
+ // Verify repository exists
36
+ try {
37
+ await githubClient.getRepository(owner, repo);
38
+ repositoryChoice = providedRepo;
39
+ ui.info(`Using repository: ${ui.highlight(repositoryChoice)}`);
40
+ console.log();
41
+ }
42
+ catch {
43
+ ui.error(`Repository '${providedRepo}' not found or not accessible.`);
44
+ console.log();
45
+ console.log(ui.dim('💡 Make sure:'));
46
+ console.log(ui.dim(' • The repository name is correct (owner/repo)'));
47
+ console.log(ui.dim(' • Your GitHub token has access to this repository'));
48
+ process.exit(1);
49
+ }
50
+ }
51
+ else {
52
+ // Interactive repository selection
53
+ const repos = await githubClient.listRepositories();
54
+ if (repos.length === 0) {
55
+ ui.error('No repositories found for your account.');
56
+ console.log();
57
+ console.log(ui.dim('💡 Tip: Make sure your GitHub token has access to the repositories you want to manage.'));
58
+ process.exit(1);
59
+ }
60
+ if (repos.length === 1) {
61
+ repositoryChoice = repos[0].full_name;
62
+ ui.info(`Using repository: ${ui.highlight(repositoryChoice)}`);
63
+ console.log();
64
+ }
65
+ else {
66
+ repositoryChoice = await select({
67
+ message: 'Select your deployment repository:',
68
+ choices: repos.map(repo => ({
69
+ name: repo.full_name,
70
+ value: repo.full_name,
71
+ })),
72
+ });
73
+ }
74
+ [owner, repo] = repositoryChoice.split('/');
75
+ }
76
+ // ============================================================
77
+ // STEP 2: BASIC CONFIGURATION
78
+ // ============================================================
79
+ ui.section('Step 2: Basic Configuration');
80
+ console.log(chalk.dim('⚙️ Configure the basic settings for your new Sembix Studio environment.'));
81
+ console.log(chalk.dim(' These settings tell us where to deploy and how to connect to AWS.'));
82
+ console.log();
83
+ // Environment Name
84
+ let environmentName;
85
+ if (providedEnvironmentName) {
86
+ // Environment name provided via CLI argument - validate it
87
+ if (!/^[a-z0-9-]+$/.test(providedEnvironmentName) || providedEnvironmentName.length < 3) {
88
+ ui.error('Invalid environment name. Must be lowercase letters, numbers, and hyphens (min 3 chars).');
89
+ process.exit(1);
90
+ }
91
+ // Check if environment already exists (only for create, not update)
92
+ if (!isUpdate) {
93
+ const envExists = await githubClient.environmentExists(owner, repo, providedEnvironmentName);
94
+ if (envExists) {
95
+ ui.error(`Environment '${providedEnvironmentName}' already exists in ${owner}/${repo}`);
96
+ console.log();
97
+ console.log(ui.dim('💡 Tip:'));
98
+ console.log(ui.dim(' • Use a different environment name'));
99
+ console.log(ui.dim(' • Or update the existing environment with: sembix studio update'));
100
+ process.exit(1);
101
+ }
102
+ }
103
+ environmentName = providedEnvironmentName;
104
+ ui.info(`Using environment name: ${ui.highlight(environmentName)}`);
105
+ console.log();
106
+ }
107
+ else {
108
+ // Interactive environment name prompt
109
+ explainField('Environment Name', 'This is the name of your GitHub Actions environment. It should be unique and descriptive.\nUse lowercase letters, numbers, and hyphens only (no spaces or special characters).', 'client-abc-production or acme-staging');
110
+ environmentName = await input({
111
+ message: 'Environment name:',
112
+ validate: (value) => {
113
+ if (!value)
114
+ return 'Environment name is required';
115
+ if (!/^[a-z0-9-]+$/.test(value)) {
116
+ return 'Must be lowercase letters, numbers, and hyphens only (no spaces)';
117
+ }
118
+ if (value.length < 3)
119
+ return 'Must be at least 3 characters long';
120
+ return true;
121
+ },
122
+ });
123
+ }
124
+ // AWS Account ID
125
+ const awsAccountId = await promptIfMissing(partialConfig?.awsAccountId, async () => {
126
+ explainField('AWS Account ID', 'This is your 12-digit AWS account number where Sembix Studio will be deployed.\nYou can find this in the AWS Console by clicking on your account name in the top right.', '123456789012');
127
+ return input({
128
+ message: 'AWS Account ID (12 digits):',
129
+ validate: (value) => {
130
+ if (!value)
131
+ return 'AWS Account ID is required';
132
+ const result = awsAccountIdSchema.safeParse(value);
133
+ if (!result.success)
134
+ return 'Must be exactly 12 digits';
135
+ return true;
136
+ },
137
+ });
138
+ }, 'AWS Account ID');
139
+ // AWS Region
140
+ const awsRegion = await promptIfMissing(partialConfig?.awsRegion, async () => {
141
+ explainField('AWS Region', 'Choose the AWS region where you want to deploy Sembix Studio.\nPick a region close to your users for better performance.', 'us-east-1 (Virginia) or eu-west-1 (Ireland)');
142
+ return select({
143
+ message: 'AWS Region:',
144
+ choices: awsRegions.map(region => ({
145
+ name: region,
146
+ value: region,
147
+ })),
148
+ default: 'us-east-1',
149
+ });
150
+ }, 'AWS Region');
151
+ // GitHub Actions Role ARN
152
+ const customerRoleArn = await promptIfMissing(partialConfig?.customerRoleArn, async () => {
153
+ explainField('GitHub Actions Deployment Role ARN', 'This is the IAM role that GitHub Actions will assume to deploy infrastructure to your AWS account.\nIt must have permissions to create and manage AWS resources (VPCs, ECS, RDS, etc.).', 'arn:aws:iam::123456789012:role/GitHubActionsDeployRole');
154
+ return input({
155
+ message: 'GitHub Actions Role ARN:',
156
+ validate: (value) => {
157
+ if (!value)
158
+ return 'Role ARN is required';
159
+ const result = iamRoleArnSchema.safeParse(value);
160
+ if (!result.success)
161
+ return 'Must be a valid IAM role ARN (arn:aws:iam::ACCOUNT:role/NAME)';
162
+ return true;
163
+ },
164
+ });
165
+ }, 'GitHub Actions Role ARN');
166
+ // Terraform State Bucket
167
+ const terraformStateBucket = await promptIfMissing(partialConfig?.terraformStateBucket, async () => {
168
+ explainField('Terraform State S3 Bucket', 'This is the S3 bucket where Terraform will store its state file.\nThe state file keeps track of all the infrastructure Terraform creates.\nMake sure this bucket already exists in your AWS account.', 'my-terraform-state-bucket');
169
+ return input({
170
+ message: 'Terraform State S3 Bucket name:',
171
+ validate: (value) => {
172
+ if (!value)
173
+ return 'Bucket name is required';
174
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(value)) {
175
+ return 'Bucket name must be lowercase letters, numbers, and hyphens';
176
+ }
177
+ return true;
178
+ },
179
+ });
180
+ }, 'Terraform State Bucket');
181
+ // ============================================================
182
+ // STEP 3: DATABASE CONFIGURATION
183
+ // ============================================================
184
+ let databaseName;
185
+ let databaseUser;
186
+ if (partialConfig?.database?.name && partialConfig?.database?.user) {
187
+ // Both database fields provided - skip section
188
+ databaseName = partialConfig.database.name;
189
+ databaseUser = partialConfig.database.user;
190
+ ui.info(`Using Database: ${ui.highlight(databaseName)} / ${ui.highlight(databaseUser)} (from config)`);
191
+ console.log();
192
+ }
193
+ else {
194
+ ui.section('Step 3: Database Configuration');
195
+ console.log(chalk.dim('🗄️ Configure the PostgreSQL database settings for Sembix Studio.'));
196
+ console.log(chalk.dim(' The database stores all your Studio data (workflows, users, etc.).'));
197
+ console.log();
198
+ // Database Name
199
+ databaseName = await promptIfMissing(partialConfig?.database?.name, async () => {
200
+ explainField('Database Name', 'The name of the PostgreSQL database that will be created for Sembix Studio.\nMust start with a letter and contain only letters, numbers, and underscores.', 'sembix_studio or studio_production');
201
+ return input({
202
+ message: 'Database name:',
203
+ default: 'sembix_studio',
204
+ validate: (value) => {
205
+ if (!value)
206
+ return 'Database name is required';
207
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) {
208
+ return 'Must start with a letter and contain only letters, numbers, and underscores';
209
+ }
210
+ return true;
211
+ },
212
+ });
213
+ }, 'Database name');
214
+ // Database User
215
+ databaseUser = await promptIfMissing(partialConfig?.database?.user, async () => {
216
+ explainField('Database User', 'The PostgreSQL username that Sembix Studio will use to connect to the database.\nMust start with a letter and contain only letters, numbers, and underscores.', 'sembix_studio_user or studio_app');
217
+ return input({
218
+ message: 'Database user:',
219
+ default: 'sembix_studio_user',
220
+ validate: (value) => {
221
+ if (!value)
222
+ return 'Database user is required';
223
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) {
224
+ return 'Must start with a letter and contain only letters, numbers, and underscores';
225
+ }
226
+ return true;
227
+ },
228
+ });
229
+ }, 'Database user');
230
+ }
231
+ // ============================================================
232
+ // STEP 4: NETWORKING & INFRASTRUCTURE
233
+ // ============================================================
234
+ ui.section('Step 4: Networking & Infrastructure');
235
+ console.log(chalk.dim('🌐 Configure networking settings for your Sembix Studio deployment.'));
236
+ console.log(chalk.dim(' You can either create a new VPC or use an existing one.'));
237
+ console.log();
238
+ // VPC Endpoints
239
+ explainField('VPC Endpoints', 'VPC endpoints allow your AWS resources to communicate with AWS services privately (without going through the internet).\nThis improves security and can reduce data transfer costs.\n\n' +
240
+ chalk.green('✓ Recommended: ') + 'Enable this unless you have a specific reason not to.', 'Choose Yes (recommended)');
241
+ const enableVpcEndpoints = await confirm({
242
+ message: 'Enable VPC endpoints for AWS services?',
243
+ default: true,
244
+ });
245
+ // Custom Networking
246
+ console.log();
247
+ console.log(chalk.cyan('❓ ' + chalk.bold('Use Existing VPC?')));
248
+ console.log(chalk.dim(' You have two options:'));
249
+ console.log(chalk.dim(' • ' + chalk.white('No (Create New)') + ' - We\'ll create a brand new VPC with all necessary networking (recommended for new deployments)'));
250
+ console.log(chalk.dim(' • ' + chalk.white('Yes (Use Existing)') + ' - You provide an existing VPC and subnets (for integrating with existing infrastructure)'));
251
+ console.log();
252
+ const useCustomNetworking = await confirm({
253
+ message: 'Do you want to use an existing VPC?',
254
+ default: false,
255
+ });
256
+ let networking;
257
+ if (useCustomNetworking) {
258
+ // ============================================================
259
+ // CUSTOM NETWORKING (Existing VPC)
260
+ // ============================================================
261
+ console.log();
262
+ console.log(chalk.yellow('📝 Using Existing VPC'));
263
+ console.log(chalk.dim(' You\'ll need to provide your VPC ID and subnet IDs from the AWS Console.'));
264
+ console.log();
265
+ explainField('VPC ID', 'The ID of your existing Virtual Private Cloud.\nYou can find this in the AWS Console under VPC → Your VPCs.', 'vpc-0123456789abcdef0');
266
+ const customVpcId = await input({
267
+ message: 'VPC ID:',
268
+ validate: (value) => {
269
+ if (!value)
270
+ return 'VPC ID is required when using existing VPC';
271
+ if (!/^vpc-[a-f0-9]+$/.test(value))
272
+ return 'Must be a valid VPC ID (vpc-xxxxx)';
273
+ return true;
274
+ },
275
+ });
276
+ explainField('Public Subnet IDs', 'Public subnets have direct internet access (used for load balancers and NAT gateways).\nProvide a comma-separated list of subnet IDs.', 'subnet-abc123,subnet-def456');
277
+ const customPublicSubnetIds = await input({
278
+ message: 'Public Subnet IDs (comma-separated):',
279
+ validate: (value) => {
280
+ if (!value)
281
+ return 'At least one public subnet is required';
282
+ const subnets = value.split(',').map(s => s.trim());
283
+ if (subnets.some(s => !/^subnet-[a-f0-9]+$/.test(s))) {
284
+ return 'All subnet IDs must be valid (subnet-xxxxx)';
285
+ }
286
+ return true;
287
+ },
288
+ });
289
+ explainField('Private Subnet IDs', 'Private subnets do NOT have direct internet access (used for databases and application servers).\nProvide a comma-separated list of subnet IDs.', 'subnet-ghi789,subnet-jkl012');
290
+ const customPrivateSubnetIds = await input({
291
+ message: 'Private Subnet IDs (comma-separated):',
292
+ validate: (value) => {
293
+ if (!value)
294
+ return 'At least one private subnet is required';
295
+ const subnets = value.split(',').map(s => s.trim());
296
+ if (subnets.some(s => !/^subnet-[a-f0-9]+$/.test(s))) {
297
+ return 'All subnet IDs must be valid (subnet-xxxxx)';
298
+ }
299
+ return true;
300
+ },
301
+ });
302
+ networking = {
303
+ enableVpcEndpoints,
304
+ useCustomNetworking: true,
305
+ customVpcId,
306
+ customPublicSubnetIds,
307
+ customPrivateSubnetIds,
308
+ };
309
+ }
310
+ else {
311
+ // ============================================================
312
+ // NEW VPC CREATION
313
+ // ============================================================
314
+ console.log();
315
+ console.log(chalk.green('✨ Creating New VPC'));
316
+ console.log(chalk.dim(' We\'ll create a new VPC with all necessary networking components.'));
317
+ console.log(chalk.dim(' You can use the default values or customize them.'));
318
+ console.log();
319
+ explainField('VPC CIDR Block', 'This is the IP address range for your entire VPC (in CIDR notation).\nThe default (10.0.0.0/16) gives you 65,536 IP addresses, which is plenty for most deployments.\n\n' +
320
+ chalk.green('💡 Tip: ') + 'Press Enter to use the default unless you have specific networking requirements.', '10.0.0.0/16 (default, recommended)');
321
+ const vpcCidr = await input({
322
+ message: 'VPC CIDR Block:',
323
+ default: '10.0.0.0/16',
324
+ validate: (value) => {
325
+ if (!value)
326
+ return 'VPC CIDR is required';
327
+ if (!/^\d+\.\d+\.\d+\.\d+\/\d+$/.test(value)) {
328
+ return 'Must be a valid CIDR block (e.g., 10.0.0.0/16)';
329
+ }
330
+ return true;
331
+ },
332
+ });
333
+ explainField('Public Subnet CIDRs', 'IP ranges for public subnets (have internet access). Format as a JSON array.\nEach subnet will be created in a different availability zone for high availability.\n\n' +
334
+ chalk.green('💡 Tip: ') + 'Press Enter to use the defaults.', '["10.0.0.0/24","10.0.3.0/24"]');
335
+ const publicSubnetCidrs = await input({
336
+ message: 'Public Subnet CIDRs (JSON array):',
337
+ default: '["10.0.0.0/24","10.0.3.0/24"]',
338
+ validate: (value) => {
339
+ if (!value)
340
+ return 'Public subnet CIDRs are required';
341
+ try {
342
+ const parsed = JSON.parse(value);
343
+ if (!Array.isArray(parsed) || parsed.length === 0) {
344
+ return 'Must be a non-empty JSON array';
345
+ }
346
+ return true;
347
+ }
348
+ catch {
349
+ return 'Must be valid JSON array (e.g., ["10.0.0.0/24","10.0.3.0/24"])';
350
+ }
351
+ },
352
+ });
353
+ explainField('Private Subnet CIDRs', 'IP ranges for private subnets (no direct internet access). Format as a JSON array.\nThese are used for databases and application servers that don\'t need public access.\n\n' +
354
+ chalk.green('💡 Tip: ') + 'Press Enter to use the defaults.', '["10.0.1.0/24","10.0.2.0/24"]');
355
+ const privateSubnetCidrs = await input({
356
+ message: 'Private Subnet CIDRs (JSON array):',
357
+ default: '["10.0.1.0/24","10.0.2.0/24"]',
358
+ validate: (value) => {
359
+ if (!value)
360
+ return 'Private subnet CIDRs are required';
361
+ try {
362
+ const parsed = JSON.parse(value);
363
+ if (!Array.isArray(parsed) || parsed.length === 0) {
364
+ return 'Must be a non-empty JSON array';
365
+ }
366
+ return true;
367
+ }
368
+ catch {
369
+ return 'Must be valid JSON array (e.g., ["10.0.1.0/24","10.0.2.0/24"])';
370
+ }
371
+ },
372
+ });
373
+ explainField('Availability Zones', 'AWS Availability Zones are isolated locations within a region.\nUsing multiple AZs provides high availability and fault tolerance.\n\n' +
374
+ chalk.green('✓ Recommended: ') + '2 AZs is sufficient for most deployments.', '2 (recommended)');
375
+ const azCount = await select({
376
+ message: 'Number of Availability Zones:',
377
+ choices: [
378
+ { name: '2 (recommended - good balance of cost and availability)', value: '2' },
379
+ { name: '3 (maximum availability)', value: '3' },
380
+ ],
381
+ default: '2',
382
+ });
383
+ networking = {
384
+ enableVpcEndpoints,
385
+ useCustomNetworking: false,
386
+ vpcCidr,
387
+ publicSubnetCidrs,
388
+ privateSubnetCidrs,
389
+ azCount,
390
+ };
391
+ }
392
+ // ============================================================
393
+ // STEP 5: SECURITY & IAM (OPTIONAL)
394
+ // ============================================================
395
+ ui.section('Step 5: Security & IAM (Optional)');
396
+ console.log(chalk.dim('🔒 Configure advanced security settings.'));
397
+ console.log(chalk.dim(' These are optional. Most users can skip this section by pressing Enter.'));
398
+ console.log();
399
+ // KMS Key
400
+ explainField('Workflow Runs KMS Key (Optional)', 'AWS KMS (Key Management Service) key for encrypting workflow execution data.\nLeave blank to use AWS-managed encryption (recommended for most users).\n\n' +
401
+ chalk.green('💡 Tip: ') + 'Press Enter to skip unless you have specific compliance requirements.', 'alias/my-kms-key or leave blank');
402
+ const workflowRunsKeyAlias = await input({
403
+ message: 'Workflow Runs KMS Key Alias (press Enter to skip):',
404
+ default: '',
405
+ });
406
+ // Custom Security Groups
407
+ console.log();
408
+ console.log(chalk.cyan('❓ ' + chalk.bold('Custom Security Groups')));
409
+ console.log(chalk.dim(' Security groups control network traffic to/from AWS resources.'));
410
+ console.log(chalk.dim(' • ' + chalk.white('No') + ' - We\'ll create new security groups with proper rules (recommended)'));
411
+ console.log(chalk.dim(' • ' + chalk.white('Yes') + ' - You provide existing security group IDs (for advanced users)'));
412
+ console.log();
413
+ const useCustomSecurityGroups = await confirm({
414
+ message: 'Use custom security groups?',
415
+ default: false,
416
+ });
417
+ let customSecurityGroups = undefined;
418
+ if (useCustomSecurityGroups) {
419
+ console.log();
420
+ console.log(chalk.yellow('📝 Custom Security Groups'));
421
+ console.log(chalk.dim(' Provide the security group IDs from AWS Console.'));
422
+ console.log();
423
+ customSecurityGroups = {
424
+ workflowEngine: await input({
425
+ message: 'Workflow Engine Security Group ID:',
426
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
427
+ }),
428
+ aurora: await input({
429
+ message: 'Aurora Database Security Group ID:',
430
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
431
+ }),
432
+ rdsProxy: await input({
433
+ message: 'RDS Proxy Security Group ID:',
434
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
435
+ }),
436
+ bffEcs: await input({
437
+ message: 'BFF ECS Service Security Group ID:',
438
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
439
+ }),
440
+ bastion: await input({
441
+ message: 'Bastion Host Security Group ID:',
442
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
443
+ }),
444
+ bffAlb: await input({
445
+ message: 'BFF Load Balancer Security Group ID:',
446
+ validate: (v) => !v || /^sg-[a-f0-9]+$/.test(v) || 'Must be valid SG ID (sg-xxxxx)'
447
+ }),
448
+ };
449
+ }
450
+ // Custom IAM Roles
451
+ console.log();
452
+ console.log(chalk.cyan('❓ ' + chalk.bold('Custom IAM Roles')));
453
+ console.log(chalk.dim(' IAM roles define permissions for AWS resources.'));
454
+ console.log(chalk.dim(' • ' + chalk.white('No') + ' - We\'ll create new IAM roles with proper permissions (recommended)'));
455
+ console.log(chalk.dim(' • ' + chalk.white('Yes') + ' - You provide existing IAM role ARNs (for advanced users)'));
456
+ console.log();
457
+ const useCustomIamPolicies = await confirm({
458
+ message: 'Use custom IAM roles?',
459
+ default: false,
460
+ });
461
+ let customIamRoles = undefined;
462
+ if (useCustomIamPolicies) {
463
+ console.log();
464
+ console.log(chalk.yellow('📝 Custom IAM Roles'));
465
+ console.log(chalk.dim(' Provide the IAM role ARNs from AWS Console.'));
466
+ console.log();
467
+ const validateRoleArn = (v) => !v || /^arn:aws:iam::\d{12}:role\/.+$/.test(v) || 'Must be valid IAM role ARN';
468
+ customIamRoles = {
469
+ workflowEngineExecutionRole: await input({
470
+ message: 'Workflow Engine Execution Role ARN:',
471
+ validate: validateRoleArn
472
+ }),
473
+ workflowEngineTaskRole: await input({
474
+ message: 'Workflow Engine Task Role ARN:',
475
+ validate: validateRoleArn
476
+ }),
477
+ bffEcsTaskExecutionRole: await input({
478
+ message: 'BFF ECS Task Execution Role ARN:',
479
+ validate: validateRoleArn
480
+ }),
481
+ bffEcsTaskRole: await input({
482
+ message: 'BFF ECS Task Role ARN:',
483
+ validate: validateRoleArn
484
+ }),
485
+ rdsProxyRole: await input({
486
+ message: 'RDS Proxy Role ARN:',
487
+ validate: validateRoleArn
488
+ }),
489
+ sembixStudioMemoryRole: await input({
490
+ message: 'Sembix Studio Memory Role ARN:',
491
+ validate: validateRoleArn
492
+ }),
493
+ sembixStudioNotificationRole: await input({
494
+ message: 'Sembix Studio Notification Role ARN:',
495
+ validate: validateRoleArn
496
+ }),
497
+ };
498
+ }
499
+ // ============================================================
500
+ // STEP 6: DNS & TLS CONFIGURATION
501
+ // ============================================================
502
+ ui.section('Step 6: DNS & TLS Configuration');
503
+ console.log(chalk.dim('🔐 Configure custom domains and SSL/TLS certificates for secure HTTPS access.'));
504
+ console.log();
505
+ console.log(chalk.dim('📌 ' + chalk.bold('Architecture Overview:')));
506
+ console.log(chalk.dim(' Sembix Studio has two public-facing components:'));
507
+ console.log(chalk.dim(' 1. CloudFront Distribution → Serves the React frontend (Studio UI)'));
508
+ console.log(chalk.dim(' 2. Application Load Balancer (ALB) → Exposes the BFF API'));
509
+ console.log(chalk.dim(' Both need custom domains and SSL certificates.'));
510
+ console.log();
511
+ // CloudFront Domain
512
+ const cloudfrontDomain = await promptIfMissing(partialConfig?.tls?.cloudfrontDomain, async () => {
513
+ explainField('CloudFront Domain', 'The custom domain name for your Studio UI (the website users will visit).\nThis domain must match the SSL certificate you provide below.\n\n' +
514
+ chalk.green('💡 Example: ') + 'If your company is Acme Corp, you might use: studio.acme.com', 'studio.example.com or studio.acme.com');
515
+ return input({
516
+ message: 'CloudFront Domain:',
517
+ validate: (value) => {
518
+ if (!value)
519
+ return 'CloudFront domain is required';
520
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/.test(value)) {
521
+ return 'Must be a valid domain name';
522
+ }
523
+ return true;
524
+ },
525
+ });
526
+ }, 'CloudFront Domain');
527
+ // CloudFront Certificate
528
+ const cloudfrontCertArn = await promptIfMissing(partialConfig?.tls?.cloudfrontCertArn, async () => {
529
+ explainField('CloudFront SSL Certificate', 'The ACM (AWS Certificate Manager) certificate ARN for your CloudFront domain.\n\n' +
530
+ chalk.red('⚠️ IMPORTANT: ') + 'This certificate ' + chalk.bold('MUST') + ' be in the ' + chalk.bold('us-east-1') + ' region.\n' +
531
+ ' (This is a CloudFront requirement, even if your infrastructure is in a different region)\n\n' +
532
+ chalk.green('💡 Tip: ') + 'Create the certificate in ACM in us-east-1, then copy the ARN here.', 'arn:aws:acm:us-east-1:123456789012:certificate/abc123...');
533
+ return input({
534
+ message: 'CloudFront Certificate ARN (must be in us-east-1):',
535
+ validate: (value) => {
536
+ if (!value)
537
+ return 'CloudFront certificate ARN is required';
538
+ const result = acmCertArnSchema.safeParse(value);
539
+ if (!result.success)
540
+ return 'Must be a valid ACM certificate ARN';
541
+ if (!value.includes('us-east-1')) {
542
+ return chalk.red('ERROR: ') + 'CloudFront certificate MUST be in us-east-1 region (CloudFront requirement)';
543
+ }
544
+ return true;
545
+ },
546
+ });
547
+ }, 'CloudFront Certificate ARN');
548
+ // ALB Certificate
549
+ const bffAlbCertificateArn = await promptIfMissing(partialConfig?.tls?.bffAlbCertificateArn, async () => {
550
+ explainField('BFF ALB SSL Certificate', 'The ACM certificate ARN for your BFF (Backend For Frontend) API load balancer.\nThis certificate should be in the same region as your infrastructure.\n\n' +
551
+ chalk.green('💡 Tip: ') + 'This can be the same domain as CloudFront or a different subdomain (e.g., api.example.com).', 'arn:aws:acm:' + awsRegion + ':123456789012:certificate/def456...');
552
+ return input({
553
+ message: 'BFF ALB Certificate ARN:',
554
+ validate: (value) => {
555
+ if (!value)
556
+ return 'BFF ALB certificate ARN is required';
557
+ const result = acmCertArnSchema.safeParse(value);
558
+ if (!result.success)
559
+ return 'Must be a valid ACM certificate ARN';
560
+ return true;
561
+ },
562
+ });
563
+ }, 'BFF ALB Certificate ARN');
564
+ // Hosted Zone ID
565
+ const hostedZoneId = await promptIfMissing(partialConfig?.tls?.hostedZoneId, async () => {
566
+ explainField('Route53 Hosted Zone ID', 'The ID of your Route53 hosted zone where DNS records will be automatically created.\nYou can find this in the AWS Console under Route53 → Hosted zones.\n\n' +
567
+ chalk.green('💡 Tip: ') + 'The hosted zone should manage the domain you\'re using (e.g., example.com).', 'Z1234567890ABC');
568
+ return input({
569
+ message: 'Route53 Hosted Zone ID:',
570
+ validate: (value) => {
571
+ if (!value)
572
+ return 'Hosted Zone ID is required';
573
+ const result = route53ZoneIdSchema.safeParse(value);
574
+ if (!result.success)
575
+ return 'Must be a valid Route53 hosted zone ID (starts with Z)';
576
+ return true;
577
+ },
578
+ });
579
+ }, 'Route53 Hosted Zone ID');
580
+ // ============================================================
581
+ // STEP 7: FEATURE CONFIGURATION
582
+ // ============================================================
583
+ ui.section('Step 7: Feature Configuration');
584
+ console.log(chalk.dim('🎛️ Enable or disable optional Sembix Studio services.'));
585
+ console.log(chalk.dim(' Most users should enable all features.'));
586
+ console.log();
587
+ // Sembix Studio Memory
588
+ explainField('Sembix Studio Memory Service', 'The Memory service provides workflow state persistence and caching.\n\n' +
589
+ chalk.green('✓ Recommended: ') + 'Enable this feature for full Studio functionality.', 'Choose Yes (recommended)');
590
+ const deploySembixStudioMemory = await confirm({
591
+ message: 'Deploy Sembix Studio Memory service?',
592
+ default: true,
593
+ });
594
+ // Sembix Studio Notifications
595
+ explainField('Sembix Studio Notifications Service', 'The Notifications service handles alerts, messaging, and event notifications.\n\n' +
596
+ chalk.green('✓ Recommended: ') + 'Enable this feature for user notifications and alerts.', 'Choose Yes (recommended)');
597
+ const deploySembixStudioNotifications = await confirm({
598
+ message: 'Deploy Sembix Studio Notifications service?',
599
+ default: true,
600
+ });
601
+ // WAF
602
+ explainField('Web Application Firewall (WAF)', 'AWS WAF protects your BFF API from common web exploits and attacks.\nIt provides an additional layer of security at the application level.\n\n' +
603
+ chalk.green('✓ Recommended: ') + 'Enable this for production environments.', 'Choose Yes (recommended for production)');
604
+ const enableBffWaf = await confirm({
605
+ message: 'Enable Web Application Firewall (WAF) for BFF?',
606
+ default: true,
607
+ });
608
+ // ============================================================
609
+ // STEP 8: FRONTEND CONFIGURATION
610
+ // ============================================================
611
+ ui.section('Step 8: Frontend Configuration');
612
+ console.log(chalk.dim('⚛️ Configure integrations for the Sembix Studio frontend (React UI).'));
613
+ console.log();
614
+ // GitHub App Client ID
615
+ const githubAppClientId = await promptIfMissing(partialConfig?.frontend?.githubAppClientId, async () => {
616
+ explainField('GitHub App Client ID', 'The OAuth Client ID from your GitHub App.\nThis allows users to authenticate with GitHub and access repositories.\n\n' +
617
+ chalk.green('💡 Tip: ') + 'Create a GitHub App in your organization settings, then copy the Client ID.', 'Iv1.0123456789abcdef');
618
+ return input({
619
+ message: 'GitHub App Client ID:',
620
+ validate: (value) => {
621
+ if (!value)
622
+ return 'GitHub App Client ID is required';
623
+ return true;
624
+ },
625
+ });
626
+ }, 'GitHub App Client ID');
627
+ // GitHub App Name
628
+ const githubAppName = await promptIfMissing(partialConfig?.frontend?.githubAppName, async () => {
629
+ explainField('GitHub App Name', 'The name of your GitHub App (as it appears in GitHub).\nThis is shown to users during the OAuth flow.', 'sembix-studio-app or acme-studio');
630
+ return input({
631
+ message: 'GitHub App Name:',
632
+ validate: (value) => {
633
+ if (!value)
634
+ return 'GitHub App Name is required';
635
+ return true;
636
+ },
637
+ });
638
+ }, 'GitHub App Name');
639
+ // Jira Client ID
640
+ const jiraClientId = await promptIfMissing(partialConfig?.frontend?.jiraClientId, async () => {
641
+ explainField('Jira Client ID', 'The OAuth Client ID from your Atlassian/Jira app.\nThis enables Jira integration for issue tracking and project management.\n\n' +
642
+ chalk.green('💡 Tip: ') + 'Create an OAuth app in your Atlassian Developer Console.', 'abc123def456ghi789');
643
+ return input({
644
+ message: 'Jira Client ID:',
645
+ validate: (value) => {
646
+ if (!value)
647
+ return 'Jira Client ID is required';
648
+ return true;
649
+ },
650
+ });
651
+ }, 'Jira Client ID');
652
+ return {
653
+ repository: { owner, repo },
654
+ environmentName,
655
+ awsAccountId,
656
+ awsRegion,
657
+ customerRoleArn,
658
+ terraformStateBucket,
659
+ database: {
660
+ name: databaseName,
661
+ user: databaseUser,
662
+ },
663
+ networking,
664
+ security: {
665
+ workflowRunsKeyAlias: workflowRunsKeyAlias || undefined,
666
+ useCustomSecurityGroups,
667
+ customSecurityGroups,
668
+ useCustomIamPolicies,
669
+ customIamRoles,
670
+ },
671
+ tls: {
672
+ cloudfrontDomain,
673
+ cloudfrontCertArn,
674
+ bffAlbCertificateArn,
675
+ hostedZoneId,
676
+ },
677
+ features: {
678
+ deploySembixStudioMemory,
679
+ deploySembixStudioNotifications,
680
+ enableBffWaf,
681
+ },
682
+ frontend: {
683
+ githubAppClientId,
684
+ githubAppName,
685
+ jiraClientId,
686
+ },
687
+ };
688
+ }
689
+ //# sourceMappingURL=environment-setup.js.map