@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,242 @@
1
+ /**
2
+ * CloudWatch Log Tailer Service
3
+ *
4
+ * Provides real-time log streaming from CloudWatch for deployment feedback.
5
+ * Uses scoped temporary credentials provided by the API for least-privilege access.
6
+ */
7
+ import { CloudWatchLogsClient, FilterLogEventsCommand, } from '@aws-sdk/client-cloudwatch-logs';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ /**
11
+ * CloudWatch Log Tailer
12
+ *
13
+ * Streams deployment logs in real-time and displays progress to the user.
14
+ */
15
+ export class CloudWatchLogTailer {
16
+ client;
17
+ config;
18
+ currentStage = '';
19
+ isComplete = false;
20
+ lastEventTime = 0;
21
+ options;
22
+ spinner;
23
+ constructor(config, options = {}) {
24
+ this.config = config;
25
+ this.options = {
26
+ pollInterval: options.pollInterval ?? 1000,
27
+ showDebug: options.showDebug ?? false,
28
+ verbose: options.verbose ?? false,
29
+ };
30
+ // Create CloudWatch client with scoped credentials
31
+ this.client = new CloudWatchLogsClient({
32
+ credentials: {
33
+ accessKeyId: config.credentials.accessKeyId,
34
+ secretAccessKey: config.credentials.secretAccessKey,
35
+ sessionToken: config.credentials.sessionToken,
36
+ },
37
+ region: config.region,
38
+ });
39
+ this.spinner = ora({ spinner: 'dots' });
40
+ }
41
+ /**
42
+ * Start tailing logs until deployment completes or fails
43
+ */
44
+ async tail() {
45
+ this.spinner.start(chalk.gray('Connecting to deployment logs...'));
46
+ const startTime = Date.now();
47
+ const timeout = 30 * 60 * 1000; // 30 min timeout
48
+ let retryCount = 0;
49
+ const maxRetries = 60; // 60 retries * 2s = 2 minutes waiting for logs to appear
50
+ let lastActivityTime = Date.now();
51
+ while (!this.isComplete) {
52
+ // Check credentials validity
53
+ if (!this.isCredentialsValid()) {
54
+ this.spinner.fail(chalk.red('Log streaming credentials expired'));
55
+ return {
56
+ message: 'Credentials expired. Deployment may still be running.',
57
+ success: false,
58
+ };
59
+ }
60
+ try {
61
+ const events = await this.fetchNewEvents();
62
+ if (events.length > 0) {
63
+ retryCount = 0; // Reset retry count on successful fetch
64
+ lastActivityTime = Date.now();
65
+ for (const event of events) {
66
+ this.processEvent(event);
67
+ }
68
+ }
69
+ // Check for timeout
70
+ if (Date.now() - startTime > timeout) {
71
+ this.spinner.fail(chalk.red('Deployment timed out after 30 minutes'));
72
+ return { message: 'Timeout', success: false };
73
+ }
74
+ // Warn if no activity for 5 minutes
75
+ if (Date.now() - lastActivityTime > 5 * 60 * 1000 && !this.isComplete) {
76
+ this.spinner.text = chalk.yellow('No activity for 5 minutes, still waiting...');
77
+ }
78
+ await this.sleep(this.options.pollInterval);
79
+ }
80
+ catch (error) {
81
+ const err = error;
82
+ // Log stream may not have events yet
83
+ if (err.name === 'ResourceNotFoundException') {
84
+ retryCount++;
85
+ if (retryCount > maxRetries) {
86
+ this.spinner.fail(chalk.red('Log stream not found after waiting'));
87
+ return { message: 'Log stream unavailable', success: false };
88
+ }
89
+ this.spinner.text = chalk.gray(`Waiting for deployment to start... (${retryCount}/${maxRetries})`);
90
+ await this.sleep(2000);
91
+ continue;
92
+ }
93
+ // Access denied = credentials issue
94
+ if (err.name === 'AccessDeniedException') {
95
+ this.spinner.fail(chalk.red('Access denied to log stream'));
96
+ return { message: 'Access denied', success: false };
97
+ }
98
+ // Unknown error
99
+ this.spinner.fail(chalk.red(`Error: ${err.message}`));
100
+ throw error;
101
+ }
102
+ }
103
+ return {
104
+ message: this.currentStage === 'completed'
105
+ ? 'Deployment complete!'
106
+ : 'Deployment failed',
107
+ success: this.currentStage === 'completed',
108
+ };
109
+ }
110
+ /**
111
+ * Fetch new events from CloudWatch
112
+ */
113
+ async fetchNewEvents() {
114
+ const command = new FilterLogEventsCommand({
115
+ logGroupName: this.config.logGroup,
116
+ logStreamNames: [this.config.logStream],
117
+ startTime: this.lastEventTime + 1, // Exclude already-seen events
118
+ });
119
+ const response = await this.client.send(command);
120
+ return (response.events || [])
121
+ .map((e) => {
122
+ // Update last event time
123
+ if (e.timestamp && e.timestamp > this.lastEventTime) {
124
+ this.lastEventTime = e.timestamp;
125
+ }
126
+ try {
127
+ return JSON.parse(e.message || '{}');
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ })
133
+ .filter((e) => e !== null);
134
+ }
135
+ /**
136
+ * Format duration in human-readable form
137
+ */
138
+ formatDuration(ms) {
139
+ const seconds = Math.floor(ms / 1000);
140
+ if (seconds < 60)
141
+ return `${seconds}s`;
142
+ const minutes = Math.floor(seconds / 60);
143
+ const secs = seconds % 60;
144
+ return `${minutes}m ${secs}s`;
145
+ }
146
+ /**
147
+ * Get color function for log level
148
+ */
149
+ getLevelColor(level) {
150
+ switch (level) {
151
+ case 'debug':
152
+ return chalk.gray;
153
+ case 'error':
154
+ return chalk.red;
155
+ case 'info':
156
+ return chalk.white;
157
+ case 'warn':
158
+ return chalk.yellow;
159
+ default:
160
+ return chalk.white;
161
+ }
162
+ }
163
+ /**
164
+ * Handle a log line event
165
+ */
166
+ handleLogEvent(event) {
167
+ if (!this.options.verbose)
168
+ return;
169
+ if (event.level === 'debug' && !this.options.showDebug)
170
+ return;
171
+ const levelColor = this.getLevelColor(event.level);
172
+ const sourceTag = chalk.dim(`[${event.source}]`);
173
+ const line = levelColor(event.line || '');
174
+ // Temporarily hide spinner, print log, restore
175
+ this.spinner.clear();
176
+ console.log(` ${sourceTag} ${line}`);
177
+ this.spinner.render();
178
+ }
179
+ /**
180
+ * Handle a stage transition event
181
+ */
182
+ handleStageEvent(event) {
183
+ this.currentStage = event.stage || '';
184
+ const { icon, message, progress, stage, totalDuration } = event;
185
+ if (stage === 'completed') {
186
+ const duration = totalDuration ? ` in ${this.formatDuration(totalDuration)}` : '';
187
+ this.spinner.succeed(chalk.green(`${icon || '🎉'} ${message}${duration}`));
188
+ this.isComplete = true;
189
+ }
190
+ else if (stage === 'failed') {
191
+ this.spinner.fail(chalk.red(`${icon || '❌'} ${message}`));
192
+ this.isComplete = true;
193
+ }
194
+ else {
195
+ const bar = this.renderProgressBar(progress || 0);
196
+ const time = totalDuration
197
+ ? chalk.dim(` (${this.formatDuration(totalDuration)})`)
198
+ : '';
199
+ this.spinner.text = `${bar} ${icon || '•'} ${chalk.cyan(message)}${time}`;
200
+ }
201
+ }
202
+ /**
203
+ * Check if credentials are still valid
204
+ */
205
+ isCredentialsValid() {
206
+ const expiration = new Date(this.config.credentials.expiration);
207
+ const now = new Date();
208
+ // Add 60 second buffer
209
+ return expiration.getTime() - now.getTime() > 60_000;
210
+ }
211
+ /**
212
+ * Process a single event
213
+ */
214
+ processEvent(event) {
215
+ if (event.type === 'stage') {
216
+ this.handleStageEvent(event);
217
+ }
218
+ else if (event.type === 'log') {
219
+ this.handleLogEvent(event);
220
+ }
221
+ }
222
+ /**
223
+ * Render a progress bar
224
+ */
225
+ renderProgressBar(progress) {
226
+ if (progress < 0)
227
+ return chalk.red('[FAILED]');
228
+ const width = 20;
229
+ const filled = Math.round((progress / 100) * width);
230
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
231
+ const pct = progress.toString().padStart(3);
232
+ return chalk.blue(`[${bar}] ${pct}%`);
233
+ }
234
+ /**
235
+ * Sleep for specified milliseconds
236
+ */
237
+ sleep(ms) {
238
+ return new Promise((resolve) => {
239
+ setTimeout(resolve, ms);
240
+ });
241
+ }
242
+ }
@@ -0,0 +1,106 @@
1
+ export interface TenantConfig {
2
+ apiUrl: string;
3
+ cognitoClientId: string;
4
+ cognitoDomain: string;
5
+ cognitoIdentityPoolId: string;
6
+ cognitoUserPoolId: string;
7
+ displayName: string;
8
+ region: string;
9
+ tenantDomain: string;
10
+ tenantId: string;
11
+ }
12
+ export interface CLIConfig {
13
+ apiUrl?: string;
14
+ bootstrapUrl?: string;
15
+ domains?: Record<string, {
16
+ apiUrl?: string;
17
+ bootstrapUrl?: string;
18
+ region?: string;
19
+ }>;
20
+ region?: string;
21
+ tenantDomain?: string;
22
+ }
23
+ /**
24
+ * Tenant Service for CLI
25
+ *
26
+ * Handles tenant resolution and Cognito configuration discovery via bootstrap endpoint.
27
+ * Supports environment variables and config file overrides.
28
+ */
29
+ export declare class TenantService {
30
+ private readonly configDir;
31
+ private readonly configPath;
32
+ private readonly defaultBootstrapUrl;
33
+ private readonly defaultDomainPath;
34
+ private readonly domain?;
35
+ constructor(domain?: string);
36
+ /**
37
+ * Clear cached tenant configuration
38
+ */
39
+ clearCache(): void;
40
+ /**
41
+ * Fetch tenant configuration from bootstrap endpoint
42
+ */
43
+ fetchTenantConfig(tenantDomain?: string): Promise<TenantConfig>;
44
+ /**
45
+ * Get all configured domains
46
+ */
47
+ getConfiguredDomains(): string[];
48
+ /**
49
+ * Get current tenant configuration (from cache or fetch)
50
+ */
51
+ getCurrentTenant(): Promise<TenantConfig>;
52
+ /**
53
+ * Get default domain from file
54
+ */
55
+ getDefaultDomain(): null | string;
56
+ /**
57
+ * Get tenant domain with fallback chain:
58
+ * 1. Constructor domain parameter (from --domain flag)
59
+ * 2. Environment variable
60
+ * 3. Default domain file
61
+ * 4. Legacy single domain from config
62
+ * 5. Return null (caller should prompt user interactively)
63
+ */
64
+ getTenantDomain(): null | string;
65
+ /**
66
+ * Save CLI configuration to file
67
+ */
68
+ saveConfig(config: CLIConfig): void;
69
+ /**
70
+ * Set default domain
71
+ */
72
+ setDefaultDomain(domain: string): void;
73
+ /**
74
+ * Set tenant domain in config file
75
+ *
76
+ * Public method for init wizard to persist tenant domain configuration.
77
+ * Creates config directory if needed and applies 0o600 permissions.
78
+ *
79
+ * @param domain - The tenant domain to save
80
+ */
81
+ setTenantDomain(domain: string): void;
82
+ /**
83
+ * Display current configuration
84
+ */
85
+ showConfig(): void;
86
+ /**
87
+ * Get API URL for a region
88
+ */
89
+ private getApiUrl;
90
+ /**
91
+ * Get bootstrap URL with fallback chain:
92
+ * 1. Environment variable
93
+ * 2. Config file
94
+ * 3. Construct from tenant domain
95
+ * 4. Default URL
96
+ */
97
+ private getBootstrapUrl;
98
+ /**
99
+ * Load CLI configuration from file
100
+ */
101
+ private loadConfigFile;
102
+ /**
103
+ * Save tenant domain to config file for future use
104
+ */
105
+ private saveTenantDomainToConfig;
106
+ }
@@ -0,0 +1,332 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ /**
7
+ * Tenant Service for CLI
8
+ *
9
+ * Handles tenant resolution and Cognito configuration discovery via bootstrap endpoint.
10
+ * Supports environment variables and config file overrides.
11
+ */
12
+ export class TenantService {
13
+ configDir;
14
+ configPath;
15
+ defaultBootstrapUrl = 'https://api.hyperdrive.bot/tenant/bootstrap';
16
+ defaultDomainPath;
17
+ domain;
18
+ constructor(domain) {
19
+ this.configDir = join(homedir(), '.hyperdrive');
20
+ this.configPath = join(this.configDir, 'config.json');
21
+ this.defaultDomainPath = join(this.configDir, 'default-domain');
22
+ this.domain = domain;
23
+ }
24
+ /**
25
+ * Clear cached tenant configuration
26
+ */
27
+ clearCache() {
28
+ const config = this.loadConfigFile();
29
+ if (config) {
30
+ // Remove tenant-specific settings but keep custom URLs
31
+ const { tenantDomain, ...rest } = config;
32
+ this.saveConfig(rest);
33
+ }
34
+ }
35
+ /**
36
+ * Fetch tenant configuration from bootstrap endpoint
37
+ */
38
+ async fetchTenantConfig(tenantDomain) {
39
+ const domain = tenantDomain || this.getTenantDomain();
40
+ if (!domain) {
41
+ throw new Error('Tenant not configured. Run `hd init` to set up your environment.');
42
+ }
43
+ const bootstrapUrl = this.getBootstrapUrl();
44
+ try {
45
+ console.log(chalk.gray(`🔗 Fetching tenant config for: ${domain}`));
46
+ console.log(chalk.gray(`📍 Using bootstrap endpoint: ${bootstrapUrl}`));
47
+ const response = await axios.get(bootstrapUrl, {
48
+ headers: {
49
+ 'Accept': 'application/json',
50
+ 'X-Tenant-Domain': domain,
51
+ },
52
+ });
53
+ const bootstrap = response.data;
54
+ if (!bootstrap.amplifyConfig) {
55
+ throw new Error('Tenant authentication not configured');
56
+ }
57
+ const { Cognito } = bootstrap.amplifyConfig.Auth;
58
+ const region = Cognito.region;
59
+ // Extract Cognito domain from OAuth config or construct from tenant ID
60
+ // The bootstrap response may provide either:
61
+ // - Just the prefix (e.g., "api-tenants-dev-semana-43-a1c4ea06")
62
+ // - Full domain (e.g., "api-tenants-dev-devsquad-804677f8.auth.sa-east-1.amazoncognito.com")
63
+ let cognitoDomain;
64
+ if (Cognito.loginWith?.oauth?.domain) {
65
+ const oauthDomain = Cognito.loginWith.oauth.domain;
66
+ // Check if domain already includes amazoncognito.com (full domain provided)
67
+ if (oauthDomain.includes('.amazoncognito.com')) {
68
+ cognitoDomain = oauthDomain;
69
+ }
70
+ else {
71
+ cognitoDomain = `${oauthDomain}.auth.${region}.amazoncognito.com`;
72
+ }
73
+ }
74
+ else {
75
+ // Fallback: construct from tenant ID
76
+ cognitoDomain = `${bootstrap.tenantId}.auth.${region}.amazoncognito.com`;
77
+ }
78
+ const config = {
79
+ apiUrl: this.getApiUrl(region, bootstrap.amplifyConfig.API, domain),
80
+ cognitoClientId: Cognito.userPoolClientId,
81
+ cognitoDomain,
82
+ cognitoIdentityPoolId: Cognito.identityPoolId || '',
83
+ cognitoUserPoolId: Cognito.userPoolId,
84
+ displayName: bootstrap.displayName,
85
+ region,
86
+ tenantDomain: domain,
87
+ tenantId: bootstrap.tenantId,
88
+ };
89
+ // Cache the tenant domain for future use
90
+ this.saveTenantDomainToConfig(domain);
91
+ // Set as default domain if no default exists
92
+ if (!this.getDefaultDomain()) {
93
+ this.setDefaultDomain(domain);
94
+ }
95
+ return config;
96
+ }
97
+ catch (error) {
98
+ if (axios.isAxiosError(error)) {
99
+ if (error.response?.status === 404) {
100
+ throw new Error(`Tenant not found: ${domain}`);
101
+ }
102
+ throw new Error(`Failed to fetch tenant config: ${error.response?.data?.message || error.message}`);
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ /**
108
+ * Get all configured domains
109
+ */
110
+ getConfiguredDomains() {
111
+ const config = this.loadConfigFile();
112
+ const domains = new Set();
113
+ // Add legacy single domain if exists
114
+ if (config?.tenantDomain) {
115
+ domains.add(config.tenantDomain);
116
+ }
117
+ // Add multi-domain entries
118
+ if (config?.domains) {
119
+ Object.keys(config.domains).forEach(d => domains.add(d));
120
+ }
121
+ return Array.from(domains);
122
+ }
123
+ /**
124
+ * Get current tenant configuration (from cache or fetch)
125
+ */
126
+ async getCurrentTenant() {
127
+ return this.fetchTenantConfig();
128
+ }
129
+ /**
130
+ * Get default domain from file
131
+ */
132
+ getDefaultDomain() {
133
+ try {
134
+ if (!existsSync(this.defaultDomainPath)) {
135
+ return null;
136
+ }
137
+ return readFileSync(this.defaultDomainPath, 'utf8').trim();
138
+ }
139
+ catch (error) {
140
+ return null;
141
+ }
142
+ }
143
+ /**
144
+ * Get tenant domain with fallback chain:
145
+ * 1. Constructor domain parameter (from --domain flag)
146
+ * 2. Environment variable
147
+ * 3. Default domain file
148
+ * 4. Legacy single domain from config
149
+ * 5. Return null (caller should prompt user interactively)
150
+ */
151
+ getTenantDomain() {
152
+ // Priority 1: Explicit domain parameter (from --domain flag)
153
+ if (this.domain) {
154
+ return this.domain;
155
+ }
156
+ // Priority 2: Environment variable
157
+ if (process.env.HYPERDRIVE_TENANT_DOMAIN) {
158
+ return process.env.HYPERDRIVE_TENANT_DOMAIN;
159
+ }
160
+ // Priority 3: Default domain file
161
+ const defaultDomain = this.getDefaultDomain();
162
+ if (defaultDomain) {
163
+ return defaultDomain;
164
+ }
165
+ // Priority 4: Legacy single domain from config file
166
+ const config = this.loadConfigFile();
167
+ if (config?.tenantDomain) {
168
+ return config.tenantDomain;
169
+ }
170
+ // Priority 5: Return null to trigger interactive prompt
171
+ return null;
172
+ }
173
+ /**
174
+ * Save CLI configuration to file
175
+ */
176
+ saveConfig(config) {
177
+ try {
178
+ if (!existsSync(this.configDir)) {
179
+ mkdirSync(this.configDir, { recursive: true });
180
+ }
181
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
182
+ console.log(chalk.gray(`✓ Configuration saved to ${this.configPath}`));
183
+ }
184
+ catch (error) {
185
+ console.error(chalk.red('Failed to save configuration:'), error);
186
+ }
187
+ }
188
+ /**
189
+ * Set default domain
190
+ */
191
+ setDefaultDomain(domain) {
192
+ try {
193
+ if (!existsSync(this.configDir)) {
194
+ mkdirSync(this.configDir, { recursive: true });
195
+ }
196
+ writeFileSync(this.defaultDomainPath, domain, { mode: 0o600 });
197
+ console.log(chalk.gray(`✓ Default domain set to ${domain}`));
198
+ }
199
+ catch (error) {
200
+ console.error(chalk.red('Failed to set default domain:'), error);
201
+ }
202
+ }
203
+ /**
204
+ * Set tenant domain in config file
205
+ *
206
+ * Public method for init wizard to persist tenant domain configuration.
207
+ * Creates config directory if needed and applies 0o600 permissions.
208
+ *
209
+ * @param domain - The tenant domain to save
210
+ */
211
+ setTenantDomain(domain) {
212
+ this.saveTenantDomainToConfig(domain);
213
+ }
214
+ /**
215
+ * Display current configuration
216
+ */
217
+ showConfig() {
218
+ const config = this.loadConfigFile();
219
+ const tenantDomain = this.getTenantDomain();
220
+ const bootstrapUrl = this.getBootstrapUrl();
221
+ console.log(chalk.blue('🔧 Current Configuration'));
222
+ console.log('');
223
+ console.log(chalk.white('Bootstrap URL:'), chalk.cyan(bootstrapUrl));
224
+ console.log(chalk.white('Source:'), process.env.HYPERDRIVE_BOOTSTRAP_URL
225
+ ? chalk.yellow('ENV')
226
+ : config?.bootstrapUrl
227
+ ? chalk.green('Config File')
228
+ : chalk.gray('Default'));
229
+ console.log('');
230
+ console.log(chalk.white('Tenant Domain:'), tenantDomain ? chalk.cyan(tenantDomain) : chalk.gray('Not set'));
231
+ if (tenantDomain) {
232
+ console.log(chalk.white('Source:'), process.env.HYPERDRIVE_TENANT_DOMAIN
233
+ ? chalk.yellow('ENV')
234
+ : config?.tenantDomain
235
+ ? chalk.green('Config File')
236
+ : chalk.gray('Unknown'));
237
+ }
238
+ console.log('');
239
+ console.log(chalk.gray('Config file: ' + this.configPath));
240
+ }
241
+ /**
242
+ * Get API URL for a region
243
+ */
244
+ getApiUrl(region, apiConfig, domain) {
245
+ // Priority 1: Environment variable (for testing/override)
246
+ if (process.env.HYPERDRIVE_API_URL) {
247
+ console.log(chalk.gray(`✓ Using API endpoint from environment: ${process.env.HYPERDRIVE_API_URL}`));
248
+ return process.env.HYPERDRIVE_API_URL;
249
+ }
250
+ // Priority 2: Domain-specific config (for multi-domain setups)
251
+ const config = this.loadConfigFile();
252
+ if (domain && config?.domains?.[domain]?.apiUrl) {
253
+ const domainApiUrl = config.domains[domain].apiUrl;
254
+ console.log(chalk.gray(`✓ Using API endpoint from domain config: ${domainApiUrl}`));
255
+ return domainApiUrl;
256
+ }
257
+ // Priority 3: Legacy global config file (for manual override)
258
+ if (config?.apiUrl) {
259
+ console.log(chalk.gray(`✓ Using API endpoint from config: ${config.apiUrl}`));
260
+ return config.apiUrl;
261
+ }
262
+ // Priority 4: From bootstrap API config (REQUIRED - no fallback)
263
+ if (apiConfig?.REST?.hyperdrive?.endpoint) {
264
+ console.log(chalk.gray(`✓ Using API endpoint from bootstrap: ${apiConfig.REST.hyperdrive.endpoint}`));
265
+ return apiConfig.REST.hyperdrive.endpoint;
266
+ }
267
+ // No hyperdrive endpoint found - user doesn't have access
268
+ throw new Error('Hyperdrive API not available for this tenant.\n\n' +
269
+ chalk.yellow('This tenant does not have access to Hyperdrive.\n') +
270
+ chalk.gray('Please contact your administrator to enable the Hyperdrive module.'));
271
+ }
272
+ /**
273
+ * Get bootstrap URL with fallback chain:
274
+ * 1. Environment variable
275
+ * 2. Config file
276
+ * 3. Construct from tenant domain
277
+ * 4. Default URL
278
+ */
279
+ getBootstrapUrl() {
280
+ // Priority 1: Environment variable
281
+ if (process.env.HYPERDRIVE_BOOTSTRAP_URL) {
282
+ return process.env.HYPERDRIVE_BOOTSTRAP_URL;
283
+ }
284
+ // Priority 2: Config file
285
+ const config = this.loadConfigFile();
286
+ if (config?.bootstrapUrl) {
287
+ return config.bootstrapUrl;
288
+ }
289
+ // Priority 3: Construct from tenant domain if configured
290
+ const tenantDomain = this.getTenantDomain();
291
+ if (tenantDomain) {
292
+ return `https://${tenantDomain}/tenant/bootstrap`;
293
+ }
294
+ // Priority 4: Default
295
+ return this.defaultBootstrapUrl;
296
+ }
297
+ /**
298
+ * Load CLI configuration from file
299
+ */
300
+ loadConfigFile() {
301
+ try {
302
+ if (!existsSync(this.configPath)) {
303
+ return null;
304
+ }
305
+ const data = readFileSync(this.configPath, 'utf8');
306
+ return JSON.parse(data);
307
+ }
308
+ catch (error) {
309
+ console.error(chalk.yellow('⚠️ Failed to load config file, using defaults'));
310
+ return null;
311
+ }
312
+ }
313
+ /**
314
+ * Save tenant domain to config file for future use
315
+ */
316
+ saveTenantDomainToConfig(tenantDomain) {
317
+ const existingConfig = this.loadConfigFile() || {};
318
+ // Initialize domains object if it doesn't exist
319
+ if (!existingConfig.domains) {
320
+ existingConfig.domains = {};
321
+ }
322
+ // Add this domain to the domains map (even if just an empty object for now)
323
+ if (!existingConfig.domains[tenantDomain]) {
324
+ existingConfig.domains[tenantDomain] = {};
325
+ }
326
+ // Also keep legacy tenantDomain for backward compatibility
327
+ this.saveConfig({
328
+ ...existingConfig,
329
+ tenantDomain,
330
+ });
331
+ }
332
+ }