@hyperdrive.bot/cli 1.0.7 → 1.0.9

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 (41) hide show
  1. package/README.md +307 -59
  2. package/dist/commands/account/list.d.ts +3 -0
  3. package/dist/commands/account/list.js +9 -2
  4. package/dist/commands/auth/logout.d.ts +11 -0
  5. package/dist/commands/auth/logout.js +86 -9
  6. package/dist/commands/git/connect.js +1 -0
  7. package/dist/commands/init.d.ts +1 -0
  8. package/dist/commands/init.js +20 -19
  9. package/dist/commands/jira/connect.d.ts +1 -0
  10. package/dist/commands/jira/connect.js +17 -6
  11. package/dist/commands/jira/hook/add.d.ts +17 -0
  12. package/dist/commands/jira/hook/add.js +147 -0
  13. package/dist/commands/jira/hook/list.d.ts +14 -0
  14. package/dist/commands/jira/hook/list.js +105 -0
  15. package/dist/commands/jira/hook/remove.d.ts +15 -0
  16. package/dist/commands/jira/hook/remove.js +119 -0
  17. package/dist/commands/jira/hook/toggle.d.ts +15 -0
  18. package/dist/commands/jira/hook/toggle.js +136 -0
  19. package/dist/commands/jira/status.js +11 -2
  20. package/dist/commands/project/init.d.ts +21 -0
  21. package/dist/commands/project/init.js +576 -0
  22. package/dist/commands/project/list.d.ts +10 -0
  23. package/dist/commands/project/list.js +119 -0
  24. package/dist/commands/project/status.d.ts +13 -0
  25. package/dist/commands/project/status.js +163 -0
  26. package/dist/commands/project/sync.d.ts +26 -0
  27. package/dist/commands/project/sync.js +406 -0
  28. package/dist/services/hyperdrive-sigv4.d.ts +125 -0
  29. package/dist/services/hyperdrive-sigv4.js +45 -0
  30. package/dist/services/tenant-service.d.ts +12 -0
  31. package/dist/services/tenant-service.js +44 -1
  32. package/dist/utils/account-flow.d.ts +2 -2
  33. package/dist/utils/account-flow.js +4 -4
  34. package/dist/utils/git-flow.d.ts +1 -0
  35. package/dist/utils/git-flow.js +2 -2
  36. package/dist/utils/hook-flow.d.ts +21 -0
  37. package/dist/utils/hook-flow.js +154 -0
  38. package/dist/utils/jira-flow.d.ts +2 -2
  39. package/dist/utils/jira-flow.js +4 -4
  40. package/oclif.manifest.json +591 -119
  41. package/package.json +5 -1
@@ -2,5 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class AccountList extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
5
8
  run(): Promise<void>;
6
9
  }
@@ -1,4 +1,4 @@
1
- import { Command } from '@oclif/core';
1
+ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
4
4
  export default class AccountList extends Command {
@@ -6,9 +6,16 @@ export default class AccountList extends Command {
6
6
  static examples = [
7
7
  '<%= config.bin %> <%= command.id %>',
8
8
  ];
9
+ static flags = {
10
+ domain: Flags.string({
11
+ char: 'd',
12
+ description: 'Tenant domain (for multi-domain setups)',
13
+ }),
14
+ };
9
15
  async run() {
16
+ const { flags } = await this.parse(AccountList);
10
17
  this.log(chalk.green('📋 Fetching AWS accounts...'));
11
- const service = new HyperdriveSigV4Service();
18
+ const service = new HyperdriveSigV4Service(flags.domain);
12
19
  try {
13
20
  const accounts = await service.accountList();
14
21
  if (accounts.length === 0) {
@@ -2,5 +2,16 @@ import { Command } from '@oclif/core';
2
2
  export default class Logout extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
5
8
  run(): Promise<void>;
9
+ /**
10
+ * Remove credentials for a specific domain
11
+ */
12
+ private logoutDomain;
13
+ /**
14
+ * Remove default domain credentials and all stored config
15
+ */
16
+ private logoutAll;
6
17
  }
@@ -1,23 +1,48 @@
1
- import { Command } from '@oclif/core';
1
+ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
4
  import { AuthService } from '../../services/auth-service.js';
5
+ import { TenantService } from '../../services/tenant-service.js';
5
6
  export default class Logout extends Command {
6
- static description = 'Remove stored credentials and logout';
7
- static examples = ['<%= config.bin %> <%= command.id %>'];
7
+ static description = 'Remove stored credentials and logout. Use --domain to logout from a single domain. Without flags, removes all credentials and stored configuration.';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --domain acme.hyperdrive.bot',
11
+ ];
12
+ static flags = {
13
+ domain: Flags.string({
14
+ char: 'd',
15
+ description: 'Logout from a specific domain only (keeps other domains and config intact)',
16
+ }),
17
+ };
8
18
  async run() {
9
- const authService = new AuthService();
10
- // Check if credentials exist
19
+ const { flags } = await this.parse(Logout);
20
+ const tenantService = new TenantService();
21
+ if (flags.domain) {
22
+ await this.logoutDomain(flags.domain, tenantService);
23
+ }
24
+ else {
25
+ await this.logoutAll(tenantService);
26
+ }
27
+ }
28
+ /**
29
+ * Remove credentials for a specific domain
30
+ */
31
+ async logoutDomain(domain, tenantService) {
32
+ const authService = new AuthService(domain);
11
33
  const credentials = authService.loadCredentials();
12
34
  if (!credentials) {
13
- this.log(chalk.yellow('⚠️ No active session found'));
35
+ this.log(chalk.yellow(`⚠️ No active session found for domain: ${domain}`));
36
+ const knownDomains = authService.getCredentialDomains();
37
+ if (knownDomains.length > 0) {
38
+ this.log(chalk.gray(` Logged-in domains: ${knownDomains.join(', ')}`));
39
+ }
14
40
  return;
15
41
  }
16
- // Confirm logout
17
42
  const { confirm } = await inquirer.prompt([
18
43
  {
19
44
  default: false,
20
- message: 'Are you sure you want to logout?',
45
+ message: `Logout from ${chalk.cyan(domain)}?`,
21
46
  name: 'confirm',
22
47
  type: 'confirm',
23
48
  },
@@ -28,8 +53,60 @@ export default class Logout extends Command {
28
53
  }
29
54
  try {
30
55
  authService.clearCredentials();
56
+ tenantService.removeDomainConfig(domain);
57
+ // If this was the default domain, clear the default too
58
+ if (tenantService.getDefaultDomain() === domain) {
59
+ tenantService.clearDefaultDomain();
60
+ }
61
+ this.log('');
62
+ this.log(chalk.green(`✅ Successfully logged out from ${domain}`));
63
+ this.log(chalk.gray('Run `hd auth login` to authenticate again.'));
64
+ }
65
+ catch (error) {
66
+ this.error(chalk.red(`Logout failed: ${error instanceof Error ? error.message : String(error)}`));
67
+ }
68
+ }
69
+ /**
70
+ * Remove default domain credentials and all stored config
71
+ */
72
+ async logoutAll(tenantService) {
73
+ const authService = new AuthService();
74
+ const allDomains = authService.getCredentialDomains();
75
+ const defaultDomain = tenantService.getDefaultDomain();
76
+ if (allDomains.length === 0) {
77
+ this.log(chalk.yellow('⚠️ No active sessions found'));
78
+ return;
79
+ }
80
+ this.log(chalk.blue('This will remove:'));
81
+ for (const domain of allDomains) {
82
+ const isDefault = domain === defaultDomain ? chalk.gray(' (default)') : '';
83
+ this.log(chalk.white(` • Credentials for ${chalk.cyan(domain)}${isDefault}`));
84
+ }
85
+ this.log(chalk.white(' • Default domain setting'));
86
+ this.log(chalk.white(` • CLI configuration ${chalk.gray('(~/.hyperdrive/config.json)')}`));
87
+ this.log('');
88
+ const { confirm } = await inquirer.prompt([
89
+ {
90
+ default: false,
91
+ message: `Logout from ${allDomains.length === 1 ? allDomains[0] : `all ${allDomains.length} domains`} and remove stored settings?`,
92
+ name: 'confirm',
93
+ type: 'confirm',
94
+ },
95
+ ]);
96
+ if (!confirm) {
97
+ this.log(chalk.gray('Logout cancelled'));
98
+ return;
99
+ }
100
+ try {
101
+ // Clear all credential files
102
+ for (const domain of allDomains) {
103
+ const domainAuth = new AuthService(domain);
104
+ domainAuth.clearCredentials();
105
+ }
106
+ // Clear all config
107
+ tenantService.clearAllConfig();
31
108
  this.log('');
32
- this.log(chalk.green('✅ Successfully logged out!'));
109
+ this.log(chalk.green('✅ Successfully logged out from all domains!'));
33
110
  this.log(chalk.gray('Run `hd auth login` to authenticate again.'));
34
111
  }
35
112
  catch (error) {
@@ -26,6 +26,7 @@ export default class GitConnect extends Command {
26
26
  }
27
27
  try {
28
28
  const result = await executeGitConnect({
29
+ domain: flags.domain,
29
30
  logger: (message) => this.log(chalk.gray(message)),
30
31
  provider,
31
32
  });
@@ -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');
@@ -4,6 +4,7 @@ export default class JiraConnect extends Command {
4
4
  static examples: string[];
5
5
  static flags: {
6
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
+ }