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