@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,171 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
export default class StageRevoke extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
stage: Args.string({
|
|
8
|
+
description: 'Stage name to revoke access from',
|
|
9
|
+
required: false,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Revoke access from a stage for a user or CI account';
|
|
13
|
+
static examples = [
|
|
14
|
+
// Interactive
|
|
15
|
+
'<%= config.bin %> <%= command.id %>',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> develop',
|
|
17
|
+
// Non-interactive
|
|
18
|
+
'<%= config.bin %> <%= command.id %> develop --user maria@company.com',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> develop --ci github-actions',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
ci: Flags.string({
|
|
23
|
+
char: 'c',
|
|
24
|
+
description: 'CI account name or ID to revoke access from',
|
|
25
|
+
exclusive: ['user'],
|
|
26
|
+
}),
|
|
27
|
+
domain: Flags.string({
|
|
28
|
+
char: 'd',
|
|
29
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
30
|
+
}),
|
|
31
|
+
user: Flags.string({
|
|
32
|
+
char: 'u',
|
|
33
|
+
description: 'User email to revoke access from',
|
|
34
|
+
exclusive: ['ci'],
|
|
35
|
+
}),
|
|
36
|
+
yes: Flags.boolean({
|
|
37
|
+
char: 'y',
|
|
38
|
+
default: false,
|
|
39
|
+
description: 'Skip confirmation',
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
async run() {
|
|
43
|
+
const { args, flags } = await this.parse(StageRevoke);
|
|
44
|
+
this.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
45
|
+
this.log(chalk.cyan('🔓 Revoke Stage Access'));
|
|
46
|
+
this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
47
|
+
this.log('');
|
|
48
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
49
|
+
// Step 1: Resolve stage
|
|
50
|
+
let stageName;
|
|
51
|
+
if (args.stage) {
|
|
52
|
+
stageName = args.stage;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
stageName = await this.promptForStage(service);
|
|
56
|
+
}
|
|
57
|
+
// Step 2: Resolve target
|
|
58
|
+
let targetType;
|
|
59
|
+
let targetId;
|
|
60
|
+
let displayName;
|
|
61
|
+
if (flags.user) {
|
|
62
|
+
targetType = 'user';
|
|
63
|
+
targetId = flags.user;
|
|
64
|
+
displayName = flags.user;
|
|
65
|
+
}
|
|
66
|
+
else if (flags.ci) {
|
|
67
|
+
const ciAccount = await this.findCIAccount(service, flags.ci);
|
|
68
|
+
if (!ciAccount) {
|
|
69
|
+
this.error(`CI account '${flags.ci}' not found`);
|
|
70
|
+
}
|
|
71
|
+
targetType = 'user';
|
|
72
|
+
targetId = ciAccount.cognitoUsername;
|
|
73
|
+
displayName = `CI: ${ciAccount.name}`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Interactive: show current access and let user select
|
|
77
|
+
const target = await this.promptForTarget(service, stageName);
|
|
78
|
+
if (!target) {
|
|
79
|
+
this.log(chalk.yellow('No access entries to revoke.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
targetType = target.type;
|
|
83
|
+
targetId = target.id;
|
|
84
|
+
displayName = target.displayName;
|
|
85
|
+
}
|
|
86
|
+
// Step 3: Confirm
|
|
87
|
+
if (!flags.yes) {
|
|
88
|
+
const { confirmed } = await inquirer.prompt([{
|
|
89
|
+
default: false,
|
|
90
|
+
message: chalk.yellow(`Revoke all access from ${displayName} on stage '${stageName}'?`),
|
|
91
|
+
name: 'confirmed',
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
}]);
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
this.log(chalk.gray('Cancelled.'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Step 4: Revoke
|
|
100
|
+
try {
|
|
101
|
+
this.log(chalk.blue(`\n🔄 Revoking access from '${stageName}' for ${displayName}...`));
|
|
102
|
+
await service.stageAccessRevoke(stageName, targetType, targetId);
|
|
103
|
+
this.log(chalk.green(`✅ Access revoked for ${displayName} from '${stageName}'`));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const err = error;
|
|
107
|
+
const message = err.response?.data?.message || err.message;
|
|
108
|
+
this.error(`Failed to revoke access: ${message}`);
|
|
109
|
+
}
|
|
110
|
+
this.log('');
|
|
111
|
+
}
|
|
112
|
+
async findCIAccount(service, nameOrId) {
|
|
113
|
+
try {
|
|
114
|
+
const accounts = await service.ciAccountList();
|
|
115
|
+
return accounts.find(a => a.name.toLowerCase() === nameOrId.toLowerCase() ||
|
|
116
|
+
a.accountId === nameOrId ||
|
|
117
|
+
a.cognitoUsername.includes(nameOrId)) || null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async promptForStage(service) {
|
|
124
|
+
this.log(chalk.gray(' Fetching stages...'));
|
|
125
|
+
const stages = await service.stageList();
|
|
126
|
+
if (!stages || stages.length === 0) {
|
|
127
|
+
this.error('No stages found');
|
|
128
|
+
}
|
|
129
|
+
const stageChoices = stages.map(s => ({
|
|
130
|
+
name: s.production
|
|
131
|
+
? `${s.name} ${chalk.red('(PROD)')}`
|
|
132
|
+
: `${s.name} ${chalk.blue('(dev)')}`,
|
|
133
|
+
value: s.slug || s.name,
|
|
134
|
+
}));
|
|
135
|
+
const { stage } = await inquirer.prompt([{
|
|
136
|
+
choices: stageChoices,
|
|
137
|
+
message: chalk.yellow('Select a stage:'),
|
|
138
|
+
name: 'stage',
|
|
139
|
+
type: 'list',
|
|
140
|
+
}]);
|
|
141
|
+
return stage;
|
|
142
|
+
}
|
|
143
|
+
async promptForTarget(service, stageName) {
|
|
144
|
+
this.log(chalk.gray(' Fetching current access...'));
|
|
145
|
+
try {
|
|
146
|
+
const accessData = await service.stageAccessGet(stageName);
|
|
147
|
+
if (!accessData.access || accessData.access.length === 0) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Build choices from current access
|
|
151
|
+
const choices = accessData.access.map(entry => ({
|
|
152
|
+
name: `${entry.targetId} (${entry.role})`,
|
|
153
|
+
value: {
|
|
154
|
+
displayName: entry.targetId,
|
|
155
|
+
id: entry.targetId,
|
|
156
|
+
type: entry.targetType,
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
const { target } = await inquirer.prompt([{
|
|
160
|
+
choices,
|
|
161
|
+
message: chalk.yellow('Select who to revoke access from:'),
|
|
162
|
+
name: 'target',
|
|
163
|
+
type: 'list',
|
|
164
|
+
}]);
|
|
165
|
+
return target;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class StageShare extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
stage: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
ci: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
group: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
role: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
static strict: boolean;
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
private findCIAccount;
|
|
19
|
+
private findGroup;
|
|
20
|
+
private promptForRole;
|
|
21
|
+
private promptForStages;
|
|
22
|
+
private promptForTarget;
|
|
23
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
5
|
+
const ROLE_CHOICES = [
|
|
6
|
+
{
|
|
7
|
+
name: 'Deployer - Can deploy to this stage',
|
|
8
|
+
short: 'Deployer',
|
|
9
|
+
value: 'deployer',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Viewer - Can view stage details and deployments',
|
|
13
|
+
short: 'Viewer',
|
|
14
|
+
value: 'viewer',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Manager - Full control including access management',
|
|
18
|
+
short: 'Manager',
|
|
19
|
+
value: 'manager',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
export default class StageShare extends Command {
|
|
23
|
+
static args = {
|
|
24
|
+
stage: Args.string({
|
|
25
|
+
description: 'Stage name to share access to',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
static description = 'Grant access to a stage for users or CI accounts';
|
|
30
|
+
static examples = [
|
|
31
|
+
// Interactive
|
|
32
|
+
'<%= config.bin %> <%= command.id %>',
|
|
33
|
+
'<%= config.bin %> <%= command.id %> develop',
|
|
34
|
+
// Non-interactive
|
|
35
|
+
'<%= config.bin %> <%= command.id %> develop --user maria@company.com --role deployer',
|
|
36
|
+
'<%= config.bin %> <%= command.id %> develop --ci github-actions --role deployer',
|
|
37
|
+
'<%= config.bin %> <%= command.id %> develop --group "CI Accounts" --role deployer',
|
|
38
|
+
'<%= config.bin %> <%= command.id %> staging production --user dev@company.com --role viewer',
|
|
39
|
+
];
|
|
40
|
+
static flags = {
|
|
41
|
+
ci: Flags.string({
|
|
42
|
+
char: 'c',
|
|
43
|
+
description: 'CI account name or ID to grant access to',
|
|
44
|
+
exclusive: ['user', 'group'],
|
|
45
|
+
}),
|
|
46
|
+
domain: Flags.string({
|
|
47
|
+
char: 'd',
|
|
48
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
49
|
+
}),
|
|
50
|
+
group: Flags.string({
|
|
51
|
+
char: 'g',
|
|
52
|
+
description: 'Group ID or name to grant access to',
|
|
53
|
+
exclusive: ['user', 'ci'],
|
|
54
|
+
}),
|
|
55
|
+
role: Flags.string({
|
|
56
|
+
char: 'r',
|
|
57
|
+
description: 'Role to grant',
|
|
58
|
+
options: ['viewer', 'deployer', 'manager'],
|
|
59
|
+
}),
|
|
60
|
+
user: Flags.string({
|
|
61
|
+
char: 'u',
|
|
62
|
+
description: 'User email to grant access to',
|
|
63
|
+
exclusive: ['ci', 'group'],
|
|
64
|
+
}),
|
|
65
|
+
yes: Flags.boolean({
|
|
66
|
+
char: 'y',
|
|
67
|
+
default: false,
|
|
68
|
+
description: 'Skip confirmation for production stages',
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
static strict = false; // Allow multiple stage args
|
|
72
|
+
async run() {
|
|
73
|
+
const { argv, flags } = await this.parse(StageShare);
|
|
74
|
+
const stageArgs = argv;
|
|
75
|
+
this.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
76
|
+
this.log(chalk.cyan('🔐 Share Stage Access'));
|
|
77
|
+
this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
78
|
+
this.log('');
|
|
79
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
80
|
+
// Step 1: Resolve stage(s)
|
|
81
|
+
let stages;
|
|
82
|
+
if (stageArgs.length > 0) {
|
|
83
|
+
stages = stageArgs;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
stages = await this.promptForStages(service);
|
|
87
|
+
}
|
|
88
|
+
// Step 2: Resolve target (user, CI, or group)
|
|
89
|
+
let targetType;
|
|
90
|
+
let targetId;
|
|
91
|
+
let displayName;
|
|
92
|
+
if (flags.group) {
|
|
93
|
+
// Find group by ID or name
|
|
94
|
+
const group = await this.findGroup(service, flags.group);
|
|
95
|
+
if (!group) {
|
|
96
|
+
this.error(`Group '${flags.group}' not found`);
|
|
97
|
+
}
|
|
98
|
+
targetType = 'group';
|
|
99
|
+
targetId = group.groupId;
|
|
100
|
+
displayName = `Group: ${group.name}`;
|
|
101
|
+
}
|
|
102
|
+
else if (flags.user) {
|
|
103
|
+
targetType = 'user';
|
|
104
|
+
targetId = flags.user;
|
|
105
|
+
displayName = flags.user;
|
|
106
|
+
}
|
|
107
|
+
else if (flags.ci) {
|
|
108
|
+
// For CI accounts, we need to look up the Cognito sub
|
|
109
|
+
const ciAccount = await this.findCIAccount(service, flags.ci);
|
|
110
|
+
if (!ciAccount) {
|
|
111
|
+
this.error(`CI account '${flags.ci}' not found`);
|
|
112
|
+
}
|
|
113
|
+
targetType = 'user';
|
|
114
|
+
// CI accounts use cognitoUsername but we need the sub for Zanzibar
|
|
115
|
+
// For now, we'll use the cognitoUsername and the backend will resolve it
|
|
116
|
+
targetId = ciAccount.cognitoUsername;
|
|
117
|
+
displayName = `CI: ${ciAccount.name}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const target = await this.promptForTarget(service);
|
|
121
|
+
targetType = target.type;
|
|
122
|
+
targetId = target.id;
|
|
123
|
+
displayName = target.displayName;
|
|
124
|
+
}
|
|
125
|
+
// Step 3: Resolve role
|
|
126
|
+
const role = flags.role || await this.promptForRole();
|
|
127
|
+
// Step 4: Confirm for production stages
|
|
128
|
+
const prodStages = stages.filter(s => s.toLowerCase().includes('prod'));
|
|
129
|
+
if (prodStages.length > 0 && !flags.yes) {
|
|
130
|
+
const { confirmed } = await inquirer.prompt([{
|
|
131
|
+
default: false,
|
|
132
|
+
message: chalk.yellow(`⚠️ You're granting access to production stage(s): ${prodStages.join(', ')}. Continue?`),
|
|
133
|
+
name: 'confirmed',
|
|
134
|
+
type: 'confirm',
|
|
135
|
+
}]);
|
|
136
|
+
if (!confirmed) {
|
|
137
|
+
this.log(chalk.gray('Cancelled.'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Step 5: Grant access to each stage
|
|
142
|
+
this.log('');
|
|
143
|
+
for (const stage of stages) {
|
|
144
|
+
try {
|
|
145
|
+
this.log(chalk.blue(`🔄 Granting ${role} access to '${stage}' for ${displayName}...`));
|
|
146
|
+
await service.stageAccessGrant(stage, [{
|
|
147
|
+
role,
|
|
148
|
+
targetId,
|
|
149
|
+
targetType,
|
|
150
|
+
}]);
|
|
151
|
+
this.log(chalk.green(`✅ Granted '${role}' access to ${displayName} on '${stage}'`));
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const err = error;
|
|
155
|
+
const message = err.response?.data?.message || err.message;
|
|
156
|
+
this.log(chalk.red(`❌ Failed to grant access to '${stage}': ${message}`));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.log('');
|
|
160
|
+
}
|
|
161
|
+
async findCIAccount(service, nameOrId) {
|
|
162
|
+
try {
|
|
163
|
+
const accounts = await service.ciAccountList();
|
|
164
|
+
return accounts.find(a => a.name.toLowerCase() === nameOrId.toLowerCase() ||
|
|
165
|
+
a.accountId === nameOrId ||
|
|
166
|
+
a.cognitoUsername.includes(nameOrId)) || null;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async findGroup(service, nameOrId) {
|
|
173
|
+
try {
|
|
174
|
+
const groups = await service.groupList();
|
|
175
|
+
return groups.find(g => g.groupId === nameOrId ||
|
|
176
|
+
g.name.toLowerCase() === nameOrId.toLowerCase()) || null;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async promptForRole() {
|
|
183
|
+
const { role } = await inquirer.prompt([{
|
|
184
|
+
choices: ROLE_CHOICES,
|
|
185
|
+
message: chalk.yellow('Select role to grant:'),
|
|
186
|
+
name: 'role',
|
|
187
|
+
type: 'list',
|
|
188
|
+
}]);
|
|
189
|
+
return role;
|
|
190
|
+
}
|
|
191
|
+
async promptForStages(service) {
|
|
192
|
+
this.log(chalk.gray(' Fetching stages...'));
|
|
193
|
+
const stagesData = await service.stageList();
|
|
194
|
+
if (!stagesData || stagesData.length === 0) {
|
|
195
|
+
this.error('No stages found. Create a stage first with: hd stage create');
|
|
196
|
+
}
|
|
197
|
+
const stageChoices = stagesData.map(s => ({
|
|
198
|
+
name: s.production
|
|
199
|
+
? `${s.name} ${chalk.red('(PROD)')}`
|
|
200
|
+
: `${s.name} ${chalk.blue('(dev)')}`,
|
|
201
|
+
value: s.slug || s.name,
|
|
202
|
+
}));
|
|
203
|
+
const { stages } = await inquirer.prompt([{
|
|
204
|
+
choices: stageChoices,
|
|
205
|
+
message: chalk.yellow('Select stage(s) to share:'),
|
|
206
|
+
name: 'stages',
|
|
207
|
+
type: 'checkbox',
|
|
208
|
+
validate: (input) => input.length > 0 || 'Select at least one stage',
|
|
209
|
+
}]);
|
|
210
|
+
return stages;
|
|
211
|
+
}
|
|
212
|
+
async promptForTarget(service) {
|
|
213
|
+
// First ask what type of target
|
|
214
|
+
const { targetType } = await inquirer.prompt([{
|
|
215
|
+
choices: [
|
|
216
|
+
{ name: 'User (by email)', value: 'user' },
|
|
217
|
+
{ name: 'CI Account', value: 'ci' },
|
|
218
|
+
{ name: 'Group', value: 'group' },
|
|
219
|
+
],
|
|
220
|
+
message: chalk.yellow('Grant access to:'),
|
|
221
|
+
name: 'targetType',
|
|
222
|
+
type: 'list',
|
|
223
|
+
}]);
|
|
224
|
+
if (targetType === 'group') {
|
|
225
|
+
// Fetch groups and let user select
|
|
226
|
+
this.log(chalk.gray(' Fetching groups...'));
|
|
227
|
+
const groups = await service.groupList();
|
|
228
|
+
if (!groups || groups.length === 0) {
|
|
229
|
+
this.error('No groups found. Create a group first or use user/CI account.');
|
|
230
|
+
}
|
|
231
|
+
const groupChoices = groups.map(g => ({
|
|
232
|
+
name: g.isSystemGroup
|
|
233
|
+
? `${g.name} ${chalk.cyan('(system)')}`
|
|
234
|
+
: g.name,
|
|
235
|
+
value: { groupId: g.groupId, name: g.name },
|
|
236
|
+
}));
|
|
237
|
+
const { group } = await inquirer.prompt([{
|
|
238
|
+
choices: groupChoices,
|
|
239
|
+
message: chalk.yellow('Select group:'),
|
|
240
|
+
name: 'group',
|
|
241
|
+
type: 'list',
|
|
242
|
+
}]);
|
|
243
|
+
return {
|
|
244
|
+
displayName: `Group: ${group.name}`,
|
|
245
|
+
id: group.groupId,
|
|
246
|
+
type: 'group',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (targetType === 'ci') {
|
|
250
|
+
// Fetch CI accounts and let user select
|
|
251
|
+
this.log(chalk.gray(' Fetching CI accounts...'));
|
|
252
|
+
const ciAccounts = await service.ciAccountList();
|
|
253
|
+
if (!ciAccounts || ciAccounts.length === 0) {
|
|
254
|
+
this.error('No CI accounts found. Create one first with: hd ci account create');
|
|
255
|
+
}
|
|
256
|
+
const ciChoices = ciAccounts.map(a => ({
|
|
257
|
+
name: `${a.name} (${a.cognitoUsername.split('@')[0]})`,
|
|
258
|
+
value: {
|
|
259
|
+
cognitoUsername: a.cognitoUsername,
|
|
260
|
+
name: a.name,
|
|
261
|
+
},
|
|
262
|
+
}));
|
|
263
|
+
const { ciAccount } = await inquirer.prompt([{
|
|
264
|
+
choices: ciChoices,
|
|
265
|
+
message: chalk.yellow('Select CI account:'),
|
|
266
|
+
name: 'ciAccount',
|
|
267
|
+
type: 'list',
|
|
268
|
+
}]);
|
|
269
|
+
return {
|
|
270
|
+
displayName: `CI: ${ciAccount.name}`,
|
|
271
|
+
id: ciAccount.cognitoUsername,
|
|
272
|
+
type: 'user',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
// For users, prompt for email
|
|
276
|
+
const { email } = await inquirer.prompt([{
|
|
277
|
+
message: chalk.yellow('Enter user email:'),
|
|
278
|
+
name: 'email',
|
|
279
|
+
validate: (input) => {
|
|
280
|
+
if (!input || !input.includes('@')) {
|
|
281
|
+
return 'Please enter a valid email address';
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
},
|
|
285
|
+
}]);
|
|
286
|
+
return {
|
|
287
|
+
displayName: email,
|
|
288
|
+
id: email,
|
|
289
|
+
type: 'user',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -3,7 +3,7 @@ export default class TestApi extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
domain: import("@oclif/core/
|
|
6
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
};
|
|
8
8
|
run(): Promise<void>;
|
|
9
9
|
}
|
|
@@ -1,84 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
clientId: string;
|
|
16
|
-
domain: string;
|
|
17
|
-
identityPoolId: string;
|
|
18
|
-
userPoolId: string;
|
|
19
|
-
}
|
|
20
|
-
export interface StoredCredentials extends CognitoTokens {
|
|
21
|
-
apiUrl: string;
|
|
22
|
-
awsCredentials: AWSCredentials;
|
|
23
|
-
cognitoConfig?: CognitoConfig;
|
|
24
|
-
obtainedAt: string;
|
|
25
|
-
region: string;
|
|
26
|
-
tenantDomain: string;
|
|
27
|
-
tenantId: string;
|
|
28
|
-
}
|
|
29
|
-
export declare class AuthService {
|
|
30
|
-
private readonly cognitoConfig;
|
|
31
|
-
private readonly credDir;
|
|
32
|
-
private readonly credPath;
|
|
33
|
-
private readonly domain?;
|
|
1
|
+
/**
|
|
2
|
+
* Hyperdrive Authentication Service
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from @devsquad/cli-auth with hyperdrive-specific configuration.
|
|
5
|
+
*/
|
|
6
|
+
import { AuthService as BaseAuthService, type AuthServiceConfig, type AWSCredentials, type CognitoConfig, type CognitoTokens, type StoredCredentials } from '@hyperdrive.bot/cli-auth';
|
|
7
|
+
export type { AWSCredentials, CognitoConfig, CognitoTokens, StoredCredentials };
|
|
8
|
+
declare const HYPERDRIVE_AUTH_CONFIG: AuthServiceConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Hyperdrive-specific AuthService
|
|
11
|
+
*
|
|
12
|
+
* Wrapper around @devsquad/cli-auth with hyperdrive defaults.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AuthService extends BaseAuthService {
|
|
34
15
|
constructor(domain?: string);
|
|
35
|
-
/**
|
|
36
|
-
* Clear stored credentials
|
|
37
|
-
*/
|
|
38
|
-
clearCredentials(): void;
|
|
39
|
-
/**
|
|
40
|
-
* Ensure credentials are valid, refresh if needed
|
|
41
|
-
* Returns valid credentials or throws error
|
|
42
|
-
*/
|
|
43
|
-
ensureValidCredentials(): Promise<StoredCredentials>;
|
|
44
|
-
/**
|
|
45
|
-
* Get AWS credentials from Cognito Identity Pool using ID token
|
|
46
|
-
*/
|
|
47
|
-
getAWSCredentials(idToken: string, region: string, cognitoConfig: CognitoConfig): Promise<AWSCredentials>;
|
|
48
|
-
/**
|
|
49
|
-
* Get all domains with stored credentials
|
|
50
|
-
*/
|
|
51
|
-
getCredentialDomains(): string[];
|
|
52
|
-
/**
|
|
53
|
-
* Check if credentials are expired
|
|
54
|
-
*/
|
|
55
|
-
isExpired(credentials: StoredCredentials): boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Load stored credentials from domain-specific path
|
|
58
|
-
*
|
|
59
|
-
* Always uses domain-specific credentials (credentials.<domain>)
|
|
60
|
-
* Domain is either explicitly specified or uses default domain
|
|
61
|
-
*/
|
|
62
|
-
loadCredentials(): StoredCredentials | null;
|
|
63
|
-
/**
|
|
64
|
-
* Check if credentials need refresh
|
|
65
|
-
* Returns true if AWS credentials expire in less than 5 minutes
|
|
66
|
-
*/
|
|
67
|
-
needsRefresh(credentials: StoredCredentials): boolean;
|
|
68
|
-
/**
|
|
69
|
-
* Refresh Cognito tokens using refresh token
|
|
70
|
-
*/
|
|
71
|
-
refreshTokens(refreshToken: string, cognitoConfig: CognitoConfig): Promise<CognitoTokens>;
|
|
72
|
-
/**
|
|
73
|
-
* Save credentials to ~/.hyperdrive/credentials
|
|
74
|
-
*/
|
|
75
|
-
saveCredentials(credentials: StoredCredentials): void;
|
|
76
|
-
/**
|
|
77
|
-
* Get the credentials file path (always domain-specific)
|
|
78
|
-
*/
|
|
79
|
-
private getCredentialsPath;
|
|
80
|
-
/**
|
|
81
|
-
* Get default domain from TenantService (avoid circular dependency by reading file directly)
|
|
82
|
-
*/
|
|
83
|
-
private getDefaultDomain;
|
|
84
16
|
}
|
|
17
|
+
export { HYPERDRIVE_AUTH_CONFIG };
|