@hyperdrive.bot/cli 1.0.13 → 1.0.16

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 (157) hide show
  1. package/README.md +1495 -474
  2. package/dist/commands/deploy.d.ts +18 -0
  3. package/dist/commands/deploy.js +239 -0
  4. package/dist/commands/deployment/create.js +10 -2
  5. package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
  6. package/dist/commands/domain/set-production.js +27 -0
  7. package/dist/commands/git/list-open-prs.d.ts +12 -0
  8. package/dist/commands/git/list-open-prs.js +87 -0
  9. package/dist/commands/hook/add.d.ts +22 -0
  10. package/dist/commands/hook/add.js +299 -0
  11. package/dist/commands/hook/list.d.ts +11 -0
  12. package/dist/commands/hook/list.js +111 -0
  13. package/dist/commands/hook/logs.d.ts +13 -0
  14. package/dist/commands/hook/logs.js +124 -0
  15. package/dist/commands/hook/remove.d.ts +12 -0
  16. package/dist/commands/hook/remove.js +115 -0
  17. package/dist/commands/hook/toggle.d.ts +12 -0
  18. package/dist/commands/hook/toggle.js +125 -0
  19. package/dist/commands/init.d.ts +1 -1
  20. package/dist/commands/init.js +49 -9
  21. package/dist/commands/module/bindings.d.ts +14 -0
  22. package/dist/commands/module/bindings.js +125 -0
  23. package/dist/commands/module/create.d.ts +3 -0
  24. package/dist/commands/module/create.js +156 -78
  25. package/dist/commands/module/list.d.ts +1 -0
  26. package/dist/commands/module/list.js +22 -1
  27. package/dist/commands/module/sync.d.ts +29 -0
  28. package/dist/commands/module/sync.js +409 -0
  29. package/dist/commands/module/unlink.d.ts +11 -0
  30. package/dist/commands/module/unlink.js +77 -0
  31. package/dist/commands/module/update.d.ts +10 -0
  32. package/dist/commands/module/update.js +168 -5
  33. package/dist/commands/network/discover.d.ts +12 -0
  34. package/dist/commands/network/discover.js +210 -0
  35. package/dist/commands/network/get.d.ts +13 -0
  36. package/dist/commands/network/get.js +90 -0
  37. package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
  38. package/dist/commands/network/list.js +71 -0
  39. package/dist/commands/network/register.d.ts +16 -0
  40. package/dist/commands/network/register.js +144 -0
  41. package/dist/commands/parameter/sync.d.ts +13 -0
  42. package/dist/commands/parameter/sync.js +69 -1
  43. package/dist/commands/project/sync.d.ts +5 -11
  44. package/dist/commands/project/sync.js +12 -381
  45. package/dist/commands/seed.d.ts +93 -0
  46. package/dist/commands/seed.js +324 -0
  47. package/dist/commands/service/backup.d.ts +17 -0
  48. package/dist/commands/service/backup.js +156 -0
  49. package/dist/commands/service/backups.d.ts +14 -0
  50. package/dist/commands/service/backups.js +110 -0
  51. package/dist/commands/service/bind.d.ts +16 -0
  52. package/dist/commands/service/bind.js +106 -0
  53. package/dist/commands/service/bindings.d.ts +13 -0
  54. package/dist/commands/service/bindings.js +78 -0
  55. package/dist/commands/service/clone.d.ts +19 -0
  56. package/dist/commands/service/clone.js +153 -0
  57. package/dist/commands/service/create.d.ts +16 -0
  58. package/dist/commands/service/create.js +212 -0
  59. package/dist/commands/service/get.d.ts +13 -0
  60. package/dist/commands/service/get.js +97 -0
  61. package/dist/commands/service/list.d.ts +12 -0
  62. package/dist/commands/service/list.js +86 -0
  63. package/dist/commands/service/register.d.ts +21 -0
  64. package/dist/commands/service/register.js +215 -0
  65. package/dist/commands/service/restore.d.ts +19 -0
  66. package/dist/commands/service/restore.js +158 -0
  67. package/dist/commands/service/seed.d.ts +17 -0
  68. package/dist/commands/service/seed.js +173 -0
  69. package/dist/commands/service/templates.d.ts +10 -0
  70. package/dist/commands/service/templates.js +66 -0
  71. package/dist/commands/service/unbind.d.ts +15 -0
  72. package/dist/commands/service/unbind.js +74 -0
  73. package/dist/commands/stage/create.d.ts +23 -0
  74. package/dist/commands/stage/create.js +145 -6
  75. package/dist/commands/stage/delete.d.ts +11 -0
  76. package/dist/commands/stage/delete.js +85 -0
  77. package/dist/commands/stage/deploy.d.ts +34 -0
  78. package/dist/commands/stage/deploy.js +294 -0
  79. package/dist/commands/stage/ensure-branches.d.ts +23 -0
  80. package/dist/commands/stage/ensure-branches.js +101 -0
  81. package/dist/commands/stage/list.js +4 -0
  82. package/dist/commands/stage/status.d.ts +14 -0
  83. package/dist/commands/stage/status.js +100 -0
  84. package/dist/commands/{jira → tracker}/connect.js +32 -23
  85. package/dist/commands/tracker/hook/add.d.ts +25 -0
  86. package/dist/commands/tracker/hook/add.js +284 -0
  87. package/dist/commands/{jira → tracker}/hook/list.js +20 -11
  88. package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
  89. package/dist/commands/tracker/hook/logs.js +126 -0
  90. package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
  91. package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
  92. package/dist/commands/tracker/project/init.d.ts +17 -0
  93. package/dist/commands/tracker/project/init.js +178 -0
  94. package/dist/commands/tracker/project/link-module.d.ts +17 -0
  95. package/dist/commands/tracker/project/link-module.js +287 -0
  96. package/dist/commands/tracker/project/list-modules.d.ts +11 -0
  97. package/dist/commands/tracker/project/list-modules.js +117 -0
  98. package/dist/commands/tracker/project/list.d.ts +10 -0
  99. package/dist/commands/tracker/project/list.js +90 -0
  100. package/dist/commands/tracker/project/status.d.ts +13 -0
  101. package/dist/commands/tracker/project/status.js +168 -0
  102. package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
  103. package/dist/commands/tracker/project/unlink-module.js +251 -0
  104. package/dist/commands/{jira → tracker}/status.js +3 -3
  105. package/dist/lib/ensure-branches.d.ts +53 -0
  106. package/dist/lib/ensure-branches.js +149 -0
  107. package/dist/lib/git-providers/github.d.ts +16 -0
  108. package/dist/lib/git-providers/github.js +157 -0
  109. package/dist/lib/git-providers/gitlab.d.ts +16 -0
  110. package/dist/lib/git-providers/gitlab.js +148 -0
  111. package/dist/lib/git-providers/index.d.ts +67 -0
  112. package/dist/lib/git-providers/index.js +39 -0
  113. package/dist/lib/lambda-warmer.d.ts +106 -0
  114. package/dist/lib/lambda-warmer.js +189 -0
  115. package/dist/services/hyperdrive-sigv4.d.ts +359 -5
  116. package/dist/services/hyperdrive-sigv4.js +177 -12
  117. package/dist/utils/hook-flow.d.ts +60 -3
  118. package/dist/utils/hook-flow.js +437 -2
  119. package/dist/utils/hook-normalize.d.ts +6 -0
  120. package/dist/utils/hook-normalize.js +33 -0
  121. package/dist/utils/lifecycle-poller.d.ts +32 -0
  122. package/dist/utils/lifecycle-poller.js +72 -0
  123. package/dist/utils/retry.d.ts +43 -0
  124. package/dist/utils/retry.js +88 -0
  125. package/dist/utils/summary-display.js +1 -1
  126. package/dist/utils/tracker-project-flow.d.ts +84 -0
  127. package/dist/utils/tracker-project-flow.js +564 -0
  128. package/package.json +35 -7
  129. package/dist/commands/auth/login.d.ts +0 -16
  130. package/dist/commands/auth/login.js +0 -179
  131. package/dist/commands/auth/logout.js +0 -116
  132. package/dist/commands/auth/refresh.d.ts +0 -6
  133. package/dist/commands/auth/refresh.js +0 -66
  134. package/dist/commands/auth/status.d.ts +0 -6
  135. package/dist/commands/auth/status.js +0 -63
  136. package/dist/commands/config/get.d.ts +0 -9
  137. package/dist/commands/config/get.js +0 -37
  138. package/dist/commands/config/set.d.ts +0 -10
  139. package/dist/commands/config/set.js +0 -48
  140. package/dist/commands/config/show.d.ts +0 -6
  141. package/dist/commands/config/show.js +0 -10
  142. package/dist/commands/domain/current.d.ts +0 -6
  143. package/dist/commands/domain/current.js +0 -18
  144. package/dist/commands/domain/list.d.ts +0 -6
  145. package/dist/commands/domain/list.js +0 -42
  146. package/dist/commands/domain/switch.js +0 -40
  147. package/dist/commands/jira/hook/add.js +0 -147
  148. package/dist/services/tenant-service.d.ts +0 -127
  149. package/dist/services/tenant-service.js +0 -396
  150. package/dist/utils/auth-flow.d.ts +0 -147
  151. package/dist/utils/auth-flow.js +0 -479
  152. package/oclif.manifest.json +0 -3519
  153. /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
  154. /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
  155. /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
  156. /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
  157. /package/dist/commands/{jira → tracker}/status.d.ts +0 -0
@@ -4,7 +4,7 @@ import inquirer from 'inquirer';
4
4
  import ora from 'ora';
5
5
  import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
6
6
  export default class JiraConnect extends Command {
7
- static description = 'Register your Jira instance with Hyperdrive (run BEFORE installing the Forge app)';
7
+ static description = 'Connect your tenant to a Jira instance via the Hyperdrive Forge app';
8
8
  static examples = [
9
9
  '<%= config.bin %> <%= command.id %>',
10
10
  '<%= config.bin %> <%= command.id %> --jira-domain dev-squad.atlassian.net',
@@ -25,8 +25,8 @@ export default class JiraConnect extends Command {
25
25
  this.log('');
26
26
  this.log(chalk.blue.bold('🔌 Hyperdrive Jira Integration Setup'));
27
27
  this.log('');
28
- this.log(chalk.dim('This wizard will pre-register your Jira instance with Hyperdrive.'));
29
- this.log(chalk.dim('After registration, you\'ll install the Forge app from the Atlassian Marketplace.'));
28
+ this.log(chalk.dim('This will request a connection between your tenant and a Jira site.'));
29
+ this.log(chalk.dim('A Jira admin must approve the connection from the Forge app settings.'));
30
30
  this.log('');
31
31
  // Create API service (automatically checks auth)
32
32
  let apiService;
@@ -82,38 +82,47 @@ export default class JiraConnect extends Command {
82
82
  // Pre-register domain
83
83
  spinner.start('Registering Jira domain with Hyperdrive...');
84
84
  try {
85
- const response = await apiService['makeSignedRequest']('POST', '/hyperdrive/jira/pre-register', {
86
- jiraDomain,
87
- });
88
- spinner.succeed('Jira domain registered successfully');
89
- // Display registration details
85
+ const response = await apiService.jiraPreRegister({ jiraDomain: jiraDomain });
86
+ // Handle already connected case
87
+ if (response.alreadyConnected) {
88
+ spinner.succeed('Jira instance already connected');
89
+ this.log('');
90
+ this.log(chalk.green('✅ Already Connected!'));
91
+ this.log('');
92
+ this.log(` Jira Domain: ${chalk.cyan(response.connection?.jiraDomain)}`);
93
+ this.log(` Status: ${chalk.green('active')}`);
94
+ this.log(` Connected since: ${chalk.dim(response.connection?.installedAt ? new Date(response.connection.installedAt).toLocaleString() : 'unknown')}`);
95
+ this.log('');
96
+ this.log(chalk.dim('Your Jira integration is already active. No action needed.'));
97
+ this.log(chalk.dim(`Run ${chalk.cyan('hd tracker status')} to see full connection details.`));
98
+ this.log('');
99
+ return;
100
+ }
101
+ spinner.succeed('Connection request submitted');
90
102
  this.log('');
91
- this.log(chalk.green(' Pre-registration Complete!'));
103
+ this.log(chalk.green('Connection Request Submitted!'));
92
104
  this.log('');
93
- this.log(chalk.bold('Registration Details:'));
94
- this.log(` Jira Domain: ${chalk.cyan(response.registration.jiraDomain)}`);
95
- this.log(` Tenant ID: ${chalk.dim(response.registration.tenantId)}`);
96
- this.log(` Registration Token: ${chalk.cyan(response.registration.token)}`);
97
- this.log(` Status: ${chalk.yellow(response.registration.status)}`);
105
+ this.log(chalk.bold('Request Details:'));
106
+ this.log(` Jira Domain: ${chalk.cyan(response.registration?.jiraDomain)}`);
107
+ this.log(` Tenant ID: ${chalk.dim(response.registration?.tenantId)}`);
108
+ this.log(` Status: ${chalk.yellow('pending approval')}`);
98
109
  this.log('');
99
- // Show install link prominently if available
110
+ // Show install link if app not yet installed on the Jira site
100
111
  if (response.nextSteps.forgeInstallUrl) {
101
- this.log(chalk.bold('Install the Forge App:'));
112
+ this.log(chalk.bold('Step 1 - Install the Forge App (if not already installed):'));
102
113
  this.log('');
103
114
  this.log(` ${chalk.cyan.underline(response.nextSteps.forgeInstallUrl)}`);
104
115
  this.log('');
105
116
  this.log(chalk.dim(' Open this link in your browser, select your Jira site, and approve the permissions.'));
106
117
  this.log('');
107
118
  }
108
- this.log(chalk.bold('Next Steps:'));
109
- response.nextSteps.instructions.forEach((instruction, index) => {
110
- this.log(` ${chalk.cyan(`${index + 1}.`)} ${instruction}`);
111
- });
119
+ this.log(chalk.bold(`${response.nextSteps.forgeInstallUrl ? 'Step 2' : 'Next Step'} - Jira Admin Approval:`));
112
120
  this.log('');
113
- this.log(chalk.dim('💡 Tip: After installing the Forge app, run:'));
114
- this.log(chalk.dim(` ${chalk.cyan('hd jira status')} - to verify the connection`));
121
+ this.log(' A Jira administrator must approve your connection request.');
122
+ this.log(` They can do this from: ${chalk.cyan('Jira Settings > Apps > Hyperdrive > Configure')}`);
123
+ this.log(` Your tenant will appear under ${chalk.bold('Pending Connection Requests')}.`);
115
124
  this.log('');
116
- this.log(chalk.yellow('⚠️ Important: You must install the Forge app for the integration to work!'));
125
+ this.log(chalk.dim(`Run ${chalk.cyan('hd tracker status')} to check the connection status.`));
117
126
  this.log('');
118
127
  }
119
128
  catch (error) {
@@ -0,0 +1,25 @@
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-config': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'action-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'trigger-conditions': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'trigger-event': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ order: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ };
20
+ run(): Promise<void>;
21
+ private handleApiError;
22
+ private runLegacyFlow;
23
+ private runV2Interactive;
24
+ private runV2NonInteractive;
25
+ }
@@ -0,0 +1,284 @@
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 { ALL_ACTION_TYPES, VALID_TRIGGER_EVENTS, getActionCategory, promptActionConfig, promptActionTypeV2, promptOrder, promptTriggerConditions, promptTriggerEvent, } from '../../../utils/hook-flow.js';
6
+ const LEGACY_ACTION_TYPES = ['adhb-enrich', 'ci-trigger', 'slack-notify', 'webhook'];
7
+ export default class HookAdd extends Command {
8
+ static args = {
9
+ project: Args.string({ description: 'Tracker project ID', required: true }),
10
+ };
11
+ static description = 'Add an automation hook to a tracker project';
12
+ static examples = [
13
+ '<%= config.bin %> tracker hook add my-project',
14
+ '<%= config.bin %> tracker hook add my-project --trigger-event status_transition --trigger-conditions \'{"statusTo":"In Progress"}\' --action-type git-create-branch --action-config \'{"branchPattern":"{issueKey}/{summary-slug}"}\'',
15
+ '<%= config.bin %> tracker hook add my-project --trigger-event issue_created --action-type slack-notify --action-config \'{"channel":"#tickets"}\'',
16
+ '<%= config.bin %> tracker hook add my-project --trigger-event status_transition --action-type git-create-mr --action-config \'{"draft":true}\' --order 2',
17
+ '<%= config.bin %> tracker hook add my-project --status "In Progress" --action adhb-enrich --config \'{"priority":"high"}\'',
18
+ '<%= config.bin %> tracker hook add my-project --json',
19
+ ];
20
+ static flags = {
21
+ // V2 flags
22
+ 'action-config': Flags.string({
23
+ description: '[V2] Action config as JSON string',
24
+ }),
25
+ 'action-type': Flags.string({
26
+ description: '[V2] Action type (git-create-branch, git-create-mr, git-delete-branch, slack-notify, adhb-enrich, webhook, ci-trigger)',
27
+ }),
28
+ 'trigger-conditions': Flags.string({
29
+ description: '[V2] Trigger conditions as JSON string',
30
+ }),
31
+ 'trigger-event': Flags.string({
32
+ description: '[V2] Trigger event (status_transition, issue_created, issue_assigned, field_changed, comment_added, label_changed)',
33
+ }),
34
+ // Legacy flags
35
+ action: Flags.string({
36
+ description: '[Legacy] Action type (slack-notify, adhb-enrich, webhook, ci-trigger)',
37
+ }),
38
+ config: Flags.string({
39
+ description: '[Legacy] Action config as JSON string',
40
+ }),
41
+ domain: Flags.string({
42
+ char: 'd',
43
+ description: 'Hyperdrive tenant domain',
44
+ }),
45
+ json: Flags.boolean({
46
+ description: 'Output raw JSON',
47
+ }),
48
+ order: Flags.integer({
49
+ description: 'Execution order (lower runs first)',
50
+ }),
51
+ status: Flags.string({
52
+ description: '[Legacy] Trigger status (Jira status name or "*" for all)',
53
+ }),
54
+ };
55
+ async run() {
56
+ const { args, flags } = await this.parse(HookAdd);
57
+ const isJson = flags.json;
58
+ // Authenticate
59
+ let apiService;
60
+ const spinner = isJson ? null : ora('Checking authentication...').start();
61
+ try {
62
+ apiService = new HyperdriveSigV4Service(flags.domain);
63
+ spinner?.succeed('Authenticated');
64
+ }
65
+ catch (error) {
66
+ spinner?.fail('Not authenticated');
67
+ this.error(`${error.message}\n\n` +
68
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
69
+ }
70
+ // Detect which flag set is in use
71
+ const hasLegacyFlags = flags.status || flags.action || flags.config;
72
+ const hasV2Flags = flags['trigger-event'] || flags['action-type'] || flags['trigger-conditions'] || flags['action-config'];
73
+ // Error if both flag sets are mixed
74
+ if (hasLegacyFlags && hasV2Flags) {
75
+ this.error('Cannot mix legacy flags (--status, --action, --config) with V2 flags (--trigger-event, --action-type, --trigger-conditions, --action-config).\n' +
76
+ 'Use one set or the other.');
77
+ }
78
+ if (hasLegacyFlags) {
79
+ await this.runLegacyFlow(apiService, args.project, flags, isJson);
80
+ }
81
+ else if (hasV2Flags) {
82
+ await this.runV2NonInteractive(apiService, args.project, flags, isJson);
83
+ }
84
+ else {
85
+ // No flags — interactive V2 flow
86
+ if (isJson) {
87
+ this.error('Interactive mode not supported with --json. Provide --trigger-event and --action-type flags.');
88
+ }
89
+ await this.runV2Interactive(apiService, args.project, isJson);
90
+ }
91
+ }
92
+ handleApiError(error) {
93
+ let errorMessage = error.message;
94
+ if (error.response) {
95
+ const status = error.response.status;
96
+ const data = error.response.data;
97
+ if (status === 401) {
98
+ errorMessage = 'Authentication failed — please run "hd auth login"';
99
+ }
100
+ else if (status === 403) {
101
+ errorMessage = 'Access denied — check your permissions';
102
+ }
103
+ else if (status === 404) {
104
+ errorMessage = 'Project not found — verify the project ID';
105
+ }
106
+ else if (data?.error) {
107
+ errorMessage = data.error;
108
+ }
109
+ else if (data?.message) {
110
+ errorMessage = data.message;
111
+ }
112
+ }
113
+ this.error(errorMessage);
114
+ }
115
+ async runLegacyFlow(apiService, projectId, flags, isJson) {
116
+ const hasAllFlags = flags.status && flags.action && flags.config;
117
+ if (!hasAllFlags) {
118
+ this.error('Non-interactive legacy mode requires all three flags: --status, --action, and --config\n' +
119
+ `Missing: ${[!flags.status && '--status', !flags.action && '--action', !flags.config && '--config'].filter(Boolean).join(', ')}`);
120
+ }
121
+ const triggerStatus = flags.status;
122
+ if (!LEGACY_ACTION_TYPES.includes(flags.action)) {
123
+ this.error(`Invalid action type: ${flags.action}. Must be one of: ${LEGACY_ACTION_TYPES.join(', ')}`);
124
+ }
125
+ const actionType = flags.action;
126
+ let actionConfig;
127
+ try {
128
+ actionConfig = JSON.parse(flags.config);
129
+ }
130
+ catch {
131
+ this.error('Invalid JSON in --config flag');
132
+ }
133
+ const createSpinner = isJson ? null : ora('Creating hook...').start();
134
+ try {
135
+ const body = { actionConfig, actionType, triggerStatus };
136
+ const hook = await apiService.trackerProjectHookCreate(projectId, body);
137
+ createSpinner?.succeed('Hook created');
138
+ if (isJson) {
139
+ this.log(JSON.stringify(hook, null, 2));
140
+ return;
141
+ }
142
+ this.log('');
143
+ this.log(chalk.green('Hook created successfully'));
144
+ this.log('');
145
+ this.log(` Hook ID: ${chalk.cyan(hook.hookId)}`);
146
+ this.log(` Trigger Status: ${chalk.cyan(hook.triggerStatus)}`);
147
+ this.log(` Action Type: ${chalk.cyan(hook.actionType)}`);
148
+ this.log(` Enabled: ${hook.enabled ? chalk.green('yes') : chalk.red('no')}`);
149
+ this.log(` Created: ${chalk.dim(hook.createdAt)}`);
150
+ this.log('');
151
+ }
152
+ catch (error) {
153
+ createSpinner?.fail('Failed to create hook');
154
+ this.handleApiError(error);
155
+ }
156
+ }
157
+ async runV2Interactive(apiService, projectId, isJson) {
158
+ this.log('');
159
+ // Step 1: Trigger event
160
+ const triggerEvent = await promptTriggerEvent();
161
+ // Step 2: Trigger conditions
162
+ const conditions = await promptTriggerConditions(triggerEvent);
163
+ // Step 3: Action type (grouped)
164
+ const { category, type: actionType } = await promptActionTypeV2();
165
+ // Step 4: Action config
166
+ const actionConfig = await promptActionConfig(actionType);
167
+ // Step 5: Order
168
+ const order = await promptOrder();
169
+ // Create hook
170
+ const createSpinner = ora('Creating hook...').start();
171
+ try {
172
+ const body = {
173
+ action: { category, config: actionConfig, type: actionType },
174
+ trigger: {
175
+ conditions: Object.keys(conditions).length > 0 ? conditions : undefined,
176
+ event: triggerEvent,
177
+ source: 'jira',
178
+ },
179
+ };
180
+ if (order !== undefined)
181
+ body.order = order;
182
+ const hook = await apiService.trackerProjectHookCreateV2(projectId, body);
183
+ createSpinner.succeed('Hook created');
184
+ this.log('');
185
+ this.log(chalk.green('Hook created successfully'));
186
+ this.log('');
187
+ this.log(` Hook ID: ${chalk.cyan(hook.hookId)}`);
188
+ this.log(` Trigger Event: ${chalk.cyan(triggerEvent)}`);
189
+ if (Object.keys(conditions).length > 0) {
190
+ this.log(` Conditions: ${chalk.cyan(JSON.stringify(conditions))}`);
191
+ }
192
+ this.log(` Action Type: ${chalk.cyan(actionType)}`);
193
+ this.log(` Category: ${chalk.cyan(category)}`);
194
+ if (order !== undefined) {
195
+ this.log(` Order: ${chalk.cyan(String(order))}`);
196
+ }
197
+ this.log(` Enabled: ${hook.enabled ? chalk.green('yes') : chalk.red('no')}`);
198
+ this.log(` Created: ${chalk.dim(hook.createdAt)}`);
199
+ this.log('');
200
+ }
201
+ catch (error) {
202
+ createSpinner.fail('Failed to create hook');
203
+ this.handleApiError(error);
204
+ }
205
+ }
206
+ async runV2NonInteractive(apiService, projectId, flags, isJson) {
207
+ // Require at minimum --trigger-event and --action-type
208
+ if (!flags['trigger-event'] || !flags['action-type']) {
209
+ this.error('V2 non-interactive mode requires at least --trigger-event and --action-type.\n' +
210
+ `Missing: ${[!flags['trigger-event'] && '--trigger-event', !flags['action-type'] && '--action-type'].filter(Boolean).join(', ')}`);
211
+ }
212
+ // Validate trigger event
213
+ const triggerEvent = flags['trigger-event'];
214
+ if (!VALID_TRIGGER_EVENTS.includes(triggerEvent)) {
215
+ this.error(`Invalid trigger event: ${triggerEvent}. Must be one of: ${VALID_TRIGGER_EVENTS.join(', ')}`);
216
+ }
217
+ // Validate action type
218
+ const actionType = flags['action-type'];
219
+ if (!ALL_ACTION_TYPES.includes(actionType)) {
220
+ this.error(`Invalid action type: ${actionType}. Must be one of: ${ALL_ACTION_TYPES.join(', ')}`);
221
+ }
222
+ // Parse optional conditions
223
+ let conditions;
224
+ if (flags['trigger-conditions']) {
225
+ try {
226
+ conditions = JSON.parse(flags['trigger-conditions']);
227
+ }
228
+ catch {
229
+ this.error('Invalid JSON in --trigger-conditions flag');
230
+ }
231
+ }
232
+ // Parse optional action config
233
+ let actionConfig = {};
234
+ if (flags['action-config']) {
235
+ try {
236
+ actionConfig = JSON.parse(flags['action-config']);
237
+ }
238
+ catch {
239
+ this.error('Invalid JSON in --action-config flag');
240
+ }
241
+ }
242
+ const category = getActionCategory(actionType);
243
+ const order = flags.order;
244
+ const createSpinner = isJson ? null : ora('Creating hook...').start();
245
+ try {
246
+ const body = {
247
+ action: { category, config: actionConfig, type: actionType },
248
+ trigger: {
249
+ conditions,
250
+ event: triggerEvent,
251
+ source: 'jira',
252
+ },
253
+ };
254
+ if (order !== undefined)
255
+ body.order = order;
256
+ const hook = await apiService.trackerProjectHookCreateV2(projectId, body);
257
+ createSpinner?.succeed('Hook created');
258
+ if (isJson) {
259
+ this.log(JSON.stringify(hook, null, 2));
260
+ return;
261
+ }
262
+ this.log('');
263
+ this.log(chalk.green('Hook created successfully'));
264
+ this.log('');
265
+ this.log(` Hook ID: ${chalk.cyan(hook.hookId)}`);
266
+ this.log(` Trigger Event: ${chalk.cyan(triggerEvent)}`);
267
+ if (conditions && Object.keys(conditions).length > 0) {
268
+ this.log(` Conditions: ${chalk.cyan(JSON.stringify(conditions))}`);
269
+ }
270
+ this.log(` Action Type: ${chalk.cyan(actionType)}`);
271
+ this.log(` Category: ${chalk.cyan(category)}`);
272
+ if (order !== undefined) {
273
+ this.log(` Order: ${chalk.cyan(String(order))}`);
274
+ }
275
+ this.log(` Enabled: ${hook.enabled ? chalk.green('yes') : chalk.red('no')}`);
276
+ this.log(` Created: ${chalk.dim(hook.createdAt)}`);
277
+ this.log('');
278
+ }
279
+ catch (error) {
280
+ createSpinner?.fail('Failed to create hook');
281
+ this.handleApiError(error);
282
+ }
283
+ }
284
+ }
@@ -2,15 +2,21 @@ import { Args, Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { normalizeHookToV2 } from '../../../utils/hook-normalize.js';
5
6
  import { printTable } from '../../../utils/table.js';
7
+ function formatConditions(conditions) {
8
+ if (!conditions || Object.keys(conditions).length === 0)
9
+ return '*';
10
+ return Object.entries(conditions).map(([k, v]) => `${k}: ${v}`).join(', ');
11
+ }
6
12
  export default class HookList extends Command {
7
13
  static args = {
8
- project: Args.string({ description: 'Hyperdrive project ID or slug', required: true }),
14
+ project: Args.string({ description: 'Tracker project ID', required: true }),
9
15
  };
10
- static description = 'List status transition hooks for a Jira-linked project';
16
+ static description = 'List automation hooks for a tracker project';
11
17
  static examples = [
12
- '<%= config.bin %> jira hook list my-project',
13
- '<%= config.bin %> jira hook list my-project --json',
18
+ '<%= config.bin %> tracker hook list my-project',
19
+ '<%= config.bin %> tracker hook list my-project --json',
14
20
  ];
15
21
  static flags = {
16
22
  domain: Flags.string({
@@ -39,8 +45,9 @@ export default class HookList extends Command {
39
45
  // Fetch hooks
40
46
  const fetchSpinner = isJson ? null : ora('Fetching hooks...').start();
41
47
  try {
42
- const response = await apiService.hookList(args.project);
43
- const hooks = response.hooks;
48
+ const response = await apiService.trackerProjectHookList(args.project);
49
+ const rawHooks = Array.isArray(response) ? response : response.hooks;
50
+ const hooks = rawHooks.map(normalizeHookToV2);
44
51
  fetchSpinner?.succeed(`Found ${hooks.length} hook${hooks.length === 1 ? '' : 's'}`);
45
52
  if (isJson) {
46
53
  this.log(JSON.stringify(hooks, null, 2));
@@ -49,19 +56,21 @@ export default class HookList extends Command {
49
56
  if (hooks.length === 0) {
50
57
  this.log('');
51
58
  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`));
59
+ this.log(chalk.dim(`Run ${chalk.cyan(`hd tracker hook add ${args.project}`)} to create one`));
53
60
  this.log('');
54
61
  return;
55
62
  }
56
63
  this.log('');
57
- printTable(hooks.map(h => ({
58
- actionType: h.actionType,
64
+ printTable(hooks.map((h) => ({
65
+ actionType: h.action?.type ?? '',
66
+ conditions: formatConditions(h.trigger?.conditions),
59
67
  createdAt: h.createdAt,
60
68
  enabled: h.enabled,
61
69
  hookId: h.hookId,
62
- triggerStatus: h.triggerStatus,
70
+ triggerEvent: h.trigger?.event ?? '',
63
71
  })), {
64
- triggerStatus: { header: 'Trigger Status' },
72
+ triggerEvent: { header: 'Trigger Event' },
73
+ conditions: { header: 'Conditions' },
65
74
  actionType: { header: 'Action Type' },
66
75
  enabled: {
67
76
  get: (row) => row.enabled ? chalk.green('enabled') : chalk.red('disabled'),
@@ -1,15 +1,14 @@
1
1
  import { Command } from '@oclif/core';
2
- export default class HookAdd extends Command {
2
+ export default class HookLogs extends Command {
3
3
  static args: {
4
4
  project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
5
  };
6
6
  static description: string;
7
7
  static examples: string[];
8
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
9
  domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
10
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
12
  status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
13
  };
15
14
  run(): Promise<void>;
@@ -0,0 +1,126 @@
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 HookLogs extends Command {
7
+ static args = {
8
+ project: Args.string({ description: 'Tracker project ID', required: true }),
9
+ };
10
+ static description = 'List recent hook execution logs for a tracker project';
11
+ static examples = [
12
+ '<%= config.bin %> tracker hook logs my-project',
13
+ '<%= config.bin %> tracker hook logs my-project --status failed',
14
+ '<%= config.bin %> tracker hook logs my-project --limit 50',
15
+ '<%= config.bin %> tracker hook logs my-project --json',
16
+ ];
17
+ static flags = {
18
+ domain: Flags.string({
19
+ char: 'd',
20
+ description: 'Hyperdrive tenant domain',
21
+ }),
22
+ json: Flags.boolean({
23
+ description: 'Output raw JSON',
24
+ }),
25
+ limit: Flags.integer({
26
+ default: 20,
27
+ description: 'Number of log entries to display',
28
+ }),
29
+ status: Flags.string({
30
+ description: 'Filter by execution status',
31
+ options: ['success', 'failed'],
32
+ }),
33
+ };
34
+ async run() {
35
+ const { args, flags } = await this.parse(HookLogs);
36
+ const isJson = flags.json;
37
+ // Authenticate
38
+ let apiService;
39
+ const spinner = isJson ? null : ora('Checking authentication...').start();
40
+ try {
41
+ apiService = new HyperdriveSigV4Service(flags.domain);
42
+ spinner?.succeed('Authenticated');
43
+ }
44
+ catch (error) {
45
+ spinner?.fail('Not authenticated');
46
+ this.error(`${error.message}\n\n` +
47
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
48
+ }
49
+ // Fetch hook logs
50
+ const fetchSpinner = isJson ? null : ora('Fetching hook logs...').start();
51
+ try {
52
+ const response = await apiService.trackerProjectHookLogList(args.project, {
53
+ limit: flags.limit,
54
+ status: flags.status,
55
+ });
56
+ const items = response.items;
57
+ fetchSpinner?.succeed(`Found ${items.length} log entr${items.length === 1 ? 'y' : 'ies'}`);
58
+ if (isJson) {
59
+ this.log(JSON.stringify(items, null, 2));
60
+ return;
61
+ }
62
+ if (items.length === 0) {
63
+ this.log('');
64
+ this.log(chalk.yellow('No hook executions found for this project. Hook logs appear after hooks are triggered.'));
65
+ this.log('');
66
+ return;
67
+ }
68
+ this.log('');
69
+ printTable(items.map(item => ({
70
+ actionType: item.actionType,
71
+ durationMs: item.durationMs,
72
+ hookId: item.hookId,
73
+ issueKey: item.issueKey,
74
+ status: item.status,
75
+ timestamp: item.timestamp,
76
+ })), {
77
+ timestamp: {
78
+ get: (row) => new Date(row.timestamp).toLocaleString(),
79
+ header: 'Timestamp',
80
+ },
81
+ hookId: {
82
+ get: (row) => row.hookId.slice(0, 8),
83
+ header: 'Hook ID',
84
+ },
85
+ actionType: { header: 'Action' },
86
+ issueKey: { header: 'Issue Key' },
87
+ status: {
88
+ get: (row) => row.status === 'success' ? chalk.green('success') : chalk.red('failed'),
89
+ header: 'Status',
90
+ },
91
+ durationMs: {
92
+ get: (row) => `${row.durationMs}ms`,
93
+ header: 'Duration',
94
+ },
95
+ }, (msg) => this.log(msg));
96
+ this.log('');
97
+ }
98
+ catch (error) {
99
+ fetchSpinner?.fail('Failed to fetch hook logs');
100
+ this.handleApiError(error);
101
+ }
102
+ }
103
+ handleApiError(error) {
104
+ let errorMessage = error.message;
105
+ if (error.response) {
106
+ const status = error.response.status;
107
+ const data = error.response.data;
108
+ if (status === 401) {
109
+ errorMessage = 'Authentication failed — please run "hd auth login"';
110
+ }
111
+ else if (status === 403) {
112
+ errorMessage = 'Access denied — check your permissions';
113
+ }
114
+ else if (status === 404) {
115
+ errorMessage = 'Project not found — verify the project ID';
116
+ }
117
+ else if (data?.error) {
118
+ errorMessage = data.error;
119
+ }
120
+ else if (data?.message) {
121
+ errorMessage = data.message;
122
+ }
123
+ }
124
+ this.error(errorMessage);
125
+ }
126
+ }
@@ -3,15 +3,16 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
5
  import { promptConfirmDelete, promptSelectHook } from '../../../utils/hook-flow.js';
6
+ import { normalizeHookToV2 } from '../../../utils/hook-normalize.js';
6
7
  export default class HookRemove extends Command {
7
8
  static args = {
8
- project: Args.string({ description: 'Hyperdrive project ID or slug', required: true }),
9
+ project: Args.string({ description: 'Tracker project ID', required: true }),
9
10
  };
10
- static description = 'Remove a status transition hook from a Jira-linked project';
11
+ static description = 'Remove an automation hook from a tracker project';
11
12
  static examples = [
12
- '<%= config.bin %> jira hook remove my-project',
13
- '<%= config.bin %> jira hook remove my-project --hook-id hook-123',
14
- '<%= config.bin %> jira hook remove my-project --hook-id hook-123 --json',
13
+ '<%= config.bin %> tracker hook remove my-project',
14
+ '<%= config.bin %> tracker hook remove my-project --hook-id hook-123',
15
+ '<%= config.bin %> tracker hook remove my-project --hook-id hook-123 --json',
15
16
  ];
16
17
  static flags = {
17
18
  domain: Flags.string({
@@ -52,8 +53,8 @@ export default class HookRemove extends Command {
52
53
  // Fetch hooks for interactive selection
53
54
  const fetchSpinner = ora('Fetching hooks...').start();
54
55
  try {
55
- const response = await apiService.hookList(args.project);
56
- const hooks = response.hooks;
56
+ const response = await apiService.trackerProjectHookList(args.project);
57
+ const hooks = (Array.isArray(response) ? response : response.hooks).map(normalizeHookToV2);
57
58
  fetchSpinner.succeed(`Found ${hooks.length} hook${hooks.length === 1 ? '' : 's'}`);
58
59
  if (hooks.length === 0) {
59
60
  this.log('');
@@ -78,7 +79,7 @@ export default class HookRemove extends Command {
78
79
  // Delete hook
79
80
  const deleteSpinner = isJson ? null : ora('Deleting hook...').start();
80
81
  try {
81
- const result = await apiService.hookDelete(args.project, hookId);
82
+ const result = await apiService.trackerProjectHookDelete(args.project, hookId);
82
83
  deleteSpinner?.succeed('Hook deleted');
83
84
  if (isJson) {
84
85
  this.log(JSON.stringify({ hookId, ...result }, null, 2));