@hyperdrive.bot/cli 1.0.6 → 1.0.8
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 +415 -67
- package/dist/commands/account/add.d.ts +6 -6
- package/dist/commands/account/list.d.ts +3 -0
- package/dist/commands/account/list.js +9 -2
- package/dist/commands/account/remove.d.ts +3 -3
- package/dist/commands/auth/login.d.ts +4 -4
- package/dist/commands/auth/login.js +1 -0
- package/dist/commands/ci/account/create.d.ts +7 -6
- package/dist/commands/ci/account/create.js +49 -3
- package/dist/commands/ci/account/delete.d.ts +3 -3
- package/dist/commands/ci/account/list.d.ts +2 -2
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/set.d.ts +2 -2
- package/dist/commands/deployment/create.d.ts +10 -10
- package/dist/commands/deployment/get.d.ts +4 -4
- package/dist/commands/deployment/launch.d.ts +6 -6
- package/dist/commands/deployment/list.d.ts +3 -3
- package/dist/commands/deployment/list.js +17 -17
- package/dist/commands/domain/switch.d.ts +1 -1
- package/dist/commands/example.d.ts +3 -3
- package/dist/commands/git/connect.d.ts +2 -2
- package/dist/commands/git/connect.js +1 -0
- package/dist/commands/git/disconnect.d.ts +3 -3
- package/dist/commands/git/list.d.ts +2 -2
- package/dist/commands/git/sync.d.ts +7 -7
- package/dist/commands/git/sync.js +24 -23
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +20 -19
- package/dist/commands/jira/connect.d.ts +2 -1
- package/dist/commands/jira/connect.js +17 -6
- package/dist/commands/jira/hook/add.d.ts +17 -0
- package/dist/commands/jira/hook/add.js +147 -0
- package/dist/commands/jira/hook/list.d.ts +14 -0
- package/dist/commands/jira/hook/list.js +105 -0
- package/dist/commands/jira/hook/remove.d.ts +15 -0
- package/dist/commands/jira/hook/remove.js +119 -0
- package/dist/commands/jira/hook/toggle.d.ts +15 -0
- package/dist/commands/jira/hook/toggle.js +136 -0
- package/dist/commands/jira/status.d.ts +1 -1
- package/dist/commands/module/analyze.d.ts +5 -5
- package/dist/commands/module/create.d.ts +17 -17
- package/dist/commands/module/create.js +9 -1
- package/dist/commands/module/destroy.d.ts +3 -3
- package/dist/commands/module/get.d.ts +2 -2
- package/dist/commands/module/link.d.ts +4 -4
- package/dist/commands/module/list.d.ts +1 -1
- package/dist/commands/module/list.js +12 -11
- package/dist/commands/module/reanalyze.d.ts +6 -6
- package/dist/commands/module/update.d.ts +19 -19
- package/dist/commands/parameter/add.d.ts +7 -7
- package/dist/commands/parameter/backfill.d.ts +4 -4
- package/dist/commands/parameter/backfill.js +4 -3
- package/dist/commands/parameter/clear.d.ts +6 -6
- package/dist/commands/parameter/list.d.ts +6 -6
- package/dist/commands/parameter/list.js +4 -3
- package/dist/commands/parameter/pull.d.ts +6 -6
- package/dist/commands/parameter/remove.d.ts +7 -7
- package/dist/commands/parameter/sync.d.ts +6 -6
- package/dist/commands/parameter/update.d.ts +7 -7
- package/dist/commands/project/init.d.ts +21 -0
- package/dist/commands/project/init.js +576 -0
- package/dist/commands/project/list.d.ts +10 -0
- package/dist/commands/project/list.js +119 -0
- package/dist/commands/project/status.d.ts +13 -0
- package/dist/commands/project/status.js +163 -0
- package/dist/commands/project/sync.d.ts +26 -0
- package/dist/commands/project/sync.js +388 -0
- package/dist/commands/stage/access.d.ts +15 -0
- package/dist/commands/stage/access.js +130 -0
- package/dist/commands/stage/create.d.ts +11 -11
- package/dist/commands/stage/list.d.ts +1 -1
- package/dist/commands/stage/list.js +21 -20
- package/dist/commands/stage/revoke.d.ts +18 -0
- package/dist/commands/stage/revoke.js +171 -0
- package/dist/commands/stage/share.d.ts +23 -0
- package/dist/commands/stage/share.js +292 -0
- package/dist/commands/test-api.d.ts +1 -1
- package/dist/services/auth-service.d.ts +15 -82
- package/dist/services/auth-service.js +24 -237
- package/dist/services/hyperdrive-sigv4.d.ts +162 -24
- package/dist/services/hyperdrive-sigv4.js +107 -193
- package/dist/services/tenant-service.d.ts +6 -0
- package/dist/services/tenant-service.js +13 -0
- package/dist/utils/account-flow.d.ts +2 -2
- package/dist/utils/account-flow.js +4 -4
- package/dist/utils/auth-flow.d.ts +1 -0
- package/dist/utils/auth-flow.js +2 -0
- package/dist/utils/git-flow.d.ts +1 -0
- package/dist/utils/git-flow.js +2 -2
- package/dist/utils/hook-flow.d.ts +21 -0
- package/dist/utils/hook-flow.js +154 -0
- package/dist/utils/jira-flow.d.ts +2 -2
- package/dist/utils/jira-flow.js +4 -4
- package/dist/utils/table.d.ts +17 -0
- package/dist/utils/table.js +41 -0
- package/oclif.manifest.json +844 -154
- package/package.json +59 -15
|
@@ -0,0 +1,576 @@
|
|
|
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
|
+
const NORMALIZED_WORKFLOW_STATES = [
|
|
7
|
+
'pending',
|
|
8
|
+
'enriching',
|
|
9
|
+
'implementing',
|
|
10
|
+
'testing',
|
|
11
|
+
'in_review',
|
|
12
|
+
'ready_to_merge',
|
|
13
|
+
'deploying',
|
|
14
|
+
'validating',
|
|
15
|
+
'done',
|
|
16
|
+
'blocked',
|
|
17
|
+
];
|
|
18
|
+
export default class ProjectInit extends Command {
|
|
19
|
+
static description = 'Initialize a project with Jira integration, repos, and status mapping';
|
|
20
|
+
static examples = [
|
|
21
|
+
'<%= config.bin %> project init',
|
|
22
|
+
'<%= config.bin %> project init --project my-project --jira-key PROJ',
|
|
23
|
+
'<%= config.bin %> project init --json --name "My Project" --jira-key PROJ',
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
domain: Flags.string({
|
|
27
|
+
char: 'd',
|
|
28
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
29
|
+
}),
|
|
30
|
+
'jira-key': Flags.string({
|
|
31
|
+
description: 'Jira project key (e.g., PROJ)',
|
|
32
|
+
}),
|
|
33
|
+
json: Flags.boolean({
|
|
34
|
+
description: 'Output result as JSON (non-interactive mode)',
|
|
35
|
+
}),
|
|
36
|
+
name: Flags.string({
|
|
37
|
+
description: 'Name for a new project (used with --json)',
|
|
38
|
+
}),
|
|
39
|
+
project: Flags.string({
|
|
40
|
+
description: 'Existing project slug to configure',
|
|
41
|
+
}),
|
|
42
|
+
repo: Flags.string({
|
|
43
|
+
description: 'Repo in format "name|gitRemote|provider|branch" (repeatable, used with --json)',
|
|
44
|
+
multiple: true,
|
|
45
|
+
}),
|
|
46
|
+
'status-map': Flags.string({
|
|
47
|
+
description: 'Status mapping in format "JiraStatus=normalizedState" (repeatable, used with --json)',
|
|
48
|
+
multiple: true,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
async run() {
|
|
52
|
+
const { flags } = await this.parse(ProjectInit);
|
|
53
|
+
// JSON mode: validate required flags upfront
|
|
54
|
+
if (flags.json) {
|
|
55
|
+
if (!flags.project && !flags.name) {
|
|
56
|
+
this.error('Non-interactive mode requires --project (existing) or --name (new project).\n\n' +
|
|
57
|
+
`Usage: ${chalk.cyan('hd project init --json --name "My Project" --jira-key PROJ')}\n` +
|
|
58
|
+
` or: ${chalk.cyan('hd project init --json --project my-project --jira-key PROJ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (!flags['jira-key']) {
|
|
61
|
+
this.error('Non-interactive mode requires --jira-key.\n\n' +
|
|
62
|
+
`Usage: ${chalk.cyan('hd project init --json --name "My Project" --jira-key PROJ')}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Step: Auth check
|
|
66
|
+
let service;
|
|
67
|
+
const spinner = ora('Checking authentication...').start();
|
|
68
|
+
try {
|
|
69
|
+
service = new HyperdriveSigV4Service(flags.domain);
|
|
70
|
+
spinner.succeed('Authenticated');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
spinner.fail('Not authenticated');
|
|
74
|
+
this.error(`${error.message}\n\nPlease authenticate first with: ${chalk.cyan('hd auth login')}`);
|
|
75
|
+
}
|
|
76
|
+
// Step (a): Project selection or creation
|
|
77
|
+
const project = await this.selectOrCreateProject(service, flags);
|
|
78
|
+
// Step (b): Jira project key
|
|
79
|
+
const jiraKey = await this.getJiraProjectKey(service, flags);
|
|
80
|
+
// Step (c): Repo addition
|
|
81
|
+
const repos = await this.addRepos(service, project, flags);
|
|
82
|
+
// Step (d): Status mapping
|
|
83
|
+
const statusMapping = await this.configureStatusMapping(service, project, jiraKey, flags);
|
|
84
|
+
// Step (e): Confirmation and save
|
|
85
|
+
const result = {
|
|
86
|
+
jiraProjectKey: jiraKey,
|
|
87
|
+
project: { name: project.name, projectId: project.projectId, slug: project.slug },
|
|
88
|
+
repos: repos.map(r => ({ defaultBranch: r.defaultBranch, gitProvider: r.gitProvider, gitRemote: r.gitRemote, name: r.name })),
|
|
89
|
+
statusMapping,
|
|
90
|
+
};
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
this.log(JSON.stringify(result, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await this.confirmAndFinalize(result);
|
|
96
|
+
}
|
|
97
|
+
async addRepos(service, project, flags) {
|
|
98
|
+
const repos = [];
|
|
99
|
+
if (flags.json) {
|
|
100
|
+
if (!flags.repo || flags.repo.length === 0)
|
|
101
|
+
return repos;
|
|
102
|
+
for (const repoStr of flags.repo) {
|
|
103
|
+
const parts = repoStr.split('|');
|
|
104
|
+
if (parts.length < 2) {
|
|
105
|
+
this.error(`Invalid --repo format: "${repoStr}". Expected "name|gitRemote|provider|branch" (provider defaults to github, branch defaults to main)`);
|
|
106
|
+
}
|
|
107
|
+
const [name, gitRemote, gitProvider = 'github', defaultBranch = 'main'] = parts;
|
|
108
|
+
const repoSpinner = ora(`Adding repository ${name}...`).start();
|
|
109
|
+
try {
|
|
110
|
+
const result = await service.projectAddRepo(project.projectId, {
|
|
111
|
+
defaultBranch: defaultBranch.trim(),
|
|
112
|
+
gitProvider: gitProvider.trim(),
|
|
113
|
+
gitRemote: gitRemote.trim(),
|
|
114
|
+
name: name.trim(),
|
|
115
|
+
});
|
|
116
|
+
repoSpinner.succeed(`Repository ${chalk.cyan(name)} added`);
|
|
117
|
+
repos.push(result);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
repoSpinner.fail(`Failed to add repository ${name}`);
|
|
121
|
+
const errorMsg = error.response?.data?.message || error.response?.data?.error || error.message;
|
|
122
|
+
this.error(errorMsg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return repos;
|
|
126
|
+
}
|
|
127
|
+
this.log('');
|
|
128
|
+
this.log(chalk.blue('Step 3: Add Repositories'));
|
|
129
|
+
this.log(chalk.dim('Link git repositories to this project'));
|
|
130
|
+
this.log('');
|
|
131
|
+
const { addRepo } = await inquirer.prompt([{
|
|
132
|
+
default: true,
|
|
133
|
+
message: 'Would you like to add a repository?',
|
|
134
|
+
name: 'addRepo',
|
|
135
|
+
type: 'confirm',
|
|
136
|
+
}]);
|
|
137
|
+
if (!addRepo)
|
|
138
|
+
return repos;
|
|
139
|
+
let addMore = true;
|
|
140
|
+
while (addMore) {
|
|
141
|
+
const { gitRemote } = await inquirer.prompt([{
|
|
142
|
+
message: 'Git remote URL:',
|
|
143
|
+
name: 'gitRemote',
|
|
144
|
+
type: 'input',
|
|
145
|
+
validate: (input) => {
|
|
146
|
+
if (!input.trim())
|
|
147
|
+
return 'Git remote URL is required';
|
|
148
|
+
if (!/^https?:\/\/(github\.com|gitlab\.com)\/.+\/.+/.test(input.trim()) &&
|
|
149
|
+
!/^git@(github\.com|gitlab\.com):.+\/.+/.test(input.trim())) {
|
|
150
|
+
return 'URL must be a valid GitHub or GitLab repository URL (e.g., https://github.com/org/repo)';
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
},
|
|
154
|
+
}]);
|
|
155
|
+
const derivedName = this.deriveRepoName(gitRemote);
|
|
156
|
+
const { repoName } = await inquirer.prompt([{
|
|
157
|
+
default: derivedName,
|
|
158
|
+
message: 'Repo name:',
|
|
159
|
+
name: 'repoName',
|
|
160
|
+
type: 'input',
|
|
161
|
+
validate: (input) => input.trim() ? true : 'Repo name is required',
|
|
162
|
+
}]);
|
|
163
|
+
const { gitProvider } = await inquirer.prompt([{
|
|
164
|
+
choices: [
|
|
165
|
+
{ name: 'GitHub', value: 'github' },
|
|
166
|
+
{ name: 'GitLab', value: 'gitlab' },
|
|
167
|
+
],
|
|
168
|
+
default: gitRemote.includes('github.com') ? 'github' : 'gitlab',
|
|
169
|
+
message: 'Git provider:',
|
|
170
|
+
name: 'gitProvider',
|
|
171
|
+
type: 'list',
|
|
172
|
+
}]);
|
|
173
|
+
const { defaultBranch } = await inquirer.prompt([{
|
|
174
|
+
default: 'main',
|
|
175
|
+
message: 'Default branch:',
|
|
176
|
+
name: 'defaultBranch',
|
|
177
|
+
type: 'input',
|
|
178
|
+
}]);
|
|
179
|
+
const repoSpinner = ora('Adding repository...').start();
|
|
180
|
+
try {
|
|
181
|
+
const result = await service.projectAddRepo(project.projectId, {
|
|
182
|
+
defaultBranch,
|
|
183
|
+
gitProvider,
|
|
184
|
+
gitRemote: gitRemote.trim(),
|
|
185
|
+
name: repoName.trim(),
|
|
186
|
+
});
|
|
187
|
+
repoSpinner.succeed(`Repository ${chalk.cyan(repoName)} added`);
|
|
188
|
+
repos.push(result);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
repoSpinner.fail('Failed to add repository');
|
|
192
|
+
const errorMsg = error.response?.data?.message || error.response?.data?.error || error.message;
|
|
193
|
+
this.log(chalk.red(` Error: ${errorMsg}`));
|
|
194
|
+
const { action } = await inquirer.prompt([{
|
|
195
|
+
choices: [
|
|
196
|
+
{ name: 'Re-enter repo details', value: 'retry' },
|
|
197
|
+
{ name: 'Skip this repo', value: 'skip' },
|
|
198
|
+
],
|
|
199
|
+
message: 'What would you like to do?',
|
|
200
|
+
name: 'action',
|
|
201
|
+
type: 'list',
|
|
202
|
+
}]);
|
|
203
|
+
if (action === 'retry')
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const { addAnother } = await inquirer.prompt([{
|
|
207
|
+
default: false,
|
|
208
|
+
message: 'Add another repository?',
|
|
209
|
+
name: 'addAnother',
|
|
210
|
+
type: 'confirm',
|
|
211
|
+
}]);
|
|
212
|
+
addMore = addAnother;
|
|
213
|
+
}
|
|
214
|
+
return repos;
|
|
215
|
+
}
|
|
216
|
+
async configureStatusMapping(service, project, jiraKey, flags) {
|
|
217
|
+
const statusMapping = {};
|
|
218
|
+
if (flags.json) {
|
|
219
|
+
// Parse --status-map flags if provided
|
|
220
|
+
if (flags['status-map']) {
|
|
221
|
+
for (const mapStr of flags['status-map']) {
|
|
222
|
+
const eqIdx = mapStr.indexOf('=');
|
|
223
|
+
if (eqIdx === -1) {
|
|
224
|
+
this.error(`Invalid --status-map format: "${mapStr}". Expected "JiraStatus=normalizedState"`);
|
|
225
|
+
}
|
|
226
|
+
const jiraStatus = mapStr.slice(0, eqIdx).trim();
|
|
227
|
+
const normalizedState = mapStr.slice(eqIdx + 1).trim();
|
|
228
|
+
if (!NORMALIZED_WORKFLOW_STATES.includes(normalizedState)) {
|
|
229
|
+
this.error(`Invalid normalized state "${normalizedState}". Valid states: ${NORMALIZED_WORKFLOW_STATES.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
statusMapping[jiraStatus] = normalizedState;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const saveSpinner = ora('Saving Jira configuration...').start();
|
|
235
|
+
try {
|
|
236
|
+
await service.projectSetJiraConfig(project.projectId, {
|
|
237
|
+
jiraProjectKey: jiraKey,
|
|
238
|
+
statusMapping,
|
|
239
|
+
});
|
|
240
|
+
saveSpinner.succeed('Jira configuration saved');
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
saveSpinner.fail('Failed to save Jira configuration');
|
|
244
|
+
const errorMsg = error.response?.data?.message || error.response?.data?.error || error.message;
|
|
245
|
+
this.error(errorMsg);
|
|
246
|
+
}
|
|
247
|
+
return statusMapping;
|
|
248
|
+
}
|
|
249
|
+
this.log('');
|
|
250
|
+
this.log(chalk.blue('Step 4: Status Mapping'));
|
|
251
|
+
this.log(chalk.dim('Map Jira statuses to normalized workflow states'));
|
|
252
|
+
this.log('');
|
|
253
|
+
// Fetch Jira statuses
|
|
254
|
+
const statusSpinner = ora('Fetching Jira statuses...').start();
|
|
255
|
+
let jiraStatuses = [];
|
|
256
|
+
try {
|
|
257
|
+
const response = await service.jiraGetProjectStatuses(jiraKey);
|
|
258
|
+
jiraStatuses = response.statuses;
|
|
259
|
+
statusSpinner.succeed(`Found ${jiraStatuses.length} Jira statuses`);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
statusSpinner.warn('Could not fetch Jira statuses');
|
|
263
|
+
this.log(chalk.yellow(' Jira may not be connected or the project key may be invalid'));
|
|
264
|
+
if (error.message) {
|
|
265
|
+
this.log(chalk.dim(` Detail: ${error.message}`));
|
|
266
|
+
}
|
|
267
|
+
const { action } = await inquirer.prompt([{
|
|
268
|
+
choices: [
|
|
269
|
+
{ name: 'Enter status names manually', value: 'manual' },
|
|
270
|
+
{ name: 'Skip status mapping', value: 'skip' },
|
|
271
|
+
],
|
|
272
|
+
message: 'How would you like to proceed?',
|
|
273
|
+
name: 'action',
|
|
274
|
+
type: 'list',
|
|
275
|
+
}]);
|
|
276
|
+
if (action === 'skip') {
|
|
277
|
+
// Save config without status mapping
|
|
278
|
+
const saveSpinner = ora('Saving Jira configuration...').start();
|
|
279
|
+
try {
|
|
280
|
+
await service.projectSetJiraConfig(project.projectId, {
|
|
281
|
+
jiraProjectKey: jiraKey,
|
|
282
|
+
statusMapping,
|
|
283
|
+
});
|
|
284
|
+
saveSpinner.succeed('Jira configuration saved (no status mapping)');
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
saveSpinner.fail('Failed to save Jira configuration');
|
|
288
|
+
const errorMsg = error.response?.data?.message || error.response?.data?.error || error.message;
|
|
289
|
+
this.log(chalk.red(` Error: ${errorMsg}`));
|
|
290
|
+
}
|
|
291
|
+
return statusMapping;
|
|
292
|
+
}
|
|
293
|
+
// Manual entry
|
|
294
|
+
const { manualStatuses } = await inquirer.prompt([{
|
|
295
|
+
message: 'Enter Jira status names (comma-separated):',
|
|
296
|
+
name: 'manualStatuses',
|
|
297
|
+
type: 'input',
|
|
298
|
+
validate: (input) => input.trim() ? true : 'Enter at least one status name',
|
|
299
|
+
}]);
|
|
300
|
+
jiraStatuses = manualStatuses.split(',').map((s, i) => ({
|
|
301
|
+
id: String(i + 1),
|
|
302
|
+
name: s.trim(),
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
// Map each Jira status to a normalized state
|
|
306
|
+
const stateChoices = [
|
|
307
|
+
...NORMALIZED_WORKFLOW_STATES.map(s => ({ name: s, value: s })),
|
|
308
|
+
{ name: "Skip (don't map)", value: '__skip__' },
|
|
309
|
+
];
|
|
310
|
+
for (const jiraStatus of jiraStatuses) {
|
|
311
|
+
const { mapped } = await inquirer.prompt([{
|
|
312
|
+
choices: stateChoices,
|
|
313
|
+
message: `Map "${chalk.cyan(jiraStatus.name)}" to:`,
|
|
314
|
+
name: 'mapped',
|
|
315
|
+
type: 'list',
|
|
316
|
+
}]);
|
|
317
|
+
if (mapped !== '__skip__') {
|
|
318
|
+
statusMapping[jiraStatus.name] = mapped;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Save Jira config
|
|
322
|
+
const saveSpinner = ora('Saving Jira configuration...').start();
|
|
323
|
+
try {
|
|
324
|
+
await service.projectSetJiraConfig(project.projectId, {
|
|
325
|
+
jiraProjectKey: jiraKey,
|
|
326
|
+
statusMapping,
|
|
327
|
+
});
|
|
328
|
+
saveSpinner.succeed('Jira configuration saved');
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
saveSpinner.fail('Failed to save Jira configuration');
|
|
332
|
+
const errorMsg = error.response?.data?.message || error.response?.data?.error || error.message;
|
|
333
|
+
this.log(chalk.red(` Error: ${errorMsg}`));
|
|
334
|
+
}
|
|
335
|
+
return statusMapping;
|
|
336
|
+
}
|
|
337
|
+
async confirmAndFinalize(result) {
|
|
338
|
+
this.log('');
|
|
339
|
+
this.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
340
|
+
this.log(chalk.blue.bold(' Project Configuration Summary'));
|
|
341
|
+
this.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
342
|
+
this.log('');
|
|
343
|
+
this.log(` Project: ${chalk.cyan(result.project.name)} (${result.project.slug})`);
|
|
344
|
+
this.log(` Jira Key: ${chalk.cyan(result.jiraProjectKey)}`);
|
|
345
|
+
this.log(` Repos: ${chalk.cyan(String(result.repos.length))}`);
|
|
346
|
+
if (result.repos.length > 0) {
|
|
347
|
+
for (const repo of result.repos) {
|
|
348
|
+
this.log(` - ${repo.name} (${repo.gitRemote})`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const mappedCount = Object.keys(result.statusMapping).length;
|
|
352
|
+
this.log(` Mapped: ${chalk.cyan(String(mappedCount))} status${mappedCount !== 1 ? 'es' : ''}`);
|
|
353
|
+
if (mappedCount > 0) {
|
|
354
|
+
for (const [jiraStatus, normalizedState] of Object.entries(result.statusMapping)) {
|
|
355
|
+
this.log(` ${jiraStatus} -> ${normalizedState}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this.log('');
|
|
359
|
+
const { confirmed } = await inquirer.prompt([{
|
|
360
|
+
default: true,
|
|
361
|
+
message: 'Configuration complete. Continue?',
|
|
362
|
+
name: 'confirmed',
|
|
363
|
+
type: 'confirm',
|
|
364
|
+
}]);
|
|
365
|
+
if (!confirmed) {
|
|
366
|
+
this.log(chalk.yellow('Setup cancelled. Your repos and Jira config were already saved during the wizard.'));
|
|
367
|
+
this.log(chalk.dim('You can modify them using the API or re-run this command.'));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.log('');
|
|
371
|
+
this.log(chalk.green('Project initialized successfully!'));
|
|
372
|
+
this.log('');
|
|
373
|
+
this.log(chalk.dim('Next steps:'));
|
|
374
|
+
this.log(` ${chalk.cyan(`hd project status ${result.project.slug}`)} — View project status`);
|
|
375
|
+
this.log(` ${chalk.cyan(`hd project sync ${result.project.slug}`)} — Sync project with Jira`);
|
|
376
|
+
this.log('');
|
|
377
|
+
}
|
|
378
|
+
deriveRepoName(gitRemote) {
|
|
379
|
+
try {
|
|
380
|
+
const url = gitRemote.replace(/\.git$/, '');
|
|
381
|
+
const parts = url.split('/');
|
|
382
|
+
return parts[parts.length - 1] || 'repo';
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return 'repo';
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async getJiraProjectKey(service, flags) {
|
|
389
|
+
if (flags.json && flags['jira-key']) {
|
|
390
|
+
// Validate uniqueness in JSON mode
|
|
391
|
+
const checkSpinner = ora('Checking Jira key availability...').start();
|
|
392
|
+
try {
|
|
393
|
+
await service.projectFindByJiraKey(flags['jira-key']);
|
|
394
|
+
checkSpinner.fail('Jira key already in use');
|
|
395
|
+
this.error(`Jira project key "${flags['jira-key']}" is already linked to another project`);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error.response?.status === 404) {
|
|
399
|
+
checkSpinner.succeed(`Jira key ${chalk.cyan(flags['jira-key'])} is available`);
|
|
400
|
+
return flags['jira-key'];
|
|
401
|
+
}
|
|
402
|
+
checkSpinner.fail('Failed to check Jira key');
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (flags['jira-key'] && !flags.json) {
|
|
407
|
+
// Flag provided in interactive mode — still validate
|
|
408
|
+
const checkSpinner = ora('Checking Jira key availability...').start();
|
|
409
|
+
try {
|
|
410
|
+
await service.projectFindByJiraKey(flags['jira-key']);
|
|
411
|
+
checkSpinner.fail('Jira key already in use');
|
|
412
|
+
this.log(chalk.red(` Jira project key "${flags['jira-key']}" is already linked to another project`));
|
|
413
|
+
// Fall through to prompt
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (error.response?.status === 404) {
|
|
417
|
+
checkSpinner.succeed(`Jira key ${chalk.cyan(flags['jira-key'])} is available`);
|
|
418
|
+
return flags['jira-key'];
|
|
419
|
+
}
|
|
420
|
+
checkSpinner.fail('Failed to check Jira key');
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.log('');
|
|
425
|
+
this.log(chalk.blue('Step 2: Jira Project Key'));
|
|
426
|
+
this.log(chalk.dim('Enter the Jira project key to link'));
|
|
427
|
+
this.log('');
|
|
428
|
+
while (true) {
|
|
429
|
+
const { jiraKey } = await inquirer.prompt([{
|
|
430
|
+
message: 'Jira project key:',
|
|
431
|
+
name: 'jiraKey',
|
|
432
|
+
type: 'input',
|
|
433
|
+
validate: (input) => {
|
|
434
|
+
if (!input.trim())
|
|
435
|
+
return 'Jira project key is required';
|
|
436
|
+
if (!/^[A-Z][A-Z0-9_]+$/.test(input.trim())) {
|
|
437
|
+
return 'Key must be uppercase alphanumeric (e.g., PROJ, MY_PROJ)';
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
},
|
|
441
|
+
}]);
|
|
442
|
+
const key = jiraKey.trim();
|
|
443
|
+
const checkSpinner = ora('Checking Jira key availability...').start();
|
|
444
|
+
try {
|
|
445
|
+
await service.projectFindByJiraKey(key);
|
|
446
|
+
checkSpinner.fail(`Jira key ${chalk.cyan(key)} is already in use`);
|
|
447
|
+
this.log(chalk.yellow(' Please choose a different key'));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error.response?.status === 404) {
|
|
451
|
+
checkSpinner.succeed(`Jira key ${chalk.cyan(key)} is available`);
|
|
452
|
+
return key;
|
|
453
|
+
}
|
|
454
|
+
checkSpinner.fail('Failed to check Jira key');
|
|
455
|
+
this.log(chalk.red(` Error: ${error.message}`));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async selectOrCreateProject(service, flags) {
|
|
460
|
+
// JSON mode with --name: create new project
|
|
461
|
+
if (flags.json && flags.name) {
|
|
462
|
+
const slug = flags.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
463
|
+
const createSpinner = ora('Creating project...').start();
|
|
464
|
+
try {
|
|
465
|
+
const result = await service.moduleCreate({
|
|
466
|
+
buildRuntime: 'nodejs',
|
|
467
|
+
framework: 'other',
|
|
468
|
+
name: flags.name,
|
|
469
|
+
runtime: 'nodejs',
|
|
470
|
+
slug,
|
|
471
|
+
sourceLocation: '',
|
|
472
|
+
});
|
|
473
|
+
createSpinner.succeed(`Project ${chalk.cyan(flags.name)} created`);
|
|
474
|
+
return { name: flags.name, projectId: result.projectId || slug, slug: result.slug || slug };
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
createSpinner.fail('Failed to create project');
|
|
478
|
+
this.error(error.response?.data?.message || error.message);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// JSON mode with --project: look up existing
|
|
482
|
+
if (flags.json && flags.project) {
|
|
483
|
+
const lookupSpinner = ora('Looking up project...').start();
|
|
484
|
+
try {
|
|
485
|
+
const result = await service.moduleGet({ slug: flags.project });
|
|
486
|
+
lookupSpinner.succeed(`Project ${chalk.cyan(result.name || flags.project)} found`);
|
|
487
|
+
return { name: result.name || flags.project, projectId: result.projectId || flags.project, slug: result.slug || flags.project };
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
lookupSpinner.fail('Project not found');
|
|
491
|
+
this.error(`Project "${flags.project}" not found. Check the slug and try again.`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Interactive mode with --project flag: skip selection
|
|
495
|
+
if (flags.project) {
|
|
496
|
+
const lookupSpinner = ora('Looking up project...').start();
|
|
497
|
+
try {
|
|
498
|
+
const result = await service.moduleGet({ slug: flags.project });
|
|
499
|
+
lookupSpinner.succeed(`Project ${chalk.cyan(result.name || flags.project)} found`);
|
|
500
|
+
return { name: result.name || flags.project, projectId: result.projectId || flags.project, slug: result.slug || flags.project };
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
lookupSpinner.fail('Project not found');
|
|
504
|
+
this.log(chalk.red(` Project "${flags.project}" not found`));
|
|
505
|
+
// Fall through to interactive selection
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
this.log('');
|
|
509
|
+
this.log(chalk.blue('Step 1: Select or Create Project'));
|
|
510
|
+
this.log(chalk.dim('Choose an existing project or create a new one'));
|
|
511
|
+
this.log('');
|
|
512
|
+
// Fetch existing projects
|
|
513
|
+
const listSpinner = ora('Fetching projects...').start();
|
|
514
|
+
let projects = [];
|
|
515
|
+
try {
|
|
516
|
+
projects = await service.moduleList();
|
|
517
|
+
listSpinner.succeed(`Found ${projects.length} project(s)`);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
listSpinner.warn('Could not fetch projects');
|
|
521
|
+
}
|
|
522
|
+
const choices = [
|
|
523
|
+
...projects.map(p => ({
|
|
524
|
+
name: `${p.name || p.slug} (${p.slug})`,
|
|
525
|
+
value: p.slug,
|
|
526
|
+
})),
|
|
527
|
+
{ name: chalk.green('+ Create new project'), value: '__create_new__' },
|
|
528
|
+
];
|
|
529
|
+
const { selection } = await inquirer.prompt([{
|
|
530
|
+
choices,
|
|
531
|
+
message: 'Select a project:',
|
|
532
|
+
name: 'selection',
|
|
533
|
+
type: 'list',
|
|
534
|
+
}]);
|
|
535
|
+
if (selection === '__create_new__') {
|
|
536
|
+
const { projectName } = await inquirer.prompt([{
|
|
537
|
+
message: 'Project name:',
|
|
538
|
+
name: 'projectName',
|
|
539
|
+
type: 'input',
|
|
540
|
+
validate: (input) => input.trim() ? true : 'Project name is required',
|
|
541
|
+
}]);
|
|
542
|
+
const derivedSlug = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
543
|
+
const { projectSlug } = await inquirer.prompt([{
|
|
544
|
+
default: derivedSlug,
|
|
545
|
+
message: 'Project slug:',
|
|
546
|
+
name: 'projectSlug',
|
|
547
|
+
type: 'input',
|
|
548
|
+
validate: (input) => input.trim() ? true : 'Project slug is required',
|
|
549
|
+
}]);
|
|
550
|
+
const createSpinner = ora('Creating project...').start();
|
|
551
|
+
try {
|
|
552
|
+
const result = await service.moduleCreate({
|
|
553
|
+
buildRuntime: 'nodejs',
|
|
554
|
+
framework: 'other',
|
|
555
|
+
name: projectName.trim(),
|
|
556
|
+
runtime: 'nodejs',
|
|
557
|
+
slug: projectSlug.trim(),
|
|
558
|
+
sourceLocation: '',
|
|
559
|
+
});
|
|
560
|
+
createSpinner.succeed(`Project ${chalk.cyan(projectName)} created`);
|
|
561
|
+
return { name: projectName.trim(), projectId: result.projectId || projectSlug, slug: result.slug || projectSlug };
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
createSpinner.fail('Failed to create project');
|
|
565
|
+
this.error(error.response?.data?.message || error.message);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Selected existing project
|
|
569
|
+
const selected = projects.find(p => p.slug === selection);
|
|
570
|
+
return {
|
|
571
|
+
name: selected?.name || selection,
|
|
572
|
+
projectId: selected?.projectId || selection,
|
|
573
|
+
slug: selected?.slug || selection,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ProjectList 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
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|