@hyperdrive.bot/cli 1.0.12 → 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 +360 -5
  116. package/dist/services/hyperdrive-sigv4.js +192 -24
  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
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Tracker Project Init Wizard Flow
3
+ *
4
+ * Multi-step wizard functions for creating a tracker project,
5
+ * linking modules, mapping statuses, and configuring hooks.
6
+ */
7
+ import chalk from 'chalk';
8
+ import { readFileSync } from 'node:fs';
9
+ import inquirer from 'inquirer';
10
+ import ora from 'ora';
11
+ import { ALL_ACTION_TYPES, getActionCategory, promptActionConfig, promptActionTypeV2, } from './hook-flow.js';
12
+ const NORMALIZED_WORKFLOW_STATES = [
13
+ 'pending', 'enriching', 'implementing', 'testing', 'in_review',
14
+ 'ready_to_merge', 'deploying', 'validating', 'done', 'blocked',
15
+ ];
16
+ // ============================================================================
17
+ // Config file parsing + validation
18
+ // ============================================================================
19
+ export function parseConfigFile(filePath) {
20
+ let raw;
21
+ try {
22
+ raw = readFileSync(filePath, 'utf-8');
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Cannot read config file "${filePath}": ${error.message}`);
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ throw new Error(`Invalid JSON in config file "${filePath}"`);
33
+ }
34
+ const statusMapping = {};
35
+ if (parsed.statusMapping) {
36
+ for (const [providerStatus, normalized] of Object.entries(parsed.statusMapping)) {
37
+ if (!NORMALIZED_WORKFLOW_STATES.includes(normalized)) {
38
+ throw new Error(`Invalid workflow state "${normalized}" for status "${providerStatus}". ` +
39
+ `Valid states: ${NORMALIZED_WORKFLOW_STATES.join(', ')}`);
40
+ }
41
+ statusMapping[providerStatus] = normalized;
42
+ }
43
+ }
44
+ const hooks = [];
45
+ if (parsed.hooks) {
46
+ if (!Array.isArray(parsed.hooks)) {
47
+ throw new Error('Config "hooks" must be an array');
48
+ }
49
+ for (const [i, hook] of parsed.hooks.entries()) {
50
+ if (!hook.actionType) {
51
+ throw new Error(`Hook[${i}]: "actionType" is required`);
52
+ }
53
+ if (!ALL_ACTION_TYPES.includes(hook.actionType)) {
54
+ throw new Error(`Hook[${i}]: invalid actionType "${hook.actionType}". ` +
55
+ `Valid types: ${ALL_ACTION_TYPES.join(', ')}`);
56
+ }
57
+ if (!hook.triggerStatus) {
58
+ throw new Error(`Hook[${i}]: "triggerStatus" is required`);
59
+ }
60
+ hooks.push({
61
+ action: {
62
+ category: getActionCategory(hook.actionType),
63
+ config: hook.actionConfig || {},
64
+ type: hook.actionType,
65
+ },
66
+ trigger: {
67
+ conditions: { statusTo: hook.triggerStatus },
68
+ event: 'status_transition',
69
+ source: 'jira',
70
+ },
71
+ });
72
+ }
73
+ }
74
+ return { hooks, statusMapping };
75
+ }
76
+ // ============================================================================
77
+ // Step (a): Select Tracker
78
+ // ============================================================================
79
+ export async function stepSelectTracker(service, preselectedTrackerId) {
80
+ const spinner = ora('Fetching tracker connections...').start();
81
+ let connections;
82
+ try {
83
+ const response = await service.jiraStatus();
84
+ // Response may contain connections array or be the connection itself
85
+ if (Array.isArray(response)) {
86
+ connections = response;
87
+ }
88
+ else if (response.connections && Array.isArray(response.connections)) {
89
+ connections = response.connections;
90
+ }
91
+ else {
92
+ // Single connection object — wrap in array
93
+ connections = [response];
94
+ }
95
+ // Filter to active connections
96
+ connections = connections.filter(c => c.status === 'active' || c.jiraDomain);
97
+ if (connections.length === 0) {
98
+ spinner.fail('No active tracker connections found');
99
+ throw new Error('No tracker connections found.\n\n' +
100
+ `Connect a tracker first with: ${chalk.cyan('hd tracker connect')}`);
101
+ }
102
+ spinner.succeed(`Found ${connections.length} tracker connection(s)`);
103
+ }
104
+ catch (error) {
105
+ if (error.message?.includes('No tracker connections'))
106
+ throw error;
107
+ spinner.fail('Failed to fetch tracker connections');
108
+ throw error;
109
+ }
110
+ // Non-interactive: match by tracker ID
111
+ if (preselectedTrackerId) {
112
+ const match = connections.find(c => c.trackerId === preselectedTrackerId ||
113
+ c.cloudId === preselectedTrackerId ||
114
+ c.jiraDomain === preselectedTrackerId);
115
+ if (!match) {
116
+ throw new Error(`Tracker "${preselectedTrackerId}" not found. ` +
117
+ `Available: ${connections.map(c => c.trackerId || c.jiraDomain || c.cloudId).join(', ')}`);
118
+ }
119
+ return {
120
+ domain: match.jiraDomain || match.domain || '',
121
+ provider: 'jira',
122
+ trackerId: match.trackerId || match.cloudId || match.jiraDomain || '',
123
+ };
124
+ }
125
+ const choices = connections.map(c => ({
126
+ name: `${c.jiraDomain || c.domain || 'Unknown'} (${chalk.dim('Jira')})`,
127
+ value: c,
128
+ }));
129
+ const { selected } = await inquirer.prompt([{
130
+ choices,
131
+ message: 'Select a tracker connection:',
132
+ name: 'selected',
133
+ type: 'list',
134
+ }]);
135
+ return {
136
+ domain: selected.jiraDomain || selected.domain || '',
137
+ provider: 'jira',
138
+ trackerId: selected.trackerId || selected.cloudId || selected.jiraDomain || '',
139
+ };
140
+ }
141
+ // ============================================================================
142
+ // Step (b): Select External Project
143
+ // ============================================================================
144
+ export async function stepSelectExternalProject(service, _trackerId, override) {
145
+ // Non-interactive: use provided values directly
146
+ if (override) {
147
+ return {
148
+ externalProjectKey: override.projectKey,
149
+ externalProjectName: override.projectName,
150
+ };
151
+ }
152
+ const spinner = ora('Fetching projects from tracker...').start();
153
+ let projects = [];
154
+ let fetchFailed = false;
155
+ try {
156
+ const response = await service.jiraListProjects();
157
+ if (response.projects && Array.isArray(response.projects)) {
158
+ projects = response.projects;
159
+ }
160
+ spinner.succeed(`Found ${projects.length} project(s)`);
161
+ }
162
+ catch {
163
+ spinner.warn('Could not fetch projects from tracker');
164
+ fetchFailed = true;
165
+ }
166
+ if (!fetchFailed && projects.length > 0) {
167
+ const choices = [
168
+ ...projects.map(p => ({
169
+ name: `${p.key} — ${p.name}`,
170
+ value: { externalProjectKey: p.key || '', externalProjectName: p.name || '' },
171
+ })),
172
+ { name: chalk.dim('Enter manually...'), value: '__manual__' },
173
+ ];
174
+ const { selected } = await inquirer.prompt([{
175
+ choices,
176
+ message: 'Select the external project:',
177
+ name: 'selected',
178
+ type: 'list',
179
+ }]);
180
+ if (selected !== '__manual__') {
181
+ return selected;
182
+ }
183
+ }
184
+ // Manual entry
185
+ const answers = await inquirer.prompt([
186
+ {
187
+ message: 'External project key (e.g., PROJ):',
188
+ name: 'externalProjectKey',
189
+ type: 'input',
190
+ validate: (input) => input.trim() ? true : 'Project key is required',
191
+ },
192
+ {
193
+ message: 'External project name:',
194
+ name: 'externalProjectName',
195
+ type: 'input',
196
+ validate: (input) => input.trim() ? true : 'Project name is required',
197
+ },
198
+ ]);
199
+ return {
200
+ externalProjectKey: answers.externalProjectKey.trim(),
201
+ externalProjectName: answers.externalProjectName.trim(),
202
+ };
203
+ }
204
+ // ============================================================================
205
+ // Step (c): Link Modules
206
+ // ============================================================================
207
+ export async function stepLinkModules(service, override) {
208
+ const spinner = ora('Fetching modules...').start();
209
+ let modules = [];
210
+ try {
211
+ modules = await service.moduleList();
212
+ if (modules.length === 0) {
213
+ spinner.fail('No modules found');
214
+ throw new Error('No modules found in your tenant.\n\n' +
215
+ `Create a module first with: ${chalk.cyan('hd module create')}`);
216
+ }
217
+ spinner.succeed(`Found ${modules.length} module(s)`);
218
+ }
219
+ catch (error) {
220
+ if (error.message?.includes('No modules found'))
221
+ throw error;
222
+ spinner.fail('Failed to fetch modules');
223
+ throw error;
224
+ }
225
+ // Non-interactive: match provided module IDs against fetched modules
226
+ if (override) {
227
+ const matched = [];
228
+ for (const id of override.moduleIds) {
229
+ const mod = modules.find(m => m.projectId === id || m.slug === id);
230
+ if (!mod) {
231
+ const available = modules.map(m => m.projectId || m.slug).join(', ');
232
+ throw new Error(`Module "${id}" not found. Available: ${available}`);
233
+ }
234
+ const moduleId = mod.projectId || mod.slug || '';
235
+ matched.push({
236
+ moduleId,
237
+ moduleName: mod.name || mod.slug || '',
238
+ role: (override.primaryModuleId && (moduleId === override.primaryModuleId || mod.slug === override.primaryModuleId))
239
+ ? 'primary'
240
+ : 'supporting',
241
+ });
242
+ }
243
+ // If only one module and no explicit primary, make it primary
244
+ if (matched.length === 1 && !override.primaryModuleId) {
245
+ matched[0].role = 'primary';
246
+ }
247
+ return matched;
248
+ }
249
+ const { selectedModules } = await inquirer.prompt([{
250
+ choices: modules.map(m => ({
251
+ name: `${m.name || m.slug} (${chalk.dim(m.slug)})`,
252
+ value: { moduleId: m.projectId || m.slug || '', moduleName: m.name || m.slug || '' },
253
+ })),
254
+ message: 'Select modules to link (space to select, enter to confirm):',
255
+ name: 'selectedModules',
256
+ type: 'checkbox',
257
+ validate: (input) => input.length > 0 ? true : 'Select at least one module',
258
+ }]);
259
+ const selected = selectedModules;
260
+ // Ask which is primary if more than one selected
261
+ let primaryId = null;
262
+ if (selected.length > 1) {
263
+ const { primary } = await inquirer.prompt([{
264
+ choices: [
265
+ ...selected.map(m => ({ name: m.moduleName, value: m.moduleId })),
266
+ { name: chalk.dim('None — all supporting'), value: '__none__' },
267
+ ],
268
+ message: 'Which module is the primary module?',
269
+ name: 'primary',
270
+ type: 'list',
271
+ }]);
272
+ if (primary !== '__none__')
273
+ primaryId = primary;
274
+ }
275
+ else {
276
+ primaryId = selected[0].moduleId;
277
+ }
278
+ return selected.map(m => ({
279
+ moduleId: m.moduleId,
280
+ moduleName: m.moduleName,
281
+ role: m.moduleId === primaryId ? 'primary' : 'supporting',
282
+ }));
283
+ }
284
+ // ============================================================================
285
+ // Step (d): Map Statuses + Configure Hooks
286
+ // ============================================================================
287
+ export async function stepMapStatuses(service, _trackerId, externalProjectKey, override) {
288
+ // Non-interactive: use config file data directly
289
+ if (override) {
290
+ return override;
291
+ }
292
+ const statusMapping = {};
293
+ const hooks = [];
294
+ const spinner = ora('Fetching provider statuses...').start();
295
+ let providerStatuses = [];
296
+ try {
297
+ const response = await service.jiraGetProjectStatuses(externalProjectKey);
298
+ providerStatuses = response.statuses || [];
299
+ spinner.succeed(`Found ${providerStatuses.length} status(es)`);
300
+ }
301
+ catch {
302
+ spinner.warn('Could not fetch statuses from tracker');
303
+ const { manualStatuses } = await inquirer.prompt([{
304
+ message: 'Enter status names (comma-separated):',
305
+ name: 'manualStatuses',
306
+ type: 'input',
307
+ validate: (input) => input.trim() ? true : 'Enter at least one status',
308
+ }]);
309
+ providerStatuses = manualStatuses.split(',').map((s, i) => ({
310
+ id: String(i + 1),
311
+ name: s.trim(),
312
+ }));
313
+ }
314
+ const stateChoices = [
315
+ ...NORMALIZED_WORKFLOW_STATES.map(s => ({ name: s, value: s })),
316
+ { name: chalk.dim("Skip (don't map)"), value: '__skip__' },
317
+ ];
318
+ for (const providerStatus of providerStatuses) {
319
+ // Map status
320
+ const { mapped } = await inquirer.prompt([{
321
+ choices: stateChoices,
322
+ message: `Map "${chalk.cyan(providerStatus.name)}" to:`,
323
+ name: 'mapped',
324
+ type: 'list',
325
+ }]);
326
+ if (mapped === '__skip__')
327
+ continue;
328
+ statusMapping[providerStatus.name] = mapped;
329
+ // Ask about hooks for this status transition
330
+ let addHook = true;
331
+ while (addHook) {
332
+ const { wantHook } = await inquirer.prompt([{
333
+ default: false,
334
+ message: `Add a hook for transition to "${chalk.cyan(providerStatus.name)}"?`,
335
+ name: 'wantHook',
336
+ type: 'confirm',
337
+ }]);
338
+ if (!wantHook)
339
+ break;
340
+ // Reuse existing hook-flow prompts
341
+ const { category, type: actionType } = await promptActionTypeV2();
342
+ const actionConfig = await promptActionConfig(actionType);
343
+ hooks.push({
344
+ action: { category, config: actionConfig, type: actionType },
345
+ trigger: {
346
+ conditions: { statusTo: providerStatus.name },
347
+ event: 'status_transition',
348
+ source: 'jira',
349
+ },
350
+ });
351
+ const { addAnother } = await inquirer.prompt([{
352
+ default: false,
353
+ message: 'Add another hook for this status?',
354
+ name: 'addAnother',
355
+ type: 'confirm',
356
+ }]);
357
+ addHook = addAnother;
358
+ }
359
+ }
360
+ return { hooks, statusMapping };
361
+ }
362
+ // ============================================================================
363
+ // Step (e): Display Summary
364
+ // ============================================================================
365
+ export function displaySummary(config, log) {
366
+ log('');
367
+ log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
368
+ log(chalk.blue.bold(' Tracker Project Configuration Summary'));
369
+ log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
370
+ log('');
371
+ log(` Provider: ${chalk.cyan(config.tracker.provider)}`);
372
+ log(` Tracker Domain: ${chalk.cyan(config.tracker.domain)}`);
373
+ log(` External Project: ${chalk.cyan(config.externalProject.externalProjectKey)} — ${config.externalProject.externalProjectName}`);
374
+ log('');
375
+ // Modules
376
+ log(chalk.bold(' Linked Modules:'));
377
+ for (const mod of config.linkedModules) {
378
+ const roleTag = mod.role === 'primary' ? chalk.green(' [primary]') : chalk.dim(' [supporting]');
379
+ log(` - ${mod.moduleName}${roleTag}`);
380
+ }
381
+ log('');
382
+ // Status mapping
383
+ const mappedCount = Object.keys(config.statusMapping).length;
384
+ log(chalk.bold(` Status Mapping (${mappedCount}):`));
385
+ if (mappedCount > 0) {
386
+ for (const [provider, normalized] of Object.entries(config.statusMapping)) {
387
+ log(` ${provider} → ${chalk.cyan(normalized)}`);
388
+ }
389
+ }
390
+ else {
391
+ log(chalk.dim(' No statuses mapped'));
392
+ }
393
+ log('');
394
+ log(` Hooks: ${chalk.cyan(String(config.hooks.length))}`);
395
+ log('');
396
+ }
397
+ // ============================================================================
398
+ // Step (e): Confirm
399
+ // ============================================================================
400
+ export async function stepConfirm() {
401
+ const { confirmed } = await inquirer.prompt([{
402
+ default: true,
403
+ message: 'Create this tracker project configuration?',
404
+ name: 'confirmed',
405
+ type: 'confirm',
406
+ }]);
407
+ return confirmed;
408
+ }
409
+ // ============================================================================
410
+ // Idempotency Guard Helpers
411
+ // ============================================================================
412
+ export async function findExistingTrackerProject(service, config) {
413
+ const projects = await service.trackerProjectList();
414
+ return projects.find(p => p.trackerId === config.tracker.trackerId &&
415
+ p.externalProjectKey === config.externalProject.externalProjectKey) || null;
416
+ }
417
+ export async function promptExistingProjectAction(existingProject) {
418
+ const { action } = await inquirer.prompt([{
419
+ choices: [
420
+ { name: 'Skip (use existing, no changes)', value: 'skip' },
421
+ { name: 'Replace hooks (delete existing hooks, recreate from config)', value: 'replace-hooks' },
422
+ { name: 'Abort (exit)', value: 'abort' },
423
+ ],
424
+ message: `Tracker project already exists for ${chalk.cyan(existingProject.externalProjectKey)} (ID: ${existingProject.trackerProjectId}). What would you like to do?`,
425
+ name: 'action',
426
+ type: 'list',
427
+ }]);
428
+ return action;
429
+ }
430
+ export async function deleteExistingHooks(service, trackerProjectId) {
431
+ const spinner = ora('Fetching existing hooks...').start();
432
+ const response = await service.trackerProjectHookList(trackerProjectId);
433
+ const hooks = response.hooks || [];
434
+ if (hooks.length === 0) {
435
+ spinner.succeed('No existing hooks to delete');
436
+ return 0;
437
+ }
438
+ spinner.text = `Deleting ${hooks.length} existing hook(s)...`;
439
+ for (const hook of hooks) {
440
+ await service.trackerProjectHookDelete(trackerProjectId, hook.hookId);
441
+ }
442
+ spinner.succeed(`Deleted ${hooks.length} existing hook(s)`);
443
+ return hooks.length;
444
+ }
445
+ // ============================================================================
446
+ // Execute: Create tracker project + link modules + create hooks
447
+ // ============================================================================
448
+ export async function executeTrackerProjectInit(service, config, options = {}) {
449
+ let trackerProject = null;
450
+ let createdInThisRun = false;
451
+ try {
452
+ // Step 0: Idempotency guard — check for existing tracker project
453
+ const checkSpinner = ora('Checking for existing tracker project...').start();
454
+ const existingProject = await findExistingTrackerProject(service, config);
455
+ checkSpinner.stop();
456
+ if (existingProject) {
457
+ if (options.nonInteractive) {
458
+ // Non-interactive: auto-skip creation, reuse existing
459
+ ora().warn(`Tracker project already exists for ${chalk.cyan(config.externalProject.externalProjectKey)} on tracker ${chalk.cyan(config.tracker.trackerId)}, skipping creation`);
460
+ trackerProject = existingProject;
461
+ }
462
+ else {
463
+ // Interactive: prompt user
464
+ const action = await promptExistingProjectAction(existingProject);
465
+ if (action === 'abort') {
466
+ throw new Error('Aborted by user');
467
+ }
468
+ trackerProject = existingProject;
469
+ if (action === 'replace-hooks') {
470
+ await deleteExistingHooks(service, existingProject.trackerProjectId);
471
+ }
472
+ else {
473
+ // 'skip' — return early with existing project info, no module linking or hook creation
474
+ return {
475
+ hooks: [],
476
+ moduleLinks: [],
477
+ trackerProject: existingProject,
478
+ };
479
+ }
480
+ }
481
+ }
482
+ // Step 1: Create tracker project (only if no existing match)
483
+ if (!trackerProject) {
484
+ const createSpinner = ora('Creating tracker project...').start();
485
+ trackerProject = await service.trackerProjectCreate({
486
+ externalProjectKey: config.externalProject.externalProjectKey,
487
+ externalProjectName: config.externalProject.externalProjectName,
488
+ provider: config.tracker.provider,
489
+ statusMapping: config.statusMapping,
490
+ trackerId: config.tracker.trackerId,
491
+ });
492
+ createdInThisRun = true;
493
+ createSpinner.succeed('Tracker project created');
494
+ }
495
+ // Step 2: Link modules
496
+ const moduleLinks = [];
497
+ for (const mod of config.linkedModules) {
498
+ const modSpinner = ora(`Linking module ${chalk.cyan(mod.moduleName)}...`).start();
499
+ const link = await service.trackerProjectLinkModule(trackerProject.trackerProjectId, {
500
+ moduleId: mod.moduleId,
501
+ role: mod.role,
502
+ });
503
+ moduleLinks.push(link);
504
+ modSpinner.succeed(`Module ${chalk.cyan(mod.moduleName)} linked`);
505
+ }
506
+ // Step 3: Create hooks
507
+ const hooks = [];
508
+ for (const hookConfig of config.hooks) {
509
+ const hookSpinner = ora(`Creating hook (${hookConfig.action.type})...`).start();
510
+ const hook = await service.trackerProjectHookCreateV2(trackerProject.trackerProjectId, hookConfig);
511
+ hooks.push(hook);
512
+ hookSpinner.succeed(`Hook created: ${chalk.cyan(hookConfig.action.type)}`);
513
+ }
514
+ return { hooks, moduleLinks, trackerProject };
515
+ }
516
+ catch (error) {
517
+ // Rollback ONLY if tracker project was created in this run (not pre-existing)
518
+ if (trackerProject && createdInThisRun) {
519
+ const rollbackSpinner = ora('Rolling back...').start();
520
+ try {
521
+ await service.trackerProjectDelete(trackerProject.trackerProjectId);
522
+ rollbackSpinner.warn('Rolled back — tracker project deleted');
523
+ }
524
+ catch (rollbackError) {
525
+ rollbackSpinner.fail('Rollback failed — manual cleanup may be needed');
526
+ }
527
+ }
528
+ throw error;
529
+ }
530
+ }
531
+ export async function selectTrackerProject(service, trackerProjectId) {
532
+ // Non-interactive: validate provided ID
533
+ if (trackerProjectId) {
534
+ const project = await service.trackerProjectGet(trackerProjectId);
535
+ return {
536
+ externalProjectKey: project.externalProjectKey,
537
+ externalProjectName: project.externalProjectName,
538
+ trackerProjectId: project.trackerProjectId,
539
+ };
540
+ }
541
+ // Interactive: fetch list and prompt
542
+ const spinner = ora('Fetching tracker projects...').start();
543
+ const projects = await service.trackerProjectList();
544
+ spinner.stop();
545
+ if (!projects || projects.length === 0) {
546
+ throw new Error('No tracker projects found.\n\n' +
547
+ `Create one first with: ${chalk.cyan('hd tracker project init')}`);
548
+ }
549
+ const { selected } = await inquirer.prompt([{
550
+ choices: projects.map(p => ({
551
+ name: `${p.externalProjectKey} — ${p.externalProjectName}`,
552
+ value: p,
553
+ })),
554
+ message: 'Select a tracker project:',
555
+ name: 'selected',
556
+ type: 'list',
557
+ }]);
558
+ const project = selected;
559
+ return {
560
+ externalProjectKey: project.externalProjectKey,
561
+ externalProjectName: project.externalProjectName,
562
+ trackerProjectId: project.trackerProjectId,
563
+ };
564
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperdrive.bot/cli",
3
3
  "description": "hyperdrive.bot is a command-line interface (CLI) tool designed for managing and deploying projects using the Hyperdrive API. The CLI acts as a proxy to the Hyperdrive API, enabling users to:",
4
- "version": "1.0.12",
4
+ "version": "1.0.16",
5
5
  "author": "marcelomarra",
6
6
  "bin": {
7
7
  "hd": "./bin/run.js",
@@ -14,12 +14,15 @@
14
14
  "@aws-sdk/client-cloudwatch-logs": "^3.958.0",
15
15
  "@aws-sdk/client-cognito-identity": "^3.922.0",
16
16
  "@aws-sdk/client-cognito-identity-provider": "^3.971.0",
17
+ "@aws-sdk/client-lambda": "^3.1041.0",
18
+ "@hyperdrive.bot/auth-plugin": "file:../auth-plugin",
17
19
  "@hyperdrive.bot/bmad-workflow": "file:../bmad-workflow",
18
- "@hyperdrive.bot/cli-auth": "^1.0.2",
20
+ "@hyperdrive.bot/cli-auth": "^1.1.4",
19
21
  "@hyperdrive.bot/gh-plugin": "file:../hyperdrive-gh",
20
22
  "@hyperdrive.bot/glab-plugin": "file:../hyperdrive-glab",
21
23
  "@hyperdrive.bot/gut": "file:../gut",
22
24
  "@hyperdrive.bot/jira-plugin": "file:../hyperdrive-jira",
25
+ "@hyperdrive.bot/plugin-telemetry": "file:../telemetry-plugin",
23
26
  "@hyperdrive.bot/vercel-plugin": "file:../hyperdrive-vercel",
24
27
  "@oclif/core": "^4",
25
28
  "@oclif/plugin-help": "^6",
@@ -30,6 +33,7 @@
30
33
  "cli-table3": "^0.6.5",
31
34
  "inquirer": "^9.2.20",
32
35
  "jsonwebtoken": "^9.0.3",
36
+ "mime-types": "^3.0.2",
33
37
  "moment": "^2.30.1",
34
38
  "open": "^10.2.0",
35
39
  "ora": "^8.0.1"
@@ -41,6 +45,7 @@
41
45
  "@types/inquirer": "^9.0.7",
42
46
  "@types/js-yaml": "^4.0.9",
43
47
  "@types/jsonwebtoken": "^9.0.10",
48
+ "@types/mime-types": "^3.0.1",
44
49
  "@types/mocha": "^10",
45
50
  "@types/node": "^18",
46
51
  "@types/sinon": "^17",
@@ -87,13 +92,23 @@
87
92
  "plugins": [
88
93
  "@oclif/plugin-help",
89
94
  "@oclif/plugin-plugins",
95
+ "@hyperdrive.bot/auth-plugin",
90
96
  "@hyperdrive.bot/gut",
91
97
  "@hyperdrive.bot/bmad-workflow",
92
98
  "@hyperdrive.bot/gh-plugin",
93
99
  "@hyperdrive.bot/glab-plugin",
94
100
  "@hyperdrive.bot/jira-plugin",
95
- "@hyperdrive.bot/vercel-plugin"
101
+ "@hyperdrive.bot/vercel-plugin",
102
+ "@hyperdrive.bot/plugin-telemetry"
96
103
  ],
104
+ "authPlugin": {
105
+ "appName": "hyperdrive",
106
+ "displayName": "Hyperdrive",
107
+ "defaultBootstrapUrl": "https://api.hyperdrive.bot/tenant/bootstrap",
108
+ "envPrefix": "HYPERDRIVE",
109
+ "ciTokenPrefix": "hd_sk_",
110
+ "primaryApiName": "hyperdrive"
111
+ },
97
112
  "topicSeparator": " ",
98
113
  "topics": {
99
114
  "entity": {
@@ -120,11 +135,20 @@
120
135
  "glab": {
121
136
  "description": "GitLab CLI passthrough (glab-plugin)"
122
137
  },
138
+ "hook": {
139
+ "description": "Manage tenant lifecycle hooks"
140
+ },
123
141
  "jira": {
124
142
  "description": "Jira CLI for issues, boards, and sprints (jira-plugin)"
125
143
  },
126
- "jira hook": {
127
- "description": "Manage Jira status transition hooks"
144
+ "tracker": {
145
+ "description": "Manage issue tracker integration (connect, status, hooks)"
146
+ },
147
+ "tracker hook": {
148
+ "description": "Manage hooks on tracker projects (add, list, remove, toggle)"
149
+ },
150
+ "tracker project": {
151
+ "description": "Manage tracker projects (init, list, status)"
128
152
  },
129
153
  "vercel": {
130
154
  "description": "Vercel CLI passthrough (vercel-plugin)"
@@ -152,5 +176,9 @@
152
176
  "version": "oclif readme && git add README.md",
153
177
  "publish:docker": "docker logout && aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/j4f5a6i6 && docker build --platform linux/amd64 -t public.ecr.aws/j4f5a6i6/devsquad/hyperdrive:latest . && docker push public.ecr.aws/j4f5a6i6/devsquad/hyperdrive:latest"
154
178
  },
155
- "types": "dist/index.d.ts"
156
- }
179
+ "types": "dist/index.d.ts",
180
+ "publishConfig": {
181
+ "access": "public",
182
+ "registry": "https://registry.npmjs.org/"
183
+ }
184
+ }
@@ -1,16 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class Login extends Command {
3
- static description: string;
4
- static examples: string[];
5
- static flags: {
6
- ci: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
- domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
- port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
- tenant: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
- };
11
- run(): Promise<void>;
12
- /**
13
- * Run CI authentication flow (non-interactive)
14
- */
15
- private runCIAuth;
16
- }