@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
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,3 @@
1
+ import {execute} from '@oclif/core'
2
+
3
+ await execute({development: true, dir: import.meta.url})
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AccountAdd extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ accountId: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
7
+ defaultRegion: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ name: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ 'no-wait': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ roleArn: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ private promptUser;
15
+ private waitForRoleVerification;
16
+ }
@@ -0,0 +1,185 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import open from 'open';
5
+ import ora from 'ora';
6
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
7
+ const VERIFY_POLL_INTERVAL = 5000; // 5 seconds
8
+ const VERIFY_TIMEOUT = 300000; // 5 minutes
9
+ export default class AccountAdd extends Command {
10
+ static description = 'Add an AWS account to Hyperdrive for deployments';
11
+ static examples = [
12
+ '<%= config.bin %> <%= command.id %> --accountId 123456789012 --defaultRegion us-east-1',
13
+ '<%= config.bin %> <%= command.id %> --accountId 123456789012 --defaultRegion us-east-1 --name "Production Account"',
14
+ '<%= config.bin %> <%= command.id %> # Interactive mode',
15
+ ];
16
+ static flags = {
17
+ accountId: Flags.string({
18
+ char: 'a',
19
+ description: 'AWS Account ID (12 digits)',
20
+ }),
21
+ defaultRegion: Flags.string({
22
+ char: 'r',
23
+ description: 'Default AWS region for deployments',
24
+ }),
25
+ domain: Flags.string({
26
+ char: 'd',
27
+ description: 'Tenant domain (for multi-domain setups)',
28
+ }),
29
+ name: Flags.string({
30
+ char: 'n',
31
+ description: 'Friendly name for the account',
32
+ }),
33
+ 'no-wait': Flags.boolean({
34
+ default: false,
35
+ description: 'Do not wait for role verification after opening CloudFormation',
36
+ }),
37
+ roleArn: Flags.string({
38
+ description: 'Cross-account IAM role ARN (optional - will be auto-generated if not provided)',
39
+ }),
40
+ };
41
+ async run() {
42
+ this.log(chalk.green('🔐 Adding AWS account to Hyperdrive...'));
43
+ const { flags } = await this.parse(AccountAdd);
44
+ const responses = await this.promptUser(flags);
45
+ const service = new HyperdriveSigV4Service(flags.domain);
46
+ const combinedData = { ...flags, ...responses };
47
+ try {
48
+ const result = await service.accountAdd({
49
+ accountId: combinedData.accountId,
50
+ defaultRegion: combinedData.defaultRegion,
51
+ name: combinedData.name,
52
+ roleArn: combinedData.roleArn,
53
+ });
54
+ this.log(chalk.blue('✅ AWS account registered successfully!'));
55
+ this.log('');
56
+ this.log(chalk.white('Account Details:'));
57
+ this.log(chalk.gray(` Account ID: ${result.accountId}`));
58
+ this.log(chalk.gray(` Name: ${result.name || 'N/A'}`));
59
+ this.log(chalk.gray(` Region: ${result.defaultRegion}`));
60
+ this.log(chalk.gray(` Role ARN: ${result.roleArn}`));
61
+ this.log('');
62
+ // If quickCreateUrl is provided, prompt user to create the cross-account role
63
+ if (result.quickCreateUrl) {
64
+ this.log(chalk.yellow('⚠️ IMPORTANT: You need to create the cross-account IAM role in the target account.'));
65
+ this.log('');
66
+ this.log(chalk.white('To create the role, open the following URL in your browser:'));
67
+ this.log(chalk.cyan(result.quickCreateUrl));
68
+ this.log('');
69
+ const { openBrowser } = await inquirer.prompt([{
70
+ default: true,
71
+ message: chalk.yellow('Open CloudFormation in browser to create the role?'),
72
+ name: 'openBrowser',
73
+ type: 'confirm',
74
+ }]);
75
+ if (openBrowser) {
76
+ this.log(chalk.gray('Opening browser...'));
77
+ await open(result.quickCreateUrl);
78
+ if (flags['no-wait']) {
79
+ this.log(chalk.green('✓ Browser opened. Complete the CloudFormation stack creation to enable deployments.'));
80
+ this.log('');
81
+ this.log(chalk.gray('Once the role is created, you can verify it with: hd account verify --accountId ' + result.accountId));
82
+ return;
83
+ }
84
+ // Poll for role verification
85
+ await this.waitForRoleVerification(service, result.accountId);
86
+ }
87
+ else {
88
+ this.log(chalk.gray('You can create the role later by visiting the URL above.'));
89
+ this.log(chalk.gray('After creating the role, verify it with: hd account verify --accountId ' + result.accountId));
90
+ }
91
+ }
92
+ else {
93
+ this.log('');
94
+ this.log(chalk.gray('You can now create stages that deploy to this account.'));
95
+ }
96
+ }
97
+ catch (error) {
98
+ const axiosError = error;
99
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
100
+ this.log(chalk.red('❌ Error adding AWS account: ' + errorMessage));
101
+ if (axiosError.response?.status === 403) {
102
+ this.log(chalk.yellow('\nTip: Make sure you are authenticated with "hd auth login".'));
103
+ }
104
+ this.exit(1);
105
+ }
106
+ }
107
+ async promptUser(flags) {
108
+ const prompts = [];
109
+ if (!flags.accountId) {
110
+ prompts.push({
111
+ message: chalk.yellow('🔢 Enter the AWS Account ID (12 digits):'),
112
+ name: 'accountId',
113
+ validate: (input) => {
114
+ if (!/^\d{12}$/.test(input)) {
115
+ return 'AWS Account ID must be exactly 12 digits';
116
+ }
117
+ return true;
118
+ },
119
+ });
120
+ }
121
+ if (!flags.name) {
122
+ prompts.push({
123
+ default: 'Production',
124
+ message: chalk.yellow('📛 Enter a friendly name for this account:'),
125
+ name: 'name',
126
+ });
127
+ }
128
+ if (!flags.defaultRegion) {
129
+ prompts.push({
130
+ choices: [
131
+ 'us-east-1',
132
+ 'us-east-2',
133
+ 'us-west-1',
134
+ 'us-west-2',
135
+ 'sa-east-1',
136
+ 'eu-west-1',
137
+ 'eu-central-1',
138
+ 'ap-southeast-1',
139
+ 'ap-northeast-1',
140
+ ],
141
+ default: 'us-east-1',
142
+ message: chalk.yellow('🌍 Select the default AWS region:'),
143
+ name: 'defaultRegion',
144
+ type: 'list',
145
+ });
146
+ }
147
+ if (prompts.length === 0) {
148
+ return {};
149
+ }
150
+ return inquirer.prompt(prompts);
151
+ }
152
+ async waitForRoleVerification(service, accountId) {
153
+ const spinner = ora('Waiting for CloudFormation stack to complete and role to be verified...').start();
154
+ const startTime = Date.now();
155
+ let attemptCount = 0;
156
+ while (Date.now() - startTime < VERIFY_TIMEOUT) {
157
+ attemptCount++;
158
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
159
+ try {
160
+ spinner.text = `Verifying role... (attempt ${attemptCount}, ${elapsed}s elapsed)`;
161
+ const result = await service.accountVerify({ accountId });
162
+ if (result.verified) {
163
+ spinner.succeed(chalk.green('✅ Cross-account role verified successfully!'));
164
+ this.log('');
165
+ this.log(chalk.green('🎉 Account setup complete! You can now create stages that deploy to this account.'));
166
+ return;
167
+ }
168
+ // Role exists but not verified yet
169
+ spinner.text = `Role not ready yet: ${result.message || 'waiting...'} (attempt ${attemptCount}, ${elapsed}s)`;
170
+ }
171
+ catch (error) {
172
+ // API error - role not ready yet, continue polling
173
+ const errorMessage = error instanceof Error ? error.message : 'unknown error';
174
+ spinner.text = `Checking role... ${errorMessage} (attempt ${attemptCount}, ${elapsed}s)`;
175
+ }
176
+ // Wait before next poll
177
+ await new Promise(resolve => setTimeout(resolve, VERIFY_POLL_INTERVAL));
178
+ }
179
+ // Timeout reached
180
+ spinner.warn(chalk.yellow('⏱️ Verification timed out after 5 minutes.'));
181
+ this.log('');
182
+ this.log(chalk.gray('The CloudFormation stack may still be creating.'));
183
+ this.log(chalk.gray('You can verify the role later with: hd account verify --accountId ' + accountId));
184
+ }
185
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AccountList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,37 @@
1
+ import { Command } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
4
+ export default class AccountList extends Command {
5
+ static description = 'List all AWS accounts registered with Hyperdrive';
6
+ static examples = [
7
+ '<%= config.bin %> <%= command.id %>',
8
+ ];
9
+ async run() {
10
+ this.log(chalk.green('📋 Fetching AWS accounts...'));
11
+ const service = new HyperdriveSigV4Service();
12
+ try {
13
+ const accounts = await service.accountList();
14
+ if (accounts.length === 0) {
15
+ this.log(chalk.yellow('\nNo AWS accounts registered yet.'));
16
+ this.log(chalk.gray('Use "hd account add" to register an AWS account.'));
17
+ return;
18
+ }
19
+ this.log(chalk.blue(`\n✅ Found ${accounts.length} AWS account(s):\n`));
20
+ for (const account of accounts) {
21
+ this.log(chalk.white(` ${account.name || 'Unnamed'} (${account.accountId})`));
22
+ this.log(chalk.gray(` Region: ${account.defaultRegion}`));
23
+ this.log(chalk.gray(` Role: ${account.roleArn}`));
24
+ if (account.createdAt) {
25
+ this.log(chalk.gray(` Added: ${new Date(account.createdAt).toLocaleDateString()}`));
26
+ }
27
+ this.log('');
28
+ }
29
+ }
30
+ catch (error) {
31
+ const axiosError = error;
32
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
33
+ this.log(chalk.red('❌ Error fetching accounts: ' + errorMessage));
34
+ this.exit(1);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AccountRemove extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ accountId: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
7
+ domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ force: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,57 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ export default class AccountRemove extends Command {
6
+ static description = 'Remove an AWS account from Hyperdrive';
7
+ static examples = [
8
+ '<%= config.bin %> <%= command.id %> --accountId 123456789012',
9
+ '<%= config.bin %> <%= command.id %> --accountId 123456789012 --force',
10
+ ];
11
+ static flags = {
12
+ accountId: Flags.string({
13
+ char: 'a',
14
+ description: 'AWS Account ID (12 digits)',
15
+ required: true,
16
+ }),
17
+ domain: Flags.string({
18
+ char: 'd',
19
+ description: 'Tenant domain (for multi-domain setups)',
20
+ }),
21
+ force: Flags.boolean({
22
+ char: 'f',
23
+ default: false,
24
+ description: 'Skip confirmation prompt',
25
+ }),
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(AccountRemove);
29
+ if (!flags.force) {
30
+ const { confirm } = await inquirer.prompt([{
31
+ default: false,
32
+ message: chalk.yellow(`Are you sure you want to remove account ${flags.accountId}?`),
33
+ name: 'confirm',
34
+ type: 'confirm',
35
+ }]);
36
+ if (!confirm) {
37
+ this.log(chalk.gray('Operation cancelled.'));
38
+ return;
39
+ }
40
+ }
41
+ this.log(chalk.yellow('🗑️ Removing AWS account from Hyperdrive...'));
42
+ const service = new HyperdriveSigV4Service(flags.domain);
43
+ try {
44
+ await service.accountRemove({ accountId: flags.accountId });
45
+ this.log(chalk.green('✅ AWS account removed successfully!'));
46
+ }
47
+ catch (error) {
48
+ const axiosError = error;
49
+ const errorMessage = axiosError.response?.data?.message ?? axiosError.message ?? 'Unknown error';
50
+ this.log(chalk.red('❌ Error removing AWS account: ' + errorMessage));
51
+ if (axiosError.response?.status === 404) {
52
+ this.log(chalk.yellow('\nAccount not found. It may have already been removed.'));
53
+ }
54
+ this.exit(1);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Login extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ ci: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
7
+ domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
+ port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
+ tenant: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ /**
13
+ * Run CI authentication flow (non-interactive)
14
+ */
15
+ private runCIAuth;
16
+ }
@@ -0,0 +1,178 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import open from 'open';
5
+ import ora from 'ora';
6
+ import { TenantService } from '../../services/tenant-service.js';
7
+ import { buildAuthUrl, exchangeCodeForTokens, executeCIAuthFlow, generateCodeChallenge, generateCodeVerifier, getAWSCredentials, getCICredentials, getCredentialsPath, isCI, saveCredentials, startCallbackServer, } from '../../utils/auth-flow.js';
8
+ export default class Login extends Command {
9
+ static description = 'Authenticate with Hyperdrive using OAuth 2.0 PKCE flow (or CI credentials)';
10
+ static examples = [
11
+ '<%= config.bin %> <%= command.id %>',
12
+ '<%= config.bin %> <%= command.id %> --domain acme.hyperdrive.bot',
13
+ '<%= config.bin %> <%= command.id %> --port 9876',
14
+ '<%= config.bin %> <%= command.id %> --ci --domain acme.hyperdrive.bot',
15
+ ];
16
+ static flags = {
17
+ ci: Flags.boolean({
18
+ default: false,
19
+ description: 'Use CI authentication (requires HD_CI_USERNAME and HD_CI_PASSWORD env vars)',
20
+ }),
21
+ domain: Flags.string({
22
+ char: 'd',
23
+ description: 'Tenant domain (e.g., acme.hyperdrive.bot) - allows multiple installations',
24
+ }),
25
+ port: Flags.integer({
26
+ char: 'p',
27
+ default: 8765,
28
+ description: 'Local callback server port',
29
+ }),
30
+ tenant: Flags.string({
31
+ char: 't',
32
+ description: '[DEPRECATED] Use --domain instead',
33
+ hidden: true,
34
+ }),
35
+ };
36
+ async run() {
37
+ const { flags } = await this.parse(Login);
38
+ // Support legacy --tenant flag (fallback to --domain)
39
+ const domainFlag = flags.domain || flags.tenant;
40
+ // Auto-detect CI environment if --ci flag not provided but CI env vars are set
41
+ const ciCredentials = getCICredentials();
42
+ const useCI = flags.ci || (isCI() && ciCredentials !== null);
43
+ // Handle CI authentication
44
+ if (useCI) {
45
+ await this.runCIAuth(domainFlag);
46
+ return;
47
+ }
48
+ const tenantService = new TenantService(domainFlag);
49
+ this.log(chalk.blue('🚀 Starting Hyperdrive authentication...'));
50
+ this.log('');
51
+ // Step 1: Resolve tenant configuration from bootstrap
52
+ let tenantConfig;
53
+ const spinner = ora('Fetching tenant configuration...').start();
54
+ try {
55
+ // Prompt for tenant if not provided
56
+ let tenantDomain = domainFlag;
57
+ if (!tenantDomain) {
58
+ spinner.stop();
59
+ // Get saved tenant domain from config file or env var
60
+ const savedTenantDomain = tenantService.getTenantDomain();
61
+ const answers = await inquirer.prompt([
62
+ {
63
+ default: savedTenantDomain || undefined,
64
+ message: 'Enter your tenant domain:',
65
+ name: 'tenant',
66
+ type: 'input',
67
+ validate: (input) => input.trim().length > 0 || 'Tenant domain is required',
68
+ },
69
+ ]);
70
+ tenantDomain = answers.tenant;
71
+ spinner.start('Fetching tenant configuration...');
72
+ }
73
+ tenantConfig = await tenantService.fetchTenantConfig(tenantDomain);
74
+ spinner.succeed(chalk.green(`Tenant found: ${tenantConfig.displayName}`));
75
+ }
76
+ catch (error) {
77
+ spinner.fail(chalk.red('Failed to fetch tenant configuration'));
78
+ this.log('');
79
+ const errorMessage = error instanceof Error ? error.message : String(error);
80
+ this.error(errorMessage + '\n\n' +
81
+ chalk.yellow('💡 Tips:\n') +
82
+ chalk.gray(' - Check your tenant domain spelling\n') +
83
+ chalk.gray(' - Set HYPERDRIVE_TENANT_DOMAIN environment variable\n') +
84
+ chalk.gray(' - Or run `hd init` to configure your environment'));
85
+ }
86
+ try {
87
+ // Generate PKCE parameters using extracted utilities
88
+ const codeVerifier = generateCodeVerifier();
89
+ const codeChallenge = generateCodeChallenge(codeVerifier);
90
+ // Start local callback server and get promise for auth code
91
+ const authCodePromise = startCallbackServer(flags.port, 300000); // 5 min timeout
92
+ // Open browser for authentication
93
+ const authUrl = buildAuthUrl(tenantConfig, codeChallenge, flags.port);
94
+ this.log(chalk.yellow('📱 Opening browser for authentication...'));
95
+ this.log(chalk.gray(`If browser doesn't open, visit: ${authUrl}`));
96
+ this.log('');
97
+ await open(authUrl);
98
+ spinner.start('Waiting for authentication...');
99
+ // Wait for callback with auth code
100
+ const code = await authCodePromise;
101
+ spinner.succeed(chalk.green('Authentication callback received!'));
102
+ // Exchange code for tokens using extracted utility
103
+ spinner.start('Exchanging authorization code for tokens...');
104
+ const tokens = await exchangeCodeForTokens(tenantConfig, code, codeVerifier, flags.port);
105
+ spinner.succeed(chalk.green('Tokens obtained successfully!'));
106
+ // Get AWS credentials from Cognito Identity Pool using extracted utility
107
+ spinner.start('Obtaining AWS credentials...');
108
+ const awsCredentials = await getAWSCredentials(tenantConfig, tokens.id_token);
109
+ spinner.succeed(chalk.green('AWS credentials obtained!'));
110
+ // Save credentials using extracted utility (domain-specific if specified)
111
+ spinner.start('Saving credentials...');
112
+ saveCredentials({
113
+ ...tokens,
114
+ apiUrl: tenantConfig.apiUrl,
115
+ awsCredentials,
116
+ cognitoConfig: {
117
+ clientId: tenantConfig.cognitoClientId,
118
+ domain: tenantConfig.cognitoDomain,
119
+ identityPoolId: tenantConfig.cognitoIdentityPoolId,
120
+ userPoolId: tenantConfig.cognitoUserPoolId,
121
+ },
122
+ obtainedAt: new Date().toISOString(),
123
+ region: tenantConfig.region,
124
+ tenantDomain: tenantConfig.tenantDomain,
125
+ tenantId: tenantConfig.tenantId,
126
+ }, tenantConfig.tenantDomain);
127
+ spinner.succeed(chalk.green('Credentials saved!'));
128
+ this.log('');
129
+ this.log(chalk.green('✅ Successfully authenticated!'));
130
+ this.log(chalk.gray('You can now use all Hyperdrive CLI commands.'));
131
+ this.log('');
132
+ this.log(chalk.gray(`✓ Credentials saved to ${getCredentialsPath(tenantConfig.tenantDomain)}`));
133
+ this.log(chalk.gray('💡 Credentials refresh automatically when needed. You\'re all set!'));
134
+ }
135
+ catch (error) {
136
+ this.error(chalk.red(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`));
137
+ }
138
+ }
139
+ /**
140
+ * Run CI authentication flow (non-interactive)
141
+ */
142
+ async runCIAuth(domainFlag) {
143
+ this.log(chalk.blue('🤖 Starting CI authentication...'));
144
+ this.log('');
145
+ // Get CI credentials from environment
146
+ const ciCredentials = getCICredentials();
147
+ if (!ciCredentials) {
148
+ this.error(chalk.red('CI authentication requires HD_TOKEN environment variable.\n\n') +
149
+ chalk.yellow('Create a CI token with:\n') +
150
+ chalk.gray(' hd ci account create --name "my-ci" --scope deploy\n\n') +
151
+ chalk.yellow('Then add to your CI environment:\n') +
152
+ chalk.gray(' HD_TOKEN=hd_sk_...'));
153
+ }
154
+ // Require domain for CI auth
155
+ if (!domainFlag) {
156
+ this.error(chalk.red('Domain is required for CI authentication.\n\n') +
157
+ chalk.yellow('Usage:\n') +
158
+ chalk.gray(' hd auth login --ci --domain acme.hyperdrive.bot'));
159
+ }
160
+ const spinner = ora('Authenticating...').start();
161
+ const result = await executeCIAuthFlow({
162
+ logger: (message) => {
163
+ spinner.text = message;
164
+ },
165
+ password: ciCredentials.password,
166
+ tenantDomain: domainFlag,
167
+ username: ciCredentials.username,
168
+ });
169
+ if (!result.success) {
170
+ spinner.fail(chalk.red('Authentication failed'));
171
+ this.error(chalk.red(result.error || 'Unknown error'));
172
+ }
173
+ spinner.succeed(chalk.green('Authenticated successfully!'));
174
+ this.log('');
175
+ this.log(chalk.green('✅ CI authentication complete!'));
176
+ this.log(chalk.gray('You can now use Hyperdrive CLI commands in your CI pipeline.'));
177
+ }
178
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Logout extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,39 @@
1
+ import { Command } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { AuthService } from '../../services/auth-service.js';
5
+ export default class Logout extends Command {
6
+ static description = 'Remove stored credentials and logout';
7
+ static examples = ['<%= config.bin %> <%= command.id %>'];
8
+ async run() {
9
+ const authService = new AuthService();
10
+ // Check if credentials exist
11
+ const credentials = authService.loadCredentials();
12
+ if (!credentials) {
13
+ this.log(chalk.yellow('⚠️ No active session found'));
14
+ return;
15
+ }
16
+ // Confirm logout
17
+ const { confirm } = await inquirer.prompt([
18
+ {
19
+ default: false,
20
+ message: 'Are you sure you want to logout?',
21
+ name: 'confirm',
22
+ type: 'confirm',
23
+ },
24
+ ]);
25
+ if (!confirm) {
26
+ this.log(chalk.gray('Logout cancelled'));
27
+ return;
28
+ }
29
+ try {
30
+ authService.clearCredentials();
31
+ this.log('');
32
+ this.log(chalk.green('✅ Successfully logged out!'));
33
+ this.log(chalk.gray('Run `hd auth login` to authenticate again.'));
34
+ }
35
+ catch (error) {
36
+ this.error(chalk.red(`Logout failed: ${error instanceof Error ? error.message : String(error)}`));
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Refresh extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,66 @@
1
+ import { Command } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { AuthService } from '../../services/auth-service.js';
5
+ export default class Refresh extends Command {
6
+ static description = 'Refresh expired AWS credentials using refresh token';
7
+ static examples = ['<%= config.bin %> <%= command.id %>'];
8
+ async run() {
9
+ const authService = new AuthService();
10
+ const spinner = ora('Checking current credentials...').start();
11
+ try {
12
+ // Load existing credentials
13
+ const credentials = authService.loadCredentials();
14
+ if (!credentials) {
15
+ spinner.fail(chalk.red('No credentials found'));
16
+ this.log('');
17
+ this.log(chalk.yellow('Please run `hd auth login` first'));
18
+ return;
19
+ }
20
+ // Check if cognitoConfig exists
21
+ if (!credentials.cognitoConfig) {
22
+ spinner.fail(chalk.red('Cognito configuration not found'));
23
+ this.log('');
24
+ this.log(chalk.yellow('Please run `hd auth login` again to update credentials'));
25
+ return;
26
+ }
27
+ // Check if refresh is needed
28
+ const needsRefresh = authService.needsRefresh(credentials);
29
+ if (!needsRefresh) {
30
+ spinner.succeed(chalk.green('Credentials are still valid!'));
31
+ const expiresIn = Math.floor((new Date(credentials.awsCredentials.expiration).getTime() - Date.now()) / 1000 / 60);
32
+ this.log(chalk.gray(`Expires in ${expiresIn} minutes`));
33
+ return;
34
+ }
35
+ // Refresh tokens
36
+ spinner.text = 'Refreshing tokens...';
37
+ const newTokens = await authService.refreshTokens(credentials.refresh_token, credentials.cognitoConfig);
38
+ spinner.succeed(chalk.green('Tokens refreshed!'));
39
+ // Get new AWS credentials
40
+ spinner.start('Obtaining new AWS credentials...');
41
+ const newAwsCredentials = await authService.getAWSCredentials(newTokens.id_token, credentials.region, credentials.cognitoConfig);
42
+ spinner.succeed(chalk.green('AWS credentials renewed!'));
43
+ // Save updated credentials (preserve tenant info)
44
+ spinner.start('Saving credentials...');
45
+ authService.saveCredentials({
46
+ ...newTokens,
47
+ apiUrl: credentials.apiUrl,
48
+ awsCredentials: newAwsCredentials,
49
+ cognitoConfig: credentials.cognitoConfig,
50
+ obtainedAt: new Date().toISOString(),
51
+ region: credentials.region,
52
+ tenantDomain: credentials.tenantDomain,
53
+ tenantId: credentials.tenantId,
54
+ });
55
+ spinner.succeed(chalk.green('Credentials saved!'));
56
+ this.log('');
57
+ this.log(chalk.green('✅ Credentials successfully refreshed!'));
58
+ this.log(chalk.gray('Your session has been extended for another hour.'));
59
+ }
60
+ catch (error) {
61
+ spinner.fail(chalk.red('Refresh failed'));
62
+ this.log('');
63
+ this.error(chalk.red(`Failed to refresh credentials: ${error instanceof Error ? error.message : String(error)}\n\nPlease run 'hd auth login' to re-authenticate.`));
64
+ }
65
+ }
66
+ }