@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.
- package/README.md +1495 -474
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.js +239 -0
- package/dist/commands/deployment/create.js +10 -2
- package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
- package/dist/commands/domain/set-production.js +27 -0
- package/dist/commands/git/list-open-prs.d.ts +12 -0
- package/dist/commands/git/list-open-prs.js +87 -0
- package/dist/commands/hook/add.d.ts +22 -0
- package/dist/commands/hook/add.js +299 -0
- package/dist/commands/hook/list.d.ts +11 -0
- package/dist/commands/hook/list.js +111 -0
- package/dist/commands/hook/logs.d.ts +13 -0
- package/dist/commands/hook/logs.js +124 -0
- package/dist/commands/hook/remove.d.ts +12 -0
- package/dist/commands/hook/remove.js +115 -0
- package/dist/commands/hook/toggle.d.ts +12 -0
- package/dist/commands/hook/toggle.js +125 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +49 -9
- package/dist/commands/module/bindings.d.ts +14 -0
- package/dist/commands/module/bindings.js +125 -0
- package/dist/commands/module/create.d.ts +3 -0
- package/dist/commands/module/create.js +156 -78
- package/dist/commands/module/list.d.ts +1 -0
- package/dist/commands/module/list.js +22 -1
- package/dist/commands/module/sync.d.ts +29 -0
- package/dist/commands/module/sync.js +409 -0
- package/dist/commands/module/unlink.d.ts +11 -0
- package/dist/commands/module/unlink.js +77 -0
- package/dist/commands/module/update.d.ts +10 -0
- package/dist/commands/module/update.js +168 -5
- package/dist/commands/network/discover.d.ts +12 -0
- package/dist/commands/network/discover.js +210 -0
- package/dist/commands/network/get.d.ts +13 -0
- package/dist/commands/network/get.js +90 -0
- package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
- package/dist/commands/network/list.js +71 -0
- package/dist/commands/network/register.d.ts +16 -0
- package/dist/commands/network/register.js +144 -0
- package/dist/commands/parameter/sync.d.ts +13 -0
- package/dist/commands/parameter/sync.js +69 -1
- package/dist/commands/project/sync.d.ts +5 -11
- package/dist/commands/project/sync.js +12 -381
- package/dist/commands/seed.d.ts +93 -0
- package/dist/commands/seed.js +324 -0
- package/dist/commands/service/backup.d.ts +17 -0
- package/dist/commands/service/backup.js +156 -0
- package/dist/commands/service/backups.d.ts +14 -0
- package/dist/commands/service/backups.js +110 -0
- package/dist/commands/service/bind.d.ts +16 -0
- package/dist/commands/service/bind.js +106 -0
- package/dist/commands/service/bindings.d.ts +13 -0
- package/dist/commands/service/bindings.js +78 -0
- package/dist/commands/service/clone.d.ts +19 -0
- package/dist/commands/service/clone.js +153 -0
- package/dist/commands/service/create.d.ts +16 -0
- package/dist/commands/service/create.js +212 -0
- package/dist/commands/service/get.d.ts +13 -0
- package/dist/commands/service/get.js +97 -0
- package/dist/commands/service/list.d.ts +12 -0
- package/dist/commands/service/list.js +86 -0
- package/dist/commands/service/register.d.ts +21 -0
- package/dist/commands/service/register.js +215 -0
- package/dist/commands/service/restore.d.ts +19 -0
- package/dist/commands/service/restore.js +158 -0
- package/dist/commands/service/seed.d.ts +17 -0
- package/dist/commands/service/seed.js +173 -0
- package/dist/commands/service/templates.d.ts +10 -0
- package/dist/commands/service/templates.js +66 -0
- package/dist/commands/service/unbind.d.ts +15 -0
- package/dist/commands/service/unbind.js +74 -0
- package/dist/commands/stage/create.d.ts +23 -0
- package/dist/commands/stage/create.js +145 -6
- package/dist/commands/stage/delete.d.ts +11 -0
- package/dist/commands/stage/delete.js +85 -0
- package/dist/commands/stage/deploy.d.ts +34 -0
- package/dist/commands/stage/deploy.js +294 -0
- package/dist/commands/stage/ensure-branches.d.ts +23 -0
- package/dist/commands/stage/ensure-branches.js +101 -0
- package/dist/commands/stage/list.js +4 -0
- package/dist/commands/stage/status.d.ts +14 -0
- package/dist/commands/stage/status.js +100 -0
- package/dist/commands/{jira → tracker}/connect.js +32 -23
- package/dist/commands/tracker/hook/add.d.ts +25 -0
- package/dist/commands/tracker/hook/add.js +284 -0
- package/dist/commands/{jira → tracker}/hook/list.js +20 -11
- package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
- package/dist/commands/tracker/hook/logs.js +126 -0
- package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
- package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
- package/dist/commands/tracker/project/init.d.ts +17 -0
- package/dist/commands/tracker/project/init.js +178 -0
- package/dist/commands/tracker/project/link-module.d.ts +17 -0
- package/dist/commands/tracker/project/link-module.js +287 -0
- package/dist/commands/tracker/project/list-modules.d.ts +11 -0
- package/dist/commands/tracker/project/list-modules.js +117 -0
- package/dist/commands/tracker/project/list.d.ts +10 -0
- package/dist/commands/tracker/project/list.js +90 -0
- package/dist/commands/tracker/project/status.d.ts +13 -0
- package/dist/commands/tracker/project/status.js +168 -0
- package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
- package/dist/commands/tracker/project/unlink-module.js +251 -0
- package/dist/commands/{jira → tracker}/status.js +3 -3
- package/dist/lib/ensure-branches.d.ts +53 -0
- package/dist/lib/ensure-branches.js +149 -0
- package/dist/lib/git-providers/github.d.ts +16 -0
- package/dist/lib/git-providers/github.js +157 -0
- package/dist/lib/git-providers/gitlab.d.ts +16 -0
- package/dist/lib/git-providers/gitlab.js +148 -0
- package/dist/lib/git-providers/index.d.ts +67 -0
- package/dist/lib/git-providers/index.js +39 -0
- package/dist/lib/lambda-warmer.d.ts +106 -0
- package/dist/lib/lambda-warmer.js +189 -0
- package/dist/services/hyperdrive-sigv4.d.ts +360 -5
- package/dist/services/hyperdrive-sigv4.js +192 -24
- package/dist/utils/hook-flow.d.ts +60 -3
- package/dist/utils/hook-flow.js +437 -2
- package/dist/utils/hook-normalize.d.ts +6 -0
- package/dist/utils/hook-normalize.js +33 -0
- package/dist/utils/lifecycle-poller.d.ts +32 -0
- package/dist/utils/lifecycle-poller.js +72 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.js +88 -0
- package/dist/utils/summary-display.js +1 -1
- package/dist/utils/tracker-project-flow.d.ts +84 -0
- package/dist/utils/tracker-project-flow.js +564 -0
- package/package.json +35 -7
- package/dist/commands/auth/login.d.ts +0 -16
- package/dist/commands/auth/login.js +0 -179
- package/dist/commands/auth/logout.js +0 -116
- package/dist/commands/auth/refresh.d.ts +0 -6
- package/dist/commands/auth/refresh.js +0 -66
- package/dist/commands/auth/status.d.ts +0 -6
- package/dist/commands/auth/status.js +0 -63
- package/dist/commands/config/get.d.ts +0 -9
- package/dist/commands/config/get.js +0 -37
- package/dist/commands/config/set.d.ts +0 -10
- package/dist/commands/config/set.js +0 -48
- package/dist/commands/config/show.d.ts +0 -6
- package/dist/commands/config/show.js +0 -10
- package/dist/commands/domain/current.d.ts +0 -6
- package/dist/commands/domain/current.js +0 -18
- package/dist/commands/domain/list.d.ts +0 -6
- package/dist/commands/domain/list.js +0 -42
- package/dist/commands/domain/switch.js +0 -40
- package/dist/commands/jira/hook/add.js +0 -147
- package/dist/services/tenant-service.d.ts +0 -127
- package/dist/services/tenant-service.js +0 -396
- package/dist/utils/auth-flow.d.ts +0 -147
- package/dist/utils/auth-flow.js +0 -479
- package/oclif.manifest.json +0 -3519
- /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
- /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.
|
|
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.
|
|
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
|
-
"
|
|
127
|
-
"description": "Manage
|
|
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
|
-
}
|