@hyperdrive.bot/cli 1.0.6 → 1.0.8

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 (97) hide show
  1. package/README.md +415 -67
  2. package/dist/commands/account/add.d.ts +6 -6
  3. package/dist/commands/account/list.d.ts +3 -0
  4. package/dist/commands/account/list.js +9 -2
  5. package/dist/commands/account/remove.d.ts +3 -3
  6. package/dist/commands/auth/login.d.ts +4 -4
  7. package/dist/commands/auth/login.js +1 -0
  8. package/dist/commands/ci/account/create.d.ts +7 -6
  9. package/dist/commands/ci/account/create.js +49 -3
  10. package/dist/commands/ci/account/delete.d.ts +3 -3
  11. package/dist/commands/ci/account/list.d.ts +2 -2
  12. package/dist/commands/config/get.d.ts +1 -1
  13. package/dist/commands/config/set.d.ts +2 -2
  14. package/dist/commands/deployment/create.d.ts +10 -10
  15. package/dist/commands/deployment/get.d.ts +4 -4
  16. package/dist/commands/deployment/launch.d.ts +6 -6
  17. package/dist/commands/deployment/list.d.ts +3 -3
  18. package/dist/commands/deployment/list.js +17 -17
  19. package/dist/commands/domain/switch.d.ts +1 -1
  20. package/dist/commands/example.d.ts +3 -3
  21. package/dist/commands/git/connect.d.ts +2 -2
  22. package/dist/commands/git/connect.js +1 -0
  23. package/dist/commands/git/disconnect.d.ts +3 -3
  24. package/dist/commands/git/list.d.ts +2 -2
  25. package/dist/commands/git/sync.d.ts +7 -7
  26. package/dist/commands/git/sync.js +24 -23
  27. package/dist/commands/init.d.ts +1 -0
  28. package/dist/commands/init.js +20 -19
  29. package/dist/commands/jira/connect.d.ts +2 -1
  30. package/dist/commands/jira/connect.js +17 -6
  31. package/dist/commands/jira/hook/add.d.ts +17 -0
  32. package/dist/commands/jira/hook/add.js +147 -0
  33. package/dist/commands/jira/hook/list.d.ts +14 -0
  34. package/dist/commands/jira/hook/list.js +105 -0
  35. package/dist/commands/jira/hook/remove.d.ts +15 -0
  36. package/dist/commands/jira/hook/remove.js +119 -0
  37. package/dist/commands/jira/hook/toggle.d.ts +15 -0
  38. package/dist/commands/jira/hook/toggle.js +136 -0
  39. package/dist/commands/jira/status.d.ts +1 -1
  40. package/dist/commands/module/analyze.d.ts +5 -5
  41. package/dist/commands/module/create.d.ts +17 -17
  42. package/dist/commands/module/create.js +9 -1
  43. package/dist/commands/module/destroy.d.ts +3 -3
  44. package/dist/commands/module/get.d.ts +2 -2
  45. package/dist/commands/module/link.d.ts +4 -4
  46. package/dist/commands/module/list.d.ts +1 -1
  47. package/dist/commands/module/list.js +12 -11
  48. package/dist/commands/module/reanalyze.d.ts +6 -6
  49. package/dist/commands/module/update.d.ts +19 -19
  50. package/dist/commands/parameter/add.d.ts +7 -7
  51. package/dist/commands/parameter/backfill.d.ts +4 -4
  52. package/dist/commands/parameter/backfill.js +4 -3
  53. package/dist/commands/parameter/clear.d.ts +6 -6
  54. package/dist/commands/parameter/list.d.ts +6 -6
  55. package/dist/commands/parameter/list.js +4 -3
  56. package/dist/commands/parameter/pull.d.ts +6 -6
  57. package/dist/commands/parameter/remove.d.ts +7 -7
  58. package/dist/commands/parameter/sync.d.ts +6 -6
  59. package/dist/commands/parameter/update.d.ts +7 -7
  60. package/dist/commands/project/init.d.ts +21 -0
  61. package/dist/commands/project/init.js +576 -0
  62. package/dist/commands/project/list.d.ts +10 -0
  63. package/dist/commands/project/list.js +119 -0
  64. package/dist/commands/project/status.d.ts +13 -0
  65. package/dist/commands/project/status.js +163 -0
  66. package/dist/commands/project/sync.d.ts +26 -0
  67. package/dist/commands/project/sync.js +388 -0
  68. package/dist/commands/stage/access.d.ts +15 -0
  69. package/dist/commands/stage/access.js +130 -0
  70. package/dist/commands/stage/create.d.ts +11 -11
  71. package/dist/commands/stage/list.d.ts +1 -1
  72. package/dist/commands/stage/list.js +21 -20
  73. package/dist/commands/stage/revoke.d.ts +18 -0
  74. package/dist/commands/stage/revoke.js +171 -0
  75. package/dist/commands/stage/share.d.ts +23 -0
  76. package/dist/commands/stage/share.js +292 -0
  77. package/dist/commands/test-api.d.ts +1 -1
  78. package/dist/services/auth-service.d.ts +15 -82
  79. package/dist/services/auth-service.js +24 -237
  80. package/dist/services/hyperdrive-sigv4.d.ts +162 -24
  81. package/dist/services/hyperdrive-sigv4.js +107 -193
  82. package/dist/services/tenant-service.d.ts +6 -0
  83. package/dist/services/tenant-service.js +13 -0
  84. package/dist/utils/account-flow.d.ts +2 -2
  85. package/dist/utils/account-flow.js +4 -4
  86. package/dist/utils/auth-flow.d.ts +1 -0
  87. package/dist/utils/auth-flow.js +2 -0
  88. package/dist/utils/git-flow.d.ts +1 -0
  89. package/dist/utils/git-flow.js +2 -2
  90. package/dist/utils/hook-flow.d.ts +21 -0
  91. package/dist/utils/hook-flow.js +154 -0
  92. package/dist/utils/jira-flow.d.ts +2 -2
  93. package/dist/utils/jira-flow.js +4 -4
  94. package/dist/utils/table.d.ts +17 -0
  95. package/dist/utils/table.js +41 -0
  96. package/oclif.manifest.json +844 -154
  97. package/package.json +59 -15
@@ -1,7 +1,8 @@
1
- import { Command, Flags, ux } from '@oclif/core';
1
+ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import * as ora from 'ora';
4
4
  import { GitService } from '../../services/git.js';
5
+ import { printTable, printHeader } from '../../utils/table.js';
5
6
  export default class Sync extends Command {
6
7
  static description = 'Sync specific branches or all open branches with a source branch (default: master)';
7
8
  static examples = [
@@ -131,7 +132,7 @@ export default class Sync extends Command {
131
132
  }
132
133
  }
133
134
  displayConfiguration(syncOptions) {
134
- ux.styledHeader('🔧 Sync Configuration');
135
+ printHeader('🔧 Sync Configuration', (msg) => this.log(msg));
135
136
  const configTable = {
136
137
  'Merge Strategy': chalk.cyan(syncOptions.mergeStrategy),
137
138
  'Mode': syncOptions.all
@@ -147,7 +148,7 @@ export default class Sync extends Command {
147
148
  }
148
149
  displayResults(results, duration) {
149
150
  this.log(); // Empty line for spacing
150
- ux.styledHeader('📊 Sync Results');
151
+ printHeader('📊 Sync Results', (msg) => this.log(msg));
151
152
  // Count results by type
152
153
  const successCount = results.filter(r => r.success).length;
153
154
  const conflictCount = results.filter(r => r.conflicts).length;
@@ -160,9 +161,10 @@ export default class Sync extends Command {
160
161
  { metric: 'Errors', status: 'error', value: errorCount.toString() },
161
162
  { metric: 'Duration', status: 'info', value: duration }
162
163
  ];
163
- ux.table(summaryData, {
164
+ printTable(summaryData, {
164
165
  metric: { header: 'Metric' },
165
166
  status: {
167
+ header: 'Status',
166
168
  get: (row) => {
167
169
  switch (row.status) {
168
170
  case 'success': return chalk.green('✅');
@@ -171,13 +173,12 @@ export default class Sync extends Command {
171
173
  default: return chalk.blue('ℹ️');
172
174
  }
173
175
  },
174
- header: 'Status'
175
176
  },
176
177
  value: { header: 'Count' }
177
- }, { 'no-truncate': true });
178
+ }, (msg) => this.log(msg));
178
179
  if (results.length > 0) {
179
180
  this.log(); // Empty line for spacing
180
- ux.styledHeader('📋 Branch Details');
181
+ printHeader('📋 Branch Details', (msg) => this.log(msg));
181
182
  // Convert results to proper table format
182
183
  const tableData = results.map(result => ({
183
184
  branch: result.branch,
@@ -186,45 +187,45 @@ export default class Sync extends Command {
186
187
  success: result.success
187
188
  }));
188
189
  // Display detailed results
189
- ux.table(tableData, {
190
+ printTable(tableData, {
190
191
  branch: {
192
+ header: 'Branch',
191
193
  get: (row) => chalk.cyan(row.branch),
192
- header: 'Branch'
193
194
  },
194
- message: {
195
+ status: {
196
+ header: 'Status',
195
197
  get: (row) => {
196
198
  if (row.success)
197
- return chalk.gray('Successfully merged and pushed');
199
+ return chalk.green(' Synced');
198
200
  if (row.conflicts)
199
- return chalk.yellow('Merge conflicts detected - manual resolution required');
200
- return chalk.red(row.error || 'Unknown error');
201
+ return chalk.yellow('⚠️ Conflicts');
202
+ return chalk.red(' Failed');
201
203
  },
202
- header: 'Message'
203
204
  },
204
- status: {
205
+ message: {
206
+ header: 'Message',
205
207
  get: (row) => {
206
208
  if (row.success)
207
- return chalk.green(' Synced');
209
+ return chalk.gray('Successfully merged and pushed');
208
210
  if (row.conflicts)
209
- return chalk.yellow('⚠️ Conflicts');
210
- return chalk.red(' Failed');
211
+ return chalk.yellow('Merge conflicts detected - manual resolution required');
212
+ return chalk.red(row.error || 'Unknown error');
211
213
  },
212
- header: 'Status'
213
214
  }
214
- }, { 'no-truncate': true });
215
+ }, (msg) => this.log(msg));
215
216
  }
216
217
  // Display actionable insights
217
218
  this.log(); // Empty line for spacing
218
219
  if (conflictCount > 0) {
219
- ux.warn(`${conflictCount} branch(es) have merge conflicts and need manual resolution`);
220
+ this.warn(`${conflictCount} branch(es) have merge conflicts and need manual resolution`);
220
221
  this.log(chalk.gray(' 💡 Tip: Resolve conflicts manually and push the changes'));
221
222
  }
222
223
  if (errorCount > 0) {
223
- ux.warn(`${errorCount} branch(es) failed to sync`);
224
+ this.warn(`${errorCount} branch(es) failed to sync`);
224
225
  this.log(chalk.gray(' 💡 Tip: Check branch permissions and network connectivity'));
225
226
  }
226
227
  if (successCount === results.length && results.length > 0) {
227
- ux.info(chalk.green('🎉 All branches synced successfully!'));
228
+ this.log(chalk.green('🎉 All branches synced successfully!'));
228
229
  }
229
230
  }
230
231
  formatDuration(startTime) {
@@ -11,6 +11,7 @@ export type AccountAddFunction = (options: {
11
11
  tenantDomain: string;
12
12
  }) => Promise<AccountResult>;
13
13
  export type GitConnectFunction = (options: {
14
+ domain?: string;
14
15
  logger?: (message: string) => void;
15
16
  provider: 'github' | 'gitlab';
16
17
  }) => Promise<GitConnectResult>;
@@ -125,10 +125,10 @@ export default class Init extends Command {
125
125
  await this.addAwsAccounts(tenantDomain);
126
126
  // Step 4: Git Provider Setup
127
127
  this.log('');
128
- await this.connectGitProvider();
128
+ await this.connectGitProvider(tenantDomain);
129
129
  // Step 5: Jira Integration Setup
130
130
  this.log('');
131
- await this.connectJira();
131
+ await this.connectJira(tenantDomain);
132
132
  // Display summary
133
133
  this.showSummary(tenantDomain);
134
134
  }
@@ -247,7 +247,7 @@ export default class Init extends Command {
247
247
  }
248
248
  // Step 3: Register account with API (WITH SPINNER - actual API work)
249
249
  const spinner = ora('Registering AWS account...').start();
250
- const result = await registerAccount(accountData);
250
+ const result = await registerAccount(accountData, tenantDomain);
251
251
  if (!result.success) {
252
252
  spinner.fail('Failed to register AWS account');
253
253
  this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
@@ -257,7 +257,7 @@ export default class Init extends Command {
257
257
  this.log(chalk.green('✓') + ` Account ${result.accountId} added${result.accountAlias ? ` (${result.accountAlias})` : ''}`);
258
258
  // Step 3: Handle role creation if needed
259
259
  if (result.quickCreateUrl) {
260
- await this.handleRoleCreation(result);
260
+ await this.handleRoleCreation(result, tenantDomain);
261
261
  }
262
262
  return {
263
263
  accountId: result.accountId,
@@ -439,7 +439,7 @@ export default class Init extends Command {
439
439
  * - Retry logic on failure
440
440
  * - Skip option with helpful message
441
441
  */
442
- async connectGitProvider() {
442
+ async connectGitProvider(tenantDomain) {
443
443
  this.log(chalk.blue('Git Provider Setup'));
444
444
  this.log(chalk.dim('Connect GitHub or GitLab for automated deployments'));
445
445
  this.log('');
@@ -466,7 +466,7 @@ export default class Init extends Command {
466
466
  return;
467
467
  }
468
468
  // Execute OAuth flow with retry logic
469
- await this.executeGitOAuthFlow(provider);
469
+ await this.executeGitOAuthFlow(provider, 0, tenantDomain);
470
470
  }
471
471
  /**
472
472
  * Connect Jira integration step in the init wizard
@@ -477,7 +477,7 @@ export default class Init extends Command {
477
477
  * 3. Pre-register domain with API
478
478
  * 4. Display marketplace URL for app installation
479
479
  */
480
- async connectJira() {
480
+ async connectJira(tenantDomain) {
481
481
  this.log(chalk.blue('Jira Integration Setup'));
482
482
  this.log(chalk.dim('Connect Jira for automated project management integration'));
483
483
  this.log('');
@@ -504,7 +504,7 @@ export default class Init extends Command {
504
504
  return;
505
505
  }
506
506
  // Execute Jira connection flow with retry limit
507
- await this.executeJiraConnect(0);
507
+ await this.executeJiraConnect(0, tenantDomain);
508
508
  }
509
509
  /**
510
510
  * Display summary of connected accounts
@@ -556,11 +556,12 @@ export default class Init extends Command {
556
556
  * @param provider - Git provider to connect (github or gitlab)
557
557
  * @param attemptCount - Current retry attempt (0-indexed)
558
558
  */
559
- async executeGitOAuthFlow(provider, attemptCount = 0) {
559
+ async executeGitOAuthFlow(provider, attemptCount = 0, tenantDomain) {
560
560
  const providerName = provider === 'github' ? 'GitHub' : 'GitLab';
561
561
  const spinner = ora(`Connecting to ${providerName}...`).start();
562
562
  try {
563
563
  const result = await getGitConnectImpl()({
564
+ domain: tenantDomain,
564
565
  logger: (message) => {
565
566
  spinner.text = message;
566
567
  },
@@ -589,7 +590,7 @@ export default class Init extends Command {
589
590
  spinner.fail(`Failed to connect to ${providerName}`);
590
591
  this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
591
592
  // Offer retry
592
- await this.handleGitRetry(provider, attemptCount);
593
+ await this.handleGitRetry(provider, attemptCount, tenantDomain);
593
594
  }
594
595
  }
595
596
  catch (error) {
@@ -610,19 +611,19 @@ export default class Init extends Command {
610
611
  *
611
612
  * @param attemptCount - Current retry attempt (0-indexed)
612
613
  */
613
- async executeJiraConnect(attemptCount = 0) {
614
+ async executeJiraConnect(attemptCount = 0, tenantDomain) {
614
615
  const MAX_RETRIES = 3;
615
616
  try {
616
617
  // Step 1: Collect Jira domain from user (NO SPINNER - user needs to see prompt!)
617
618
  const domainData = await promptJiraDomain();
618
619
  // Step 2: Register domain with API (WITH SPINNER - actual API work)
619
620
  const spinner = ora('Registering Jira domain...').start();
620
- const result = await registerJiraDomain(domainData.jiraDomain);
621
+ const result = await registerJiraDomain(domainData.jiraDomain, tenantDomain);
621
622
  if (!result.success) {
622
623
  spinner.fail('Failed to register Jira domain');
623
624
  this.log(chalk.red('✗') + ` Error: ${result.error || 'Unknown error'}`);
624
625
  // Offer retry if under max attempts
625
- await this.handleJiraRetry(attemptCount);
626
+ await this.handleJiraRetry(attemptCount, tenantDomain);
626
627
  return;
627
628
  }
628
629
  spinner.succeed('Jira domain registered successfully');
@@ -665,7 +666,7 @@ export default class Init extends Command {
665
666
  * @param provider - Git provider to connect (github or gitlab)
666
667
  * @param attemptCount - Current retry attempt (0-indexed)
667
668
  */
668
- async handleGitRetry(provider, attemptCount) {
669
+ async handleGitRetry(provider, attemptCount, tenantDomain) {
669
670
  const MAX_RETRIES = 3;
670
671
  if (attemptCount >= MAX_RETRIES) {
671
672
  // Max retries reached
@@ -695,7 +696,7 @@ export default class Init extends Command {
695
696
  }]);
696
697
  if (retry) {
697
698
  // Re-execute OAuth flow for same provider with incremented attempt count
698
- await this.executeGitOAuthFlow(provider, attemptCount + 1);
699
+ await this.executeGitOAuthFlow(provider, attemptCount + 1, tenantDomain);
699
700
  }
700
701
  else {
701
702
  // User chose not to retry - mark as skipped
@@ -708,7 +709,7 @@ export default class Init extends Command {
708
709
  *
709
710
  * @param attemptCount - Current retry attempt (0-indexed)
710
711
  */
711
- async handleJiraRetry(attemptCount) {
712
+ async handleJiraRetry(attemptCount, tenantDomain) {
712
713
  const MAX_RETRIES = 3;
713
714
  if (attemptCount >= MAX_RETRIES) {
714
715
  // Max retries reached
@@ -738,7 +739,7 @@ export default class Init extends Command {
738
739
  }]);
739
740
  if (retry) {
740
741
  // Re-execute Jira connection with incremented attempt count
741
- await this.executeJiraConnect(attemptCount + 1);
742
+ await this.executeJiraConnect(attemptCount + 1, tenantDomain);
742
743
  }
743
744
  else {
744
745
  // User chose not to retry - mark as skipped
@@ -749,7 +750,7 @@ export default class Init extends Command {
749
750
  /**
750
751
  * Handle cross-account IAM role creation
751
752
  */
752
- async handleRoleCreation(result) {
753
+ async handleRoleCreation(result, tenantDomain) {
753
754
  this.log('');
754
755
  this.log(chalk.yellow('⚠') + ' A cross-account IAM role needs to be created.');
755
756
  const { openBrowser } = await inquirer.prompt([{
@@ -766,7 +767,7 @@ export default class Init extends Command {
766
767
  await openCloudFormationUrl(result.quickCreateUrl);
767
768
  // Wait for role verification
768
769
  const verifySpinner = ora('Waiting for role verification...').start();
769
- const verifyResult = await waitForRoleVerification(result.accountId, (message) => { verifySpinner.text = message; });
770
+ const verifyResult = await waitForRoleVerification(result.accountId, (message) => { verifySpinner.text = message; }, tenantDomain);
770
771
  if (verifyResult.verified) {
771
772
  verifySpinner.succeed('Cross-account role verified');
772
773
  this.log(chalk.green('✓') + ' Role verified successfully');
@@ -3,7 +3,8 @@ export default class JiraConnect extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'jira-domain': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
8
  };
8
9
  run(): Promise<void>;
9
10
  }
@@ -7,11 +7,16 @@ export default class JiraConnect extends Command {
7
7
  static description = 'Register your Jira instance with Hyperdrive (run BEFORE installing the Forge app)';
8
8
  static examples = [
9
9
  '<%= config.bin %> <%= command.id %>',
10
- '<%= config.bin %> <%= command.id %> --domain dev-squad.atlassian.net',
10
+ '<%= config.bin %> <%= command.id %> --jira-domain dev-squad.atlassian.net',
11
+ '<%= config.bin %> <%= command.id %> --domain sankhya.hyperdrivebot.dev --jira-domain dev-squad.atlassian.net',
11
12
  ];
12
13
  static flags = {
13
14
  domain: Flags.string({
14
15
  char: 'd',
16
+ description: 'Tenant domain (for multi-domain setups)',
17
+ }),
18
+ 'jira-domain': Flags.string({
19
+ char: 'j',
15
20
  description: 'Your Jira domain (e.g., dev-squad.atlassian.net)',
16
21
  }),
17
22
  };
@@ -27,7 +32,7 @@ export default class JiraConnect extends Command {
27
32
  let apiService;
28
33
  const spinner = ora('Checking authentication...').start();
29
34
  try {
30
- apiService = new HyperdriveSigV4Service();
35
+ apiService = new HyperdriveSigV4Service(flags.domain);
31
36
  spinner.succeed('Authenticated');
32
37
  }
33
38
  catch (error) {
@@ -36,7 +41,7 @@ export default class JiraConnect extends Command {
36
41
  `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
37
42
  }
38
43
  // Get Jira domain
39
- let jiraDomain = flags.domain;
44
+ let jiraDomain = flags['jira-domain'];
40
45
  if (!jiraDomain) {
41
46
  this.log('');
42
47
  const answers = await inquirer.prompt([
@@ -91,14 +96,20 @@ export default class JiraConnect extends Command {
91
96
  this.log(` Registration Token: ${chalk.cyan(response.registration.token)}`);
92
97
  this.log(` Status: ${chalk.yellow(response.registration.status)}`);
93
98
  this.log('');
99
+ // Show install link prominently if available
100
+ if (response.nextSteps.forgeInstallUrl) {
101
+ this.log(chalk.bold('Install the Forge App:'));
102
+ this.log('');
103
+ this.log(` ${chalk.cyan.underline(response.nextSteps.forgeInstallUrl)}`);
104
+ this.log('');
105
+ this.log(chalk.dim(' Open this link in your browser, select your Jira site, and approve the permissions.'));
106
+ this.log('');
107
+ }
94
108
  this.log(chalk.bold('Next Steps:'));
95
109
  response.nextSteps.instructions.forEach((instruction, index) => {
96
110
  this.log(` ${chalk.cyan(`${index + 1}.`)} ${instruction}`);
97
111
  });
98
112
  this.log('');
99
- this.log(chalk.bold('Marketplace URL:'));
100
- this.log(` ${chalk.cyan(response.nextSteps.marketplaceUrl)}`);
101
- this.log('');
102
113
  this.log(chalk.dim('💡 Tip: After installing the Forge app, run:'));
103
114
  this.log(chalk.dim(` ${chalk.cyan('hd jira status')} - to verify the connection`));
104
115
  this.log('');
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class HookAdd extends Command {
3
+ static args: {
4
+ project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ private handleApiError;
17
+ }
@@ -0,0 +1,147 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { promptActionConfig, promptActionType, promptTriggerStatus } from '../../../utils/hook-flow.js';
6
+ const VALID_ACTION_TYPES = ['adhb-enrich', 'ci-trigger', 'slack-notify', 'webhook'];
7
+ export default class HookAdd extends Command {
8
+ static args = {
9
+ project: Args.string({ description: 'Hyperdrive project ID or slug', required: true }),
10
+ };
11
+ static description = 'Add a status transition hook to a Jira-linked project';
12
+ static examples = [
13
+ '<%= config.bin %> jira hook add my-project',
14
+ '<%= config.bin %> jira hook add my-project --status "In Progress" --action adhb-enrich --config \'{"priority":"high"}\'',
15
+ '<%= config.bin %> jira hook add my-project --json',
16
+ ];
17
+ static flags = {
18
+ action: Flags.string({
19
+ description: 'Action type (slack-notify, adhb-enrich, webhook, ci-trigger)',
20
+ }),
21
+ config: Flags.string({
22
+ description: 'Action config as JSON string',
23
+ }),
24
+ domain: Flags.string({
25
+ char: 'd',
26
+ description: 'Hyperdrive tenant domain',
27
+ }),
28
+ json: Flags.boolean({
29
+ description: 'Output raw JSON',
30
+ }),
31
+ status: Flags.string({
32
+ description: 'Trigger status (Jira status name or "*" for all)',
33
+ }),
34
+ };
35
+ async run() {
36
+ const { args, flags } = await this.parse(HookAdd);
37
+ const isJson = flags.json;
38
+ // Authenticate
39
+ let apiService;
40
+ const spinner = isJson ? null : ora('Checking authentication...').start();
41
+ try {
42
+ apiService = new HyperdriveSigV4Service(flags.domain);
43
+ spinner?.succeed('Authenticated');
44
+ }
45
+ catch (error) {
46
+ spinner?.fail('Not authenticated');
47
+ this.error(`${error.message}\n\n` +
48
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
49
+ }
50
+ // Determine interactive vs non-interactive mode
51
+ const hasAllFlags = flags.status && flags.action && flags.config;
52
+ const hasPartialFlags = flags.status || flags.action || flags.config;
53
+ if (hasPartialFlags && !hasAllFlags) {
54
+ this.error('Non-interactive mode requires all three flags: --status, --action, and --config\n' +
55
+ `Missing: ${[!flags.status && '--status', !flags.action && '--action', !flags.config && '--config'].filter(Boolean).join(', ')}`);
56
+ }
57
+ let triggerStatus;
58
+ let actionType;
59
+ let actionConfig;
60
+ if (hasAllFlags) {
61
+ // Non-interactive mode
62
+ triggerStatus = flags.status;
63
+ if (!VALID_ACTION_TYPES.includes(flags.action)) {
64
+ this.error(`Invalid action type: ${flags.action}. Must be one of: ${VALID_ACTION_TYPES.join(', ')}`);
65
+ }
66
+ actionType = flags.action;
67
+ try {
68
+ actionConfig = JSON.parse(flags.config);
69
+ }
70
+ catch {
71
+ this.error('Invalid JSON in --config flag');
72
+ }
73
+ }
74
+ else {
75
+ // Interactive mode
76
+ if (isJson) {
77
+ this.error('Interactive mode not supported with --json. Provide --status, --action, and --config flags.');
78
+ }
79
+ // Fetch project statuses for the wizard
80
+ spinner?.start('Fetching project statuses...');
81
+ let statuses = [];
82
+ try {
83
+ const result = await apiService.projectGetJiraStatuses(args.project);
84
+ statuses = result.statuses.map(s => s.name);
85
+ spinner?.succeed(`Found ${statuses.length} statuses`);
86
+ }
87
+ catch {
88
+ spinner?.warn('Could not fetch statuses — you can still enter a status manually');
89
+ }
90
+ this.log('');
91
+ triggerStatus = await promptTriggerStatus(statuses);
92
+ actionType = await promptActionType();
93
+ actionConfig = await promptActionConfig(actionType);
94
+ }
95
+ // Create hook
96
+ const createSpinner = isJson ? null : ora('Creating hook...').start();
97
+ try {
98
+ const body = {
99
+ actionConfig,
100
+ actionType,
101
+ triggerStatus,
102
+ };
103
+ const hook = await apiService.hookCreate(args.project, body);
104
+ createSpinner?.succeed('Hook created');
105
+ if (isJson) {
106
+ this.log(JSON.stringify(hook, null, 2));
107
+ return;
108
+ }
109
+ this.log('');
110
+ this.log(chalk.green('✅ Hook created successfully'));
111
+ this.log('');
112
+ this.log(` Hook ID: ${chalk.cyan(hook.hookId)}`);
113
+ this.log(` Trigger Status: ${chalk.cyan(hook.triggerStatus)}`);
114
+ this.log(` Action Type: ${chalk.cyan(hook.actionType)}`);
115
+ this.log(` Enabled: ${hook.enabled ? chalk.green('yes') : chalk.red('no')}`);
116
+ this.log(` Created: ${chalk.dim(hook.createdAt)}`);
117
+ this.log('');
118
+ }
119
+ catch (error) {
120
+ createSpinner?.fail('Failed to create hook');
121
+ this.handleApiError(error);
122
+ }
123
+ }
124
+ handleApiError(error) {
125
+ let errorMessage = error.message;
126
+ if (error.response) {
127
+ const status = error.response.status;
128
+ const data = error.response.data;
129
+ if (status === 401) {
130
+ errorMessage = 'Authentication failed — please run "hd auth login"';
131
+ }
132
+ else if (status === 403) {
133
+ errorMessage = 'Access denied — check your permissions';
134
+ }
135
+ else if (status === 404) {
136
+ errorMessage = 'Project not found — verify the project ID';
137
+ }
138
+ else if (data?.error) {
139
+ errorMessage = data.error;
140
+ }
141
+ else if (data?.message) {
142
+ errorMessage = data.message;
143
+ }
144
+ }
145
+ this.error(errorMessage);
146
+ }
147
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class HookList extends Command {
3
+ static args: {
4
+ project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ private handleApiError;
14
+ }
@@ -0,0 +1,105 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../../utils/table.js';
6
+ export default class HookList extends Command {
7
+ static args = {
8
+ project: Args.string({ description: 'Hyperdrive project ID or slug', required: true }),
9
+ };
10
+ static description = 'List status transition hooks for a Jira-linked project';
11
+ static examples = [
12
+ '<%= config.bin %> jira hook list my-project',
13
+ '<%= config.bin %> jira hook list my-project --json',
14
+ ];
15
+ static flags = {
16
+ domain: Flags.string({
17
+ char: 'd',
18
+ description: 'Hyperdrive tenant domain',
19
+ }),
20
+ json: Flags.boolean({
21
+ description: 'Output raw JSON',
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(HookList);
26
+ const isJson = flags.json;
27
+ // Authenticate
28
+ let apiService;
29
+ const spinner = isJson ? null : ora('Checking authentication...').start();
30
+ try {
31
+ apiService = new HyperdriveSigV4Service(flags.domain);
32
+ spinner?.succeed('Authenticated');
33
+ }
34
+ catch (error) {
35
+ spinner?.fail('Not authenticated');
36
+ this.error(`${error.message}\n\n` +
37
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
38
+ }
39
+ // Fetch hooks
40
+ const fetchSpinner = isJson ? null : ora('Fetching hooks...').start();
41
+ try {
42
+ const response = await apiService.hookList(args.project);
43
+ const hooks = response.hooks;
44
+ fetchSpinner?.succeed(`Found ${hooks.length} hook${hooks.length === 1 ? '' : 's'}`);
45
+ if (isJson) {
46
+ this.log(JSON.stringify(hooks, null, 2));
47
+ return;
48
+ }
49
+ if (hooks.length === 0) {
50
+ this.log('');
51
+ this.log(chalk.yellow('No hooks configured for this project'));
52
+ this.log(chalk.dim(`Run ${chalk.cyan(`hd jira hook add ${args.project}`)} to create one`));
53
+ this.log('');
54
+ return;
55
+ }
56
+ this.log('');
57
+ printTable(hooks.map(h => ({
58
+ actionType: h.actionType,
59
+ createdAt: h.createdAt,
60
+ enabled: h.enabled,
61
+ hookId: h.hookId,
62
+ triggerStatus: h.triggerStatus,
63
+ })), {
64
+ triggerStatus: { header: 'Trigger Status' },
65
+ actionType: { header: 'Action Type' },
66
+ enabled: {
67
+ get: (row) => row.enabled ? chalk.green('enabled') : chalk.red('disabled'),
68
+ header: 'Enabled',
69
+ },
70
+ createdAt: {
71
+ get: (row) => new Date(row.createdAt).toLocaleDateString(),
72
+ header: 'Created',
73
+ },
74
+ }, (msg) => this.log(msg));
75
+ this.log('');
76
+ }
77
+ catch (error) {
78
+ fetchSpinner?.fail('Failed to fetch hooks');
79
+ this.handleApiError(error);
80
+ }
81
+ }
82
+ handleApiError(error) {
83
+ let errorMessage = error.message;
84
+ if (error.response) {
85
+ const status = error.response.status;
86
+ const data = error.response.data;
87
+ if (status === 401) {
88
+ errorMessage = 'Authentication failed — please run "hd auth login"';
89
+ }
90
+ else if (status === 403) {
91
+ errorMessage = 'Access denied — check your permissions';
92
+ }
93
+ else if (status === 404) {
94
+ errorMessage = 'Project not found — verify the project ID';
95
+ }
96
+ else if (data?.error) {
97
+ errorMessage = data.error;
98
+ }
99
+ else if (data?.message) {
100
+ errorMessage = data.message;
101
+ }
102
+ }
103
+ this.error(errorMessage);
104
+ }
105
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class HookRemove extends Command {
3
+ static args: {
4
+ project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'hook-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ private handleApiError;
15
+ }