@hyperdrive.bot/cli 1.0.2

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 (127) hide show
  1. package/README.md +1598 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/account/add.d.ts +16 -0
  7. package/dist/commands/account/add.js +185 -0
  8. package/dist/commands/account/list.d.ts +6 -0
  9. package/dist/commands/account/list.js +37 -0
  10. package/dist/commands/account/remove.d.ts +11 -0
  11. package/dist/commands/account/remove.js +57 -0
  12. package/dist/commands/auth/login.d.ts +16 -0
  13. package/dist/commands/auth/login.js +178 -0
  14. package/dist/commands/auth/logout.d.ts +6 -0
  15. package/dist/commands/auth/logout.js +39 -0
  16. package/dist/commands/auth/refresh.d.ts +6 -0
  17. package/dist/commands/auth/refresh.js +66 -0
  18. package/dist/commands/auth/status.d.ts +6 -0
  19. package/dist/commands/auth/status.js +63 -0
  20. package/dist/commands/ci/account/create.d.ts +16 -0
  21. package/dist/commands/ci/account/create.js +158 -0
  22. package/dist/commands/ci/account/delete.d.ts +14 -0
  23. package/dist/commands/ci/account/delete.js +88 -0
  24. package/dist/commands/ci/account/list.d.ts +10 -0
  25. package/dist/commands/ci/account/list.js +65 -0
  26. package/dist/commands/config/get.d.ts +9 -0
  27. package/dist/commands/config/get.js +37 -0
  28. package/dist/commands/config/set.d.ts +10 -0
  29. package/dist/commands/config/set.js +48 -0
  30. package/dist/commands/config/show.d.ts +6 -0
  31. package/dist/commands/config/show.js +10 -0
  32. package/dist/commands/deployment/create.d.ts +30 -0
  33. package/dist/commands/deployment/create.js +188 -0
  34. package/dist/commands/deployment/get.d.ts +13 -0
  35. package/dist/commands/deployment/get.js +101 -0
  36. package/dist/commands/deployment/launch.d.ts +15 -0
  37. package/dist/commands/deployment/launch.js +105 -0
  38. package/dist/commands/deployment/list.d.ts +11 -0
  39. package/dist/commands/deployment/list.js +91 -0
  40. package/dist/commands/domain/current.d.ts +6 -0
  41. package/dist/commands/domain/current.js +18 -0
  42. package/dist/commands/domain/list.d.ts +6 -0
  43. package/dist/commands/domain/list.js +42 -0
  44. package/dist/commands/domain/switch.d.ts +9 -0
  45. package/dist/commands/domain/switch.js +40 -0
  46. package/dist/commands/example.d.ts +13 -0
  47. package/dist/commands/example.js +24 -0
  48. package/dist/commands/git/connect.d.ts +10 -0
  49. package/dist/commands/git/connect.js +56 -0
  50. package/dist/commands/git/disconnect.d.ts +11 -0
  51. package/dist/commands/git/disconnect.js +93 -0
  52. package/dist/commands/git/list.d.ts +10 -0
  53. package/dist/commands/git/list.js +53 -0
  54. package/dist/commands/git/sync.d.ts +18 -0
  55. package/dist/commands/git/sync.js +235 -0
  56. package/dist/commands/init.d.ts +188 -0
  57. package/dist/commands/init.js +817 -0
  58. package/dist/commands/jira/connect.d.ts +9 -0
  59. package/dist/commands/jira/connect.js +141 -0
  60. package/dist/commands/jira/status.d.ts +9 -0
  61. package/dist/commands/jira/status.js +118 -0
  62. package/dist/commands/module/analyze.d.ts +29 -0
  63. package/dist/commands/module/analyze.js +201 -0
  64. package/dist/commands/module/create.d.ts +42 -0
  65. package/dist/commands/module/create.js +498 -0
  66. package/dist/commands/module/destroy.d.ts +11 -0
  67. package/dist/commands/module/destroy.js +77 -0
  68. package/dist/commands/module/get.d.ts +10 -0
  69. package/dist/commands/module/get.js +43 -0
  70. package/dist/commands/module/link.d.ts +15 -0
  71. package/dist/commands/module/link.js +175 -0
  72. package/dist/commands/module/list.d.ts +9 -0
  73. package/dist/commands/module/list.js +51 -0
  74. package/dist/commands/module/reanalyze.d.ts +30 -0
  75. package/dist/commands/module/reanalyze.js +206 -0
  76. package/dist/commands/module/update.d.ts +27 -0
  77. package/dist/commands/module/update.js +102 -0
  78. package/dist/commands/parameter/add.d.ts +15 -0
  79. package/dist/commands/parameter/add.js +99 -0
  80. package/dist/commands/parameter/backfill.d.ts +12 -0
  81. package/dist/commands/parameter/backfill.js +113 -0
  82. package/dist/commands/parameter/clear.d.ts +14 -0
  83. package/dist/commands/parameter/clear.js +95 -0
  84. package/dist/commands/parameter/list.d.ts +14 -0
  85. package/dist/commands/parameter/list.js +92 -0
  86. package/dist/commands/parameter/pull.d.ts +14 -0
  87. package/dist/commands/parameter/pull.js +124 -0
  88. package/dist/commands/parameter/remove.d.ts +15 -0
  89. package/dist/commands/parameter/remove.js +90 -0
  90. package/dist/commands/parameter/sync.d.ts +14 -0
  91. package/dist/commands/parameter/sync.js +153 -0
  92. package/dist/commands/parameter/update.d.ts +15 -0
  93. package/dist/commands/parameter/update.js +100 -0
  94. package/dist/commands/stage/create.d.ts +28 -0
  95. package/dist/commands/stage/create.js +312 -0
  96. package/dist/commands/stage/list.d.ts +9 -0
  97. package/dist/commands/stage/list.js +63 -0
  98. package/dist/commands/test-api.d.ts +9 -0
  99. package/dist/commands/test-api.js +40 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/services/auth-service.d.ts +84 -0
  103. package/dist/services/auth-service.js +240 -0
  104. package/dist/services/git.d.ts +46 -0
  105. package/dist/services/git.js +409 -0
  106. package/dist/services/hyperdrive-sigv4.d.ts +449 -0
  107. package/dist/services/hyperdrive-sigv4.js +375 -0
  108. package/dist/services/hyperdrive.d.ts +87 -0
  109. package/dist/services/hyperdrive.js +108 -0
  110. package/dist/services/log-tailer.d.ts +95 -0
  111. package/dist/services/log-tailer.js +242 -0
  112. package/dist/services/tenant-service.d.ts +106 -0
  113. package/dist/services/tenant-service.js +332 -0
  114. package/dist/utils/account-flow.d.ts +74 -0
  115. package/dist/utils/account-flow.js +228 -0
  116. package/dist/utils/auth-flow.d.ts +146 -0
  117. package/dist/utils/auth-flow.js +477 -0
  118. package/dist/utils/git-flow.d.ts +72 -0
  119. package/dist/utils/git-flow.js +232 -0
  120. package/dist/utils/jira-flow.d.ts +71 -0
  121. package/dist/utils/jira-flow.js +120 -0
  122. package/dist/utils/summary-display.d.ts +59 -0
  123. package/dist/utils/summary-display.js +140 -0
  124. package/dist/utils/validation.d.ts +15 -0
  125. package/dist/utils/validation.js +32 -0
  126. package/oclif.manifest.json +2819 -0
  127. package/package.json +112 -0
@@ -0,0 +1,817 @@
1
+ import { Command } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import { TenantService } from '../services/tenant-service.js';
6
+ import { executeAccountAdd as executeAccountAddOriginal, openCloudFormationUrl, promptAccountDetails, registerAccount, waitForRoleVerification, } from '../utils/account-flow.js';
7
+ import { executeAuthFlow as executeAuthFlowOriginal } from '../utils/auth-flow.js';
8
+ import { executeGitConnect as executeGitConnectOriginal, promptGitProvider, } from '../utils/git-flow.js';
9
+ import { executeJiraConnect as executeJiraConnectOriginal, promptJiraConnect, promptJiraDomain, registerJiraDomain, } from '../utils/jira-flow.js';
10
+ import { displaySetupSummary } from '../utils/summary-display.js';
11
+ import { validateTenantDomain } from '../utils/validation.js';
12
+ // Module-level auth flow function - can be replaced for testing
13
+ let authFlowImpl = executeAuthFlowOriginal;
14
+ // Module-level account add flow function - can be replaced for testing
15
+ let accountAddImpl = executeAccountAddOriginal;
16
+ // Module-level git connect flow function - can be replaced for testing
17
+ let gitConnectImpl = executeGitConnectOriginal;
18
+ // Module-level jira connect flow function - can be replaced for testing
19
+ let jiraConnectImpl = executeJiraConnectOriginal;
20
+ /**
21
+ * Set a custom auth flow implementation (for testing)
22
+ */
23
+ export function setAuthFlowImpl(impl) {
24
+ authFlowImpl = impl;
25
+ }
26
+ /**
27
+ * Reset auth flow to original implementation
28
+ */
29
+ export function resetAuthFlowImpl() {
30
+ authFlowImpl = executeAuthFlowOriginal;
31
+ }
32
+ /**
33
+ * Get the current auth flow implementation
34
+ */
35
+ export function getAuthFlowImpl() {
36
+ return authFlowImpl;
37
+ }
38
+ /**
39
+ * Set a custom account add flow implementation (for testing)
40
+ */
41
+ export function setAccountAddImpl(impl) {
42
+ accountAddImpl = impl;
43
+ }
44
+ /**
45
+ * Reset account add flow to original implementation
46
+ */
47
+ export function resetAccountAddImpl() {
48
+ accountAddImpl = executeAccountAddOriginal;
49
+ }
50
+ /**
51
+ * Get the current account add flow implementation
52
+ */
53
+ export function getAccountAddImpl() {
54
+ return accountAddImpl;
55
+ }
56
+ /**
57
+ * Set a custom git connect flow implementation (for testing)
58
+ */
59
+ export function setGitConnectImpl(impl) {
60
+ gitConnectImpl = impl;
61
+ }
62
+ /**
63
+ * Reset git connect flow to original implementation
64
+ */
65
+ export function resetGitConnectImpl() {
66
+ gitConnectImpl = executeGitConnectOriginal;
67
+ }
68
+ /**
69
+ * Get the current git connect flow implementation
70
+ */
71
+ export function getGitConnectImpl() {
72
+ return gitConnectImpl;
73
+ }
74
+ /**
75
+ * Set a custom jira connect flow implementation (for testing)
76
+ */
77
+ export function setJiraConnectImpl(impl) {
78
+ jiraConnectImpl = impl;
79
+ }
80
+ /**
81
+ * Reset jira connect flow to original implementation
82
+ */
83
+ export function resetJiraConnectImpl() {
84
+ jiraConnectImpl = executeJiraConnectOriginal;
85
+ }
86
+ /**
87
+ * Get the current jira connect flow implementation
88
+ */
89
+ export function getJiraConnectImpl() {
90
+ return jiraConnectImpl;
91
+ }
92
+ export default class Init extends Command {
93
+ static description = 'Initialize Hyperdrive CLI with guided setup wizard';
94
+ static examples = [
95
+ '<%= config.bin %> <%= command.id %>',
96
+ ];
97
+ accountsSkipped = false;
98
+ authSkipped = false;
99
+ connectedAccounts = [];
100
+ connectedGitProvider;
101
+ connectedJira;
102
+ gitSkipped = false;
103
+ jiraSkipped = false;
104
+ tenantService;
105
+ constructor(argv, config) {
106
+ super(argv, config);
107
+ this.tenantService = new TenantService();
108
+ }
109
+ async run() {
110
+ this.displayWelcome();
111
+ const shouldContinue = await this.promptContinue();
112
+ if (!shouldContinue) {
113
+ this.log('');
114
+ this.log(chalk.yellow('👋 Setup cancelled. Run `hd init` when you\'re ready to configure Hyperdrive.'));
115
+ return;
116
+ }
117
+ // Step 1: Configure tenant domain
118
+ this.log('');
119
+ const tenantDomain = await this.configureTenantDomain();
120
+ // Step 2: Authenticate user
121
+ this.log('');
122
+ await this.authenticateUser(tenantDomain);
123
+ // Step 3: AWS Account Setup (only if authenticated or skipped)
124
+ this.log('');
125
+ await this.addAwsAccounts(tenantDomain);
126
+ // Step 4: Git Provider Setup
127
+ this.log('');
128
+ await this.connectGitProvider();
129
+ // Step 5: Jira Integration Setup
130
+ this.log('');
131
+ await this.connectJira();
132
+ // Display summary
133
+ this.showSummary(tenantDomain);
134
+ }
135
+ /**
136
+ * Add AWS accounts step in the init wizard
137
+ *
138
+ * Implements:
139
+ * - Initial prompt to add an AWS account
140
+ * - Loop to add multiple accounts
141
+ * - Skip option with helpful message
142
+ * - Summary display of connected accounts
143
+ */
144
+ async addAwsAccounts(tenantDomain) {
145
+ this.log(chalk.blue('AWS Account Setup'));
146
+ this.log(chalk.dim('Connect AWS accounts that Hyperdrive can deploy to'));
147
+ this.log('');
148
+ // Check if authentication was skipped - warn user but allow to try
149
+ if (this.authSkipped) {
150
+ this.log(chalk.yellow('⚠') + ' Authentication was skipped. Account setup requires authentication.');
151
+ const { tryAnyway } = await inquirer.prompt([{
152
+ default: false,
153
+ message: 'Would you like to try adding an AWS account anyway?',
154
+ name: 'tryAnyway',
155
+ type: 'confirm',
156
+ }]);
157
+ if (!tryAnyway) {
158
+ this.accountsSkipped = true;
159
+ this.log(chalk.yellow('⚠') + " Skipped AWS account setup. Run 'hd account add' later.");
160
+ return;
161
+ }
162
+ }
163
+ // Initial prompt to add an account
164
+ const { addAccount } = await inquirer.prompt([{
165
+ default: true,
166
+ message: 'Would you like to add an AWS account?',
167
+ name: 'addAccount',
168
+ type: 'confirm',
169
+ }]);
170
+ if (!addAccount) {
171
+ this.accountsSkipped = true;
172
+ this.log(chalk.yellow('⚠') + " Skipped AWS account setup. Run 'hd account add' later.");
173
+ return;
174
+ }
175
+ // Loop to add multiple accounts
176
+ let continueAdding = true;
177
+ while (continueAdding) {
178
+ const result = await this.addSingleAwsAccount(tenantDomain);
179
+ if (result.success && result.accountId) {
180
+ this.connectedAccounts.push({
181
+ accountId: result.accountId,
182
+ alias: result.alias,
183
+ });
184
+ // Prompt to add another
185
+ const { addAnother } = await inquirer.prompt([{
186
+ default: false,
187
+ message: 'Add another AWS account?',
188
+ name: 'addAnother',
189
+ type: 'confirm',
190
+ }]);
191
+ continueAdding = addAnother;
192
+ }
193
+ else {
194
+ // Account add failed - offer retry or skip
195
+ const { action } = await inquirer.prompt([{
196
+ choices: [
197
+ { name: 'Retry adding this account', value: 'retry' },
198
+ { name: 'Skip this account and continue', value: 'skip' },
199
+ { name: 'Skip AWS account setup entirely', value: 'skip_all' },
200
+ ],
201
+ message: 'Account setup failed. What would you like to do?',
202
+ name: 'action',
203
+ type: 'list',
204
+ }]);
205
+ if (action === 'retry') {
206
+ // Continue the loop to retry
207
+ continue;
208
+ }
209
+ else if (action === 'skip') {
210
+ // Ask if they want to add a different account
211
+ const { addAnother } = await inquirer.prompt([{
212
+ default: false,
213
+ message: 'Would you like to add a different AWS account?',
214
+ name: 'addAnother',
215
+ type: 'confirm',
216
+ }]);
217
+ continueAdding = addAnother;
218
+ }
219
+ else {
220
+ // Skip all
221
+ continueAdding = false;
222
+ }
223
+ }
224
+ }
225
+ // Display summary
226
+ this.displayAccountSummary();
227
+ }
228
+ /**
229
+ * Add a single AWS account with proper separation of concerns
230
+ *
231
+ * Architecture:
232
+ * 1. Prompt for account details (no spinner - user needs to see prompts)
233
+ * 2. Check for duplicate account ID
234
+ * 3. Register account with API (with spinner - actual work happening)
235
+ * 4. Handle role creation if needed
236
+ */
237
+ async addSingleAwsAccount(tenantDomain) {
238
+ try {
239
+ // Step 1: Collect account details from user (NO SPINNER - user needs to see prompts!)
240
+ const accountData = await promptAccountDetails();
241
+ // Step 2: Check for duplicate account ID
242
+ const isDuplicate = this.connectedAccounts.some(account => account.accountId === accountData.accountId);
243
+ if (isDuplicate) {
244
+ this.log(chalk.yellow('⚠') + ` Account ${accountData.accountId} is already connected`);
245
+ this.log(chalk.gray(' Skipping duplicate account...'));
246
+ return { success: false };
247
+ }
248
+ // Step 3: Register account with API (WITH SPINNER - actual API work)
249
+ const spinner = ora('Registering AWS account...').start();
250
+ const result = await registerAccount(accountData);
251
+ if (!result.success) {
252
+ spinner.fail('Failed to register AWS account');
253
+ this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
254
+ return { success: false };
255
+ }
256
+ spinner.succeed('AWS account registered');
257
+ this.log(chalk.green('✓') + ` Account ${result.accountId} added${result.accountAlias ? ` (${result.accountAlias})` : ''}`);
258
+ // Step 3: Handle role creation if needed
259
+ if (result.quickCreateUrl) {
260
+ await this.handleRoleCreation(result);
261
+ }
262
+ return {
263
+ accountId: result.accountId,
264
+ alias: result.accountAlias,
265
+ success: true,
266
+ };
267
+ }
268
+ catch (error) {
269
+ const errorMessage = error instanceof Error ? error.message : String(error);
270
+ this.log(chalk.red('✗') + ` Error: ${errorMessage}`);
271
+ return { success: false };
272
+ }
273
+ }
274
+ /**
275
+ * Authenticate user using OAuth PKCE flow with retry logic
276
+ *
277
+ * Implements:
278
+ * - Spinner display during authentication
279
+ * - Retry logic (max 3 attempts)
280
+ * - Skip option after failed attempts
281
+ */
282
+ async authenticateUser(tenantDomain) {
283
+ const MAX_RETRIES = 3;
284
+ let attempts = 0;
285
+ this.log(chalk.blue('Authenticating with Hyperdrive...'));
286
+ while (attempts < MAX_RETRIES) {
287
+ attempts++;
288
+ const spinner = ora('Opening browser for authentication...').start();
289
+ try {
290
+ const result = await getAuthFlowImpl()({
291
+ logger: (message) => this.log(message),
292
+ tenantDomain
293
+ });
294
+ if (result.success) {
295
+ spinner.succeed('Browser authentication complete');
296
+ this.log(chalk.green('✓') + ' Authentication successful');
297
+ return;
298
+ }
299
+ else {
300
+ spinner.fail('Authentication failed');
301
+ this.log(chalk.red('✗') + ` Authentication failed: ${result.error || 'Unknown error'}`);
302
+ // Check if we should retry or offer skip
303
+ if (attempts < MAX_RETRIES) {
304
+ const { retry } = await inquirer.prompt([
305
+ {
306
+ default: true,
307
+ message: 'Authentication failed. Would you like to retry?',
308
+ name: 'retry',
309
+ type: 'confirm',
310
+ },
311
+ ]);
312
+ if (!retry) {
313
+ // User chose not to retry, offer skip
314
+ await this.handleAuthSkip();
315
+ return;
316
+ }
317
+ }
318
+ else {
319
+ // Max retries reached, offer skip
320
+ const { skip } = await inquirer.prompt([
321
+ {
322
+ default: false,
323
+ message: 'Authentication failed 3 times. Skip and continue?',
324
+ name: 'skip',
325
+ type: 'confirm',
326
+ },
327
+ ]);
328
+ if (skip) {
329
+ await this.handleAuthSkip();
330
+ return;
331
+ }
332
+ else {
333
+ // User chose not to skip after max retries, exit
334
+ this.log('');
335
+ this.log(chalk.yellow('⚠') + ' Setup cannot continue without authentication.');
336
+ this.log(chalk.gray("Run 'hd auth login' to authenticate separately."));
337
+ return;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ catch (error) {
343
+ spinner.fail('Authentication failed');
344
+ const errorMessage = error instanceof Error ? error.message : String(error);
345
+ this.log(chalk.red('✗') + ` Authentication failed: ${errorMessage}`);
346
+ // Check if we should retry or offer skip
347
+ if (attempts < MAX_RETRIES) {
348
+ const { retry } = await inquirer.prompt([
349
+ {
350
+ default: true,
351
+ message: 'Authentication failed. Would you like to retry?',
352
+ name: 'retry',
353
+ type: 'confirm',
354
+ },
355
+ ]);
356
+ if (!retry) {
357
+ // User chose not to retry, offer skip
358
+ await this.handleAuthSkip();
359
+ return;
360
+ }
361
+ }
362
+ else {
363
+ // Max retries reached, offer skip
364
+ const { skip } = await inquirer.prompt([
365
+ {
366
+ default: false,
367
+ message: 'Authentication failed 3 times. Skip and continue?',
368
+ name: 'skip',
369
+ type: 'confirm',
370
+ },
371
+ ]);
372
+ if (skip) {
373
+ await this.handleAuthSkip();
374
+ return;
375
+ }
376
+ else {
377
+ // User chose not to skip after max retries, exit
378
+ this.log('');
379
+ this.log(chalk.yellow('⚠') + ' Setup cannot continue without authentication.');
380
+ this.log(chalk.gray("Run 'hd auth login' to authenticate separately."));
381
+ return;
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ /**
388
+ * Configure tenant domain - prompts for input or offers to keep existing
389
+ * Returns the configured domain for potential use in subsequent steps
390
+ */
391
+ async configureTenantDomain() {
392
+ const existingDomain = this.tenantService.getTenantDomain();
393
+ let domain;
394
+ if (existingDomain) {
395
+ // Existing domain found - offer to keep or change
396
+ const { action } = await inquirer.prompt([
397
+ {
398
+ choices: [
399
+ { name: `Keep current domain (${existingDomain})`, value: 'keep' },
400
+ { name: 'Enter a different domain', value: 'change' },
401
+ ],
402
+ message: `Tenant domain already configured: ${chalk.cyan(existingDomain)}`,
403
+ name: 'action',
404
+ type: 'list',
405
+ },
406
+ ]);
407
+ if (action === 'keep') {
408
+ domain = existingDomain;
409
+ this.log(chalk.green('✓') + ' Tenant domain configured: ' + chalk.cyan(domain));
410
+ return domain;
411
+ }
412
+ }
413
+ // Prompt for new domain
414
+ const { newDomain } = await inquirer.prompt([
415
+ {
416
+ message: 'Enter your tenant domain (e.g., acme.hyperdrive.bot):',
417
+ name: 'newDomain',
418
+ type: 'input',
419
+ validate: (input) => {
420
+ if (validateTenantDomain(input)) {
421
+ return true;
422
+ }
423
+ return 'Invalid domain format. Use subdomain.hyperdrive.bot or a valid custom domain';
424
+ },
425
+ },
426
+ ]);
427
+ domain = newDomain.trim();
428
+ // Save the domain to config
429
+ this.tenantService.setTenantDomain(domain);
430
+ this.log(chalk.green('✓') + ' Tenant domain configured: ' + chalk.cyan(domain));
431
+ return domain;
432
+ }
433
+ /**
434
+ * Connect Git provider step in the init wizard
435
+ *
436
+ * Implements:
437
+ * - Provider selection prompt (GitHub, GitLab, Skip)
438
+ * - OAuth flow execution with spinner
439
+ * - Retry logic on failure
440
+ * - Skip option with helpful message
441
+ */
442
+ async connectGitProvider() {
443
+ this.log(chalk.blue('Git Provider Setup'));
444
+ this.log(chalk.dim('Connect GitHub or GitLab for automated deployments'));
445
+ this.log('');
446
+ // Check if authentication was skipped - warn user but allow to try
447
+ if (this.authSkipped) {
448
+ this.log(chalk.yellow('⚠') + ' Authentication was skipped. Git provider setup requires authentication.');
449
+ const { tryAnyway } = await inquirer.prompt([{
450
+ default: false,
451
+ message: 'Would you like to try connecting a Git provider anyway?',
452
+ name: 'tryAnyway',
453
+ type: 'confirm',
454
+ }]);
455
+ if (!tryAnyway) {
456
+ this.gitSkipped = true;
457
+ this.log(chalk.yellow('⚠') + " Skipped Git provider setup. Run 'hd git connect' later.");
458
+ return;
459
+ }
460
+ }
461
+ // Prompt for provider selection (includes Skip option)
462
+ const provider = await promptGitProvider(true);
463
+ if (provider === 'skip') {
464
+ this.gitSkipped = true;
465
+ this.log(chalk.yellow('⚠') + " Skipped Git provider setup. Run 'hd git connect' later.");
466
+ return;
467
+ }
468
+ // Execute OAuth flow with retry logic
469
+ await this.executeGitOAuthFlow(provider);
470
+ }
471
+ /**
472
+ * Connect Jira integration step in the init wizard
473
+ *
474
+ * Implements:
475
+ * 1. Prompt to connect Jira (includes Skip option)
476
+ * 2. Prompt for Jira domain
477
+ * 3. Pre-register domain with API
478
+ * 4. Display marketplace URL for app installation
479
+ */
480
+ async connectJira() {
481
+ this.log(chalk.blue('Jira Integration Setup'));
482
+ this.log(chalk.dim('Connect Jira for automated project management integration'));
483
+ this.log('');
484
+ // Check if authentication was skipped - warn user but allow to try
485
+ if (this.authSkipped) {
486
+ this.log(chalk.yellow('⚠') + ' Authentication was skipped. Jira integration requires authentication.');
487
+ const { tryAnyway } = await inquirer.prompt([{
488
+ default: false,
489
+ message: 'Would you like to try connecting Jira anyway?',
490
+ name: 'tryAnyway',
491
+ type: 'confirm',
492
+ }]);
493
+ if (!tryAnyway) {
494
+ this.jiraSkipped = true;
495
+ this.log(chalk.yellow('⚠') + " Skipped Jira integration. Run 'hd jira connect' later.");
496
+ return;
497
+ }
498
+ }
499
+ // Prompt to connect or skip
500
+ const action = await promptJiraConnect(true);
501
+ if (action === 'skip') {
502
+ this.jiraSkipped = true;
503
+ this.log(chalk.yellow('⚠') + " Skipped Jira integration. Run 'hd jira connect' later.");
504
+ return;
505
+ }
506
+ // Execute Jira connection flow with retry limit
507
+ await this.executeJiraConnect(0);
508
+ }
509
+ /**
510
+ * Display summary of connected accounts
511
+ */
512
+ displayAccountSummary() {
513
+ this.log('');
514
+ if (this.connectedAccounts.length > 0) {
515
+ this.log(chalk.green('✓') + ` Connected ${this.connectedAccounts.length} AWS account(s)`);
516
+ for (const account of this.connectedAccounts) {
517
+ this.log(chalk.gray(` - ${account.accountId}${account.alias ? ` (${account.alias})` : ''}`));
518
+ }
519
+ }
520
+ else {
521
+ this.log(chalk.yellow('⚠') + ' No AWS accounts connected');
522
+ this.log(chalk.gray(" Run 'hd account add' later to connect AWS accounts."));
523
+ }
524
+ }
525
+ /**
526
+ * Display welcome message with Hyperdrive branding and setup steps
527
+ */
528
+ displayWelcome() {
529
+ this.log('');
530
+ this.log(chalk.blue('╔═══════════════════════════════════════════════════════════════╗'));
531
+ this.log(chalk.blue('║') + chalk.white.bold(' Welcome to Hyperdrive CLI Setup ') + chalk.blue('║'));
532
+ this.log(chalk.blue('╚═══════════════════════════════════════════════════════════════╝'));
533
+ this.log('');
534
+ this.log(chalk.white('This wizard will guide you through configuring Hyperdrive CLI.'));
535
+ this.log(chalk.white('The following steps will be completed:'));
536
+ this.log('');
537
+ this.log(chalk.cyan(' 1. Tenant Domain'));
538
+ this.log(chalk.gray(' Configure your organization\'s Hyperdrive tenant domain'));
539
+ this.log('');
540
+ this.log(chalk.cyan(' 2. Authentication'));
541
+ this.log(chalk.gray(' Sign in to your Hyperdrive account using OAuth'));
542
+ this.log('');
543
+ this.log(chalk.cyan(' 3. AWS Accounts'));
544
+ this.log(chalk.gray(' Link AWS accounts for deployment targets'));
545
+ this.log('');
546
+ this.log(chalk.cyan(' 4. Git Provider'));
547
+ this.log(chalk.gray(' Connect your Git provider for repository access'));
548
+ this.log('');
549
+ this.log(chalk.cyan(' 5. Jira Integration'));
550
+ this.log(chalk.gray(' Connect Jira for project management integration'));
551
+ this.log('');
552
+ }
553
+ /**
554
+ * Execute Git OAuth flow with retry logic
555
+ *
556
+ * @param provider - Git provider to connect (github or gitlab)
557
+ * @param attemptCount - Current retry attempt (0-indexed)
558
+ */
559
+ async executeGitOAuthFlow(provider, attemptCount = 0) {
560
+ const providerName = provider === 'github' ? 'GitHub' : 'GitLab';
561
+ const spinner = ora(`Connecting to ${providerName}...`).start();
562
+ try {
563
+ const result = await getGitConnectImpl()({
564
+ logger: (message) => {
565
+ spinner.text = message;
566
+ },
567
+ provider,
568
+ });
569
+ if (result.success) {
570
+ spinner.succeed(`Connected to ${providerName}`);
571
+ // Display connected account info
572
+ if (result.accountName) {
573
+ this.log(chalk.green('✓') + ` Linked ${result.accountName}`);
574
+ }
575
+ // List connected installations if available
576
+ if (result.installations && result.installations.length > 0) {
577
+ for (const installation of result.installations) {
578
+ const accountName = installation.accountLogin || installation.gitlabUsername || 'Unknown';
579
+ this.log(chalk.gray(` - ${accountName} (${installation.provider})`));
580
+ }
581
+ }
582
+ // Store connected provider info for summary
583
+ this.connectedGitProvider = {
584
+ accountName: result.accountName,
585
+ provider: result.provider,
586
+ };
587
+ }
588
+ else {
589
+ spinner.fail(`Failed to connect to ${providerName}`);
590
+ this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
591
+ // Offer retry
592
+ await this.handleGitRetry(provider, attemptCount);
593
+ }
594
+ }
595
+ catch (error) {
596
+ spinner.fail(`Failed to connect to ${providerName}`);
597
+ const errorMessage = error instanceof Error ? error.message : String(error);
598
+ this.log(chalk.red('✗') + ` Error: ${errorMessage}`);
599
+ // Offer retry
600
+ await this.handleGitRetry(provider, attemptCount);
601
+ }
602
+ }
603
+ /**
604
+ * Execute Jira connection with proper separation of concerns
605
+ *
606
+ * Architecture:
607
+ * 1. Prompt for Jira domain (no spinner - user needs to see prompt)
608
+ * 2. Register domain with API (with spinner - actual work happening)
609
+ * 3. Display marketplace URL and next steps
610
+ *
611
+ * @param attemptCount - Current retry attempt (0-indexed)
612
+ */
613
+ async executeJiraConnect(attemptCount = 0) {
614
+ const MAX_RETRIES = 3;
615
+ try {
616
+ // Step 1: Collect Jira domain from user (NO SPINNER - user needs to see prompt!)
617
+ const domainData = await promptJiraDomain();
618
+ // Step 2: Register domain with API (WITH SPINNER - actual API work)
619
+ const spinner = ora('Registering Jira domain...').start();
620
+ const result = await registerJiraDomain(domainData.jiraDomain);
621
+ if (!result.success) {
622
+ spinner.fail('Failed to register Jira domain');
623
+ this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
624
+ // Offer retry if under max attempts
625
+ await this.handleJiraRetry(attemptCount);
626
+ return;
627
+ }
628
+ spinner.succeed('Jira domain registered successfully');
629
+ this.log(chalk.green('✓') + ` Domain ${result.jiraDomain} registered`);
630
+ // Store connected Jira info for summary
631
+ this.connectedJira = {
632
+ jiraDomain: result.jiraDomain,
633
+ registrationToken: result.registrationToken,
634
+ };
635
+ // Display next steps
636
+ this.log('');
637
+ this.log(chalk.bold('📋 Next Steps:'));
638
+ this.log(` ${chalk.cyan('1.')} Install the Hyperdrive Forge app from the Atlassian Marketplace`);
639
+ this.log(` ${chalk.cyan('2.')} Enter the registration token during installation`);
640
+ this.log('');
641
+ this.log(chalk.bold('Marketplace URL:'));
642
+ this.log(` ${chalk.cyan(result.marketplaceUrl || 'https://marketplace.atlassian.com')}`);
643
+ this.log('');
644
+ this.log(chalk.dim('💡 Tip: Run') + chalk.cyan(' hd jira status ') + chalk.dim('to verify the connection'));
645
+ this.log('');
646
+ }
647
+ catch (error) {
648
+ const errorMessage = error instanceof Error ? error.message : String(error);
649
+ this.log(chalk.red('✗') + ` Error: ${errorMessage}`);
650
+ // Offer retry if under max attempts
651
+ await this.handleJiraRetry(attemptCount);
652
+ }
653
+ }
654
+ /**
655
+ * Handle authentication skip - set flag and display warning
656
+ */
657
+ async handleAuthSkip() {
658
+ this.authSkipped = true;
659
+ this.log('');
660
+ this.log(chalk.yellow('⚠') + " Authentication skipped. You'll need to run 'hd auth login' to use most commands.");
661
+ }
662
+ /**
663
+ * Handle Git connection failure with retry option
664
+ *
665
+ * @param provider - Git provider to connect (github or gitlab)
666
+ * @param attemptCount - Current retry attempt (0-indexed)
667
+ */
668
+ async handleGitRetry(provider, attemptCount) {
669
+ const MAX_RETRIES = 3;
670
+ if (attemptCount >= MAX_RETRIES) {
671
+ // Max retries reached
672
+ const { skip } = await inquirer.prompt([{
673
+ default: false,
674
+ message: 'Git connection failed 3 times. Skip and continue?',
675
+ name: 'skip',
676
+ type: 'confirm',
677
+ }]);
678
+ if (skip) {
679
+ this.gitSkipped = true;
680
+ this.log(chalk.yellow('⚠') + " Skipped Git provider setup. Run 'hd git connect' later.");
681
+ }
682
+ else {
683
+ this.log('');
684
+ this.log(chalk.yellow('⚠') + ' Setup cannot continue without Git provider.');
685
+ this.log(chalk.gray("Run 'hd git connect' to connect Git separately."));
686
+ }
687
+ return;
688
+ }
689
+ // Under max retries - offer retry
690
+ const { retry } = await inquirer.prompt([{
691
+ default: true,
692
+ message: 'Would you like to retry?',
693
+ name: 'retry',
694
+ type: 'confirm',
695
+ }]);
696
+ if (retry) {
697
+ // Re-execute OAuth flow for same provider with incremented attempt count
698
+ await this.executeGitOAuthFlow(provider, attemptCount + 1);
699
+ }
700
+ else {
701
+ // User chose not to retry - mark as skipped
702
+ this.gitSkipped = true;
703
+ this.log(chalk.yellow('⚠') + " Skipped Git provider setup. Run 'hd git connect' later.");
704
+ }
705
+ }
706
+ /**
707
+ * Handle Jira connection failure with retry option
708
+ *
709
+ * @param attemptCount - Current retry attempt (0-indexed)
710
+ */
711
+ async handleJiraRetry(attemptCount) {
712
+ const MAX_RETRIES = 3;
713
+ if (attemptCount >= MAX_RETRIES) {
714
+ // Max retries reached
715
+ const { skip } = await inquirer.prompt([{
716
+ default: false,
717
+ message: 'Jira connection failed 3 times. Skip and continue?',
718
+ name: 'skip',
719
+ type: 'confirm',
720
+ }]);
721
+ if (skip) {
722
+ this.jiraSkipped = true;
723
+ this.log(chalk.yellow('⚠') + " Skipped Jira integration. Run 'hd jira connect' later.");
724
+ }
725
+ else {
726
+ this.log('');
727
+ this.log(chalk.yellow('⚠') + ' Setup cannot continue without Jira connection.');
728
+ this.log(chalk.gray("Run 'hd jira connect' to connect Jira separately."));
729
+ }
730
+ return;
731
+ }
732
+ // Under max retries - offer retry
733
+ const { retry } = await inquirer.prompt([{
734
+ default: true,
735
+ message: 'Would you like to retry?',
736
+ name: 'retry',
737
+ type: 'confirm',
738
+ }]);
739
+ if (retry) {
740
+ // Re-execute Jira connection with incremented attempt count
741
+ await this.executeJiraConnect(attemptCount + 1);
742
+ }
743
+ else {
744
+ // User chose not to retry - mark as skipped
745
+ this.jiraSkipped = true;
746
+ this.log(chalk.yellow('⚠') + " Skipped Jira integration. Run 'hd jira connect' later.");
747
+ }
748
+ }
749
+ /**
750
+ * Handle cross-account IAM role creation
751
+ */
752
+ async handleRoleCreation(result) {
753
+ this.log('');
754
+ this.log(chalk.yellow('⚠') + ' A cross-account IAM role needs to be created.');
755
+ const { openBrowser } = await inquirer.prompt([{
756
+ default: true,
757
+ message: 'Open CloudFormation in browser to create the role?',
758
+ name: 'openBrowser',
759
+ type: 'confirm',
760
+ }]);
761
+ if (openBrowser) {
762
+ this.log(chalk.gray('Opening browser for CloudFormation stack creation...'));
763
+ this.log(chalk.dim('If browser doesn\'t open or opens in wrong profile, copy this URL:'));
764
+ this.log(chalk.cyan(` ${result.quickCreateUrl}`));
765
+ this.log('');
766
+ await openCloudFormationUrl(result.quickCreateUrl);
767
+ // Wait for role verification
768
+ const verifySpinner = ora('Waiting for role verification...').start();
769
+ const verifyResult = await waitForRoleVerification(result.accountId, (message) => { verifySpinner.text = message; });
770
+ if (verifyResult.verified) {
771
+ verifySpinner.succeed('Cross-account role verified');
772
+ this.log(chalk.green('✓') + ' Role verified successfully');
773
+ }
774
+ else {
775
+ verifySpinner.warn('Role verification timed out');
776
+ this.log(chalk.yellow('⚠') + ' Role may still be creating. You can verify later with:');
777
+ this.log(chalk.gray(` hd account verify --accountId ${result.accountId}`));
778
+ }
779
+ }
780
+ else {
781
+ this.log(chalk.gray('You can create the role later by running:'));
782
+ this.log(chalk.gray(` hd account verify --accountId ${result.accountId}`));
783
+ }
784
+ }
785
+ /**
786
+ * Prompt user to continue or exit the setup wizard
787
+ */
788
+ async promptContinue() {
789
+ const answers = await inquirer.prompt([
790
+ {
791
+ default: true,
792
+ message: 'Ready to continue with setup?',
793
+ name: 'continue',
794
+ type: 'confirm',
795
+ },
796
+ ]);
797
+ return answers.continue;
798
+ }
799
+ /**
800
+ * Display final setup summary using the summary-display utility
801
+ */
802
+ showSummary(tenantDomain) {
803
+ const results = {
804
+ accountsSkipped: this.accountsSkipped,
805
+ authCompleted: !this.authSkipped,
806
+ authSkipped: this.authSkipped,
807
+ connectedAccounts: this.connectedAccounts,
808
+ gitAccountName: this.connectedGitProvider?.accountName,
809
+ gitProvider: this.connectedGitProvider?.provider,
810
+ gitSkipped: this.gitSkipped,
811
+ jiraDomain: this.connectedJira?.jiraDomain,
812
+ jiraSkipped: this.jiraSkipped,
813
+ tenantDomain,
814
+ };
815
+ displaySetupSummary(results, (message) => this.log(message));
816
+ }
817
+ }