@hyperdrive.bot/cli 1.0.13 → 1.0.17

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 +4526 -780
  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 +41 -13
  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
@@ -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 { promptSelectHook } from '../../../utils/hook-flow.js';
6
+ import { normalizeHookToV2 } from '../../../utils/hook-normalize.js';
6
7
  export default class HookToggle 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 = 'Toggle the enabled state of a status transition hook';
11
+ static description = 'Toggle the enabled state of an automation hook';
11
12
  static examples = [
12
- '<%= config.bin %> jira hook toggle my-project',
13
- '<%= config.bin %> jira hook toggle my-project --hook-id hook-123',
14
- '<%= config.bin %> jira hook toggle my-project --hook-id hook-123 --json',
13
+ '<%= config.bin %> tracker hook toggle my-project',
14
+ '<%= config.bin %> tracker hook toggle my-project --hook-id hook-123',
15
+ '<%= config.bin %> tracker hook toggle my-project --hook-id hook-123 --json',
15
16
  ];
16
17
  static flags = {
17
18
  domain: Flags.string({
@@ -53,8 +54,8 @@ export default class HookToggle extends Command {
53
54
  // Fetch hooks for interactive selection
54
55
  const fetchSpinner = ora('Fetching hooks...').start();
55
56
  try {
56
- const response = await apiService.hookList(args.project);
57
- const hooks = response.hooks;
57
+ const response = await apiService.trackerProjectHookList(args.project);
58
+ const hooks = (Array.isArray(response) ? response : response.hooks).map(normalizeHookToV2);
58
59
  fetchSpinner.succeed(`Found ${hooks.length} hook${hooks.length === 1 ? '' : 's'}`);
59
60
  if (hooks.length === 0) {
60
61
  this.log('');
@@ -80,8 +81,9 @@ export default class HookToggle extends Command {
80
81
  // If we don't know the current state (non-interactive with --hook-id), fetch it first
81
82
  let enabled;
82
83
  if (newEnabled === undefined) {
83
- const response = await apiService.hookList(args.project);
84
- const hook = response.hooks.find(h => h.hookId === hookId);
84
+ const response = await apiService.trackerProjectHookList(args.project);
85
+ const toggleHooks = (Array.isArray(response) ? response : response.hooks).map(normalizeHookToV2);
86
+ const hook = toggleHooks.find(h => h.hookId === hookId);
85
87
  if (!hook) {
86
88
  toggleSpinner?.fail('Hook not found');
87
89
  this.error(`Hook ${hookId} not found in project ${args.project}`);
@@ -91,7 +93,7 @@ export default class HookToggle extends Command {
91
93
  else {
92
94
  enabled = newEnabled;
93
95
  }
94
- const updatedHook = await apiService.hookUpdate(args.project, hookId, { enabled });
96
+ const updatedHook = await apiService.trackerProjectHookUpdate(args.project, hookId, { enabled });
95
97
  toggleSpinner?.succeed('Hook toggled');
96
98
  if (isJson) {
97
99
  this.log(JSON.stringify(updatedHook, null, 2));
@@ -100,8 +102,8 @@ export default class HookToggle extends Command {
100
102
  this.log('');
101
103
  this.log(chalk.green('✅ Hook updated'));
102
104
  this.log(` Hook ID: ${chalk.cyan(updatedHook.hookId)}`);
103
- this.log(` Trigger Status: ${chalk.cyan(updatedHook.triggerStatus)}`);
104
- this.log(` Action Type: ${chalk.cyan(updatedHook.actionType)}`);
105
+ this.log(` Trigger Event: ${chalk.cyan(updatedHook.trigger?.event ?? '')}`);
106
+ this.log(` Action Type: ${chalk.cyan(updatedHook.action?.type ?? '')}`);
105
107
  this.log(` Enabled: ${updatedHook.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
106
108
  this.log('');
107
109
  }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectInit extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ modules: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'primary-module': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'project-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'project-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'tracker-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,178 @@
1
+ import { 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 { displaySummary, executeTrackerProjectInit, parseConfigFile, stepConfirm, stepLinkModules, stepMapStatuses, stepSelectExternalProject, stepSelectTracker, } from '../../../utils/tracker-project-flow.js';
6
+ export default class TrackerProjectInit extends Command {
7
+ static description = 'Initialize a tracker project with module linking, status mapping, and hooks';
8
+ static examples = [
9
+ '<%= config.bin %> tracker project init',
10
+ '<%= config.bin %> tracker project init --domain my-tenant.hyperdrivebot.dev',
11
+ '<%= config.bin %> tracker project init --json',
12
+ '<%= config.bin %> tracker project init --tracker-id abc123 --project-key PROJ --project-name "My Project" --modules mod1,mod2 --primary-module mod1 --config ./tracker-init.json --yes',
13
+ ];
14
+ static flags = {
15
+ config: Flags.string({
16
+ description: 'Path to JSON config file with statusMapping and hooks',
17
+ }),
18
+ domain: Flags.string({
19
+ char: 'd',
20
+ description: 'Tenant domain (for multi-domain setups)',
21
+ }),
22
+ json: Flags.boolean({
23
+ description: 'Output result as JSON',
24
+ }),
25
+ modules: Flags.string({
26
+ description: 'Comma-separated module IDs to link',
27
+ }),
28
+ 'primary-module': Flags.string({
29
+ description: 'Primary module ID (from --modules list)',
30
+ }),
31
+ 'project-key': Flags.string({
32
+ description: 'External project key (e.g., PROJ)',
33
+ }),
34
+ 'project-name': Flags.string({
35
+ description: 'External project name',
36
+ }),
37
+ 'tracker-id': Flags.string({
38
+ description: 'Tracker connection ID (skips interactive selection)',
39
+ }),
40
+ yes: Flags.boolean({
41
+ char: 'y',
42
+ description: 'Skip confirmation prompt',
43
+ }),
44
+ };
45
+ async run() {
46
+ const { flags } = await this.parse(TrackerProjectInit);
47
+ const isJson = flags.json;
48
+ const skipConfirm = flags.yes;
49
+ if (!isJson) {
50
+ this.log('');
51
+ this.log(chalk.blue.bold('🔗 Tracker Project Init Wizard'));
52
+ this.log('');
53
+ this.log(chalk.dim('This wizard will create a tracker project, link modules,'));
54
+ this.log(chalk.dim('map statuses, and optionally configure hooks.'));
55
+ this.log('');
56
+ }
57
+ // Auth check
58
+ let service;
59
+ const spinner = isJson ? null : ora('Checking authentication...').start();
60
+ try {
61
+ service = new HyperdriveSigV4Service(flags.domain);
62
+ spinner?.succeed('Authenticated');
63
+ }
64
+ catch (error) {
65
+ spinner?.fail('Not authenticated');
66
+ this.error(`${error.message}\n\n` +
67
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
68
+ }
69
+ // Parse config file if provided
70
+ let configFileData;
71
+ if (flags.config) {
72
+ try {
73
+ configFileData = parseConfigFile(flags.config);
74
+ }
75
+ catch (error) {
76
+ this.error(error.message);
77
+ }
78
+ }
79
+ try {
80
+ // Step (a): Select tracker
81
+ if (!isJson && !flags['tracker-id']) {
82
+ this.log('');
83
+ this.log(chalk.blue('Step 1: Select Tracker'));
84
+ this.log(chalk.dim('Choose a connected tracker instance'));
85
+ this.log('');
86
+ }
87
+ const tracker = await stepSelectTracker(service, flags['tracker-id']);
88
+ // Step (b): Select external project
89
+ const projectOverride = flags['project-key']
90
+ ? { projectKey: flags['project-key'], projectName: flags['project-name'] || flags['project-key'] }
91
+ : undefined;
92
+ if (!isJson && !projectOverride) {
93
+ this.log('');
94
+ this.log(chalk.blue('Step 2: Select External Project'));
95
+ this.log(chalk.dim('Choose the project from your tracker'));
96
+ this.log('');
97
+ }
98
+ const externalProject = await stepSelectExternalProject(service, tracker.trackerId, projectOverride);
99
+ // Step (c): Link modules
100
+ const modulesOverride = flags.modules
101
+ ? { moduleIds: flags.modules.split(',').map(s => s.trim()), primaryModuleId: flags['primary-module'] }
102
+ : undefined;
103
+ if (!isJson && !modulesOverride) {
104
+ this.log('');
105
+ this.log(chalk.blue('Step 3: Link Modules'));
106
+ this.log(chalk.dim('Select Hyperdrive modules to link'));
107
+ this.log('');
108
+ }
109
+ const linkedModules = await stepLinkModules(service, modulesOverride);
110
+ // Step (d): Map statuses + configure hooks
111
+ if (!isJson && !configFileData) {
112
+ this.log('');
113
+ this.log(chalk.blue('Step 4: Map Statuses & Configure Hooks'));
114
+ this.log(chalk.dim('Map provider statuses to normalized workflow states'));
115
+ this.log('');
116
+ }
117
+ const { hooks, statusMapping } = await stepMapStatuses(service, tracker.trackerId, externalProject.externalProjectKey, configFileData);
118
+ const config = {
119
+ externalProject,
120
+ hooks,
121
+ linkedModules,
122
+ statusMapping,
123
+ tracker,
124
+ };
125
+ // Step (e): Confirm and save
126
+ if (!isJson && !skipConfirm) {
127
+ displaySummary(config, this.log.bind(this));
128
+ const confirmed = await stepConfirm();
129
+ if (!confirmed) {
130
+ this.log(chalk.yellow('Setup cancelled. No changes were made.'));
131
+ return;
132
+ }
133
+ }
134
+ // Execute
135
+ const result = await executeTrackerProjectInit(service, config, { nonInteractive: skipConfirm });
136
+ if (isJson) {
137
+ this.log(JSON.stringify({
138
+ hooks: result.hooks,
139
+ moduleLinks: result.moduleLinks,
140
+ trackerProject: result.trackerProject,
141
+ }, null, 2));
142
+ return;
143
+ }
144
+ this.log('');
145
+ this.log(chalk.green('Tracker project initialized successfully!'));
146
+ this.log('');
147
+ this.log(` Project ID: ${chalk.cyan(result.trackerProject.trackerProjectId)}`);
148
+ this.log(` Provider: ${chalk.cyan(result.trackerProject.provider)}`);
149
+ this.log(` External Project: ${chalk.cyan(result.trackerProject.externalProjectKey)}`);
150
+ this.log(` Modules Linked: ${chalk.cyan(String(result.moduleLinks.length))}`);
151
+ this.log(` Hooks Created: ${chalk.cyan(String(result.hooks.length))}`);
152
+ this.log('');
153
+ this.log(chalk.dim('Next steps:'));
154
+ this.log(` ${chalk.cyan('hd tracker project list')} — View tracker projects`);
155
+ this.log('');
156
+ }
157
+ catch (error) {
158
+ let errorMessage = error.message;
159
+ if (error.response) {
160
+ const status = error.response.status;
161
+ const data = error.response.data;
162
+ if (status === 401) {
163
+ errorMessage = 'Authentication failed — please run "hd auth login"';
164
+ }
165
+ else if (status === 403) {
166
+ errorMessage = 'Access denied — check your permissions';
167
+ }
168
+ else if (data?.error) {
169
+ errorMessage = data.error;
170
+ }
171
+ else if (data?.message) {
172
+ errorMessage = data.message;
173
+ }
174
+ }
175
+ this.error(errorMessage);
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectLinkModule extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ modules: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'primary-module': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'tracker-project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ private executeLinking;
15
+ private promptModuleSelection;
16
+ private resolveNonInteractiveModules;
17
+ }
@@ -0,0 +1,287 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
6
+ import { printTable } from '../../../utils/table.js';
7
+ import { selectTrackerProject } from '../../../utils/tracker-project-flow.js';
8
+ export default class TrackerProjectLinkModule extends Command {
9
+ static description = 'Link one or more modules to an existing tracker project';
10
+ static examples = [
11
+ '<%= config.bin %> tracker project link-module',
12
+ '<%= config.bin %> tracker project link-module --tracker-project-id abc123',
13
+ '<%= config.bin %> tracker project link-module --tracker-project-id abc123 --modules mod1,mod2 --primary-module mod1 --yes',
14
+ '<%= config.bin %> tracker project link-module --json',
15
+ ];
16
+ static flags = {
17
+ domain: Flags.string({
18
+ char: 'd',
19
+ description: 'Tenant domain (for multi-domain setups)',
20
+ }),
21
+ json: Flags.boolean({
22
+ default: false,
23
+ description: 'Output raw JSON response',
24
+ }),
25
+ modules: Flags.string({
26
+ description: 'Comma-separated module IDs to link (non-interactive)',
27
+ }),
28
+ 'primary-module': Flags.string({
29
+ description: 'Module ID to designate as primary',
30
+ }),
31
+ 'tracker-project-id': Flags.string({
32
+ description: 'Tracker project ID (skips interactive selection)',
33
+ }),
34
+ yes: Flags.boolean({
35
+ char: 'y',
36
+ default: false,
37
+ description: 'Skip confirmation prompt',
38
+ }),
39
+ };
40
+ async run() {
41
+ const { flags } = await this.parse(TrackerProjectLinkModule);
42
+ const isJson = flags.json;
43
+ const isNonInteractive = Boolean(flags.modules);
44
+ // Auth
45
+ let service;
46
+ const authSpinner = isJson ? null : ora('Checking authentication...').start();
47
+ try {
48
+ service = new HyperdriveSigV4Service(flags.domain);
49
+ authSpinner?.succeed('Authenticated');
50
+ }
51
+ catch (error) {
52
+ authSpinner?.fail('Not authenticated');
53
+ this.error(`${error.message}\n\n` +
54
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
55
+ }
56
+ try {
57
+ // Step 1: Select tracker project
58
+ const project = await selectTrackerProject(service, flags['tracker-project-id']);
59
+ if (!isJson) {
60
+ this.log(`\nTracker project: ${chalk.cyan(project.externalProjectKey)} — ${project.externalProjectName}\n`);
61
+ }
62
+ // Step 2: Fetch linked modules and all available modules
63
+ const fetchSpinner = isJson ? null : ora('Fetching modules...').start();
64
+ const [linkedModules, allModules] = await Promise.all([
65
+ service.trackerProjectListModules(project.trackerProjectId),
66
+ service.moduleList(),
67
+ ]);
68
+ fetchSpinner?.succeed(`Found ${allModules.length} module(s), ${linkedModules.length} already linked`);
69
+ // Build set of already-linked module IDs
70
+ const linkedIds = new Set(linkedModules.map(m => m.moduleId));
71
+ // Filter available (unlinked) modules
72
+ const availableModules = allModules.filter(m => !linkedIds.has(m.projectId || ''));
73
+ if (availableModules.length === 0) {
74
+ if (isJson) {
75
+ this.log(JSON.stringify({ message: 'All modules are already linked to this tracker project.', results: [] }, null, 2));
76
+ }
77
+ else {
78
+ this.log(chalk.yellow('\nAll modules are already linked to this tracker project.'));
79
+ this.log(chalk.gray(`Use ${chalk.cyan('hd tracker project list-modules')} to see linked modules.`));
80
+ }
81
+ return;
82
+ }
83
+ // Step 3: Select modules to link
84
+ let modulesToLink;
85
+ if (isNonInteractive) {
86
+ modulesToLink = this.resolveNonInteractiveModules(flags, allModules, linkedIds);
87
+ }
88
+ else {
89
+ modulesToLink = await this.promptModuleSelection(availableModules);
90
+ }
91
+ if (modulesToLink.length === 0) {
92
+ this.log(chalk.yellow('\nNo modules to link.'));
93
+ return;
94
+ }
95
+ // Step 4: Display hooks info banner (not in JSON mode)
96
+ if (!isJson) {
97
+ this.log('');
98
+ this.log(chalk.blue('ℹ Hooks configured on this tracker project apply to ALL linked modules.'));
99
+ this.log(chalk.blue(' Linking a new module means existing hooks (git-create-branch, webhooks, etc.)'));
100
+ this.log(chalk.blue(' will fire for that module\'s issues too.'));
101
+ this.log('');
102
+ // Display summary
103
+ this.log(chalk.bold('Modules to link:'));
104
+ for (const mod of modulesToLink) {
105
+ const roleTag = mod.role === 'primary' ? chalk.green(' [primary]') : chalk.dim(' [supporting]');
106
+ this.log(` - ${mod.moduleName}${roleTag}`);
107
+ }
108
+ this.log('');
109
+ }
110
+ // Step 5: Confirmation
111
+ if (!flags.yes && !isNonInteractive && !isJson) {
112
+ const { confirmed } = await inquirer.prompt([{
113
+ default: true,
114
+ message: 'Link these modules?',
115
+ name: 'confirmed',
116
+ type: 'confirm',
117
+ }]);
118
+ if (!confirmed) {
119
+ this.log(chalk.yellow('Cancelled. No modules were linked.'));
120
+ return;
121
+ }
122
+ }
123
+ // Step 6: Execute linking
124
+ const results = await this.executeLinking(service, project.trackerProjectId, modulesToLink, isJson);
125
+ // Step 7: Output results
126
+ if (isJson) {
127
+ this.log(JSON.stringify(results, null, 2));
128
+ return;
129
+ }
130
+ const linked = results.filter(r => r.status === 'linked');
131
+ const skipped = results.filter(r => r.status === 'skipped');
132
+ if (linked.length > 0) {
133
+ this.log('');
134
+ printTable(linked, {
135
+ moduleName: {
136
+ header: 'Module',
137
+ minWidth: 20,
138
+ },
139
+ role: {
140
+ get: (row) => {
141
+ const r = row;
142
+ return r.role === 'primary' ? chalk.green(r.role) : chalk.dim(r.role);
143
+ },
144
+ header: 'Role',
145
+ minWidth: 14,
146
+ },
147
+ status: {
148
+ get: () => chalk.green('linked'),
149
+ header: 'Status',
150
+ },
151
+ }, (msg) => this.log(msg));
152
+ }
153
+ this.log('');
154
+ this.log(chalk.green(`${linked.length} module(s) linked successfully.`));
155
+ if (skipped.length > 0) {
156
+ this.log(chalk.yellow(`${skipped.length} module(s) skipped (already linked or not found).`));
157
+ }
158
+ this.log('');
159
+ }
160
+ catch (error) {
161
+ let errorMessage = error.message;
162
+ if (error.response) {
163
+ const data = error.response.data;
164
+ if (error.response.status === 401) {
165
+ errorMessage = 'Authentication failed — please run "hd auth login"';
166
+ }
167
+ else if (data?.error) {
168
+ errorMessage = data.error;
169
+ }
170
+ else if (data?.message) {
171
+ errorMessage = data.message;
172
+ }
173
+ }
174
+ this.error(errorMessage);
175
+ }
176
+ }
177
+ async executeLinking(service, trackerProjectId, modules, isJson) {
178
+ const results = [];
179
+ for (const mod of modules) {
180
+ const spinner = isJson ? null : ora(`Linking ${chalk.cyan(mod.moduleName)}...`).start();
181
+ try {
182
+ await service.trackerProjectLinkModule(trackerProjectId, {
183
+ moduleId: mod.moduleId,
184
+ role: mod.role,
185
+ });
186
+ spinner?.succeed(`Module ${chalk.cyan(mod.moduleName)} linked`);
187
+ results.push({
188
+ moduleId: mod.moduleId,
189
+ moduleName: mod.moduleName,
190
+ role: mod.role,
191
+ status: 'linked',
192
+ });
193
+ }
194
+ catch (error) {
195
+ const status = error.response?.status;
196
+ if (status === 409) {
197
+ spinner?.warn(`Module ${chalk.cyan(mod.moduleName)} is already linked — skipping`);
198
+ this.warn(`Module "${mod.moduleName}" is already linked — skipping`);
199
+ results.push({
200
+ moduleId: mod.moduleId,
201
+ moduleName: mod.moduleName,
202
+ role: mod.role,
203
+ status: 'skipped',
204
+ warning: 'Already linked',
205
+ });
206
+ }
207
+ else {
208
+ const msg = error.response?.data?.message || error.response?.data?.error || error.message;
209
+ spinner?.warn(`Failed to link ${chalk.cyan(mod.moduleName)}: ${msg}`);
210
+ this.warn(`Failed to link "${mod.moduleName}": ${msg}`);
211
+ results.push({
212
+ moduleId: mod.moduleId,
213
+ moduleName: mod.moduleName,
214
+ role: mod.role,
215
+ status: 'skipped',
216
+ warning: msg,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ return results;
222
+ }
223
+ async promptModuleSelection(availableModules) {
224
+ const { selectedModules } = await inquirer.prompt([{
225
+ choices: availableModules.map(m => ({
226
+ name: `${m.name || m.slug} (${chalk.dim(m.slug)})`,
227
+ value: { moduleId: m.projectId || m.slug || '', moduleName: m.name || m.slug || '' },
228
+ })),
229
+ message: 'Select modules to link (space to select, enter to confirm):',
230
+ name: 'selectedModules',
231
+ type: 'checkbox',
232
+ validate: (input) => input.length > 0 ? true : 'Select at least one module',
233
+ }]);
234
+ const selected = selectedModules;
235
+ // Ask which is primary if more than one selected
236
+ let primaryId = null;
237
+ if (selected.length > 1) {
238
+ const { primary } = await inquirer.prompt([{
239
+ choices: [
240
+ ...selected.map(m => ({ name: m.moduleName, value: m.moduleId })),
241
+ { name: chalk.dim('None — all supporting'), value: '__none__' },
242
+ ],
243
+ message: 'Which module is the primary module?',
244
+ name: 'primary',
245
+ type: 'list',
246
+ }]);
247
+ if (primary !== '__none__')
248
+ primaryId = primary;
249
+ }
250
+ else {
251
+ primaryId = selected[0].moduleId;
252
+ }
253
+ return selected.map(m => ({
254
+ moduleId: m.moduleId,
255
+ moduleName: m.moduleName,
256
+ role: m.moduleId === primaryId ? 'primary' : 'supporting',
257
+ }));
258
+ }
259
+ resolveNonInteractiveModules(flags, allModules, linkedIds) {
260
+ const moduleIds = flags.modules.split(',').map(s => s.trim()).filter(Boolean);
261
+ const primaryModuleId = flags['primary-module'];
262
+ const result = [];
263
+ for (const id of moduleIds) {
264
+ const mod = allModules.find(m => m.projectId === id || m.slug === id);
265
+ if (!mod) {
266
+ this.warn(`Module "${id}" not found — skipping`);
267
+ continue;
268
+ }
269
+ const moduleId = mod.projectId || mod.slug || '';
270
+ if (linkedIds.has(moduleId)) {
271
+ this.warn(`Module "${mod.name || mod.slug}" is already linked — skipping`);
272
+ continue;
273
+ }
274
+ const isPrimary = primaryModuleId && (moduleId === primaryModuleId || mod.slug === primaryModuleId);
275
+ result.push({
276
+ moduleId,
277
+ moduleName: mod.name || mod.slug || '',
278
+ role: isPrimary ? 'primary' : 'supporting',
279
+ });
280
+ }
281
+ // If only one module and no explicit primary, make it primary
282
+ if (result.length === 1 && !primaryModuleId) {
283
+ result[0].role = 'primary';
284
+ }
285
+ return result;
286
+ }
287
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectListModules extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'tracker-project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }