@hyperdrive.bot/cli 1.0.13 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +359 -5
- package/dist/services/hyperdrive-sigv4.js +177 -12
- 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
package/dist/utils/hook-flow.js
CHANGED
|
@@ -1,4 +1,55 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
|
+
const SMART_ACTION_TYPES = ['git-create-branch', 'git-create-mr', 'git-delete-branch'];
|
|
3
|
+
export const VALID_TRIGGER_EVENTS = [
|
|
4
|
+
'status_transition', 'issue_created', 'issue_assigned',
|
|
5
|
+
'field_changed', 'comment_added', 'label_changed',
|
|
6
|
+
];
|
|
7
|
+
export const ALL_ACTION_TYPES = [
|
|
8
|
+
'git-create-branch', 'git-create-mr', 'git-delete-branch',
|
|
9
|
+
'adhb-enrich', 'ci-trigger', 'slack-notify', 'webhook',
|
|
10
|
+
'stage-create', 'stage-delete', 'deploy',
|
|
11
|
+
];
|
|
12
|
+
export const VALID_LIFECYCLE_EVENTS = [
|
|
13
|
+
'stage.created', 'stage.provisioned', 'stage.domain.ready', 'stage.domain.failed',
|
|
14
|
+
'stage.destroying', 'stage.destroyed',
|
|
15
|
+
'deploy.created', 'deploy.ready', 'deploy.launched', 'deploy.completed', 'deploy.failed',
|
|
16
|
+
'launch.created', 'launch.deployed', 'launch.failed', 'launch.terminated',
|
|
17
|
+
'mission.started', 'mission.wave.completed', 'mission.completed', 'mission.failed',
|
|
18
|
+
];
|
|
19
|
+
export const LIFECYCLE_EVENT_GROUPS = [
|
|
20
|
+
new inquirer.Separator('── Stage Events ──'),
|
|
21
|
+
{ name: 'stage.created — Stage record created', value: 'stage.created' },
|
|
22
|
+
{ name: 'stage.provisioned — Stage infrastructure ready', value: 'stage.provisioned' },
|
|
23
|
+
{ name: 'stage.domain.ready — Custom domain live', value: 'stage.domain.ready' },
|
|
24
|
+
{ name: 'stage.domain.failed — Domain provisioning failed', value: 'stage.domain.failed' },
|
|
25
|
+
{ name: 'stage.destroying — Stage teardown started', value: 'stage.destroying' },
|
|
26
|
+
{ name: 'stage.destroyed — Stage fully deleted', value: 'stage.destroyed' },
|
|
27
|
+
new inquirer.Separator('── Deploy Events ──'),
|
|
28
|
+
{ name: 'deploy.created — Deployment queued for build', value: 'deploy.created' },
|
|
29
|
+
{ name: 'deploy.ready — Build artifacts available', value: 'deploy.ready' },
|
|
30
|
+
{ name: 'deploy.launched — Live and serving traffic', value: 'deploy.launched' },
|
|
31
|
+
{ name: 'deploy.completed — Full deploy cycle done', value: 'deploy.completed' },
|
|
32
|
+
{ name: 'deploy.failed — Build or launch failed', value: 'deploy.failed' },
|
|
33
|
+
new inquirer.Separator('── Launch Events ──'),
|
|
34
|
+
{ name: 'launch.created — Launch record created', value: 'launch.created' },
|
|
35
|
+
{ name: 'launch.deployed — Launch live', value: 'launch.deployed' },
|
|
36
|
+
{ name: 'launch.failed — Launch failed', value: 'launch.failed' },
|
|
37
|
+
{ name: 'launch.terminated — Replaced by newer launch', value: 'launch.terminated' },
|
|
38
|
+
new inquirer.Separator('── Mission Events ──'),
|
|
39
|
+
{ name: 'mission.started — Wave-based deploy started', value: 'mission.started' },
|
|
40
|
+
{ name: 'mission.wave.completed — One dependency wave finished', value: 'mission.wave.completed' },
|
|
41
|
+
{ name: 'mission.completed — All waves done', value: 'mission.completed' },
|
|
42
|
+
{ name: 'mission.failed — Mission aborted', value: 'mission.failed' },
|
|
43
|
+
];
|
|
44
|
+
export const LIFECYCLE_CONDITION_FIELDS = {
|
|
45
|
+
deploy: ['stageName', 'projectSlug', 'isFirstDeploy', 'deploymentName'],
|
|
46
|
+
launch: ['stageName', 'projectSlug', 'region'],
|
|
47
|
+
mission: ['stageName', 'waveIndex', 'totalWaves'],
|
|
48
|
+
stage: ['stageName', 'accountId', 'isProduction', 'hasAutoLaunch'],
|
|
49
|
+
};
|
|
50
|
+
export function getActionCategory(type) {
|
|
51
|
+
return SMART_ACTION_TYPES.includes(type) ? 'smart' : 'flex';
|
|
52
|
+
}
|
|
2
53
|
/**
|
|
3
54
|
* Prompt user to select a trigger status from available Jira statuses
|
|
4
55
|
*/
|
|
@@ -121,17 +172,295 @@ export async function promptActionConfig(actionType) {
|
|
|
121
172
|
config.ref = answers.ref;
|
|
122
173
|
return config;
|
|
123
174
|
}
|
|
175
|
+
case 'git-create-branch': {
|
|
176
|
+
const { branchPattern } = await inquirer.prompt([{
|
|
177
|
+
default: '{issueKey}/{summary-slug}',
|
|
178
|
+
message: 'Branch pattern (tokens: {issueKey}, {summary-slug}):',
|
|
179
|
+
name: 'branchPattern',
|
|
180
|
+
type: 'input',
|
|
181
|
+
}]);
|
|
182
|
+
return { branchPattern };
|
|
183
|
+
}
|
|
184
|
+
case 'git-create-mr': {
|
|
185
|
+
const answers = await inquirer.prompt([
|
|
186
|
+
{
|
|
187
|
+
default: '',
|
|
188
|
+
message: 'Target branch (leave empty for repo default):',
|
|
189
|
+
name: 'targetBranch',
|
|
190
|
+
type: 'input',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
default: true,
|
|
194
|
+
message: 'Create as draft?',
|
|
195
|
+
name: 'draft',
|
|
196
|
+
type: 'confirm',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
default: '{issueKey}: {summary}',
|
|
200
|
+
message: 'Title pattern (tokens: {issueKey}, {summary}):',
|
|
201
|
+
name: 'titlePattern',
|
|
202
|
+
type: 'input',
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
const config = { draft: answers.draft, titlePattern: answers.titlePattern };
|
|
206
|
+
if (answers.targetBranch)
|
|
207
|
+
config.targetBranch = answers.targetBranch;
|
|
208
|
+
return config;
|
|
209
|
+
}
|
|
210
|
+
case 'git-delete-branch':
|
|
211
|
+
return {};
|
|
212
|
+
case 'stage-create': {
|
|
213
|
+
const answers = await inquirer.prompt([
|
|
214
|
+
{
|
|
215
|
+
default: '{{issueKey}}',
|
|
216
|
+
message: 'Stage name template (tokens: {{issueKey}}, {{summary}}):',
|
|
217
|
+
name: 'nameTemplate',
|
|
218
|
+
type: 'input',
|
|
219
|
+
validate: (input) => input.includes('{{issueKey}}') || input.includes('{issueKey}')
|
|
220
|
+
? true : 'Must contain {{issueKey}} or {issueKey}',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
message: 'AWS account ID (12-digit):',
|
|
224
|
+
name: 'accountId',
|
|
225
|
+
type: 'input',
|
|
226
|
+
validate: (input) => /^\d{12}$/.test(input.trim()) ? true : 'Must be a 12-digit AWS account ID',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
default: 'sa-east-1',
|
|
230
|
+
message: 'Regions (comma-separated):',
|
|
231
|
+
name: 'regions',
|
|
232
|
+
type: 'input',
|
|
233
|
+
validate: (input) => input.trim() ? true : 'At least one region is required',
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
const config = {
|
|
237
|
+
accountId: answers.accountId.trim(),
|
|
238
|
+
nameTemplate: answers.nameTemplate,
|
|
239
|
+
regions: answers.regions.split(',').map((r) => r.trim()).filter(Boolean),
|
|
240
|
+
};
|
|
241
|
+
return config;
|
|
242
|
+
}
|
|
243
|
+
case 'stage-delete':
|
|
244
|
+
return { stageResolver: 'issueKey' };
|
|
245
|
+
case 'deploy': {
|
|
246
|
+
const answers = await inquirer.prompt([
|
|
247
|
+
{
|
|
248
|
+
default: '',
|
|
249
|
+
message: 'Project slug (leave empty for all modules on stage):',
|
|
250
|
+
name: 'projectSlug',
|
|
251
|
+
type: 'input',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
default: true,
|
|
255
|
+
message: 'Launch deployment immediately?',
|
|
256
|
+
name: 'launch',
|
|
257
|
+
type: 'confirm',
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
const config = { stageResolver: 'issueKey' };
|
|
261
|
+
if (answers.projectSlug.trim())
|
|
262
|
+
config.projectSlug = answers.projectSlug.trim();
|
|
263
|
+
if (!answers.launch)
|
|
264
|
+
config.launch = false;
|
|
265
|
+
return config;
|
|
266
|
+
}
|
|
124
267
|
default:
|
|
125
268
|
throw new Error(`Unknown action type: ${actionType}`);
|
|
126
269
|
}
|
|
127
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Prompt user to select a trigger event type
|
|
273
|
+
*/
|
|
274
|
+
export async function promptTriggerEvent() {
|
|
275
|
+
const { triggerEvent } = await inquirer.prompt([{
|
|
276
|
+
choices: [
|
|
277
|
+
{ name: 'Status transition', value: 'status_transition' },
|
|
278
|
+
{ name: 'Issue created', value: 'issue_created' },
|
|
279
|
+
{ name: 'Issue assigned', value: 'issue_assigned' },
|
|
280
|
+
{ name: 'Field changed', value: 'field_changed' },
|
|
281
|
+
{ name: 'Comment added', value: 'comment_added' },
|
|
282
|
+
{ name: 'Label changed', value: 'label_changed' },
|
|
283
|
+
],
|
|
284
|
+
message: 'Trigger event:',
|
|
285
|
+
name: 'triggerEvent',
|
|
286
|
+
type: 'list',
|
|
287
|
+
}]);
|
|
288
|
+
return triggerEvent;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Prompt for event-specific trigger conditions
|
|
292
|
+
*/
|
|
293
|
+
export async function promptTriggerConditions(event) {
|
|
294
|
+
const conditions = {};
|
|
295
|
+
switch (event) {
|
|
296
|
+
case 'status_transition': {
|
|
297
|
+
const { statusFrom } = await inquirer.prompt([{
|
|
298
|
+
default: '',
|
|
299
|
+
message: 'Status from (optional, leave blank for any):',
|
|
300
|
+
name: 'statusFrom',
|
|
301
|
+
type: 'input',
|
|
302
|
+
}]);
|
|
303
|
+
if (statusFrom && statusFrom.trim() !== '')
|
|
304
|
+
conditions.statusFrom = statusFrom.trim();
|
|
305
|
+
const { statusTo } = await inquirer.prompt([{
|
|
306
|
+
default: '*',
|
|
307
|
+
message: 'Status to (* for any):',
|
|
308
|
+
name: 'statusTo',
|
|
309
|
+
type: 'input',
|
|
310
|
+
}]);
|
|
311
|
+
if (statusTo && statusTo.trim() !== '*')
|
|
312
|
+
conditions.statusTo = statusTo.trim();
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case 'issue_created': {
|
|
316
|
+
const { issueType } = await inquirer.prompt([{
|
|
317
|
+
default: '',
|
|
318
|
+
message: 'Issue type filter (optional, e.g. Bug, Story):',
|
|
319
|
+
name: 'issueType',
|
|
320
|
+
type: 'input',
|
|
321
|
+
}]);
|
|
322
|
+
if (issueType)
|
|
323
|
+
conditions.issueType = issueType;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case 'issue_assigned': {
|
|
327
|
+
const { assignee } = await inquirer.prompt([{
|
|
328
|
+
default: '',
|
|
329
|
+
message: 'Assignee filter (optional):',
|
|
330
|
+
name: 'assignee',
|
|
331
|
+
type: 'input',
|
|
332
|
+
}]);
|
|
333
|
+
if (assignee)
|
|
334
|
+
conditions.assignee = assignee;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'field_changed': {
|
|
338
|
+
const { fieldName } = await inquirer.prompt([{
|
|
339
|
+
message: 'Field name:',
|
|
340
|
+
name: 'fieldName',
|
|
341
|
+
type: 'input',
|
|
342
|
+
validate: (input) => input.trim() ? true : 'Field name is required',
|
|
343
|
+
}]);
|
|
344
|
+
conditions.fieldName = fieldName;
|
|
345
|
+
const { fieldValue } = await inquirer.prompt([{
|
|
346
|
+
default: '',
|
|
347
|
+
message: 'Field value filter (optional):',
|
|
348
|
+
name: 'fieldValue',
|
|
349
|
+
type: 'input',
|
|
350
|
+
}]);
|
|
351
|
+
if (fieldValue)
|
|
352
|
+
conditions.fieldValue = fieldValue;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case 'comment_added': {
|
|
356
|
+
const { pattern } = await inquirer.prompt([{
|
|
357
|
+
default: '',
|
|
358
|
+
message: 'Comment pattern (optional regex, e.g. @hyperdrive deploy):',
|
|
359
|
+
name: 'pattern',
|
|
360
|
+
type: 'input',
|
|
361
|
+
}]);
|
|
362
|
+
if (pattern)
|
|
363
|
+
conditions.pattern = pattern;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case 'label_changed': {
|
|
367
|
+
const { label } = await inquirer.prompt([{
|
|
368
|
+
message: 'Label:',
|
|
369
|
+
name: 'label',
|
|
370
|
+
type: 'input',
|
|
371
|
+
validate: (input) => input.trim() ? true : 'Label is required',
|
|
372
|
+
}]);
|
|
373
|
+
conditions.label = label;
|
|
374
|
+
const { action } = await inquirer.prompt([{
|
|
375
|
+
choices: [
|
|
376
|
+
{ name: 'added', value: 'added' },
|
|
377
|
+
{ name: 'removed', value: 'removed' },
|
|
378
|
+
],
|
|
379
|
+
message: 'Label action:',
|
|
380
|
+
name: 'action',
|
|
381
|
+
type: 'list',
|
|
382
|
+
}]);
|
|
383
|
+
conditions.action = action;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return conditions;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Prompt user to select an action type with category grouping
|
|
391
|
+
*/
|
|
392
|
+
export async function promptActionTypeV2() {
|
|
393
|
+
const { Separator } = inquirer;
|
|
394
|
+
const { actionType } = await inquirer.prompt([{
|
|
395
|
+
choices: [
|
|
396
|
+
new Separator('── Smart Actions ──'),
|
|
397
|
+
{ name: 'Create branches (git-create-branch)', value: 'git-create-branch' },
|
|
398
|
+
{ name: 'Create merge/pull requests (git-create-mr)', value: 'git-create-mr' },
|
|
399
|
+
{ name: 'Delete branches (git-delete-branch)', value: 'git-delete-branch' },
|
|
400
|
+
new Separator('── Native Actions ──'),
|
|
401
|
+
{ name: 'Create stage (stage-create)', value: 'stage-create' },
|
|
402
|
+
{ name: 'Delete stage (stage-delete)', value: 'stage-delete' },
|
|
403
|
+
{ name: 'Deploy (deploy)', value: 'deploy' },
|
|
404
|
+
new Separator('── Flex Hooks ──'),
|
|
405
|
+
{ name: 'Slack notification (slack-notify)', value: 'slack-notify' },
|
|
406
|
+
{ name: 'ADHB enrichment (adhb-enrich)', value: 'adhb-enrich' },
|
|
407
|
+
{ name: 'Webhook (webhook)', value: 'webhook' },
|
|
408
|
+
{ name: 'CI trigger (ci-trigger)', value: 'ci-trigger' },
|
|
409
|
+
],
|
|
410
|
+
message: 'Action type:',
|
|
411
|
+
name: 'actionType',
|
|
412
|
+
type: 'list',
|
|
413
|
+
}]);
|
|
414
|
+
return {
|
|
415
|
+
category: getActionCategory(actionType),
|
|
416
|
+
type: actionType,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Prompt for execution order
|
|
421
|
+
*/
|
|
422
|
+
export async function promptOrder() {
|
|
423
|
+
const { order } = await inquirer.prompt([{
|
|
424
|
+
default: '',
|
|
425
|
+
message: 'Execution order (lower runs first, leave empty for default):',
|
|
426
|
+
name: 'order',
|
|
427
|
+
type: 'input',
|
|
428
|
+
validate: (input) => {
|
|
429
|
+
if (!input.trim())
|
|
430
|
+
return true;
|
|
431
|
+
const num = Number(input);
|
|
432
|
+
if (Number.isInteger(num) && num > 0)
|
|
433
|
+
return true;
|
|
434
|
+
return 'Must be a positive integer or empty';
|
|
435
|
+
},
|
|
436
|
+
}]);
|
|
437
|
+
return order ? Number(order) : undefined;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Format a hook label for display in selection prompts
|
|
441
|
+
*/
|
|
442
|
+
function formatHookLabel(h) {
|
|
443
|
+
// V2 shape: has trigger/action objects
|
|
444
|
+
if ('trigger' in h && h.trigger) {
|
|
445
|
+
const event = h.trigger.event ?? '';
|
|
446
|
+
const conditions = h.trigger.conditions;
|
|
447
|
+
const condStr = conditions && Object.keys(conditions).length > 0
|
|
448
|
+
? Object.entries(conditions).map(([k, v]) => `${k}: ${v}`).join(', ')
|
|
449
|
+
: '*';
|
|
450
|
+
const actionType = h.action?.type ?? '';
|
|
451
|
+
return `[${event}] ${condStr} → ${actionType} (${h.enabled ? 'enabled' : 'disabled'})`;
|
|
452
|
+
}
|
|
453
|
+
// V1 shape: has triggerStatus/actionType
|
|
454
|
+
const v1 = h;
|
|
455
|
+
return `[${v1.triggerStatus}] ${v1.actionType} (${v1.enabled ? 'enabled' : 'disabled'})`;
|
|
456
|
+
}
|
|
128
457
|
/**
|
|
129
458
|
* Prompt user to select a hook from a list
|
|
130
459
|
*/
|
|
131
460
|
export async function promptSelectHook(hooks) {
|
|
132
461
|
const { selectedHook } = await inquirer.prompt([{
|
|
133
462
|
choices: hooks.map(h => ({
|
|
134
|
-
name:
|
|
463
|
+
name: formatHookLabel(h),
|
|
135
464
|
value: h,
|
|
136
465
|
})),
|
|
137
466
|
message: 'Select a hook:',
|
|
@@ -146,9 +475,115 @@ export async function promptSelectHook(hooks) {
|
|
|
146
475
|
export async function promptConfirmDelete(hook) {
|
|
147
476
|
const { confirmed } = await inquirer.prompt([{
|
|
148
477
|
default: false,
|
|
149
|
-
message: `Delete hook
|
|
478
|
+
message: `Delete hook ${formatHookLabel(hook)}?`,
|
|
150
479
|
name: 'confirmed',
|
|
151
480
|
type: 'confirm',
|
|
152
481
|
}]);
|
|
153
482
|
return confirmed;
|
|
154
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Prompt user to choose between lifecycle event trigger and cron trigger
|
|
486
|
+
*/
|
|
487
|
+
export async function promptTriggerType() {
|
|
488
|
+
const { triggerType } = await inquirer.prompt([{
|
|
489
|
+
choices: [
|
|
490
|
+
{ name: 'Lifecycle event — fire on deploy, stage, launch, or mission events', value: 'lifecycle' },
|
|
491
|
+
{ name: 'Cron schedule — fire on a recurring time-based schedule', value: 'cron' },
|
|
492
|
+
],
|
|
493
|
+
message: 'Select trigger type:',
|
|
494
|
+
name: 'triggerType',
|
|
495
|
+
type: 'list',
|
|
496
|
+
}]);
|
|
497
|
+
return triggerType;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Prompt user for a cron expression or rate expression
|
|
501
|
+
*/
|
|
502
|
+
export async function promptCronExpression() {
|
|
503
|
+
const { expression } = await inquirer.prompt([{
|
|
504
|
+
message: 'Cron expression or rate (e.g. "0 3 * * ? *", "rate(1 hour)"):',
|
|
505
|
+
name: 'expression',
|
|
506
|
+
type: 'input',
|
|
507
|
+
validate: (input) => {
|
|
508
|
+
if (!input.trim())
|
|
509
|
+
return 'Expression is required';
|
|
510
|
+
return true;
|
|
511
|
+
},
|
|
512
|
+
}]);
|
|
513
|
+
return expression.trim();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Prompt user for an IANA timezone
|
|
517
|
+
*/
|
|
518
|
+
export async function promptCronTimezone() {
|
|
519
|
+
const { timezone } = await inquirer.prompt([{
|
|
520
|
+
default: 'UTC',
|
|
521
|
+
message: 'Timezone (IANA, e.g. "America/Sao_Paulo", "UTC"):',
|
|
522
|
+
name: 'timezone',
|
|
523
|
+
type: 'input',
|
|
524
|
+
validate: (input) => {
|
|
525
|
+
if (!input.trim())
|
|
526
|
+
return 'Timezone is required';
|
|
527
|
+
return true;
|
|
528
|
+
},
|
|
529
|
+
}]);
|
|
530
|
+
return timezone.trim();
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Prompt user for the flexible time window (optional)
|
|
534
|
+
*/
|
|
535
|
+
export async function promptFlexibleWindow() {
|
|
536
|
+
const { flexWindow } = await inquirer.prompt([{
|
|
537
|
+
default: '',
|
|
538
|
+
message: 'Flexible time window in minutes (default: 15, leave empty to use default):',
|
|
539
|
+
name: 'flexWindow',
|
|
540
|
+
type: 'input',
|
|
541
|
+
validate: (input) => {
|
|
542
|
+
if (!input.trim())
|
|
543
|
+
return true;
|
|
544
|
+
const num = Number(input);
|
|
545
|
+
if (Number.isInteger(num) && num >= 0 && num <= 1440)
|
|
546
|
+
return true;
|
|
547
|
+
return 'Must be an integer between 0 and 1440, or empty for default';
|
|
548
|
+
},
|
|
549
|
+
}]);
|
|
550
|
+
return flexWindow ? Number(flexWindow) : undefined;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Prompt user to select a lifecycle event for tenant hooks
|
|
554
|
+
*/
|
|
555
|
+
export async function promptLifecycleEvent() {
|
|
556
|
+
const { event } = await inquirer.prompt([{
|
|
557
|
+
choices: LIFECYCLE_EVENT_GROUPS,
|
|
558
|
+
message: 'Select lifecycle event:',
|
|
559
|
+
name: 'event',
|
|
560
|
+
type: 'list',
|
|
561
|
+
}]);
|
|
562
|
+
return event;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Prompt for lifecycle event-specific conditions
|
|
566
|
+
*
|
|
567
|
+
* Extracts group prefix from the event (e.g., deploy.completed → deploy)
|
|
568
|
+
* and prompts for each field in LIFECYCLE_CONDITION_FIELDS[group].
|
|
569
|
+
* Returns only non-empty values.
|
|
570
|
+
*/
|
|
571
|
+
export async function promptLifecycleConditions(event) {
|
|
572
|
+
const group = event.split('.')[0];
|
|
573
|
+
const fields = LIFECYCLE_CONDITION_FIELDS[group];
|
|
574
|
+
if (!fields || fields.length === 0)
|
|
575
|
+
return {};
|
|
576
|
+
const conditions = {};
|
|
577
|
+
for (const field of fields) {
|
|
578
|
+
const answer = await inquirer.prompt([{
|
|
579
|
+
default: '',
|
|
580
|
+
message: `${field} (leave empty to skip):`,
|
|
581
|
+
name: field,
|
|
582
|
+
type: 'input',
|
|
583
|
+
}]);
|
|
584
|
+
if (answer[field]?.trim()) {
|
|
585
|
+
conditions[field] = answer[field].trim();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return conditions;
|
|
589
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TrackerHookResponseV2 } from '../services/hyperdrive-sigv4.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a hook response to V2 shape.
|
|
4
|
+
* Handles both V1 (triggerStatus/actionType/actionConfig) and V2 (trigger/action) formats.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeHookToV2(hook: Record<string, any>): TrackerHookResponseV2;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getActionCategory } from './hook-flow.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a hook response to V2 shape.
|
|
4
|
+
* Handles both V1 (triggerStatus/actionType/actionConfig) and V2 (trigger/action) formats.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeHookToV2(hook) {
|
|
7
|
+
// Already V2 — has trigger and action objects
|
|
8
|
+
if (hook.trigger && hook.action) {
|
|
9
|
+
return hook;
|
|
10
|
+
}
|
|
11
|
+
// V1 → V2 conversion
|
|
12
|
+
const triggerStatus = hook.triggerStatus ?? '*';
|
|
13
|
+
const actionType = hook.actionType ?? '';
|
|
14
|
+
const actionConfig = hook.actionConfig ?? {};
|
|
15
|
+
return {
|
|
16
|
+
action: {
|
|
17
|
+
category: getActionCategory(actionType) ?? 'flex',
|
|
18
|
+
config: actionConfig,
|
|
19
|
+
type: actionType,
|
|
20
|
+
},
|
|
21
|
+
createdAt: hook.createdAt,
|
|
22
|
+
enabled: hook.enabled,
|
|
23
|
+
hookId: hook.hookId,
|
|
24
|
+
order: hook.order,
|
|
25
|
+
trackerProjectId: hook.trackerProjectId,
|
|
26
|
+
trigger: {
|
|
27
|
+
conditions: triggerStatus === '*' ? {} : { statusTo: triggerStatus },
|
|
28
|
+
event: 'status_transition',
|
|
29
|
+
source: 'jira',
|
|
30
|
+
},
|
|
31
|
+
updatedAt: hook.updatedAt ?? hook.createdAt,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface PollerOptions<T> {
|
|
2
|
+
/** Function to call on each poll cycle */
|
|
3
|
+
pollFn: () => Promise<T>;
|
|
4
|
+
/** Extract the status string from the polled entity */
|
|
5
|
+
getStatus: (entity: T) => string;
|
|
6
|
+
/** Set of status values that mean the operation finished (success or failure) */
|
|
7
|
+
terminalStates: Set<string>;
|
|
8
|
+
/** Set of status values that indicate success */
|
|
9
|
+
successStates: Set<string>;
|
|
10
|
+
/** Optional: extract a phase string (for clone multi-phase display) */
|
|
11
|
+
getPhase?: (entity: T) => string | undefined;
|
|
12
|
+
/** Optional: extract an error message on failure */
|
|
13
|
+
getErrorMessage?: (entity: T) => string | undefined;
|
|
14
|
+
/** Label shown in spinner (e.g. "Backing up", "Restoring") */
|
|
15
|
+
operationLabel: string;
|
|
16
|
+
/** Poll interval in ms (default 10_000) */
|
|
17
|
+
intervalMs?: number;
|
|
18
|
+
/** Max timeout in ms (default 30 minutes) */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PollerResult<T> {
|
|
22
|
+
entity: T;
|
|
23
|
+
success: boolean;
|
|
24
|
+
timedOut: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generic lifecycle polling utility for async Hyperdrive operations.
|
|
28
|
+
*
|
|
29
|
+
* Polls a status endpoint at a fixed interval, displaying an ora spinner
|
|
30
|
+
* with phase and elapsed time until a terminal state is reached or timeout.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pollLifecycle<T>(options: PollerOptions<T>, isJson: boolean): Promise<PollerResult<T>>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
const CLONE_PHASE_LABELS = {
|
|
4
|
+
'completed': 'Completed',
|
|
5
|
+
'credential-rotation': 'Rotating credentials',
|
|
6
|
+
'failed': 'Failed',
|
|
7
|
+
'registering': 'Registering new service',
|
|
8
|
+
'restoring': 'Restoring from snapshot',
|
|
9
|
+
'snapshot-creating': 'Creating snapshot',
|
|
10
|
+
'snapshot-sharing': 'Sharing snapshot cross-account',
|
|
11
|
+
};
|
|
12
|
+
function formatElapsed(startTime) {
|
|
13
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
14
|
+
const mins = Math.floor(elapsed / 60);
|
|
15
|
+
const secs = elapsed % 60;
|
|
16
|
+
return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generic lifecycle polling utility for async Hyperdrive operations.
|
|
20
|
+
*
|
|
21
|
+
* Polls a status endpoint at a fixed interval, displaying an ora spinner
|
|
22
|
+
* with phase and elapsed time until a terminal state is reached or timeout.
|
|
23
|
+
*/
|
|
24
|
+
export async function pollLifecycle(options, isJson) {
|
|
25
|
+
const { pollFn, getStatus, terminalStates, successStates, getPhase, getErrorMessage, operationLabel, intervalMs = 10_000, timeoutMs = 30 * 60 * 1000, } = options;
|
|
26
|
+
const spinner = isJson ? null : ora(`${operationLabel}...`).start();
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
30
|
+
try {
|
|
31
|
+
const entity = await pollFn();
|
|
32
|
+
const status = getStatus(entity);
|
|
33
|
+
const elapsed = formatElapsed(startTime);
|
|
34
|
+
if (terminalStates.has(status)) {
|
|
35
|
+
const isSuccess = successStates.has(status);
|
|
36
|
+
if (isSuccess) {
|
|
37
|
+
spinner?.succeed(chalk.green(`${operationLabel} completed (${elapsed})`));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
spinner?.fail(chalk.red(`${operationLabel} failed (${elapsed})`));
|
|
41
|
+
const errorMsg = getErrorMessage?.(entity);
|
|
42
|
+
if (errorMsg && !isJson) {
|
|
43
|
+
// Print error on a new line under the spinner
|
|
44
|
+
process.stderr.write(chalk.red(` Reason: ${errorMsg}\n`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { entity, success: isSuccess, timedOut: false };
|
|
48
|
+
}
|
|
49
|
+
// Still in progress — update spinner with phase + elapsed
|
|
50
|
+
const phase = getPhase?.(entity);
|
|
51
|
+
const phaseLabel = phase ? (CLONE_PHASE_LABELS[phase] || phase) : status;
|
|
52
|
+
spinner?.start(`${operationLabel}... ${chalk.gray(`(${phaseLabel} — ${elapsed})`)}`);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Transient error — keep polling
|
|
56
|
+
const elapsed = formatElapsed(startTime);
|
|
57
|
+
spinner?.start(`${operationLabel}... ${chalk.gray(`(checking — ${elapsed})`)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Timeout
|
|
61
|
+
const elapsed = formatElapsed(startTime);
|
|
62
|
+
spinner?.warn(chalk.yellow(`${operationLabel} timed out (${elapsed})`));
|
|
63
|
+
// Return a best-effort result — poll one last time
|
|
64
|
+
try {
|
|
65
|
+
const entity = await pollFn();
|
|
66
|
+
return { entity, success: false, timedOut: true };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Can't even fetch final state — return a synthetic timeout result
|
|
70
|
+
return { entity: undefined, success: false, timedOut: true };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff for transient upload failures.
|
|
3
|
+
*
|
|
4
|
+
* Classifies errors as retryable (network errors, 429, 5xx) vs permanent
|
|
5
|
+
* (4xx except 429) and only retries transient failures.
|
|
6
|
+
*/
|
|
7
|
+
export interface RetryConfig {
|
|
8
|
+
/** Multiplier for exponential backoff (default: 2) */
|
|
9
|
+
backoffMultiplier?: number;
|
|
10
|
+
/** Initial delay in milliseconds (default: 1000) */
|
|
11
|
+
baseDelayMs?: number;
|
|
12
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RetryResult<T> {
|
|
16
|
+
/** Number of attempts made (1 = no retries needed) */
|
|
17
|
+
attempts: number;
|
|
18
|
+
/** The result if successful */
|
|
19
|
+
result: T;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check whether an error is transient and should be retried.
|
|
23
|
+
*
|
|
24
|
+
* Retryable:
|
|
25
|
+
* - Network errors: ECONNRESET, ETIMEDOUT, EPIPE, ENOTFOUND, etc.
|
|
26
|
+
* - HTTP 429 (rate limited)
|
|
27
|
+
* - HTTP 5xx (server errors)
|
|
28
|
+
*
|
|
29
|
+
* Non-retryable:
|
|
30
|
+
* - HTTP 4xx except 429 (client errors — bad URL, expired signature, etc.)
|
|
31
|
+
*/
|
|
32
|
+
export declare function isRetryableError(error: unknown): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Execute an async function with exponential backoff retry.
|
|
35
|
+
*
|
|
36
|
+
* @param fn - Async function to execute. Should throw or return a failed Response on failure.
|
|
37
|
+
* @param config - Retry configuration
|
|
38
|
+
* @returns The successful result and attempt count
|
|
39
|
+
* @throws The last error after all retries are exhausted
|
|
40
|
+
*/
|
|
41
|
+
export declare function retryWithBackoff<T>(fn: () => Promise<T>, config?: RetryConfig): Promise<RetryResult<T>>;
|
|
42
|
+
/** Awaitable sleep helper (exported for testing) */
|
|
43
|
+
export declare function sleep(ms: number): Promise<void>;
|