@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.
- package/README.md +307 -59
- package/dist/commands/account/list.d.ts +3 -0
- package/dist/commands/account/list.js +9 -2
- package/dist/commands/auth/logout.d.ts +11 -0
- package/dist/commands/auth/logout.js +86 -9
- package/dist/commands/git/connect.js +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +20 -19
- package/dist/commands/jira/connect.d.ts +1 -0
- package/dist/commands/jira/connect.js +17 -6
- package/dist/commands/jira/hook/add.d.ts +17 -0
- package/dist/commands/jira/hook/add.js +147 -0
- package/dist/commands/jira/hook/list.d.ts +14 -0
- package/dist/commands/jira/hook/list.js +105 -0
- package/dist/commands/jira/hook/remove.d.ts +15 -0
- package/dist/commands/jira/hook/remove.js +119 -0
- package/dist/commands/jira/hook/toggle.d.ts +15 -0
- package/dist/commands/jira/hook/toggle.js +136 -0
- package/dist/commands/jira/status.js +11 -2
- package/dist/commands/project/init.d.ts +21 -0
- package/dist/commands/project/init.js +576 -0
- package/dist/commands/project/list.d.ts +10 -0
- package/dist/commands/project/list.js +119 -0
- package/dist/commands/project/status.d.ts +13 -0
- package/dist/commands/project/status.js +163 -0
- package/dist/commands/project/sync.d.ts +26 -0
- package/dist/commands/project/sync.js +406 -0
- package/dist/services/hyperdrive-sigv4.d.ts +125 -0
- package/dist/services/hyperdrive-sigv4.js +45 -0
- package/dist/services/tenant-service.d.ts +12 -0
- package/dist/services/tenant-service.js +44 -1
- package/dist/utils/account-flow.d.ts +2 -2
- package/dist/utils/account-flow.js +4 -4
- package/dist/utils/git-flow.d.ts +1 -0
- package/dist/utils/git-flow.js +2 -2
- package/dist/utils/hook-flow.d.ts +21 -0
- package/dist/utils/hook-flow.js +154 -0
- package/dist/utils/jira-flow.d.ts +2 -2
- package/dist/utils/jira-flow.js +4 -4
- package/oclif.manifest.json +591 -119
- 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 = [
|
|
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
|
|
10
|
-
|
|
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(
|
|
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:
|
|
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) {
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|